From 1c5b483a7ee2d87a5b1834722586759218ab57c2 Mon Sep 17 00:00:00 2001 From: Timo K Date: Tue, 7 Apr 2026 14:11:55 +0200 Subject: [PATCH 01/52] Only send callEndedEvent if the user has joined the call. --- src/analytics/PosthogEvents.ts | 50 +++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/src/analytics/PosthogEvents.ts b/src/analytics/PosthogEvents.ts index f0f059f5..5553829a 100644 --- a/src/analytics/PosthogEvents.ts +++ b/src/analytics/PosthogEvents.ts @@ -27,8 +27,8 @@ interface CallEnded extends IPosthogEvent { } export class CallEndedTracker { - private cache: { startTime: Date; maxParticipantsCount: number } = { - startTime: new Date(0), + private cache: { startTime?: Date; maxParticipantsCount: number } = { + startTime: undefined, maxParticipantsCount: 0, }; @@ -49,26 +49,32 @@ export class CallEndedTracker { sendInstantly: boolean, rtcSession: MatrixRTCSession, ): void { - PosthogAnalytics.instance.trackEvent( - { - eventName: "CallEnded", - callId: callId, - callParticipantsMax: this.cache.maxParticipantsCount, - callParticipantsOnLeave: callParticipantsNow, - callDuration: (Date.now() - this.cache.startTime.getTime()) / 1000, - roomEventEncryptionKeysSent: - rtcSession.statistics.counters.roomEventEncryptionKeysSent, - roomEventEncryptionKeysReceived: - rtcSession.statistics.counters.roomEventEncryptionKeysReceived, - roomEventEncryptionKeysReceivedAverageAge: - rtcSession.statistics.counters.roomEventEncryptionKeysReceived > 0 - ? rtcSession.statistics.totals - .roomEventEncryptionKeysReceivedTotalAge / - rtcSession.statistics.counters.roomEventEncryptionKeysReceived - : 0, - }, - { send_instantly: sendInstantly }, - ); + if (this.cache.startTime) { + PosthogAnalytics.instance.trackEvent( + { + eventName: "CallEnded", + callId: callId, + callParticipantsMax: this.cache.maxParticipantsCount, + callParticipantsOnLeave: callParticipantsNow, + callDuration: (Date.now() - this.cache.startTime.getTime()) / 1000, + roomEventEncryptionKeysSent: + rtcSession.statistics.counters.roomEventEncryptionKeysSent, + roomEventEncryptionKeysReceived: + rtcSession.statistics.counters.roomEventEncryptionKeysReceived, + roomEventEncryptionKeysReceivedAverageAge: + rtcSession.statistics.counters.roomEventEncryptionKeysReceived > 0 + ? rtcSession.statistics.totals + .roomEventEncryptionKeysReceivedTotalAge / + rtcSession.statistics.counters.roomEventEncryptionKeysReceived + : 0, + }, + { send_instantly: sendInstantly }, + ); + } else { + logger.warn( + "[PosthogEvents] Failed to send posthog callEnded event due to missing startTime", + ); + } } } From 270726d4464dc9cee547d0e18b7837861b955ac6 Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 13 Apr 2026 13:38:35 +0200 Subject: [PATCH 02/52] Add tests --- src/analytics/PosthogEvents.test.ts | 162 ++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 src/analytics/PosthogEvents.test.ts diff --git a/src/analytics/PosthogEvents.test.ts b/src/analytics/PosthogEvents.test.ts new file mode 100644 index 00000000..35b86f5d --- /dev/null +++ b/src/analytics/PosthogEvents.test.ts @@ -0,0 +1,162 @@ +/* +Copyright 2025 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 { + expect, + describe, + it, + vi, + beforeEach, + beforeAll, + afterAll, +} from "vitest"; +import { logger } from "matrix-js-sdk/lib/logger"; +import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc"; + +import { PosthogAnalytics } from "./PosthogAnalytics"; +import { CallEndedTracker } from "./PosthogEvents"; +import { mockConfig } from "../utils/test"; + +const defaultCounters = { + roomEventEncryptionKeysSent: 10, + roomEventEncryptionKeysReceived: 5, +}; + +const defaultTotals = { + roomEventEncryptionKeysReceivedTotalAge: 500, +}; + +function createMockRtcSession(overrides?: { + counters?: Partial; + totals?: Partial; +}): MatrixRTCSession { + return { + statistics: { + counters: { ...defaultCounters, ...overrides?.counters }, + totals: { ...defaultTotals, ...overrides?.totals }, + }, + } as unknown as MatrixRTCSession; +} + +describe("CallEnded", () => { + beforeAll(() => { + mockConfig(); + }); + + beforeEach(() => { + vi.restoreAllMocks(); + vi.spyOn(PosthogAnalytics.instance, "trackEvent").mockImplementation( + () => {}, + ); + }); + + afterAll(() => { + PosthogAnalytics.resetInstance(); + }); + + it("warns if startTime is missing when track is called", () => { + const warnSpy = vi.spyOn(logger, "warn"); + const tracker = new CallEndedTracker(); + const mockSession = createMockRtcSession(); + + tracker.track("test-call-id", 2, false, mockSession); + + expect(warnSpy).toHaveBeenCalledWith( + "[PosthogEvents] Failed to send posthog callEnded event due to missing startTime", + ); + expect(PosthogAnalytics.instance.trackEvent).not.toHaveBeenCalled(); + }); + + it("tracks event with correct properties when startTime is set", () => { + const tracker = new CallEndedTracker(); + const mockSession = createMockRtcSession(); + + tracker.cacheStartCall(new Date(Date.now() - 60000)); + tracker.cacheParticipantCountChanged(5); + tracker.track("test-call-id", 3, true, mockSession); + + expect(PosthogAnalytics.instance.trackEvent).toHaveBeenCalledWith( + { + eventName: "CallEnded", + callId: "test-call-id", + callParticipantsMax: 5, + callParticipantsOnLeave: 3, + callDuration: expect.closeTo(60, 1), + roomEventEncryptionKeysSent: 10, + roomEventEncryptionKeysReceived: 5, + roomEventEncryptionKeysReceivedAverageAge: 100, + }, + { send_instantly: true }, + ); + }); + + it("tracks maxParticipantsCount correctly across multiple changes", () => { + const tracker = new CallEndedTracker(); + const mockSession = createMockRtcSession(); + + tracker.cacheStartCall(new Date()); + tracker.cacheParticipantCountChanged(3); + tracker.cacheParticipantCountChanged(7); + tracker.cacheParticipantCountChanged(2); + tracker.track("test-call-id", 1, false, mockSession); + + expect(PosthogAnalytics.instance.trackEvent).toHaveBeenCalledWith( + expect.objectContaining({ + callParticipantsMax: 7, + }), + expect.anything(), + ); + }); + + it("computes roomEventEncryptionKeysReceivedAverageAge as 0 when no keys received", () => { + const tracker = new CallEndedTracker(); + const mockSession = createMockRtcSession({ + counters: { roomEventEncryptionKeysReceived: 0 }, + }); + + tracker.cacheStartCall(new Date()); + tracker.track("test-call-id", 1, false, mockSession); + + expect(PosthogAnalytics.instance.trackEvent).toHaveBeenCalledWith( + expect.objectContaining({ + roomEventEncryptionKeysReceivedAverageAge: 0, + }), + expect.anything(), + ); + }); + + it("computes roomEventEncryptionKeysReceivedAverageAge correctly when keys are received", () => { + const tracker = new CallEndedTracker(); + const mockSession = createMockRtcSession({ + counters: { roomEventEncryptionKeysReceived: 4 }, + totals: { roomEventEncryptionKeysReceivedTotalAge: 200 }, + }); + + tracker.cacheStartCall(new Date()); + tracker.track("test-call-id", 1, false, mockSession); + + expect(PosthogAnalytics.instance.trackEvent).toHaveBeenCalledWith( + expect.objectContaining({ + roomEventEncryptionKeysReceivedAverageAge: 50, + }), + expect.anything(), + ); + }); + + it("passes send_instantly option correctly", () => { + const tracker = new CallEndedTracker(); + const mockSession = createMockRtcSession(); + + tracker.cacheStartCall(new Date()); + tracker.track("test-call-id", 1, false, mockSession); + + expect(PosthogAnalytics.instance.trackEvent).toHaveBeenCalledWith( + expect.anything(), + { send_instantly: false }, + ); + }); +}); From 4d09651fe1e4add2ed8516d9aba54582f289c76c Mon Sep 17 00:00:00 2001 From: Timo K Date: Tue, 21 Apr 2026 10:00:36 +0200 Subject: [PATCH 03/52] Bump pnpm to v10 --- package.json | 2 +- pnpm-lock.yaml | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index c0f4d505..867822c9 100644 --- a/package.json +++ b/package.json @@ -156,5 +156,5 @@ "esbuild": "^0.27.7" } }, - "packageManager": "pnpm@9.0.0" + "packageManager": "pnpm@10.0.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c800b0c2..7bbc1dcb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,7 +14,7 @@ overrides: js-yaml: ^4.1.1 esbuild: ^0.27.7 -pnpmfileChecksum: rxqlpiscahzxqq6bf4el6c6jvu +pnpmfileChecksum: sha256-toM8G/xDIU9KCsLzvrWKlNM+7K7S70ZB0m5kCeEztjs= importers: @@ -190,7 +190,7 @@ importers: version: 6.10.2(eslint@8.57.1) eslint-plugin-matrix-org: specifier: 2.1.0 - version: 2.1.0(@babel/core@7.29.0)(@babel/eslint-parser@7.28.6(@babel/core@7.29.0)(eslint@8.57.1))(@babel/eslint-plugin@7.27.1(@babel/eslint-parser@7.28.6(@babel/core@7.29.0)(eslint@8.57.1))(eslint@8.57.1))(@stylistic/eslint-plugin@3.1.0(eslint@8.57.1)(typescript@5.9.3))(@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(@typescript-eslint/parser@8.58.2(eslint@8.57.1)(typescript@5.9.3))(eslint-config-google@0.14.0(eslint@8.57.1))(eslint-config-prettier@10.1.8(eslint@8.57.1))(eslint-plugin-deprecate@0.9.0(eslint@8.57.1))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint-plugin-jest@29.15.2(@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.1))(eslint-plugin-react-hooks@5.2.0(eslint@8.57.1))(eslint-plugin-react@7.37.5(eslint@8.57.1))(eslint-plugin-unicorn@56.0.1(eslint@8.57.1))(eslint@8.57.1)(prettier@3.8.3)(typescript@5.9.3) + version: 2.1.0(09e41ec1b738154136ea8b36e5821317) eslint-plugin-react: specifier: ^7.29.4 version: 7.37.5(eslint@8.57.1) @@ -238,7 +238,7 @@ importers: version: 1.9.2 matrix-js-sdk: specifier: matrix-org/matrix-js-sdk#develop - version: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/668183d7226ccb4819788018fb48e9a58f85a45b + version: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/4ee3e591bf6498eecc1a92aad92b2ff3fc604fb8 matrix-widget-api: specifier: ^1.16.1 version: 1.17.0 @@ -334,7 +334,7 @@ importers: version: 4.1.4(@types/node@24.12.2)(@vitest/coverage-v8@4.1.4)(jsdom@26.1.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)) vitest-axe: specifier: ^1.0.0-pre.3 - version: 1.0.0-pre.5(vitest@4.1.4(@types/node@24.12.2)(@vitest/coverage-v8@4.1.4)(jsdom@26.1.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))) + version: 1.0.0-pre.5(vitest@4.1.4) packages: @@ -4976,8 +4976,8 @@ packages: matrix-events-sdk@0.0.1: resolution: {integrity: sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==} - matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/668183d7226ccb4819788018fb48e9a58f85a45b: - resolution: {tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/668183d7226ccb4819788018fb48e9a58f85a45b} + matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/4ee3e591bf6498eecc1a92aad92b2ff3fc604fb8: + resolution: {tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/4ee3e591bf6498eecc1a92aad92b2ff3fc604fb8} version: 41.3.0 engines: {node: '>=22.0.0'} @@ -10664,8 +10664,8 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - ? eslint-plugin-matrix-org@2.1.0(@babel/core@7.29.0)(@babel/eslint-parser@7.28.6(@babel/core@7.29.0)(eslint@8.57.1))(@babel/eslint-plugin@7.27.1(@babel/eslint-parser@7.28.6(@babel/core@7.29.0)(eslint@8.57.1))(eslint@8.57.1))(@stylistic/eslint-plugin@3.1.0(eslint@8.57.1)(typescript@5.9.3))(@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(@typescript-eslint/parser@8.58.2(eslint@8.57.1)(typescript@5.9.3))(eslint-config-google@0.14.0(eslint@8.57.1))(eslint-config-prettier@10.1.8(eslint@8.57.1))(eslint-plugin-deprecate@0.9.0(eslint@8.57.1))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint-plugin-jest@29.15.2(@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.1))(eslint-plugin-react-hooks@5.2.0(eslint@8.57.1))(eslint-plugin-react@7.37.5(eslint@8.57.1))(eslint-plugin-unicorn@56.0.1(eslint@8.57.1))(eslint@8.57.1)(prettier@3.8.3)(typescript@5.9.3) - : dependencies: + eslint-plugin-matrix-org@2.1.0(09e41ec1b738154136ea8b36e5821317): + dependencies: '@babel/core': 7.29.0 '@babel/eslint-parser': 7.28.6(@babel/core@7.29.0)(eslint@8.57.1) '@babel/eslint-plugin': 7.27.1(@babel/eslint-parser@7.28.6(@babel/core@7.29.0)(eslint@8.57.1))(eslint@8.57.1) @@ -11751,7 +11751,7 @@ snapshots: matrix-events-sdk@0.0.1: {} - matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/668183d7226ccb4819788018fb48e9a58f85a45b: + matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/4ee3e591bf6498eecc1a92aad92b2ff3fc604fb8: dependencies: '@babel/runtime': 7.29.2 '@matrix-org/matrix-sdk-crypto-wasm': 18.1.0 @@ -13542,7 +13542,7 @@ snapshots: terser: 5.46.1 yaml: 2.8.3 - vitest-axe@1.0.0-pre.5(vitest@4.1.4(@types/node@24.12.2)(@vitest/coverage-v8@4.1.4)(jsdom@26.1.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))): + vitest-axe@1.0.0-pre.5(vitest@4.1.4): dependencies: '@vitest/pretty-format': 3.2.4 axe-core: 4.11.3 From 00d6e034a08ca6bcb0e66c195f72498ad84b9ad9 Mon Sep 17 00:00:00 2001 From: Timo K Date: Tue, 21 Apr 2026 10:53:56 +0200 Subject: [PATCH 04/52] fixup some missing yarn -> pnpm transitions --- .githooks/post-commit | 5 ++--- .githooks/pre-commit | 3 +-- README.md | 14 +++++++------- WIDGET_TEST.md | 4 ++-- backend/dev_nginx.conf | 20 ++++++++++---------- docs/linking.md | 6 +++--- docs/self-hosting.md | 4 ++-- embedded/android/publish_android_package.sh | 8 ++++---- knip.ts | 8 +------- renovate.json | 4 ++-- sdk/README.md | 4 ++-- sdk/main.ts | 2 +- src/livekit/BlurBackgroundTransformer.ts | 2 +- 13 files changed, 38 insertions(+), 46 deletions(-) diff --git a/.githooks/post-commit b/.githooks/post-commit index 9054c0e1..243b4f51 100755 --- a/.githooks/post-commit +++ b/.githooks/post-commit @@ -4,9 +4,8 @@ FILE=.links.cjs FILE_DIS=.links.temp-disabled.cjs if test -f "$FILE_DIS"; then # Only do the post-commit hook if the file was temp-disabled by the pre-commit hook. - # Otherwise linking was actively (`yarn links:disable`) disabled and this hook should noop. + # Otherwise linking was actively (`pnpm links:disable`) disabled and this hook should noop. mv $FILE_DIS $FILE - yarnLog=$(yarn) - echo "[yarn-linker] The post-commit hook has re-enabled $FILE" + echo "[pnpm-linker] The post-commit hook has re-enabled $FILE" exit 1 fi diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 6d8d26b0..f8ecfe4d 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -4,9 +4,8 @@ FILE=.links.cjs FILE_DIS=.links.temp-disabled.cjs if test -f "$FILE"; then mv $FILE .links.temp-disabled.cjs - # echo "running yarn" x=$(pnpm install) y=$(git add pnpm-lock.yaml) - echo "[yarn-linker] The pre-commit hook has disabled $FILE and MODIFIED the pnpm-lock.yaml file. Review the staged changes (the hook added pnpm-lock.yaml, was this desired?) and run \`git commit \` again if they look okay. The post-commit hook will re-enable your links." + echo "[pnpm-linker] The pre-commit hook has disabled $FILE and MODIFIED the pnpm-lock.yaml file. Review the staged changes (the hook added pnpm-lock.yaml, was this desired?) and run \`git commit \` again if they look okay. The post-commit hook will re-enable your links." exit 1 fi diff --git a/README.md b/README.md index 688a7a7f..3685e523 100644 --- a/README.md +++ b/README.md @@ -186,7 +186,7 @@ To get started clone and set up this project: git clone https://github.com/element-hq/element-call.git cd element-call corepack enable -yarn +pnpm install ``` To use it, create a local config by, e.g., @@ -197,7 +197,7 @@ environment as outlined in the next section out of box. You're now ready to launch the development server: ```sh -yarn dev +pnpm dev ``` See also: @@ -230,7 +230,7 @@ only for local development and **_never be exposed to the public Internet._** Run backend components: ```sh -yarn backend +pnpm backend # or for podman-compose # podman-compose -f dev-backend-docker-compose.yml up ``` @@ -260,13 +260,13 @@ on https://localhost:3000 (this is configured in `playwright.config.ts`) - this is what will be tested. The local backend environment should be running for the test to work: -`yarn backend` +`pnpm backend` There are a few different ways to run the tests yourself. The simplest is to run: ```shell -yarn run test:playwright +pnpm run test:playwright ``` This will run the Playwright tests once, non-interactively. @@ -274,7 +274,7 @@ This will run the Playwright tests once, non-interactively. There is a more user-friendly way to run the tests in interactive mode: ```shell -yarn run test:playwright:open +pnpm run test:playwright:open ``` The easiest way to develop new test is to use the codegen feature of Playwright: @@ -316,7 +316,7 @@ To add a new translation key you can do these steps: 1. Add the new key entry to the code where the new key is used: `t("some_new_key")` -1. Run `yarn i18n` to extract the new key and update the translation files. This +1. Run `pnpm i18n` to extract the new key and update the translation files. This will add a skeleton entry to the `locales/en/app.json` file: ```jsonc diff --git a/WIDGET_TEST.md b/WIDGET_TEST.md index 53e26a29..fbad026a 100644 --- a/WIDGET_TEST.md +++ b/WIDGET_TEST.md @@ -1,6 +1,6 @@ # Testing Element-Call in widget mode -When running `yarn backend` the latest element-web develop will be deployed and served on `http://localhost:8081`. +When running `pnpm backend` the latest element-web develop will be deployed and served on `http://localhost:8081`. In a development environment, you might prefer to just use the `element-web` repo directly, but this setup is useful for CI/CD testing. ## Setup @@ -18,7 +18,7 @@ that uses It is part of the existing backend setup. To start the backend, run: ```sh -yarn backend +pnpm backend ``` Then open `http://localhost:8081` in your browser. diff --git a/backend/dev_nginx.conf b/backend/dev_nginx.conf index d3ddbc53..bfd79fa2 100644 --- a/backend/dev_nginx.conf +++ b/backend/dev_nginx.conf @@ -28,11 +28,11 @@ server { # Reason: the lk-jwt-service uses the federation API for the openid token # verification, which requires TLS location ~ ^(/_matrix|/_synapse/client) { - proxy_pass "http://homeserver:8008"; + proxy_pass "http://homeserver:8008"; proxy_http_version 1.1; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header Host $host; + proxy_set_header Host $host; } error_page 500 502 503 504 /50x.html; @@ -73,7 +73,7 @@ server { proxy_http_version 1.1; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header Host $host; + proxy_set_header Host $host; } error_page 500 502 503 504 /50x.html; @@ -108,7 +108,7 @@ server { # JWT Service running at port 6080 proxy_pass http://jwt-auth-services/; - + } location ^~ /livekit/sfu/ { @@ -128,7 +128,7 @@ server { # LiveKit SFU websocket connection running at port 7880 proxy_pass http://livekit-sfu:7880/; } - + error_page 500 502 503 504 /50x.html; } @@ -156,7 +156,7 @@ server { # JWT Service running at port 16080 proxy_pass http://auth-service-1:16080/; - + } location ^~ /livekit/sfu/ { @@ -176,14 +176,14 @@ server { # LiveKit SFU websocket connection running at port 17880 proxy_pass http://livekit-sfu-1:17880/; } - + error_page 500 502 503 504 /50x.html; } # Convenience reverse proxy for the call.m.localhost domain to element call # running on the host either via -# - yarn dev --host or +# - pnpm dev --host or # - falling back to http (the element call docker container) server { listen 80; @@ -260,7 +260,7 @@ server { proxy_ssl_verify off; } - + error_page 500 502 503 504 /50x.html; } @@ -293,7 +293,7 @@ server { proxy_ssl_verify off; } - + error_page 500 502 503 504 /50x.html; } diff --git a/docs/linking.md b/docs/linking.md index 689a6f02..4ac6abf7 100644 --- a/docs/linking.md +++ b/docs/linking.md @@ -23,18 +23,18 @@ One always needs to run: ```bash mv .links.cjs .links.disabled.cjs -yarn +pnpm install ``` before committing a change. To make it more convenient to work with this linking system we added git hooks. -A `pre-commit` hook will run `mv .links.yaml .links.disabled.yaml`, `yarn` and `git add yarn.lock` if it detects +A `pre-commit` hook will run `mv .links.yaml .links.disabled.yaml`, `pnpm install` and `git add yarn.lock` if it detects a `.links.yaml` file and abort the commit. You will than need to check if the resulting changes are appropriate and commit again. A `post-commit` hook will setup the linking as it was -before if a `.links.disabled.yaml` is present. It runs `mv .links.disabled.yaml .links.yaml` and `yarn`. +before if a `.links.disabled.cjs` is present. It runs `mv .links.disabled.cjs .links.cjs` and `pnpm install`. To activate the hooks automatically configure git with diff --git a/docs/self-hosting.md b/docs/self-hosting.md index d6d46421..dc1dd687 100644 --- a/docs/self-hosting.md +++ b/docs/self-hosting.md @@ -237,8 +237,8 @@ source. First, clone and install the package: git clone https://github.com/element-hq/element-call.git cd element-call corepack enable -yarn -yarn build +pnpm install +pnpm build ``` If all went well, you can now find the build output under `dist` as a series of diff --git a/embedded/android/publish_android_package.sh b/embedded/android/publish_android_package.sh index 8c310c9b..31693317 100755 --- a/embedded/android/publish_android_package.sh +++ b/embedded/android/publish_android_package.sh @@ -11,7 +11,7 @@ pushd $CURRENT_DIR > /dev/null function build_assets() { echo "Generating Element Call assets..." pushd ../.. > /dev/null - yarn build + pnpm build popd > /dev/null } @@ -26,7 +26,7 @@ function copy_assets() { } getopts :sh opt -case $opt in +case $opt in s) SKIP=1 ;; @@ -41,7 +41,7 @@ if [ ! $SKIP ]; then echo "" if [[ $REPLY =~ ^[Yy]$ ]]; then build_assets - else + else echo "Using existing assets from ../../dist" fi copy_assets @@ -56,4 +56,4 @@ echo "Publishing the Android project" ./gradlew publishAndReleaseToMavenCentral --no-daemon -popd > /dev/null \ No newline at end of file +popd > /dev/null diff --git a/knip.ts b/knip.ts index 3be3e653..aa6aee79 100644 --- a/knip.ts +++ b/knip.ts @@ -30,16 +30,10 @@ export default { "@types/content-type", "@types/sdp-transform", "@types/uuid", - // We obviously use this, but if the package has been linked with yarn link, + // We obviously use this, but if the package has been linked with pnpm link, // then Knip will flag it as a false positive // https://github.com/webpro-nl/knip/issues/766 "@vector-im/compound-web", - // Yarn plugins are allowed to depend on packages provided by the Yarn - // runtime. These shouldn't be listed in package.json, because plugins - // should work before Yarn even installs dependencies for the first time. - // https://yarnpkg.com/advanced/plugin-tutorial#what-does-a-plugin-look-like - "@yarnpkg/core", - "@yarnpkg/parsers", "matrix-widget-api", ], ignoreExportsUsedInFile: true, diff --git a/renovate.json b/renovate.json index 612e6674..39fbf0c1 100644 --- a/renovate.json +++ b/renovate.json @@ -54,8 +54,8 @@ "matchFileNames": ["embedded/**/*"] }, { - "groupName": "Yarn", - "matchDepNames": ["yarn"] + "groupName": "Pnpm", + "matchDepNames": ["pnpm"] } ], "semanticCommits": "disabled", diff --git a/sdk/README.md b/sdk/README.md index ad8ff97e..7102ba29 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -13,8 +13,8 @@ This folder contains an example index.html file that showcases the sdk in use (h To get started run ``` -yarn -yarn build:sdk +pnpm install +pnpm build:sdk ``` in the repository root. diff --git a/sdk/main.ts b/sdk/main.ts index c65bf4a7..286c16ea 100644 --- a/sdk/main.ts +++ b/sdk/main.ts @@ -8,7 +8,7 @@ Please see LICENSE in the repository root for full details. /** * EXPERIMENTAL * - * This file is the entrypoint for the sdk build of element call: `yarn build:sdk` + * This file is the entrypoint for the sdk build of element call: `pnpm build:sdk` * use in widgets. * It exposes the `createMatrixRTCSdk` which creates the `MatrixRTCSdk` interface (see below) that * can be used to join a rtc session and exchange realtime data. diff --git a/src/livekit/BlurBackgroundTransformer.ts b/src/livekit/BlurBackgroundTransformer.ts index c3d885ba..7bec3cc4 100644 --- a/src/livekit/BlurBackgroundTransformer.ts +++ b/src/livekit/BlurBackgroundTransformer.ts @@ -29,7 +29,7 @@ interface WasmFileset { // MediaPipe and depend on node_modules having this specific structure. It's // easy to see this breaking if our dependencies changed and MediaPipe were // no longer hoisted, or if we switched to another dependency loader such as -// Yarn PnP. +// pnpm PnP. // https://github.com/google-ai-edge/mediapipe/issues/5961 const wasmFileset: WasmFileset = { wasmLoaderPath: new URL( From e9fd84816ee90bf2ee60a6cc8f4e561c84477f48 Mon Sep 17 00:00:00 2001 From: Robin Date: Tue, 21 Apr 2026 11:17:57 +0200 Subject: [PATCH 05/52] Invert the colors of the camera and microphone buttons again Turns out design prefers the button colors how they were prior to fa844446b6a745731ceb20178314bf8ba0b0ca41. --- src/button/Button.tsx | 4 ++-- src/room/__snapshots__/InCallView.test.tsx.snap | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/button/Button.tsx b/src/button/Button.tsx index 73938794..8a22668e 100644 --- a/src/button/Button.tsx +++ b/src/button/Button.tsx @@ -47,7 +47,7 @@ export const MicButton: FC = ({ enabled, ...props }) => { = ({ enabled, ...props }) => { rendering > renders 1`] = ` aria-disabled="true" aria-labelledby="_r_i_" class="_button_13vu4_8 _has-icon_13vu4_60 _icon-only_13vu4_53" - data-kind="secondary" + data-kind="primary" data-size="lg" data-testid="incall_mute" role="switch" @@ -354,7 +354,7 @@ exports[`InCallView > rendering > renders 1`] = ` aria-disabled="true" aria-labelledby="_r_n_" class="_button_13vu4_8 _has-icon_13vu4_60 _icon-only_13vu4_53" - data-kind="secondary" + data-kind="primary" data-size="lg" data-testid="incall_videomute" role="switch" From 84ed5943eab25ef6ef45448759ee23bc29556e1c Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 21 Apr 2026 11:55:03 +0200 Subject: [PATCH 06/52] fix: using vars. instead of secrets. for NETLIFY_SITE_ID --- .github/workflows/test-netlify.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-netlify.yaml b/.github/workflows/test-netlify.yaml index 959f7af0..bccde445 100644 --- a/.github/workflows/test-netlify.yaml +++ b/.github/workflows/test-netlify.yaml @@ -42,7 +42,7 @@ jobs: branch: ${{ github.event.workflow_run.head_branch }} revision: ${{ github.event.workflow_run.head_sha }} token: ${{ secrets.NETLIFY_AUTH_TOKEN }} - site_id: ${{ vars.NETLIFY_SITE_ID }} + site_id: ${{ secrets.NETLIFY_SITE_ID }} desc: Playwright Report deployment_env: EndToEndTests prefix: "e2e-" From b58076f1b93bdd17ec33ac0596d11c831b832fec Mon Sep 17 00:00:00 2001 From: Timo K Date: Tue, 21 Apr 2026 15:06:06 +0200 Subject: [PATCH 07/52] remove pnpmfileChecksum and .pnpmfile.cjs --- .pnpmfile.cjs | 58 -------------------------------------------------- pnpm-lock.yaml | 2 -- 2 files changed, 60 deletions(-) delete mode 100644 .pnpmfile.cjs diff --git a/.pnpmfile.cjs b/.pnpmfile.cjs deleted file mode 100644 index f5759c26..00000000 --- a/.pnpmfile.cjs +++ /dev/null @@ -1,58 +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. -*/ - -// Created based on https://github.com/element-hq/element-call/blob/60fae70a60e3697eb41210ccf1e400cab37df7c8/.yarn/plugins/linker.cjs -// and the following prompt history: -// - Can you convert this yarn plugin into a pnpm plugin. -// - The goal is to not have modifications to the package.json and lock files so that we do not track links on gh. -// This seems to modify the package.json file. -// What can we do with pnpm to have the link inforamtion in a seperate file -// - why do you cache the loaded links. When does this file get executed? -// Do we need this optimization. -// How do we guarantee, that we aleays use the most recent content from the links file? -// -// Manual transition to cjs. Claude proposed manual yaml parsing. - -const fs = require("fs"); -const path = require("path"); - -function loadLinks() { - try { - return require(path.join(__dirname, ".links.cjs")); - } catch (e) { - return null; - } -} - -function readPackage(pkg, context) { - const links = loadLinks(); - if (!links) return pkg; - - const manifest = JSON.parse( - fs.readFileSync(path.join(__dirname, "package.json"), "utf8"), - ); - if (pkg.name !== manifest.name) return pkg; - - for (const [name, linkPath] of Object.entries(links)) { - const resolved = `link:${path.resolve(__dirname, linkPath)}`; - if (pkg.dependencies && pkg.dependencies[name]) { - context.log(`Linking ${name} -> ${resolved}`); - pkg.dependencies[name] = resolved; - } else if (pkg.devDependencies && pkg.devDependencies[name]) { - context.log(`Linking ${name} -> ${resolved}`); - pkg.devDependencies[name] = resolved; - } - } - - return pkg; -} - -module.exports = { - hooks: { - readPackage, - }, -}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7bbc1dcb..f75fdaa1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,8 +14,6 @@ overrides: js-yaml: ^4.1.1 esbuild: ^0.27.7 -pnpmfileChecksum: sha256-toM8G/xDIU9KCsLzvrWKlNM+7K7S70ZB0m5kCeEztjs= - importers: .: From 741b82b0263ff79c98ac48a0f6ea9dbd7e8b2e95 Mon Sep 17 00:00:00 2001 From: Timo K Date: Tue, 21 Apr 2026 16:06:51 +0200 Subject: [PATCH 08/52] update linking realted logic - remove post-commit hook - remove .links.cjs enable/disable (instead just add/rm .pnpmfile.cjs) - rename pnpm links:enable -> pnpm links:on - rename pnpm links:disable -> pnpm links:off - unify doc filenames `-` -> `_` - add linking_concept_reasoning.md to provide background why the linking is how it is. --- .githooks/post-commit | 11 ---- .githooks/pre-commit | 14 ++--- README.md | 6 +- docs/README.md | 6 +- ...d-standalone.md => embedded_standalone.md} | 6 +- docs/linking.md | 45 +++++++++----- docs/linking_concept_reasoning.md | 27 ++++++++ docs/{self-hosting.md => self_hosting.md} | 0 docs/{url-params.md => url_params.md} | 14 ++--- package.json | 4 +- pnpm-lock.yaml | 8 +-- scripts/.pnpmfile.cjs | 62 +++++++++++++++++++ scripts/setup-linking.sh | 26 ++++++++ 13 files changed, 174 insertions(+), 55 deletions(-) delete mode 100755 .githooks/post-commit rename docs/{embedded-standalone.md => embedded_standalone.md} (97%) create mode 100644 docs/linking_concept_reasoning.md rename docs/{self-hosting.md => self_hosting.md} (100%) rename docs/{url-params.md => url_params.md} (97%) create mode 100644 scripts/.pnpmfile.cjs create mode 100755 scripts/setup-linking.sh diff --git a/.githooks/post-commit b/.githooks/post-commit deleted file mode 100755 index 243b4f51..00000000 --- a/.githooks/post-commit +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/sh - -FILE=.links.cjs -FILE_DIS=.links.temp-disabled.cjs -if test -f "$FILE_DIS"; then - # Only do the post-commit hook if the file was temp-disabled by the pre-commit hook. - # Otherwise linking was actively (`pnpm links:disable`) disabled and this hook should noop. - mv $FILE_DIS $FILE - echo "[pnpm-linker] The post-commit hook has re-enabled $FILE" - exit 1 -fi diff --git a/.githooks/pre-commit b/.githooks/pre-commit index f8ecfe4d..2656c9b9 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -1,11 +1,9 @@ -#!/usr/bin/sh +#!/usr/bin/env bash -FILE=.links.cjs -FILE_DIS=.links.temp-disabled.cjs -if test -f "$FILE"; then - mv $FILE .links.temp-disabled.cjs - x=$(pnpm install) - y=$(git add pnpm-lock.yaml) - echo "[pnpm-linker] The pre-commit hook has disabled $FILE and MODIFIED the pnpm-lock.yaml file. Review the staged changes (the hook added pnpm-lock.yaml, was this desired?) and run \`git commit \` again if they look okay. The post-commit hook will re-enable your links." +# Checks if there currently is linking configured. Informs the user to disable linking before committing. + +PNPMFILE=.pnpmfile.cjs +if test -f "$PNPMFILE"; then + echo "[pnpm-linker] The pre-commit hook detected $PNPMFILE which implies you have linked packages in your pnpm-lock.yaml. Run pnpm links:off and commit again. See also linking.md." exit 1 fi diff --git a/README.md b/README.md index 3685e523..0c82e4b0 100644 --- a/README.md +++ b/README.md @@ -108,17 +108,17 @@ recommended method for embedding Element Call.

For more details on the packages, see the -[Embedded vs. Standalone Guide](./docs/embedded-standalone.md). +[Embedded vs. Standalone Guide](./docs/embedded_standalone.md). ## 🛠️ Self-Hosting For operating and deploying Element Call on your own server, refer to the -[**Self-Hosting Guide**](./docs/self-hosting.md). +[**Self-Hosting Guide**](./docs/self_hosting.md). ## 🧭 MatrixRTC Backend Discovery and Selection For proper Element Call operation each site deployment needs a MatrixRTC backend -setup as outlined in the [Self-Hosting](#self-hosting). A typical federated site +setup as outlined in the [Self-Hosting](#self_hosting). A typical federated site deployment for three different sites A, B and C is depicted below.

diff --git a/docs/README.md b/docs/README.md index d97e8d56..e5a5d08a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,8 +2,8 @@ This folder contains documentation for setup, usage, and development of Element Call. -- [Embedded vs standalone mode](./embedded-standalone.md) -- [Url format and parameters](./url-params.md) +- [Embedded vs standalone mode](./embedded_standalone.md) +- [Url format and parameters](./url_params.md) - [Global JS controls](./controls.md) -- [Self-Hosting](./self-hosting.md) +- [Self-Hosting](./self_hosting.md) - [Developing with linked packages](./linking.md) diff --git a/docs/embedded-standalone.md b/docs/embedded_standalone.md similarity index 97% rename from docs/embedded-standalone.md rename to docs/embedded_standalone.md index 440dfac0..24ad2a7d 100644 --- a/docs/embedded-standalone.md +++ b/docs/embedded_standalone.md @@ -14,7 +14,7 @@ The table below provides a comparison of the two packages: | **Release artifacts** | Docker Image, Tarball | Tarball, NPM for Web, Android AAR, SwiftPM for iOS | | **Recommended for** | Standalone/guest access usage | Embedding within messenger apps | | **Responsibility for regulatory compliance** | The administrator that is deploying the app is responsible for compliance with any applicable regulations (e.g. privacy) | The developer of the messenger app is responsible for compliance | -| **Analytics consent** | Element Call will show a consent UI. | Element Call will not show a consent UI. The messenger app should only provide the embedded Element Call with the [analytics URL parameters](./url-params.md#embedded-only-parameters) if consent has been granted. | +| **Analytics consent** | Element Call will show a consent UI. | Element Call will not show a consent UI. The messenger app should only provide the embedded Element Call with the [analytics URL parameters](./url_params.md#embedded-only-parameters) if consent has been granted. | | **Analytics data** | Element Call will send data to the Posthog, Sentry and Open Telemetry targets specified by the administrator in the `config.json` | Element Call will send data to the Posthog and Sentry targets specified in the URL parameters by the messenger app | ### Using the embedded package within a messenger app @@ -26,7 +26,7 @@ The basics are: 1. Add the appropriate platform dependency as given for a [release](https://github.com/element-hq/element-call/releases), or use the embedded tarball. e.g. `npm install @element-hq/element-call-embedded@0.9.0` 2. Include the assets from the platform dependency in the build process. e.g. copy the assets during a [Webpack](https://github.com/element-hq/element-web/blob/247cd8d56d832d006d7dfb919d1042529d712b59/webpack.config.js#L677-L682) build. 3. Use the `index.html` entrypointof the imported assets when you are constructing the WebView or iframe. e.g. using a [relative path in a webapp](https://github.com/element-hq/element-web/blob/247cd8d56d832d006d7dfb919d1042529d712b59/src/models/Call.ts#L680), or on the the Android [WebViewAssetLoader](https://github.com/element-hq/element-x-android/blob/fe5aab6588ecdcf9354a3bfbd9e97c1b31175a8f/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt#L20) -4. Set any of the [embedded-only URL parameters](./url-params.md#embedded-only-parameters) that you need. +4. Set any of the [embedded-only URL parameters](./url_params.md#embedded-only-parameters) that you need. ## Widget vs standalone mode @@ -36,4 +36,4 @@ As a widget, the app only uses the core calling (MatrixRTC) parts. The rest (aut Element Call and the hosting client are connected via the widget API. Element Call detects that it is run as a widget if a widgetId is defined in the url parameters. If `widgetId` is present then Element Call will try to connect to the client via the widget postMessage API using the parameters provided in [Url Format and parameters -](./url-params.md). +](./url_params.md). diff --git a/docs/linking.md b/docs/linking.md index 4ac6abf7..b9c62edf 100644 --- a/docs/linking.md +++ b/docs/linking.md @@ -1,8 +1,25 @@ +## Quickstart guide +run +```bash +./scripts/setup-linking.sh +``` +Read the script output: +``` +Setup complete. +Update: .links.cjs to your liking +Run: 'pnpm links:on' to test your .links.cjs +Run: 'git commit' with links enabled to test the git pre-commit hook. +Run: 'pnpm links:off' to be able to commit again +Run: 'git config --local core.hooksPath ""' to allow committing with linking (not recommended) +Run: 'rm links.cjs' & 'git config --local core.hooksPath ""' to fully revert what this script did +``` + + # Developing with linked packages -If you want to make changes to a package that Element Call depends on and see those changes applied in real time, you can create a link to a local copy of the package. Pnpm has a command for this (`pnpm link`), but it's not recommended to use it as it ends up modifying package.json with details specific to your development environment. +If you want to make changes to a package that Element Call depends on and see those changes applied in real time, you can create a link to a local copy of the package. `pnpm` has a command for this (`pnpm link`), but it's not recommended to use it as it ends up modifying package.json with details specific to your development environment. -Instead, you can use our little 'linker' plugin. Create a file named `.links.cjs` in the Element Call project directory, listing the names and paths of any dependencies you want to link. For example: +Instead, create a file named `.links.cjs` in the Element Call project directory (or run `./scripts/setup-linking.sh` to create a template), listing the names and paths of any dependencies you want to link. For example: ```cjs // Packages to link to local checkouts @@ -12,32 +29,32 @@ module.exports = { }; ``` -Then run `pnpm install`. +Then run `pnpm links:on`. (this will activate the pnpm file + run `pnpm install` to setup the linking) ## Hooks -Changes in `.links.yaml` will also update `pnpm-lock.yaml` when `pnpm` is executed. The lockfile will then contain the local +Changes in `.links.cjs` will also update `pnpm-lock.yaml` when `pnpm install` is executed. The lockfile will then contain the local version of the package which would not work on others dev setups or the github CI. -One always needs to run: +One always needs to remove the pnpm `readPackage` script (the `.pnpmfile.cjs`) and run: ```bash -mv .links.cjs .links.disabled.cjs pnpm install ``` before committing a change. -To make it more convenient to work with this linking system we added git hooks. -A `pre-commit` hook will run `mv .links.yaml .links.disabled.yaml`, `pnpm install` and `git add yarn.lock` if it detects -a `.links.yaml` file and abort the commit. -You will than need to check if the resulting changes are appropriate and commit again. +To make this less of a foot gun we added a git hook. +A `pre-commit` hook will check if linking is currently used. If it detects +a `.pnpmfile.cjs` file it will abort the commit with an explanatory message. +You will than need to run `pnpm links:off` and commit again. -A `post-commit` hook will setup the linking as it was -before if a `.links.disabled.cjs` is present. It runs `mv .links.disabled.cjs .links.cjs` and `pnpm install`. - -To activate the hooks automatically configure git with +To activate the hooks configure git with (when using the setup script (`./scripts/setup-linking.sh`) this is already done): ```bash git config --local core.hooksPath .githooks ``` +This will add the hook path for this repository only to .gihooks. which is a tracked (by git) folder containing the pre-commit hook. + +## Background +Information, why this approach is used can be found in the [linking concept reasoning](./linking_concept_reasoning.md) document. diff --git a/docs/linking_concept_reasoning.md b/docs/linking_concept_reasoning.md new file mode 100644 index 00000000..37221344 --- /dev/null +++ b/docs/linking_concept_reasoning.md @@ -0,0 +1,27 @@ + +### Why do we not enable .pnpmfile.cjs by default +Background: The presence of the `.pnpmfile.cjs` adds a field to the `pnpm-lock.yaml` called: `pnpmfileChecksum`. This field is a checksum of the content of the `.pnpmfile.cjs` file. +`pnpm install --frozen-lockfile` **fails** if there is a `.pnpmfile.cjs` but no `pnpmfileChecksum` or vice versa (or on mismatch). + +_TLDR: running with `--ignore-pnpmfile` will fail if `pnpmfileChecksum` is present._ + +#### `pnpmfileChecksum` + renovate bot +When the renovate bot creates a PR it runs `pnpm install --ignore-pnpmfile`. This means that the `pnpmfileChecksum` in the lockfile will be **empty**. +This breaks builds that **don't** ignore the `.pnpmfile.cjs`-file. (CI that runs on the renovate PR) +From here we have two possible paths: + - ignore `.pnpmfile.cjs` in all CI builds CI will also fail if we accidently add it locally. + - fixup the `pnpm-lock.yaml` in the renovate PR to contain the correct `pnpmfileChecksum`. + + Ignoring in all CI builds means that CI will always fail if we enable the linking system. + This is annoying but can be worked around with the git hook we provide that at least lets us know that we are + commiting with enabled linking. + Only if we remember setting it back/disbale linking (or let ourselves remember by the git hook) the CI will work. + + #### Summary + - We will always run into conflicts with the `pnpmfileChecksum` because in renovate prs it will be empty (`--ignore-pnpmfile`) + - To keep it simple we set `--ignore-pnpmfile` in all of ours CI to see issues immediately. + - The only solution is to never have a `.pnpmfile.cjs` in the repository when pushing. + - This way there will never be a commit with `pnpmfileChecksum` in the lockfile. + - renovate (which uses `--ignore-pnpmfile` which we cannot disable) and other CI will work + - We are able to use the linking system locally if we `cp` this file from the scripts folder into `./` on demand. + - `pnpm links:on` and `pnpm links:off` + `./scripts/setup-linking.sh` will help us with this. diff --git a/docs/self-hosting.md b/docs/self_hosting.md similarity index 100% rename from docs/self-hosting.md rename to docs/self_hosting.md diff --git a/docs/url-params.md b/docs/url_params.md similarity index 97% rename from docs/url-params.md rename to docs/url_params.md index a1e4793d..e88e7095 100644 --- a/docs/url-params.md +++ b/docs/url_params.md @@ -4,7 +4,7 @@ There are two formats for Element Call URLs. ## Link for sharing -Requires Element Call to be deployed in [standalone](./embedded-standalone.md) mode. +Requires Element Call to be deployed in [standalone](./embedded_standalone.md) mode. ```text https://element_call.domain/room/# @@ -36,15 +36,15 @@ possible to support encryption. | Package | Deployment | URL | | ------------------------------------ | ----------------------------- | ----------------------------------------------------------------------------- | -| [Full](./embedded-standalone.md) | All | `https://element_call.domain/room` | -| [Embedded](./embedded-standalone.md) | Remote URL | `https://element_call.domain/` n.b. no `/room` part | -| [Embedded](./embedded-standalone.md) | Embedded within messenger app | Platform dependent, but you load the `index.html` file without a `/room` part | +| [Full](./embedded_standalone.md) | All | `https://element_call.domain/room` | +| [Embedded](./embedded_standalone.md) | Remote URL | `https://element_call.domain/` n.b. no `/room` part | +| [Embedded](./embedded_standalone.md) | Embedded within messenger app | Platform dependent, but you load the `index.html` file without a `/room` part | ## Parameters ### Common Parameters -These parameters are relevant to both [widget](./embedded-standalone.md) and [standalone](./embedded-standalone.md) modes: +These parameters are relevant to both [widget](./embedded_standalone.md) and [standalone](./embedded_standalone.md) modes: | Name | Values | Required for widget | Required for SPA | Description | | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -74,7 +74,7 @@ These parameters are relevant to both [widget](./embedded-standalone.md) and [st ### Widget-only parameters -These parameters are only supported in [widget](./embedded-standalone.md) mode. +These parameters are only supported in [widget](./embedded_standalone.md) mode. | Name | Values | Required | Description | | --------------- | ----------------------------------------------------------------------------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -89,7 +89,7 @@ These parameters are only supported in [widget](./embedded-standalone.md) mode. ### Embedded-only parameters -These parameters are only supported in the [embedded](./embedded-standalone.md) package of Element Call and will be ignored in the [full](./embedded-standalone.md) package. +These parameters are only supported in the [embedded](./embedded_standalone.md) package of Element Call and will be ignored in the [full](./embedded_standalone.md) package. | Name | Values | Required | Description | | -------------------- | -------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------- | diff --git a/package.json b/package.json index 867822c9..da6b7a2d 100644 --- a/package.json +++ b/package.json @@ -32,8 +32,8 @@ "backend-playwright": "docker-compose -f playwright-backend-docker-compose.yml -f playwright-backend-docker-compose.override.yml up", "test:playwright": "playwright test", "test:playwright:open": "pnpm test:playwright --ui", - "links:enable": "mv .links.disabled.cjs .links.cjs & touch .links.cjs", - "links:disable": "mv .links.cjs .links.disabled.cjs", + "links:on": "cp scripts/.pnpmfile.cjs .pnpmfile.cjs & pnpm install", + "links:off": "rm .pnpmfile.cjs & pnpm install", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f75fdaa1..c9d752ab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -236,7 +236,7 @@ importers: version: 1.9.2 matrix-js-sdk: specifier: matrix-org/matrix-js-sdk#develop - version: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/4ee3e591bf6498eecc1a92aad92b2ff3fc604fb8 + version: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/d7d771fadb983bf40bed2c97aa31c398d8eec875 matrix-widget-api: specifier: ^1.16.1 version: 1.17.0 @@ -4974,8 +4974,8 @@ packages: matrix-events-sdk@0.0.1: resolution: {integrity: sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==} - matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/4ee3e591bf6498eecc1a92aad92b2ff3fc604fb8: - resolution: {tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/4ee3e591bf6498eecc1a92aad92b2ff3fc604fb8} + matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/d7d771fadb983bf40bed2c97aa31c398d8eec875: + resolution: {tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/d7d771fadb983bf40bed2c97aa31c398d8eec875} version: 41.3.0 engines: {node: '>=22.0.0'} @@ -11749,7 +11749,7 @@ snapshots: matrix-events-sdk@0.0.1: {} - matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/4ee3e591bf6498eecc1a92aad92b2ff3fc604fb8: + matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/d7d771fadb983bf40bed2c97aa31c398d8eec875: dependencies: '@babel/runtime': 7.29.2 '@matrix-org/matrix-sdk-crypto-wasm': 18.1.0 diff --git a/scripts/.pnpmfile.cjs b/scripts/.pnpmfile.cjs new file mode 100644 index 00000000..23b0759f --- /dev/null +++ b/scripts/.pnpmfile.cjs @@ -0,0 +1,62 @@ +/* +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. +*/ +// DONT RUN THIS FILE MANUALLY +// This file is intended to be used with `pnpm links:on` and `pnpm links:off` which will copy this file to the project root. +// See docs/linking.md for details. +// +// +// Created based on https://github.com/element-hq/element-call/blob/60fae70a60e3697eb41210ccf1e400cab37df7c8/.yarn/plugins/linker.cjs +// and the following prompt history: +// - Can you convert this yarn plugin into a pnpm plugin. +// - The goal is to not have modifications to the package.json and lock files so that we do not track links on gh. +// This seems to modify the package.json file. +// What can we do with pnpm to have the link inforamtion in a seperate file +// - why do you cache the loaded links. When does this file get executed? +// Do we need this optimization. +// How do we guarantee, that we aleays use the most recent content from the links file? +// +// Manual transition to cjs. Claude proposed manual yaml parsing. + +const fs = require("fs"); +const path = require("path"); + +function loadLinks() { + try { + return require(path.join(__dirname, ".links.cjs")); + } catch (e) { + return null; + } +} + +function readPackage(pkg, context) { + const links = loadLinks(); + if (!links) return pkg; + + const manifest = JSON.parse( + fs.readFileSync(path.join(__dirname, "package.json"), "utf8"), + ); + if (pkg.name !== manifest.name) return pkg; + + for (const [name, linkPath] of Object.entries(links)) { + const resolved = `link:${path.resolve(__dirname, linkPath)}`; + if (pkg.dependencies && pkg.dependencies[name]) { + context.log(`Linking ${name} -> ${resolved}`); + pkg.dependencies[name] = resolved; + } else if (pkg.devDependencies && pkg.devDependencies[name]) { + context.log(`Linking ${name} -> ${resolved}`); + pkg.devDependencies[name] = resolved; + } + } + + return pkg; +} + +module.exports = { + hooks: { + readPackage, + }, +}; diff --git a/scripts/setup-linking.sh b/scripts/setup-linking.sh new file mode 100755 index 00000000..b20228d6 --- /dev/null +++ b/scripts/setup-linking.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +# Checks if there currently is linking configured. Informs the user to disable linking before committing. + +LINKSFILE=.links.cjs +echo "Checking for existing linking configuration in $LINKSFILE..." +if test -f "$LINKSFILE"; then +echo "Linking configuration found in $LINKSFILE." +else + echo "No $LINKSFILE -> Creating $LINKSFILE with default values. Please edit this file to point to your local checkouts of the dependencies you want to link." + echo '''// Packages to link to local checkouts +module.exports = { + "matrix-js-sdk": "../your/path/matrix-js-sdk", + "matrix-widget-api": "../your/path/matrix-widget-api", +};''' > $LINKSFILE +fi +echo "updating local git hookPath to .githooks" +git config --local core.hooksPath .githooks +echo "" +echo "Setup complete." +echo "Update: .links.cjs to your liking" +echo "Run: 'pnpm links:on' to test your .links.cjs" +echo "Run: 'git commit' with links enabled to test the git pre-commit hook." +echo "Run: 'pnpm links:off' to be able to commit again" +echo "Run: 'git config --local core.hooksPath \"\"' to allow committing with linking on (not recommended)" +echo "Run: 'rm links.cjs' & 'git config --local core.hooksPath \"\"' to fully revert what this script did" From 8728676eb6abf0eed37486f78a6bfee175c217b0 Mon Sep 17 00:00:00 2001 From: Timo K Date: Tue, 21 Apr 2026 16:10:45 +0200 Subject: [PATCH 09/52] update CI to ignore pnpmfile - This is save since we never run pnpmfiles in ci - We can still run it locally - It helps us detect if we accidently commit it anyways. --- .github/workflows/build-element-call.yaml | 3 ++- .github/workflows/lint.yaml | 3 ++- .github/workflows/test.yaml | 6 ++++-- .github/workflows/translations-download.yaml | 3 ++- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-element-call.yaml b/.github/workflows/build-element-call.yaml index 190cdfdb..f4071975 100644 --- a/.github/workflows/build-element-call.yaml +++ b/.github/workflows/build-element-call.yaml @@ -43,7 +43,8 @@ jobs: cache: "pnpm" node-version-file: ".node-version" - name: Install dependencies - run: "pnpm install --frozen-lockfile" + # ignore-pnpmfile should never be commited. Make CI crash if it happened (`pnpmfileChecksum` is present) + run: "pnpm install --frozen-lockfile --ignore-pnpmfile" - name: Build Element Call run: pnpm run build:"$PACKAGE":"$BUILD_MODE" env: diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 60b9095e..0638eca6 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -18,7 +18,8 @@ jobs: cache: "pnpm" node-version-file: ".node-version" - name: Install dependencies - run: "pnpm install --frozen-lockfile" + # ignore-pnpmfile should never be commited. Make CI crash if it happened (`pnpmfileChecksum` is present) + run: "pnpm install --frozen-lockfile --ignore-pnpmfile" - name: Prettier run: "pnpm run prettier:check" - name: i18n diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index c436c5ee..d8af6f92 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -20,7 +20,8 @@ jobs: cache: "pnpm" node-version-file: ".node-version" - name: Install dependencies - run: "pnpm install --frozen-lockfile" + # ignore-pnpmfile should never be commited. Make CI crash if it happened (`pnpmfileChecksum` is present) + run: "pnpm install --frozen-lockfile --ignore-pnpmfile" - name: Vitest run: "pnpm run test:coverage" - name: Upload to codecov @@ -45,7 +46,8 @@ jobs: cache: "pnpm" node-version-file: ".node-version" - name: Install dependencies - run: pnpm install --frozen-lockfile + # ignore-pnpmfile should never be commited. Make CI crash if it happened (`pnpmfileChecksum` is present) + run: pnpm install --frozen-lockfile --ignore-pnpmfile - name: Install Playwright Browsers run: pnpm exec playwright install --with-deps - name: Run backend components diff --git a/.github/workflows/translations-download.yaml b/.github/workflows/translations-download.yaml index 6339e378..08260a5a 100644 --- a/.github/workflows/translations-download.yaml +++ b/.github/workflows/translations-download.yaml @@ -26,7 +26,8 @@ jobs: node-version-file: ".node-version" - name: Install Deps - run: "pnpm install --frozen-lockfile" + # ignore-pnpmfile should never be commited. Make CI crash if it happened (`pnpmfileChecksum` is present) + run: "pnpm install --frozen-lockfile --ignore-pnpmfile" - name: Prune i18n run: "rm -R locales" From 6ae88531ecec1ebc4259b38fda3e7a8817d8e723 Mon Sep 17 00:00:00 2001 From: Timo K Date: Tue, 21 Apr 2026 16:10:57 +0200 Subject: [PATCH 10/52] pnpm v10.33.0 + lock file --- package.json | 2 +- pnpm-lock.yaml | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index da6b7a2d..87204c66 100644 --- a/package.json +++ b/package.json @@ -156,5 +156,5 @@ "esbuild": "^0.27.7" } }, - "packageManager": "pnpm@10.0.0" + "packageManager": "pnpm@10.33.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c9d752ab..b342500d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1700,41 +1700,49 @@ packages: resolution: {integrity: sha512-heV2+jmXyYnUrpUXSPugqWDRpnsQcDm2AX4wzTuvgdlZfoNYO0O3W2AVpJYaDn9AG4JdM6Kxom8+foE7/BcSig==} cpu: [arm64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-arm64-musl@11.19.1': resolution: {integrity: sha512-jvo2Pjs1c9KPxMuMPIeQsgu0mOJF9rEb3y3TdpsrqwxRM+AN6/nDDwv45n5ZrUnQMsdBy5gIabioMKnQfWo9ew==} cpu: [arm64] os: [linux] + libc: [musl] '@oxc-resolver/binding-linux-ppc64-gnu@11.19.1': resolution: {integrity: sha512-vLmdNxWCdN7Uo5suays6A/+ywBby2PWBBPXctWPg5V0+eVuzsJxgAn6MMB4mPlshskYbppjpN2Zg83ArHze9gQ==} cpu: [ppc64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-riscv64-gnu@11.19.1': resolution: {integrity: sha512-/b+WgR+VTSBxzgOhDO7TlMXC1ufPIMR6Vj1zN+/x+MnyXGW7prTLzU9eW85Aj7Th7CCEG9ArCbTeqxCzFWdg2w==} cpu: [riscv64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-riscv64-musl@11.19.1': resolution: {integrity: sha512-YlRdeWb9j42p29ROh+h4eg/OQ3dTJlpHSa+84pUM9+p6i3djtPz1q55yLJhgW9XfDch7FN1pQ/Vd6YP+xfRIuw==} cpu: [riscv64] os: [linux] + libc: [musl] '@oxc-resolver/binding-linux-s390x-gnu@11.19.1': resolution: {integrity: sha512-EDpafVOQWF8/MJynsjOGFThcqhRHy417sRyLfQmeiamJ8qVhSKAn2Dn2VVKUGCjVB9C46VGjhNo7nOPUi1x6uA==} cpu: [s390x] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-x64-gnu@11.19.1': resolution: {integrity: sha512-NxjZe+rqWhr+RT8/Ik+5ptA3oz7tUw361Wa5RWQXKnfqwSSHdHyrw6IdcTfYuml9dM856AlKWZIUXDmA9kkiBQ==} cpu: [x64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-x64-musl@11.19.1': resolution: {integrity: sha512-cM/hQwsO3ReJg5kR+SpI69DMfvNCp+A/eVR4b4YClE5bVZwz8rh2Nh05InhwI5HR/9cArbEkzMjcKgTHS6UaNw==} cpu: [x64] os: [linux] + libc: [musl] '@oxc-resolver/binding-openharmony-arm64@11.19.1': resolution: {integrity: sha512-QF080IowFB0+9Rh6RcD19bdgh49BpQHUW5TajG1qvWHvmrQznTZZjYlgE2ltLXyKY+qs4F/v5xuX1XS7Is+3qA==} @@ -1790,36 +1798,42 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.6': resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.6': resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.6': resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.6': resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.6': resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [musl] '@parcel/watcher-win32-arm64@2.5.6': resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} @@ -2327,36 +2341,42 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': resolution: {integrity: sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': resolution: {integrity: sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': resolution: {integrity: sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': resolution: {integrity: sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': resolution: {integrity: sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': resolution: {integrity: sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==} @@ -2443,66 +2463,79 @@ packages: resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.60.1': resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.60.1': resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.60.1': resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.60.1': resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.60.1': resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.60.1': resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.60.1': resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.60.1': resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.60.1': resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.60.1': resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.60.1': resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.60.1': resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.60.1': resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} @@ -4857,24 +4890,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} From 92338a2e7cf02910c5d24cb77d99a04c8a2aa360 Mon Sep 17 00:00:00 2001 From: Timo K Date: Tue, 21 Apr 2026 16:12:14 +0200 Subject: [PATCH 11/52] review --- src/livekit/BlurBackgroundTransformer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/livekit/BlurBackgroundTransformer.ts b/src/livekit/BlurBackgroundTransformer.ts index 7bec3cc4..f86120d3 100644 --- a/src/livekit/BlurBackgroundTransformer.ts +++ b/src/livekit/BlurBackgroundTransformer.ts @@ -29,7 +29,7 @@ interface WasmFileset { // MediaPipe and depend on node_modules having this specific structure. It's // easy to see this breaking if our dependencies changed and MediaPipe were // no longer hoisted, or if we switched to another dependency loader such as -// pnpm PnP. +// yarn PnP. // https://github.com/google-ai-edge/mediapipe/issues/5961 const wasmFileset: WasmFileset = { wasmLoaderPath: new URL( From d28cad3f12431111fa44e172cb6a35f6ac885278 Mon Sep 17 00:00:00 2001 From: Timo K Date: Tue, 21 Apr 2026 16:45:00 +0200 Subject: [PATCH 12/52] fix matrix-js-sdk branch reference --- pnpm-lock.yaml | 8 ++++---- pnpm-workspace.yaml | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 pnpm-workspace.yaml diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b342500d..fa9ebebb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -236,7 +236,7 @@ importers: version: 1.9.2 matrix-js-sdk: specifier: matrix-org/matrix-js-sdk#develop - version: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/d7d771fadb983bf40bed2c97aa31c398d8eec875 + version: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/4b33892d48017b3733a00a48cf5d30182be4a6fb matrix-widget-api: specifier: ^1.16.1 version: 1.17.0 @@ -5011,8 +5011,8 @@ packages: matrix-events-sdk@0.0.1: resolution: {integrity: sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==} - matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/d7d771fadb983bf40bed2c97aa31c398d8eec875: - resolution: {tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/d7d771fadb983bf40bed2c97aa31c398d8eec875} + matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/4b33892d48017b3733a00a48cf5d30182be4a6fb: + resolution: {tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/4b33892d48017b3733a00a48cf5d30182be4a6fb} version: 41.3.0 engines: {node: '>=22.0.0'} @@ -11786,7 +11786,7 @@ snapshots: matrix-events-sdk@0.0.1: {} - matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/d7d771fadb983bf40bed2c97aa31c398d8eec875: + matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/4b33892d48017b3733a00a48cf5d30182be4a6fb: dependencies: '@babel/runtime': 7.29.2 '@matrix-org/matrix-sdk-crypto-wasm': 18.1.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 00000000..3fbe34a8 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +# dependencies where we use branches and hashes in the package.json. But that also use a pre/post install script. +onlyBuiltDependencies: + - "matrix-js-sdk" From 2d16487ca1d54fe34b8a42024fd867bd876c4f77 Mon Sep 17 00:00:00 2001 From: Timo K Date: Tue, 21 Apr 2026 17:11:40 +0200 Subject: [PATCH 13/52] fix sticky send race --- playwright/spa-call-sticky.spec.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/playwright/spa-call-sticky.spec.ts b/playwright/spa-call-sticky.spec.ts index 246b4a73..33af94bf 100644 --- a/playwright/spa-call-sticky.spec.ts +++ b/playwright/spa-call-sticky.spec.ts @@ -28,7 +28,7 @@ async function setupTwoUserSpaCall( await page.goto("/"); let androlHasSentStickyEvent = false; - + const androlResolver = Promise.withResolvers(); await interceptEventSend( page, // This room is not encrypted, so the event is sent in clear @@ -36,6 +36,7 @@ async function setupTwoUserSpaCall( (req) => { androlHasSentStickyEvent = androlHasSentStickyEvent || isStickySend(req.url()); + androlResolver.resolve(); }, ); @@ -53,6 +54,7 @@ async function setupTwoUserSpaCall( let pevaraHasSentStickyEvent = false; + const paveraResolver = Promise.withResolvers(); await interceptEventSend( guestPage, // This room is not encrypted, so the event is sent in clear @@ -60,6 +62,7 @@ async function setupTwoUserSpaCall( (req) => { pevaraHasSentStickyEvent = pevaraHasSentStickyEvent || isStickySend(req.url()); + paveraResolver.resolve(); }, ); @@ -70,7 +73,9 @@ async function setupTwoUserSpaCall( "2_0", ); // Assert both sides have sent sticky membership events + await androlResolver.promise; expect(androlHasSentStickyEvent).toEqual(true); + await paveraResolver.promise; expect(pevaraHasSentStickyEvent).toEqual(true); return { guestPage }; From ecb2a6846f63e02da810b8af749bf40d2393ca81 Mon Sep 17 00:00:00 2001 From: Timo K Date: Tue, 21 Apr 2026 17:13:13 +0200 Subject: [PATCH 14/52] prettier --- docs/linking.md | 7 ++++++- docs/linking_concept_reasoning.md | 35 +++++++++++++++++-------------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/docs/linking.md b/docs/linking.md index b9c62edf..1016fffb 100644 --- a/docs/linking.md +++ b/docs/linking.md @@ -1,9 +1,13 @@ ## Quickstart guide + run + ```bash ./scripts/setup-linking.sh ``` + Read the script output: + ``` Setup complete. Update: .links.cjs to your liking @@ -14,7 +18,6 @@ Run: 'git config --local core.hooksPath ""' to allow committing with linking (no Run: 'rm links.cjs' & 'git config --local core.hooksPath ""' to fully revert what this script did ``` - # Developing with linked packages If you want to make changes to a package that Element Call depends on and see those changes applied in real time, you can create a link to a local copy of the package. `pnpm` has a command for this (`pnpm link`), but it's not recommended to use it as it ends up modifying package.json with details specific to your development environment. @@ -54,7 +57,9 @@ To activate the hooks configure git with (when using the setup script (`./script ```bash git config --local core.hooksPath .githooks ``` + This will add the hook path for this repository only to .gihooks. which is a tracked (by git) folder containing the pre-commit hook. ## Background + Information, why this approach is used can be found in the [linking concept reasoning](./linking_concept_reasoning.md) document. diff --git a/docs/linking_concept_reasoning.md b/docs/linking_concept_reasoning.md index 37221344..7c135a96 100644 --- a/docs/linking_concept_reasoning.md +++ b/docs/linking_concept_reasoning.md @@ -1,27 +1,30 @@ - ### Why do we not enable .pnpmfile.cjs by default + Background: The presence of the `.pnpmfile.cjs` adds a field to the `pnpm-lock.yaml` called: `pnpmfileChecksum`. This field is a checksum of the content of the `.pnpmfile.cjs` file. `pnpm install --frozen-lockfile` **fails** if there is a `.pnpmfile.cjs` but no `pnpmfileChecksum` or vice versa (or on mismatch). _TLDR: running with `--ignore-pnpmfile` will fail if `pnpmfileChecksum` is present._ #### `pnpmfileChecksum` + renovate bot + When the renovate bot creates a PR it runs `pnpm install --ignore-pnpmfile`. This means that the `pnpmfileChecksum` in the lockfile will be **empty**. This breaks builds that **don't** ignore the `.pnpmfile.cjs`-file. (CI that runs on the renovate PR) From here we have two possible paths: - - ignore `.pnpmfile.cjs` in all CI builds CI will also fail if we accidently add it locally. - - fixup the `pnpm-lock.yaml` in the renovate PR to contain the correct `pnpmfileChecksum`. - - Ignoring in all CI builds means that CI will always fail if we enable the linking system. - This is annoying but can be worked around with the git hook we provide that at least lets us know that we are - commiting with enabled linking. - Only if we remember setting it back/disbale linking (or let ourselves remember by the git hook) the CI will work. - #### Summary - - We will always run into conflicts with the `pnpmfileChecksum` because in renovate prs it will be empty (`--ignore-pnpmfile`) - - To keep it simple we set `--ignore-pnpmfile` in all of ours CI to see issues immediately. - - The only solution is to never have a `.pnpmfile.cjs` in the repository when pushing. - - This way there will never be a commit with `pnpmfileChecksum` in the lockfile. - - renovate (which uses `--ignore-pnpmfile` which we cannot disable) and other CI will work - - We are able to use the linking system locally if we `cp` this file from the scripts folder into `./` on demand. - - `pnpm links:on` and `pnpm links:off` + `./scripts/setup-linking.sh` will help us with this. +- ignore `.pnpmfile.cjs` in all CI builds CI will also fail if we accidently add it locally. +- fixup the `pnpm-lock.yaml` in the renovate PR to contain the correct `pnpmfileChecksum`. + +Ignoring in all CI builds means that CI will always fail if we enable the linking system. +This is annoying but can be worked around with the git hook we provide that at least lets us know that we are +commiting with enabled linking. +Only if we remember setting it back/disbale linking (or let ourselves remember by the git hook) the CI will work. + +#### Summary + +- We will always run into conflicts with the `pnpmfileChecksum` because in renovate prs it will be empty (`--ignore-pnpmfile`) +- To keep it simple we set `--ignore-pnpmfile` in all of ours CI to see issues immediately. +- The only solution is to never have a `.pnpmfile.cjs` in the repository when pushing. + - This way there will never be a commit with `pnpmfileChecksum` in the lockfile. + - renovate (which uses `--ignore-pnpmfile` which we cannot disable) and other CI will work +- We are able to use the linking system locally if we `cp` this file from the scripts folder into `./` on demand. +- `pnpm links:on` and `pnpm links:off` + `./scripts/setup-linking.sh` will help us with this. From bbb73b27cb834171ffcfadc92014843094f4f0f0 Mon Sep 17 00:00:00 2001 From: Timo K Date: Tue, 21 Apr 2026 17:20:58 +0200 Subject: [PATCH 15/52] fix knip --- knip.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/knip.ts b/knip.ts index aa6aee79..d7628941 100644 --- a/knip.ts +++ b/knip.ts @@ -18,6 +18,7 @@ export default { // https://docs.docker.com/compose/migrate/ "docker-compose", ], + ignoreFiles: ["scripts/.pnpmfile.cjs"], ignoreDependencies: [ // Used in CSS "normalize.css", From ea73e5881ae854427dce58d94fb1a5158fa7ffe1 Mon Sep 17 00:00:00 2001 From: Timo K Date: Tue, 21 Apr 2026 17:29:15 +0200 Subject: [PATCH 16/52] pevara --- playwright/spa-call-sticky.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/playwright/spa-call-sticky.spec.ts b/playwright/spa-call-sticky.spec.ts index 33af94bf..328a65da 100644 --- a/playwright/spa-call-sticky.spec.ts +++ b/playwright/spa-call-sticky.spec.ts @@ -54,7 +54,7 @@ async function setupTwoUserSpaCall( let pevaraHasSentStickyEvent = false; - const paveraResolver = Promise.withResolvers(); + const pevaraResolver = Promise.withResolvers(); await interceptEventSend( guestPage, // This room is not encrypted, so the event is sent in clear @@ -62,7 +62,7 @@ async function setupTwoUserSpaCall( (req) => { pevaraHasSentStickyEvent = pevaraHasSentStickyEvent || isStickySend(req.url()); - paveraResolver.resolve(); + pevaraResolver.resolve(); }, ); @@ -75,7 +75,7 @@ async function setupTwoUserSpaCall( // Assert both sides have sent sticky membership events await androlResolver.promise; expect(androlHasSentStickyEvent).toEqual(true); - await paveraResolver.promise; + await pevaraResolver.promise; expect(pevaraHasSentStickyEvent).toEqual(true); return { guestPage }; From 4211405e7b419cec745076ff137244150b9b6057 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 3 Apr 2026 16:09:30 +0200 Subject: [PATCH 17/52] Use synapse API to register instead doing via UI --- backend/dev_homeserver-othersite.yaml | 3 + backend/dev_homeserver.yaml | 3 + backend/dev_nginx.conf | 21 ++- backend/playwright_homeserver-othersite.yaml | 3 + backend/playwright_homeserver.yaml | 3 + playwright.config.ts | 11 ++ playwright/utils/synapse-admin.ts | 142 +++++++++++++++++++ playwright/widget/test-helpers.ts | 50 ++++--- 8 files changed, 213 insertions(+), 23 deletions(-) create mode 100644 playwright/utils/synapse-admin.ts diff --git a/backend/dev_homeserver-othersite.yaml b/backend/dev_homeserver-othersite.yaml index 81e775ca..7eb8f294 100644 --- a/backend/dev_homeserver-othersite.yaml +++ b/backend/dev_homeserver-othersite.yaml @@ -50,6 +50,9 @@ max_event_delay_duration: 24h enable_registration: true enable_registration_without_verification: true +# Shared secret for admin user registration via API (for testing only!) +registration_shared_secret: "test_shared_secret_for_local_dev_only" + report_stats: false serve_server_wellknown: true diff --git a/backend/dev_homeserver.yaml b/backend/dev_homeserver.yaml index dc7b42c8..0aea2ece 100644 --- a/backend/dev_homeserver.yaml +++ b/backend/dev_homeserver.yaml @@ -50,6 +50,9 @@ max_event_delay_duration: 24h enable_registration: true enable_registration_without_verification: true +# Shared secret for admin user registration via API (for testing only!) +registration_shared_secret: "test_shared_secret_for_local_dev_only" + report_stats: false serve_server_wellknown: true diff --git a/backend/dev_nginx.conf b/backend/dev_nginx.conf index d3ddbc53..fdefa3bd 100644 --- a/backend/dev_nginx.conf +++ b/backend/dev_nginx.conf @@ -28,11 +28,18 @@ server { # Reason: the lk-jwt-service uses the federation API for the openid token # verification, which requires TLS location ~ ^(/_matrix|/_synapse/client) { - proxy_pass "http://homeserver:8008"; + proxy_pass "http://homeserver:8008"; proxy_http_version 1.1; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header Host $host; + proxy_set_header Host $host; + } + location ~ ^(/_matrix|/_synapse/admin) { + proxy_pass "http://homeserver:8008"; + proxy_http_version 1.1; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $host; } error_page 500 502 503 504 /50x.html; @@ -73,7 +80,15 @@ server { proxy_http_version 1.1; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header Host $host; + proxy_set_header Host $host; + } + + location ~ ^(/_matrix|/_synapse/admin) { + proxy_pass "http://homeserver-1:18008"; + proxy_http_version 1.1; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $host; } error_page 500 502 503 504 /50x.html; diff --git a/backend/playwright_homeserver-othersite.yaml b/backend/playwright_homeserver-othersite.yaml index 35640ae9..86c77b35 100644 --- a/backend/playwright_homeserver-othersite.yaml +++ b/backend/playwright_homeserver-othersite.yaml @@ -50,6 +50,9 @@ max_event_delay_duration: 24h enable_registration: true enable_registration_without_verification: true +# Shared secret for admin user registration via API (for testing only!) +registration_shared_secret: "test_shared_secret_for_local_dev_only" + report_stats: false serve_server_wellknown: true diff --git a/backend/playwright_homeserver.yaml b/backend/playwright_homeserver.yaml index a83247cd..8f437524 100644 --- a/backend/playwright_homeserver.yaml +++ b/backend/playwright_homeserver.yaml @@ -50,6 +50,9 @@ max_event_delay_duration: 24h enable_registration: true enable_registration_without_verification: true +# Shared secret for admin user registration via API (for testing only!) +registration_shared_secret: "test_shared_secret_for_local_dev_only" + report_stats: false serve_server_wellknown: true diff --git a/playwright.config.ts b/playwright.config.ts index 4fb86b95..84afed64 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -7,11 +7,22 @@ Please see LICENSE in the repository root for full details. */ import { defineConfig, devices } from "@playwright/test"; +import { join } from "path"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; const baseURL = process.env.USE_DOCKER ? "http://localhost:8080" : "https://localhost:3000"; +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +// Needed by the synapse admin API called in fixtures +process.env.NODE_EXTRA_CA_CERTS = join( + __dirname, + "backend/dev_tls_local-ca.crt", +); + /** * See https://playwright.dev/docs/test-configuration. */ diff --git a/playwright/utils/synapse-admin.ts b/playwright/utils/synapse-admin.ts new file mode 100644 index 00000000..b1d0039c --- /dev/null +++ b/playwright/utils/synapse-admin.ts @@ -0,0 +1,142 @@ +/* +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 { createHmac } from "crypto"; + +/** + * Response from Synapse registration API + */ +export interface SynapseRegistrationResponse { + access_token: string; + user_id: string; + home_server: string; + device_id: string; +} + +/** + * Utility class for interacting with Synapse Admin API + * This provides fast user registration without going through the UI + * + * @see https://matrix-org.github.io/synapse/latest/admin_api/register_api.html + */ +export class SynapseAdmin { + public constructor( + private baseUrl: string = "https://synapse.m.localhost", + private sharedSecret: string = "test_shared_secret_for_local_dev_only", + ) {} + + /** + * Register a user using the Synapse Admin API + * This is much faster than going through the UI registration flow + * + * @param username - The username (localpart) for the new user + * @param password - The password for the new user + * @param displayName - Optional display name (defaults to username) + * @param admin - Whether the user should be an admin (defaults to false) + * @returns Registration response containing access token and user ID + */ + public async registerUser( + username: string, + password: string, + displayName?: string, + admin: boolean = false, + ): Promise { + // Get a nonce first + const nonce = await this.getNonce(); + + // Generate the HMAC + const mac = this.generateMac(username, password, admin, nonce); + + // Make the registration request + const response = await fetch(`${this.baseUrl}/_synapse/admin/v1/register`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + nonce, + username, + password, + displayname: displayName || username, + admin, + mac, + }), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error( + `Failed to register user ${username}: ${response.status} ${error}`, + ); + } + + return response.json(); + } + + /** + * Get a nonce for registration + * The nonce is required for the HMAC calculation + * + * @returns A nonce string + */ + private async getNonce(): Promise { + const response = await fetch(`${this.baseUrl}/_synapse/admin/v1/register`, { + method: "GET", + }); + + if (!response.ok) { + throw new Error( + `Failed to get nonce: ${response.status} ${await response.text()}`, + ); + } + + const data = await response.json(); + return data.nonce; + } + + /** + * Generate HMAC for shared secret registration + * This is the authentication mechanism for the admin API + * + * @param username - The username + * @param password - The password + * @param admin - Whether the user is an admin + * @param nonce - The nonce from the server + * @returns The HMAC hex string + */ + private generateMac( + username: string, + password: string, + admin: boolean, + nonce: string, + ): string { + const mac = createHmac("sha1", this.sharedSecret); + mac.update(nonce); + mac.update("\x00"); + mac.update(username); + mac.update("\x00"); + mac.update(password); + mac.update("\x00"); + mac.update(admin ? "admin" : "notadmin"); + + return mac.digest("hex"); + } + + /** + * Create a new SynapseAdmin instance for a different homeserver + * + * @param baseUrl - The base URL of the homeserver + * @param sharedSecret - The shared secret (defaults to test secret) + * @returns A new SynapseAdmin instance + */ + public static forHomeserver( + baseUrl: string, + sharedSecret: string = "test_shared_secret_for_local_dev_only", + ): SynapseAdmin { + return new SynapseAdmin(baseUrl, sharedSecret); + } +} diff --git a/playwright/widget/test-helpers.ts b/playwright/widget/test-helpers.ts index ff5fa7e2..d0674f8a 100644 --- a/playwright/widget/test-helpers.ts +++ b/playwright/widget/test-helpers.ts @@ -13,6 +13,8 @@ import { } from "@playwright/test"; import { type MatrixClient } from "matrix-js-sdk"; +import { SynapseAdmin } from "../utils/synapse-admin.ts"; + const PASSWORD = "foobarbaz1!"; export const HOST1 = "https://app.m.localhost/#/welcome"; @@ -74,29 +76,41 @@ export class TestHelpers { clientHandle: JSHandle; mxId: string; }> { + // Determine which homeserver to use based on the host + const synapseBaseUrl = + host === HOST2 + ? "https://synapse.othersite.m.localhost" + : "https://synapse.m.localhost"; + + // Register user via Synapse Admin API to speed things up + const synapseAdmin = SynapseAdmin.forHomeserver(synapseBaseUrl); + const credentials = await synapseAdmin.registerUser( + username, + PASSWORD, + username, + ); + + // STEP 2: Open browser and login const userContext = await browser.newContext({ reducedMotion: "reduce", }); const page = await userContext.newPage(); - await page.goto(host); - await page.getByRole("link", { name: "Create Account" }).click(); + await page.goto(host + "/#/login"); + + await page.getByRole("link", { name: "Sign in" }).click(); + await page.getByRole("textbox", { name: "Username" }).fill(username); - await page.getByRole("textbox", { name: "Password", exact: true }).click(); - await page - .getByRole("textbox", { name: "Password", exact: true }) - .fill(PASSWORD); - await page.getByRole("textbox", { name: "Confirm password" }).click(); - await page - .getByRole("textbox", { name: "Confirm password" }) - .fill(PASSWORD); - await page.getByRole("button", { name: "Register" }).click(); + await page.getByRole("textbox", { name: "Password" }).fill(PASSWORD); + await page.getByRole("button", { name: "Sign in" }).click(); + + // 😤For reasons web is staying on an infinite loading page after login, so we reload the page + // Super annoying to have to wait... + await page.waitForTimeout(2000); + await page.reload(); await expect( page.getByRole("heading", { name: `Welcome ${username}` }), - ).toBeVisible({ - // Increase timeout as registration can be slow :/ - timeout: 15_000, - }); + ).toBeVisible(); await this.maybeDismissBrowserNotSupportedToast(page); await this.maybeDismissServiceWorkerWarningToast(page); @@ -106,11 +120,7 @@ export class TestHelpers { const clientHandle = await page.evaluateHandle(() => window.mxMatrixClientPeg.get(), ); - const mxId = (await clientHandle.evaluate( - (cli: MatrixClient) => cli.getUserId(), - clientHandle, - ))!; - + const mxId = credentials.user_id; return { page, clientHandle, mxId }; } From 24e721f572625ffd55da82d765194e79639918f6 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 7 Apr 2026 13:43:19 +0200 Subject: [PATCH 18/52] fix web stuck problem! go to / and not to #/login --- playwright/widget/test-helpers.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/playwright/widget/test-helpers.ts b/playwright/widget/test-helpers.ts index d0674f8a..2279e20e 100644 --- a/playwright/widget/test-helpers.ts +++ b/playwright/widget/test-helpers.ts @@ -95,7 +95,7 @@ export class TestHelpers { reducedMotion: "reduce", }); const page = await userContext.newPage(); - await page.goto(host + "/#/login"); + await page.goto(host); await page.getByRole("link", { name: "Sign in" }).click(); @@ -103,11 +103,6 @@ export class TestHelpers { await page.getByRole("textbox", { name: "Password" }).fill(PASSWORD); await page.getByRole("button", { name: "Sign in" }).click(); - // 😤For reasons web is staying on an infinite loading page after login, so we reload the page - // Super annoying to have to wait... - await page.waitForTimeout(2000); - await page.reload(); - await expect( page.getByRole("heading", { name: `Welcome ${username}` }), ).toBeVisible(); From 0127040a8c88f60e6ad1009804994f8fc3cb316a Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 7 Apr 2026 15:43:55 +0200 Subject: [PATCH 19/52] Remove slow? no registration anymore --- playwright/mobile/create-call-mobile.spec.ts | 1 - playwright/widget/huddle-call.test.ts | 2 -- playwright/widget/pip-call-button-interaction.test.ts | 2 -- playwright/widget/pip-call.test.ts | 2 -- playwright/widget/simple-create.spec.ts | 2 -- playwright/widget/voice-call-dm.spec.ts | 6 ------ 6 files changed, 15 deletions(-) diff --git a/playwright/mobile/create-call-mobile.spec.ts b/playwright/mobile/create-call-mobile.spec.ts index b66ad6c4..1d9d3af0 100644 --- a/playwright/mobile/create-call-mobile.spec.ts +++ b/playwright/mobile/create-call-mobile.spec.ts @@ -53,7 +53,6 @@ test("@mobile Start a new call then leave and show the feedback screen", async ( mobileTest( "Test earpiece overlay in controlledAudioDevices mode", async ({ asMobile, browser }) => { - test.slow(); // Triples the timeout const { creatorPage, inviteLink } = asMobile; // ======== diff --git a/playwright/widget/huddle-call.test.ts b/playwright/widget/huddle-call.test.ts index 9c7cfc80..ea5d64b6 100644 --- a/playwright/widget/huddle-call.test.ts +++ b/playwright/widget/huddle-call.test.ts @@ -16,8 +16,6 @@ widgetTest("Create and join a group call", async ({ addUser, browserName }) => { "The is test is not working on firefox CI environment. No mic/audio device inputs so cam/mic are disabled", ); - test.slow(); // We are registering multiple users here, give it more time - const valere = await addUser("Valere", HOST1); const timo = await addUser("Timo", HOST1); const robin = await addUser("Robin", HOST1); diff --git a/playwright/widget/pip-call-button-interaction.test.ts b/playwright/widget/pip-call-button-interaction.test.ts index 5e59f822..95aa4196 100644 --- a/playwright/widget/pip-call-button-interaction.test.ts +++ b/playwright/widget/pip-call-button-interaction.test.ts @@ -16,8 +16,6 @@ widgetTest("Footer interaction in PiP", async ({ addUser, browserName }) => { "The is test is not working on firefox CI environment. No mic/audio device inputs so cam/mic are disabled", ); - test.slow(); - const valere = await addUser("Valere", HOST1); const callRoom = "CallRoom"; diff --git a/playwright/widget/pip-call.test.ts b/playwright/widget/pip-call.test.ts index 63ba2050..ef19a36e 100644 --- a/playwright/widget/pip-call.test.ts +++ b/playwright/widget/pip-call.test.ts @@ -16,8 +16,6 @@ widgetTest("Put call in PIP", async ({ addUser, browserName }) => { "The is test is not working on firefox CI environment. No mic/audio device inputs so cam/mic are disabled", ); - test.slow(); - const valere = await addUser("Valere", HOST1); const timo = await addUser("Timo", HOST1); diff --git a/playwright/widget/simple-create.spec.ts b/playwright/widget/simple-create.spec.ts index 4686e99d..6b5d17bb 100644 --- a/playwright/widget/simple-create.spec.ts +++ b/playwright/widget/simple-create.spec.ts @@ -17,8 +17,6 @@ widgetTest.skip( ); widgetTest("Start a new call as widget", async ({ asWidget, browserName }) => { - test.slow(); // Triples the timeout - const { brooks, whistler } = asWidget; await TestHelpers.startCallInCurrentRoom(brooks.page, false); diff --git a/playwright/widget/voice-call-dm.spec.ts b/playwright/widget/voice-call-dm.spec.ts index a4e6255b..7a13c58b 100644 --- a/playwright/widget/voice-call-dm.spec.ts +++ b/playwright/widget/voice-call-dm.spec.ts @@ -20,8 +20,6 @@ widgetTest( "The is test is not working on firefox CI environment. No mic/audio device inputs so cam/mic are disabled", ); - test.slow(); // Triples the timeout - const { brooks, whistler } = asWidget; await TestHelpers.startCallInCurrentRoom(brooks.page, true); @@ -113,8 +111,6 @@ widgetTest( "The is test is not working on firefox CI environment. No mic/audio device inputs so cam/mic are disabled", ); - test.slow(); // Triples the timeout - const { brooks, whistler } = asWidget; await TestHelpers.startCallInCurrentRoom(brooks.page, false); @@ -200,8 +196,6 @@ widgetTest( "The is test is not working on firefox CI environment. No mic/audio device inputs so cam/mic are disabled", ); - test.slow(); // Triples the timeout - const { brooks, whistler } = asWidget; await TestHelpers.startCallInCurrentRoom(brooks.page, false); From 701edd952273de7ba77bd3309cdcb8227eb793f2 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 21 Apr 2026 17:54:10 +0200 Subject: [PATCH 20/52] fixup lint --- playwright/widget/simple-create.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright/widget/simple-create.spec.ts b/playwright/widget/simple-create.spec.ts index 6b5d17bb..c8fb8013 100644 --- a/playwright/widget/simple-create.spec.ts +++ b/playwright/widget/simple-create.spec.ts @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ -import { expect, test } from "@playwright/test"; +import { expect } from "@playwright/test"; import { widgetTest } from "../fixtures/widget-user.ts"; import { TestHelpers } from "./test-helpers.ts"; From 754a42ffd1ff2f9eff8f7999b07457386ba588e8 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 21 Apr 2026 18:14:51 +0200 Subject: [PATCH 21/52] increase default timeout on huddle test --- playwright/widget/huddle-call.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/playwright/widget/huddle-call.test.ts b/playwright/widget/huddle-call.test.ts index ea5d64b6..7bc94f81 100644 --- a/playwright/widget/huddle-call.test.ts +++ b/playwright/widget/huddle-call.test.ts @@ -11,6 +11,9 @@ import { widgetTest } from "../fixtures/widget-user.ts"; import { HOST1, TestHelpers } from "./test-helpers.ts"; widgetTest("Create and join a group call", async ({ addUser, browserName }) => { + // increase the timeouts, it is a long test and it is annoying to retry from the beginning for a single timeout. + test.slow(); + test.skip( browserName === "firefox", "The is test is not working on firefox CI environment. No mic/audio device inputs so cam/mic are disabled", From 0e2d2c6f72e8953a3d0be2c26093bc66e1070ad6 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 21 Apr 2026 18:36:40 +0200 Subject: [PATCH 22/52] try paralelize registrations? --- playwright/widget/federated-call.test.ts | 6 ++++-- playwright/widget/huddle-call.test.ts | 14 ++++++++------ playwright/widget/screen-share.test.ts | 8 +++++--- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/playwright/widget/federated-call.test.ts b/playwright/widget/federated-call.test.ts index fda58250..2b17a706 100644 --- a/playwright/widget/federated-call.test.ts +++ b/playwright/widget/federated-call.test.ts @@ -26,8 +26,10 @@ modePairs.forEach(([rtcMode1, rtcMode2]) => { "The is test is not working on firefox CI environment. No mic/audio device inputs so cam/mic are disabled", ); - const florian = await addUser("floriant", HOST1); - const timo = await addUser("timo", HOST2); + const [florian, timo] = await Promise.all([ + addUser("florian", HOST1), + addUser("timo", HOST2), + ]); const roomName = "Call Room"; diff --git a/playwright/widget/huddle-call.test.ts b/playwright/widget/huddle-call.test.ts index 7bc94f81..07f12d8a 100644 --- a/playwright/widget/huddle-call.test.ts +++ b/playwright/widget/huddle-call.test.ts @@ -8,7 +8,7 @@ Please see LICENSE in the repository root for full details. import { expect, test } from "@playwright/test"; import { widgetTest } from "../fixtures/widget-user.ts"; -import { HOST1, TestHelpers } from "./test-helpers.ts"; +import { HOST1, HOST2, TestHelpers } from "./test-helpers.ts"; widgetTest("Create and join a group call", async ({ addUser, browserName }) => { // increase the timeouts, it is a long test and it is annoying to retry from the beginning for a single timeout. @@ -19,11 +19,13 @@ widgetTest("Create and join a group call", async ({ addUser, browserName }) => { "The is test is not working on firefox CI environment. No mic/audio device inputs so cam/mic are disabled", ); - const valere = await addUser("Valere", HOST1); - const timo = await addUser("Timo", HOST1); - const robin = await addUser("Robin", HOST1); - const halfshot = await addUser("Halfshot", HOST1); - const florian = await addUser("florian", HOST1); + const [valere, timo, robin, halfshot, florian] = await Promise.all([ + addUser("Valere", HOST1), + addUser("Timo", HOST1), + addUser("Robin", HOST1), + addUser("Halfshot", HOST1), + addUser("florian", HOST1), + ]); const roomName = "Group Call Room"; await TestHelpers.createRoom(roomName, valere.page, [ diff --git a/playwright/widget/screen-share.test.ts b/playwright/widget/screen-share.test.ts index c60ef697..f8437e1f 100644 --- a/playwright/widget/screen-share.test.ts +++ b/playwright/widget/screen-share.test.ts @@ -18,9 +18,11 @@ widgetTest("Sharing screen in group call", async ({ addUser, browserName }) => { test.slow(); // We are registering multiple users here, give it more time - const alice = await addUser("Alice", HOST1); - const bob = await addUser("Bob", HOST1); - const carol = await addUser("Carol", HOST1); + const [alice, bob, carol] = await Promise.all([ + addUser("Alice", HOST1), + addUser("Bob", HOST1), + addUser("Carol", HOST1), + ]); const roomName = "Meeting Room"; await TestHelpers.createRoom(roomName, alice.page, [bob.mxId, carol.mxId]); From 55de3fd216ef84c0501754c6f704a9f8f492063b Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 22 Apr 2026 09:03:57 +0200 Subject: [PATCH 23/52] tests optimizations --- .../widget/hotswap-legacy-compat.test.ts | 12 +-- playwright/widget/huddle-call.test.ts | 93 ++++++++++--------- playwright/widget/test-helpers.ts | 5 +- 3 files changed, 57 insertions(+), 53 deletions(-) diff --git a/playwright/widget/hotswap-legacy-compat.test.ts b/playwright/widget/hotswap-legacy-compat.test.ts index e4695624..f58a562d 100644 --- a/playwright/widget/hotswap-legacy-compat.test.ts +++ b/playwright/widget/hotswap-legacy-compat.test.ts @@ -65,15 +65,13 @@ widgetTest( .contentFrame(); await expect(frame.getByTestId("videoTile")).toHaveCount(2); - // There are no other options than to wait for all media to be ready? - // Or it is too flaky :/ - await user.page.waitForTimeout(3000); - // No one should be waiting for media - await expect(frame.getByText("Waiting for media...")).not.toBeVisible(); + // Wait for "Waiting for media..." to disappear (with timeout) + await expect(frame.getByText("Waiting for media...")).not.toBeVisible({ + timeout: 10000, // Maximum time to wait + }); // There should be 2 video elements, visible and autoplaying - const videoElements = await frame.locator("video").all(); - expect(videoElements.length).toBe(2); + await expect(frame.locator("video")).toHaveCount(2); const blockDisplayCount = await frame .locator("video") diff --git a/playwright/widget/huddle-call.test.ts b/playwright/widget/huddle-call.test.ts index 07f12d8a..262b66db 100644 --- a/playwright/widget/huddle-call.test.ts +++ b/playwright/widget/huddle-call.test.ts @@ -8,7 +8,7 @@ Please see LICENSE in the repository root for full details. import { expect, test } from "@playwright/test"; import { widgetTest } from "../fixtures/widget-user.ts"; -import { HOST1, HOST2, TestHelpers } from "./test-helpers.ts"; +import { HOST1, TestHelpers } from "./test-helpers.ts"; widgetTest("Create and join a group call", async ({ addUser, browserName }) => { // increase the timeouts, it is a long test and it is annoying to retry from the beginning for a single timeout. @@ -50,52 +50,56 @@ widgetTest("Create and join a group call", async ({ addUser, browserName }) => { await TestHelpers.joinCallFromLobby(valere.page); - for (const user of [timo, robin, halfshot, florian]) { - await TestHelpers.joinCallInCurrentRoom(user.page); - } + await Promise.all( + [timo, robin, halfshot, florian].map(async (user) => { + await TestHelpers.joinCallInCurrentRoom(user.page); + }), + ); - for (const user of [timo, robin, halfshot, florian]) { - const frame = user.page - .locator('iframe[title="Element Call"]') - .contentFrame(); - // No lobby, should start with video on - await expect( - frame.getByRole("switch", { name: "Stop video", checked: true }), - ).toBeVisible(); - } + await Promise.all( + [timo, robin, halfshot, florian].map(async (user) => { + const frame = user.page + .locator('iframe[title="Element Call"]') + .contentFrame(); + await expect( + frame.getByRole("switch", { name: "Stop video", checked: true }), + ).toBeVisible(); + }), + ); // We should see 5 video tiles everywhere now - for (const user of [valere, timo, robin, halfshot, florian]) { - const frame = user.page - .locator('iframe[title="Element Call"]') - .contentFrame(); - await expect(frame.getByTestId("videoTile")).toHaveCount(5); - for (const participant of [valere, timo, robin, halfshot, florian]) { - // Check the names are correct - await expect(frame.getByText(participant.displayName)).toBeVisible(); - } + await Promise.all( + [valere, timo, robin, halfshot, florian].map(async (user) => { + const frame = user.page + .locator('iframe[title="Element Call"]') + .contentFrame(); + await expect(frame.getByTestId("videoTile")).toHaveCount(5); + for (const participant of [valere, timo, robin, halfshot, florian]) { + // Check the names are correct + await expect(frame.getByText(participant.displayName)).toBeVisible(); + } - // There is no other options than to wait for all media to be ready? - // Or it is too flaky :/ - await user.page.waitForTimeout(5000); - // No one should be waiting for media - await expect(frame.getByText("Waiting for media...")).not.toBeVisible(); + // No one should be waiting for media + await expect(frame.getByText("Waiting for media...")).not.toBeVisible({ + // Use a bigger timeout here + timeout: 10000, + }); - // There should be 5 video elements, visible and autoplaying - const videoElements = await frame.locator("video").all(); - expect(videoElements.length).toBe(5); - await expect(frame.locator("video[autoplay]")).toHaveCount(5); + // There should be 5 video elements, visible and autoplaying + await expect(frame.locator("video")).toHaveCount(5); + await expect(frame.locator("video[autoplay]")).toHaveCount(5); - const blockDisplayCount = await frame - .locator("video") - .evaluateAll( - (videos: Element[]) => - videos.filter( - (v: Element) => window.getComputedStyle(v).display === "block", - ).length, - ); - expect(blockDisplayCount).toBe(5); - } + const blockDisplayCount = await frame + .locator("video") + .evaluateAll( + (videos: Element[]) => + videos.filter( + (v: Element) => window.getComputedStyle(v).display === "block", + ).length, + ); + expect(blockDisplayCount).toBe(5); + }), + ); // Quickly test muting one participant to see it reflects and that our asserts works const florianFrame = florian.page @@ -111,15 +115,14 @@ widgetTest("Create and join a group call", async ({ addUser, browserName }) => { await expect(florianVideoButton).toHaveAccessibleName("Start video"); await expect(florianVideoButton).not.toBeChecked(); - // wait a bit for the state to propagate - await valere.page.waitForTimeout(3000); { const frame = valere.page .locator('iframe[title="Element Call"]') .contentFrame(); - const videoElements = await frame.locator("video").all(); - expect(videoElements.length).toBe(5); + await expect(frame.locator("video")).toHaveCount(5, { + timeout: 10000, + }); const blockDisplayCount = await frame .locator("video") diff --git a/playwright/widget/test-helpers.ts b/playwright/widget/test-helpers.ts index 2279e20e..e37e4b5b 100644 --- a/playwright/widget/test-helpers.ts +++ b/playwright/widget/test-helpers.ts @@ -105,7 +105,10 @@ export class TestHelpers { await expect( page.getByRole("heading", { name: `Welcome ${username}` }), - ).toBeVisible(); + ).toBeVisible({ + // Increase timeout here + timeout: 10000, + }); await this.maybeDismissBrowserNotSupportedToast(page); await this.maybeDismissServiceWorkerWarningToast(page); From 33172837e7490eb53b86ce86cb740eb9672fbe64 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 07:23:47 +0000 Subject: [PATCH 24/52] Update dependency livekit-client to v2.18.4 --- pnpm-lock.yaml | 134 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 111 insertions(+), 23 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fa9ebebb..2d3e891d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,16 +47,16 @@ importers: version: 11.7.12 '@livekit/components-core': specifier: ^0.12.0 - version: 0.12.13(livekit-client@2.18.3(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1) + version: 0.12.13(livekit-client@2.18.4(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1) '@livekit/components-react': specifier: ^2.0.0 - version: 2.9.20(livekit-client@2.18.3(@types/dom-mediacapture-record@1.0.22))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tslib@2.8.1) + version: 2.9.20(livekit-client@2.18.4(@types/dom-mediacapture-record@1.0.22))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tslib@2.8.1) '@livekit/protocol': specifier: ^1.42.2 version: 1.45.3 '@livekit/track-processors': specifier: ^0.7.1 - version: 0.7.2(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.18.3(@types/dom-mediacapture-record@1.0.22)) + version: 0.7.2(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.18.4(@types/dom-mediacapture-record@1.0.22)) '@mediapipe/tasks-vision': specifier: ^0.10.18 version: 0.10.34 @@ -227,7 +227,7 @@ importers: version: 5.88.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.2)(typescript@5.9.3) livekit-client: specifier: ^2.18.1 - version: 2.18.3(@types/dom-mediacapture-record@1.0.22) + version: 2.18.4(@types/dom-mediacapture-record@1.0.22) lodash-es: specifier: ^4.17.21 version: 4.18.1 @@ -236,7 +236,7 @@ importers: version: 1.9.2 matrix-js-sdk: specifier: matrix-org/matrix-js-sdk#develop - version: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/4b33892d48017b3733a00a48cf5d30182be4a6fb + version: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/fef093747e1c35a60b0fdc687a7166a7d7eb8a28 matrix-widget-api: specifier: ^1.16.1 version: 1.17.0 @@ -2981,6 +2981,12 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/project-service@8.59.0': + resolution: {integrity: sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/scope-manager@5.62.0': resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2989,12 +2995,22 @@ packages: resolution: {integrity: sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/scope-manager@8.59.0': + resolution: {integrity: sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/tsconfig-utils@8.58.2': resolution: {integrity: sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/tsconfig-utils@8.59.0': + resolution: {integrity: sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/type-utils@8.58.2': resolution: {integrity: sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3010,6 +3026,10 @@ packages: resolution: {integrity: sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.59.0': + resolution: {integrity: sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@5.62.0': resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3025,6 +3045,12 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/typescript-estree@8.59.0': + resolution: {integrity: sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/utils@5.62.0': resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3038,6 +3064,13 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/utils@8.59.0': + resolution: {integrity: sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/visitor-keys@5.62.0': resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3046,6 +3079,10 @@ packages: resolution: {integrity: sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/visitor-keys@8.59.0': + resolution: {integrity: sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} @@ -4936,8 +4973,8 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - livekit-client@2.18.3: - resolution: {integrity: sha512-A8QDaVPo+Ye35bJFyKe6PjMOtY33dmdRXGKP/3+BG48ynEES3YwFzHbsPHJiScgI4OZouNef3Ew/BPazXKwo8Q==} + livekit-client@2.18.4: + resolution: {integrity: sha512-kjbH9WdA85gZNqFiAMY9jKJ3HArk8F+AQvAvSE5N6PnY+wNCV9OYe9yeb9BVezZiD6ltGyB3CkTxGyuX6BXYAw==} peerDependencies: '@types/dom-mediacapture-record': ^1 @@ -5011,8 +5048,8 @@ packages: matrix-events-sdk@0.0.1: resolution: {integrity: sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==} - matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/4b33892d48017b3733a00a48cf5d30182be4a6fb: - resolution: {tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/4b33892d48017b3733a00a48cf5d30182be4a6fb} + matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/fef093747e1c35a60b0fdc687a7166a7d7eb8a28: + resolution: {tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/fef093747e1c35a60b0fdc687a7166a7d7eb8a28} version: 41.3.0 engines: {node: '>=22.0.0'} @@ -6611,8 +6648,8 @@ packages: webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} - webrtc-adapter@9.0.4: - resolution: {integrity: sha512-5ZZY1+lGq8LEKuDlg9M2RPJHlH3R7OVwyHqMcUsLKCgd9Wvf+QrFTCItkXXYPmrJn8H6gRLXbSgxLLdexiqHxw==} + webrtc-adapter@9.0.5: + resolution: {integrity: sha512-U9vjByy/sK2OMXu5mmfuZFKTMIUQe34c0JXRO+oDrxJTsntdYT2iIFwYMOV7HhMTuktcZLGf2W1N/OcSf9ssWg==} engines: {node: '>=6.0.0', npm: '>=3.10.0'} whatwg-encoding@3.1.1: @@ -8115,21 +8152,21 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@livekit/components-core@0.12.13(livekit-client@2.18.3(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1)': + '@livekit/components-core@0.12.13(livekit-client@2.18.4(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1)': dependencies: '@floating-ui/dom': 1.7.4 - livekit-client: 2.18.3(@types/dom-mediacapture-record@1.0.22) + livekit-client: 2.18.4(@types/dom-mediacapture-record@1.0.22) loglevel: 1.9.1 rxjs: 7.8.2 tslib: 2.8.1 - '@livekit/components-react@2.9.20(livekit-client@2.18.3(@types/dom-mediacapture-record@1.0.22))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tslib@2.8.1)': + '@livekit/components-react@2.9.20(livekit-client@2.18.4(@types/dom-mediacapture-record@1.0.22))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tslib@2.8.1)': dependencies: - '@livekit/components-core': 0.12.13(livekit-client@2.18.3(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1) + '@livekit/components-core': 0.12.13(livekit-client@2.18.4(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1) clsx: 2.1.1 events: 3.3.0 jose: 6.2.2 - livekit-client: 2.18.3(@types/dom-mediacapture-record@1.0.22) + livekit-client: 2.18.4(@types/dom-mediacapture-record@1.0.22) react: 19.2.5 react-dom: 19.2.5(react@19.2.5) tslib: 2.8.1 @@ -8141,11 +8178,11 @@ snapshots: dependencies: '@bufbuild/protobuf': 1.10.1 - '@livekit/track-processors@0.7.2(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.18.3(@types/dom-mediacapture-record@1.0.22))': + '@livekit/track-processors@0.7.2(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.18.4(@types/dom-mediacapture-record@1.0.22))': dependencies: '@mediapipe/tasks-vision': 0.10.34 '@types/dom-mediacapture-transform': 0.1.11 - livekit-client: 2.18.3(@types/dom-mediacapture-record@1.0.22) + livekit-client: 2.18.4(@types/dom-mediacapture-record@1.0.22) '@matrix-org/matrix-sdk-crypto-wasm@18.1.0': {} @@ -9399,6 +9436,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/project-service@8.59.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.59.0(typescript@5.9.3) + '@typescript-eslint/types': 8.59.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/scope-manager@5.62.0': dependencies: '@typescript-eslint/types': 5.62.0 @@ -9409,10 +9455,19 @@ snapshots: '@typescript-eslint/types': 8.58.2 '@typescript-eslint/visitor-keys': 8.58.2 + '@typescript-eslint/scope-manager@8.59.0': + dependencies: + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/visitor-keys': 8.59.0 + '@typescript-eslint/tsconfig-utils@8.58.2(typescript@5.9.3)': dependencies: typescript: 5.9.3 + '@typescript-eslint/tsconfig-utils@8.59.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + '@typescript-eslint/type-utils@8.58.2(eslint@8.57.1)(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.58.2 @@ -9429,6 +9484,8 @@ snapshots: '@typescript-eslint/types@8.58.2': {} + '@typescript-eslint/types@8.59.0': {} + '@typescript-eslint/typescript-estree@5.62.0(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 5.62.0 @@ -9458,6 +9515,21 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/typescript-estree@8.59.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.59.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.59.0(typescript@5.9.3) + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/visitor-keys': 8.59.0 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.7.4 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/utils@5.62.0(eslint@8.57.1)(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) @@ -9484,6 +9556,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/utils@8.59.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + '@typescript-eslint/scope-manager': 8.59.0 + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/typescript-estree': 8.59.0(typescript@5.9.3) + eslint: 8.57.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/visitor-keys@5.62.0': dependencies: '@typescript-eslint/types': 5.62.0 @@ -9494,6 +9577,11 @@ snapshots: '@typescript-eslint/types': 8.58.2 eslint-visitor-keys: 5.0.1 + '@typescript-eslint/visitor-keys@8.59.0': + dependencies: + '@typescript-eslint/types': 8.59.0 + eslint-visitor-keys: 5.0.1 + '@ungap/structured-clone@1.3.0': {} '@use-gesture/core@10.3.1': {} @@ -10652,7 +10740,7 @@ snapshots: eslint-plugin-jest@29.15.2(@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3): dependencies: - '@typescript-eslint/utils': 8.58.2(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.0(eslint@8.57.1)(typescript@5.9.3) eslint: 8.57.1 optionalDependencies: '@typescript-eslint/eslint-plugin': 8.58.2(@typescript-eslint/parser@8.58.2(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) @@ -11710,7 +11798,7 @@ snapshots: lines-and-columns@1.2.4: {} - livekit-client@2.18.3(@types/dom-mediacapture-record@1.0.22): + livekit-client@2.18.4(@types/dom-mediacapture-record@1.0.22): dependencies: '@livekit/mutex': 1.1.1 '@livekit/protocol': 1.45.3 @@ -11721,7 +11809,7 @@ snapshots: sdp-transform: 2.15.0 tslib: 2.8.1 typed-emitter: 2.1.0 - webrtc-adapter: 9.0.4 + webrtc-adapter: 9.0.5 locate-path@5.0.0: dependencies: @@ -11786,7 +11874,7 @@ snapshots: matrix-events-sdk@0.0.1: {} - matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/4b33892d48017b3733a00a48cf5d30182be4a6fb: + matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/fef093747e1c35a60b0fdc687a7166a7d7eb8a28: dependencies: '@babel/runtime': 7.29.2 '@matrix-org/matrix-sdk-crypto-wasm': 18.1.0 @@ -13643,7 +13731,7 @@ snapshots: webpack-virtual-modules@0.6.2: {} - webrtc-adapter@9.0.4: + webrtc-adapter@9.0.5: dependencies: sdp: 3.2.2 From 2b46925e77a2b9685953d8a8e5a448e7e4ee6d5b Mon Sep 17 00:00:00 2001 From: Timo K Date: Wed, 22 Apr 2026 11:51:33 +0200 Subject: [PATCH 25/52] Call window.controls.onBackButtonPressed if esc is pressed without any focus. --- src/useCallViewKeyboardShortcuts.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/useCallViewKeyboardShortcuts.ts b/src/useCallViewKeyboardShortcuts.ts index 728a2614..3d9654be 100644 --- a/src/useCallViewKeyboardShortcuts.ts +++ b/src/useCallViewKeyboardShortcuts.ts @@ -68,6 +68,8 @@ export function useCallViewKeyboardShortcuts( } else if (KeyToReactionMap[event.key]) { event.preventDefault(); sendReaction(KeyToReactionMap[event.key]); + } else if (event.key === "Escape") { + window.controls.onBackButtonPressed?.(); } }, [ From 56bab70534d52c368c5d2b05e2c61cc5eb34e5aa Mon Sep 17 00:00:00 2001 From: Timo K Date: Wed, 22 Apr 2026 12:26:03 +0200 Subject: [PATCH 26/52] add tests --- src/useCallViewKeyboardShortcuts.test.tsx | 72 ++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/src/useCallViewKeyboardShortcuts.test.tsx b/src/useCallViewKeyboardShortcuts.test.tsx index e22380d1..5a327f83 100644 --- a/src/useCallViewKeyboardShortcuts.test.tsx +++ b/src/useCallViewKeyboardShortcuts.test.tsx @@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details. */ import { render } from "@testing-library/react"; -import { type FC, useRef } from "react"; +import { type FC, useRef, useState } from "react"; import { expect, test, vi } from "vitest"; import { Button } from "@vector-im/compound-web"; import userEvent from "@testing-library/user-event"; @@ -17,6 +17,7 @@ import { ReactionSet, ReactionsRowSize, } from "./reactions"; +import { type Controls } from "./controls"; // Test Explanation: // - The main objective is to test `useCallViewKeyboardShortcuts`. @@ -27,6 +28,7 @@ interface TestComponentProps { onButtonClick?: () => void; sendReaction?: () => void; toggleHandRaised?: () => void; + initialModalOpen?: boolean; } const TestComponent: FC = ({ @@ -34,7 +36,9 @@ const TestComponent: FC = ({ onButtonClick = (): void => {}, sendReaction = (reaction: ReactionOption): void => {}, toggleHandRaised = (): void => {}, + initialModalOpen = false, }) => { + const [modalOpen, setModalOpen] = useState(initialModalOpen); const ref = useRef(null); useCallViewKeyboardShortcuts( ref, @@ -47,6 +51,19 @@ const TestComponent: FC = ({ return (

+ {modalOpen && ( + { + if (e.key === "Escape") { + e.preventDefault(); + setModalOpen(false); + } + }} + > + + + )}
); }; @@ -118,6 +135,27 @@ test("raised hand can be sent via keyboard presses", async () => { expect(toggleHandRaised).toHaveBeenCalledOnce(); }); +test("raised hand cannot be sent via keyboard presses if modal open and focussed", async () => { + const user = userEvent.setup(); + const toggleHandRaised = vi.fn(); + const { getByRole } = render( + , + ); + getByRole("button", { name: "InModalButton" }).focus(); + await user.keyboard("h"); + + expect(toggleHandRaised).not.toHaveBeenCalledOnce(); + + // once we press esc... + await user.keyboard("[Escape]"); + // we can toggle the hand raise... + await user.keyboard("h"); + expect(toggleHandRaised).toHaveBeenCalledOnce(); +}); + test("unmuting happens in place of the default action", async () => { const user = userEvent.setup(); const defaultPrevented = vi.fn(); @@ -138,3 +176,35 @@ test("unmuting happens in place of the default action", async () => { await user.keyboard("[Space]"); expect(defaultPrevented).toBeCalledWith(true); }); + +test("escape button triggers the controls back action", async () => { + const user = userEvent.setup(); + + window.controls = { onBackButtonPressed: vi.fn() } as unknown as Controls; + // In the real application, we mostly just want the spacebar shortcut to avoid + // scrolling the page. But to test that here in JSDOM, we need some kind of + // container element that can be interactive and receive focus / keydown + // events.