From f2622f00322b40cd3f80ccea544fad2ba74836c8 Mon Sep 17 00:00:00 2001 From: "Timo K." Date: Sat, 15 Aug 2026 22:30:17 +0200 Subject: [PATCH 01/18] Add vm to developer settings tab --- src/room/InCallView.tsx | 1 + src/settings/DeveloperSettingsTab.tsx | 4 ++++ src/settings/SettingsModal.tsx | 5 +++++ 3 files changed, 10 insertions(+) diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 322380eee..5aa9667ce 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -632,6 +632,7 @@ export const InCallView: FC = ({ onDismiss={(): void => setSettingsOpen(false)} tab={settingsTab} onTabChange={setSettingsTab} + vm={vm} livekitRooms={allConnections .getConnections() .map((connectionItem) => ({ diff --git a/src/settings/DeveloperSettingsTab.tsx b/src/settings/DeveloperSettingsTab.tsx index 70db13db9..19af03373 100644 --- a/src/settings/DeveloperSettingsTab.tsx +++ b/src/settings/DeveloperSettingsTab.tsx @@ -67,6 +67,7 @@ import styles from "./DeveloperSettingsTab.module.css"; import settingsStyles from "./SettingsModal.module.css"; import { Slider } from "../Slider"; import { useUrlParams } from "../UrlParams"; +import { type CallViewModel } from "../state/CallViewModel/CallViewModel.ts"; import { getSFUConfigWithOpenID } from "../livekit/openIDSFU"; interface Props { @@ -79,6 +80,8 @@ interface Props { livekitAlias?: string; }[]; env: ImportMetaEnv; + /** Only available while in a call. */ + vm?: CallViewModel; } export const DeveloperSettingsTab: FC = ({ @@ -86,6 +89,7 @@ export const DeveloperSettingsTab: FC = ({ livekitRooms, roomId, env, + vm, }) => { const { t } = useTranslation(); const [duplicateTiles, setDuplicateTiles] = useSetting(duplicateTilesSetting); diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index 665eadf01..f47cfea4e 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -34,6 +34,7 @@ import { FieldRow, InputField } from "../input/Input"; import { useSubmitRageshake } from "./submit-rageshake"; import { useUrlParams } from "../UrlParams"; import { useBehavior } from "../useBehavior"; +import { type CallViewModel } from "../state/CallViewModel/CallViewModel.ts"; type SettingsTab = | "audio" @@ -56,6 +57,8 @@ interface Props { url: string; isLocal?: boolean; }[]; + /** Only available while in a call. Used by the developer tab. */ + vm?: CallViewModel; } export const defaultSettingsTab: SettingsTab = "audio"; @@ -68,6 +71,7 @@ export const SettingsModal: FC = ({ client, roomId, livekitRooms, + vm, }) => { const { t } = useTranslation(); @@ -220,6 +224,7 @@ export const SettingsModal: FC = ({ client={client} livekitRooms={livekitRooms} roomId={roomId} + vm={vm} /> ), }; From 0aa9d94826fb0af052a150daf7205040f7c2cdd9 Mon Sep 17 00:00:00 2001 From: "Timo K." Date: Sat, 15 Aug 2026 22:32:23 +0200 Subject: [PATCH 02/18] add key rotation debug information --- src/settings/DeveloperSettingsTab.tsx | 18 ++++++++++++++++++ src/state/CallViewModel/CallViewModel.ts | 13 +++++++++++++ src/state/SessionBehaviors.ts | 19 +++++++++++++++++++ src/utils/test.ts | 2 ++ 4 files changed, 52 insertions(+) diff --git a/src/settings/DeveloperSettingsTab.tsx b/src/settings/DeveloperSettingsTab.tsx index 19af03373..76e0c03ec 100644 --- a/src/settings/DeveloperSettingsTab.tsx +++ b/src/settings/DeveloperSettingsTab.tsx @@ -69,6 +69,23 @@ import { Slider } from "../Slider"; import { useUrlParams } from "../UrlParams"; import { type CallViewModel } from "../state/CallViewModel/CallViewModel.ts"; import { getSFUConfigWithOpenID } from "../livekit/openIDSFU"; +import { useBehavior } from "../useBehavior"; + +/** + * Shows whether the call is large enough that MatrixRTC has stopped rotating the media key. + */ +const KeyRotationStatus: FC<{ vm: CallViewModel }> = ({ vm }) => { + const suppressed = useBehavior(vm.keyRotationSuppressed$); + const participantCount = useBehavior(vm.participantCount$); + return ( +

+ Media key rotation:{" "} + {suppressed + ? `suppressed, participant limit reached (${participantCount} participants)` + : `active (${participantCount} participants)`} +

+ ); +}; interface Props { client: MatrixClient; @@ -368,6 +385,7 @@ export const DeveloperSettingsTab: FC = ({ id: client.getDeviceId() || "unknown", })}

+ {vm && } ; + /** + * Whether the call has grown large enough that MatrixRTC has stopped rotating the media + * encryption key. While this is true the key in use is still shared with new joiners, but no new + * key is generated when someone joins or leaves. + */ + keyRotationSuppressed$: Behavior; allConnections$: Behavior; /** Participants sorted by livekit room so they can be used in the audio rendering */ livekitRoomItems$: Behavior; @@ -856,6 +863,11 @@ export function createCallViewModel$( matrixLivekitMembers$.pipe(map((ms) => ms.length)), ); + const keyRotationSuppressed$ = createKeyRotationSuppressed$( + scope, + matrixRTCSession, + ); + const leaveSoundEffect$ = userMedia$.pipe( pairwise(), filter( @@ -1782,6 +1794,7 @@ export function createCallViewModel$( ), allConnections$, participantCount$: participantCount$, + keyRotationSuppressed$: keyRotationSuppressed$, handsRaised$: handsRaised$, reactions$: reactions$, joinSoundEffect$: joinSoundEffect$, diff --git a/src/state/SessionBehaviors.ts b/src/state/SessionBehaviors.ts index 652e43b5f..784e33666 100644 --- a/src/state/SessionBehaviors.ts +++ b/src/state/SessionBehaviors.ts @@ -88,3 +88,22 @@ export const createMemberships$ = ( new Epoch(matrixRTCSession.memberships), ); }; + +/** + * Whether the session has grown large enough that MatrixRTC has stopped rotating the media + * encryption key. While this is true the key in use is still shared with new joiners, but no new + * key is generated when someone joins or leaves. + */ +export const createKeyRotationSuppressed$ = ( + scope: ObservableScope, + matrixRTCSession: MatrixRTCSession, +): Behavior => { + return scope.behavior( + fromEvent( + matrixRTCSession, + MatrixRTCSessionEvent.KeyRotationSuppressedChanged, + (suppressed: boolean) => suppressed, + ), + matrixRTCSession.isKeyRotationSuppressed, + ); +}; diff --git a/src/utils/test.ts b/src/utils/test.ts index 206db88f5..38a1bb4a5 100644 --- a/src/utils/test.ts +++ b/src/utils/test.ts @@ -490,6 +490,8 @@ export class MockRTCSession extends TypedEventEmitter< return this.joined; } + public isKeyRotationSuppressed = false; + public withMemberships( rtcMembers$: Behavior[]>, ): MockRTCSession { From 9857ed53e3edf42d9f2ef70d44fc2ac2f7162de7 Mon Sep 17 00:00:00 2001 From: "Timo K." Date: Mon, 17 Aug 2026 11:16:11 +0200 Subject: [PATCH 03/18] Add key roatation limit to config.json --- config/config.sample.json | 3 +- package.json | 2 +- pnpm-lock.yaml | 44 +++++++++++++------ src/config/ConfigOptions.ts | 13 ++++++ .../CallViewModel/localMember/LocalMember.ts | 2 + 5 files changed, 48 insertions(+), 16 deletions(-) diff --git a/config/config.sample.json b/config/config.sample.json index 78f9536da..3ff689278 100644 --- a/config/config.sample.json +++ b/config/config.sample.json @@ -18,6 +18,7 @@ "membership_event_expiry_ms": 180000000, "delayed_leave_event_delay_ms": 18000, "delayed_leave_event_restart_ms": 4000, - "network_error_retry_ms": 100 + "network_error_retry_ms": 100, + "key_rotation_participant_limit": 30 } } diff --git a/package.json b/package.json index 2f4faa601..3bec6cd52 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "livekit-client": "^2.18.1", "lodash-es": "^4.17.21", "loglevel": "^1.9.1", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#toger5/add-key-rotation-participant-size", "matrix-widget-api": "^1.18.0", "node-stdlib-browser": "^1.3.1", "normalize.css": "^8.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0519571fa..21a907ec4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -191,8 +191,8 @@ importers: specifier: ^1.9.1 version: 1.9.2 matrix-js-sdk: - specifier: github:matrix-org/matrix-js-sdk#develop - version: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/178dc6528ebc6a15e2f6fab38410127f16b500d0 + specifier: github:matrix-org/matrix-js-sdk#toger5/add-key-rotation-participant-size + version: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/f4a278c571bcb4e8e553ef031488ecdabe4e0ede matrix-widget-api: specifier: ^1.18.0 version: 1.18.0 @@ -1191,8 +1191,8 @@ packages: '@types/dom-mediacapture-transform': ^0.1.9 livekit-client: ^1.12.0 || ^2.1.0 - '@matrix-org/matrix-sdk-crypto-wasm@18.4.0': - resolution: {integrity: sha512-osxkU1DQ+05+anGHapjWyvZqdHUb94Id37gy54mCKn1Cq/D7iGT5oEUEhjp4oTnCLo4TOtI6ULJ/LHsapaIptQ==} + '@matrix-org/matrix-sdk-crypto-wasm@18.5.0': + resolution: {integrity: sha512-E826Hy1rG26LanPjtSsOiVRcVoHfSgPgj2r2Xsb5RPScpaKi9XJADQ0u3dNjRCitZPX4oyLNl1FDd5AfDlmIwQ==} engines: {node: '>= 18'} '@mdx-js/react@3.1.1': @@ -3664,8 +3664,8 @@ packages: constants-browserify@1.0.0: resolution: {integrity: sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==} - content-type@2.0.0: - resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + content-type@2.1.0: + resolution: {integrity: sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==} engines: {node: '>=18'} convert-source-map@2.0.0: @@ -4516,6 +4516,10 @@ packages: jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + jwt-decode@4.0.0: + resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} + engines: {node: '>=18'} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -4679,9 +4683,9 @@ packages: matrix-events-sdk@0.0.1: resolution: {integrity: sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==} - matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/178dc6528ebc6a15e2f6fab38410127f16b500d0: - resolution: {gitHosted: true, integrity: sha512-UmlkoXs9VMZvNaDeQqBb2k+gLifnIqEZc5cxrLeEwdYEDakQLbRqrFtmvIp4ipUCjdIPkCgtzb7QLlpPBLAwqA==, tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/178dc6528ebc6a15e2f6fab38410127f16b500d0} - version: 42.1.0 + matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/f4a278c571bcb4e8e553ef031488ecdabe4e0ede: + resolution: {gitHosted: true, integrity: sha512-6iaM5Uyr063LCbDcjhDXI1qf5X8q270dMl0iLNb6S/mcOucqdKURw0ySWM6PWHAnbMw2rdkiTx3Mp37+J9F/pg==, tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/f4a278c571bcb4e8e553ef031488ecdabe4e0ede} + version: 41.8.0 engines: {node: '>=22.0.0'} matrix-widget-api@1.18.0: @@ -4823,6 +4827,10 @@ packages: resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} engines: {node: '>=12.20.0'} + oidc-client-ts@3.5.0: + resolution: {integrity: sha512-l2q8l9CTCTOlbX+AnK4p3M+4CEpKpyQhle6blQkdFhm0IsBqsxm15bYaSa11G7pWdsYr6epdsRZxJpCyCRbT8A==} + engines: {node: '>=18'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -7096,7 +7104,7 @@ snapshots: '@types/dom-mediacapture-transform': 0.1.11 livekit-client: 2.21.0(@types/dom-mediacapture-record@1.0.22) - '@matrix-org/matrix-sdk-crypto-wasm@18.4.0': {} + '@matrix-org/matrix-sdk-crypto-wasm@18.5.0': {} '@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8)': dependencies: @@ -9189,7 +9197,7 @@ snapshots: constants-browserify@1.0.0: {} - content-type@2.0.0: {} + content-type@2.1.0: {} convert-source-map@2.0.0: {} @@ -10116,6 +10124,8 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jwt-decode@4.0.0: {} + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -10269,16 +10279,18 @@ snapshots: matrix-events-sdk@0.0.1: {} - matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/178dc6528ebc6a15e2f6fab38410127f16b500d0: + matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/f4a278c571bcb4e8e553ef031488ecdabe4e0ede: dependencies: '@babel/runtime': 8.0.0 - '@matrix-org/matrix-sdk-crypto-wasm': 18.4.0 + '@matrix-org/matrix-sdk-crypto-wasm': 18.5.0 another-json: 0.2.0 bs58: 6.0.0 - content-type: 2.0.0 + content-type: 2.1.0 + jwt-decode: 4.0.0 loglevel: 1.9.2 matrix-events-sdk: 0.0.1 matrix-widget-api: 1.18.0 + oidc-client-ts: 3.5.0 p-retry: 8.0.0 sdp-transform: 3.0.0 unhomoglyph: 1.0.6 @@ -10428,6 +10440,10 @@ snapshots: obug@2.1.4: {} + oidc-client-ts@3.5.0: + dependencies: + jwt-decode: 4.0.0 + once@1.4.0: dependencies: wrappy: 1.0.2 diff --git a/src/config/ConfigOptions.ts b/src/config/ConfigOptions.ts index 75704cfc8..f01477d80 100644 --- a/src/config/ConfigOptions.ts +++ b/src/config/ConfigOptions.ts @@ -229,6 +229,18 @@ export interface ConfigOptions { * This is what goes into the m.rtc.member event expiry field and is typically set to a number of hours. */ membership_event_expiry_ms?: number; + + /** + * The number of participants in the session at which the media encryption key will no longer + * be rotated. + * + * Rotating a key requires sending it to every participant device, so in large sessions the + * cost of rotating on every join/leave becomes prohibitive. At this limit the current key is + * kept and distributed to new joiners; no new keys are generated for joiners/leavers. + * + * Defaults to the js-sdk default (30). + */ + key_rotation_participant_limit?: number; }; } @@ -262,6 +274,7 @@ export interface ResolvedConfigOptions extends ConfigOptions { delayed_leave_event_restart_ms?: number; network_error_retry_ms: number; membership_event_expiry_ms?: number; + key_rotation_participant_limit?: number; }; } diff --git a/src/state/CallViewModel/localMember/LocalMember.ts b/src/state/CallViewModel/localMember/LocalMember.ts index fcc8ed0a6..078c5a036 100644 --- a/src/state/CallViewModel/localMember/LocalMember.ts +++ b/src/state/CallViewModel/localMember/LocalMember.ts @@ -908,6 +908,8 @@ export function enterRTCSession( makeKeyDelay: matrixRtcSessionConfig?.wait_for_key_rotation_ms, membershipEventExpiryMs: matrixRtcSessionConfig?.membership_event_expiry_ms, + keyRotationParticipantLimit: + matrixRtcSessionConfig?.key_rotation_participant_limit, unstableSendStickyEvents: matrixRTCMode === MatrixRTCMode.Matrix_2_0, maximumNetworkErrorRetryCount: maximumNetworkErrorRetryCount, }, From 9999a755dd2c9a76be3c391682e96ce63f3b698c Mon Sep 17 00:00:00 2001 From: "Timo K." Date: Mon, 17 Aug 2026 12:33:31 +0200 Subject: [PATCH 04/18] Update pnpm-lock.yaml --- pnpm-lock.yaml | 26 +++++--------------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 21a907ec4..617b06f34 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -192,7 +192,7 @@ importers: version: 1.9.2 matrix-js-sdk: specifier: github:matrix-org/matrix-js-sdk#toger5/add-key-rotation-participant-size - version: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/f4a278c571bcb4e8e553ef031488ecdabe4e0ede + version: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/14aeb18fe5c6ca96a6325f9f02ab6a7639060f6a matrix-widget-api: specifier: ^1.18.0 version: 1.18.0 @@ -4516,10 +4516,6 @@ packages: jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} - jwt-decode@4.0.0: - resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} - engines: {node: '>=18'} - keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -4683,9 +4679,9 @@ packages: matrix-events-sdk@0.0.1: resolution: {integrity: sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==} - matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/f4a278c571bcb4e8e553ef031488ecdabe4e0ede: - resolution: {gitHosted: true, integrity: sha512-6iaM5Uyr063LCbDcjhDXI1qf5X8q270dMl0iLNb6S/mcOucqdKURw0ySWM6PWHAnbMw2rdkiTx3Mp37+J9F/pg==, tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/f4a278c571bcb4e8e553ef031488ecdabe4e0ede} - version: 41.8.0 + matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/14aeb18fe5c6ca96a6325f9f02ab6a7639060f6a: + resolution: {gitHosted: true, integrity: sha512-nCscA1xab7FkNS7Cak1VmxeMBLJrtfcdH/+xKne44X6mKgGZrR0kKsBc0JKAjrSFJq6jwf/bHDxKi+UsNMmKUQ==, tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/14aeb18fe5c6ca96a6325f9f02ab6a7639060f6a} + version: 42.1.0 engines: {node: '>=22.0.0'} matrix-widget-api@1.18.0: @@ -4827,10 +4823,6 @@ packages: resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} engines: {node: '>=12.20.0'} - oidc-client-ts@3.5.0: - resolution: {integrity: sha512-l2q8l9CTCTOlbX+AnK4p3M+4CEpKpyQhle6blQkdFhm0IsBqsxm15bYaSa11G7pWdsYr6epdsRZxJpCyCRbT8A==} - engines: {node: '>=18'} - once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -10124,8 +10116,6 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 - jwt-decode@4.0.0: {} - keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -10279,18 +10269,16 @@ snapshots: matrix-events-sdk@0.0.1: {} - matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/f4a278c571bcb4e8e553ef031488ecdabe4e0ede: + matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/14aeb18fe5c6ca96a6325f9f02ab6a7639060f6a: dependencies: '@babel/runtime': 8.0.0 '@matrix-org/matrix-sdk-crypto-wasm': 18.5.0 another-json: 0.2.0 bs58: 6.0.0 content-type: 2.1.0 - jwt-decode: 4.0.0 loglevel: 1.9.2 matrix-events-sdk: 0.0.1 matrix-widget-api: 1.18.0 - oidc-client-ts: 3.5.0 p-retry: 8.0.0 sdp-transform: 3.0.0 unhomoglyph: 1.0.6 @@ -10440,10 +10428,6 @@ snapshots: obug@2.1.4: {} - oidc-client-ts@3.5.0: - dependencies: - jwt-decode: 4.0.0 - once@1.4.0: dependencies: wrappy: 1.0.2 From f2ffb06f6c37efad0fb70046896fd323c425415f Mon Sep 17 00:00:00 2001 From: "Timo K." Date: Tue, 25 Aug 2026 22:03:59 +0200 Subject: [PATCH 05/18] use develop branch of matrix-js-sdk --- config/config.sample.json | 2 +- package.json | 2 +- pnpm-lock.yaml | 34 +++++++++++++++++----------------- src/config/ConfigOptions.ts | 2 +- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/config/config.sample.json b/config/config.sample.json index 3ff689278..dd1699530 100644 --- a/config/config.sample.json +++ b/config/config.sample.json @@ -19,6 +19,6 @@ "delayed_leave_event_delay_ms": 18000, "delayed_leave_event_restart_ms": 4000, "network_error_retry_ms": 100, - "key_rotation_participant_limit": 30 + "key_rotation_participant_limit": null } } diff --git a/package.json b/package.json index 3bec6cd52..2f4faa601 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "livekit-client": "^2.18.1", "lodash-es": "^4.17.21", "loglevel": "^1.9.1", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#toger5/add-key-rotation-participant-size", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "matrix-widget-api": "^1.18.0", "node-stdlib-browser": "^1.3.1", "normalize.css": "^8.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 617b06f34..04d1ca4a6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,7 +66,7 @@ importers: version: 10.1.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@rolldown/plugin-babel': specifier: ^0.2.3 - version: 0.2.3(@babel/core@7.29.7(supports-color@7.2.0))(@babel/runtime@7.29.7)(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)) + version: 0.2.3(@babel/core@7.29.7(supports-color@7.2.0))(@babel/runtime@8.0.0)(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)) '@sentry/react': specifier: ^8.0.0 version: 8.55.2(react@19.2.8) @@ -138,7 +138,7 @@ importers: version: 10.0.1(@fontsource/inconsolata@5.3.0)(@fontsource/inter@5.3.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@vector-im/compound-design-tokens@10.2.3(@types/react@19.2.17)(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@vitejs/plugin-react': specifier: ^6.0.2 - version: 6.0.5(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7(supports-color@7.2.0))(@babel/runtime@7.29.7)(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)))(babel-plugin-react-compiler@1.0.0)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)) + version: 6.0.5(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7(supports-color@7.2.0))(@babel/runtime@8.0.0)(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)))(babel-plugin-react-compiler@1.0.0)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)) '@vitest/browser-playwright': specifier: ^4.1.5 version: 4.1.10(playwright@1.62.0)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))(vitest@4.1.10) @@ -191,8 +191,8 @@ importers: specifier: ^1.9.1 version: 1.9.2 matrix-js-sdk: - specifier: github:matrix-org/matrix-js-sdk#toger5/add-key-rotation-participant-size - version: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/14aeb18fe5c6ca96a6325f9f02ab6a7639060f6a + specifier: github:matrix-org/matrix-js-sdk#develop + version: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/24929be0e741be6a5d0a7226f1c682e245263b8a matrix-widget-api: specifier: ^1.18.0 version: 1.18.0 @@ -3664,9 +3664,9 @@ packages: constants-browserify@1.0.0: resolution: {integrity: sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==} - content-type@2.1.0: - resolution: {integrity: sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==} - engines: {node: '>=18'} + content-type@3.0.0: + resolution: {integrity: sha512-AIi5H6p0xk5uknXcN3/rmhP8jgp69OfSe/JuKiQAFprJ7UGw7mwj7m4XcmDzlrnJDG+cGpphAINGdU3g3g7kDw==} + engines: {node: '>=22'} convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -4679,9 +4679,9 @@ packages: matrix-events-sdk@0.0.1: resolution: {integrity: sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==} - matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/14aeb18fe5c6ca96a6325f9f02ab6a7639060f6a: - resolution: {gitHosted: true, integrity: sha512-nCscA1xab7FkNS7Cak1VmxeMBLJrtfcdH/+xKne44X6mKgGZrR0kKsBc0JKAjrSFJq6jwf/bHDxKi+UsNMmKUQ==, tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/14aeb18fe5c6ca96a6325f9f02ab6a7639060f6a} - version: 42.1.0 + matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/24929be0e741be6a5d0a7226f1c682e245263b8a: + resolution: {gitHosted: true, integrity: sha512-0EGwbXzvG88sBOxq4ZlCRwT4VpQj2xkizfyy0A3bTSK0ysb6/gXJ/o1WPFoqCjRGMDgPq4gAsk92xGtHA7EhPg==, tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/24929be0e741be6a5d0a7226f1c682e245263b8a} + version: 42.2.0 engines: {node: '>=22.0.0'} matrix-widget-api@1.18.0: @@ -8047,13 +8047,13 @@ snapshots: '@rolldown/binding-win32-x64-msvc@1.1.5': optional: true - '@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7(supports-color@7.2.0))(@babel/runtime@7.29.7)(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))': + '@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7(supports-color@7.2.0))(@babel/runtime@8.0.0)(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.7(supports-color@7.2.0) picomatch: 4.0.4 rolldown: 1.1.5 optionalDependencies: - '@babel/runtime': 7.29.7 + '@babel/runtime': 8.0.0 vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0) '@rolldown/pluginutils@1.0.1': {} @@ -8738,12 +8738,12 @@ snapshots: - '@types/react-dom' - react-dom - '@vitejs/plugin-react@6.0.5(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7(supports-color@7.2.0))(@babel/runtime@7.29.7)(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)))(babel-plugin-react-compiler@1.0.0)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))': + '@vitejs/plugin-react@6.0.5(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7(supports-color@7.2.0))(@babel/runtime@8.0.0)(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)))(babel-plugin-react-compiler@1.0.0)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))': dependencies: '@rolldown/pluginutils': 1.0.1 vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0) optionalDependencies: - '@rolldown/plugin-babel': 0.2.3(@babel/core@7.29.7(supports-color@7.2.0))(@babel/runtime@7.29.7)(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)) + '@rolldown/plugin-babel': 0.2.3(@babel/core@7.29.7(supports-color@7.2.0))(@babel/runtime@8.0.0)(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)) babel-plugin-react-compiler: 1.0.0 '@vitest/browser-playwright@4.1.10(playwright@1.62.0)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))(vitest@4.1.10)': @@ -9189,7 +9189,7 @@ snapshots: constants-browserify@1.0.0: {} - content-type@2.1.0: {} + content-type@3.0.0: {} convert-source-map@2.0.0: {} @@ -10269,13 +10269,13 @@ snapshots: matrix-events-sdk@0.0.1: {} - matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/14aeb18fe5c6ca96a6325f9f02ab6a7639060f6a: + matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/24929be0e741be6a5d0a7226f1c682e245263b8a: dependencies: '@babel/runtime': 8.0.0 '@matrix-org/matrix-sdk-crypto-wasm': 18.5.0 another-json: 0.2.0 bs58: 6.0.0 - content-type: 2.1.0 + content-type: 3.0.0 loglevel: 1.9.2 matrix-events-sdk: 0.0.1 matrix-widget-api: 1.18.0 diff --git a/src/config/ConfigOptions.ts b/src/config/ConfigOptions.ts index f01477d80..c7909659d 100644 --- a/src/config/ConfigOptions.ts +++ b/src/config/ConfigOptions.ts @@ -238,7 +238,7 @@ export interface ConfigOptions { * cost of rotating on every join/leave becomes prohibitive. At this limit the current key is * kept and distributed to new joiners; no new keys are generated for joiners/leavers. * - * Defaults to the js-sdk default (30). + * Defaults to the js-sdk default (undefined). Which means that rotation will always happen. */ key_rotation_participant_limit?: number; }; From 5399f1401236ccc2f357b4dc46d5cc8c09e07700 Mon Sep 17 00:00:00 2001 From: "Timo K." Date: Tue, 25 Aug 2026 22:43:15 +0200 Subject: [PATCH 06/18] add tests --- .../localMember/LocalMember.test.ts | 61 +++++++++++++++++++ src/state/SessionBehaviors.test.ts | 60 ++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 src/state/SessionBehaviors.test.ts diff --git a/src/state/CallViewModel/localMember/LocalMember.test.ts b/src/state/CallViewModel/localMember/LocalMember.test.ts index 16ffe1493..fb93a62b8 100644 --- a/src/state/CallViewModel/localMember/LocalMember.test.ts +++ b/src/state/CallViewModel/localMember/LocalMember.test.ts @@ -136,6 +136,67 @@ describe("LocalMembership", () => { }), ); }); + + it("passes keyRotationParticipantLimit from config to joinRTCSession", () => { + const focusFromOlderMembership = { + type: "livekit", + livekit_service_url: "http://my-oldest-member-service-url.com", + livekit_alias: "my-oldest-member-service-alias", + }; + + mockConfig({ + livekit: { livekit_service_url: "http://my-default-service-url.com" }, + matrix_rtc_session: { + delayed_leave_event_delay_ms: 0, + network_error_retry_ms: 0, + key_rotation_participant_limit: 50, + }, + }); + + const mockedSession = vi.mocked({ + room: { + roomId: "roomId", + client: { + getDomain: vi.fn().mockReturnValue("example.org"), + getOpenIdToken: vi.fn().mockResolvedValue({ + access_token: "ACCCESS_TOKEN", + token_type: "Bearer", + matrix_server_name: "localhost", + expires_in: 10000, + }), + }, + }, + memberships: [], + getFocusInUse: vi.fn().mockReturnValue(focusFromOlderMembership), + getOldestMembership: vi.fn().mockReturnValue({ + getPreferredFoci: vi.fn().mockReturnValue([focusFromOlderMembership]), + }), + joinRTCSession: vi.fn(), + }) as unknown as MatrixRTCSession; + + enterRTCSession( + mockedSession, + ownMemberMock, + { + livekit_alias: "roomId", + livekit_service_url: "http://my-livekit-service-url.com", + type: "livekit", + }, + { + encryptMedia: true, + matrixRTCMode: MATRIX_RTC_MODE, + }, + ); + + expect(mockedSession.joinRTCSession).toHaveBeenLastCalledWith( + expect.any(Object), + expect.any(Array), + undefined, + expect.objectContaining({ + keyRotationParticipantLimit: 50, + }), + ); + }); }); const defaultCreateLocalMemberValues = { diff --git a/src/state/SessionBehaviors.test.ts b/src/state/SessionBehaviors.test.ts new file mode 100644 index 000000000..9f05159c5 --- /dev/null +++ b/src/state/SessionBehaviors.test.ts @@ -0,0 +1,60 @@ +/* +Copyright 2025 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { describe, expect, it, vi } from "vitest"; +import { MatrixRTCSessionEvent } from "matrix-js-sdk/lib/matrixrtc"; +import { EventEmitter } from "events"; + +import { createKeyRotationSuppressed$ } from "./SessionBehaviors"; +import { ObservableScope } from "./ObservableScope"; + +describe("SessionBehaviors", () => { + describe("createKeyRotationSuppressed$", () => { + it("emits initial value from isKeyRotationSuppressed", () => { + const scope = new ObservableScope(); + + const mockSession = { + on: vi.fn(), + off: vi.fn(), + isKeyRotationSuppressed: false, + }; + + const keyRotationSuppressed$ = createKeyRotationSuppressed$( + scope, + mockSession as any, + ); + + expect(keyRotationSuppressed$.value).toBe(false); + scope.end(); + }); + + it("updates when KeyRotationSuppressedChanged event is emitted", () => { + const scope = new ObservableScope(); + const emitter = new EventEmitter(); + + const mockSession = Object.assign(emitter, { + isKeyRotationSuppressed: false, + }); + + const keyRotationSuppressed$ = createKeyRotationSuppressed$( + scope, + mockSession as any, + ); + + expect(keyRotationSuppressed$.value).toBe(false); + + emitter.emit(MatrixRTCSessionEvent.KeyRotationSuppressedChanged, true); + + expect(keyRotationSuppressed$.value).toBe(true); + + emitter.emit(MatrixRTCSessionEvent.KeyRotationSuppressedChanged, false); + + expect(keyRotationSuppressed$.value).toBe(false); + scope.end(); + }); + }); +}); From 2b5979162a349fa24aa8f020ac80a297980b1c0c Mon Sep 17 00:00:00 2001 From: "Timo K." Date: Tue, 25 Aug 2026 23:30:22 +0200 Subject: [PATCH 07/18] add more tests --- src/settings/DeveloperSettingsTab.test.tsx | 70 ++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/settings/DeveloperSettingsTab.test.tsx b/src/settings/DeveloperSettingsTab.test.tsx index d4c7b8c8f..a824022f0 100644 --- a/src/settings/DeveloperSettingsTab.test.tsx +++ b/src/settings/DeveloperSettingsTab.test.tsx @@ -9,6 +9,7 @@ import { afterEach, describe, expect, it, type Mock, vi } from "vitest"; import { render, waitFor, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { TooltipProvider } from "@vector-im/compound-web"; +import { BehaviorSubject } from "rxjs"; import type { MatrixClient } from "matrix-js-sdk"; import type { Room as LivekitRoom } from "livekit-client"; @@ -412,4 +413,73 @@ describe("DeveloperSettingsTab", () => { }, ); }); + + describe("KeyRotationStatus", () => { + it("displays active status when key rotation is not suppressed", async () => { + const client = createMockMatrixClient(); + const mockVm = { + keyRotationSuppressed$: new BehaviorSubject(false), + participantCount$: new BehaviorSubject(5), + } as unknown as any; + + render( + + + , + ); + + await waitFor(() => + expect(client.doesServerSupportUnstableFeature).toHaveBeenCalled(), + ); + + expect(screen.getByText(/Media key rotation: active \(5 participants\)/)).toBeInTheDocument(); + }); + + it("displays suppressed status when key rotation is suppressed", async () => { + const client = createMockMatrixClient(); + const mockVm = { + keyRotationSuppressed$: new BehaviorSubject(true), + participantCount$: new BehaviorSubject(50), + } as unknown as any; + + render( + + + , + ); + + await waitFor(() => + expect(client.doesServerSupportUnstableFeature).toHaveBeenCalled(), + ); + + expect(screen.getByText(/Media key rotation: suppressed, participant limit reached \(50 participants\)/)).toBeInTheDocument(); + }); + + it("does not render KeyRotationStatus when vm is not provided", async () => { + const client = createMockMatrixClient(); + + render( + + + , + ); + + await waitFor(() => + expect(client.doesServerSupportUnstableFeature).toHaveBeenCalled(), + ); + + expect(screen.queryByText(/Media key rotation:/)).not.toBeInTheDocument(); + }); + }); }); From 06b9105cee23c453548a8fe7f3c2e4c5e8a2e9c6 Mon Sep 17 00:00:00 2001 From: "Timo K." Date: Tue, 25 Aug 2026 23:32:22 +0200 Subject: [PATCH 08/18] Update DeveloperSettingsTab.test.tsx --- src/settings/DeveloperSettingsTab.test.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/settings/DeveloperSettingsTab.test.tsx b/src/settings/DeveloperSettingsTab.test.tsx index a824022f0..3abb4990a 100644 --- a/src/settings/DeveloperSettingsTab.test.tsx +++ b/src/settings/DeveloperSettingsTab.test.tsx @@ -436,7 +436,9 @@ describe("DeveloperSettingsTab", () => { expect(client.doesServerSupportUnstableFeature).toHaveBeenCalled(), ); - expect(screen.getByText(/Media key rotation: active \(5 participants\)/)).toBeInTheDocument(); + expect( + screen.getByText(/Media key rotation: active \(5 participants\)/), + ).toBeInTheDocument(); }); it("displays suppressed status when key rotation is suppressed", async () => { @@ -460,7 +462,11 @@ describe("DeveloperSettingsTab", () => { expect(client.doesServerSupportUnstableFeature).toHaveBeenCalled(), ); - expect(screen.getByText(/Media key rotation: suppressed, participant limit reached \(50 participants\)/)).toBeInTheDocument(); + expect( + screen.getByText( + /Media key rotation: suppressed, participant limit reached \(50 participants\)/, + ), + ).toBeInTheDocument(); }); it("does not render KeyRotationStatus when vm is not provided", async () => { From 1146d3820a4a4fa96740255a2c8535ec91eb904b Mon Sep 17 00:00:00 2001 From: "Timo K." Date: Fri, 28 Aug 2026 15:20:49 +0200 Subject: [PATCH 09/18] review. Dont pass full vm to developer settings --- src/room/InCallView.test.tsx | 14 +++--- src/room/InCallView.tsx | 21 ++++++-- src/settings/DeveloperSettingsTab.test.tsx | 39 ++++++++++----- src/settings/DeveloperSettingsTab.tsx | 47 +++++++++++------- .../DeveloperSettingsTabViewModel.test.ts | 48 +++++++++++++++++++ src/settings/DeveloperSettingsTabViewModel.ts | 43 +++++++++++++++++ src/settings/SettingsModal.tsx | 14 ++++-- src/utils/test-viewmodel.ts | 4 ++ 8 files changed, 187 insertions(+), 43 deletions(-) create mode 100644 src/settings/DeveloperSettingsTabViewModel.test.ts create mode 100644 src/settings/DeveloperSettingsTabViewModel.ts diff --git a/src/room/InCallView.test.tsx b/src/room/InCallView.test.tsx index 94b152e43..eafb54c55 100644 --- a/src/room/InCallView.test.tsx +++ b/src/room/InCallView.test.tsx @@ -122,12 +122,13 @@ function createInCallView(args: CreateInCallViewArgs = {}): RenderResult & { remoteParticipants$: of([remoteParticipant]), }, ); - const { vm, footerVm, rtcSession } = getBasicCallViewModelEnvironment( - [local, alice], - undefined, - mediaDevices, - args.callViewModelOptions, - ); + const { vm, footerVm, developerSettingsVm, rtcSession } = + getBasicCallViewModelEnvironment( + [local, alice], + undefined, + mediaDevices, + args.callViewModelOptions, + ); rtcSession.joined = true; const room = rtcSession.room; @@ -140,6 +141,7 @@ function createInCallView(args: CreateInCallViewArgs = {}): RenderResult & { muteStates={muteState} vm={vm} footerVm={footerVm} + developerSettingsVm={developerSettingsVm} matrixInfo={{ userId: "", displayName: "", diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 5aa9667ce..f8a8f3896 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -83,6 +83,8 @@ import { ObservableScope } from "../state/ObservableScope.ts"; import { CallFooter, type FooterSnapshot } from "../components/CallFooter.tsx"; import { SettingsIconButton } from "../button/Button.tsx"; import { createCallFooterViewModel } from "../components/CallFooterViewModel.tsx"; +import { createDeveloperSettingsTabViewModel } from "../settings/DeveloperSettingsTabViewModel.ts"; +import { type DeveloperSettingsSnapshot } from "../settings/DeveloperSettingsTab.tsx"; import { type ViewModel } from "../state/ViewModel.ts"; import { RingingStatus } from "../tile/RingingStatus.tsx"; import { RingingAudioRenderer } from "./RingingAudioRenderer.tsx"; @@ -96,7 +98,7 @@ declare module "react" { export interface ActiveCallProps extends Omit< InCallViewProps, - "vm" | "livekitRoom" | "connState" | "footerVm" + "vm" | "livekitRoom" | "connState" | "footerVm" | "developerSettingsVm" > { e2eeSystem: EncryptionSystem; // TODO refactor those reasons into an enum @@ -110,6 +112,9 @@ export const ActiveCall: FC = (props) => { const [footerVm, setFooterVm] = useState | null>( null, ); + const [developerSettingsVm, setDeveloperSettingsVm] = + useState | null>(null); + const urlParams = useUrlParams(); const mediaDevices = useMediaDevices(); const trackProcessorState$ = useTrackProcessorObservable$(); @@ -168,7 +173,9 @@ export const ActiveCall: FC = (props) => { mediaDevices, `${props.client.getUserId()}:${props.client.getDeviceId()}`, ); + const developerSettingsVm = createDeveloperSettingsTabViewModel(scope, vm); setFooterVm(footerVm); + setDeveloperSettingsVm(developerSettingsVm); return (): void => { scope.end(); @@ -188,10 +195,16 @@ export const ActiveCall: FC = (props) => { if (vm === null) return null; if (footerVm === null) return null; + if (developerSettingsVm === null) return null; return ( - + ); }; @@ -200,6 +213,7 @@ export interface InCallViewProps { client: MatrixClient; vm: CallViewModel; footerVm: ViewModel; + developerSettingsVm: ViewModel; matrixInfo: MatrixInfo; rtcSession: MatrixRTCSession; matrixRoom: MatrixRoom; @@ -211,6 +225,7 @@ export const InCallView: FC = ({ client, vm, footerVm, + developerSettingsVm, matrixInfo, matrixRoom, muteStates, @@ -632,7 +647,7 @@ export const InCallView: FC = ({ onDismiss={(): void => setSettingsOpen(false)} tab={settingsTab} onTabChange={setSettingsTab} - vm={vm} + developerSettingsVm={developerSettingsVm} livekitRooms={allConnections .getConnections() .map((connectionItem) => ({ diff --git a/src/settings/DeveloperSettingsTab.test.tsx b/src/settings/DeveloperSettingsTab.test.tsx index 3abb4990a..c6777ad5e 100644 --- a/src/settings/DeveloperSettingsTab.test.tsx +++ b/src/settings/DeveloperSettingsTab.test.tsx @@ -9,11 +9,15 @@ import { afterEach, describe, expect, it, type Mock, vi } from "vitest"; import { render, waitFor, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { TooltipProvider } from "@vector-im/compound-web"; -import { BehaviorSubject } from "rxjs"; import type { MatrixClient } from "matrix-js-sdk"; import type { Room as LivekitRoom } from "livekit-client"; -import { DeveloperSettingsTab } from "./DeveloperSettingsTab"; +import { + DeveloperSettingsTab, + type DeveloperSettingsSnapshot, +} from "./DeveloperSettingsTab"; +import { outOfCallDeveloperSettingsTabViewModel } from "./DeveloperSettingsTabViewModel"; +import { createStaticViewModel } from "../state/ViewModel"; import { getSFUConfigWithOpenID } from "../livekit/openIDSFU"; import { customLivekitUrl as customLivekitUrlSetting, @@ -108,6 +112,7 @@ describe("DeveloperSettingsTab", () => { roomId={"#room:example.org"} livekitRooms={livekitRooms} env={{ MY_MOCK_ENV: 10, ENV: "test" } as unknown as ImportMetaEnv} + vm={outOfCallDeveloperSettingsTabViewModel} />, ); @@ -137,6 +142,7 @@ describe("DeveloperSettingsTab", () => { , ); @@ -160,6 +166,7 @@ describe("DeveloperSettingsTab", () => { client={client} roomId="#testRoom" env={{} as unknown as ImportMetaEnv} + vm={outOfCallDeveloperSettingsTabViewModel} /> , ); @@ -182,6 +189,7 @@ describe("DeveloperSettingsTab", () => { client={client} roomId="#testRoom" env={{} as unknown as ImportMetaEnv} + vm={outOfCallDeveloperSettingsTabViewModel} /> , ); @@ -207,6 +215,7 @@ describe("DeveloperSettingsTab", () => { client={client} roomId="#testRoom" env={{} as unknown as ImportMetaEnv} + vm={outOfCallDeveloperSettingsTabViewModel} /> , ); @@ -237,6 +246,7 @@ describe("DeveloperSettingsTab", () => { client={client} roomId="#testRoom" env={{} as unknown as ImportMetaEnv} + vm={outOfCallDeveloperSettingsTabViewModel} /> , ); @@ -274,6 +284,7 @@ describe("DeveloperSettingsTab", () => { , ); @@ -306,6 +317,7 @@ describe("DeveloperSettingsTab", () => { , ); @@ -350,6 +362,7 @@ describe("DeveloperSettingsTab", () => { , ); @@ -389,6 +402,7 @@ describe("DeveloperSettingsTab", () => { , ); @@ -417,17 +431,16 @@ describe("DeveloperSettingsTab", () => { describe("KeyRotationStatus", () => { it("displays active status when key rotation is not suppressed", async () => { const client = createMockMatrixClient(); - const mockVm = { - keyRotationSuppressed$: new BehaviorSubject(false), - participantCount$: new BehaviorSubject(5), - } as unknown as any; + const vm = createStaticViewModel({ + keyRotation: { suppressed: false, participantCount: 5 }, + }); render( , ); @@ -443,17 +456,16 @@ describe("DeveloperSettingsTab", () => { it("displays suppressed status when key rotation is suppressed", async () => { const client = createMockMatrixClient(); - const mockVm = { - keyRotationSuppressed$: new BehaviorSubject(true), - participantCount$: new BehaviorSubject(50), - } as unknown as any; + const vm = createStaticViewModel({ + keyRotation: { suppressed: true, participantCount: 50 }, + }); render( , ); @@ -469,7 +481,7 @@ describe("DeveloperSettingsTab", () => { ).toBeInTheDocument(); }); - it("does not render KeyRotationStatus when vm is not provided", async () => { + it("does not render KeyRotationStatus when not in a call", async () => { const client = createMockMatrixClient(); render( @@ -477,6 +489,7 @@ describe("DeveloperSettingsTab", () => { , ); diff --git a/src/settings/DeveloperSettingsTab.tsx b/src/settings/DeveloperSettingsTab.tsx index 76e0c03ec..915052bd1 100644 --- a/src/settings/DeveloperSettingsTab.tsx +++ b/src/settings/DeveloperSettingsTab.tsx @@ -67,25 +67,40 @@ import styles from "./DeveloperSettingsTab.module.css"; import settingsStyles from "./SettingsModal.module.css"; import { Slider } from "../Slider"; import { useUrlParams } from "../UrlParams"; -import { type CallViewModel } from "../state/CallViewModel/CallViewModel.ts"; import { getSFUConfigWithOpenID } from "../livekit/openIDSFU"; import { useBehavior } from "../useBehavior"; +import { type ViewModel } from "../state/ViewModel.ts"; + +/** + * The state of MatrixRTC's media key rotation. + */ +export interface KeyRotationInfo { + /** Whether the call is large enough that MatrixRTC has stopped rotating the media key. */ + suppressed: boolean; + participantCount: number; +} + +/** + * The Snapshot combines all fields the developer settings tab needs from the + * surrounding call. Everything else in this tab is read from the settings store + * or the environment directly. + */ +export interface DeveloperSettingsSnapshot { + /** The media key rotation state, or `null` when we are not in a call. */ + keyRotation: KeyRotationInfo | null; +} /** * Shows whether the call is large enough that MatrixRTC has stopped rotating the media key. */ -const KeyRotationStatus: FC<{ vm: CallViewModel }> = ({ vm }) => { - const suppressed = useBehavior(vm.keyRotationSuppressed$); - const participantCount = useBehavior(vm.participantCount$); - return ( -

- Media key rotation:{" "} - {suppressed - ? `suppressed, participant limit reached (${participantCount} participants)` - : `active (${participantCount} participants)`} -

- ); -}; +const KeyRotationStatus: FC<{ info: KeyRotationInfo }> = ({ info }) => ( +

+ Media key rotation:{" "} + {info.suppressed + ? `suppressed, participant limit reached (${info.participantCount} participants)` + : `active (${info.participantCount} participants)`} +

+); interface Props { client: MatrixClient; @@ -97,8 +112,7 @@ interface Props { livekitAlias?: string; }[]; env: ImportMetaEnv; - /** Only available while in a call. */ - vm?: CallViewModel; + vm: ViewModel; } export const DeveloperSettingsTab: FC = ({ @@ -109,6 +123,7 @@ export const DeveloperSettingsTab: FC = ({ vm, }) => { const { t } = useTranslation(); + const keyRotation = useBehavior(vm.keyRotation$); const [duplicateTiles, setDuplicateTiles] = useSetting(duplicateTilesSetting); const [debugTileLayout, setDebugTileLayout] = useSetting( debugTileLayoutSetting, @@ -385,7 +400,7 @@ export const DeveloperSettingsTab: FC = ({ id: client.getDeviceId() || "unknown", })}

- {vm && } + {keyRotation !== null && } { + it("projects the key rotation state of the call", () => { + const keyRotationSuppressed$ = new BehaviorSubject(false); + const participantCount$ = new BehaviorSubject(5); + const vm = createDeveloperSettingsTabViewModel(testScope(), { + keyRotationSuppressed$, + participantCount$, + } as unknown as CallViewModel); + + expect(vm.keyRotation$.value).toEqual({ + suppressed: false, + participantCount: 5, + }); + + participantCount$.next(50); + keyRotationSuppressed$.next(true); + + expect(vm.keyRotation$.value).toEqual({ + suppressed: true, + participantCount: 50, + }); + }); +}); + +describe("outOfCallDeveloperSettingsTabViewModel", () => { + it("has no key rotation state", () => { + expect(outOfCallDeveloperSettingsTabViewModel.keyRotation$.value).toBe( + null, + ); + }); +}); diff --git a/src/settings/DeveloperSettingsTabViewModel.ts b/src/settings/DeveloperSettingsTabViewModel.ts new file mode 100644 index 000000000..9bfc7440b --- /dev/null +++ b/src/settings/DeveloperSettingsTabViewModel.ts @@ -0,0 +1,43 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { combineLatest } from "rxjs"; + +import { type CallViewModel } from "../state/CallViewModel/CallViewModel.ts"; +import { type ObservableScope } from "../state/ObservableScope.ts"; +import { createStaticViewModel, type ViewModel } from "../state/ViewModel.ts"; +import { type DeveloperSettingsSnapshot } from "./DeveloperSettingsTab.tsx"; + +/** + * Creates the ViewModel for the developer settings tab while in a call. + * + * Only the call state the tab actually renders is projected here, so that the + * tab does not need to know about the CallViewModel. + * + * @param scope - ObservableScope that bounds the lifetime of derived behaviors. + * @param callModel - The root CallViewModel; provides the key rotation state. + */ +export function createDeveloperSettingsTabViewModel( + scope: ObservableScope, + callModel: CallViewModel, +): ViewModel { + return { + keyRotation$: scope.behavior( + combineLatest( + [callModel.keyRotationSuppressed$, callModel.participantCount$], + (suppressed, participantCount) => ({ suppressed, participantCount }), + ), + ), + }; +} + +/** + * The ViewModel for the developer settings tab outside of a call (lobby, user + * menu), where no call state exists. All call specific fields are `null`. + */ +export const outOfCallDeveloperSettingsTabViewModel: ViewModel = + createStaticViewModel({ keyRotation: null }); diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index f47cfea4e..b2ffef4ab 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -29,12 +29,16 @@ import { PreferencesSettingsTab } from "./PreferencesSettingsTab"; import { Slider } from "../Slider"; import { DeviceSelection } from "./DeviceSelection"; import { useTrackProcessor } from "../livekit/TrackProcessorContext"; -import { DeveloperSettingsTab } from "./DeveloperSettingsTab"; +import { + DeveloperSettingsTab, + type DeveloperSettingsSnapshot, +} from "./DeveloperSettingsTab"; import { FieldRow, InputField } from "../input/Input"; import { useSubmitRageshake } from "./submit-rageshake"; import { useUrlParams } from "../UrlParams"; import { useBehavior } from "../useBehavior"; -import { type CallViewModel } from "../state/CallViewModel/CallViewModel.ts"; +import { type ViewModel } from "../state/ViewModel.ts"; +import { outOfCallDeveloperSettingsTabViewModel } from "./DeveloperSettingsTabViewModel"; type SettingsTab = | "audio" @@ -58,7 +62,7 @@ interface Props { isLocal?: boolean; }[]; /** Only available while in a call. Used by the developer tab. */ - vm?: CallViewModel; + developerSettingsVm?: ViewModel; } export const defaultSettingsTab: SettingsTab = "audio"; @@ -71,7 +75,7 @@ export const SettingsModal: FC = ({ client, roomId, livekitRooms, - vm, + developerSettingsVm, }) => { const { t } = useTranslation(); @@ -224,7 +228,7 @@ export const SettingsModal: FC = ({ client={client} livekitRooms={livekitRooms} roomId={roomId} - vm={vm} + vm={developerSettingsVm ?? outOfCallDeveloperSettingsTabViewModel} /> ), }; diff --git a/src/utils/test-viewmodel.ts b/src/utils/test-viewmodel.ts index 526fc95c9..fde9aac59 100644 --- a/src/utils/test-viewmodel.ts +++ b/src/utils/test-viewmodel.ts @@ -42,6 +42,8 @@ import { MatrixRTCMode } from "../config/ConfigOptions"; import { createCallFooterViewModel } from "../components/CallFooterViewModel"; import { type FooterSnapshot } from "../components/CallFooter"; import { type ViewModel } from "../state/ViewModel"; +import { createDeveloperSettingsTabViewModel } from "../settings/DeveloperSettingsTabViewModel"; +import { type DeveloperSettingsSnapshot } from "../settings/DeveloperSettingsTab"; mockConfig({ livekit: { livekit_service_url: "https://example.com" } }); @@ -140,6 +142,7 @@ export function getBasicCallViewModelEnvironment( ): { vm: CallViewModel; footerVm: ViewModel; + developerSettingsVm: ViewModel; rtcMemberships$: BehaviorSubject; rtcSession: MockRTCSession; handRaisedSubject$: BehaviorSubject>; @@ -188,6 +191,7 @@ export function getBasicCallViewModelEnvironment( return { vm, footerVm, + developerSettingsVm: createDeveloperSettingsTabViewModel(testScope(), vm), rtcMemberships$, rtcSession, handRaisedSubject$: handRaisedSubject$, From a526a8ae19ae808622fa69c8f144ecea48e2934d Mon Sep 17 00:00:00 2001 From: "Timo K." Date: Fri, 28 Aug 2026 15:26:00 +0200 Subject: [PATCH 10/18] Update config.sample.json --- config/config.sample.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/config/config.sample.json b/config/config.sample.json index dd1699530..78f9536da 100644 --- a/config/config.sample.json +++ b/config/config.sample.json @@ -18,7 +18,6 @@ "membership_event_expiry_ms": 180000000, "delayed_leave_event_delay_ms": 18000, "delayed_leave_event_restart_ms": 4000, - "network_error_retry_ms": 100, - "key_rotation_participant_limit": null + "network_error_retry_ms": 100 } } From 92f504d886d196e5602040d5dbe4dbd084adc3a5 Mon Sep 17 00:00:00 2001 From: "Timo K." Date: Fri, 28 Aug 2026 15:29:16 +0200 Subject: [PATCH 11/18] remove oldest membership mocking --- .../CallViewModel/localMember/LocalMember.test.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/state/CallViewModel/localMember/LocalMember.test.ts b/src/state/CallViewModel/localMember/LocalMember.test.ts index 4a408f114..4070e1c0b 100644 --- a/src/state/CallViewModel/localMember/LocalMember.test.ts +++ b/src/state/CallViewModel/localMember/LocalMember.test.ts @@ -123,12 +123,6 @@ describe("LocalMembership", () => { }); it("passes keyRotationParticipantLimit from config to joinRTCSession", () => { - const focusFromOlderMembership = { - type: "livekit", - livekit_service_url: "http://my-oldest-member-service-url.com", - livekit_alias: "my-oldest-member-service-alias", - }; - mockConfig({ livekit: { livekit_service_url: "http://my-default-service-url.com" }, matrix_rtc_session: { @@ -152,10 +146,6 @@ describe("LocalMembership", () => { }, }, memberships: [], - getFocusInUse: vi.fn().mockReturnValue(focusFromOlderMembership), - getOldestMembership: vi.fn().mockReturnValue({ - getPreferredFoci: vi.fn().mockReturnValue([focusFromOlderMembership]), - }), joinRTCSession: vi.fn(), }) as unknown as MatrixRTCSession; From 3d37abf490debca6c5f6a7f0d27a2c78f1256554 Mon Sep 17 00:00:00 2001 From: "Timo K." Date: Fri, 28 Aug 2026 15:31:23 +0200 Subject: [PATCH 12/18] merge two tests --- src/state/SessionBehaviors.test.ts | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/src/state/SessionBehaviors.test.ts b/src/state/SessionBehaviors.test.ts index 9f05159c5..bccfa0d0d 100644 --- a/src/state/SessionBehaviors.test.ts +++ b/src/state/SessionBehaviors.test.ts @@ -14,29 +14,12 @@ import { ObservableScope } from "./ObservableScope"; describe("SessionBehaviors", () => { describe("createKeyRotationSuppressed$", () => { - it("emits initial value from isKeyRotationSuppressed", () => { - const scope = new ObservableScope(); - - const mockSession = { - on: vi.fn(), - off: vi.fn(), - isKeyRotationSuppressed: false, - }; - - const keyRotationSuppressed$ = createKeyRotationSuppressed$( - scope, - mockSession as any, - ); - - expect(keyRotationSuppressed$.value).toBe(false); - scope.end(); - }); - - it("updates when KeyRotationSuppressedChanged event is emitted", () => { + it("emits initial value from isKeyRotationSuppressed and updates when KeyRotationSuppressedChanged event is emitted", () => { const scope = new ObservableScope(); const emitter = new EventEmitter(); - const mockSession = Object.assign(emitter, { + on: vi.fn(), + off: vi.fn(), isKeyRotationSuppressed: false, }); @@ -46,7 +29,6 @@ describe("SessionBehaviors", () => { ); expect(keyRotationSuppressed$.value).toBe(false); - emitter.emit(MatrixRTCSessionEvent.KeyRotationSuppressedChanged, true); expect(keyRotationSuppressed$.value).toBe(true); From 17d9d19c91f811203a8201c37a360ac371011384 Mon Sep 17 00:00:00 2001 From: "Timo K." Date: Fri, 28 Aug 2026 15:32:59 +0200 Subject: [PATCH 13/18] ObservableScope -> testScope --- src/state/SessionBehaviors.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/state/SessionBehaviors.test.ts b/src/state/SessionBehaviors.test.ts index bccfa0d0d..fca24767c 100644 --- a/src/state/SessionBehaviors.test.ts +++ b/src/state/SessionBehaviors.test.ts @@ -10,12 +10,12 @@ import { MatrixRTCSessionEvent } from "matrix-js-sdk/lib/matrixrtc"; import { EventEmitter } from "events"; import { createKeyRotationSuppressed$ } from "./SessionBehaviors"; -import { ObservableScope } from "./ObservableScope"; +import { testScope } from "../utils/test"; describe("SessionBehaviors", () => { describe("createKeyRotationSuppressed$", () => { it("emits initial value from isKeyRotationSuppressed and updates when KeyRotationSuppressedChanged event is emitted", () => { - const scope = new ObservableScope(); + const scope = testScope(); const emitter = new EventEmitter(); const mockSession = Object.assign(emitter, { on: vi.fn(), @@ -36,7 +36,6 @@ describe("SessionBehaviors", () => { emitter.emit(MatrixRTCSessionEvent.KeyRotationSuppressedChanged, false); expect(keyRotationSuppressed$.value).toBe(false); - scope.end(); }); }); }); From 17231e545c3d60766327b47a5cc3924a681f14c9 Mon Sep 17 00:00:00 2001 From: "Timo K." Date: Fri, 28 Aug 2026 15:38:57 +0200 Subject: [PATCH 14/18] Update DeveloperSettingsTab.test.tsx --- src/settings/DeveloperSettingsTab.test.tsx | 23 +++++++++------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/settings/DeveloperSettingsTab.test.tsx b/src/settings/DeveloperSettingsTab.test.tsx index 9fbe7fca4..4533531c2 100644 --- a/src/settings/DeveloperSettingsTab.test.tsx +++ b/src/settings/DeveloperSettingsTab.test.tsx @@ -434,12 +434,10 @@ describe("DeveloperSettingsTab", () => { ); await waitFor(() => - expect(client.doesServerSupportUnstableFeature).toHaveBeenCalled(), + expect( + screen.getByText(/Media key rotation: active \(5 participants\)/), + ).toBeInTheDocument() ); - - expect( - screen.getByText(/Media key rotation: active \(5 participants\)/), - ).toBeInTheDocument(); }); it("displays suppressed status when key rotation is suppressed", async () => { @@ -459,14 +457,12 @@ describe("DeveloperSettingsTab", () => { ); await waitFor(() => - expect(client.doesServerSupportUnstableFeature).toHaveBeenCalled(), + expect( + screen.getByText( + /Media key rotation: suppressed, participant limit reached \(50 participants\)/, + ), + ).toBeInTheDocument() ); - - expect( - screen.getByText( - /Media key rotation: suppressed, participant limit reached \(50 participants\)/, - ), - ).toBeInTheDocument(); }); it("does not render KeyRotationStatus when not in a call", async () => { @@ -483,10 +479,9 @@ describe("DeveloperSettingsTab", () => { ); await waitFor(() => - expect(client.doesServerSupportUnstableFeature).toHaveBeenCalled(), + expect(screen.queryByText(/Media key rotation:/)).not.toBeInTheDocument() ); - expect(screen.queryByText(/Media key rotation:/)).not.toBeInTheDocument(); }); }); }); From 790bd1e2bf9bcd11732a20714171bcc3c6d14167 Mon Sep 17 00:00:00 2001 From: "Timo K." Date: Fri, 28 Aug 2026 15:44:46 +0200 Subject: [PATCH 15/18] Update LocalMember.test.ts --- src/state/CallViewModel/localMember/LocalMember.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/state/CallViewModel/localMember/LocalMember.test.ts b/src/state/CallViewModel/localMember/LocalMember.test.ts index 4070e1c0b..9ea6bb72c 100644 --- a/src/state/CallViewModel/localMember/LocalMember.test.ts +++ b/src/state/CallViewModel/localMember/LocalMember.test.ts @@ -165,8 +165,8 @@ describe("LocalMembership", () => { expect(mockedSession.joinRTCSession).toHaveBeenLastCalledWith( expect.any(Object), - expect.any(Array), - undefined, + [], + expect.any(Object), expect.objectContaining({ keyRotationParticipantLimit: 50, }), From d7d28a1b55441aa3bcd5a73d096b61eee8024d1a Mon Sep 17 00:00:00 2001 From: "Timo K." Date: Fri, 28 Aug 2026 15:50:48 +0200 Subject: [PATCH 16/18] formatting --- src/settings/DeveloperSettingsTab.test.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/settings/DeveloperSettingsTab.test.tsx b/src/settings/DeveloperSettingsTab.test.tsx index 4533531c2..bd2b40b7d 100644 --- a/src/settings/DeveloperSettingsTab.test.tsx +++ b/src/settings/DeveloperSettingsTab.test.tsx @@ -436,7 +436,7 @@ describe("DeveloperSettingsTab", () => { await waitFor(() => expect( screen.getByText(/Media key rotation: active \(5 participants\)/), - ).toBeInTheDocument() + ).toBeInTheDocument(), ); }); @@ -461,7 +461,7 @@ describe("DeveloperSettingsTab", () => { screen.getByText( /Media key rotation: suppressed, participant limit reached \(50 participants\)/, ), - ).toBeInTheDocument() + ).toBeInTheDocument(), ); }); @@ -479,9 +479,10 @@ describe("DeveloperSettingsTab", () => { ); await waitFor(() => - expect(screen.queryByText(/Media key rotation:/)).not.toBeInTheDocument() + expect( + screen.queryByText(/Media key rotation:/), + ).not.toBeInTheDocument(), ); - }); }); }); From 5bab17b66833fc46b1219e308784e6e60728d199 Mon Sep 17 00:00:00 2001 From: "Timo K." Date: Fri, 28 Aug 2026 15:52:52 +0200 Subject: [PATCH 17/18] Update InCallView.tsx --- src/room/InCallView.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 6945b8b6b..a57dcce2b 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -173,9 +173,8 @@ export const ActiveCall: FC = (props) => { mediaDevices, `${props.client.getUserId()}:${props.client.getDeviceId()}`, ); - const developerSettingsVm = createDeveloperSettingsTabViewModel(scope, vm); setFooterVm(footerVm); - setDeveloperSettingsVm(developerSettingsVm); + setDeveloperSettingsVm(createDeveloperSettingsTabViewModel(scope, vm)); return (): void => { scope.end(); From 2a7c6c3c06f6b50fe5d521e01bea0b21ebcd26fe Mon Sep 17 00:00:00 2001 From: "Timo K." Date: Fri, 28 Aug 2026 16:22:08 +0200 Subject: [PATCH 18/18] Oh dear test coverage... --- src/room/InCallView.test.tsx | 64 ++++++++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/src/room/InCallView.test.tsx b/src/room/InCallView.test.tsx index eafb54c55..3113c0727 100644 --- a/src/room/InCallView.test.tsx +++ b/src/room/InCallView.test.tsx @@ -22,7 +22,7 @@ import { TooltipProvider } from "@vector-im/compound-web"; import { RoomContext, useLocalParticipant } from "@livekit/components-react"; import userEvent from "@testing-library/user-event"; -import { InCallView } from "./InCallView"; +import { ActiveCall, InCallView } from "./InCallView"; import { mockLivekitRoom, mockLocalParticipant, @@ -33,7 +33,10 @@ import { type MockRTCSession, } from "../utils/test"; import { E2eeType } from "../e2ee/e2eeType"; -import { getBasicCallViewModelEnvironment } from "../utils/test-viewmodel"; +import { + getBasicCallViewModelEnvironment, + getBasicRTCSession, +} from "../utils/test-viewmodel"; import { type CallViewModel, type CallViewModelOptions, @@ -45,6 +48,8 @@ import { LivekitRoomAudioRenderer } from "../livekit/MatrixAudioRenderer"; import { MediaDevicesContext } from "../MediaDevicesContext"; import { type MediaDevices as ECMediaDevices } from "../state/MediaDevices"; import { AppBar } from "../AppBar"; +import { type MatrixInfo } from "./VideoPreview"; +import { ProcessorProvider } from "../livekit/TrackProcessorContext"; import { initializeWidget } from "../widget"; initializeWidget(); @@ -78,6 +83,17 @@ const remoteParticipant = mockRemoteParticipant({ identity: "@alice:example.org:AAAAAA", }); +const matrixInfo = { + userId: "", + displayName: "", + avatarUrl: "", + roomId: "", + roomName: "", + roomAlias: null, + roomAvatar: null, + e2eeSystem: { kind: E2eeType.NONE }, +} satisfies MatrixInfo; + let useRoomEncryptionSystemMock: MockedFunction; beforeEach(() => { @@ -142,18 +158,7 @@ function createInCallView(args: CreateInCallViewArgs = {}): RenderResult & { vm={vm} footerVm={footerVm} developerSettingsVm={developerSettingsVm} - matrixInfo={{ - userId: "", - displayName: "", - avatarUrl: "", - roomId: "", - roomName: "", - roomAlias: null, - roomAvatar: null, - e2eeSystem: { - kind: E2eeType.NONE, - }, - }} + matrixInfo={matrixInfo} matrixRoom={room} onShareClick={null} /> @@ -228,3 +233,34 @@ describe("InCallView", () => { }); }); }); + +describe("ActiveCall", () => { + it("creates the view models and renders the call", async () => { + const mediaDevices = mockMediaDevices({}); + const { rtcSession, matrixRoom } = getBasicRTCSession([local, alice]); + const { findByTestId } = render( + + + + + + {}} + /> + + + + + , + ); + // Rendering at all proves ActiveCall created all of its view models + expect(await findByTestId("incall_leave")).toBeVisible(); + }); +});