mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-29 21:15:19 +00:00
Merge pull request #4172 from element-hq/toger5/add-key-rotation-participant-limit
Add key rotation participant limit to devtools and config.json
This commit is contained in:
40
pnpm-lock.yaml
generated
40
pnpm-lock.yaml
generated
@@ -66,7 +66,7 @@ importers:
|
|||||||
version: 10.1.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
version: 10.1.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||||
'@rolldown/plugin-babel':
|
'@rolldown/plugin-babel':
|
||||||
specifier: ^0.2.3
|
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':
|
'@sentry/react':
|
||||||
specifier: ^8.0.0
|
specifier: ^8.0.0
|
||||||
version: 8.55.2(react@19.2.8)
|
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.4(@types/react@19.2.17)(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
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.4(@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':
|
'@vitejs/plugin-react':
|
||||||
specifier: ^6.0.2
|
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':
|
'@vitest/browser-playwright':
|
||||||
specifier: ^4.1.5
|
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)
|
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)
|
||||||
@@ -192,7 +192,7 @@ importers:
|
|||||||
version: 1.9.2
|
version: 1.9.2
|
||||||
matrix-js-sdk:
|
matrix-js-sdk:
|
||||||
specifier: github:matrix-org/matrix-js-sdk#develop
|
specifier: github:matrix-org/matrix-js-sdk#develop
|
||||||
version: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/178dc6528ebc6a15e2f6fab38410127f16b500d0
|
version: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/24929be0e741be6a5d0a7226f1c682e245263b8a
|
||||||
matrix-widget-api:
|
matrix-widget-api:
|
||||||
specifier: ^1.18.0
|
specifier: ^1.18.0
|
||||||
version: 1.18.0
|
version: 1.18.0
|
||||||
@@ -1191,8 +1191,8 @@ packages:
|
|||||||
'@types/dom-mediacapture-transform': ^0.1.9
|
'@types/dom-mediacapture-transform': ^0.1.9
|
||||||
livekit-client: ^1.12.0 || ^2.1.0
|
livekit-client: ^1.12.0 || ^2.1.0
|
||||||
|
|
||||||
'@matrix-org/matrix-sdk-crypto-wasm@18.4.0':
|
'@matrix-org/matrix-sdk-crypto-wasm@18.5.0':
|
||||||
resolution: {integrity: sha512-osxkU1DQ+05+anGHapjWyvZqdHUb94Id37gy54mCKn1Cq/D7iGT5oEUEhjp4oTnCLo4TOtI6ULJ/LHsapaIptQ==}
|
resolution: {integrity: sha512-E826Hy1rG26LanPjtSsOiVRcVoHfSgPgj2r2Xsb5RPScpaKi9XJADQ0u3dNjRCitZPX4oyLNl1FDd5AfDlmIwQ==}
|
||||||
engines: {node: '>= 18'}
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
'@mdx-js/react@3.1.1':
|
'@mdx-js/react@3.1.1':
|
||||||
@@ -3670,9 +3670,9 @@ packages:
|
|||||||
constants-browserify@1.0.0:
|
constants-browserify@1.0.0:
|
||||||
resolution: {integrity: sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==}
|
resolution: {integrity: sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==}
|
||||||
|
|
||||||
content-type@2.0.0:
|
content-type@3.0.0:
|
||||||
resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==}
|
resolution: {integrity: sha512-AIi5H6p0xk5uknXcN3/rmhP8jgp69OfSe/JuKiQAFprJ7UGw7mwj7m4XcmDzlrnJDG+cGpphAINGdU3g3g7kDw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
convert-source-map@2.0.0:
|
convert-source-map@2.0.0:
|
||||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||||
@@ -4685,9 +4685,9 @@ packages:
|
|||||||
matrix-events-sdk@0.0.1:
|
matrix-events-sdk@0.0.1:
|
||||||
resolution: {integrity: sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==}
|
resolution: {integrity: sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==}
|
||||||
|
|
||||||
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/24929be0e741be6a5d0a7226f1c682e245263b8a:
|
||||||
resolution: {gitHosted: true, integrity: sha512-UmlkoXs9VMZvNaDeQqBb2k+gLifnIqEZc5cxrLeEwdYEDakQLbRqrFtmvIp4ipUCjdIPkCgtzb7QLlpPBLAwqA==, tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/178dc6528ebc6a15e2f6fab38410127f16b500d0}
|
resolution: {gitHosted: true, integrity: sha512-0EGwbXzvG88sBOxq4ZlCRwT4VpQj2xkizfyy0A3bTSK0ysb6/gXJ/o1WPFoqCjRGMDgPq4gAsk92xGtHA7EhPg==, tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/24929be0e741be6a5d0a7226f1c682e245263b8a}
|
||||||
version: 42.1.0
|
version: 42.2.0
|
||||||
engines: {node: '>=22.0.0'}
|
engines: {node: '>=22.0.0'}
|
||||||
|
|
||||||
matrix-widget-api@1.18.0:
|
matrix-widget-api@1.18.0:
|
||||||
@@ -7102,7 +7102,7 @@ snapshots:
|
|||||||
'@types/dom-mediacapture-transform': 0.1.11
|
'@types/dom-mediacapture-transform': 0.1.11
|
||||||
livekit-client: 2.22.0(@types/dom-mediacapture-record@1.0.22)
|
livekit-client: 2.22.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)':
|
'@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -8053,13 +8053,13 @@ snapshots:
|
|||||||
'@rolldown/binding-win32-x64-msvc@1.1.5':
|
'@rolldown/binding-win32-x64-msvc@1.1.5':
|
||||||
optional: true
|
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:
|
dependencies:
|
||||||
'@babel/core': 7.29.7(supports-color@7.2.0)
|
'@babel/core': 7.29.7(supports-color@7.2.0)
|
||||||
picomatch: 4.0.4
|
picomatch: 4.0.4
|
||||||
rolldown: 1.1.5
|
rolldown: 1.1.5
|
||||||
optionalDependencies:
|
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)
|
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': {}
|
'@rolldown/pluginutils@1.0.1': {}
|
||||||
@@ -8744,12 +8744,12 @@ snapshots:
|
|||||||
- '@types/react-dom'
|
- '@types/react-dom'
|
||||||
- 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:
|
dependencies:
|
||||||
'@rolldown/pluginutils': 1.0.1
|
'@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)
|
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:
|
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
|
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)':
|
'@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)':
|
||||||
@@ -9195,7 +9195,7 @@ snapshots:
|
|||||||
|
|
||||||
constants-browserify@1.0.0: {}
|
constants-browserify@1.0.0: {}
|
||||||
|
|
||||||
content-type@2.0.0: {}
|
content-type@3.0.0: {}
|
||||||
|
|
||||||
convert-source-map@2.0.0: {}
|
convert-source-map@2.0.0: {}
|
||||||
|
|
||||||
@@ -10275,13 +10275,13 @@ snapshots:
|
|||||||
|
|
||||||
matrix-events-sdk@0.0.1: {}
|
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/24929be0e741be6a5d0a7226f1c682e245263b8a:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 8.0.0
|
'@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
|
another-json: 0.2.0
|
||||||
bs58: 6.0.0
|
bs58: 6.0.0
|
||||||
content-type: 2.0.0
|
content-type: 3.0.0
|
||||||
loglevel: 1.9.2
|
loglevel: 1.9.2
|
||||||
matrix-events-sdk: 0.0.1
|
matrix-events-sdk: 0.0.1
|
||||||
matrix-widget-api: 1.18.0
|
matrix-widget-api: 1.18.0
|
||||||
|
|||||||
@@ -219,6 +219,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.
|
* 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;
|
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 (undefined). Which means that rotation will always happen.
|
||||||
|
*/
|
||||||
|
key_rotation_participant_limit?: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,6 +264,7 @@ export interface ResolvedConfigOptions extends ConfigOptions {
|
|||||||
delayed_leave_event_restart_ms?: number;
|
delayed_leave_event_restart_ms?: number;
|
||||||
network_error_retry_ms: number;
|
network_error_retry_ms: number;
|
||||||
membership_event_expiry_ms?: number;
|
membership_event_expiry_ms?: number;
|
||||||
|
key_rotation_participant_limit?: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { TooltipProvider } from "@vector-im/compound-web";
|
|||||||
import { RoomContext, useLocalParticipant } from "@livekit/components-react";
|
import { RoomContext, useLocalParticipant } from "@livekit/components-react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
|
|
||||||
import { InCallView } from "./InCallView";
|
import { ActiveCall, InCallView } from "./InCallView";
|
||||||
import {
|
import {
|
||||||
mockLivekitRoom,
|
mockLivekitRoom,
|
||||||
mockLocalParticipant,
|
mockLocalParticipant,
|
||||||
@@ -33,7 +33,10 @@ import {
|
|||||||
type MockRTCSession,
|
type MockRTCSession,
|
||||||
} from "../utils/test";
|
} from "../utils/test";
|
||||||
import { E2eeType } from "../e2ee/e2eeType";
|
import { E2eeType } from "../e2ee/e2eeType";
|
||||||
import { getBasicCallViewModelEnvironment } from "../utils/test-viewmodel";
|
import {
|
||||||
|
getBasicCallViewModelEnvironment,
|
||||||
|
getBasicRTCSession,
|
||||||
|
} from "../utils/test-viewmodel";
|
||||||
import {
|
import {
|
||||||
type CallViewModel,
|
type CallViewModel,
|
||||||
type CallViewModelOptions,
|
type CallViewModelOptions,
|
||||||
@@ -45,6 +48,8 @@ import { LivekitRoomAudioRenderer } from "../livekit/MatrixAudioRenderer";
|
|||||||
import { MediaDevicesContext } from "../MediaDevicesContext";
|
import { MediaDevicesContext } from "../MediaDevicesContext";
|
||||||
import { type MediaDevices as ECMediaDevices } from "../state/MediaDevices";
|
import { type MediaDevices as ECMediaDevices } from "../state/MediaDevices";
|
||||||
import { AppBar } from "../AppBar";
|
import { AppBar } from "../AppBar";
|
||||||
|
import { type MatrixInfo } from "./VideoPreview";
|
||||||
|
import { ProcessorProvider } from "../livekit/TrackProcessorContext";
|
||||||
import { initializeWidget } from "../widget";
|
import { initializeWidget } from "../widget";
|
||||||
|
|
||||||
initializeWidget();
|
initializeWidget();
|
||||||
@@ -78,6 +83,17 @@ const remoteParticipant = mockRemoteParticipant({
|
|||||||
identity: "@alice:example.org:AAAAAA",
|
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<typeof useRoomEncryptionSystem>;
|
let useRoomEncryptionSystemMock: MockedFunction<typeof useRoomEncryptionSystem>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -122,7 +138,8 @@ function createInCallView(args: CreateInCallViewArgs = {}): RenderResult & {
|
|||||||
remoteParticipants$: of([remoteParticipant]),
|
remoteParticipants$: of([remoteParticipant]),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const { vm, footerVm, rtcSession } = getBasicCallViewModelEnvironment(
|
const { vm, footerVm, developerSettingsVm, rtcSession } =
|
||||||
|
getBasicCallViewModelEnvironment(
|
||||||
[local, alice],
|
[local, alice],
|
||||||
undefined,
|
undefined,
|
||||||
mediaDevices,
|
mediaDevices,
|
||||||
@@ -140,18 +157,8 @@ function createInCallView(args: CreateInCallViewArgs = {}): RenderResult & {
|
|||||||
muteStates={muteState}
|
muteStates={muteState}
|
||||||
vm={vm}
|
vm={vm}
|
||||||
footerVm={footerVm}
|
footerVm={footerVm}
|
||||||
matrixInfo={{
|
developerSettingsVm={developerSettingsVm}
|
||||||
userId: "",
|
matrixInfo={matrixInfo}
|
||||||
displayName: "",
|
|
||||||
avatarUrl: "",
|
|
||||||
roomId: "",
|
|
||||||
roomName: "",
|
|
||||||
roomAlias: null,
|
|
||||||
roomAvatar: null,
|
|
||||||
e2eeSystem: {
|
|
||||||
kind: E2eeType.NONE,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
matrixRoom={room}
|
matrixRoom={room}
|
||||||
onShareClick={null}
|
onShareClick={null}
|
||||||
/>
|
/>
|
||||||
@@ -226,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(
|
||||||
|
<BrowserRouter>
|
||||||
|
<MediaDevicesContext value={mediaDevices}>
|
||||||
|
<ProcessorProvider>
|
||||||
|
<TooltipProvider>
|
||||||
|
<RoomContext value={mockLivekitRoom({ localParticipant })}>
|
||||||
|
<ActiveCall
|
||||||
|
client={matrixRoom.client}
|
||||||
|
rtcSession={rtcSession.asMockedSession()}
|
||||||
|
matrixRoom={matrixRoom}
|
||||||
|
muteStates={mockMuteStates()}
|
||||||
|
matrixInfo={matrixInfo}
|
||||||
|
onShareClick={null}
|
||||||
|
e2eeSystem={{ kind: E2eeType.NONE }}
|
||||||
|
onLeft={(): void => {}}
|
||||||
|
/>
|
||||||
|
</RoomContext>
|
||||||
|
</TooltipProvider>
|
||||||
|
</ProcessorProvider>
|
||||||
|
</MediaDevicesContext>
|
||||||
|
</BrowserRouter>,
|
||||||
|
);
|
||||||
|
// Rendering at all proves ActiveCall created all of its view models
|
||||||
|
expect(await findByTestId("incall_leave")).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -83,6 +83,8 @@ import { ObservableScope } from "../state/ObservableScope.ts";
|
|||||||
import { CallFooter, type FooterSnapshot } from "../components/CallFooter.tsx";
|
import { CallFooter, type FooterSnapshot } from "../components/CallFooter.tsx";
|
||||||
import { SettingsIconButton } from "../button/Button.tsx";
|
import { SettingsIconButton } from "../button/Button.tsx";
|
||||||
import { createCallFooterViewModel } from "../components/CallFooterViewModel.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 { type ViewModel } from "../state/ViewModel.ts";
|
||||||
import { RingingStatus } from "../tile/RingingStatus.tsx";
|
import { RingingStatus } from "../tile/RingingStatus.tsx";
|
||||||
import { RingingAudioRenderer } from "./RingingAudioRenderer.tsx";
|
import { RingingAudioRenderer } from "./RingingAudioRenderer.tsx";
|
||||||
@@ -96,7 +98,7 @@ declare module "react" {
|
|||||||
|
|
||||||
export interface ActiveCallProps extends Omit<
|
export interface ActiveCallProps extends Omit<
|
||||||
InCallViewProps,
|
InCallViewProps,
|
||||||
"vm" | "livekitRoom" | "connState" | "footerVm"
|
"vm" | "livekitRoom" | "connState" | "footerVm" | "developerSettingsVm"
|
||||||
> {
|
> {
|
||||||
e2eeSystem: EncryptionSystem;
|
e2eeSystem: EncryptionSystem;
|
||||||
// TODO refactor those reasons into an enum
|
// TODO refactor those reasons into an enum
|
||||||
@@ -110,6 +112,9 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
|||||||
const [footerVm, setFooterVm] = useState<ViewModel<FooterSnapshot> | null>(
|
const [footerVm, setFooterVm] = useState<ViewModel<FooterSnapshot> | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
const [developerSettingsVm, setDeveloperSettingsVm] =
|
||||||
|
useState<ViewModel<DeveloperSettingsSnapshot> | null>(null);
|
||||||
|
|
||||||
const urlParams = useUrlParams();
|
const urlParams = useUrlParams();
|
||||||
const mediaDevices = useMediaDevices();
|
const mediaDevices = useMediaDevices();
|
||||||
const trackProcessorState$ = useTrackProcessorObservable$();
|
const trackProcessorState$ = useTrackProcessorObservable$();
|
||||||
@@ -169,6 +174,7 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
|||||||
`${props.client.getUserId()}:${props.client.getDeviceId()}`,
|
`${props.client.getUserId()}:${props.client.getDeviceId()}`,
|
||||||
);
|
);
|
||||||
setFooterVm(footerVm);
|
setFooterVm(footerVm);
|
||||||
|
setDeveloperSettingsVm(createDeveloperSettingsTabViewModel(scope, vm));
|
||||||
|
|
||||||
return (): void => {
|
return (): void => {
|
||||||
scope.end();
|
scope.end();
|
||||||
@@ -188,10 +194,16 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
|||||||
|
|
||||||
if (vm === null) return null;
|
if (vm === null) return null;
|
||||||
if (footerVm === null) return null;
|
if (footerVm === null) return null;
|
||||||
|
if (developerSettingsVm === null) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactionsSenderProvider vm={vm} rtcSession={props.rtcSession}>
|
<ReactionsSenderProvider vm={vm} rtcSession={props.rtcSession}>
|
||||||
<InCallView {...props} vm={vm} footerVm={footerVm} />
|
<InCallView
|
||||||
|
{...props}
|
||||||
|
vm={vm}
|
||||||
|
footerVm={footerVm}
|
||||||
|
developerSettingsVm={developerSettingsVm}
|
||||||
|
/>
|
||||||
</ReactionsSenderProvider>
|
</ReactionsSenderProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -200,6 +212,7 @@ export interface InCallViewProps {
|
|||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
vm: CallViewModel;
|
vm: CallViewModel;
|
||||||
footerVm: ViewModel<FooterSnapshot>;
|
footerVm: ViewModel<FooterSnapshot>;
|
||||||
|
developerSettingsVm: ViewModel<DeveloperSettingsSnapshot>;
|
||||||
matrixInfo: MatrixInfo;
|
matrixInfo: MatrixInfo;
|
||||||
rtcSession: MatrixRTCSession;
|
rtcSession: MatrixRTCSession;
|
||||||
matrixRoom: MatrixRoom;
|
matrixRoom: MatrixRoom;
|
||||||
@@ -211,6 +224,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
client,
|
client,
|
||||||
vm,
|
vm,
|
||||||
footerVm,
|
footerVm,
|
||||||
|
developerSettingsVm,
|
||||||
matrixInfo,
|
matrixInfo,
|
||||||
matrixRoom,
|
matrixRoom,
|
||||||
muteStates,
|
muteStates,
|
||||||
@@ -633,6 +647,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
onDismiss={(): void => setSettingsOpen(false)}
|
onDismiss={(): void => setSettingsOpen(false)}
|
||||||
tab={settingsTab}
|
tab={settingsTab}
|
||||||
onTabChange={setSettingsTab}
|
onTabChange={setSettingsTab}
|
||||||
|
developerSettingsVm={developerSettingsVm}
|
||||||
livekitRooms={allConnections
|
livekitRooms={allConnections
|
||||||
.getConnections()
|
.getConnections()
|
||||||
.map((connectionItem) => ({
|
.map((connectionItem) => ({
|
||||||
|
|||||||
@@ -12,7 +12,12 @@ import { TooltipProvider } from "@vector-im/compound-web";
|
|||||||
|
|
||||||
import type { MatrixClient } from "matrix-js-sdk";
|
import type { MatrixClient } from "matrix-js-sdk";
|
||||||
import type { Room as LivekitRoom } from "livekit-client";
|
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 { getSFUConfigWithOpenID } from "../livekit/openIDSFU";
|
||||||
import {
|
import {
|
||||||
customLivekitUrl as customLivekitUrlSetting,
|
customLivekitUrl as customLivekitUrlSetting,
|
||||||
@@ -107,6 +112,7 @@ describe("DeveloperSettingsTab", () => {
|
|||||||
roomId={"#room:example.org"}
|
roomId={"#room:example.org"}
|
||||||
livekitRooms={livekitRooms}
|
livekitRooms={livekitRooms}
|
||||||
env={{ MY_MOCK_ENV: 10, ENV: "test" } as unknown as ImportMetaEnv}
|
env={{ MY_MOCK_ENV: 10, ENV: "test" } as unknown as ImportMetaEnv}
|
||||||
|
vm={outOfCallDeveloperSettingsTabViewModel}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -136,6 +142,7 @@ describe("DeveloperSettingsTab", () => {
|
|||||||
<DeveloperSettingsTab
|
<DeveloperSettingsTab
|
||||||
client={client}
|
client={client}
|
||||||
env={{} as unknown as ImportMetaEnv}
|
env={{} as unknown as ImportMetaEnv}
|
||||||
|
vm={outOfCallDeveloperSettingsTabViewModel}
|
||||||
/>
|
/>
|
||||||
</TooltipProvider>,
|
</TooltipProvider>,
|
||||||
);
|
);
|
||||||
@@ -159,6 +166,7 @@ describe("DeveloperSettingsTab", () => {
|
|||||||
client={client}
|
client={client}
|
||||||
roomId="#testRoom"
|
roomId="#testRoom"
|
||||||
env={{} as unknown as ImportMetaEnv}
|
env={{} as unknown as ImportMetaEnv}
|
||||||
|
vm={outOfCallDeveloperSettingsTabViewModel}
|
||||||
/>
|
/>
|
||||||
</TooltipProvider>,
|
</TooltipProvider>,
|
||||||
);
|
);
|
||||||
@@ -181,6 +189,7 @@ describe("DeveloperSettingsTab", () => {
|
|||||||
client={client}
|
client={client}
|
||||||
roomId="#testRoom"
|
roomId="#testRoom"
|
||||||
env={{} as unknown as ImportMetaEnv}
|
env={{} as unknown as ImportMetaEnv}
|
||||||
|
vm={outOfCallDeveloperSettingsTabViewModel}
|
||||||
/>
|
/>
|
||||||
</TooltipProvider>,
|
</TooltipProvider>,
|
||||||
);
|
);
|
||||||
@@ -206,6 +215,7 @@ describe("DeveloperSettingsTab", () => {
|
|||||||
client={client}
|
client={client}
|
||||||
roomId="#testRoom"
|
roomId="#testRoom"
|
||||||
env={{} as unknown as ImportMetaEnv}
|
env={{} as unknown as ImportMetaEnv}
|
||||||
|
vm={outOfCallDeveloperSettingsTabViewModel}
|
||||||
/>
|
/>
|
||||||
</TooltipProvider>,
|
</TooltipProvider>,
|
||||||
);
|
);
|
||||||
@@ -236,6 +246,7 @@ describe("DeveloperSettingsTab", () => {
|
|||||||
client={client}
|
client={client}
|
||||||
roomId="#testRoom"
|
roomId="#testRoom"
|
||||||
env={{} as unknown as ImportMetaEnv}
|
env={{} as unknown as ImportMetaEnv}
|
||||||
|
vm={outOfCallDeveloperSettingsTabViewModel}
|
||||||
/>
|
/>
|
||||||
</TooltipProvider>,
|
</TooltipProvider>,
|
||||||
);
|
);
|
||||||
@@ -273,6 +284,7 @@ describe("DeveloperSettingsTab", () => {
|
|||||||
<DeveloperSettingsTab
|
<DeveloperSettingsTab
|
||||||
client={client}
|
client={client}
|
||||||
env={{} as unknown as ImportMetaEnv}
|
env={{} as unknown as ImportMetaEnv}
|
||||||
|
vm={outOfCallDeveloperSettingsTabViewModel}
|
||||||
/>
|
/>
|
||||||
</TooltipProvider>,
|
</TooltipProvider>,
|
||||||
);
|
);
|
||||||
@@ -305,6 +317,7 @@ describe("DeveloperSettingsTab", () => {
|
|||||||
<DeveloperSettingsTab
|
<DeveloperSettingsTab
|
||||||
client={client}
|
client={client}
|
||||||
env={{} as unknown as ImportMetaEnv}
|
env={{} as unknown as ImportMetaEnv}
|
||||||
|
vm={outOfCallDeveloperSettingsTabViewModel}
|
||||||
/>
|
/>
|
||||||
</TooltipProvider>,
|
</TooltipProvider>,
|
||||||
);
|
);
|
||||||
@@ -345,6 +358,7 @@ describe("DeveloperSettingsTab", () => {
|
|||||||
<DeveloperSettingsTab
|
<DeveloperSettingsTab
|
||||||
client={client}
|
client={client}
|
||||||
env={{} as unknown as ImportMetaEnv}
|
env={{} as unknown as ImportMetaEnv}
|
||||||
|
vm={outOfCallDeveloperSettingsTabViewModel}
|
||||||
/>
|
/>
|
||||||
</TooltipProvider>,
|
</TooltipProvider>,
|
||||||
);
|
);
|
||||||
@@ -378,6 +392,7 @@ describe("DeveloperSettingsTab", () => {
|
|||||||
<DeveloperSettingsTab
|
<DeveloperSettingsTab
|
||||||
client={client}
|
client={client}
|
||||||
env={{} as unknown as ImportMetaEnv}
|
env={{} as unknown as ImportMetaEnv}
|
||||||
|
vm={outOfCallDeveloperSettingsTabViewModel}
|
||||||
/>
|
/>
|
||||||
</TooltipProvider>,
|
</TooltipProvider>,
|
||||||
);
|
);
|
||||||
@@ -400,4 +415,74 @@ describe("DeveloperSettingsTab", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("KeyRotationStatus", () => {
|
||||||
|
it("displays active status when key rotation is not suppressed", async () => {
|
||||||
|
const client = createMockMatrixClient();
|
||||||
|
const vm = createStaticViewModel<DeveloperSettingsSnapshot>({
|
||||||
|
keyRotation: { suppressed: false, participantCount: 5 },
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TooltipProvider>
|
||||||
|
<DeveloperSettingsTab
|
||||||
|
client={client}
|
||||||
|
env={{} as unknown as ImportMetaEnv}
|
||||||
|
vm={vm}
|
||||||
|
/>
|
||||||
|
</TooltipProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(
|
||||||
|
screen.getByText(/Media key rotation: active \(5 participants\)/),
|
||||||
|
).toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("displays suppressed status when key rotation is suppressed", async () => {
|
||||||
|
const client = createMockMatrixClient();
|
||||||
|
const vm = createStaticViewModel<DeveloperSettingsSnapshot>({
|
||||||
|
keyRotation: { suppressed: true, participantCount: 50 },
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TooltipProvider>
|
||||||
|
<DeveloperSettingsTab
|
||||||
|
client={client}
|
||||||
|
env={{} as unknown as ImportMetaEnv}
|
||||||
|
vm={vm}
|
||||||
|
/>
|
||||||
|
</TooltipProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(
|
||||||
|
screen.getByText(
|
||||||
|
/Media key rotation: suppressed, participant limit reached \(50 participants\)/,
|
||||||
|
),
|
||||||
|
).toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render KeyRotationStatus when not in a call", async () => {
|
||||||
|
const client = createMockMatrixClient();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TooltipProvider>
|
||||||
|
<DeveloperSettingsTab
|
||||||
|
client={client}
|
||||||
|
env={{} as unknown as ImportMetaEnv}
|
||||||
|
vm={outOfCallDeveloperSettingsTabViewModel}
|
||||||
|
/>
|
||||||
|
</TooltipProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(
|
||||||
|
screen.queryByText(/Media key rotation:/),
|
||||||
|
).not.toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -68,6 +68,39 @@ import settingsStyles from "./SettingsModal.module.css";
|
|||||||
import { Slider } from "../Slider";
|
import { Slider } from "../Slider";
|
||||||
import { useUrlParams } from "../UrlParams";
|
import { useUrlParams } from "../UrlParams";
|
||||||
import { getSFUConfigWithOpenID } from "../livekit/openIDSFU";
|
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<{ info: KeyRotationInfo }> = ({ info }) => (
|
||||||
|
<p>
|
||||||
|
Media key rotation:{" "}
|
||||||
|
{info.suppressed
|
||||||
|
? `suppressed, participant limit reached (${info.participantCount} participants)`
|
||||||
|
: `active (${info.participantCount} participants)`}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
@@ -79,6 +112,7 @@ interface Props {
|
|||||||
livekitAlias?: string;
|
livekitAlias?: string;
|
||||||
}[];
|
}[];
|
||||||
env: ImportMetaEnv;
|
env: ImportMetaEnv;
|
||||||
|
vm: ViewModel<DeveloperSettingsSnapshot>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeveloperSettingsTab: FC<Props> = ({
|
export const DeveloperSettingsTab: FC<Props> = ({
|
||||||
@@ -86,8 +120,10 @@ export const DeveloperSettingsTab: FC<Props> = ({
|
|||||||
livekitRooms,
|
livekitRooms,
|
||||||
roomId,
|
roomId,
|
||||||
env,
|
env,
|
||||||
|
vm,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const keyRotation = useBehavior(vm.keyRotation$);
|
||||||
const [duplicateTiles, setDuplicateTiles] = useSetting(duplicateTilesSetting);
|
const [duplicateTiles, setDuplicateTiles] = useSetting(duplicateTilesSetting);
|
||||||
const [debugTileLayout, setDebugTileLayout] = useSetting(
|
const [debugTileLayout, setDebugTileLayout] = useSetting(
|
||||||
debugTileLayoutSetting,
|
debugTileLayoutSetting,
|
||||||
@@ -364,6 +400,7 @@ export const DeveloperSettingsTab: FC<Props> = ({
|
|||||||
id: client.getDeviceId() || "unknown",
|
id: client.getDeviceId() || "unknown",
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
|
{keyRotation !== null && <KeyRotationStatus info={keyRotation} />}
|
||||||
<Separator />
|
<Separator />
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<InputField
|
<InputField
|
||||||
|
|||||||
48
src/settings/DeveloperSettingsTabViewModel.test.ts
Normal file
48
src/settings/DeveloperSettingsTabViewModel.test.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
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 { describe, expect, it } from "vitest";
|
||||||
|
import { BehaviorSubject } from "rxjs";
|
||||||
|
|
||||||
|
import { testScope } from "../utils/test";
|
||||||
|
import { type CallViewModel } from "../state/CallViewModel/CallViewModel";
|
||||||
|
import {
|
||||||
|
createDeveloperSettingsTabViewModel,
|
||||||
|
outOfCallDeveloperSettingsTabViewModel,
|
||||||
|
} from "./DeveloperSettingsTabViewModel";
|
||||||
|
|
||||||
|
describe("createDeveloperSettingsTabViewModel", () => {
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
43
src/settings/DeveloperSettingsTabViewModel.ts
Normal file
43
src/settings/DeveloperSettingsTabViewModel.ts
Normal file
@@ -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<DeveloperSettingsSnapshot> {
|
||||||
|
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<DeveloperSettingsSnapshot> =
|
||||||
|
createStaticViewModel<DeveloperSettingsSnapshot>({ keyRotation: null });
|
||||||
@@ -29,11 +29,16 @@ import { PreferencesSettingsTab } from "./PreferencesSettingsTab";
|
|||||||
import { Slider } from "../Slider";
|
import { Slider } from "../Slider";
|
||||||
import { DeviceSelection } from "./DeviceSelection";
|
import { DeviceSelection } from "./DeviceSelection";
|
||||||
import { useTrackProcessor } from "../livekit/TrackProcessorContext";
|
import { useTrackProcessor } from "../livekit/TrackProcessorContext";
|
||||||
import { DeveloperSettingsTab } from "./DeveloperSettingsTab";
|
import {
|
||||||
|
DeveloperSettingsTab,
|
||||||
|
type DeveloperSettingsSnapshot,
|
||||||
|
} from "./DeveloperSettingsTab";
|
||||||
import { FieldRow, InputField } from "../input/Input";
|
import { FieldRow, InputField } from "../input/Input";
|
||||||
import { useSubmitRageshake } from "./submit-rageshake";
|
import { useSubmitRageshake } from "./submit-rageshake";
|
||||||
import { useUrlParams } from "../UrlParams";
|
import { useUrlParams } from "../UrlParams";
|
||||||
import { useBehavior } from "../useBehavior";
|
import { useBehavior } from "../useBehavior";
|
||||||
|
import { type ViewModel } from "../state/ViewModel.ts";
|
||||||
|
import { outOfCallDeveloperSettingsTabViewModel } from "./DeveloperSettingsTabViewModel";
|
||||||
|
|
||||||
type SettingsTab =
|
type SettingsTab =
|
||||||
| "audio"
|
| "audio"
|
||||||
@@ -56,6 +61,8 @@ interface Props {
|
|||||||
url: string;
|
url: string;
|
||||||
isLocal?: boolean;
|
isLocal?: boolean;
|
||||||
}[];
|
}[];
|
||||||
|
/** Only available while in a call. Used by the developer tab. */
|
||||||
|
developerSettingsVm?: ViewModel<DeveloperSettingsSnapshot>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultSettingsTab: SettingsTab = "audio";
|
export const defaultSettingsTab: SettingsTab = "audio";
|
||||||
@@ -68,6 +75,7 @@ export const SettingsModal: FC<Props> = ({
|
|||||||
client,
|
client,
|
||||||
roomId,
|
roomId,
|
||||||
livekitRooms,
|
livekitRooms,
|
||||||
|
developerSettingsVm,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -220,6 +228,7 @@ export const SettingsModal: FC<Props> = ({
|
|||||||
client={client}
|
client={client}
|
||||||
livekitRooms={livekitRooms}
|
livekitRooms={livekitRooms}
|
||||||
roomId={roomId}
|
roomId={roomId}
|
||||||
|
vm={developerSettingsVm ?? outOfCallDeveloperSettingsTabViewModel}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ import {
|
|||||||
type LocalTransport,
|
type LocalTransport,
|
||||||
} from "./localMember/LocalTransport.ts";
|
} from "./localMember/LocalTransport.ts";
|
||||||
import {
|
import {
|
||||||
|
createKeyRotationSuppressed$,
|
||||||
createMemberships$,
|
createMemberships$,
|
||||||
membershipsAndTransports$,
|
membershipsAndTransports$,
|
||||||
} from "../SessionBehaviors.ts";
|
} from "../SessionBehaviors.ts";
|
||||||
@@ -302,6 +303,12 @@ export interface CallViewModel {
|
|||||||
* multiple devices.
|
* multiple devices.
|
||||||
*/
|
*/
|
||||||
participantCount$: Behavior<number>;
|
participantCount$: Behavior<number>;
|
||||||
|
/**
|
||||||
|
* 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<boolean>;
|
||||||
allConnections$: Behavior<ConnectionManagerData>;
|
allConnections$: Behavior<ConnectionManagerData>;
|
||||||
/** Participants sorted by livekit room so they can be used in the audio rendering */
|
/** Participants sorted by livekit room so they can be used in the audio rendering */
|
||||||
livekitRoomItems$: Behavior<LivekitRoomItem[]>;
|
livekitRoomItems$: Behavior<LivekitRoomItem[]>;
|
||||||
@@ -861,6 +868,11 @@ export function createCallViewModel$(
|
|||||||
matrixLivekitMembers$.pipe(map((ms) => ms.length)),
|
matrixLivekitMembers$.pipe(map((ms) => ms.length)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const keyRotationSuppressed$ = createKeyRotationSuppressed$(
|
||||||
|
scope,
|
||||||
|
matrixRTCSession,
|
||||||
|
);
|
||||||
|
|
||||||
const leaveSoundEffect$ = userMedia$.pipe(
|
const leaveSoundEffect$ = userMedia$.pipe(
|
||||||
pairwise(),
|
pairwise(),
|
||||||
filter(
|
filter(
|
||||||
@@ -1797,6 +1809,7 @@ export function createCallViewModel$(
|
|||||||
),
|
),
|
||||||
allConnections$,
|
allConnections$,
|
||||||
participantCount$: participantCount$,
|
participantCount$: participantCount$,
|
||||||
|
keyRotationSuppressed$: keyRotationSuppressed$,
|
||||||
handsRaised$: handsRaised$,
|
handsRaised$: handsRaised$,
|
||||||
reactions$: reactions$,
|
reactions$: reactions$,
|
||||||
joinSoundEffect$: joinSoundEffect$,
|
joinSoundEffect$: joinSoundEffect$,
|
||||||
|
|||||||
@@ -121,6 +121,57 @@ describe("LocalMembership", () => {
|
|||||||
expect.objectContaining({ manageMediaKeys: true }),
|
expect.objectContaining({ manageMediaKeys: true }),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("passes keyRotationParticipantLimit from config to joinRTCSession", () => {
|
||||||
|
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: [],
|
||||||
|
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(Object),
|
||||||
|
expect.objectContaining({
|
||||||
|
keyRotationParticipantLimit: 50,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultCreateLocalMemberValues = {
|
const defaultCreateLocalMemberValues = {
|
||||||
|
|||||||
@@ -902,6 +902,8 @@ export function enterRTCSession(
|
|||||||
makeKeyDelay: matrixRtcSessionConfig?.wait_for_key_rotation_ms,
|
makeKeyDelay: matrixRtcSessionConfig?.wait_for_key_rotation_ms,
|
||||||
membershipEventExpiryMs:
|
membershipEventExpiryMs:
|
||||||
matrixRtcSessionConfig?.membership_event_expiry_ms,
|
matrixRtcSessionConfig?.membership_event_expiry_ms,
|
||||||
|
keyRotationParticipantLimit:
|
||||||
|
matrixRtcSessionConfig?.key_rotation_participant_limit,
|
||||||
unstableSendStickyEvents: matrixRTCMode === MatrixRTCMode.Matrix_2_0,
|
unstableSendStickyEvents: matrixRTCMode === MatrixRTCMode.Matrix_2_0,
|
||||||
maximumNetworkErrorRetryCount: maximumNetworkErrorRetryCount,
|
maximumNetworkErrorRetryCount: maximumNetworkErrorRetryCount,
|
||||||
},
|
},
|
||||||
|
|||||||
41
src/state/SessionBehaviors.test.ts
Normal file
41
src/state/SessionBehaviors.test.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
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 { testScope } from "../utils/test";
|
||||||
|
|
||||||
|
describe("SessionBehaviors", () => {
|
||||||
|
describe("createKeyRotationSuppressed$", () => {
|
||||||
|
it("emits initial value from isKeyRotationSuppressed and updates when KeyRotationSuppressedChanged event is emitted", () => {
|
||||||
|
const scope = testScope();
|
||||||
|
const emitter = new EventEmitter();
|
||||||
|
const mockSession = Object.assign(emitter, {
|
||||||
|
on: vi.fn(),
|
||||||
|
off: vi.fn(),
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -88,3 +88,22 @@ export const createMemberships$ = (
|
|||||||
new Epoch(matrixRTCSession.memberships),
|
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<boolean> => {
|
||||||
|
return scope.behavior(
|
||||||
|
fromEvent(
|
||||||
|
matrixRTCSession,
|
||||||
|
MatrixRTCSessionEvent.KeyRotationSuppressedChanged,
|
||||||
|
(suppressed: boolean) => suppressed,
|
||||||
|
),
|
||||||
|
matrixRTCSession.isKeyRotationSuppressed,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ import { MatrixRTCMode } from "../config/ConfigOptions";
|
|||||||
import { createCallFooterViewModel } from "../components/CallFooterViewModel";
|
import { createCallFooterViewModel } from "../components/CallFooterViewModel";
|
||||||
import { type FooterSnapshot } from "../components/CallFooter";
|
import { type FooterSnapshot } from "../components/CallFooter";
|
||||||
import { type ViewModel } from "../state/ViewModel";
|
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" } });
|
mockConfig({ livekit: { livekit_service_url: "https://example.com" } });
|
||||||
|
|
||||||
@@ -140,6 +142,7 @@ export function getBasicCallViewModelEnvironment(
|
|||||||
): {
|
): {
|
||||||
vm: CallViewModel;
|
vm: CallViewModel;
|
||||||
footerVm: ViewModel<FooterSnapshot>;
|
footerVm: ViewModel<FooterSnapshot>;
|
||||||
|
developerSettingsVm: ViewModel<DeveloperSettingsSnapshot>;
|
||||||
rtcMemberships$: BehaviorSubject<CallMembership[]>;
|
rtcMemberships$: BehaviorSubject<CallMembership[]>;
|
||||||
rtcSession: MockRTCSession;
|
rtcSession: MockRTCSession;
|
||||||
handRaisedSubject$: BehaviorSubject<Record<string, RaisedHandInfo>>;
|
handRaisedSubject$: BehaviorSubject<Record<string, RaisedHandInfo>>;
|
||||||
@@ -188,6 +191,7 @@ export function getBasicCallViewModelEnvironment(
|
|||||||
return {
|
return {
|
||||||
vm,
|
vm,
|
||||||
footerVm,
|
footerVm,
|
||||||
|
developerSettingsVm: createDeveloperSettingsTabViewModel(testScope(), vm),
|
||||||
rtcMemberships$,
|
rtcMemberships$,
|
||||||
rtcSession,
|
rtcSession,
|
||||||
handRaisedSubject$: handRaisedSubject$,
|
handRaisedSubject$: handRaisedSubject$,
|
||||||
|
|||||||
@@ -487,6 +487,8 @@ export class MockRTCSession extends TypedEventEmitter<
|
|||||||
return this.joined;
|
return this.joined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isKeyRotationSuppressed = false;
|
||||||
|
|
||||||
public withMemberships(
|
public withMemberships(
|
||||||
rtcMembers$: Behavior<Partial<CallMembership>[]>,
|
rtcMembers$: Behavior<Partial<CallMembership>[]>,
|
||||||
): MockRTCSession {
|
): MockRTCSession {
|
||||||
|
|||||||
Reference in New Issue
Block a user