diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 0b887ecc77..e7d06e75cf 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -37,14 +37,14 @@ jobs: install: true - name: Login to Docker Hub - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 + uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3 if: github.event_name != 'pull_request' with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 + uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3 if: github.event_name != 'pull_request' with: registry: ghcr.io @@ -96,7 +96,7 @@ jobs: - name: Docker meta id: meta - uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5 + uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5 if: github.event_name != 'pull_request' with: images: | diff --git a/.stylelintrc.js b/.stylelintrc.js index ffc6c345b9..3244d122c5 100644 --- a/.stylelintrc.js +++ b/.stylelintrc.js @@ -70,5 +70,13 @@ module.exports = { ], }, ], + "property-no-deprecated": [ + true, + { + ignoreProperties: ["-webkit-box-orient", "word-wrap"], + }, + ], + "nesting-selector-no-missing-scoping-root": null, + "no-invalid-position-declaration": null, }, }; diff --git a/Dockerfile b/Dockerfile index ff9ec91f0a..172a03b229 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # syntax=docker.io/docker/dockerfile:1.17-labs@sha256:9187104f31e3a002a8a6a3209ea1f937fb7486c093cbbde1e14b0fa0d7e4f1b5 # Builder -FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:a80324457a2c8d09c83ff9edf2bdf71f378d3288de920e68a358bd3c484b8c4a AS builder +FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:2d63e0f812d023c4c764e83d7e30dc94949304443ebc372d5c445e63a5ae49c1 AS builder # Support custom branch of the js-sdk. This also helps us build images of element-web develop. ARG USE_CUSTOM_SDKS=false @@ -19,7 +19,7 @@ RUN /src/scripts/docker-package.sh RUN cp /src/config.sample.json /src/webapp/config.json # App -FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:86df552d36eb24c45d3f5becf6423bd056a3fd235d7085fe3d5ea28ba89a8232 +FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:e61b77b27c8f3124fad6d19e894ca5b603bcaf6a34a2df035511299dfa6fad35 # Need root user to install packages & manipulate the usr directory USER root diff --git a/package.json b/package.json index 25763179fa..eb43f43d29 100644 --- a/package.json +++ b/package.json @@ -73,10 +73,10 @@ "test:storybook:update": "playwright-screenshots --entrypoint yarn --with-node-modules && playwright-screenshots --entrypoint /work/node_modules/.bin/test-storybook --with-node-modules --url http://host.docker.internal:6007/ --updateSnapshot" }, "resolutions": { - "**/pretty-format/react-is": "19.1.0", - "@playwright/test": "1.54.1", - "@types/react": "19.1.8", - "@types/react-dom": "19.1.6", + "**/pretty-format/react-is": "19.1.1", + "@playwright/test": "1.54.2", + "@types/react": "19.1.9", + "@types/react-dom": "19.1.7", "oidc-client-ts": "3.3.0", "jwt-decode": "4.0.0", "caniuse-lite": "1.0.30001724", @@ -94,10 +94,10 @@ "@matrix-org/emojibase-bindings": "^1.3.4", "@matrix-org/react-sdk-module-api": "^2.4.0", "@matrix-org/spec": "^1.7.0", - "@sentry/browser": "^9.0.0", + "@sentry/browser": "^10.0.0", "@types/png-chunks-extract": "^1.0.2", "@types/react-virtualized": "^9.21.30", - "@vector-im/compound-design-tokens": "^5.0.0", + "@vector-im/compound-design-tokens": "^6.0.0", "@vector-im/compound-web": "^8.1.2", "@vector-im/matrix-wysiwyg": "2.39.0", "@zxcvbn-ts/core": "^3.0.4", @@ -186,7 +186,7 @@ "@babel/preset-typescript": "^7.12.7", "@babel/runtime": "^7.12.5", "@casualbot/jest-sonar-reporter": "2.2.7", - "@element-hq/element-call-embedded": "0.13.1", + "@element-hq/element-call-embedded": "0.14.1", "@element-hq/element-web-playwright-common": "^1.4.4", "@peculiar/webcrypto": "^1.4.3", "@playwright/test": "^1.50.1", @@ -223,9 +223,9 @@ "@types/node-fetch": "^2.6.2", "@types/pako": "^2.0.0", "@types/qrcode": "^1.3.5", - "@types/react": "19.1.8", + "@types/react": "19.1.9", "@types/react-beautiful-dnd": "^13.0.0", - "@types/react-dom": "19.1.6", + "@types/react-dom": "19.1.7", "@types/react-transition-group": "^4.4.0", "@types/sanitize-html": "2.16.0", "@types/semver": "^7.5.8", @@ -300,8 +300,8 @@ "semver": "^7.5.2", "source-map-loader": "^5.0.0", "storybook": "^9.0.12", - "stylelint": "^16.13.0", - "stylelint-config-standard": "^38.0.0", + "stylelint": "^16.23.0", + "stylelint-config-standard": "^39.0.0", "stylelint-scss": "^6.0.0", "stylelint-value-no-unknown-custom-properties": "^6.0.1", "terser-webpack-plugin": "^5.3.9", diff --git a/playwright/e2e/audio-player/audio-player.spec.ts b/playwright/e2e/audio-player/audio-player.spec.ts index 8532362f6b..3e5e2ce079 100644 --- a/playwright/e2e/audio-player/audio-player.spec.ts +++ b/playwright/e2e/audio-player/audio-player.spec.ts @@ -40,7 +40,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => { // wait for the tile to finish loading await expect( page - .locator(".mx_AudioPlayer_mediaName") + .getByTestId("audio-player-name") .last() .filter({ hasText: file.split("/").at(-1) }), ).toBeVisible(); @@ -55,12 +55,10 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => { // Check that the audio player is rendered and its button becomes visible const checkPlayerVisibility = async (locator: Locator) => { // Assert that the audio player and media information are visible - const mediaInfo = locator.locator( - ".mx_EventTile_mediaLine .mx_MAudioBody .mx_AudioPlayer_container .mx_AudioPlayer_mediaInfo", - ); - await expect(mediaInfo.locator(".mx_AudioPlayer_mediaName", { hasText: ".ogg" })).toBeVisible(); // extension - await expect(mediaInfo.locator(".mx_AudioPlayer_byline", { hasText: "00:01" })).toBeVisible(); - await expect(mediaInfo.locator(".mx_AudioPlayer_byline", { hasText: "(3.56 KB)" })).toBeVisible(); // actual size + const mediaInfo = locator.getByRole("region", { name: "Audio player" }); + await expect(mediaInfo.getByText(".ogg")).toBeVisible(); // extension + await expect(mediaInfo.getByRole("time")).toHaveText("00:01"); // duration + await expect(mediaInfo.getByText("(3.56 KB)")).toBeVisible(); // actual size; // Assert that the play button can be found and is visible await expect(locator.getByRole("button", { name: "Play" })).toBeVisible(); @@ -79,7 +77,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => { } // Check the status of the seek bar - expect(await page.locator(".mx_AudioPlayer_seek input[type='range']").count()).toBeGreaterThan(0); + expect(await page.getByRole("region", { name: "Audio player" }).getByRole("slider").count()).toBeGreaterThan(0); // Enable IRC layout await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); @@ -101,7 +99,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => { display: none !important; } `, - mask: [page.locator(".mx_AudioPlayer_seek")], + mask: [page.getByTestId("audio-player-seek")], }; // Take a snapshot of mx_EventTile_last on IRC layout @@ -187,9 +185,9 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => { await uploadFile(page, "playwright/sample-files/1sec.ogg"); // Assert that the audio player is rendered - const container = page.locator(".mx_EventTile_last .mx_AudioPlayer_container"); + const container = page.locator(".mx_EventTile_last").getByRole("region", { name: "Audio player" }); // Assert that the counter is zero before clicking the play button - await expect(container.locator(".mx_AudioPlayer_seek [role='timer']", { hasText: "00:00" })).toBeVisible(); + await expect(container.getByRole("timer")).toHaveText("00:00"); // Find and click "Play" button, the wait is to make the test less flaky await expect(container.getByRole("button", { name: "Play" })).toBeVisible(); @@ -199,7 +197,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => { await expect(container.getByRole("button", { name: "Pause" })).toBeVisible(); // Assert that the timer is reset when the audio file finished playing - await expect(container.locator(".mx_AudioPlayer_seek [role='timer']", { hasText: "00:00" })).toBeVisible(); + await expect(container.getByRole("timer")).toHaveText("00:00"); // Assert that "Play" button can be found await expect(container.getByRole("button", { name: "Play" })).toBeVisible(); @@ -227,7 +225,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => { await uploadFile(page, "playwright/sample-files/1sec.ogg"); // Assert the audio player is rendered - await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible(); + await expect(page.getByRole("region", { name: "Audio player" })).toBeVisible(); // Find and click "Reply" button on MessageActionBar const tile = page.locator(".mx_EventTile_last"); @@ -237,7 +235,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => { await uploadFile(page, "playwright/sample-files/1sec.ogg"); // Assert that the audio player is rendered - await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible(); + await expect(tile.getByRole("region", { name: "Audio player" })).toBeVisible(); // Assert that replied audio file is rendered as file button inside ReplyChain const button = tile.locator(".mx_ReplyChain_wrapper .mx_MFileBody_info[role='button']"); @@ -262,7 +260,9 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => { await uploadFile(page, "playwright/sample-files/upload-first.ogg"); // Assert that the audio player is rendered - await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible(); + await expect( + page.locator(".mx_EventTile_last").getByRole("region", { name: "Audio player" }), + ).toBeVisible(); await clickButtonReply(tile); @@ -270,7 +270,9 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => { await uploadFile(page, "playwright/sample-files/upload-second.ogg"); // Assert that the audio player is rendered - await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible(); + await expect( + page.locator(".mx_EventTile_last").getByRole("region", { name: "Audio player" }), + ).toBeVisible(); await clickButtonReply(tile); @@ -278,7 +280,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => { await uploadFile(page, "playwright/sample-files/upload-third.ogg"); // Assert that the audio player is rendered - await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible(); + await expect(tile.getByRole("region", { name: "Audio player" })).toBeVisible(); // Assert that there are two "mx_ReplyChain" elements await expect(tile.locator(".mx_ReplyChain")).toHaveCount(2); @@ -314,7 +316,9 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => { // On the main timeline const messageList = page.locator(".mx_RoomView_MessageList"); // Assert the audio player is rendered - await expect(messageList.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible(); + await expect( + messageList.locator(".mx_EventTile_last").getByRole("region", { name: "Audio player" }), + ).toBeVisible(); // Find and click "Reply in thread" button await messageList.locator(".mx_EventTile_last").hover(); await messageList.locator(".mx_EventTile_last").getByRole("button", { name: "Reply in thread" }).click(); @@ -322,10 +326,10 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => { // On a thread const thread = page.locator(".mx_ThreadView"); const threadTile = thread.locator(".mx_EventTile_last"); - const audioPlayer = threadTile.locator(".mx_AudioPlayer_container"); + const audioPlayer = threadTile.getByRole("region", { name: "Audio player" }); // Assert that the counter is zero before clicking the play button - await expect(audioPlayer.locator(".mx_AudioPlayer_seek [role='timer']", { hasText: "00:00" })).toBeVisible(); + await expect(audioPlayer.getByRole("timer")).toHaveText("00:00"); // Find and click "Play" button, the wait is to make the test less flaky await expect(audioPlayer.getByRole("button", { name: "Play" })).toBeVisible(); @@ -335,7 +339,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => { await expect(audioPlayer.getByRole("button", { name: "Pause" })).toBeVisible(); // Assert that the timer is reset when the audio file finished playing - await expect(audioPlayer.locator(".mx_AudioPlayer_seek [role='timer']", { hasText: "00:00" })).toBeVisible(); + await expect(audioPlayer.getByRole("timer")).toHaveText("00:00"); // Assert that "Play" button can be found await expect(audioPlayer.getByRole("button", { name: "Play" })).not.toBeDisabled(); diff --git a/playwright/e2e/composer/CIDER.spec.ts b/playwright/e2e/composer/CIDER.spec.ts index 03fc59cd0f..8214c8058b 100644 --- a/playwright/e2e/composer/CIDER.spec.ts +++ b/playwright/e2e/composer/CIDER.spec.ts @@ -28,7 +28,7 @@ test.describe("Composer", () => { test.describe("CIDER", () => { test("sends a message when you click send or press Enter", async ({ page }) => { - const composer = page.getByRole("textbox", { name: "Send a message…" }); + const composer = page.getByRole("textbox", { name: "Send an unencrypted message…" }); // Type a message await composer.pressSequentially("my message 0"); @@ -52,7 +52,7 @@ test.describe("Composer", () => { }); test("can write formatted text", async ({ page }) => { - const composer = page.getByRole("textbox", { name: "Send a message…" }); + const composer = page.getByRole("textbox", { name: "Send an unencrypted message…" }); await composer.pressSequentially("my bold"); await composer.press(`${CtrlOrMeta}+KeyB`); @@ -68,7 +68,7 @@ test.describe("Composer", () => { await page.getByTestId("mx_EmojiPicker").locator(".mx_EmojiPicker_item", { hasText: "😇" }).click(); await page.locator(".mx_ContextualMenu_background").click(); // Close emoji picker - await page.getByRole("textbox", { name: "Send a message…" }).press("Enter"); // Send message + await page.getByRole("textbox", { name: "Send an unencrypted message…" }).press("Enter"); // Send message await expect(page.locator(".mx_EventTile_body", { hasText: "😇" })).toBeVisible(); }); @@ -79,7 +79,7 @@ test.describe("Composer", () => { }); test("only sends when you press Control+Enter", async ({ page }) => { - const composer = page.getByRole("textbox", { name: "Send a message…" }); + const composer = page.getByRole("textbox", { name: "Send an unencrypted message…" }); // Type a message and press Enter await composer.pressSequentially("my message 3"); await composer.press("Enter"); diff --git a/playwright/e2e/crypto/backups-mas.spec.ts b/playwright/e2e/crypto/backups-mas.spec.ts index 6a7fff6672..fda82c220d 100644 --- a/playwright/e2e/crypto/backups-mas.spec.ts +++ b/playwright/e2e/crypto/backups-mas.spec.ts @@ -91,10 +91,10 @@ test.describe("Key backup reset from elsewhere", () => { await csAPI.deleteBackupVersion(backupInfo.version); - await page.getByRole("textbox", { name: "Send an encrypted message…" }).fill("/discardsession"); + await page.getByRole("textbox", { name: "Send a message…" }).fill("/discardsession"); await page.getByRole("button", { name: "Send message" }).click(); - await page.getByRole("textbox", { name: "Send an encrypted message…" }).fill("Message with broken key backup"); + await page.getByRole("textbox", { name: "Send a message…" }).fill("Message with broken key backup"); await page.getByRole("button", { name: "Send message" }).click(); // Should be the message we sent plus the room creation event diff --git a/playwright/e2e/crypto/crypto.spec.ts b/playwright/e2e/crypto/crypto.spec.ts index 6d01435546..2602f1d02e 100644 --- a/playwright/e2e/crypto/crypto.spec.ts +++ b/playwright/e2e/crypto/crypto.spec.ts @@ -154,8 +154,8 @@ test.describe("Cryptography", function () { await app.client.bootstrapCrossSigning(aliceCredentials); await startDMWithBob(page, bob); // send first message - await page.getByRole("textbox", { name: "Send a message…" }).fill("Hey!"); - await page.getByRole("textbox", { name: "Send a message…" }).press("Enter"); + await page.getByRole("textbox", { name: "Send an unencrypted message…" }).fill("Hey!"); + await page.getByRole("textbox", { name: "Send an unencrypted message…" }).press("Enter"); await checkDMRoom(page); const bobRoomId = await bobJoin(page, bob); await expect(page.locator(".mx_MessageComposer_e2eIcon")).toMatchScreenshot("composer-e2e-icon-normal.png"); diff --git a/playwright/e2e/crypto/device-verification.spec.ts b/playwright/e2e/crypto/device-verification.spec.ts index eb21dfc909..ab36c37a76 100644 --- a/playwright/e2e/crypto/device-verification.spec.ts +++ b/playwright/e2e/crypto/device-verification.spec.ts @@ -209,7 +209,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => { const dialog = page.locator(".mx_Dialog"); // We use `pressSequentially` here to make sure that the FocusLock isn't causing us any problems // (cf https://github.com/element-hq/element-web/issues/30089) - await dialog.locator("textarea").pressSequentially(recoveryKey); + await dialog.getByTitle("Recovery key").pressSequentially(recoveryKey); await dialog.getByRole("button", { name: "Continue", disabled: false }).click(); await page.getByRole("button", { name: "Done" }).click(); diff --git a/playwright/e2e/crypto/utils.ts b/playwright/e2e/crypto/utils.ts index 289b123e86..0521df236e 100644 --- a/playwright/e2e/crypto/utils.ts +++ b/playwright/e2e/crypto/utils.ts @@ -228,7 +228,7 @@ export async function logIntoElement(page: Page, credentials: Credentials, secur await useSecurityKey.click(); } // Fill in the recovery key - await page.locator(".mx_Dialog").locator("textarea").fill(securityKey); + await page.locator(".mx_Dialog").getByTitle("Recovery key").fill(securityKey); await page.getByRole("button", { name: "Continue", disabled: false }).click(); await page.getByRole("button", { name: "Done" }).click(); } @@ -263,7 +263,7 @@ export async function verifySession(app: ElementAppPage, securityKey: string) { const settings = await app.settings.openUserSettings("Encryption"); await settings.getByRole("button", { name: "Verify this device" }).click(); await app.page.getByRole("button", { name: "Verify with Recovery Key" }).click(); - await app.page.locator(".mx_Dialog").locator("textarea").fill(securityKey); + await app.page.locator(".mx_Dialog").getByTitle("Recovery key").fill(securityKey); await app.page.getByRole("button", { name: "Continue", disabled: false }).click(); await app.page.getByRole("button", { name: "Done" }).click(); await app.settings.closeDialog(); diff --git a/playwright/e2e/messages/messages.spec.ts b/playwright/e2e/messages/messages.spec.ts index f430d6b18b..1a36ac97e9 100644 --- a/playwright/e2e/messages/messages.spec.ts +++ b/playwright/e2e/messages/messages.spec.ts @@ -13,7 +13,7 @@ import { type Locator, type Page } from "@playwright/test"; import { test, expect } from "../../element-web-test"; async function sendMessage(page: Page, message: string): Promise { - await page.getByRole("textbox", { name: "Send a message…" }).fill(message); + await page.getByRole("textbox", { name: "Send an unencrypted message…" }).fill(message); await page.getByRole("button", { name: "Send message" }).click(); const msgTile = page.locator(".mx_EventTile_last"); @@ -22,7 +22,7 @@ async function sendMessage(page: Page, message: string): Promise { } async function sendMultilineMessages(page: Page, messages: string[]) { - await page.getByRole("textbox", { name: "Send a message…" }).focus(); + await page.getByRole("textbox", { name: "Send an unencrypted message…" }).focus(); for (let i = 0; i < messages.length; i++) { await page.keyboard.type(messages[i]); if (i < messages.length - 1) await page.keyboard.press("Shift+Enter"); @@ -40,7 +40,7 @@ async function replyMessage(page: Page, message: Locator, replyMessage: string): await line.hover(); await line.getByRole("button", { name: "Reply", exact: true }).click(); - await page.getByRole("textbox", { name: "Send a reply…" }).fill(replyMessage); + await page.getByRole("textbox", { name: "Send an unencrypted reply…" }).fill(replyMessage); await page.getByRole("button", { name: "Send message" }).click(); const msgTile = page.locator(".mx_EventTile_last"); diff --git a/playwright/e2e/regression-tests/pills-click-in-app.spec.ts b/playwright/e2e/regression-tests/pills-click-in-app.spec.ts index 3670e64308..6856e70609 100644 --- a/playwright/e2e/regression-tests/pills-click-in-app.spec.ts +++ b/playwright/e2e/regression-tests/pills-click-in-app.spec.ts @@ -29,7 +29,7 @@ test.describe("Pills", () => { // send a message using the built-in room mention functionality (autocomplete) await page - .getByRole("textbox", { name: "Send a message…" }) + .getByRole("textbox", { name: "Send an unencrypted message…" }) .pressSequentially(`Hello world! Join here: #${targetLocalpart.substring(0, 3)}`); await page.locator(".mx_Autocomplete_Completion_title").click(); await page.getByRole("button", { name: "Send message" }).click(); diff --git a/playwright/e2e/right-panel/file-panel.spec.ts b/playwright/e2e/right-panel/file-panel.spec.ts index d69b7d4731..f6d89511b7 100644 --- a/playwright/e2e/right-panel/file-panel.spec.ts +++ b/playwright/e2e/right-panel/file-panel.spec.ts @@ -63,9 +63,7 @@ test.describe("FilePanel", () => { await expect(roomViewBody.locator(".mx_EventTile[data-layout='group'] img[alt='riot.png']")).toBeVisible(); // Assert that the audio player is rendered - await expect( - roomViewBody.locator(".mx_EventTile[data-layout='group'] .mx_AudioPlayer_container"), - ).toBeVisible(); + await expect(roomViewBody.getByRole("region", { name: "Audio player" })).toBeVisible(); // Assert that the file button exists await expect( @@ -97,9 +95,7 @@ test.describe("FilePanel", () => { await expect(image.locator("img[alt='riot.png']")).toBeVisible(); // Detect the audio file - const audio = filePanelMessageList.locator( - ".mx_EventTile_mediaLine .mx_MAudioBody .mx_AudioPlayer_container", - ); + const audio = filePanelMessageList.getByRole("region", { name: "Audio player" }); // Assert that the play button is rendered await expect(audio.getByRole("button", { name: "Play" })).toBeVisible(); @@ -130,7 +126,7 @@ test.describe("FilePanel", () => { // Take a snapshot of file tiles list on FilePanel await expect(filePanelMessageList).toMatchScreenshot("file-tiles-list.png", { // Exclude timestamps & flaky seek bar from snapshot - mask: [page.locator(".mx_MessageTimestamp, .mx_AudioPlayer_seek")], + mask: [page.locator(".mx_MessageTimestamp"), page.getByTestId("audio-player-seek")], }); }); @@ -138,21 +134,19 @@ test.describe("FilePanel", () => { // Upload an image file await uploadFile(page, "playwright/sample-files/1sec.ogg"); - const audioBody = page.locator( - ".mx_FilePanel .mx_RoomView_MessageList .mx_EventTile_mediaLine .mx_MAudioBody .mx_AudioPlayer_container", - ); + const audioBody = page.getByTestId("right-panel").getByRole("region", { name: "Audio player" }); + // Assert that the audio player is rendered - // Assert that the audio file information is rendered - const mediaInfo = audioBody.locator(".mx_AudioPlayer_mediaInfo"); - await expect(mediaInfo.locator(".mx_AudioPlayer_mediaName").getByText("1sec.ogg")).toBeVisible(); - await expect(mediaInfo.locator(".mx_AudioPlayer_byline", { hasText: "00:01" })).toBeVisible(); - await expect(mediaInfo.locator(".mx_AudioPlayer_byline", { hasText: "(3.56 KB)" })).toBeVisible(); // actual size + // Assert that the audio file information is rendered; + await expect(audioBody.getByText("1sec.ogg")).toBeVisible(); // extension + await expect(audioBody.getByRole("time")).toHaveText("00:01"); // duration + await expect(audioBody.getByText("(3.56 KB)")).toBeVisible(); // actual size; // Assert that the duration counter is 00:01 before clicking the play button - await expect(audioBody.locator(".mx_AudioPlayer_mediaInfo time", { hasText: "00:01" })).toBeVisible(); + await expect(audioBody.getByRole("time")).toHaveText("00:01"); // Assert that the counter is zero before clicking the play button - await expect(audioBody.locator(".mx_AudioPlayer_seek [role='timer']", { hasText: "00:00" })).toBeVisible(); + await expect(audioBody.getByRole("timer")).toHaveText("00:00"); // Click the play button await audioBody.getByRole("button", { name: "Play" }).click(); @@ -161,7 +155,7 @@ test.describe("FilePanel", () => { await expect(audioBody.getByRole("button", { name: "Pause" })).toBeVisible(); // Assert that the timer is reset when the audio file finished playing - await expect(audioBody.locator(".mx_AudioPlayer_seek [role='timer']", { hasText: "00:00" })).toBeVisible(); + await expect(audioBody.getByRole("timer")).toHaveText("00:00"); // Assert that the play button is rendered await expect(audioBody.getByRole("button", { name: "Play" })).toBeVisible(); diff --git a/playwright/e2e/spotlight/spotlight.spec.ts b/playwright/e2e/spotlight/spotlight.spec.ts index 7a5f7d4ea8..542b537c13 100644 --- a/playwright/e2e/spotlight/spotlight.spec.ts +++ b/playwright/e2e/spotlight/spotlight.spec.ts @@ -30,7 +30,7 @@ async function startDM(app: ElementAppPage, page: Page, name: string): Promise { // Send first message to actually start DM await expect(roomHeaderName(page)).toHaveText(bot2.credentials.displayName); - const locator = page.getByRole("textbox", { name: "Send a message…" }); + const locator = page.getByRole("textbox", { name: "Send an unencrypted message…" }); await locator.fill("Hey!"); await locator.press("Enter"); diff --git a/playwright/e2e/threads/threads.spec.ts b/playwright/e2e/threads/threads.spec.ts index 89cfe418ba..98039e1242 100644 --- a/playwright/e2e/threads/threads.spec.ts +++ b/playwright/e2e/threads/threads.spec.ts @@ -43,7 +43,7 @@ test.describe("Threads", () => { const roomViewLocator = page.locator(".mx_RoomView_body"); // User sends message - const textbox = roomViewLocator.getByRole("textbox", { name: "Send a message…" }); + const textbox = roomViewLocator.getByRole("textbox", { name: "Send an unencrypted message…" }); await textbox.fill("Hello Mr. Bot"); await textbox.press("Enter"); @@ -108,7 +108,7 @@ test.describe("Threads", () => { await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group); // User responds in thread - locator = page.locator(".mx_ThreadView").getByRole("textbox", { name: "Send a message…" }); + locator = page.locator(".mx_ThreadView").getByRole("textbox", { name: "Send an unencrypted message…" }); await locator.fill("Test"); await locator.press("Enter"); @@ -262,7 +262,7 @@ test.describe("Threads", () => { await locator.locator(".mx_EventTile_line").click(); // User responds & asserts - locator = page.locator(".mx_ThreadView").getByRole("textbox", { name: "Send a message…" }); + locator = page.locator(".mx_ThreadView").getByRole("textbox", { name: "Send an unencrypted message…" }); await locator.fill("Great!"); await locator.press("Enter"); @@ -335,8 +335,8 @@ test.describe("Threads", () => { // Send message const locator = page.locator(".mx_RoomView_body"); - await locator.getByRole("textbox", { name: "Send a message…" }).fill("Hello Mr. Bot"); - await locator.getByRole("textbox", { name: "Send a message…" }).press("Enter"); + await locator.getByRole("textbox", { name: "Send an unencrypted message…" }).fill("Hello Mr. Bot"); + await locator.getByRole("textbox", { name: "Send an unencrypted message…" }).press("Enter"); // Create thread const locator2 = locator.locator(".mx_EventTile[data-scroll-tokens]").filter({ hasText: "Hello Mr. Bot" }); await locator2.hover(); @@ -366,7 +366,7 @@ test.describe("Threads", () => { let locator = page.locator(".mx_RoomView_body"); // User sends message - let textbox = locator.getByRole("textbox", { name: "Send a message…" }); + let textbox = locator.getByRole("textbox", { name: "Send an unencrypted message…" }); await textbox.fill("Hello Mr. Bot"); await textbox.press("Enter"); // Wait for message to send, get its ID and save as @threadId @@ -395,7 +395,7 @@ test.describe("Threads", () => { locator = page.locator(".mx_ThreadView"); await locator.locator(".mx_EventTile_last").hover(); await locator.locator(".mx_EventTile_last").getByRole("button", { name: "Reply" }).click(); - textbox = locator.getByRole("textbox", { name: "Reply to thread…" }); + textbox = locator.getByRole("textbox", { name: "Reply to unencrypted thread…" }); await textbox.fill("Please come here"); await textbox.press("Enter"); // Wait until the reply is sent @@ -414,7 +414,7 @@ test.describe("Threads", () => { // Send message let locator = page.locator(".mx_RoomView_body"); - let textbox = locator.getByRole("textbox", { name: "Send a message…" }); + let textbox = locator.getByRole("textbox", { name: "Send an unencrypted message…" }); await textbox.fill("Hello Mr. Bot"); await textbox.press("Enter"); // Create thread @@ -425,7 +425,7 @@ test.describe("Threads", () => { // Send message to thread locator = page.locator(".mx_ThreadPanel"); - textbox = locator.getByRole("textbox", { name: "Send a message…" }); + textbox = locator.getByRole("textbox", { name: "Send an unencrypted message…" }); await textbox.fill("Hello Mr. User"); await textbox.press("Enter"); await expect(locator.locator(".mx_EventTile_last").getByText("Hello Mr. User")).toBeAttached(); @@ -456,7 +456,7 @@ test.describe("Threads", () => { */ const sendMessage = async (message: string) => { const messageComposer = page.getByRole("region", { name: "Message composer" }); - const textbox = messageComposer.getByRole("textbox", { name: "Send a message…" }); + const textbox = messageComposer.getByRole("textbox", { name: "Send an unencrypted message…" }); await textbox.fill(message); await textbox.press("Enter"); }; @@ -478,7 +478,7 @@ test.describe("Threads", () => { // Send a message in the thread const threadPanel = page.locator(".mx_ThreadPanel"); - const textbox = threadPanel.getByRole("textbox", { name: "Send a message…" }); + const textbox = threadPanel.getByRole("textbox", { name: "Send an unencrypted message…" }); await textbox.fill(threadMessage); await textbox.press("Enter"); await expect(threadPanel.locator(".mx_EventTile_last").getByText(threadMessage)).toBeVisible(); diff --git a/playwright/e2e/timeline/timeline.spec.ts b/playwright/e2e/timeline/timeline.spec.ts index e5bfa0c71b..d169afd66f 100644 --- a/playwright/e2e/timeline/timeline.spec.ts +++ b/playwright/e2e/timeline/timeline.spec.ts @@ -461,11 +461,11 @@ test.describe("Timeline", () => { // Send a emote await page .locator(".mx_RoomView_body") - .getByRole("textbox", { name: "Send a message…" }) + .getByRole("textbox", { name: "Send an unencrypted message…" }) .fill("/me says hello to Mr. Bot"); await page .locator(".mx_RoomView_body") - .getByRole("textbox", { name: "Send a message…" }) + .getByRole("textbox", { name: "Send an unencrypted message…" }) .press("Enter"); // Check inline start margin of its avatar // Here --right-padding is for the avatar on the message line diff --git a/playwright/shared-component-snapshots/audio-audioplayerview--default-linux.png b/playwright/shared-component-snapshots/audio-audioplayerview--default-linux.png new file mode 100644 index 0000000000..e4d6a84d23 Binary files /dev/null and b/playwright/shared-component-snapshots/audio-audioplayerview--default-linux.png differ diff --git a/playwright/shared-component-snapshots/audio-audioplayerview--has-error-linux.png b/playwright/shared-component-snapshots/audio-audioplayerview--has-error-linux.png new file mode 100644 index 0000000000..36684675f7 Binary files /dev/null and b/playwright/shared-component-snapshots/audio-audioplayerview--has-error-linux.png differ diff --git a/playwright/shared-component-snapshots/audio-audioplayerview--no-media-name-linux.png b/playwright/shared-component-snapshots/audio-audioplayerview--no-media-name-linux.png new file mode 100644 index 0000000000..c46e59ac21 Binary files /dev/null and b/playwright/shared-component-snapshots/audio-audioplayerview--no-media-name-linux.png differ diff --git a/playwright/shared-component-snapshots/audio-audioplayerview--no-size-linux.png b/playwright/shared-component-snapshots/audio-audioplayerview--no-size-linux.png new file mode 100644 index 0000000000..928f6f0197 Binary files /dev/null and b/playwright/shared-component-snapshots/audio-audioplayerview--no-size-linux.png differ diff --git a/playwright/shared-component-snapshots/audio-clock--default-linux.png b/playwright/shared-component-snapshots/audio-clock--default-linux.png new file mode 100644 index 0000000000..be66f4b70c Binary files /dev/null and b/playwright/shared-component-snapshots/audio-clock--default-linux.png differ diff --git a/playwright/shared-component-snapshots/audio-clock--lot-of-seconds-linux.png b/playwright/shared-component-snapshots/audio-clock--lot-of-seconds-linux.png new file mode 100644 index 0000000000..b4879e1a0c Binary files /dev/null and b/playwright/shared-component-snapshots/audio-clock--lot-of-seconds-linux.png differ diff --git a/playwright/shared-component-snapshots/audio-playpausebutton--default-linux.png b/playwright/shared-component-snapshots/audio-playpausebutton--default-linux.png new file mode 100644 index 0000000000..8c8baa7a65 Binary files /dev/null and b/playwright/shared-component-snapshots/audio-playpausebutton--default-linux.png differ diff --git a/playwright/shared-component-snapshots/audio-playpausebutton--playing-linux.png b/playwright/shared-component-snapshots/audio-playpausebutton--playing-linux.png new file mode 100644 index 0000000000..53d58a4c2f Binary files /dev/null and b/playwright/shared-component-snapshots/audio-playpausebutton--playing-linux.png differ diff --git a/playwright/shared-component-snapshots/audio-seekbar--default-linux.png b/playwright/shared-component-snapshots/audio-seekbar--default-linux.png new file mode 100644 index 0000000000..60e51020cf Binary files /dev/null and b/playwright/shared-component-snapshots/audio-seekbar--default-linux.png differ diff --git a/playwright/shared-component-snapshots/audio-seekbar--disabled-linux.png b/playwright/shared-component-snapshots/audio-seekbar--disabled-linux.png new file mode 100644 index 0000000000..128f7e2ee5 Binary files /dev/null and b/playwright/shared-component-snapshots/audio-seekbar--disabled-linux.png differ diff --git a/playwright/shared-component-snapshots/messagebody-mediabody--default-linux.png b/playwright/shared-component-snapshots/messagebody-mediabody--default-linux.png new file mode 100644 index 0000000000..56b8072d2d Binary files /dev/null and b/playwright/shared-component-snapshots/messagebody-mediabody--default-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--bubble-layout-linux.png index 165033dbe9..17bc4bfc78 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--bubble-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--bubble-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--group-layout-linux.png index f309d57bc0..30e0de96ce 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--group-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--group-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--irc-layout-linux.png index bd02a2f21a..f2fd0de114 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--irc-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--irc-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--bubble-layout-linux.png index 16e0624b83..ab9b63960d 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--bubble-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--bubble-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--group-layout-linux.png index 1e78930256..7e6137c4eb 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--group-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--group-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--irc-layout-linux.png index 6a43aac7ef..519a46371d 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--irc-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--irc-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--bubble-layout-linux.png index 014b8dbaec..0a299b63d1 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--bubble-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--bubble-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--group-layout-linux.png index 156d89053c..8307809872 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--group-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--group-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--irc-layout-linux.png index caf6e1e698..f57a1e27a6 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--irc-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--irc-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--bubble-layout-linux.png index c9591ebf49..8a6dec9273 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--bubble-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--bubble-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--group-layout-linux.png index 794ac11b01..b6a2ed0bfc 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--group-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--group-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--irc-layout-linux.png index 2b6475fbdf..c59ef184c4 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--irc-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--irc-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-bubble-layout-linux.png index 0f643ee43a..9d86393932 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-bubble-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-bubble-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-bubble-layout-linux.png index bc9d6c88c3..470366cb9b 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-bubble-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-bubble-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-group-layout-linux.png index 2b867170ae..2dac381c78 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-group-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-group-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-irc-layout-linux.png index 459ebd3584..b09c0ceed4 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-irc-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-irc-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-group-layout-linux.png index da97c28029..16f51d0262 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-group-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-group-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-irc-layout-linux.png index 009ea38f7b..1952c0681d 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-irc-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-irc-layout-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-selection-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-selection-linux.png index b802eb7888..185b1581ba 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-selection-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-selection-linux.png differ diff --git a/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png b/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png index 0a4abba833..b7101622f5 100644 Binary files a/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png and b/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png differ diff --git a/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png b/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png index f00047fe84..66e755231f 100644 Binary files a/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png and b/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png differ diff --git a/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-All-new-pinned-messages-linux.png b/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-All-new-pinned-messages-linux.png index 3e78322813..c4c7cb9b0d 100644 Binary files a/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-All-new-pinned-messages-linux.png and b/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-All-new-pinned-messages-linux.png differ diff --git a/playwright/snapshots/right-panel/file-panel.spec.ts/file-tiles-list-linux.png b/playwright/snapshots/right-panel/file-panel.spec.ts/file-tiles-list-linux.png index 1c75a92373..f9170b0cda 100644 Binary files a/playwright/snapshots/right-panel/file-panel.spec.ts/file-tiles-list-linux.png and b/playwright/snapshots/right-panel/file-panel.spec.ts/file-tiles-list-linux.png differ diff --git a/playwright/snapshots/threads/threads.spec.ts/Initial-ThreadView-on-bubble-layout-linux.png b/playwright/snapshots/threads/threads.spec.ts/Initial-ThreadView-on-bubble-layout-linux.png index 60d6dc9e18..663bd9c9a7 100644 Binary files a/playwright/snapshots/threads/threads.spec.ts/Initial-ThreadView-on-bubble-layout-linux.png and b/playwright/snapshots/threads/threads.spec.ts/Initial-ThreadView-on-bubble-layout-linux.png differ diff --git a/playwright/snapshots/threads/threads.spec.ts/Initial-ThreadView-on-group-layout-linux.png b/playwright/snapshots/threads/threads.spec.ts/Initial-ThreadView-on-group-layout-linux.png index f217fe7094..416edb59f8 100644 Binary files a/playwright/snapshots/threads/threads.spec.ts/Initial-ThreadView-on-group-layout-linux.png and b/playwright/snapshots/threads/threads.spec.ts/Initial-ThreadView-on-group-layout-linux.png differ diff --git a/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png b/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png index e43e41dd79..28ecd15484 100644 Binary files a/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png and b/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png differ diff --git a/playwright/snapshots/threads/threads.spec.ts/ThreadView-with-reaction-and-a-hidden-event-on-bubble-layout-linux.png b/playwright/snapshots/threads/threads.spec.ts/ThreadView-with-reaction-and-a-hidden-event-on-bubble-layout-linux.png index cff1b27bd3..86179e2de2 100644 Binary files a/playwright/snapshots/threads/threads.spec.ts/ThreadView-with-reaction-and-a-hidden-event-on-bubble-layout-linux.png and b/playwright/snapshots/threads/threads.spec.ts/ThreadView-with-reaction-and-a-hidden-event-on-bubble-layout-linux.png differ diff --git a/playwright/snapshots/threads/threads.spec.ts/ThreadView-with-reaction-and-a-hidden-event-on-group-layout-linux.png b/playwright/snapshots/threads/threads.spec.ts/ThreadView-with-reaction-and-a-hidden-event-on-group-layout-linux.png index 30fa37ab9e..474dd2e1d7 100644 Binary files a/playwright/snapshots/threads/threads.spec.ts/ThreadView-with-reaction-and-a-hidden-event-on-group-layout-linux.png and b/playwright/snapshots/threads/threads.spec.ts/ThreadView-with-reaction-and-a-hidden-event-on-group-layout-linux.png differ diff --git a/playwright/snapshots/threads/threads.spec.ts/ThreadView-with-redacted-messages-on-bubble-layout-linux.png b/playwright/snapshots/threads/threads.spec.ts/ThreadView-with-redacted-messages-on-bubble-layout-linux.png index c92780196d..d13de053ff 100644 Binary files a/playwright/snapshots/threads/threads.spec.ts/ThreadView-with-redacted-messages-on-bubble-layout-linux.png and b/playwright/snapshots/threads/threads.spec.ts/ThreadView-with-redacted-messages-on-bubble-layout-linux.png differ diff --git a/playwright/snapshots/threads/threads.spec.ts/ThreadView-with-redacted-messages-on-group-layout-linux.png b/playwright/snapshots/threads/threads.spec.ts/ThreadView-with-redacted-messages-on-group-layout-linux.png index 4bad759050..35215eab83 100644 Binary files a/playwright/snapshots/threads/threads.spec.ts/ThreadView-with-redacted-messages-on-group-layout-linux.png and b/playwright/snapshots/threads/threads.spec.ts/ThreadView-with-redacted-messages-on-group-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png index f8135426c2..3dc1539094 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-bubble-layout-linux.png index 4d75b3b966..d210cc58a9 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-bubble-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-bubble-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/configured-room-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/configured-room-irc-layout-linux.png index a6e31b89cd..5290094b1f 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/configured-room-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/configured-room-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-line-inline-start-margin-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-line-inline-start-margin-irc-layout-linux.png index 8d3d7b09ed..c65216a418 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-line-inline-start-margin-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-line-inline-start-margin-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-modern-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-modern-linux.png index ca4ce5933f..67a207c687 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-modern-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-modern-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-bubble-layout-linux.png index 785681b28c..9b3c124739 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-bubble-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-bubble-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-compact-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-compact-modern-layout-linux.png index e5e312ac73..c66b0fd8c5 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-compact-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-compact-modern-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-irc-layout-linux.png index f79934e621..64c6586e22 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-modern-layout-linux.png index ff4fa7c1b9..19b75f03b8 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-modern-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-and-messages-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-and-messages-irc-layout-linux.png index f6840e8daf..07399c19e3 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-and-messages-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-and-messages-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-bubble-layout-linux.png index 6154a0a268..37683b4a2f 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-bubble-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-bubble-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-emote-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-emote-irc-layout-linux.png index 06853769d7..9b995aaf17 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-emote-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-emote-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-irc-layout-linux.png index 8d3d7b09ed..c65216a418 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-modern-layout-linux.png index f1a95a8275..386940c6d9 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-modern-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-redaction-placeholder-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-redaction-placeholder-linux.png index 9d9dacd1bf..9c3d9782ef 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-redaction-placeholder-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-redaction-placeholder-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-padding-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-padding-modern-layout-linux.png index e0523d6eec..633ee3bacf 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-padding-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-padding-modern-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-zero-padding-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-zero-padding-irc-layout-linux.png index 5c6d7710f6..6235ecb8cf 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-zero-padding-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-zero-padding-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/search-aux-panel-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/search-aux-panel-linux.png index 125c47ee35..d46e898b9c 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/search-aux-panel-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/search-aux-panel-linux.png differ diff --git a/playwright/testcontainers/synapse.ts b/playwright/testcontainers/synapse.ts index 6c8962abf2..627ad9c79a 100644 --- a/playwright/testcontainers/synapse.ts +++ b/playwright/testcontainers/synapse.ts @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. import { SynapseContainer as BaseSynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers"; -const TAG = "develop@sha256:2f6fff14ff23f356705abdbf2ed62c3dd6ca2103cef4ae813714ddc199bbd76a"; +const TAG = "develop@sha256:a0d8db97e39321166959cecdc4c684daff36fefdc7968de47be7e0fdb1cb4541"; /** * SynapseContainer which freezes the docker digest to stabilise tests, diff --git a/res/css/_components.pcss b/res/css/_components.pcss index bd002fb80c..b140fd1d7e 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -95,7 +95,6 @@ @import "./structures/auth/_Registration.pcss"; @import "./structures/auth/_SessionLockStolenView.pcss"; @import "./structures/auth/_SetupEncryptionBody.pcss"; -@import "./views/audio_messages/_AudioPlayer.pcss"; @import "./views/audio_messages/_PlayPauseButton.pcss"; @import "./views/audio_messages/_PlaybackContainer.pcss"; @import "./views/audio_messages/_SeekBar.pcss"; diff --git a/res/css/components/views/dialogs/polls/_PollListItem.pcss b/res/css/components/views/dialogs/polls/_PollListItem.pcss index 6cb46a21d2..cd24c75937 100644 --- a/res/css/components/views/dialogs/polls/_PollListItem.pcss +++ b/res/css/components/views/dialogs/polls/_PollListItem.pcss @@ -15,7 +15,7 @@ Please see LICENSE files in the repository root for full details. display: grid; justify-content: left; align-items: center; - grid-gap: $spacing-8; + gap: $spacing-8; grid-template-columns: auto auto auto; grid-template-rows: auto; cursor: pointer; diff --git a/res/css/components/views/dialogs/polls/_PollListItemEnded.pcss b/res/css/components/views/dialogs/polls/_PollListItemEnded.pcss index 772b47c9a4..2eb7a185ac 100644 --- a/res/css/components/views/dialogs/polls/_PollListItemEnded.pcss +++ b/res/css/components/views/dialogs/polls/_PollListItemEnded.pcss @@ -22,7 +22,7 @@ Please see LICENSE files in the repository root for full details. display: grid; justify-content: left; align-items: center; - grid-gap: $spacing-8; + gap: $spacing-8; grid-template-columns: min-content 1fr min-content; grid-template-rows: auto; } @@ -47,7 +47,7 @@ Please see LICENSE files in the repository root for full details. .mx_PollListItemEnded_answers { display: grid; - grid-gap: $spacing-8; + gap: $spacing-8; margin-top: $spacing-12; } diff --git a/res/css/components/views/settings/devices/_DeviceDetailHeading.pcss b/res/css/components/views/settings/devices/_DeviceDetailHeading.pcss index 789efa9e7f..82b19b9ff1 100644 --- a/res/css/components/views/settings/devices/_DeviceDetailHeading.pcss +++ b/res/css/components/views/settings/devices/_DeviceDetailHeading.pcss @@ -19,7 +19,7 @@ Please see LICENSE files in the repository root for full details. .mx_DeviceDetailHeading_renameForm { display: grid; - grid-gap: $spacing-16; + gap: $spacing-16; justify-content: left; grid-template-columns: 100%; } diff --git a/res/css/components/views/settings/devices/_DeviceDetails.pcss b/res/css/components/views/settings/devices/_DeviceDetails.pcss index d3635710f3..4b311d1c7c 100644 --- a/res/css/components/views/settings/devices/_DeviceDetails.pcss +++ b/res/css/components/views/settings/devices/_DeviceDetails.pcss @@ -23,7 +23,7 @@ Please see LICENSE files in the repository root for full details. border-bottom: 1px solid $quinary-content; display: grid; - grid-gap: $spacing-24; + gap: $spacing-24; justify-content: left; grid-template-columns: 100%; diff --git a/res/css/components/views/settings/devices/_DeviceTile.pcss b/res/css/components/views/settings/devices/_DeviceTile.pcss index e4096329d6..07ee70792d 100644 --- a/res/css/components/views/settings/devices/_DeviceTile.pcss +++ b/res/css/components/views/settings/devices/_DeviceTile.pcss @@ -35,7 +35,7 @@ Please see LICENSE files in the repository root for full details. .mx_DeviceTile_actions { display: grid; - grid-gap: $spacing-8; + gap: $spacing-8; grid-auto-flow: column; margin-left: $spacing-8; } diff --git a/res/css/components/views/settings/devices/_FilteredDeviceList.pcss b/res/css/components/views/settings/devices/_FilteredDeviceList.pcss index aac5986280..06f5a80b65 100644 --- a/res/css/components/views/settings/devices/_FilteredDeviceList.pcss +++ b/res/css/components/views/settings/devices/_FilteredDeviceList.pcss @@ -15,7 +15,7 @@ Please see LICENSE files in the repository root for full details. .mx_FilteredDeviceList_list { list-style-type: none; display: grid; - grid-gap: $spacing-16; + gap: $spacing-16; margin: 0; padding: 0 $spacing-16; } diff --git a/res/css/components/views/settings/shared/_SettingsSubsection.pcss b/res/css/components/views/settings/shared/_SettingsSubsection.pcss index 0d03a12b1d..3b22c679c5 100644 --- a/res/css/components/views/settings/shared/_SettingsSubsection.pcss +++ b/res/css/components/views/settings/shared/_SettingsSubsection.pcss @@ -39,7 +39,7 @@ Please see LICENSE files in the repository root for full details. .mx_SettingsSubsection_content { width: 100%; display: grid; - grid-gap: $spacing-8; + gap: $spacing-8; /* setting minwidth 0 makes columns definitely sized fixing horizontal overflow */ grid-template-columns: minmax(0, 1fr); justify-items: flex-start; diff --git a/res/css/views/audio_messages/_AudioPlayer.pcss b/res/css/views/audio_messages/_AudioPlayer.pcss deleted file mode 100644 index 51e97611f5..0000000000 --- a/res/css/views/audio_messages/_AudioPlayer.pcss +++ /dev/null @@ -1,59 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2021 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -.mx_MediaBody.mx_AudioPlayer_container { - padding: 16px 12px 12px 12px; - - .mx_AudioPlayer_primaryContainer { - display: flex; - - .mx_PlayPauseButton { - margin-right: 8px; - } - - .mx_AudioPlayer_mediaInfo { - flex: 1; - overflow: hidden; /* makes the ellipsis on the file name work */ - - & > * { - display: block; - } - - .mx_AudioPlayer_mediaName { - color: $primary-content; - font-size: $font-15px; - line-height: $font-15px; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - padding-bottom: 4px; /* mimics the line-height differences in the Figma */ - } - - .mx_AudioPlayer_byline { - font-size: $font-12px; - line-height: $font-12px; - } - } - } - - .mx_AudioPlayer_seek { - display: flex; - align-items: center; - - .mx_SeekBar { - flex: 1; - } - - .mx_Clock { - min-width: $font-42px; /* for flexbox */ - padding-left: $spacing-4; /* isolate from seek bar */ - text-align: justify; - white-space: nowrap; - } - } -} diff --git a/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss b/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss index 9d81097d60..2c78a62f8d 100644 --- a/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss +++ b/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss @@ -15,6 +15,23 @@ Please see LICENSE files in the repository root for full details. } .mx_AccessSecretStorageDialog_primaryContainer { + .mx_AccessSecretStorageDialog_recoveryKeyEntry { + /* + * Be specific here to avoid "margin: 9px" from _common.pcss + */ + :not(.mx_textinput):not(.mx_Field):not(.mx_no_textinput) { + input { + /* + * From figma: https://www.figma.com/design/ZodBLtGnKmRTGJo5SGLnH3/ER-137--Excluding-Insecure-Devices?node-id=102-43729&t=QmewENUd7f6Tmw9U-1 + */ + width: 448px; + height: 70px; + margin: 0px; + border: 1px solid; + } + } + } + .mx_AccessSecretStorageDialog_recoveryKeyFeedback { &::before { content: ""; diff --git a/res/css/views/messages/_MPollBody.pcss b/res/css/views/messages/_MPollBody.pcss index 33144083ea..9889bb81bb 100644 --- a/res/css/views/messages/_MPollBody.pcss +++ b/res/css/views/messages/_MPollBody.pcss @@ -60,7 +60,7 @@ Please see LICENSE files in the repository root for full details. .mx_MPollBody_allOptions { display: grid; - grid-gap: $spacing-16; + gap: $spacing-16; margin-bottom: $spacing-8; max-width: 550px; } diff --git a/res/css/views/polls/pollHistory/_PollHistoryList.pcss b/res/css/views/polls/pollHistory/_PollHistoryList.pcss index 95d54192f9..b732545178 100644 --- a/res/css/views/polls/pollHistory/_PollHistoryList.pcss +++ b/res/css/views/polls/pollHistory/_PollHistoryList.pcss @@ -21,12 +21,12 @@ Please see LICENSE files in the repository root for full details. flex: 1 1 0; align-content: flex-start; display: grid; - grid-gap: $spacing-20; + gap: $spacing-20; padding-right: $spacing-64; margin: $spacing-32 0; &.mx_PollHistoryList_list_ENDED { - grid-gap: $spacing-32; + gap: $spacing-32; } } diff --git a/res/css/views/settings/_NotificationSettings2.pcss b/res/css/views/settings/_NotificationSettings2.pcss index d579c22b95..285282c89c 100644 --- a/res/css/views/settings/_NotificationSettings2.pcss +++ b/res/css/views/settings/_NotificationSettings2.pcss @@ -30,7 +30,7 @@ Please see LICENSE files in the repository root for full details. .mx_SettingsSubsection_content { margin-top: 12px; - grid-gap: 12px; + gap: 12px; justify-items: stretch; justify-content: stretch; } @@ -40,7 +40,7 @@ Please see LICENSE files in the repository root for full details. } .mx_NotificationSettings2_flags { - grid-gap: 4px; + gap: 4px; } .mx_StyledRadioButton_content { diff --git a/res/css/views/settings/_Notifications.pcss b/res/css/views/settings/_Notifications.pcss index e4e450fd58..a97d7529d5 100644 --- a/res/css/views/settings/_Notifications.pcss +++ b/res/css/views/settings/_Notifications.pcss @@ -11,7 +11,7 @@ Please see LICENSE files in the repository root for full details. display: grid; grid-template-columns: auto repeat(3, 62px); place-items: center center; - grid-gap: 8px; + gap: 8px; /* Override StyledRadioButton default styles */ .mx_StyledRadioButton { diff --git a/res/css/views/settings/tabs/_SettingsSection.pcss b/res/css/views/settings/tabs/_SettingsSection.pcss index 997343190d..ce3c9266c3 100644 --- a/res/css/views/settings/tabs/_SettingsSection.pcss +++ b/res/css/views/settings/tabs/_SettingsSection.pcss @@ -34,7 +34,7 @@ Please see LICENSE files in the repository root for full details. .mx_SettingsSection_subSections { display: grid; grid-template-columns: minmax(0, 1fr); - grid-gap: $spacing-32; + gap: $spacing-32; padding: $spacing-16 0; } diff --git a/res/css/views/settings/tabs/_SettingsTab.pcss b/res/css/views/settings/tabs/_SettingsTab.pcss index e0abf08e83..99690d5657 100644 --- a/res/css/views/settings/tabs/_SettingsTab.pcss +++ b/res/css/views/settings/tabs/_SettingsTab.pcss @@ -84,7 +84,7 @@ Please see LICENSE files in the repository root for full details. .mx_SettingsTab_sections { display: grid; grid-template-columns: 1fr; - grid-gap: $spacing-32; + gap: $spacing-32; padding-bottom: $spacing-16; } diff --git a/res/css/views/settings/tabs/user/_KeyboardUserSettingsTab.pcss b/res/css/views/settings/tabs/user/_KeyboardUserSettingsTab.pcss index b3251f3e3c..44452be744 100644 --- a/res/css/views/settings/tabs/user/_KeyboardUserSettingsTab.pcss +++ b/res/css/views/settings/tabs/user/_KeyboardUserSettingsTab.pcss @@ -12,7 +12,7 @@ Please see LICENSE files in the repository root for full details. padding: 0; width: 100%; display: grid; - grid-gap: $spacing-4; + gap: $spacing-4; } .mx_KeyboardShortcut_shortcutRow, diff --git a/sonar-project.properties b/sonar-project.properties index 23333a43cc..31ce8d776f 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -6,7 +6,7 @@ sonar.organization=element-hq sonar.sources=src,res sonar.tests=test,playwright,src -sonar.test.inclusions=test/*,playwright/*,src/**/*.test.tsx +sonar.test.inclusions=test/*,playwright/*,src/**/*.test.* sonar.exclusions=__mocks__,docs,element.io,nginx sonar.cpd.exclusions=src/i18n/strings/*.json diff --git a/src/DateUtils.ts b/src/DateUtils.ts index e788ca09bf..a5e22c0891 100644 --- a/src/DateUtils.ts +++ b/src/DateUtils.ts @@ -13,6 +13,8 @@ import { type Optional } from "matrix-events-sdk"; import { _t, getUserLanguage } from "./languageHandler"; import { getUserTimezone } from "./TimezoneHandler"; +export { formatSeconds } from "./shared-components/utils/DateUtils"; + export const MINUTE_MS = 60000; export const HOUR_MS = MINUTE_MS * 60; export const DAY_MS = HOUR_MS * 24; @@ -180,31 +182,6 @@ export function formatTime(date: Date, showTwelveHour = false, locale?: string): }).format(date); } -export function formatSeconds(inSeconds: number): string { - const isNegative = inSeconds < 0; - inSeconds = Math.abs(inSeconds); - - const hours = Math.floor(inSeconds / (60 * 60)) - .toFixed(0) - .padStart(2, "0"); - const minutes = Math.floor((inSeconds % (60 * 60)) / 60) - .toFixed(0) - .padStart(2, "0"); - const seconds = Math.floor((inSeconds % (60 * 60)) % 60) - .toFixed(0) - .padStart(2, "0"); - - let output = ""; - if (hours !== "00") output += `${hours}:`; - output += `${minutes}:${seconds}`; - - if (isNegative) { - output = "-" + output; - } - - return output; -} - export function formatTimeLeft(inSeconds: number): string { const hours = Math.floor(inSeconds / (60 * 60)).toFixed(0); const minutes = Math.floor((inSeconds % (60 * 60)) / 60).toFixed(0); diff --git a/src/RoomInvite.tsx b/src/RoomInvite.tsx index a02530a1cf..70aabed3de 100644 --- a/src/RoomInvite.tsx +++ b/src/RoomInvite.tsx @@ -26,9 +26,9 @@ export interface IInviteResult { } /** - * Invites multiple addresses to a room - * Simpler interface to utils/MultiInviter but with - * no option to cancel. + * Invites multiple addresses to a room. + * + * Simpler interface to {@link MultiInviter}. * * @param {string} roomId The ID of the room to invite to * @param {string[]} addresses Array of strings of addresses to invite. May be matrix IDs or 3pids. diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx index b63e5b2a00..604dd2142f 100644 --- a/src/TextForEvent.tsx +++ b/src/TextForEvent.tsx @@ -36,9 +36,9 @@ import { RoomSettingsTab } from "./components/views/dialogs/RoomSettingsDialog"; import AccessibleButton from "./components/views/elements/AccessibleButton"; import RightPanelStore from "./stores/right-panel/RightPanelStore"; import { highlightEvent, isLocationEvent } from "./utils/EventUtils"; -import { ElementCall } from "./models/Call"; import { getSenderName } from "./utils/event/getSenderName"; import PosthogTrackers from "./PosthogTrackers.ts"; +import { ElementCallEventType } from "./call-types.ts"; function getRoomMemberDisplayname(client: MatrixClient, event: MatrixEvent, userId = event.getSender()): string { const roomId = event.getRoomId(); @@ -922,7 +922,7 @@ for (const evType of ALL_RULE_TYPES) { } // Add both stable and unstable m.call events -for (const evType of ElementCall.CALL_EVENT_TYPE.names) { +for (const evType of ElementCallEventType.names) { stateHandlers[evType] = textForCallEvent; } diff --git a/src/audio/Playback.ts b/src/audio/Playback.ts index 54d2c710d0..96930d43af 100644 --- a/src/audio/Playback.ts +++ b/src/audio/Playback.ts @@ -15,7 +15,7 @@ import { arrayFastResample } from "../utils/arrays"; import { type IDestroyable } from "../utils/IDestroyable"; import { PlaybackClock } from "./PlaybackClock"; import { createAudioContext, decodeOgg } from "./compat"; -import { clamp } from "../utils/numbers"; +import { clamp } from "../shared-components/utils/numbers"; import { DEFAULT_WAVEFORM, PLAYBACK_WAVEFORM_SAMPLES } from "./consts"; import { PlaybackEncoder } from "../PlaybackEncoder"; diff --git a/src/audio/RecorderWorklet.ts b/src/audio/RecorderWorklet.ts index ec4a143c4e..5f96e686ce 100644 --- a/src/audio/RecorderWorklet.ts +++ b/src/audio/RecorderWorklet.ts @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import { type IAmplitudePayload, type ITimingPayload, PayloadEvent, WORKLET_NAME } from "./consts"; -import { percentageOf } from "../utils/numbers"; +import { percentageOf } from "../shared-components/utils/numbers"; // from AudioWorkletGlobalScope: https://developer.mozilla.org/en-US/docs/Web/API/AudioWorkletGlobalScope declare const currentTime: number; diff --git a/src/audio/VoiceRecording.ts b/src/audio/VoiceRecording.ts index bde86f9dd7..2c5788ef21 100644 --- a/src/audio/VoiceRecording.ts +++ b/src/audio/VoiceRecording.ts @@ -19,7 +19,7 @@ import { PayloadEvent, WORKLET_NAME } from "./consts"; import { UPDATE_EVENT } from "../stores/AsyncStore"; import { createAudioContext } from "./compat"; import { FixedRollingArray } from "../utils/FixedRollingArray"; -import { clamp } from "../utils/numbers"; +import { clamp } from "../shared-components/utils/numbers"; import recorderWorkletFactory from "./recorderWorkletFactory"; const CHANNELS = 1; // stereo isn't important diff --git a/src/call-types.ts b/src/call-types.ts index 6586bcf3b9..9ee870945f 100644 --- a/src/call-types.ts +++ b/src/call-types.ts @@ -6,6 +6,9 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ +import { EventType } from "matrix-js-sdk/src/matrix"; +import { NamespacedValue } from "matrix-js-sdk/src/NamespacedValue"; + export const JitsiCallMemberEventType = "io.element.video.member"; export interface JitsiCallMemberContent { @@ -14,3 +17,9 @@ export interface JitsiCallMemberContent { // Time at which this state event should be considered stale expires_ts: number; } + +// Element Call no longer sends this event type; it only exists to support timeline rendering of +// group calls from a previous iteration of the group VoIP MSCs (MSC3401) which used it. +export const ElementCallEventType = new NamespacedValue(null, EventType.GroupCallPrefix); + +export const ElementCallMemberEventType = new NamespacedValue(null, EventType.GroupCallMemberPrefix); diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 26e127f21f..1769a96400 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -111,6 +111,7 @@ interface IState { backgroundImage?: string; } +const NEW_ROOM_LIST_MIN_WIDTH = 224; /** * This is what our MatrixChat shows when we are logged in. The precise view is * determined by the page_type property. @@ -261,10 +262,15 @@ class LoggedInView extends React.Component { let panelCollapsed: boolean; const useNewRoomList = SettingsStore.getValue("feature_new_room_list"); // TODO decrease this once Spaces launches as it'll no longer need to include the 56px Community Panel - const toggleSize = useNewRoomList ? 224 : 206 - 50; + const toggleSize = useNewRoomList ? NEW_ROOM_LIST_MIN_WIDTH : 206 - 50; + const collapseConfig: ICollapseConfig = { toggleSize, onCollapsed: (collapsed) => { + if (useNewRoomList) { + // The new room list does not support collapsing. + return; + } panelCollapsed = collapsed; if (collapsed) { dis.dispatch({ action: "hide_left_panel" }); @@ -281,11 +287,13 @@ class LoggedInView extends React.Component { this.props.resizeNotifier.startResizing(); }, onResizeStop: () => { - if (!panelCollapsed) window.localStorage.setItem("mx_lhs_size", "" + panelSize); + // Always save the lhs size for the new room list. + if (useNewRoomList || !panelCollapsed) window.localStorage.setItem("mx_lhs_size", "" + panelSize); this.props.resizeNotifier.stopResizing(); }, isItemCollapsed: (domNode) => { - return domNode.classList.contains("mx_LeftPanel_minimized"); + // New rooms list does not support collapsing. + return !useNewRoomList && domNode.classList.contains("mx_LeftPanel_minimized"); }, handler: this.resizeHandler.current ?? undefined, }; @@ -299,8 +307,11 @@ class LoggedInView extends React.Component { } private loadResizerPreferences(): void { + const useNewRoomList = SettingsStore.getValue("feature_new_room_list"); let lhsSize = parseInt(window.localStorage.getItem("mx_lhs_size")!, 10); - if (isNaN(lhsSize)) { + // If the user has not set a size, or for the new room list if the size is less than the minimum width, + // set a default size. + if (isNaN(lhsSize) || (useNewRoomList && lhsSize < NEW_ROOM_LIST_MIN_WIDTH)) { lhsSize = 350; } this.resizer?.forHandleWithId("lp-resizer")?.resize(lhsSize); diff --git a/src/components/views/audio_messages/AudioPlayer.tsx b/src/components/views/audio_messages/AudioPlayer.tsx deleted file mode 100644 index 6f674e504d..0000000000 --- a/src/components/views/audio_messages/AudioPlayer.tsx +++ /dev/null @@ -1,65 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2021-2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import React, { type ReactNode } from "react"; - -import PlayPauseButton from "./PlayPauseButton"; -import { formatBytes } from "../../../utils/FormattingUtils"; -import DurationClock from "./DurationClock"; -import { _t } from "../../../languageHandler"; -import SeekBar from "./SeekBar"; -import PlaybackClock from "./PlaybackClock"; -import AudioPlayerBase from "./AudioPlayerBase"; -import { PlaybackState } from "../../../audio/Playback"; - -export default class AudioPlayer extends AudioPlayerBase { - protected renderFileSize(): string | null { - const bytes = this.props.playback.sizeBytes; - if (!bytes) return null; - - // Not translated here - we're just presenting the data which should already - // be translated if needed. - return `(${formatBytes(bytes)})`; - } - - protected renderComponent(): ReactNode { - // tabIndex=0 to ensure that the whole component becomes a tab stop, where we handle keyboard - // events for accessibility - return ( -
-
- -
- - {this.props.mediaName || _t("timeline|m.audio|unnamed_audio")} - -
- -   {/* easiest way to introduce a gap between the components */} - {this.renderFileSize()} -
-
-
-
- - -
-
- ); - } -} diff --git a/src/components/views/audio_messages/AudioPlayerBase.tsx b/src/components/views/audio_messages/AudioPlayerBase.tsx index 71bea008a2..9da1020634 100644 --- a/src/components/views/audio_messages/AudioPlayerBase.tsx +++ b/src/components/views/audio_messages/AudioPlayerBase.tsx @@ -14,7 +14,7 @@ import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import { _t } from "../../../languageHandler"; import { getKeyBindingsManager } from "../../../KeyBindingsManager"; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; -import type SeekBar from "./SeekBar"; +import type LegacySeekBar from "./LegacySeekBar"; import type PlayPauseButton from "./PlayPauseButton"; export interface IProps { @@ -31,7 +31,7 @@ interface IState { } export default abstract class AudioPlayerBase extends React.PureComponent { - protected seekRef = createRef(); + protected seekRef = createRef(); protected playPauseRef = createRef(); public constructor(props: T) { diff --git a/src/components/views/audio_messages/DurationClock.tsx b/src/components/views/audio_messages/DurationClock.tsx deleted file mode 100644 index 920baa99be..0000000000 --- a/src/components/views/audio_messages/DurationClock.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2021 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; - -import Clock from "./Clock"; -import { type Playback } from "../../../audio/Playback"; - -interface IProps { - playback: Playback; -} - -interface IState { - durationSeconds: number; -} - -/** - * A clock which shows a clip's maximum duration. - */ -export default class DurationClock extends React.PureComponent { - public constructor(props: IProps) { - super(props); - - this.state = { - // we track the duration on state because we won't really know what the clip duration - // is until the first time update, and as a PureComponent we are trying to dedupe state - // updates as much as possible. This is just the easiest way to avoid a forceUpdate() or - // member property to track "did we get a duration". - durationSeconds: this.props.playback.clockInfo.durationSeconds, - }; - } - - public componentDidMount(): void { - this.props.playback.clockInfo.liveData.onUpdate(this.onTimeUpdate); - } - - private onTimeUpdate = (time: number[]): void => { - this.setState({ durationSeconds: time[1] }); - }; - - public render(): React.ReactNode { - return ; - } -} diff --git a/src/components/views/audio_messages/SeekBar.tsx b/src/components/views/audio_messages/LegacySeekBar.tsx similarity index 94% rename from src/components/views/audio_messages/SeekBar.tsx rename to src/components/views/audio_messages/LegacySeekBar.tsx index 587975ce1b..798a4322b7 100644 --- a/src/components/views/audio_messages/SeekBar.tsx +++ b/src/components/views/audio_messages/LegacySeekBar.tsx @@ -10,7 +10,7 @@ import React, { type ChangeEvent, type CSSProperties, type ReactNode } from "rea import { type PlaybackInterface } from "../../../audio/Playback"; import { MarkedExecution } from "../../../utils/MarkedExecution"; -import { percentageOf } from "../../../utils/numbers"; +import { percentageOf } from "../../../shared-components/utils/numbers"; import { _t } from "../../../languageHandler"; interface IProps { @@ -35,7 +35,10 @@ interface ISeekCSS extends CSSProperties { const ARROW_SKIP_SECONDS = 5; // arbitrary -export default class SeekBar extends React.PureComponent { +/** + * @deprecated Use {@link SeekBar} instead. + */ +export default class LegacySeekBar extends React.PureComponent { // We use an animation frame request to avoid overly spamming prop updates, even if we aren't // really using anything demanding on the CSS front. diff --git a/src/components/views/audio_messages/LiveRecordingClock.tsx b/src/components/views/audio_messages/LiveRecordingClock.tsx index 74415566a6..7807f30b72 100644 --- a/src/components/views/audio_messages/LiveRecordingClock.tsx +++ b/src/components/views/audio_messages/LiveRecordingClock.tsx @@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import { type IRecordingUpdate } from "../../../audio/VoiceRecording"; -import Clock from "./Clock"; +import { Clock } from "../../../shared-components/audio/Clock"; import { MarkedExecution } from "../../../utils/MarkedExecution"; import { type VoiceMessageRecording } from "../../../audio/VoiceMessageRecording"; diff --git a/src/components/views/audio_messages/PlaybackClock.tsx b/src/components/views/audio_messages/PlaybackClock.tsx index 0d69793379..9dc6cdced3 100644 --- a/src/components/views/audio_messages/PlaybackClock.tsx +++ b/src/components/views/audio_messages/PlaybackClock.tsx @@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details. import React from "react"; -import Clock from "./Clock"; +import { Clock } from "../../../shared-components/audio/Clock"; import { type Playback, PlaybackState } from "../../../audio/Playback"; import { UPDATE_EVENT } from "../../../stores/AsyncStore"; diff --git a/src/components/views/audio_messages/PlaybackWaveform.tsx b/src/components/views/audio_messages/PlaybackWaveform.tsx index c1f470f4b1..895348fb51 100644 --- a/src/components/views/audio_messages/PlaybackWaveform.tsx +++ b/src/components/views/audio_messages/PlaybackWaveform.tsx @@ -11,7 +11,7 @@ import React from "react"; import { arraySeed, arrayTrimFill } from "../../../utils/arrays"; import Waveform from "./Waveform"; import { type Playback } from "../../../audio/Playback"; -import { percentageOf } from "../../../utils/numbers"; +import { percentageOf } from "../../../shared-components/utils/numbers"; import { PLAYBACK_WAVEFORM_SAMPLES } from "../../../audio/consts"; interface IProps { diff --git a/src/components/views/audio_messages/RecordingPlayback.tsx b/src/components/views/audio_messages/RecordingPlayback.tsx index c0e3337787..75e1419ce6 100644 --- a/src/components/views/audio_messages/RecordingPlayback.tsx +++ b/src/components/views/audio_messages/RecordingPlayback.tsx @@ -11,7 +11,7 @@ import React, { type ReactNode } from "react"; import PlayPauseButton from "./PlayPauseButton"; import PlaybackClock from "./PlaybackClock"; import AudioPlayerBase, { type IProps as IAudioPlayerBaseProps } from "./AudioPlayerBase"; -import SeekBar from "./SeekBar"; +import LegacySeekBar from "./LegacySeekBar"; import PlaybackWaveform from "./PlaybackWaveform"; import { PlaybackState } from "../../../audio/Playback"; @@ -49,7 +49,7 @@ export default class RecordingPlayback extends AudioPlayerBase { <>
- { - private inputRef = React.createRef(); + private inputRef = React.createRef(); public constructor(props: IProps) { super(props); @@ -119,7 +118,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent): void => { + private onRecoveryKeyChange = (ev: ChangeEvent): void => { this.setState({ recoveryKey: ev.target.value, }); @@ -181,17 +180,14 @@ export default class AccessSecretStorageDialog extends React.PureComponent
-
diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx index cfb95abcc3..027f25cc3a 100644 --- a/src/components/views/elements/AppTile.tsx +++ b/src/components/views/elements/AppTile.tsx @@ -62,6 +62,20 @@ import { parseUrl } from "../../../utils/UrlUtils"; import RightPanelStore from "../../../stores/right-panel/RightPanelStore.ts"; import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases.ts"; +// Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin +// because that would allow the iframe to programmatically remove the sandbox attribute, but +// this would only be for content hosted on the same origin as the element client: anything +// hosted on the same origin as the client will get the same access as if you clicked +// a link to it. +const sandboxFlags = + "allow-forms allow-popups allow-popups-to-escape-sandbox " + + "allow-same-origin allow-scripts allow-presentation allow-downloads"; + +// Additional iframe feature permissions +// (see - https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-permissions-in-cross-origin-iframes and https://wicg.github.io/feature-policy/) +const iframeFeatures = + "microphone; camera; encrypted-media; autoplay; display-capture; clipboard-write; clipboard-read;"; + interface IProps { app: IWidget | IApp; // If room is not specified then it is an account level widget @@ -138,7 +152,7 @@ export default class AppTile extends React.Component { }; private contextMenuButton = createRef(); - private iframe?: HTMLIFrameElement; // ref to the iframe (callback style) + private iframeParent: HTMLElement | null = null; // parent div of the iframe private allowedWidgetsWatchRef?: string; private persistKey: string; private sgWidget?: StopGapWidget; @@ -397,18 +411,47 @@ export default class AppTile extends React.Component { }); } + /** + * Creates the widget iframe and opens communication with the widget. + */ private startMessaging(): void { - try { - this.sgWidget?.startMessaging(this.iframe!); - } catch (e) { - logger.error("Failed to start widget", e); - } + // We create the iframe ourselves rather than leaving the job to React, + // because we need the lifetime of the messaging and the iframe to be + // the same; we don't want strict mode, for instance, to cause the + // messaging to restart (lose its state) without also killing the widget + const iframe = document.createElement("iframe"); + iframe.title = WidgetUtils.getWidgetName(this.props.app); + iframe.allow = iframeFeatures; + iframe.src = this.sgWidget!.embedUrl; + iframe.allowFullscreen = true; + iframe.sandbox = sandboxFlags; + this.iframeParent!.appendChild(iframe); + // In order to start the widget messaging we need iframe.contentWindow + // to exist. Waiting until the next layout gives the browser a chance to + // initialize it. + requestAnimationFrame(() => { + // Handle the race condition (seen in strict mode) where the element + // is added and then removed before we enter this callback + if (iframe.parentElement === null) return; + try { + this.sgWidget?.startMessaging(iframe); + } catch (e) { + logger.error("Failed to start widget", e); + } + }); } - private iframeRefChange = (ref: HTMLIFrameElement): void => { - this.iframe = ref; + /** + * Callback ref for the parent div of the iframe. + */ + private iframeParentRef = (element: HTMLElement | null): void => { if (this.unmounted) return; - if (ref) { + // Detach the existing iframe (if any) from the document so we know not + // to do anything further with it, like starting up the messaging + this.iframeParent?.querySelector("iframe")?.remove(); + this.iframeParent = element; + + if (element && this.sgWidget) { this.startMessaging(); } else { this.resetWidget(this.props); @@ -426,24 +469,8 @@ export default class AppTile extends React.Component { /** * Ends all widget interaction, such as cancelling calls and disabling webcams. - * @private - * @returns {Promise<*>} Resolves when the widget is terminated, or timeout passed. */ - private async endWidgetActions(): Promise { - // widget migration dev note: async to maintain signature - // HACK: This is a really dirty way to ensure that Jitsi cleans up - // its hold on the webcam. Without this, the widget holds a media - // stream open, even after death. See https://github.com/vector-im/element-web/issues/7351 - if (this.iframe) { - // In practice we could just do `+= ''` to trick the browser - // into thinking the URL changed, however I can foresee this - // being optimized out by a browser. Instead, we'll just point - // the iframe at a page that is reasonably safe to use in the - // event the iframe doesn't wink away. - // This is relative to where the Element instance is located. - this.iframe.src = "about:blank"; - } - + private endWidgetActions(): void { if (WidgetType.JITSI.matches(this.props.app.type) && this.props.room) { LegacyCallHandler.instance.hangupCallApp(this.props.room.roomId); } @@ -457,6 +484,7 @@ export default class AppTile extends React.Component { this.sgWidget?.stopMessaging({ forceDestroy: true }); } + private onWidgetReady = (): void => { this.setState({ loading: false }); }; @@ -554,16 +582,11 @@ export default class AppTile extends React.Component { } private reload(): void { - this.endWidgetActions().then(() => { - // reset messaging - this.resetWidget(this.props); - this.startMessaging(); - - if (this.iframe && this.sgWidget) { - // Reload iframe - this.iframe.src = this.sgWidget.embedUrl; - } - }); + this.endWidgetActions(); + // reset messaging + this.resetWidget(this.props); + this.iframeParent?.querySelector("iframe")?.remove(); + this.startMessaging(); } // TODO replace with full screen interactions @@ -621,20 +644,6 @@ export default class AppTile extends React.Component { public render(): React.ReactNode { let appTileBody: JSX.Element | undefined; - // Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin - // because that would allow the iframe to programmatically remove the sandbox attribute, but - // this would only be for content hosted on the same origin as the element client: anything - // hosted on the same origin as the client will get the same access as if you clicked - // a link to it. - const sandboxFlags = - "allow-forms allow-popups allow-popups-to-escape-sandbox " + - "allow-same-origin allow-scripts allow-presentation allow-downloads"; - - // Additional iframe feature permissions - // (see - https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-permissions-in-cross-origin-iframes and https://wicg.github.io/feature-policy/) - const iframeFeatures = - "microphone; camera; encrypted-media; autoplay; display-capture; clipboard-write; " + "clipboard-read;"; - const appTileBodyClass = classNames({ "mx_AppTileBody": true, "mx_AppTileBody--large": !this.props.miniMode, @@ -654,8 +663,6 @@ export default class AppTile extends React.Component {
); - const widgetTitle = WidgetUtils.getWidgetName(this.props.app); - if (this.sgWidget === null) { appTileBody = (
@@ -692,16 +699,8 @@ export default class AppTile extends React.Component { } else if (this.sgWidget) { appTileBody = ( <> -
+
{this.state.loading && loadingElement} -