From dc789e63f2d7cd693c1b728b109dfd14eab94449 Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 15 Aug 2025 18:32:37 +0200 Subject: [PATCH 01/12] Avoid using the deprecated 'room' field on MatrixRTCSession --- src/room/GroupCallView.tsx | 1 + src/room/InCallView.test.tsx | 1 + src/room/InCallView.tsx | 16 ++++++++++------ src/state/CallViewModel.test.ts | 5 +++-- src/state/CallViewModel.ts | 17 +++++++++++------ src/utils/test-viewmodel.ts | 11 +++++++---- 6 files changed, 33 insertions(+), 18 deletions(-) diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 76352523..ea57bd10 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -452,6 +452,7 @@ export const GroupCallView: FC = ({ client={client} matrixInfo={matrixInfo} rtcSession={rtcSession as MatrixRTCSession} + matrixRoom={room} participantCount={participantCount} onLeave={onLeave} header={header} diff --git a/src/room/InCallView.test.tsx b/src/room/InCallView.test.tsx index b88aaad7..ec057e94 100644 --- a/src/room/InCallView.test.tsx +++ b/src/room/InCallView.test.tsx @@ -175,6 +175,7 @@ function createInCallView(): RenderResult & { kind: E2eeType.NONE, }, }} + matrixRoom={room} livekitRoom={livekitRoom} participantCount={0} onLeave={function (): void { diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 5aa270d2..2061289a 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -7,8 +7,8 @@ Please see LICENSE in the repository root for full details. import { RoomContext, useLocalParticipant } from "@livekit/components-react"; import { IconButton, Text, Tooltip } from "@vector-im/compound-web"; -import { ConnectionState, type Room } from "livekit-client"; -import { type MatrixClient } from "matrix-js-sdk"; +import { ConnectionState, type Room as LivekitRoom } from "livekit-client"; +import { type MatrixClient, type Room as MatrixRoom } from "matrix-js-sdk"; import { type FC, type PointerEvent, @@ -166,6 +166,7 @@ export const ActiveCall: FC = (props) => { const reactionsReader = new ReactionsReader(props.rtcSession); const vm = new CallViewModel( props.rtcSession, + props.matrixRoom, livekitRoom, mediaDevices, { @@ -184,6 +185,7 @@ export const ActiveCall: FC = (props) => { } }, [ props.rtcSession, + props.matrixRoom, livekitRoom, mediaDevices, props.e2eeSystem, @@ -212,7 +214,8 @@ export interface InCallViewProps { vm: CallViewModel; matrixInfo: MatrixInfo; rtcSession: MatrixRTCSession; - livekitRoom: Room; + matrixRoom: MatrixRoom; + livekitRoom: LivekitRoom; muteStates: MuteStates; participantCount: number; /** Function to call when the user explicitly ends the call */ @@ -228,6 +231,7 @@ export const InCallView: FC = ({ vm, matrixInfo, rtcSession, + matrixRoom, livekitRoom, muteStates, participantCount, @@ -272,7 +276,7 @@ export const InCallView: FC = ({ const [useExperimentalToDeviceTransport] = useSetting( useExperimentalToDeviceTransportSetting, ); - const encryptionSystem = useRoomEncryptionSystem(rtcSession.room.roomId); + const encryptionSystem = useRoomEncryptionSystem(matrixRoom.roomId); const memberships = useMatrixRTCSessionMemberships(rtcSession); const showToDeviceEncryption = useMemo( @@ -642,7 +646,7 @@ export const InCallView: FC = ({ }; const rageshakeRequestModalProps = useRageshakeRequestModal( - rtcSession.room.roomId, + matrixRoom.roomId, ); const toggleScreensharing = useCallback(() => { @@ -800,7 +804,7 @@ export const InCallView: FC = ({ of()); - const liveKitRoom = mockLivekitRoom( + const livekitRoom = mockLivekitRoom( { localParticipant }, { remoteParticipants$ }, ); @@ -288,7 +288,8 @@ function withCallViewModel( const vm = new CallViewModel( rtcSession as unknown as MatrixRTCSession, - liveKitRoom, + room, + livekitRoom, mediaDevices, options, connectionState$, diff --git a/src/state/CallViewModel.ts b/src/state/CallViewModel.ts index 70183a37..80076cb2 100644 --- a/src/state/CallViewModel.ts +++ b/src/state/CallViewModel.ts @@ -18,7 +18,11 @@ import { type RemoteParticipant, Track, } from "livekit-client"; -import { RoomStateEvent, type Room, type RoomMember } from "matrix-js-sdk"; +import { + RoomStateEvent, + type Room as MatrixRoom, + type RoomMember, +} from "matrix-js-sdk"; import { BehaviorSubject, EMPTY, @@ -368,7 +372,7 @@ type MediaItem = UserMedia | ScreenShare; function getRoomMemberFromRtcMember( rtcMember: CallMembership, - room: Room, + room: MatrixRoom, ): { id: string; member: RoomMember | undefined } { // WARN! This is not exactly the sender but the user defined in the state key. // This will be available once we change to the new "member as object" format in the MatrixRTC object. @@ -481,7 +485,7 @@ export class CallViewModel extends ViewModel { // Handle call membership changes. fromEvent(this.matrixRTCSession, MatrixRTCSessionEvent.MembershipsChanged), // Handle room membership changes (and displayname updates) - fromEvent(this.matrixRTCSession.room, RoomStateEvent.Members), + fromEvent(this.matrixRoom, RoomStateEvent.Members), ).pipe( startWith(this.matrixRTCSession.memberships), map(() => { @@ -497,7 +501,7 @@ export class CallViewModel extends ViewModel { public readonly memberDisplaynames$ = this.memberships$.pipe( map((memberships) => { const displaynameMap = new Map(); - const { room } = this.matrixRTCSession; + const room = this.matrixRoom; // We only consider RTC members for disambiguation as they are the only visible members. for (const rtcMember of memberships) { @@ -565,7 +569,7 @@ export class CallViewModel extends ViewModel { ) => { const newItems = new Map( function* (this: CallViewModel): Iterable<[string, MediaItem]> { - const room = this.matrixRTCSession.room; + const room = this.matrixRoom; // m.rtc.members are the basis for calculating what is visible in the call for (const rtcMember of this.matrixRTCSession.memberships) { const { member, id: livekitParticipantId } = @@ -783,7 +787,7 @@ export class CallViewModel extends ViewModel { public readonly allOthersLeft$ = this.matrixUserChanges$.pipe( map(({ userIds, leftUserIds }) => { - const userId = this.matrixRTCSession.room.client.getUserId(); + const userId = this.matrixRoom.client.getUserId(); if (!userId) { logger.warn("Could access client.getUserId to compute allOthersLeft"); return false; @@ -1485,6 +1489,7 @@ export class CallViewModel extends ViewModel { public constructor( // A call is permanently tied to a single Matrix room and LiveKit room private readonly matrixRTCSession: MatrixRTCSession, + private readonly matrixRoom: MatrixRoom, private readonly livekitRoom: LivekitRoom, private readonly mediaDevices: MediaDevices, private readonly options: CallViewModelOptions, diff --git a/src/utils/test-viewmodel.ts b/src/utils/test-viewmodel.ts index 4781bf3d..7978bd96 100644 --- a/src/utils/test-viewmodel.ts +++ b/src/utils/test-viewmodel.ts @@ -15,7 +15,7 @@ import { vitest } from "vitest"; import { type RelationsContainer } from "matrix-js-sdk/lib/models/relations-container"; import EventEmitter from "events"; -import type { RoomMember, MatrixClient } from "matrix-js-sdk"; +import type { RoomMember, MatrixClient, Room } from "matrix-js-sdk"; import { E2eeType } from "../e2ee/e2eeType"; import { CallViewModel } from "../state/CallViewModel"; import { @@ -37,6 +37,7 @@ export function getBasicRTCSession( initialRemoteRtcMemberships: CallMembership[] = [aliceRtcMember], ): { rtcSession: MockRTCSession; + matrixRoom: Room; remoteRtcMemberships$: BehaviorSubject; } { const matrixRoomId = "!myRoomId:example.com"; @@ -102,6 +103,7 @@ export function getBasicRTCSession( return { rtcSession, + matrixRoom, remoteRtcMemberships$, }; } @@ -122,7 +124,7 @@ export function getBasicCallViewModelEnvironment( handRaisedSubject$: BehaviorSubject>; reactionsSubject$: BehaviorSubject>; } { - const { rtcSession, remoteRtcMemberships$ } = getBasicRTCSession( + const { rtcSession, matrixRoom, remoteRtcMemberships$ } = getBasicRTCSession( members, initialRemoteRtcMemberships, ); @@ -130,13 +132,14 @@ export function getBasicCallViewModelEnvironment( const reactionsSubject$ = new BehaviorSubject({}); const remoteParticipants$ = of([aliceParticipant]); - const liveKitRoom = mockLivekitRoom( + const livekitRoom = mockLivekitRoom( { localParticipant }, { remoteParticipants$ }, ); const vm = new CallViewModel( rtcSession as unknown as MatrixRTCSession, - liveKitRoom, + matrixRoom, + livekitRoom, mockMediaDevices({}), { encryptionSystem: { kind: E2eeType.PER_PARTICIPANT }, From f08ae36f9e2218f815acd7cd538112c18a7ed448 Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 15 Aug 2025 18:38:52 +0200 Subject: [PATCH 02/12] Pause media tracks and show a message when reconnecting to MatrixRTC --- locales/en/app.json | 1 + src/Toast.tsx | 53 +++++++----- src/room/InCallView.tsx | 14 +++- .../__snapshots__/InCallView.test.tsx.snap | 12 +-- src/state/CallViewModel.ts | 82 +++++++++++++++++-- src/utils/test.ts | 1 + 6 files changed, 127 insertions(+), 36 deletions(-) diff --git a/locales/en/app.json b/locales/en/app.json index d375b629..007e372a 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -55,6 +55,7 @@ "profile": "Profile", "reaction": "Reaction", "reactions": "Reactions", + "reconnecting": "Reconnecting…", "settings": "Settings", "unencrypted": "Not encrypted", "username": "Username", diff --git a/src/Toast.tsx b/src/Toast.tsx index ada5b29c..105572c8 100644 --- a/src/Toast.tsx +++ b/src/Toast.tsx @@ -45,6 +45,12 @@ interface Props { * A supporting icon to display within the toast. */ Icon?: ComponentType>; + /** + * Whether the toast should be portaled into the root of the document (rather + * than rendered in-place within the component tree). + * @default true + */ + portal?: boolean; } /** @@ -56,6 +62,7 @@ export const Toast: FC = ({ autoDismiss, children, Icon, + portal = true, }) => { const onOpenChange = useCallback( (open: boolean) => { @@ -71,29 +78,33 @@ export const Toast: FC = ({ } }, [open, autoDismiss, onDismiss]); + const content = ( + <> + + + + + + {children} + + + {Icon && } + + + + ); + return ( - - - - - - - {children} - - - {Icon && } - - - + {portal ? {content} : content} ); }; diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 2061289a..f9bd681c 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -113,6 +113,7 @@ import { useMediaDevices } from "../MediaDevicesContext.ts"; import { EarpieceOverlay } from "./EarpieceOverlay.tsx"; import { useAppBarHidden, useAppBarSecondaryButton } from "../AppBar.tsx"; import { useBehavior } from "../useBehavior.ts"; +import { Toast } from "../Toast.tsx"; const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); @@ -313,6 +314,7 @@ export const InCallView: FC = ({ () => void toggleRaisedHand(), ); + const reconnecting = useBehavior(vm.reconnecting$); const windowMode = useBehavior(vm.windowMode$); const layout = useBehavior(vm.layout$); const tileStoreGeneration = useBehavior(vm.tileStoreGeneration$); @@ -766,6 +768,9 @@ export const InCallView: FC = ({ ); + // The reconnecting toast cannot be dismissed + const onDismissReconnectingToast = useCallback(() => {}, []); + return (
= ({ {renderContent()} + + {t("common.reconnecting")} + diff --git a/src/room/__snapshots__/InCallView.test.tsx.snap b/src/room/__snapshots__/InCallView.test.tsx.snap index b45a30ad..7d6ab966 100644 --- a/src/room/__snapshots__/InCallView.test.tsx.snap +++ b/src/room/__snapshots__/InCallView.test.tsx.snap @@ -256,7 +256,7 @@ exports[`InCallView > rendering > renders 1`] = ` >