mirror of
https://github.com/vector-im/element-call.git
synced 2026-09-22 22:29:30 +00:00
Add rust based createCallViewModel
This commit is contained in:
+125
-46
@@ -16,8 +16,10 @@ Status legend: ☐ todo · ◐ in progress · ☑ done.
|
||||
**Where things stand (2026-09-14):** S0a, S0b, S1a, S1b and S2 are
|
||||
implemented and green (`pnpm lint`, `pnpm format:check`, `pnpm test:unit`:
|
||||
101 files / 785 tests). Nothing is committed yet, in either repository: the
|
||||
crate changes (C2–C8; C1 was reverted in favour of slot opening; C11 and C12 done;
|
||||
C9 and C10 are pending) sit uncommitted in
|
||||
crate changes C2–C12 (C1 reverted in favour of slot opening) are done; C2–C8,
|
||||
C11 and C12 are committed in the draft repo as `a8e21b3`, C5 (final form), C9
|
||||
and C10 are still uncommitted there. In Element Call everything sits
|
||||
uncommitted in
|
||||
`~/Projects/matrix-rust-rtc/MatrixSdkArchitectureDraft`, and Element Call's
|
||||
branch `toger5/oxidation` holds the vendored bindings, the driver layer
|
||||
(`src/driver/**`), the participation layer (`src/state/rtc/**`) and the
|
||||
@@ -136,8 +138,9 @@ membership, roster, encryption, impairments} | Leaving`.
|
||||
is closed, so a room without an `m.rtc.slot` has no call, in every dialect.
|
||||
`openSlot(application, encrypted)` / `closeSlot()` send the state event;
|
||||
the slot id `m.call#ROOM` matches js-sdk's default.
|
||||
- `FfiElementCallCompat.{Off, StickyEvents, StateEvents}` today; `StickyEvents`
|
||||
is removed by C9, leaving `Off` (spec MSC4143) and `StateEvents` (MSC3401).
|
||||
- `FfiElementCallCompat.{Off, StateEvents}`: spec MSC4143, or MSC3401 state
|
||||
events for the clients that predate sticky events (C9 removed the never
|
||||
deployed 2025 sticky dialect).
|
||||
- All `u64` fields are `bigint` in TypeScript; `Vec<u8>` is `ArrayBuffer`.
|
||||
- Listener callbacks arrive one timer tick after the emitting call (pumps
|
||||
sleep through `setTimeout`, `executor.rs:86`); getters are fresh.
|
||||
@@ -157,20 +160,21 @@ membership, roster, encryption, impairments} | Leaving`.
|
||||
|
||||
**Must change in the crate (S0a) — each blocks a later slice:**
|
||||
|
||||
| # | Problem | Change |
|
||||
| --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| C1 | A successful `read_state("m.rtc.slot")` returning `[]` marks slot state supplied and every slot other than the legacy `""` resolves `Closed` (`session/state.rs`); `join()` then fails with `SlotClosed` and every MSC4143 peer is excluded. Element Call never sent `m.rtc.slot`. | **Kept as the crate has it: no slot means no call.** Element Call opens the slot when nobody has (`CallParticipation.join` with a `SlotPolicy`: `openSlot("m.call", encrypted)`, then wait for the echo), which needs the power level to send `org.matrix.msc4143.rtc.slot`; without it the join fails with `NoOpenSlotError`. Existing rooms keep working because the first call in a room opens its slot. A compat-mode relaxation was tried and reverted. |
|
||||
| C2 | `manage_media_keys`, `require_cross_signed_sender`, `use_key_delay_ms` are not settable over the FFI; defaults are `true`, `true`, 1000 ms. | New record `FfiParticipationConfig { compat, manage_media_keys, require_cross_signed_sender, use_key_delay_ms }` as the constructor argument (replaces the bare `compat`). |
|
||||
| C3 | `StickyEvents` compat sent keys as `org.matrix.msc4143.rtc.encryption_key` while deployed clients read only `io.element.call.encryption_keys`. | Made moot by C9 and reverted with it: the sticky dialect goes away entirely, so `Off` sends the spec key message and `StateEvents` the legacy one, with no middle case. |
|
||||
| C4 | `FfiMember` has no membership `event_id`; reactions relate to it (§4.3). | `Member.event_id: Option<String>` threaded through `session/dispatch.rs` → `convert/*` → `state.rs` (currently dropped at `state.rs:402`) → `FfiMember.event_id`. |
|
||||
| C5 | Delegating the delayed leave (MSC4195) is split between the crate and the host: the draft's driver method makes the _adapter_ perform the whole delegation, its demo adapter calls a homeserver endpoint that 401s on a widget client, and Element Call today does it differently (probe, then the authorisation service's `get_token` with `delay_id`/`delay_timeout`/`delay_cs_api_url`). Nothing of this exists in the real `crates/` yet. | **The crate owns the policy; the driver keeps two primitives.** (a) `delegate_delayed_leave_via_homeserver(room, slot, member, delay_id)`: one authenticated POST to the CS API endpoint (`/_matrix/client/unstable/io.element.msc4195/rtc/livekit/delegate_delayed_leave`, the spelling Element Call probes today; a widget client answers `Unsupported`). (b) `LivekitTokenRequest.delegation: Option<{ delay_id, delay_timeout_ms }>`: the adapter appends `delay_id`, `delay_timeout` and `delay_cs_api_url` (its own homeserver URL) to the `get_token` / `sfu/get` body it already sends. The own-membership machine tries (a) first and, on `Unsupported` or any failure, (b) against the transport we publish on, i.e. Element Call's OpenID → JWT → scheduled-event path; only if both fail does it keep restarting the switch itself. **Arm-after-confirm:** the short delayed leave stays armed through the join; delegation arms a second, ≥ 1 h delayed leave, delegates _that_, and cancels the short one on success (or the long one on failure), so no moment is left without an armed leave and a failed delegation costs nothing. `KeepAlive::Delegated` says which route succeeded. The interim C5 (service URL and delay on the request) and Element Call's driver-side probe and JWT delegation are replaced by this. |
|
||||
| C6 | The transport identity is a pure function (`connections/mod.rs:116-135`) but not exported; the own identity is needed before our membership echo to set our own media key. | Export `FfiParticipationManager.own_transport_identity(): Option<String>`. |
|
||||
| C7 | Doc comment on `FfiMembership.connections` says `ws_url`s. | Fix the comment. |
|
||||
| C8 | Both converters set `display_name` / `avatar_url` to `None`, so every host would re-derive them from room members. | The session records each `m.room.member` profile (whether or not the roster condition is enforced) and stamps it on members at projection time. |
|
||||
| C9 | `ElementCallCompat::StickyEvents` models the 2025 Element Call sticky dialect (`member: {user_id, device_id, id}`, flat `rtc_transports`, `versions`, legacy key message). No deployment uses it: sticky-event calls have not shipped. | **Remove the mode.** Delete `own_membership/compat_2025.rs`, the 2025 block in `session/convert/msc4143.rs` and its dispatch arm, the `StickyEvents` variants of `ElementCallCompat` / `FfiElementCallCompat`, and their tests and acceptance tests; `outbound_event_type` / `build_content` keep two arms (`Off`, `StateEvents`). Element Call then maps `Matrix_2_0 → Off`: spec MSC4143 sticky events with slots (which Element Call opens, C1) and the spec key message. |
|
||||
| C10 | `MediaKeyState` has `holds_our_key`, `have_their_key` and `rejection`, so a tile learns about an unsigned sender only when `require_cross_signed_sender` is on (the key is discarded, `rejection: NotCrossSigned`). With the check off the verdict is dropped on accept and the tile cannot show an unverified sender. | Keep the MSC4153 verdict of the accepted key per member and expose it as `FfiMediaKeyState.sender_cross_signed: Option<bool>` (`None` when the host could not tell). Element Call runs with the check off (§5.8) and wants to show the state on the tile until it is turned on. |
|
||||
| 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. |
|
||||
| # | Problem | Change |
|
||||
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| C1 | A successful `read_state("m.rtc.slot")` returning `[]` marks slot state supplied and every slot other than the legacy `""` resolves `Closed` (`session/state.rs`); `join()` then fails with `SlotClosed` and every MSC4143 peer is excluded. Element Call never sent `m.rtc.slot`. | **Kept as the crate has it: no slot means no call.** Element Call opens the slot when nobody has (`CallParticipation.join` with a `SlotPolicy`: `openSlot("m.call", encrypted)`, then wait for the echo), which needs the power level to send `org.matrix.msc4143.rtc.slot`; without it the join fails with `NoOpenSlotError`. Existing rooms keep working because the first call in a room opens its slot. A compat-mode relaxation was tried and reverted. |
|
||||
| C2 | `manage_media_keys`, `require_cross_signed_sender`, `use_key_delay_ms` are not settable over the FFI; defaults are `true`, `true`, 1000 ms. | New record `FfiParticipationConfig { compat, manage_media_keys, require_cross_signed_sender, use_key_delay_ms }` as the constructor argument (replaces the bare `compat`). |
|
||||
| C3 | `StickyEvents` compat sent keys as `org.matrix.msc4143.rtc.encryption_key` while deployed clients read only `io.element.call.encryption_keys`. | Made moot by C9 and reverted with it: the sticky dialect goes away entirely, so `Off` sends the spec key message and `StateEvents` the legacy one, with no middle case. |
|
||||
| C4 | `FfiMember` has no membership `event_id`; reactions relate to it (§4.3). | `Member.event_id: Option<String>` threaded through `session/dispatch.rs` → `convert/*` → `state.rs` (currently dropped at `state.rs:402`) → `FfiMember.event_id`. |
|
||||
| C5 | **Done.** Delegating the delayed leave (MSC4195) is split between the crate and the host: the draft's driver method makes the _adapter_ perform the whole delegation, its demo adapter calls a homeserver endpoint that 401s on a widget client, and Element Call today does it differently (probe, then the authorisation service's `get_token` with `delay_id`/`delay_timeout`/`delay_cs_api_url`). Nothing of this exists in the real `crates/` yet. | **The crate owns the policy; the driver keeps two primitives.** (a) `delegate_delayed_leave_via_homeserver(room, slot, member, delay_id)`: one authenticated POST to the CS API endpoint (`/_matrix/client/unstable/io.element.msc4195/rtc/livekit/delegate_delayed_leave`, the spelling Element Call probes today; a widget client answers `Unsupported`). (b) `LivekitTokenRequest.delegation: Option<{ delay_id, delay_timeout_ms }>`: the adapter appends `delay_id`, `delay_timeout` and `delay_cs_api_url` (its own homeserver URL) to the `get_token` / `sfu/get` body it already sends. The own-membership machine tries (a) first and, on `Unsupported` or any failure, (b) against the transport we publish on, i.e. Element Call's OpenID → JWT → scheduled-event path; only if both fail does it keep restarting the switch itself. **Arm-after-confirm:** the short delayed leave stays armed through the join; delegation arms a second, ≥ 1 h delayed leave, delegates _that_, and cancels the short one on success (or the long one on failure), so no moment is left without an armed leave and a failed delegation costs nothing. `KeepAlive::Delegated` says which route succeeded. The interim C5 (service URL and delay on the request) and Element Call's driver-side probe and JWT delegation are replaced by this. **Real-backend check (2026-09-15):** both routes send the MSC4195 `member` _claims_ (`{ id, claimed_user_id, claimed_device_id }`), not the member block of the event. The homeserver route body is what lk-jwt-service 0.7 accepts behind a Synapse that proxies `rtc/livekit/*` to it (MSC4512, `backend/app-service.yaml`): `{ url, room_id, slot_id, member, delay_id, delay_timeout }`, where `url` is the SFU websocket URL the transport's token named (the service checks it is its own). The transport resolver therefore returns `ResolvedTransport { transport, sfu_url }` and the machine carries the url into `Action::DelegateViaHomeserver`; without an SFU url it goes straight to the service route, and a receive-only member (no transport) delegates nothing. Verified end to end against `pnpm backend`: the homeserver route takes the 1 h leave (`KeepAlive::Delegated { via: Homeserver }`, one delayed event of 3 600 000 ms on the server). |
|
||||
| C6 | The transport identity is a pure function (`connections/mod.rs:116-135`) but not exported; the own identity is needed before our membership echo to set our own media key. | Export `FfiParticipationManager.own_transport_identity(): Option<String>`. |
|
||||
| C7 | Doc comment on `FfiMembership.connections` says `ws_url`s. | Fix the comment. |
|
||||
| C8 | Both converters set `display_name` / `avatar_url` to `None`, so every host would re-derive them from room members. | The session records each `m.room.member` profile (whether or not the roster condition is enforced) and stamps it on members at projection time. |
|
||||
| C9 | `ElementCallCompat::StickyEvents` models the 2025 Element Call sticky dialect (`member: {user_id, device_id, id}`, flat `rtc_transports`, `versions`, legacy key message). No deployment uses it: sticky-event calls have not shipped. | **Done.** Removed: `own_membership/compat_2025.rs`, the 2025 block in `session/convert/msc4143.rs` and its dispatch arm, the `StickyEvents` variants of `ElementCallCompat` / `FfiElementCallCompat`, and their tests and acceptance tests; `outbound_event_type` / `build_content` keep two arms (`Off`, `StateEvents`). Element Call then maps `Matrix_2_0 → Off`: spec MSC4143 sticky events with slots (which Element Call opens, C1) and the spec key message. |
|
||||
| C10 | `MediaKeyState` has `holds_our_key`, `have_their_key` and `rejection`, so a tile learns about an unsigned sender only when `require_cross_signed_sender` is on (the key is discarded, `rejection: NotCrossSigned`). With the check off the verdict is dropped on accept and the tile cannot show an unverified sender. | **Done.** The inbound key store keeps the MSC4153 verdict of the accepted key per member and exposes it as `FfiMediaKeyState.sender_cross_signed: Option<bool>` (`None` when the host could not tell). Element Call runs with the check off (§5.8) and wants to show the state on the tile until it is turned on. |
|
||||
| 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. |
|
||||
|
||||
No crate work is deferred: `update_application` is C11.
|
||||
|
||||
@@ -401,9 +405,8 @@ documented as such in `docs/`: `delayed_leave.restart_ms`,
|
||||
`FfiParticipationConfig.useKeyDelayMs`.
|
||||
|
||||
Compat: `MatrixRTCMode.Compatibility → StateEvents`, `Matrix_2_0 → Off` (spec
|
||||
MSC4143: sticky member events, slots, the spec key message). Until C9 lands the
|
||||
code still maps `Matrix_2_0` to the crate's `StickyEvents`; `compatForMode` is
|
||||
the one place that changes.
|
||||
MSC4143: sticky member events, slots, the spec key message), in
|
||||
`compatForMode`.
|
||||
|
||||
### 4.4 React tree
|
||||
|
||||
@@ -485,8 +488,8 @@ Differences from the draft, all required by the widget client:
|
||||
is one `authedRequest` on a full client and `Unsupported` on a widget
|
||||
client; `getLivekitToken` appends `delay_id`, `delay_timeout` and
|
||||
`delay_cs_api_url` (`client.baseUrl`) when the request carries a
|
||||
delegation. The crate decides when to call which (C5). Until C5 lands the
|
||||
driver still carries Element Call's probe and JWT delegation; both go then.
|
||||
delegation. The crate decides when to call which (C5). Element Call carries no
|
||||
delegation probe or policy.
|
||||
- sticky listener attached after `startClient()` resolves (the widget room
|
||||
only exists then, `embedded.ts:326`).
|
||||
- `getLivekitToken` reuses today's request shapes (`slot_id: "m.call#ROOM"`,
|
||||
@@ -530,8 +533,9 @@ RateLimited`, 403 → `Rejected`, 404/`M_UNRECOGNIZED` → `Unsupported`.
|
||||
9. **Delegation is the crate's alone.** Element Call always asks for it; the
|
||||
crate tries the CS API, then the authorisation service's token endpoint,
|
||||
then its own restarts, and arms the long delegated leave only once
|
||||
delegation is confirmed (C5). Element Call keeps no probe and no
|
||||
delegation code.
|
||||
delegation is confirmed (C5, done). Element Call keeps no probe and no
|
||||
delegation code; `delegated_delayed_leave.delay_ms` feeds
|
||||
`FfiJoinParams.delegatedDelayMs`.
|
||||
10. **Widget trust model:** origins synthesised from claimed device ids equal
|
||||
today's js-sdk trust level; the crate records them as
|
||||
`DeviceAttribution::Verified` because it cannot tell. Documented in the
|
||||
@@ -539,6 +543,20 @@ RateLimited`, 403 → `Rejected`, 404/`M_UNRECOGNIZED` → `Unsupported`.
|
||||
11. **Scratch files** go to `agent-workspace/oxidation/`; this plan lives at
|
||||
the repo root because it was asked for by name.
|
||||
12. **Branch** `toger5/oxidation`, one commit per slice.
|
||||
13. **Compatibility mode has no slot.** Under `StateEvents` the crate projects
|
||||
the session from the MSC3401 state events alone and requires the legacy
|
||||
slot id `""` (`LEGACY_SLOT_ID` in `src/state/rtc/slot.ts`,
|
||||
`slotIdForCompat`); `CallParticipation` picks it from the config and skips
|
||||
the slot check and the slot open. Found by the real-backend check: with
|
||||
`m.call#ROOM` the crate saw its own legacy membership as a candidate but
|
||||
never projected it. Element Call's own rooms already let every member send
|
||||
the legacy member state event (`state_default: 0`); a plain room does not,
|
||||
which the backend test reproduces.
|
||||
14. **Member events in encrypted rooms are Megolm-encrypted** by matrix-js-sdk
|
||||
(sticky events are timeline events; the SDK exempts only reactions and
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -600,26 +618,50 @@ Gate: `cargo test --features uniffi`, `cargo clippy --all-targets --features uni
|
||||
`Disconnected{LeftByHost}`; join → leave → join; scope end destroys the
|
||||
manager; fallback transport when the host throws or advertises none.
|
||||
|
||||
### S3 — view model, four slices ☐
|
||||
### S3 — view model, four slices ☑ (side by side with the js-sdk view model)
|
||||
|
||||
- **S3a** `Connection`/`ConnectionManager`/`ConnectionFactory` keyed by
|
||||
`serviceUrl`, fed by `Behavior<{ serviceUrl, wsUrl, jwt, expiresAtTs }[]>`,
|
||||
with a temporary adapter from today's `SFUConfig` so `openIDSFU` stays
|
||||
until S3c. Introduces `mockCallParticipation()` and `mockFfiMembership()` in
|
||||
`src/utils/test.ts`.
|
||||
- **S3b** `MatrixLivekitMembers` + `MatrixKeyProvider` on `FfiMembership` /
|
||||
key changes, with an adapter from `CallMembership` for the still-js-sdk
|
||||
`memberships$`.
|
||||
- **S3c** `LocalMember` on `callParticipation.join/leave` + status-derived
|
||||
connectivity; delete `LocalTransport.ts`, `RtcTransportAutoDiscovery.ts`,
|
||||
`HomeserverConnected.ts`, `openIDSFU.ts`, `enterRTCSession`.
|
||||
- **S3d** `createCallViewModel$` signature, `CallNotificationLifecycle`,
|
||||
`MatrixMemberMetadata`, `SessionBehaviors.ts` and
|
||||
`useMatrixRTCSessionMemberships.ts` deleted, `ReactionsReader` on
|
||||
`participation` + `TimelineDriver`; test kit swapped
|
||||
(`MockRTCSession`/`mockRtcMembership` deleted). To stay green before S5,
|
||||
`CallView` builds a `JsSdkMatrixDriver` from its existing `client` /
|
||||
`rtcSession.room` props as a temporary shim.
|
||||
- **Shape (2026-09-15):** the existing factory is renamed
|
||||
`createJsClientCallViewModel$` and the new `createCallViewModel$(scope,
|
||||
participation, clientDriver, …)` sits next to it; both build a
|
||||
`CallViewModelCore` and hand it to the shared `assembleCallViewModel`, so
|
||||
the layout/tile half is one piece of code and the two Matrix sides can be
|
||||
reviewed side by side. `MatrixLivekitMember.membership$` is a neutral
|
||||
`CallMember` (`userId`, `deviceId`, `memberId`, `rtcBackendIdentity`) that
|
||||
js-sdk's `CallMembership` satisfies; `CallNotificationLifecycle` takes a
|
||||
neutral `DeclineEvent`. Nothing js-sdk is deleted yet (that is S6).
|
||||
- **S3a** `remoteMembers/ParticipationConnections.ts`: one `Connection` per
|
||||
service URL in `participation.connections$`, started with the crate's
|
||||
token; a refreshed token keeps the connection (used on the next
|
||||
(re)connect). `Connection`/`ECConnectionFactory` accept a `null` client.
|
||||
Test fakes live in `src/utils/test-participation.ts`
|
||||
(`FakeParticipation`, `fakeMembership`, `fakeConnection`, `fakeMediaKey`).
|
||||
- **S3b** `remoteMembers/ParticipationMembers.ts` (remote members matched by
|
||||
`transportIdentity`, keyed by `memberId`; `callMemberOf`) and
|
||||
`e2ee/participationKeyProvider.ts` (`keyMap$` × `memberships$` ×
|
||||
`ownTransportIdentity$` → `onSetEncryptionKey`, once per (member, index,
|
||||
identity); a key that arrives before the identity waits for the roster).
|
||||
- **S3c** `localMember/LocalMedia.ts` is the LiveKit half extracted from
|
||||
`LocalMember.ts` (publisher, tracks, screen share, upstream pausing, host
|
||||
notify) and shared; `localMember/ParticipationLocalMember.ts` joins and
|
||||
leaves through the participation (slot policy from `roomInfo`, custom
|
||||
LiveKit URL as a `Publish` intent, `updateApplication` on camera toggle),
|
||||
derives connected / reconnecting / `probablyLeft` from `status$`
|
||||
(`HomeserverUnreachable` → "sync", `RestartFailing`/`Expired` →
|
||||
"probablyLeft") and the fatal error from `errorForStatus`. Deletions
|
||||
(`LocalTransport.ts`, `RtcTransportAutoDiscovery.ts`,
|
||||
`HomeserverConnected.ts`, `openIDSFU.ts`, `enterRTCSession`) wait for S6.
|
||||
- **S3d** `ParticipationCallNotification.ts` sends `org.matrix.msc4075.rtc.notification`
|
||||
after our own echo when nobody was in the session before us (resets on
|
||||
leave) and reads declines from the `TimelineDriver`;
|
||||
`remoteMembers/ParticipationMemberMetadata.ts` adapts the client driver's
|
||||
roster to `RoomMemberMap` so `createMatrixMemberMetadata$` and the ringing
|
||||
name work unchanged. Still open: `ReactionsReader` on `participation` +
|
||||
`TimelineDriver` (the new factory takes the hands/reactions observables
|
||||
as inputs like the old one; S4b), `keyRotationSuppressed$` is `constant(false)`,
|
||||
and the js-sdk test kit stays until S6. Tests:
|
||||
`CallViewModel.participation.test.ts` (real wasm, mock drivers, mocked
|
||||
LiveKit: join → connection → own tile → peer → leave; transport-missing →
|
||||
`fatalError$`), plus one file per module.
|
||||
|
||||
### S4 — React tree, two slices ☐
|
||||
|
||||
@@ -635,9 +677,17 @@ Gate: `cargo test --features uniffi`, `cargo clippy --all-targets --features uni
|
||||
`ReactionsReader` keyed by `memberId`, `CallNotificationLifecycle` sending
|
||||
through the driver; tests incl. a membership re-send mid-call.
|
||||
|
||||
### S5 — hosts ☐
|
||||
### S5 — hosts ☐ (component props done 2026-09-15)
|
||||
|
||||
- `component/index.tsx`, `component/matrix-js-sdk.ts`, `component/package.json`
|
||||
- **Done:** `ElementCallProps` takes `rtcDriver` + `clientDriver` (`roomId`
|
||||
optional, asserted equal); `ElementCallClientBased` takes `client` +
|
||||
`roomId` and builds the two js-sdk drivers (the dev harness uses it); the
|
||||
driver types and the js-sdk drivers are exported from the component index.
|
||||
**Temporary:** until S4 the tree under `CallView` still runs on the
|
||||
client, so `ElementCall` requires the client driver to be
|
||||
`JsSdkElementCallMatrixClientDriver` (its `client`/`room` are public for
|
||||
this) and throws for any other driver.
|
||||
- `component/matrix-js-sdk.ts`, `component/package.json`
|
||||
(`exports`, peers), `vite-component.config.ts` (two entries, `fileName`
|
||||
function, externals shrink, `?url&no-inline`), `component/tsconfig.build.json`,
|
||||
`component/dev/Harness.tsx`, `README.md`.
|
||||
@@ -691,6 +741,7 @@ Gate: `cargo test --features uniffi`, `cargo clippy --all-targets --features uni
|
||||
| four builds | | ● | | | | | ● | ● |
|
||||
| Playwright standalone + widget + component | | | | | | | ● | ● |
|
||||
| Manual: two harness panes hear each other, E2EE, hand raise, reaction, leave | | | | | | | ● | |
|
||||
| Real backend (`CallParticipation.backend.test.ts`, opt-in, both modes) | | | | ● | | | ● | |
|
||||
|
||||
---
|
||||
|
||||
@@ -728,3 +779,31 @@ gaps (§2); knip `ignore` vs `ignoreFiles`, oxlint/oxfmt ignores, `.d.ts` for
|
||||
the glue, lazy wasm boot, Storybook `beforeAll`; slice re-cut (S0a/b, S1a/b,
|
||||
S3a–d, S4a/b, temporary `CallView` shim); `sdk/main.ts` status wiring and the
|
||||
Playwright delegation helper.
|
||||
|
||||
**Real-backend check (2026-09-15), `pnpm backend` + `src/state/rtc/CallParticipation.backend.test.ts`**
|
||||
(`MATRIX_RTC_BACKEND=1 NODE_TLS_REJECT_UNAUTHORIZED=0 pnpm vitest run --project unit src/state/rtc/CallParticipation.backend.test.ts`;
|
||||
two registered users with rust crypto in an encrypted room, `matrix_2_0` and
|
||||
`compatibility`): passes end to end — transport discovery from
|
||||
`/rtc/transports`, slot open + echo, sticky member event with `msc4354_sticky`,
|
||||
delayed leave, delegation via the homeserver, roster with display names,
|
||||
Olm-encrypted media keys both ways with `senderCrossSigned`, leave cancelling
|
||||
the delayed event, and `HomeserverUnreachable` raised and cleared across a
|
||||
simulated network outage. Fixed on the way: MSC4195 `member` claims and the
|
||||
homeserver route's body (`url`, `delay_timeout`; C5), the missing session
|
||||
listener (C13), the legacy slot id under compatibility mode (§5.13). Noted:
|
||||
`vite-plugin-node-polyfills` shadows `process`, so tests read the environment
|
||||
through `node:process`; Synapse develop already proxies the MSC4195 endpoint
|
||||
to lk-jwt-service; member events are Megolm-encrypted in encrypted rooms
|
||||
(§5.14). Not verified here: the widget host path (Element Web) and the
|
||||
authorisation-service route (the homeserver route is taken first on this
|
||||
stack; it is covered by unit tests only).
|
||||
|
||||
**S3 (2026-09-15):** the driver-based `createCallViewModel$` landed next to
|
||||
the renamed `createJsClientCallViewModel$` (shared `assembleCallViewModel`,
|
||||
neutral `CallMember`/`DeclineEvent`, `LocalMedia.ts` extracted); component
|
||||
props moved to the two drivers with `ElementCallClientBased` on top. Gates:
|
||||
`pnpm lint`, `format:check`, `test:unit` (803), `i18n:check`,
|
||||
`build:component` green. Open from this slice: reactions reader on the
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user