2025-04-17 02:31:34 -05:00
import React , {
createContext ,
useState ,
useContext ,
useMemo ,
useCallback ,
ReactNode ,
useEffect ,
} from 'react' ;
2025-04-15 22:14:43 -05:00
import { logger } from 'matrix-js-sdk/lib/logger' ;
2025-04-29 15:28:13 -05:00
import { WidgetApiToWidgetAction , WidgetApiAction , ClientWidgetApi } from 'matrix-widget-api' ;
2025-05-01 16:24:36 -05:00
import { useParams } from 'react-router-dom' ;
import { useMatrixClient } from '../../hooks/useMatrixClient' ;
2025-04-17 02:31:34 -05:00
interface MediaStatePayload {
2025-04-29 15:28:13 -05:00
data ? : {
audio_enabled? : boolean ;
video_enabled? : boolean ;
} ;
2025-04-17 02:31:34 -05:00
}
const WIDGET_MEDIA_STATE_UPDATE_ACTION = 'io.element.device_mute' ;
2025-04-29 15:28:13 -05:00
const WIDGET_HANGUP_ACTION = 'im.vector.hangup' ;
2025-05-01 16:24:36 -05:00
const WIDGET_ON_SCREEN_ACTION = 'set_always_on_screen' ;
const WIDGET_JOIN_ACTION = 'io.element.join' ;
2025-04-15 22:14:43 -05:00
interface CallContextState {
activeCallRoomId : string | null ;
setActiveCallRoomId : ( roomId : string | null ) = > void ;
hangUp : ( ) = > void ;
2025-04-29 15:28:13 -05:00
activeClientWidgetApi : ClientWidgetApi | null ;
registerActiveClientWidgetApi : (
roomId : string | null ,
clientWidgetApi : ClientWidgetApi | null
) = > void ;
2025-04-15 22:14:43 -05:00
sendWidgetAction : < T = unknown > (
2025-04-16 19:49:11 -05:00
action : WidgetApiToWidgetAction | string ,
2025-04-15 22:14:43 -05:00
data : T
2025-04-16 19:49:11 -05:00
) = > Promise < void > ;
2025-04-17 02:31:34 -05:00
isAudioEnabled : boolean ;
isVideoEnabled : boolean ;
2025-04-18 03:01:44 -05:00
isChatOpen : boolean ;
2025-05-01 16:24:36 -05:00
isCallActive : boolean ;
2025-04-17 02:31:34 -05:00
toggleAudio : ( ) = > Promise < void > ;
toggleVideo : ( ) = > Promise < void > ;
2025-04-18 03:01:44 -05:00
toggleChat : ( ) = > Promise < void > ;
2025-04-15 22:14:43 -05:00
}
const CallContext = createContext < CallContextState | undefined > ( undefined ) ;
interface CallProviderProps {
children : ReactNode ;
}
2025-04-22 22:34:25 -04:00
const DEFAULT_AUDIO_ENABLED = true ;
2025-04-17 02:31:34 -05:00
const DEFAULT_VIDEO_ENABLED = false ;
2025-04-18 03:01:44 -05:00
const DEFAULT_CHAT_OPENED = false ;
2025-05-01 16:24:36 -05:00
const DEFAULT_CALL_ACTIVE = false ;
2025-04-17 02:31:34 -05:00
2025-04-15 22:14:43 -05:00
export function CallProvider ( { children } : CallProviderProps ) {
const [ activeCallRoomId , setActiveCallRoomIdState ] = useState < string | null > ( null ) ;
2025-04-29 15:28:13 -05:00
const [ activeClientWidgetApi , setActiveClientWidgetApiState ] = useState < ClientWidgetApi | null > (
null
) ;
const [ clientWidgetApiRoomId , setClientWidgetApiRoomId ] = useState < string | null > ( null ) ;
2025-04-15 22:14:43 -05:00
2025-04-17 02:31:34 -05:00
const [ isAudioEnabled , setIsAudioEnabledState ] = useState < boolean > ( DEFAULT_AUDIO_ENABLED ) ;
const [ isVideoEnabled , setIsVideoEnabledState ] = useState < boolean > ( DEFAULT_VIDEO_ENABLED ) ;
2025-04-18 03:01:44 -05:00
const [ isChatOpen , setIsChatOpenState ] = useState < boolean > ( DEFAULT_CHAT_OPENED ) ;
2025-05-01 16:24:36 -05:00
const [ isCallActive , setIsCallActive ] = useState < boolean > ( DEFAULT_CALL_ACTIVE ) ;
const { roomIdOrAlias : viewedRoomId } = useParams < { roomIdOrAlias : string } > ( ) ;
const mx = useMatrixClient ( ) ;
const room = mx . getRoom ( viewedRoomId ) ;
2025-04-17 02:31:34 -05:00
const resetMediaState = useCallback ( ( ) = > {
logger . debug ( 'CallContext: Resetting media state to defaults.' ) ;
setIsAudioEnabledState ( DEFAULT_AUDIO_ENABLED ) ;
setIsVideoEnabledState ( DEFAULT_VIDEO_ENABLED ) ;
2025-04-18 03:01:44 -05:00
setIsChatOpenState ( DEFAULT_CHAT_OPENED ) ;
2025-04-17 02:31:34 -05:00
} , [ ] ) ;
2025-04-15 22:14:43 -05:00
const setActiveCallRoomId = useCallback (
( roomId : string | null ) = > {
2025-04-17 02:31:34 -05:00
logger . warn ( ` CallContext: Setting activeCallRoomId to ${ roomId } ` ) ;
const previousRoomId = activeCallRoomId ;
2025-04-15 22:14:43 -05:00
setActiveCallRoomIdState ( roomId ) ;
2025-04-17 02:31:34 -05:00
if ( roomId !== previousRoomId ) {
logger . debug ( ` CallContext: Active call room changed, resetting media state. ` ) ;
resetMediaState ( ) ;
}
2025-04-29 15:28:13 -05:00
if ( roomId === null || roomId !== clientWidgetApiRoomId ) {
2025-04-17 02:31:34 -05:00
logger . warn (
2025-04-29 15:28:13 -05:00
` CallContext: Clearing active clientWidgetApi because active room changed to ${ roomId } or was cleared. `
2025-04-15 22:14:43 -05:00
) ;
2025-04-29 15:28:13 -05:00
setActiveClientWidgetApiState ( null ) ;
setClientWidgetApiRoomId ( null ) ;
2025-04-15 22:14:43 -05:00
}
} ,
2025-04-29 15:28:13 -05:00
[ clientWidgetApiRoomId , resetMediaState , activeCallRoomId ]
2025-04-16 19:49:11 -05:00
) ;
2025-04-15 22:14:43 -05:00
const hangUp = useCallback ( ( ) = > {
logger . debug ( ` CallContext: Hang up called. ` ) ;
2025-05-01 16:24:36 -05:00
// activeClientWidgetApi?.transport.send(`action:${WIDGET_HANGUP_ACTION}`, {});
2025-04-15 22:14:43 -05:00
setActiveCallRoomIdState ( null ) ;
2025-04-29 15:28:13 -05:00
logger . debug ( ` CallContext: Clearing active clientWidgetApi due to hangup. ` ) ;
setActiveClientWidgetApiState ( null ) ;
setClientWidgetApiRoomId ( null ) ;
2025-04-17 02:31:34 -05:00
resetMediaState ( ) ;
} , [ resetMediaState ] ) ;
2025-04-29 15:28:13 -05:00
const setActiveClientWidgetApi = useCallback (
( clientWidgetApi : ClientWidgetApi | null , roomId : string | null ) = > {
setActiveClientWidgetApiState ( clientWidgetApi ) ;
setClientWidgetApiRoomId ( roomId ) ;
} ,
[ ]
) ;
2025-04-15 22:14:43 -05:00
2025-04-29 15:28:13 -05:00
const registerActiveClientWidgetApi = useCallback (
( roomId : string | null , clientWidgetApi : ClientWidgetApi | null ) = > {
if ( activeClientWidgetApi && activeClientWidgetApi !== clientWidgetApi ) {
logger . debug ( ` CallContext: Cleaning up listeners for previous clientWidgetApi instance. ` ) ;
2025-04-17 02:31:34 -05:00
}
2025-04-29 15:28:13 -05:00
if ( roomId && clientWidgetApi ) {
logger . debug ( ` CallContext: Registering active clientWidgetApi for room ${ roomId } . ` ) ;
setActiveClientWidgetApi ( clientWidgetApi , roomId ) ;
} else if ( roomId === clientWidgetApiRoomId || roomId === null ) {
logger . debug (
` CallContext: Clearing active clientWidgetApi for room ${ clientWidgetApiRoomId } . `
) ;
setActiveClientWidgetApi ( null , null ) ;
2025-04-17 02:31:34 -05:00
resetMediaState ( ) ;
2025-04-15 22:14:43 -05:00
} else {
2025-04-16 19:49:11 -05:00
logger . debug (
2025-04-29 15:28:13 -05:00
` CallContext: Ignoring clientWidgetApi registration/clear request for room ${ roomId } , as current clientWidgetApi belongs to ${ clientWidgetApiRoomId } . `
2025-04-16 19:49:11 -05:00
) ;
2025-04-15 22:14:43 -05:00
}
} ,
2025-04-29 15:28:13 -05:00
[ activeClientWidgetApi , clientWidgetApiRoomId , setActiveClientWidgetApi , resetMediaState ]
2025-04-15 22:14:43 -05:00
) ;
2025-04-17 02:31:34 -05:00
useEffect ( ( ) = > {
2025-04-29 15:28:13 -05:00
if ( ! activeClientWidgetApi || ! activeCallRoomId || clientWidgetApiRoomId !== activeCallRoomId ) {
2025-04-17 02:31:34 -05:00
return ;
}
2025-04-29 15:28:13 -05:00
const clientWidgetApi = activeClientWidgetApi ;
2025-04-17 02:31:34 -05:00
const handleHangup = ( ev : CustomEvent ) = > {
2025-04-29 15:28:13 -05:00
ev . preventDefault ( ) ;
clientWidgetApi . transport . reply ( ev . detail , { } ) ;
2025-04-17 02:31:34 -05:00
logger . warn (
` CallContext: Received hangup action from widget in room ${ activeCallRoomId } . ` ,
ev
) ;
2025-05-01 16:24:36 -05:00
setIsCallActive ( false ) ;
// hangUp();
2025-04-17 02:31:34 -05:00
} ;
const handleMediaStateUpdate = ( ev : CustomEvent < MediaStatePayload > ) = > {
ev . preventDefault ( ) ;
logger . debug (
` CallContext: Received media state update from widget in room ${ activeCallRoomId } : ` ,
ev . detail
) ;
2025-04-29 15:28:13 -05:00
const { audio_enabled , video_enabled } = ev . detail . data ;
2025-04-22 00:27:31 -04:00
if ( typeof audio_enabled === 'boolean' && audio_enabled !== isAudioEnabled ) {
logger . debug ( ` CallContext: Updating audio enabled state from widget: ${ audio_enabled } ` ) ;
setIsAudioEnabledState ( audio_enabled ) ;
2025-04-17 02:31:34 -05:00
}
2025-04-22 00:27:31 -04:00
if ( typeof video_enabled === 'boolean' && video_enabled !== isVideoEnabled ) {
logger . debug ( ` CallContext: Updating video enabled state from widget: ${ video_enabled } ` ) ;
setIsVideoEnabledState ( video_enabled ) ;
2025-04-17 02:31:34 -05:00
}
} ;
2025-05-01 16:24:36 -05:00
const handleOnScreenStateUpdate = ( ev : CustomEvent ) = > {
ev . preventDefault ( ) ;
activeClientWidgetApi . transport . reply ( ev . detail , { } ) ;
} ;
const handleJoin = ( ev : CustomEvent ) = > {
ev . preventDefault ( ) ;
setIsCallActive ( true ) ;
} ;
2025-04-29 15:28:13 -05:00
logger . debug (
` CallContext: Setting up listeners for clientWidgetApi in room ${ activeCallRoomId } `
) ;
2025-05-01 16:24:36 -05:00
clientWidgetApi . on ( ` action: ${ WIDGET_HANGUP_ACTION } ` , handleHangup ) ;
2025-04-29 15:28:13 -05:00
clientWidgetApi . on ( ` action: ${ WIDGET_MEDIA_STATE_UPDATE_ACTION } ` , handleMediaStateUpdate ) ;
2025-05-01 16:24:36 -05:00
clientWidgetApi . on ( ` action: ${ WIDGET_ON_SCREEN_ACTION } ` , handleOnScreenStateUpdate ) ;
clientWidgetApi . on ( ` action: ${ WIDGET_JOIN_ACTION } ` , handleJoin ) ;
2025-04-17 02:31:34 -05:00
return ( ) = > {
2025-04-29 15:28:13 -05:00
logger . debug (
` CallContext: Cleaning up listeners for clientWidgetApi in room ${ activeCallRoomId } `
) ;
if ( clientWidgetApi ) {
clientWidgetApi . off ( ` action: ${ WIDGET_HANGUP_ACTION } ` , handleHangup ) ;
clientWidgetApi . off ( ` action: ${ WIDGET_MEDIA_STATE_UPDATE_ACTION } ` , handleMediaStateUpdate ) ;
2025-04-17 02:31:34 -05:00
}
} ;
} , [
2025-04-29 15:28:13 -05:00
activeClientWidgetApi ,
2025-04-17 02:31:34 -05:00
activeCallRoomId ,
2025-04-29 15:28:13 -05:00
clientWidgetApiRoomId ,
2025-04-17 02:31:34 -05:00
hangUp ,
2025-04-18 03:01:44 -05:00
isChatOpen ,
2025-04-17 02:31:34 -05:00
isAudioEnabled ,
isVideoEnabled ,
2025-05-01 16:24:36 -05:00
isCallActive ,
2025-04-17 02:31:34 -05:00
] ) ;
2025-04-15 22:14:43 -05:00
const sendWidgetAction = useCallback (
2025-04-16 19:49:11 -05:00
async < T = unknown , > ( action : WidgetApiToWidgetAction | string , data : T ) : Promise < void > = > {
2025-04-29 15:28:13 -05:00
if ( ! activeClientWidgetApi ) {
2025-04-15 22:14:43 -05:00
logger . warn (
2025-04-29 15:28:13 -05:00
` CallContext: Cannot send action ' ${ action } ', no active API clientWidgetApi registered. `
2025-04-15 22:14:43 -05:00
) ;
2025-04-29 15:28:13 -05:00
return Promise . reject ( new Error ( 'No active call clientWidgetApi' ) ) ;
2025-04-15 22:14:43 -05:00
}
2025-04-29 15:28:13 -05:00
if ( ! clientWidgetApiRoomId || clientWidgetApiRoomId !== activeCallRoomId ) {
logger . debug (
` CallContext: Cannot send action ' ${ action } ', clientWidgetApi room ( ${ clientWidgetApiRoomId } ) does not match active call room ( ${ activeCallRoomId } ). Stale clientWidgetApi? `
2025-04-15 22:14:43 -05:00
) ;
2025-04-29 15:28:13 -05:00
return Promise . reject ( new Error ( 'Mismatched active call clientWidgetApi' ) ) ;
2025-04-15 22:14:43 -05:00
}
try {
logger . debug (
2025-04-29 15:28:13 -05:00
` CallContext: Sending action ' ${ action } ' via active clientWidgetApi (room: ${ clientWidgetApiRoomId } ) with data: ` ,
2025-04-15 22:14:43 -05:00
data
) ;
2025-04-29 15:28:13 -05:00
await activeClientWidgetApi . transport . send ( action as WidgetApiAction , data ) ;
2025-04-15 22:14:43 -05:00
} catch ( error ) {
logger . error ( ` CallContext: Error sending action ' ${ action } ': ` , error ) ;
2025-04-17 02:31:34 -05:00
throw error ;
2025-04-15 22:14:43 -05:00
}
} ,
2025-04-29 15:28:13 -05:00
[ activeClientWidgetApi , activeCallRoomId , clientWidgetApiRoomId ]
2025-04-15 22:14:43 -05:00
) ;
2025-04-17 02:31:34 -05:00
const toggleAudio = useCallback ( async ( ) = > {
const newState = ! isAudioEnabled ;
logger . debug ( ` CallContext: Toggling audio. New state: enabled= ${ newState } ` ) ;
setIsAudioEnabledState ( newState ) ;
try {
2025-05-01 16:24:36 -05:00
await sendWidgetAction ( WIDGET_MEDIA_STATE_UPDATE_ACTION , {
2025-04-22 00:27:31 -04:00
audio_enabled : newState ,
video_enabled : isVideoEnabled ,
2025-04-17 02:31:34 -05:00
} ) ;
logger . debug ( ` CallContext: Successfully sent audio toggle action. ` ) ;
} catch ( error ) {
logger . error ( ` CallContext: Failed to send audio toggle action. Reverting state. ` , error ) ;
setIsAudioEnabledState ( ! newState ) ;
throw error ;
}
} , [ isAudioEnabled , isVideoEnabled , sendWidgetAction ] ) ;
const toggleVideo = useCallback ( async ( ) = > {
const newState = ! isVideoEnabled ;
logger . debug ( ` CallContext: Toggling video. New state: enabled= ${ newState } ` ) ;
setIsVideoEnabledState ( newState ) ;
try {
2025-05-01 16:24:36 -05:00
await sendWidgetAction ( WIDGET_MEDIA_STATE_UPDATE_ACTION , {
2025-04-22 00:27:31 -04:00
audio_enabled : isAudioEnabled ,
video_enabled : newState ,
2025-04-17 02:31:34 -05:00
} ) ;
logger . debug ( ` CallContext: Successfully sent video toggle action. ` ) ;
} catch ( error ) {
logger . error ( ` CallContext: Failed to send video toggle action. Reverting state. ` , error ) ;
setIsVideoEnabledState ( ! newState ) ;
throw error ;
}
} , [ isVideoEnabled , isAudioEnabled , sendWidgetAction ] ) ;
2025-04-18 03:01:44 -05:00
const toggleChat = useCallback ( async ( ) = > {
const newState = ! isChatOpen ;
2025-04-22 22:29:07 -04:00
setIsChatOpenState ( newState ) ;
2025-04-18 03:01:44 -05:00
} , [ isChatOpen ] ) ;
2025-04-15 22:14:43 -05:00
const contextValue = useMemo < CallContextState > (
( ) = > ( {
activeCallRoomId ,
setActiveCallRoomId ,
hangUp ,
2025-04-29 15:28:13 -05:00
activeClientWidgetApi ,
registerActiveClientWidgetApi ,
2025-04-15 22:14:43 -05:00
sendWidgetAction ,
2025-04-18 03:01:44 -05:00
isChatOpen ,
2025-04-17 02:31:34 -05:00
isAudioEnabled ,
isVideoEnabled ,
2025-05-02 02:35:51 -05:00
isCallActive ,
2025-04-17 02:31:34 -05:00
toggleAudio ,
toggleVideo ,
2025-04-18 03:01:44 -05:00
toggleChat ,
2025-04-15 22:14:43 -05:00
} ) ,
[
activeCallRoomId ,
setActiveCallRoomId ,
hangUp ,
2025-04-29 15:28:13 -05:00
activeClientWidgetApi ,
registerActiveClientWidgetApi ,
2025-04-15 22:14:43 -05:00
sendWidgetAction ,
2025-04-22 00:27:31 -04:00
isChatOpen ,
2025-04-17 02:31:34 -05:00
isAudioEnabled ,
isVideoEnabled ,
2025-05-02 02:35:51 -05:00
isCallActive ,
2025-04-17 02:31:34 -05:00
toggleAudio ,
toggleVideo ,
2025-04-22 00:27:31 -04:00
toggleChat ,
2025-04-15 22:14:43 -05:00
]
) ;
return < CallContext.Provider value = { contextValue } > { children } < / CallContext.Provider > ;
}
export function useCallState ( ) : CallContextState {
const context = useContext ( CallContext ) ;
if ( context === undefined ) {
throw new Error ( 'useCallState must be used within a CallProvider' ) ;
}
return context ;
}