diff --git a/.eslintrc.js b/.eslintrc.js index 2b0dd2c186..a017112b4e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,5 +1,5 @@ module.exports = { - plugins: ["matrix-org"], + plugins: ["matrix-org", "eslint-plugin-react-compiler"], extends: ["plugin:matrix-org/babel", "plugin:matrix-org/react", "plugin:matrix-org/a11y"], parserOptions: { project: ["./tsconfig.json"], @@ -170,6 +170,8 @@ module.exports = { "jsx-a11y/role-supports-aria-props": "off", "matrix-org/require-copyright-header": "error", + + "react-compiler/react-compiler": "error", }, overrides: [ { @@ -262,6 +264,7 @@ module.exports = { // These are fine in tests "no-restricted-globals": "off", + "react-compiler/react-compiler": "off", }, }, { diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index af52a6b77d..e7a976a7be 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -13,7 +13,6 @@ # Ignore translations as those will be updated by GHA for Localazy download /src/i18n/strings -/src/i18n/strings/en_EN.json @element-hq/element-web-reviewers # Ignore the synapse plugin as this is updated by GHA for docker image updating /playwright/plugins/homeserver/synapse/index.ts diff --git a/.github/workflows/end-to-end-tests.yaml b/.github/workflows/end-to-end-tests.yaml index 5a75040866..1e40d0dfd5 100644 --- a/.github/workflows/end-to-end-tests.yaml +++ b/.github/workflows/end-to-end-tests.yaml @@ -130,12 +130,8 @@ jobs: if: steps.playwright-cache.outputs.cache-hit != 'true' run: yarn playwright install --with-deps --no-shell chromium - # We skip tests tagged with @mergequeue when running on PRs, but run them in MQ and everywhere else - name: Run Playwright tests - run: | - yarn playwright test \ - --shard "${{ matrix.runner }}/${{ strategy.job-total }}" \ - ${{ github.event_name == 'pull_request' && '--grep-invert @mergequeue' || '' }} + run: yarn playwright test --shard ${{ matrix.runner }}/${{ strategy.job-total }} - name: Upload blob report to GitHub Actions Artifacts if: always() diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 35803a60f1..0c531f89b4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -104,7 +104,7 @@ jobs: - name: Skip SonarCloud in merge queue if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true' - uses: guibranco/github-status-action-v2@d469d49426f5a7b8a1fbcac20ad274d3e4892321 + uses: guibranco/github-status-action-v2@66088c44e212a906c32a047529a213d81809ec1c with: authToken: ${{ secrets.GITHUB_TOKEN }} state: success diff --git a/__mocks__/maplibre-gl.js b/__mocks__/maplibre-gl.js index c410e4f24c..b47d4c02f8 100644 --- a/__mocks__/maplibre-gl.js +++ b/__mocks__/maplibre-gl.js @@ -17,6 +17,7 @@ class MockMap extends EventEmitter { setCenter = jest.fn(); setStyle = jest.fn(); fitBounds = jest.fn(); + remove = jest.fn(); } const MockMapInstance = new MockMap(); diff --git a/docs/playwright.md b/docs/playwright.md index 4af3194220..1454c4868f 100644 --- a/docs/playwright.md +++ b/docs/playwright.md @@ -217,10 +217,3 @@ instead of the native `toHaveScreenshot`. If you are running Linux and are unfortunate that the screenshots are not rendering identically, you may wish to specify `--ignore-snapshots` and rely on Docker to render them for you. - -## Test Tags - -We use test tags to categorise tests for running subsets more efficiently. - -- `@mergequeue`: Tests that are slow or flaky and cover areas of the app we update seldom, should not be run on every PR commit but will be run in the Merge Queue. -- `@screenshot`: Tests that use `toMatchScreenshot` to speed up a run of `test:playwright:screenshots`. A test with this tag must not also have the `@mergequeue` tag as this would cause false positives in the stale screenshot detection. diff --git a/package.json b/package.json index 9cd0163945..902852a8ef 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ "@matrix-org/spec": "^1.7.0", "@sentry/browser": "^8.0.0", "@vector-im/compound-design-tokens": "^2.0.1", - "@vector-im/compound-web": "^7.5.0", + "@vector-im/compound-web": "^7.4.0", "@vector-im/matrix-wysiwyg": "2.37.13", "@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4", @@ -116,10 +116,10 @@ "jsrsasign": "^11.0.0", "jszip": "^3.7.0", "katex": "^0.16.0", - "linkify-element": "4.2.0", - "linkify-react": "4.2.0", - "linkify-string": "4.2.0", - "linkifyjs": "4.2.0", + "linkify-element": "4.1.4", + "linkify-react": "4.1.4", + "linkify-string": "4.1.4", + "linkifyjs": "4.1.4", "lodash": "^4.17.21", "maplibre-gl": "^4.0.0", "matrix-encrypt-attachment": "^1.0.3", @@ -233,6 +233,7 @@ "eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-matrix-org": "^2.0.2", "eslint-plugin-react": "^7.28.0", + "eslint-plugin-react-compiler": "^19.0.0-beta-df7b47d-20241124", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-unicorn": "^56.0.0", "express": "^4.18.2", @@ -269,7 +270,7 @@ "postcss-preset-env": "^10.0.0", "postcss-scss": "^4.0.4", "postcss-simple-vars": "^7.0.1", - "prettier": "3.4.2", + "prettier": "3.4.1", "process": "^0.11.10", "raw-loader": "^4.0.2", "rimraf": "^6.0.0", diff --git a/playwright/e2e/crypto/user-verification.spec.ts b/playwright/e2e/crypto/user-verification.spec.ts index bd3d859526..4c8d641e6f 100644 --- a/playwright/e2e/crypto/user-verification.spec.ts +++ b/playwright/e2e/crypto/user-verification.spec.ts @@ -8,7 +8,6 @@ Please see LICENSE files in the repository root for full details. import { type Preset, type Visibility } from "matrix-js-sdk/src/matrix"; -import type { Page } from "@playwright/test"; import { test, expect } from "../../element-web-test"; import { doTwoWaySasVerification, awaitVerifier } from "./utils"; import { Client } from "../../pages/client"; @@ -39,8 +38,6 @@ test.describe("User verification", () => { toasts, room: { roomId: dmRoomId }, }) => { - await waitForDeviceKeys(page); - // once Alice has joined, Bob starts the verification const bobVerificationRequest = await bob.evaluateHandle( async (client, { dmRoomId, aliceCredentials }) => { @@ -90,8 +87,6 @@ test.describe("User verification", () => { toasts, room: { roomId: dmRoomId }, }) => { - await waitForDeviceKeys(page); - // once Alice has joined, Bob starts the verification const bobVerificationRequest = await bob.evaluateHandle( async (client, { dmRoomId, aliceCredentials }) => { @@ -154,15 +149,3 @@ async function createDMRoom(client: Client, userId: string): Promise { ], }); } - -/** - * Wait until we get the other user's device keys. - * In newer rust-crypto versions, the verification request will be ignored if we - * don't have the sender's device keys. - */ -async function waitForDeviceKeys(page: Page): Promise { - await expect(page.getByRole("button", { name: "Avatar" })).toBeVisible(); - const avatar = await page.getByRole("button", { name: "Avatar" }); - await avatar.click(); - await expect(page.getByText("1 session")).toBeVisible(); -} diff --git a/playwright/e2e/pinned-messages/index.ts b/playwright/e2e/pinned-messages/index.ts index 545d0e3438..ac50b62294 100644 --- a/playwright/e2e/pinned-messages/index.ts +++ b/playwright/e2e/pinned-messages/index.ts @@ -129,7 +129,6 @@ export class Helpers { const timelineMessage = this.page.locator(".mx_MTextBody", { hasText: message }); await timelineMessage.click({ button: "right" }); await this.page.getByRole("menuitem", { name: "Pin", exact: true }).click(); - await this.assertMessageInBanner(message); } /** diff --git a/playwright/e2e/read-receipts/editing-messages-in-threads.spec.ts b/playwright/e2e/read-receipts/editing-messages-in-threads.spec.ts index 42191831c8..277de62876 100644 --- a/playwright/e2e/read-receipts/editing-messages-in-threads.spec.ts +++ b/playwright/e2e/read-receipts/editing-messages-in-threads.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", { tag: "@mergequeue" }, () => { +test.describe("Read receipts", () => { test.describe("editing messages", () => { test.describe("in threads", () => { test("An edit of a threaded message makes the room unread", async ({ diff --git a/playwright/e2e/read-receipts/editing-messages-main-timeline.spec.ts b/playwright/e2e/read-receipts/editing-messages-main-timeline.spec.ts index a464822305..027ab08e2d 100644 --- a/playwright/e2e/read-receipts/editing-messages-main-timeline.spec.ts +++ b/playwright/e2e/read-receipts/editing-messages-main-timeline.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", { tag: "@mergequeue" }, () => { +test.describe("Read receipts", () => { test.describe("editing messages", () => { test.describe("in the main timeline", () => { test("Editing a message leaves a room read", async ({ roomAlpha: room1, roomBeta: room2, util, msg }) => { diff --git a/playwright/e2e/read-receipts/editing-messages-thread-roots.spec.ts b/playwright/e2e/read-receipts/editing-messages-thread-roots.spec.ts index 506ed603bd..e653b5d9bd 100644 --- a/playwright/e2e/read-receipts/editing-messages-thread-roots.spec.ts +++ b/playwright/e2e/read-receipts/editing-messages-thread-roots.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", { tag: "@mergequeue" }, () => { +test.describe("Read receipts", () => { test.describe("editing messages", () => { test.describe("thread roots", () => { test("An edit of a thread root leaves the room read", async ({ diff --git a/playwright/e2e/read-receipts/high-level.spec.ts b/playwright/e2e/read-receipts/high-level.spec.ts index 457cf99481..7d4f4eb133 100644 --- a/playwright/e2e/read-receipts/high-level.spec.ts +++ b/playwright/e2e/read-receipts/high-level.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { customEvent, many, test } from "."; -test.describe("Read receipts", { tag: "@mergequeue" }, () => { +test.describe("Read receipts", () => { test.describe("Ignored events", () => { test("If all events after receipt are unimportant, the room is read", async ({ roomAlpha: room1, diff --git a/playwright/e2e/read-receipts/message-ordering.spec.ts b/playwright/e2e/read-receipts/message-ordering.spec.ts index d7f77fae5f..65875cf4a9 100644 --- a/playwright/e2e/read-receipts/message-ordering.spec.ts +++ b/playwright/e2e/read-receipts/message-ordering.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", { tag: "@mergequeue" }, () => { +test.describe("Read receipts", () => { test.describe("Message ordering", () => { test.describe("in the main timeline", () => { test.fixme( diff --git a/playwright/e2e/read-receipts/missing-referents.spec.ts b/playwright/e2e/read-receipts/missing-referents.spec.ts index a1741851e2..f798d7d455 100644 --- a/playwright/e2e/read-receipts/missing-referents.spec.ts +++ b/playwright/e2e/read-receipts/missing-referents.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", { tag: "@mergequeue" }, () => { +test.describe("Read receipts", () => { test.describe("messages with missing referents", () => { test.fixme( "A message in an unknown thread is not visible and the room is read", diff --git a/playwright/e2e/read-receipts/new-messages-in-threads.spec.ts b/playwright/e2e/read-receipts/new-messages-in-threads.spec.ts index 5407f3cb44..91d850fac8 100644 --- a/playwright/e2e/read-receipts/new-messages-in-threads.spec.ts +++ b/playwright/e2e/read-receipts/new-messages-in-threads.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { many, test } from "."; -test.describe("Read receipts", { tag: "@mergequeue" }, () => { +test.describe("Read receipts", () => { test.describe("new messages", () => { test.describe("in threads", () => { test("Receiving a message makes a room unread", async ({ diff --git a/playwright/e2e/read-receipts/new-messages-main-timeline.spec.ts b/playwright/e2e/read-receipts/new-messages-main-timeline.spec.ts index 92f7b10cdd..2000e444d6 100644 --- a/playwright/e2e/read-receipts/new-messages-main-timeline.spec.ts +++ b/playwright/e2e/read-receipts/new-messages-main-timeline.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { many, test } from "."; -test.describe("Read receipts", { tag: "@mergequeue" }, () => { +test.describe("Read receipts", () => { test.describe("new messages", () => { test.describe("in the main timeline", () => { test("Receiving a message makes a room unread", async ({ diff --git a/playwright/e2e/read-receipts/new-messages-thread-roots.spec.ts b/playwright/e2e/read-receipts/new-messages-thread-roots.spec.ts index 3c8ed7849f..878d0d4419 100644 --- a/playwright/e2e/read-receipts/new-messages-thread-roots.spec.ts +++ b/playwright/e2e/read-receipts/new-messages-thread-roots.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { many, test } from "."; -test.describe("Read receipts", { tag: "@mergequeue" }, () => { +test.describe("Read receipts", () => { test.describe("new messages", () => { test.describe("thread roots", () => { test("Reading a thread root does not mark the thread as read", async ({ diff --git a/playwright/e2e/read-receipts/notifications.spec.ts b/playwright/e2e/read-receipts/notifications.spec.ts index 46edc9a7a3..3050987be7 100644 --- a/playwright/e2e/read-receipts/notifications.spec.ts +++ b/playwright/e2e/read-receipts/notifications.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", { tag: "@mergequeue" }, () => { +test.describe("Read receipts", () => { test.describe("Notifications", () => { test.describe("in the main timeline", () => { test.fixme("A new message that mentions me shows a notification", () => {}); diff --git a/playwright/e2e/read-receipts/reactions-in-threads.spec.ts b/playwright/e2e/read-receipts/reactions-in-threads.spec.ts index 45b5e071ec..bc4a184744 100644 --- a/playwright/e2e/read-receipts/reactions-in-threads.spec.ts +++ b/playwright/e2e/read-receipts/reactions-in-threads.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test, expect } from "."; -test.describe("Read receipts", { tag: "@mergequeue" }, () => { +test.describe("Read receipts", () => { test.describe("reactions", () => { test.describe("in threads", () => { test("A reaction to a threaded message does not make the room unread", async ({ diff --git a/playwright/e2e/read-receipts/reactions-main-timeline.spec.ts b/playwright/e2e/read-receipts/reactions-main-timeline.spec.ts index 16d5c92eca..59d6eaea40 100644 --- a/playwright/e2e/read-receipts/reactions-main-timeline.spec.ts +++ b/playwright/e2e/read-receipts/reactions-main-timeline.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", { tag: "@mergequeue" }, () => { +test.describe("Read receipts", () => { test.describe("reactions", () => { test.describe("in the main timeline", () => { test("Receiving a reaction to a message does not make a room unread", async ({ diff --git a/playwright/e2e/read-receipts/reactions-thread-roots.spec.ts b/playwright/e2e/read-receipts/reactions-thread-roots.spec.ts index 817597a27e..219a73d5e4 100644 --- a/playwright/e2e/read-receipts/reactions-thread-roots.spec.ts +++ b/playwright/e2e/read-receipts/reactions-thread-roots.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", { tag: "@mergequeue" }, () => { +test.describe("Read receipts", () => { test.describe("reactions", () => { test.describe("thread roots", () => { test("A reaction to a thread root does not make the room unread", async ({ diff --git a/playwright/e2e/read-receipts/read-receipts.spec.ts b/playwright/e2e/read-receipts/read-receipts.spec.ts index f6515361f2..3056cc4a54 100644 --- a/playwright/e2e/read-receipts/read-receipts.spec.ts +++ b/playwright/e2e/read-receipts/read-receipts.spec.ts @@ -13,7 +13,7 @@ import { ElementAppPage } from "../../pages/ElementAppPage"; import { Bot } from "../../pages/bot"; import { test } from "."; -test.describe("Read receipts", { tag: "@mergequeue" }, () => { +test.describe("Read receipts", () => { test.use({ displayName: "Mae", botCreateOpts: { displayName: "Other User" }, diff --git a/playwright/e2e/read-receipts/redactions-in-threads.spec.ts b/playwright/e2e/read-receipts/redactions-in-threads.spec.ts index 25c19a4f97..715bb4e9fc 100644 --- a/playwright/e2e/read-receipts/redactions-in-threads.spec.ts +++ b/playwright/e2e/read-receipts/redactions-in-threads.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", { tag: "@mergequeue" }, () => { +test.describe("Read receipts", () => { test.describe("redactions", () => { test.describe("in threads", () => { test("Redacting the threaded message pointed to by my receipt leaves the room read", async ({ diff --git a/playwright/e2e/read-receipts/redactions-main-timeline.spec.ts b/playwright/e2e/read-receipts/redactions-main-timeline.spec.ts index 143d9685d8..c1dceda6a0 100644 --- a/playwright/e2e/read-receipts/redactions-main-timeline.spec.ts +++ b/playwright/e2e/read-receipts/redactions-main-timeline.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", { tag: "@mergequeue" }, () => { +test.describe("Read receipts", () => { test.describe("redactions", () => { test.describe("in the main timeline", () => { test("Redacting the message pointed to by my receipt leaves the room read", async ({ diff --git a/playwright/e2e/read-receipts/redactions-thread-roots.spec.ts b/playwright/e2e/read-receipts/redactions-thread-roots.spec.ts index 01f296075c..a8dc38c47e 100644 --- a/playwright/e2e/read-receipts/redactions-thread-roots.spec.ts +++ b/playwright/e2e/read-receipts/redactions-thread-roots.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", { tag: "@mergequeue" }, () => { +test.describe("Read receipts", () => { test.describe("redactions", () => { test.describe("thread roots", () => { test("Redacting a thread root after it was read leaves the room read", async ({ diff --git a/playwright/e2e/read-receipts/room-list-order.spec.ts b/playwright/e2e/read-receipts/room-list-order.spec.ts index 80dda202a3..052e2d756a 100644 --- a/playwright/e2e/read-receipts/room-list-order.spec.ts +++ b/playwright/e2e/read-receipts/room-list-order.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", { tag: "@mergequeue" }, () => { +test.describe("Read receipts", () => { test.describe("Room list order", () => { test("Rooms with unread messages appear at the top of room list if 'unread first' is selected", async ({ roomAlpha: room1, diff --git a/playwright/plugins/homeserver/synapse/index.ts b/playwright/plugins/homeserver/synapse/index.ts index 078ca2848f..51c2723f30 100644 --- a/playwright/plugins/homeserver/synapse/index.ts +++ b/playwright/plugins/homeserver/synapse/index.ts @@ -20,7 +20,7 @@ import { randB64Bytes } from "../../utils/rand"; // Docker tag to use for synapse docker 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:6b82dba715fa7ae641010b4cc5e71edaeb9cc05a50ac5b9e4ff09afa9cd2a80d"; +const DOCKER_TAG = "develop@sha256:48308e18c5b3ad20bc0d090119618f45b6be4ba727522e37fbf7827d1a109531"; async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise> { const templateDir = path.join(__dirname, "templates", opts.template); 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 index 41ffca6c93..b2b71375bd 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png 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/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-added-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-added-linux.png index 147fcfa057..0d18bff1c2 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-added-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-added-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-linux.png index 5475f9a537..9cadcde415 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-removed-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-removed-linux.png index 23b88c022c..1ec17661fe 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-removed-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-removed-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-dark-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-dark-linux.png index 6378098d7a..75db794a1a 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-dark-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-dark-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-light-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-light-linux.png index f2269a0532..357790598d 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-light-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-light-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-match-system-enabled-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-match-system-enabled-linux.png index 6b41f30acd..42f27d10bf 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-match-system-enabled-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-match-system-enabled-linux.png differ diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index f921cd291f..be36c5b689 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -44,7 +44,6 @@ import { IConfigOptions } from "../IConfigOptions"; import { MatrixDispatcher } from "../dispatcher/dispatcher"; import { DeepReadonly } from "./common"; import MatrixChat from "../components/structures/MatrixChat"; -import { InitialCryptoSetupStore } from "../stores/InitialCryptoSetupStore"; /* eslint-disable @typescript-eslint/naming-convention */ @@ -118,7 +117,6 @@ declare global { mxPerformanceEntryNames: any; mxUIStore: UIStore; mxSetupEncryptionStore?: SetupEncryptionStore; - mxInitialCryptoStore?: InitialCryptoSetupStore; mxRoomScrollStateStore?: RoomScrollStateStore; mxActiveWidgetStore?: ActiveWidgetStore; mxOnRecaptchaLoaded?: () => void; diff --git a/src/accessibility/context_menu/ContextMenuButton.tsx b/src/accessibility/context_menu/ContextMenuButton.tsx index 9d8b5585e3..d8c7d912c1 100644 --- a/src/accessibility/context_menu/ContextMenuButton.tsx +++ b/src/accessibility/context_menu/ContextMenuButton.tsx @@ -8,24 +8,25 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { forwardRef, Ref } from "react"; +import React, { ComponentProps, forwardRef, Ref } from "react"; -import AccessibleButton, { ButtonProps } from "../../components/views/elements/AccessibleButton"; +import AccessibleButton from "../../components/views/elements/AccessibleButton"; -type Props = ButtonProps & { +type Props = ComponentProps> & { label?: string; // whether the context menu is currently open isExpanded: boolean; }; // Semantic component for representing the AccessibleButton which launches a -export const ContextMenuButton = forwardRef(function ( - { label, isExpanded, children, onClick, onContextMenu, ...props }: Props, - ref: Ref, +export const ContextMenuButton = forwardRef(function ( + { label, isExpanded, children, onClick, onContextMenu, element, ...props }: Props, + ref: Ref, ) { return ( = ButtonProps & { +type Props = ComponentProps> & { // whether the context menu is currently open isExpanded: boolean; }; // Semantic component for representing the AccessibleButton which launches a -export const ContextMenuTooltipButton = forwardRef(function ( - { isExpanded, children, onClick, onContextMenu, ...props }: Props, - ref: Ref, +export const ContextMenuTooltipButton = forwardRef(function ( + { isExpanded, children, onClick, onContextMenu, element, ...props }: Props, + ref: Ref, ) { return ( = Omit, "tabIndex"> & { - inputRef?: RefObject; +type Props = Omit< + ComponentProps>, + "inputRef" | "tabIndex" +> & { + inputRef?: Ref; focusOnMouseOver?: boolean; }; // Wrapper to allow use of useRovingTabIndex for simple AccessibleButtons outside of React Functional Components. -export const RovingAccessibleButton = ({ +export const RovingAccessibleButton = ({ inputRef, onFocus, onMouseOver, focusOnMouseOver, + element, ...props }: Props): JSX.Element => { - const [onFocusInternal, isActive, ref] = useRovingTabIndex(inputRef); + const [onFocusInternal, isActive, ref] = useRovingTabIndex(inputRef); return ( ) => { + element={element as keyof JSX.IntrinsicElements} + onFocus={(event: React.FocusEvent) => { onFocusInternal(); onFocus?.(event); }} - onMouseOver={(event: React.MouseEvent) => { + onMouseOver={(event: React.MouseEvent) => { if (focusOnMouseOver) onFocusInternal(); onMouseOver?.(event); }} diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index ee120c430a..548dbff983 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -132,7 +132,6 @@ import { SessionLockStolenView } from "./auth/SessionLockStolenView"; import { ConfirmSessionLockTheftView } from "./auth/ConfirmSessionLockTheftView"; import { LoginSplashView } from "./auth/LoginSplashView"; import { cleanUpDraftsIfRequired } from "../../DraftCleaner"; -import { InitialCryptoSetupStore } from "../../stores/InitialCryptoSetupStore"; // legacy export export { default as Views } from "../../Views"; @@ -429,12 +428,6 @@ export default class MatrixChat extends React.PureComponent { !(await shouldSkipSetupEncryption(cli)) ) { // if cross-signing is not yet set up, do so now if possible. - InitialCryptoSetupStore.sharedInstance().startInitialCryptoSetup( - cli, - Boolean(this.tokenLogin), - this.stores, - this.onCompleteSecurityE2eSetupFinished, - ); this.setStateForNewView({ view: Views.E2E_SETUP }); } else { this.onLoggedIn(); @@ -2080,7 +2073,14 @@ export default class MatrixChat extends React.PureComponent { } else if (this.state.view === Views.COMPLETE_SECURITY) { view = ; } else if (this.state.view === Views.E2E_SETUP) { - view = ; + view = ( + + ); } else if (this.state.view === Views.LOGGED_IN) { // `ready` and `view==LOGGED_IN` may be set before `page_type` (because the // latter is set via the dispatcher). If we don't yet have a `page_type`, diff --git a/src/components/structures/RoomSearchView.tsx b/src/components/structures/RoomSearchView.tsx index 82146bcc5e..9e6263bcfb 100644 --- a/src/components/structures/RoomSearchView.tsx +++ b/src/components/structures/RoomSearchView.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { forwardRef, useCallback, useContext, useEffect, useRef, useState } from "react"; +import React, { forwardRef, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import { ISearchResults, IThreadBundledRelationship, @@ -58,7 +58,7 @@ export const RoomSearchView = forwardRef( const [results, setResults] = useState(null); const aborted = useRef(false); // A map from room ID to permalink creator - const permalinkCreators = useRef(new Map()).current; + const permalinkCreators = useMemo(() => new Map(), []); const innerRef = useRef(); useEffect(() => { diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 772d5698a3..891e6b97f4 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -2372,11 +2372,7 @@ export class RoomView extends React.Component { ); const pinnedMessageBanner = ( - + ); let messageComposer; diff --git a/src/components/structures/auth/E2eSetup.tsx b/src/components/structures/auth/E2eSetup.tsx index 3b064d6134..80a135fe19 100644 --- a/src/components/structures/auth/E2eSetup.tsx +++ b/src/components/structures/auth/E2eSetup.tsx @@ -7,13 +7,17 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; +import { MatrixClient } from "matrix-js-sdk/src/matrix"; import AuthPage from "../../views/auth/AuthPage"; import CompleteSecurityBody from "../../views/auth/CompleteSecurityBody"; -import { InitialCryptoSetupDialog } from "../../views/dialogs/security/InitialCryptoSetupDialog"; +import CreateCrossSigningDialog from "../../views/dialogs/security/CreateCrossSigningDialog"; interface IProps { + matrixClient: MatrixClient; onFinished: () => void; + accountPassword?: string; + tokenLogin: boolean; } export default class E2eSetup extends React.Component { @@ -21,7 +25,12 @@ export default class E2eSetup extends React.Component { return ( - + ); diff --git a/src/components/structures/auth/SoftLogout.tsx b/src/components/structures/auth/SoftLogout.tsx index 9c7a900643..db72a0a04b 100644 --- a/src/components/structures/auth/SoftLogout.tsx +++ b/src/components/structures/auth/SoftLogout.tsx @@ -235,7 +235,12 @@ export default class SoftLogout extends React.Component { value={this.state.password} disabled={this.state.busy} /> - + {_t("action|sign_in")} diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index ae5c07e348..b1360f5560 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -910,7 +910,7 @@ export class SSOAuthEntry extends React.Component extends React.Component { protected popupWindow: Window | null; - protected fallbackButton = createRef(); + protected fallbackButton = createRef(); public constructor(props: IAuthEntryProps & T) { super(props); diff --git a/src/components/views/dialogs/devtools/SettingExplorer.tsx b/src/components/views/dialogs/devtools/SettingExplorer.tsx index ae37fa3e1c..ed4b64d870 100644 --- a/src/components/views/dialogs/devtools/SettingExplorer.tsx +++ b/src/components/views/dialogs/devtools/SettingExplorer.tsx @@ -298,7 +298,7 @@ const SettingsList: React.FC = ({ onBack, onView, onEdit }) {i} onEdit(i)} className="mx_DevTools_SettingsExplorer_edit" > diff --git a/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx new file mode 100644 index 0000000000..73da6b178c --- /dev/null +++ b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx @@ -0,0 +1,99 @@ +/* +Copyright 2024 New Vector Ltd. +Copyright 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2018, 2019 New Vector Ltd + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import React, { useCallback, useEffect, useState } from "react"; +import { logger } from "matrix-js-sdk/src/logger"; +import { MatrixClient } from "matrix-js-sdk/src/matrix"; + +import { _t } from "../../../../languageHandler"; +import DialogButtons from "../../elements/DialogButtons"; +import BaseDialog from "../BaseDialog"; +import Spinner from "../../elements/Spinner"; +import { createCrossSigning } from "../../../../CreateCrossSigning"; + +interface Props { + matrixClient: MatrixClient; + accountPassword?: string; + tokenLogin: boolean; + onFinished: (success?: boolean) => void; +} + +/* + * Walks the user through the process of creating a cross-signing keys. In most + * cases, only a spinner is shown, but for more complex auth like SSO, the user + * may need to complete some steps to proceed. + */ +const CreateCrossSigningDialog: React.FC = ({ matrixClient, accountPassword, tokenLogin, onFinished }) => { + const [error, setError] = useState(false); + + const bootstrapCrossSigning = useCallback(async () => { + const cryptoApi = matrixClient.getCrypto(); + if (!cryptoApi) return; + + setError(false); + + try { + await createCrossSigning(matrixClient, tokenLogin, accountPassword); + onFinished(true); + } catch (e) { + if (tokenLogin) { + // ignore any failures, we are relying on grace period here + onFinished(false); + return; + } + + setError(true); + logger.error("Error bootstrapping cross-signing", e); + } + }, [matrixClient, tokenLogin, accountPassword, onFinished]); + + const onCancel = useCallback(() => { + onFinished(false); + }, [onFinished]); + + useEffect(() => { + bootstrapCrossSigning(); + }, [bootstrapCrossSigning]); + + let content; + if (error) { + content = ( +
+

{_t("encryption|unable_to_setup_keys_error")}

+
+ +
+
+ ); + } else { + content = ( +
+ +
+ ); + } + + return ( + +
{content}
+
+ ); +}; + +export default CreateCrossSigningDialog; diff --git a/src/components/views/dialogs/security/InitialCryptoSetupDialog.tsx b/src/components/views/dialogs/security/InitialCryptoSetupDialog.tsx deleted file mode 100644 index 22635662ce..0000000000 --- a/src/components/views/dialogs/security/InitialCryptoSetupDialog.tsx +++ /dev/null @@ -1,71 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. -Copyright 2018, 2019 New Vector Ltd - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React, { useCallback } from "react"; - -import { _t } from "../../../../languageHandler"; -import DialogButtons from "../../elements/DialogButtons"; -import BaseDialog from "../BaseDialog"; -import Spinner from "../../elements/Spinner"; -import { InitialCryptoSetupStore, useInitialCryptoSetupStatus } from "../../../../stores/InitialCryptoSetupStore"; - -interface Props { - onFinished: (success?: boolean) => void; -} - -/* - * Walks the user through the process of creating a cross-signing keys. - * In most cases, only a spinner is shown, but for more - * complex auth like SSO, the user may need to complete some steps to proceed. - */ -export const InitialCryptoSetupDialog: React.FC = ({ onFinished }) => { - const onRetryClick = useCallback(() => { - InitialCryptoSetupStore.sharedInstance().retry(); - }, []); - - const onCancelClick = useCallback(() => { - onFinished(false); - }, [onFinished]); - - const status = useInitialCryptoSetupStatus(InitialCryptoSetupStore.sharedInstance()); - - let content; - if (status === "error") { - content = ( -
-

{_t("encryption|unable_to_setup_keys_error")}

-
- -
-
- ); - } else { - content = ( -
- -
- ); - } - - return ( - -
{content}
-
- ); -}; diff --git a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx index a87b7341e7..20d6825b9b 100644 --- a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx +++ b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx @@ -1253,7 +1253,7 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n {filterToLabel(filter)} = ButtonProps & { - className?: string; +type TooltipOptionProps = ButtonProps & { endAdornment?: ReactNode; inputRef?: Ref; }; -export const TooltipOption = ({ +export const TooltipOption = ({ inputRef, className, + element, ...props }: TooltipOptionProps): JSX.Element => { const [onFocus, isActive, ref] = useRovingTabIndex(inputRef); @@ -34,6 +34,7 @@ export const TooltipOption = ({ tabIndex={-1} aria-selected={isActive} role="option" + element={element as keyof JSX.IntrinsicElements} /> ); }; diff --git a/src/components/views/directory/NetworkDropdown.tsx b/src/components/views/directory/NetworkDropdown.tsx index a1b1986f47..43d123676b 100644 --- a/src/components/views/directory/NetworkDropdown.tsx +++ b/src/components/views/directory/NetworkDropdown.tsx @@ -168,7 +168,7 @@ export const NetworkDropdown: React.FC = ({ protocols, config, setConfig adornment: ( setUserDefinedServers(without(userDefinedServers, roomServer))} /> ), diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx index 8b58f251c3..b8b5297384 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -6,15 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { - ComponentProps, - ComponentPropsWithoutRef, - forwardRef, - FunctionComponent, - ReactElement, - KeyboardEvent, - Ref, -} from "react"; +import React, { ComponentProps, forwardRef, FunctionComponent, HTMLAttributes, InputHTMLAttributes, Ref } from "react"; import classnames from "classnames"; import { Tooltip } from "@vector-im/compound-web"; @@ -46,8 +38,20 @@ export type AccessibleButtonKind = | "icon_primary" | "icon_primary_outline"; -type ElementType = keyof HTMLElementTagNameMap; -const defaultElement = "div"; +/** + * This type construct allows us to specifically pass those props down to the element we’re creating that the element + * actually supports. + * + * e.g., if element is set to "a", we’ll support href and target, if it’s set to "input", we support type. + * + * To remain compatible with existing code, we’ll continue to support InputHTMLAttributes + */ +type DynamicHtmlElementProps = + JSX.IntrinsicElements[T] extends HTMLAttributes<{}> ? DynamicElementProps : DynamicElementProps<"div">; +type DynamicElementProps = Partial< + Omit +> & + Omit, "onClick">; type TooltipProps = ComponentProps; @@ -56,7 +60,7 @@ type TooltipProps = ComponentProps; * * Extends props accepted by the underlying element specified using the `element` prop. */ -type Props = { +type Props = DynamicHtmlElementProps & { /** * The base element type. "div" by default. */ @@ -101,12 +105,14 @@ type Props = { disableTooltip?: TooltipProps["disabled"]; }; -export type ButtonProps = Props & Omit, keyof Props>; +export type ButtonProps = Props; /** * Type of the props passed to the element that is rendered by AccessibleButton. */ -type RenderedElementProps = React.InputHTMLAttributes & RefProp; +interface RenderedElementProps extends React.InputHTMLAttributes { + ref?: React.Ref; +} /** * AccessibleButton is a generic wrapper for any element that should be treated @@ -118,9 +124,9 @@ type RenderedElementProps = React.InputHTMLAttributes( +const AccessibleButton = forwardRef(function ( { - element, + element = "div" as T, onClick, children, kind, @@ -135,10 +141,10 @@ const AccessibleButton = forwardRef(function , - ref: Ref, + }: Props, + ref: Ref, ): JSX.Element { - const newProps = restProps as RenderedElementProps; + const newProps: RenderedElementProps = restProps; newProps["aria-label"] = newProps["aria-label"] ?? title; if (disabled) { newProps["aria-disabled"] = true; @@ -156,7 +162,7 @@ const AccessibleButton = forwardRef(function ) => { + newProps.onKeyDown = (e) => { const action = getKeyBindingsManager().getAccessibilityAction(e); switch (action) { @@ -172,7 +178,7 @@ const AccessibleButton = forwardRef(function ) => { + newProps.onKeyUp = (e) => { const action = getKeyBindingsManager().getAccessibilityAction(e); switch (action) { @@ -201,7 +207,7 @@ const AccessibleButton = forwardRef(function { - ref?: Ref; -} - -interface ButtonComponent { - // With the explicit `element` prop - (props: { element?: C } & ButtonProps & RefProp): ReactElement; - // Without the explicit `element` prop - (props: ButtonProps<"div"> & RefProp<"div">): ReactElement; -} - -export default AccessibleButton as ButtonComponent; +export default AccessibleButton; diff --git a/src/components/views/elements/EditableItemList.tsx b/src/components/views/elements/EditableItemList.tsx index ad2d9aceee..dc6e6c09a1 100644 --- a/src/components/views/elements/EditableItemList.tsx +++ b/src/components/views/elements/EditableItemList.tsx @@ -133,7 +133,12 @@ export default class EditableItemList

extends React.PureComponent - + {_t("action|add")} diff --git a/src/components/views/elements/EffectsOverlay.tsx b/src/components/views/elements/EffectsOverlay.tsx index 3e5a5ead60..ad7d9c825e 100644 --- a/src/components/views/elements/EffectsOverlay.tsx +++ b/src/components/views/elements/EffectsOverlay.tsx @@ -58,11 +58,10 @@ const EffectsOverlay: FunctionComponent = ({ roomWidth }) => { if (canvas) canvas.height = UIStore.instance.windowHeight; UIStore.instance.on(UI_EVENTS.Resize, resize); + const currentEffects = effectsRef.current; // this is not a react node ref, warning can be safely ignored return () => { dis.unregister(dispatcherRef); UIStore.instance.off(UI_EVENTS.Resize, resize); - // eslint-disable-next-line react-hooks/exhaustive-deps - const currentEffects = effectsRef.current; // this is not a react node ref, warning can be safely ignored for (const effect in currentEffects) { const effectModule: ICanvasEffect = currentEffects.get(effect)!; if (effectModule && effectModule.isRunning) { diff --git a/src/components/views/emojipicker/Emoji.tsx b/src/components/views/emojipicker/Emoji.tsx index a852122b75..c3dfb24bd1 100644 --- a/src/components/views/emojipicker/Emoji.tsx +++ b/src/components/views/emojipicker/Emoji.tsx @@ -31,7 +31,7 @@ class Emoji extends React.PureComponent { return ( onClick(ev, emoji)} + onClick={(ev) => onClick(ev, emoji)} onMouseEnter={() => onMouseEnter(emoji)} onMouseLeave={() => onMouseLeave(emoji)} className="mx_EmojiPicker_item_wrapper" diff --git a/src/components/views/messages/MPollEndBody.tsx b/src/components/views/messages/MPollEndBody.tsx index 1129b3538e..94671fea12 100644 --- a/src/components/views/messages/MPollEndBody.tsx +++ b/src/components/views/messages/MPollEndBody.tsx @@ -90,7 +90,7 @@ export const MPollEndBody = React.forwardRef(({ mxEvent, ...pro const { pollStartEvent, isLoadingPollStartEvent } = usePollStartEvent(mxEvent); if (!pollStartEvent) { - const pollEndFallbackMessage = M_TEXT.findIn(mxEvent.getContent()) || textForEvent(mxEvent, cli); + const pollEndFallbackMessage = M_TEXT.findIn(mxEvent.getContent()) || textForEvent(mxEvent, cli); return ( <> diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx index 579db054e9..9d21b8fa45 100644 --- a/src/components/views/messages/MessageActionBar.tsx +++ b/src/components/views/messages/MessageActionBar.tsx @@ -435,7 +435,7 @@ export default class MessageActionBar extends React.PureComponent this.onPinClick(e, isPinned)} + onClick={(e) => this.onPinClick(e, isPinned)} onContextMenu={(e: ButtonEvent) => this.onPinClick(e, isPinned)} key="pin" placement="left" diff --git a/src/components/views/rooms/PinnedMessageBanner.tsx b/src/components/views/rooms/PinnedMessageBanner.tsx index 32000d5792..f44b4417c9 100644 --- a/src/components/views/rooms/PinnedMessageBanner.tsx +++ b/src/components/views/rooms/PinnedMessageBanner.tsx @@ -6,10 +6,10 @@ * Please see LICENSE files in the repository root for full details. */ -import React, { JSX, useEffect, useRef, useState } from "react"; +import React, { JSX, useEffect, useState } from "react"; import PinIcon from "@vector-im/compound-design-tokens/assets/web/icons/pin-solid"; import { Button } from "@vector-im/compound-web"; -import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; +import { Room } from "matrix-js-sdk/src/matrix"; import classNames from "classnames"; import { usePinnedEvents, useSortedFetchedPinnedEvents } from "../../../hooks/usePinnedEvents"; @@ -25,7 +25,6 @@ import { Action } from "../../../dispatcher/actions"; import MessageEvent from "../messages/MessageEvent"; import PosthogTrackers from "../../../PosthogTrackers.ts"; import { EventPreview } from "./EventPreview.tsx"; -import ResizeNotifier from "../../../utils/ResizeNotifier"; /** * The props for the {@link PinnedMessageBanner} component. @@ -39,20 +38,12 @@ interface PinnedMessageBannerProps { * The room where the banner is displayed */ room: Room; - /** - * The resize notifier to notify the timeline to resize itself when the banner is displayed or hidden. - */ - resizeNotifier: ResizeNotifier; } /** * A banner that displays the pinned messages in a room. */ -export function PinnedMessageBanner({ - room, - permalinkCreator, - resizeNotifier, -}: PinnedMessageBannerProps): JSX.Element | null { +export function PinnedMessageBanner({ room, permalinkCreator }: PinnedMessageBannerProps): JSX.Element | null { const pinnedEventIds = usePinnedEvents(room); const pinnedEvents = useSortedFetchedPinnedEvents(room, pinnedEventIds); const eventCount = pinnedEvents.length; @@ -65,8 +56,6 @@ export function PinnedMessageBanner({ }, [eventCount]); const pinnedEvent = pinnedEvents[currentEventIndex]; - useNotifyTimeline(pinnedEvent, resizeNotifier); - if (!pinnedEvent) return null; const shouldUseMessageEvent = pinnedEvent.isRedacted() || pinnedEvent.isDecryptionFailure(); @@ -139,23 +128,6 @@ export function PinnedMessageBanner({ ); } -/** - * When the banner is displayed or hidden, we want to notify the timeline to resize itself. - * @param pinnedEvent - * @param resizeNotifier - */ -function useNotifyTimeline(pinnedEvent: MatrixEvent | null, resizeNotifier: ResizeNotifier): void { - const previousEvent = useRef(null); - useEffect(() => { - // If we switch from a pinned message to no pinned message or the opposite, we want to resize the timeline - if ((previousEvent.current && !pinnedEvent) || (!previousEvent.current && pinnedEvent)) { - resizeNotifier.notifyTimelineHeightChanged(); - } - - previousEvent.current = pinnedEvent; - }, [pinnedEvent, resizeNotifier]); -} - const MAX_INDICATORS = 3; /** diff --git a/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx index 98597c7360..a28d274646 100644 --- a/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { ForwardedRef, forwardRef, MutableRefObject, useRef } from "react"; +import React, { ForwardedRef, forwardRef, MutableRefObject, useMemo } from "react"; import classNames from "classnames"; import EditorStateTransfer from "../../../../utils/EditorStateTransfer"; @@ -44,7 +44,7 @@ export default function EditWysiwygComposer({ className, ...props }: EditWysiwygComposerProps): JSX.Element { - const defaultContextValue = useRef(getDefaultContextValue({ editorStateTransfer })); + const defaultContextValue = useMemo(() => getDefaultContextValue({ editorStateTransfer }), []); const initialContent = useInitialContent(editorStateTransfer); const isReady = !editorStateTransfer || initialContent !== undefined; @@ -55,7 +55,7 @@ export default function EditWysiwygComposer({ } return ( - + getDefaultContextValue({ eventRelation: props.eventRelation }), []); return ( - + } diff --git a/src/components/views/settings/SetIdServer.tsx b/src/components/views/settings/SetIdServer.tsx index 6dc3ae48a2..8ed6461d0a 100644 --- a/src/components/views/settings/SetIdServer.tsx +++ b/src/components/views/settings/SetIdServer.tsx @@ -407,6 +407,7 @@ export default class SetIdServer extends React.Component { forceValidity={this.state.error ? false : undefined} /> new ThemeWatcher(), []); const customThemeEnabled = useSettingValue("feature_custom_themes"); return ( - {themeWatcher.current.isSystemThemeSupported() && ( + {themeWatcher.isSystemThemeSupported() && ( )} diff --git a/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx b/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx index a04430a0c2..e7839b71da 100644 --- a/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx +++ b/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx @@ -7,21 +7,23 @@ Please see LICENSE files in the repository root for full details. */ import classNames from "classnames"; -import React from "react"; +import React, { ComponentProps } from "react"; import { ChevronDownIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { _t } from "../../../../languageHandler"; -import AccessibleButton, { ButtonProps } from "../../elements/AccessibleButton"; +import AccessibleButton from "../../elements/AccessibleButton"; -type Props = Omit< - ButtonProps, - "aria-label" | "title" | "kind" | "className" | "element" +type Props = Omit< + ComponentProps>, + "aria-label" | "title" | "kind" | "className" | "onClick" | "element" > & { isExpanded: boolean; + onClick: () => void; }; -export const DeviceExpandDetailsButton = ({ +export const DeviceExpandDetailsButton = ({ isExpanded, + onClick, ...rest }: Props): JSX.Element => { const label = isExpanded ? _t("settings|sessions|hide_details") : _t("settings|sessions|show_details"); @@ -34,6 +36,7 @@ export const DeviceExpandDetailsButton = className={classNames("mx_DeviceExpandDetailsButton", { mx_DeviceExpandDetailsButton_expanded: isExpanded, })} + onClick={onClick} > diff --git a/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx index 783ea1bce3..14de26629b 100644 --- a/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx @@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details. import React, { useCallback, useMemo, useState } from "react"; import { JoinRule, EventType, RoomState, Room } from "matrix-js-sdk/src/matrix"; +import { RoomPowerLevelsEventContent } from "matrix-js-sdk/src/types"; import { _t } from "../../../../../languageHandler"; import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch"; @@ -24,48 +25,49 @@ interface ElementCallSwitchProps { const ElementCallSwitch: React.FC = ({ room }) => { const isPublic = useMemo(() => room.getJoinRule() === JoinRule.Public, [room]); - const [content, events, maySend] = useRoomState( + const [content, maySend] = useRoomState( room, useCallback( (state: RoomState) => { - const content = state?.getStateEvents(EventType.RoomPowerLevels, "")?.getContent(); + const content = state + ?.getStateEvents(EventType.RoomPowerLevels, "") + ?.getContent(); return [ content ?? {}, - content?.["events"] ?? {}, state?.maySendStateEvent(EventType.RoomPowerLevels, room.client.getSafeUserId()), - ]; + ] as const; }, [room.client], ), ); const [elementCallEnabled, setElementCallEnabled] = useState(() => { - return events[ElementCall.MEMBER_EVENT_TYPE.name] === 0; + return content.events?.[ElementCall.MEMBER_EVENT_TYPE.name] === 0; }); const onChange = useCallback( (enabled: boolean): void => { setElementCallEnabled(enabled); + // Take a copy to avoid mutating the original + const newContent = { events: {}, ...content }; + if (enabled) { - const userLevel = events[EventType.RoomMessage] ?? content.users_default ?? 0; + const userLevel = newContent.events[EventType.RoomMessage] ?? content.users_default ?? 0; const moderatorLevel = content.kick ?? 50; - events[ElementCall.CALL_EVENT_TYPE.name] = isPublic ? moderatorLevel : userLevel; - events[ElementCall.MEMBER_EVENT_TYPE.name] = userLevel; + newContent.events[ElementCall.CALL_EVENT_TYPE.name] = isPublic ? moderatorLevel : userLevel; + newContent.events[ElementCall.MEMBER_EVENT_TYPE.name] = userLevel; } else { - const adminLevel = events[EventType.RoomPowerLevels] ?? content.state_default ?? 100; + const adminLevel = newContent.events[EventType.RoomPowerLevels] ?? content.state_default ?? 100; - events[ElementCall.CALL_EVENT_TYPE.name] = adminLevel; - events[ElementCall.MEMBER_EVENT_TYPE.name] = adminLevel; + newContent.events[ElementCall.CALL_EVENT_TYPE.name] = adminLevel; + newContent.events[ElementCall.MEMBER_EVENT_TYPE.name] = adminLevel; } - room.client.sendStateEvent(room.roomId, EventType.RoomPowerLevels, { - events: events, - ...content, - }); + room.client.sendStateEvent(room.roomId, EventType.RoomPowerLevels, newContent); }, - [room.client, room.roomId, content, events, isPublic], + [room.client, room.roomId, content, isPublic], ); const brand = SdkConfig.get("element_call").brand ?? DEFAULTS.element_call.brand; diff --git a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx index 3e86d779ff..9ad7df31e9 100644 --- a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx @@ -268,6 +268,7 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState> onChange={this.onPersonalRuleChanged} /> value={this.state.newList} onChange={this.onNewListChanged} /> - + {_t("action|subscribe")} diff --git a/src/components/views/spaces/QuickThemeSwitcher.tsx b/src/components/views/spaces/QuickThemeSwitcher.tsx index 195fcb9899..f4c229ae04 100644 --- a/src/components/views/spaces/QuickThemeSwitcher.tsx +++ b/src/components/views/spaces/QuickThemeSwitcher.tsx @@ -27,7 +27,7 @@ type Props = { const MATCH_SYSTEM_THEME_ID = "MATCH_SYSTEM_THEME_ID"; const QuickThemeSwitcher: React.FC = ({ requestClose }) => { - const orderedThemes = useMemo(getOrderedThemes, []); + const orderedThemes = useMemo(() => getOrderedThemes(), []); const themeState = useTheme(); const nonHighContrast = findNonHighContrastTheme(themeState.theme); diff --git a/src/components/views/spaces/SpaceBasicSettings.tsx b/src/components/views/spaces/SpaceBasicSettings.tsx index 63a70a97cd..8311e6728e 100644 --- a/src/components/views/spaces/SpaceBasicSettings.tsx +++ b/src/components/views/spaces/SpaceBasicSettings.tsx @@ -71,6 +71,7 @@ export const SpaceAvatar: React.FC avatarUploadRef.current?.click()} + alt="" /> avatarUploadRef.current?.click()} diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index 73bb66af38..af484445b4 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -221,7 +221,7 @@ const CreateSpaceButton: React.FC { - const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(); + const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(); useEffect(() => { if (!isPanelCollapsed && menuDisplayed) { diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index 38329c39b7..cee4cf54ec 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -30,7 +30,7 @@ import defaultDispatcher from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton"; import { toRightOf, useContextMenu } from "../../structures/ContextMenu"; -import AccessibleButton, { ButtonEvent, ButtonProps as AccessibleButtonProps } from "../elements/AccessibleButton"; +import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; import { NotificationLevel } from "../../../stores/notifications/NotificationLevel"; import { getKeyBindingsManager } from "../../../KeyBindingsManager"; @@ -39,8 +39,8 @@ import SpaceContextMenu from "../context_menus/SpaceContextMenu"; import { useRovingTabIndex } from "../../../accessibility/RovingTabIndex"; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; -type ButtonProps = Omit< - AccessibleButtonProps, +type ButtonProps = Omit< + ComponentProps>, "title" | "onClick" | "size" | "element" > & { space?: Room; @@ -52,12 +52,12 @@ type ButtonProps = Omit< notificationState?: NotificationState; isNarrow?: boolean; size: string; - innerRef?: RefObject; + innerRef?: RefObject; ContextMenuComponent?: ComponentType>; onClick?(ev?: ButtonEvent): void; }; -export const SpaceButton = ({ +export const SpaceButton = ({ space, spaceKey: _spaceKey, className, @@ -72,8 +72,8 @@ export const SpaceButton = ({ ContextMenuComponent, ...props }: ButtonProps): JSX.Element => { - const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(innerRef); - const [onFocus, isActive, ref] = useRovingTabIndex(handle); + const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(innerRef); + const [onFocus, isActive, ref] = useRovingTabIndex(handle); const tabIndex = isActive ? 0 : -1; const spaceKey = _spaceKey ?? space?.roomId; diff --git a/src/components/views/spaces/threads-activity-centre/useUnreadThreadRooms.ts b/src/components/views/spaces/threads-activity-centre/useUnreadThreadRooms.ts index 110c9d51f8..1ea10bed68 100644 --- a/src/components/views/spaces/threads-activity-centre/useUnreadThreadRooms.ts +++ b/src/components/views/spaces/threads-activity-centre/useUnreadThreadRooms.ts @@ -6,7 +6,7 @@ * Please see LICENSE files in the repository root for full details. */ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { ClientEvent, MatrixClient, MatrixEventEvent, Room } from "matrix-js-sdk/src/matrix"; import { throttle } from "lodash"; @@ -42,14 +42,12 @@ export function useUnreadThreadRooms(forceComputation: boolean): Result { setResult(computeUnreadThreadRooms(mxClient, msc3946ProcessDynamicPredecessor, settingTACOnlyNotifs)); }, [mxClient, msc3946ProcessDynamicPredecessor, settingTACOnlyNotifs]); - // The exhautive deps lint rule can't compute dependencies here since it's not a plain inline func. - // We make this as simple as possible so its only dep is doUpdate itself. - // eslint-disable-next-line react-hooks/exhaustive-deps - const scheduleUpdate = useCallback( - throttle(doUpdate, MIN_UPDATE_INTERVAL_MS, { - leading: false, - trailing: true, - }), + const scheduleUpdate = useMemo( + () => + throttle(doUpdate, MIN_UPDATE_INTERVAL_MS, { + leading: false, + trailing: true, + }), [doUpdate], ); diff --git a/src/components/views/voip/LegacyCallView/LegacyCallViewButtons.tsx b/src/components/views/voip/LegacyCallView/LegacyCallViewButtons.tsx index 105736d04e..bdcd3713cb 100644 --- a/src/components/views/voip/LegacyCallView/LegacyCallViewButtons.tsx +++ b/src/components/views/voip/LegacyCallView/LegacyCallViewButtons.tsx @@ -69,7 +69,7 @@ interface IDropdownButtonProps extends ButtonProps { } const LegacyCallViewDropdownButton: React.FC = ({ state, deviceKinds, ...props }) => { - const [menuDisplayed, buttonRef, openMenu, closeMenu] = useContextMenu(); + const [menuDisplayed, buttonRef, openMenu, closeMenu] = useContextMenu(); const [hoveringDropdown, setHoveringDropdown] = useState(false); const classes = classNames("mx_LegacyCallViewButtons_button", "mx_LegacyCallViewButtons_dropdownButton", { diff --git a/src/contexts/ToastContext.tsx b/src/contexts/ToastContext.tsx index 4ae4875c96..e9bd392a60 100644 --- a/src/contexts/ToastContext.tsx +++ b/src/contexts/ToastContext.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { ReactNode, createContext, useCallback, useContext, useEffect, useRef, useState } from "react"; +import { ReactNode, createContext, useCallback, useContext, useEffect, useState, useMemo } from "react"; /** * A ToastContext helps components display any kind of toast message and can be provided @@ -33,19 +33,19 @@ export function useToastContext(): ToastRack { * the ToastRack object that should be provided to the context */ export function useActiveToast(): [ReactNode | undefined, ToastRack] { - const toastRack = useRef(new ToastRack()); + const toastRack = useMemo(() => new ToastRack(), []); - const [activeToast, setActiveToast] = useState(toastRack.current.getActiveToast()); + const [activeToast, setActiveToast] = useState(toastRack.getActiveToast()); const updateCallback = useCallback(() => { - setActiveToast(toastRack.current.getActiveToast()); + setActiveToast(toastRack.getActiveToast()); }, [setActiveToast, toastRack]); useEffect(() => { - toastRack.current.setCallback(updateCallback); + toastRack.setCallback(updateCallback); }, [toastRack, updateCallback]); - return [activeToast, toastRack.current]; + return [activeToast, toastRack]; } interface DisplayedToast { diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 5a4a6043fb..f8298a28e9 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -2936,7 +2936,6 @@ "warning": "WARNUNG: " }, "share": { - "link_copied": "Link kopiert", "permalink_message": "Link zur ausgewählten Nachricht", "permalink_most_recent": "Link zur aktuellsten Nachricht", "share_call": "Konferenzeinladungslink", diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e9ac73b48b..fe5e54e559 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -398,7 +398,7 @@ }, "bug_reporting": { "additional_context": "If there is additional context that would help in analysing the issue, such as what you were doing at the time, room IDs, user IDs, etc., please include those things here.", - "before_submitting": "We recommend creating a GitHub issue to ensure that your report is reviewed.", + "before_submitting": "Before submitting logs, you must create a GitHub issue to describe your problem.", "collecting_information": "Collecting app version information", "collecting_logs": "Collecting logs", "create_new_issue": "Please create a new issue on GitHub so that we can investigate this bug.", diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index 66ac807080..3d5833446c 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -340,7 +340,7 @@ "set_email_prompt": "Czy chcesz ustawić adres e-mail?", "sign_in_description": "Użyj swojego konta, aby kontynuować.", "sign_in_instead": "Zamiast tego zaloguj się", - "sign_in_instead_prompt": "Masz już konto? Zaloguj się tutaj", + "sign_in_instead_prompt": "Zamiast tego zaloguj się", "sign_in_or_register": "Zaloguj się lub utwórz konto", "sign_in_or_register_description": "Użyj konta lub utwórz nowe, aby kontynuować.", "sign_in_prompt": "Posiadasz już konto? Zaloguj się", @@ -505,7 +505,6 @@ "matrix": "Matrix", "message": "Wiadomość", "message_layout": "Wygląd wiadomości", - "message_timestamp_invalid": "Nieprawidłowy znacznik czasu", "microphone": "Mikrofon", "model": "Model", "modern": "Współczesny", @@ -909,8 +908,6 @@ "warning": "Jeżeli nie ustawiłeś nowej metody odzyskiwania, atakujący może uzyskać dostęp do Twojego konta. Zmień hasło konta i natychmiast ustaw nową metodę odzyskiwania w Ustawieniach." }, "not_supported": "", - "pinned_identity_changed": "Tożsamość użytkownika %(displayName)s (%(userId)s) uległa zmianie. Dowiedz się więcej", - "pinned_identity_changed_no_displayname": "Tożsamość użytkownika %(userId)s uległa zmianie Dowiedz się więcej", "recovery_method_removed": { "description_1": "Ta sesja wykryła, że Twoja fraza bezpieczeństwa i klucz dla bezpiecznych wiadomości zostały usunięte.", "description_2": "Jeśli zrobiłeś to przez pomyłkę, możesz ustawić bezpieczne wiadomości w tej sesji, co zaszyfruje ponownie historię wiadomości za pomocą nowej metody odzyskiwania.", @@ -999,7 +996,7 @@ "unverified_sessions_toast_description": "Sprawdź, by upewnić się że Twoje konto jest bezpieczne", "unverified_sessions_toast_reject": "Później", "unverified_sessions_toast_title": "Masz niezweryfikowane sesje", - "verification_description": "Zweryfikuj swoją tożsamość, aby uzyskać dostęp do wiadomości szyfrowanych i potwierdzić swoją tożsamość innym. Jeśli korzystasz z urządzenia mobilnego, otwórz na niej aplikację.", + "verification_description": "Zweryfikuj swoją tożsamość, aby uzyskać dostęp do wiadomości szyfrowanych i potwierdzić swoją tożsamość innym.", "verification_dialog_title_device": "Zweryfikuj drugie urządzenie", "verification_dialog_title_user": "Żądanie weryfikacji", "verification_skip_warning": "Bez weryfikacji, nie będziesz posiadać dostępu do wszystkich swoich wiadomości, a inni będą Cię widzieć jako niezaufanego.", @@ -1105,15 +1102,7 @@ "you": "Dodano reakcję %(reaction)s do %(message)s" }, "m.sticker": "%(senderName)s: %(stickerName)s", - "m.text": "%(senderName)s: %(message)s", - "prefix": { - "audio": "Audio", - "file": "Plik", - "image": "Obraz", - "poll": "Ankieta", - "video": "Wideo" - }, - "preview": "%(prefix)s: %(preview)s" + "m.text": "%(senderName)s: %(message)s" }, "export_chat": { "cancelled": "Eksport został anulowany", @@ -2950,7 +2939,6 @@ "warning": "OSTRZEŻENIE: " }, "share": { - "link_copied": "Skopiowano link", "permalink_message": "Link do zaznaczonej wiadomości", "permalink_most_recent": "Link do najnowszej wiadomości", "share_call": "Link zaproszenia do konferencji", @@ -3259,8 +3247,8 @@ "historical_event_no_key_backup": "Historia wiadomości nie jest dostępna na tym urządzeniu", "historical_event_unverified_device": "Musisz zweryfikować to urządzenie, aby wyświetlić historię wiadomości", "historical_event_user_not_joined": "Nie masz dostępu do tej wiadomości", - "sender_identity_previously_verified": "Zweryfikowana tożsamość nadawcy uległa zmianie", - "sender_unsigned_device": "Wysłano z niezabezpieczonego urządzenia.", + "sender_identity_previously_verified": "Zweryfikowana tożsamość uległa zmianie", + "sender_unsigned_device": "Zaszyfrowano przez urządzenie niezweryfikowane przez właściciela.", "unable_to_decrypt": "Nie można rozszyfrować wiadomości" }, "disambiguated_profile": "%(displayName)s (%(matrixId)s)", @@ -3732,7 +3720,6 @@ "error_files_too_large": "Te pliki są zbyt duże do wysłania. Ograniczenie wielkości plików to %(limit)s.", "error_some_files_too_large": "Niektóre pliki są zbyt duże do wysłania. Ograniczenie wielkości plików to %(limit)s.", "error_title": "Błąd wysyłania", - "not_image": "Wybrany plik nie jest prawidłowym plikiem obrazu.", "title": "Prześlij pliki", "title_progress": "Prześlij pliki (%(current)s z %(total)s)", "upload_all_button": "Prześlij wszystko", diff --git a/src/stores/InitialCryptoSetupStore.ts b/src/stores/InitialCryptoSetupStore.ts deleted file mode 100644 index 0c2e49f5ca..0000000000 --- a/src/stores/InitialCryptoSetupStore.ts +++ /dev/null @@ -1,140 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import EventEmitter from "events"; -import { MatrixClient } from "matrix-js-sdk/src/matrix"; -import { logger } from "matrix-js-sdk/src/logger"; -import { useEffect, useState } from "react"; - -import { createCrossSigning } from "../CreateCrossSigning"; -import { SdkContextClass } from "../contexts/SDKContext"; - -type Status = "in_progress" | "complete" | "error" | undefined; - -export const useInitialCryptoSetupStatus = (store: InitialCryptoSetupStore): Status => { - const [status, setStatus] = useState(store.getStatus()); - - useEffect(() => { - const update = (): void => { - setStatus(store.getStatus()); - }; - - store.on("update", update); - - return () => { - store.off("update", update); - }; - }, [store]); - - return status; -}; - -/** - * Logic for setting up crypto state that's done immediately after - * a user registers. Should be transparent to the user, not requiring - * interaction in most cases. - * As distinct from SetupEncryptionStore which is for setting up - * 4S or verifying the device, will always require interaction - * from the user in some form. - */ -export class InitialCryptoSetupStore extends EventEmitter { - private status: Status = undefined; - - private client?: MatrixClient; - private isTokenLogin?: boolean; - private stores?: SdkContextClass; - private onFinished?: (success: boolean) => void; - - public static sharedInstance(): InitialCryptoSetupStore { - if (!window.mxInitialCryptoStore) window.mxInitialCryptoStore = new InitialCryptoSetupStore(); - return window.mxInitialCryptoStore; - } - - public getStatus(): Status { - return this.status; - } - - /** - * Start the initial crypto setup process. - * - * @param {MatrixClient} client The client to use for the setup - * @param {boolean} isTokenLogin True if the user logged in via a token login, otherwise false - * @param {SdkContextClass} stores The stores to use for the setup - */ - public startInitialCryptoSetup( - client: MatrixClient, - isTokenLogin: boolean, - stores: SdkContextClass, - onFinished: (success: boolean) => void, - ): void { - this.client = client; - this.isTokenLogin = isTokenLogin; - this.stores = stores; - this.onFinished = onFinished; - - // We just start this process: it's progress is tracked by the events rather - // than returning a promise, so we don't bother. - this.doSetup().catch(() => logger.error("Initial crypto setup failed")); - } - - /** - * Retry the initial crypto setup process. - * - * If no crypto setup is currently in process, this will return false. - * - * @returns {boolean} True if a retry was initiated, otherwise false - */ - public retry(): boolean { - if (this.client === undefined || this.isTokenLogin === undefined || this.stores == undefined) return false; - - this.doSetup().catch(() => logger.error("Initial crypto setup failed")); - - return true; - } - - private reset(): void { - this.client = undefined; - this.isTokenLogin = undefined; - this.stores = undefined; - } - - private async doSetup(): Promise { - if (this.client === undefined || this.isTokenLogin === undefined || this.stores == undefined) { - throw new Error("No setup is in progress"); - } - - const cryptoApi = this.client.getCrypto(); - if (!cryptoApi) throw new Error("No crypto module found!"); - - this.status = "in_progress"; - this.emit("update"); - - try { - await createCrossSigning(this.client, this.isTokenLogin, this.stores.accountPasswordStore.getPassword()); - - this.reset(); - - this.status = "complete"; - this.emit("update"); - this.onFinished?.(true); - } catch (e) { - if (this.isTokenLogin) { - // ignore any failures, we are relying on grace period here - this.reset(); - - this.status = "complete"; - this.emit("update"); - this.onFinished?.(true); - - return; - } - logger.error("Error bootstrapping cross-signing", e); - this.status = "error"; - this.emit("update"); - } - } -} diff --git a/src/stores/SetupEncryptionStore.ts b/src/stores/SetupEncryptionStore.ts index a13ba26f72..70c721b1ca 100644 --- a/src/stores/SetupEncryptionStore.ts +++ b/src/stores/SetupEncryptionStore.ts @@ -33,11 +33,6 @@ export enum Phase { ConfirmReset = 6, } -/** - * Logic for setting up 4S and/or verifying the user's device: a process requiring - * ongoing interaction with the user, as distinct from InitialCryptoSetupStore which - * a (usually) non-interactive process that happens immediately after registration. - */ export class SetupEncryptionStore extends EventEmitter { private started?: boolean; public phase?: Phase; diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index de7a71fa80..5bc2ac7fc0 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -194,7 +194,6 @@ export class StopGapWidgetDriver extends WidgetDriver { EventType.CallSDPStreamMetadataChanged, EventType.CallSDPStreamMetadataChangedPrefix, EventType.CallReplaces, - EventType.CallEncryptionKeysPrefix, ]; for (const eventType of sendRecvToDevice) { this.allowedCapabilities.add( diff --git a/src/utils/location/useMap.ts b/src/utils/location/useMap.ts index 308aedc205..300a15a4ec 100644 --- a/src/utils/location/useMap.ts +++ b/src/utils/location/useMap.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { useEffect, useState } from "react"; +import { useEffect, useMemo } from "react"; import type { Map as MapLibreMap } from "maplibre-gl"; import { createMap } from "./map"; @@ -26,29 +26,25 @@ interface UseMapProps { */ export const useMap = ({ interactive, bodyId, onError }: UseMapProps): MapLibreMap | undefined => { const cli = useMatrixClientContext(); - const [map, setMap] = useState(); - useEffect( - () => { - try { - setMap(createMap(cli, !!interactive, bodyId, onError)); - } catch (error) { - console.error("Error encountered in useMap", error); - if (error instanceof Error) { - onError?.(error); - } + const map = useMemo(() => { + try { + return createMap(cli, !!interactive, bodyId, onError); + } catch (error) { + console.error("Error encountered in useMap", error); + if (error instanceof Error) { + onError?.(error); } - return () => { - if (map) { - map.remove(); - setMap(undefined); - } - }; - }, - // map is excluded as a dependency - // eslint-disable-next-line react-hooks/exhaustive-deps - [interactive, bodyId, onError], - ); + } + }, [bodyId, cli, interactive, onError]); + + // cleanup + useEffect(() => { + if (!map) return; + return () => { + map.remove(); + }; + }, [map]); return map; }; diff --git a/test/components/views/dialogs/security/CreateCrossSigningDialog-test.tsx b/test/components/views/dialogs/security/CreateCrossSigningDialog-test.tsx new file mode 100644 index 0000000000..3e5dc4eb94 --- /dev/null +++ b/test/components/views/dialogs/security/CreateCrossSigningDialog-test.tsx @@ -0,0 +1,131 @@ +/* +Copyright 2024 New Vector Ltd. +Copyright 2018-2022 The Matrix.org Foundation C.I.C. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import React from "react"; +import { render, screen, waitFor } from "jest-matrix-react"; +import { mocked } from "jest-mock"; +import { MatrixClient } from "matrix-js-sdk/src/matrix"; + +import { createCrossSigning } from "../../../../../src/CreateCrossSigning"; +import CreateCrossSigningDialog from "../../../../../src/components/views/dialogs/security/CreateCrossSigningDialog"; +import { createTestClient } from "../../../../test-utils"; + +jest.mock("../../../../../src/CreateCrossSigning", () => ({ + createCrossSigning: jest.fn(), +})); + +describe("CreateCrossSigningDialog", () => { + let client: MatrixClient; + let createCrossSigningResolve: () => void; + let createCrossSigningReject: (e: Error) => void; + + beforeEach(() => { + client = createTestClient(); + mocked(createCrossSigning).mockImplementation(() => { + return new Promise((resolve, reject) => { + createCrossSigningResolve = resolve; + createCrossSigningReject = reject; + }); + }); + }); + + afterEach(() => { + jest.resetAllMocks(); + jest.restoreAllMocks(); + }); + + it("should call createCrossSigning and show a spinner while it runs", async () => { + const onFinished = jest.fn(); + + render( + , + ); + + expect(createCrossSigning).toHaveBeenCalledWith(client, false, "hunter2"); + expect(screen.getByTestId("spinner")).toBeInTheDocument(); + + createCrossSigningResolve!(); + + await waitFor(() => expect(onFinished).toHaveBeenCalledWith(true)); + }); + + it("should display an error if createCrossSigning fails", async () => { + render( + , + ); + + createCrossSigningReject!(new Error("generic error message")); + + await expect(await screen.findByRole("button", { name: "Retry" })).toBeInTheDocument(); + }); + + it("ignores failures when tokenLogin is true", async () => { + const onFinished = jest.fn(); + + render( + , + ); + + createCrossSigningReject!(new Error("generic error message")); + + await waitFor(() => expect(onFinished).toHaveBeenCalledWith(false)); + }); + + it("cancels the dialog when the cancel button is clicked", async () => { + const onFinished = jest.fn(); + + render( + , + ); + + createCrossSigningReject!(new Error("generic error message")); + + const cancelButton = await screen.findByRole("button", { name: "Cancel" }); + cancelButton.click(); + + expect(onFinished).toHaveBeenCalledWith(false); + }); + + it("should retry when the retry button is clicked", async () => { + render( + , + ); + + createCrossSigningReject!(new Error("generic error message")); + + const retryButton = await screen.findByRole("button", { name: "Retry" }); + retryButton.click(); + + expect(createCrossSigning).toHaveBeenCalledTimes(2); + }); +}); diff --git a/test/components/views/dialogs/security/InitialCryptoSetupDialog-test.tsx b/test/components/views/dialogs/security/InitialCryptoSetupDialog-test.tsx deleted file mode 100644 index a589b55289..0000000000 --- a/test/components/views/dialogs/security/InitialCryptoSetupDialog-test.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2018-2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { render, screen } from "jest-matrix-react"; -import userEvent from "@testing-library/user-event"; - -import { InitialCryptoSetupDialog } from "../../../../../src/components/views/dialogs/security/InitialCryptoSetupDialog"; -import { InitialCryptoSetupStore } from "../../../../../src/stores/InitialCryptoSetupStore"; - -describe("InitialCryptoSetupDialog", () => { - const storeMock = { - getStatus: jest.fn(), - retry: jest.fn(), - on: jest.fn(), - off: jest.fn(), - }; - - beforeEach(() => { - jest.spyOn(InitialCryptoSetupStore, "sharedInstance").mockReturnValue(storeMock as any); - }); - - afterEach(() => { - jest.resetAllMocks(); - jest.restoreAllMocks(); - }); - - it("should show a spinner while the setup is in progress", async () => { - const onFinished = jest.fn(); - - storeMock.getStatus.mockReturnValue("in_progress"); - - render(); - - expect(screen.getByTestId("spinner")).toBeInTheDocument(); - }); - - it("should display an error if setup has failed", async () => { - storeMock.getStatus.mockReturnValue("error"); - - render(); - - await expect(await screen.findByRole("button", { name: "Retry" })).toBeInTheDocument(); - }); - - it("calls retry when retry button pressed", async () => { - const onFinished = jest.fn(); - storeMock.getStatus.mockReturnValue("error"); - - render(); - - await userEvent.click(await screen.findByRole("button", { name: "Retry" })); - - expect(storeMock.retry).toHaveBeenCalled(); - }); -}); diff --git a/test/unit-tests/components/structures/__snapshots__/MatrixChat-test.tsx.snap b/test/unit-tests/components/structures/__snapshots__/MatrixChat-test.tsx.snap index 1bdbe016d4..94c2678388 100644 --- a/test/unit-tests/components/structures/__snapshots__/MatrixChat-test.tsx.snap +++ b/test/unit-tests/components/structures/__snapshots__/MatrixChat-test.tsx.snap @@ -314,6 +314,7 @@ exports[` with a soft-logged-out session should show the soft-logo class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary" role="button" tabindex="0" + type="submit" > Sign in diff --git a/test/unit-tests/components/views/right_panel/__snapshots__/RoomSummaryCard-test.tsx.snap b/test/unit-tests/components/views/right_panel/__snapshots__/RoomSummaryCard-test.tsx.snap index a4496312f3..5fb1e66115 100644 --- a/test/unit-tests/components/views/right_panel/__snapshots__/RoomSummaryCard-test.tsx.snap +++ b/test/unit-tests/components/views/right_panel/__snapshots__/RoomSummaryCard-test.tsx.snap @@ -135,9 +135,8 @@ exports[` has button to edit topic 1`] = ` style="--mx-box-flex: 1;" >

renders the room summary 1`] = ` style="--mx-box-flex: 1;" >

renders the room topic in the summary 1`] = ` style="--mx-box-flex: 1;" >

", () => { const userId = "@alice:server.org"; @@ -29,12 +28,10 @@ describe("", () => { let mockClient: MatrixClient; let room: Room; let permalinkCreator: RoomPermalinkCreator; - let resizeNotifier: ResizeNotifier; beforeEach(() => { mockClient = stubClient(); room = new Room(roomId, mockClient, userId); permalinkCreator = new RoomPermalinkCreator(room); - resizeNotifier = new ResizeNotifier(); jest.spyOn(dis, "dispatch").mockReturnValue(undefined); }); @@ -80,7 +77,7 @@ describe("", () => { */ function renderBanner() { return render( - , + , withClientContextRenderOptions(mockClient), ); } @@ -148,9 +145,7 @@ describe("", () => { event3.getId()!, ]); jest.spyOn(pinnedEventHooks, "useSortedFetchedPinnedEvents").mockReturnValue([event1, event2, event3]); - rerender( - , - ); + rerender(); await expect(screen.findByText("Third pinned message")).resolves.toBeVisible(); expect(asFragment()).toMatchSnapshot(); }); @@ -211,42 +206,6 @@ describe("", () => { expect(asFragment()).toMatchSnapshot(); }); - describe("Notify the timeline to resize", () => { - beforeEach(() => { - jest.spyOn(resizeNotifier, "notifyTimelineHeightChanged"); - jest.spyOn(pinnedEventHooks, "usePinnedEvents").mockReturnValue([event1.getId()!, event2.getId()!]); - jest.spyOn(pinnedEventHooks, "useSortedFetchedPinnedEvents").mockReturnValue([event1, event2]); - }); - - it("should notify the timeline to resize when we display the banner", async () => { - renderBanner(); - await expect(screen.findByText("Second pinned message")).resolves.toBeVisible(); - // The banner is displayed, so we need to resize the timeline - expect(resizeNotifier.notifyTimelineHeightChanged).toHaveBeenCalledTimes(1); - - await userEvent.click(screen.getByRole("button", { name: "View the pinned message in the timeline." })); - await expect(screen.findByText("First pinned message")).resolves.toBeVisible(); - // The banner is already displayed, so we don't need to resize the timeline - expect(resizeNotifier.notifyTimelineHeightChanged).toHaveBeenCalledTimes(1); - }); - - it("should notify the timeline to resize when we hide the banner", async () => { - const { rerender } = renderBanner(); - await expect(screen.findByText("Second pinned message")).resolves.toBeVisible(); - // The banner is displayed, so we need to resize the timeline - expect(resizeNotifier.notifyTimelineHeightChanged).toHaveBeenCalledTimes(1); - - // The banner has no event to display and is hidden - jest.spyOn(pinnedEventHooks, "usePinnedEvents").mockReturnValue([]); - jest.spyOn(pinnedEventHooks, "useSortedFetchedPinnedEvents").mockReturnValue([]); - rerender( - , - ); - // The timeline should be resized - expect(resizeNotifier.notifyTimelineHeightChanged).toHaveBeenCalledTimes(2); - }); - }); - describe("Right button", () => { beforeEach(() => { jest.spyOn(pinnedEventHooks, "usePinnedEvents").mockReturnValue([event1.getId()!, event2.getId()!]); @@ -258,8 +217,6 @@ describe("", () => { jest.spyOn(RightPanelStore.instance, "isOpenForRoom").mockReturnValue(false); renderBanner(); - await expect(screen.findByText("Second pinned message")).resolves.toBeVisible(); - expect(screen.getByRole("button", { name: "View all" })).toBeVisible(); }); @@ -271,8 +228,6 @@ describe("", () => { }); renderBanner(); - await expect(screen.findByText("Second pinned message")).resolves.toBeVisible(); - expect(screen.getByRole("button", { name: "View all" })).toBeVisible(); }); @@ -284,8 +239,6 @@ describe("", () => { }); renderBanner(); - await expect(screen.findByText("Second pinned message")).resolves.toBeVisible(); - expect(screen.getByRole("button", { name: "Close list" })).toBeVisible(); }); @@ -310,7 +263,6 @@ describe("", () => { }); renderBanner(); - await expect(screen.findByText("Second pinned message")).resolves.toBeVisible(); expect(screen.getByRole("button", { name: "Close list" })).toBeVisible(); jest.spyOn(RightPanelStore.instance, "isOpenForRoom").mockReturnValue(false); diff --git a/test/unit-tests/components/views/rooms/ReadReceiptGroup-test.tsx b/test/unit-tests/components/views/rooms/ReadReceiptGroup-test.tsx index 13500f9b1f..ff89aa6942 100644 --- a/test/unit-tests/components/views/rooms/ReadReceiptGroup-test.tsx +++ b/test/unit-tests/components/views/rooms/ReadReceiptGroup-test.tsx @@ -10,7 +10,6 @@ import React, { ComponentProps } from "react"; import { render, screen, waitFor } from "jest-matrix-react"; import { RoomMember } from "matrix-js-sdk/src/matrix"; import userEvent from "@testing-library/user-event"; -import { mocked } from "jest-mock"; import { determineAvatarPosition, @@ -21,9 +20,6 @@ import * as languageHandler from "../../../../../src/languageHandler"; import { stubClient } from "../../../../test-utils"; import dispatcher from "../../../../../src/dispatcher/dispatcher"; import { Action } from "../../../../../src/dispatcher/actions"; -import { formatDate } from "../../../../../src/DateUtils"; - -jest.mock("../../../../../src/DateUtils"); describe("ReadReceiptGroup", () => { describe("TooltipText", () => { @@ -91,10 +87,6 @@ describe("ReadReceiptGroup", () => { describe("", () => { stubClient(); - // We pick a fixed time but this can still vary depending on the locale - // the tests are run in. We are not testing date formatting here, so stub it out. - mocked(formatDate).mockReturnValue("==MOCK FORMATTED DATE=="); - const ROOM_ID = "roomId"; const USER_ID = "@alice:example.org"; diff --git a/test/unit-tests/components/views/rooms/__snapshots__/ReadReceiptGroup-test.tsx.snap b/test/unit-tests/components/views/rooms/__snapshots__/ReadReceiptGroup-test.tsx.snap index 60e8f844af..b0ba944a66 100644 --- a/test/unit-tests/components/views/rooms/__snapshots__/ReadReceiptGroup-test.tsx.snap +++ b/test/unit-tests/components/views/rooms/__snapshots__/ReadReceiptGroup-test.tsx.snap @@ -84,7 +84,7 @@ exports[`ReadReceiptGroup should render 1`] = `

- ==MOCK FORMATTED DATE== + Wed, 15 May, 0:00

diff --git a/test/unit-tests/components/views/settings/SetIntegrationManager-test.tsx b/test/unit-tests/components/views/settings/SetIntegrationManager-test.tsx index 5c77e88d93..888499d524 100644 --- a/test/unit-tests/components/views/settings/SetIntegrationManager-test.tsx +++ b/test/unit-tests/components/views/settings/SetIntegrationManager-test.tsx @@ -35,7 +35,7 @@ describe("SetIntegrationManager", () => { deleteThreePid: jest.fn(), }); - let stores!: SdkContextClass; + let stores: SdkContextClass; const getComponent = () => ( diff --git a/test/unit-tests/components/views/settings/__snapshots__/LayoutSwitcher-test.tsx.snap b/test/unit-tests/components/views/settings/__snapshots__/LayoutSwitcher-test.tsx.snap index 2aa08adb94..fcf3406620 100644 --- a/test/unit-tests/components/views/settings/__snapshots__/LayoutSwitcher-test.tsx.snap +++ b/test/unit-tests/components/views/settings/__snapshots__/LayoutSwitcher-test.tsx.snap @@ -19,14 +19,14 @@ exports[` should render 1`] = ` class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi" >