add in call state (mute/video/hangup) context and transport handling for said contexts

This commit is contained in:
Gigiaj 2025-04-17 02:31:34 -05:00
parent 14206dbb96
commit f76641b538

View file

@ -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 { 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 { interface CallContextState {
activeCallRoomId: string | null; activeCallRoomId: string | null;
@ -12,6 +34,10 @@ interface CallContextState {
action: WidgetApiToWidgetAction | string, action: WidgetApiToWidgetAction | string,
data: T data: T
) => Promise<void>; ) => Promise<void>;
isAudioEnabled: boolean;
isVideoEnabled: boolean;
toggleAudio: () => Promise<void>;
toggleVideo: () => Promise<void>;
} }
const CallContext = createContext<CallContextState | undefined>(undefined); const CallContext = createContext<CallContextState | undefined>(undefined);
@ -20,53 +46,133 @@ interface CallProviderProps {
children: ReactNode; children: ReactNode;
} }
const DEFAULT_AUDIO_ENABLED = false;
const DEFAULT_VIDEO_ENABLED = false;
export function CallProvider({ children }: CallProviderProps) { export function CallProvider({ children }: CallProviderProps) {
const [activeCallRoomId, setActiveCallRoomIdState] = useState<string | null>(null); const [activeCallRoomId, setActiveCallRoomIdState] = useState<string | null>(null);
const [activeApiTransport, setActiveApiTransport] = useState<ITransport | null>(null); const [activeApiTransport, setActiveApiTransportState] = useState<ITransport | null>(null);
const [transportRoomId, setTransportRoomId] = useState<string | null>(null); const [transportRoomId, setTransportRoomId] = useState<string | null>(null);
const [isAudioEnabled, setIsAudioEnabledState] = useState<boolean>(DEFAULT_AUDIO_ENABLED);
const [isVideoEnabled, setIsVideoEnabledState] = useState<boolean>(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( const setActiveCallRoomId = useCallback(
(roomId: string | null) => { (roomId: string | null) => {
logger.debug(`CallContext: Setting activeCallRoomId to ${roomId}`); logger.warn(`CallContext: Setting activeCallRoomId to ${roomId}`);
const previousRoomId = activeCallRoomId;
setActiveCallRoomIdState(roomId); setActiveCallRoomIdState(roomId);
if (roomId !== previousRoomId) {
logger.debug(`CallContext: Active call room changed, resetting media state.`);
resetMediaState();
}
if (roomId === null || roomId !== transportRoomId) { if (roomId === null || roomId !== transportRoomId) {
logger.debug( logger.warn(
`CallContext: Clearing active transport because active room changed or was cleared.` `CallContext: Clearing active transport because active room changed to ${roomId} or was cleared.`
); );
setActiveApiTransport(null); setActiveApiTransportState(null);
setTransportRoomId(null); setTransportRoomId(null);
} }
}, },
[transportRoomId] [transportRoomId, resetMediaState, activeCallRoomId]
); );
const hangUp = useCallback(() => { const hangUp = useCallback(() => {
logger.debug(`CallContext: Hang up called.`); logger.debug(`CallContext: Hang up called.`);
setActiveCallRoomIdState(null); setActiveCallRoomIdState(null);
logger.debug(`CallContext: Clearing active transport due to hangup.`); logger.debug(`CallContext: Clearing active transport due to hangup.`);
setActiveApiTransport(null); setActiveApiTransportState(null);
setTransportRoomId(null); setTransportRoomId(null);
resetMediaState();
}, [resetMediaState]);
const setActiveTransport = useCallback((transport: ITransport | null, roomId: string | null) => {
setActiveApiTransportState(transport);
setTransportRoomId(roomId);
}, []); }, []);
const registerActiveTransport = useCallback( const registerActiveTransport = useCallback(
(roomId: string | null, transport: ITransport | null) => { (roomId: string | null, transport: ITransport | null) => {
if (activeApiTransport && activeApiTransport !== transport) {
logger.debug(`CallContext: Cleaning up listeners for previous transport instance.`);
}
if (roomId && transport) { if (roomId && transport) {
logger.debug(`CallContext: Registering active transport for room ${roomId}.`); logger.debug(`CallContext: Registering active transport for room ${roomId}.`);
setActiveApiTransport(transport); setActiveTransport(transport, roomId);
setTransportRoomId(roomId);
} else if (roomId === transportRoomId || roomId === null) { } else if (roomId === transportRoomId || roomId === null) {
logger.debug(`CallContext: Clearing active transport for room ${transportRoomId}.`); logger.debug(`CallContext: Clearing active transport for room ${transportRoomId}.`);
setActiveApiTransport(null); setActiveTransport(null, null);
setTransportRoomId(null); resetMediaState();
} else { } else {
logger.debug( 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<MediaStatePayload>) => {
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( const sendWidgetAction = useCallback(
async <T = unknown,>(action: WidgetApiToWidgetAction | string, data: T): Promise<void> => { async <T = unknown,>(action: WidgetApiToWidgetAction | string, data: T): Promise<void> => {
if (!activeApiTransport) { if (!activeApiTransport) {
@ -89,12 +195,46 @@ export function CallProvider({ children }: CallProviderProps) {
await activeApiTransport.send<T>(action as WidgetApiAction, data); await activeApiTransport.send<T>(action as WidgetApiAction, data);
} catch (error) { } catch (error) {
logger.error(`CallContext: Error sending action '${action}':`, error); logger.error(`CallContext: Error sending action '${action}':`, error);
return Promise.reject(error); throw error;
} }
}, },
[activeApiTransport, activeCallRoomId, transportRoomId] [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<CallContextState>( const contextValue = useMemo<CallContextState>(
() => ({ () => ({
activeCallRoomId, activeCallRoomId,
@ -103,6 +243,10 @@ export function CallProvider({ children }: CallProviderProps) {
activeApiTransport, activeApiTransport,
registerActiveTransport, registerActiveTransport,
sendWidgetAction, sendWidgetAction,
isAudioEnabled,
isVideoEnabled,
toggleAudio,
toggleVideo,
}), }),
[ [
activeCallRoomId, activeCallRoomId,
@ -111,6 +255,10 @@ export function CallProvider({ children }: CallProviderProps) {
activeApiTransport, activeApiTransport,
registerActiveTransport, registerActiveTransport,
sendWidgetAction, sendWidgetAction,
isAudioEnabled,
isVideoEnabled,
toggleAudio,
toggleVideo,
] ]
); );