From e741285b11212e9feedcb71a250232024e1aec1c Mon Sep 17 00:00:00 2001 From: Timo K Date: Fri, 7 Nov 2025 14:04:40 +0100 Subject: [PATCH] Fix lints, move CallViewModel.test.ts. Fix audio renderer --- src/button/ReactionToggleButton.test.tsx | 2 +- src/button/ReactionToggleButton.tsx | 2 +- src/reactions/useReactionsSender.tsx | 2 +- src/room/CallEventAudioRenderer.test.tsx | 2 +- src/room/CallEventAudioRenderer.tsx | 2 +- src/room/InCallView.tsx | 5 ++-- src/room/ReactionAudioRenderer.test.tsx | 2 +- src/room/ReactionAudioRenderer.tsx | 2 +- src/room/ReactionsOverlay.tsx | 2 +- src/settings/DeveloperSettingsTab.tsx | 1 - .../{ => CallViewModel}/CallViewModel.test.ts | 20 ++++++------- src/state/CallViewModel/CallViewModel.ts | 28 ++++++++++++++--- .../localMember/LocalMembership.ts | 5 ++-- .../CallViewModel/localMember/Publisher.ts | 6 ++-- .../remoteMembers/Connection.test.ts | 21 ++++++------- .../remoteMembers/MatrixLivekitMembers.ts | 2 +- .../remoteMembers/displayname.test.ts | 30 +++++-------------- src/tile/GridTile.test.tsx | 2 +- src/utils/test-viewmodel.ts | 2 +- 19 files changed, 71 insertions(+), 67 deletions(-) rename src/state/{ => CallViewModel}/CallViewModel.test.ts (99%) diff --git a/src/button/ReactionToggleButton.test.tsx b/src/button/ReactionToggleButton.test.tsx index c7ac5aa0..f6b7a2ea 100644 --- a/src/button/ReactionToggleButton.test.tsx +++ b/src/button/ReactionToggleButton.test.tsx @@ -13,7 +13,7 @@ import { type ReactNode } from "react"; import { ReactionToggleButton } from "./ReactionToggleButton"; import { ElementCallReactionEventType } from "../reactions"; -import { type CallViewModel } from "../state/CallViewModel"; +import { type CallViewModel } from "../state/CallViewModel/CallViewModel"; import { getBasicCallViewModelEnvironment } from "../utils/test-viewmodel"; import { alice, local, localRtcMember } from "../utils/test-fixtures"; import { type MockRTCSession } from "../utils/test"; diff --git a/src/button/ReactionToggleButton.tsx b/src/button/ReactionToggleButton.tsx index 69673293..0c722baf 100644 --- a/src/button/ReactionToggleButton.tsx +++ b/src/button/ReactionToggleButton.tsx @@ -33,7 +33,7 @@ import { ReactionsRowSize, } from "../reactions"; import { Modal } from "../Modal"; -import { type CallViewModel } from "../state/CallViewModel"; +import { type CallViewModel } from "../state/CallViewModel/CallViewModel"; import { useBehavior } from "../useBehavior"; interface InnerButtonProps extends ComponentPropsWithoutRef<"button"> { diff --git a/src/reactions/useReactionsSender.tsx b/src/reactions/useReactionsSender.tsx index ec29c2af..afb9b789 100644 --- a/src/reactions/useReactionsSender.tsx +++ b/src/reactions/useReactionsSender.tsx @@ -20,7 +20,7 @@ import { logger } from "matrix-js-sdk/lib/logger"; import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships"; import { useClientState } from "../ClientContext"; import { ElementCallReactionEventType, type ReactionOption } from "."; -import { type CallViewModel } from "../state/CallViewModel"; +import { type CallViewModel } from "../state/CallViewModel/CallViewModel"; import { useBehavior } from "../useBehavior"; interface ReactionsSenderContextType { diff --git a/src/room/CallEventAudioRenderer.test.tsx b/src/room/CallEventAudioRenderer.test.tsx index e49c7011..733346eb 100644 --- a/src/room/CallEventAudioRenderer.test.tsx +++ b/src/room/CallEventAudioRenderer.test.tsx @@ -38,7 +38,7 @@ import { local, localRtcMember, } from "../utils/test-fixtures"; -import { MAX_PARTICIPANT_COUNT_FOR_SOUND } from "../state/CallViewModel"; +import { MAX_PARTICIPANT_COUNT_FOR_SOUND } from "../state/CallViewModel/CallViewModel"; vitest.mock("livekit-client/e2ee-worker?worker"); vitest.mock("../useAudioContext"); diff --git a/src/room/CallEventAudioRenderer.tsx b/src/room/CallEventAudioRenderer.tsx index 23997c37..d33f3b84 100644 --- a/src/room/CallEventAudioRenderer.tsx +++ b/src/room/CallEventAudioRenderer.tsx @@ -7,7 +7,7 @@ Please see LICENSE in the repository root for full details. import { type ReactNode, useEffect } from "react"; -import { type CallViewModel } from "../state/CallViewModel"; +import { type CallViewModel } from "../state/CallViewModel/CallViewModel"; import joinCallSoundMp3 from "../sound/join_call.mp3"; import joinCallSoundOgg from "../sound/join_call.ogg"; import leftCallSoundMp3 from "../sound/left_call.mp3"; diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 06c1ccb4..7f469460 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -734,7 +734,8 @@ export const InCallView: FC = ({ = ({ key={url} url={url} livekitRoom={livekitRoom} - validIdentities={participants.map((p) => p.identity)} + validIdentities={participants} muted={muteAllAudio} /> ))} diff --git a/src/room/ReactionAudioRenderer.test.tsx b/src/room/ReactionAudioRenderer.test.tsx index 83188be7..31c0a0cb 100644 --- a/src/room/ReactionAudioRenderer.test.tsx +++ b/src/room/ReactionAudioRenderer.test.tsx @@ -27,7 +27,7 @@ import { import { useAudioContext } from "../useAudioContext"; import { GenericReaction, ReactionSet } from "../reactions"; import { prefetchSounds } from "../soundUtils"; -import { type CallViewModel } from "../state/CallViewModel"; +import { type CallViewModel } from "../state/CallViewModel/CallViewModel"; import { getBasicCallViewModelEnvironment } from "../utils/test-viewmodel"; import { alice, diff --git a/src/room/ReactionAudioRenderer.tsx b/src/room/ReactionAudioRenderer.tsx index 2b95acb9..06170d19 100644 --- a/src/room/ReactionAudioRenderer.tsx +++ b/src/room/ReactionAudioRenderer.tsx @@ -12,7 +12,7 @@ import { GenericReaction, ReactionSet } from "../reactions"; import { useAudioContext } from "../useAudioContext"; import { prefetchSounds } from "../soundUtils"; import { useLatest } from "../useLatest"; -import { type CallViewModel } from "../state/CallViewModel"; +import { type CallViewModel } from "../state/CallViewModel/CallViewModel"; const soundMap = Object.fromEntries([ ...ReactionSet.filter((v) => v.sound !== undefined).map((v) => [ diff --git a/src/room/ReactionsOverlay.tsx b/src/room/ReactionsOverlay.tsx index f3dff848..e7c097d2 100644 --- a/src/room/ReactionsOverlay.tsx +++ b/src/room/ReactionsOverlay.tsx @@ -8,7 +8,7 @@ Please see LICENSE in the repository root for full details. import { type ReactNode } from "react"; import styles from "./ReactionsOverlay.module.css"; -import { type CallViewModel } from "../state/CallViewModel"; +import { type CallViewModel } from "../state/CallViewModel/CallViewModel"; import { useBehavior } from "../useBehavior"; export function ReactionsOverlay({ vm }: { vm: CallViewModel }): ReactNode { diff --git a/src/settings/DeveloperSettingsTab.tsx b/src/settings/DeveloperSettingsTab.tsx index e29e9c15..a8f485b6 100644 --- a/src/settings/DeveloperSettingsTab.tsx +++ b/src/settings/DeveloperSettingsTab.tsx @@ -26,7 +26,6 @@ import { duplicateTiles as duplicateTilesSetting, debugTileLayout as debugTileLayoutSetting, showConnectionStats as showConnectionStatsSetting, - multiSfu as multiSfuSetting, muteAllAudio as muteAllAudioSetting, alwaysShowIphoneEarpiece as alwaysShowIphoneEarpieceSetting, matrixRTCMode as matrixRTCModeSetting, diff --git a/src/state/CallViewModel.test.ts b/src/state/CallViewModel/CallViewModel.test.ts similarity index 99% rename from src/state/CallViewModel.test.ts rename to src/state/CallViewModel/CallViewModel.test.ts index fec6b8cf..13693dc1 100644 --- a/src/state/CallViewModel.test.ts +++ b/src/state/CallViewModel/CallViewModel.test.ts @@ -50,7 +50,7 @@ import { deepCompare } from "matrix-js-sdk/lib/utils"; import { AutoDiscovery } from "matrix-js-sdk/lib/autodiscovery"; import { CallViewModel, type CallViewModelOptions } from "./CallViewModel"; -import { type Layout } from "./layout-types"; +import { type Layout } from "../layout-types.ts"; import { mockLocalParticipant, mockMatrixRoom, @@ -65,9 +65,9 @@ import { testScope, mockLivekitRoom, exampleTransport, -} from "../utils/test"; -import { E2eeType } from "../e2ee/e2eeType"; -import type { RaisedHandInfo, ReactionInfo } from "../reactions"; +} from "../../utils/test.ts"; +import { E2eeType } from "../../e2ee/e2eeType.ts"; +import type { RaisedHandInfo, ReactionInfo } from "../../reactions/index.ts"; import { alice, aliceDoppelganger, @@ -89,15 +89,15 @@ import { localId, localRtcMember, localRtcMemberDevice2, -} from "../utils/test-fixtures"; -import { MediaDevices } from "./MediaDevices"; -import { getValue } from "../utils/observable"; -import { type Behavior, constant } from "./Behavior"; -import type { ProcessorState } from "../livekit/TrackProcessorContext.tsx"; +} from "../../utils/test-fixtures.ts"; +import { MediaDevices } from "../MediaDevices.ts"; +import { getValue } from "../../utils/observable.ts"; +import { type Behavior, constant } from "../Behavior.ts"; +import type { ProcessorState } from "../../livekit/TrackProcessorContext.tsx"; import { type ElementCallError, MatrixRTCTransportMissingError, -} from "../utils/errors.ts"; +} from "../../utils/errors.ts"; vi.mock("rxjs", async (importOriginal) => ({ ...(await importOriginal()), diff --git a/src/state/CallViewModel/CallViewModel.ts b/src/state/CallViewModel/CallViewModel.ts index f3c1bca9..ef4be238 100644 --- a/src/state/CallViewModel/CallViewModel.ts +++ b/src/state/CallViewModel/CallViewModel.ts @@ -156,7 +156,11 @@ interface LayoutScanState { } type MediaItem = UserMedia | ScreenShare; - +type AudioLivekitItem = { + livekitRoom: LivekitRoom; + participants: string[]; + url: string; +}; /** * A view model providing all the application logic needed to show the in-call * UI (may eventually be expanded to cover the lobby and feedback screens in the @@ -166,8 +170,6 @@ type MediaItem = UserMedia | ScreenShare; // state and LiveKit state. We use the common terminology of room "members", RTC // "memberships", and LiveKit "participants". export class CallViewModel { - private readonly urlParams = getUrlParams(); - private readonly userId = this.matrixRoom.client.getUserId()!; private readonly deviceId = this.matrixRoom.client.getDeviceId()!; @@ -285,6 +287,7 @@ export class CallViewModel { // ------------------------------------------------------------------------ // ROOM MEMBER tracking TODO + // eslint-disable-next-line @typescript-eslint/no-unused-vars private roomMembers$ = createRoomMembers$(this.scope, this.matrixRoom); /** * If there is a configuration error with the call (e.g. misconfigured E2EE). @@ -311,6 +314,7 @@ export class CallViewModel { * than whether all connections are truly up and running. */ // DISCUSS ? lets think why we need joined and how to do it better + // eslint-disable-next-line @typescript-eslint/no-unused-vars private readonly joined$ = this.localMembership.connected$; /** @@ -327,7 +331,23 @@ export class CallViewModel { public readonly audioParticipants$ = this.scope.behavior( this.matrixLivekitMembers$.pipe( - map((members) => members.value.map((m) => m.participant)), + map((members) => + members.value.reduce((acc, curr) => { + const url = curr.connection?.transport.livekit_service_url; + const livekitRoom = curr.connection?.livekitRoom; + const participant = curr.participant?.identity; + + if (!url || !livekitRoom || !participant) return acc; + + const existing = acc.find((item) => item.url === url); + if (existing) { + existing.participants.push(participant); + } else { + acc.push({ livekitRoom, participants: [participant], url }); + } + return acc; + }, []), + ), ), ); diff --git a/src/state/CallViewModel/localMember/LocalMembership.ts b/src/state/CallViewModel/localMember/LocalMembership.ts index 96edd8da..a0b06d46 100644 --- a/src/state/CallViewModel/localMember/LocalMembership.ts +++ b/src/state/CallViewModel/localMember/LocalMembership.ts @@ -135,7 +135,8 @@ export const createLocalMembership$ = ({ startTracks: () => Behavior; requestDisconnect: () => Observable | null; connectionState: LocalMemberConnectionState; - sharingScreen$: Behavior; + // Use null here since behavior cannot be initialised with undefined. + sharingScreen$: Behavior; toggleScreenSharing: (() => void) | null; // deprecated fields @@ -432,7 +433,7 @@ export const createLocalMembership$ = ({ const sharingScreen$ = scope.behavior( connection$.pipe( switchMap((c) => { - if (!c) return of(undefined); + if (!c) return of(null); if (c.state$.value.state === "ConnectedToLkRoom") return observeSharingScreen$(c.livekitRoom.localParticipant); return of(false); diff --git a/src/state/CallViewModel/localMember/Publisher.ts b/src/state/CallViewModel/localMember/Publisher.ts index 9be50bde..ffdd2487 100644 --- a/src/state/CallViewModel/localMember/Publisher.ts +++ b/src/state/CallViewModel/localMember/Publisher.ts @@ -31,7 +31,7 @@ import { } from "../../../livekit/TrackProcessorContext.tsx"; import { getUrlParams } from "../../../UrlParams.ts"; import { observeTrackReference$ } from "../../MediaViewModel.ts"; -import { type Connection } from "../CallViewModel/remoteMembers/Connection.ts"; +import { type Connection } from "../remoteMembers/Connection.ts"; import { type ObservableScope } from "../../ObservableScope.ts"; /** @@ -64,7 +64,7 @@ export class Publisher { const room = connection.livekitRoom; - room.setE2EEEnabled(e2eeLivekitOptions !== undefined)?.catch((e) => { + room.setE2EEEnabled(e2eeLivekitOptions !== undefined)?.catch((e: Error) => { this.logger?.error("Failed to set E2EE enabled on room", e); }); @@ -249,7 +249,7 @@ export class Publisher { ) { lkRoom .switchActiveDevice(kind, device.id) - .catch((e) => + .catch((e: Error) => this.logger?.error( `Failed to sync ${kind} device with LiveKit`, e, diff --git a/src/state/CallViewModel/remoteMembers/Connection.test.ts b/src/state/CallViewModel/remoteMembers/Connection.test.ts index 7d87781f..a3a42928 100644 --- a/src/state/CallViewModel/remoteMembers/Connection.test.ts +++ b/src/state/CallViewModel/remoteMembers/Connection.test.ts @@ -22,6 +22,7 @@ import { type Room as LivekitRoom, RoomEvent, type RoomOptions, + ConnectionState as LivekitConnectionState, } from "livekit-client"; import fetchMock from "fetch-mock"; import EventEmitter from "events"; @@ -32,6 +33,7 @@ import type { LivekitTransport, } from "matrix-js-sdk/lib/matrixrtc"; import { + Connection, type ConnectionOpts, type ConnectionState, type PublishingParticipant, @@ -103,7 +105,7 @@ function setupTest(): void { disconnect: vi.fn(), remoteParticipants: new Map(), localParticipant: fakeLocalParticipant, - state: ConnectionState.Disconnected, + state: LivekitConnectionState.Disconnected, on: fakeRoomEventEmiter.on.bind(fakeRoomEventEmiter), off: fakeRoomEventEmiter.off.bind(fakeRoomEventEmiter), addListener: fakeRoomEventEmiter.addListener.bind(fakeRoomEventEmiter), @@ -115,11 +117,10 @@ function setupTest(): void { } as unknown as LivekitRoom); } -function setupRemoteConnection(): RemoteConnection { +function setupRemoteConnection(): Connection { const opts: ConnectionOpts = { client: client, transport: livekitFocus, - remoteTransports$: fakeMembershipsFocusMap$, scope: testScope, livekitRoomFactory: () => fakeLivekitRoom, }; @@ -136,7 +137,7 @@ function setupRemoteConnection(): RemoteConnection { fakeLivekitRoom.connect.mockResolvedValue(undefined); - return new RemoteConnection(opts, undefined); + return new Connection(opts); } afterEach(() => { @@ -152,11 +153,10 @@ describe("Start connection states", () => { const opts: ConnectionOpts = { client: client, transport: livekitFocus, - remoteTransports$: fakeMembershipsFocusMap$, scope: testScope, livekitRoomFactory: () => fakeLivekitRoom, }; - const connection = new RemoteConnection(opts, undefined); + const connection = new Connection(opts); expect(connection.state$.getValue().state).toEqual("Initialized"); }); @@ -168,12 +168,11 @@ describe("Start connection states", () => { const opts: ConnectionOpts = { client: client, transport: livekitFocus, - remoteTransports$: fakeMembershipsFocusMap$, scope: testScope, livekitRoomFactory: () => fakeLivekitRoom, }; - const connection = new RemoteConnection(opts, undefined); + const connection = new Connection(opts, undefined); const capturedStates: ConnectionState[] = []; const s = connection.state$.subscribe((value) => { @@ -221,12 +220,11 @@ describe("Start connection states", () => { const opts: ConnectionOpts = { client: client, transport: livekitFocus, - remoteTransports$: fakeMembershipsFocusMap$, scope: testScope, livekitRoomFactory: () => fakeLivekitRoom, }; - const connection = new RemoteConnection(opts, undefined); + const connection = new Connection(opts, undefined); const capturedStates: ConnectionState[] = []; const s = connection.state$.subscribe((value) => { @@ -278,12 +276,11 @@ describe("Start connection states", () => { const opts: ConnectionOpts = { client: client, transport: livekitFocus, - remoteTransports$: fakeMembershipsFocusMap$, scope: testScope, livekitRoomFactory: () => fakeLivekitRoom, }; - const connection = new RemoteConnection(opts, undefined); + const connection = new Connection(opts, undefined); const capturedStates: ConnectionState[] = []; const s = connection.state$.subscribe((value) => { diff --git a/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.ts b/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.ts index 544f5241..c9434327 100644 --- a/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.ts +++ b/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.ts @@ -55,7 +55,7 @@ interface Props { // => Extract an AvatarService instead? // Better with just `getMember` matrixRoom: Pick & NodeStyleEventEmitter; - roomMember$: Behavior>; + // roomMember$: Behavior>; } // Alternative structure idea: // const livekitMatrixMember$ = (callMemberships$,connectionManager,scope): Observable => { diff --git a/src/state/CallViewModel/remoteMembers/displayname.test.ts b/src/state/CallViewModel/remoteMembers/displayname.test.ts index 9822e486..dd359318 100644 --- a/src/state/CallViewModel/remoteMembers/displayname.test.ts +++ b/src/state/CallViewModel/remoteMembers/displayname.test.ts @@ -14,7 +14,7 @@ import { } from "matrix-js-sdk"; import EventEmitter from "events"; -import { ObservableScope } from "../../ObservableScope.ts"; +import { ObservableScope, trackEpoch } from "../../ObservableScope.ts"; import type { Room as MatrixRoom } from "matrix-js-sdk/lib/models/room"; import { mockCallMembership, withTestScheduler } from "../../../utils/test.ts"; import { memberDisplaynames$ } from "./displayname.ts"; @@ -90,9 +90,7 @@ test("should always have our own user", () => { mockMatrixRoom, cold("a", { a: [], - }), - "@local:example.com", - "DEVICE000", + }).pipe(trackEpoch()), ); expectObservable(dn$).toBe("a", { @@ -125,9 +123,7 @@ test("should get displayName for users", () => { mockCallMembership("@alice:example.com", "DEVICE1"), mockCallMembership("@bob:example.com", "DEVICE1"), ], - }), - "@local:example.com", - "DEVICE000", + }).pipe(trackEpoch()), ); expectObservable(dn$).toBe("a", { @@ -149,9 +145,7 @@ test("should use userId if no display name", () => { mockMatrixRoom, cold("a", { a: [mockCallMembership("@no-name:foo.bar", "D000")], - }), - "@local:example.com", - "DEVICE000", + }).pipe(trackEpoch()), ); expectObservable(dn$).toBe("a", { @@ -178,9 +172,7 @@ test("should disambiguate users with same display name", () => { mockCallMembership("@carl:example.com", "C000"), mockCallMembership("@evil:example.com", "E000"), ], - }), - "@local:example.com", - "DEVICE000", + }).pipe(trackEpoch()), ); expectObservable(dn$).toBe("a", { @@ -209,9 +201,7 @@ test("should disambiguate when needed", () => { mockCallMembership("@bob:example.com", "DEVICE1"), mockCallMembership("@bob:foo.bar", "BOB000"), ], - }), - "@local:example.com", - "DEVICE000", + }).pipe(trackEpoch()), ); expectObservable(dn$).toBe("ab", { @@ -241,9 +231,7 @@ test.skip("should keep disambiguated name when other leave", () => { mockCallMembership("@bob:foo.bar", "BOB000"), ], b: [mockCallMembership("@bob:example.com", "DEVICE1")], - }), - "@local:example.com", - "DEVICE000", + }).pipe(trackEpoch()), ); expectObservable(dn$).toBe("ab", { @@ -272,9 +260,7 @@ test("should disambiguate on name change", () => { mockCallMembership("@bob:example.com", "B000"), mockCallMembership("@carl:example.com", "C000"), ], - }), - "@local:example.com", - "DEVICE000", + }).pipe(trackEpoch()), ); schedule("-a", { diff --git a/src/tile/GridTile.test.tsx b/src/tile/GridTile.test.tsx index dd0bc9d6..e3172a22 100644 --- a/src/tile/GridTile.test.tsx +++ b/src/tile/GridTile.test.tsx @@ -15,7 +15,7 @@ import { GridTile } from "./GridTile"; import { mockRtcMembership, createRemoteMedia } from "../utils/test"; import { GridTileViewModel } from "../state/TileViewModel"; import { ReactionsSenderProvider } from "../reactions/useReactionsSender"; -import type { CallViewModel } from "../state/CallViewModel"; +import type { CallViewModel } from "../state/CallViewModel/CallViewModel"; import { constant } from "../state/Behavior"; global.IntersectionObserver = class MockIntersectionObserver { diff --git a/src/utils/test-viewmodel.ts b/src/utils/test-viewmodel.ts index 5cd64eb3..5a0d7526 100644 --- a/src/utils/test-viewmodel.ts +++ b/src/utils/test-viewmodel.ts @@ -22,7 +22,7 @@ import { E2eeType } from "../e2ee/e2eeType"; import { CallViewModel, type CallViewModelOptions, -} from "../state/CallViewModel"; +} from "../state/CallViewModel/CallViewModel"; import { mockLivekitRoom, mockLocalParticipant,