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();
+ });
+});