From c367c90a96d2833580390f08f650b8842e82da59 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Tue, 15 Apr 2025 22:15:22 -0500 Subject: [PATCH] add PersistentCallContainer (moved iframe to here from RoomView) --- .../pages/call/PersistentCallContainer.tsx | 275 ++++++++++++++++++ 1 file changed, 275 insertions(+) create mode 100644 src/app/pages/call/PersistentCallContainer.tsx diff --git a/src/app/pages/call/PersistentCallContainer.tsx b/src/app/pages/call/PersistentCallContainer.tsx new file mode 100644 index 00000000..691d62e3 --- /dev/null +++ b/src/app/pages/call/PersistentCallContainer.tsx @@ -0,0 +1,275 @@ +import React, { useEffect, useRef, useMemo } from 'react'; +import { logger } from 'matrix-js-sdk/lib/logger'; +import { ClientWidgetApi, IWidgetApiRequest, MatrixCapabilities } from 'matrix-widget-api'; // Assuming imports + +// --- Required Imports (Adjust paths as needed) --- +import { useCallState } from '../client/CallProvider'; +import { + createVirtualWidget, + Edget, + getWidgetData, + getWidgetUrl, +} from '../../features/room/SmallWidget'; +import { useMatrixClient } from '../../hooks/useMatrixClient'; +import { RoomViewHeader } from '../../features/room/RoomViewHeader'; +import { PowerLevelsContextProvider, usePowerLevels } from '../../hooks/usePowerLevels'; +import { Box } from 'folds'; +import { IsDirectRoomProvider, RoomProvider, useRoom } from '../../hooks/useRoom'; +import { useSelectedRoom } from '../../hooks/router/useSelectedRoom'; +import { Page, PageRoot } from '../../components/page'; +import { RouteSpaceProvider, Space, SpaceRouteRoomProvider } from '../client/space'; +import { MobileFriendlyPageNav } from '../MobileFriendly'; +import { SPACE_PATH } from '../paths'; +import { SpaceProvider } from '../../hooks/useSpace'; +import { useSelectedSpace } from '../../hooks/router/useSelectedSpace'; +import { useAtomValue } from 'jotai'; +import { mDirectAtom } from '../../state/mDirectList'; + +// --- Component Props --- +interface PersistentCallContainerProps { + isVisible: boolean; // Prop passed from parent to control display +} + +// --- PersistentCallContainer Component --- + +export function PersistentCallContainer({ isVisible }: PersistentCallContainerProps) { + // Global state access + const { activeCallRoomId, setActiveCallRoomId } = useCallState(); // Get setter for hangup action + const mx = useMatrixClient(); + + // Refs + const iframeRef = useRef(null); + const widgetApiRef = useRef(null); + const edgetRef = useRef(null); + + // Effect to manage iframe src and Widget API lifecycle based SOLELY on activeCallRoomId + useEffect(() => { + // Store the room ID associated with the current Edget instance for clearer cleanup logging + const cleanupRoomId = edgetRef.current?.roomId; + logger.debug(`PersistentCallContainer effect running. activeCallRoomId: ${activeCallRoomId}`); + + // --- Cleanup Function --- + // This runs BEFORE the effect runs again OR when the component unmounts. + // Crucially, it cleans up resources associated with the *previous* activeCallRoomId. + const cleanup = () => { + logger.info(`PersistentCallContainer: Cleaning up for previous room: ${cleanupRoomId}`); + if (edgetRef.current) { + // Ensure stopMessaging handles removing listeners etc. + edgetRef.current.stopMessaging(); + } + // Potentially call widgetApi.stop() or similar if the API instance has it + if (widgetApiRef.current) { + // widgetApiRef.current.stop?.(); // Example + } + // Clear refs for the old instances + widgetApiRef.current = null; + edgetRef.current = null; + }; + + // --- Setup Logic for NEW activeCallRoomId --- + if (activeCallRoomId && mx?.getUserId()) { + // --- 1. Generate new URL and App definition --- + const newUrl = getWidgetUrl(mx, activeCallRoomId); // Use activeCallRoomId + const userId = mx.getUserId() ?? ''; + const app = createVirtualWidget( + mx, + `element-call-${activeCallRoomId}`, // ID based on active room + userId, + 'Element Call', + 'm.call', + newUrl, + false, + // Pass activeCallRoomId to getWidgetData + getWidgetData(mx, activeCallRoomId, {}, { skipLobby: true /* other configs */ }), + activeCallRoomId // Pass activeCallRoomId as the room ID for the widget context + ); + + // --- 2. Update iframe src --- + // This triggers the iframe reload if the src is different. + // The cleanup function from the *previous* effect run will have already cleaned up the old API connection. + if (iframeRef.current && iframeRef.current.src !== newUrl.toString()) { + logger.info( + `PersistentCallContainer: Updating iframe src for ${activeCallRoomId} to ${newUrl.toString()}` + ); + iframeRef.current.src = newUrl.toString(); + } else if (iframeRef.current && !iframeRef.current.src) { + // Handle initial load case if src starts blank + logger.info( + `PersistentCallContainer: Setting initial iframe src for ${activeCallRoomId} to ${newUrl.toString()}` + ); + iframeRef.current.src = newUrl.toString(); + } + + // --- 3. Setup new Widget API connection --- + const iframeElement = iframeRef.current; + if (!iframeElement) { + logger.error('PersistentCallContainer: iframeRef is null, cannot setup API.'); + return cleanup; // Should not happen if iframe is always rendered + } + + // Create and store new Edget instance + logger.debug(`PersistentCallContainer: Creating new Edget/API for ${activeCallRoomId}`); + const edget = new Edget(app); + edgetRef.current = edget; // Store ref to new instance + + try { + const widgetApiInstance = edget.startMessaging(iframeElement); + widgetApiRef.current = widgetApiInstance; // Store ref to new instance + + // --- 4. Add necessary listeners to the NEW widgetApiInstance --- + widgetApiInstance.once('ready', () => { + logger.info(`PersistentCallContainer: Widget for ${activeCallRoomId} is ready.`); + }); + + // Example listener for read_events (adjust as needed) + widgetApiInstance.on( + 'action:org.matrix.msc2876.read_events', + (ev: CustomEvent) => { + logger.info(`PersistentCallContainer: Widget requested 'read_events':`, ev.detail.data); + ev.preventDefault(); // Prevent default driver handling + // Use the current widgetApiRef to reply + widgetApiRef.current?.transport?.reply(ev.detail, { approved: true }); + } + ); + + // Listener for hangup action from the widget + widgetApiInstance.on('action:im.vector.hangup', () => { + logger.info( + `PersistentCallContainer: Received hangup action from widget in room ${activeCallRoomId}.` + ); + // Call the global state function to clear the active call + // Check if we are still the active call before clearing, prevents race conditions + if (edgetRef.current?.roomId === activeCallRoomId) { + setActiveCallRoomId(null); + } + }); + + // Add other listeners (TURN servers, etc.)... + } catch (error) { + logger.error( + `PersistentCallContainer: Error initializing widget messaging for ${activeCallRoomId}:`, + error + ); + // Cleanup immediately if setup fails + cleanup(); + } + } else { + // --- No active call --- + // Ensure src is blank and perform cleanup for any previous instance + if (iframeRef.current && iframeRef.current.src !== 'about:blank') { + logger.info('PersistentCallContainer: No active call, setting src to about:blank'); + iframeRef.current.src = 'about:blank'; + } + // Run cleanup in case an instance was active before becoming null + cleanup(); + } + + // Return the cleanup function to be executed when dependencies change or component unmounts + return cleanup; + + // CRITICAL Dependencies: This effect manages the lifecycle based on the active call ID + }, [activeCallRoomId, mx, setActiveCallRoomId]); + + // --- Render --- + // Apply conditional styling based on the isVisible prop passed by the parent + const containerStyle: React.CSSProperties = { + width: '100%', + height: '100%', + // --- Visibility Control --- + top: '1', + left: '1', + display: isVisible ? 'flex' : 'none', // Use flex/block as appropriate for your layout + flexDirection: 'row', + //overflow: 'hidden', + }; + const roomId = useSelectedRoom(); + const room = mx.getRoom(roomId); + const selectedSpaceId = useSelectedSpace(); + const space = mx.getRoom(selectedSpaceId); + const mDirects = useAtomValue(mDirectAtom); + const powerLevels = usePowerLevels(room); + + // Assuming necessary imports: React, Box, Page, PageRoot, MobileFriendlyPageNav, + // Space, RoomViewHeader, iframeRef, PowerLevelsContextProvider, + // RouteSpaceProvider, SpaceRouteRoomProvider, SPACE_PATH, powerLevels, containerStyle + + return ( + // Outer container div controlled by parent via isVisible prop +
+ {/* Context provider(s) needed by components inside */} + {/* Pass actual powerLevels if required */} + + {/* Route/Space specific context providers MUST wrap both Space and Header */} + + + {/* Main layout container inside providers: Flex Row */} + {/* Assuming Box handles flex layout */} + + {/* --- Left Side (Nav/Space) --- */} + + {' '} + {/* Example style */} + {/* PageRoot likely renders the nav prop */} + + {/* Space component requires the providers above */} + + + } + /> + + {/* --- Right Side (Header + Iframe) --- */} + {/* This Box takes remaining space and arranges header/iframe vertically */} + + {/* Header Area */} + + {' '} + {/* Header doesn't grow/shrink */} + {/* RoomViewHeader requires the providers above */} + + + + {/* Iframe Area (takes remaining space) */} + + {' '} + {/* Use relative positioning for absolute child */} +