diff --git a/locales/en/app.json b/locales/en/app.json index 0b1fb776..1d5eaa19 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -145,6 +145,7 @@ }, "layout_grid_label": "Grid", "layout_spotlight_label": "Spotlight", + "layout_switch_label": "Layout", "lobby": { "ask_to_join": "Request to join call", "join_as_guest": "Join as guest", diff --git a/playwright/widget/test-helpers.ts b/playwright/widget/test-helpers.ts index f86c9654..512399ca 100644 --- a/playwright/widget/test-helpers.ts +++ b/playwright/widget/test-helpers.ts @@ -142,9 +142,7 @@ export class TestHelpers { timeout: 15000, }); - await this.maybeDismissBrowserNotSupportedToast(page); - await this.maybeDismissServiceWorkerWarningToast(page); - await this.maybeDismissBackupChat(page); + await this.dismissStartupToasts(page); await TestHelpers.setDevToolElementCallDevUrl(page); @@ -155,73 +153,39 @@ export class TestHelpers { return { page, clientHandle, mxId }; } - private static async maybeDismissBrowserNotSupportedToast( - page: Page, - ): Promise { - const browserUnsupportedToast = page - .getByText("Element does not support this browser") - .locator("..") - .locator(".."); + // Dismisses any toasts that appear on startup, such as "Failed to load service worker" or "Back up your chats". + // Toast can be stacked, and only the top one can be dismiss, so just look at what is on top and + // dismiss (if part of expected toats) + public static async dismissStartupToasts(page: Page): Promise { + const expectedToasts = [ + { title: "Failed to load service worker", button: "OK" }, + { title: "Back up your chats", button: "Dismiss" }, + { title: "Element does not support this browser", button: "Dismiss" }, + ]; - // Dismiss incompatible browser toast - const dismissButton = browserUnsupportedToast.getByRole("button", { - name: "Dismiss", - }); - try { - await expect(dismissButton).toBeVisible({ timeout: 700 }); - await dismissButton.click(); - } catch { - // dismissButton not visible, continue as normal - } - } + const toast = page.locator(".mx_Toast_toast"); - private static async maybeDismissServiceWorkerWarningToast( - page: Page, - ): Promise { - const toast = page - .locator(".mx_Toast_toast") - .getByText("Failed to load service worker"); + // eslint-disable-next-line no-constant-condition + while (true) { + try { + await toast.waitFor({ state: "visible", timeout: 700 }); + const title = await toast.locator(".mx_Toast_title h2").textContent(); - try { - await expect(toast).toBeVisible({ timeout: 700 }); - await page - .locator(".mx_Toast_toast") - .getByRole("button", { name: "OK" }) - .click(); - } catch { - // toast not visible, continue as normal - } - } + // Find the matching toast config + const toastConfig = expectedToasts.find((t) => + title?.includes(t.title), + ); - private static async maybeDismissBackupChat(page: Page): Promise { - const toast = page - .locator(".mx_Toast_toast") - .getByText("Back up your chats"); - - try { - await expect(toast).toBeVisible({ timeout: 700 }); - await page - .locator(".mx_Toast_toast") - .getByRole("button", { name: "Dismiss" }) - .click(); - } catch { - // toast not visible, continue as normal - } - } - - public static async maybeDismissKeyBackupToast(page: Page): Promise { - const toast = page - .locator(".mx_Toast_toast") - .getByText("Back up your chats"); - - try { - await expect(toast).toBeVisible({ timeout: 700 }); - await page - .locator(".mx_Toast_toast") - .getByRole("button", { name: "Dismiss" }) - .click(); - } catch { - // toast not visible, continue as normal + if (toastConfig) { + await toast.getByRole("button", { name: toastConfig.button }).click(); + } else { + // Unknown toast. We don't want to act on unknown toasts + break; + } + } catch { + // No toast visible, exit loop + break; + } } } @@ -244,7 +208,7 @@ export class TestHelpers { timeout: 10000, }); await expect(page.getByText("Encryption enabled")).toBeVisible(); - await TestHelpers.maybeDismissKeyBackupToast(page); + await TestHelpers.dismissStartupToasts(page); // Invite users if any if (andInvite.length > 0) { @@ -283,7 +247,7 @@ export class TestHelpers { await expect( page.getByRole("main").getByRole("heading", { name: roomName }), ).toBeVisible(); - await TestHelpers.maybeDismissKeyBackupToast(page); + await TestHelpers.dismissStartupToasts(page); } /** diff --git a/playwright/widget/voice-call-dm.spec.ts b/playwright/widget/voice-call-dm.spec.ts index d540e45e..acbad422 100644 --- a/playwright/widget/voice-call-dm.spec.ts +++ b/playwright/widget/voice-call-dm.spec.ts @@ -217,7 +217,7 @@ widgetTest( ).toBeVisible(); await expect(whistler.page.getByText("Incoming video call")).toBeVisible(); - await whistler.page.getByRole("button", { name: "Ignore" }).click(); + await whistler.page.getByRole("button", { name: "Decline" }).click(); await expect( whistler.page.locator('iframe[title="Element Call"]'), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7ea54394..eda33a67 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.8(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1) + version: 0.12.13(livekit-client@2.18.9(@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.8(@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.21(livekit-client@2.18.9(@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.6 '@livekit/track-processors': specifier: ^0.7.1 - version: 0.7.2(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.18.8(@types/dom-mediacapture-record@1.0.22)) + version: 0.7.2(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.18.9(@types/dom-mediacapture-record@1.0.22)) '@mediapipe/tasks-vision': specifier: ^0.10.18 version: 0.10.34 @@ -236,7 +236,7 @@ importers: version: 5.88.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.12.2)(typescript@5.9.3) livekit-client: specifier: ^2.18.1 - version: 2.18.8(@types/dom-mediacapture-record@1.0.22) + version: 2.18.9(@types/dom-mediacapture-record@1.0.22) lodash-es: specifier: ^4.17.21 version: 4.18.1 @@ -245,7 +245,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/69985ee350a33ba75f1ad11f96468344f0c92a8d + version: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/349e8c5023b74b7ee17b2e9a0cba6dfce6818d68 matrix-widget-api: specifier: ^1.16.1 version: 1.17.0 @@ -1571,12 +1571,12 @@ packages: livekit-client: ^2.17.2 tslib: ^2.6.2 - '@livekit/components-react@2.9.20': - resolution: {integrity: sha512-hjkYOsJj9Jbghb7wM5cI8HoVisKeL6Zcy1VnRWTLm0sqVbto8GJp/17T4Udx85mCPY6Jgh8I1Cv0yVzgz7CQtg==} + '@livekit/components-react@2.9.21': + resolution: {integrity: sha512-6hU9VucJJL+gAhilNGe4MBCDCZVk64qyjP9Ck86krvOIdVU76WeWksddg1MYUP10AlUwwrfD7davz41pJTcMJw==} engines: {node: '>=18'} peerDependencies: '@livekit/krisp-noise-filter': ^0.2.12 || ^0.3.0 - livekit-client: ^2.17.2 + livekit-client: ^2.18.2 react: '>=18' react-dom: '>=18' tslib: ^2.6.2 @@ -3031,8 +3031,8 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.59.1': - resolution: {integrity: sha512-+MuHQlHiEr00Of/IQbE/MmEoi44znZHbR/Pz7Opq4HryUOlRi+/44dro9Ycy8Fyo+/024IWtw8m4JUMCGTYxDg==} + '@typescript-eslint/project-service@8.59.2': + resolution: {integrity: sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' @@ -3049,8 +3049,8 @@ packages: resolution: {integrity: sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/scope-manager@8.59.1': - resolution: {integrity: sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==} + '@typescript-eslint/scope-manager@8.59.2': + resolution: {integrity: sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript-eslint/tsconfig-utils@8.58.2': @@ -3065,8 +3065,8 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/tsconfig-utils@8.59.1': - resolution: {integrity: sha512-/0nEyPbX7gRsk0Uwfe4ALwwgxuA66d/l2mhRDNlAvaj4U3juhUtJNq0DsY8M2AYwwb9rEq2hrC3IcIcEt++iJA==} + '@typescript-eslint/tsconfig-utils@8.59.2': + resolution: {integrity: sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' @@ -3094,6 +3094,10 @@ packages: resolution: {integrity: sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.59.2': + resolution: {integrity: sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==} + 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} @@ -3115,8 +3119,8 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/typescript-estree@8.59.1': - resolution: {integrity: sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==} + '@typescript-eslint/typescript-estree@8.59.2': + resolution: {integrity: sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' @@ -3141,8 +3145,8 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.59.1': - resolution: {integrity: sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA==} + '@typescript-eslint/utils@8.59.2': + resolution: {integrity: sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -3160,12 +3164,13 @@ packages: resolution: {integrity: sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/visitor-keys@8.59.1': - resolution: {integrity: sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==} + '@typescript-eslint/visitor-keys@8.59.2': + resolution: {integrity: sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@use-gesture/core@10.3.1': resolution: {integrity: sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==} @@ -4892,9 +4897,6 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true - jose@6.2.2: - resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==} - jose@6.2.3: resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} @@ -5076,8 +5078,8 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - livekit-client@2.18.8: - resolution: {integrity: sha512-E+bSpnBVng/1xG4RfL1Q51dHUpBwL14Wix4sR5bS0djEzKMEtrxcUyhWLltdwQ0USf1t0PaxW6WL4oVb2s4Fsw==} + livekit-client@2.18.9: + resolution: {integrity: sha512-l0cADcxxBCWCBMtU9eWY6RpdbRfgA5c1/05yngQXo08mcy3VOttmSE2pNZ74k2B2zQym149g5/Y1B3vq2FWwlw==} peerDependencies: '@types/dom-mediacapture-record': ^1 @@ -5151,8 +5153,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/69985ee350a33ba75f1ad11f96468344f0c92a8d: - resolution: {tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/69985ee350a33ba75f1ad11f96468344f0c92a8d} + matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/349e8c5023b74b7ee17b2e9a0cba6dfce6818d68: + resolution: {tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/349e8c5023b74b7ee17b2e9a0cba6dfce6818d68} version: 41.4.0 engines: {node: '>=22.0.0'} @@ -6058,6 +6060,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.8.0: + resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} + engines: {node: '>=10'} + hasBin: true + set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} @@ -8280,21 +8287,21 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@livekit/components-core@0.12.13(livekit-client@2.18.8(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1)': + '@livekit/components-core@0.12.13(livekit-client@2.18.9(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1)': dependencies: '@floating-ui/dom': 1.7.4 - livekit-client: 2.18.8(@types/dom-mediacapture-record@1.0.22) + livekit-client: 2.18.9(@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.8(@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.21(livekit-client@2.18.9(@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.8(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1) + '@livekit/components-core': 0.12.13(livekit-client@2.18.9(@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.8(@types/dom-mediacapture-record@1.0.22) + jose: 6.2.3 + livekit-client: 2.18.9(@types/dom-mediacapture-record@1.0.22) react: 19.2.5 react-dom: 19.2.5(react@19.2.5) tslib: 2.8.1 @@ -8310,11 +8317,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.8(@types/dom-mediacapture-record@1.0.22))': + '@livekit/track-processors@0.7.2(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.18.9(@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.8(@types/dom-mediacapture-record@1.0.22) + livekit-client: 2.18.9(@types/dom-mediacapture-record@1.0.22) '@matrix-org/matrix-sdk-crypto-wasm@18.2.0': {} @@ -9593,10 +9600,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.59.1(typescript@5.9.3)': + '@typescript-eslint/project-service@8.59.2(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.59.1(typescript@5.9.3) - '@typescript-eslint/types': 8.59.1 + '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@5.9.3) + '@typescript-eslint/types': 8.59.2 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: @@ -9617,10 +9624,10 @@ snapshots: '@typescript-eslint/types': 8.59.0 '@typescript-eslint/visitor-keys': 8.59.0 - '@typescript-eslint/scope-manager@8.59.1': + '@typescript-eslint/scope-manager@8.59.2': dependencies: - '@typescript-eslint/types': 8.59.1 - '@typescript-eslint/visitor-keys': 8.59.1 + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/visitor-keys': 8.59.2 '@typescript-eslint/tsconfig-utils@8.58.2(typescript@5.9.3)': dependencies: @@ -9630,7 +9637,7 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/tsconfig-utils@8.59.1(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.59.2(typescript@5.9.3)': dependencies: typescript: 5.9.3 @@ -9654,6 +9661,8 @@ snapshots: '@typescript-eslint/types@8.59.1': {} + '@typescript-eslint/types@8.59.2': {} + '@typescript-eslint/typescript-estree@5.62.0(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 5.62.0 @@ -9698,15 +9707,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.59.1(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.59.2(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.59.1(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.59.1(typescript@5.9.3) - '@typescript-eslint/types': 8.59.1 - '@typescript-eslint/visitor-keys': 8.59.1 + '@typescript-eslint/project-service': 8.59.2(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@5.9.3) + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/visitor-keys': 8.59.2 debug: 4.4.3 minimatch: 10.2.5 - semver: 7.7.4 + semver: 7.8.0 tinyglobby: 0.2.16 ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 @@ -9750,12 +9759,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.59.1(eslint@8.57.1)(typescript@5.9.3)': + '@typescript-eslint/utils@8.59.2(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.1 - '@typescript-eslint/types': 8.59.1 - '@typescript-eslint/typescript-estree': 8.59.1(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.59.2 + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3) eslint: 8.57.1 typescript: 5.9.3 transitivePeerDependencies: @@ -9776,9 +9785,9 @@ snapshots: '@typescript-eslint/types': 8.59.0 eslint-visitor-keys: 5.0.1 - '@typescript-eslint/visitor-keys@8.59.1': + '@typescript-eslint/visitor-keys@8.59.2': dependencies: - '@typescript-eslint/types': 8.59.1 + '@typescript-eslint/types': 8.59.2 eslint-visitor-keys: 5.0.1 '@ungap/structured-clone@1.3.0': {} @@ -10982,7 +10991,7 @@ snapshots: eslint-plugin-jest@29.15.2(@typescript-eslint/eslint-plugin@8.59.0(@typescript-eslint/parser@8.59.0(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.59.1(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.2(eslint@8.57.1)(typescript@5.9.3) eslint: 8.57.1 optionalDependencies: '@typescript-eslint/eslint-plugin': 8.59.0(@typescript-eslint/parser@8.59.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) @@ -11873,8 +11882,6 @@ snapshots: jiti@2.6.1: {} - jose@6.2.2: {} - jose@6.2.3: {} js-tokens@10.0.0: {} @@ -12044,7 +12051,7 @@ snapshots: lines-and-columns@1.2.4: {} - livekit-client@2.18.8(@types/dom-mediacapture-record@1.0.22): + livekit-client@2.18.9(@types/dom-mediacapture-record@1.0.22): dependencies: '@livekit/mutex': 1.1.1 '@livekit/protocol': 1.45.3 @@ -12120,7 +12127,7 @@ snapshots: matrix-events-sdk@0.0.1: {} - matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/69985ee350a33ba75f1ad11f96468344f0c92a8d: + matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/349e8c5023b74b7ee17b2e9a0cba6dfce6818d68: dependencies: '@babel/runtime': 7.29.2 '@matrix-org/matrix-sdk-crypto-wasm': 18.2.0 @@ -13201,6 +13208,8 @@ snapshots: semver@7.7.4: {} + semver@7.8.0: {} + set-blocking@2.0.0: {} set-cookie-parser@2.7.2: {} diff --git a/src/components/CallFooter.tsx b/src/components/CallFooter.tsx index 6be68217..85a0ffa9 100644 --- a/src/components/CallFooter.tsx +++ b/src/components/CallFooter.tsx @@ -7,8 +7,13 @@ Please see LICENSE in the repository root for full details. import { type FC, type JSX, type Ref, useMemo } from "react"; import classNames from "classnames"; +import { + SpotlightIcon, + GridIcon, +} from "@vector-im/compound-design-tokens/assets/web/icons"; import { combineLatest, map } from "rxjs"; import { supportsBackgroundProcessors } from "@livekit/track-processors"; +import { Switch } from "@vector-im/compound-web"; import LogoMark from "../icons/LogoMark.svg?react"; import LogoType from "../icons/LogoType.svg?react"; @@ -24,7 +29,6 @@ import { type ReactionData, } from "../button"; import styles from "./CallFooter.module.css"; -import { LayoutToggle } from "../room/LayoutToggle"; import { type CallViewModel, type GridMode, @@ -45,6 +49,7 @@ import type { ObservableScope } from "../state/ObservableScope"; import { type MuteStates } from "../state/MuteStates"; import { type ViewModel, useViewModel } from "../state/ViewModel"; import { getUrlParams, HeaderStyle } from "../UrlParams"; +import { t } from "i18next"; export interface AudioOutputSwitcher { targetOutput: string; @@ -554,10 +559,18 @@ export const CallFooter: FC = ({ ref, children, vm }) => { {!hideControls &&
{buttons}
} {setLayoutMode && layoutMode && ( - + name="layoutMode" + aria-label={t("layout_switch_label")} + leftLabel={t("layout_spotlight_label")} + leftValue="spotlight" + leftIcon={SpotlightIcon} + rightLabel={t("layout_grid_label")} + rightValue="grid" + rightIcon={GridIcon} className={styles.layout} - layout={layoutMode} - setLayout={setLayoutMode} + value={layoutMode} + onChange={setLayoutMode} /> )} diff --git a/src/config/ConfigOptions.ts b/src/config/ConfigOptions.ts index 93403a96..165a14f0 100644 --- a/src/config/ConfigOptions.ts +++ b/src/config/ConfigOptions.ts @@ -164,6 +164,14 @@ export interface ResolvedConfigOptions extends ConfigOptions { }; sync_disconnect_grace_period_ms: number; ssla: string; + matrix_rtc_session: { + wait_for_key_rotation_ms?: number; + delayed_leave_event_delay_ms: number; + delayed_leave_event_restart_local_timeout_ms?: number; + delayed_leave_event_restart_ms?: number; + network_error_retry_ms: number; + membership_event_expiry_ms?: number; + }; } export const DEFAULT_CONFIG: ResolvedConfigOptions = { @@ -178,4 +186,8 @@ export const DEFAULT_CONFIG: ResolvedConfigOptions = { }, sync_disconnect_grace_period_ms: 10000, ssla: "https://static.element.io/legal/element-software-and-services-license-agreement-uk-1.pdf", + matrix_rtc_session: { + delayed_leave_event_delay_ms: 10000, + network_error_retry_ms: 1000, + }, }; diff --git a/src/livekit/openIDSFU.test.ts b/src/livekit/openIDSFU.test.ts index fc0b6d54..d269569f 100644 --- a/src/livekit/openIDSFU.test.ts +++ b/src/livekit/openIDSFU.test.ts @@ -84,6 +84,99 @@ describe("getSFUConfigWithOpenID", () => { expect.fail("Expected test to throw;"); }); + it("should retry without delay params if the JWT service legacy endpoint returns M_BAD_JSON 400", async () => { + let callCount = 0; + + fetchMock.post( + "https://sfu.example.org/sfu/get", + (url, opts) => { + callCount++; + const body = JSON.parse(opts.body as string); + + // First call: check if it has delay parts and return 400 + if (callCount === 1) { + expect(body).toHaveProperty("delay_id", "mock_delay_id"); + return { + status: 400, + body: { errcode: "M_BAD_JSON", error: "Unsupported parameters" }, + }; + } + + // Second call: check if delay parts were stripped and return success + expect(body).not.toHaveProperty("delay_id"); + expect(body).not.toHaveProperty("delay_timeout"); + expect(body).not.toHaveProperty("delay_cs_api_url"); + + return { + status: 200, + body: { url: sfuUrl, jwt: testJWTToken }, + }; + }, + { overwriteRoutes: true }, + ); + + // Note: Assuming getSFUConfigWithOpenID eventually calls getLiveKitJWT + const config = await getSFUConfigWithOpenID( + matrixClient, + ownMemberMock, + "https://sfu.example.org", + "!example_room_id", + { + delayEndpointBaseUrl: "https://matrix.homeserverserver.org", + delayId: "mock_delay_id", + }, + ); + + expect(config.jwt).toBe(testJWTToken); + expect(callCount).toBe(2); + void (await fetchMock.flush()); + }); + + it("should successfully send delay parameters to the JWT service legacy endpoint", async () => { + fetchMock.post( + "https://sfu.example.org/sfu/get", + (url, opts) => { + const body = JSON.parse(opts.body as string); + + // Verify, that the request contains the expected delay parameters + if ( + body.delay_id === "mock_delay_id" && + body.delay_timeout === 10000 && + body.delay_cs_api_url === "https://homeserverserver.org/cs_api" + ) { + return { + status: 200, + body: { url: sfuUrl, jwt: testJWTToken }, + }; + } + return { + status: 400, + body: { error: "Missing expected delay params" }, + }; + }, + { overwriteRoutes: true }, + ); + + const config = await getSFUConfigWithOpenID( + matrixClient, + ownMemberMock, + "https://sfu.example.org", + "!example_room_id", + { + delayEndpointBaseUrl: "https://homeserverserver.org/cs_api", + delayId: "mock_delay_id", + }, + ); + + // Prüfe das Ergebnis + expect(config).toMatchObject({ + jwt: testJWTToken, + url: sfuUrl, + }); + + void (await fetchMock.flush()); + }); + it("should try legacy and then new endpoint with delay delegation", async () => { fetchMock.post("https://sfu.example.org/get_token", () => { return { @@ -121,7 +214,7 @@ describe("getSFUConfigWithOpenID", () => { expect(calls[0][0]).toStrictEqual("https://sfu.example.org/get_token"); expect(calls[0][1]).toStrictEqual({ // check if it uses correct delayID! - body: '{"room_id":"!example_room_id","slot_id":"m.call#ROOM","member":{"id":"@alice:example.org:DEVICE","claimed_user_id":"@alice:example.org","claimed_device_id":"DEVICE"},"delay_id":"mock_delay_id","delay_timeout":1000,"delay_cs_api_url":"https://matrix.homeserverserver.org"}', + body: '{"room_id":"!example_room_id","slot_id":"m.call#ROOM","member":{"id":"@alice:example.org:DEVICE","claimed_user_id":"@alice:example.org","claimed_device_id":"DEVICE"},"delay_id":"mock_delay_id","delay_timeout":10000,"delay_cs_api_url":"https://matrix.homeserverserver.org"}', method: "POST", headers: { "Content-Type": "application/json", @@ -131,7 +224,7 @@ describe("getSFUConfigWithOpenID", () => { expect(calls[1][0]).toStrictEqual("https://sfu.example.org/sfu/get"); expect(calls[1][1]).toStrictEqual({ - body: '{"room":"!example_room_id","device_id":"DEVICE"}', + body: '{"room":"!example_room_id","device_id":"DEVICE","delay_id":"mock_delay_id","delay_timeout":10000,"delay_cs_api_url":"https://matrix.homeserverserver.org"}', headers: { "Content-Type": "application/json", }, @@ -176,7 +269,7 @@ describe("getSFUConfigWithOpenID", () => { expect(calls[0][0]).toStrictEqual("https://sfu.example.org/get_token"); expect(calls[0][1]).toStrictEqual({ // check if it uses correct delayID! - body: '{"room_id":"!example_room_id","slot_id":"m.call#ROOM","member":{"id":"@alice:example.org:DEVICE","claimed_user_id":"@alice:example.org","claimed_device_id":"DEVICE"},"delay_id":"mock_delay_id","delay_timeout":1000,"delay_cs_api_url":"https://matrix.homeserverserver.org"}', + body: '{"room_id":"!example_room_id","slot_id":"m.call#ROOM","member":{"id":"@alice:example.org:DEVICE","claimed_user_id":"@alice:example.org","claimed_device_id":"DEVICE"},"delay_id":"mock_delay_id","delay_timeout":10000,"delay_cs_api_url":"https://matrix.homeserverserver.org"}', method: "POST", headers: { "Content-Type": "application/json", diff --git a/src/livekit/openIDSFU.ts b/src/livekit/openIDSFU.ts index dfe04323..2d6c45b6 100644 --- a/src/livekit/openIDSFU.ts +++ b/src/livekit/openIDSFU.ts @@ -155,6 +155,8 @@ export async function getSFUConfigWithOpenID( serviceUrl, roomId, openIdToken, + opts?.delayEndpointBaseUrl, + opts?.delayId, ); logger?.info(`Got JWT from call's active focus URL.`); return extractFullConfigFromToken(sfuConfig); @@ -187,20 +189,62 @@ async function getLiveKitJWT( livekitServiceURL: string, matrixRoomId: string, openIDToken: IOpenIDToken, + delayEndpointBaseUrl?: string, + delayId?: string, ): Promise<{ url: string; jwt: string }> { - const res = await doNetworkOperationWithRetry(async () => { + interface IDelayParams { + delay_id?: string; + delay_timeout?: number; + delay_cs_api_url?: string; + } + let bodyDalayParts: IDelayParams = {}; + // Also check for empty string + if (delayId && delayEndpointBaseUrl) { + const delayTimeoutMs = + Config.get().matrix_rtc_session?.delayed_leave_event_delay_ms; + bodyDalayParts = { + delay_id: delayId, + delay_timeout: delayTimeoutMs, + delay_cs_api_url: delayEndpointBaseUrl, + }; + } + + const makeRequest = async (delayParts: IDelayParams): Promise => { return await fetch(livekitServiceURL + "/sfu/get", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ - // This is the actual livekit room alias. For the legacy jwt endpoint simply the room id was used. + // The legacy JWT endpoint uses only the matrix room id to calculate the livekit room alias. + // However, the livekit room alias is provided as part of the JWT payload. room: matrixRoomId, openid_token: openIDToken, device_id: deviceId, + ...delayParts, }), }); + }; + + const res = await doNetworkOperationWithRetry(async () => { + let response = await makeRequest(bodyDalayParts); + + // Old service compatibility check + const oldServiceDoesNotSupportDelayParts = + response.status === 400 && Object.keys(bodyDalayParts).length > 0; + // If http status 400 with M_BAD_JSON and we sent delay parts, retry without them + if (oldServiceDoesNotSupportDelayParts) { + try { + const errorBody = await response.json(); + if (errorBody.errcode === "M_BAD_JSON") { + response = await makeRequest({}); + } + } catch { + // If we can't parse the error, treat as real error + } + } + + return response; }); if (!res.ok) { @@ -241,7 +285,7 @@ export async function getLiveKitJWTWithDelayDelegation( // Also check for empty string if (delayId && delayEndpointBaseUrl) { const delayTimeoutMs = - Config.get().matrix_rtc_session?.delayed_leave_event_delay_ms ?? 1000; + Config.get().matrix_rtc_session?.delayed_leave_event_delay_ms; bodyDalayParts = { delay_id: delayId, delay_timeout: delayTimeoutMs, diff --git a/src/room/LayoutToggle.module.css b/src/room/LayoutToggle.module.css deleted file mode 100644 index d9ae5813..00000000 --- a/src/room/LayoutToggle.module.css +++ /dev/null @@ -1,79 +0,0 @@ -/* -Copyright 2023, 2024 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE in the repository root for full details. -*/ - -.toggle { - padding: 2px; - border: 1px solid var(--cpd-color-border-interactive-secondary); - border-radius: var(--cpd-radius-pill-effect); - background: var(--cpd-color-bg-canvas-default); - display: flex; - position: relative; -} - -.toggle input { - appearance: none; - /* Safari puts a margin on these, which is not removed via appearance: none */ - margin: 0; - block-size: var(--cpd-space-11x); - inline-size: var(--cpd-space-11x); - cursor: pointer; - border-radius: var(--cpd-radius-pill-effect); - background: var(--cpd-color-bg-action-secondary-rest); - box-shadow: var(--small-drop-shadow); - transition: background-color 0.1s; -} - -.toggle svg { - display: block; - position: absolute; - padding: calc(2.5 * var(--cpd-space-1x)); - pointer-events: none; - color: var(--cpd-color-icon-primary); - transition: color 0.1s; -} - -.toggle svg:nth-child(2) { - inset-inline-start: 2px; -} - -.toggle svg:nth-child(4) { - inset-inline-end: 2px; -} - -@media (hover: hover) { - .toggle input:hover { - background: var(--cpd-color-bg-action-secondary-hovered); - box-shadow: none; - } -} - -.toggle input:active { - background: var(--cpd-color-bg-action-secondary-pressed); - box-shadow: none; -} - -.toggle input:checked { - background: var(--cpd-color-bg-action-primary-rest); -} - -.toggle input:checked + svg { - color: var(--cpd-color-icon-on-solid-primary); -} - -@media (hover: hover) { - .toggle input:checked:hover { - background: var(--cpd-color-bg-action-primary-hovered); - } -} - -.toggle input:checked:active { - background: var(--cpd-color-bg-action-primary-pressed); -} - -.toggle input:first-child { - margin-inline-end: 5px; -} diff --git a/src/room/LayoutToggle.stories.tsx b/src/room/LayoutToggle.stories.tsx deleted file mode 100644 index 72a2ffad..00000000 --- a/src/room/LayoutToggle.stories.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/* -Copyright 2026 Element Creations Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE in the repository root for full details. -*/ - -import { fn } from "storybook/test"; - -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { LayoutToggle } from "./LayoutToggle"; - -const meta = { - component: LayoutToggle, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - args: { - layout: "grid", - setLayout: fn(), - }, -}; diff --git a/src/room/LayoutToggle.tsx b/src/room/LayoutToggle.tsx deleted file mode 100644 index 98ed91d3..00000000 --- a/src/room/LayoutToggle.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* -Copyright 2023, 2024 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE in the repository root for full details. -*/ - -import { type ChangeEvent, type FC, useCallback } from "react"; -import { useTranslation } from "react-i18next"; -import { Tooltip } from "@vector-im/compound-web"; -import { - SpotlightIcon, - GridIcon, -} from "@vector-im/compound-design-tokens/assets/web/icons"; -import classNames from "classnames"; - -import styles from "./LayoutToggle.module.css"; - -export type Layout = "spotlight" | "grid"; - -type Props = { - layout: Layout; - setLayout: (layout: Layout) => void; - className?: string; -}; - -export const LayoutToggle: FC = ({ layout, setLayout, className }) => { - const { t } = useTranslation(); - - const onChange = useCallback( - (e: ChangeEvent) => setLayout(e.target.value as Layout), - [setLayout], - ); - - return ( -
- - - - - - - - - - ); -}; diff --git a/src/room/__snapshots__/InCallView.test.tsx.snap b/src/room/__snapshots__/InCallView.test.tsx.snap index 3ad3dcfa..d9f768e7 100644 --- a/src/room/__snapshots__/InCallView.test.tsx.snap +++ b/src/room/__snapshots__/InCallView.test.tsx.snap @@ -396,21 +396,23 @@ exports[`InCallView > rendering > renders 1`] = ` -