diff --git a/src/app/pages/client/CallProvider.tsx b/src/app/pages/client/CallProvider.tsx index f3ec3eb5..086fa5a8 100644 --- a/src/app/pages/client/CallProvider.tsx +++ b/src/app/pages/client/CallProvider.tsx @@ -1,6 +1,28 @@ -import React, { createContext, useState, useContext, useMemo, useCallback, ReactNode } from 'react'; +import React, { + createContext, + useState, + useContext, + useMemo, + useCallback, + ReactNode, + useEffect, +} from 'react'; import { logger } from 'matrix-js-sdk/lib/logger'; -import { WidgetApiToWidgetAction, ITransport, WidgetApiAction } from 'matrix-widget-api'; +import { + WidgetApiToWidgetAction, + ITransport, + WidgetApiAction, + WidgetApiFromWidgetAction, +} from 'matrix-widget-api'; + +interface MediaStatePayload { + audioEnabled?: boolean; + videoEnabled?: boolean; +} + +const WIDGET_MEDIA_STATE_UPDATE_ACTION = 'io.element.device_mute'; + +const SET_MEDIA_STATE_ACTION = 'io.element.device_mute'; interface CallContextState { activeCallRoomId: string | null; @@ -12,6 +34,10 @@ interface CallContextState { action: WidgetApiToWidgetAction | string, data: T ) => Promise; + isAudioEnabled: boolean; + isVideoEnabled: boolean; + toggleAudio: () => Promise; + toggleVideo: () => Promise; } const CallContext = createContext(undefined); @@ -20,53 +46,133 @@ interface CallProviderProps { children: ReactNode; } +const DEFAULT_AUDIO_ENABLED = false; +const DEFAULT_VIDEO_ENABLED = false; + export function CallProvider({ children }: CallProviderProps) { const [activeCallRoomId, setActiveCallRoomIdState] = useState(null); - const [activeApiTransport, setActiveApiTransport] = useState(null); + const [activeApiTransport, setActiveApiTransportState] = useState(null); const [transportRoomId, setTransportRoomId] = useState(null); + const [isAudioEnabled, setIsAudioEnabledState] = useState(DEFAULT_AUDIO_ENABLED); + const [isVideoEnabled, setIsVideoEnabledState] = useState(DEFAULT_VIDEO_ENABLED); + + const resetMediaState = useCallback(() => { + logger.debug('CallContext: Resetting media state to defaults.'); + setIsAudioEnabledState(DEFAULT_AUDIO_ENABLED); + setIsVideoEnabledState(DEFAULT_VIDEO_ENABLED); + }, []); + const setActiveCallRoomId = useCallback( (roomId: string | null) => { - logger.debug(`CallContext: Setting activeCallRoomId to ${roomId}`); + logger.warn(`CallContext: Setting activeCallRoomId to ${roomId}`); + const previousRoomId = activeCallRoomId; setActiveCallRoomIdState(roomId); + + if (roomId !== previousRoomId) { + logger.debug(`CallContext: Active call room changed, resetting media state.`); + resetMediaState(); + } + if (roomId === null || roomId !== transportRoomId) { - logger.debug( - `CallContext: Clearing active transport because active room changed or was cleared.` + logger.warn( + `CallContext: Clearing active transport because active room changed to ${roomId} or was cleared.` ); - setActiveApiTransport(null); + setActiveApiTransportState(null); setTransportRoomId(null); } }, - [transportRoomId] + [transportRoomId, resetMediaState, activeCallRoomId] ); const hangUp = useCallback(() => { logger.debug(`CallContext: Hang up called.`); setActiveCallRoomIdState(null); logger.debug(`CallContext: Clearing active transport due to hangup.`); - setActiveApiTransport(null); + setActiveApiTransportState(null); setTransportRoomId(null); + resetMediaState(); + }, [resetMediaState]); + + const setActiveTransport = useCallback((transport: ITransport | null, roomId: string | null) => { + setActiveApiTransportState(transport); + setTransportRoomId(roomId); }, []); const registerActiveTransport = useCallback( (roomId: string | null, transport: ITransport | null) => { + if (activeApiTransport && activeApiTransport !== transport) { + logger.debug(`CallContext: Cleaning up listeners for previous transport instance.`); + } + if (roomId && transport) { logger.debug(`CallContext: Registering active transport for room ${roomId}.`); - setActiveApiTransport(transport); - setTransportRoomId(roomId); + setActiveTransport(transport, roomId); } else if (roomId === transportRoomId || roomId === null) { logger.debug(`CallContext: Clearing active transport for room ${transportRoomId}.`); - setActiveApiTransport(null); - setTransportRoomId(null); + setActiveTransport(null, null); + resetMediaState(); } else { logger.debug( - `CallContext: Ignoring transport clear request for room ${roomId}, as current transport belongs to ${transportRoomId}.` + `CallContext: Ignoring transport registration/clear request for room ${roomId}, as current transport belongs to ${transportRoomId}.` ); } }, - [transportRoomId] + [activeApiTransport, transportRoomId, setActiveTransport, resetMediaState] ); + useEffect(() => { + if (!activeApiTransport || !activeCallRoomId || transportRoomId !== activeCallRoomId) { + return; + } + + const transport = activeApiTransport; + + const handleHangup = (ev: CustomEvent) => { + logger.warn( + `CallContext: Received hangup action from widget in room ${activeCallRoomId}.`, + ev + ); + hangUp(); + }; + + const handleMediaStateUpdate = (ev: CustomEvent) => { + ev.preventDefault(); + logger.debug( + `CallContext: Received media state update from widget in room ${activeCallRoomId}:`, + ev.detail + ); + const { audioEnabled, videoEnabled } = ev.detail; + if (typeof audioEnabled === 'boolean' && audioEnabled !== isAudioEnabled) { + logger.debug(`CallContext: Updating audio enabled state from widget: ${audioEnabled}`); + setIsAudioEnabledState(audioEnabled); + } + if (typeof videoEnabled === 'boolean' && videoEnabled !== isVideoEnabled) { + logger.debug(`CallContext: Updating video enabled state from widget: ${videoEnabled}`); + setIsVideoEnabledState(videoEnabled); + } + }; + + logger.debug(`CallContext: Setting up listeners for transport in room ${activeCallRoomId}`); + transport.on(`action:${WidgetApiFromWidgetAction.HangupCall}`, handleHangup); // Use standard hangup action name + transport.on(`action:${WIDGET_MEDIA_STATE_UPDATE_ACTION}`, handleMediaStateUpdate); + + return () => { + logger.debug(`CallContext: Cleaning up listeners for transport in room ${activeCallRoomId}`); + if (transport) { + transport.off(`action:${WidgetApiFromWidgetAction.HangupCall}`, handleHangup); + transport.off(`action:${WIDGET_MEDIA_STATE_UPDATE_ACTION}`, handleMediaStateUpdate); + } + }; + }, [ + activeApiTransport, + activeCallRoomId, + transportRoomId, + hangUp, + isAudioEnabled, + isVideoEnabled, + ]); + const sendWidgetAction = useCallback( async (action: WidgetApiToWidgetAction | string, data: T): Promise => { if (!activeApiTransport) { @@ -89,12 +195,46 @@ export function CallProvider({ children }: CallProviderProps) { await activeApiTransport.send(action as WidgetApiAction, data); } catch (error) { logger.error(`CallContext: Error sending action '${action}':`, error); - return Promise.reject(error); + throw error; } }, [activeApiTransport, activeCallRoomId, transportRoomId] ); + const toggleAudio = useCallback(async () => { + const newState = !isAudioEnabled; + logger.debug(`CallContext: Toggling audio. New state: enabled=${newState}`); + setIsAudioEnabledState(newState); + try { + await sendWidgetAction(SET_MEDIA_STATE_ACTION, { + audioEnabled: newState, + videoEnabled: isVideoEnabled, + }); + 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 { + await sendWidgetAction(SET_MEDIA_STATE_ACTION, { + audioEnabled: isAudioEnabled, + videoEnabled: newState, + }); + 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]); + const contextValue = useMemo( () => ({ activeCallRoomId, @@ -103,6 +243,10 @@ export function CallProvider({ children }: CallProviderProps) { activeApiTransport, registerActiveTransport, sendWidgetAction, + isAudioEnabled, + isVideoEnabled, + toggleAudio, + toggleVideo, }), [ activeCallRoomId, @@ -111,6 +255,10 @@ export function CallProvider({ children }: CallProviderProps) { activeApiTransport, registerActiveTransport, sendWidgetAction, + isAudioEnabled, + isVideoEnabled, + toggleAudio, + toggleVideo, ] );