diff --git a/src/app/features/call/CallView.tsx b/src/app/features/call/CallView.tsx index 3bb6465f..1d86dfee 100644 --- a/src/app/features/call/CallView.tsx +++ b/src/app/features/call/CallView.tsx @@ -1,5 +1,5 @@ import { Room } from 'matrix-js-sdk'; -import React, { useContext, useMemo, useCallback, useEffect, useRef, MouseEventHandler, useState, ReactNode } from 'react'; +import React, { useContext, useCallback, useEffect, useRef, MouseEventHandler, useState, ReactNode } from 'react'; import { Box, Button, config, Spinner, Text } from 'folds'; import { useCallState } from '../../pages/client/call/CallProvider'; import { useCallMembers } from '../../hooks/useCallMemberships'; @@ -157,7 +157,7 @@ export function CallView({ room }: { room: Room }) { navigateRoom(room.roomId); } if (!callIsCurrentAndReady) { - hangUp(room.roomId); + hangUp(); setActiveCallRoomId(room.roomId); } }; diff --git a/src/app/features/call/SmallWidget.ts b/src/app/features/call/SmallWidget.ts index fd5a1a4a..82efc2b4 100644 --- a/src/app/features/call/SmallWidget.ts +++ b/src/app/features/call/SmallWidget.ts @@ -77,6 +77,7 @@ export interface IApp extends IWidget { roomId: string; eventId?: string; avatar_url?: string; + sender: string; 'io.element.managed_hybrid'?: boolean; } @@ -91,7 +92,7 @@ export class SmallWidget extends EventEmitter { public url?: string; - public iframe: HTMLElement | null; + public iframe: HTMLIFrameElement | null = null; private type: string; // Type of the widget (e.g., 'm.call') @@ -143,8 +144,11 @@ export class SmallWidget extends EventEmitter { // Timelines are most recent last const events = room.getLiveTimeline()?.getEvents() || []; const roomEvent = events[events.length - 1]; - if (!roomEvent) continue; // force later code to think the room is fresh - this.readUpToMap[room.roomId] = roomEvent.getId()!; + // force later code to think the room is fresh + if (roomEvent) { + const eventId = roomEvent.getId(); + if(eventId) this.readUpToMap[room.roomId] = eventId; + } } this.messaging.on('action:org.matrix.msc2876.read_events', (ev: CustomEvent) => { @@ -163,28 +167,16 @@ export class SmallWidget extends EventEmitter { const stateEvents = state.events?.get(type); - for (const [key, eventObject] of stateEvents?.entries() ?? []) { - events.push(eventObject.event); - } + Array.from(stateEvents?.values() ?? []).forEach(eventObject => { + events.push(eventObject.event) + }) return this.messaging?.transport.reply(ev.detail, { events }); }); - /* - this.messaging?.on('action:content_loaded', () => { - this.messaging?.transport?.send('io.element.join', { - audioInput: 'true', - videoInput: 'true', - }); - }); - */ - this.client.on(ClientEvent.Event, this.onEvent); this.client.on(MatrixEventEvent.Decrypted, this.onEventDecrypted); - //this.client.on(RoomStateEvent.Events, this.onStateUpdate); this.client.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent); - //this.client.on(RoomStateEvent.Events, this.onReadEvent); - // this.messaging.setViewedRoomId(this.roomId ?? null); this.messaging.on( `action:${WidgetApiFromWidgetAction.UpdateAlwaysOnScreen}`, async (ev: CustomEvent) => { @@ -196,7 +188,7 @@ export class SmallWidget extends EventEmitter { this.messaging.transport.reply(ev.detail, {}); } // Stop being persistent can be done instantly - //MAKE PERSISTENT HERE + // MAKE PERSISTENT HERE // Send the ack after the widget actually has become sticky. } } @@ -272,21 +264,28 @@ export class SmallWidget extends EventEmitter { const timeline = room.getLiveTimeline(); const events = this.arrayFastClone(timeline.getEvents()).reverse().slice(0, 100); - for (const timelineEvent of events) { - if (timelineEvent.getId() === upToEventId) { + let advanced = false; + + events.some(timelineEvent => { + const id = timelineEvent.getId(); + + if (id === upToEventId) { // The event must be somewhere before the "read up to" marker - return false; - } - if (timelineEvent.getId() === ev.getId()) { - // The event is after the marker; advance it - this.readUpToMap[roomId] = evId; return true; } - } - // We can't say for sure whether the widget has seen the event; let's - // just assume that it has - return false; + if (id === evId) { + // The event is after the marker; advance it + this.readUpToMap[roomId] = evId; + advanced = true; + return true; + } + // We can't say for sure whether the widget has seen the event; let's + // just assume that it has + return false; + }); + + return advanced; } private feedEvent(ev: MatrixEvent): void { @@ -401,12 +400,4 @@ export const createVirtualWidget = ( roomId, // Add other required fields from IWidget if necessary sender: creatorUserId, // Example: Assuming sender is the creator - content: { - // Example content structure - type, - url: url.toString(), - name, - data, - creatorUserId, - }, }); diff --git a/src/app/features/room-nav/RoomCallNavStatus.tsx b/src/app/features/room-nav/RoomCallNavStatus.tsx index 72c0b656..b4885721 100644 --- a/src/app/features/room-nav/RoomCallNavStatus.tsx +++ b/src/app/features/room-nav/RoomCallNavStatus.tsx @@ -60,7 +60,7 @@ export function CallNavStatus() { > - {mx.getRoom(activeCallRoomId)?.name} + {activeCallRoomId ? mx.getRoom(activeCallRoomId)?.name : ""} )} diff --git a/src/app/pages/client/call/CallProvider.tsx b/src/app/pages/client/call/CallProvider.tsx index f5b9b113..5f6c448d 100644 --- a/src/app/pages/client/call/CallProvider.tsx +++ b/src/app/pages/client/call/CallProvider.tsx @@ -8,7 +8,7 @@ import React, { useEffect, } from 'react'; import { logger } from 'matrix-js-sdk/lib/logger'; -import { WidgetApiToWidgetAction, WidgetApiAction, ClientWidgetApi } from 'matrix-widget-api'; +import { WidgetApiToWidgetAction, WidgetApiAction, ClientWidgetApi, IWidgetApiRequestData } from 'matrix-widget-api'; import { useParams } from 'react-router-dom'; import { SmallWidget } from '../../../features/call/SmallWidget'; @@ -30,15 +30,16 @@ interface CallContextState { setActiveCallRoomId: (roomId: string | null) => void; viewedCallRoomId: string | null; setViewedCallRoomId: (roomId: string | null) => void; - hangUp: (room: string) => void; + hangUp: () => void; activeClientWidgetApi: ClientWidgetApi | null; activeClientWidget: SmallWidget | null; registerActiveClientWidgetApi: ( roomId: string | null, clientWidgetApi: ClientWidgetApi | null, - clientWidget: SmallWidget + clientWidget: SmallWidget, + activeClientIframeRef: HTMLIFrameElement ) => void; - sendWidgetAction: ( + sendWidgetAction: ( action: WidgetApiToWidgetAction | string, data: T ) => Promise; @@ -60,7 +61,6 @@ interface CallProviderProps { const DEFAULT_AUDIO_ENABLED = true; const DEFAULT_VIDEO_ENABLED = false; const DEFAULT_CHAT_OPENED = false; -const DEFAULT_CALL_ACTIVE = false; export function CallProvider({ children }: CallProviderProps) { const [activeCallRoomId, setActiveCallRoomIdState] = useState(null); @@ -69,11 +69,12 @@ export function CallProvider({ children }: CallProviderProps) { const [activeClientWidgetApi, setActiveClientWidgetApiState] = useState(null); const [activeClientWidget, setActiveClientWidget] = useState(null); const [activeClientWidgetApiRoomId, setActiveClientWidgetApiRoomId] = useState(null); + const [activeClientWidgetIframeRef, setActiveClientWidgetIframeRef] = useState(null) const [isAudioEnabled, setIsAudioEnabledState] = useState(DEFAULT_AUDIO_ENABLED); const [isVideoEnabled, setIsVideoEnabledState] = useState(DEFAULT_VIDEO_ENABLED); const [isChatOpen, setIsChatOpenState] = useState(DEFAULT_CHAT_OPENED); - const [isActiveCallReady, setIsActiveCallReady] = useState(DEFAULT_CALL_ACTIVE); + const [isActiveCallReady, setIsActiveCallReady] = useState(false); const { roomIdOrAlias: viewedRoomId } = useParams<{ roomIdOrAlias: string }>(); @@ -97,11 +98,13 @@ export function CallProvider({ children }: CallProviderProps) { ( clientWidgetApi: ClientWidgetApi | null, clientWidget: SmallWidget | null, - roomId: string | null + roomId: string | null, + clientWidgetIframeRef: HTMLIFrameElement | null ) => { setActiveClientWidgetApiState(clientWidgetApi); setActiveClientWidget(clientWidget); setActiveClientWidgetApiRoomId(roomId); + setActiveClientWidgetIframeRef(clientWidgetIframeRef) }, [] ); @@ -110,17 +113,19 @@ export function CallProvider({ children }: CallProviderProps) { ( roomId: string | null, clientWidgetApi: ClientWidgetApi | null, - clientWidget: SmallWidget | null + clientWidget: SmallWidget | null, + clientWidgetIframeRef: HTMLIFrameElement | null ) => { if (activeClientWidgetApi && activeClientWidgetApi !== clientWidgetApi) { logger.debug(`CallContext: Cleaning up listeners for previous clientWidgetApi instance.`); } + if (roomId && clientWidgetApi) { logger.debug(`CallContext: Registering active clientWidgetApi for room ${roomId}.`); - setActiveClientWidgetApi(clientWidgetApi, clientWidget, roomId); + setActiveClientWidgetApi(clientWidgetApi, clientWidget, roomId, clientWidgetIframeRef); } else if (roomId === activeClientWidgetApiRoomId || roomId === null) { - setActiveClientWidgetApi(null, null, null); + setActiveClientWidgetApi(null, null, null, null); } }, [activeClientWidgetApi, activeClientWidgetApiRoomId, setActiveClientWidgetApi] @@ -128,7 +133,7 @@ export function CallProvider({ children }: CallProviderProps) { const hangUp = useCallback( () => { - setActiveClientWidgetApi(null, null, null); + setActiveClientWidgetApi(null, null, null, null); setActiveCallRoomIdState(null); activeClientWidgetApi?.transport.send(`${WIDGET_HANGUP_ACTION}`, {}); setIsActiveCallReady(false); @@ -143,7 +148,7 @@ export function CallProvider({ children }: CallProviderProps) { const sendWidgetAction = useCallback( - async (action: WidgetApiToWidgetAction | string, data: T): Promise => { + async (action: WidgetApiToWidgetAction | string, data: T): Promise => { if (!activeClientWidgetApi) { logger.warn( `CallContext: Cannot send action '${action}', no active API clientWidgetApi registered.` @@ -163,6 +168,8 @@ export function CallProvider({ children }: CallProviderProps) { data ); await activeClientWidgetApi.transport.send(action as WidgetApiAction, data); + + return Promise.resolve() }, [activeClientWidgetApi, activeCallRoomId, activeClientWidgetApiRoomId] ); @@ -211,15 +218,14 @@ export function CallProvider({ children }: CallProviderProps) { return; } - const api = activeClientWidgetApi; - if (!api) { + if (!activeClientWidgetApi) { return; } const handleHangup = (ev: CustomEvent) => { ev.preventDefault(); - if (isActiveCallReady && ev.detail.widgetId === activeClientWidgetApi?.widget.id) { - activeClientWidgetApi?.transport.reply(ev.detail, {}); + if (isActiveCallReady && ev.detail.widgetId === activeClientWidgetApi.widget.id) { + activeClientWidgetApi.transport.reply(ev.detail, {}); } logger.debug( `CallContext: Received hangup action from widget in room ${activeCallRoomId}.`, @@ -251,33 +257,39 @@ export function CallProvider({ children }: CallProviderProps) { const handleOnScreenStateUpdate = (ev: CustomEvent) => { ev.preventDefault(); - api.transport.reply(ev.detail, {}); + activeClientWidgetApi.transport.reply(ev.detail, {}); }; const handleOnTileLayout = (ev: CustomEvent) => { ev.preventDefault(); - api.transport.reply(ev.detail, {}); + activeClientWidgetApi.transport.reply(ev.detail, {}); }; const handleJoin = (ev: CustomEvent) => { ev.preventDefault(); - api.transport.reply(ev.detail, {}); + activeClientWidgetApi.transport.reply(ev.detail, {}); + const iframeDoc = - api.iframe?.contentDocument || - api.iframe?.contentWindow.document; - const observer = new MutationObserver(() => { - const button = iframeDoc.querySelector('[data-testid="incall_leave"]'); - if (button) { - button.addEventListener('click', () => { - hangUp() - }); - } - observer.disconnect(); - }); - logger.debug('Join Call'); - observer.observe(iframeDoc, { childList: true, subtree: true }); + activeClientWidgetIframeRef?.contentWindow?.document || + activeClientWidgetIframeRef?.contentDocument; + + if(iframeDoc){ + + const observer = new MutationObserver(() => { + const button = iframeDoc.querySelector('[data-testid="incall_leave"]'); + if (button) { + button.addEventListener('click', () => { + hangUp() + }); + } + observer.disconnect(); + }); + logger.debug('Join Call'); + observer.observe(iframeDoc, { childList: true, subtree: true }); + } + setIsActiveCallReady(true); }; @@ -291,13 +303,14 @@ export function CallProvider({ children }: CallProviderProps) { video_enabled: isVideoEnabled, }); - api.on(`action:${WIDGET_HANGUP_ACTION}`, handleHangup); - api.on(`action:${WIDGET_MEDIA_STATE_UPDATE_ACTION}`, handleMediaStateUpdate); - api.on(`action:${WIDGET_TILE_UPDATE}`, handleOnTileLayout); - api.on(`action:${WIDGET_ON_SCREEN_ACTION}`, handleOnScreenStateUpdate); - api.on(`action:${WIDGET_JOIN_ACTION}`, handleJoin); + activeClientWidgetApi.on(`action:${WIDGET_HANGUP_ACTION}`, handleHangup); + activeClientWidgetApi.on(`action:${WIDGET_MEDIA_STATE_UPDATE_ACTION}`, handleMediaStateUpdate); + activeClientWidgetApi.on(`action:${WIDGET_TILE_UPDATE}`, handleOnTileLayout); + activeClientWidgetApi.on(`action:${WIDGET_ON_SCREEN_ACTION}`, handleOnScreenStateUpdate); + activeClientWidgetApi.on(`action:${WIDGET_JOIN_ACTION}`, handleJoin); }, [ + activeClientWidgetIframeRef, activeClientWidgetApi, activeCallRoomId, activeClientWidgetApiRoomId, diff --git a/src/app/pages/client/call/PersistentCallContainer.tsx b/src/app/pages/client/call/PersistentCallContainer.tsx index f2e4144e..4256bffd 100644 --- a/src/app/pages/client/call/PersistentCallContainer.tsx +++ b/src/app/pages/client/call/PersistentCallContainer.tsx @@ -1,8 +1,7 @@ -import React, { createContext, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { createContext, ReactNode, useCallback, useEffect, useMemo, useRef } from 'react'; import { logger } from 'matrix-js-sdk/lib/logger'; import { ClientWidgetApi } from 'matrix-widget-api'; import { Box } from 'folds'; -import { useParams } from 'react-router-dom'; import { useCallState } from './CallProvider'; import { createVirtualWidget, @@ -19,7 +18,7 @@ interface PersistentCallContainerProps { children: ReactNode; } -export const CallRefContext = createContext(null); +export const CallRefContext = createContext | null>(null); export function PersistentCallContainer({ children }: PersistentCallContainerProps) { const callIframeRef = useRef(null); @@ -44,10 +43,10 @@ export function PersistentCallContainer({ children }: PersistentCallContainerPro const setupWidget = useCallback( ( - widgetApiRef: { current: ClientWidgetApi }, - smallWidgetRef: { current: SmallWidget }, - iframeRef: { current: { src: string } }, - skipLobby: { toString: () => any }, + widgetApiRef: React.MutableRefObject, + smallWidgetRef: React.MutableRefObject, + iframeRef: React.MutableRefObject, + skipLobby: boolean, themeKind: ThemeKind | null ) => { if (mx?.getUserId()) { @@ -112,7 +111,7 @@ export function PersistentCallContainer({ children }: PersistentCallContainerPro const widgetApiInstance = smallWidget.startMessaging(iframeElement); widgetApiRef.current = widgetApiInstance; - registerActiveClientWidgetApi(roomIdToSet, widgetApiRef.current, smallWidget); + registerActiveClientWidgetApi(roomIdToSet, widgetApiRef.current, smallWidget, iframeElement); widgetApiInstance.once('ready', () => { logger.info(`PersistentCallContainer: Widget for ${roomIdToSet} is ready.`);