From d3a4234f4a33e8d0c26b95b9c8aeb7c529c15c7f Mon Sep 17 00:00:00 2001 From: fkwp Date: Fri, 6 Feb 2026 11:54:22 +0100 Subject: [PATCH 01/44] setup webhooks for SFUs --- backend/dev_livekit-othersite.yaml | 4 ++++ backend/dev_livekit.yaml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/backend/dev_livekit-othersite.yaml b/backend/dev_livekit-othersite.yaml index 0ae98c24..53fc9ce9 100644 --- a/backend/dev_livekit-othersite.yaml +++ b/backend/dev_livekit-othersite.yaml @@ -18,3 +18,7 @@ keys: devkey: secret room: auto_create: false +webhook: + api_key: devkey + urls: + - https://matrix-rtc.othersite.m.localhost/livekit/jwt/sfu_webhook diff --git a/backend/dev_livekit.yaml b/backend/dev_livekit.yaml index 157e4d04..6cef4241 100644 --- a/backend/dev_livekit.yaml +++ b/backend/dev_livekit.yaml @@ -18,3 +18,7 @@ keys: devkey: secret room: auto_create: false +webhook: + api_key: devkey + urls: + - https://matrix-rtc.m.localhost/livekit/jwt/sfu_webhook From 7b1eb03e2a8ebd4ffa81338ee9ad7a6628205b5e Mon Sep 17 00:00:00 2001 From: fkwp Date: Fri, 6 Feb 2026 11:54:39 +0100 Subject: [PATCH 02/44] add ca certs to SFU containers such that webhooks are accepted --- dev-backend-docker-compose.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dev-backend-docker-compose.yml b/dev-backend-docker-compose.yml index 28682a33..31694c01 100644 --- a/dev-backend-docker-compose.yml +++ b/dev-backend-docker-compose.yml @@ -62,7 +62,10 @@ services: - 7882:7882/tcp - 50100-50200:50100-50200/udp volumes: + - ./backend/dev_tls_m.localhost.crt:/local_cert.pem:Z - ./backend/dev_livekit.yaml:/etc/livekit.yaml:Z + environment: + - SSL_CERT_FILE=/local_cert.pem networks: - ecbackend @@ -82,7 +85,10 @@ services: - 17882:17882/tcp - 50300-50400:50300-50400/udp volumes: + - ./backend/dev_tls_m.localhost.crt:/local_cert.pem:Z - ./backend/dev_livekit-othersite.yaml:/etc/livekit.yaml:Z + environment: + - SSL_CERT_FILE=/local_cert.pem networks: - ecbackend @@ -164,6 +170,8 @@ services: - "8448:8448" extra_hosts: - "host.docker.internal:host-gateway" + - "auth-server:127.0.0.1" + - "auth-server-1:127.0.0.1" depends_on: - synapse networks: From 9e2eef09d413cd4a6f5f6d34e6f6f9a9cd9ec5c7 Mon Sep 17 00:00:00 2001 From: fkwp Date: Wed, 22 Apr 2026 15:18:50 +0200 Subject: [PATCH 03/44] Add support for a grace period for /sync (aka homeserver disconnected) interruptions --- config/config.sample.json | 1 + src/config/ConfigOptions.ts | 8 ++++++++ src/state/CallViewModel/CallViewModel.ts | 2 ++ .../localMember/HomeserverConnected.test.ts | 19 ++++++++++--------- .../localMember/HomeserverConnected.ts | 14 ++++++++++++-- 5 files changed, 33 insertions(+), 11 deletions(-) diff --git a/config/config.sample.json b/config/config.sample.json index 126d7626..5fc63667 100644 --- a/config/config.sample.json +++ b/config/config.sample.json @@ -12,6 +12,7 @@ "feature_use_device_session_member_events": true }, "ssla": "https://static.element.io/legal/element-software-and-services-license-agreement-uk-1.pdf", + "sync_disconnect_grace_period_ms": 60000, "matrix_rtc_session": { "wait_for_key_rotation_ms": 3000, "membership_event_expiry_ms": 180000000, diff --git a/src/config/ConfigOptions.ts b/src/config/ConfigOptions.ts index 1b120546..d5839fb3 100644 --- a/src/config/ConfigOptions.ts +++ b/src/config/ConfigOptions.ts @@ -97,6 +97,13 @@ export interface ConfigOptions { enable_video?: boolean; }; + /** + * Grace period in milliseconds to wait before reporting the sync loop as disconnected. + * This allows brief sync interruptions without triggering a reconnection message. + * Default is 60000ms (60 seconds). Set to 0 to disable the grace period. + */ + sync_disconnect_grace_period_ms?: number; + /** * These are low level options that are used to configure the MatrixRTC session. * Take care when changing these options. @@ -168,5 +175,6 @@ export const DEFAULT_CONFIG: ResolvedConfigOptions = { features: { feature_use_device_session_member_events: true, }, + sync_disconnect_grace_period_ms: 60000, ssla: "https://static.element.io/legal/element-software-and-services-license-agreement-uk-1.pdf", }; diff --git a/src/state/CallViewModel/CallViewModel.ts b/src/state/CallViewModel/CallViewModel.ts index e298bcfd..8677f2d0 100644 --- a/src/state/CallViewModel/CallViewModel.ts +++ b/src/state/CallViewModel/CallViewModel.ts @@ -83,6 +83,7 @@ import { E2eeType } from "../../e2ee/e2eeType"; import { MatrixKeyProvider } from "../../e2ee/matrixKeyProvider"; import { type MuteStates } from "../MuteStates"; import { getUrlParams, HeaderStyle } from "../../UrlParams"; +import { Config } from "../../config/Config"; import { type ProcessorState } from "../../livekit/TrackProcessorContext"; import { ElementWidgetActions, widget } from "../../widget"; import { @@ -536,6 +537,7 @@ export function createCallViewModel$( scope, client, matrixRTCSession, + Config.get().sync_disconnect_grace_period_ms, ), muteStates, joinMatrixRTC: (transport: LivekitTransportConfig) => { diff --git a/src/state/CallViewModel/localMember/HomeserverConnected.test.ts b/src/state/CallViewModel/localMember/HomeserverConnected.test.ts index 87ca35d0..e8861641 100644 --- a/src/state/CallViewModel/localMember/HomeserverConnected.test.ts +++ b/src/state/CallViewModel/localMember/HomeserverConnected.test.ts @@ -96,19 +96,20 @@ describe("createHomeserverConnected$", () => { // LLM generated test cases. They are a bit overkill but I improved the mocking so it is // easy enough to read them so I think they can stay. + // Note: gracePeriodMs is set to 0 to avoid debouncing delays in tests it("is false when sync state is not Syncing", () => { - const hsConnected = createHomeserverConnected$(scope, client, session); + const hsConnected = createHomeserverConnected$(scope, client, session, 0); expect(hsConnected.combined$.value).toBe(false); }); it("remains false while membership status is not Connected even if sync is Syncing", () => { - const hsConnected = createHomeserverConnected$(scope, client, session); + const hsConnected = createHomeserverConnected$(scope, client, session, 0); client.setSyncState(SyncState.Syncing); expect(hsConnected.combined$.value).toBe(false); // membership still disconnected }); it("is false when membership status transitions to Connected but ProbablyLeft is true", () => { - const hsConnected = createHomeserverConnected$(scope, client, session); + const hsConnected = createHomeserverConnected$(scope, client, session, 0); // Make sync loop OK client.setSyncState(SyncState.Syncing); // Indicate probable leave before connection @@ -118,7 +119,7 @@ describe("createHomeserverConnected$", () => { }); it("becomes true only when all three conditions are satisfied", () => { - const hsConnected = createHomeserverConnected$(scope, client, session); + const hsConnected = createHomeserverConnected$(scope, client, session, 0); // 1. Sync loop connected client.setSyncState(SyncState.Syncing); expect(hsConnected.combined$.value).toBe(false); // not yet membership connected @@ -128,7 +129,7 @@ describe("createHomeserverConnected$", () => { }); it("drops back to false when sync loop leaves Syncing", () => { - const hsConnected = createHomeserverConnected$(scope, client, session); + const hsConnected = createHomeserverConnected$(scope, client, session, 0); // Reach connected state client.setSyncState(SyncState.Syncing); session.setMembershipStatus(Status.Connected); @@ -140,7 +141,7 @@ describe("createHomeserverConnected$", () => { }); it("drops back to false when membership status becomes disconnected", () => { - const hsConnected = createHomeserverConnected$(scope, client, session); + const hsConnected = createHomeserverConnected$(scope, client, session, 0); client.setSyncState(SyncState.Syncing); session.setMembershipStatus(Status.Connected); expect(hsConnected.combined$.value).toBe(true); @@ -150,7 +151,7 @@ describe("createHomeserverConnected$", () => { }); it("drops to false when ProbablyLeft is emitted after being true", () => { - const hsConnected = createHomeserverConnected$(scope, client, session); + const hsConnected = createHomeserverConnected$(scope, client, session, 0); client.setSyncState(SyncState.Syncing); session.setMembershipStatus(Status.Connected); expect(hsConnected.combined$.value).toBe(true); @@ -160,7 +161,7 @@ describe("createHomeserverConnected$", () => { }); it("recovers to true if ProbablyLeft becomes false again while other conditions remain true", () => { - const hsConnected = createHomeserverConnected$(scope, client, session); + const hsConnected = createHomeserverConnected$(scope, client, session, 0); client.setSyncState(SyncState.Syncing); session.setMembershipStatus(Status.Connected); expect(hsConnected.combined$.value).toBe(true); @@ -174,7 +175,7 @@ describe("createHomeserverConnected$", () => { }); it("composite sequence reflects each individual failure reason", () => { - const hsConnected = createHomeserverConnected$(scope, client, session); + const hsConnected = createHomeserverConnected$(scope, client, session, 0); // Initially false (sync error + disconnected + not probably left) expect(hsConnected.combined$.value).toBe(false); diff --git a/src/state/CallViewModel/localMember/HomeserverConnected.ts b/src/state/CallViewModel/localMember/HomeserverConnected.ts index c8bcd021..84da6780 100644 --- a/src/state/CallViewModel/localMember/HomeserverConnected.ts +++ b/src/state/CallViewModel/localMember/HomeserverConnected.ts @@ -12,9 +12,11 @@ import { type MatrixRTCSession, } from "matrix-js-sdk/lib/matrixrtc"; import { ClientEvent, type MatrixClient, SyncState } from "matrix-js-sdk"; -import { fromEvent, startWith, map, tap, type Observable } from "rxjs"; +import { fromEvent, startWith, map, tap, type Observable, debounceTime } from "rxjs"; import { logger as rootLogger } from "matrix-js-sdk/lib/logger"; +import { Config } from "../../../config/Config"; + import { type ObservableScope } from "../../ObservableScope"; import { type Behavior } from "../../Behavior"; import { and$ } from "../../../utils/observable"; @@ -35,21 +37,29 @@ export interface HomeserverConnected { * for the purposes of a MatrixRTC session. * * Becomes FALSE if ANY sub-condition is fulfilled: - * 1. Sync loop is not in SyncState.Syncing + * 1. Sync loop is not in SyncState.Syncing (after grace period) * 2. membershipStatus !== Status.Connected * 3. probablyLeft === true + * + * @param gracePeriodMs - Grace period in milliseconds to wait before reporting sync disconnect. + * If not provided, uses the config value (default 60000ms). */ export function createHomeserverConnected$( scope: ObservableScope, client: NodeStyleEventEmitter & Pick, matrixRTCSession: NodeStyleEventEmitter & Pick, + gracePeriodMs?: number, ): HomeserverConnected { + // Get grace period from parameter or config (default 60000ms) + const graceMs = gracePeriodMs ?? Config.get().sync_disconnect_grace_period_ms ?? 60000; + const syncing$ = ( fromEvent(client, ClientEvent.Sync) as Observable<[SyncState]> ).pipe( startWith([client.getSyncState()]), map(([state]) => state === SyncState.Syncing), + debounceTime(graceMs), ); const rtsSession$ = scope.behavior( From 9fccd49d7ddbf776b68bb72980ba0bb2b2026801 Mon Sep 17 00:00:00 2001 From: fkwp Date: Wed, 22 Apr 2026 21:08:31 +0200 Subject: [PATCH 04/44] Improve the handling of the grace period for the home server connection to support both immediate and delayed emissions. --- .../localMember/HomeserverConnected.ts | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/state/CallViewModel/localMember/HomeserverConnected.ts b/src/state/CallViewModel/localMember/HomeserverConnected.ts index 84da6780..cad987c8 100644 --- a/src/state/CallViewModel/localMember/HomeserverConnected.ts +++ b/src/state/CallViewModel/localMember/HomeserverConnected.ts @@ -12,11 +12,20 @@ import { type MatrixRTCSession, } from "matrix-js-sdk/lib/matrixrtc"; import { ClientEvent, type MatrixClient, SyncState } from "matrix-js-sdk"; -import { fromEvent, startWith, map, tap, type Observable, debounceTime } from "rxjs"; +import { + fromEvent, + startWith, + map, + tap, + type Observable, + distinctUntilChanged, + switchMap, + of, + delay, +} from "rxjs"; import { logger as rootLogger } from "matrix-js-sdk/lib/logger"; import { Config } from "../../../config/Config"; - import { type ObservableScope } from "../../ObservableScope"; import { type Behavior } from "../../Behavior"; import { and$ } from "../../../utils/observable"; @@ -41,6 +50,9 @@ export interface HomeserverConnected { * 2. membershipStatus !== Status.Connected * 3. probablyLeft === true * + * @param scope - The observable scope for lifecycle management. + * @param client - The Matrix client to monitor sync state. + * @param matrixRTCSession - The RTC session to monitor membership. * @param gracePeriodMs - Grace period in milliseconds to wait before reporting sync disconnect. * If not provided, uses the config value (default 60000ms). */ @@ -52,14 +64,24 @@ export function createHomeserverConnected$( gracePeriodMs?: number, ): HomeserverConnected { // Get grace period from parameter or config (default 60000ms) - const graceMs = gracePeriodMs ?? Config.get().sync_disconnect_grace_period_ms ?? 60000; + const graceMs = + gracePeriodMs ?? Config.get().sync_disconnect_grace_period_ms ?? 60000; const syncing$ = ( fromEvent(client, ClientEvent.Sync) as Observable<[SyncState]> ).pipe( startWith([client.getSyncState()]), map(([state]) => state === SyncState.Syncing), - debounceTime(graceMs), + distinctUntilChanged(), + switchMap((isSyncing) => +{ + if (isSyncing || graceMs <= 0) { + return of(isSyncing); // Sofortige Emission (Synchron) + } + return of(false).pipe(delay(graceMs)); // Verzögertes false + } ), + startWith(client.getSyncState() === SyncState.Syncing), + distinctUntilChanged(), ); const rtsSession$ = scope.behavior( From 032ec662e92b0cbc1f3ab89af5a47334af1119d3 Mon Sep 17 00:00:00 2001 From: fkwp Date: Wed, 22 Apr 2026 21:08:49 +0200 Subject: [PATCH 05/44] bump lk-jwt-service --- dev-backend-docker-compose.yml | 350 ++++++++++++++++----------------- 1 file changed, 175 insertions(+), 175 deletions(-) diff --git a/dev-backend-docker-compose.yml b/dev-backend-docker-compose.yml index 36fc7f44..0a42dad5 100644 --- a/dev-backend-docker-compose.yml +++ b/dev-backend-docker-compose.yml @@ -1,175 +1,175 @@ -networks: - ecbackend: - -services: - auth-service: - image: ghcr.io/element-hq/lk-jwt-service:0.4.4 - pull_policy: always - hostname: auth-server - environment: - - LIVEKIT_JWT_PORT=6080 - - LIVEKIT_URL=wss://matrix-rtc.m.localhost/livekit/sfu - - LIVEKIT_KEY=devkey - - LIVEKIT_SECRET=secret - # If the configured homeserver runs on localhost, it'll probably be using - # a self-signed certificate - - LIVEKIT_INSECURE_SKIP_VERIFY_TLS=YES_I_KNOW_WHAT_I_AM_DOING - - LIVEKIT_FULL_ACCESS_HOMESERVERS=* - deploy: - restart_policy: - condition: on-failure - ports: - # HOST_PORT:CONTAINER_PORT - - 6080:6080 - networks: - - ecbackend - - auth-service-1: - image: ghcr.io/element-hq/lk-jwt-service:0.4.4 - pull_policy: always - hostname: auth-server-1 - environment: - - LIVEKIT_JWT_PORT=16080 - - LIVEKIT_URL=wss://matrix-rtc.othersite.m.localhost/livekit/sfu - - LIVEKIT_KEY=devkey - - LIVEKIT_SECRET=secret - # If the configured homeserver runs on localhost, it'll probably be using - # a self-signed certificate - - LIVEKIT_INSECURE_SKIP_VERIFY_TLS=YES_I_KNOW_WHAT_I_AM_DOING - - LIVEKIT_FULL_ACCESS_HOMESERVERS=* - deploy: - restart_policy: - condition: on-failure - ports: - # HOST_PORT:CONTAINER_PORT - - 16080:16080 - networks: - - ecbackend - - livekit: - image: livekit/livekit-server:v1.10.1 - pull_policy: always - hostname: livekit-sfu - command: --dev --config /etc/livekit.yaml - restart: unless-stopped - # The SFU seems to work far more reliably when we let it share the host - # network rather than opening specific ports (but why?? we're not missing - # any…) - ports: - # HOST_PORT:CONTAINER_PORT - - 7880:7880/tcp - - 7881:7881/tcp - - 7882:7882/tcp - - 50100-50200:50100-50200/udp - volumes: - - ./backend/dev_livekit.yaml:/etc/livekit.yaml:Z - networks: - - ecbackend - - livekit-1: - image: livekit/livekit-server:v1.10.1 - pull_policy: always - hostname: livekit-sfu-1 - command: --dev --config /etc/livekit.yaml - restart: unless-stopped - # The SFU seems to work far more reliably when we let it share the host - # network rather than opening specific ports (but why?? we're not missing - # any…) - ports: - # HOST_PORT:CONTAINER_PORT - - 17880:17880/tcp - - 17881:17881/tcp - - 17882:17882/tcp - - 50300-50400:50300-50400/udp - volumes: - - ./backend/dev_livekit-othersite.yaml:/etc/livekit.yaml:Z - networks: - - ecbackend - - synapse: - hostname: homeserver - image: ghcr.io/element-hq/synapse:latest - pull_policy: always - environment: - - SYNAPSE_CONFIG_PATH=/data/cfg/homeserver.yaml - # Needed for rootless podman-compose such that the uid/gid mapping does - # fit local user uid. If the container runs as root (uid 0) it is fine as - # it actually maps to your non-root user on the host (e.g. 1000). - # Otherwise uid mapping will not match your non-root user. - - UID=0 - - GID=0 - volumes: - - ./backend/synapse_tmp:/data:Z - - ./backend/dev_homeserver.yaml:/data/cfg/homeserver.yaml:Z - networks: - - ecbackend - - synapse-1: - hostname: homeserver-1 - image: ghcr.io/element-hq/synapse:latest - pull_policy: always - environment: - - SYNAPSE_CONFIG_PATH=/data/cfg/homeserver.yaml - # Needed for rootless podman-compose such that the uid/gid mapping does - # fit local user uid. If the container runs as root (uid 0) it is fine as - # it actually maps to your non-root user on the host (e.g. 1000). - # Otherwise uid mapping will not match your non-root user. - - UID=0 - - GID=0 - volumes: - - ./backend/synapse_tmp_othersite:/data:Z - - ./backend/dev_homeserver-othersite.yaml:/data/cfg/homeserver.yaml:Z - networks: - - ecbackend - - element-web: - image: ghcr.io/element-hq/element-web:develop - pull_policy: always - volumes: - - ./backend/ew.test.config.json:/app/config.json:Z - environment: - ELEMENT_WEB_PORT: 8081 - ports: - - "8081:8081" - networks: - - ecbackend - - element-web-1: - image: ghcr.io/element-hq/element-web:develop - pull_policy: always - volumes: - - ./backend/ew.test.othersite.config.json:/app/config.json:Z - environment: - ELEMENT_WEB_PORT: 18081 - ports: - # HOST_PORT:CONTAINER_PORT - - "18081:18081" - networks: - - ecbackend - - nginx: - # see backend/dev_tls_setup for how to generate the tls certs - hostname: synapse.m.localhost - image: nginx:latest - pull_policy: always - volumes: - - ./backend/dev_nginx.conf:/etc/nginx/conf.d/default.conf:Z - - ./backend/dev_tls_m.localhost.key:/root/ssl/key.pem:Z - - ./backend/dev_tls_m.localhost.crt:/root/ssl/cert.pem:Z - ports: - # HOST_PORT:CONTAINER_PORT - - "443:443" - - "8008:80" - - "4443:443" - - "8448:8448" - extra_hosts: - - "host.docker.internal:host-gateway" - depends_on: - - synapse - networks: - ecbackend: - aliases: - - synapse.m.localhost - - synapse.othersite.m.localhost - - matrix-rtc.m.localhost - - matrix-rtc.othersite.m.localhost +networks: + ecbackend: + +services: + auth-service: + image: ghcr.io/element-hq/lk-jwt-service:pr_171 + pull_policy: always + hostname: auth-server + environment: + - LIVEKIT_JWT_PORT=6080 + - LIVEKIT_URL=wss://matrix-rtc.m.localhost/livekit/sfu + - LIVEKIT_KEY=devkey + - LIVEKIT_SECRET=secret + # If the configured homeserver runs on localhost, it'll probably be using + # a self-signed certificate + - LIVEKIT_INSECURE_SKIP_VERIFY_TLS=YES_I_KNOW_WHAT_I_AM_DOING + - LIVEKIT_FULL_ACCESS_HOMESERVERS=* + deploy: + restart_policy: + condition: on-failure + ports: + # HOST_PORT:CONTAINER_PORT + - 6080:6080 + networks: + - ecbackend + + auth-service-1: + image: ghcr.io/element-hq/lk-jwt-service:pr_171 + pull_policy: always + hostname: auth-server-1 + environment: + - LIVEKIT_JWT_PORT=16080 + - LIVEKIT_URL=wss://matrix-rtc.othersite.m.localhost/livekit/sfu + - LIVEKIT_KEY=devkey + - LIVEKIT_SECRET=secret + # If the configured homeserver runs on localhost, it'll probably be using + # a self-signed certificate + - LIVEKIT_INSECURE_SKIP_VERIFY_TLS=YES_I_KNOW_WHAT_I_AM_DOING + - LIVEKIT_FULL_ACCESS_HOMESERVERS=* + deploy: + restart_policy: + condition: on-failure + ports: + # HOST_PORT:CONTAINER_PORT + - 16080:16080 + networks: + - ecbackend + + livekit: + image: livekit/livekit-server:v1.10.1 + pull_policy: always + hostname: livekit-sfu + command: --dev --config /etc/livekit.yaml + restart: unless-stopped + # The SFU seems to work far more reliably when we let it share the host + # network rather than opening specific ports (but why?? we're not missing + # any…) + ports: + # HOST_PORT:CONTAINER_PORT + - 7880:7880/tcp + - 7881:7881/tcp + - 7882:7882/tcp + - 50100-50200:50100-50200/udp + volumes: + - ./backend/dev_livekit.yaml:/etc/livekit.yaml:Z + networks: + - ecbackend + + livekit-1: + image: livekit/livekit-server:v1.10.1 + pull_policy: always + hostname: livekit-sfu-1 + command: --dev --config /etc/livekit.yaml + restart: unless-stopped + # The SFU seems to work far more reliably when we let it share the host + # network rather than opening specific ports (but why?? we're not missing + # any…) + ports: + # HOST_PORT:CONTAINER_PORT + - 17880:17880/tcp + - 17881:17881/tcp + - 17882:17882/tcp + - 50300-50400:50300-50400/udp + volumes: + - ./backend/dev_livekit-othersite.yaml:/etc/livekit.yaml:Z + networks: + - ecbackend + + synapse: + hostname: homeserver + image: ghcr.io/element-hq/synapse:latest + pull_policy: always + environment: + - SYNAPSE_CONFIG_PATH=/data/cfg/homeserver.yaml + # Needed for rootless podman-compose such that the uid/gid mapping does + # fit local user uid. If the container runs as root (uid 0) it is fine as + # it actually maps to your non-root user on the host (e.g. 1000). + # Otherwise uid mapping will not match your non-root user. + - UID=0 + - GID=0 + volumes: + - ./backend/synapse_tmp:/data:Z + - ./backend/dev_homeserver.yaml:/data/cfg/homeserver.yaml:Z + networks: + - ecbackend + + synapse-1: + hostname: homeserver-1 + image: ghcr.io/element-hq/synapse:latest + pull_policy: always + environment: + - SYNAPSE_CONFIG_PATH=/data/cfg/homeserver.yaml + # Needed for rootless podman-compose such that the uid/gid mapping does + # fit local user uid. If the container runs as root (uid 0) it is fine as + # it actually maps to your non-root user on the host (e.g. 1000). + # Otherwise uid mapping will not match your non-root user. + - UID=0 + - GID=0 + volumes: + - ./backend/synapse_tmp_othersite:/data:Z + - ./backend/dev_homeserver-othersite.yaml:/data/cfg/homeserver.yaml:Z + networks: + - ecbackend + + element-web: + image: ghcr.io/element-hq/element-web:develop + pull_policy: always + volumes: + - ./backend/ew.test.config.json:/app/config.json:Z + environment: + ELEMENT_WEB_PORT: 8081 + ports: + - "8081:8081" + networks: + - ecbackend + + element-web-1: + image: ghcr.io/element-hq/element-web:develop + pull_policy: always + volumes: + - ./backend/ew.test.othersite.config.json:/app/config.json:Z + environment: + ELEMENT_WEB_PORT: 18081 + ports: + # HOST_PORT:CONTAINER_PORT + - "18081:18081" + networks: + - ecbackend + + nginx: + # see backend/dev_tls_setup for how to generate the tls certs + hostname: synapse.m.localhost + image: nginx:latest + pull_policy: always + volumes: + - ./backend/dev_nginx.conf:/etc/nginx/conf.d/default.conf:Z + - ./backend/dev_tls_m.localhost.key:/root/ssl/key.pem:Z + - ./backend/dev_tls_m.localhost.crt:/root/ssl/cert.pem:Z + ports: + # HOST_PORT:CONTAINER_PORT + - "443:443" + - "8008:80" + - "4443:443" + - "8448:8448" + extra_hosts: + - "host.docker.internal:host-gateway" + depends_on: + - synapse + networks: + ecbackend: + aliases: + - synapse.m.localhost + - synapse.othersite.m.localhost + - matrix-rtc.m.localhost + - matrix-rtc.othersite.m.localhost From 283c606b9d35ad6c04c6c22442e922cb556f6140 Mon Sep 17 00:00:00 2001 From: fkwp Date: Wed, 22 Apr 2026 21:46:25 +0200 Subject: [PATCH 06/44] Add tests for grace period handling in createHomeserverConnected$ --- .../localMember/HomeserverConnected.test.ts | 87 ++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/src/state/CallViewModel/localMember/HomeserverConnected.test.ts b/src/state/CallViewModel/localMember/HomeserverConnected.test.ts index e8861641..5b759cd1 100644 --- a/src/state/CallViewModel/localMember/HomeserverConnected.test.ts +++ b/src/state/CallViewModel/localMember/HomeserverConnected.test.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { EventEmitter } from "events"; import { ClientEvent, SyncState } from "matrix-js-sdk"; import { MembershipManagerEvent, Status } from "matrix-js-sdk/lib/matrixrtc"; @@ -201,3 +201,88 @@ describe("createHomeserverConnected$", () => { expect(hsConnected.combined$.value).toBe(false); }); }); + +describe("createHomeserverConnected$ - Grace Period", () => { + let scope: ObservableScope; + let client: MockMatrixClient; + let session: MockMatrixRTCSession; + const GRACE_PERIOD = 5000; + + beforeEach(() => { + vi.useFakeTimers(); + scope = new ObservableScope(); + // Initialize with values that satisfy the "Connected" condition + client = new MockMatrixClient(SyncState.Syncing); + session = new MockMatrixRTCSession({ + membershipStatus: Status.Connected, + probablyLeft: false, + }); + }); + + afterEach(() => { + scope.end(); + vi.useRealTimers(); + }); + + it("respects gracePeriodMs: stays true during grace period and flips false after", () => { + const hsConnected = createHomeserverConnected$(scope, client, session, GRACE_PERIOD); + + session.setMembershipStatus(Status.Connected); + session.setProbablyLeft(false); + + // Initial state: Everything is connected + expect(hsConnected.combined$.value).toBe(true); + + // 1. Sync loses connection -> should remain TRUE due to grace period + client.setSyncState(SyncState.Error); + expect(hsConnected.combined$.value).toBe(true); + + // 2. Fast forward time (just before expiration) + vi.advanceTimersByTime(GRACE_PERIOD - 1); + expect(hsConnected.combined$.value).toBe(true); + + // 3. Fast forward time (expiration) + vi.advanceTimersByTime(1); + expect(hsConnected.combined$.value).toBe(false); + }); + + it("recovers immediately if sync returns during grace period", () => { + const hsConnected = createHomeserverConnected$(scope, client, session, GRACE_PERIOD); + + session.setMembershipStatus(Status.Connected); + session.setProbablyLeft(false); + + // Initial state: Connected + expect(hsConnected.combined$.value).toBe(true); + + // 1. Sync error occurs + client.setSyncState(SyncState.Error); + vi.advanceTimersByTime(GRACE_PERIOD / 2); + expect(hsConnected.combined$.value).toBe(true); + + // 2. Sync recovers BEFORE the grace period expires + client.setSyncState(SyncState.Syncing); + expect(hsConnected.combined$.value).toBe(true); + + // 3. Fast forward the remaining time -> should stay TRUE + vi.advanceTimersByTime(GRACE_PERIOD); + expect(hsConnected.combined$.value).toBe(true); + }); + + it("flips to true IMMEDIATELY even if a grace period was pending", () => { + const hsConnected = createHomeserverConnected$(scope, client, session, GRACE_PERIOD); + + session.setMembershipStatus(Status.Connected); + session.setProbablyLeft(false); + + // 1. Initial error: wait until it flips to false + client.setSyncState(SyncState.Error); + expect(hsConnected.combined$.value).toBe(true); + vi.advanceTimersByTime(GRACE_PERIOD + 1); + expect(hsConnected.combined$.value).toBe(false); + + // 2. Back to Syncing -> Must be TRUE immediately (synchronously) + client.setSyncState(SyncState.Syncing); + expect(hsConnected.combined$.value).toBe(true); + }); +}); \ No newline at end of file From c1f821ca0f1fe002deea4f7efc23e0e09da739fb Mon Sep 17 00:00:00 2001 From: fkwp Date: Thu, 23 Apr 2026 15:33:00 +0200 Subject: [PATCH 07/44] Update default sync disconnect grace period to 10000ms in configuration and related functions --- src/config/ConfigOptions.ts | 4 ++-- src/state/CallViewModel/CallViewModel.ts | 2 -- src/state/CallViewModel/localMember/HomeserverConnected.ts | 6 +++--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/config/ConfigOptions.ts b/src/config/ConfigOptions.ts index d5839fb3..1781dc97 100644 --- a/src/config/ConfigOptions.ts +++ b/src/config/ConfigOptions.ts @@ -100,7 +100,7 @@ export interface ConfigOptions { /** * Grace period in milliseconds to wait before reporting the sync loop as disconnected. * This allows brief sync interruptions without triggering a reconnection message. - * Default is 60000ms (60 seconds). Set to 0 to disable the grace period. + * Default is 10000ms (10 seconds). Set to 0 to disable the grace period. */ sync_disconnect_grace_period_ms?: number; @@ -175,6 +175,6 @@ export const DEFAULT_CONFIG: ResolvedConfigOptions = { features: { feature_use_device_session_member_events: true, }, - sync_disconnect_grace_period_ms: 60000, + sync_disconnect_grace_period_ms: 10000, ssla: "https://static.element.io/legal/element-software-and-services-license-agreement-uk-1.pdf", }; diff --git a/src/state/CallViewModel/CallViewModel.ts b/src/state/CallViewModel/CallViewModel.ts index 8677f2d0..e298bcfd 100644 --- a/src/state/CallViewModel/CallViewModel.ts +++ b/src/state/CallViewModel/CallViewModel.ts @@ -83,7 +83,6 @@ import { E2eeType } from "../../e2ee/e2eeType"; import { MatrixKeyProvider } from "../../e2ee/matrixKeyProvider"; import { type MuteStates } from "../MuteStates"; import { getUrlParams, HeaderStyle } from "../../UrlParams"; -import { Config } from "../../config/Config"; import { type ProcessorState } from "../../livekit/TrackProcessorContext"; import { ElementWidgetActions, widget } from "../../widget"; import { @@ -537,7 +536,6 @@ export function createCallViewModel$( scope, client, matrixRTCSession, - Config.get().sync_disconnect_grace_period_ms, ), muteStates, joinMatrixRTC: (transport: LivekitTransportConfig) => { diff --git a/src/state/CallViewModel/localMember/HomeserverConnected.ts b/src/state/CallViewModel/localMember/HomeserverConnected.ts index cad987c8..d66d8657 100644 --- a/src/state/CallViewModel/localMember/HomeserverConnected.ts +++ b/src/state/CallViewModel/localMember/HomeserverConnected.ts @@ -54,7 +54,7 @@ export interface HomeserverConnected { * @param client - The Matrix client to monitor sync state. * @param matrixRTCSession - The RTC session to monitor membership. * @param gracePeriodMs - Grace period in milliseconds to wait before reporting sync disconnect. - * If not provided, uses the config value (default 60000ms). + * If not provided, uses the config value (default 10000ms). */ export function createHomeserverConnected$( scope: ObservableScope, @@ -63,9 +63,9 @@ export function createHomeserverConnected$( Pick, gracePeriodMs?: number, ): HomeserverConnected { - // Get grace period from parameter or config (default 60000ms) + // Get grace period from parameter or config (default 10000ms) const graceMs = - gracePeriodMs ?? Config.get().sync_disconnect_grace_period_ms ?? 60000; + gracePeriodMs ?? Config.get().sync_disconnect_grace_period_ms ?? 10000; const syncing$ = ( fromEvent(client, ClientEvent.Sync) as Observable<[SyncState]> From 0abc32b80ee5055fc4106f2baf9d48025116ede1 Mon Sep 17 00:00:00 2001 From: fkwp Date: Thu, 23 Apr 2026 15:47:40 +0200 Subject: [PATCH 08/44] prettier --- .../localMember/HomeserverConnected.test.ts | 23 +++++++++++++++---- .../localMember/HomeserverConnected.ts | 13 +++++------ 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/state/CallViewModel/localMember/HomeserverConnected.test.ts b/src/state/CallViewModel/localMember/HomeserverConnected.test.ts index 5b759cd1..96feeeb1 100644 --- a/src/state/CallViewModel/localMember/HomeserverConnected.test.ts +++ b/src/state/CallViewModel/localMember/HomeserverConnected.test.ts @@ -225,7 +225,12 @@ describe("createHomeserverConnected$ - Grace Period", () => { }); it("respects gracePeriodMs: stays true during grace period and flips false after", () => { - const hsConnected = createHomeserverConnected$(scope, client, session, GRACE_PERIOD); + const hsConnected = createHomeserverConnected$( + scope, + client, + session, + GRACE_PERIOD, + ); session.setMembershipStatus(Status.Connected); session.setProbablyLeft(false); @@ -247,7 +252,12 @@ describe("createHomeserverConnected$ - Grace Period", () => { }); it("recovers immediately if sync returns during grace period", () => { - const hsConnected = createHomeserverConnected$(scope, client, session, GRACE_PERIOD); + const hsConnected = createHomeserverConnected$( + scope, + client, + session, + GRACE_PERIOD, + ); session.setMembershipStatus(Status.Connected); session.setProbablyLeft(false); @@ -270,7 +280,12 @@ describe("createHomeserverConnected$ - Grace Period", () => { }); it("flips to true IMMEDIATELY even if a grace period was pending", () => { - const hsConnected = createHomeserverConnected$(scope, client, session, GRACE_PERIOD); + const hsConnected = createHomeserverConnected$( + scope, + client, + session, + GRACE_PERIOD, + ); session.setMembershipStatus(Status.Connected); session.setProbablyLeft(false); @@ -285,4 +300,4 @@ describe("createHomeserverConnected$ - Grace Period", () => { client.setSyncState(SyncState.Syncing); expect(hsConnected.combined$.value).toBe(true); }); -}); \ No newline at end of file +}); diff --git a/src/state/CallViewModel/localMember/HomeserverConnected.ts b/src/state/CallViewModel/localMember/HomeserverConnected.ts index d66d8657..ff25a250 100644 --- a/src/state/CallViewModel/localMember/HomeserverConnected.ts +++ b/src/state/CallViewModel/localMember/HomeserverConnected.ts @@ -73,13 +73,12 @@ export function createHomeserverConnected$( startWith([client.getSyncState()]), map(([state]) => state === SyncState.Syncing), distinctUntilChanged(), - switchMap((isSyncing) => -{ - if (isSyncing || graceMs <= 0) { - return of(isSyncing); // Sofortige Emission (Synchron) - } - return of(false).pipe(delay(graceMs)); // Verzögertes false - } ), + switchMap((isSyncing) => { + if (isSyncing || graceMs <= 0) { + return of(isSyncing); + } + return of(false).pipe(delay(graceMs)); + }), startWith(client.getSyncState() === SyncState.Syncing), distinctUntilChanged(), ); From 44b6db6ff1156d4cb1a93c228fb87416380bb519 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Fri, 24 Apr 2026 11:47:24 +0100 Subject: [PATCH 09/44] Replace LayoutToggle with compound-web Switch component --- package.json | 2 +- pnpm-lock.yaml | 11 +++-- src/components/CallFooter.tsx | 19 ++++++-- src/room/LayoutToggle.module.css | 79 ------------------------------- src/room/LayoutToggle.stories.tsx | 25 ---------- src/room/LayoutToggle.tsx | 59 ----------------------- 6 files changed, 22 insertions(+), 173 deletions(-) delete mode 100644 src/room/LayoutToggle.module.css delete mode 100644 src/room/LayoutToggle.stories.tsx delete mode 100644 src/room/LayoutToggle.tsx diff --git a/package.json b/package.json index c0f4d505..5fa4fed2 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "@typescript-eslint/parser": "^8.31.0", "@use-gesture/react": "^10.2.11", "@vector-im/compound-design-tokens": "^10.0.0", - "@vector-im/compound-web": "^9.0.0", + "@vector-im/compound-web": "element-hq/compound-web#e7c91ef18e20f2fc70069696f4414018361ac512", "@vitejs/plugin-react": "^4.0.1", "@vitest/coverage-v8": "^4.0.18", "babel-plugin-transform-vite-meta-env": "^1.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c800b0c2..07bc5fbc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -150,8 +150,8 @@ importers: specifier: ^10.0.0 version: 10.1.0(@types/react@19.2.14)(react@19.2.5) '@vector-im/compound-web': - specifier: ^9.0.0 - version: 9.2.0(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + specifier: element-hq/compound-web#e7c91ef18e20f2fc70069696f4414018361ac512 + version: https://codeload.github.com/element-hq/compound-web/tar.gz/e7c91ef18e20f2fc70069696f4414018361ac512(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@vitejs/plugin-react': specifier: ^4.0.1 version: 4.7.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) @@ -3037,8 +3037,9 @@ packages: react: optional: true - '@vector-im/compound-web@9.2.0': - resolution: {integrity: sha512-jHbABGEQ2yqNtm5xRIkklQs198VEfSk9AJQolI+e4WSJ0xg8Ozyv9t9KIuKQAmjdSV9aow5G6hDE861XB6DQgw==} + '@vector-im/compound-web@https://codeload.github.com/element-hq/compound-web/tar.gz/e7c91ef18e20f2fc70069696f4414018361ac512': + resolution: {tarball: https://codeload.github.com/element-hq/compound-web/tar.gz/e7c91ef18e20f2fc70069696f4414018361ac512} + version: 9.2.1 peerDependencies: '@fontsource/inconsolata': ^5 '@fontsource/inter': ^5 @@ -9473,7 +9474,7 @@ snapshots: '@types/react': 19.2.14 react: 19.2.5 - '@vector-im/compound-web@9.2.0(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@vector-im/compound-web@https://codeload.github.com/element-hq/compound-web/tar.gz/e7c91ef18e20f2fc70069696f4414018361ac512(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@floating-ui/react': 0.27.19(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@fontsource/inconsolata': 5.2.8 diff --git a/src/components/CallFooter.tsx b/src/components/CallFooter.tsx index 4e728d3b..21ff52c6 100644 --- a/src/components/CallFooter.tsx +++ b/src/components/CallFooter.tsx @@ -8,6 +8,12 @@ Please see LICENSE in the repository root for full details. import { type FC, type JSX, type Ref, useMemo } from "react"; import classNames from "classnames"; import { BehaviorSubject } from "rxjs"; +import { Switch } from "@vector-im/compound-web"; +import { t } from "i18next"; +import { + SpotlightIcon, + GridIcon, +} from "@vector-im/compound-design-tokens/assets/web/icons"; import LogoMark from "../icons/LogoMark.svg?react"; import LogoType from "../icons/LogoType.svg?react"; @@ -23,7 +29,6 @@ import { type ReactionData, } from "../button"; import styles from "./CallFooter.module.css"; -import { LayoutToggle } from "../room/LayoutToggle"; import { type GridMode } from "../state/CallViewModel/CallViewModel"; export interface AudioOutputSwitcher { @@ -232,10 +237,16 @@ export const CallFooter: FC = ({ {!hideControls &&
{buttons}
} {setLayoutMode && layoutMode && showLayoutSwitcher && ( - )} diff --git a/src/room/LayoutToggle.module.css b/src/room/LayoutToggle.module.css deleted file mode 100644 index d9ae5813..00000000 --- a/src/room/LayoutToggle.module.css +++ /dev/null @@ -1,79 +0,0 @@ -/* -Copyright 2023, 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. -*/ - -.toggle { - padding: 2px; - border: 1px solid var(--cpd-color-border-interactive-secondary); - border-radius: var(--cpd-radius-pill-effect); - background: var(--cpd-color-bg-canvas-default); - display: flex; - position: relative; -} - -.toggle input { - appearance: none; - /* Safari puts a margin on these, which is not removed via appearance: none */ - margin: 0; - block-size: var(--cpd-space-11x); - inline-size: var(--cpd-space-11x); - cursor: pointer; - border-radius: var(--cpd-radius-pill-effect); - background: var(--cpd-color-bg-action-secondary-rest); - box-shadow: var(--small-drop-shadow); - transition: background-color 0.1s; -} - -.toggle svg { - display: block; - position: absolute; - padding: calc(2.5 * var(--cpd-space-1x)); - pointer-events: none; - color: var(--cpd-color-icon-primary); - transition: color 0.1s; -} - -.toggle svg:nth-child(2) { - inset-inline-start: 2px; -} - -.toggle svg:nth-child(4) { - inset-inline-end: 2px; -} - -@media (hover: hover) { - .toggle input:hover { - background: var(--cpd-color-bg-action-secondary-hovered); - box-shadow: none; - } -} - -.toggle input:active { - background: var(--cpd-color-bg-action-secondary-pressed); - box-shadow: none; -} - -.toggle input:checked { - background: var(--cpd-color-bg-action-primary-rest); -} - -.toggle input:checked + svg { - color: var(--cpd-color-icon-on-solid-primary); -} - -@media (hover: hover) { - .toggle input:checked:hover { - background: var(--cpd-color-bg-action-primary-hovered); - } -} - -.toggle input:checked:active { - background: var(--cpd-color-bg-action-primary-pressed); -} - -.toggle input:first-child { - margin-inline-end: 5px; -} diff --git a/src/room/LayoutToggle.stories.tsx b/src/room/LayoutToggle.stories.tsx deleted file mode 100644 index 72a2ffad..00000000 --- a/src/room/LayoutToggle.stories.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/* -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 { fn } from "storybook/test"; - -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { LayoutToggle } from "./LayoutToggle"; - -const meta = { - component: LayoutToggle, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - args: { - layout: "grid", - setLayout: fn(), - }, -}; diff --git a/src/room/LayoutToggle.tsx b/src/room/LayoutToggle.tsx deleted file mode 100644 index 98ed91d3..00000000 --- a/src/room/LayoutToggle.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* -Copyright 2023, 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 { type ChangeEvent, type FC, useCallback } from "react"; -import { useTranslation } from "react-i18next"; -import { Tooltip } from "@vector-im/compound-web"; -import { - SpotlightIcon, - GridIcon, -} from "@vector-im/compound-design-tokens/assets/web/icons"; -import classNames from "classnames"; - -import styles from "./LayoutToggle.module.css"; - -export type Layout = "spotlight" | "grid"; - -type Props = { - layout: Layout; - setLayout: (layout: Layout) => void; - className?: string; -}; - -export const LayoutToggle: FC = ({ layout, setLayout, className }) => { - const { t } = useTranslation(); - - const onChange = useCallback( - (e: ChangeEvent) => setLayout(e.target.value as Layout), - [setLayout], - ); - - return ( -
- - - - - - - - - - ); -}; From d8be06974767e36598f71003121b80b5c7673a00 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Fri, 24 Apr 2026 11:54:24 +0100 Subject: [PATCH 10/44] Fix type --- locales/en/app.json | 1 + package.json | 2 +- src/components/CallFooter.tsx | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/locales/en/app.json b/locales/en/app.json index 5398930f..2545bb4b 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -143,6 +143,7 @@ "text": "This call already exists, would you like to join?", "title": "Join existing call?" }, + "layout_switch_label": "Layout", "layout_grid_label": "Grid", "layout_spotlight_label": "Spotlight", "lobby": { diff --git a/package.json b/package.json index 5fa4fed2..f27cf8f4 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "@typescript-eslint/parser": "^8.31.0", "@use-gesture/react": "^10.2.11", "@vector-im/compound-design-tokens": "^10.0.0", - "@vector-im/compound-web": "element-hq/compound-web#e7c91ef18e20f2fc70069696f4414018361ac512", + "@vector-im/compound-web": "element-hq/compound-web#fc2e677326aaefec61ef74fb1d9de3c01eecfa7e", "@vitejs/plugin-react": "^4.0.1", "@vitest/coverage-v8": "^4.0.18", "babel-plugin-transform-vite-meta-env": "^1.0.3", diff --git a/src/components/CallFooter.tsx b/src/components/CallFooter.tsx index 21ff52c6..d10e4ecf 100644 --- a/src/components/CallFooter.tsx +++ b/src/components/CallFooter.tsx @@ -237,7 +237,8 @@ export const CallFooter: FC = ({ {!hideControls &&
{buttons}
} {setLayoutMode && layoutMode && showLayoutSwitcher && ( - + aria-label={t("layout_switch_label")} leftLabel={t("layout_spotlight_label")} leftValue="spotlight" leftIcon={SpotlightIcon} From 9b71070ef8e312bd17d1b0c71eda775c36cf4e30 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Fri, 24 Apr 2026 11:57:13 +0100 Subject: [PATCH 11/44] publish lock changes --- pnpm-lock.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 07bc5fbc..06269963 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -150,8 +150,8 @@ importers: specifier: ^10.0.0 version: 10.1.0(@types/react@19.2.14)(react@19.2.5) '@vector-im/compound-web': - specifier: element-hq/compound-web#e7c91ef18e20f2fc70069696f4414018361ac512 - version: https://codeload.github.com/element-hq/compound-web/tar.gz/e7c91ef18e20f2fc70069696f4414018361ac512(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + specifier: element-hq/compound-web#fc2e677326aaefec61ef74fb1d9de3c01eecfa7e + version: https://codeload.github.com/element-hq/compound-web/tar.gz/fc2e677326aaefec61ef74fb1d9de3c01eecfa7e(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@vitejs/plugin-react': specifier: ^4.0.1 version: 4.7.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) @@ -3037,8 +3037,8 @@ packages: react: optional: true - '@vector-im/compound-web@https://codeload.github.com/element-hq/compound-web/tar.gz/e7c91ef18e20f2fc70069696f4414018361ac512': - resolution: {tarball: https://codeload.github.com/element-hq/compound-web/tar.gz/e7c91ef18e20f2fc70069696f4414018361ac512} + '@vector-im/compound-web@https://codeload.github.com/element-hq/compound-web/tar.gz/fc2e677326aaefec61ef74fb1d9de3c01eecfa7e': + resolution: {tarball: https://codeload.github.com/element-hq/compound-web/tar.gz/fc2e677326aaefec61ef74fb1d9de3c01eecfa7e} version: 9.2.1 peerDependencies: '@fontsource/inconsolata': ^5 @@ -9474,7 +9474,7 @@ snapshots: '@types/react': 19.2.14 react: 19.2.5 - '@vector-im/compound-web@https://codeload.github.com/element-hq/compound-web/tar.gz/e7c91ef18e20f2fc70069696f4414018361ac512(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@vector-im/compound-web@https://codeload.github.com/element-hq/compound-web/tar.gz/fc2e677326aaefec61ef74fb1d9de3c01eecfa7e(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@floating-ui/react': 0.27.19(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@fontsource/inconsolata': 5.2.8 From 62751787ca0c4f95572d3feadd880297d3933e5e Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Fri, 24 Apr 2026 13:02:47 +0100 Subject: [PATCH 12/44] Use actual package --- package.json | 2 +- pnpm-lock.yaml | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index f27cf8f4..a8dc49fa 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "@typescript-eslint/parser": "^8.31.0", "@use-gesture/react": "^10.2.11", "@vector-im/compound-design-tokens": "^10.0.0", - "@vector-im/compound-web": "element-hq/compound-web#fc2e677326aaefec61ef74fb1d9de3c01eecfa7e", + "@vector-im/compound-web": "^9.3.0", "@vitejs/plugin-react": "^4.0.1", "@vitest/coverage-v8": "^4.0.18", "babel-plugin-transform-vite-meta-env": "^1.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 06269963..41e3f943 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -150,8 +150,8 @@ importers: specifier: ^10.0.0 version: 10.1.0(@types/react@19.2.14)(react@19.2.5) '@vector-im/compound-web': - specifier: element-hq/compound-web#fc2e677326aaefec61ef74fb1d9de3c01eecfa7e - version: https://codeload.github.com/element-hq/compound-web/tar.gz/fc2e677326aaefec61ef74fb1d9de3c01eecfa7e(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + specifier: ^9.3.0 + version: 9.3.0(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@vitejs/plugin-react': specifier: ^4.0.1 version: 4.7.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) @@ -3037,9 +3037,8 @@ packages: react: optional: true - '@vector-im/compound-web@https://codeload.github.com/element-hq/compound-web/tar.gz/fc2e677326aaefec61ef74fb1d9de3c01eecfa7e': - resolution: {tarball: https://codeload.github.com/element-hq/compound-web/tar.gz/fc2e677326aaefec61ef74fb1d9de3c01eecfa7e} - version: 9.2.1 + '@vector-im/compound-web@9.3.0': + resolution: {integrity: sha512-Elu4Uw8RbfP6JaudQYkVibALYT6qpwubqfKhteTxIPWBWzSYM+P5T+B1uX+ra+grNcXwXUt2xfMxpqYQsAHgYA==} peerDependencies: '@fontsource/inconsolata': ^5 '@fontsource/inter': ^5 @@ -9474,7 +9473,7 @@ snapshots: '@types/react': 19.2.14 react: 19.2.5 - '@vector-im/compound-web@https://codeload.github.com/element-hq/compound-web/tar.gz/fc2e677326aaefec61ef74fb1d9de3c01eecfa7e(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@vector-im/compound-web@9.3.0(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@floating-ui/react': 0.27.19(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@fontsource/inconsolata': 5.2.8 From 5aa45714bfa194a3224e0181a2e8970a0b693ff1 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 27 Apr 2026 09:34:46 +0100 Subject: [PATCH 13/44] Snap updates --- locales/en/app.json | 2 +- .../ReactionToggleButton.test.tsx.snap | 10 +++---- .../GroupCallErrorBoundary.test.tsx.snap | 18 ++++++------ .../__snapshots__/InCallView.test.tsx.snap | 28 +++++++++---------- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/locales/en/app.json b/locales/en/app.json index 2545bb4b..b51c6ed9 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -143,9 +143,9 @@ "text": "This call already exists, would you like to join?", "title": "Join existing call?" }, - "layout_switch_label": "Layout", "layout_grid_label": "Grid", "layout_spotlight_label": "Spotlight", + "layout_switch_label": "Layout", "lobby": { "ask_to_join": "Request to join call", "join_as_guest": "Join as guest", diff --git a/src/button/__snapshots__/ReactionToggleButton.test.tsx.snap b/src/button/__snapshots__/ReactionToggleButton.test.tsx.snap index 608c1a0f..a1e319d9 100644 --- a/src/button/__snapshots__/ReactionToggleButton.test.tsx.snap +++ b/src/button/__snapshots__/ReactionToggleButton.test.tsx.snap @@ -10,7 +10,7 @@ exports[`Can close reaction dialog 1`] = ` aria-expanded="true" aria-haspopup="true" aria-labelledby="_r_bb_" - class="_button_13vu4_8 _has-icon_13vu4_60 _icon-only_13vu4_53" + class="_button_1nw83_8 _has-icon_1nw83_60 _icon-only_1nw83_53" data-kind="primary" data-size="lg" role="button" @@ -44,7 +44,7 @@ exports[`Can fully expand emoji picker 1`] = ` aria-expanded="true" aria-haspopup="true" aria-labelledby="_r_7m_" - class="_button_13vu4_8 _has-icon_13vu4_60 _icon-only_13vu4_53" + class="_button_1nw83_8 _has-icon_1nw83_60 _icon-only_1nw83_53" data-kind="primary" data-size="lg" role="button" @@ -75,7 +75,7 @@ exports[`Can lower hand 1`] = ` aria-expanded="false" aria-haspopup="true" aria-labelledby="_r_36_" - class="_button_13vu4_8 _has-icon_13vu4_60 _icon-only_13vu4_53" + class="_button_1nw83_8 _has-icon_1nw83_60 _icon-only_1nw83_53" data-kind="secondary" data-size="lg" role="button" @@ -109,7 +109,7 @@ exports[`Can open menu 1`] = ` aria-expanded="true" aria-haspopup="true" aria-labelledby="_r_0_" - class="_button_13vu4_8 _has-icon_13vu4_60 _icon-only_13vu4_53" + class="_button_1nw83_8 _has-icon_1nw83_60 _icon-only_1nw83_53" data-kind="primary" data-size="lg" role="button" @@ -140,7 +140,7 @@ exports[`Can raise hand 1`] = ` aria-expanded="false" aria-haspopup="true" aria-labelledby="_r_1j_" - class="_button_13vu4_8 raisedButton _has-icon_13vu4_60 _icon-only_13vu4_53" + class="_button_1nw83_8 raisedButton _has-icon_1nw83_60 _icon-only_1nw83_53" data-kind="primary" data-size="lg" role="button" diff --git a/src/room/__snapshots__/GroupCallErrorBoundary.test.tsx.snap b/src/room/__snapshots__/GroupCallErrorBoundary.test.tsx.snap index 0d2d39bc..92a6fe54 100644 --- a/src/room/__snapshots__/GroupCallErrorBoundary.test.tsx.snap +++ b/src/room/__snapshots__/GroupCallErrorBoundary.test.tsx.snap @@ -134,7 +134,7 @@ exports[`ConnectionLostError: Action handling should reset error state 1`] = ` You were disconnected from the call.

-
); diff --git a/src/button/ReactionToggleButton.tsx b/src/button/ReactionToggleButton.tsx index 39804a5f..c71642e9 100644 --- a/src/button/ReactionToggleButton.tsx +++ b/src/button/ReactionToggleButton.tsx @@ -173,7 +173,7 @@ export interface ReactionData { interface ReactionToggleButtonProps extends ComponentPropsWithoutRef<"button"> { reactionData: ReactionData; identifier: string; - size?: "sm" | "lg"; + size?: "md" | "lg"; /** List of participants raising their hand */ } diff --git a/src/button/__snapshots__/ReactionToggleButton.test.tsx.snap b/src/button/__snapshots__/ReactionToggleButton.test.tsx.snap index 608c1a0f..a1e319d9 100644 --- a/src/button/__snapshots__/ReactionToggleButton.test.tsx.snap +++ b/src/button/__snapshots__/ReactionToggleButton.test.tsx.snap @@ -10,7 +10,7 @@ exports[`Can close reaction dialog 1`] = ` aria-expanded="true" aria-haspopup="true" aria-labelledby="_r_bb_" - class="_button_13vu4_8 _has-icon_13vu4_60 _icon-only_13vu4_53" + class="_button_1nw83_8 _has-icon_1nw83_60 _icon-only_1nw83_53" data-kind="primary" data-size="lg" role="button" @@ -44,7 +44,7 @@ exports[`Can fully expand emoji picker 1`] = ` aria-expanded="true" aria-haspopup="true" aria-labelledby="_r_7m_" - class="_button_13vu4_8 _has-icon_13vu4_60 _icon-only_13vu4_53" + class="_button_1nw83_8 _has-icon_1nw83_60 _icon-only_1nw83_53" data-kind="primary" data-size="lg" role="button" @@ -75,7 +75,7 @@ exports[`Can lower hand 1`] = ` aria-expanded="false" aria-haspopup="true" aria-labelledby="_r_36_" - class="_button_13vu4_8 _has-icon_13vu4_60 _icon-only_13vu4_53" + class="_button_1nw83_8 _has-icon_1nw83_60 _icon-only_1nw83_53" data-kind="secondary" data-size="lg" role="button" @@ -109,7 +109,7 @@ exports[`Can open menu 1`] = ` aria-expanded="true" aria-haspopup="true" aria-labelledby="_r_0_" - class="_button_13vu4_8 _has-icon_13vu4_60 _icon-only_13vu4_53" + class="_button_1nw83_8 _has-icon_1nw83_60 _icon-only_1nw83_53" data-kind="primary" data-size="lg" role="button" @@ -140,7 +140,7 @@ exports[`Can raise hand 1`] = ` aria-expanded="false" aria-haspopup="true" aria-labelledby="_r_1j_" - class="_button_13vu4_8 raisedButton _has-icon_13vu4_60 _icon-only_13vu4_53" + class="_button_1nw83_8 raisedButton _has-icon_1nw83_60 _icon-only_1nw83_53" data-kind="primary" data-size="lg" role="button" diff --git a/src/components/CallFooter.tsx b/src/components/CallFooter.tsx index 4e728d3b..fdedf36c 100644 --- a/src/components/CallFooter.tsx +++ b/src/components/CallFooter.tsx @@ -101,7 +101,7 @@ export const CallFooter: FC = ({ tileStoreGeneration, }) => { const buttons: JSX.Element[] = []; - const buttonSize = asPip ? "sm" : "lg"; + const buttonSize = asPip ? "md" : "lg"; const showSettingsButton = openSettings !== undefined && !asPip && !hideControls; const showLayoutSwitcher = !asPip && !hideControls; diff --git a/src/input/AvatarInputField.tsx b/src/input/AvatarInputField.tsx index 4a3173b4..f9b14707 100644 --- a/src/input/AvatarInputField.tsx +++ b/src/input/AvatarInputField.tsx @@ -113,7 +113,7 @@ export const AvatarInputField: FC = ({ iconOnly Icon={EditIcon} kind="tertiary" - size="sm" + size="md" aria-label={t("action.edit")} /> } @@ -136,7 +136,7 @@ export const AvatarInputField: FC = ({ iconOnly Icon={EditIcon} kind="tertiary" - size="sm" + size="md" aria-label={t("action.edit")} onClick={onSelectUpload} /> diff --git a/src/room/EarpieceOverlay.tsx b/src/room/EarpieceOverlay.tsx index 6835bdd7..574792f0 100644 --- a/src/room/EarpieceOverlay.tsx +++ b/src/room/EarpieceOverlay.tsx @@ -30,7 +30,7 @@ export const EarpieceOverlay: FC = ({ show, onBackToVideoPressed }) => { {t("handset.overlay_description")} + <> +
+ +
+ {/*// modal lives outside of the root*/} {modalOpen && ( = ({ )} -
+ ); }; @@ -164,12 +165,13 @@ test("unmuting happens in place of the default action", async () => { // container element that can be interactive and receive focus / keydown // events.