diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 3f86f093d..7ba5f424a 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -44,9 +44,17 @@ jobs:
flags: unittests
fail_ci_if_error: true
playwright:
- name: Run end-to-end tests
+ name: Run end-to-end tests (${{ matrix.call_implementation }})
timeout-minutes: 60
runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ # Both MatrixRTC implementations, until matrix-rtc becomes the
+ # default and matrix-js-sdk goes (oxidation plan §5.15). The
+ # matrix-rtc run does not block until it has passed once.
+ call_implementation: [matrix-js-sdk, matrix-rtc]
+ continue-on-error: ${{ matrix.call_implementation == 'matrix-rtc' }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
@@ -80,11 +88,12 @@ jobs:
- name: Run Playwright tests
env:
USE_DOCKER: 1
+ CALL_VIEW_MODEL_IMPLEMENTATION: ${{ matrix.call_implementation }}
run: pnpm exec playwright test
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: ${{ !cancelled() }}
with:
- name: html-report
+ name: html-report-${{ matrix.call_implementation }}
path: playwright-report
if-no-files-found: error
retention-days: 4
diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx
index 5b841b829..3f6139968 100644
--- a/.storybook/preview.tsx
+++ b/.storybook/preview.tsx
@@ -10,6 +10,7 @@ import { TooltipProvider } from "@vector-im/compound-web";
import { logger } from "matrix-js-sdk/lib/logger";
import EN from "../locales/en/app.json";
+import { Config } from "../src/config/Config";
import { initReactI18next } from "react-i18next";
import { i18n } from "../src/utils/i18n";
import "../src/index.css";
@@ -35,6 +36,10 @@ i18n
})
.catch((e) => logger.warn("Failed to init i18n for stories", e));
+// Stories run without a config.json; the defaults stand in, as they do for
+// a component host that passes none.
+Config.initWith({});
+
const preview: Preview = {
parameters: {
layout: "centered",
diff --git a/component/index.tsx b/component/index.tsx
index 60e6d4810..840a8a176 100644
--- a/component/index.tsx
+++ b/component/index.tsx
@@ -43,7 +43,7 @@ import {
useRef,
useState,
} from "react";
-import { type MatrixClient } from "matrix-js-sdk";
+import { type MatrixClient, type Room } from "matrix-js-sdk";
import { logger } from "matrix-js-sdk/lib/logger";
import { I18nextProvider } from "react-i18next";
import { TooltipProvider } from "@vector-im/compound-web";
@@ -56,7 +56,6 @@ import LanguageDetector from "i18next-browser-languagedetector";
import EN from "../locales/en/app.json";
import { CallView } from "../src/room/CallView";
import { ErrorPage } from "../src/FullScreenView";
-import { ClientProvider } from "../src/ClientContext";
import { HostBridgeProvider } from "../src/HostBridge";
import { RootElementProvider, useRootElement } from "../src/RootElementContext";
import {
@@ -80,8 +79,15 @@ import { useTheme } from "../src/useTheme";
import { useStableValue } from "../src/useStableValue";
import { type RtcMatrixDriver } from "../src/driver/RtcMatrixDriver";
import { type ElementCallMatrixClientDriver } from "../src/driver/ElementCallMatrixClientDriver";
-import { JsSdkRtcMatrixDriver } from "../src/driver/jsSdk/JsSdkRtcMatrixDriver";
-import { JsSdkElementCallMatrixClientDriver } from "../src/driver/jsSdk/JsSdkElementCallMatrixClientDriver";
+import { useJsSdkDrivers } from "../src/driver/jsSdk/useJsSdkDrivers";
+import {
+ initMatrixRtcSdk,
+ type MatrixRtcWasmSource,
+} from "../src/matrix-rtc-sdk";
+import {
+ type MatrixDrivers,
+ MatrixDriverProvider,
+} from "../src/driver/MatrixDriverContext";
import styles from "./ElementCall.module.css";
import {
type ElementCallHandle,
@@ -231,10 +237,24 @@ export type ElementCallClientBasedProps = Omit<
*
* Await this once, before rendering {@link ElementCall}.
*/
+export interface InitializeElementCallOptions {
+ /**
+ * Where to load the MatrixRTC SDK's wasm from, for a host that serves it
+ * from somewhere other than next to this bundle. Left out, the bundled
+ * copy is loaded when the first call needs it.
+ */
+ matrixRtcWasm?: MatrixRtcWasmSource;
+}
+
export async function initializeElementCall(
config: ConfigOptions = {},
+ { matrixRtcWasm }: InitializeElementCallOptions = {},
): Promise {
const polyfills: Promise[] = [];
+ // The call runs on the Rust MatrixRTC crate; its wasm is loaded up front
+ // when the host says where from, and lazily otherwise.
+ if (matrixRtcWasm !== undefined)
+ polyfills.push(initMatrixRtcSdk(matrixRtcWasm));
if (shouldPolyfillSegmenter())
polyfills.push(import("@formatjs/intl-segmenter/polyfill-force"));
if (shouldPolyfillDurationFormat())
@@ -343,22 +363,11 @@ export const ElementCall: FC = ({
};
}, [controlledAudioDevices, callIntent]);
- // Until the React tree below reads the drivers (plan slice S4), it runs on
- // matrix-js-sdk directly, so for now the drivers have to be the matrix-js-sdk
- // ones and the client is taken back out of them. `ElementCallClientBased`
- // is the way to get here in the meantime; a host with drivers of its own
- // cannot be served yet.
- if (!(clientDriver instanceof JsSdkElementCallMatrixClientDriver))
- throw new Error(
- "Element Call cannot yet run on drivers other than the matrix-js-sdk ones; use ElementCallClientBased",
- );
- // `rtcDriver` is what the call will run on once the tree reads it; for now
- // it is only checked to be there.
- void rtcDriver;
- const { client, room } = clientDriver;
- const rtcSession = useMemo(
- () => client.matrixRTC.getRoomSession(room),
- [client, room],
+ // The call runs on the Rust MatrixRTC crate over these two drivers; no
+ // matrix-js-sdk client is involved.
+ const drivers = useMemo(
+ (): MatrixDrivers => ({ rtcDriver, clientDriver }),
+ [rtcDriver, clientDriver],
);
// Everything the call needs is in hand once these exist, and the first
@@ -366,8 +375,7 @@ export const ElementCall: FC = ({
// is told that Element Call has loaded, as the widget tells its client once
// its own initialisation is over. Once per mount, however often the pieces
// are later swapped out.
- const ready =
- container !== null && rtcSession !== null && mediaDevices !== null;
+ const ready = container !== null && mediaDevices !== null;
const announcedLoaded = useRef(false);
useEffect(() => {
if (!ready || announcedLoaded.current) return;
@@ -393,12 +401,10 @@ export const ElementCall: FC = ({
>
-
+
= ({
/>
-
+
@@ -425,28 +431,19 @@ export const ElementCallClientBased: FC = ({
...props
}): ReactNode => {
const room = client.getRoom(roomId);
- const drivers = useMemo(
- () =>
- room === null
- ? null
- : {
- rtcDriver: new JsSdkRtcMatrixDriver(client, room),
- clientDriver: new JsSdkElementCallMatrixClientDriver(client, room),
- },
- [client, room],
- );
- // The RTC driver hooks client listeners for the crate's sinks; let go of
- // them with the drivers.
- useEffect(() => {
- if (drivers === null) return;
- return (): void => drivers.rtcDriver.detach();
- }, [drivers]);
-
- if (drivers === null) {
+ if (room === null) {
logger.error(
`Element Call was asked to call in ${roomId}, which its host's client does not know about`,
);
return null;
}
+ return ;
+};
+
+/** {@link ElementCallClientBased} once the room is known: builds the drivers. */
+const ClientBasedCall: FC<
+ Omit & { room: Room }
+> = ({ client, room, ...props }): ReactNode => {
+ const drivers = useJsSdkDrivers(client, room);
return ;
};
diff --git a/element-call-oxidation-plan.md b/element-call-oxidation-plan.md
index a1d51ac2c..df5aaf74f 100644
--- a/element-call-oxidation-plan.md
+++ b/element-call-oxidation-plan.md
@@ -175,6 +175,7 @@ membership, roster, encryption, impairments} | Leaving`.
| C11 | No way to change `application["m.call.intent"]` while joined; Element Call flips it between `audio` and `video` when the camera is toggled (`updateCallIntent`). | `update_application(intent)` on the own-membership manager, facade and FFI: while connected the membership is re-published at once on the refresh path (a failure retries like a refresh); during a join the join event carries it; refused with `NotJoined` otherwise. |
| C12 | Homeserver connectivity lived only in Element Call's driver; the crate could not tell a dead homeserver from a quiet one, and a participation's status said nothing about it. | **Done.** `ConnectivityDriver` (`is_homeserver_connected`, `subscribe_connectivity`) joins the `MatrixDriver` sum; the FFI adds `ConnectivitySink`, the two callback methods and `FfiParticipationManager.is_homeserver_connected()`; the facade pump consumes the stream and reports `Impairment::HomeserverUnreachable { since_ts }` (Critical, sorted first) in every non-disconnected status until the driver reports the homeserver back. The web-test-app mock and js-sdk driver implement it. A matrix-rust-sdk adapter implements the same two methods later. |
| C13 | `session()` moved (seed done, slot opened by somebody else) without any listener firing, so a host's `session$` stayed stale until a membership or status change happened to refresh it. Found by the real-backend check. | **Done.** `SessionListener` / `set_session_listener` on the FFI manager (`on_session_change` on the facade), fired publish-on-change from `refresh_outputs`; `CallParticipation.session$` is fed from it. |
+| C14 | The crate logs through the `log` facade (104 call sites) but installed no logger, so in wasm every line was dropped: seeding, joins, delegation fallbacks and key rotation left no trace in a rageshake. | **Done (2026-09-16).** `LogSink` foreign trait (`log(level, target, message)`), `FfiLogLevel`, and `set_log_sink(sink, max_level)`, which installs a `log::Log` forwarding to the sink (a host that already installed a Rust logger keeps it). Element Call installs `matrixRtcLogSink` under `[matrix-rtc]` on the js-sdk root logger from `initMatrixRtcSdk()` at debug; the web-test-app installs a console sink (warn in tests). |
No crate work is deferred: `update_application` is C11.
@@ -512,8 +513,11 @@ RateLimited`, 403 → `Rejected`, 404/`M_UNRECOGNIZED` → `Unsupported`.
`assetsInlineLimit`; `?url&no-inline` emits a file. App builds use `?url`;
the component build uses `?url&no-inline` plus an `exports` entry for
`./dist/assets/*`, and `initializeElementCall(config, { matrixRtcWasm })`
- lets a host point elsewhere. vitest reads the file from disk; wasm boot is
- **lazy** (only suites that need it call `initMatrixRtcSdk()`), never in
+ lets a host point elsewhere. Wasm boot is **lazy** everywhere: in the app
+ `useCallParticipation` awaits `initMatrixRtcSdk()` before constructing
+ the participation (so the js-sdk path never fetches it, §5.15), not the
+ `Initializer`; vitest reads the file from disk and only suites that need
+ it call `initMatrixRtcSdk()`, never in
`src/vitest.setup.ts`; Storybook boots it in `.storybook/preview.tsx`
`beforeAll`. Suites using `vi.useFakeTimers` never share a file with
real-wasm tests (pumps sleep on `setTimeout`).
@@ -557,6 +561,24 @@ RateLimited`, 403 → `Rejected`, 404/`M_UNRECOGNIZED` → `Unsupported`.
redactions), as they are with the js-sdk MatrixRTC code today. The driver
decrypts them on the way in; the sticky marker (`msc4354_sticky`) stays in
the clear. To cross-check against Element X before relying on it.
+15. **The two view models stay switchable for one release.** A developer
+ setting, `callViewModelImplementation` (`"matrix-js-sdk"` |
+ `"matrix-rtc"`, `src/settings/settings.ts`, next to `matrixRTCMode`),
+ picks between `createJsClientCallViewModel$` and the driver-based
+ `createCallViewModel$` in the same build. Like `matrixRTCMode` it is
+ shown in the Developer Settings tab, sampled when the call is joined
+ (switching mid-call needs a rejoin), and a deployment can pin it through
+ `config.json` (`call_view_model_implementation`), which overrides the
+ user's choice and greys the control out. The default is
+ `"matrix-js-sdk"` when the setting lands (S4a) and flips to
+ `"matrix-rtc"` once the S5 gate is green; S6 removes the setting together
+ with the js-sdk path. The point is that a broken call can be compared
+ against the old path in the same session, and that Playwright can run
+ both paths from one build. Everything the js-sdk path needs (the client,
+ the `MatrixRTCSession`, `ReactionsReader` over the session) therefore
+ stays reachable from `CallView` until S6, and the `CallParticipation` is
+ created only when the crate path is selected, so neither path pays for
+ the other.
---
@@ -663,19 +685,116 @@ participation, clientDriver, …)` sits next to it; both build a
LiveKit: join → connection → own tile → peer → leave; transport-missing →
`fatalError$`), plus one file per module.
-### S4 — React tree, two slices ☐
+### S4 — React tree, two slices ☑ (2026-09-16)
+- **S4a-0, the switch (§5.15) ☑:** `CallViewModelImplementation` enum,
+ `callViewModelImplementation` setting (`src/settings/settings.ts`),
+ `call_view_model_implementation` pin (`ConfigOptions.ts`, validated in
+ `Config.ts`), the radio group in `DeveloperSettingsTab.tsx`, the
+ implementation logged at join and sent as `call_view_model_implementation`
+ in rageshakes, `effectiveCallViewModelImplementation()` in
+ `src/state/rtc/implementation.ts`. `CallView` samples it once per mount;
+ with `matrix-rtc` and no drivers it warns and lets matrix-js-sdk carry the
+ call. Still open from this item: the Playwright helper and the second CI
+ run.
+- **S4a done so far:** `MatrixDriverProvider` / `useMatrixDrivers()`
+ (`src/driver/MatrixDriverContext.tsx`), provided by the component from its
+ props and by `RoomPage` through `useJsSdkDrivers(client, room)`;
+ `useCallParticipation(drivers, config)` owned by `CallView`, created only
+ when the crate path is selected; `ActiveCall` takes `participation` and
+ builds either view model; the lobby's member count, the big-call auto-mute
+ and the error boundary's "were we joined" read from whichever side carries
+ the call; `window.matrixRtc = { participation }` next to `window.rtcSession`.
+- (The original S4a-0 description follows for reference.) `callViewModelImplementation` setting and
+ `config.json` pin (`ConfigOptions.ts`, validated at load like
+ `matrix_rtc_mode`); a radio group in `DeveloperSettingsTab.tsx` beside the
+ MatrixRTC mode; `ActiveCall` reads the sampled value and calls either
+ factory — for `"matrix-rtc"` it takes the drivers from the
+ `MatrixDriverProvider` and the `CallParticipation` from `CallView`, for
+ `"matrix-js-sdk"` it keeps `rtcSession`/`matrixRoom` as today. The chosen
+ implementation is logged at join and added to the rageshake fields so a
+ report says which path it came from. Playwright gets a helper that sets
+ the setting (local storage) before the call so every call spec can run
+ under both values; CI runs the suite twice until the default flips.
+ Lands first in S4a, before anything else in the tree moves, so that every
+ later S4 change is verifiable against the old path.
- **S4a** views/hooks/settings on the driver: `CallView.tsx` (owns
- `CallParticipation`), `InCallView.tsx`, `LobbyView.tsx`, `CallEndedView.tsx`,
+ `CallParticipation` when the crate path is selected), `InCallView.tsx`, `LobbyView.tsx`, `CallEndedView.tsx`,
`VideoPreview.tsx`, `useRoomInfo()` (replaces `useRoomName/Avatar/JoinRule/State`),
`InviteModal.tsx`, `Avatar.tsx`, `useOwnProfile.ts`, `ProfileSettingsTab.tsx`,
`SettingsModal.tsx`, `DeveloperSettingsTab.tsx`, `submit-rageshake.ts`,
`DisconnectedBanner.tsx`, `analytics/PosthogEvents.ts`, `controls.ts`, and a
first `CallView.stories.tsx` (lobby, in call, ended) driven by
`MockMatrixDriver`.
-- **S4b** reactions and notifications: `useReactionsSender.tsx`,
- `ReactionsReader` keyed by `memberId`, `CallNotificationLifecycle` sending
- through the driver; tests incl. a membership re-send mid-call.
+- **S4b ☑ (2026-09-16):** `ReactionsSenderProvider` takes `ownIdentifier`,
+ `ownMembershipEventId` and a `ReactionsTimeline` (`jsSdkReactionsTimeline`
+ over a client, or the client driver); `ParticipationReactionsReader`
+ (`src/reactions/`) reads hands and reactions from the participation and
+ the timeline driver, keyed by the member's media id
+ (`memberMediaId`, `src/state/rtc/mediaId.ts`), and re-resolves a hand on a
+ re-sent membership instead of dropping it blindly. Notifications went
+ through the driver in S3d. Tests: `ParticipationReactionsReader.test.ts`,
+ `useCallParticipation.test.tsx`, three `CallView.test.tsx` cases for the
+ switch.
+- **S4a views on the drivers (2026-09-16):** `CallView` now requires the
+ drivers (`useMatrixDrivers()`; both hosts provide them) and reads the
+ room through `useRoomInfo()` (`src/room/useRoomInfo.ts`; `useRoomAvatar`,
+ `useJoinRule`, `useRoomState` deleted, `useRoomName` stays for `RoomPage`),
+ our own profile through `useOwnProfile()` (`src/profile/useOwnProfile.ts`)
+ and the encryption system through `useEncryptionSystemFor(roomId,
+roomInfo.encrypted)` (`useRoomEncryptionSystem` keeps the client for the
+ home page). `MatrixInfo` is built from those. `InviteModal` takes
+ `roomId`/`roomName`/`e2eeSystem`; `CallEndedView` lost its `client` prop;
+ `Avatar` resolves thumbnails through `clientDriver.thumbnailUrl` when
+ drivers are provided (host `downloadMedia` first, client fallback for the
+ shell); rageshakes carry the driver's `getDiagnostics()` as `driver_*`
+ fields and the crate's `matrix_rtc_snapshot`.
+- **The component is client-free (2026-09-16):** `CallView`'s `client` and
+ `rtcSession` are optional; without them the crate carries the call
+ whatever the setting says (`useMatrixRtc = setting || no session`), and
+ every js-sdk-only piece (the `MembershipManagerError` listener, the room
+ sanity check, `ReactionsReader`, `mediaKeyStatisticsOf`) is skipped.
+ `ActiveCall`/`InCallView` take `roomId` instead of `matrixRoom`, and
+ `client`/`rtcSession` optionally. `ProfileSettingsTab` edits through the
+ client driver's optional `setDisplayName`/`setAvatar(file | null)` and
+ shows read-only fields without them; `DeveloperSettingsTab` reads the
+ sticky probe from `getCapabilities()`, the crypto version from
+ `getDiagnostics()` and validates a custom LiveKit URL through
+ `rtcDriver.getLivekitToken` when there is no client; the rageshake request
+ event goes through the client driver's timeline; reactions are supported
+ unless a client state forbids them. `ElementCall` renders no
+ `ClientProvider` and no shim — it hands its two drivers down and nothing
+ below asks for a client; `initializeElementCall(config, { matrixRtcWasm })`
+ can preload the wasm. `PosthogEvents.eventCallEnded.track` takes
+ `MediaKeyStatistics` (from the session on the js-sdk path, zero on the
+ crate path until the crate counts). `useTypedEventEmitter` and the js-sdk
+ client driver's public `client`/`room` are gone. **Consequence:** the
+ component and its dev harness (`ElementCallClientBased`) now run every
+ call on the crate.
+- **Banner on the driver (2026-09-16):** `useHomeserverConnected(drivers,
+graceMs)` (`src/driver/`) follows `rtcDriver.isHomeserverConnected()` /
+ `subscribeConnectivity` and reports a lapse only after
+ `sync_disconnect_grace_period_ms`, since the driver reports every sync
+ hiccup where the client state waited for a `ConnectionError`;
+ `DisconnectedBanner` uses it whenever drivers are provided and falls back
+ to the client state for the shell. The mock RTC driver keeps several
+ connectivity sinks (the crate's and the UI's).
+- **S4 closed (2026-09-16):** `CallView.stories.tsx` (Lobby with a peer,
+ NoTransport as the error path, Ended) over the mock drivers, no client;
+ `.storybook/preview.tsx` initialises the config; the Storybook vitest
+ project passes. The Playwright switch is `CALL_VIEW_MODEL_IMPLEMENTATION`
+ read by `playwright.config.ts` into `use.storageState` (the developer
+ setting in local storage for every context), and the CI Playwright job is
+ a matrix over both implementations (`matrix-rtc` non-blocking until it
+ has passed once). `CallParticipation.mediaKeyStatistics()` counts sent
+ and received keys and their age from the crate's key changes for the
+ ended-call event. Found on the way: `CallView` rendered `ActiveCall`
+ before the participation existed (wasm loads on first use) — it now waits;
+ and `onLeft` had gained the member count as a dependency, which rebuilt
+ the view model on every roster change on both paths — read through
+ `useLatest` now. `create-call.spec.ts` passes in Chromium on both
+ implementations with two view model creations each (StrictMode): the
+ crate path's first full call in a browser.
### S5 — hosts ☐ (component props done 2026-09-15)
@@ -708,6 +827,9 @@ participation, clientDriver, …)` sits next to it; both build a
`src/IndexedDBWorker.ts`, `src/room/KnockLobbyView.tsx`, `src/settings/rageshake.ts`)
banning `matrix-js-sdk` except `matrix-js-sdk/lib/logger`.
- `ServiceInterruptionsViewModel` fed from `status$.impairments`.
+- `callViewModelImplementation` setting, its `config.json` pin and the
+ Developer Settings control removed with `createJsClientCallViewModel$`
+ (§5.15); Playwright runs the suite once again.
- `docs/agents/architecture.md`, `docs/matrix_rtc_modes.md` updated;
`src/@types/matrix-js-sdk.d.ts` removed if nothing merges into js-sdk types.
@@ -807,3 +929,35 @@ props moved to the two drivers with `ElementCallClientBased` on top. Gates:
participation (S4b), the `?url&no-inline` wasm asset for the component build
(the wasm is loaded lazily and inlined into the component bundle today), and
the S6 deletions.
+
+**S4 (2026-09-16):** the implementation switch (§5.15) is in with its
+config pin, developer control and rageshake field; `CallView` owns a
+`CallParticipation` when the crate path is selected and `ActiveCall` builds
+the matching view model; the reactions reader and sender have participation
+and driver counterparts. Both paths run in the same build. Gates: `pnpm
+lint`, `format:check`, `i18n:check`, `test:unit` (815), `build:component`
+green. Not verified: a real call on the crate path through the UI (the
+`pnpm backend` check covers the view model's Matrix side; the browser run is
+the S5 gate).
+
+**Logging (2026-09-16):** C14 — the crate's log lines reach the host through
+a `LogSink`; Element Call routes them to its rageshake logger under
+`[matrix-rtc]`, the web-test-app to the console. Verified by a unit test that
+sees the crate's "session created" line through the sink.
+
+**S4 views (2026-09-16):** room info, own profile, encryption system,
+avatars, the invite and ended views and the rageshake fields read the
+drivers; `CallView` requires drivers. Three room hooks deleted. Gates:
+`pnpm lint`, `format:check`, `test:unit` (819), `build:component` green.
+
+**Client-free component (2026-09-16):** `CallView` and everything under it
+run without a matrix-js-sdk client on the crate path; the component passes
+only its drivers. Gates: `pnpm lint`, `format:check`, `test:unit` (819),
+`build:component` green. Not yet done: a browser run of the component on
+the crate path (the dev harness now is that run).
+
+**S4 done (2026-09-16):** stories, Playwright switch and CI matrix, key
+statistics. `create-call.spec.ts` green on `matrix-js-sdk` and `matrix-rtc`
+in Chromium against the dev server; the rest of the suite under
+`matrix-rtc` is the S5 gate. Gates: `pnpm lint`, `format:check`,
+`test:unit` (822), `test:storybook` (28), `build:component` green.
diff --git a/locales/en/app.json b/locales/en/app.json
index 7b1775be5..6b9909d40 100644
--- a/locales/en/app.json
+++ b/locales/en/app.json
@@ -58,6 +58,18 @@
},
"developer_mode": {
"always_show_iphone_earpiece": "Show iPhone earpiece option on all platforms",
+ "callViewModelImplementation": {
+ "forced": "Your deployment pins the call implementation.",
+ "matrix_js_sdk": {
+ "description": "The MatrixRTC session of matrix-js-sdk carries the call, as before",
+ "label": "matrix-js-sdk"
+ },
+ "matrix_rtc": {
+ "description": "The Rust matrix-rtc crate carries the call through the host's drivers",
+ "label": "matrix-rtc (Rust)"
+ },
+ "title": "Call implementation"
+ },
"crypto_version": "Crypto version: {{version}}",
"custom_livekit_url": {
"current_url": "Currently set to: ",
diff --git a/playwright.config.ts b/playwright.config.ts
index e6dcc5249..7f557e083 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -19,6 +19,32 @@ const baseURL = process.env.USE_DOCKER
const __dirname = path.dirname(fileURLToPath(import.meta.url));
+/**
+ * Which MatrixRTC implementation carries the calls under test, `matrix-js-sdk`
+ * or `matrix-rtc` (see `CallViewModelImplementation` in
+ * src/config/ConfigOptions.ts). Unset, Element Call's default applies. Set,
+ * every browser context starts with the developer setting in local storage,
+ * so the whole suite runs on that implementation.
+ */
+const callViewModelImplementation = process.env.CALL_VIEW_MODEL_IMPLEMENTATION;
+const storageState =
+ callViewModelImplementation === undefined
+ ? undefined
+ : {
+ cookies: [],
+ origins: [
+ {
+ origin: baseURL,
+ localStorage: [
+ {
+ name: "matrix-setting-call-view-model-implementation",
+ value: JSON.stringify(callViewModelImplementation),
+ },
+ ],
+ },
+ ],
+ };
+
// Needed by the synapse admin API called in fixtures
process.env.NODE_EXTRA_CA_CERTS = join(
__dirname,
@@ -44,6 +70,7 @@ export default defineConfig({
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL,
+ storageState,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
diff --git a/src/Avatar.tsx b/src/Avatar.tsx
index 185ae97b4..a34948f70 100644
--- a/src/Avatar.tsx
+++ b/src/Avatar.tsx
@@ -16,6 +16,7 @@ import { Avatar as CompoundAvatar } from "@vector-im/compound-web";
import { type MatrixClient } from "matrix-js-sdk";
import { useClientState } from "./ClientContext";
+import { useOptionalMatrixDrivers } from "./driver/MatrixDriverContext";
import { useHostBridge } from "./HostBridge";
export enum Size {
@@ -76,6 +77,9 @@ export const Avatar: FC = ({
}) => {
const clientState = useClientState();
const hostBridge = useHostBridge();
+ // Under a call the host's client driver resolves media; the shell outside
+ // a call still has only the client.
+ const drivers = useOptionalMatrixDrivers();
const sizePx = useMemo(
() =>
@@ -95,16 +99,30 @@ export const Avatar: FC = ({
return;
}
- let blob: Promise;
+ let url: Promise;
if (hostBridge.downloadMedia) {
- blob = hostBridge.downloadMedia(src);
+ url = hostBridge
+ .downloadMedia(src)
+ .then((blob) => URL.createObjectURL(blob));
+ } else if (drivers !== null) {
+ const px = Math.floor(sizePx * window.devicePixelRatio);
+ url = drivers.clientDriver.thumbnailUrl(
+ src,
+ px,
+ px,
+ sizePx <= 96 ? "crop" : "scale",
+ );
} else if (
clientState?.state === "valid" &&
clientState.authenticated?.client &&
sizePx
) {
- blob = getAvatarFromServer(clientState.authenticated.client, src, sizePx);
+ url = getAvatarFromServer(
+ clientState.authenticated.client,
+ src,
+ sizePx,
+ ).then((blob) => URL.createObjectURL(blob));
} else {
setAvatarUrl(undefined);
return;
@@ -112,13 +130,14 @@ export const Avatar: FC = ({
let objectUrl: string | undefined;
let stale = false;
- blob
- .then((blob) => {
- if (stale) {
+ url
+ .then((resolved) => {
+ if (stale || resolved === null) {
return;
}
- objectUrl = URL.createObjectURL(blob);
- setAvatarUrl(objectUrl);
+ // Only what we created (or the driver created for us) is ours to revoke.
+ if (resolved.startsWith("blob:")) objectUrl = resolved;
+ setAvatarUrl(resolved);
})
.catch((ex) => {
if (stale) {
@@ -133,7 +152,7 @@ export const Avatar: FC = ({
URL.revokeObjectURL(objectUrl);
}
};
- }, [clientState, hostBridge, src, sizePx]);
+ }, [clientState, hostBridge, drivers, src, sizePx]);
return (
{
children?: ReactNode;
@@ -23,10 +25,16 @@ export const DisconnectedBanner: FC = ({
...rest
}) => {
const { t } = useTranslation();
+ // Under a call the host's RTC driver says whether the homeserver is
+ // reachable; the shell outside a call only has the client's sync state.
+ const drivers = useOptionalMatrixDrivers();
+ const homeserverConnected = useHomeserverConnected(drivers);
const clientState = useClientState();
let shouldShowBanner = false;
- if (clientState?.state === "valid") {
+ if (drivers !== null) {
+ shouldShowBanner = !homeserverConnected;
+ } else if (clientState?.state === "valid") {
const validClientState = clientState as ValidClientState;
shouldShowBanner = validClientState.disconnected;
}
diff --git a/src/analytics/PosthogEvents.test.ts b/src/analytics/PosthogEvents.test.ts
index 83ef4d7c7..da5abf6e8 100644
--- a/src/analytics/PosthogEvents.test.ts
+++ b/src/analytics/PosthogEvents.test.ts
@@ -19,6 +19,7 @@ import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
import { PosthogAnalytics } from "./PosthogAnalytics";
import {
+ mediaKeyStatisticsOf,
CallEndedTracker,
CallReconnectingTracker,
type CallReconnectingReason,
@@ -67,7 +68,7 @@ describe("CallEnded", () => {
const tracker = new CallEndedTracker();
const mockSession = createMockRtcSession();
- tracker.track("test-call-id", 2, false, mockSession);
+ tracker.track("test-call-id", 2, false, mediaKeyStatisticsOf(mockSession));
expect(warnSpy).toHaveBeenCalledWith(
"[PosthogEvents] Failed to send posthog callEnded event due to missing startTime",
@@ -81,7 +82,7 @@ describe("CallEnded", () => {
tracker.cacheStartCall(new Date(Date.now() - 60000));
tracker.cacheParticipantCountChanged(5);
- tracker.track("test-call-id", 3, true, mockSession);
+ tracker.track("test-call-id", 3, true, mediaKeyStatisticsOf(mockSession));
expect(PosthogAnalytics.instance.trackEvent).toHaveBeenCalledWith(
{
@@ -111,7 +112,7 @@ describe("CallEnded", () => {
tracker.cacheParticipantCountChanged(3);
tracker.cacheParticipantCountChanged(7);
tracker.cacheParticipantCountChanged(2);
- tracker.track("test-call-id", 1, false, mockSession);
+ tracker.track("test-call-id", 1, false, mediaKeyStatisticsOf(mockSession));
expect(PosthogAnalytics.instance.trackEvent).toHaveBeenCalledWith(
expect.objectContaining({
@@ -128,7 +129,7 @@ describe("CallEnded", () => {
});
tracker.cacheStartCall(new Date());
- tracker.track("test-call-id", 1, false, mockSession);
+ tracker.track("test-call-id", 1, false, mediaKeyStatisticsOf(mockSession));
expect(PosthogAnalytics.instance.trackEvent).toHaveBeenCalledWith(
expect.objectContaining({
@@ -146,7 +147,7 @@ describe("CallEnded", () => {
});
tracker.cacheStartCall(new Date());
- tracker.track("test-call-id", 1, false, mockSession);
+ tracker.track("test-call-id", 1, false, mediaKeyStatisticsOf(mockSession));
expect(PosthogAnalytics.instance.trackEvent).toHaveBeenCalledWith(
expect.objectContaining({
@@ -161,7 +162,7 @@ describe("CallEnded", () => {
const mockSession = createMockRtcSession();
tracker.cacheStartCall(new Date());
- tracker.track("test-call-id", 1, false, mockSession);
+ tracker.track("test-call-id", 1, false, mediaKeyStatisticsOf(mockSession));
expect(PosthogAnalytics.instance.trackEvent).toHaveBeenCalledWith(
expect.anything(),
@@ -178,7 +179,7 @@ describe("CallEnded", () => {
tracker.cacheReconnecting("sync");
tracker.cacheReconnecting("livekit");
tracker.cacheReconnecting("membership");
- tracker.track("test-call-id", 1, false, mockSession);
+ tracker.track("test-call-id", 1, false, mediaKeyStatisticsOf(mockSession));
expect(PosthogAnalytics.instance.trackEvent).toHaveBeenCalledWith(
expect.objectContaining({
diff --git a/src/analytics/PosthogEvents.ts b/src/analytics/PosthogEvents.ts
index 56ca08af4..9ea9b1825 100644
--- a/src/analytics/PosthogEvents.ts
+++ b/src/analytics/PosthogEvents.ts
@@ -15,6 +15,34 @@ import {
RegistrationType,
} from "./PosthogAnalytics";
+/** How many media keys went out and came in over a call, for the ended event. */
+export interface MediaKeyStatistics {
+ sent: number;
+ received: number;
+ /** Sum of the ages of the received keys, in ms; averaged on send. */
+ receivedTotalAge: number;
+}
+
+/** No key statistics (the Rust crate does not count them yet). */
+export const NO_MEDIA_KEY_STATISTICS: MediaKeyStatistics = {
+ sent: 0,
+ received: 0,
+ receivedTotalAge: 0,
+};
+
+/** {@link MediaKeyStatistics} from a matrix-js-sdk session. */
+export function mediaKeyStatisticsOf(
+ rtcSession: MatrixRTCSession,
+): MediaKeyStatistics {
+ const { counters, totals } = rtcSession.statistics;
+ return {
+ sent: counters.roomEventEncryptionKeysSent,
+ received: counters.roomEventEncryptionKeysReceived,
+ // Only meaningful with received keys; a mocked session may not carry it.
+ receivedTotalAge: totals?.roomEventEncryptionKeysReceivedTotalAge ?? 0,
+ };
+}
+
interface CallEnded extends IPosthogEvent {
eventName: "CallEnded";
// the callId posthog key is essentially a Matrix roomId
@@ -80,7 +108,7 @@ export class CallEndedTracker {
callId: string,
callParticipantsNow: number,
sendInstantly: boolean,
- rtcSession: MatrixRTCSession,
+ keys: MediaKeyStatistics,
): void {
if (this.cache.startTime) {
PosthogAnalytics.instance.trackEvent(
@@ -90,16 +118,10 @@ export class CallEndedTracker {
callParticipantsMax: this.cache.maxParticipantsCount,
callParticipantsOnLeave: callParticipantsNow,
callDuration: (Date.now() - this.cache.startTime.getTime()) / 1000,
- roomEventEncryptionKeysSent:
- rtcSession.statistics.counters.roomEventEncryptionKeysSent,
- roomEventEncryptionKeysReceived:
- rtcSession.statistics.counters.roomEventEncryptionKeysReceived,
+ roomEventEncryptionKeysSent: keys.sent,
+ roomEventEncryptionKeysReceived: keys.received,
roomEventEncryptionKeysReceivedAverageAge:
- rtcSession.statistics.counters.roomEventEncryptionKeysReceived > 0
- ? rtcSession.statistics.totals
- .roomEventEncryptionKeysReceivedTotalAge /
- rtcSession.statistics.counters.roomEventEncryptionKeysReceived
- : 0,
+ keys.received > 0 ? keys.receivedTotalAge / keys.received : 0,
callReconnectingCount: this.cache.reconnectingCount,
callReconnectingCountSync: this.cache.reconnectingCountByReason.sync,
callReconnectingCountMembership:
diff --git a/src/button/ReactionToggleButton.test.tsx b/src/button/ReactionToggleButton.test.tsx
index 5c8d375cb..e8562f596 100644
--- a/src/button/ReactionToggleButton.test.tsx
+++ b/src/button/ReactionToggleButton.test.tsx
@@ -17,7 +17,10 @@ import { type CallViewModel } from "../state/CallViewModel/CallViewModel";
import { getBasicCallViewModelEnvironment } from "../utils/test-viewmodel";
import { alice, local, localRtcMember } from "../utils/test-fixtures";
import { type MockRTCSession } from "../utils/test";
-import { ReactionsSenderProvider } from "../reactions/useReactionsSender";
+import {
+ jsSdkReactionsTimeline,
+ ReactionsSenderProvider,
+} from "../reactions/useReactionsSender";
import { initializeWidget } from "../widget";
initializeWidget();
vi.mock("livekit-client/e2ee-worker?worker");
@@ -35,7 +38,12 @@ function TestComponent({
{
afterEach(() => {
@@ -101,3 +105,21 @@ describe("Config.initWith", () => {
expect(Config.get().ssla).toBe("https://second.invalid/ssla");
});
});
+
+describe("validateConfig call_view_model_implementation", () => {
+ it.each(Object.values(CallViewModelImplementation))(
+ "keeps a valid call_view_model_implementation value (%s)",
+ (value) => {
+ const result = validateConfig({ call_view_model_implementation: value });
+ expect(result.call_view_model_implementation).toBe(value);
+ },
+ );
+
+ it("drops an unknown call_view_model_implementation value", () => {
+ const result = validateConfig({
+ call_view_model_implementation:
+ "yes-please" as unknown as CallViewModelImplementation,
+ });
+ expect(result.call_view_model_implementation).toBeUndefined();
+ });
+});
diff --git a/src/config/Config.ts b/src/config/Config.ts
index e2c5eb8fa..5a2cda8ca 100644
--- a/src/config/Config.ts
+++ b/src/config/Config.ts
@@ -15,11 +15,14 @@ import {
type ResolvedConfigOptions,
} from "./ConfigOptions";
import { isFailure } from "../utils/fetch";
-import { MatrixRTCMode } from "./ConfigOptions";
+import { CallViewModelImplementation, MatrixRTCMode } from "./ConfigOptions";
const VALID_MATRIX_RTC_MODES: ReadonlySet = new Set(
Object.values(MatrixRTCMode),
);
+const VALID_CALL_VIEW_MODEL_IMPLEMENTATIONS: ReadonlySet = new Set(
+ Object.values(CallViewModelImplementation),
+);
export class Config {
private static internalInstance: Config | undefined;
@@ -136,6 +139,16 @@ export function validateConfig(config: ConfigOptions): ConfigOptions {
);
delete config.matrix_rtc_mode;
}
+ const implementation = config.call_view_model_implementation;
+ if (
+ implementation !== undefined &&
+ !VALID_CALL_VIEW_MODEL_IMPLEMENTATIONS.has(implementation)
+ ) {
+ logger.warn(
+ `Ignoring invalid call_view_model_implementation in config.json: ${String(implementation)}`,
+ );
+ delete config.call_view_model_implementation;
+ }
return config;
}
diff --git a/src/config/ConfigOptions.ts b/src/config/ConfigOptions.ts
index 872e35168..10a176f64 100644
--- a/src/config/ConfigOptions.ts
+++ b/src/config/ConfigOptions.ts
@@ -24,6 +24,18 @@ export enum MatrixRTCMode {
Matrix_2_0 = "matrix_2_0",
}
+/**
+ * Which MatrixRTC implementation carries a call: matrix-js-sdk's
+ * `MatrixRTCSession`, or the Rust `matrix-rtc` crate through the host's
+ * drivers. Both are in the build while the crate path is being proven, so a
+ * broken call can be compared against the other path in the same session;
+ * the js-sdk path and this choice go away together.
+ */
+export enum CallViewModelImplementation {
+ MatrixJsSdk = "matrix-js-sdk",
+ MatrixRtc = "matrix-rtc",
+}
+
export interface DelayedLeaveTimings {
/**
* The delay (in milliseconds) with which delayed leave events are sent.
@@ -202,6 +214,14 @@ export interface ConfigOptions {
*/
matrix_rtc_mode?: MatrixRTCMode;
+ /**
+ * Pins the {@link CallViewModelImplementation} for all clients on this
+ * deployment, overriding any per-user choice from the Developer Settings.
+ * If unset, the user's Developer Settings choice (or its default) wins.
+ * Temporary: goes away with the matrix-js-sdk implementation.
+ */
+ call_view_model_implementation?: CallViewModelImplementation;
+
/**
* These are low level options that are used to configure the MatrixRTC session.
* Take care when changing these options.
diff --git a/src/driver/ElementCallMatrixClientDriver.ts b/src/driver/ElementCallMatrixClientDriver.ts
index bc3a5c480..7448b9202 100644
--- a/src/driver/ElementCallMatrixClientDriver.ts
+++ b/src/driver/ElementCallMatrixClientDriver.ts
@@ -125,7 +125,8 @@ export interface ProfileDriver {
getOwnProfile(): OwnProfile;
subscribeOwnProfile(listener: (profile: OwnProfile) => void): Unsubscribe;
setDisplayName?(name: string): Promise;
- setAvatar?(file: Blob): Promise;
+ /** Sets the avatar to `file`, or removes it with `null`. */
+ setAvatar?(file: Blob | null): Promise;
}
export interface MediaDriver {
diff --git a/src/driver/MatrixDriverContext.tsx b/src/driver/MatrixDriverContext.tsx
new file mode 100644
index 000000000..e1980626d
--- /dev/null
+++ b/src/driver/MatrixDriverContext.tsx
@@ -0,0 +1,41 @@
+/*
+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 { createContext, use } from "react";
+
+import { type ElementCallMatrixClientDriver } from "./ElementCallMatrixClientDriver";
+import { type RtcMatrixDriver } from "./RtcMatrixDriver";
+
+/** The host's two drivers for the room a call is in. */
+export interface MatrixDrivers {
+ rtcDriver: RtcMatrixDriver;
+ clientDriver: ElementCallMatrixClientDriver;
+}
+
+const MatrixDriverContext = createContext(null);
+
+/**
+ * Makes the drivers available to the call tree. Every host that renders a
+ * `CallView` provides them: the component from its props, the standalone
+ * app and the widget from the matrix-js-sdk drivers over their client.
+ */
+export const MatrixDriverProvider = MatrixDriverContext.Provider;
+
+/** The drivers, or null where no host provided any (a test rendering a leaf). */
+export function useOptionalMatrixDrivers(): MatrixDrivers | null {
+ return use(MatrixDriverContext);
+}
+
+/** The drivers; throws where none were provided. */
+export function useMatrixDrivers(): MatrixDrivers {
+ const drivers = useOptionalMatrixDrivers();
+ if (drivers === null)
+ throw new Error(
+ "No Matrix drivers were provided; wrap the call in a MatrixDriverProvider",
+ );
+ return drivers;
+}
diff --git a/src/driver/MockRtcMatrixDriver.ts b/src/driver/MockRtcMatrixDriver.ts
index 58d68c256..aaded05ea 100644
--- a/src/driver/MockRtcMatrixDriver.ts
+++ b/src/driver/MockRtcMatrixDriver.ts
@@ -145,7 +145,7 @@ export class MockRtcMatrixDriver implements RtcMatrixDriver {
private roomEventSink?: RoomEventSinkLike;
private toDeviceSink?: ToDeviceSinkLike;
private stateUpdateSink?: StateUpdateSinkLike;
- private connectivitySink?: ConnectivitySinkLike;
+ private readonly connectivitySinks = new Set();
private homeserverConnected = true;
private nextDelayId = 0;
@@ -403,7 +403,7 @@ export class MockRtcMatrixDriver implements RtcMatrixDriver {
}
public subscribeConnectivity(sink: ConnectivitySinkLike): void {
- this.connectivitySink = sink;
+ this.connectivitySinks.add(sink);
}
public isHomeserverConnected(): boolean {
@@ -413,7 +413,8 @@ export class MockRtcMatrixDriver implements RtcMatrixDriver {
/** The homeserver comes or goes, as a syncing client would report it. */
public setHomeserverConnected(connected: boolean): void {
this.homeserverConnected = connected;
- this.connectivitySink?.emit(connected);
+ for (const sink of this.connectivitySinks)
+ if (!sink.emit(connected)) this.connectivitySinks.delete(sink);
}
/** Emit any room event — sticky or state; the crate dispatches on type. */
diff --git a/src/driver/jsSdk/JsSdkElementCallMatrixClientDriver.ts b/src/driver/jsSdk/JsSdkElementCallMatrixClientDriver.ts
index 27a00d43d..6cb615c1d 100644
--- a/src/driver/jsSdk/JsSdkElementCallMatrixClientDriver.ts
+++ b/src/driver/jsSdk/JsSdkElementCallMatrixClientDriver.ts
@@ -64,13 +64,8 @@ export class JsSdkElementCallMatrixClientDriver implements ElementCallMatrixClie
private capabilities: Promise | null = null;
public constructor(
- /**
- * The client and room this driver wraps. Public only for the React tree
- * that still runs on matrix-js-sdk directly (`CallView` and below); once
- * that tree reads the drivers (plan slice S4) these become private.
- */
- public readonly client: MatrixClient,
- public readonly room: Room,
+ private readonly client: MatrixClient,
+ private readonly room: Room,
options: JsSdkElementCallMatrixClientDriverOptions = {},
) {
const userId = client.getUserId();
@@ -271,7 +266,11 @@ export class JsSdkElementCallMatrixClientDriver implements ElementCallMatrixClie
await this.client.setDisplayName(name);
}
- public async setAvatar(file: Blob): Promise {
+ public async setAvatar(file: Blob | null): Promise {
+ if (file === null) {
+ await this.client.setAvatarUrl("");
+ return;
+ }
const { content_uri: uri } = await this.client.uploadContent(file);
await this.client.setAvatarUrl(uri);
}
diff --git a/src/driver/jsSdk/useJsSdkDrivers.ts b/src/driver/jsSdk/useJsSdkDrivers.ts
new file mode 100644
index 000000000..b26565c8f
--- /dev/null
+++ b/src/driver/jsSdk/useJsSdkDrivers.ts
@@ -0,0 +1,37 @@
+/*
+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 { type MatrixClient, type Room } from "matrix-js-sdk";
+import { useEffect, useMemo } from "react";
+
+import { type MatrixDrivers } from "../MatrixDriverContext";
+import { JsSdkElementCallMatrixClientDriver } from "./JsSdkElementCallMatrixClientDriver";
+import { JsSdkRtcMatrixDriver } from "./JsSdkRtcMatrixDriver";
+
+/**
+ * The two matrix-js-sdk drivers for a room, for as long as the client and
+ * room stay the same. The RTC driver hooks listeners on the client for the
+ * crate's sinks; they are let go of when the drivers are replaced or the
+ * caller unmounts.
+ */
+export function useJsSdkDrivers(
+ client: MatrixClient,
+ room: Room,
+): MatrixDrivers {
+ const drivers = useMemo(
+ (): MatrixDrivers => ({
+ rtcDriver: new JsSdkRtcMatrixDriver(client, room),
+ clientDriver: new JsSdkElementCallMatrixClientDriver(client, room),
+ }),
+ [client, room],
+ );
+ useEffect(
+ () => (): void => (drivers.rtcDriver as JsSdkRtcMatrixDriver).detach(),
+ [drivers],
+ );
+ return drivers;
+}
diff --git a/src/driver/useHomeserverConnected.test.tsx b/src/driver/useHomeserverConnected.test.tsx
new file mode 100644
index 000000000..c641cfd61
--- /dev/null
+++ b/src/driver/useHomeserverConnected.test.tsx
@@ -0,0 +1,63 @@
+/*
+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 { act, renderHook } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+import { type MatrixDrivers } from "./MatrixDriverContext";
+import { MockElementCallMatrixClientDriver } from "./MockElementCallMatrixClientDriver";
+import { MockRtcMatrixDriver } from "./MockRtcMatrixDriver";
+import { useHomeserverConnected } from "./useHomeserverConnected";
+
+describe("useHomeserverConnected", () => {
+ beforeEach(() => vi.useFakeTimers());
+ afterEach(() => vi.useRealTimers());
+
+ it("reports a lapse only after the grace period, and a return at once", () => {
+ const rtcDriver = new MockRtcMatrixDriver();
+ const drivers: MatrixDrivers = {
+ rtcDriver,
+ clientDriver: new MockElementCallMatrixClientDriver(),
+ };
+ const { result } = renderHook(() => useHomeserverConnected(drivers, 1000));
+ expect(result.current).toBe(true);
+
+ act(() => rtcDriver.setHomeserverConnected(false));
+ expect(result.current).toBe(true);
+ act(() => vi.advanceTimersByTime(999));
+ expect(result.current).toBe(true);
+ act(() => vi.advanceTimersByTime(1));
+ expect(result.current).toBe(false);
+
+ act(() => rtcDriver.setHomeserverConnected(true));
+ expect(result.current).toBe(true);
+
+ // A blip shorter than the grace period is never shown.
+ act(() => rtcDriver.setHomeserverConnected(false));
+ act(() => vi.advanceTimersByTime(500));
+ act(() => rtcDriver.setHomeserverConnected(true));
+ act(() => vi.advanceTimersByTime(1000));
+ expect(result.current).toBe(true);
+ });
+
+ it("starts from the driver's current answer", () => {
+ const rtcDriver = new MockRtcMatrixDriver();
+ rtcDriver.setHomeserverConnected(false);
+ const drivers: MatrixDrivers = {
+ rtcDriver,
+ clientDriver: new MockElementCallMatrixClientDriver(),
+ };
+ const { result } = renderHook(() => useHomeserverConnected(drivers, 1000));
+ act(() => vi.advanceTimersByTime(1000));
+ expect(result.current).toBe(false);
+ });
+
+ it("is connected without drivers", () => {
+ const { result } = renderHook(() => useHomeserverConnected(null, 1000));
+ expect(result.current).toBe(true);
+ });
+});
diff --git a/src/driver/useHomeserverConnected.ts b/src/driver/useHomeserverConnected.ts
new file mode 100644
index 000000000..e4747f898
--- /dev/null
+++ b/src/driver/useHomeserverConnected.ts
@@ -0,0 +1,58 @@
+/*
+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 { useEffect, useState } from "react";
+
+import { Config } from "../config/Config";
+import { type MatrixDrivers } from "./MatrixDriverContext";
+
+/**
+ * Whether the homeserver is reachable, from the RTC driver's connectivity
+ * (the same signal the crate turns into `HomeserverUnreachable`). The driver
+ * reports every lapse of the sync loop; the UI should not, so a lapse counts
+ * only once it has lasted `graceMs` (the deployment's
+ * `sync_disconnect_grace_period_ms` by default). Coming back counts at once.
+ *
+ * `null` drivers (a host that provided none) read as connected: there is
+ * nothing to report on.
+ */
+export function useHomeserverConnected(
+ drivers: MatrixDrivers | null,
+ graceMs: number = Config.get().sync_disconnect_grace_period_ms,
+): boolean {
+ const [connected, setConnected] = useState(true);
+ useEffect(() => {
+ if (drivers === null) {
+ setConnected(true);
+ return;
+ }
+ const { rtcDriver } = drivers;
+ let live = true;
+ let lapse: ReturnType | null = null;
+ const report = (isConnected: boolean): void => {
+ if (lapse !== null) {
+ clearTimeout(lapse);
+ lapse = null;
+ }
+ if (isConnected) setConnected(true);
+ else lapse = setTimeout(() => setConnected(false), graceMs);
+ };
+ report(rtcDriver.isHomeserverConnected());
+ rtcDriver.subscribeConnectivity({
+ emit: (isConnected) => {
+ if (!live) return false;
+ report(isConnected);
+ return true;
+ },
+ });
+ return (): void => {
+ live = false;
+ if (lapse !== null) clearTimeout(lapse);
+ };
+ }, [drivers, graceMs]);
+ return connected;
+}
diff --git a/src/e2ee/sharedKeyManagement.ts b/src/e2ee/sharedKeyManagement.ts
index b29ede319..134743013 100644
--- a/src/e2ee/sharedKeyManagement.ts
+++ b/src/e2ee/sharedKeyManagement.ts
@@ -87,8 +87,16 @@ export type SharedSecret = { kind: E2eeType.SHARED_KEY; secret: string };
export type PerParticipantE2EE = { kind: E2eeType.PER_PARTICIPANT };
export type EncryptionSystem = Unencrypted | SharedSecret | PerParticipantE2EE;
-export function useRoomEncryptionSystem(roomId: string): EncryptionSystem {
- const { client } = useClient();
+/**
+ * The encryption system for a room whose `m.room.encryption` state the
+ * caller already knows: a shared secret from the URL or storage wins, else
+ * per-participant keys in an encrypted room, else nothing. The call tree
+ * reads `roomEncrypted` from the client driver's room info.
+ */
+export function useEncryptionSystemFor(
+ roomId: string,
+ roomEncrypted: boolean,
+): EncryptionSystem {
const { roomId: paramsRoomId, password } = useUrlParams();
const [storedPassword] = useRoomSharedKey(
@@ -100,18 +108,29 @@ export function useRoomEncryptionSystem(roomId: string): EncryptionSystem {
keyForRoom(roomId, paramsRoomId, password) ?? undefined,
);
- const room = client?.getRoom(roomId);
- const e2eeSystem = useMemo(() => {
- if (!room) return { kind: E2eeType.NONE };
+ return useMemo(() => {
if (storedPassword)
return {
kind: E2eeType.SHARED_KEY,
secret: storedPassword,
};
- if (room.hasEncryptionStateEvent()) {
+ if (roomEncrypted) {
return { kind: E2eeType.PER_PARTICIPANT };
}
return { kind: E2eeType.NONE };
- }, [room, storedPassword]);
- return e2eeSystem;
+ }, [roomEncrypted, storedPassword]);
+}
+
+/** {@link useEncryptionSystemFor} over the client's room; nothing for a room it does not know. */
+export function useRoomEncryptionSystem(roomId: string): EncryptionSystem {
+ const { client } = useClient();
+ const room = client?.getRoom(roomId);
+ const e2eeSystem = useEncryptionSystemFor(
+ roomId,
+ room?.hasEncryptionStateEvent() ?? false,
+ );
+ return useMemo(
+ () => (room ? e2eeSystem : { kind: E2eeType.NONE }),
+ [room, e2eeSystem],
+ );
}
diff --git a/src/matrix-rtc-sdk/generated/VERSION b/src/matrix-rtc-sdk/generated/VERSION
index 8966fc8d9..2cb52ee9e 100644
--- a/src/matrix-rtc-sdk/generated/VERSION
+++ b/src/matrix-rtc-sdk/generated/VERSION
@@ -1,2 +1,2 @@
-matrix-rtc (MatrixSdkArchitectureDraft) 088a598-dirty
-built 2026-09-15T18:22:09Z by scripts/sync-matrix-rtc-sdk.sh
+matrix-rtc (MatrixSdkArchitectureDraft) 61140bc-dirty
+built 2026-09-16T12:10:06Z by scripts/sync-matrix-rtc-sdk.sh
diff --git a/src/matrix-rtc-sdk/generated/matrix_rtc-ffi.ts b/src/matrix-rtc-sdk/generated/matrix_rtc-ffi.ts
index 67a59c954..fdd4103da 100644
--- a/src/matrix-rtc-sdk/generated/matrix_rtc-ffi.ts
+++ b/src/matrix-rtc-sdk/generated/matrix_rtc-ffi.ts
@@ -36,6 +36,14 @@ export type UniffiVTableCallbackInterfaceMatrixRtcKeyRejectedListener = {
uniffi_clone: UniffiCallbackInterfaceCloneMatrixRtcKeyRejectedListener;
on_key_rejected: UniffiCallbackInterfaceMatrixRtcKeyRejectedListenerMethod0;
};
+type UniffiCallbackInterfaceMatrixRtcLogSinkMethod0 = (uniffiHandle: bigint, level: Uint8Array, target: Uint8Array, message: Uint8Array) => UniffiResult;
+type UniffiCallbackInterfaceCloneMatrixRtcLogSink = (handle: bigint) => UniffiResult;
+type UniffiCallbackInterfaceFreeMatrixRtcLogSink = (handle: bigint) => void;
+export type UniffiVTableCallbackInterfaceMatrixRtcLogSink = {
+ uniffi_free: UniffiCallbackInterfaceFreeMatrixRtcLogSink;
+ uniffi_clone: UniffiCallbackInterfaceCloneMatrixRtcLogSink;
+ log: UniffiCallbackInterfaceMatrixRtcLogSinkMethod0;
+};
export type UniffiForeignFutureResultRustBuffer = {
return_value: Uint8Array;
call_status: UniffiRustCallStatus;
diff --git a/src/matrix-rtc-sdk/generated/matrix_rtc.ts b/src/matrix-rtc-sdk/generated/matrix_rtc.ts
index 9f2ef3691..d34986c1f 100644
--- a/src/matrix-rtc-sdk/generated/matrix_rtc.ts
+++ b/src/matrix-rtc-sdk/generated/matrix_rtc.ts
@@ -4,7 +4,7 @@
/* eslint-disable */
// @ts-nocheck
import * as wasmBundle from "./wasm-bindgen/index.js";
-import { type UniffiRustFutureContinuationCallback, type UniffiForeignFutureDroppedCallback, type UniffiForeignFutureDroppedCallbackStruct, type UniffiVTableCallbackInterfaceMatrixRtcConnectionsListener, type UniffiVTableCallbackInterfaceMatrixRtcKeyMapListener, type UniffiVTableCallbackInterfaceMatrixRtcKeyRejectedListener, type UniffiForeignFutureResultRustBuffer, type UniffiForeignFutureCompleterustBuffer, type UniffiForeignFutureResultVoid, type UniffiForeignFutureCompletevoid, type UniffiVTableCallbackInterfaceMatrixRtcMatrixDriverCallback, type UniffiVTableCallbackInterfaceMatrixRtcMembershipsListener, type UniffiVTableCallbackInterfaceMatrixRtcSessionListener, type UniffiVTableCallbackInterfaceMatrixRtcStatusListener,
+import { type UniffiRustFutureContinuationCallback, type UniffiForeignFutureDroppedCallback, type UniffiForeignFutureDroppedCallbackStruct, type UniffiVTableCallbackInterfaceMatrixRtcConnectionsListener, type UniffiVTableCallbackInterfaceMatrixRtcKeyMapListener, type UniffiVTableCallbackInterfaceMatrixRtcKeyRejectedListener, type UniffiVTableCallbackInterfaceMatrixRtcLogSink, type UniffiForeignFutureResultRustBuffer, type UniffiForeignFutureCompleterustBuffer, type UniffiForeignFutureResultVoid, type UniffiForeignFutureCompletevoid, type UniffiVTableCallbackInterfaceMatrixRtcMatrixDriverCallback, type UniffiVTableCallbackInterfaceMatrixRtcMembershipsListener, type UniffiVTableCallbackInterfaceMatrixRtcSessionListener, type UniffiVTableCallbackInterfaceMatrixRtcStatusListener,
} from "./matrix_rtc-ffi";
import { type FfiConverter, type UniffiByteArray, type UniffiGcObject, type UniffiHandle, type UniffiObjectFactory, type UniffiReferenceHolder, type UniffiRustCallStatus, AbstractFfiConverterByteArray, Cursor, FfiConverterArray, FfiConverterArrayBuffer, FfiConverterBool, FfiConverterObject, FfiConverterObjectWithCallbacks, FfiConverterOptional, FfiConverterUInt32, FfiConverterUInt64, FfiConverterUInt8, RustBuffer, UniffiAbstractObject, UniffiEnum, UniffiError, UniffiInternalError, UniffiResult, UniffiRustCaller, destructorGuardSymbol, pointerLiteralSymbol, uniffiCreateFfiConverterString, uniffiCreateRecord, uniffiRustCallAsync, uniffiTraitInterfaceCall, uniffiTraitInterfaceCallAsyncWithError, uniffiTypeNameSymbol, variantOrdinalSymbol,
} from "@ubjs/core";
@@ -75,6 +75,23 @@ export function impairmentSeverity(impairment: FfiImpairment): FfiSeverity {
}
}
+/**
+ * Routes the crate's log lines to `sink`, at `max_level` and above. Call
+ * once after the bindings are initialised; calling again replaces the sink
+ * and the level. The `log` facade accepts one logger per process, so if the
+ * host already installed a Rust logger of its own (a native host with
+ * `tracing`), that one keeps the lines and the sink stays silent.
+ */
+export function setLogSink(sink: LogSink, maxLevel: FfiLogLevel): void {uniffiCaller.rustCall(
+ /*caller:*/ (callStatus) => { nativeModule().ubrn_uniffi_matrix_rtc_fn_func_set_log_sink(
+ FfiConverterTypeLogSink.lower(sink, nativeModule().rustbuffer_alloc),
+ FfiConverterTypeFfiLogLevel.lower(maxLevel, nativeModule().rustbuffer_alloc),
+ callStatus);
+ },
+ /*liftString:*/ FfiConverterString.lift.bind(FfiConverterString),
+ );
+ }
+
const stringConverter = (() => {
const encoder = new TextEncoder();
const decoder = new TextDecoder();
@@ -3788,6 +3805,46 @@ const FfiConverterTypeFfiKeepAlive = (() => {
return new FFIConverter();
})();
+/**
+ * The `log` crate's levels, for a [`LogSink`].
+ */
+export enum FfiLogLevel {
+ Error,
+ Warn,
+ Info,
+ Debug,
+ Trace
+}
+
+const FfiConverterTypeFfiLogLevel = (() => {
+ type TypeName = FfiLogLevel;
+ class FFIConverter extends AbstractFfiConverterByteArray {
+ readFromCursor(c: Cursor): TypeName {
+ switch (c.readI32()) {
+ case 1: return FfiLogLevel.Error;
+ case 2: return FfiLogLevel.Warn;
+ case 3: return FfiLogLevel.Info;
+ case 4: return FfiLogLevel.Debug;
+ case 5: return FfiLogLevel.Trace;
+ default: throw new UniffiInternalError.UnexpectedEnumCase();
+ }
+ }
+ writeIntoCursor(value: TypeName, c: Cursor): void {
+ switch (value) {
+ case FfiLogLevel.Error: return c.writeI32(1);
+ case FfiLogLevel.Warn: return c.writeI32(2);
+ case FfiLogLevel.Info: return c.writeI32(3);
+ case FfiLogLevel.Debug: return c.writeI32(4);
+ case FfiLogLevel.Trace: return c.writeI32(5);
+ }
+ }
+ allocationSize(value: TypeName): number {
+ return 4;
+ }
+ }
+ return new FFIConverter();
+})();
+
// Enum: FfiRosterPresence
export enum FfiRosterPresence_Tags {
@@ -7027,6 +7084,189 @@ const uniffiTypeFfiParticipationManagerObjectFactory: UniffiObjectFactory { nativeModule().ubrn_uniffi_matrix_rtc_fn_method_logsink_log(
+ uniffiTypeLogSinkImplObjectFactory.clonePointer(this),
+ FfiConverterTypeFfiLogLevel.lower(level, nativeModule().rustbuffer_alloc),
+ FfiConverterString.lower(target, nativeModule().rustbuffer_alloc),
+ FfiConverterString.lower(message, nativeModule().rustbuffer_alloc),
+ callStatus);
+ },
+ /*liftString:*/ FfiConverterString.lift.bind(FfiConverterString),
+ );
+ }
+
+
+ uniffiDestroy(): void {
+ const ptr = (this as any)[destructorGuardSymbol];
+ if (ptr !== undefined) {
+ const pointer = uniffiTypeLogSinkImplObjectFactory.pointer(this);
+ uniffiTypeLogSinkImplObjectFactory.freePointer(pointer);
+ uniffiTypeLogSinkImplObjectFactory.unbless(ptr);
+ delete (this as any)[destructorGuardSymbol];
+ }
+ }
+
+ static instanceOf(obj_: any): obj_ is LogSinkImpl {
+ return uniffiTypeLogSinkImplObjectFactory.isConcreteType(obj_);
+ }
+
+
+}
+
+const uniffiTypeLogSinkImplObjectFactory: UniffiObjectFactory = (() => {
+
+ ///
+ const registry = typeof FinalizationRegistry !== 'undefined' ? new FinalizationRegistry((heldValue: UniffiHandle) => {
+ uniffiTypeLogSinkImplObjectFactory.freePointer(heldValue);
+ }) : null;
+
+ return {
+ create(pointer: UniffiHandle): LogSink {
+ const instance = Object.create(LogSinkImpl.prototype);
+ instance[pointerLiteralSymbol] = pointer;
+ instance[destructorGuardSymbol] = this.bless(pointer);
+ instance[uniffiTypeNameSymbol] = "LogSinkImpl";
+ return instance;
+ },
+
+
+ bless(p: UniffiHandle): UniffiGcObject {
+ const ptr = {
+ p, // make sure this object doesn't get optimized away.
+ markDestroyed: () => undefined,
+ };
+ if (registry) {
+ registry.register(ptr, p, ptr);
+ }
+ return ptr;
+ },
+
+ unbless(ptr_: UniffiGcObject) {
+ if (registry) {
+ registry.unregister(ptr_);
+ }
+ },
+
+ pointer(obj_: LogSink): UniffiHandle {
+ if ((obj_ as any)[destructorGuardSymbol] === undefined) {
+ throw new UniffiInternalError.UnexpectedNullPointer();
+ }
+ return (obj_ as any)[pointerLiteralSymbol];
+ },
+
+ clonePointer(obj_: LogSink): UniffiHandle {
+ const pointer = this.pointer(obj_);
+ return uniffiCaller.rustCall(
+ /*caller:*/ (callStatus) => nativeModule().ubrn_uniffi_matrix_rtc_fn_clone_logsink(pointer, callStatus),
+ /*liftString:*/ FfiConverterString.lift
+ );
+ },
+
+ freePointer(pointer: UniffiHandle): void {
+ uniffiCaller.rustCall(
+ /*caller:*/ (callStatus) => nativeModule().ubrn_uniffi_matrix_rtc_fn_free_logsink(pointer, callStatus),
+ /*liftString:*/ FfiConverterString.lift
+ );
+ },
+
+ isConcreteType(obj_: any): obj_ is LogSink {
+ return obj_[destructorGuardSymbol] && obj_[uniffiTypeNameSymbol] === "LogSinkImpl";
+ },
+}})();
+const FfiConverterTypeLogSink = new FfiConverterObjectWithCallbacks(uniffiTypeLogSinkImplObjectFactory);
+
+// Add a vtable for the callbacks that go in LogSink.
+
+// Put the implementation in a struct so we don't pollute the top-level namespace
+const uniffiCallbackInterfaceLogSink: { vtable: any; register: () => void; } = {
+ // Create the VTable using a series of closures.
+ // ts automatically converts these into C callback functions.
+ vtable: {
+ log: (
+ uniffiHandle: bigint,
+ level: Uint8Array,
+ target: Uint8Array,
+ message: Uint8Array,) => {
+ const uniffiMakeCall =
+ ()
+ : void => {
+ const jsCallback = FfiConverterTypeLogSink.lift(uniffiHandle);
+ return jsCallback.log(
+ FfiConverterTypeFfiLogLevel.lift(level),
+ FfiConverterString.lift(target),
+ FfiConverterString.lift(message)
+ )
+ };
+ const uniffiResult = UniffiResult.ready();
+ const uniffiHandleSuccess = (obj: any) => {};
+ const uniffiHandleError = (code: number, errBuf: UniffiByteArray) => {
+ UniffiResult.writeError(uniffiResult, code, errBuf);
+ };
+ uniffiTraitInterfaceCall(
+ /*makeCall:*/ uniffiMakeCall,
+ /*handleSuccess:*/ uniffiHandleSuccess,
+ /*handleError:*/ uniffiHandleError,
+ /*lowerString:*/ FfiConverterString.lower.bind(FfiConverterString),
+ /*alloc:*/ nativeModule().rustbuffer_alloc,
+ )
+ return uniffiResult;
+ },
+ uniffi_free: (uniffiHandle: UniffiHandle): void => {
+ // this will throw a stale handle error if the handle isn't found.
+ FfiConverterTypeLogSink.drop(uniffiHandle);
+ },
+ uniffi_clone: (uniffiHandle: UniffiHandle): UniffiHandle => {
+ return FfiConverterTypeLogSink.clone(uniffiHandle);
+ }
+ },
+ register: () => {nativeModule().ubrn_uniffi_matrix_rtc_fn_init_callback_vtable_logsink(
+ uniffiCallbackInterfaceLogSink.vtable
+ );
+ },
+};
+
/**
* One end of a driver event stream: Rust-exported objects handed to the
* foreign driver through the `subscribe_*` methods. The host calls `emit`
@@ -8963,6 +9203,9 @@ function uniffiEnsureInitialized() {
if (nativeModule().ubrn_uniffi_matrix_rtc_checksum_func_impairment_severity() !== 9139) {
throw new UniffiInternalError.ApiChecksumMismatch("uniffi_matrix_rtc_checksum_func_impairment_severity");
}
+ if (nativeModule().ubrn_uniffi_matrix_rtc_checksum_func_set_log_sink() !== 7798) {
+ throw new UniffiInternalError.ApiChecksumMismatch("uniffi_matrix_rtc_checksum_func_set_log_sink");
+ }
if (nativeModule().ubrn_uniffi_matrix_rtc_checksum_method_connectionslistener_on_connections_change() !== 24219) {
throw new UniffiInternalError.ApiChecksumMismatch("uniffi_matrix_rtc_checksum_method_connectionslistener_on_connections_change");
}
@@ -9047,6 +9290,9 @@ function uniffiEnsureInitialized() {
if (nativeModule().ubrn_uniffi_matrix_rtc_checksum_method_keyrejectedlistener_on_key_rejected() !== 53044) {
throw new UniffiInternalError.ApiChecksumMismatch("uniffi_matrix_rtc_checksum_method_keyrejectedlistener_on_key_rejected");
}
+ if (nativeModule().ubrn_uniffi_matrix_rtc_checksum_method_logsink_log() !== 15220) {
+ throw new UniffiInternalError.ApiChecksumMismatch("uniffi_matrix_rtc_checksum_method_logsink_log");
+ }
if (nativeModule().ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_send_sticky_event() !== 39725) {
throw new UniffiInternalError.ApiChecksumMismatch("uniffi_matrix_rtc_checksum_method_matrixdrivercallback_send_sticky_event");
}
@@ -9126,6 +9372,7 @@ function uniffiEnsureInitialized() {
uniffiCallbackInterfaceMembershipsListener.register();
uniffiCallbackInterfaceSessionListener.register();
uniffiCallbackInterfaceStatusListener.register();
+ uniffiCallbackInterfaceLogSink.register();
uniffiCallbackInterfaceMatrixDriverCallback.register();
}
@@ -9157,6 +9404,7 @@ export default Object.freeze({
FfiConverterTypeFfiKeyRejection,
FfiConverterTypeFfiLivekitToken,
FfiConverterTypeFfiLivekitTokenRequest,
+ FfiConverterTypeFfiLogLevel,
FfiConverterTypeFfiMatrixDriver,
FfiConverterTypeFfiMediaKey,
FfiConverterTypeFfiMediaKeyState,
@@ -9179,6 +9427,7 @@ export default Object.freeze({
FfiConverterTypeFfiTransportIntent,
FfiConverterTypeKeyMapListener,
FfiConverterTypeKeyRejectedListener,
+ FfiConverterTypeLogSink,
FfiConverterTypeMatrixDriverCallback,
FfiConverterTypeMembershipsListener,
FfiConverterTypeRoomEventSink,
diff --git a/src/matrix-rtc-sdk/generated/wasm-bindgen/index.js b/src/matrix-rtc-sdk/generated/wasm-bindgen/index.js
index b221ba0b5..fd45f635d 100644
--- a/src/matrix-rtc-sdk/generated/wasm-bindgen/index.js
+++ b/src/matrix-rtc-sdk/generated/wasm-bindgen/index.js
@@ -359,9 +359,9 @@ export function ubrn_uniffi_matrix_rtc_fn_method_keyrejectedlistener_on_key_reje
* @param {RustCallStatus} f_status_
* @returns {bigint}
*/
-export function ubrn_uniffi_matrix_rtc_fn_clone_matrixdrivercallback(handle, f_status_) {
+export function ubrn_uniffi_matrix_rtc_fn_clone_logsink(handle, f_status_) {
_assertClass(f_status_, RustCallStatus);
- const ret = wasm.ubrn_uniffi_matrix_rtc_fn_clone_matrixdrivercallback(handle, f_status_.__wbg_ptr);
+ const ret = wasm.ubrn_uniffi_matrix_rtc_fn_clone_logsink(handle, f_status_.__wbg_ptr);
return BigInt.asUintN(64, ret);
}
@@ -369,9 +369,63 @@ export function ubrn_uniffi_matrix_rtc_fn_clone_matrixdrivercallback(handle, f_s
* @param {bigint} handle
* @param {RustCallStatus} f_status_
*/
-export function ubrn_uniffi_matrix_rtc_fn_free_matrixdrivercallback(handle, f_status_) {
+export function ubrn_uniffi_matrix_rtc_fn_free_logsink(handle, f_status_) {
_assertClass(f_status_, RustCallStatus);
- wasm.ubrn_uniffi_matrix_rtc_fn_free_matrixdrivercallback(handle, f_status_.__wbg_ptr);
+ wasm.ubrn_uniffi_matrix_rtc_fn_free_logsink(handle, f_status_.__wbg_ptr);
+}
+
+/**
+ * @param {bigint} ptr
+ * @param {Uint8Array} room_id
+ * @param {Uint8Array} delay_id
+ * @returns {bigint}
+ */
+export function ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_restart_delayed_event(ptr, room_id, delay_id) {
+ const ptr0 = passArray8ToWasm0(room_id, wasm.__wbindgen_malloc);
+ const len0 = WASM_VECTOR_LEN;
+ const ptr1 = passArray8ToWasm0(delay_id, wasm.__wbindgen_malloc);
+ const len1 = WASM_VECTOR_LEN;
+ const ret = wasm.ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_restart_delayed_event(ptr, ptr0, len0, ptr1, len1);
+ return BigInt.asUintN(64, ret);
+}
+
+/**
+ * @param {bigint} ptr
+ * @param {Uint8Array} room_id
+ * @param {Uint8Array} delay_id
+ * @returns {bigint}
+ */
+export function ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_cancel_delayed_event(ptr, room_id, delay_id) {
+ const ptr0 = passArray8ToWasm0(room_id, wasm.__wbindgen_malloc);
+ const len0 = WASM_VECTOR_LEN;
+ const ptr1 = passArray8ToWasm0(delay_id, wasm.__wbindgen_malloc);
+ const len1 = WASM_VECTOR_LEN;
+ const ret = wasm.ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_cancel_delayed_event(ptr, ptr0, len0, ptr1, len1);
+ return BigInt.asUintN(64, ret);
+}
+
+/**
+ * @param {bigint} ptr
+ * @param {Uint8Array} request
+ * @returns {bigint}
+ */
+export function ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_delegate_delayed_leave_via_homeserver(ptr, request) {
+ const ptr0 = passArray8ToWasm0(request, wasm.__wbindgen_malloc);
+ const len0 = WASM_VECTOR_LEN;
+ const ret = wasm.ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_delegate_delayed_leave_via_homeserver(ptr, ptr0, len0);
+ return BigInt.asUintN(64, ret);
+}
+
+/**
+ * @param {bigint} ptr
+ * @param {Uint8Array} request
+ * @returns {bigint}
+ */
+export function ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_delegate_delayed_leave_via_transport(ptr, request) {
+ const ptr0 = passArray8ToWasm0(request, wasm.__wbindgen_malloc);
+ const len0 = WASM_VECTOR_LEN;
+ const ret = wasm.ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_delegate_delayed_leave_via_transport(ptr, ptr0, len0);
+ return BigInt.asUintN(64, ret);
}
/**
@@ -429,49 +483,49 @@ export function ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_read_event
return BigInt.asUintN(64, ret);
}
+/**
+ * @param {any} vtable
+ */
+export function ubrn_uniffi_matrix_rtc_fn_init_callback_vtable_logsink(vtable) {
+ wasm.ubrn_uniffi_matrix_rtc_fn_init_callback_vtable_logsink(vtable);
+}
+
/**
* @param {bigint} ptr
- * @param {Uint8Array} event_type
- * @param {Uint8Array} state_key
+ * @param {Uint8Array} level
+ * @param {Uint8Array} target
+ * @param {Uint8Array} message
+ * @param {RustCallStatus} f_status_
+ */
+export function ubrn_uniffi_matrix_rtc_fn_method_logsink_log(ptr, level, target, message, f_status_) {
+ const ptr0 = passArray8ToWasm0(level, wasm.__wbindgen_malloc);
+ const len0 = WASM_VECTOR_LEN;
+ const ptr1 = passArray8ToWasm0(target, wasm.__wbindgen_malloc);
+ const len1 = WASM_VECTOR_LEN;
+ const ptr2 = passArray8ToWasm0(message, wasm.__wbindgen_malloc);
+ const len2 = WASM_VECTOR_LEN;
+ _assertClass(f_status_, RustCallStatus);
+ wasm.ubrn_uniffi_matrix_rtc_fn_method_logsink_log(ptr, ptr0, len0, ptr1, len1, ptr2, len2, f_status_.__wbg_ptr);
+}
+
+/**
+ * @param {bigint} handle
+ * @param {RustCallStatus} f_status_
* @returns {bigint}
*/
-export function ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_read_state(ptr, event_type, state_key) {
- const ptr0 = passArray8ToWasm0(event_type, wasm.__wbindgen_malloc);
- const len0 = WASM_VECTOR_LEN;
- const ptr1 = passArray8ToWasm0(state_key, wasm.__wbindgen_malloc);
- const len1 = WASM_VECTOR_LEN;
- const ret = wasm.ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_read_state(ptr, ptr0, len0, ptr1, len1);
+export function ubrn_uniffi_matrix_rtc_fn_clone_matrixdrivercallback(handle, f_status_) {
+ _assertClass(f_status_, RustCallStatus);
+ const ret = wasm.ubrn_uniffi_matrix_rtc_fn_clone_matrixdrivercallback(handle, f_status_.__wbg_ptr);
return BigInt.asUintN(64, ret);
}
/**
- * @param {bigint} ptr
- * @param {bigint} sink
+ * @param {bigint} handle
* @param {RustCallStatus} f_status_
*/
-export function ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_subscribe_room_events(ptr, sink, f_status_) {
+export function ubrn_uniffi_matrix_rtc_fn_free_matrixdrivercallback(handle, f_status_) {
_assertClass(f_status_, RustCallStatus);
- wasm.ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_subscribe_room_events(ptr, sink, f_status_.__wbg_ptr);
-}
-
-/**
- * @param {bigint} ptr
- * @param {bigint} sink
- * @param {RustCallStatus} f_status_
- */
-export function ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_subscribe_to_device_events(ptr, sink, f_status_) {
- _assertClass(f_status_, RustCallStatus);
- wasm.ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_subscribe_to_device_events(ptr, sink, f_status_.__wbg_ptr);
-}
-
-/**
- * @param {bigint} ptr
- * @param {bigint} sink
- * @param {RustCallStatus} f_status_
- */
-export function ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_subscribe_state_updates(ptr, sink, f_status_) {
- _assertClass(f_status_, RustCallStatus);
- wasm.ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_subscribe_state_updates(ptr, sink, f_status_.__wbg_ptr);
+ wasm.ubrn_uniffi_matrix_rtc_fn_free_matrixdrivercallback(handle, f_status_.__wbg_ptr);
}
/**
@@ -567,77 +621,64 @@ export function ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_send_delay
/**
* @param {bigint} ptr
- * @param {Uint8Array} room_id
- * @param {Uint8Array} delay_id
+ * @param {Uint8Array} event_type
+ * @param {Uint8Array} state_key
* @returns {bigint}
*/
-export function ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_restart_delayed_event(ptr, room_id, delay_id) {
- const ptr0 = passArray8ToWasm0(room_id, wasm.__wbindgen_malloc);
+export function ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_read_state(ptr, event_type, state_key) {
+ const ptr0 = passArray8ToWasm0(event_type, wasm.__wbindgen_malloc);
const len0 = WASM_VECTOR_LEN;
- const ptr1 = passArray8ToWasm0(delay_id, wasm.__wbindgen_malloc);
+ const ptr1 = passArray8ToWasm0(state_key, wasm.__wbindgen_malloc);
const len1 = WASM_VECTOR_LEN;
- const ret = wasm.ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_restart_delayed_event(ptr, ptr0, len0, ptr1, len1);
+ const ret = wasm.ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_read_state(ptr, ptr0, len0, ptr1, len1);
return BigInt.asUintN(64, ret);
}
-/**
- * @param {bigint} ptr
- * @param {Uint8Array} room_id
- * @param {Uint8Array} delay_id
- * @returns {bigint}
- */
-export function ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_cancel_delayed_event(ptr, room_id, delay_id) {
- const ptr0 = passArray8ToWasm0(room_id, wasm.__wbindgen_malloc);
- const len0 = WASM_VECTOR_LEN;
- const ptr1 = passArray8ToWasm0(delay_id, wasm.__wbindgen_malloc);
- const len1 = WASM_VECTOR_LEN;
- const ret = wasm.ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_cancel_delayed_event(ptr, ptr0, len0, ptr1, len1);
- return BigInt.asUintN(64, ret);
-}
-
-/**
- * @param {bigint} ptr
- * @param {Uint8Array} request
- * @returns {bigint}
- */
-export function ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_delegate_delayed_leave_via_homeserver(ptr, request) {
- const ptr0 = passArray8ToWasm0(request, wasm.__wbindgen_malloc);
- const len0 = WASM_VECTOR_LEN;
- const ret = wasm.ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_delegate_delayed_leave_via_homeserver(ptr, ptr0, len0);
- return BigInt.asUintN(64, ret);
-}
-
-/**
- * @param {bigint} ptr
- * @param {Uint8Array} request
- * @returns {bigint}
- */
-export function ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_delegate_delayed_leave_via_transport(ptr, request) {
- const ptr0 = passArray8ToWasm0(request, wasm.__wbindgen_malloc);
- const len0 = WASM_VECTOR_LEN;
- const ret = wasm.ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_delegate_delayed_leave_via_transport(ptr, ptr0, len0);
- return BigInt.asUintN(64, ret);
-}
-
-/**
- * @param {bigint} ptr
- * @param {RustCallStatus} f_status_
- * @returns {number}
- */
-export function ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_is_homeserver_connected(ptr, f_status_) {
- _assertClass(f_status_, RustCallStatus);
- const ret = wasm.ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_is_homeserver_connected(ptr, f_status_.__wbg_ptr);
- return ret;
-}
-
/**
* @param {bigint} ptr
* @param {bigint} sink
* @param {RustCallStatus} f_status_
*/
-export function ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_subscribe_connectivity(ptr, sink, f_status_) {
+export function ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_subscribe_room_events(ptr, sink, f_status_) {
_assertClass(f_status_, RustCallStatus);
- wasm.ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_subscribe_connectivity(ptr, sink, f_status_.__wbg_ptr);
+ wasm.ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_subscribe_room_events(ptr, sink, f_status_.__wbg_ptr);
+}
+
+/**
+ * @param {bigint} ptr
+ * @param {bigint} sink
+ * @param {RustCallStatus} f_status_
+ */
+export function ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_subscribe_to_device_events(ptr, sink, f_status_) {
+ _assertClass(f_status_, RustCallStatus);
+ wasm.ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_subscribe_to_device_events(ptr, sink, f_status_.__wbg_ptr);
+}
+
+/**
+ * @param {bigint} ptr
+ * @param {bigint} sink
+ * @param {RustCallStatus} f_status_
+ */
+export function ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_subscribe_state_updates(ptr, sink, f_status_) {
+ _assertClass(f_status_, RustCallStatus);
+ wasm.ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_subscribe_state_updates(ptr, sink, f_status_.__wbg_ptr);
+}
+
+/**
+ * @param {bigint} ptr
+ * @param {Uint8Array} event_json
+ * @param {Uint8Array} origin
+ * @param {RustCallStatus} f_status_
+ * @returns {number}
+ */
+export function ubrn_uniffi_matrix_rtc_fn_method_roomeventsink_emit(ptr, event_json, origin, f_status_) {
+ const ptr0 = passArray8ToWasm0(event_json, wasm.__wbindgen_malloc);
+ const len0 = WASM_VECTOR_LEN;
+ const ptr1 = passArray8ToWasm0(origin, wasm.__wbindgen_malloc);
+ const len1 = WASM_VECTOR_LEN;
+ _assertClass(f_status_, RustCallStatus);
+ const ret = wasm.ubrn_uniffi_matrix_rtc_fn_method_roomeventsink_emit(ptr, ptr0, len0, ptr1, len1, f_status_.__wbg_ptr);
+ return ret;
}
/**
@@ -645,9 +686,9 @@ export function ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_subscribe_
* @param {RustCallStatus} f_status_
* @returns {bigint}
*/
-export function ubrn_uniffi_matrix_rtc_fn_clone_membershipslistener(handle, f_status_) {
+export function ubrn_uniffi_matrix_rtc_fn_clone_sessionlistener(handle, f_status_) {
_assertClass(f_status_, RustCallStatus);
- const ret = wasm.ubrn_uniffi_matrix_rtc_fn_clone_membershipslistener(handle, f_status_.__wbg_ptr);
+ const ret = wasm.ubrn_uniffi_matrix_rtc_fn_clone_sessionlistener(handle, f_status_.__wbg_ptr);
return BigInt.asUintN(64, ret);
}
@@ -655,9 +696,16 @@ export function ubrn_uniffi_matrix_rtc_fn_clone_membershipslistener(handle, f_st
* @param {bigint} handle
* @param {RustCallStatus} f_status_
*/
-export function ubrn_uniffi_matrix_rtc_fn_free_membershipslistener(handle, f_status_) {
+export function ubrn_uniffi_matrix_rtc_fn_free_sessionlistener(handle, f_status_) {
_assertClass(f_status_, RustCallStatus);
- wasm.ubrn_uniffi_matrix_rtc_fn_free_membershipslistener(handle, f_status_.__wbg_ptr);
+ wasm.ubrn_uniffi_matrix_rtc_fn_free_sessionlistener(handle, f_status_.__wbg_ptr);
+}
+
+/**
+ * @param {any} vtable
+ */
+export function ubrn_uniffi_matrix_rtc_fn_init_callback_vtable_sessionlistener(vtable) {
+ wasm.ubrn_uniffi_matrix_rtc_fn_init_callback_vtable_sessionlistener(vtable);
}
/**
@@ -721,28 +769,30 @@ export function ubrn_uniffi_matrix_rtc_fn_clone_statuslistener(handle, f_status_
* @param {bigint} handle
* @param {RustCallStatus} f_status_
*/
-export function ubrn_uniffi_matrix_rtc_fn_free_statuslistener(handle, f_status_) {
+export function ubrn_uniffi_matrix_rtc_fn_free_connectionslistener(handle, f_status_) {
_assertClass(f_status_, RustCallStatus);
- wasm.ubrn_uniffi_matrix_rtc_fn_free_statuslistener(handle, f_status_.__wbg_ptr);
-}
-
-/**
- * @param {any} vtable
- */
-export function ubrn_uniffi_matrix_rtc_fn_init_callback_vtable_statuslistener(vtable) {
- wasm.ubrn_uniffi_matrix_rtc_fn_init_callback_vtable_statuslistener(vtable);
+ wasm.ubrn_uniffi_matrix_rtc_fn_free_connectionslistener(handle, f_status_.__wbg_ptr);
}
/**
* @param {bigint} ptr
- * @param {Uint8Array} status
+ * @param {RustCallStatus} f_status_
+ * @returns {number}
+ */
+export function ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_is_homeserver_connected(ptr, f_status_) {
+ _assertClass(f_status_, RustCallStatus);
+ const ret = wasm.ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_is_homeserver_connected(ptr, f_status_.__wbg_ptr);
+ return ret;
+}
+
+/**
+ * @param {bigint} ptr
+ * @param {bigint} sink
* @param {RustCallStatus} f_status_
*/
-export function ubrn_uniffi_matrix_rtc_fn_method_statuslistener_on_status_change(ptr, status, f_status_) {
- const ptr0 = passArray8ToWasm0(status, wasm.__wbindgen_malloc);
- const len0 = WASM_VECTOR_LEN;
+export function ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_subscribe_connectivity(ptr, sink, f_status_) {
_assertClass(f_status_, RustCallStatus);
- wasm.ubrn_uniffi_matrix_rtc_fn_method_statuslistener_on_status_change(ptr, ptr0, len0, f_status_.__wbg_ptr);
+ wasm.ubrn_uniffi_matrix_rtc_fn_method_matrixdrivercallback_subscribe_connectivity(ptr, sink, f_status_.__wbg_ptr);
}
/**
@@ -750,9 +800,9 @@ export function ubrn_uniffi_matrix_rtc_fn_method_statuslistener_on_status_change
* @param {RustCallStatus} f_status_
* @returns {bigint}
*/
-export function ubrn_uniffi_matrix_rtc_fn_clone_todevicesink(handle, f_status_) {
+export function ubrn_uniffi_matrix_rtc_fn_clone_membershipslistener(handle, f_status_) {
_assertClass(f_status_, RustCallStatus);
- const ret = wasm.ubrn_uniffi_matrix_rtc_fn_clone_todevicesink(handle, f_status_.__wbg_ptr);
+ const ret = wasm.ubrn_uniffi_matrix_rtc_fn_clone_membershipslistener(handle, f_status_.__wbg_ptr);
return BigInt.asUintN(64, ret);
}
@@ -760,9 +810,9 @@ export function ubrn_uniffi_matrix_rtc_fn_clone_todevicesink(handle, f_status_)
* @param {bigint} handle
* @param {RustCallStatus} f_status_
*/
-export function ubrn_uniffi_matrix_rtc_fn_free_connectionslistener(handle, f_status_) {
+export function ubrn_uniffi_matrix_rtc_fn_free_membershipslistener(handle, f_status_) {
_assertClass(f_status_, RustCallStatus);
- wasm.ubrn_uniffi_matrix_rtc_fn_free_connectionslistener(handle, f_status_.__wbg_ptr);
+ wasm.ubrn_uniffi_matrix_rtc_fn_free_membershipslistener(handle, f_status_.__wbg_ptr);
}
/**
@@ -804,20 +854,110 @@ export function ubrn_uniffi_matrix_rtc_fn_free_roomeventsink(handle, f_status_)
wasm.ubrn_uniffi_matrix_rtc_fn_free_roomeventsink(handle, f_status_.__wbg_ptr);
}
+/**
+ * @param {bigint} handle
+ * @param {RustCallStatus} f_status_
+ */
+export function ubrn_uniffi_matrix_rtc_fn_free_statuslistener(handle, f_status_) {
+ _assertClass(f_status_, RustCallStatus);
+ wasm.ubrn_uniffi_matrix_rtc_fn_free_statuslistener(handle, f_status_.__wbg_ptr);
+}
+
+/**
+ * @param {any} vtable
+ */
+export function ubrn_uniffi_matrix_rtc_fn_init_callback_vtable_statuslistener(vtable) {
+ wasm.ubrn_uniffi_matrix_rtc_fn_init_callback_vtable_statuslistener(vtable);
+}
+
/**
* @param {bigint} ptr
- * @param {Uint8Array} event_json
- * @param {Uint8Array} origin
+ * @param {Uint8Array} status
+ * @param {RustCallStatus} f_status_
+ */
+export function ubrn_uniffi_matrix_rtc_fn_method_statuslistener_on_status_change(ptr, status, f_status_) {
+ const ptr0 = passArray8ToWasm0(status, wasm.__wbindgen_malloc);
+ const len0 = WASM_VECTOR_LEN;
+ _assertClass(f_status_, RustCallStatus);
+ wasm.ubrn_uniffi_matrix_rtc_fn_method_statuslistener_on_status_change(ptr, ptr0, len0, f_status_.__wbg_ptr);
+}
+
+/**
+ * @param {bigint} handle
* @param {RustCallStatus} f_status_
* @returns {number}
*/
-export function ubrn_uniffi_matrix_rtc_fn_method_roomeventsink_emit(ptr, event_json, origin, f_status_) {
- const ptr0 = passArray8ToWasm0(event_json, wasm.__wbindgen_malloc);
- const len0 = WASM_VECTOR_LEN;
- const ptr1 = passArray8ToWasm0(origin, wasm.__wbindgen_malloc);
- const len1 = WASM_VECTOR_LEN;
+export function ubrn_ffi_matrix_rtc_rust_future_complete_u8(handle, f_status_) {
_assertClass(f_status_, RustCallStatus);
- const ret = wasm.ubrn_uniffi_matrix_rtc_fn_method_roomeventsink_emit(ptr, ptr0, len0, ptr1, len1, f_status_.__wbg_ptr);
+ const ret = wasm.ubrn_ffi_matrix_rtc_rust_future_complete_u8(handle, f_status_.__wbg_ptr);
+ return ret;
+}
+
+/**
+ * @param {bigint} handle
+ * @param {any} callback
+ * @param {bigint} callback_data
+ */
+export function ubrn_ffi_matrix_rtc_rust_future_poll_i8(handle, callback, callback_data) {
+ wasm.ubrn_ffi_matrix_rtc_rust_future_poll_i8(handle, callback, callback_data);
+}
+
+/**
+ * @param {bigint} handle
+ */
+export function ubrn_ffi_matrix_rtc_rust_future_cancel_i8(handle) {
+ wasm.ubrn_ffi_matrix_rtc_rust_future_cancel_i8(handle);
+}
+
+/**
+ * @param {bigint} handle
+ */
+export function ubrn_ffi_matrix_rtc_rust_future_free_i8(handle) {
+ wasm.ubrn_ffi_matrix_rtc_rust_future_free_i8(handle);
+}
+
+/**
+ * @param {bigint} handle
+ * @param {RustCallStatus} f_status_
+ * @returns {number}
+ */
+export function ubrn_ffi_matrix_rtc_rust_future_complete_i8(handle, f_status_) {
+ _assertClass(f_status_, RustCallStatus);
+ const ret = wasm.ubrn_ffi_matrix_rtc_rust_future_complete_i8(handle, f_status_.__wbg_ptr);
+ return ret;
+}
+
+/**
+ * @param {bigint} handle
+ * @param {any} callback
+ * @param {bigint} callback_data
+ */
+export function ubrn_ffi_matrix_rtc_rust_future_poll_u16(handle, callback, callback_data) {
+ wasm.ubrn_ffi_matrix_rtc_rust_future_poll_u16(handle, callback, callback_data);
+}
+
+/**
+ * @param {bigint} handle
+ */
+export function ubrn_ffi_matrix_rtc_rust_future_cancel_u16(handle) {
+ wasm.ubrn_ffi_matrix_rtc_rust_future_cancel_u16(handle);
+}
+
+/**
+ * @param {bigint} handle
+ */
+export function ubrn_ffi_matrix_rtc_rust_future_free_u16(handle) {
+ wasm.ubrn_ffi_matrix_rtc_rust_future_free_u16(handle);
+}
+
+/**
+ * @param {bigint} handle
+ * @param {RustCallStatus} f_status_
+ * @returns {number}
+ */
+export function ubrn_ffi_matrix_rtc_rust_future_complete_u16(handle, f_status_) {
+ _assertClass(f_status_, RustCallStatus);
+ const ret = wasm.ubrn_ffi_matrix_rtc_rust_future_complete_u16(handle, f_status_.__wbg_ptr);
return ret;
}
@@ -826,28 +966,12 @@ export function ubrn_uniffi_matrix_rtc_fn_method_roomeventsink_emit(ptr, event_j
* @param {RustCallStatus} f_status_
* @returns {bigint}
*/
-export function ubrn_uniffi_matrix_rtc_fn_clone_sessionlistener(handle, f_status_) {
+export function ubrn_uniffi_matrix_rtc_fn_clone_todevicesink(handle, f_status_) {
_assertClass(f_status_, RustCallStatus);
- const ret = wasm.ubrn_uniffi_matrix_rtc_fn_clone_sessionlistener(handle, f_status_.__wbg_ptr);
+ const ret = wasm.ubrn_uniffi_matrix_rtc_fn_clone_todevicesink(handle, f_status_.__wbg_ptr);
return BigInt.asUintN(64, ret);
}
-/**
- * @param {bigint} handle
- * @param {RustCallStatus} f_status_
- */
-export function ubrn_uniffi_matrix_rtc_fn_free_sessionlistener(handle, f_status_) {
- _assertClass(f_status_, RustCallStatus);
- wasm.ubrn_uniffi_matrix_rtc_fn_free_sessionlistener(handle, f_status_.__wbg_ptr);
-}
-
-/**
- * @param {any} vtable
- */
-export function ubrn_uniffi_matrix_rtc_fn_init_callback_vtable_sessionlistener(vtable) {
- wasm.ubrn_uniffi_matrix_rtc_fn_init_callback_vtable_sessionlistener(vtable);
-}
-
/**
* @param {bigint} handle
* @param {RustCallStatus} f_status_
@@ -901,83 +1025,6 @@ export function ubrn_uniffi_matrix_rtc_fn_func_compute_sessions_from_events(even
return v3;
}
-/**
- * @param {bigint} handle
- * @param {any} callback
- * @param {bigint} callback_data
- */
-export function ubrn_ffi_matrix_rtc_rust_future_poll_u16(handle, callback, callback_data) {
- wasm.ubrn_ffi_matrix_rtc_rust_future_poll_u16(handle, callback, callback_data);
-}
-
-/**
- * @param {bigint} handle
- */
-export function ubrn_ffi_matrix_rtc_rust_future_cancel_u16(handle) {
- wasm.ubrn_ffi_matrix_rtc_rust_future_cancel_u16(handle);
-}
-
-/**
- * @param {bigint} handle
- */
-export function ubrn_ffi_matrix_rtc_rust_future_free_u16(handle) {
- wasm.ubrn_ffi_matrix_rtc_rust_future_free_u16(handle);
-}
-
-/**
- * @param {bigint} handle
- * @param {RustCallStatus} f_status_
- * @returns {number}
- */
-export function ubrn_ffi_matrix_rtc_rust_future_complete_u16(handle, f_status_) {
- _assertClass(f_status_, RustCallStatus);
- const ret = wasm.ubrn_ffi_matrix_rtc_rust_future_complete_u16(handle, f_status_.__wbg_ptr);
- return ret;
-}
-
-/**
- * @param {bigint} handle
- * @param {any} callback
- * @param {bigint} callback_data
- */
-export function ubrn_ffi_matrix_rtc_rust_future_poll_i16(handle, callback, callback_data) {
- wasm.ubrn_ffi_matrix_rtc_rust_future_poll_i16(handle, callback, callback_data);
-}
-
-/**
- * @param {bigint} handle
- */
-export function ubrn_ffi_matrix_rtc_rust_future_cancel_i16(handle) {
- wasm.ubrn_ffi_matrix_rtc_rust_future_cancel_i16(handle);
-}
-
-/**
- * @param {bigint} handle
- */
-export function ubrn_ffi_matrix_rtc_rust_future_free_i16(handle) {
- wasm.ubrn_ffi_matrix_rtc_rust_future_free_i16(handle);
-}
-
-/**
- * @param {bigint} handle
- * @param {RustCallStatus} f_status_
- * @returns {number}
- */
-export function ubrn_ffi_matrix_rtc_rust_future_complete_i16(handle, f_status_) {
- _assertClass(f_status_, RustCallStatus);
- const ret = wasm.ubrn_ffi_matrix_rtc_rust_future_complete_i16(handle, f_status_.__wbg_ptr);
- return ret;
-}
-
-/**
- * @param {bigint} handle
- * @param {any} callback
- * @param {bigint} callback_data
- */
-export function ubrn_ffi_matrix_rtc_rust_future_poll_u32(handle, callback, callback_data) {
- wasm.ubrn_ffi_matrix_rtc_rust_future_poll_u32(handle, callback, callback_data);
-}
-
/**
* @param {Uint8Array} impairment
* @param {RustCallStatus} f_status_
@@ -993,6 +1040,18 @@ export function ubrn_uniffi_matrix_rtc_fn_func_impairment_severity(impairment, f
return v2;
}
+/**
+ * @param {bigint} sink
+ * @param {Uint8Array} max_level
+ * @param {RustCallStatus} f_status_
+ */
+export function ubrn_uniffi_matrix_rtc_fn_func_set_log_sink(sink, max_level, f_status_) {
+ const ptr0 = passArray8ToWasm0(max_level, wasm.__wbindgen_malloc);
+ const len0 = WASM_VECTOR_LEN;
+ _assertClass(f_status_, RustCallStatus);
+ wasm.ubrn_uniffi_matrix_rtc_fn_func_set_log_sink(sink, ptr0, len0, f_status_.__wbg_ptr);
+}
+
/**
* @param {bigint} handle
* @param {any} callback
@@ -1016,176 +1075,27 @@ export function ubrn_ffi_matrix_rtc_rust_future_free_u8(handle) {
wasm.ubrn_ffi_matrix_rtc_rust_future_free_u8(handle);
}
-/**
- * @param {bigint} handle
- * @param {RustCallStatus} f_status_
- * @returns {number}
- */
-export function ubrn_ffi_matrix_rtc_rust_future_complete_u8(handle, f_status_) {
- _assertClass(f_status_, RustCallStatus);
- const ret = wasm.ubrn_ffi_matrix_rtc_rust_future_complete_u8(handle, f_status_.__wbg_ptr);
- return ret;
-}
-
/**
* @param {bigint} handle
* @param {any} callback
* @param {bigint} callback_data
*/
-export function ubrn_ffi_matrix_rtc_rust_future_poll_i8(handle, callback, callback_data) {
- wasm.ubrn_ffi_matrix_rtc_rust_future_poll_i8(handle, callback, callback_data);
+export function ubrn_ffi_matrix_rtc_rust_future_poll_i16(handle, callback, callback_data) {
+ wasm.ubrn_ffi_matrix_rtc_rust_future_poll_i16(handle, callback, callback_data);
}
/**
* @param {bigint} handle
*/
-export function ubrn_ffi_matrix_rtc_rust_future_cancel_i8(handle) {
- wasm.ubrn_ffi_matrix_rtc_rust_future_cancel_i8(handle);
+export function ubrn_ffi_matrix_rtc_rust_future_cancel_i16(handle) {
+ wasm.ubrn_ffi_matrix_rtc_rust_future_cancel_i16(handle);
}
/**
* @param {bigint} handle
*/
-export function ubrn_ffi_matrix_rtc_rust_future_free_i8(handle) {
- wasm.ubrn_ffi_matrix_rtc_rust_future_free_i8(handle);
-}
-
-/**
- * @param {bigint} handle
- * @param {RustCallStatus} f_status_
- * @returns {number}
- */
-export function ubrn_ffi_matrix_rtc_rust_future_complete_i8(handle, f_status_) {
- _assertClass(f_status_, RustCallStatus);
- const ret = wasm.ubrn_ffi_matrix_rtc_rust_future_complete_i8(handle, f_status_.__wbg_ptr);
- return ret;
-}
-
-/**
- * @param {bigint} handle
- */
-export function ubrn_ffi_matrix_rtc_rust_future_cancel_u32(handle) {
- wasm.ubrn_ffi_matrix_rtc_rust_future_cancel_u32(handle);
-}
-
-/**
- * @param {bigint} handle
- */
-export function ubrn_ffi_matrix_rtc_rust_future_free_u32(handle) {
- wasm.ubrn_ffi_matrix_rtc_rust_future_free_u32(handle);
-}
-
-/**
- * @param {bigint} handle
- * @param {RustCallStatus} f_status_
- * @returns {number}
- */
-export function ubrn_ffi_matrix_rtc_rust_future_complete_u32(handle, f_status_) {
- _assertClass(f_status_, RustCallStatus);
- const ret = wasm.ubrn_ffi_matrix_rtc_rust_future_complete_u32(handle, f_status_.__wbg_ptr);
- return ret >>> 0;
-}
-
-/**
- * @param {bigint} handle
- */
-export function ubrn_ffi_matrix_rtc_rust_future_cancel_i64(handle) {
- wasm.ubrn_ffi_matrix_rtc_rust_future_cancel_i64(handle);
-}
-
-/**
- * @param {bigint} handle
- */
-export function ubrn_ffi_matrix_rtc_rust_future_free_i64(handle) {
- wasm.ubrn_ffi_matrix_rtc_rust_future_free_i64(handle);
-}
-
-/**
- * @param {bigint} handle
- * @param {RustCallStatus} f_status_
- * @returns {bigint}
- */
-export function ubrn_ffi_matrix_rtc_rust_future_complete_i64(handle, f_status_) {
- _assertClass(f_status_, RustCallStatus);
- const ret = wasm.ubrn_ffi_matrix_rtc_rust_future_complete_i64(handle, f_status_.__wbg_ptr);
- return ret;
-}
-
-/**
- * @param {bigint} handle
- * @param {any} callback
- * @param {bigint} callback_data
- */
-export function ubrn_ffi_matrix_rtc_rust_future_poll_f32(handle, callback, callback_data) {
- wasm.ubrn_ffi_matrix_rtc_rust_future_poll_f32(handle, callback, callback_data);
-}
-
-/**
- * @param {bigint} handle
- */
-export function ubrn_ffi_matrix_rtc_rust_future_cancel_f32(handle) {
- wasm.ubrn_ffi_matrix_rtc_rust_future_cancel_f32(handle);
-}
-
-/**
- * @param {bigint} handle
- */
-export function ubrn_ffi_matrix_rtc_rust_future_free_f32(handle) {
- wasm.ubrn_ffi_matrix_rtc_rust_future_free_f32(handle);
-}
-
-/**
- * @param {bigint} handle
- * @param {RustCallStatus} f_status_
- * @returns {number}
- */
-export function ubrn_ffi_matrix_rtc_rust_future_complete_f32(handle, f_status_) {
- _assertClass(f_status_, RustCallStatus);
- const ret = wasm.ubrn_ffi_matrix_rtc_rust_future_complete_f32(handle, f_status_.__wbg_ptr);
- return ret;
-}
-
-/**
- * @param {bigint} handle
- * @param {any} callback
- * @param {bigint} callback_data
- */
-export function ubrn_ffi_matrix_rtc_rust_future_poll_f64(handle, callback, callback_data) {
- wasm.ubrn_ffi_matrix_rtc_rust_future_poll_f64(handle, callback, callback_data);
-}
-
-/**
- * @param {bigint} handle
- * @param {any} callback
- * @param {bigint} callback_data
- */
-export function ubrn_ffi_matrix_rtc_rust_future_poll_i32(handle, callback, callback_data) {
- wasm.ubrn_ffi_matrix_rtc_rust_future_poll_i32(handle, callback, callback_data);
-}
-
-/**
- * @param {bigint} handle
- */
-export function ubrn_ffi_matrix_rtc_rust_future_cancel_i32(handle) {
- wasm.ubrn_ffi_matrix_rtc_rust_future_cancel_i32(handle);
-}
-
-/**
- * @param {bigint} handle
- */
-export function ubrn_ffi_matrix_rtc_rust_future_free_i32(handle) {
- wasm.ubrn_ffi_matrix_rtc_rust_future_free_i32(handle);
-}
-
-/**
- * @param {bigint} handle
- * @param {RustCallStatus} f_status_
- * @returns {number}
- */
-export function ubrn_ffi_matrix_rtc_rust_future_complete_i32(handle, f_status_) {
- _assertClass(f_status_, RustCallStatus);
- const ret = wasm.ubrn_ffi_matrix_rtc_rust_future_complete_i32(handle, f_status_.__wbg_ptr);
- return ret;
+export function ubrn_ffi_matrix_rtc_rust_future_free_i16(handle) {
+ wasm.ubrn_ffi_matrix_rtc_rust_future_free_i16(handle);
}
/**
@@ -1234,15 +1144,26 @@ export function ubrn_ffi_matrix_rtc_rust_future_poll_i64(handle, callback, callb
/**
* @param {bigint} handle
*/
-export function ubrn_ffi_matrix_rtc_rust_future_cancel_f64(handle) {
- wasm.ubrn_ffi_matrix_rtc_rust_future_cancel_f64(handle);
+export function ubrn_ffi_matrix_rtc_rust_future_cancel_i64(handle) {
+ wasm.ubrn_ffi_matrix_rtc_rust_future_cancel_i64(handle);
}
/**
* @param {bigint} handle
*/
-export function ubrn_ffi_matrix_rtc_rust_future_free_f64(handle) {
- wasm.ubrn_ffi_matrix_rtc_rust_future_free_f64(handle);
+export function ubrn_ffi_matrix_rtc_rust_future_free_i64(handle) {
+ wasm.ubrn_ffi_matrix_rtc_rust_future_free_i64(handle);
+}
+
+/**
+ * @param {bigint} handle
+ * @param {RustCallStatus} f_status_
+ * @returns {bigint}
+ */
+export function ubrn_ffi_matrix_rtc_rust_future_complete_i64(handle, f_status_) {
+ _assertClass(f_status_, RustCallStatus);
+ const ret = wasm.ubrn_ffi_matrix_rtc_rust_future_complete_i64(handle, f_status_.__wbg_ptr);
+ return ret;
}
/**
@@ -1250,9 +1171,9 @@ export function ubrn_ffi_matrix_rtc_rust_future_free_f64(handle) {
* @param {RustCallStatus} f_status_
* @returns {number}
*/
-export function ubrn_ffi_matrix_rtc_rust_future_complete_f64(handle, f_status_) {
+export function ubrn_ffi_matrix_rtc_rust_future_complete_i16(handle, f_status_) {
_assertClass(f_status_, RustCallStatus);
- const ret = wasm.ubrn_ffi_matrix_rtc_rust_future_complete_f64(handle, f_status_.__wbg_ptr);
+ const ret = wasm.ubrn_ffi_matrix_rtc_rust_future_complete_i16(handle, f_status_.__wbg_ptr);
return ret;
}
@@ -1261,114 +1182,101 @@ export function ubrn_ffi_matrix_rtc_rust_future_complete_f64(handle, f_status_)
* @param {any} callback
* @param {bigint} callback_data
*/
-export function ubrn_ffi_matrix_rtc_rust_future_poll_rust_buffer(handle, callback, callback_data) {
- wasm.ubrn_ffi_matrix_rtc_rust_future_poll_rust_buffer(handle, callback, callback_data);
-}
-
-/**
- * @returns {number}
- */
-export function ubrn_uniffi_matrix_rtc_checksum_func_impairment_severity() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_func_impairment_severity();
- return ret;
-}
-
-/**
- * @returns {number}
- */
-export function ubrn_uniffi_matrix_rtc_checksum_method_connectionslistener_on_connections_change() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_connectionslistener_on_connections_change();
- return ret;
-}
-
-/**
- * @returns {number}
- */
-export function ubrn_uniffi_matrix_rtc_checksum_method_connectivitysink_emit() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_connectivitysink_emit();
- return ret;
-}
-
-/**
- * @returns {number}
- */
-export function ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_close_slot() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_close_slot();
- return ret;
-}
-
-/**
- * @returns {number}
- */
-export function ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_connection_problems() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_connection_problems();
- return ret;
-}
-
-/**
- * @returns {number}
- */
-export function ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_connections() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_connections();
- return ret;
-}
-
-/**
- * @returns {number}
- */
-export function ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_debug_snapshot() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_debug_snapshot();
- return ret;
-}
-
-/**
- * @returns {number}
- */
-export function ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_is_homeserver_connected() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_is_homeserver_connected();
- return ret;
-}
-
-/**
- * @returns {number}
- */
-export function ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_join() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_join();
- return ret;
-}
-
-/**
- * @param {any} vtable
- */
-export function ubrn_uniffi_matrix_rtc_fn_init_callback_vtable_connectionslistener(vtable) {
- wasm.ubrn_uniffi_matrix_rtc_fn_init_callback_vtable_connectionslistener(vtable);
+export function ubrn_ffi_matrix_rtc_rust_future_poll_u32(handle, callback, callback_data) {
+ wasm.ubrn_ffi_matrix_rtc_rust_future_poll_u32(handle, callback, callback_data);
}
/**
* @param {bigint} handle
*/
-export function ubrn_ffi_matrix_rtc_rust_future_cancel_rust_buffer(handle) {
- wasm.ubrn_ffi_matrix_rtc_rust_future_cancel_rust_buffer(handle);
+export function ubrn_ffi_matrix_rtc_rust_future_cancel_u32(handle) {
+ wasm.ubrn_ffi_matrix_rtc_rust_future_cancel_u32(handle);
}
/**
* @param {bigint} handle
*/
-export function ubrn_ffi_matrix_rtc_rust_future_free_rust_buffer(handle) {
- wasm.ubrn_ffi_matrix_rtc_rust_future_free_rust_buffer(handle);
+export function ubrn_ffi_matrix_rtc_rust_future_free_u32(handle) {
+ wasm.ubrn_ffi_matrix_rtc_rust_future_free_u32(handle);
}
/**
* @param {bigint} handle
* @param {RustCallStatus} f_status_
- * @returns {Uint8Array}
+ * @returns {number}
*/
-export function ubrn_ffi_matrix_rtc_rust_future_complete_rust_buffer(handle, f_status_) {
+export function ubrn_ffi_matrix_rtc_rust_future_complete_u32(handle, f_status_) {
_assertClass(f_status_, RustCallStatus);
- const ret = wasm.ubrn_ffi_matrix_rtc_rust_future_complete_rust_buffer(handle, f_status_.__wbg_ptr);
- var v1 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
- wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
- return v1;
+ const ret = wasm.ubrn_ffi_matrix_rtc_rust_future_complete_u32(handle, f_status_.__wbg_ptr);
+ return ret >>> 0;
+}
+
+/**
+ * @param {bigint} handle
+ * @param {any} callback
+ * @param {bigint} callback_data
+ */
+export function ubrn_ffi_matrix_rtc_rust_future_poll_i32(handle, callback, callback_data) {
+ wasm.ubrn_ffi_matrix_rtc_rust_future_poll_i32(handle, callback, callback_data);
+}
+
+/**
+ * @param {bigint} handle
+ */
+export function ubrn_ffi_matrix_rtc_rust_future_cancel_i32(handle) {
+ wasm.ubrn_ffi_matrix_rtc_rust_future_cancel_i32(handle);
+}
+
+/**
+ * @param {bigint} handle
+ */
+export function ubrn_ffi_matrix_rtc_rust_future_free_i32(handle) {
+ wasm.ubrn_ffi_matrix_rtc_rust_future_free_i32(handle);
+}
+
+/**
+ * @param {bigint} handle
+ * @param {RustCallStatus} f_status_
+ * @returns {number}
+ */
+export function ubrn_ffi_matrix_rtc_rust_future_complete_i32(handle, f_status_) {
+ _assertClass(f_status_, RustCallStatus);
+ const ret = wasm.ubrn_ffi_matrix_rtc_rust_future_complete_i32(handle, f_status_.__wbg_ptr);
+ return ret;
+}
+
+/**
+ * @param {bigint} handle
+ * @param {any} callback
+ * @param {bigint} callback_data
+ */
+export function ubrn_ffi_matrix_rtc_rust_future_poll_f32(handle, callback, callback_data) {
+ wasm.ubrn_ffi_matrix_rtc_rust_future_poll_f32(handle, callback, callback_data);
+}
+
+/**
+ * @param {bigint} handle
+ */
+export function ubrn_ffi_matrix_rtc_rust_future_cancel_f32(handle) {
+ wasm.ubrn_ffi_matrix_rtc_rust_future_cancel_f32(handle);
+}
+
+/**
+ * @param {bigint} handle
+ */
+export function ubrn_ffi_matrix_rtc_rust_future_free_f32(handle) {
+ wasm.ubrn_ffi_matrix_rtc_rust_future_free_f32(handle);
+}
+
+/**
+ * @param {bigint} handle
+ * @param {RustCallStatus} f_status_
+ * @returns {number}
+ */
+export function ubrn_ffi_matrix_rtc_rust_future_complete_f32(handle, f_status_) {
+ _assertClass(f_status_, RustCallStatus);
+ const ret = wasm.ubrn_ffi_matrix_rtc_rust_future_complete_f32(handle, f_status_.__wbg_ptr);
+ return ret;
}
/**
@@ -1414,120 +1322,133 @@ export function ubrn_uniffi_matrix_rtc_checksum_func_compute_sessions_from_event
/**
* @returns {number}
*/
-export function ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_key_map() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_key_map();
+export function ubrn_uniffi_matrix_rtc_checksum_func_impairment_severity() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_func_impairment_severity();
return ret;
}
/**
* @returns {number}
*/
-export function ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_leave() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_leave();
+export function ubrn_uniffi_matrix_rtc_checksum_func_set_log_sink() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_func_set_log_sink();
return ret;
}
/**
* @returns {number}
*/
-export function ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_memberships() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_memberships();
+export function ubrn_uniffi_matrix_rtc_checksum_method_connectionslistener_on_connections_change() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_connectionslistener_on_connections_change();
return ret;
}
/**
* @returns {number}
*/
-export function ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_set_session_listener() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_set_session_listener();
+export function ubrn_uniffi_matrix_rtc_checksum_method_connectivitysink_emit() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_connectivitysink_emit();
+ return ret;
+}
+
+/**
+ * @param {any} vtable
+ */
+export function ubrn_uniffi_matrix_rtc_fn_init_callback_vtable_connectionslistener(vtable) {
+ wasm.ubrn_uniffi_matrix_rtc_fn_init_callback_vtable_connectionslistener(vtable);
+}
+
+/**
+ * @param {bigint} handle
+ * @param {any} callback
+ * @param {bigint} callback_data
+ */
+export function ubrn_ffi_matrix_rtc_rust_future_poll_f64(handle, callback, callback_data) {
+ wasm.ubrn_ffi_matrix_rtc_rust_future_poll_f64(handle, callback, callback_data);
+}
+
+/**
+ * @param {bigint} handle
+ */
+export function ubrn_ffi_matrix_rtc_rust_future_cancel_f64(handle) {
+ wasm.ubrn_ffi_matrix_rtc_rust_future_cancel_f64(handle);
+}
+
+/**
+ * @param {bigint} handle
+ */
+export function ubrn_ffi_matrix_rtc_rust_future_free_f64(handle) {
+ wasm.ubrn_ffi_matrix_rtc_rust_future_free_f64(handle);
+}
+
+/**
+ * @param {bigint} handle
+ * @param {RustCallStatus} f_status_
+ * @returns {number}
+ */
+export function ubrn_ffi_matrix_rtc_rust_future_complete_f64(handle, f_status_) {
+ _assertClass(f_status_, RustCallStatus);
+ const ret = wasm.ubrn_ffi_matrix_rtc_rust_future_complete_f64(handle, f_status_.__wbg_ptr);
+ return ret;
+}
+
+/**
+ * @param {bigint} handle
+ * @param {any} callback
+ * @param {bigint} callback_data
+ */
+export function ubrn_ffi_matrix_rtc_rust_future_poll_rust_buffer(handle, callback, callback_data) {
+ wasm.ubrn_ffi_matrix_rtc_rust_future_poll_rust_buffer(handle, callback, callback_data);
+}
+
+/**
+ * @param {bigint} handle
+ */
+export function ubrn_ffi_matrix_rtc_rust_future_cancel_rust_buffer(handle) {
+ wasm.ubrn_ffi_matrix_rtc_rust_future_cancel_rust_buffer(handle);
+}
+
+/**
+ * @param {bigint} handle
+ */
+export function ubrn_ffi_matrix_rtc_rust_future_free_rust_buffer(handle) {
+ wasm.ubrn_ffi_matrix_rtc_rust_future_free_rust_buffer(handle);
+}
+
+/**
+ * @param {bigint} handle
+ * @param {RustCallStatus} f_status_
+ * @returns {Uint8Array}
+ */
+export function ubrn_ffi_matrix_rtc_rust_future_complete_rust_buffer(handle, f_status_) {
+ _assertClass(f_status_, RustCallStatus);
+ const ret = wasm.ubrn_ffi_matrix_rtc_rust_future_complete_rust_buffer(handle, f_status_.__wbg_ptr);
+ var v1 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
+ wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
+ return v1;
+}
+
+/**
+ * @returns {number}
+ */
+export function ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_close_slot() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_close_slot();
return ret;
}
/**
* @returns {number}
*/
-export function ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_set_status_listener() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_set_status_listener();
+export function ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_connection_problems() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_connection_problems();
return ret;
}
/**
* @returns {number}
*/
-export function ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_status() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_status();
- return ret;
-}
-
-/**
- * @returns {number}
- */
-export function ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_update_application() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_update_application();
- return ret;
-}
-
-/**
- * @returns {number}
- */
-export function ubrn_uniffi_matrix_rtc_checksum_method_keymaplistener_on_key_map_change() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_keymaplistener_on_key_map_change();
- return ret;
-}
-
-/**
- * @returns {number}
- */
-export function ubrn_uniffi_matrix_rtc_checksum_method_keyrejectedlistener_on_key_rejected() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_keyrejectedlistener_on_key_rejected();
- return ret;
-}
-
-/**
- * @returns {number}
- */
-export function ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_send_sticky_event() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_send_sticky_event();
- return ret;
-}
-
-/**
- * @returns {number}
- */
-export function ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_send_state_event() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_send_state_event();
- return ret;
-}
-
-/**
- * @returns {number}
- */
-export function ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_send_delayed_event() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_send_delayed_event();
- return ret;
-}
-
-/**
- * @returns {number}
- */
-export function ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_open_slot() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_open_slot();
- return ret;
-}
-
-/**
- * @returns {number}
- */
-export function ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_own_member_id() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_own_member_id();
- return ret;
-}
-
-/**
- * @returns {number}
- */
-export function ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_own_membership() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_own_membership();
+export function ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_connections() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_connections();
return ret;
}
@@ -1582,104 +1503,120 @@ export function ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_s
/**
* @returns {number}
*/
-export function ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_send_delayed_state_event() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_send_delayed_state_event();
+export function ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_set_session_listener() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_set_session_listener();
return ret;
}
/**
* @returns {number}
*/
-export function ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_restart_delayed_event() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_restart_delayed_event();
+export function ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_set_status_listener() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_set_status_listener();
return ret;
}
/**
* @returns {number}
*/
-export function ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_cancel_delayed_event() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_cancel_delayed_event();
+export function ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_status() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_status();
return ret;
}
/**
* @returns {number}
*/
-export function ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_subscribe_state_updates() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_subscribe_state_updates();
+export function ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_debug_snapshot() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_debug_snapshot();
return ret;
}
/**
* @returns {number}
*/
-export function ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_is_homeserver_connected() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_is_homeserver_connected();
+export function ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_is_homeserver_connected() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_is_homeserver_connected();
return ret;
}
/**
* @returns {number}
*/
-export function ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_subscribe_connectivity() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_subscribe_connectivity();
+export function ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_join() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_join();
return ret;
}
/**
* @returns {number}
*/
-export function ubrn_uniffi_matrix_rtc_checksum_method_membershipslistener_on_memberships_change() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_membershipslistener_on_memberships_change();
+export function ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_key_map() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_key_map();
return ret;
}
/**
* @returns {number}
*/
-export function ubrn_uniffi_matrix_rtc_checksum_method_roomeventsink_emit() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_roomeventsink_emit();
+export function ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_leave() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_leave();
return ret;
}
/**
* @returns {number}
*/
-export function ubrn_uniffi_matrix_rtc_checksum_method_sessionlistener_on_session_change() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_sessionlistener_on_session_change();
+export function ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_memberships() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_memberships();
return ret;
}
/**
* @returns {number}
*/
-export function ubrn_uniffi_matrix_rtc_checksum_method_stateupdatesink_emit() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_stateupdatesink_emit();
+export function ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_open_slot() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_open_slot();
return ret;
}
/**
* @returns {number}
*/
-export function ubrn_uniffi_matrix_rtc_checksum_method_statuslistener_on_status_change() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_statuslistener_on_status_change();
+export function ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_own_member_id() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_own_member_id();
return ret;
}
/**
* @returns {number}
*/
-export function ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_delegate_delayed_leave_via_homeserver() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_delegate_delayed_leave_via_homeserver();
+export function ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_own_membership() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_own_membership();
return ret;
}
/**
* @returns {number}
*/
-export function ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_delegate_delayed_leave_via_transport() {
- const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_delegate_delayed_leave_via_transport();
+export function ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_update_application() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_ffiparticipationmanager_update_application();
+ return ret;
+}
+
+/**
+ * @returns {number}
+ */
+export function ubrn_uniffi_matrix_rtc_checksum_method_keymaplistener_on_key_map_change() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_keymaplistener_on_key_map_change();
+ return ret;
+}
+
+/**
+ * @returns {number}
+ */
+export function ubrn_uniffi_matrix_rtc_checksum_method_keyrejectedlistener_on_key_rejected() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_keyrejectedlistener_on_key_rejected();
return ret;
}
@@ -1739,6 +1676,154 @@ export function ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_subs
return ret;
}
+/**
+ * @returns {number}
+ */
+export function ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_subscribe_state_updates() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_subscribe_state_updates();
+ return ret;
+}
+
+/**
+ * @returns {number}
+ */
+export function ubrn_uniffi_matrix_rtc_checksum_method_logsink_log() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_logsink_log();
+ return ret;
+}
+
+/**
+ * @returns {number}
+ */
+export function ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_send_sticky_event() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_send_sticky_event();
+ return ret;
+}
+
+/**
+ * @returns {number}
+ */
+export function ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_send_state_event() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_send_state_event();
+ return ret;
+}
+
+/**
+ * @returns {number}
+ */
+export function ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_send_delayed_event() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_send_delayed_event();
+ return ret;
+}
+
+/**
+ * @returns {number}
+ */
+export function ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_send_delayed_state_event() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_send_delayed_state_event();
+ return ret;
+}
+
+/**
+ * @returns {number}
+ */
+export function ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_restart_delayed_event() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_restart_delayed_event();
+ return ret;
+}
+
+/**
+ * @returns {number}
+ */
+export function ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_cancel_delayed_event() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_cancel_delayed_event();
+ return ret;
+}
+
+/**
+ * @returns {number}
+ */
+export function ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_delegate_delayed_leave_via_homeserver() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_delegate_delayed_leave_via_homeserver();
+ return ret;
+}
+
+/**
+ * @returns {number}
+ */
+export function ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_delegate_delayed_leave_via_transport() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_delegate_delayed_leave_via_transport();
+ return ret;
+}
+
+/**
+ * @returns {number}
+ */
+export function ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_is_homeserver_connected() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_is_homeserver_connected();
+ return ret;
+}
+
+/**
+ * @returns {number}
+ */
+export function ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_subscribe_connectivity() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_matrixdrivercallback_subscribe_connectivity();
+ return ret;
+}
+
+/**
+ * @returns {number}
+ */
+export function ubrn_uniffi_matrix_rtc_checksum_method_membershipslistener_on_memberships_change() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_membershipslistener_on_memberships_change();
+ return ret;
+}
+
+/**
+ * @returns {number}
+ */
+export function ubrn_uniffi_matrix_rtc_checksum_method_roomeventsink_emit() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_roomeventsink_emit();
+ return ret;
+}
+
+/**
+ * @param {bigint} ptr
+ * @param {Uint8Array} connections
+ * @param {RustCallStatus} f_status_
+ */
+export function ubrn_uniffi_matrix_rtc_fn_method_connectionslistener_on_connections_change(ptr, connections, f_status_) {
+ const ptr0 = passArray8ToWasm0(connections, wasm.__wbindgen_malloc);
+ const len0 = WASM_VECTOR_LEN;
+ _assertClass(f_status_, RustCallStatus);
+ wasm.ubrn_uniffi_matrix_rtc_fn_method_connectionslistener_on_connections_change(ptr, ptr0, len0, f_status_.__wbg_ptr);
+}
+
+/**
+ * @returns {number}
+ */
+export function ubrn_uniffi_matrix_rtc_checksum_method_sessionlistener_on_session_change() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_sessionlistener_on_session_change();
+ return ret;
+}
+
+/**
+ * @returns {number}
+ */
+export function ubrn_uniffi_matrix_rtc_checksum_method_stateupdatesink_emit() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_stateupdatesink_emit();
+ return ret;
+}
+
+/**
+ * @returns {number}
+ */
+export function ubrn_uniffi_matrix_rtc_checksum_method_statuslistener_on_status_change() {
+ const ret = wasm.ubrn_uniffi_matrix_rtc_checksum_method_statuslistener_on_status_change();
+ return ret;
+}
+
/**
* @returns {number}
*/
@@ -1771,18 +1856,6 @@ export function ubrn_ffi_matrix_rtc_uniffi_contract_version() {
return ret >>> 0;
}
-/**
- * @param {bigint} ptr
- * @param {Uint8Array} connections
- * @param {RustCallStatus} f_status_
- */
-export function ubrn_uniffi_matrix_rtc_fn_method_connectionslistener_on_connections_change(ptr, connections, f_status_) {
- const ptr0 = passArray8ToWasm0(connections, wasm.__wbindgen_malloc);
- const len0 = WASM_VECTOR_LEN;
- _assertClass(f_status_, RustCallStatus);
- wasm.ubrn_uniffi_matrix_rtc_fn_method_connectionslistener_on_connections_change(ptr, ptr0, len0, f_status_.__wbg_ptr);
-}
-
/**
* @param {Uint8Array} room_id
* @param {Uint8Array} slot_id
@@ -1999,7 +2072,7 @@ function __wbg_adapter_24(arg0, arg1) {
}
function __wbg_adapter_27(arg0, arg1, arg2) {
- wasm.closure562_externref_shim(arg0, arg1, arg2);
+ wasm.closure572_externref_shim(arg0, arg1, arg2);
}
const ForeignFutureCompleteF32Finalization = (typeof FinalizationRegistry === 'undefined')
@@ -2484,6 +2557,9 @@ function __wbg_get_imports() {
const ret = arg0.call(arg1, BigInt.asUintN(64, arg2), v0, v1, v2, v3, ForeignFutureCompleteRustBuffer.__wrap(arg11), BigInt.asUintN(64, arg12));
return ret;
};
+ imports.wbg.__wbg_call_2317578a4ec85f27 = function(arg0, arg1, arg2) {
+ arg0.call(arg1, BigInt.asUintN(64, arg2));
+ };
imports.wbg.__wbg_call_2798409ff618ef7d = function(arg0, arg1, arg2) {
arg0.call(arg1, BigInt.asUintN(64, arg2));
};
@@ -2523,6 +2599,16 @@ function __wbg_get_imports() {
const ret = arg0.call(arg1, BigInt.asUintN(64, arg2), v0, v1, v2, ForeignFutureCompleteRustBuffer.__wrap(arg9), BigInt.asUintN(64, arg10));
return ret;
};
+ imports.wbg.__wbg_call_63c160b52f6d962d = function(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8) {
+ var v0 = getArrayU8FromWasm0(arg3, arg4).slice();
+ wasm.__wbindgen_free(arg3, arg4 * 1, 1);
+ var v1 = getArrayU8FromWasm0(arg5, arg6).slice();
+ wasm.__wbindgen_free(arg5, arg6 * 1, 1);
+ var v2 = getArrayU8FromWasm0(arg7, arg8).slice();
+ wasm.__wbindgen_free(arg7, arg8 * 1, 1);
+ const ret = arg0.call(arg1, BigInt.asUintN(64, arg2), v0, v1, v2);
+ return ret;
+ };
imports.wbg.__wbg_call_65360e4d1b0f41fa = function(arg0, arg1, arg2) {
arg0.call(arg1, BigInt.asUintN(64, arg2));
};
@@ -2563,6 +2649,10 @@ function __wbg_get_imports() {
const ret = arg0.call(arg1, BigInt.asUintN(64, arg2), v0, v1, arg7 >>> 0, ForeignFutureCompleteRustBuffer.__wrap(arg8), BigInt.asUintN(64, arg9));
return ret;
};
+ imports.wbg.__wbg_call_94b3adcf4a0499cf = function(arg0, arg1, arg2) {
+ const ret = arg0.call(arg1, BigInt.asUintN(64, arg2));
+ return ret;
+ };
imports.wbg.__wbg_call_98882f14cac8324a = function(arg0, arg1, arg2, arg3, arg4, arg5, arg6) {
var v0 = getArrayU8FromWasm0(arg3, arg4).slice();
wasm.__wbindgen_free(arg3, arg4 * 1, 1);
@@ -2755,6 +2845,10 @@ function __wbg_get_imports() {
const ret = arg0.is_homeserver_connected;
return ret;
};
+ imports.wbg.__wbg_log_f27a8f2ebe07bb7c = function(arg0) {
+ const ret = arg0.log;
+ return ret;
+ };
imports.wbg.__wbg_msCrypto_d562bbe83e0d4b91 = function(arg0) {
const ret = arg0.msCrypto;
return ret;
@@ -2983,6 +3077,10 @@ function __wbg_get_imports() {
const ret = arg0.uniffi_clone;
return ret;
};
+ imports.wbg.__wbg_unifficlone_f8d7fe966d3a99d9 = function(arg0) {
+ const ret = arg0.uniffi_clone;
+ return ret;
+ };
imports.wbg.__wbg_unifficlone_fe0240768db0e3d6 = function(arg0) {
const ret = arg0.uniffi_clone;
return ret;
@@ -3011,6 +3109,10 @@ function __wbg_get_imports() {
const ret = arg0.uniffi_free;
return ret;
};
+ imports.wbg.__wbg_uniffifree_b48fc51f60f5be76 = function(arg0) {
+ const ret = arg0.uniffi_free;
+ return ret;
+ };
imports.wbg.__wbg_uniffifree_c315e1fbfd7f9e6a = function(arg0) {
const ret = arg0.uniffi_free;
return ret;
@@ -3028,12 +3130,12 @@ function __wbg_get_imports() {
const ret = false;
return ret;
};
- imports.wbg.__wbindgen_closure_wrapper2259 = function(arg0, arg1, arg2) {
- const ret = makeMutClosure(arg0, arg1, 551, __wbg_adapter_24);
+ imports.wbg.__wbindgen_closure_wrapper2297 = function(arg0, arg1, arg2) {
+ const ret = makeMutClosure(arg0, arg1, 561, __wbg_adapter_24);
return ret;
};
- imports.wbg.__wbindgen_closure_wrapper2283 = function(arg0, arg1, arg2) {
- const ret = makeMutClosure(arg0, arg1, 563, __wbg_adapter_27);
+ imports.wbg.__wbindgen_closure_wrapper2321 = function(arg0, arg1, arg2) {
+ const ret = makeMutClosure(arg0, arg1, 573, __wbg_adapter_27);
return ret;
};
imports.wbg.__wbindgen_init_externref_table = function() {
diff --git a/src/matrix-rtc-sdk/generated/wasm-bindgen/index_bg.wasm b/src/matrix-rtc-sdk/generated/wasm-bindgen/index_bg.wasm
index 972914482..24fdf7b66 100644
Binary files a/src/matrix-rtc-sdk/generated/wasm-bindgen/index_bg.wasm and b/src/matrix-rtc-sdk/generated/wasm-bindgen/index_bg.wasm differ
diff --git a/src/matrix-rtc-sdk/index.test.ts b/src/matrix-rtc-sdk/index.test.ts
index 5f75385a7..3b6025e2d 100644
--- a/src/matrix-rtc-sdk/index.test.ts
+++ b/src/matrix-rtc-sdk/index.test.ts
@@ -5,10 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
-import { beforeAll, describe, expect, it } from "vitest";
+import { beforeAll, describe, expect, it, onTestFinished, vi } from "vitest";
+import { type Logger } from "matrix-js-sdk/lib/logger";
import { initMatrixRtcSdkForTests } from "../utils/test-matrix-rtc";
+import { installMatrixRtcLogSink, matrixRtcLogSink } from "./logSink";
import {
+ FfiLogLevel,
+ setLogSink,
FfiElementCallCompat,
FfiMatrixDriver,
FfiParticipationManager,
@@ -80,6 +84,62 @@ describe("matrix-rtc-sdk", () => {
expect(manager.ownTransportIdentity()).toBeUndefined();
manager.uniffiDestroy();
});
+
+ it("routes the crate's log lines to the installed sink", () => {
+ const lines: { level: FfiLogLevel; target: string; message: string }[] = [];
+ setLogSink(
+ {
+ log: (level, target, message) => {
+ lines.push({ level, target, message });
+ },
+ },
+ FfiLogLevel.Debug,
+ );
+ // Put the default sink back for the other suites.
+ onTestFinished(() => installMatrixRtcLogSink());
+
+ const driver = new FfiMatrixDriver(new InertDriver());
+ const manager = new FfiParticipationManager(
+ ROOM_ID,
+ "m.call#ROOM",
+ "@me:example.org",
+ "MYDEV",
+ driver,
+ {
+ compat: FfiElementCallCompat.Off,
+ manageMediaKeys: false,
+ requireCrossSignedSender: false,
+ useKeyDelayMs: 1000n,
+ },
+ );
+ manager.uniffiDestroy();
+
+ const created = lines.find((l) => l.message.includes("session created"));
+ expect(created).toBeDefined();
+ expect(created?.level).toBe(FfiLogLevel.Info);
+ expect(created?.target).toMatch(/^matrix_rtc::/);
+ });
+
+ it("maps the crate's levels onto a matrix-js-sdk logger", () => {
+ const target = {
+ error: vi.fn(),
+ warn: vi.fn(),
+ info: vi.fn(),
+ debug: vi.fn(),
+ trace: vi.fn(),
+ };
+ const sink = matrixRtcLogSink(target as unknown as Logger);
+ sink.log(FfiLogLevel.Error, "matrix_rtc::a", "boom");
+ sink.log(FfiLogLevel.Warn, "matrix_rtc::a", "hm");
+ sink.log(FfiLogLevel.Info, "matrix_rtc::a", "fyi");
+ sink.log(FfiLogLevel.Debug, "matrix_rtc::a", "dbg");
+ sink.log(FfiLogLevel.Trace, "matrix_rtc::a", "trc");
+ expect(target.error).toHaveBeenCalledWith("[matrix_rtc::a] boom");
+ expect(target.warn).toHaveBeenCalledWith("[matrix_rtc::a] hm");
+ expect(target.info).toHaveBeenCalledWith("[matrix_rtc::a] fyi");
+ expect(target.debug).toHaveBeenCalledWith("[matrix_rtc::a] dbg");
+ expect(target.trace).toHaveBeenCalledWith("[matrix_rtc::a] trc");
+ });
});
/** A driver that answers every read with nothing and never sends. */
diff --git a/src/matrix-rtc-sdk/index.ts b/src/matrix-rtc-sdk/index.ts
index 62278771c..f14e4255a 100644
--- a/src/matrix-rtc-sdk/index.ts
+++ b/src/matrix-rtc-sdk/index.ts
@@ -18,6 +18,7 @@ Please see LICENSE in the repository root for full details.
import initAsync, { type InitInput } from "./generated/wasm-bindgen/index.js";
import bindings from "./generated/matrix_rtc";
+import { installMatrixRtcLogSink } from "./logSink";
export {
FfiMatrixDriver,
@@ -31,6 +32,8 @@ export {
FfiImpairment,
FfiJoinError,
FfiKeepAlive,
+ FfiLogLevel,
+ setLogSink,
FfiMembershipState,
FfiTransportIntent,
RtcError,
@@ -54,6 +57,7 @@ export type {
FfiToDeviceRecipient,
FfiTransportDelegationRequest,
ConnectivitySinkLike,
+ LogSink,
MatrixDriverCallback,
RoomEventSinkLike,
StateUpdateSinkLike,
@@ -83,6 +87,8 @@ export async function initMatrixRtcSdk(
loading ??= (async (): Promise => {
await initAsync({ module_or_path: source ?? (await bundledWasm()) });
bindings.initialize();
+ // The crate is silent until told where to log.
+ installMatrixRtcLogSink();
})();
await loading;
}
diff --git a/src/matrix-rtc-sdk/logSink.ts b/src/matrix-rtc-sdk/logSink.ts
new file mode 100644
index 000000000..c93fcf83a
--- /dev/null
+++ b/src/matrix-rtc-sdk/logSink.ts
@@ -0,0 +1,55 @@
+/*
+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 { type Logger, logger as rootLogger } from "matrix-js-sdk/lib/logger";
+
+import { FfiLogLevel, type LogSink, setLogSink } from "./generated/matrix_rtc";
+
+/**
+ * A {@link LogSink} that writes the crate's lines to a matrix-js-sdk logger,
+ * so they land in the same console and rageshake as everything else, under
+ * the logger's own level filtering. Each line names the Rust module it came
+ * from.
+ */
+export function matrixRtcLogSink(target: Logger): LogSink {
+ return {
+ log(level, module, message): void {
+ const line = `[${module}] ${message}`;
+ switch (level) {
+ case FfiLogLevel.Error:
+ target.error(line);
+ break;
+ case FfiLogLevel.Warn:
+ target.warn(line);
+ break;
+ case FfiLogLevel.Info:
+ target.info(line);
+ break;
+ case FfiLogLevel.Debug:
+ target.debug(line);
+ break;
+ case FfiLogLevel.Trace:
+ target.trace(line);
+ break;
+ }
+ },
+ };
+}
+
+/**
+ * Routes the crate's log lines to `[matrix-rtc]` under Element Call's root
+ * logger. `maxLevel` is the crate-side cut-off: what it does not format it
+ * does not send, so trace stays off unless asked for.
+ */
+export function installMatrixRtcLogSink(
+ maxLevel: FfiLogLevel = FfiLogLevel.Debug,
+): void {
+ setLogSink(
+ matrixRtcLogSink(rootLogger.getChild("[matrix-rust-rtc]")),
+ maxLevel,
+ );
+}
diff --git a/src/profile/useOwnProfile.ts b/src/profile/useOwnProfile.ts
new file mode 100644
index 000000000..051462f3e
--- /dev/null
+++ b/src/profile/useOwnProfile.ts
@@ -0,0 +1,33 @@
+/*
+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 { useEffect, useState } from "react";
+
+import {
+ type OwnProfile,
+ type ProfileDriver,
+} from "../driver/ElementCallMatrixClientDriver";
+import { useMatrixDrivers } from "../driver/MatrixDriverContext";
+
+/** The user's own display name and avatar from a profile driver, kept current. */
+export function useOwnProfileFrom(profile: ProfileDriver): OwnProfile {
+ const [own, setOwn] = useState(() => profile.getOwnProfile());
+ useEffect(() => {
+ setOwn(profile.getOwnProfile());
+ return profile.subscribeOwnProfile(setOwn);
+ }, [profile]);
+ return own;
+}
+
+/**
+ * {@link useOwnProfileFrom} over the client driver the host provided. The
+ * read-only counterpart of `useProfile(client)`, for the call tree; editing
+ * stays with the profile settings, which a host may not offer.
+ */
+export function useOwnProfile(): OwnProfile {
+ return useOwnProfileFrom(useMatrixDrivers().clientDriver);
+}
diff --git a/src/reactions/ParticipationReactionsReader.test.ts b/src/reactions/ParticipationReactionsReader.test.ts
new file mode 100644
index 000000000..ba145c4a3
--- /dev/null
+++ b/src/reactions/ParticipationReactionsReader.test.ts
@@ -0,0 +1,158 @@
+/*
+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 { MockElementCallMatrixClientDriver } from "../driver/MockElementCallMatrixClientDriver";
+import { testScope } from "../utils/test";
+import { FakeParticipation, fakeMembership } from "../utils/test-participation";
+import { ElementCallReactionEventType, type RaisedHandInfo } from ".";
+import { ParticipationReactionsReader } from "./ParticipationReactionsReader";
+
+const alice = fakeMembership({
+ member: {
+ memberId: "m-alice",
+ userId: "@alice:example.org",
+ deviceId: "ALICE",
+ eventId: "$alice-join",
+ },
+});
+const aliceId = "@alice:example.org:ALICE";
+
+function raisedHand(
+ timeline: MockElementCallMatrixClientDriver,
+ member = alice,
+ eventId = "$hand",
+): void {
+ timeline.emitTimelineEvent({
+ eventId,
+ type: "m.reaction",
+ sender: member.member.userId,
+ content: {
+ "m.relates_to": {
+ rel_type: "m.annotation",
+ event_id: member.member.eventId,
+ key: "🖐️",
+ },
+ },
+ originServerTs: 1000,
+ });
+}
+
+function setUp(): {
+ participation: FakeParticipation;
+ timeline: MockElementCallMatrixClientDriver;
+ hands: () => Record;
+ reactions: () => string[];
+} {
+ const participation = new FakeParticipation();
+ const timeline = new MockElementCallMatrixClientDriver();
+ const reader = new ParticipationReactionsReader(
+ testScope(),
+ participation,
+ timeline,
+ );
+ let hands: Record = {};
+ reader.raisedHands$.subscribe((h) => (hands = h));
+ let reactions: string[] = [];
+ reader.reactions$.subscribe(
+ (r) => (reactions = Object.values(r).map((v) => v.reactionOption.emoji)),
+ );
+ return {
+ participation,
+ timeline,
+ hands: () => hands,
+ reactions: () => reactions,
+ };
+}
+
+describe("ParticipationReactionsReader", () => {
+ it("raises and lowers a hand with the member's reaction and its redaction", () => {
+ const { participation, timeline, hands } = setUp();
+ participation.setMemberships([alice]);
+ raisedHand(timeline);
+ expect(hands()).toEqual({
+ [aliceId]: {
+ membershipEventId: "$alice-join",
+ reactionEventId: "$hand",
+ time: new Date(1000),
+ },
+ });
+ timeline.emitTimelineEvent({
+ eventId: "$redaction",
+ type: "m.room.redaction",
+ sender: alice.member.userId,
+ content: {},
+ originServerTs: 2000,
+ redacts: "$hand",
+ });
+ expect(hands()).toEqual({});
+ });
+
+ it("ignores a reaction that does not relate to the sender's own membership", () => {
+ const { participation, timeline, hands } = setUp();
+ participation.setMemberships([alice]);
+ timeline.emitTimelineEvent({
+ eventId: "$forged",
+ type: "m.reaction",
+ sender: "@mallory:example.org",
+ content: {
+ "m.relates_to": {
+ rel_type: "m.annotation",
+ event_id: "$alice-join",
+ key: "🖐️",
+ },
+ },
+ originServerTs: 1000,
+ });
+ expect(hands()).toEqual({});
+ });
+
+ it("picks up a hand raised before we looked, and drops it when the member leaves", () => {
+ const { participation, timeline, hands } = setUp();
+ // The reaction is already in the room when the roster arrives.
+ raisedHand(timeline);
+ expect(hands()).toEqual({});
+ participation.setMemberships([alice]);
+ expect(Object.keys(hands())).toEqual([aliceId]);
+ participation.setMemberships([]);
+ expect(hands()).toEqual({});
+ });
+
+ it("re-resolves a hand when the member re-sends their membership", () => {
+ const { participation, timeline, hands } = setUp();
+ participation.setMemberships([alice]);
+ raisedHand(timeline);
+ expect(Object.keys(hands())).toEqual([aliceId]);
+ // A new membership event without a hand on it: the hand goes.
+ const resent = fakeMembership({
+ member: { ...alice.member, eventId: "$alice-join-2" },
+ });
+ participation.setMemberships([resent]);
+ expect(hands()).toEqual({});
+ // Raised again on the new event: back.
+ raisedHand(timeline, resent, "$hand-2");
+ expect(hands()[aliceId]?.membershipEventId).toBe("$alice-join-2");
+ });
+
+ it("shows a reaction keyed by the member's media id", () => {
+ const { participation, timeline, reactions } = setUp();
+ participation.setMemberships([alice]);
+ timeline.emitTimelineEvent({
+ eventId: "$reaction",
+ type: ElementCallReactionEventType,
+ sender: alice.member.userId,
+ content: {
+ "m.relates_to": { rel_type: "m.reference", event_id: "$alice-join" },
+ emoji: "🎉",
+ name: "party",
+ },
+ originServerTs: 1000,
+ });
+ expect(reactions()).toEqual(["🎉"]);
+ });
+});
diff --git a/src/reactions/ParticipationReactionsReader.ts b/src/reactions/ParticipationReactionsReader.ts
new file mode 100644
index 000000000..3400ddbb4
--- /dev/null
+++ b/src/reactions/ParticipationReactionsReader.ts
@@ -0,0 +1,250 @@
+/*
+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 { BehaviorSubject, delay } from "rxjs";
+import { logger } from "matrix-js-sdk/lib/logger";
+
+import {
+ ElementCallReactionEventType,
+ GenericReaction,
+ type RaisedHandInfo,
+ type ReactionInfo,
+ ReactionSet,
+} from ".";
+import { REACTION_ACTIVE_TIME_MS } from "./ReactionsReader";
+import { type Behavior } from "../state/Behavior";
+import { type Epoch, type ObservableScope } from "../state/ObservableScope";
+import { type FfiMembership } from "../matrix-rtc-sdk";
+import {
+ type TimelineDriver,
+ type TimelineEvent,
+} from "../driver/ElementCallMatrixClientDriver";
+import { memberMediaId } from "../state/rtc/mediaId";
+
+const RAISED_HAND_KEY = "🖐️";
+const REACTION_EVENT_TYPE = "m.reaction";
+const REDACTION_EVENT_TYPE = "m.room.redaction";
+
+/** What the reader needs from a {@link CallParticipation}. */
+export interface ParticipationReactionsSource {
+ memberships$: Behavior>;
+}
+
+interface Relation {
+ rel_type?: string;
+ event_id?: string;
+ key?: string;
+}
+
+function relationOf(content: Record): Relation | undefined {
+ return content["m.relates_to"] as Relation | undefined;
+}
+
+/**
+ * Raised hands and reactions over a {@link CallParticipation} and the client
+ * driver's timeline: the counterpart of {@link ReactionsReader}, which reads
+ * the same from a matrix-js-sdk session.
+ *
+ * Hands and reactions relate to the sender's current membership event, as
+ * they always have. Both outputs are keyed by the member's media id
+ * (`${userId}:${deviceId}`), which is what the tiles are keyed by.
+ */
+export class ParticipationReactionsReader {
+ private readonly raisedHandsSubject$ = new BehaviorSubject<
+ Record
+ >({});
+ public readonly raisedHands$ = this.raisedHandsSubject$.asObservable();
+
+ private readonly reactionsSubject$ = new BehaviorSubject<
+ Record
+ >({});
+ public readonly reactions$ = this.reactionsSubject$.asObservable();
+
+ private memberships: FfiMembership[] = [];
+
+ public constructor(
+ scope: ObservableScope,
+ participation: ParticipationReactionsSource,
+ private readonly timeline: Pick<
+ TimelineDriver,
+ "subscribeTimeline" | "getRelatedEvents"
+ >,
+ ) {
+ // Hide reactions after a given time.
+ this.reactionsSubject$
+ .pipe(delay(REACTION_ACTIVE_TIME_MS), scope.bind())
+ .subscribe((reactions) => {
+ const date = new Date();
+ const nextEntries = Object.fromEntries(
+ Object.entries(reactions).filter(([_, hr]) => hr.expireAfter > date),
+ );
+ if (Object.keys(reactions).length === Object.keys(nextEntries).length)
+ return;
+ this.reactionsSubject$.next(nextEntries);
+ });
+
+ scope.onEnd(timeline.subscribeTimeline(this.handleEvent));
+ participation.memberships$
+ .pipe(scope.bind())
+ .subscribe((memberships) => this.onMembershipsChanged(memberships.value));
+ }
+
+ /** The member whose current membership event is `eventId`, sent by `sender`. */
+ private memberFor(
+ eventId: string | undefined,
+ sender: string,
+ ): FfiMembership | undefined {
+ if (eventId === undefined) return undefined;
+ return this.memberships.find(
+ (m) => m.member.eventId === eventId && m.member.userId === sender,
+ );
+ }
+
+ /** The hand `member` has raised on their current membership event, if any. */
+ private findRaisedHand(member: FfiMembership): RaisedHandInfo | undefined {
+ const eventId = member.member.eventId;
+ if (eventId === undefined) return undefined;
+ const reaction = this.timeline
+ .getRelatedEvents(eventId, "m.annotation", REACTION_EVENT_TYPE)
+ .find(
+ (e) =>
+ e.sender === member.member.userId &&
+ relationOf(e.content)?.key === RAISED_HAND_KEY,
+ );
+ if (reaction === undefined) return undefined;
+ return {
+ membershipEventId: eventId,
+ reactionEventId: reaction.eventId,
+ time: new Date(reaction.originServerTs),
+ };
+ }
+
+ /**
+ * Drops the hands of members who left, keeps a hand across a re-sent
+ * membership when it was raised again on the new event, and picks up hands
+ * that were raised before we looked.
+ */
+ private onMembershipsChanged(memberships: FfiMembership[]): void {
+ this.memberships = memberships;
+ const present = new Map(
+ memberships.map((m) => [memberMediaId(m.member), m]),
+ );
+ const hands = { ...this.raisedHandsSubject$.value };
+ let changed = false;
+
+ for (const id of Object.keys(hands)) {
+ const member = present.get(id);
+ if (member === undefined) {
+ delete hands[id];
+ changed = true;
+ } else if (hands[id].membershipEventId !== member.member.eventId) {
+ // The member re-sent their membership: the hand stands only if it
+ // was raised on the new event too.
+ const hand = this.findRaisedHand(member);
+ if (hand) hands[id] = hand;
+ else delete hands[id];
+ changed = true;
+ }
+ }
+ for (const [id, member] of present) {
+ if (id in hands) continue;
+ const hand = this.findRaisedHand(member);
+ if (hand) {
+ hands[id] = hand;
+ changed = true;
+ }
+ }
+ if (changed) this.raisedHandsSubject$.next(hands);
+ }
+
+ private addRaisedHand(identifier: string, info: RaisedHandInfo): void {
+ this.raisedHandsSubject$.next({
+ ...this.raisedHandsSubject$.value,
+ [identifier]: info,
+ });
+ }
+
+ private removeRaisedHand(identifier: string): void {
+ this.raisedHandsSubject$.next(
+ Object.fromEntries(
+ Object.entries(this.raisedHandsSubject$.value).filter(
+ ([id]) => id !== identifier,
+ ),
+ ),
+ );
+ }
+
+ private handleEvent = (event: TimelineEvent): void => {
+ if (event.type === ElementCallReactionEventType) {
+ const relation = relationOf(event.content);
+ const member = this.memberFor(relation?.event_id, event.sender);
+ if (member === undefined) {
+ logger.warn(
+ `Reaction target was not a membership event for ${event.sender}, ignoring`,
+ );
+ return;
+ }
+ const identifier = memberMediaId(member.member);
+ const rawEmoji = event.content.emoji;
+ if (typeof rawEmoji !== "string" || rawEmoji === "") {
+ logger.warn(`Reaction had no emoji from ${event.eventId}`);
+ return;
+ }
+ const emoji = new Intl.Segmenter(undefined, { granularity: "grapheme" })
+ .segment(rawEmoji)
+ [Symbol.iterator]()
+ .next().value?.segment;
+ if (!emoji?.trim()) {
+ logger.warn(
+ `Reaction had no emoji from ${event.eventId} after splitting`,
+ );
+ return;
+ }
+ const reaction = {
+ ...GenericReaction,
+ emoji,
+ // If we don't find a reaction, we can fallback to the generic sound.
+ ...ReactionSet.find((r) => r.name === event.content.name),
+ };
+ const current = this.reactionsSubject$.value;
+ if (current[identifier]) {
+ // We've still got a reaction from this user, ignore it to prevent spamming
+ logger.warn(`Got reaction from ${identifier} but one is still playing`);
+ return;
+ }
+ this.reactionsSubject$.next({
+ ...current,
+ [identifier]: {
+ reactionOption: reaction,
+ expireAfter: new Date(Date.now() + REACTION_ACTIVE_TIME_MS),
+ },
+ });
+ } else if (event.type === REACTION_EVENT_TYPE) {
+ const relation = relationOf(event.content);
+ const member = this.memberFor(relation?.event_id, event.sender);
+ if (member === undefined) {
+ logger.warn(
+ `Reaction target was not a membership event for ${event.sender}, ignoring`,
+ );
+ return;
+ }
+ if (relation?.key === RAISED_HAND_KEY)
+ this.addRaisedHand(memberMediaId(member.member), {
+ reactionEventId: event.eventId,
+ membershipEventId: relation.event_id!,
+ time: new Date(event.originServerTs),
+ });
+ } else if (event.type === REDACTION_EVENT_TYPE) {
+ const redacts =
+ event.redacts ?? (event.content.redacts as string | undefined);
+ const target = Object.entries(this.raisedHandsSubject$.value).find(
+ ([, hand]) => hand.reactionEventId === redacts,
+ )?.[0];
+ if (target !== undefined) this.removeRaisedHand(target);
+ }
+ };
+}
diff --git a/src/reactions/useReactionsSender.tsx b/src/reactions/useReactionsSender.tsx
index 1b7e099a2..19359a75e 100644
--- a/src/reactions/useReactionsSender.tsx
+++ b/src/reactions/useReactionsSender.tsx
@@ -5,20 +5,18 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
-import { EventType, RelationType } from "matrix-js-sdk";
+import { EventType, type MatrixClient, RelationType } from "matrix-js-sdk";
import {
createContext,
use,
type ReactNode,
useCallback,
- useMemo,
type JSX,
} from "react";
-import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
import { logger } from "matrix-js-sdk/lib/logger";
-import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
import { useClientState } from "../ClientContext";
+import { type TimelineDriver } from "../driver/ElementCallMatrixClientDriver";
import { ElementCallReactionEventType, type ReactionOption } from ".";
import { type CallViewModel } from "../state/CallViewModel/CallViewModel";
import { useBehavior } from "../useBehavior";
@@ -44,116 +42,108 @@ export const useReactionsSender = (): ReactionsSenderContextType => {
/**
* Provider that handles sending a reaction or hand raised event to a call.
*/
+/** How to send and take back a reaction: a room event, and a redaction. */
+export type ReactionsTimeline = Pick<
+ TimelineDriver,
+ "sendRoomEvent" | "redactEvent"
+>;
+
+/** {@link ReactionsTimeline} over a matrix-js-sdk client. */
+export function jsSdkReactionsTimeline(
+ client: Pick,
+ roomId: string,
+): ReactionsTimeline {
+ return {
+ sendRoomEvent: async (eventType, content) => {
+ const { event_id: eventId } = await client.sendEvent(
+ roomId,
+ eventType as never,
+ content as never,
+ );
+ return { eventId };
+ },
+ redactEvent: async (eventId) => {
+ await client.redactEvent(roomId, eventId);
+ },
+ };
+}
+
export const ReactionsSenderProvider = ({
children,
- rtcSession,
vm,
+ ownIdentifier,
+ ownMembershipEventId,
+ timeline,
}: {
children: ReactNode;
- rtcSession: MatrixRTCSession;
vm: CallViewModel;
+ /** Our key in `vm.reactions$` / `vm.handsRaised$` (`${userId}:${deviceId}`). */
+ ownIdentifier: string;
+ /** The event id of our current membership, which reactions relate to. */
+ ownMembershipEventId: string | undefined;
+ timeline: ReactionsTimeline;
}): JSX.Element => {
- const memberships = useMatrixRTCSessionMemberships(rtcSession);
+ // A widget host may forbid reactions; without a client state (the
+ // component, the crate path) there is nobody to forbid them.
const clientState = useClientState();
const supportsReactions =
- clientState?.state === "valid" && clientState.supportedFeatures.reactions;
- const room = rtcSession.room;
- const myUserId = room.client.getUserId();
- const myDeviceId = room.client.getDeviceId();
- const myMembershipIdentifier = `${myUserId}:${myDeviceId}`;
-
- const myMembershipEvent = useMemo(
- () =>
- memberships.find(
- (m) => m.userId === myUserId && m.deviceId === myDeviceId,
- )?.eventId,
- [memberships, myUserId, myDeviceId],
- );
+ clientState === undefined ||
+ (clientState.state === "valid" && clientState.supportedFeatures.reactions);
const reactions = useBehavior(vm.reactions$);
- const myReaction = useMemo(
- () =>
- myMembershipIdentifier !== undefined
- ? reactions[myMembershipIdentifier]
- : undefined,
- [myMembershipIdentifier, reactions],
- );
+ const myReaction = reactions[ownIdentifier];
const handsRaised = useBehavior(vm.handsRaised$);
- const myRaisedHand = useMemo(
- () =>
- myMembershipIdentifier !== undefined
- ? handsRaised[myMembershipIdentifier]
- : undefined,
- [myMembershipIdentifier, handsRaised],
- );
+ const myRaisedHand = handsRaised[ownIdentifier];
const toggleRaisedHand = useCallback(async () => {
- if (!myMembershipIdentifier) {
- return;
- }
const myReactionId = myRaisedHand?.reactionEventId;
if (!myReactionId) {
try {
- if (!myMembershipEvent) {
+ if (!ownMembershipEventId) {
throw new Error("Cannot find own membership event");
}
- const reaction = await room.client.sendEvent(
- rtcSession.room.roomId,
- EventType.Reaction,
- {
- "m.relates_to": {
- rel_type: RelationType.Annotation,
- event_id: myMembershipEvent,
- key: "🖐️",
- },
+ const { eventId } = await timeline.sendRoomEvent(EventType.Reaction, {
+ "m.relates_to": {
+ rel_type: RelationType.Annotation,
+ event_id: ownMembershipEventId,
+ key: "🖐️",
},
- );
- logger.debug("Sent raise hand event", reaction.event_id);
+ });
+ logger.debug("Sent raise hand event", eventId);
} catch (ex) {
logger.error("Failed to send raised hand", ex);
}
} else {
try {
- await room.client.redactEvent(rtcSession.room.roomId, myReactionId);
+ await timeline.redactEvent(myReactionId);
logger.debug("Redacted raise hand event");
} catch (ex) {
logger.error("Failed to redact reaction event", myReactionId, ex);
throw ex;
}
}
- }, [
- myMembershipEvent,
- myMembershipIdentifier,
- myRaisedHand,
- rtcSession,
- room,
- ]);
+ }, [ownMembershipEventId, myRaisedHand, timeline]);
const sendReaction = useCallback(
async (reaction: ReactionOption) => {
- if (!myMembershipIdentifier || myReaction) {
- // We're still reacting
+ if (myReaction) {
return;
}
- if (!myMembershipEvent) {
+ if (!ownMembershipEventId) {
throw new Error("Cannot find own membership event");
}
- await room.client.sendEvent(
- rtcSession.room.roomId,
- ElementCallReactionEventType,
- {
- "m.relates_to": {
- rel_type: RelationType.Reference,
- event_id: myMembershipEvent,
- },
- emoji: reaction.emoji,
- name: reaction.name,
+ await timeline.sendRoomEvent(ElementCallReactionEventType, {
+ "m.relates_to": {
+ rel_type: RelationType.Reference,
+ event_id: ownMembershipEventId,
},
- );
+ emoji: reaction.emoji,
+ name: reaction.name,
+ });
},
- [myMembershipEvent, myReaction, room, myMembershipIdentifier, rtcSession],
+ [ownMembershipEventId, myReaction, timeline],
);
return (
diff --git a/src/room/CallEndedView.tsx b/src/room/CallEndedView.tsx
index 58417c38a..f68399350 100644
--- a/src/room/CallEndedView.tsx
+++ b/src/room/CallEndedView.tsx
@@ -6,13 +6,12 @@ Please see LICENSE in the repository root for full details.
*/
import { type FC, type FormEventHandler, useCallback, useState } from "react";
-import { type MatrixClient } from "matrix-js-sdk";
import { Trans, useTranslation } from "react-i18next";
import { Button, Heading, Text } from "@vector-im/compound-web";
import styles from "./CallEndedView.module.css";
import feedbackStyle from "../input/FeedbackInput.module.css";
-import { useProfile } from "../profile/useProfile";
+import { useOwnProfile } from "../profile/useOwnProfile";
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { FieldRow, InputField } from "../input/Input";
@@ -22,7 +21,6 @@ import { LeaveToHomeLink } from "../button/LeaveToHomeLink";
import { useLeaveToHome } from "../LeaveToHomeContext";
interface Props {
- client: MatrixClient;
isPasswordlessUser: boolean;
hideHeader: boolean;
confineToRoom: boolean;
@@ -30,7 +28,6 @@ interface Props {
}
export const CallEndedView: FC = ({
- client,
isPasswordlessUser,
hideHeader,
confineToRoom,
@@ -39,7 +36,7 @@ export const CallEndedView: FC = ({
const { t } = useTranslation();
const leaveToHome = useLeaveToHome();
- const { displayName } = useProfile(client);
+ const { displayName } = useOwnProfile();
const [surveySubmitted, setSurveySubmitted] = useState(false);
const [starRating, setStarRating] = useState(0);
const [submitting, setSubmitting] = useState(false);
diff --git a/src/room/CallView.stories.tsx b/src/room/CallView.stories.tsx
new file mode 100644
index 000000000..9b46a88c1
--- /dev/null
+++ b/src/room/CallView.stories.tsx
@@ -0,0 +1,225 @@
+/*
+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.
+*/
+
+/**
+ * `CallView` over the mock drivers: no matrix-js-sdk client, the Rust crate
+ * carrying the call. The lobby and the ended screen render as they would for
+ * a host; the in-call screen needs a LiveKit SFU, which no mock provides yet,
+ * so its story shows the path a call takes when the homeserver advertises no
+ * transport instead.
+ */
+
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { type FC, type ReactNode, useEffect, useMemo, useState } from "react";
+import { BrowserRouter } from "react-router-dom";
+
+import { CallView } from "./CallView";
+import { CallEndedView } from "./CallEndedView";
+import {
+ type MatrixDrivers,
+ MatrixDriverProvider,
+} from "../driver/MatrixDriverContext";
+import { MockElementCallMatrixClientDriver } from "../driver/MockElementCallMatrixClientDriver";
+import {
+ MockRtcMatrixDriver,
+ type MockRtcMatrixDriverOptions,
+ slotEvent,
+} from "../driver/MockRtcMatrixDriver";
+import { HostBridgeProvider, nullHostBridge } from "../HostBridge";
+import { RootElementProvider } from "../RootElementContext";
+import {
+ componentProperties,
+ configurationForIntent,
+ type UrlParams,
+ UrlParamsProvider,
+ UserIntent,
+} from "../UrlParams";
+import { MediaDevicesContext } from "../MediaDevicesContext";
+import { MediaDevices } from "../state/MediaDevices";
+import { ObservableScope } from "../state/ObservableScope";
+import { ProcessorProvider } from "../livekit/TrackProcessorContext";
+
+const ROOM_ID = "!story:example.org";
+const USER_ID = "@me:example.org";
+const DEVICE_ID = "STORYDEV";
+
+const alice = {
+ userId: "@alice:example.org",
+ deviceId: "ALICEDEV",
+ memberId: "m-alice",
+};
+
+interface HostProps {
+ intent: UserIntent;
+ /** Members already in the call when the view mounts. */
+ peers?: (typeof alice)[];
+ /** The RTC driver's options, e.g. no transports for the error path. */
+ rtc?: MockRtcMatrixDriverOptions;
+ children: ReactNode;
+}
+
+/**
+ * What a host provides around a call: the drivers, the host bridge, the
+ * call's parameters, the media devices and the root element.
+ */
+const Host: FC = ({ intent, peers = [], rtc, children }) => {
+ const drivers = useMemo((): MatrixDrivers => {
+ const rtcDriver = new MockRtcMatrixDriver({
+ userId: USER_ID,
+ deviceId: DEVICE_ID,
+ roomId: ROOM_ID,
+ roomState: [slotEvent({ roomId: ROOM_ID, status: "open" })],
+ ...rtc,
+ });
+ for (const peer of peers) rtcDriver.addPeer(peer);
+ return {
+ rtcDriver,
+ clientDriver: new MockElementCallMatrixClientDriver({
+ userId: USER_ID,
+ deviceId: DEVICE_ID,
+ roomId: ROOM_ID,
+ roomInfo: { name: "Weekly sync", joinRule: "public" },
+ ownProfile: { displayName: "Me", avatarUrl: null },
+ members: [
+ {
+ userId: USER_ID,
+ displayName: "Me",
+ avatarUrl: null,
+ membership: "join",
+ },
+ ...peers.map((p) => ({
+ userId: p.userId,
+ displayName: p.userId.slice(1).split(":")[0],
+ avatarUrl: null,
+ membership: "join" as const,
+ })),
+ ],
+ }),
+ };
+ }, [peers, rtc]);
+ const params = useMemo(
+ (): UrlParams => ({
+ ...componentProperties,
+ roomId: ROOM_ID,
+ ...configurationForIntent(intent),
+ }),
+ [intent],
+ );
+ // The peers join once the crate listens to the room: the participation is
+ // created after the wasm has loaded, so poll for its sink.
+ useEffect(() => {
+ if (peers.length === 0) return;
+ const rtcDriver = drivers.rtcDriver as MockRtcMatrixDriver;
+ let tries = 0;
+ const timer = setInterval(() => {
+ tries++;
+ try {
+ for (const peer of peers) rtcDriver.peerJoins(peer);
+ clearInterval(timer);
+ } catch {
+ if (tries > 200) clearInterval(timer);
+ }
+ }, 50);
+ return (): void => clearInterval(timer);
+ }, [drivers, peers]);
+ const [mediaDevices, setMediaDevices] = useState(null);
+ useEffect(() => {
+ const scope = new ObservableScope();
+ setMediaDevices(new MediaDevices(scope, { controlledAudioDevices: false }));
+ return (): void => {
+ setMediaDevices(null);
+ scope.end();
+ };
+ }, []);
+ const [root, setRoot] = useState(null);
+ return (
+
+
+
+
+
+ {root !== null && mediaDevices !== null && (
+
+
+
+ <>{children}>
+
+
+
+ )}
+
+
+
+
+
+ );
+};
+
+const meta: Meta = {
+ title: "Room/CallView",
+ component: CallView,
+ parameters: { layout: "fullscreen" },
+};
+export default meta;
+
+type Story = StoryObj;
+
+/** The lobby of a call that Alice is already in. */
+export const Lobby: Story = {
+ args: {
+ isPasswordlessUser: false,
+ confineToRoom: true,
+ preload: false,
+ skipLobby: false,
+ },
+ render: (args) => (
+
+
+
+ ),
+};
+
+/**
+ * Straight into the call, with a homeserver that advertises no transport:
+ * the crate fails the join and the view shows the error it turns into.
+ */
+export const NoTransport: Story = {
+ args: {
+ isPasswordlessUser: false,
+ confineToRoom: true,
+ preload: false,
+ skipLobby: true,
+ },
+ render: (args) => (
+
+
+
+ ),
+};
+
+/** The screen after leaving, as a passwordless user sees it. */
+export const Ended: Story = {
+ args: {
+ isPasswordlessUser: true,
+ confineToRoom: false,
+ preload: false,
+ skipLobby: false,
+ },
+ render: (args) => (
+
+
+
+ ),
+};
diff --git a/src/room/CallView.test.tsx b/src/room/CallView.test.tsx
index 8ac920b84..b0ae57d3a 100644
--- a/src/room/CallView.test.tsx
+++ b/src/room/CallView.test.tsx
@@ -11,6 +11,7 @@ Please see LICENSE in the repository root for full details.
import {
beforeEach,
+ describe,
expect,
type MockedFunction,
onTestFinished,
@@ -64,6 +65,15 @@ import { MatrixRTCTransportMissingError } from "../utils/errors";
import { ProcessorProvider } from "../livekit/TrackProcessorContext";
import { MediaDevicesContext } from "../MediaDevicesContext";
import { constant } from "../state/Behavior";
+import {
+ type MatrixDrivers,
+ MatrixDriverProvider,
+} from "../driver/MatrixDriverContext";
+import { MockRtcMatrixDriver } from "../driver/MockRtcMatrixDriver";
+import { MockElementCallMatrixClientDriver } from "../driver/MockElementCallMatrixClientDriver";
+import { initMatrixRtcSdkForTests } from "../utils/test-matrix-rtc";
+import { callViewModelImplementation } from "../settings/settings";
+import { CallViewModelImplementation } from "../config/ConfigOptions";
vi.mock("../soundUtils");
vi.mock("../useAudioContext");
@@ -138,6 +148,23 @@ beforeEach(() => {
);
});
+/** Mock drivers over the same room and identity as the mocked session. */
+function defaultDrivers(): MatrixDrivers {
+ return {
+ rtcDriver: new MockRtcMatrixDriver({
+ userId: localRtcMember.userId,
+ deviceId: localRtcMember.deviceId,
+ roomId,
+ }),
+ clientDriver: new MockElementCallMatrixClientDriver({
+ userId: localRtcMember.userId,
+ deviceId: localRtcMember.deviceId,
+ roomId,
+ roomInfo: { joinRule: "invite" },
+ }),
+ };
+}
+
function createCallView(
hostBridge: HostBridge,
joined = true,
@@ -145,6 +172,8 @@ function createCallView(
withErrorBoundary?: boolean;
/** Wait for the host to say when to join, rather than joining at once. */
preload?: boolean;
+ /** The host's drivers, for the matrix-rtc implementation. */
+ drivers?: MatrixDrivers;
} = {},
): {
rtcSession: MatrixRTCSession;
@@ -194,13 +223,15 @@ function createCallView(
- {options.withErrorBoundary ? (
-
- {callView}
-
- ) : (
- callView
- )}
+
+ {options.withErrorBoundary ? (
+
+ {callView}
+
+ ) : (
+ callView
+ )}
+
@@ -424,3 +455,46 @@ test("user can reconnect after a membership manager error", async () => {
// In-call controls should be visible again
await waitFor(() => screen.getByRole("button", { name: "Leave" }));
});
+
+describe("the call implementation switch", () => {
+ test("matrix-js-sdk carries the call by default: no participation", async () => {
+ createCallView(nullHostBridge);
+ await waitFor(() => expect(ActiveCall).toHaveBeenCalled());
+ expect(
+ vi.mocked(ActiveCall).mock.calls.at(-1)?.[0].participation,
+ ).toBeNull();
+ expect(window.matrixRtc).toBeUndefined();
+ });
+
+ test("with matrix-rtc selected and drivers provided, the crate carries the call", async () => {
+ await initMatrixRtcSdkForTests();
+ callViewModelImplementation.setValue(CallViewModelImplementation.MatrixRtc);
+ onTestFinished(() =>
+ callViewModelImplementation.setValue(
+ CallViewModelImplementation.MatrixJsSdk,
+ ),
+ );
+ const drivers: MatrixDrivers = {
+ rtcDriver: new MockRtcMatrixDriver({
+ userId: localRtcMember.userId,
+ deviceId: localRtcMember.deviceId,
+ roomId,
+ }),
+ clientDriver: new MockElementCallMatrixClientDriver({
+ userId: localRtcMember.userId,
+ deviceId: localRtcMember.deviceId,
+ roomId,
+ }),
+ };
+ createCallView(nullHostBridge, true, { drivers });
+ await waitFor(() =>
+ expect(
+ vi.mocked(ActiveCall).mock.calls.at(-1)?.[0].participation,
+ ).not.toBeNull(),
+ );
+ const { participation } = vi.mocked(ActiveCall).mock.calls.at(-1)![0];
+ expect(window.matrixRtc?.participation).toBe(participation);
+ // The participation is bound to the drivers' room and identity.
+ expect(participation?.session$.value.roomId).toBe(roomId);
+ });
+});
diff --git a/src/room/CallView.tsx b/src/room/CallView.tsx
index 3c34dc845..3860a9b40 100644
--- a/src/room/CallView.tsx
+++ b/src/room/CallView.tsx
@@ -12,11 +12,11 @@ import {
useEffect,
useMemo,
useState,
+ useRef,
} from "react";
import {
type MatrixClient,
JoinRule,
- type Room,
UnsupportedStickyEventsEndpointError,
} from "matrix-js-sdk";
import {
@@ -34,7 +34,11 @@ import { LobbyView } from "./LobbyView";
import { type MatrixInfo } from "./VideoPreview";
import { CallEndedView } from "./CallEndedView";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
-import { useProfile } from "../profile/useProfile";
+import {
+ mediaKeyStatisticsOf,
+ NO_MEDIA_KEY_STATISTICS,
+} from "../analytics/PosthogEvents";
+import { useOwnProfile } from "../profile/useOwnProfile";
import { findDeviceByName } from "../utils/media";
import { ActiveCall } from "./InCallView";
import { type MuteStates } from "../state/MuteStates";
@@ -42,11 +46,9 @@ import { useMediaDevices } from "../MediaDevicesContext";
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
import {
saveKeyForRoom,
- useRoomEncryptionSystem,
+ useEncryptionSystemFor,
} from "../e2ee/sharedKeyManagement";
-import { useRoomAvatar } from "./useRoomAvatar";
-import { useRoomName } from "./useRoomName";
-import { useJoinRule } from "./useJoinRule";
+import { useRoomInfo } from "./useRoomInfo";
import { InviteModal } from "./InviteModal";
import { HeaderStyle, type UrlParams, useUrlParams } from "../UrlParams";
import { E2eeType } from "../e2ee/e2eeType";
@@ -64,7 +66,6 @@ import {
UnknownCallError,
} from "../utils/errors.ts";
import { GroupCallErrorBoundary } from "./GroupCallErrorBoundary.tsx";
-import { useTypedEventEmitter } from "../useEvents";
import { muteAllAudio$ } from "../state/MuteAllAudioModel.ts";
import { useAppBarTitle } from "../AppBar.tsx";
import { useBehavior } from "../useBehavior.ts";
@@ -72,6 +73,20 @@ import { useRootElement } from "../RootElementContext.ts";
import { useHostBridge } from "../HostBridge.ts";
import { useMuteStates } from "../state/useMuteStates.ts";
import { useLeaveToHome } from "../LeaveToHomeContext.ts";
+import { useMatrixDrivers } from "../driver/MatrixDriverContext.tsx";
+import { useCallParticipation } from "../state/rtc/useCallParticipation.ts";
+import { type CallParticipation } from "../state/rtc/CallParticipation.ts";
+import { participationConfig } from "../state/rtc/joinParams.ts";
+import { effectiveCallViewModelImplementation } from "../state/rtc/implementation.ts";
+import {
+ CallViewModelImplementation,
+ type MatrixRTCMode,
+} from "../config/ConfigOptions.ts";
+import { Config } from "../config/Config.ts";
+import { matrixRTCMode as matrixRTCModeSetting } from "../settings/settings.ts";
+import { constant } from "../state/Behavior.ts";
+import { Epoch } from "../state/ObservableScope.ts";
+import { FfiStatus, type FfiMembership } from "../matrix-rtc-sdk";
/**
* If there already are this many participants in the call, we automatically mute
@@ -82,14 +97,26 @@ export const MUTE_PARTICIPANT_COUNT = 8;
declare global {
interface Window {
rtcSession?: MatrixRTCSession;
+ /** The crate's participation, when the Rust implementation carries the call. */
+ matrixRtc?: { participation: CallParticipation };
}
}
+/** What the crate's roster reads as while matrix-js-sdk carries the call. */
+const NO_PARTICIPATION_MEMBERSHIPS = constant(
+ new Epoch([], 0),
+);
+
interface Props {
- /** The client to place the call with. */
- client: MatrixClient;
+ /**
+ * The matrix-js-sdk client and session, for the matrix-js-sdk
+ * implementation of the call. Without them the Rust crate carries the call
+ * through the drivers whatever the developer setting says (the component
+ * has no client to offer).
+ */
+ client?: MatrixClient;
/** The call to join. */
- rtcSession: MatrixRTCSession;
+ rtcSession?: MatrixRTCSession;
/**
* Whether the user is signed in as a guest, and so should be offered the
* chance to create an account when the call ends.
@@ -161,7 +188,52 @@ const LoadedCallView: FC = ({
const [externalError, setExternalError] = useState(
null,
);
- const memberships = useMatrixRTCSessionMemberships(rtcSession);
+ const jsSdkMemberships = useMatrixRTCSessionMemberships(rtcSession);
+ // The host's drivers: what the room is called and looks like, who we are,
+ // and (on the crate path) the session itself come from them.
+ const drivers = useMatrixDrivers();
+ const { roomId } = drivers.clientDriver;
+ const roomInfo = useRoomInfo();
+ const e2eeSystem = useEncryptionSystemFor(roomId, roomInfo.encrypted);
+
+ // Which implementation carries this call, sampled once for the view's
+ // lifetime (§5.15 of the oxidation plan): the Rust crate through the
+ // host's drivers, or matrix-js-sdk's session — which needs a session.
+ const [implementation] = useState(() =>
+ effectiveCallViewModelImplementation(),
+ );
+ const useMatrixRtc =
+ implementation === CallViewModelImplementation.MatrixRtc ||
+ rtcSession === undefined;
+ // Sampled once, like the implementation: the participation is the call,
+ // and rebuilding it for a later change of these would leave and rejoin.
+ const [participationConfigValue] = useState(() =>
+ useMatrixRtc
+ ? participationConfig({
+ // matrix_rtc_mode in config.json overrides the user's choice.
+ mode:
+ (Config.get().matrix_rtc_mode as MatrixRTCMode | undefined) ??
+ matrixRTCModeSetting.value$.value,
+ manageMediaKeys: e2eeSystem.kind === E2eeType.PER_PARTICIPANT,
+ session: Config.get().matrix_rtc_session,
+ })
+ : null,
+ );
+ const participation = useCallParticipation(
+ useMatrixRtc ? drivers : null,
+ participationConfigValue,
+ );
+ const participationMemberships = useBehavior(
+ participation?.memberships$ ?? NO_PARTICIPATION_MEMBERSHIPS,
+ );
+ // The call's members, whichever side lists them; only who they are matters here.
+ const memberUserIds = useMemo(
+ () =>
+ useMatrixRtc
+ ? participationMemberships.value.map((m) => m.member.userId)
+ : jsSdkMemberships.map((m) => m.userId!),
+ [useMatrixRtc, participationMemberships, jsSdkMemberships],
+ );
const rootElement = useRootElement();
const hostBridge = useHostBridge();
// A host that can close us is a host that decides when we stop existing, so
@@ -178,12 +250,16 @@ const LoadedCallView: FC = ({
muted: muteAllAudio,
}),
);
- // This should use `useEffectEvent` (only available in experimental versions)
+ // Joining a big call starts muted. Decided once, the first time the roster
+ // is known: at mount for matrix-js-sdk, after the seed for the crate.
+ const mutedForCallSize = useRef(false);
useEffect(() => {
- if (memberships.length >= MUTE_PARTICIPANT_COUNT)
+ if (mutedForCallSize.current) return;
+ if (useMatrixRtc && !participationMemberships.value.length) return;
+ mutedForCallSize.current = true;
+ if (memberUserIds.length >= MUTE_PARTICIPANT_COUNT)
muteStates.audio.setEnabled$.value?.(false);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+ }, [useMatrixRtc, participationMemberships, memberUserIds, muteStates]);
useEffect(() => {
logger.info("[Lifecycle] CallView Component mounted");
@@ -203,18 +279,20 @@ const LoadedCallView: FC = ({
}, [rootElement]);
useEffect(() => {
- window.rtcSession = rtcSession;
+ if (rtcSession !== undefined) window.rtcSession = rtcSession;
+ if (participation !== null) window.matrixRtc = { participation };
return (): void => {
delete window.rtcSession;
+ delete window.matrixRtc;
};
- }, [rtcSession]);
+ }, [rtcSession, participation]);
// TODO move this into the callViewModel LocalMembership.ts
// We might actually not need this at all. Since we get into fatalError on those errors already?
- useTypedEventEmitter(
- rtcSession,
- MatrixRTCSessionEvent.MembershipManagerError,
- (error) => {
+ // matrix-js-sdk only; the crate reports these through `fatalError$`.
+ useEffect(() => {
+ if (rtcSession === undefined) return;
+ const onError = (error: unknown): void => {
// When matrix_rtc_mode=matrix_2_0 is in effect but the homeserver does
// not advertise MSC4354 (sticky events), the SDK throws an
// `UnsupportedStickyEventsEndpointError`. The MembershipManager
@@ -227,79 +305,93 @@ const LoadedCallView: FC = ({
} else {
setExternalError(new ConnectionLostError());
}
- },
- );
+ };
+ rtcSession.on(MatrixRTCSessionEvent.MembershipManagerError, onError);
+ return (): void => {
+ rtcSession.off(MatrixRTCSessionEvent.MembershipManagerError, onError);
+ };
+ }, [rtcSession]);
useEffect(() => {
+ if (client === undefined || rtcSession === undefined) return;
// Sanity check the room object
if (client.getRoom(rtcSession.room.roomId) !== rtcSession.room)
logger.warn(
`We've ended up with multiple rooms for the same ID (${rtcSession.room.roomId}). This indicates a bug in the group call loading code, and may lead to incomplete room state.`,
);
- }, [client, rtcSession.room]);
+ }, [client, rtcSession]);
- const room = rtcSession.room as Room;
- const { displayName, avatarUrl } = useProfile(client);
- const roomName = useRoomName(room);
- const roomAvatar = useRoomAvatar(room);
+ const { displayName, avatarUrl } = useOwnProfile();
+ const roomName = roomInfo.name;
+ const roomAvatar = roomInfo.avatarUrl;
const {
perParticipantE2EE,
returnToLobby,
password: passwordFromUrl,
header,
} = useUrlParams();
- const e2eeSystem = useRoomEncryptionSystem(room.roomId);
// Save the password once we start the groupCallView
useEffect(() => {
- if (passwordFromUrl) saveKeyForRoom(room.roomId, passwordFromUrl);
- }, [passwordFromUrl, room.roomId]);
+ if (passwordFromUrl) saveKeyForRoom(roomId, passwordFromUrl);
+ }, [passwordFromUrl, roomId]);
useAppBarTitle(roomName);
+ const { userId: ownUserId } = drivers.clientDriver;
+ const roomAlias = roomInfo.canonicalAlias;
const matrixInfo = useMemo((): MatrixInfo => {
return {
- userId: client.getUserId()!,
- displayName: displayName!,
- avatarUrl: avatarUrl!,
- roomId: room.roomId,
+ userId: ownUserId,
+ displayName: displayName ?? ownUserId,
+ avatarUrl: avatarUrl ?? "",
+ roomId,
roomName,
- roomAlias: room.getCanonicalAlias(),
+ roomAlias,
roomAvatar,
e2eeSystem,
};
- }, [client, displayName, avatarUrl, roomName, room, roomAvatar, e2eeSystem]);
+ }, [
+ ownUserId,
+ displayName,
+ avatarUrl,
+ roomName,
+ roomId,
+ roomAlias,
+ roomAvatar,
+ e2eeSystem,
+ ]);
// Count each member only once, regardless of how many devices they use
const participantCount = useMemo(
- () => new Set(memberships.map((m) => m.userId!)).size,
- [memberships],
+ () => new Set(memberUserIds).size,
+ [memberUserIds],
);
const mediaDevices = useMediaDevices();
const latestMuteStates = useLatest(muteStates);
+ // Read at leave time, not a dependency: `onLeft` feeds the in-call view's
+ // effect, and a roster change must not rebuild the call.
+ const latestMemberUserIds = useLatest(memberUserIds);
- const enterRTCSessionOrError = useCallback(
- async (rtcSession: MatrixRTCSession): Promise => {
- try {
- setJoined(true);
- // TODO-MULTI-SFU what to do with error handling now that we don't use this function?
- // @BillCarsonFr
- } catch (e) {
- if (e instanceof ElementCallError) {
- setExternalError(e);
- } else {
- logger.error(`Unknown Error while entering RTC session`, e);
- const error = new UnknownCallError(
- e instanceof Error ? e : new Error("Unknown error", { cause: e }),
- );
- setExternalError(error);
- }
+ const enterRTCSessionOrError = useCallback(async (): Promise => {
+ try {
+ setJoined(true);
+ // TODO-MULTI-SFU what to do with error handling now that we don't use this function?
+ // @BillCarsonFr
+ } catch (e) {
+ if (e instanceof ElementCallError) {
+ setExternalError(e);
+ } else {
+ logger.error(`Unknown Error while entering RTC session`, e);
+ const error = new UnknownCallError(
+ e instanceof Error ? e : new Error("Unknown error", { cause: e }),
+ );
+ setExternalError(error);
}
- return Promise.resolve();
- },
- [setJoined],
- );
+ }
+ return Promise.resolve();
+ }, [setJoined]);
useEffect(() => {
const defaultDeviceSetup = async ({
@@ -409,10 +501,14 @@ const LoadedCallView: FC = ({
// queuing/batching of requests.
const sendInstantly = hostControlsLifetime;
PosthogAnalytics.instance.eventCallEnded.track(
- room.roomId,
- rtcSession.memberships.length,
+ roomId,
+ latestMemberUserIds.current.length,
sendInstantly,
- rtcSession,
+ participation !== null
+ ? participation.mediaKeyStatistics()
+ : rtcSession === undefined
+ ? NO_MEDIA_KEY_STATISTICS
+ : mediaKeyStatisticsOf(rtcSession),
);
// Unfortunately the PostHog library provides no way to await the
// tracking of an event, but we don't really want it to hold up our
@@ -457,8 +553,10 @@ const LoadedCallView: FC = ({
leaveSoundContext,
hostBridge,
hostControlsLifetime,
- room.roomId,
+ roomId,
+ latestMemberUserIds,
rtcSession,
+ participation,
isPasswordlessUser,
confineToRoom,
returnToLobby,
@@ -474,7 +572,7 @@ const LoadedCallView: FC = ({
});
}, [hostBridge, joined, rtcSession]);
- const joinRule = useJoinRule(room);
+ const joinRule = roomInfo.joinRule;
const [shareModalOpen, setInviteModalOpen] = useState(false);
const onDismissInviteModal = useCallback(
@@ -495,7 +593,9 @@ const LoadedCallView: FC = ({
const shareModal = (
@@ -525,6 +625,10 @@ const LoadedCallView: FC = ({
throw externalError;
};
body = ;
+ } else if (joined && useMatrixRtc && participation === null) {
+ // Joined before the crate is ready (its wasm loads on first use): the
+ // call appears with the participation, a render later.
+ body = null;
} else if (joined) {
body = (
<>
@@ -532,8 +636,9 @@ const LoadedCallView: FC = ({
= ({
if (isPasswordlessUser || PosthogAnalytics.instance.isEnabled()) {
body = (
= ({
setExternalError(null);
if (action == "reconnect") {
setLeft(false);
- await enterRTCSessionOrError(rtcSession).catch((e) => {
+ await enterRTCSessionOrError().catch((e) => {
logger.error("Error re-entering RTC session", e);
});
}
}}
onError={(_error) => {
- if (rtcSession.isJoined()) onLeft("error");
+ const joinedViaCrate =
+ participation !== null &&
+ FfiStatus.Connected.instanceOf(participation.status$.value);
+ if (rtcSession?.isJoined() === true || joinedViaCrate) onLeft("error");
// If there is an error we need to be dismissible again. This is done in
// `onLeft` as well; we need it here explicitly in case
// rtcSession.isJoined is false.
diff --git a/src/room/InCallView.test.tsx b/src/room/InCallView.test.tsx
index 357bc186e..2bfc5ecb5 100644
--- a/src/room/InCallView.test.tsx
+++ b/src/room/InCallView.test.tsx
@@ -42,7 +42,10 @@ import {
type CallViewModelOptions,
} from "../state/CallViewModel/CallViewModel";
import { alice, local } from "../utils/test-fixtures";
-import { ReactionsSenderProvider } from "../reactions/useReactionsSender";
+import {
+ jsSdkReactionsTimeline,
+ ReactionsSenderProvider,
+} from "../reactions/useReactionsSender";
import { useRoomEncryptionSystem } from "../e2ee/sharedKeyManagement";
import { LivekitRoomAudioRenderer } from "../livekit/MatrixAudioRenderer";
import { MediaDevicesContext } from "../MediaDevicesContext";
@@ -160,7 +163,7 @@ function createInCallView(args: CreateInCallViewArgs = {}): RenderResult & {
footerVm={footerVm}
developerSettingsVm={developerSettingsVm}
matrixInfo={matrixInfo}
- matrixRoom={room}
+ roomId={room.roomId}
onShareClick={null}
/>
);
@@ -172,7 +175,9 @@ function createInCallView(args: CreateInCallViewArgs = {}): RenderResult & {
{content}
@@ -248,7 +253,8 @@ describe("ActiveCall", () => {
{
{
e2eeSystem: EncryptionSystem;
+ /**
+ * The crate's participation in the session when the Rust implementation
+ * carries this call (see `CallViewModelImplementation`); null when
+ * matrix-js-sdk's `rtcSession` does.
+ */
+ participation: CallParticipation | null;
// TODO refactor those reasons into an enum
onLeft: (
reason: "user" | "timeout" | "decline" | "allOthersLeft" | "error",
@@ -125,34 +141,77 @@ export const ActiveCall: FC = (props) => {
// The element we have to draw the call in: the page, or the container a host
// gave us. Its size, not the window's, decides how the call is laid out.
const rootElement = useRootElement();
+ // The drivers, where a host provided them; required with a participation.
+ const drivers = useOptionalMatrixDrivers();
+ const { participation, rtcSession, client, roomId } = props;
+ if (participation !== null && drivers === null)
+ throw new Error(
+ "A call over the matrix-rtc crate needs the Matrix drivers to be provided",
+ );
+ if (
+ participation === null &&
+ (rtcSession === undefined || client === undefined)
+ )
+ throw new Error(
+ "A call needs either a participation (the crate) or a matrix-js-sdk client and session",
+ );
useEffect(() => {
rootLogger.info("START CALL VIEW SCOPE");
const scope = new ObservableScope();
- const reactionsReader = new ReactionsReader(scope, props.rtcSession);
const { autoLeaveWhenOthersLeft, waitForCallPickup, sendNotificationType } =
urlParams;
+ const options = {
+ ...callViewModelOptionsFromParams(urlParams),
+ encryptionSystem: props.e2eeSystem,
+ hostBridge,
+ autoLeaveWhenOthersLeft,
+ waitForCallPickup: waitForCallPickup && sendNotificationType === "ring",
+ // We merely sample the current mode here, so the user would need to
+ // manually rejoin to switch to a different one.
+ matrixRTCMode: matrixRTCModeSetting.value$.value,
+ windowSize$: scope.behavior(observeElementSize$(rootElement)),
+ };
- const vm = createJsClientCallViewModel$(
- scope,
- props.rtcSession,
- props.matrixRoom,
- mediaDevices,
- props.muteStates,
- {
- ...callViewModelOptionsFromParams(urlParams),
- encryptionSystem: props.e2eeSystem,
- hostBridge,
- autoLeaveWhenOthersLeft,
- waitForCallPickup: waitForCallPickup && sendNotificationType === "ring",
- // We merely sample the current mode here, so the user would need to
- // manually rejoin to switch to a different one.
- matrixRTCMode: matrixRTCModeSetting.value$.value,
- windowSize$: scope.behavior(observeElementSize$(rootElement)),
- },
- reactionsReader.raisedHands$,
- reactionsReader.reactions$,
- scope.behavior(trackProcessorState$),
- );
+ let vm: CallViewModel;
+ if (participation !== null && drivers !== null) {
+ rootLogger.info(
+ `Call view model implementation: ${CallViewModelImplementation.MatrixRtc}`,
+ );
+ const reactionsReader = new ParticipationReactionsReader(
+ scope,
+ participation,
+ drivers.clientDriver,
+ );
+ vm = createCallViewModel$(
+ scope,
+ participation,
+ drivers.clientDriver,
+ mediaDevices,
+ props.muteStates,
+ options,
+ reactionsReader.raisedHands$,
+ reactionsReader.reactions$,
+ scope.behavior(trackProcessorState$),
+ );
+ } else if (rtcSession !== undefined) {
+ rootLogger.info(
+ `Call view model implementation: ${CallViewModelImplementation.MatrixJsSdk}`,
+ );
+ const reactionsReader = new ReactionsReader(scope, rtcSession);
+ vm = createJsClientCallViewModel$(
+ scope,
+ rtcSession,
+ rtcSession.room,
+ mediaDevices,
+ props.muteStates,
+ options,
+ reactionsReader.raisedHands$,
+ reactionsReader.reactions$,
+ scope.behavior(trackProcessorState$),
+ );
+ } else {
+ return; // unreachable: checked above
+ }
// TODO move this somewhere else once we use the callViewModel in the lobby as well!
vm.join();
setVm(vm);
@@ -163,8 +222,8 @@ export const ActiveCall: FC = (props) => {
scope.end();
};
}, [
- props.rtcSession,
- props.matrixRoom,
+ rtcSession,
+ client,
props.muteStates,
props.e2eeSystem,
props.onLeft,
@@ -172,10 +231,39 @@ export const ActiveCall: FC = (props) => {
hostBridge,
mediaDevices,
trackProcessorState$,
- props.client,
rootElement,
+ participation,
+ drivers,
]);
+ // Who we are, as the tiles, hands and reactions are keyed. Both drivers
+ // and client name the same identity; the drivers are always there.
+ const ownIdentifier =
+ drivers !== null
+ ? `${drivers.clientDriver.userId}:${drivers.clientDriver.deviceId}`
+ : `${client?.getUserId()}:${client?.getDeviceId()}`;
+
+ // Reactions relate to our current membership event, wherever that lives.
+ const jsSdkMemberships = useMatrixRTCSessionMemberships(rtcSession);
+ const ownParticipationMembership = useBehavior(
+ participation?.ownMembership$ ?? NO_OWN_MEMBERSHIP,
+ );
+ const ownMembershipEventId =
+ participation !== null
+ ? ownParticipationMembership?.member.eventId
+ : jsSdkMemberships.find(
+ (m) =>
+ m.userId === client?.getUserId() &&
+ m.deviceId === client?.getDeviceId(),
+ )?.eventId;
+ const reactionsTimeline = useMemo(
+ () =>
+ participation === null && client !== undefined
+ ? jsSdkReactionsTimeline(client, roomId)
+ : drivers!.clientDriver,
+ [participation, drivers, client, roomId],
+ );
+
useEffect(() => {
if (vm === null) return;
@@ -185,7 +273,7 @@ export const ActiveCall: FC = (props) => {
vm,
props.muteStates,
mediaDevices,
- `${props.client.getUserId()}:${props.client.getDeviceId()}`,
+ ownIdentifier,
{ showControls: urlParams.showControls, header: urlParams.header },
);
setFooterVm(footerVm);
@@ -195,15 +283,15 @@ export const ActiveCall: FC = (props) => {
scope.end();
};
}, [
- props.rtcSession,
- props.matrixRoom,
+ rtcSession,
+ client,
props.muteStates,
props.e2eeSystem,
props.onLeft,
urlParams,
mediaDevices,
trackProcessorState$,
- props.client,
+ ownIdentifier,
vm,
]);
@@ -212,7 +300,12 @@ export const ActiveCall: FC = (props) => {
if (developerSettingsVm === null) return null;
return (
-
+
= (props) => {
};
export interface InCallViewProps {
- client: MatrixClient;
+ /** The matrix-js-sdk client, when that implementation carries the call. */
+ client?: MatrixClient;
vm: CallViewModel;
footerVm: ViewModel;
developerSettingsVm: ViewModel;
matrixInfo: MatrixInfo;
- rtcSession: MatrixRTCSession;
- matrixRoom: MatrixRoom;
+ /** The matrix-js-sdk session, when that implementation carries the call. */
+ rtcSession?: MatrixRTCSession;
+ roomId: string;
muteStates: MuteStates;
onShareClick: (() => void) | null;
}
@@ -241,7 +336,7 @@ export const InCallView: FC = ({
footerVm,
developerSettingsVm,
matrixInfo,
- matrixRoom,
+ roomId,
muteStates,
onShareClick,
}) => {
@@ -634,9 +729,7 @@ export const InCallView: FC = ({
}
};
- const rageshakeRequestModalProps = useRageshakeRequestModal(
- matrixRoom.roomId,
- );
+ const rageshakeRequestModalProps = useRageshakeRequestModal(roomId);
useAppBarSecondaryButton(
= ({
setSettingsOpen(false)}
tab={settingsTab}
diff --git a/src/room/InviteModal.test.tsx b/src/room/InviteModal.test.tsx
index 79f3f9285..2d66d17e0 100644
--- a/src/room/InviteModal.test.tsx
+++ b/src/room/InviteModal.test.tsx
@@ -7,25 +7,27 @@ Please see LICENSE in the repository root for full details.
import { render, screen } from "@testing-library/react";
import { expect, test, vi } from "vitest";
-import { type Room } from "matrix-js-sdk";
import { axe } from "vitest-axe";
import { BrowserRouter } from "react-router-dom";
import userEvent from "@testing-library/user-event";
import { InviteModal } from "./InviteModal";
+import { E2eeType } from "../e2ee/e2eeType";
// Used by copy-to-clipboard
window.prompt = (): null => null;
test("InviteModal is accessible", async () => {
const user = userEvent.setup();
- const room = {
- roomId: "!a:example.org",
- name: "Mission Control",
- } as unknown as Room;
const onDismiss = vi.fn();
const { container } = render(
- ,
+ ,
{ wrapper: BrowserRouter },
);
diff --git a/src/room/InviteModal.tsx b/src/room/InviteModal.tsx
index 759dce302..94545afe0 100644
--- a/src/room/InviteModal.tsx
+++ b/src/room/InviteModal.tsx
@@ -13,7 +13,6 @@ import {
useState,
} from "react";
import { useTranslation } from "react-i18next";
-import { type Room } from "matrix-js-sdk";
import { Button, Text } from "@vector-im/compound-web";
import {
LinkIcon,
@@ -25,22 +24,29 @@ import { Modal } from "../Modal";
import { getAbsoluteRoomUrl } from "../utils/matrix";
import styles from "./InviteModal.module.css";
import { Toast } from "../Toast";
-import { useRoomEncryptionSystem } from "../e2ee/sharedKeyManagement";
+import { type EncryptionSystem } from "../e2ee/sharedKeyManagement";
import { QrCode } from "../QrCode";
interface Props {
- room: Room;
+ roomId: string;
+ roomName: string;
+ e2eeSystem: EncryptionSystem;
open: boolean;
onDismiss: () => void;
}
-export const InviteModal: FC = ({ room, open, onDismiss }) => {
+export const InviteModal: FC = ({
+ roomId,
+ roomName,
+ e2eeSystem,
+ open,
+ onDismiss,
+}) => {
const { t } = useTranslation();
- const e2eeSystem = useRoomEncryptionSystem(room.roomId);
const url = useMemo(
- () => getAbsoluteRoomUrl(room.roomId, e2eeSystem, room.name),
- [e2eeSystem, room.name, room.roomId],
+ () => getAbsoluteRoomUrl(roomId, e2eeSystem, roomName),
+ [e2eeSystem, roomName, roomId],
);
const [toastOpen, setToastOpen] = useState(false);
const onToastDismiss = useCallback(() => setToastOpen(false), [setToastOpen]);
diff --git a/src/room/LobbyView.tsx b/src/room/LobbyView.tsx
index 9e6e0ed99..b75744c50 100644
--- a/src/room/LobbyView.tsx
+++ b/src/room/LobbyView.tsx
@@ -53,7 +53,8 @@ import { type ViewModel } from "../state/ViewModel";
import { useAppBarPrimaryButtonIconKind } from "../AppBar";
interface Props {
- client: MatrixClient;
+ /** The matrix-js-sdk client, for what the developer settings still read from it. */
+ client?: MatrixClient;
matrixInfo: MatrixInfo;
muteStates: MuteStates;
onEnter: () => void;
@@ -258,15 +259,13 @@ export const LobbyView: FC = ({
)}
- {client && (
-
- )}
+
>
);
};
diff --git a/src/room/RoomPage.tsx b/src/room/RoomPage.tsx
index 71bceea76..a9494216a 100644
--- a/src/room/RoomPage.tsx
+++ b/src/room/RoomPage.tsx
@@ -6,7 +6,14 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
-import { type FC, useEffect, useState, type ReactNode, useRef } from "react";
+import {
+ type ComponentProps,
+ type FC,
+ useEffect,
+ useState,
+ type ReactNode,
+ useRef,
+} from "react";
import { type MatrixError } from "matrix-js-sdk";
import { logger } from "matrix-js-sdk/lib/logger";
import { Trans, useTranslation } from "react-i18next";
@@ -16,6 +23,10 @@ import { useClientLegacy } from "../ClientContext";
import { ErrorPage, FullScreenView, LoadingPage } from "../FullScreenView";
import { RoomAuthView } from "./RoomAuthView";
import { CallView } from "./CallView";
+import { type MatrixClient } from "matrix-js-sdk";
+import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
+import { MatrixDriverProvider } from "../driver/MatrixDriverContext";
+import { useJsSdkDrivers } from "../driver/jsSdk/useJsSdkDrivers";
import { useRoomIdentifier, useUrlParams } from "../UrlParams";
import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser";
import { HomePage } from "../home/HomePage";
@@ -102,7 +113,7 @@ export const RoomPage: FC = (): ReactNode => {
switch (groupCallState.kind) {
case "loaded":
return (
- {
if (!roomIdOrAlias) return ;
return groupCallView();
};
+
+/**
+ * The call, with the matrix-js-sdk drivers over this page's client provided
+ * for the Rust implementation (see `CallViewModelImplementation`).
+ */
+const LoadedCall: FC<
+ ComponentProps & {
+ client: MatrixClient;
+ rtcSession: MatrixRTCSession;
+ }
+> = (props) => {
+ const drivers = useJsSdkDrivers(props.client, props.rtcSession.room);
+ return (
+
+
+
+ );
+};
diff --git a/src/room/useJoinRule.ts b/src/room/useJoinRule.ts
deleted file mode 100644
index ae17e1626..000000000
--- a/src/room/useJoinRule.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
-Copyright 2022-2024 New Vector Ltd.
-
-SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
-Please see LICENSE in the repository root for full details.
-*/
-
-import { useCallback } from "react";
-
-import type { JoinRule, Room } from "matrix-js-sdk";
-import { useRoomState } from "./useRoomState";
-
-export function useJoinRule(room: Room): JoinRule {
- return useRoomState(
- room,
- useCallback((state) => state.getJoinRule(), []),
- );
-}
diff --git a/src/room/useRoomAvatar.ts b/src/room/useRoomAvatar.ts
deleted file mode 100644
index 7287c6521..000000000
--- a/src/room/useRoomAvatar.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
-Copyright 2022-2024 New Vector Ltd.
-
-SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
-Please see LICENSE in the repository root for full details.
-*/
-
-import { useCallback } from "react";
-import { type Room } from "matrix-js-sdk";
-
-import { useRoomState } from "./useRoomState";
-
-export function useRoomAvatar(room: Room): string | null {
- return useRoomState(
- room,
- useCallback(() => room.getMxcAvatarUrl(), [room]),
- );
-}
diff --git a/src/room/useRoomInfo.test.tsx b/src/room/useRoomInfo.test.tsx
new file mode 100644
index 000000000..1b2032e92
--- /dev/null
+++ b/src/room/useRoomInfo.test.tsx
@@ -0,0 +1,64 @@
+/*
+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 { act, renderHook } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+import { type ReactNode } from "react";
+
+import { MatrixDriverProvider } from "../driver/MatrixDriverContext";
+import { MockElementCallMatrixClientDriver } from "../driver/MockElementCallMatrixClientDriver";
+import { MockRtcMatrixDriver } from "../driver/MockRtcMatrixDriver";
+import { useOwnProfile } from "../profile/useOwnProfile";
+import { useRoomInfo } from "./useRoomInfo";
+
+function providing(clientDriver: MockElementCallMatrixClientDriver) {
+ return ({ children }: { children: ReactNode }): ReactNode => (
+
+ {children}
+
+ );
+}
+
+describe("useRoomInfo", () => {
+ it("reads the room from the client driver and follows its changes", () => {
+ const clientDriver = new MockElementCallMatrixClientDriver({
+ roomInfo: { name: "Mission Control", joinRule: "invite" },
+ });
+ const { result } = renderHook(() => useRoomInfo(), {
+ wrapper: providing(clientDriver),
+ });
+ expect(result.current).toMatchObject({
+ name: "Mission Control",
+ joinRule: "invite",
+ });
+ act(() => clientDriver.setRoomInfo({ name: "Launch Control" }));
+ expect(result.current.name).toBe("Launch Control");
+ });
+
+ it("throws without drivers", () => {
+ expect(() => renderHook(() => useRoomInfo())).toThrow(/No Matrix drivers/);
+ });
+});
+
+describe("useOwnProfile", () => {
+ it("reads our own profile and follows its changes", () => {
+ const clientDriver = new MockElementCallMatrixClientDriver({
+ ownProfile: { displayName: "Me", avatarUrl: "mxc://x/me" },
+ });
+ const { result } = renderHook(() => useOwnProfile(), {
+ wrapper: providing(clientDriver),
+ });
+ expect(result.current).toEqual({
+ displayName: "Me",
+ avatarUrl: "mxc://x/me",
+ });
+ act(() => clientDriver.setOwnProfile({ displayName: "Myself" }));
+ expect(result.current.displayName).toBe("Myself");
+ });
+});
diff --git a/src/room/useRoomInfo.ts b/src/room/useRoomInfo.ts
new file mode 100644
index 000000000..385b6e70b
--- /dev/null
+++ b/src/room/useRoomInfo.ts
@@ -0,0 +1,36 @@
+/*
+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 { useEffect, useState } from "react";
+
+import {
+ type RoomDriver,
+ type RoomInfo,
+} from "../driver/ElementCallMatrixClientDriver";
+import { useMatrixDrivers } from "../driver/MatrixDriverContext";
+
+/**
+ * The room's name, alias, avatar, join rule, encryption and whether we may
+ * open its slot, from the given room driver, kept current.
+ */
+export function useRoomInfoFrom(room: RoomDriver): RoomInfo {
+ const [info, setInfo] = useState(() => room.getRoomInfo());
+ useEffect(() => {
+ setInfo(room.getRoomInfo());
+ return room.subscribeRoomInfo(setInfo);
+ }, [room]);
+ return info;
+}
+
+/**
+ * {@link useRoomInfoFrom} for the call's room: the client driver the host
+ * provided. Replaces `useRoomName`, `useRoomAvatar` and `useJoinRule` under
+ * `CallView`.
+ */
+export function useRoomInfo(): RoomInfo {
+ return useRoomInfoFrom(useMatrixDrivers().clientDriver);
+}
diff --git a/src/room/useRoomState.ts b/src/room/useRoomState.ts
deleted file mode 100644
index ad08f7a21..000000000
--- a/src/room/useRoomState.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
-Copyright 2022-2024 New Vector Ltd.
-
-SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
-Please see LICENSE in the repository root for full details.
-*/
-
-import { useCallback } from "react";
-import {
- type RoomState,
- RoomStateEvent,
- type Room,
- RoomEvent,
-} from "matrix-js-sdk";
-
-import { useTypedEventEmitterState } from "../useEvents";
-
-/**
- * A React hook for values computed from room state.
- * @param room The room.
- * @param f A mapping from the current room state to the computed value.
- * @returns The computed value.
- */
-export function useRoomState(room: Room, f: (state: RoomState) => T): T {
- // TODO: matrix-js-sdk says that Room.currentState is deprecated, but it's not
- // clear how to reactively track the current state of the room without it
- const currentState = useTypedEventEmitterState(
- room,
- RoomEvent.CurrentStateUpdated,
- useCallback(() => room.currentState, [room]),
- );
- return useTypedEventEmitterState(
- currentState,
- RoomStateEvent.Update,
- useCallback(() => f(currentState), [f, currentState]),
- );
-}
diff --git a/src/settings/DeveloperSettingsTab.tsx b/src/settings/DeveloperSettingsTab.tsx
index ec5bf7b08..d73beca15 100644
--- a/src/settings/DeveloperSettingsTab.tsx
+++ b/src/settings/DeveloperSettingsTab.tsx
@@ -45,6 +45,7 @@ import {
muteAllAudio as muteAllAudioSetting,
alwaysShowIphoneEarpiece as alwaysShowIphoneEarpieceSetting,
matrixRTCMode as matrixRTCModeSetting,
+ callViewModelImplementation as callViewModelImplementationSetting,
customLivekitUrl as customLivekitUrlSetting,
advancedScreenShare as advancedScreenShareSetting,
screenShareResolution as screenShareResolutionSetting,
@@ -62,12 +63,17 @@ import {
type VideoCodec,
enableExtendedLivekitLogs as enableExtendedLivekitLogsSetting,
} from "./settings";
-import { MatrixRTCMode } from "../config/ConfigOptions";
+import {
+ CallViewModelImplementation,
+ MatrixRTCMode,
+} from "../config/ConfigOptions";
import styles from "./DeveloperSettingsTab.module.css";
import settingsStyles from "./SettingsModal.module.css";
import { Slider } from "../Slider";
import { useUrlParams } from "../UrlParams";
import { getSFUConfigWithOpenID } from "../livekit/openIDSFU";
+import { useOptionalMatrixDrivers } from "../driver/MatrixDriverContext";
+import { ELEMENT_CALL_SLOT_ID } from "../state/rtc/slot";
import { useBehavior } from "../useBehavior";
import { type ViewModel } from "../state/ViewModel.ts";
@@ -103,7 +109,8 @@ const KeyRotationStatus: FC<{ info: KeyRotationInfo }> = ({ info }) => (
);
interface Props {
- client: MatrixClient;
+ /** The matrix-js-sdk client, when the host has one; the drivers otherwise. */
+ client?: MatrixClient;
roomId?: string;
livekitRooms?: {
room: LivekitRoom;
@@ -129,17 +136,40 @@ export const DeveloperSettingsTab: FC = ({
debugTileLayoutSetting,
);
+ const drivers = useOptionalMatrixDrivers();
const [stickyEventsSupported, setStickyEventsSupported] = useState(false);
useEffect(() => {
- client
- .doesServerSupportUnstableFeature(UNSTABLE_MSC4354_STICKY_EVENTS)
+ const probe =
+ client !== undefined
+ ? client.doesServerSupportUnstableFeature(
+ UNSTABLE_MSC4354_STICKY_EVENTS,
+ )
+ : drivers !== null
+ ? drivers.clientDriver
+ .getCapabilities()
+ .then((capabilities) => capabilities.stickyEvents)
+ : Promise.resolve(false);
+ probe
.then((result) => {
setStickyEventsSupported(result);
})
.catch((ex) => {
logger.warn("Failed to check if sticky events are supported", ex);
});
- }, [client]);
+ }, [client, drivers]);
+ // What the host knows about itself, for the facts below.
+ const [diagnostics, setDiagnostics] = useState>({});
+ useEffect(() => {
+ drivers?.clientDriver
+ .getDiagnostics?.()
+ .then(setDiagnostics)
+ .catch((ex) =>
+ logger.warn("Could not read the driver's diagnostics", ex),
+ );
+ }, [drivers]);
+ const ownUserId = client?.getUserId() ?? drivers?.clientDriver.userId ?? null;
+ const ownDeviceId =
+ client?.getDeviceId() ?? drivers?.clientDriver.deviceId ?? null;
const [matrixRTCMode, setMatrixRTCMode] = useSetting(matrixRTCModeSetting);
const matrixRTCModeRadioGroup = useId();
@@ -155,6 +185,20 @@ export const DeveloperSettingsTab: FC = ({
const matrixRTCModeForced = configMatrixRTCMode !== undefined;
const effectiveMatrixRTCMode = configMatrixRTCMode ?? matrixRTCMode;
+ const [implementation, setImplementation] = useSetting(
+ callViewModelImplementationSetting,
+ );
+ const implementationRadioGroup = useId();
+ const onImplementationChange = useCallback(
+ (e: ChangeEvent) => {
+ setImplementation(e.target.value as CallViewModelImplementation);
+ },
+ [setImplementation],
+ );
+ const configImplementation = Config.get().call_view_model_implementation;
+ const implementationForced = configImplementation !== undefined;
+ const effectiveImplementation = configImplementation ?? implementation;
+
const [showConnectionStats, setShowConnectionStats] = useSetting(
showConnectionStatsSetting,
);
@@ -387,17 +431,20 @@ export const DeveloperSettingsTab: FC = ({
{t("developer_mode.crypto_version", {
- version: client.getCrypto()?.getVersion() || "unknown",
+ version:
+ client?.getCrypto()?.getVersion() ||
+ diagnostics.crypto_version ||
+ "unknown",
})}
{t("developer_mode.matrix_id", {
- id: client.getUserId() || "unknown",
+ id: ownUserId || "unknown",
})}
{t("developer_mode.device_id", {
- id: client.getDeviceId() || "unknown",
+ id: ownDeviceId || "unknown",
})}
{keyRotation !== null && }
@@ -512,25 +559,45 @@ export const DeveloperSettingsTab: FC = ({
}
try {
- const userId = client.getUserId();
- const deviceId = client.getDeviceId();
-
- if (userId === null || deviceId === null) {
+ if (ownUserId === null || ownDeviceId === null) {
throw new Error("Invalid user or device ID");
}
- await getSFUConfigWithOpenID(
- client,
- { userId, deviceId, memberId: "" },
- customLivekitUrlTextBuffer,
- roomId,
- );
+ if (client !== undefined) {
+ await getSFUConfigWithOpenID(
+ client,
+ { userId: ownUserId, deviceId: ownDeviceId, memberId: "" },
+ customLivekitUrlTextBuffer,
+ roomId,
+ );
+ } else if (drivers !== null) {
+ // Ask the service for a token the way the crate would.
+ await drivers.rtcDriver.getLivekitToken({
+ url: customLivekitUrlTextBuffer,
+ roomId,
+ slotId: ELEMENT_CALL_SLOT_ID,
+ memberJson: JSON.stringify({
+ id: "",
+ claimed_user_id: ownUserId,
+ claimed_device_id: ownDeviceId,
+ }),
+ legacySfuGet: false,
+ });
+ }
setCustomLivekitUrlUpdateError(null);
setCustomLivekitUrl(customLivekitUrlTextBuffer);
} catch {
setCustomLivekitUrlUpdateError("invalid URL (did not update)");
}
},
- [customLivekitUrlTextBuffer, setCustomLivekitUrl, client, roomId],
+ [
+ customLivekitUrlTextBuffer,
+ setCustomLivekitUrl,
+ client,
+ drivers,
+ roomId,
+ ownUserId,
+ ownDeviceId,
+ ],
)}
value={customLivekitUrlTextBuffer ?? ""}
onChange={useCallback(
@@ -590,6 +657,63 @@ export const DeveloperSettingsTab: FC = ({
+
+
+ {t("developer_mode.callViewModelImplementation.title")}
+
+ {implementationForced && (
+ {t("developer_mode.callViewModelImplementation.forced")}
+ )}
+
{livekitRooms?.map((livekitRoom) => (
diff --git a/src/settings/ProfileSettingsTab.tsx b/src/settings/ProfileSettingsTab.tsx
index 7a4ac0770..e7c4bc9bc 100644
--- a/src/settings/ProfileSettingsTab.tsx
+++ b/src/settings/ProfileSettingsTab.tsx
@@ -5,23 +5,48 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
-import { type FC, useCallback, useEffect, useMemo, useRef } from "react";
-import { type MatrixClient } from "matrix-js-sdk";
+import { type FC, useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { logger } from "matrix-js-sdk/lib/logger";
-import { useProfile } from "../profile/useProfile";
+import { useOwnProfile } from "../profile/useOwnProfile";
+import { useMatrixDrivers } from "../driver/MatrixDriverContext";
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
import { AvatarInputField } from "../input/AvatarInputField";
import styles from "./ProfileSettingsTab.module.css";
-interface Props {
- client: MatrixClient;
-}
-export const ProfileSettingsTab: FC = ({ client }) => {
+export const ProfileSettingsTab: FC = () => {
const { t } = useTranslation();
- const { error, displayName, avatarUrl, saveProfile } = useProfile(client);
- const userId = useMemo(() => client.getUserId(), [client]);
+ const { clientDriver } = useMatrixDrivers();
+ const { displayName, avatarUrl } = useOwnProfile();
+ const userId = clientDriver.userId;
+ const [error, setError] = useState(undefined);
+ // The driver may offer no way to edit the profile; then it is shown as is.
+ const canEdit =
+ clientDriver.setDisplayName !== undefined &&
+ clientDriver.setAvatar !== undefined;
+ const saveProfile = useCallback(
+ async ({
+ displayName,
+ avatar,
+ removeAvatar,
+ }: {
+ displayName: string;
+ avatar: Blob | undefined;
+ removeAvatar: boolean;
+ }): Promise => {
+ try {
+ await clientDriver.setDisplayName?.(displayName);
+ if (removeAvatar) await clientDriver.setAvatar?.(null);
+ else if (avatar) await clientDriver.setAvatar?.(avatar);
+ setError(undefined);
+ } catch (e) {
+ setError(e instanceof Error ? e : new Error(String(e)));
+ throw e;
+ }
+ },
+ [clientDriver],
+ );
const formRef = useRef(null);
@@ -58,9 +83,7 @@ export const ProfileSettingsTab: FC = ({ client }) => {
saveProfile({
displayName,
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- avatar: avatar && avatarSize > 0 ? avatar : undefined,
+ avatar: avatar instanceof Blob && avatarSize > 0 ? avatar : undefined,
removeAvatar: removeAvatar.current && (!avatar || avatarSize === 0),
}).catch((e) => {
logger.error("Failed to save profile", e);
@@ -72,15 +95,16 @@ export const ProfileSettingsTab: FC = ({ client }) => {
return (
Currently, no overwrite is set. Url from config is used.
@@ -285,10 +285,10 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
class="_container_1ug7n_10"
>
renders and matches snapshot 1`] = `
>
Compatibility: state events & multi SFU
Compatible with homeservers that do not support sticky events (but all other EC clients are v0.17.0 or later)
@@ -326,9 +326,9 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
class="_container_1ug7n_10"
>
renders and matches snapshot 1`] = `
>
Matrix 2.0: sticky events & multi SFU
Compatible only with homservers supporting sticky events and all EC clients v0.17.0 or later
+
+
+ Call implementation
+
+
@@ -459,7 +555,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
class="_field_1bd8c0 _checkboxField_1bd8c0"
>
@@ -489,7 +585,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
Configure resolution, framerate, bitrate, and codec for camera video. Changes apply on next call join.
@@ -511,7 +607,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
class="_field_1bd8c0 _checkboxField_1bd8c0"
>
@@ -541,7 +637,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
Configure resolution, framerate, bitrate, and codec for screen sharing
@@ -566,7 +662,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
class="_field_1bd8c0 _checkboxField_1bd8c0"
>
renders and matches snapshot 1`] = `
class="_field_1bd8c0 _checkboxField_1bd8c0"
>
renders and matches snapshot 1`] = `
class="_field_1bd8c0 _checkboxField_1bd8c0"
>
{
public constructor(
@@ -153,6 +156,18 @@ export const matrixRTCMode = new Setting
(
MatrixRTCMode.Compatibility,
);
+/**
+ * Which implementation carries the call (see
+ * {@link CallViewModelImplementation}). Sampled when the call view mounts;
+ * `config.json` may pin it. Defaults to matrix-js-sdk until the crate path
+ * has passed its gate.
+ */
+export const callViewModelImplementation =
+ new Setting(
+ "call-view-model-implementation",
+ CallViewModelImplementation.MatrixJsSdk,
+ );
+
export const customLivekitUrl = new Setting(
"custom-livekit-url",
null,
diff --git a/src/settings/submit-rageshake.ts b/src/settings/submit-rageshake.ts
index eaa304e72..a30494667 100644
--- a/src/settings/submit-rageshake.ts
+++ b/src/settings/submit-rageshake.ts
@@ -23,6 +23,8 @@ import { deepCompare } from "matrix-js-sdk/lib/utils";
import { advancedCamera as advancedCameraSetting } from "./settings";
import { advancedScreenShare as advancedScreenShareSetting } from "./settings";
import { DEFAULT_CONFIG } from "../config/ConfigOptions";
+import { effectiveCallViewModelImplementation } from "../state/rtc/implementation";
+import { useOptionalMatrixDrivers } from "../driver/MatrixDriverContext";
const gzip = async (text: string): Promise => {
// pako is relatively large (200KB), so we only import it when needed
const { gzip: pakoGzip } = await import("pako");
@@ -143,6 +145,7 @@ export function useSubmitRageshake(
available: boolean;
} {
const { client } = useClient();
+ const drivers = useOptionalMatrixDrivers();
const [{ sending, sent, error }, setState] = useState<{
sending: boolean;
@@ -202,6 +205,25 @@ export function useSubmitRageshake(
body.append("installed_pwa", "false");
body.append("touch_input", touchInput);
body.append("call_backend", "livekit");
+ body.append(
+ "call_view_model_implementation",
+ effectiveCallViewModelImplementation(),
+ );
+ // What the host's drivers know, and the crate's own view of the
+ // call when it carries it.
+ if (drivers?.clientDriver.getDiagnostics) {
+ try {
+ for (const [key, value] of Object.entries(
+ await drivers.clientDriver.getDiagnostics(),
+ ))
+ body.append(`driver_${key}`, value);
+ } catch (e) {
+ logger.warn("Could not collect the driver's diagnostics", e);
+ }
+ }
+ const participation = window.matrixRtc?.participation;
+ if (participation)
+ body.append("matrix_rtc_snapshot", participation.debugSnapshot());
body.append("hostname", window.location.hostname);
if (client) {
@@ -316,7 +338,7 @@ export function useSubmitRageshake(
logger.error(error);
}
},
- [client, sending, injectedGetRageshakeSubmitUrl],
+ [client, drivers, sending, injectedGetRageshakeSubmitUrl],
);
return {
@@ -333,19 +355,24 @@ export function useRageshakeRequest(): (
rageshakeRequestId: string,
) => void {
const { client } = useClient();
+ const drivers = useOptionalMatrixDrivers();
const sendRageshakeRequest = useCallback(
(roomId: string, rageshakeRequestId: string) => {
- client!
- // @ts-expect-error - org.matrix.rageshake_request is not part of `keyof TimelineEvents` but it is okay to sent a custom event.
- .sendEvent(roomId, "org.matrix.rageshake_request", {
- request_id: rageshakeRequestId,
- })
- .catch((e) => {
- logger.error("Failed to send org.matrix.rageshake_request event", e);
- });
+ const content = { request_id: rageshakeRequestId };
+ const sent: Promise =
+ drivers !== null
+ ? drivers.clientDriver.sendRoomEvent(
+ "org.matrix.rageshake_request",
+ content,
+ )
+ : // @ts-expect-error - org.matrix.rageshake_request is not part of `keyof TimelineEvents` but it is okay to sent a custom event.
+ client!.sendEvent(roomId, "org.matrix.rageshake_request", content);
+ sent.catch((e: unknown) => {
+ logger.error("Failed to send org.matrix.rageshake_request event", e);
+ });
},
- [client],
+ [client, drivers],
);
return sendRageshakeRequest;
}
@@ -356,9 +383,22 @@ export function useRageshakeRequestModal(
const [open, setOpen] = useState(false);
const onDismiss = useCallback(() => setOpen(false), [setOpen]);
const { client } = useClient();
+ const drivers = useOptionalMatrixDrivers();
const [rageshakeRequestId, setRageshakeRequestId] = useState();
useEffect(() => {
+ if (drivers !== null) {
+ const { clientDriver } = drivers;
+ return clientDriver.subscribeTimeline((event) => {
+ if (
+ event.type === "org.matrix.rageshake_request" &&
+ event.sender !== clientDriver.userId
+ ) {
+ setRageshakeRequestId(event.content.request_id as string);
+ setOpen(true);
+ }
+ });
+ }
if (!client) return;
const onEvent = (event: MatrixEvent): void => {
@@ -379,7 +419,7 @@ export function useRageshakeRequestModal(
return (): void => {
client.removeListener(ClientEvent.Event, onEvent);
};
- }, [setOpen, roomId, client]);
+ }, [setOpen, roomId, client, drivers]);
return {
rageshakeRequestId: rageshakeRequestId ?? "",
diff --git a/src/state/CallViewModel/remoteMembers/ParticipationMembers.ts b/src/state/CallViewModel/remoteMembers/ParticipationMembers.ts
index a4cb30dba..ac6d6d92b 100644
--- a/src/state/CallViewModel/remoteMembers/ParticipationMembers.ts
+++ b/src/state/CallViewModel/remoteMembers/ParticipationMembers.ts
@@ -24,7 +24,11 @@ export interface ParticipationRoster {
ownMemberId$: Behavior;
}
-/** The crate's membership as a tile sees it. */
+/**
+ * The crate's membership as a tile sees it. `deviceId` falls back to the
+ * member id so that `${userId}:${deviceId}` is the member's media id
+ * (`memberMediaId` in `src/state/rtc/mediaId.ts`).
+ */
export function callMemberOf(membership: FfiMembership): CallMember {
const { member } = membership;
return {
diff --git a/src/state/rtc/CallParticipation.test.ts b/src/state/rtc/CallParticipation.test.ts
index 5a46d44fe..3ed7cb0dd 100644
--- a/src/state/rtc/CallParticipation.test.ts
+++ b/src/state/rtc/CallParticipation.test.ts
@@ -306,6 +306,11 @@ describe("CallParticipation", () => {
callParticipation.keyMap$.value.some((k) => k.memberId === peer.memberId),
);
expect(changes.some((k) => k.memberId === peer.memberId)).toBe(true);
+ // Counted for the ended-call analytics: theirs received, ours sent.
+ expect(callParticipation.mediaKeyStatistics()).toMatchObject({
+ received: 1,
+ sent: changes.filter((k) => k.memberId !== peer.memberId).length,
+ });
driver.peerLeaves(peer);
// the crate keeps a LeftWithKeys entry; the behavior does not
await waitFor(
diff --git a/src/state/rtc/CallParticipation.ts b/src/state/rtc/CallParticipation.ts
index e284ce7f4..e4b36a86c 100644
--- a/src/state/rtc/CallParticipation.ts
+++ b/src/state/rtc/CallParticipation.ts
@@ -67,6 +67,14 @@ export interface SlotPolicy {
/** How long to wait for the seed, and for our own slot event to echo back. */
const SLOT_WAIT_MS = 15_000;
+/** Media keys sent and received over a participation; see {@link CallParticipation.mediaKeyStatistics}. */
+export interface MediaKeyStatistics {
+ sent: number;
+ received: number;
+ /** Sum of the ages of the received keys on arrival, in ms. */
+ receivedTotalAge: number;
+}
+
export interface CallParticipationOptions {
/**
* One manager per `(room, slot)`; Element Call has one slot per room.
@@ -203,6 +211,7 @@ export class CallParticipation {
this.manager.setKeyMapListener({
onKeyMapChange: (keyMap, change) => {
if (this.ended) return;
+ this.countKey(change);
this.keyMapSubject$.next(keyMap);
this.keyChangesSubject$.next(change);
},
@@ -301,6 +310,34 @@ export class CallParticipation {
}
}
+ private readonly keyStatistics: MediaKeyStatistics = {
+ sent: 0,
+ received: 0,
+ receivedTotalAge: 0,
+ };
+
+ private countKey(change: FfiMediaKey): void {
+ if (change.memberId === this.manager.ownMemberId()) {
+ this.keyStatistics.sent++;
+ } else {
+ this.keyStatistics.received++;
+ this.keyStatistics.receivedTotalAge += Math.max(
+ 0,
+ Date.now() - Number(change.creationTsMs),
+ );
+ }
+ }
+
+ /**
+ * How many media keys this participation has sent and received so far,
+ * and how old the received ones were on arrival (summed), for the
+ * ended-call analytics. Counted from the crate's key changes: one of ours
+ * per index we rotate to, one of theirs per key that reaches us.
+ */
+ public mediaKeyStatistics(): MediaKeyStatistics {
+ return { ...this.keyStatistics };
+ }
+
/** Leave the session. A no-op when not joined. */
public async leave(code?: string, reason?: string): Promise {
if (this.ended) return;
diff --git a/src/state/rtc/implementation.ts b/src/state/rtc/implementation.ts
new file mode 100644
index 000000000..04dadf882
--- /dev/null
+++ b/src/state/rtc/implementation.ts
@@ -0,0 +1,31 @@
+/*
+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 { Config } from "../../config/Config";
+import { CallViewModelImplementation } from "../../config/ConfigOptions";
+import { callViewModelImplementation } from "../../settings/settings";
+
+/**
+ * Which implementation carries the call right now: the deployment's pin in
+ * `config.json` when there is one, else the user's Developer Settings
+ * choice. Read when the call view mounts; a change takes effect on the next
+ * call.
+ */
+export function effectiveCallViewModelImplementation(): CallViewModelImplementation {
+ return (
+ Config.get().call_view_model_implementation ??
+ callViewModelImplementation.value$.value
+ );
+}
+
+/** Whether the Rust `matrix-rtc` crate carries the call. */
+export function usesMatrixRtc(): boolean {
+ return (
+ effectiveCallViewModelImplementation() ===
+ CallViewModelImplementation.MatrixRtc
+ );
+}
diff --git a/src/state/rtc/mediaId.ts b/src/state/rtc/mediaId.ts
new file mode 100644
index 000000000..92217b4f5
--- /dev/null
+++ b/src/state/rtc/mediaId.ts
@@ -0,0 +1,18 @@
+/*
+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 { type FfiMember } from "../../matrix-rtc-sdk";
+
+/**
+ * The id the call view keys a member's tiles, hands and reactions by:
+ * `${userId}:${deviceId}`, as it has always been. The crate does not always
+ * know a member's device (an unencrypted room, a pre-sticky event); the
+ * member id stands in then, which keeps the id unique.
+ */
+export function memberMediaId(member: FfiMember): string {
+ return `${member.userId}:${member.deviceId ?? member.memberId}`;
+}
diff --git a/src/state/rtc/useCallParticipation.test.tsx b/src/state/rtc/useCallParticipation.test.tsx
new file mode 100644
index 000000000..2924623cc
--- /dev/null
+++ b/src/state/rtc/useCallParticipation.test.tsx
@@ -0,0 +1,56 @@
+/*
+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 { beforeAll, describe, expect, it } from "vitest";
+import { renderHook, waitFor } from "@testing-library/react";
+
+import { MatrixRTCMode } from "../../config/ConfigOptions";
+import { type MatrixDrivers } from "../../driver/MatrixDriverContext";
+import { MockElementCallMatrixClientDriver } from "../../driver/MockElementCallMatrixClientDriver";
+import { MockRtcMatrixDriver } from "../../driver/MockRtcMatrixDriver";
+import { initMatrixRtcSdkForTests } from "../../utils/test-matrix-rtc";
+import { participationConfig } from "./joinParams";
+import { useCallParticipation } from "./useCallParticipation";
+
+const config = participationConfig({
+ mode: MatrixRTCMode.Matrix_2_0,
+ manageMediaKeys: false,
+ session: {
+ delayed_leave: { delay_ms: 15_000 },
+ delegated_delayed_leave: { delay_ms: 3_600_000 },
+ network_error_retry_ms: 1000,
+ },
+});
+
+describe("useCallParticipation", () => {
+ beforeAll(async () => {
+ await initMatrixRtcSdkForTests();
+ });
+
+ it("creates a participation for the drivers and ends it on unmount", async () => {
+ const drivers: MatrixDrivers = {
+ rtcDriver: new MockRtcMatrixDriver(),
+ clientDriver: new MockElementCallMatrixClientDriver(),
+ };
+ const { result, rerender, unmount } = renderHook(
+ ({ drivers }) => useCallParticipation(drivers, config),
+ { initialProps: { drivers: null as MatrixDrivers | null } },
+ );
+ // Nothing without drivers (matrix-js-sdk carries the call).
+ expect(result.current).toBeNull();
+
+ rerender({ drivers });
+ await waitFor(() => expect(result.current).not.toBeNull());
+ const participation = result.current!;
+ // It seeds the session from the driver right away.
+ await waitFor(() => expect(participation.session$.value.seeded).toBe(true));
+
+ unmount();
+ // Ended: the manager is gone, its diagnostics say nothing.
+ expect(participation.debugSnapshot()).toBe("{}");
+ });
+});
diff --git a/src/state/rtc/useCallParticipation.ts b/src/state/rtc/useCallParticipation.ts
new file mode 100644
index 000000000..e3d11b719
--- /dev/null
+++ b/src/state/rtc/useCallParticipation.ts
@@ -0,0 +1,86 @@
+/*
+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 { useEffect, useState } from "react";
+import { logger } from "matrix-js-sdk/lib/logger";
+
+import { type MatrixDrivers } from "../../driver/MatrixDriverContext";
+import {
+ type FfiParticipationConfig,
+ initMatrixRtcSdk,
+} from "../../matrix-rtc-sdk";
+import { ObservableScope } from "../ObservableScope";
+import {
+ CallParticipation,
+ type CallParticipationOptions,
+} from "./CallParticipation";
+
+/**
+ * A {@link CallParticipation} for the mounted call view: created when the
+ * drivers or the configuration change, ended (leaving the session if still
+ * joined) when the view unmounts. Null for the first render, like the other
+ * scoped objects the views own.
+ *
+ * Owned by `CallView` rather than the in-call view so that the lobby already
+ * sees the roster and a rejoin does not rebuild the session seed.
+ *
+ * The SDK's wasm is loaded here, on first use, rather than at app start: the
+ * matrix-js-sdk implementation must not pay for the crate's ~6.5 MB
+ * (oxidation plan §5.15), and the load is idempotent, so a rejoin or a later
+ * call finds it already booted. A failed load is thrown from the hook so the
+ * nearest error boundary shows it instead of the call silently never
+ * starting.
+ */
+export function useCallParticipation(
+ drivers: MatrixDrivers | null,
+ config: FfiParticipationConfig | null,
+ options: Omit = {},
+): CallParticipation | null {
+ const [participation, setParticipation] = useState(
+ null,
+ );
+ const [loadError, setLoadError] = useState(null);
+ const { transportFallbackUrl, slotId } = options;
+ useEffect(() => {
+ if (drivers === null || config === null) return;
+ const scope = new ObservableScope();
+ const { clientDriver, rtcDriver } = drivers;
+ // Whether the view is still mounted with these inputs by the time the
+ // SDK is ready; if not, nothing is created and the scope is already over.
+ let ended = false;
+ initMatrixRtcSdk().then(
+ () => {
+ if (ended) return;
+ logger.info(
+ `[Lifecycle] Creating the call participation for ${clientDriver.roomId} (compat ${config.compat})`,
+ );
+ const participation = new CallParticipation(
+ scope,
+ rtcDriver,
+ clientDriver.roomId,
+ clientDriver.userId,
+ clientDriver.deviceId,
+ { config, transportFallbackUrl, slotId },
+ );
+ setParticipation(participation);
+ },
+ (e: unknown) => {
+ if (ended) return;
+ logger.error("[Lifecycle] Failed to load the MatrixRTC SDK", e);
+ setLoadError(e);
+ },
+ );
+ return (): void => {
+ ended = true;
+ logger.info("[Lifecycle] Ending the call participation");
+ setParticipation(null);
+ scope.end();
+ };
+ }, [drivers, config, transportFallbackUrl, slotId]);
+ if (loadError !== null) throw loadError;
+ return participation;
+}
diff --git a/src/tile/GridTile.test.tsx b/src/tile/GridTile.test.tsx
index 60fbc303c..55f195106 100644
--- a/src/tile/GridTile.test.tsx
+++ b/src/tile/GridTile.test.tsx
@@ -12,7 +12,6 @@ import {
import { test, expect } from "vitest";
import { act, render, screen } from "@testing-library/react";
import { axe } from "vitest-axe";
-import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
import { BehaviorSubject } from "rxjs";
import { GridTile } from "./GridTile";
@@ -25,7 +24,10 @@ import {
mockMediaDevices,
} from "../utils/test";
import { GridTileViewModel } from "../state/TileViewModel";
-import { ReactionsSenderProvider } from "../reactions/useReactionsSender";
+import {
+ ReactionsSenderProvider,
+ type ReactionsTimeline,
+} from "../reactions/useReactionsSender";
import type { CallViewModel } from "../state/CallViewModel/CallViewModel";
import { constant } from "../state/Behavior";
import {
@@ -39,21 +41,10 @@ global.IntersectionObserver = class MockIntersectionObserver {
public disconnect(): void {}
} as unknown as typeof IntersectionObserver;
-const fakeRtcSession = {
- on: () => {},
- off: () => {},
- room: {
- on: () => {},
- off: () => {},
- client: {
- getUserId: () => null,
- getDeviceId: () => null,
- on: () => {},
- off: () => {},
- },
- },
- memberships: [],
-} as unknown as MatrixRTCSession;
+const fakeTimeline: ReactionsTimeline = {
+ sendRoomEvent: async () => Promise.resolve({ eventId: "$reaction" }),
+ redactEvent: async () => Promise.resolve(),
+};
const callVm = {
reactions$: constant({}),
@@ -75,7 +66,12 @@ test("GridTile displays remote media", async () => {
);
const { container } = render(
-
+
{}}
@@ -109,7 +105,12 @@ test("GridTile displays local media", async () => {
);
const { container } = render(
-
+
{}}
@@ -142,7 +143,12 @@ test("GridTile displays ringing media", async () => {
});
const { container } = render(
-
+
{}}
diff --git a/src/useEvents.ts b/src/useEvents.ts
index 3495cc574..82fbeeeca 100644
--- a/src/useEvents.ts
+++ b/src/useEvents.ts
@@ -35,26 +35,6 @@ export function useEventTarget(
}, [target, eventType, listener, options]);
}
-/**
- * Shortcut for registering a listener on a TypedEventEmitter.
- */
-export function useTypedEventEmitter<
- Events extends string,
- Arguments extends ListenerMap,
- T extends Events,
->(
- emitter: TypedEventEmitter,
- eventType: T,
- listener: Listener,
-): void {
- useEffect(() => {
- emitter.on(eventType, listener);
- return (): void => {
- emitter.off(eventType, listener);
- };
- }, [emitter, eventType, listener]);
-}
-
/**
* Reactively tracks a value which is recalculated whenever the provided event
* emitter emits an event. This is useful for bridging state from matrix-js-sdk
diff --git a/src/useMatrixRTCSessionMemberships.ts b/src/useMatrixRTCSessionMemberships.ts
index 0dba6b152..f94020bba 100644
--- a/src/useMatrixRTCSessionMemberships.ts
+++ b/src/useMatrixRTCSessionMemberships.ts
@@ -1,5 +1,5 @@
/*
-Copyright 2023, 2024 New Vector Ltd.
+Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
@@ -10,16 +10,29 @@ import {
type MatrixRTCSession,
MatrixRTCSessionEvent,
} from "matrix-js-sdk/lib/matrixrtc";
-import { useCallback } from "react";
+import { useCallback, useSyncExternalStore } from "react";
-import { useTypedEventEmitterState } from "./useEvents";
+const NONE: CallMembership[] = [];
+/**
+ * The memberships of a matrix-js-sdk session, kept current; none without a
+ * session (the Rust crate carries the call then).
+ */
export function useMatrixRTCSessionMemberships(
- rtcSession: MatrixRTCSession,
+ rtcSession: MatrixRTCSession | undefined,
): CallMembership[] {
- return useTypedEventEmitterState(
- rtcSession,
- MatrixRTCSessionEvent.MembershipsChanged,
- useCallback(() => rtcSession.memberships, [rtcSession]),
+ const subscribe = useCallback(
+ (onChange: () => void) => {
+ if (rtcSession === undefined) return (): void => {};
+ rtcSession.on(MatrixRTCSessionEvent.MembershipsChanged, onChange);
+ return (): void => {
+ rtcSession.off(MatrixRTCSessionEvent.MembershipsChanged, onChange);
+ };
+ },
+ [rtcSession],
+ );
+ return useSyncExternalStore(
+ subscribe,
+ useCallback(() => rtcSession?.memberships ?? NONE, [rtcSession]),
);
}