diff --git a/.eslintrc.js b/.eslintrc.js index 327119f045..3eefd22206 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -78,6 +78,11 @@ module.exports = { name: "matrix-react-sdk/", message: "Please use matrix-react-sdk/src/index instead", }, + { + name: "emojibase-regex", + message: + "This regex doesn't actually test for emoji. See the docs at https://emojibase.dev/docs/regex/ and prefer our own EMOJI_REGEX from HtmlUtils.", + }, ], patterns: [ { @@ -115,13 +120,9 @@ module.exports = { "!matrix-js-sdk/src/extensible_events_v1/InvalidEventError", "!matrix-js-sdk/src/crypto", "!matrix-js-sdk/src/crypto/aes", - "!matrix-js-sdk/src/crypto/olmlib", - "!matrix-js-sdk/src/crypto/crypto", "!matrix-js-sdk/src/crypto/keybackup", - "!matrix-js-sdk/src/crypto/RoomList", "!matrix-js-sdk/src/crypto/deviceinfo", "!matrix-js-sdk/src/crypto/key_passphrase", - "!matrix-js-sdk/src/crypto/CrossSigning", "!matrix-js-sdk/src/crypto/recoverykey", "!matrix-js-sdk/src/crypto/dehydration", "!matrix-js-sdk/src/oidc", @@ -144,6 +145,11 @@ module.exports = { ], message: "Please use matrix-js-sdk/src/matrix instead", }, + { + group: ["emojibase-regex/emoji*"], + message: + "This regex doesn't actually test for emoji. See the docs at https://emojibase.dev/docs/regex/ and prefer our own EMOJI_REGEX from HtmlUtils.", + }, ], }, ], diff --git a/.github/workflows/end-to-end-tests.yaml b/.github/workflows/end-to-end-tests.yaml index 832e0d5f4e..acd59406e9 100644 --- a/.github/workflows/end-to-end-tests.yaml +++ b/.github/workflows/end-to-end-tests.yaml @@ -56,6 +56,7 @@ jobs: - uses: actions/setup-node@v4 with: cache: "yarn" + node-version: "lts/*" - name: Fetch layered build id: layered_build @@ -103,7 +104,7 @@ jobs: fail-fast: false matrix: # Run multiple instances in parallel to speed up the tests - runner: [1, 2, 3, 4, 5, 6, 7, 8] + runner: [1, 2, 3, 4, 5, 6] steps: - uses: actions/checkout@v4 with: @@ -121,6 +122,7 @@ jobs: with: cache: "yarn" cache-dependency-path: matrix-react-sdk/yarn.lock + node-version: "lts/*" - name: Install dependencies working-directory: matrix-react-sdk @@ -145,10 +147,8 @@ jobs: run: yarn playwright install --with-deps - name: Run Playwright tests - uses: coactions/setup-xvfb@6b00cf1889f4e1d5a48635647013c0508128ee1a - with: - run: yarn playwright test --shard ${{ matrix.runner }}/${{ strategy.job-total }} - working-directory: matrix-react-sdk + run: yarn playwright test --shard ${{ matrix.runner }}/${{ strategy.job-total }} + working-directory: matrix-react-sdk - name: Upload blob report to GitHub Actions Artifacts if: always() @@ -174,6 +174,7 @@ jobs: if: inputs.skip != true with: cache: "yarn" + node-version: "lts/*" - name: Install dependencies if: inputs.skip != true diff --git a/.github/workflows/playwright-image-updates.yaml b/.github/workflows/playwright-image-updates.yaml index 15bea28e0f..a160b77bcf 100644 --- a/.github/workflows/playwright-image-updates.yaml +++ b/.github/workflows/playwright-image-updates.yaml @@ -7,7 +7,7 @@ jobs: update: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Update matrixdotorg/synapse image run: | @@ -20,7 +20,7 @@ jobs: - name: Create Pull Request id: cpr - uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 # v5 + uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6 with: token: ${{ secrets.ELEMENT_BOT_TOKEN }} branch: actions/playwright-image-updates diff --git a/.github/workflows/pull_request_base_branch.yaml b/.github/workflows/pull_request_base_branch.yaml index 13542a30f4..49d7bcef7c 100644 --- a/.github/workflows/pull_request_base_branch.yaml +++ b/.github/workflows/pull_request_base_branch.yaml @@ -7,7 +7,7 @@ jobs: name: Check PR base branch runs-on: ubuntu-latest steps: - - uses: actions/github-script@v3 + - uses: actions/github-script@v7 with: script: | const baseBranch = context.payload.pull_request.base.ref; diff --git a/.github/workflows/static_analysis.yaml b/.github/workflows/static_analysis.yaml index 6e225467af..94ed2c7488 100644 --- a/.github/workflows/static_analysis.yaml +++ b/.github/workflows/static_analysis.yaml @@ -25,6 +25,7 @@ jobs: - uses: actions/setup-node@v4 with: cache: "yarn" + node-version: "lts/*" - name: Install Deps run: "./scripts/ci/install-deps.sh" @@ -83,6 +84,7 @@ jobs: - uses: actions/setup-node@v4 with: cache: "yarn" + node-version: "lts/*" # Does not need branch matching as only analyses this layer - name: Install Deps @@ -100,6 +102,7 @@ jobs: - uses: actions/setup-node@v4 with: cache: "yarn" + node-version: "lts/*" # Does not need branch matching as only analyses this layer - name: Install Deps @@ -117,6 +120,7 @@ jobs: - uses: actions/setup-node@v4 with: cache: "yarn" + node-version: "lts/*" # Does not need branch matching as only analyses this layer - name: Install Deps @@ -134,6 +138,7 @@ jobs: - uses: actions/setup-node@v4 with: cache: "yarn" + node-version: "lts/*" - name: Install Deps run: "scripts/ci/layered.sh" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3815c4fb4c..e8418a9519 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -44,6 +44,7 @@ jobs: - name: Yarn cache uses: actions/setup-node@v4 with: + node-version: "lts/*" cache: "yarn" - name: Install Deps @@ -115,6 +116,7 @@ jobs: - uses: actions/setup-node@v4 with: cache: "yarn" + node-version: "lts/*" - name: Run tests run: "./scripts/ci/app-tests.sh" diff --git a/jest.config.ts b/jest.config.ts index 182c28f68a..7293e5b3be 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -22,7 +22,7 @@ const config: Config = { testEnvironment: "jsdom", testMatch: ["/test/**/*-test.[jt]s?(x)"], globalSetup: "/test/globalSetup.ts", - setupFiles: ["jest-canvas-mock"], + setupFiles: ["jest-canvas-mock", "web-streams-polyfill/polyfill"], setupFilesAfterEnv: ["/test/setupTests.ts"], moduleNameMapper: { "\\.(gif|png|ttf|woff2)$": "/__mocks__/imageMock.js", diff --git a/package.json b/package.json index 0b6deb0f65..4b4cecd9f4 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "test:playwright:open": "yarn test:playwright --ui", "test:playwright:screenshots": "yarn test:playwright:screenshots:build && yarn test:playwright:screenshots:run", "test:playwright:screenshots:build": "docker build playwright -t matrix-react-sdk-playwright", - "test:playwright:screenshots:run": "docker run --rm --network host -v $(pwd)/../:/work/ -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/:/tmp/ -it matrix-react-sdk-playwright", + "test:playwright:screenshots:run": "docker run --rm --network host -e BASE_URL -v $(pwd)/../:/work/ -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/:/tmp/ -it matrix-react-sdk-playwright", "coverage": "yarn test --coverage", "lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'" }, @@ -65,20 +65,20 @@ "@types/seedrandom": "3.0.8", "oidc-client-ts": "3.0.1", "jwt-decode": "4.0.0", - "@floating-ui/react": "0.26.11" + "@floating-ui/react": "0.26.11", + "@radix-ui/react-id": "1.1.0" }, "dependencies": { "@babel/runtime": "^7.12.5", "@matrix-org/analytics-events": "^0.23.0", "@matrix-org/emojibase-bindings": "^1.1.2", - "@matrix-org/matrix-wysiwyg": "2.37.3", - "@matrix-org/olm": "3.2.15", + "@matrix-org/matrix-wysiwyg": "2.37.4", "@matrix-org/react-sdk-module-api": "^2.4.0", "@matrix-org/spec": "^1.7.0", "@sentry/browser": "^8.0.0", "@testing-library/react-hooks": "^8.0.1", "@vector-im/compound-design-tokens": "^1.2.0", - "@vector-im/compound-web": "^4.9.0", + "@vector-im/compound-web": "^5.2.3", "@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4", "@zxcvbn-ts/language-en": "^3.0.2", @@ -96,7 +96,6 @@ "filesize": "10.1.2", "github-markdown-css": "^5.5.1", "glob-to-regexp": "^0.4.1", - "graphemer": "^1.4.0", "highlight.js": "^11.3.1", "html-entities": "^2.0.0", "is-ip": "^3.1.0", @@ -119,8 +118,7 @@ "opus-recorder": "^8.0.3", "pako": "^2.0.3", "png-chunks-extract": "^1.0.0", - "posthog-js": "1.139.2", - "proposal-temporal": "^0.9.0", + "posthog-js": "1.141.3", "qrcode": "1.5.3", "re-resizable": "^6.9.0", "react": "17.0.2", @@ -133,6 +131,7 @@ "sanitize-filename": "^1.6.3", "sanitize-html": "2.13.0", "tar-js": "^0.3.0", + "temporal-polyfill": "^0.2.5", "ua-parser-js": "^1.0.2", "uuid": "^10.0.0", "what-input": "^5.2.10" @@ -188,7 +187,7 @@ "@types/seedrandom": "3.0.8", "@types/tar-js": "^0.3.2", "@types/ua-parser-js": "^0.7.36", - "@types/uuid": "^9.0.2", + "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/parser": "^7.0.0", "axe-core": "4.9.1", @@ -204,7 +203,7 @@ "eslint-plugin-matrix-org": "1.2.1", "eslint-plugin-react": "^7.28.0", "eslint-plugin-react-hooks": "^4.3.0", - "eslint-plugin-unicorn": "^53.0.0", + "eslint-plugin-unicorn": "^54.0.0", "express": "^4.18.2", "fake-indexeddb": "^6.0.0", "fetch-mock-jest": "^1.5.1", @@ -227,7 +226,8 @@ "stylelint-config-standard": "^36.0.0", "stylelint-scss": "^6.0.0", "ts-node": "^10.9.1", - "typescript": "5.4.5" + "typescript": "5.5.2", + "web-streams-polyfill": "^4.0.0" }, "peerDependencies": { "postcss": "^8.4.19", diff --git a/playwright/Dockerfile b/playwright/Dockerfile index 7179e08ab0..f20a77b952 100644 --- a/playwright/Dockerfile +++ b/playwright/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/playwright:v1.44.1-jammy +FROM mcr.microsoft.com/playwright:v1.45.0-jammy WORKDIR /work/matrix-react-sdk VOLUME ["/work/element-web/node_modules"] diff --git a/playwright/e2e/audio-player/audio-player.spec.ts b/playwright/e2e/audio-player/audio-player.spec.ts index 4581801db5..e60d273299 100644 --- a/playwright/e2e/audio-player/audio-player.spec.ts +++ b/playwright/e2e/audio-player/audio-player.spec.ts @@ -160,7 +160,7 @@ test.describe("Audio player", () => { // Enable high contrast manually const settings = await app.settings.openUserSettings("Appearance"); - await settings.getByTestId("mx_ThemeChoicePanel").getByText("Use high contrast").click(); + await settings.getByRole("radio", { name: "High contrast" }).click(); await app.closeDialog(); diff --git a/playwright/e2e/crypto/crypto.spec.ts b/playwright/e2e/crypto/crypto.spec.ts index 995c37d358..98f75d54e1 100644 --- a/playwright/e2e/crypto/crypto.spec.ts +++ b/playwright/e2e/crypto/crypto.spec.ts @@ -103,7 +103,7 @@ const verify = async (page: Page, bob: Bot) => { const bobsVerificationRequestPromise = waitForVerificationRequest(bob); const roomInfo = await openRoomInfo(page); - await roomInfo.getByRole("menuitem", { name: "People" }).click(); + await page.locator(".mx_RightPanelTabs").getByText("People").click(); await roomInfo.getByText("Bob").click(); await roomInfo.getByRole("button", { name: "Verify" }).click(); await roomInfo.getByRole("button", { name: "Start Verification" }).click(); @@ -279,7 +279,7 @@ test.describe("Cryptography", function () { // Assert that verified icon is rendered await page.getByRole("button", { name: "Room members" }).click(); - await page.getByRole("button", { name: "Room information" }).click(); + await page.locator(".mx_RightPanelTabs").getByText("Info").click(); await expect(page.locator('.mx_RoomSummaryCard_badges [data-kind="success"]')).toContainText("Encrypted"); // Take a snapshot of RoomSummaryCard with a verified E2EE icon diff --git a/playwright/e2e/crypto/dehydration.spec.ts b/playwright/e2e/crypto/dehydration.spec.ts index 13da99ccad..eb9efde4ee 100644 --- a/playwright/e2e/crypto/dehydration.spec.ts +++ b/playwright/e2e/crypto/dehydration.spec.ts @@ -102,7 +102,7 @@ test.describe("Dehydration", () => { await viewRoomSummaryByName(page, app, ROOM_NAME); - await page.getByRole("menuitem", { name: "People" }).click(); + await page.locator(".mx_RightPanelTabs").getByText("People").click(); await expect(page.locator(".mx_MemberList")).toBeVisible(); await getMemberTileByName(page, NAME).click(); diff --git a/playwright/e2e/crypto/verification.spec.ts b/playwright/e2e/crypto/verification.spec.ts index 93344d2c08..167c302b47 100644 --- a/playwright/e2e/crypto/verification.spec.ts +++ b/playwright/e2e/crypto/verification.spec.ts @@ -45,7 +45,6 @@ test.describe("Device verification", () => { // Create a new device for alice aliceBotClient = new Bot(page, homeserver, { - rustCrypto: true, bootstrapCrossSigning: true, bootstrapSecretStorage: true, }); diff --git a/playwright/e2e/forgot-password/forgot-password.spec.ts b/playwright/e2e/forgot-password/forgot-password.spec.ts new file mode 100644 index 0000000000..260242ebc6 --- /dev/null +++ b/playwright/e2e/forgot-password/forgot-password.spec.ts @@ -0,0 +1,77 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { expect, test } from "../../element-web-test"; +import { selectHomeserver } from "../utils"; + +const username = "user1234"; +// this has to be password-like enough to please zxcvbn. Needless to say it's just from pwgen. +const password = "oETo7MPf0o"; +const email = "user@nowhere.dummy"; + +test.describe("Forgot Password", () => { + test.use({ + startHomeserverOpts: ({ mailhog }, use) => + use({ + template: "email", + variables: { + SMTP_HOST: "host.containers.internal", + SMTP_PORT: mailhog.instance.smtpPort, + }, + }), + }); + + test("renders properly", async ({ page, homeserver }) => { + await page.goto("/"); + + await page.getByRole("link", { name: "Sign in" }).click(); + + // need to select a homeserver at this stage, before entering the forgot password flow + await selectHomeserver(page, homeserver.config.baseUrl); + + await page.getByRole("button", { name: "Forgot password?" }).click(); + + await expect(page.getByRole("main")).toMatchScreenshot("forgot-password.png"); + }); + + test("renders email verification dialog properly", async ({ page, homeserver }) => { + const user = await homeserver.registerUser(username, password); + + await homeserver.setThreepid(user.userId, "email", email); + + await page.goto("/"); + + await page.getByRole("link", { name: "Sign in" }).click(); + await selectHomeserver(page, homeserver.config.baseUrl); + + await page.getByRole("button", { name: "Forgot password?" }).click(); + + await page.getByRole("textbox", { name: "Email address" }).fill(email); + + await page.getByRole("button", { name: "Send email" }).click(); + + await page.getByRole("button", { name: "Next" }).click(); + + await page.getByRole("textbox", { name: "New Password", exact: true }).fill(password); + await page.getByRole("textbox", { name: "Confirm new password", exact: true }).fill(password); + + await page.getByRole("button", { name: "Reset password" }).click(); + + await expect(page.getByRole("button", { name: "Resend" })).toBeInViewport(); + + await expect(page.locator(".mx_Dialog")).toMatchScreenshot("forgot-password-verify-email.png"); + }); +}); diff --git a/playwright/e2e/lazy-loading/lazy-loading.spec.ts b/playwright/e2e/lazy-loading/lazy-loading.spec.ts index 8b81589813..c04bcb8c64 100644 --- a/playwright/e2e/lazy-loading/lazy-loading.spec.ts +++ b/playwright/e2e/lazy-loading/lazy-loading.spec.ts @@ -80,7 +80,7 @@ test.describe("Lazy Loading", () => { async function openMemberlist(page: Page): Promise { await page.locator(".mx_LegacyRoomHeader").getByRole("button", { name: "Room info" }).click(); - await page.locator(".mx_RoomSummaryCard").getByRole("menuitem", { name: "People" }).click(); // \d represents the number of the room members + await page.locator(".mx_RightPanelTabs").getByText("People").click(); } function getMemberInMemberlist(page: Page, name: string): Locator { diff --git a/playwright/e2e/login/login.spec.ts b/playwright/e2e/login/login.spec.ts index b1b02c0a9a..fc8de6499e 100644 --- a/playwright/e2e/login/login.spec.ts +++ b/playwright/e2e/login/login.spec.ts @@ -14,11 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Page } from "@playwright/test"; - import { expect, test } from "../../element-web-test"; import { doTokenRegistration } from "./utils"; import { isDendrite } from "../../plugins/homeserver/dendrite"; +import { selectHomeserver } from "../utils"; test.describe("Login", () => { test.describe("Password login", () => { @@ -85,17 +84,6 @@ test.describe("Login", () => { await expect(page).toHaveURL(/\/#\/room\/!room:id$/); await expect(page.getByRole("button", { name: "Join the discussion" })).toBeVisible(); }); - - async function selectHomeserver(page: Page, homeserverUrl: string) { - await page.getByRole("button", { name: "Edit" }).click(); - await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserverUrl); - await page.getByRole("button", { name: "Continue", exact: true }).click(); - // wait for the dialog to go away - await expect(page.locator(".mx_ServerPickerDialog")).toHaveCount(0); - - await expect(page.locator(".mx_Spinner")).toHaveCount(0); - await expect(page.locator(".mx_ServerPicker_server")).toHaveText(homeserverUrl); - } }); // tests for old-style SSO login, in which we exchange tokens with Synapse, and Synapse talks to an auth server diff --git a/playwright/e2e/read-receipts/index.ts b/playwright/e2e/read-receipts/index.ts index 4dd0450fb9..484df2251d 100644 --- a/playwright/e2e/read-receipts/index.ts +++ b/playwright/e2e/read-receipts/index.ts @@ -399,11 +399,10 @@ class Helpers { } /** - * Close the threads panel. (Actually, close any right panel, but for these - * tests we only open the threads panel.) + * Close the threads panel. */ async closeThreadsPanel() { - await this.page.locator(".mx_RightPanel").getByLabel("Close").click(); + await this.page.locator(".mx_LegacyRoomHeader").getByLabel("Threads").click(); await expect(this.page.locator(".mx_RightPanel")).not.toBeVisible(); } @@ -411,7 +410,7 @@ class Helpers { * Return to the list of threads, given we are viewing a single thread. */ async backToThreadsList() { - await this.page.locator(".mx_RightPanel").getByLabel("Threads").click(); + await this.page.locator(".mx_LegacyRoomHeader").getByLabel("Threads").click(); } /** diff --git a/playwright/e2e/right-panel/right-panel.spec.ts b/playwright/e2e/right-panel/right-panel.spec.ts index 4f578748d6..e323a4b24f 100644 --- a/playwright/e2e/right-panel/right-panel.spec.ts +++ b/playwright/e2e/right-panel/right-panel.spec.ts @@ -113,7 +113,7 @@ test.describe("RightPanel", () => { test("should handle viewing room member", async ({ page, app }) => { await viewRoomSummaryByName(page, app, ROOM_NAME); - await page.getByRole("menuitem", { name: "People" }).click(); + await page.locator(".mx_RightPanelTabs").getByText("People").click(); await expect(page.locator(".mx_MemberList")).toBeVisible(); await getMemberTileByName(page, NAME).click(); @@ -123,7 +123,7 @@ test.describe("RightPanel", () => { await page.getByRole("button", { name: "Room members" }).click(); await expect(page.locator(".mx_MemberList")).toBeVisible(); - await page.getByRole("button", { name: "Room information" }).click(); + await page.locator(".mx_RightPanelTabs").getByText("Info").click(); await checkRoomSummaryCard(page, ROOM_NAME); }); }); diff --git a/playwright/e2e/settings/appearance-user-settings-tab.spec.ts b/playwright/e2e/settings/appearance-user-settings-tab.spec.ts deleted file mode 100644 index 7e16d73955..0000000000 --- a/playwright/e2e/settings/appearance-user-settings-tab.spec.ts +++ /dev/null @@ -1,219 +0,0 @@ -/* -Copyright 2023 Suguru Hirahara - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { test, expect } from "../../element-web-test"; -import { SettingLevel } from "../../../src/settings/SettingLevel"; - -test.describe("Appearance user settings tab", () => { - test.use({ - displayName: "Hanako", - }); - - test("should be rendered properly", async ({ page, user, app }) => { - const tab = await app.settings.openUserSettings("Appearance"); - - // Click "Show advanced" link button - await tab.getByRole("button", { name: "Show advanced" }).click(); - - // Assert that "Hide advanced" link button is rendered - await expect(tab.getByRole("button", { name: "Hide advanced" })).toBeVisible(); - - await expect(tab).toMatchScreenshot("appearance-tab.png"); - }); - - test("should support switching layouts", async ({ page, user, app }) => { - // Create and view a room first - await app.client.createRoom({ name: "Test Room" }); - await app.viewRoomByName("Test Room"); - - await app.settings.openUserSettings("Appearance"); - - const buttons = page.locator(".mx_LayoutSwitcher_RadioButton"); - - // Assert that the layout selected by default is "Modern" - await expect( - buttons.locator(".mx_StyledRadioButton_enabled", { - hasText: "Modern", - }), - ).toBeVisible(); - - // Assert that the room layout is set to group (modern) layout - await expect(page.locator(".mx_RoomView_body[data-layout='group']")).toBeVisible(); - - // Select the first layout - await buttons.first().click(); - // Assert that the layout selected is "IRC (Experimental)" - await expect(buttons.locator(".mx_StyledRadioButton_enabled", { hasText: "IRC (Experimental)" })).toBeVisible(); - - // Assert that the room layout is set to IRC layout - await expect(page.locator(".mx_RoomView_body[data-layout='irc']")).toBeVisible(); - - // Select the last layout - await buttons.last().click(); - - // Assert that the layout selected is "Message bubbles" - await expect(buttons.locator(".mx_StyledRadioButton_enabled", { hasText: "Message bubbles" })).toBeVisible(); - - // Assert that the room layout is set to bubble layout - await expect(page.locator(".mx_RoomView_body[data-layout='bubble']")).toBeVisible(); - }); - - test("should support changing font size by using the font size dropdown", async ({ page, app, user }) => { - await app.settings.openUserSettings("Appearance"); - - const tab = page.getByTestId("mx_AppearanceUserSettingsTab"); - const fontDropdown = tab.locator(".mx_FontScalingPanel_Dropdown"); - await expect(fontDropdown.getByLabel("Font size")).toBeVisible(); - - // Default browser font size is 16px and the select value is 0 - // -4 value is 12px - await fontDropdown.getByLabel("Font size").selectOption({ value: "-4" }); - - await expect(page).toMatchScreenshot("window-12px.png"); - }); - - test("should support enabling compact group (modern) layout", async ({ page, app, user }) => { - // Create and view a room first - await app.client.createRoom({ name: "Test Room" }); - await app.viewRoomByName("Test Room"); - - await app.settings.openUserSettings("Appearance"); - - // Click "Show advanced" link button - const tab = page.getByTestId("mx_AppearanceUserSettingsTab"); - await tab.getByRole("button", { name: "Show advanced" }).click(); - - await tab.locator("label", { hasText: "Use a more compact 'Modern' layout" }).click(); - - // Assert that the room layout is set to compact group (modern) layout - await expect(page.locator("#matrixchat .mx_MatrixChat_wrapper.mx_MatrixChat_useCompactLayout")).toBeVisible(); - }); - - test("should disable compact group (modern) layout option on IRC layout and bubble layout", async ({ - page, - app, - user, - }) => { - await app.settings.openUserSettings("Appearance"); - const tab = page.getByTestId("mx_AppearanceUserSettingsTab"); - - const checkDisabled = async () => { - await expect(tab.getByRole("checkbox", { name: "Use a more compact 'Modern' layout" })).toBeDisabled(); - }; - - // Click "Show advanced" link button - await tab.getByRole("button", { name: "Show advanced" }).click(); - - const buttons = page.locator(".mx_LayoutSwitcher_RadioButton"); - - // Enable IRC layout - await buttons.first().click(); - - // Assert that the layout selected is "IRC (Experimental)" - await expect(buttons.locator(".mx_StyledRadioButton_enabled", { hasText: "IRC (Experimental)" })).toBeVisible(); - - await checkDisabled(); - - // Enable bubble layout - await buttons.last().click(); - - // Assert that the layout selected is "IRC (Experimental)" - await expect(buttons.locator(".mx_StyledRadioButton_enabled", { hasText: "Message bubbles" })).toBeVisible(); - - await checkDisabled(); - }); - - test("should support enabling system font", async ({ page, app, user }) => { - await app.settings.openUserSettings("Appearance"); - const tab = page.getByTestId("mx_AppearanceUserSettingsTab"); - - // Click "Show advanced" link button - await tab.getByRole("button", { name: "Show advanced" }).click(); - - await tab.locator(".mx_Checkbox", { hasText: "Use bundled emoji font" }).click(); - await tab.locator(".mx_Checkbox", { hasText: "Use a system font" }).click(); - - // Assert that the font-family value was removed - await expect(page.locator("body")).toHaveCSS("font-family", '""'); - }); - - test.describe("Theme Choice Panel", () => { - test.beforeEach(async ({ app, user }) => { - // Disable the default theme for consistency in case ThemeWatcher automatically chooses it - await app.settings.setValue("use_system_theme", null, SettingLevel.DEVICE, false); - }); - - test("should be rendered with the light theme selected", async ({ page, app }) => { - await app.settings.openUserSettings("Appearance"); - const themePanel = page.getByTestId("mx_ThemeChoicePanel"); - - const useSystemTheme = themePanel.getByTestId("checkbox-use-system-theme"); - await expect(useSystemTheme.getByText("Match system theme")).toBeVisible(); - // Assert that 'Match system theme' is not checked - // Note that mx_Checkbox_checkmark exists and is hidden by CSS if it is not checked - await expect(useSystemTheme.locator(".mx_Checkbox_checkmark")).not.toBeVisible(); - - const selectors = themePanel.getByTestId("theme-choice-panel-selectors"); - await expect(selectors.locator(".mx_ThemeSelector_light")).toBeVisible(); - await expect(selectors.locator(".mx_ThemeSelector_dark")).toBeVisible(); - // Assert that the light theme is selected - await expect(selectors.locator(".mx_ThemeSelector_light.mx_StyledRadioButton_enabled")).toBeVisible(); - // Assert that the buttons for the light and dark theme are not enabled - await expect(selectors.locator(".mx_ThemeSelector_light.mx_StyledRadioButton_disabled")).not.toBeVisible(); - await expect(selectors.locator(".mx_ThemeSelector_dark.mx_StyledRadioButton_disabled")).not.toBeVisible(); - - // Assert that the checkbox for the high contrast theme is rendered - await expect(themePanel.locator(".mx_Checkbox", { hasText: "Use high contrast" })).toBeVisible(); - }); - - test("should disable the labels for themes and the checkbox for the high contrast theme if the checkbox for the system theme is clicked", async ({ - page, - app, - }) => { - await app.settings.openUserSettings("Appearance"); - const themePanel = page.getByTestId("mx_ThemeChoicePanel"); - - await themePanel.locator(".mx_Checkbox", { hasText: "Match system theme" }).click(); - - // Assert that the labels for the light theme and dark theme are disabled - await expect(themePanel.locator(".mx_ThemeSelector_light.mx_StyledRadioButton_disabled")).toBeVisible(); - await expect(themePanel.locator(".mx_ThemeSelector_dark.mx_StyledRadioButton_disabled")).toBeVisible(); - - // Assert that there does not exist a label for an enabled theme - await expect(themePanel.locator("label.mx_StyledRadioButton_enabled")).not.toBeVisible(); - - // Assert that the checkbox and label to enable the high contrast theme should not exist - await expect(themePanel.locator(".mx_Checkbox", { hasText: "Use high contrast" })).not.toBeVisible(); - }); - - test("should not render the checkbox and the label for the high contrast theme if the dark theme is selected", async ({ - page, - app, - }) => { - await app.settings.openUserSettings("Appearance"); - const themePanel = page.getByTestId("mx_ThemeChoicePanel"); - - // Assert that the checkbox and the label to enable the high contrast theme should exist - await expect(themePanel.locator(".mx_Checkbox", { hasText: "Use high contrast" })).toBeVisible(); - - // Enable the dark theme - await themePanel.locator(".mx_ThemeSelector_dark").click(); - - // Assert that the checkbox and the label should not exist - await expect(themePanel.locator(".mx_Checkbox", { hasText: "Use high contrast" })).not.toBeVisible(); - }); - }); -}); diff --git a/playwright/e2e/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts b/playwright/e2e/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts new file mode 100644 index 0000000000..79c78b71f7 --- /dev/null +++ b/playwright/e2e/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts @@ -0,0 +1,172 @@ +/* +Copyright 2023 Suguru Hirahara + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { expect, test } from "."; + +test.describe("Appearance user settings tab", () => { + test.use({ + displayName: "Hanako", + }); + + test("should be rendered properly", async ({ page, user, app }) => { + const tab = await app.settings.openUserSettings("Appearance"); + + // Click "Show advanced" link button + await tab.getByRole("button", { name: "Show advanced" }).click(); + + // Assert that "Hide advanced" link button is rendered + await expect(tab.getByRole("button", { name: "Hide advanced" })).toBeVisible(); + + await expect(tab).toMatchScreenshot("appearance-tab.png"); + }); + + test("should support changing font size by using the font size dropdown", async ({ page, app, user }) => { + await app.settings.openUserSettings("Appearance"); + + const tab = page.getByTestId("mx_AppearanceUserSettingsTab"); + const fontDropdown = tab.locator(".mx_FontScalingPanel_Dropdown"); + await expect(fontDropdown.getByLabel("Font size")).toBeVisible(); + + // Default browser font size is 16px and the select value is 0 + // -4 value is 12px + await fontDropdown.getByLabel("Font size").selectOption({ value: "-4" }); + + await expect(page).toMatchScreenshot("window-12px.png"); + }); + + test("should support enabling system font", async ({ page, app, user }) => { + await app.settings.openUserSettings("Appearance"); + const tab = page.getByTestId("mx_AppearanceUserSettingsTab"); + + // Click "Show advanced" link button + await tab.getByRole("button", { name: "Show advanced" }).click(); + + await tab.locator(".mx_Checkbox", { hasText: "Use bundled emoji font" }).click(); + await tab.locator(".mx_Checkbox", { hasText: "Use a system font" }).click(); + + // Assert that the font-family value was removed + await expect(page.locator("body")).toHaveCSS("font-family", '""'); + }); + + test.describe("Message Layout Panel", () => { + test.beforeEach(async ({ app, user, util }) => { + await util.createAndDisplayRoom(); + await util.assertModernLayout(); + await util.openAppearanceTab(); + }); + + test("should change the message layout from modern to bubble", async ({ page, app, user, util }) => { + await util.assertScreenshot(util.getMessageLayoutPanel(), "message-layout-panel-modern.png"); + + await util.getBubbleLayout().click(); + + // Assert that modern are irc layout are not selected + await expect(util.getBubbleLayout()).toBeChecked(); + await expect(util.getModernLayout()).not.toBeChecked(); + await expect(util.getIRCLayout()).not.toBeChecked(); + + // Assert that the room layout is set to bubble layout + await util.assertBubbleLayout(); + await util.assertScreenshot(util.getMessageLayoutPanel(), "message-layout-panel-bubble.png"); + }); + + test("should enable compact layout when the modern layout is selected", async ({ page, app, user, util }) => { + await expect(util.getCompactLayoutCheckbox()).not.toBeChecked(); + + await util.getCompactLayoutCheckbox().click(); + await util.assertCompactLayout(); + }); + + test("should disable compact layout when the modern layout is not selected", async ({ + page, + app, + user, + util, + }) => { + await expect(util.getCompactLayoutCheckbox()).not.toBeDisabled(); + + // Select the bubble layout, which should disable the compact layout checkbox + await util.getBubbleLayout().click(); + await expect(util.getCompactLayoutCheckbox()).toBeDisabled(); + }); + }); + + test.describe("Theme Choice Panel", () => { + test.beforeEach(async ({ app, user, util }) => { + // Disable the default theme for consistency in case ThemeWatcher automatically chooses it + await util.disableSystemTheme(); + await util.openAppearanceTab(); + }); + + test("should be rendered with the light theme selected", async ({ page, app, util }) => { + // Assert that 'Match system theme' is not checked + await expect(util.getMatchSystemThemeCheckbox()).not.toBeChecked(); + + // Assert that the light theme is selected + await expect(util.getLightTheme()).toBeChecked(); + // Assert that the dark and high contrast themes are not selected + await expect(util.getDarkTheme()).not.toBeChecked(); + await expect(util.getHighContrastTheme()).not.toBeChecked(); + + await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-light.png"); + }); + + test("should disable the themes when the system theme is clicked", async ({ page, app, util }) => { + await util.getMatchSystemThemeCheckbox().click(); + + // Assert that the themes are disabled + await expect(util.getLightTheme()).toBeDisabled(); + await expect(util.getDarkTheme()).toBeDisabled(); + await expect(util.getHighContrastTheme()).toBeDisabled(); + + await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-match-system-enabled.png"); + }); + + test("should change the theme to dark", async ({ page, app, util }) => { + // Assert that the light theme is selected + await expect(util.getLightTheme()).toBeChecked(); + + await util.getDarkTheme().click(); + + // Assert that the light and high contrast themes are not selected + await expect(util.getLightTheme()).not.toBeChecked(); + await expect(util.getDarkTheme()).toBeChecked(); + await expect(util.getHighContrastTheme()).not.toBeChecked(); + + await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-dark.png"); + }); + + test.describe("custom theme", () => { + test.use({ + labsFlags: ["feature_custom_themes"], + }); + + test("should render the custom theme section", async ({ page, app, util }) => { + await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme.png"); + }); + + test("should be able to add and remove a custom theme", async ({ page, app, util }) => { + await util.addCustomTheme(); + + await expect(util.getCustomTheme()).not.toBeChecked(); + await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme-added.png"); + + await util.removeCustomTheme(); + await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme.png"); + }); + }); + }); +}); diff --git a/playwright/e2e/settings/appearance-user-settings-tab/index.ts b/playwright/e2e/settings/appearance-user-settings-tab/index.ts new file mode 100644 index 0000000000..e8641306ed --- /dev/null +++ b/playwright/e2e/settings/appearance-user-settings-tab/index.ts @@ -0,0 +1,241 @@ +/* + * Copyright 2024 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Locator, Page } from "@playwright/test"; + +import { ElementAppPage } from "../../../pages/ElementAppPage"; +import { test as base, expect } from "../../../element-web-test"; +import { SettingLevel } from "../../../../src/settings/SettingLevel"; +import { Layout } from "../../../../src/settings/enums/Layout"; + +export { expect }; + +/** + * Set up for the appearance tab test + */ +export const test = base.extend<{ + util: Helpers; +}>({ + util: async ({ page, app }, use) => { + await use(new Helpers(page, app)); + }, +}); + +/** + * A collection of helper functions for the appearance tab test + * The goal is to make easier to get and interact with the button, input, or other elements of the appearance tab + */ +class Helpers { + private CUSTOM_THEME_URL = "http://custom.theme"; + private CUSTOM_THEME = { + name: "Custom theme", + isDark: false, + colors: {}, + }; + + constructor( + private page: Page, + private app: ElementAppPage, + ) {} + + /** + * Open the appearance tab + */ + openAppearanceTab() { + return this.app.settings.openUserSettings("Appearance"); + } + + /** + * Compare screenshot and hide the matrix chat + * @param locator + * @param screenshot + */ + assertScreenshot(locator: Locator, screenshot: `${string}.png`) { + return expect(locator).toMatchScreenshot(screenshot, { + css: ` + #matrixchat { + display: none; + } + `, + }); + } + + // Theme Panel + + /** + * Disable in the settings the system theme + */ + disableSystemTheme() { + return this.app.settings.setValue("use_system_theme", null, SettingLevel.DEVICE, false); + } + + /** + * Return the theme section + */ + getThemePanel() { + return this.page.getByTestId("themePanel"); + } + + /** + * Return the system theme toggle + */ + getMatchSystemThemeCheckbox() { + return this.getThemePanel().getByRole("checkbox", { name: "Match system theme" }); + } + + /** + * Return the theme radio button + * @param theme - the theme to select + * @private + */ + private getThemeRadio(theme: string) { + return this.getThemePanel().getByRole("radio", { name: theme }); + } + + /** + * Return the light theme radio button + */ + getLightTheme() { + return this.getThemeRadio("Light"); + } + + /** + * Return the dark theme radio button + */ + getDarkTheme() { + return this.getThemeRadio("Dark"); + } + + /** + * Return the custom theme radio button + */ + getCustomTheme() { + return this.getThemeRadio(this.CUSTOM_THEME.name); + } + + /** + * Return the high contrast theme radio button + */ + getHighContrastTheme() { + return this.getThemeRadio("High contrast"); + } + + /** + * Add a custom theme + * Mock the request to the custom and return a fake local custom theme + */ + async addCustomTheme() { + await this.page.route(this.CUSTOM_THEME_URL, (route) => + route.fulfill({ body: JSON.stringify(this.CUSTOM_THEME) }), + ); + await this.page.getByRole("textbox", { name: "Add custom theme" }).fill(this.CUSTOM_THEME_URL); + await this.page.getByRole("button", { name: "Add custom theme" }).click(); + await this.page.unroute(this.CUSTOM_THEME_URL); + } + + /** + * Remove the custom theme + */ + removeCustomTheme() { + return this.getThemePanel().getByRole("listitem", { name: this.CUSTOM_THEME.name }).getByRole("button").click(); + } + + // Message layout Panel + + /** + * Create and display a room named Test Room + */ + async createAndDisplayRoom() { + await this.app.client.createRoom({ name: "Test Room" }); + await this.app.viewRoomByName("Test Room"); + } + + /** + * Assert the room layout + * @param layout + * @private + */ + private assertRoomLayout(layout: Layout) { + return expect(this.page.locator(`.mx_RoomView_body[data-layout=${layout}]`)).toBeVisible(); + } + + /** + * Assert the room layout is modern + */ + assertModernLayout() { + return this.assertRoomLayout(Layout.Group); + } + + /** + * Assert the room layout is bubble + */ + assertBubbleLayout() { + return this.assertRoomLayout(Layout.Bubble); + } + + /** + * Return the layout panel + */ + getMessageLayoutPanel() { + return this.page.getByTestId("layoutPanel"); + } + + /** + * Return the layout radio button + * @param layoutName + * @private + */ + private getLayout(layoutName: string) { + return this.getMessageLayoutPanel().getByRole("radio", { name: layoutName }); + } + + /** + * Return the message bubbles layout radio button + */ + getBubbleLayout() { + return this.getLayout("Message bubbles"); + } + + /** + * Return the modern layout radio button + */ + getModernLayout() { + return this.getLayout("Modern"); + } + + /** + * Return the IRC layout radio button + */ + getIRCLayout() { + return this.getLayout("IRC (experimental)"); + } + + /** + * Return the compact layout checkbox + */ + getCompactLayoutCheckbox() { + return this.getMessageLayoutPanel().getByRole("checkbox", { name: "Show compact text and messages" }); + } + + /** + * Assert the compact layout is enabled + */ + assertCompactLayout() { + return expect( + this.page.locator("#matrixchat .mx_MatrixChat_wrapper.mx_MatrixChat_useCompactLayout"), + ).toBeVisible(); + } +} diff --git a/playwright/e2e/settings/general-user-settings-tab.spec.ts b/playwright/e2e/settings/general-user-settings-tab.spec.ts index 0244962914..050cd76d00 100644 --- a/playwright/e2e/settings/general-user-settings-tab.spec.ts +++ b/playwright/e2e/settings/general-user-settings-tab.spec.ts @@ -73,29 +73,6 @@ test.describe("General user settings tab", () => { // Assert that the add button is rendered await expect(phoneNumbers.getByRole("button", { name: "Add" })).toBeVisible(); - // Check language and region setting dropdown - const languageInput = uut.locator(".mx_GeneralUserSettingsTab_section_languageInput"); - await languageInput.scrollIntoViewIfNeeded(); - // Check the default value - await expect(languageInput.getByText("English")).toBeVisible(); - // Click the button to display the dropdown menu - await languageInput.getByRole("button", { name: "Language Dropdown" }).click(); - // Assert that the default option is rendered and highlighted - languageInput.getByRole("option", { name: /Albanian/ }); - await expect(languageInput.getByRole("option", { name: /Albanian/ })).toHaveClass( - /mx_Dropdown_option_highlight/, - ); - await expect(languageInput.getByRole("option", { name: /Deutsch/ })).toBeVisible(); - // Click again to close the dropdown - await languageInput.getByRole("button", { name: "Language Dropdown" }).click(); - // Assert that the default value is rendered again - await expect(languageInput.getByText("English")).toBeVisible(); - - const setIdServer = uut.locator(".mx_SetIdServer"); - await setIdServer.scrollIntoViewIfNeeded(); - // Assert that an input area for identity server exists - await expect(setIdServer.getByRole("textbox", { name: "Enter a new identity server" })).toBeVisible(); - const setIntegrationManager = uut.locator(".mx_SetIntegrationManager"); await setIntegrationManager.scrollIntoViewIfNeeded(); await expect( diff --git a/playwright/e2e/settings/preferences-user-settings-tab.spec.ts b/playwright/e2e/settings/preferences-user-settings-tab.spec.ts index 2dbd267162..22baa19a8a 100644 --- a/playwright/e2e/settings/preferences-user-settings-tab.spec.ts +++ b/playwright/e2e/settings/preferences-user-settings-tab.spec.ts @@ -1,5 +1,6 @@ /* Copyright 2023 Suguru Hirahara +Copyright 2024 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,6 +20,10 @@ import { test, expect } from "../../element-web-test"; test.describe("Preferences user settings tab", () => { test.use({ displayName: "Bob", + uut: async ({ app, user }, use) => { + const locator = await app.settings.openUserSettings("Preferences"); + await use(locator); + }, }); test("should be rendered properly", async ({ app, user }) => { @@ -28,4 +33,24 @@ test.describe("Preferences user settings tab", () => { await expect(tab.getByRole("heading", { name: "Preferences" })).toBeVisible(); await expect(tab).toMatchScreenshot(); }); + + test("should be able to change the app language", async ({ uut, user }) => { + // Check language and region setting dropdown + const languageInput = uut.locator(".mx_GeneralUserSettingsTab_section_languageInput"); + await languageInput.scrollIntoViewIfNeeded(); + // Check the default value + await expect(languageInput.getByText("English")).toBeVisible(); + // Click the button to display the dropdown menu + await languageInput.getByRole("button", { name: "Language Dropdown" }).click(); + // Assert that the default option is rendered and highlighted + languageInput.getByRole("option", { name: /Albanian/ }); + await expect(languageInput.getByRole("option", { name: /Albanian/ })).toHaveClass( + /mx_Dropdown_option_highlight/, + ); + await expect(languageInput.getByRole("option", { name: /Deutsch/ })).toBeVisible(); + // Click again to close the dropdown + await languageInput.getByRole("button", { name: "Language Dropdown" }).click(); + // Assert that the default value is rendered again + await expect(languageInput.getByText("English")).toBeVisible(); + }); }); diff --git a/playwright/e2e/settings/security-user-settings-tab.spec.ts b/playwright/e2e/settings/security-user-settings-tab.spec.ts index 08640f603b..5cd2a92c16 100644 --- a/playwright/e2e/settings/security-user-settings-tab.spec.ts +++ b/playwright/e2e/settings/security-user-settings-tab.spec.ts @@ -47,5 +47,14 @@ test.describe("Security user settings tab", () => { await expect(page.locator(".mx_AnalyticsLearnMoreDialog_wrapper .mx_Dialog")).toMatchScreenshot(); }); }); + + test("should contain section to set ID server", async ({ app }) => { + const tab = await app.settings.openUserSettings("Security"); + + const setIdServer = tab.locator(".mx_SetIdServer"); + await setIdServer.scrollIntoViewIfNeeded(); + // Assert that an input area for identity server exists + await expect(setIdServer.getByRole("textbox", { name: "Enter a new identity server" })).toBeVisible(); + }); }); }); diff --git a/playwright/e2e/spaces/threads-activity-centre/index.ts b/playwright/e2e/spaces/threads-activity-centre/index.ts index 8bafe2e804..8b013c44bb 100644 --- a/playwright/e2e/spaces/threads-activity-centre/index.ts +++ b/playwright/e2e/spaces/threads-activity-centre/index.ts @@ -337,12 +337,10 @@ export class Helpers { } /** - * Assert that the thread panel is focused (actually the 'close' button, specifically) + * Assert that the thread tab is focused */ - assertThreadPanelFocused() { - return expect( - this.page.locator(".mx_ThreadPanel").locator(".mx_BaseCard_header").getByLabel("Close"), - ).toBeFocused(); + assertThreadTabFocused() { + return expect(this.page.locator("#thread-panel-tab")).toBeFocused(); } /** diff --git a/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts b/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts index 7d0b694ef5..66a3bc58e5 100644 --- a/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts +++ b/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts @@ -161,17 +161,12 @@ test.describe("Threads Activity Centre", () => { await util.assertNoTacIndicator(); }); - test("should focus the thread panel close button when clicking an item in the TAC", async ({ - room1, - room2, - util, - msg, - }) => { + test("should focus the thread tab when clicking an item in the TAC", async ({ room1, room2, util, msg }) => { await util.receiveMessages(room1, ["Msg1", msg.threadedOff("Msg1", "Resp1")]); await util.openTac(); await util.clickRoomInTac(room1.name); - await util.assertThreadPanelFocused(); + await util.assertThreadTabFocused(); }); }); diff --git a/playwright/e2e/timeline/timeline.spec.ts b/playwright/e2e/timeline/timeline.spec.ts index de4df133d0..47ec61aecf 100644 --- a/playwright/e2e/timeline/timeline.spec.ts +++ b/playwright/e2e/timeline/timeline.spec.ts @@ -781,10 +781,10 @@ test.describe("Timeline", () => { await page.locator(".mx_LegacyRoomHeader").getByRole("button", { name: "Search" }).click(); - await expect(page.locator(".mx_SearchBar")).toMatchScreenshot("search-bar-on-timeline.png"); + await page.locator(".mx_RoomSummaryCard_search").getByRole("searchbox").fill("Message"); + await page.locator(".mx_RoomSummaryCard_search").getByRole("searchbox").press("Enter"); - await page.locator(".mx_SearchBar_input").getByRole("textbox").fill("Message"); - await page.locator(".mx_SearchBar_input").getByRole("textbox").press("Enter"); + await expect(page.locator(".mx_RoomSearchAuxPanel")).toMatchScreenshot("search-aux-panel.png"); for (const locator of await page .locator(".mx_EventTile:not(.mx_EventTile_contextual) .mx_EventTile_searchHighlight") @@ -822,8 +822,8 @@ test.describe("Timeline", () => { await page.locator(".mx_LegacyRoomHeader").getByRole("button", { name: "Search" }).click(); // Search the string to display both the message and TextualEvent on search results panel - await page.locator(".mx_SearchBar").getByRole("textbox").fill(stringToSearch); - await page.locator(".mx_SearchBar").getByRole("textbox").press("Enter"); + await page.locator(".mx_RoomSummaryCard_search").getByRole("searchbox").fill(stringToSearch); + await page.locator(".mx_RoomSummaryCard_search").getByRole("searchbox").press("Enter"); // On search results panel const resultsPanel = page.locator(".mx_RoomView_searchResultsPanel"); diff --git a/playwright/e2e/utils.ts b/playwright/e2e/utils.ts index 30aff64dd8..e7587c7dfb 100644 --- a/playwright/e2e/utils.ts +++ b/playwright/e2e/utils.ts @@ -17,8 +17,8 @@ limitations under the License. */ import { uniqueId } from "lodash"; +import { expect, type Page } from "@playwright/test"; -import type { Page } from "@playwright/test"; import type { ClientEvent, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; import { Client } from "../pages/client"; @@ -63,4 +63,15 @@ export async function waitForRoom( ); } +export async function selectHomeserver(page: Page, homeserverUrl: string) { + await page.getByRole("button", { name: "Edit" }).click(); + await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserverUrl); + await page.getByRole("button", { name: "Continue", exact: true }).click(); + // wait for the dialog to go away + await expect(page.locator(".mx_ServerPickerDialog")).toHaveCount(0); + + await expect(page.locator(".mx_Spinner")).toHaveCount(0); + await expect(page.locator(".mx_ServerPicker_server")).toHaveText(homeserverUrl); +} + export const CommandOrControl = process.platform === "darwin" ? "Meta" : "Control"; diff --git a/playwright/flaky-reporter.ts b/playwright/flaky-reporter.ts index 3d358bb74d..95023e31ba 100644 --- a/playwright/flaky-reporter.ts +++ b/playwright/flaky-reporter.ts @@ -53,7 +53,10 @@ class FlakyReporter implements Reporter { const headers = { Authorization: `Bearer ${GITHUB_TOKEN}` }; // Fetch all existing issues with the flaky-test label. - const issuesRequest = await fetch(`${GITHUB_API_URL}/repos/${REPO}/issues?labels=${LABEL}`, { headers }); + const issuesRequest = await fetch( + `${GITHUB_API_URL}/repos/${REPO}/issues?labels=${LABEL}&state=all&per_page=100&sort=created`, + { headers }, + ); const issues = await issuesRequest.json(); for (const flake of this.flakes) { const title = ISSUE_TITLE_PREFIX + "`" + flake + "`"; @@ -61,6 +64,12 @@ class FlakyReporter implements Reporter { if (existingIssue) { console.log(`Found issue ${existingIssue.number} for ${flake}, adding comment...`); + // Ensure that the test is open + await fetch(existingIssue.url, { + method: "PATCH", + headers, + body: JSON.stringify({ state: "open" }), + }); await fetch(`${existingIssue.url}/comments`, { method: "POST", headers, diff --git a/playwright/pages/bot.ts b/playwright/pages/bot.ts index 333d895dfe..3b46130108 100644 --- a/playwright/pages/bot.ts +++ b/playwright/pages/bot.ts @@ -45,10 +45,6 @@ export interface CreateBotOpts { * Whether to generate cross-signing keys */ bootstrapCrossSigning?: boolean; - /** - * Whether to use the rust crypto impl. Defaults to false (for now!) - */ - rustCrypto?: boolean; /** * Whether to bootstrap the secret storage */ @@ -188,11 +184,7 @@ export class Bot extends Client { return cli; } - if (opts.rustCrypto) { - await cli.initRustCrypto({ useIndexedDB: false }); - } else { - await cli.initCrypto(); - } + await cli.initRustCrypto({ useIndexedDB: false }); cli.setGlobalErrorOnUnknownDevices(false); await cli.startClient(); diff --git a/playwright/plugins/homeserver/index.ts b/playwright/plugins/homeserver/index.ts index 1e0cfb3b39..b14ba70082 100644 --- a/playwright/plugins/homeserver/index.ts +++ b/playwright/plugins/homeserver/index.ts @@ -39,6 +39,15 @@ export interface HomeserverInstance { * @param password login password */ loginUser(userId: string, password: string): Promise; + + /** + * Sets a third party identifier for the given user. This only supports setting a single 3pid and will + * replace any others. + * @param userId The full ID of the user to edit (as returned from registerUser) + * @param medium The medium of the 3pid to set + * @param address The address of the 3pid to set + */ + setThreepid(userId: string, medium: string, address: string): Promise; } export interface StartHomeserverOpts { diff --git a/playwright/plugins/homeserver/synapse/index.ts b/playwright/plugins/homeserver/synapse/index.ts index ed836e2637..0385f64d03 100644 --- a/playwright/plugins/homeserver/synapse/index.ts +++ b/playwright/plugins/homeserver/synapse/index.ts @@ -28,7 +28,7 @@ import { randB64Bytes } from "../../utils/rand"; // Docker tag to use for `matrixdotorg/synapse` image. // We target a specific digest as every now and then a Synapse update will break our CI. // This digest is updated by the playwright-image-updates.yaml workflow periodically. -const DOCKER_TAG = "develop@sha256:38bdd185e32dbfb40d11a69a26c5b04c0ccf1cb7d4078a14d6fdb16620bd4b3c"; +const DOCKER_TAG = "develop@sha256:db5f8e8ca4a903379ea18b010ac3360bd843c9ac7eb2e73ad89f5059d01f8104"; async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise> { const templateDir = path.join(__dirname, "templates", opts.template); @@ -94,6 +94,8 @@ export class Synapse implements Homeserver, HomeserverInstance { protected docker: Docker = new Docker(); public config: HomeserverConfig & { serverId: string }; + private adminToken?: string; + public constructor(private readonly request: APIRequestContext) {} /** @@ -152,12 +154,17 @@ export class Synapse implements Homeserver, HomeserverInstance { return [path.join(synapseLogsPath, "stdout.log"), path.join(synapseLogsPath, "stderr.log")]; } - public async registerUser(username: string, password: string, displayName?: string): Promise { + private async registerUserInternal( + username: string, + password: string, + displayName?: string, + admin = false, + ): Promise { const url = `${this.config.baseUrl}/_synapse/admin/v1/register`; const { nonce } = await this.request.get(url).then((r) => r.json()); const mac = crypto .createHmac("sha1", this.config.registrationSecret) - .update(`${nonce}\0${username}\0${password}\0notadmin`) + .update(`${nonce}\0${username}\0${password}\0${admin ? "" : "not"}admin`) .digest("hex"); const res = await this.request.post(url, { data: { @@ -165,7 +172,7 @@ export class Synapse implements Homeserver, HomeserverInstance { username, password, mac, - admin: false, + admin, displayname: displayName, }, }); @@ -185,6 +192,10 @@ export class Synapse implements Homeserver, HomeserverInstance { }; } + public registerUser(username: string, password: string, displayName?: string): Promise { + return this.registerUserInternal(username, password, displayName, false); + } + public async loginUser(userId: string, password: string): Promise { const url = `${this.config.baseUrl}/_matrix/client/v3/login`; const res = await this.request.post(url, { @@ -207,4 +218,30 @@ export class Synapse implements Homeserver, HomeserverInstance { homeServer: json.home_server, }; } + + public async setThreepid(userId: string, medium: string, address: string): Promise { + if (this.adminToken === undefined) { + const result = await this.registerUserInternal("admin", "totalyinsecureadminpassword", undefined, true); + this.adminToken = result.accessToken; + } + + const url = `${this.config.baseUrl}/_synapse/admin/v2/users/${userId}`; + const res = await this.request.put(url, { + data: { + threepids: [ + { + medium, + address, + }, + ], + }, + headers: { + Authorization: `Bearer ${this.adminToken}`, + }, + }); + + if (!res.ok()) { + throw await res.json(); + } + } } diff --git a/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png b/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png index 27d51ff123..2c6160f2a1 100644 Binary files a/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png and b/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png differ diff --git a/playwright/snapshots/editing/editing.spec.ts/message-edit-history-dialog-linux.png b/playwright/snapshots/editing/editing.spec.ts/message-edit-history-dialog-linux.png index 9f46fce516..bfbfccbaeb 100644 Binary files a/playwright/snapshots/editing/editing.spec.ts/message-edit-history-dialog-linux.png and b/playwright/snapshots/editing/editing.spec.ts/message-edit-history-dialog-linux.png differ diff --git a/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-linux.png b/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-linux.png new file mode 100644 index 0000000000..891f024bf8 Binary files /dev/null and b/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-linux.png differ diff --git a/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-verify-email-linux.png b/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-verify-email-linux.png new file mode 100644 index 0000000000..22b4e109c8 Binary files /dev/null and b/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-verify-email-linux.png differ diff --git a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png index c8b8dba45b..614533956b 100644 Binary files a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png and b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png differ diff --git a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png index 852cb85518..51f365f353 100644 Binary files a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png and b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png differ diff --git a/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png b/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png index bb8240913e..943cc9dfc8 100644 Binary files a/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png and b/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png new file mode 100644 index 0000000000..12ea3aa847 Binary files /dev/null and b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/message-layout-panel-bubble-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/message-layout-panel-bubble-linux.png new file mode 100644 index 0000000000..3f39a3b01d Binary files /dev/null and b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/message-layout-panel-bubble-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/message-layout-panel-modern-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/message-layout-panel-modern-linux.png new file mode 100644 index 0000000000..74aaf9a763 Binary files /dev/null and b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/message-layout-panel-modern-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/theme-panel-custom-theme-added-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/theme-panel-custom-theme-added-linux.png new file mode 100644 index 0000000000..d44c107307 Binary files /dev/null and b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/theme-panel-custom-theme-added-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/theme-panel-custom-theme-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/theme-panel-custom-theme-linux.png new file mode 100644 index 0000000000..76a0befd33 Binary files /dev/null and b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/theme-panel-custom-theme-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/theme-panel-dark-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/theme-panel-dark-linux.png new file mode 100644 index 0000000000..3b9c243138 Binary files /dev/null and b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/theme-panel-dark-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/theme-panel-light-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/theme-panel-light-linux.png new file mode 100644 index 0000000000..ca90917116 Binary files /dev/null and b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/theme-panel-light-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/theme-panel-match-system-enabled-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/theme-panel-match-system-enabled-linux.png new file mode 100644 index 0000000000..1aed777c8d Binary files /dev/null and b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/theme-panel-match-system-enabled-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png new file mode 100644 index 0000000000..1ce0ffd520 Binary files /dev/null and b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png differ diff --git a/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-linux.png b/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-linux.png index 73666d61c0..4cf8df1c32 100644 Binary files a/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-linux.png and b/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-linux.png differ diff --git a/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-smallscreen-linux.png b/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-smallscreen-linux.png index 0b5a08979d..d0380e6a4f 100644 Binary files a/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-smallscreen-linux.png and b/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-smallscreen-linux.png differ diff --git a/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png b/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png index d0bc1288ec..ca2e75dfbb 100644 Binary files a/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png and b/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/highlighted-search-results-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/highlighted-search-results-linux.png index fcf3acd7e8..0d0cc0eec3 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/highlighted-search-results-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/highlighted-search-results-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 new file mode 100644 index 0000000000..fe96a9e6be Binary files /dev/null and b/playwright/snapshots/timeline/timeline.spec.ts/search-aux-panel-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/search-bar-on-timeline-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/search-bar-on-timeline-linux.png deleted file mode 100644 index 64d44a9778..0000000000 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/search-bar-on-timeline-linux.png and /dev/null differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/search-results-with-TextualEvent-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/search-results-with-TextualEvent-linux.png index 4c553cfdaf..89ce0a9f2d 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/search-results-with-TextualEvent-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/search-results-with-TextualEvent-linux.png differ diff --git a/res/css/_common.pcss b/res/css/_common.pcss index 32a2610c1f..a454789efc 100644 --- a/res/css/_common.pcss +++ b/res/css/_common.pcss @@ -177,9 +177,9 @@ a:visited { color: $accent-alt; } -input[type="text"], -input[type="search"], -input[type="password"] { +:not(.mx_no_textinput):not(.mx_textinput):not(.mx_Field) > input[type="text"], +:not(.mx_no_textinput):not(.mx_textinput):not(.mx_Field) > input[type="search"], +:not(.mx_no_textinput):not(.mx_textinput):not(.mx_Field) > input[type="password"] { padding: 9px; font: var(--cpd-font-body-md-semibold); font-weight: var(--cpd-font-weight-semibold); @@ -522,6 +522,8 @@ legend { content: ""; width: 28px; height: 28px; + left: 0; + top: 0; position: absolute; mask-image: url("@vector-im/compound-design-tokens/icons/close.svg"); mask-repeat: no-repeat; @@ -604,7 +606,7 @@ legend { .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ), + ):not(.mx_ThemeChoicePanel_CustomTheme button), .mx_Dialog input[type="submit"], .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton), .mx_Dialog_buttons input[type="submit"] { @@ -624,14 +626,14 @@ legend { .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):last-child { + ):not(.mx_ThemeChoicePanel_CustomTheme button):last-child { margin-right: 0px; } .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):focus, + ):not(.mx_ThemeChoicePanel_CustomTheme button):focus, .mx_Dialog input[type="submit"]:focus, .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):focus, .mx_Dialog_buttons input[type="submit"]:focus { @@ -643,7 +645,7 @@ legend { .mx_Dialog_buttons button.mx_Dialog_primary:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ), + ):not(.mx_ThemeChoicePanel_CustomTheme button), .mx_Dialog_buttons input[type="submit"].mx_Dialog_primary { color: var(--cpd-color-text-on-solid-primary); background-color: var(--cpd-color-bg-action-primary-rest); @@ -654,7 +656,9 @@ legend { .mx_Dialog button.danger:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]), .mx_Dialog input[type="submit"].danger, .mx_Dialog_buttons - button.danger:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(.mx_UserProfileSettings button), + button.danger:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(.mx_UserProfileSettings button):not( + .mx_ThemeChoicePanel_CustomTheme button + ), .mx_Dialog_buttons input[type="submit"].danger { background-color: var(--cpd-color-bg-critical-primary); border: solid 1px var(--cpd-color-bg-critical-primary); @@ -670,7 +674,7 @@ legend { .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):disabled, + ):not(.mx_ThemeChoicePanel_CustomTheme button):disabled, .mx_Dialog input[type="submit"]:disabled, .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):disabled, .mx_Dialog_buttons input[type="submit"]:disabled { diff --git a/res/css/_components.pcss b/res/css/_components.pcss index a7c79bfbf2..327b86da08 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -261,6 +261,7 @@ @import "./views/right_panel/_BaseCard.pcss"; @import "./views/right_panel/_EncryptionInfo.pcss"; @import "./views/right_panel/_PinnedMessagesCard.pcss"; +@import "./views/right_panel/_RightPanelTabs.pcss"; @import "./views/right_panel/_RoomSummaryCard.pcss"; @import "./views/right_panel/_ThreadPanel.pcss"; @import "./views/right_panel/_TimelineCard.pcss"; @@ -306,10 +307,10 @@ @import "./views/rooms/_RoomListHeader.pcss"; @import "./views/rooms/_RoomPreviewBar.pcss"; @import "./views/rooms/_RoomPreviewCard.pcss"; +@import "./views/rooms/_RoomSearchAuxPanel.pcss"; @import "./views/rooms/_RoomSublist.pcss"; @import "./views/rooms/_RoomTile.pcss"; @import "./views/rooms/_RoomUpgradeWarningBar.pcss"; -@import "./views/rooms/_SearchBar.pcss"; @import "./views/rooms/_SendMessageComposer.pcss"; @import "./views/rooms/_SpaceScopeHeader.pcss"; @import "./views/rooms/_Stickers.pcss"; diff --git a/res/css/components/views/settings/shared/_SettingsSubsection.pcss b/res/css/components/views/settings/shared/_SettingsSubsection.pcss index 44d0a34426..f0e31285cb 100644 --- a/res/css/components/views/settings/shared/_SettingsSubsection.pcss +++ b/res/css/components/views/settings/shared/_SettingsSubsection.pcss @@ -17,6 +17,12 @@ limitations under the License. .mx_SettingsSubsection { width: 100%; box-sizing: border-box; + + &.mx_SettingsSubsection_newUi { + display: flex; + flex-direction: column; + gap: var(--cpd-space-8x); + } } .mx_SettingsSubsection_description { @@ -54,4 +60,8 @@ limitations under the License. &.mx_SettingsSubsection_noHeading { margin-top: 0; } + &.mx_SettingsSubsection_content_newUi { + gap: var(--cpd-space-6x); + margin-top: 0; + } } diff --git a/res/css/views/right_panel/_RightPanelTabs.pcss b/res/css/views/right_panel/_RightPanelTabs.pcss new file mode 100644 index 0000000000..afaae6c657 --- /dev/null +++ b/res/css/views/right_panel/_RightPanelTabs.pcss @@ -0,0 +1,25 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_RightPanelTabs { + margin: 0; + height: 64px; + box-sizing: border-box; + + ul { + margin-left: 16px; + } +} diff --git a/res/css/views/right_panel/_RoomSummaryCard.pcss b/res/css/views/right_panel/_RoomSummaryCard.pcss index 4c3ff2f888..549eb69ee4 100644 --- a/res/css/views/right_panel/_RoomSummaryCard.pcss +++ b/res/css/views/right_panel/_RoomSummaryCard.pcss @@ -235,28 +235,15 @@ limitations under the License. } .mx_RoomSummaryCard_header { - padding: 15px 12px; + padding: 24px 12px 15px; } -.mx_RoomSummaryCard_search input { - /* Overriding very broad CSS rules */ - border: 0 !important; - margin: 0 !important; - cursor: pointer; -} +.mx_RoomSummaryCard_search { + flex-grow: 1; + min-width: 0; -.mx_RoomSummaryCard_searchBtn { - background: var(--cpd-color-bg-canvas-default); - color: var(--cpd-color-icon-primary); - border: 1px solid var(--cpd-color-gray-400); - border-radius: 50%; - width: 36px; - height: 36px; - padding: var(--cpd-space-2x); - cursor: pointer; - - &:hover { - background: var(--cpd-color-bg-subtle-primary); + input[type="search"]::-webkit-search-cancel-button { + display: unset; /* override _common.pcss which inhibits this */ } } diff --git a/res/css/views/rooms/_MemberList.pcss b/res/css/views/rooms/_MemberList.pcss index 086a60810f..6e2e5a43a4 100644 --- a/res/css/views/rooms/_MemberList.pcss +++ b/res/css/views/rooms/_MemberList.pcss @@ -19,6 +19,7 @@ limitations under the License. display: flex; flex-direction: column; min-height: 0; + margin-top: 24px; .mx_Spinner { flex: 1 0 auto; diff --git a/res/css/views/rooms/_RoomHeader.pcss b/res/css/views/rooms/_RoomHeader.pcss index bc66cd2141..2af5e5ca10 100644 --- a/res/css/views/rooms/_RoomHeader.pcss +++ b/res/css/views/rooms/_RoomHeader.pcss @@ -20,7 +20,7 @@ limitations under the License. padding: 0 var(--cpd-space-3x); border-bottom: 1px solid $separator; background-color: $background; - transition: all 0.3s ease; + transition: all 0.2s ease; } .mx_RoomHeader:hover { @@ -74,14 +74,17 @@ limitations under the License. } } -.mx_RoomHeader:hover .mx_RoomHeader_topic { - /* height needed to compute the transition, it equals to the `line-height` +.mx_RoomHeader:hover, +.mx_RoomHeader:focus-within { + .mx_RoomHeader_topic { + /* height needed to compute the transition, it equals to the `line-height` value in pixels */ - height: calc($font-13px * 1.5); - opacity: 1; + height: calc($font-13px * 1.5); + opacity: 1; - a:hover { - text-decoration: underline; + a:hover { + text-decoration: underline; + } } } diff --git a/res/css/views/rooms/_RoomSearchAuxPanel.pcss b/res/css/views/rooms/_RoomSearchAuxPanel.pcss new file mode 100644 index 0000000000..a47616a685 --- /dev/null +++ b/res/css/views/rooms/_RoomSearchAuxPanel.pcss @@ -0,0 +1,72 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_RoomSearchAuxPanel { + /* use `min-height` rather than height, to allow room for the text to wrap if the window is narrow */ + min-height: 84px; + display: flex; + align-items: center; + border-color: var(--cpd-color-bg-canvas-default); + border-style: solid; + border-width: 1px 0; + padding: var(--cpd-space-3x); + box-sizing: border-box; + gap: var(--cpd-space-2x); + + .mx_RoomSearchAuxPanel_summary { + flex-grow: 1; + display: inherit; /* flex */ + gap: var(--cpd-space-2x); + align-items: center; + overflow: hidden; + + > svg { + padding: var(--cpd-space-2x); + border-radius: var(--cpd-space-2x); + background-color: var(--cpd-color-bg-subtle-secondary); + color: var(--cpd-color-icon-secondary); + flex-shrink: 0; + } + + .mx_RoomSearchAuxPanel_summary_text { + display: flex; + flex-direction: column; + font-size: $font-15px; + line-height: $font-22px; + overflow: hidden; + + span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + .mx_SearchWarning { + display: contents; + font-size: $font-13px; + line-height: $font-20px; + color: var(--cpd-color-text-secondary); + } + } + + .mx_RoomSearchAuxPanel_buttons { + display: inherit; /* flex */ + gap: var(--cpd-space-6x); + align-items: center; + flex-shrink: 0; + } +} diff --git a/res/css/views/rooms/_SearchBar.pcss b/res/css/views/rooms/_SearchBar.pcss deleted file mode 100644 index ca999c7bea..0000000000 --- a/res/css/views/rooms/_SearchBar.pcss +++ /dev/null @@ -1,83 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_SearchBar { - /* use `min-height` rather than height, to allow room for the text to wrap if the window is narrow */ - min-height: 56px; - display: flex; - align-items: center; - border-bottom: 1px solid $primary-hairline-color; - - .mx_SearchBar_input { - --size-button-search: 37px; /* size of the search button inside `input` element */ - - /* border: 1px solid $input-border-color; */ - /* font-size: $font-15px; */ - flex: 1 1 0; - margin-left: 22px; - - /* do not allow the input element to shrink below the width needed for the placeholder 'Search…' - and the search button */ - min-width: calc(7em + var(--size-button-search)); - - input { - box-sizing: border-box; /* include padding value into width calculation */ - } - } - - .mx_SearchBar_searchButton { - cursor: pointer; - width: var(--size-button-search); - height: var(--size-button-search); - background-color: $accent; - mask: url("$(res)/img/feather-customised/search-input.svg"); - mask-repeat: no-repeat; - mask-position: center; - } - - .mx_SearchBar_buttons { - display: inherit; /* flex */ - min-width: 0; /* have the close button displayed even on a very narrow timeline */ - } - - .mx_SearchBar_button { - border: 0; - margin: 0 0 0 22px; - padding: 5px; - font-size: $font-15px; - cursor: pointer; - color: $primary-content; - border-bottom: 2px solid $accent; - font-weight: var(--cpd-font-weight-semibold); - word-break: break-all; /* prevent the input area and cancel button from being overlapped by BaseCard */ - } - - .mx_SearchBar_unselected { - color: $input-darker-fg-color; - border-color: transparent; - } - - .mx_SearchBar_cancel { - background-color: $alert; - mask: url("$(res)/img/cancel.svg"); - mask-repeat: no-repeat; - mask-position: center; - mask-size: 14px; - padding: 9px; - margin: 0 12px 0 3px; - cursor: pointer; - } -} diff --git a/res/css/views/settings/_LayoutSwitcher.pcss b/res/css/views/settings/_LayoutSwitcher.pcss index 571b9a1cf1..96fe27b420 100644 --- a/res/css/views/settings/_LayoutSwitcher.pcss +++ b/res/css/views/settings/_LayoutSwitcher.pcss @@ -15,79 +15,80 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_LayoutSwitcher_RadioButtons { +.mx_LayoutSwitcher_LayoutSelector { display: flex; - flex-direction: row; - gap: 24px; - width: 100%; + flex-direction: column; + /** + * The settings form has a default gap of 10px + * We want to have a bigger gap between the layout options + */ + gap: var(--cpd-space-4x) !important; - color: $primary-content; + .mxLayoutSwitcher_LayoutSelector_LayoutRadio { + border: 1px solid var(--cpd-color-border-interactive-primary); + border-radius: var(--cpd-space-2x); - > .mx_LayoutSwitcher_RadioButton { - flex-grow: 0; - flex-shrink: 1; - display: flex; - flex-direction: column; - overflow: hidden; - - flex-basis: 33%; - min-width: 0; - - border: 1px solid $quinary-content; - border-radius: 10px; - - .mx_EventTile_msgOption, - .mx_MessageActionBar { - display: none; + .mxLayoutSwitcher_LayoutSelector_LayoutRadio_inline { + display: flex; + /* + * 10px + */ + gap: calc(var(--cpd-space-2x) + var(--cpd-space-0-5x)); + align-items: center; } - .mx_LayoutSwitcher_RadioButton_preview { - flex-grow: 1; - display: flex; - align-items: center; - padding: 10px; + .mxLayoutSwitcher_LayoutSelector_LayoutRadio_inline, + .mxLayoutSwitcher_LayoutSelector_LayoutRadio_EventTilePreview { + margin: var(--cpd-space-3x); + } + + /** + * Override the event tile style to make it fit in the selector + * Tweak also hover style and remove action bar + */ + .mxLayoutSwitcher_LayoutSelector_LayoutRadio_EventTilePreview { pointer-events: none; - .mx_EventTile[data-layout="bubble"] .mx_EventTile_line { - padding-right: 11px; + .mx_EventTile { + margin: 0; + + /** + * Hide the message options and message action bar in the preview + */ + .mx_EventTile_msgOption, + .mx_MessageActionBar { + display: none; + } + + .mx_EventTile_content { + margin-right: 0; + } + + &[data-layout="group"] { + margin-top: calc(var(--cpd-space-3x) * -1); + } + + /** + * Add margin to center the bubble + */ + &[data-layout="bubble"] { + /** + * Add the layout margin and the margin to vertically center the bubble + */ + margin-top: var(--cpd-space-6x); + margin-right: 34px; + flex-shrink: 1; + } + + .mx_EventTile_line { + max-width: 100%; + } } } - .mx_StyledRadioButton { - flex-grow: 0; - padding: 10px; - } - - .mx_EventTile_content { - margin-right: 0; - } - - &.mx_LayoutSwitcher_RadioButton_selected { - border-color: var(--cpd-color-bg-accent-rest); - } - } - - .mx_StyledRadioButton { - border-top: 1px solid $quinary-content; - } - - .mx_StyledRadioButton_checked { - background-color: var(--cpd-color-bg-subtle-secondary); - } - - .mx_EventTile { - margin: 0; - &[data-layout="bubble"] { - margin-right: 40px; - flex-shrink: 1; - } - &[data-layout="irc"] { - > a { - display: none; - } - } - .mx_EventTile_line { - max-width: 90%; + .mxLayoutSwitcher_LayoutSelector_LayoutRadio_separator { + border-top: 0; + border-bottom: 1px solid var(--cpd-color-border-interactive-secondary); } } } diff --git a/res/css/views/settings/_ThemeChoicePanel.pcss b/res/css/views/settings/_ThemeChoicePanel.pcss index 8616668224..f70cdf92e3 100644 --- a/res/css/views/settings/_ThemeChoicePanel.pcss +++ b/res/css/views/settings/_ThemeChoicePanel.pcss @@ -14,48 +14,72 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_ThemeChoicePanel_themeSelectors { - color: $primary-content; +.mx_ThemeChoicePanel_ThemeSelectors { display: flex; - flex-direction: row; flex-wrap: wrap; + /* Override form default style */ + flex-direction: row !important; + gap: var(--cpd-space-4x) !important; - > .mx_StyledRadioButton { - align-items: center; - padding: $font-16px; - box-sizing: border-box; - border-radius: 10px; - width: 180px; + .mx_ThemeChoicePanel_themeSelector { + border: 1px solid var(--cpd-color-border-interactive-secondary); + border-radius: var(--cpd-space-1-5x); + padding: var(--cpd-space-3x) var(--cpd-space-5x) var(--cpd-space-3x) var(--cpd-space-3x); + gap: var(--cpd-space-2x); + background-color: var(--cpd-color-bg-canvas-default); - background: $accent-200; - opacity: 0.4; - - flex-shrink: 1; - flex-grow: 0; - - margin-right: 15px; - margin-top: 10px; - - font-weight: var(--cpd-font-weight-semibold); - - > span { - justify-content: center; - } - } - - > .mx_StyledRadioButton_enabled { - opacity: 1; - - /* These colors need to be hardcoded because they don't change with the theme */ - &.mx_ThemeSelector_light { - background-color: #f3f8fd; - color: #2e2f32; + &.mx_ThemeChoicePanel_themeSelector_enabled { + border-color: var(--cpd-color-border-interactive-primary); } - &.mx_ThemeSelector_dark { - /* 5% lightened version of 181b21 */ - background-color: #25282e; - color: #f3f8fd; + &.mx_ThemeChoicePanel_themeSelector_disabled { + border-color: var(--cpd-color-border-disabled); + } + + .mx_ThemeChoicePanel_themeSelector_Label { + color: var(--cpd-color-text-primary); + font: var(--cpd-font-body-md-semibold); + } + } +} + +.mx_ThemeChoicePanel_CustomTheme { + width: 100%; + display: flex; + flex-direction: column; + gap: var(--cpd-space-4x); + + .mx_ThemeChoicePanel_CustomTheme_EditInPlace input:focus { + /* + * When the input is focused, the border is growing + * We need to move it a bit to avoid the left border to be under the left panel + */ + margin-left: var(--cpd-space-0-5x); + } + + .mx_ThemeChoicePanel_CustomThemeList { + display: flex; + flex-direction: column; + gap: var(--cpd-space-4x); + /* + * Override the default padding/margin of the list + */ + padding: 0; + margin: 0; + + .mx_ThemeChoicePanel_CustomThemeList_theme { + display: flex; + justify-content: space-between; + align-items: center; + background-color: var(--cpd-color-gray-200); + padding: var(--cpd-space-2x) var(--cpd-space-2x) var(--cpd-space-2x) var(--cpd-space-4x); + + .mx_ThemeChoicePanel_CustomThemeList_name { + font: var(--cpd-font-body-sm-semibold); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } } } } diff --git a/res/css/views/settings/_UserProfileSettings.pcss b/res/css/views/settings/_UserProfileSettings.pcss index f9ca149bf7..31c1ef628f 100644 --- a/res/css/views/settings/_UserProfileSettings.pcss +++ b/res/css/views/settings/_UserProfileSettings.pcss @@ -35,6 +35,7 @@ limitations under the License. .mx_UserProfileSettings_profile_controls_userId { width: 100%; + margin-top: var(--cpd-space-4x); .mx_CopyableText { margin-top: var(--cpd-space-1x); width: 100%; @@ -46,6 +47,15 @@ limitations under the License. font-size: 15px; font-weight: 500; } + + .mx_UserProfileSettings_profile_buttons { + margin-top: var(--cpd-space-8x); + margin-bottom: var(--cpd-space-8x); + } + + .mx_UserProfileSettings_accountmanageIcon { + margin-right: var(--cpd-space-2x); + } } @media (max-width: 768px) { diff --git a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.pcss b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.pcss index 76c5834fa8..a59f64b391 100644 --- a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.pcss +++ b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.pcss @@ -34,3 +34,8 @@ limitations under the License. margin-right: $spacing-8; margin-bottom: 2px; } + +.mx_GeneralUserSettingsTab_section_hint { + font: var(--cpd-font-body-sm-regular); + color: var(--cpd-color-text-secondary); +} diff --git a/res/css/views/terms/_InlineTermsAgreement.pcss b/res/css/views/terms/_InlineTermsAgreement.pcss index d7732b2a0d..162d1341e4 100644 --- a/res/css/views/terms/_InlineTermsAgreement.pcss +++ b/res/css/views/terms/_InlineTermsAgreement.pcss @@ -15,6 +15,7 @@ limitations under the License. */ .mx_InlineTermsAgreement_cbContainer { + margin-top: var(--cpd-space-4x); margin-bottom: 10px; font: var(--cpd-font-body-md-regular); diff --git a/src/DecryptionFailureTracker.ts b/src/DecryptionFailureTracker.ts index 548555cd1d..d16ddedbde 100644 --- a/src/DecryptionFailureTracker.ts +++ b/src/DecryptionFailureTracker.ts @@ -20,6 +20,7 @@ import { Error as ErrorEvent } from "@matrix-org/analytics-events/types/typescri import { DecryptionFailureCode } from "matrix-js-sdk/src/crypto-api"; import { PosthogAnalytics } from "./PosthogAnalytics"; +import { MEGOLM_ENCRYPTION_ALGORITHM } from "./utils/crypto"; /** The key that we use to store the `reportedEvents` bloom filter in localstorage */ const DECRYPTION_FAILURE_STORAGE_KEY = "mx_decryption_failure_event_ids"; @@ -207,7 +208,7 @@ export class DecryptionFailureTracker { */ private eventDecrypted(e: MatrixEvent, nowTs: number): void { // for now we only track megolm decryption failures - if (e.getWireContent().algorithm != "m.megolm.v1.aes-sha2") { + if (e.getWireContent().algorithm != MEGOLM_ENCRYPTION_ALGORITHM) { return; } const errCode = e.decryptionFailureReason; diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 655be4ac92..888c30d76c 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -20,13 +20,11 @@ limitations under the License. import React, { LegacyRef, ReactNode } from "react"; import sanitizeHtml from "sanitize-html"; import classNames from "classnames"; -import EMOJIBASE_REGEX from "emojibase-regex"; import katex from "katex"; import { decode } from "html-entities"; import { IContent } from "matrix-js-sdk/src/matrix"; import { Optional } from "matrix-events-sdk"; import escapeHtml from "escape-html"; -import GraphemeSplitter from "graphemer"; import { getEmojiFromUnicode } from "@matrix-org/emojibase-bindings"; import { IExtendedSanitizeOptions } from "./@types/sanitize-html"; @@ -34,6 +32,7 @@ import SettingsStore from "./settings/SettingsStore"; import { stripHTMLReply, stripPlainReply } from "./utils/Reply"; import { PERMITTED_URL_SCHEMES } from "./utils/UrlUtils"; import { sanitizeHtmlParams, transformTags } from "./Linkify"; +import { graphemeSegmenter } from "./utils/strings"; export { Linkify, linkifyElement, linkifyAndSanitizeHtml } from "./Linkify"; @@ -46,10 +45,35 @@ const SURROGATE_PAIR_PATTERN = /([\ud800-\udbff])([\udc00-\udfff])/; const SYMBOL_PATTERN = /([\u2100-\u2bff])/; // Regex pattern for non-emoji characters that can appear in an "all-emoji" message -// (Zero-Width Joiner, Zero-Width Space, Emoji presentation character, other whitespace) -const EMOJI_SEPARATOR_REGEX = /[\u200D\u200B\s]|\uFE0F/g; +// (Zero-Width Space, other whitespace) +const EMOJI_SEPARATOR_REGEX = /[\u200B\s]/g; -const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, "i"); +// Regex for emoji. This includes any RGI_Emoji sequence followed by an optional +// emoji presentation VS (U+FE0F), but not those sequences that are followed by +// a text presentation VS (U+FE0E). We also count lone regional indicators +// (U+1F1E6-U+1F1FF). Technically this regex produces false negatives for emoji +// followed by U+FE0E when the emoji doesn't have a text variant, but in +// practice this doesn't matter. +export const EMOJI_REGEX = (() => { + try { + // Per our support policy, v mode is available to us, but we still don't + // want the app to completely crash on older platforms. We use the + // constructor here to avoid a syntax error on such platforms. + return new RegExp("\\p{RGI_Emoji}(?!\\uFE0E)(?:(? { + try { + return new RegExp(`^(${EMOJI_REGEX.source})+$`, "iv"); + } catch (_e) { + // Fall back, just like for EMOJI_REGEX + return /(?!)/; + } +})(); /* * Return true if the given string contains emoji @@ -265,17 +289,16 @@ export function formatEmojis(message: string | undefined, isHtmlMessage?: boolea let text = ""; let key = 0; - const splitter = new GraphemeSplitter(); - for (const char of splitter.iterateGraphemes(message)) { - if (EMOJIBASE_REGEX.test(char)) { + for (const data of graphemeSegmenter.segment(message)) { + if (EMOJI_REGEX.test(data.segment)) { if (text) { result.push(text); text = ""; } - result.push(emojiToSpan(char, key)); + result.push(emojiToSpan(data.segment, key)); key++; } else { - text += char; + text += data.segment; } } if (text) { diff --git a/src/LegacyCallHandler.tsx b/src/LegacyCallHandler.tsx index 7abdb236ae..c8ee1d5c74 100644 --- a/src/LegacyCallHandler.tsx +++ b/src/LegacyCallHandler.tsx @@ -66,6 +66,7 @@ import { localNotificationsAreSilenced } from "./utils/notifications"; import { SdkContextClass } from "./contexts/SDKContext"; import { showCantStartACallDialog } from "./voice-broadcast/utils/showCantStartACallDialog"; import { isNotNull } from "./Typeguards"; +import { BackgroundAudio } from "./audio/BackgroundAudio"; export const PROTOCOL_PSTN = "m.protocol.pstn"; export const PROTOCOL_PSTN_PREFIXED = "im.vector.protocol.pstn"; @@ -157,8 +158,6 @@ export default class LegacyCallHandler extends EventEmitter { // Calls started as an attended transfer, ie. with the intention of transferring another // call with a different party to this one. private transferees = new Map(); // callId (target) -> call (transferee) - private audioPromises = new Map>(); - private audioElementsWithListeners = new Map(); private supportsPstnProtocol: boolean | null = null; private pstnSupportPrefixed: boolean | null = null; // True if the server only support the prefixed pstn protocol private supportsSipNativeVirtual: boolean | null = null; // im.vector.protocol.sip_virtual and im.vector.protocol.sip_native @@ -170,6 +169,9 @@ export default class LegacyCallHandler extends EventEmitter { private silencedCalls = new Set(); // callIds + private backgroundAudio = new BackgroundAudio(); + private playingSources: Record = {}; // Record them for stopping + public static get instance(): LegacyCallHandler { if (!window.mxLegacyCallHandler) { window.mxLegacyCallHandler = new LegacyCallHandler(); @@ -199,33 +201,11 @@ export default class LegacyCallHandler extends EventEmitter { } public start(): void { - // add empty handlers for media actions, otherwise the media keys - // end up causing the audio elements with our ring/ringback etc - // audio clips in to play. - if (navigator.mediaSession) { - navigator.mediaSession.setActionHandler("play", function () {}); - navigator.mediaSession.setActionHandler("pause", function () {}); - navigator.mediaSession.setActionHandler("seekbackward", function () {}); - navigator.mediaSession.setActionHandler("seekforward", function () {}); - navigator.mediaSession.setActionHandler("previoustrack", function () {}); - navigator.mediaSession.setActionHandler("nexttrack", function () {}); - } - if (SettingsStore.getValue(UIFeature.Voip)) { MatrixClientPeg.safeGet().on(CallEventHandlerEvent.Incoming, this.onCallIncoming); } this.checkProtocols(CHECK_PROTOCOLS_ATTEMPTS); - - // Add event listeners for the