diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 667aaecf..8e396841 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -27,7 +27,7 @@ import { ConnectionState, Room, Track } from "livekit-client"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { Room as MatrixRoom } from "matrix-js-sdk/src/models/room"; -import { Ref, useCallback, useEffect, useMemo, useRef } from "react"; +import { Ref, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import useMeasure from "react-use-measure"; import { OverlayTriggerState } from "@react-stately/overlays"; @@ -73,7 +73,10 @@ import { MuteStates } from "./MuteStates"; import { MatrixInfo } from "./VideoPreview"; import { ShareButton } from "../button/ShareButton"; import { LayoutToggle } from "./LayoutToggle"; -import { ECConnectionState } from "../livekit/useECConnectionState"; +import { + ECAddonConnectionState, + ECConnectionState, +} from "../livekit/useECConnectionState"; import { useOpenIDSFU } from "../livekit/openIDSFU"; const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); @@ -82,6 +85,9 @@ const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); // For now we can disable screensharing in Safari. const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); +// How long we wait after a focus switch before showing the real participant list again +const POST_FOCUS_PARTICIPANT_UPDATE_DELAY_MS = 3000; + export interface ActiveCallProps extends Omit { e2eeConfig?: E2EEConfig; @@ -236,7 +242,7 @@ export function InCallView({ const reducedControls = boundsValid && bounds.width <= 340; const noControls = reducedControls && bounds.height <= 400; - const items = useParticipantTiles(livekitRoom, rtcSession.room); + const items = useParticipantTiles(livekitRoom, rtcSession.room, connState); const { fullscreenItem, toggleFullscreen, exitFullscreen } = useFullscreen(items); @@ -476,8 +482,11 @@ function findMatrixMember( function useParticipantTiles( livekitRoom: Room, - matrixRoom: MatrixRoom + matrixRoom: MatrixRoom, + connState: ECConnectionState ): TileDescriptor[] { + const previousTiles = useRef[]>([]); + const sfuParticipants = useParticipants({ room: livekitRoom, }); @@ -559,5 +568,44 @@ function useParticipantTiles( return allGhosts ? [] : tiles; }, [matrixRoom, sfuParticipants]); - return items; + // We carry over old tiles from the previous focus for some time after a focus switch + // so that the video tiles don't all disappear and reappear. + // This is set to true when the state transitions to Switching Focus and remains + // true for a short time after it changes (ie. connState is only switching focus for + // the time it takes us to reconnect to the conference). + // If there are still members that haven't reconnected after that time, they'll just + // appear to disconnect and will reappear once they reconnect. + const [isSwitchingFocus, setIsSwitchingFocus] = useState(false); + + useEffect(() => { + if (connState === ECAddonConnectionState.ECSwitchingFocus) { + setIsSwitchingFocus(true); + } else if (isSwitchingFocus) { + setTimeout(() => { + setIsSwitchingFocus(false); + }, POST_FOCUS_PARTICIPANT_UPDATE_DELAY_MS); + } + }, [connState, setIsSwitchingFocus, isSwitchingFocus]); + + if ( + connState === ECAddonConnectionState.ECSwitchingFocus || + isSwitchingFocus + ) { + logger.debug("Switching focus: injecting previous tiles"); + + // inject the previous tile for members that haven't rejoined yet + const newItems = items.slice(0); + const rejoined = new Set(newItems.map((p) => p.id)); + + for (const prevTile of previousTiles.current) { + if (!rejoined.has(prevTile.id)) { + newItems.push(prevTile); + } + } + + return newItems; + } else { + previousTiles.current = items; + return items; + } }