Files
cinny/src/app/pages/client/call/CallProvider.tsx
T

491 lines
16 KiB
TypeScript
Raw Normal View History

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';
import { WidgetApiToWidgetAction, WidgetApiAction, ClientWidgetApi } from 'matrix-widget-api';
import { useParams } from 'react-router-dom';
import { SmallWidget } from '../../../features/call/SmallWidget';
interface MediaStatePayload {
data?: {
audio_enabled?: boolean;
video_enabled?: boolean;
};
}
const WIDGET_MEDIA_STATE_UPDATE_ACTION = 'io.element.device_mute';
const WIDGET_HANGUP_ACTION = 'im.vector.hangup';
const WIDGET_ON_SCREEN_ACTION = 'set_always_on_screen';
const WIDGET_JOIN_ACTION = 'io.element.join';
const WIDGET_TILE_UPDATE = 'io.element.tile_layout';
2025-04-15 22:14:43 -05:00
interface CallContextState {
activeCallRoomId: string | null;
setActiveCallRoomId: (roomId: string | null) => void;
2025-05-10 20:41:57 -05:00
viewedCallRoomId: string | null;
setViewedCallRoomId: (roomId: string | null) => void;
2025-04-15 22:14:43 -05:00
hangUp: () => void;
activeClientWidgetApi: ClientWidgetApi | null;
activeClientWidget: SmallWidget | null;
registerActiveClientWidgetApi: (
roomId: string | null,
clientWidgetApi: ClientWidgetApi | null,
clientWidget: SmallWidget
) => void;
viewedClientWidgetApi: ClientWidgetApi | null;
viewedClientWidget: SmallWidget | null;
2025-05-10 20:41:57 -05:00
registerViewedClientWidgetApi: (
roomId: string | null,
clientWidgetApi: ClientWidgetApi | null,
clientWidget: SmallWidget
2025-05-10 20:41:57 -05:00
) => 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>;
isAudioEnabled: boolean;
isVideoEnabled: boolean;
2025-04-18 03:01:44 -05:00
isChatOpen: boolean;
isCallActive: boolean;
isPrimaryIframe: boolean;
toggleAudio: () => Promise<void>;
toggleVideo: () => Promise<void>;
2025-04-18 03:01:44 -05:00
toggleChat: () => Promise<void>;
toggleIframe: () => Promise<void>;
2025-04-15 22:14:43 -05:00
}
const CallContext = createContext<CallContextState | undefined>(undefined);
interface CallProviderProps {
children: ReactNode;
}
const DEFAULT_AUDIO_ENABLED = true;
const DEFAULT_VIDEO_ENABLED = false;
2025-04-18 03:01:44 -05:00
const DEFAULT_CHAT_OPENED = false;
const DEFAULT_CALL_ACTIVE = false;
2025-05-10 20:41:57 -05:00
const DEFAULT_PRIMARY_IFRAME = true;
2025-04-15 22:14:43 -05:00
export function CallProvider({ children }: CallProviderProps) {
const [activeCallRoomId, setActiveCallRoomIdState] = useState<string | null>(null);
2025-05-10 20:41:57 -05:00
const [viewedCallRoomId, setViewedCallRoomIdState] = useState<string | null>(null);
const [activeClientWidgetApi, setActiveClientWidgetApiState] = useState<ClientWidgetApi | null>(
null
);
const [activeClientWidget, setActiveClientWidget] = useState<SmallWidget | null>(null);
2025-05-10 20:41:57 -05:00
const [activeClientWidgetApiRoomId, setActiveClientWidgetApiRoomId] = useState<string | null>(
null
);
const [viewedClientWidgetApi, setViewedClientWidgetApiState] = useState<ClientWidgetApi | null>(
null
);
const [viewedClientWidget, setViewedClientWidget] = useState<SmallWidget | null>(null);
2025-05-10 20:41:57 -05:00
const [viewedClientWidgetApiRoomId, setViewedClientWidgetApiRoomId] = useState<string | null>(
null
);
2025-04-15 22:14:43 -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);
const [isCallActive, setIsCallActive] = useState<boolean>(DEFAULT_CALL_ACTIVE);
const [isPrimaryIframe, setIsPrimaryIframe] = useState<boolean>(DEFAULT_PRIMARY_IFRAME);
const { roomIdOrAlias: viewedRoomId } = useParams<{ roomIdOrAlias: string }>();
const resetMediaState = useCallback(() => {
logger.debug('CallContext: Resetting media state to defaults.');
setIsAudioEnabledState(DEFAULT_AUDIO_ENABLED);
setIsVideoEnabledState(DEFAULT_VIDEO_ENABLED);
}, []);
2025-04-15 22:14:43 -05:00
const setActiveCallRoomId = useCallback(
(roomId: string | null) => {
logger.warn(`CallContext: Setting activeCallRoomId to ${roomId}`);
const previousRoomId = activeCallRoomId;
2025-04-15 22:14:43 -05:00
setActiveCallRoomIdState(roomId);
if (roomId !== previousRoomId) {
logger.debug(`CallContext: Active call room changed, resetting media state.`);
resetMediaState();
}
2025-05-10 20:41:57 -05:00
if (roomId === null || roomId !== activeClientWidgetApiRoomId) {
logger.warn(
`CallContext: Clearing active clientWidgetApi because active room changed to ${roomId} or was cleared.`
2025-04-15 22:14:43 -05:00
);
}
},
2025-05-10 20:41:57 -05:00
[activeClientWidgetApiRoomId, resetMediaState, activeCallRoomId]
);
const setViewedCallRoomId = useCallback(
(roomId: string | null) => {
logger.warn(`CallContext: Setting activeCallRoomId to ${roomId}`);
setViewedCallRoomIdState(roomId);
},
[setViewedCallRoomIdState]
2025-04-16 19:49:11 -05:00
);
2025-04-15 22:14:43 -05:00
const setActiveClientWidgetApi = useCallback(
(
clientWidgetApi: ClientWidgetApi | null,
clientWidget: SmallWidget | null,
roomId: string | null
) => {
setActiveClientWidgetApiState(clientWidgetApi);
setActiveClientWidget(clientWidget);
2025-05-10 20:41:57 -05:00
setActiveClientWidgetApiRoomId(roomId);
},
[]
);
2025-04-15 22:14:43 -05:00
const registerActiveClientWidgetApi = useCallback(
(
roomId: string | null,
clientWidgetApi: ClientWidgetApi | null,
clientWidget: SmallWidget | null
) => {
if (activeClientWidgetApi && activeClientWidgetApi !== clientWidgetApi) {
logger.debug(`CallContext: Cleaning up listeners for previous clientWidgetApi instance.`);
//activeClientWidgetApi.removeAllListeners();
}
if (roomId && clientWidgetApi) {
logger.debug(`CallContext: Registering active clientWidgetApi for room ${roomId}.`);
setActiveClientWidgetApi(clientWidgetApi, clientWidget, roomId);
2025-05-10 20:41:57 -05:00
} else if (roomId === activeClientWidgetApiRoomId || roomId === null) {
setActiveClientWidgetApi(null, null, null);
resetMediaState();
2025-05-10 20:41:57 -05:00
}
},
[activeClientWidgetApi, activeClientWidgetApiRoomId, setActiveClientWidgetApi, resetMediaState]
);
const setViewedClientWidgetApi = useCallback(
(
clientWidgetApi: ClientWidgetApi | null,
clientWidget: SmallWidget | null,
roomId: string | null
) => {
2025-05-10 20:41:57 -05:00
setViewedClientWidgetApiState(clientWidgetApi);
setViewedClientWidget(clientWidget);
setViewedClientWidgetApiRoomId(roomId);
2025-05-10 20:41:57 -05:00
},
[]
);
const registerViewedClientWidgetApi = useCallback(
(
roomId: string | null,
clientWidgetApi: ClientWidgetApi | null,
clientWidget: SmallWidget | null
) => {
2025-05-10 20:41:57 -05:00
if (viewedClientWidgetApi && viewedClientWidgetApi !== clientWidgetApi) {
logger.debug(`CallContext: Cleaning up listeners for previous clientWidgetApi instance.`);
2025-05-10 20:41:57 -05:00
}
if (roomId && clientWidgetApi) {
logger.debug(`CallContext: Registering viewed clientWidgetApi for room ${roomId}.`);
setViewedClientWidgetApi(clientWidgetApi, clientWidget, roomId);
2025-05-10 20:41:57 -05:00
} else if (roomId === viewedClientWidgetApiRoomId || roomId === null) {
logger.debug(
`CallContext: Clearing viewed clientWidgetApi for room ${viewedClientWidgetApiRoomId}.`
2025-05-10 20:41:57 -05:00
);
setViewedClientWidgetApi(null, null, null);
2025-04-15 22:14:43 -05:00
}
},
2025-05-10 20:41:57 -05:00
[viewedClientWidgetApi, viewedClientWidgetApiRoomId, setViewedClientWidgetApi]
2025-04-15 22:14:43 -05:00
);
const hangUp = useCallback(
(nextRoom) => {
setIsCallActive(false);
if (typeof nextRoom !== 'string') {
if (viewedCallRoomId === activeCallRoomId) {
setIsPrimaryIframe(!isPrimaryIframe);
} else {
setViewedCallRoomId(activeCallRoomId);
}
} else if (viewedCallRoomId !== null) {
setIsPrimaryIframe(!isPrimaryIframe);
setViewedCallRoomId(null);
}
setActiveClientWidgetApi(null, null, null);
setActiveCallRoomId(null);
logger.debug(`CallContext: Hang up called.`);
activeClientWidgetApi?.transport.send(`${WIDGET_HANGUP_ACTION}`, {});
},
[
activeCallRoomId,
activeClientWidgetApi?.transport,
isPrimaryIframe,
setActiveCallRoomId,
setActiveClientWidgetApi,
setViewedCallRoomId,
viewedCallRoomId,
]
);
useEffect(() => {
if (!activeCallRoomId && !viewedCallRoomId) {
return;
}
const handleHangup = (ev: CustomEvent) => {
ev.preventDefault();
if (ev.detail.widgetId === activeClientWidgetApi?.widget.id) {
activeClientWidgetApi?.transport.reply(ev.detail, {});
/*
if (viewedRoomId === activeCallRoomId) {
if (viewedCallRoomId !== activeCallRoomId) {
setViewedCallRoomId(activeCallRoomId);
setIsPrimaryIframe(!isPrimaryIframe);
} else {
setIsPrimaryIframe(!isPrimaryIframe);
}
setViewedClientWidgetApi(viewedClientWidgetApi, viewedClientWidget, viewedCallRoomId);
}
*/
// setActiveClientWidgetApi(null, null, null);
// setActiveCallRoomId(null);
// setIsCallActive(false);
}
logger.debug(
`CallContext: Received hangup action from widget in room ${activeCallRoomId}.`,
ev
);
};
const handleMediaStateUpdate = (ev: CustomEvent<MediaStatePayload>) => {
ev.preventDefault();
logger.debug(
`CallContext: Received media state update from widget in room ${activeCallRoomId}:`,
ev.detail
);
/* eslint-disable camelcase */
const { audio_enabled, video_enabled } = ev.detail.data ?? {};
if (typeof audio_enabled === 'boolean' && audio_enabled !== isAudioEnabled) {
logger.debug(`CallContext: Updating audio enabled state from widget: ${audio_enabled}`);
setIsAudioEnabledState(audio_enabled);
}
if (typeof video_enabled === 'boolean' && video_enabled !== isVideoEnabled) {
logger.debug(`CallContext: Updating video enabled state from widget: ${video_enabled}`);
setIsVideoEnabledState(video_enabled);
}
/* eslint-enable camelcase */
};
const handleOnScreenStateUpdate = (ev: CustomEvent) => {
ev.preventDefault();
2025-05-10 20:41:57 -05:00
if (isPrimaryIframe) {
activeClientWidgetApi?.transport.reply(ev.detail, {});
} else {
viewedClientWidgetApi?.transport.reply(ev.detail, {});
}
};
const handleOnTileLayout = (ev: CustomEvent) => {
ev.preventDefault();
if (isPrimaryIframe) {
activeClientWidgetApi?.transport.reply(ev.detail, {});
} else {
viewedClientWidgetApi?.transport.reply(ev.detail, {});
}
};
const handleJoin = (ev: CustomEvent) => {
ev.preventDefault();
2025-05-23 13:57:01 -05:00
const setViewedAsActive = () => {
setActiveClientWidgetApi(viewedClientWidgetApi, viewedClientWidget, viewedCallRoomId);
2025-05-23 13:57:01 -05:00
setActiveCallRoomIdState(viewedCallRoomId);
setIsPrimaryIframe(!isPrimaryIframe);
setIsCallActive(true);
};
activeClientWidgetApi?.transport.reply(ev.detail, {});
if (ev.detail.widgetId === activeClientWidgetApi?.widget.id) {
setIsCallActive(true);
return;
}
if (activeClientWidgetApi) {
if (isCallActive && viewedClientWidgetApi && viewedCallRoomId) {
activeClientWidgetApi?.removeAllListeners();
activeClientWidgetApi?.transport.send(WIDGET_HANGUP_ACTION, {}).then(() => {
2025-05-23 13:57:01 -05:00
setViewedAsActive();
});
} else {
setIsCallActive(true);
}
} else {
2025-05-23 13:57:01 -05:00
setViewedAsActive();
}
};
logger.debug(
`CallContext: Setting up listeners for clientWidgetApi in room ${activeCallRoomId}`
);
2025-05-10 20:41:57 -05:00
activeClientWidgetApi?.on(`action:${WIDGET_HANGUP_ACTION}`, handleHangup);
activeClientWidgetApi?.on(`action:${WIDGET_MEDIA_STATE_UPDATE_ACTION}`, handleMediaStateUpdate);
viewedClientWidgetApi?.on(`action:${WIDGET_TILE_UPDATE}`, handleOnTileLayout);
2025-05-10 20:41:57 -05:00
activeClientWidgetApi?.on(`action:${WIDGET_ON_SCREEN_ACTION}`, handleOnScreenStateUpdate);
activeClientWidgetApi?.on(`action:${WIDGET_JOIN_ACTION}`, handleJoin);
viewedClientWidgetApi?.on(`action:${WIDGET_JOIN_ACTION}`, handleJoin);
viewedClientWidgetApi?.on(`action:${WIDGET_MEDIA_STATE_UPDATE_ACTION}`, handleMediaStateUpdate);
viewedClientWidgetApi?.on(`action:${WIDGET_TILE_UPDATE}`, handleOnTileLayout);
2025-05-10 20:41:57 -05:00
viewedClientWidgetApi?.on(`action:${WIDGET_ON_SCREEN_ACTION}`, handleOnScreenStateUpdate);
viewedClientWidgetApi?.on(`action:${WIDGET_HANGUP_ACTION}`, handleHangup);
}, [
activeClientWidgetApi,
activeCallRoomId,
2025-05-10 20:41:57 -05:00
activeClientWidgetApiRoomId,
hangUp,
2025-04-18 03:01:44 -05:00
isChatOpen,
isAudioEnabled,
isVideoEnabled,
isCallActive,
2025-05-10 20:41:57 -05:00
viewedRoomId,
viewedClientWidgetApi,
isPrimaryIframe,
viewedCallRoomId,
setViewedClientWidgetApi,
setActiveClientWidgetApi,
viewedClientWidget,
]);
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> => {
if (!activeClientWidgetApi) {
2025-04-15 22:14:43 -05:00
logger.warn(
`CallContext: Cannot send action '${action}', no active API clientWidgetApi registered.`
2025-04-15 22:14:43 -05:00
);
return Promise.reject(new Error('No active call clientWidgetApi'));
2025-04-15 22:14:43 -05:00
}
2025-05-10 20:41:57 -05:00
if (!activeClientWidgetApiRoomId || activeClientWidgetApiRoomId !== activeCallRoomId) {
logger.debug(
2025-05-10 20:41:57 -05:00
`CallContext: Cannot send action '${action}', clientWidgetApi room (${activeClientWidgetApiRoomId}) does not match active call room (${activeCallRoomId}). Stale clientWidgetApi?`
2025-04-15 22:14:43 -05:00
);
return Promise.reject(new Error('Mismatched active call clientWidgetApi'));
2025-04-15 22:14:43 -05:00
}
2025-05-22 23:14:24 -05:00
logger.debug(
`CallContext: Sending action '${action}' via active clientWidgetApi (room: ${activeClientWidgetApiRoomId}) with data:`,
data
);
await activeClientWidgetApi.transport.send(action as WidgetApiAction, data);
2025-04-15 22:14:43 -05:00
},
2025-05-10 20:41:57 -05:00
[activeClientWidgetApi, activeCallRoomId, activeClientWidgetApiRoomId]
2025-04-15 22:14:43 -05:00
);
const toggleAudio = useCallback(async () => {
const newState = !isAudioEnabled;
logger.debug(`CallContext: Toggling audio. New state: enabled=${newState}`);
setIsAudioEnabledState(newState);
try {
await sendWidgetAction(WIDGET_MEDIA_STATE_UPDATE_ACTION, {
audio_enabled: newState,
video_enabled: isVideoEnabled,
});
logger.debug(`CallContext: Successfully sent audio toggle action.`);
} catch (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(WIDGET_MEDIA_STATE_UPDATE_ACTION, {
audio_enabled: isAudioEnabled,
video_enabled: newState,
});
logger.debug(`CallContext: Successfully sent video toggle action.`);
} catch (error) {
setIsVideoEnabledState(!newState);
throw error;
}
}, [isVideoEnabled, isAudioEnabled, sendWidgetAction]);
2025-04-18 03:01:44 -05:00
const toggleChat = useCallback(async () => {
const newState = !isChatOpen;
setIsChatOpenState(newState);
2025-04-18 03:01:44 -05:00
}, [isChatOpen]);
const toggleIframe = useCallback(async () => {
const newState = !isPrimaryIframe;
setIsPrimaryIframe(newState);
}, [isPrimaryIframe]);
2025-04-15 22:14:43 -05:00
const contextValue = useMemo<CallContextState>(
() => ({
activeCallRoomId,
setActiveCallRoomId,
2025-05-10 20:41:57 -05:00
viewedCallRoomId,
setViewedCallRoomId,
2025-04-15 22:14:43 -05:00
hangUp,
activeClientWidgetApi,
registerActiveClientWidgetApi,
activeClientWidget,
viewedClientWidgetApi,
2025-05-10 20:41:57 -05:00
registerViewedClientWidgetApi,
viewedClientWidget,
2025-04-15 22:14:43 -05:00
sendWidgetAction,
2025-04-18 03:01:44 -05:00
isChatOpen,
isAudioEnabled,
isVideoEnabled,
2025-05-02 02:35:51 -05:00
isCallActive,
isPrimaryIframe,
toggleAudio,
toggleVideo,
2025-04-18 03:01:44 -05:00
toggleChat,
toggleIframe,
2025-04-15 22:14:43 -05:00
}),
[
activeCallRoomId,
setActiveCallRoomId,
2025-05-10 20:41:57 -05:00
viewedCallRoomId,
setViewedCallRoomId,
2025-04-15 22:14:43 -05:00
hangUp,
activeClientWidgetApi,
registerActiveClientWidgetApi,
activeClientWidget,
viewedClientWidgetApi,
2025-05-10 20:41:57 -05:00
registerViewedClientWidgetApi,
viewedClientWidget,
2025-04-15 22:14:43 -05:00
sendWidgetAction,
isChatOpen,
isAudioEnabled,
isVideoEnabled,
2025-05-02 02:35:51 -05:00
isCallActive,
isPrimaryIframe,
toggleAudio,
toggleVideo,
toggleChat,
toggleIframe,
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;
}