diff --git a/.github/actions/download-verify-element-tarball/action.yml b/.github/actions/download-verify-element-tarball/action.yml index 75d325c543..a64bc3241b 100644 --- a/.github/actions/download-verify-element-tarball/action.yml +++ b/.github/actions/download-verify-element-tarball/action.yml @@ -11,7 +11,7 @@ runs: using: composite steps: - name: Download release tarball - uses: robinraju/release-downloader@a96f54c1b5f5e09e47d9504526e96febd949d4c2 # v1 + uses: robinraju/release-downloader@daf26c55d821e836577a15f77d86ddc078948b05 # v1 with: tag: ${{ inputs.tag }} fileName: element-*.tar.gz* diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 35630e4c2e..1cf24facde 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -37,14 +37,14 @@ jobs: install: true - name: Login to Docker Hub - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3 + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 if: github.event_name != 'pull_request' with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3 + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 if: github.event_name != 'pull_request' with: registry: ghcr.io diff --git a/.github/workflows/static_analysis.yaml b/.github/workflows/static_analysis.yaml index ee731b3ac3..8b15fdfe76 100644 --- a/.github/workflows/static_analysis.yaml +++ b/.github/workflows/static_analysis.yaml @@ -51,6 +51,7 @@ jobs: error|invalid_json error|misconfigured welcome_to_element + devtools|settings|elementCallUrl rethemendex_lint: name: "Rethemendex Check" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 710e996b39..e94c35299a 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@5ef6e175c333bc629f3718b083c8a2ff6e0bbfbc + uses: guibranco/github-status-action-v2@fe98467f9071758c7fc214af9dbac7f301bd23d4 with: authToken: ${{ secrets.GITHUB_TOKEN }} state: success diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ac1856386..01c67c34c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,41 @@ +Changes in [1.11.97](https://github.com/element-hq/element-web/releases/tag/v1.11.97) (2025-04-08) +================================================================================================== +## ✨ Features + +* New room list: reduce padding between avatar and room list border ([#29634](https://github.com/element-hq/element-web/pull/29634)). Contributed by @florianduros. +* Bundle Element Call with Element Web packages ([#29309](https://github.com/element-hq/element-web/pull/29309)). Contributed by @t3chguy. +* Hide an event notification if it is redacted ([#29605](https://github.com/element-hq/element-web/pull/29605)). Contributed by @Half-Shot. +* Docker: Use nginx-unprivileged as base image ([#29353](https://github.com/element-hq/element-web/pull/29353)). Contributed by @AndrewFerr. +* Switch away from nesting React trees and mangling the DOM ([#29586](https://github.com/element-hq/element-web/pull/29586)). Contributed by @t3chguy. +* New room list: add notification decoration ([#29552](https://github.com/element-hq/element-web/pull/29552)). Contributed by @florianduros. +* RoomListStore: Unread filter should match rooms that were marked as unread ([#29580](https://github.com/element-hq/element-web/pull/29580)). Contributed by @MidhunSureshR. +* Add support for hiding videos ([#29496](https://github.com/element-hq/element-web/pull/29496)). Contributed by @Half-Shot. +* Use an outline icon for the report room button ([#29573](https://github.com/element-hq/element-web/pull/29573)). Contributed by @robintown. +* Generate/load pickle key on SSO ([#29568](https://github.com/element-hq/element-web/pull/29568)). Contributed by @Jujure. +* Add report room dialog button/dialog. ([#29513](https://github.com/element-hq/element-web/pull/29513)). Contributed by @Half-Shot. +* RoomListViewModel: Make the active room sticky in the list ([#29551](https://github.com/element-hq/element-web/pull/29551)). Contributed by @MidhunSureshR. +* Replace checkboxes with Compound checkboxes, and appropriately label each checkbox. ([#29363](https://github.com/element-hq/element-web/pull/29363)). Contributed by @Half-Shot. +* New room list: add selection decoration ([#29531](https://github.com/element-hq/element-web/pull/29531)). Contributed by @florianduros. +* Simplified Sliding Sync ([#28515](https://github.com/element-hq/element-web/pull/28515)). Contributed by @dbkr. +* Add ability to hide images after clicking "show image" ([#29467](https://github.com/element-hq/element-web/pull/29467)). Contributed by @Half-Shot. + +## 🐛 Bug Fixes + +* Fix scroll issues in memberlist ([#29392](https://github.com/element-hq/element-web/pull/29392)). Contributed by @MidhunSureshR. +* Ensure clicks on spoilers do not get handled by the hidden content ([#29618](https://github.com/element-hq/element-web/pull/29618)). Contributed by @t3chguy. +* New room list: add cursor pointer on room list item ([#29627](https://github.com/element-hq/element-web/pull/29627)). Contributed by @florianduros. +* Fix missing ambiguous url tooltips on Element Desktop ([#29619](https://github.com/element-hq/element-web/pull/29619)). Contributed by @t3chguy. +* New room list: fix spacing and padding ([#29607](https://github.com/element-hq/element-web/pull/29607)). Contributed by @florianduros. +* Make fetchdep check out matching branch name ([#29601](https://github.com/element-hq/element-web/pull/29601)). Contributed by @dbkr. +* Fix MFileBody fileName not considering `filename` ([#29589](https://github.com/element-hq/element-web/pull/29589)). Contributed by @t3chguy. +* Fix token expiry racing with login causing wrong error to be shown ([#29566](https://github.com/element-hq/element-web/pull/29566)). Contributed by @t3chguy. +* Fix bug which caused startup to hang if the clock was wound back since a previous session ([#29558](https://github.com/element-hq/element-web/pull/29558)). Contributed by @richvdh. +* RoomListViewModel: Reset any primary filter on secondary filter change ([#29562](https://github.com/element-hq/element-web/pull/29562)). Contributed by @MidhunSureshR. +* RoomListStore: Unread filter should only filter rooms having unread counts ([#29555](https://github.com/element-hq/element-web/pull/29555)). Contributed by @MidhunSureshR. +* In force-verify mode, prevent bypassing by cancelling device verification ([#29487](https://github.com/element-hq/element-web/pull/29487)). Contributed by @andybalaam. +* Add title attribute to user identifier ([#29547](https://github.com/element-hq/element-web/pull/29547)). Contributed by @arpitbatra123. + + Changes in [1.11.96](https://github.com/element-hq/element-web/releases/tag/v1.11.96) (2025-03-25) ================================================================================================== ## ✨ Features diff --git a/Dockerfile b/Dockerfile index cd1766347a..0258850d89 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,7 +19,10 @@ RUN /src/scripts/docker-package.sh RUN cp /src/config.sample.json /src/webapp/config.json # App -FROM nginx:alpine-slim +FROM nginxinc/nginx-unprivileged:alpine-slim + +# Need root user to install packages & manipulate the usr directory +USER root # Install jq and moreutils for sponge, both used by our entrypoints RUN apk add jq moreutils @@ -31,13 +34,6 @@ COPY --from=builder /src/webapp /app COPY /docker/nginx-templates/* /etc/nginx/templates/ COPY /docker/docker-entrypoint.d/* /docker-entrypoint.d/ -# Tell nginx to put its pidfile elsewhere, so it can run as non-root -RUN sed -i -e 's,/var/run/nginx.pid,/tmp/nginx.pid,' /etc/nginx/nginx.conf - -# nginx user must own the cache and etc directory to write cache and tweak the nginx config -RUN chown -R nginx:0 /var/cache/nginx /etc/nginx -RUN chmod -R g+w /var/cache/nginx /etc/nginx - RUN rm -rf /usr/share/nginx/html \ && ln -s /app /usr/share/nginx/html diff --git a/docs/config.md b/docs/config.md index 9ae9f21254..f1ced14fd2 100644 --- a/docs/config.md +++ b/docs/config.md @@ -384,8 +384,6 @@ The VoIP and Jitsi options are: 5. `audio_stream_url`: Optional URL to pass to Jitsi to enable live streaming. This option is considered experimental and may be removed at any time without notice. 6. `element_call`: Optional configuration for native group calls using Element Call, with the following subkeys: - - `url`: The URL of the Element Call instance to use for native group calls. This option is considered experimental - and may be removed at any time without notice. Defaults to `https://call.element.io`. - `use_exclusively`: A boolean specifying whether Element Call should be used exclusively as the only VoIP stack in the app, removing the ability to start legacy 1:1 calls or Jitsi calls. Defaults to `false`. - `participant_limit`: The maximum number of users who can join a call; if diff --git a/knip.ts b/knip.ts index 0188e096e5..e129c11481 100644 --- a/knip.ts +++ b/knip.ts @@ -40,6 +40,8 @@ export default { // Used by webpack "process", "util", + // Embedded into webapp + "@element-hq/element-call-embedded", ], ignoreBinaries: [ // Used in scripts & workflows diff --git a/package.json b/package.json index 0ba14d96d1..06ce879b01 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "element-web", - "version": "1.11.96", + "version": "1.11.97", "description": "Element: the future of secure communication", "author": "New Vector Ltd.", "repository": { @@ -65,16 +65,17 @@ "test:playwright:screenshots": "playwright-screenshots --project=Chrome", "coverage": "yarn test --coverage", "analyse:webpack-bundles": "webpack-bundle-analyzer webpack-stats.json webapp", - "update:jitsi": "curl -s https://meet.element.io/libs/external_api.min.js > ./res/jitsi_external_api.min.js" + "update:jitsi": "curl -s https://meet.element.io/libs/external_api.min.js > ./res/jitsi_external_api.min.js", + "postinstall": "patch-package" }, "resolutions": { - "@playwright/test": "1.50.1", + "@playwright/test": "1.51.1", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", - "oidc-client-ts": "3.1.0", + "oidc-client-ts": "3.2.0", "jwt-decode": "4.0.0", - "caniuse-lite": "1.0.30001701", - "testcontainers": "10.20.0", + "caniuse-lite": "1.0.30001704", + "testcontainers": "10.21.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0", "wrap-ansi": "npm:wrap-ansi@^7.0.0" }, @@ -92,7 +93,7 @@ "@types/png-chunks-extract": "^1.0.2", "@types/react-virtualized": "^9.21.30", "@vector-im/compound-design-tokens": "^4.0.0", - "@vector-im/compound-web": "^7.7.2", + "@vector-im/compound-web": "^7.9.0", "@vector-im/matrix-wysiwyg": "2.38.2", "@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4", @@ -107,6 +108,7 @@ "css-tree": "^3.0.0", "diff-dom": "^5.0.0", "diff-match-patch": "^1.0.5", + "domutils": "^3.2.2", "emojibase-regex": "15.3.2", "escape-html": "^1.0.3", "file-saver": "^2.0.5", @@ -115,12 +117,12 @@ "glob-to-regexp": "^0.4.1", "highlight.js": "^11.3.1", "html-entities": "^2.0.0", + "html-react-parser": "^5.2.2", "is-ip": "^3.1.0", "js-xxhash": "^4.0.0", "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", @@ -128,7 +130,7 @@ "maplibre-gl": "^5.0.0", "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "0.0.1", - "matrix-js-sdk": "37.2.0", + "matrix-js-sdk": "37.3.0", "matrix-widget-api": "^1.10.0", "memoize-one": "^6.0.0", "mime": "^4.0.4", @@ -144,6 +146,7 @@ "react-blurhash": "^0.3.0", "react-dom": "^18.3.1", "react-focus-lock": "^2.5.1", + "react-string-replace": "^1.1.1", "react-transition-group": "^4.4.1", "react-virtualized": "^9.22.5", "rfc4648": "^1.4.0", @@ -177,6 +180,7 @@ "@babel/preset-typescript": "^7.12.7", "@babel/runtime": "^7.12.5", "@casualbot/jest-sonar-reporter": "2.2.7", + "@element-hq/element-call-embedded": "0.9.0", "@element-hq/element-web-playwright-common": "^1.1.5", "@peculiar/webcrypto": "^1.4.3", "@playwright/test": "^1.50.1", @@ -263,6 +267,7 @@ "minimist": "^1.2.6", "modernizr": "^3.12.0", "node-fetch": "^2.6.7", + "patch-package": "^8.0.0", "playwright-core": "^1.51.0", "postcss": "8.4.46", "postcss-easings": "^4.0.0", @@ -274,7 +279,7 @@ "postcss-preset-env": "^10.0.0", "postcss-scss": "^4.0.4", "postcss-simple-vars": "^7.0.1", - "prettier": "3.5.2", + "prettier": "3.5.3", "process": "^0.11.10", "raw-loader": "^4.0.2", "rimraf": "^6.0.0", diff --git a/patches/@matrix-org+react-sdk-module-api+2.5.0.patch b/patches/@matrix-org+react-sdk-module-api+2.5.0.patch new file mode 100644 index 0000000000..5b17244da0 --- /dev/null +++ b/patches/@matrix-org+react-sdk-module-api+2.5.0.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/@matrix-org/react-sdk-module-api/lib/ModuleApi.d.ts b/node_modules/@matrix-org/react-sdk-module-api/lib/ModuleApi.d.ts +index 917a7fc..a2710c6 100644 +--- a/node_modules/@matrix-org/react-sdk-module-api/lib/ModuleApi.d.ts ++++ b/node_modules/@matrix-org/react-sdk-module-api/lib/ModuleApi.d.ts +@@ -37,7 +37,7 @@ export interface ModuleApi { + * @returns Whether the user submitted the dialog or closed it, and the model returned by the + * dialog component if submitted. + */ +- openDialog = DialogContent

>(initialTitleOrOptions: string | ModuleUiDialogOptions, body: (props: P, ref: React.RefObject) => React.ReactNode, props?: Omit): Promise<{ ++ openDialog = DialogContent

>(initialTitleOrOptions: string | ModuleUiDialogOptions, body: (props: P, ref: React.RefObject) => React.ReactNode, props?: Omit): Promise<{ + didOkOrSubmit: boolean; + model: M; + }>; diff --git a/patches/@types+react+18.3.18.patch b/patches/@types+react+18.3.18.patch new file mode 100644 index 0000000000..18a1e64524 --- /dev/null +++ b/patches/@types+react+18.3.18.patch @@ -0,0 +1,76 @@ +diff --git a/node_modules/@types/react/index.d.ts b/node_modules/@types/react/index.d.ts +index 6ea73ef..cb51757 100644 +--- a/node_modules/@types/react/index.d.ts ++++ b/node_modules/@types/react/index.d.ts +@@ -151,7 +151,7 @@ declare namespace React { + /** + * The current value of the ref. + */ +- readonly current: T | null; ++ current: T; + } + + interface DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES { +@@ -186,7 +186,7 @@ declare namespace React { + * @see {@link RefObject} + */ + +- type Ref = RefCallback | RefObject | null; ++ type Ref = RefCallback | RefObject | null; + /** + * A legacy implementation of refs where you can pass a string to a ref prop. + * +@@ -300,7 +300,7 @@ declare namespace React { + * + * @see {@link https://react.dev/learn/referencing-values-with-refs#refs-and-the-dom React Docs} + */ +- ref?: LegacyRef | undefined; ++ ref?: LegacyRef | undefined; + } + + /** +@@ -1234,7 +1234,7 @@ declare namespace React { + * + * @see {@link ForwardRefRenderFunction} + */ +- type ForwardedRef = ((instance: T | null) => void) | MutableRefObject | null; ++ type ForwardedRef = ((instance: T | null) => void) | RefObject | null; + + /** + * The type of the function passed to {@link forwardRef}. This is considered different +@@ -1565,7 +1565,7 @@ declare namespace React { + [propertyName: string]: any; + } + +- function createRef(): RefObject; ++ function createRef(): RefObject; + + /** + * The type of the component returned from {@link forwardRef}. +@@ -1989,7 +1989,7 @@ declare namespace React { + * @version 16.8.0 + * @see {@link https://react.dev/reference/react/useRef} + */ +- function useRef(initialValue: T): MutableRefObject; ++ function useRef(initialValue: T): RefObject; + // convenience overload for refs given as a ref prop as they typically start with a null value + /** + * `useRef` returns a mutable ref object whose `.current` property is initialized to the passed argument +@@ -2004,7 +2004,7 @@ declare namespace React { + * @version 16.8.0 + * @see {@link https://react.dev/reference/react/useRef} + */ +- function useRef(initialValue: T | null): RefObject; ++ function useRef(initialValue: T | null): RefObject; + // convenience overload for potentially undefined initialValue / call with 0 arguments + // has a default to stop it from defaulting to {} instead + /** +@@ -2017,7 +2017,7 @@ declare namespace React { + * @version 16.8.0 + * @see {@link https://react.dev/reference/react/useRef} + */ +- function useRef(initialValue?: undefined): MutableRefObject; ++ function useRef(initialValue: T | undefined): RefObject; + /** + * The signature is identical to `useEffect`, but it fires synchronously after all DOM mutations. + * Use this to read layout from the DOM and synchronously re-render. Updates scheduled inside diff --git a/playwright/e2e/crypto/crypto.spec.ts b/playwright/e2e/crypto/crypto.spec.ts index 2d294ff7c2..6adef5be60 100644 --- a/playwright/e2e/crypto/crypto.spec.ts +++ b/playwright/e2e/crypto/crypto.spec.ts @@ -162,6 +162,7 @@ test.describe("Cryptography", function () { } test("Can reset cross-signing keys", async ({ page, app, user: aliceCredentials }) => { + await app.client.bootstrapCrossSigning(aliceCredentials); const secretStorageKey = await enableKeyBackup(app); // Fetch the current cross-signing keys diff --git a/playwright/e2e/crypto/dehydration.spec.ts b/playwright/e2e/crypto/dehydration.spec.ts index 89ee854c91..2f545b8d14 100644 --- a/playwright/e2e/crypto/dehydration.spec.ts +++ b/playwright/e2e/crypto/dehydration.spec.ts @@ -27,16 +27,22 @@ test.use({ test.describe("Dehydration", () => { test.skip(isDendrite, "does not yet support dehydration v2"); - test("'Set up secure backup' creates dehydrated device", async ({ page, user, app }, workerInfo) => { - // Create a backup (which will create SSSS, and dehydrated device) + test("Verify device and reset creates dehydrated device", async ({ page, user, credentials, app }, workerInfo) => { + // Verify the device by resetting the key (which will create SSSS, and dehydrated device) const securityTab = await app.settings.openUserSettings("Security & Privacy"); - - await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible(); await expect(securityTab.getByText("Offline device enabled")).not.toBeVisible(); - await securityTab.getByRole("button", { name: "Set up", exact: true }).click(); - await completeCreateSecretStorageDialog(page); + await app.closeDialog(); + + // Verify the device by resetting the key + const settings = await app.settings.openUserSettings("Encryption"); + await settings.getByRole("button", { name: "Verify this device" }).click(); + await page.getByRole("button", { name: "Proceed with reset" }).click(); + await page.getByRole("button", { name: "Continue" }).click(); + await page.getByRole("button", { name: "Copy" }).click(); + await page.getByRole("button", { name: "Continue" }).click(); + await page.getByRole("button", { name: "Done" }).click(); await expectDehydratedDeviceEnabled(app); diff --git a/playwright/e2e/crypto/event-shields.spec.ts b/playwright/e2e/crypto/event-shields.spec.ts index 7ac0df28ec..f73f190e10 100644 --- a/playwright/e2e/crypto/event-shields.spec.ts +++ b/playwright/e2e/crypto/event-shields.spec.ts @@ -324,7 +324,7 @@ test.describe("Cryptography", function () { await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/); await lastE2eIcon.focus(); await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText( - "Sender's verified identity has changed", + "Sender's verified identity was reset", ); }); }); diff --git a/playwright/e2e/crypto/invisible-crypto.spec.ts b/playwright/e2e/crypto/invisible-crypto.spec.ts index 198abdac6d..aadb6817ff 100644 --- a/playwright/e2e/crypto/invisible-crypto.spec.ts +++ b/playwright/e2e/crypto/invisible-crypto.spec.ts @@ -52,6 +52,6 @@ test.describe("Invisible cryptography", () => { /* should show an error for a message from a previously verified device */ await bobSecondDevice.sendMessage(testRoomId, "test encrypted from user that was previously verified"); const lastTile = page.locator(".mx_EventTile_last"); - await expect(lastTile).toContainText("Sender's verified identity has changed"); + await expect(lastTile).toContainText("Sender's verified identity was reset"); }); }); diff --git a/playwright/e2e/crypto/toasts.spec.ts b/playwright/e2e/crypto/toasts.spec.ts index 905a8fb1ed..5b80f822a0 100644 --- a/playwright/e2e/crypto/toasts.spec.ts +++ b/playwright/e2e/crypto/toasts.spec.ts @@ -29,7 +29,9 @@ test.describe("Key storage out of sync toast", () => { }); test("should prompt for recovery key if 'enter recovery key' pressed", { tag: "@screenshot" }, async ({ page }) => { - // Need to wait for 2 to appear since playwright only evaluates 'first()' initially, so the waiting won't work + // We need to wait for there to be two toasts as the wait below won't work in isolation: + // playwright only evaluates the 'first()' call initially, not subsequent times it checks, so + // it would always be checking the same toast, even if another one is now the first. await expect(page.getByRole("alert")).toHaveCount(2); await expect(page.getByRole("alert").first()).toMatchScreenshot("key-storage-out-of-sync-toast.png"); diff --git a/playwright/e2e/crypto/utils.ts b/playwright/e2e/crypto/utils.ts index a317a2a215..77f1b747a1 100644 --- a/playwright/e2e/crypto/utils.ts +++ b/playwright/e2e/crypto/utils.ts @@ -221,6 +221,9 @@ export async function logIntoElement(page: Page, credentials: Credentials, secur await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Recovery Key" }).click(); const useSecurityKey = page.locator(".mx_Dialog").getByRole("button", { name: "use your Recovery Key" }); + // If the user has set a recovery *passphrase*, they'll be prompted for that first and have to click + // through to enter the recovery key which is what we have here. If they haven't, they'll be prompted + // for a recovery key straight away. We click the button if it's there so this works in both cases. if (await useSecurityKey.isVisible()) { await useSecurityKey.click(); } @@ -289,17 +292,28 @@ export async function doTwoWaySasVerification(page: Page, verifier: JSHandle { - await app.settings.openUserSettings("Security & Privacy"); - await app.page.getByRole("button", { name: "Set up Secure Backup" }).click(); + const encryptionTab = await app.settings.openUserSettings("Encryption"); - return await completeCreateSecretStorageDialog(app.page); + const keyStorageToggle = encryptionTab.getByRole("checkbox", { name: "Allow key storage" }); + if (!(await keyStorageToggle.isChecked())) { + await encryptionTab.getByRole("checkbox", { name: "Allow key storage" }).click(); + } + + await encryptionTab.getByRole("button", { name: "Set up recovery" }).click(); + await encryptionTab.getByRole("button", { name: "Continue" }).click(); + + const recoveryKey = await encryptionTab.getByTestId("recoveryKey").innerText(); + await encryptionTab.getByRole("button", { name: "Continue" }).click(); + await encryptionTab.getByRole("textbox").fill(recoveryKey); + await encryptionTab.getByRole("button", { name: "Finish set up" }).click(); + await app.settings.closeDialog(); + return recoveryKey; } /** diff --git a/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts b/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts index bb87f90f19..3ebfb22de1 100644 --- a/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts +++ b/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts @@ -36,17 +36,21 @@ test.describe("Room list filters and sort", () => { return page.getByTestId("room-list"); } + let unReadDmId: string | undefined; + let unReadRoomId: string | undefined; + test.beforeEach(async ({ page, app, bot, user }) => { await app.client.createRoom({ name: "empty room" }); - const unReadDmId = await bot.createRoom({ + unReadDmId = await bot.createRoom({ name: "unread dm", invite: [user.userId], is_direct: true, }); + await app.client.joinRoom(unReadDmId); await bot.sendMessage(unReadDmId, "I am a robot. Beep."); - const unReadRoomId = await app.client.createRoom({ name: "unread room" }); + unReadRoomId = await app.client.createRoom({ name: "unread room" }); await app.client.inviteUser(unReadRoomId, bot.credentials.userId); await bot.joinRoom(unReadRoomId); await bot.sendMessage(unReadRoomId, "I am a robot. Beep."); @@ -88,6 +92,30 @@ test.describe("Room list filters and sort", () => { await expect(roomList.getByRole("gridcell", { name: "empty room" })).toBeVisible(); expect(await roomList.locator("role=gridcell").count()).toBe(3); }); + + test("unread filter should only match unread rooms that have a count", async ({ page, app, bot }) => { + const roomListView = getRoomList(page); + + // Let's configure unread dm room so that we only get notification for mentions and keywords + await app.viewRoomById(unReadDmId); + await app.settings.openRoomSettings("Notifications"); + await page.getByText("@mentions & keywords").click(); + await app.settings.closeDialog(); + + // Let's open a room other than unread room or unread dm + await roomListView.getByRole("gridcell", { name: "Open room favourite room" }).click(); + + // Let's make the bot send a new message in both rooms + await bot.sendMessage(unReadDmId, "Hello!"); + await bot.sendMessage(unReadRoomId, "Hello!"); + + // Let's activate the unread filter now + await page.getByRole("option", { name: "Unread" }).click(); + + // Unread filter should only show unread room and not unread dm! + await expect(roomListView.getByRole("gridcell", { name: "Open room unread room" })).toBeVisible(); + await expect(roomListView.getByRole("gridcell", { name: "Open room unread dm" })).not.toBeVisible(); + }); }); test.describe("Empty room list", () => { diff --git a/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts b/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts index 493ed0d1ab..ebca899a13 100644 --- a/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts +++ b/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts @@ -13,6 +13,9 @@ test.describe("Room list", () => { test.use({ displayName: "Alice", labsFlags: ["feature_new_room_list"], + botCreateOpts: { + displayName: "BotBob", + }, }); /** @@ -26,55 +29,199 @@ test.describe("Room list", () => { test.beforeEach(async ({ page, app, user }) => { // The notification toast is displayed above the search section await app.closeNotificationToast(); - for (let i = 0; i < 30; i++) { - await app.client.createRoom({ name: `room${i}` }); - } }); - test("should render the room list", { tag: "@screenshot" }, async ({ page, app, user }) => { - const roomListView = getRoomList(page); - await expect(roomListView.getByRole("gridcell", { name: "Open room room29" })).toBeVisible(); - await expect(roomListView).toMatchScreenshot("room-list.png"); + test.describe("Room list", () => { + test.beforeEach(async ({ page, app, user }) => { + for (let i = 0; i < 30; i++) { + await app.client.createRoom({ name: `room${i}` }); + } + }); - await roomListView.hover(); - // Scroll to the end of the room list - await page.mouse.wheel(0, 1000); - await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible(); - await expect(roomListView).toMatchScreenshot("room-list-scrolled.png"); + test("should render the room list", { tag: "@screenshot" }, async ({ page, app, user }) => { + const roomListView = getRoomList(page); + await expect(roomListView.getByRole("gridcell", { name: "Open room room29" })).toBeVisible(); + await expect(roomListView).toMatchScreenshot("room-list.png"); + + await roomListView.hover(); + // Scroll to the end of the room list + await page.mouse.wheel(0, 1000); + await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible(); + await expect(roomListView).toMatchScreenshot("room-list-scrolled.png"); + }); + + test("should open the room when it is clicked", async ({ page, app, user }) => { + const roomListView = getRoomList(page); + await roomListView.getByRole("gridcell", { name: "Open room room29" }).click(); + await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible(); + }); + + test("should open the more options menu", { tag: "@screenshot" }, async ({ page, app, user }) => { + const roomListView = getRoomList(page); + const roomItem = roomListView.getByRole("gridcell", { name: "Open room room29" }); + await roomItem.hover(); + + await expect(roomItem).toMatchScreenshot("room-list-item-hover.png"); + const roomItemMenu = roomItem.getByRole("button", { name: "More Options" }); + await roomItemMenu.click(); + await expect(page).toMatchScreenshot("room-list-item-open-more-options.png"); + + // It should make the room favourited + await page.getByRole("menuitemcheckbox", { name: "Favourited" }).click(); + + // Check that the room is favourited + await roomItem.hover(); + await roomItemMenu.click(); + await expect(page.getByRole("menuitemcheckbox", { name: "Favourited" })).toBeChecked(); + // It should show the invite dialog + await page.getByRole("menuitem", { name: "invite" }).click(); + await expect(page.getByRole("heading", { name: "Invite to room29" })).toBeVisible(); + await app.closeDialog(); + + // It should leave the room + await roomItem.hover(); + await roomItemMenu.click(); + await page.getByRole("menuitem", { name: "leave room" }).click(); + await expect(roomItem).not.toBeVisible(); + }); + + test("should scroll to the current room", async ({ page, app, user }) => { + const roomListView = getRoomList(page); + await roomListView.hover(); + // Scroll to the end of the room list + await page.mouse.wheel(0, 1000); + + await roomListView.getByRole("gridcell", { name: "Open room room0" }).click(); + + const filters = page.getByRole("listbox", { name: "Room list filters" }); + await filters.getByRole("option", { name: "People" }).click(); + await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).not.toBeVisible(); + + await filters.getByRole("option", { name: "People" }).click(); + await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible(); + }); }); - test("should open the room when it is clicked", async ({ page, app, user }) => { - const roomListView = getRoomList(page); - await roomListView.getByRole("gridcell", { name: "Open room room29" }).click(); - await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible(); - }); + test.describe("Notification decoration", () => { + test("should render the invitation decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => { + const roomListView = getRoomList(page); - test("should open the more options menu", { tag: "@screenshot" }, async ({ page, app, user }) => { - const roomListView = getRoomList(page); - const roomItem = roomListView.getByRole("gridcell", { name: "Open room room29" }); - await roomItem.hover(); + await bot.createRoom({ + name: "invited room", + invite: [user.userId], + is_direct: true, + }); + const invitedRoom = roomListView.getByRole("gridcell", { name: "invited room" }); + await expect(invitedRoom).toBeVisible(); + await expect(invitedRoom).toMatchScreenshot("room-list-item-invited.png"); + }); - await expect(roomItem).toMatchScreenshot("room-list-item-hover.png"); - const roomItemMenu = roomItem.getByRole("button", { name: "More Options" }); - await roomItemMenu.click(); - await expect(page).toMatchScreenshot("room-list-item-open-more-options.png"); + test("should render the regular decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => { + const roomListView = getRoomList(page); - // It should make the room favourited - await page.getByRole("menuitemcheckbox", { name: "Favourited" }).click(); + const roomId = await app.client.createRoom({ name: "2 notifications" }); + await app.client.inviteUser(roomId, bot.credentials.userId); + await bot.joinRoom(roomId); - // Check that the room is favourited - await roomItem.hover(); - await roomItemMenu.click(); - await expect(page.getByRole("menuitemcheckbox", { name: "Favourited" })).toBeChecked(); - // It should show the invite dialog - await page.getByRole("menuitem", { name: "invite" }).click(); - await expect(page.getByRole("heading", { name: "Invite to room29" })).toBeVisible(); - await app.closeDialog(); + await bot.sendMessage(roomId, "I am a robot. Beep."); + await bot.sendMessage(roomId, "I am a robot. Beep."); - // It should leave the room - await roomItem.hover(); - await roomItemMenu.click(); - await page.getByRole("menuitem", { name: "leave room" }).click(); - await expect(roomItem).not.toBeVisible(); + const room = roomListView.getByRole("gridcell", { name: "2 notifications" }); + await expect(room).toBeVisible(); + await expect(room.getByTestId("notification-decoration")).toHaveText("2"); + await expect(room).toMatchScreenshot("room-list-item-notification.png"); + }); + + test("should render the mention decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => { + const roomListView = getRoomList(page); + + const roomId = await app.client.createRoom({ name: "mention" }); + await app.client.inviteUser(roomId, bot.credentials.userId); + await bot.joinRoom(roomId); + + const clientBot = await bot.prepareClient(); + await clientBot.evaluate( + async (client, { roomId, userId }) => { + await client.sendMessage(roomId, { + // @ts-ignore ignore usage of MsgType.text + "msgtype": "m.text", + "body": "User", + "format": "org.matrix.custom.html", + "formatted_body": `User`, + "m.mentions": { + user_ids: [userId], + }, + }); + }, + { roomId, userId: user.userId }, + ); + await bot.sendMessage(roomId, "I am a robot. Beep."); + + const room = roomListView.getByRole("gridcell", { name: "mention" }); + await expect(room).toBeVisible(); + await expect(room).toMatchScreenshot("room-list-item-mention.png"); + }); + + test("should render an activity decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => { + const roomListView = getRoomList(page); + + const otherRoomId = await app.client.createRoom({ name: "other room" }); + + const roomId = await app.client.createRoom({ name: "activity" }); + await app.client.inviteUser(roomId, bot.credentials.userId); + await bot.joinRoom(roomId); + + await app.viewRoomById(roomId); + await app.settings.openRoomSettings("Notifications"); + await page.getByText("@mentions & keywords").click(); + await app.settings.closeDialog(); + + await app.settings.openUserSettings("Notifications"); + await page.getByText("Show all activity in the room list (dots or number of unread messages)").click(); + await app.settings.closeDialog(); + + // Switch to the other room to avoid the notification to be cleared + await app.viewRoomById(otherRoomId); + await bot.sendMessage(roomId, "I am a robot. Beep."); + + const room = roomListView.getByRole("gridcell", { name: "activity" }); + await expect(room.getByTestId("notification-decoration")).toBeVisible(); + await expect(room).toMatchScreenshot("room-list-item-activity.png"); + }); + + test("should render a mark as unread decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => { + const roomListView = getRoomList(page); + + const roomId = await app.client.createRoom({ name: "mark as unread" }); + await app.client.inviteUser(roomId, bot.credentials.userId); + await bot.joinRoom(roomId); + + const room = roomListView.getByRole("gridcell", { name: "mark as unread" }); + await room.hover(); + await room.getByRole("button", { name: "More Options" }).click(); + await page.getByRole("menuitem", { name: "mark as unread" }).click(); + + // Remove hover on the room list item + await roomListView.hover(); + + await expect(room).toMatchScreenshot("room-list-item-mark-as-unread.png"); + }); + + test("should render silent decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => { + const roomListView = getRoomList(page); + + const roomId = await app.client.createRoom({ name: "silent" }); + await app.client.inviteUser(roomId, bot.credentials.userId); + await bot.joinRoom(roomId); + + await app.viewRoomById(roomId); + await app.settings.openRoomSettings("Notifications"); + await page.getByText("Off").click(); + await app.settings.closeDialog(); + + const room = roomListView.getByRole("gridcell", { name: "silent" }); + await expect(room.getByTestId("notification-decoration")).toBeVisible(); + await expect(room).toMatchScreenshot("room-list-item-silent.png"); + }); }); }); diff --git a/playwright/e2e/login/login-consent.spec.ts b/playwright/e2e/login/login-consent.spec.ts index 28948dc3b9..c4a4b1409f 100644 --- a/playwright/e2e/login/login-consent.spec.ts +++ b/playwright/e2e/login/login-consent.spec.ts @@ -13,6 +13,7 @@ import { selectHomeserver } from "../utils"; import { type Credentials, type HomeserverInstance } from "../../plugins/homeserver"; import { consentHomeserver } from "../../plugins/homeserver/synapse/consentHomeserver.ts"; import { isDendrite } from "../../plugins/homeserver/dendrite"; +import { createBot } from "../crypto/utils.ts"; // This test requires fixed credentials for the device signing keys below to work const username = "user1234"; @@ -258,6 +259,34 @@ test.describe("Login", () => { await expect(h1.locator(".mx_CompleteSecurity_skip")).toHaveCount(0); }); + + test("Continues to show verification prompt after cancelling device verification", async ({ + page, + homeserver, + credentials, + }) => { + // Create a different device which is cross-signed, meaning we need to verify this device + await createBot(page, homeserver, credentials, true); + + // Wait to avoid homeserver rate limit on logins + await page.waitForTimeout(100); + + // Load the page and see that we are asked to verify + await page.goto("/#/welcome"); + await login(page, homeserver, credentials); + let h1 = page.getByRole("heading", { name: "Verify this device", level: 1 }); + await expect(h1).toBeVisible(); + + // Click "Verify with another device" + await page.getByRole("button", { name: "Verify with another device" }).click(); + + // Cancel the new dialog + await page.getByRole("button", { name: "Close dialog" }).click(); + + // Check that we are still being asked to verify + h1 = page.getByRole("heading", { name: "Verify this device", level: 1 }); + await expect(h1).toBeVisible(); + }); }); }); }); diff --git a/playwright/e2e/oidc/oidc-native.spec.ts b/playwright/e2e/oidc/oidc-native.spec.ts index 8c9128c39b..2592311554 100644 --- a/playwright/e2e/oidc/oidc-native.spec.ts +++ b/playwright/e2e/oidc/oidc-native.spec.ts @@ -73,4 +73,33 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => { await revokeAccessTokenPromise; await revokeRefreshTokenPromise; }); + + test( + "it should log out the user & wipe data when logging out via MAS", + { tag: "@screenshot" }, + async ({ mas, page, mailpitClient }, testInfo) => { + // We use this over the `user` fixture to ensure we get an OIDC session rather than a compatibility one + await page.goto("/#/login"); + await page.getByRole("button", { name: "Continue" }).click(); + + const userId = `alice_${testInfo.testId}`; + await registerAccountMas(page, mailpitClient, userId, "alice@email.com", "Pa$sW0rD!"); + + await expect(page.getByText("Welcome")).toBeVisible(); + await page.goto("about:blank"); + + // @ts-expect-error + const result = await mas.manage("kill-sessions", userId); + expect(result.output).toContain("Ended 1 active OAuth 2.0 session"); + + await page.goto("http://localhost:8080"); + await expect( + page.getByText("For security, this session has been signed out. Please sign in again."), + ).toBeVisible(); + await expect(page).toMatchScreenshot("token-expired.png", { includeDialogBackground: true }); + + const localStorageKeys = await page.evaluate(() => Object.keys(localStorage)); + expect(localStorageKeys).toHaveLength(0); + }, + ); }); diff --git a/playwright/e2e/right-panel/right-panel.spec.ts b/playwright/e2e/right-panel/right-panel.spec.ts index cc03963801..44b051c987 100644 --- a/playwright/e2e/right-panel/right-panel.spec.ts +++ b/playwright/e2e/right-panel/right-panel.spec.ts @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024, 2025 New Vector Ltd. Copyright 2022 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial @@ -10,6 +10,7 @@ import { type Locator, type Page } from "@playwright/test"; import { test, expect } from "../../element-web-test"; import { checkRoomSummaryCard, viewRoomSummaryByName } from "./utils"; +import { isDendrite } from "../../plugins/homeserver/dendrite"; const ROOM_NAME = "Test room"; const ROOM_NAME_LONG = @@ -133,6 +134,17 @@ test.describe("RightPanel", () => { await page.getByLabel("Room info").nth(1).click(); await checkRoomSummaryCard(page, ROOM_NAME); }); + test.describe("room reporting", () => { + test.skip(isDendrite, "Dendrite does not implement room reporting"); + test("should handle reporting a room", async ({ page, app }) => { + await viewRoomSummaryByName(page, app, ROOM_NAME); + await page.getByRole("menuitem", { name: "Report room" }).click(); + const dialog = await page.getByRole("dialog", { name: "Report Room" }); + await dialog.getByLabel("reason").fill("This room should be reported"); + await dialog.getByRole("button", { name: "Send report" }).click(); + await expect(page.getByText("Your report was sent.")).toBeVisible(); + }); + }); }); test.describe("in spaces", () => { 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 index b4eeed7728..53f9c37dd0 100644 --- 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 @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024,2025 New Vector Ltd. Copyright 2023 Suguru Hirahara SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial @@ -50,8 +50,8 @@ test.describe("Appearance user settings tab", () => { // 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(); + await tab.getByLabel("Use bundled emoji font").click(); + await tab.getByLabel("Use a system font").click(); // Assert that the font-family value was removed await expect(page.locator("body")).toHaveCSS("font-family", '""'); diff --git a/playwright/e2e/settings/preferences-user-settings-tab.spec.ts b/playwright/e2e/settings/preferences-user-settings-tab.spec.ts index 5c7c9efffb..33392d8848 100644 --- a/playwright/e2e/settings/preferences-user-settings-tab.spec.ts +++ b/playwright/e2e/settings/preferences-user-settings-tab.spec.ts @@ -28,7 +28,10 @@ test.describe("Preferences user settings tab", () => { const tab = await app.settings.openUserSettings("Preferences"); // Assert that the top heading is rendered await expect(tab.getByRole("heading", { name: "Preferences" })).toBeVisible(); - await expect(tab).toMatchScreenshot("Preferences-user-settings-tab-should-be-rendered-properly-1.png"); + await expect(tab).toMatchScreenshot("Preferences-user-settings-tab-should-be-rendered-properly-1.png", { + // masked due to daylight saving time + mask: [tab.locator("#mx_dropdownUserTimezone_value")], + }); }); test("should be able to change the app language", { tag: ["@no-firefox", "@no-webkit"] }, async ({ uut, user }) => { diff --git a/playwright/e2e/sliding-sync/sliding-sync.spec.ts b/playwright/e2e/sliding-sync/sliding-sync.spec.ts index bf992a0edd..118bd4585e 100644 --- a/playwright/e2e/sliding-sync/sliding-sync.spec.ts +++ b/playwright/e2e/sliding-sync/sliding-sync.spec.ts @@ -7,47 +7,15 @@ Please see LICENSE files in the repository root for full details. */ import { type Page, type Request } from "@playwright/test"; -import { GenericContainer, type StartedTestContainer, Wait } from "testcontainers"; import { test as base, expect } from "../../element-web-test"; import type { ElementAppPage } from "../../pages/ElementAppPage"; import type { Bot } from "../../pages/bot"; const test = base.extend<{ - slidingSyncProxy: StartedTestContainer; testRoom: { roomId: string; name: string }; joinedBot: Bot; }>({ - slidingSyncProxy: async ({ logger, network, postgres, page, homeserver }, use, testInfo) => { - const container = await new GenericContainer("ghcr.io/matrix-org/sliding-sync:v0.99.3") - .withNetwork(network) - .withExposedPorts(8008) - .withLogConsumer(logger.getConsumer("sliding-sync-proxy")) - .withWaitStrategy(Wait.forHttp("/client/server.json", 8008)) - .withEnvironment({ - SYNCV3_SECRET: "bwahahaha", - SYNCV3_DB: `user=${postgres.getUsername()} dbname=postgres password=${postgres.getPassword()} host=postgres sslmode=disable`, - SYNCV3_SERVER: `http://homeserver:8008`, - }) - .start(); - - const proxyAddress = `http://${container.getHost()}:${container.getMappedPort(8008)}`; - await page.addInitScript((proxyAddress) => { - window.localStorage.setItem( - "mx_local_settings", - JSON.stringify({ - feature_sliding_sync_proxy_url: proxyAddress, - }), - ); - window.localStorage.setItem("mx_labs_feature_feature_sliding_sync", "true"); - }, proxyAddress); - await use(container); - await container.stop(); - }, - // Ensure slidingSyncProxy is set up before the user fixture as it relies on an init script - credentials: async ({ slidingSyncProxy, credentials }, use) => { - await use(credentials); - }, testRoom: async ({ user, app }, use) => { const name = "Test Room"; const roomId = await app.client.createRoom({ name }); @@ -82,6 +50,14 @@ test.describe("Sliding Sync", () => { }); }; + test.use({ + config: { + features: { + feature_simplified_sliding_sync: true, + }, + }, + }); + // Load the user fixture for all tests test.beforeEach(({ user }) => {}); @@ -188,15 +164,7 @@ test.describe("Sliding Sync", () => { ).not.toBeAttached(); }); - test("should not show unread indicators", async ({ page, app, joinedBot: bot, testRoom }) => { - // TODO: for now. Later we should. - - // disable notifs in this room (TODO: CS API call?) - const locator = page.getByRole("treeitem", { name: "Test Room" }); - await locator.hover(); - await locator.getByRole("button", { name: "Notification options" }).click(); - await page.getByRole("menuitemradio", { name: "Mute room" }).click(); - + test("should show unread indicators", async ({ page, app, joinedBot: bot, testRoom }) => { // create a new room so we know when the message has been received as it'll re-shuffle the room list await app.client.createRoom({ name: "Dummy" }); @@ -207,9 +175,7 @@ test.describe("Sliding Sync", () => { // wait for this message to arrive, tell by the room list resorting await checkOrder(["Test Room", "Dummy"], page); - await expect( - page.getByRole("treeitem", { name: "Test Room" }).locator(".mx_NotificationBadge"), - ).not.toBeAttached(); + await expect(page.getByRole("treeitem", { name: "Test Room" }).locator(".mx_NotificationBadge")).toBeAttached(); }); test("should update user settings promptly", async ({ page, app }) => { @@ -221,6 +187,37 @@ test.describe("Sliding Sync", () => { await expect(locator.locator(".mx_ToggleSwitch_on")).toBeAttached(); }); + test("should send subscribe_rooms on room switch if room not already subscribed", async ({ page, app }) => { + // create rooms and check room names are correct + const roomIds: string[] = []; + for (const fruit of ["Apple", "Pineapple", "Orange"]) { + const id = await app.client.createRoom({ name: fruit }); + roomIds.push(id); + await expect(page.getByRole("treeitem", { name: fruit })).toBeVisible(); + } + const [roomAId, roomPId] = roomIds; + + const matchRoomSubRequest = (subRoomId: string) => (request: Request) => { + if (!request.url().includes("/sync")) return false; + const body = request.postDataJSON(); + return body.room_subscriptions?.[subRoomId]; + }; + + // Select the Test Room and wait for playwright to get the request + const [request] = await Promise.all([ + page.waitForRequest(matchRoomSubRequest(roomAId)), + page.getByRole("treeitem", { name: "Apple", exact: true }).click(), + ]); + const roomSubscriptions = request.postDataJSON().room_subscriptions; + expect(roomSubscriptions, "room_subscriptions is object").toBeDefined(); + + // Switch to another room and wait for playwright to get the request + await Promise.all([ + page.waitForRequest(matchRoomSubRequest(roomPId)), + page.getByRole("treeitem", { name: "Pineapple", exact: true }).click(), + ]); + }); + test("should show and be able to accept/reject/rescind invites", async ({ page, app, @@ -361,52 +358,4 @@ test.describe("Sliding Sync", () => { // ensure the reply-to does not disappear await expect(page.locator(".mx_ReplyPreview")).toBeVisible(); }); - - test("should send unsubscribe_rooms for every room switch", async ({ page, app }) => { - // create rooms and check room names are correct - const roomIds: string[] = []; - for (const fruit of ["Apple", "Pineapple", "Orange"]) { - const id = await app.client.createRoom({ name: fruit }); - roomIds.push(id); - await expect(page.getByRole("treeitem", { name: fruit })).toBeVisible(); - } - const [roomAId, roomPId, roomOId] = roomIds; - - const matchRoomSubRequest = (subRoomId: string) => (request: Request) => { - if (!request.url().includes("/sync")) return false; - const body = request.postDataJSON(); - return body.txn_id && body.room_subscriptions?.[subRoomId]; - }; - const matchRoomUnsubRequest = (unsubRoomId: string) => (request: Request) => { - if (!request.url().includes("/sync")) return false; - const body = request.postDataJSON(); - return ( - body.txn_id && body.unsubscribe_rooms?.includes(unsubRoomId) && !body.room_subscriptions?.[unsubRoomId] - ); - }; - - // Select the Test Room and wait for playwright to get the request - const [request] = await Promise.all([ - page.waitForRequest(matchRoomSubRequest(roomAId)), - page.getByRole("treeitem", { name: "Apple", exact: true }).click(), - ]); - const roomSubscriptions = request.postDataJSON().room_subscriptions; - expect(roomSubscriptions, "room_subscriptions is object").toBeDefined(); - - // Switch to another room and wait for playwright to get the request - await Promise.all([ - page.waitForRequest(matchRoomSubRequest(roomPId)), - page.waitForRequest(matchRoomUnsubRequest(roomAId)), - page.getByRole("treeitem", { name: "Pineapple", exact: true }).click(), - ]); - - // And switch to even another room and wait for playwright to get the request - await Promise.all([ - page.waitForRequest(matchRoomSubRequest(roomOId)), - page.waitForRequest(matchRoomUnsubRequest(roomPId)), - page.getByRole("treeitem", { name: "Orange", exact: true }).click(), - ]); - - // TODO: Add tests for encrypted rooms - }); }); diff --git a/playwright/e2e/spaces/spaces.spec.ts b/playwright/e2e/spaces/spaces.spec.ts index dce7515ef0..56c8ae22b8 100644 --- a/playwright/e2e/spaces/spaces.spec.ts +++ b/playwright/e2e/spaces/spaces.spec.ts @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024,2025 New Vector Ltd. Copyright 2022 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial @@ -35,17 +35,18 @@ function spaceCreateOptions(spaceName: string, roomIds: string[] = []): ICreateR name: spaceName, }, }, - ...roomIds.map(spaceChildInitialState), + ...roomIds.map((r) => spaceChildInitialState(r)), ], }; } -function spaceChildInitialState(roomId: string): ICreateRoomOpts["initial_state"]["0"] { +function spaceChildInitialState(roomId: string, order?: string): ICreateRoomOpts["initial_state"]["0"] { return { type: "m.space.child", state_key: roomId, content: { via: [roomId.split(":")[1]], + order, }, }; } @@ -121,9 +122,10 @@ test.describe("Spaces", () => { await page.getByRole("button", { name: "Skip for now" }).click(); // Assert rooms exist in the room list - await expect(page.getByRole("treeitem", { name: "General", exact: true })).toBeVisible(); - await expect(page.getByRole("treeitem", { name: "Random", exact: true })).toBeVisible(); - await expect(page.getByRole("treeitem", { name: "Projects", exact: true })).toBeVisible(); + const roomList = page.getByRole("tree", { name: "Rooms" }); + await expect(roomList.getByRole("treeitem", { name: "General", exact: true })).toBeVisible(); + await expect(roomList.getByRole("treeitem", { name: "Random", exact: true })).toBeVisible(); + await expect(roomList.getByRole("treeitem", { name: "Projects", exact: true })).toBeVisible(); // Assert rooms exist in the space explorer await expect( @@ -155,7 +157,7 @@ test.describe("Spaces", () => { await page.getByRole("button", { name: "Just me" }).click(); - await page.getByText("Sample Room").click({ force: true }); // force click as checkbox size is zero + await page.getByRole("checkbox", { name: "Sample Room" }).click(); // Temporal implementation as multiple elements with the role "button" and name "Add" are found await page.locator(".mx_AddExistingToSpace_footer").getByRole("button", { name: "Add" }).click(); @@ -165,6 +167,50 @@ test.describe("Spaces", () => { ).toBeVisible(); }); + test( + "should allow user to add an existing room to a space after creation", + { tag: "@screenshot" }, + async ({ page, app, user }) => { + await app.client.createRoom({ + name: "Sample Room", + }); + await app.client.createRoom({ + name: "A Room that will not be selected", + }); + + const menu = await openSpaceCreateMenu(page); + await menu.getByRole("button", { name: "Private" }).click(); + + await menu + .locator('.mx_SpaceBasicSettings_avatarContainer input[type="file"]') + .setInputFiles("playwright/sample-files/riot.png"); + await expect(menu.getByRole("textbox", { name: "Address" })).not.toBeVisible(); + await menu + .getByRole("textbox", { name: "Description" }) + .fill("This is a personal space to mourn Riot.im..."); + await menu.getByRole("textbox", { name: "Name" }).fill("This is my Riot"); + await menu.getByRole("textbox", { name: "Name" }).press("Enter"); + + await page.getByRole("button", { name: "Just me" }).click(); + + await page.getByRole("button", { name: "Skip for now" }).click(); + + await page.getByRole("button", { name: "Add room" }).click(); + await page.getByRole("menuitem", { name: "Add existing room" }).click(); + + await page.getByRole("checkbox", { name: "Sample Room" }).click(); + + await expect(page.getByRole("dialog", { name: "Avatar Add existing rooms" })).toMatchScreenshot( + "add-existing-rooms-dialog.png", + ); + + await page.getByRole("button", { name: "Add" }).click(); + await expect( + page.locator(".mx_SpaceHierarchy_list").getByRole("treeitem", { name: "Sample Room" }), + ).toBeVisible(); + }, + ); + test("should allow user to invite another to a space", { tag: "@no-webkit" }, async ({ page, app, user, bot }) => { await app.client.createSpace({ visibility: "public" as any, @@ -291,4 +337,36 @@ test.describe("Spaces", () => { // Assert we get shown the new room intro, and thus not the soft crash screen await expect(page.locator(".mx_NewRoomIntro")).toBeVisible(); }); + + test("should render spaces view", { tag: "@screenshot" }, async ({ page, app, user, axe }) => { + axe.disableRules([ + // Disable this check as it triggers on nested roving tab index elements which are in practice fine + "nested-interactive", + // XXX: We have some known contrast issues here + "color-contrast", + ]); + + const childSpaceId1 = await app.client.createSpace({ + name: "Child Space 1", + initial_state: [], + }); + const childSpaceId2 = await app.client.createSpace({ + name: "Child Space 2", + initial_state: [], + }); + const childSpaceId3 = await app.client.createSpace({ + name: "Child Space 3", + initial_state: [], + }); + await app.client.createSpace({ + name: "Root Space", + initial_state: [ + spaceChildInitialState(childSpaceId1, "a"), + spaceChildInitialState(childSpaceId2, "b"), + spaceChildInitialState(childSpaceId3, "c"), + ], + }); + await app.viewSpaceByName("Root Space"); + await expect(page.locator(".mx_SpaceRoomView")).toMatchScreenshot("space-room-view.png"); + }); }); diff --git a/playwright/e2e/timeline/timeline.spec.ts b/playwright/e2e/timeline/timeline.spec.ts index 7b13d1ccb1..e5bfa0c71b 100644 --- a/playwright/e2e/timeline/timeline.spec.ts +++ b/playwright/e2e/timeline/timeline.spec.ts @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024, 2025 New Vector Ltd. Copyright 2022, 2023 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial @@ -28,6 +28,8 @@ const NEW_AVATAR = fs.readFileSync("playwright/sample-files/element.png"); const OLD_NAME = "Alan"; const NEW_NAME = "Alan (away)"; +const VIDEO_FILE = fs.readFileSync("playwright/sample-files/5secvid.webm"); + const getEventTilesWithBodies = (page: Page): Locator => { return page.locator(".mx_EventTile").filter({ has: page.locator(".mx_EventTile_body") }); }; @@ -905,6 +907,39 @@ test.describe("Timeline", () => { mask: [page.locator(".mx_MessageTimestamp")], }); }); + + test("should be able to hide an image", { tag: "@screenshot" }, async ({ page, app, room, context }) => { + await app.viewRoomById(room.roomId); + await sendImage(app.client, room.roomId, NEW_AVATAR); + await app.timeline.scrollToBottom(); + const imgTile = page.locator(".mx_MImageBody").first(); + await expect(imgTile).toBeVisible(); + await imgTile.hover(); + await page.getByRole("button", { name: "Hide" }).click(); + + // Check that the image is now hidden. + await expect(page.getByRole("button", { name: "Show image" })).toBeVisible(); + }); + + test("should be able to hide a video", async ({ page, app, room, context }) => { + await app.viewRoomById(room.roomId); + const upload = await app.client.uploadContent(VIDEO_FILE, { name: "bbb.webm", type: "video/webm" }); + await app.client.sendEvent(room.roomId, null, "m.room.message" as EventType, { + msgtype: "m.video" as MsgType, + body: "bbb.webm", + url: upload.content_uri, + }); + + await app.timeline.scrollToBottom(); + const imgTile = page.locator(".mx_MVideoBody").first(); + await expect(imgTile).toBeVisible(); + await imgTile.hover(); + await page.getByRole("button", { name: "Hide" }).click(); + + // Check that the video is now hidden. + await expect(page.getByRole("button", { name: "Show video" })).toBeVisible(); + await expect(page.locator("video")).not.toBeVisible(); + }); }); test.describe("message sending", { tag: ["@no-firefox", "@no-webkit"] }, () => { @@ -1306,4 +1341,44 @@ test.describe("Timeline", () => { ); }); }); + + test.describe("spoilers", { tag: "@screenshot" }, () => { + test("clicking a spoiler containing the pill de-spoilers on 1st click, then follows link on 2nd", async ({ + page, + user, + app, + room, + }) => { + // View room + await page.goto(`/#/room/${room.roomId}`); + + // Send a spoilered pill + await app.client.sendMessage(room.roomId, { + msgtype: "m.text", + body: user.userId, + format: "org.matrix.custom.html", + formatted_body: `https://matrix.to/#/${user.userId}`, + }); + + const screenshotOptions = { + css: ` + .mx_MessageTimestamp { + display: none !important; + } + `, + }; + + const eventTile = page.locator(".mx_RoomView_body .mx_EventTile_last"); + await expect(eventTile).toMatchScreenshot("spoiler.png", screenshotOptions); + + const rightPanelButton = page.getByText("Share profile"); + const pill = page.locator(".mx_UserPill"); + await pill.click({ force: true }); // force to click the spoiler wrapper instead + await expect(eventTile).toMatchScreenshot("spoiler-uncovered.png", screenshotOptions); + await expect(rightPanelButton).not.toBeVisible(); // assert the right panel is not yet open + + await pill.click(); + await expect(rightPanelButton).toBeVisible(); // assert the right panel is open + }); + }); }); diff --git a/playwright/pages/ElementAppPage.ts b/playwright/pages/ElementAppPage.ts index 15b475a5d1..afc814b3e1 100644 --- a/playwright/pages/ElementAppPage.ts +++ b/playwright/pages/ElementAppPage.ts @@ -114,7 +114,7 @@ export class ElementAppPage { * @param isRightPanel whether to select the right panel composer, otherwise the main timeline composer */ public getComposerField(isRightPanel?: boolean): Locator { - return this.getComposer(isRightPanel).locator("[contenteditable]"); + return this.getComposer(isRightPanel).locator("div[contenteditable]"); } /** diff --git a/playwright/sample-files/5secvid.webm b/playwright/sample-files/5secvid.webm new file mode 100644 index 0000000000..ec5a695fa4 Binary files /dev/null and b/playwright/sample-files/5secvid.webm differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-linux.png index cba5fa86d4..313f6a1d14 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-activity-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-activity-linux.png new file mode 100644 index 0000000000..8da208a85d Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-activity-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-hover-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-hover-linux.png index 45d5588733..eccf48a528 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-hover-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-hover-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-invited-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-invited-linux.png new file mode 100644 index 0000000000..d6e1d2e549 Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-invited-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-mark-as-unread-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-mark-as-unread-linux.png new file mode 100644 index 0000000000..ded76f85e3 Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-mark-as-unread-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-mention-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-mention-linux.png new file mode 100644 index 0000000000..ae5bfdabc1 Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-mention-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-notification-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-notification-linux.png new file mode 100644 index 0000000000..d48bf49aa4 Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-notification-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png index 0501bf1e4c..40206cc22b 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-silent-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-silent-linux.png new file mode 100644 index 0000000000..35bcbea2a1 Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-silent-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-linux.png index 5d88f45aa5..35de485457 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-scrolled-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-scrolled-linux.png index 157b0ce156..87c88e8e46 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-scrolled-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-scrolled-linux.png differ diff --git a/playwright/snapshots/oidc/oidc-native.spec.ts/token-expired-linux.png b/playwright/snapshots/oidc/oidc-native.spec.ts/token-expired-linux.png new file mode 100644 index 0000000000..17479bb870 Binary files /dev/null and b/playwright/snapshots/oidc/oidc-native.spec.ts/token-expired-linux.png differ diff --git a/playwright/snapshots/right-panel/right-panel.spec.ts/with-leave-room-linux.png b/playwright/snapshots/right-panel/right-panel.spec.ts/with-leave-room-linux.png index a2bc63aaf4..931e9f9491 100644 Binary files a/playwright/snapshots/right-panel/right-panel.spec.ts/with-leave-room-linux.png and b/playwright/snapshots/right-panel/right-panel.spec.ts/with-leave-room-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 index d1c320bff1..88d7919dcc 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png 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/encryption-user-tab/encryption-tab.spec.ts/default-tab-linux.png b/playwright/snapshots/settings/encryption-user-tab/encryption-tab.spec.ts/default-tab-linux.png index 3af3e2aedf..bf57e8c875 100644 Binary files a/playwright/snapshots/settings/encryption-user-tab/encryption-tab.spec.ts/default-tab-linux.png and b/playwright/snapshots/settings/encryption-user-tab/encryption-tab.spec.ts/default-tab-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 077c8ba462..91d402ed8a 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/settings/quick-settings-menu.spec.ts/quick-settings-linux.png b/playwright/snapshots/settings/quick-settings-menu.spec.ts/quick-settings-linux.png index 135c669fb0..b40d183830 100644 Binary files a/playwright/snapshots/settings/quick-settings-menu.spec.ts/quick-settings-linux.png and b/playwright/snapshots/settings/quick-settings-menu.spec.ts/quick-settings-linux.png differ diff --git a/playwright/snapshots/spaces/spaces.spec.ts/add-existing-rooms-dialog-linux.png b/playwright/snapshots/spaces/spaces.spec.ts/add-existing-rooms-dialog-linux.png new file mode 100644 index 0000000000..53ccabf4f2 Binary files /dev/null and b/playwright/snapshots/spaces/spaces.spec.ts/add-existing-rooms-dialog-linux.png differ diff --git a/playwright/snapshots/spaces/spaces.spec.ts/space-room-view-linux.png b/playwright/snapshots/spaces/spaces.spec.ts/space-room-view-linux.png new file mode 100644 index 0000000000..8e627ec9f2 Binary files /dev/null and b/playwright/snapshots/spaces/spaces.spec.ts/space-room-view-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/spoiler-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/spoiler-linux.png new file mode 100644 index 0000000000..529d254dc4 Binary files /dev/null and b/playwright/snapshots/timeline/timeline.spec.ts/spoiler-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/spoiler-uncovered-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/spoiler-uncovered-linux.png new file mode 100644 index 0000000000..41d5bc8bee Binary files /dev/null and b/playwright/snapshots/timeline/timeline.spec.ts/spoiler-uncovered-linux.png differ diff --git a/playwright/testcontainers/synapse.ts b/playwright/testcontainers/synapse.ts index f9ea2cd1c7..19d1544196 100644 --- a/playwright/testcontainers/synapse.ts +++ b/playwright/testcontainers/synapse.ts @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. import { SynapseContainer as BaseSynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers"; -const TAG = "develop@sha256:65a8084668f7b468310cf4e1f29ac6987439ab3f4cb5608d6e02ef0acb819b67"; +const TAG = "develop@sha256:66955f34a593cfc3b6e77b8d5510c60c6094f5bade8a17d2feaefbb8662ccf09"; /** * SynapseContainer which freezes the docker digest to stabilise tests, diff --git a/res/css/_components.pcss b/res/css/_components.pcss index a3284fb379..d622f69b84 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -128,7 +128,6 @@ @import "./views/dialogs/_AddExistingToSpaceDialog.pcss"; @import "./views/dialogs/_AnalyticsLearnMoreDialog.pcss"; @import "./views/dialogs/_BugReportDialog.pcss"; -@import "./views/dialogs/_BulkRedactDialog.pcss"; @import "./views/dialogs/_ChangelogDialog.pcss"; @import "./views/dialogs/_CompoundDialog.pcss"; @import "./views/dialogs/_ConfirmSpaceUserActionDialog.pcss"; @@ -153,6 +152,7 @@ @import "./views/dialogs/_ModalWidgetDialog.pcss"; @import "./views/dialogs/_PollCreateDialog.pcss"; @import "./views/dialogs/_RegistrationEmailPromptDialog.pcss"; +@import "./views/dialogs/_ReportRoomDialog.pcss"; @import "./views/dialogs/_RoomSettingsDialog.pcss"; @import "./views/dialogs/_RoomSettingsDialogBridges.pcss"; @import "./views/dialogs/_RoomUpgradeDialog.pcss"; @@ -212,7 +212,6 @@ @import "./views/elements/_ServerPicker.pcss"; @import "./views/elements/_SettingsFlag.pcss"; @import "./views/elements/_Spinner.pcss"; -@import "./views/elements/_StyledCheckbox.pcss"; @import "./views/elements/_StyledRadioButton.pcss"; @import "./views/elements/_SyntaxHighlight.pcss"; @import "./views/elements/_TagComposer.pcss"; @@ -228,6 +227,7 @@ @import "./views/messages/_DisambiguatedProfile.pcss"; @import "./views/messages/_EventTileBubble.pcss"; @import "./views/messages/_HiddenBody.pcss"; +@import "./views/messages/_HiddenMediaPlaceholder.pcss"; @import "./views/messages/_JumpToDatePicker.pcss"; @import "./views/messages/_LegacyCallEvent.pcss"; @import "./views/messages/_MEmoteBody.pcss"; diff --git a/res/css/components/views/settings/devices/_SelectableDeviceTile.pcss b/res/css/components/views/settings/devices/_SelectableDeviceTile.pcss index 927a53937f..6743410f80 100644 --- a/res/css/components/views/settings/devices/_SelectableDeviceTile.pcss +++ b/res/css/components/views/settings/devices/_SelectableDeviceTile.pcss @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024,2025 New Vector Ltd. Copyright 2022 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial @@ -16,9 +16,9 @@ Please see LICENSE files in the repository root for full details. .mx_SelectableDeviceTile_checkbox { flex: 1 0; - .mx_Checkbox_background + div { - flex: 1 0; - /* override more specific selector */ - margin-left: $spacing-16 !important; + > div { + margin-top: auto; + margin-bottom: auto; + margin-right: var(--cpd-space-1x); } } diff --git a/res/css/structures/_QuickSettingsButton.pcss b/res/css/structures/_QuickSettingsButton.pcss index 52aa2377ac..f4b881f955 100644 --- a/res/css/structures/_QuickSettingsButton.pcss +++ b/res/css/structures/_QuickSettingsButton.pcss @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024,2025 New Vector Ltd. Copyright 2021 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial @@ -70,38 +70,26 @@ Please see LICENSE files in the repository root for full details. text-transform: uppercase; color: var(--cpd-color-text-secondary); margin: 20px 0 12px; - } - - .mx_QuickSettingsButton_pinToSidebarHeading { - padding-left: 24px; position: relative; - } - - .mx_Checkbox { - margin-bottom: 8px; - } - - .mx_QuickSettingsButton_favouritesCheckbox, - .mx_QuickSettingsButton_peopleCheckbox { - .mx_Checkbox_background + div { - padding-left: 22px; - position: relative; - margin-left: 6px; - font-size: $font-15px; - line-height: $font-24px; - color: var(--cpd-color-text-primary); - } + display: flex; } .mx_QuickSettingsButton_moreOptionsButton { - padding-left: 22px; - margin-left: 22px; + margin-left: var(--cpd-space-7x); font-size: $font-15px; line-height: $font-24px; color: var(--cpd-color-text-primary); position: relative; margin-bottom: 16px; } + + .mx_QuickSettingsButton_option { + margin-bottom: var(--cpd-space-3x); + label { + /* Correctly line up icons and text. */ + display: flex; + } + } } .mx_QuickSettingsButton_ContextMenuWrapper_new_room_list { @@ -111,15 +99,10 @@ Please see LICENSE files in the repository root for full details. } .mx_QuickSettingsButton_icon { - // TODO remove when all icons have fill=currentColor - * { - fill: $secondary-content; - } + margin-right: var(--cpd-space-1x); color: $secondary-content; - width: 16px; - height: 16px; - position: absolute; - left: 0; - top: 50%; - transform: translateY(-50%); + width: 18px; + height: 18px; + margin-top: auto; + margin-bottom: auto; } diff --git a/res/css/structures/_SpaceHierarchy.pcss b/res/css/structures/_SpaceHierarchy.pcss index 02f39a0b72..c1be2eb142 100644 --- a/res/css/structures/_SpaceHierarchy.pcss +++ b/res/css/structures/_SpaceHierarchy.pcss @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024,2025 New Vector Ltd. Copyright 2021 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial @@ -247,15 +247,6 @@ Please see LICENSE files in the repository root for full details. .mx_AccessibleButton_kind_primary_outline { padding: 3px 16px; /* to account for the 1px border */ } - - .mx_Checkbox { - display: inline-flex; - - label { - width: 16px; - height: 16px; - } - } } &:hover, diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.pcss b/res/css/views/dialogs/_AddExistingToSpaceDialog.pcss index 7dc25d6811..cd5cff94a0 100644 --- a/res/css/views/dialogs/_AddExistingToSpaceDialog.pcss +++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.pcss @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024,2025 New Vector Ltd. Copyright 2021 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial @@ -32,6 +32,11 @@ Please see LICENSE files in the repository root for full details. .mx_AddExistingToSpace_section { margin-right: 12px; + ul { + list-style: none; + padding-left: 0; + } + // provides space for scrollbar so that checkbox and scrollbar do not collide &:not(:first-child) { @@ -214,6 +219,12 @@ Please see LICENSE files in the repository root for full details. display: flex; margin-top: 12px; + form { + /* Align checkboxes. */ + margin-top: auto; + margin-bottom: auto; + } + .mx_DecoratedRoomAvatar, /* we can't target .mx_BaseAvatar here as it'll break the decorated avatar styling */ { margin-right: 12px; } @@ -227,8 +238,4 @@ Please see LICENSE files in the repository root for full details. text-overflow: ellipsis; margin-right: 12px; } - - .mx_Checkbox { - align-items: center; - } } diff --git a/res/css/views/dialogs/_BulkRedactDialog.pcss b/res/css/views/dialogs/_BulkRedactDialog.pcss deleted file mode 100644 index 3e274798eb..0000000000 --- a/res/css/views/dialogs/_BulkRedactDialog.pcss +++ /dev/null @@ -1,19 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2021 Robin Townsend - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -.mx_BulkRedactDialog { - .mx_Checkbox, - .mx_BulkRedactDialog_checkboxMicrocopy { - line-height: $font-20px; - } - - .mx_BulkRedactDialog_checkboxMicrocopy { - margin-left: 26px; - color: $secondary-content; - } -} diff --git a/res/css/views/dialogs/_ExportDialog.pcss b/res/css/views/dialogs/_ExportDialog.pcss index 46c0c578c6..cda2fab55f 100644 --- a/res/css/views/dialogs/_ExportDialog.pcss +++ b/res/css/views/dialogs/_ExportDialog.pcss @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024,2025 New Vector Ltd. Copyright 2021 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial @@ -43,11 +43,6 @@ Please see LICENSE files in the repository root for full details. .mx_Field_valid.mx_Field:focus-within { border-color: $input-border-color; } - - .mx_Checkbox input[type="checkbox"]:checked + label > .mx_Checkbox_background { - background: $info-plinth-fg-color; - border-color: $info-plinth-fg-color; - } } .mx_ExportDialog_progress { diff --git a/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.pcss b/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.pcss index f77ebf7b6b..ed51968dec 100644 --- a/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.pcss +++ b/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.pcss @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024,2025 New Vector Ltd. Copyright 2021 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial @@ -74,10 +74,6 @@ Please see LICENSE files in the repository root for full details. line-height: $font-15px; color: $tertiary-content; } - - .mx_Checkbox { - align-items: center; - } } } diff --git a/res/css/views/dialogs/_ReportRoomDialog.pcss b/res/css/views/dialogs/_ReportRoomDialog.pcss new file mode 100644 index 0000000000..7323ac65c9 --- /dev/null +++ b/res/css/views/dialogs/_ReportRoomDialog.pcss @@ -0,0 +1,16 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +.mx_ReportRoomDialog { + textarea { + font: var(--cpd-font-body-md-regular); + border: 1px solid var(--cpd-color-border-interactive-primary); + background: var(--cpd-color-bg-canvas-default); + border-radius: 0.5rem; + padding: var(--cpd-space-3x) var(--cpd-space-4x); + } +} diff --git a/res/css/views/dialogs/_WidgetCapabilitiesPromptDialog.pcss b/res/css/views/dialogs/_WidgetCapabilitiesPromptDialog.pcss index 9e1024b9a4..62805a24ae 100644 --- a/res/css/views/dialogs/_WidgetCapabilitiesPromptDialog.pcss +++ b/res/css/views/dialogs/_WidgetCapabilitiesPromptDialog.pcss @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024,2025 New Vector Ltd. Copyright 2020 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial @@ -19,13 +19,6 @@ Please see LICENSE files in the repository root for full details. margin-top: 20px; font-size: $font-15px; line-height: $font-15px; - - .mx_WidgetCapabilitiesPromptDialog_byline { - color: $muted-fg-color; - margin-left: 26px; - font-size: $font-12px; - line-height: $font-12px; - } } .mx_Dialog_buttons { diff --git a/res/css/views/elements/_LabelledCheckbox.pcss b/res/css/views/elements/_LabelledCheckbox.pcss index add8f14998..ffbf749dab 100644 --- a/res/css/views/elements/_LabelledCheckbox.pcss +++ b/res/css/views/elements/_LabelledCheckbox.pcss @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024,2025 New Vector Ltd. Copyright 2022 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial @@ -7,26 +7,5 @@ Please see LICENSE files in the repository root for full details. */ .mx_LabelledCheckbox { - display: flex; - gap: 8px; - flex-direction: row; - - .mx_Checkbox { - margin-top: 3px; /* visually align with label text */ - } - - .mx_LabelledCheckbox_labels { - flex: 1; - - .mx_LabelledCheckbox_label { - vertical-align: middle; - } - - .mx_LabelledCheckbox_byline { - display: block; - padding-top: $spacing-4; - color: $muted-fg-color; - font-size: $font-11px; - } - } + margin-top: var(--cpd-space-2x); } diff --git a/res/css/views/elements/_StyledCheckbox.pcss b/res/css/views/elements/_StyledCheckbox.pcss deleted file mode 100644 index 77382d711c..0000000000 --- a/res/css/views/elements/_StyledCheckbox.pcss +++ /dev/null @@ -1,98 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2020 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -.mx_Checkbox { - $size: $font-16px; - $border-radius: 0.27rem; - - display: flex; - align-items: flex-start; - - input[type="checkbox"] { - appearance: none; - margin: 0; - padding: 0; - - & + label { - display: flex; - align-items: center; - - flex-grow: 1; - } - - & + label > .mx_Checkbox_background { - display: inline-flex; - position: relative; - - flex-shrink: 0; - - height: $size; - width: $size; - size: 0.5rem; - border: 1px solid var(--cpd-color-border-interactive-primary); - box-sizing: border-box; - border-radius: $border-radius; - - .mx_Checkbox_checkmark { - display: none; - - height: 100%; - width: 100%; - mask-image: url("@vector-im/compound-design-tokens/icons/check.svg"); - mask-position: center; - mask-size: 100%; - mask-repeat: no-repeat; - } - } - - &:checked + label > .mx_Checkbox_background .mx_Checkbox_checkmark { - display: block; - } - - & + label > *:not(.mx_Checkbox_background) { - margin-left: 10px; - } - - &:disabled + label { - cursor: not-allowed; - } - - &:focus-visible { - & + label .mx_Checkbox_background { - @mixin unreal-focus; - } - } - } -} - -.mx_Checkbox.mx_Checkbox_kind_solid input[type="checkbox"] { - & + label > .mx_Checkbox_background .mx_Checkbox_checkmark { - background: var(--cpd-color-icon-on-solid-primary); - } - - &:checked + label > .mx_Checkbox_background { - background: var(--cpd-color-bg-accent-rest); - border-color: var(--cpd-color-bg-accent-rest); - } - - &:checked:disabled + label > .mx_Checkbox_background { - background: var(--cpd-color-bg-action-primary-disabled); - border-color: var(--cpd-color-bg-action-primary-disabled); - } -} - -.mx_Checkbox.mx_Checkbox_kind_outline input[type="checkbox"] { - & + label > .mx_Checkbox_background .mx_Checkbox_checkmark { - background: var(--cpd-color-bg-accent-rest); - } - - &:checked + label > .mx_Checkbox_background { - background: transparent; - border-color: var(--cpd-color-bg-accent-rest); - } -} diff --git a/res/css/views/messages/_HiddenMediaPlaceholder.pcss b/res/css/views/messages/_HiddenMediaPlaceholder.pcss new file mode 100644 index 0000000000..c7efe6ec7e --- /dev/null +++ b/res/css/views/messages/_HiddenMediaPlaceholder.pcss @@ -0,0 +1,29 @@ +.mx_HiddenMediaPlaceholder { + border: none; + width: 100%; + height: 100%; + inset: 0; + + /* To center the text in the middle of the frame */ + display: flex; + align-items: center; + justify-content: center; + text-align: center; + + cursor: pointer; + background-color: $header-panel-bg-color; + + > div { + color: $accent; + /* Icon alignment */ + display: flex; + > svg { + margin-top: auto; + margin-bottom: auto; + } + } +} + +.mx_EventTile:hover .mx_HiddenMediaPlaceholder { + background-color: $background; +} diff --git a/res/css/views/messages/_MImageBody.pcss b/res/css/views/messages/_MImageBody.pcss index 5942bb44b3..bb5f879568 100644 --- a/res/css/views/messages/_MImageBody.pcss +++ b/res/css/views/messages/_MImageBody.pcss @@ -79,39 +79,3 @@ Please see LICENSE files in the repository root for full details. color: $imagebody-giflabel-color; pointer-events: none; } - -.mx_HiddenImagePlaceholder { - position: absolute; - inset: 0; - - /* To center the text in the middle of the frame */ - display: flex; - align-items: center; - justify-content: center; - text-align: center; - - cursor: pointer; - background-color: $header-panel-bg-color; - - .mx_HiddenImagePlaceholder_button { - color: $accent; - - span.mx_HiddenImagePlaceholder_eye { - margin-right: 8px; - - background-color: $accent; - mask-image: url("$(res)/img/element-icons/eye.svg"); - display: inline-block; - width: 18px; - height: 14px; - } - - span:not(.mx_HiddenImagePlaceholder_eye) { - vertical-align: text-bottom; - } - } -} - -.mx_EventTile:hover .mx_HiddenImagePlaceholder { - background-color: $background; -} diff --git a/res/css/views/right_panel/_RoomSummaryCard.pcss b/res/css/views/right_panel/_RoomSummaryCard.pcss index eb3b14579f..3c1c07677c 100644 --- a/res/css/views/right_panel/_RoomSummaryCard.pcss +++ b/res/css/views/right_panel/_RoomSummaryCard.pcss @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024, 2025 New Vector Ltd. Copyright 2020 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial @@ -101,6 +101,6 @@ Please see LICENSE files in the repository root for full details. margin: $spacing-12 0 $spacing-4; } -.mx_RoomSummaryCard_leave { +.mx_RoomSummaryCard_bottomOptions { margin: 0 0 var(--cpd-space-8x); } diff --git a/res/css/views/rooms/RoomListPanel/_RoomList.pcss b/res/css/views/rooms/RoomListPanel/_RoomList.pcss index 2563c1b675..54798f1ea9 100644 --- a/res/css/views/rooms/RoomListPanel/_RoomList.pcss +++ b/res/css/views/rooms/RoomListPanel/_RoomList.pcss @@ -7,9 +7,4 @@ .mx_RoomList { height: 100%; - - .mx_RoomList_List { - /* Avoid when on hover, the background color to be on top of the right border */ - padding-right: 1px; - } } diff --git a/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss b/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss index e53ba3dc79..1594781dbd 100644 --- a/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss +++ b/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss @@ -16,18 +16,22 @@ */ .mx_RoomListItemView { all: unset; + cursor: pointer; &:hover { background-color: var(--cpd-color-bg-action-secondary-hovered); + + .mx_RoomListItemView_content { + padding-right: var(--cpd-space-1-5x); + } } .mx_RoomListItemView_container { - padding-left: var(--cpd-space-3x); + padding-left: var(--cpd-space-2x); font: var(--cpd-font-body-md-regular); height: 100%; .mx_RoomListItemView_content { - padding-right: var(--cpd-space-3x); height: 100%; flex: 1; /* The border is only under the room name and the future hover menu */ @@ -46,4 +50,24 @@ .mx_RoomListItemView_menu_open { background-color: var(--cpd-color-bg-action-secondary-hovered); + + .mx_RoomListItemView_content { + padding-right: var(--cpd-space-1-5x); + } +} + +.mx_RoomListItemView_selected { + background-color: var(--cpd-color-bg-action-secondary-pressed); +} + +.mx_RoomListItemView_notification_decoration { + .mx_RoomListItemView_content { + padding-right: var(--cpd-space-2x); + } +} + +.mx_RoomListItemView_empty { + .mx_RoomListItemView_content { + padding-right: var(--cpd-space-3x); + } } diff --git a/res/css/views/rooms/_EventTile.pcss b/res/css/views/rooms/_EventTile.pcss index 54f91bd5f4..b4172fc003 100644 --- a/res/css/views/rooms/_EventTile.pcss +++ b/res/css/views/rooms/_EventTile.pcss @@ -816,11 +816,13 @@ $left-gutter: 64px; .mx_EventTile_spoiler_content { filter: blur(5px) saturate(0.1) sepia(1); transition-duration: 0.5s; + pointer-events: none; } &.visible > .mx_EventTile_spoiler_content { filter: none; user-select: auto; + pointer-events: auto; } } diff --git a/res/css/views/rooms/_RoomSublist.pcss b/res/css/views/rooms/_RoomSublist.pcss index f5d0e4ed91..7b0da5608e 100644 --- a/res/css/views/rooms/_RoomSublist.pcss +++ b/res/css/views/rooms/_RoomSublist.pcss @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024,2025 New Vector Ltd. Copyright 2020 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial @@ -393,8 +393,7 @@ Please see LICENSE files in the repository root for full details. margin-bottom: 4px; } - .mx_StyledRadioButton, - .mx_Checkbox { + .mx_StyledRadioButton { margin-top: 8px; } } diff --git a/res/css/views/settings/tabs/user/_SidebarUserSettingsTab.pcss b/res/css/views/settings/tabs/user/_SidebarUserSettingsTab.pcss index cb360cc898..a8d45f9f77 100644 --- a/res/css/views/settings/tabs/user/_SidebarUserSettingsTab.pcss +++ b/res/css/views/settings/tabs/user/_SidebarUserSettingsTab.pcss @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024,2025 New Vector Ltd. Copyright 2021 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial @@ -14,17 +14,12 @@ Please see LICENSE files in the repository root for full details. } } -.mx_SidebarUserSettingsTab_checkbox { - margin-bottom: $spacing-8; - /* override checkbox styles */ - label { - align-items: flex-start !important; - } - - svg { - height: 16px; - width: 16px; - margin-right: $spacing-8; - margin-bottom: -1px; - } +.mx_SidebarUserSettingsTab_icon { + margin-right: var(--cpd-space-2x); + margin-top: auto; + margin-bottom: auto; +} + +.mx_SidebarUserSettingsTab_checkbox label { + display: flex; } diff --git a/scripts/fetchdep.sh b/scripts/fetchdep.sh index 1c34beaf42..1299815bbb 100755 --- a/scripts/fetchdep.sh +++ b/scripts/fetchdep.sh @@ -45,10 +45,7 @@ getPRInfo() { # Some CIs don't give us enough info, so we just get the PR number and ask the # GH API for more info - "fork:branch". Some give us this directly. -if [ -n "$BUILDKITE_BRANCH" ]; then - # BuildKite - head=$BUILDKITE_BRANCH -elif [ -n "$PR_NUMBER" ]; then +if [ -n "$PR_NUMBER" ]; then # GitHub getPRInfo $PR_NUMBER elif [ -n "$REVIEW_ID" ]; then @@ -79,11 +76,14 @@ if [[ "$GITHUB_EVENT_NAME" == "merge_group" ]]; then clone $deforg $defrepo ${withoutPrefix%%/pr-*} fi -# Try the target branch of the push or PR. -if [ -n "$GITHUB_BASE_REF" ]; then - clone $deforg $defrepo $GITHUB_BASE_REF -elif [ -n "$BUILDKITE_PULL_REQUEST_BASE_BRANCH" ]; then - clone $deforg $defrepo $BUILDKITE_PULL_REQUEST_BASE_BRANCH +# Try the target branch of the push or PR, or the branch that was pushed to +# (ie. the 'master' branch should use matching 'master' dependencies) +base_or_branch=$GITHUB_BASE_REF +if [[ "$GITHUB_EVENT_NAME" == "push" ]]; then + base_or_branch=${GITHUB_REF} +fi +if [ -n "$base_or_branch" ]; then + clone $deforg $defrepo $base_or_branch fi # Try HEAD which is the branch name in Netlify (not BRANCH which is pull/xxxx/head for PR builds) diff --git a/src/@types/common.ts b/src/@types/common.ts index 85e0b95043..4c8c707c4a 100644 --- a/src/@types/common.ts +++ b/src/@types/common.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { type JSXElementConstructor } from "react"; +import { type JSX, type JSXElementConstructor } from "react"; export type { NonEmptyArray, XOR, Writeable } from "matrix-js-sdk/src/matrix"; diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts index 2093881cc5..c7b7825fe6 100644 --- a/src/BasePlatform.ts +++ b/src/BasePlatform.ts @@ -16,6 +16,7 @@ import { type SSOAction, encodeUnpaddedBase64, type OidcRegistrationClientMetadata, + MatrixEventEvent, } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; @@ -228,6 +229,16 @@ export default abstract class BasePlatform { window.focus(); }; + const closeHandler = (): void => notification.close(); + + // Clear a notification from a redacted event. + if (ev) { + ev.once(MatrixEventEvent.BeforeRedaction, closeHandler); + notification.onclose = () => { + ev.off(MatrixEventEvent.BeforeRedaction, closeHandler); + }; + } + return notification; } diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 0a6243a12d..5bc20095f3 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type LegacyRef, type ReactNode } from "react"; +import React, { type JSX, type LegacyRef, type ReactNode } from "react"; import sanitizeHtml, { type IOptions } from "sanitize-html"; import classNames from "classnames"; import katex from "katex"; @@ -25,7 +25,7 @@ import { PERMITTED_URL_SCHEMES } from "./utils/UrlUtils"; import { sanitizeHtmlParams, transformTags } from "./Linkify"; import { graphemeSegmenter } from "./utils/strings"; -export { Linkify, linkifyElement, linkifyAndSanitizeHtml } from "./Linkify"; +export { Linkify, linkifyAndSanitizeHtml } from "./Linkify"; // Anything outside the basic multilingual plane will be a surrogate pair const SURROGATE_PAIR_PATTERN = /([\ud800-\udbff])([\udc00-\udfff])/; @@ -365,53 +365,6 @@ function analyseEvent(content: IContent, highlights: Optional, opts: E } } -export function bodyToDiv( - content: IContent, - highlights: Optional, - opts: EventRenderOpts = {}, - ref?: React.Ref, -): ReactNode { - const { strippedBody, formattedBody, emojiBodyElements, className } = bodyToNode(content, highlights, opts); - - return formattedBody ? ( -

- ) : ( -
- {emojiBodyElements || strippedBody} -
- ); -} - -export function bodyToSpan( - content: IContent, - highlights: Optional, - opts: EventRenderOpts = {}, - ref?: React.Ref, - includeDir = true, -): ReactNode { - const { strippedBody, formattedBody, emojiBodyElements, className } = bodyToNode(content, highlights, opts); - - return formattedBody ? ( - - ) : ( - - {emojiBodyElements || strippedBody} - - ); -} - interface BodyToNodeReturn { strippedBody: string; formattedBody?: string; @@ -419,7 +372,11 @@ interface BodyToNodeReturn { className: string; } -function bodyToNode(content: IContent, highlights: Optional, opts: EventRenderOpts = {}): BodyToNodeReturn { +export function bodyToNode( + content: IContent, + highlights: Optional, + opts: EventRenderOpts = {}, +): BodyToNodeReturn { const eventInfo = analyseEvent(content, highlights, opts); let emojiBody = false; diff --git a/src/IConfigOptions.ts b/src/IConfigOptions.ts index 67b7515eac..8b88a18075 100644 --- a/src/IConfigOptions.ts +++ b/src/IConfigOptions.ts @@ -117,7 +117,6 @@ export interface IConfigOptions { obey_asserted_identity?: boolean; // MSC3086 }; element_call: { - url?: string; guest_spa_url?: string; use_exclusively?: boolean; participant_limit?: number; diff --git a/src/LegacyCallHandler.tsx b/src/LegacyCallHandler.tsx index 4361d3d027..608d396dda 100644 --- a/src/LegacyCallHandler.tsx +++ b/src/LegacyCallHandler.tsx @@ -22,7 +22,6 @@ import { type MatrixCall, } from "matrix-js-sdk/src/webrtc/call"; import { logger } from "matrix-js-sdk/src/logger"; -import { PushProcessor } from "matrix-js-sdk/src/pushprocessor"; import { CallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/callEventHandler"; import { MatrixClientPeg } from "./MatrixClientPeg"; @@ -596,7 +595,7 @@ export default class LegacyCallHandler extends TypedEventEmitter if (enableGuest && guestHsUrl && fragmentQueryParams.guest_user_id && fragmentQueryParams.guest_access_token) { logger.log("Using guest access credentials"); - return doSetLoggedIn( + await doSetLoggedIn( { userId: fragmentQueryParams.guest_user_id as string, accessToken: fragmentQueryParams.guest_access_token as string, @@ -206,7 +207,8 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise }, true, false, - ).then(() => true); + ); + return true; } const success = await restoreSessionFromStorage({ ignoreGuest: Boolean(opts.ignoreGuest), @@ -225,6 +227,11 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise // fall back to welcome screen return false; } catch (e) { + // We may be aborted e.g. because our token expired, so don't show an error here + if (opts.abortSignal?.aborted) { + return false; + } + if (e instanceof AbortLoginAndRebuildStorage) { // If we're aborting login because of a storage inconsistency, we don't // need to show the general failure dialog. Instead, just go back to welcome. @@ -236,7 +243,7 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise return false; } - return handleLoadSessionFailure(e); + return handleLoadSessionFailure(e, opts); } } @@ -406,6 +413,39 @@ export function attemptTokenLogin( }); } +/** + * Load the pickle key inside the credentials or create it if it does not exist for this device. + * + * @param credentials Holds the device to load/store the pickle key + * + * @returns {Promise} promise which resolves to the loaded or generated pickle key or undefined if + * none was loaded nor generated + */ +async function loadOrCreatePickleKey(credentials: IMatrixClientCreds): Promise { + // Try to load the pickle key + const userId = credentials.userId; + const deviceId = credentials.deviceId; + let pickleKey = (await PlatformPeg.get()?.getPickleKey(userId, deviceId ?? "")) ?? undefined; + if (!pickleKey) { + // Create it if it did not exist + pickleKey = + userId && deviceId + ? ((await PlatformPeg.get()?.createPickleKey(userId, deviceId)) ?? undefined) + : undefined; + if (pickleKey) { + logger.log(`Created pickle key for ${credentials.userId}|${credentials.deviceId}`); + } else { + logger.log("Pickle key not created"); + } + } else { + logger.log( + `Pickle key already exists for ${credentials.userId}|${credentials.deviceId} do not create a new one`, + ); + } + + return pickleKey; +} + /** * Called after a successful token login or OIDC authorization. * Clear storage then save new credentials in storage @@ -413,6 +453,8 @@ export function attemptTokenLogin( */ async function onSuccessfulDelegatedAuthLogin(credentials: IMatrixClientCreds): Promise { await clearStorage(); + // SSO does not go through setLoggedIn so we need to load/create the pickle key here too + credentials.pickleKey = await loadOrCreatePickleKey(credentials); await persistCredentials(credentials); // remember that we just logged in @@ -621,7 +663,7 @@ export async function restoreSessionFromStorage(opts?: { ignoreGuest?: boolean } } } -async function handleLoadSessionFailure(e: unknown): Promise { +async function handleLoadSessionFailure(e: unknown, loadSessionOpts?: ILoadSessionOpts): Promise { logger.error("Unable to load session", e); const modal = Modal.createDialog(SessionRestoreErrorDialog, { @@ -636,7 +678,7 @@ async function handleLoadSessionFailure(e: unknown): Promise { } // try, try again - return loadSession(); + return loadSession(loadSessionOpts); } /** @@ -655,18 +697,8 @@ async function handleLoadSessionFailure(e: unknown): Promise { export async function setLoggedIn(credentials: IMatrixClientCreds): Promise { credentials.freshLogin = true; stopMatrixClient(); - const pickleKey = - credentials.userId && credentials.deviceId - ? await PlatformPeg.get()?.createPickleKey(credentials.userId, credentials.deviceId) - : null; - - if (pickleKey) { - logger.log(`Created pickle key for ${credentials.userId}|${credentials.deviceId}`); - } else { - logger.log("Pickle key not created"); - } - - return doSetLoggedIn({ ...credentials, pickleKey: pickleKey ?? undefined }, true, true); + credentials.pickleKey = await loadOrCreatePickleKey(credentials); + return doSetLoggedIn(credentials, true, true); } /** @@ -1124,12 +1156,13 @@ window.mxLoginWithAccessToken = async (hsUrl: string, accessToken: string): Prom baseUrl: hsUrl, accessToken, }); - const { user_id: userId } = await tempClient.whoami(); + const { user_id: userId, device_id: deviceId } = await tempClient.whoami(); await doSetLoggedIn( { homeserverUrl: hsUrl, accessToken, userId, + deviceId, }, true, false, diff --git a/src/Linkify.tsx b/src/Linkify.tsx index a51b312d48..27dd4783be 100644 --- a/src/Linkify.tsx +++ b/src/Linkify.tsx @@ -11,12 +11,7 @@ import sanitizeHtml, { type IOptions } from "sanitize-html"; import { merge } from "lodash"; import _Linkify from "linkify-react"; -import { - _linkifyElement, - _linkifyString, - ELEMENT_URL_PATTERN, - options as linkifyMatrixOptions, -} from "./linkify-matrix"; +import { _linkifyString, ELEMENT_URL_PATTERN, options as linkifyMatrixOptions } from "./linkify-matrix"; import SettingsStore from "./settings/SettingsStore"; import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks"; import { mediaFromMxc } from "./customisations/Media"; @@ -223,17 +218,6 @@ export function linkifyString(str: string, options = linkifyMatrixOptions): stri return _linkifyString(str, options); } -/** - * Linkifies the given DOM element. This is a wrapper around 'linkifyjs/element'. - * - * @param {object} element DOM element to linkify - * @param {object} [options] Options for linkifyElement. Default: linkifyMatrixOptions - * @returns {object} - */ -export function linkifyElement(element: HTMLElement, options = linkifyMatrixOptions): HTMLElement { - return _linkifyElement(element, options); -} - /** * Linkify the given string and sanitize the HTML afterwards. * diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 9682b41800..288878cddc 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -299,6 +299,12 @@ class MatrixClientPegClass implements IMatrixClientPeg { opts.threadSupport = true; if (SettingsStore.getValue("feature_sliding_sync")) { + throw new UserFriendlyError("sliding_sync_legacy_no_longer_supported"); + } + + // If the user has enabled the labs feature for sliding sync, set it up + // otherwise check if the feature is supported + if (SettingsStore.getValue("feature_simplified_sliding_sync")) { opts.slidingSync = await SlidingSyncManager.instance.setup(this.matrixClient); } else { SlidingSyncManager.instance.checkSupport(this.matrixClient); diff --git a/src/NodeAnimator.tsx b/src/NodeAnimator.tsx index 6ce0cf177c..d350de8dd8 100644 --- a/src/NodeAnimator.tsx +++ b/src/NodeAnimator.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type Key, type MutableRefObject, type ReactElement, type RefCallback } from "react"; +import React, { type Key, type RefObject, type ReactElement, type RefCallback } from "react"; interface IChildProps { style: React.CSSProperties; @@ -20,7 +20,7 @@ interface IProps { // a list of state objects to apply to each child node in turn startStyles: React.CSSProperties[]; - innerRef?: MutableRefObject; + innerRef?: RefObject; } function isReactElement(c: ReturnType<(typeof React.Children)["toArray"]>[number]): c is ReactElement { diff --git a/src/RoomNotifs.ts b/src/RoomNotifs.ts index e6064d2691..d5254d523d 100644 --- a/src/RoomNotifs.ts +++ b/src/RoomNotifs.ts @@ -238,25 +238,25 @@ export function determineUnreadState( room?: Room, threadId?: string, includeThreads?: boolean, -): { level: NotificationLevel; symbol: string | null; count: number } { +): { level: NotificationLevel; symbol: string | null; count: number; invited: boolean } { if (!room) { - return { symbol: null, count: 0, level: NotificationLevel.None }; + return { symbol: null, count: 0, level: NotificationLevel.None, invited: false }; } if (getUnsentMessages(room, threadId).length > 0) { - return { symbol: "!", count: 1, level: NotificationLevel.Unsent }; + return { symbol: "!", count: 1, level: NotificationLevel.Unsent, invited: false }; } if (getEffectiveMembership(room.getMyMembership()) === EffectiveMembership.Invite) { - return { symbol: "!", count: 1, level: NotificationLevel.Highlight }; + return { symbol: "!", count: 1, level: NotificationLevel.Highlight, invited: true }; } if (SettingsStore.getValue("feature_ask_to_join") && isKnockDenied(room)) { - return { symbol: "!", count: 1, level: NotificationLevel.Highlight }; + return { symbol: "!", count: 1, level: NotificationLevel.Highlight, invited: false }; } if (getRoomNotifsState(room.client, room.roomId) === RoomNotifState.Mute) { - return { symbol: null, count: 0, level: NotificationLevel.None }; + return { symbol: null, count: 0, level: NotificationLevel.None, invited: false }; } const redNotifs = getUnreadNotificationCount( @@ -269,12 +269,12 @@ export function determineUnreadState( const trueCount = greyNotifs || redNotifs; if (redNotifs > 0) { - return { symbol: null, count: trueCount, level: NotificationLevel.Highlight }; + return { symbol: null, count: trueCount, level: NotificationLevel.Highlight, invited: false }; } const markedUnreadState = getMarkedUnreadState(room); if (greyNotifs > 0 || markedUnreadState) { - return { symbol: null, count: trueCount, level: NotificationLevel.Notification }; + return { symbol: null, count: trueCount, level: NotificationLevel.Notification, invited: false }; } // We don't have any notified messages, but we might have unread messages. Let's find out. @@ -293,5 +293,6 @@ export function determineUnreadState( symbol: null, count: trueCount, level: hasUnread ? NotificationLevel.Activity : NotificationLevel.None, + invited: false, }; } diff --git a/src/SdkConfig.ts b/src/SdkConfig.ts index a41b890b19..9576aa209e 100644 --- a/src/SdkConfig.ts +++ b/src/SdkConfig.ts @@ -30,7 +30,6 @@ export const DEFAULTS: DeepReadonly = { preferred_domain: "meet.element.io", }, element_call: { - url: "https://call.element.io", use_exclusively: false, participant_limit: 8, brand: "Element Call", diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 4a4a80f723..865da9e51c 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import * as React from "react"; +import React from "react"; import { ContentHelpers, Direction, diff --git a/src/SlidingSyncManager.ts b/src/SlidingSyncManager.ts index adfca7c3c3..88b839312d 100644 --- a/src/SlidingSyncManager.ts +++ b/src/SlidingSyncManager.ts @@ -36,45 +36,51 @@ Please see LICENSE files in the repository root for full details. * list ops) */ -import { type MatrixClient, EventType, AutoDiscovery, Method, timeoutSignal } from "matrix-js-sdk/src/matrix"; +import { type MatrixClient, ClientEvent, EventType, type Room } from "matrix-js-sdk/src/matrix"; import { type MSC3575Filter, type MSC3575List, + type MSC3575SlidingSyncResponse, MSC3575_STATE_KEY_LAZY, MSC3575_STATE_KEY_ME, MSC3575_WILDCARD, SlidingSync, + SlidingSyncEvent, + SlidingSyncState, } from "matrix-js-sdk/src/sliding-sync"; import { logger } from "matrix-js-sdk/src/logger"; import { defer, sleep } from "matrix-js-sdk/src/utils"; -import SettingsStore from "./settings/SettingsStore"; -import SlidingSyncController from "./settings/controllers/SlidingSyncController"; - // how long to long poll for const SLIDING_SYNC_TIMEOUT_MS = 20 * 1000; +// The state events we will get for every single room/space/old room/etc +// This list is only augmented when a direct room subscription is made. (e.g you view a room) +const REQUIRED_STATE_LIST = [ + [EventType.RoomJoinRules, ""], // the public icon on the room list + [EventType.RoomAvatar, ""], // any room avatar + [EventType.RoomCanonicalAlias, ""], // for room name calculations + [EventType.RoomTombstone, ""], // lets JS SDK hide rooms which are dead + [EventType.RoomEncryption, ""], // lets rooms be configured for E2EE correctly + [EventType.RoomCreate, ""], // for isSpaceRoom checks + [EventType.SpaceChild, MSC3575_WILDCARD], // all space children + [EventType.SpaceParent, MSC3575_WILDCARD], // all space parents + [EventType.RoomMember, MSC3575_STATE_KEY_ME], // lets the client calculate that we are in fact in the room +]; + // the things to fetch when a user clicks on a room const DEFAULT_ROOM_SUBSCRIPTION_INFO = { timeline_limit: 50, // missing required_state which will change depending on the kind of room include_old_rooms: { timeline_limit: 0, - required_state: [ - // state needed to handle space navigation and tombstone chains - [EventType.RoomCreate, ""], - [EventType.RoomTombstone, ""], - [EventType.SpaceChild, MSC3575_WILDCARD], - [EventType.SpaceParent, MSC3575_WILDCARD], - [EventType.RoomMember, MSC3575_STATE_KEY_ME], - ], + required_state: REQUIRED_STATE_LIST, }, }; // lazy load room members so rooms like Matrix HQ don't take forever to load const UNENCRYPTED_SUBSCRIPTION_NAME = "unencrypted"; const UNENCRYPTED_SUBSCRIPTION = { required_state: [ - [MSC3575_WILDCARD, MSC3575_WILDCARD], // all events [EventType.RoomMember, MSC3575_STATE_KEY_ME], // except for m.room.members, get our own membership [EventType.RoomMember, MSC3575_STATE_KEY_LAZY], // ...and lazy load the rest. ], @@ -90,6 +96,72 @@ const ENCRYPTED_SUBSCRIPTION = { ...DEFAULT_ROOM_SUBSCRIPTION_INFO, }; +// the complete set of lists made in SSS. The manager will spider all of these lists depending +// on the count for each one. +const sssLists: Record = { + spaces: { + ranges: [[0, 10]], + timeline_limit: 0, // we don't care about the most recent message for spaces + required_state: REQUIRED_STATE_LIST, + include_old_rooms: { + timeline_limit: 0, + required_state: REQUIRED_STATE_LIST, + }, + filters: { + room_types: ["m.space"], + }, + }, + invites: { + ranges: [[0, 10]], + timeline_limit: 1, // most recent message display + required_state: REQUIRED_STATE_LIST, + include_old_rooms: { + timeline_limit: 0, + required_state: REQUIRED_STATE_LIST, + }, + filters: { + is_invite: true, + }, + }, + favourites: { + ranges: [[0, 10]], + timeline_limit: 1, // most recent message display + required_state: REQUIRED_STATE_LIST, + include_old_rooms: { + timeline_limit: 0, + required_state: REQUIRED_STATE_LIST, + }, + filters: { + tags: ["m.favourite"], + }, + }, + dms: { + ranges: [[0, 10]], + timeline_limit: 1, // most recent message display + required_state: REQUIRED_STATE_LIST, + include_old_rooms: { + timeline_limit: 0, + required_state: REQUIRED_STATE_LIST, + }, + filters: { + is_dm: true, + is_invite: false, + // If a DM has a Favourite & Low Prio tag then it'll be shown in those lists instead + not_tags: ["m.favourite", "m.lowpriority"], + }, + }, + untagged: { + // SSS will dupe suppress invites/dms from here, so we don't need "not dms, not invites" + ranges: [[0, 10]], + timeline_limit: 1, // most recent message display + required_state: REQUIRED_STATE_LIST, + include_old_rooms: { + timeline_limit: 0, + required_state: REQUIRED_STATE_LIST, + }, + }, +}; + export type PartialSlidingSyncRequest = { filters?: MSC3575Filter; sort?: string[]; @@ -103,6 +175,8 @@ export type PartialSlidingSyncRequest = { * sync options and code. */ export class SlidingSyncManager { + public static serverSupportsSlidingSync: boolean; + public static readonly ListSpaces = "space_list"; public static readonly ListSearch = "search_list"; private static readonly internalInstance = new SlidingSyncManager(); @@ -116,48 +190,17 @@ export class SlidingSyncManager { return SlidingSyncManager.internalInstance; } - public configure(client: MatrixClient, proxyUrl: string): SlidingSync { + private configure(client: MatrixClient, proxyUrl: string): SlidingSync { this.client = client; + // create the set of lists we will use. + const lists = new Map(); + for (const listName in sssLists) { + lists.set(listName, sssLists[listName]); + } // by default use the encrypted subscription as that gets everything, which is a safer // default than potentially missing member events. - this.slidingSync = new SlidingSync( - proxyUrl, - new Map(), - ENCRYPTED_SUBSCRIPTION, - client, - SLIDING_SYNC_TIMEOUT_MS, - ); + this.slidingSync = new SlidingSync(proxyUrl, lists, ENCRYPTED_SUBSCRIPTION, client, SLIDING_SYNC_TIMEOUT_MS); this.slidingSync.addCustomSubscription(UNENCRYPTED_SUBSCRIPTION_NAME, UNENCRYPTED_SUBSCRIPTION); - // set the space list - this.slidingSync.setList(SlidingSyncManager.ListSpaces, { - ranges: [[0, 20]], - sort: ["by_name"], - slow_get_all_rooms: true, - timeline_limit: 0, - required_state: [ - [EventType.RoomJoinRules, ""], // the public icon on the room list - [EventType.RoomAvatar, ""], // any room avatar - [EventType.RoomTombstone, ""], // lets JS SDK hide rooms which are dead - [EventType.RoomEncryption, ""], // lets rooms be configured for E2EE correctly - [EventType.RoomCreate, ""], // for isSpaceRoom checks - [EventType.SpaceChild, MSC3575_WILDCARD], // all space children - [EventType.SpaceParent, MSC3575_WILDCARD], // all space parents - [EventType.RoomMember, MSC3575_STATE_KEY_ME], // lets the client calculate that we are in fact in the room - ], - include_old_rooms: { - timeline_limit: 0, - required_state: [ - [EventType.RoomCreate, ""], - [EventType.RoomTombstone, ""], // lets JS SDK hide rooms which are dead - [EventType.SpaceChild, MSC3575_WILDCARD], // all space children - [EventType.SpaceParent, MSC3575_WILDCARD], // all space parents - [EventType.RoomMember, MSC3575_STATE_KEY_ME], // lets the client calculate that we are in fact in the room - ], - }, - filters: { - room_types: ["m.space"], - }, - }); this.configureDefer.resolve(); return this.slidingSync; } @@ -220,99 +263,113 @@ export class SlidingSyncManager { return this.slidingSync!.getListParams(listKey)!; } - public async setRoomVisible(roomId: string, visible: boolean): Promise { + /** + * Announces that the user has chosen to view the given room and that room will now + * be displayed, so it should have more state loaded. + * @param roomId The room to set visible + */ + public async setRoomVisible(roomId: string): Promise { await this.configureDefer.promise; const subscriptions = this.slidingSync!.getRoomSubscriptions(); - if (visible) { - subscriptions.add(roomId); - } else { - subscriptions.delete(roomId); - } + if (subscriptions.has(roomId)) return; + + subscriptions.add(roomId); + const room = this.client?.getRoom(roomId); - let shouldLazyLoad = !(await this.client?.getCrypto()?.isEncryptionEnabledInRoom(roomId)); - if (!room) { - // default to safety: request all state if we can't work it out. This can happen if you - // refresh the app whilst viewing a room: we call setRoomVisible before we know anything - // about the room. - shouldLazyLoad = false; + // default to safety: request all state if we can't work it out. This can happen if you + // refresh the app whilst viewing a room: we call setRoomVisible before we know anything + // about the room. + let shouldLazyLoad = false; + if (room) { + // do not lazy load encrypted rooms as we need the entire member list. + shouldLazyLoad = !(await this.client?.getCrypto()?.isEncryptionEnabledInRoom(roomId)); } - logger.log("SlidingSync setRoomVisible:", roomId, visible, "shouldLazyLoad:", shouldLazyLoad); + logger.log("SlidingSync setRoomVisible:", roomId, "shouldLazyLoad:", shouldLazyLoad); if (shouldLazyLoad) { // lazy load this room this.slidingSync!.useCustomSubscription(roomId, UNENCRYPTED_SUBSCRIPTION_NAME); } - const p = this.slidingSync!.modifyRoomSubscriptions(subscriptions); + this.slidingSync!.modifyRoomSubscriptions(subscriptions); if (room) { - return roomId; // we have data already for this room, show immediately e.g it's in a list + return; // we have data already for this room, show immediately e.g it's in a list } - try { - // wait until the next sync before returning as RoomView may need to know the current state - await p; - } catch { - logger.warn("SlidingSync setRoomVisible:", roomId, visible, "failed to confirm transaction"); - } - return roomId; + // wait until we know about this room. This may take a little while. + return new Promise((resolve) => { + logger.log(`SlidingSync setRoomVisible room ${roomId} not found, waiting for ClientEvent.Room`); + const waitForRoom = (r: Room): void => { + if (r.roomId === roomId) { + this.client?.off(ClientEvent.Room, waitForRoom); + logger.log(`SlidingSync room ${roomId} found, resolving setRoomVisible`); + resolve(); + } + }; + this.client?.on(ClientEvent.Room, waitForRoom); + }); } /** - * Retrieve all rooms on the user's account. Used for pre-populating the local search cache. - * Retrieval is gradual over time. + * Retrieve all rooms on the user's account. Retrieval is gradual over time. + * This function MUST be called BEFORE the first sync request goes out. * @param batchSize The number of rooms to return in each request. * @param gapBetweenRequestsMs The number of milliseconds to wait between requests. */ - public async startSpidering(batchSize: number, gapBetweenRequestsMs: number): Promise { - await sleep(gapBetweenRequestsMs); // wait a bit as this is called on first render so let's let things load - let startIndex = batchSize; - let hasMore = true; - let firstTime = true; - while (hasMore) { - const endIndex = startIndex + batchSize - 1; - try { - const ranges = [ - [0, batchSize - 1], - [startIndex, endIndex], - ]; - if (firstTime) { - await this.slidingSync!.setList(SlidingSyncManager.ListSearch, { - // e.g [0,19] [20,39] then [0,19] [40,59]. We keep [0,20] constantly to ensure - // any changes to the list whilst spidering are caught. - ranges: ranges, - sort: [ - "by_recency", // this list isn't shown on the UI so just sorting by timestamp is enough - ], - timeline_limit: 0, // we only care about the room details, not messages in the room - required_state: [ - [EventType.RoomJoinRules, ""], // the public icon on the room list - [EventType.RoomAvatar, ""], // any room avatar - [EventType.RoomTombstone, ""], // lets JS SDK hide rooms which are dead - [EventType.RoomEncryption, ""], // lets rooms be configured for E2EE correctly - [EventType.RoomCreate, ""], // for isSpaceRoom checks - [EventType.RoomMember, MSC3575_STATE_KEY_ME], // lets the client calculate that we are in fact in the room - ], - // we don't include_old_rooms here in an effort to reduce the impact of spidering all rooms - // on the user's account. This means some data in the search dialog results may be inaccurate - // e.g membership of space, but this will be corrected when the user clicks on the room - // as the direct room subscription does include old room iterations. - filters: { - // we get spaces via a different list, so filter them out - not_room_types: ["m.space"], - }, - }); - } else { - await this.slidingSync!.setListRanges(SlidingSyncManager.ListSearch, ranges); - } - } catch { - // do nothing, as we reject only when we get interrupted but that's fine as the next - // request will include our data - } finally { - // gradually request more over time, even on errors. - await sleep(gapBetweenRequestsMs); + private async startSpidering( + slidingSync: SlidingSync, + batchSize: number, + gapBetweenRequestsMs: number, + ): Promise { + // The manager has created several lists (see `sssLists` in this file), all of which will be spidered simultaneously. + // There are multiple lists to ensure that we can populate invites/favourites/DMs sections immediately, rather than + // potentially waiting minutes if they are all very old rooms (and hence are returned last by the server). In this + // way, the lists are effectively priority requests. We don't actually care which room goes into which list at this + // point, as the RoomListStore will calculate this based on the returned data. + + // copy the initial set of list names and ranges, we'll keep this map updated. + const listToUpperBound = new Map( + Object.keys(sssLists).map((listName) => { + return [listName, sssLists[listName].ranges[0][1]]; + }), + ); + console.log("startSpidering:", listToUpperBound); + + // listen for a response from the server. ANY 200 OK will do here, as we assume that it is ACKing + // the request change we have sent out. TODO: this may not be true if you concurrently subscribe to a room :/ + // but in that case, for spidering at least, it isn't the end of the world as request N+1 includes all indexes + // from request N. + const lifecycle = async ( + state: SlidingSyncState, + _: MSC3575SlidingSyncResponse | null, + err?: Error, + ): Promise => { + if (state !== SlidingSyncState.Complete) { + return; } - const listData = this.slidingSync!.getListData(SlidingSyncManager.ListSearch)!; - hasMore = endIndex + 1 < listData.joinedCount; - startIndex += batchSize; - firstTime = false; - } + await sleep(gapBetweenRequestsMs); // don't tightloop; even on errors + if (err) { + return; + } + + // for all lists with total counts > range => increase the range + let hasSetRanges = false; + listToUpperBound.forEach((currentUpperBound, listName) => { + const totalCount = slidingSync.getListData(listName)?.joinedCount || 0; + if (currentUpperBound < totalCount) { + // increment the upper bound + const newUpperBound = currentUpperBound + batchSize; + console.log(`startSpidering: ${listName} ${currentUpperBound} => ${newUpperBound}`); + listToUpperBound.set(listName, newUpperBound); + // make the next request. This will only send the request when this callback has finished, so if + // we set all the list ranges at once we will only send 1 new request. + slidingSync.setListRanges(listName, [[0, newUpperBound]]); + hasSetRanges = true; + } + }); + if (!hasSetRanges) { + // finish spidering + slidingSync.off(SlidingSyncEvent.Lifecycle, lifecycle); + } + }; + slidingSync.on(SlidingSyncEvent.Lifecycle, lifecycle); } /** @@ -325,42 +382,10 @@ export class SlidingSyncManager { * @returns A working Sliding Sync or undefined */ public async setup(client: MatrixClient): Promise { - const baseUrl = client.baseUrl; - const proxyUrl = SettingsStore.getValue("feature_sliding_sync_proxy_url"); - const wellKnownProxyUrl = await this.getProxyFromWellKnown(client); - - const slidingSyncEndpoint = proxyUrl || wellKnownProxyUrl || baseUrl; - - this.configure(client, slidingSyncEndpoint); - logger.info("Sliding sync activated at", slidingSyncEndpoint); - this.startSpidering(100, 50); // 100 rooms at a time, 50ms apart - - return this.slidingSync; - } - - /** - * Get the sliding sync proxy URL from the client well known - * @param client The MatrixClient to use - * @return The proxy url - */ - public async getProxyFromWellKnown(client: MatrixClient): Promise { - let proxyUrl: string | undefined; - - try { - const clientDomain = await client.getDomain(); - if (clientDomain === null) { - throw new RangeError("Homeserver domain is null"); - } - const clientWellKnown = await AutoDiscovery.findClientConfig(clientDomain); - proxyUrl = clientWellKnown?.["org.matrix.msc3575.proxy"]?.url; - } catch { - // Either client.getDomain() is null so we've shorted out, or is invalid so `AutoDiscovery.findClientConfig` has thrown - } - - if (proxyUrl != undefined) { - logger.log("getProxyFromWellKnown: client well-known declares sliding sync proxy at", proxyUrl); - } - return proxyUrl; + const slidingSync = this.configure(client, client.baseUrl); + logger.info("Simplified Sliding Sync activated at", client.baseUrl); + this.startSpidering(slidingSync, 50, 50); // 50 rooms at a time, 50ms apart + return slidingSync; } /** @@ -371,9 +396,9 @@ export class SlidingSyncManager { public async nativeSlidingSyncSupport(client: MatrixClient): Promise { // Per https://github.com/matrix-org/matrix-spec-proposals/pull/3575/files#r1589542561 // `client` can be undefined/null in tests for some reason. - const support = await client?.doesServerSupportUnstableFeature("org.matrix.msc3575"); + const support = await client?.doesServerSupportUnstableFeature("org.matrix.simplified_msc3575"); if (support) { - logger.log("nativeSlidingSyncSupport: sliding sync advertised as unstable"); + logger.log("nativeSlidingSyncSupport: org.matrix.simplified_msc3575 sliding sync advertised as unstable"); } return support; } @@ -387,20 +412,9 @@ export class SlidingSyncManager { */ public async checkSupport(client: MatrixClient): Promise { if (await this.nativeSlidingSyncSupport(client)) { - SlidingSyncController.serverSupportsSlidingSync = true; + SlidingSyncManager.serverSupportsSlidingSync = true; return; } - - const proxyUrl = await this.getProxyFromWellKnown(client); - if (proxyUrl != undefined) { - const response = await fetch(new URL("/client/server.json", proxyUrl), { - method: Method.Get, - signal: timeoutSignal(10 * 1000), // 10s - }); - if (response.status === 200) { - logger.log("checkSupport: well-known sliding sync proxy is up at", proxyUrl); - SlidingSyncController.serverSupportsSlidingSync = true; - } - } + SlidingSyncManager.serverSupportsSlidingSync = false; } } diff --git a/src/Unread.ts b/src/Unread.ts index 2c8fa0cff3..e8f4769e25 100644 --- a/src/Unread.ts +++ b/src/Unread.ts @@ -11,7 +11,6 @@ import { logger } from "matrix-js-sdk/src/logger"; import shouldHideEvent from "./shouldHideEvent"; import { haveRendererForEvent } from "./events/EventTileFactory"; -import SettingsStore from "./settings/SettingsStore"; import { RoomNotifState, getRoomNotifsState } from "./RoomNotifs"; /** @@ -44,12 +43,6 @@ export function eventTriggersUnreadCount(client: MatrixClient, ev: MatrixEvent): } export function doesRoomHaveUnreadMessages(room: Room, includeThreads: boolean): boolean { - if (SettingsStore.getValue("feature_sliding_sync")) { - // TODO: https://github.com/vector-im/element-web/issues/23207 - // Sliding Sync doesn't support unread indicator dots (yet...) - return false; - } - const toCheck: Array = [room]; if (includeThreads) { toCheck.push(...room.getThreads()); diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index 45f3367aab..7d4e291e5b 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -354,7 +354,7 @@ export const RovingTabIndexProvider: React.FC = ({ * nodeRef = inputRef when inputRef argument is provided. */ export const useRovingTabIndex = ( - inputRef?: RefObject, + inputRef?: RefObject, ): [FocusHandler, boolean, RefCallback, RefObject] => { const context = useContext(RovingTabIndexContext); diff --git a/src/accessibility/context_menu/StyledMenuItemCheckbox.tsx b/src/accessibility/context_menu/StyledMenuItemCheckbox.tsx index 472e7bcae2..f208eb1091 100644 --- a/src/accessibility/context_menu/StyledMenuItemCheckbox.tsx +++ b/src/accessibility/context_menu/StyledMenuItemCheckbox.tsx @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024,2025 New Vector Ltd. Copyright 2019 The Matrix.org Foundation C.I.C. Copyright 2018 New Vector Ltd Copyright 2015, 2016 OpenMarket Ltd @@ -16,13 +16,12 @@ import { KeyBindingAction } from "../KeyboardShortcuts"; import { getKeyBindingsManager } from "../../KeyBindingsManager"; interface IProps extends React.ComponentProps { - label?: string; onChange(): void; // we handle keyup/down ourselves so lose the ChangeEvent onClose(): void; // gets called after onChange on KeyBindingAction.ActivateSelectedButton } // Semantic component for representing a styled role=menuitemcheckbox -export const StyledMenuItemCheckbox: React.FC = ({ children, label, onChange, onClose, ...props }) => { +export const StyledMenuItemCheckbox: React.FC = ({ children, onChange, onClose, ...props }) => { const [onFocus, isActive, ref] = useRovingTabIndex(); const onKeyDown = (e: React.KeyboardEvent): void => { @@ -63,7 +62,6 @@ export const StyledMenuItemCheckbox: React.FC = ({ children, label, onCh = Omit, "tabIndex"> & { - inputRef?: RefObject; + inputRef?: RefObject; focusOnMouseOver?: boolean; }; diff --git a/src/accessibility/roving/RovingTabIndexWrapper.tsx b/src/accessibility/roving/RovingTabIndexWrapper.tsx index aa9b1ad8c1..f2c7c98dd3 100644 --- a/src/accessibility/roving/RovingTabIndexWrapper.tsx +++ b/src/accessibility/roving/RovingTabIndexWrapper.tsx @@ -6,14 +6,14 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { type ReactElement, type RefCallback } from "react"; +import { type ReactElement, type RefCallback, type RefObject } from "react"; import type React from "react"; import { useRovingTabIndex } from "../RovingTabIndex"; -import { type FocusHandler, type Ref } from "./types"; +import { type FocusHandler } from "./types"; interface IProps { - inputRef?: Ref; + inputRef?: RefObject; children(renderProps: { onFocus: FocusHandler; isActive: boolean; diff --git a/src/accessibility/roving/types.ts b/src/accessibility/roving/types.ts index e3fa694f11..f06c15cf7a 100644 --- a/src/accessibility/roving/types.ts +++ b/src/accessibility/roving/types.ts @@ -6,8 +6,4 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { type RefObject } from "react"; - -export type Ref = RefObject; - export type FocusHandler = () => void; diff --git a/src/async-components/structures/ErrorView.tsx b/src/async-components/structures/ErrorView.tsx index 57cf048403..0f9a61781e 100644 --- a/src/async-components/structures/ErrorView.tsx +++ b/src/async-components/structures/ErrorView.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type ReactNode } from "react"; +import React, { type JSX, type ReactNode } from "react"; import { Text, Heading, Button, Separator } from "@vector-im/compound-web"; import PopOutIcon from "@vector-im/compound-design-tokens/assets/web/icons/pop-out"; diff --git a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx index 30d04baec1..0bc6fea219 100644 --- a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React from "react"; +import React, { type JSX } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import { MatrixClientPeg } from "../../../../MatrixClientPeg"; diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx index 2bcf577d9f..0d17ddaa39 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { createRef } from "react"; +import React, { type JSX, createRef } from "react"; import FileSaver from "file-saver"; import { logger } from "matrix-js-sdk/src/logger"; import { type AuthDict } from "matrix-js-sdk/src/matrix"; diff --git a/src/async-components/views/dialogs/security/ExportE2eKeysDialog.tsx b/src/async-components/views/dialogs/security/ExportE2eKeysDialog.tsx index c2a2510173..31c7c7ca8d 100644 --- a/src/async-components/views/dialogs/security/ExportE2eKeysDialog.tsx +++ b/src/async-components/views/dialogs/security/ExportE2eKeysDialog.tsx @@ -178,7 +178,9 @@ export default class ExportE2eKeysDialog extends React.Component type="password" disabled={disableForm} autoComplete="new-password" - fieldRef={(field) => (this.fieldPassword = field)} + fieldRef={(field) => { + this.fieldPassword = field; + }} />
@@ -195,7 +197,9 @@ export default class ExportE2eKeysDialog extends React.Component type="password" disabled={disableForm} autoComplete="new-password" - fieldRef={(field) => (this.fieldPasswordConfirm = field)} + fieldRef={(field) => { + this.fieldPasswordConfirm = field; + }} />
diff --git a/src/components/structures/AutoHideScrollbar.tsx b/src/components/structures/AutoHideScrollbar.tsx index 7e52e9b192..9cd68b5c58 100644 --- a/src/components/structures/AutoHideScrollbar.tsx +++ b/src/components/structures/AutoHideScrollbar.tsx @@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details. */ import classNames from "classnames"; -import React, { type HTMLAttributes, type ReactHTML, type ReactNode, type WheelEvent } from "react"; +import React, { type HTMLAttributes, type JSX, type ReactNode, type WheelEvent } from "react"; type DynamicHtmlElementProps = JSX.IntrinsicElements[T] extends HTMLAttributes ? DynamicElementProps : DynamicElementProps<"div">; @@ -27,10 +27,10 @@ export type IProps = Omit extends React.Component> { public static defaultProps = { - element: "div" as keyof ReactHTML, + element: "div" as keyof HTMLElementTagNameMap, }; - public readonly containerRef: React.RefObject = React.createRef(); + public readonly containerRef = React.createRef(); public componentDidMount(): void { if (this.containerRef.current && this.props.onScroll) { diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index 5a6ca20e35..bdbd8b4a08 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type CSSProperties, type RefObject, type SyntheticEvent, useRef, useState } from "react"; +import React, { type JSX, type CSSProperties, type RefObject, type SyntheticEvent, useRef, useState } from "react"; import ReactDOM from "react-dom"; import classNames from "classnames"; import FocusLock from "react-focus-lock"; @@ -440,7 +440,7 @@ export default class ContextMenu extends React.PureComponent = [ boolean, - RefObject, + RefObject, (ev?: SyntheticEvent) => void, (ev?: SyntheticEvent) => void, (val: boolean) => void, ]; // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint -export const useContextMenu = (inputRef?: RefObject): ContextMenuTuple => { +export const useContextMenu = ( + inputRef?: RefObject, +): ContextMenuTuple => { let button = useRef(null); if (inputRef) { // if we are given a ref, use it instead of ours diff --git a/src/components/structures/EmbeddedPage.tsx b/src/components/structures/EmbeddedPage.tsx index 4b89102d6f..f1c7b75b96 100644 --- a/src/components/structures/EmbeddedPage.tsx +++ b/src/components/structures/EmbeddedPage.tsx @@ -40,8 +40,8 @@ export default class EmbeddedPage extends React.PureComponent { private unmounted = false; private dispatcherRef?: string; - public constructor(props: IProps, context: React.ContextType) { - super(props, context); + public constructor(props: IProps) { + super(props); this.state = { page: "", diff --git a/src/components/structures/GenericDropdownMenu.tsx b/src/components/structures/GenericDropdownMenu.tsx index 63846e9e3c..78cd3d3ad4 100644 --- a/src/components/structures/GenericDropdownMenu.tsx +++ b/src/components/structures/GenericDropdownMenu.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import classNames from "classnames"; -import React, { type FunctionComponent, type Key, type PropsWithChildren, type ReactNode } from "react"; +import React, { type JSX, type FunctionComponent, type Key, type PropsWithChildren, type ReactNode } from "react"; import { MenuItemRadio } from "../../accessibility/context_menu/MenuItemRadio"; import { type ButtonEvent } from "../views/elements/AccessibleButton"; diff --git a/src/components/structures/HomePage.tsx b/src/components/structures/HomePage.tsx index 01c0042752..5cbd2858c5 100644 --- a/src/components/structures/HomePage.tsx +++ b/src/components/structures/HomePage.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import * as React from "react"; +import React, { type JSX } from "react"; import { useContext, useState } from "react"; import AutoHideScrollbar from "./AutoHideScrollbar"; diff --git a/src/components/structures/IndicatorScrollbar.tsx b/src/components/structures/IndicatorScrollbar.tsx index 5244074bf8..1667d73336 100644 --- a/src/components/structures/IndicatorScrollbar.tsx +++ b/src/components/structures/IndicatorScrollbar.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { createRef } from "react"; +import React, { createRef, type JSX } from "react"; import AutoHideScrollbar, { type IProps as AutoHideScrollbarProps } from "./AutoHideScrollbar"; import UIStore, { UI_EVENTS } from "../../stores/UIStore"; diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index f168c39070..3686d52bca 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import * as React from "react"; +import React, { type JSX } from "react"; import { createRef } from "react"; import classNames from "classnames"; diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 5969dd1ccb..cf68f85b4c 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -124,9 +124,9 @@ class LoggedInView extends React.Component { public static displayName = "LoggedInView"; protected readonly _matrixClient: MatrixClient; - protected readonly _roomView: React.RefObject; - protected readonly _resizeContainer: React.RefObject; - protected readonly resizeHandler: React.RefObject; + protected readonly _roomView: React.RefObject; + protected readonly _resizeContainer: React.RefObject; + protected readonly resizeHandler: React.RefObject; protected layoutWatcherRef?: string; protected compactLayoutWatcherRef?: string; protected backgroundImageWatcherRef?: string; diff --git a/src/components/structures/MainSplit.tsx b/src/components/structures/MainSplit.tsx index 291201f510..6b6736d4ec 100644 --- a/src/components/structures/MainSplit.tsx +++ b/src/components/structures/MainSplit.tsx @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type ReactNode } from "react"; +import React, { type JSX, type ReactNode } from "react"; import { type NumberSize, Resizable } from "re-resizable"; import { type Direction } from "re-resizable/lib/resizer"; import { type WebPanelResize } from "@matrix-org/analytics-events/types/typescript/WebPanelResize"; diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 3d802d34ad..0f722ae53d 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { createRef, lazy } from "react"; +import React, { type JSX, createRef, lazy } from "react"; import { ClientEvent, createClient, @@ -235,6 +235,7 @@ export default class MatrixChat extends React.PureComponent { private themeWatcher?: ThemeWatcher; private fontWatcher?: FontWatcher; private readonly stores: SdkContextClass; + private loadSessionAbortController = new AbortController(); public constructor(props: IProps) { super(props); @@ -327,7 +328,7 @@ export default class MatrixChat extends React.PureComponent { // When the session loads it'll be detected as soft logged out and a dispatch // will be sent out to say that, triggering this MatrixChat to show the soft // logout page. - Lifecycle.loadSession(); + Lifecycle.loadSession({ abortSignal: this.loadSessionAbortController.signal }); return; } @@ -552,6 +553,7 @@ export default class MatrixChat extends React.PureComponent { guestHsUrl: this.getServerProperties().serverConfig.hsUrl, guestIsUrl: this.getServerProperties().serverConfig.isUrl, defaultDeviceDisplayName: this.props.defaultDeviceDisplayName, + abortSignal: this.loadSessionAbortController.signal, }); }) .then((loadedSession) => { @@ -1388,7 +1390,7 @@ export default class MatrixChat extends React.PureComponent { // so show the homepage. dis.dispatch({ action: Action.ViewHomePage, justRegistered: true }); } - } else { + } else if (!(await this.shouldForceVerification())) { this.showScreenAfterLogin(); } @@ -1565,26 +1567,33 @@ export default class MatrixChat extends React.PureComponent { dis.fire(Action.FocusSendMessageComposer); }); - cli.on(HttpApiEvent.SessionLoggedOut, function (errObj) { + cli.on(HttpApiEvent.SessionLoggedOut, (errObj) => { + this.loadSessionAbortController.abort(errObj); + this.loadSessionAbortController = new AbortController(); + if (Lifecycle.isLoggingOut()) return; // A modal might have been open when we were logged out by the server Modal.forceCloseAllModals(); - if (errObj.httpStatus === 401 && errObj.data && errObj.data["soft_logout"]) { + if (errObj.httpStatus === 401 && errObj.data?.["soft_logout"]) { logger.warn("Soft logout issued by server - avoiding data deletion"); Lifecycle.softLogout(); return; } + dis.dispatch( + { + action: "logout", + }, + true, + ); + + // The above dispatch closes all modals, so open the modal after calling it synchronously Modal.createDialog(ErrorDialog, { title: _t("auth|session_logged_out_title"), description: _t("auth|session_logged_out_description"), }); - - dis.dispatch({ - action: "logout", - }); }); cli.on(HttpApiEvent.NoConsent, function (message, consentUri) { Modal.createDialog( @@ -2003,9 +2012,17 @@ export default class MatrixChat extends React.PureComponent { }; // complete security / e2e setup has finished - private onCompleteSecurityE2eSetupFinished = (): void => { - // This is async but we making this function async to wait for it isn't useful - this.onShowPostLoginScreen().catch((e) => { + private onCompleteSecurityE2eSetupFinished = async (): Promise => { + const forceVerify = await this.shouldForceVerification(); + if (forceVerify) { + const isVerified = await MatrixClientPeg.safeGet().getCrypto()?.isCrossSigningReady(); + if (!isVerified) { + // We must verify but we haven't yet verified - don't continue logging in + return; + } + } + + await this.onShowPostLoginScreen().catch((e) => { logger.error("Exception showing post-login screen", e); }); }; diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 9f535da257..2441182bc8 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { createRef, type ReactNode, type TransitionEvent } from "react"; +import React, { type JSX, createRef, type ReactNode, type TransitionEvent } from "react"; import classNames from "classnames"; import { type Room, @@ -259,8 +259,8 @@ export default class MessagePanel extends React.Component { // A map to allow groupers to maintain consistent keys even if their first event is uprooted due to back-pagination. public grouperKeyMap = new WeakMap(); - public constructor(props: IProps, context: React.ContextType) { - super(props, context); + public constructor(props: IProps) { + super(props); this.state = { // previous positions the read marker has been in, so we can @@ -292,6 +292,7 @@ export default class MessagePanel extends React.Component { this.props.room?.currentState.off(RoomStateEvent.Update, this.calculateRoomMembersCount); SettingsStore.unwatchSetting(this.showTypingNotificationsWatcherRef); this.readReceiptMap = {}; + this.resizeObserver.disconnect(); } public componentDidUpdate(prevProps: IProps, prevState: IState): void { @@ -800,7 +801,7 @@ export default class MessagePanel extends React.Component { isRedacted={mxEv.isRedacted()} replacingEventId={mxEv.replacingEventId()} editState={isEditing ? this.props.editState : undefined} - onHeightChanged={this.onHeightChanged} + resizeObserver={this.resizeObserver} readReceipts={readReceipts} readReceiptMap={this.readReceiptMap} showUrlPreview={this.props.showUrlPreview} @@ -953,15 +954,13 @@ export default class MessagePanel extends React.Component { this.eventTiles[eventId] = node; }; - // once dynamic content in the events load, make the scrollPanel check the - // scroll offsets. + // Once dynamic content in the events load, make the scrollPanel check the scroll offsets. public onHeightChanged = (): void => { - const scrollPanel = this.scrollPanel.current; - if (scrollPanel) { - scrollPanel.checkScroll(); - } + this.scrollPanel.current?.checkScroll(); }; + private resizeObserver = new ResizeObserver(this.onHeightChanged); + private onTypingShown = (): void => { const scrollPanel = this.scrollPanel.current; // this will make the timeline grow, so checkScroll diff --git a/src/components/structures/NonUrgentToastContainer.tsx b/src/components/structures/NonUrgentToastContainer.tsx index 4e37110160..481c9f0bf9 100644 --- a/src/components/structures/NonUrgentToastContainer.tsx +++ b/src/components/structures/NonUrgentToastContainer.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import * as React from "react"; +import React from "react"; import { type EmptyObject } from "matrix-js-sdk/src/matrix"; import { type ComponentClass } from "../../@types/common"; diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx index cd71710782..b31ed6bab6 100644 --- a/src/components/structures/NotificationPanel.tsx +++ b/src/components/structures/NotificationPanel.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React from "react"; +import React, { type JSX } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import NotificationsIcon from "@vector-im/compound-design-tokens/assets/web/icons/notifications"; @@ -38,8 +38,8 @@ export default class NotificationPanel extends React.PureComponent(); - public constructor(props: IProps, context: React.ContextType) { - super(props, context); + public constructor(props: IProps) { + super(props); this.state = { narrow: false, diff --git a/src/components/structures/PictureInPictureDragger.tsx b/src/components/structures/PictureInPictureDragger.tsx index ffdf1fd7c8..057d600eef 100644 --- a/src/components/structures/PictureInPictureDragger.tsx +++ b/src/components/structures/PictureInPictureDragger.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { createRef } from "react"; +import React, { type JSX, createRef } from "react"; import UIStore, { UI_EVENTS } from "../../stores/UIStore"; import { lerp } from "../../utils/AnimationUtils"; diff --git a/src/components/structures/PipContainer.tsx b/src/components/structures/PipContainer.tsx index 54129bd493..e46267c149 100644 --- a/src/components/structures/PipContainer.tsx +++ b/src/components/structures/PipContainer.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type MutableRefObject, type ReactNode, useRef } from "react"; +import React, { type RefObject, type ReactNode, useRef } from "react"; import { CallEvent, CallState, type MatrixCall } from "matrix-js-sdk/src/webrtc/call"; import { logger } from "matrix-js-sdk/src/logger"; import { type Optional } from "matrix-events-sdk"; @@ -34,7 +34,7 @@ const SHOW_CALL_IN_STATES = [ ]; interface IProps { - movePersistedElement: MutableRefObject<(() => void) | undefined>; + movePersistedElement: RefObject<(() => void) | null>; } interface IState { @@ -280,7 +280,7 @@ class PipContainerInner extends React.Component { } export const PipContainer: React.FC = () => { - const movePersistedElement = useRef<() => void>(); + const movePersistedElement = useRef<() => void>(null); return ; }; diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index 8d46c9b1e3..21e2a2ee71 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -64,8 +64,10 @@ export default class RightPanel extends React.Component { public static contextType = MatrixClientContext; declare public context: React.ContextType; - public constructor(props: Props, context: React.ContextType) { - super(props, context); + public constructor(props: Props) { + super(props); + + this.state = RightPanel.getDerivedStateFromProps(props); } private readonly delayedUpdate = throttle( diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx index 439c1e47fe..2130f4fa21 100644 --- a/src/components/structures/RoomSearch.tsx +++ b/src/components/structures/RoomSearch.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import classNames from "classnames"; -import * as React from "react"; +import React from "react"; import { ALTERNATE_KEY_NAME } from "../../accessibility/KeyboardShortcuts"; import defaultDispatcher from "../../dispatcher/dispatcher"; diff --git a/src/components/structures/RoomSearchView.tsx b/src/components/structures/RoomSearchView.tsx index d37cd3f19d..cd03a0140a 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 OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { forwardRef, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; +import React, { type JSX, forwardRef, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import { type ISearchResults, type IThreadBundledRelationship, @@ -59,7 +59,7 @@ export const RoomSearchView = forwardRef( const aborted = useRef(false); // A map from room ID to permalink creator const permalinkCreators = useMemo(() => new Map(), []); - const innerRef = useRef(); + const innerRef = useRef(null); useEffect(() => { return () => { @@ -198,12 +198,6 @@ export const RoomSearchView = forwardRef( } } - // once dynamic content in the search results load, make the scrollPanel check - // the scroll offsets. - const onHeightChanged = (): void => { - innerRef.current?.checkScroll(); - }; - const onRef = (e: ScrollPanel | null): void => { if (typeof ref === "function") { ref(e); @@ -302,7 +296,6 @@ export const RoomSearchView = forwardRef( searchHighlights={highlights ?? []} resultLink={resultLink} permalinkCreator={permalinkCreator} - onHeightChanged={onHeightChanged} />, ); diff --git a/src/components/structures/RoomStatusBar.tsx b/src/components/structures/RoomStatusBar.tsx index ad5d5c1cdf..4793f1a65b 100644 --- a/src/components/structures/RoomStatusBar.tsx +++ b/src/components/structures/RoomStatusBar.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type ReactNode } from "react"; +import React, { type JSX, type ReactNode } from "react"; import { ClientEvent, EventStatus, diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 23558fc3df..b86e7937e8 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -256,7 +256,7 @@ interface LocalRoomViewProps { localRoom: LocalRoom; resizeNotifier: ResizeNotifier; permalinkCreator: RoomPermalinkCreator; - roomView: RefObject; + roomView: RefObject; onFileDrop: (dataTransfer: DataTransfer) => Promise; mainSplitContentType: MainSplitContentType; } diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx index d1f3677816..0d41e56c92 100644 --- a/src/components/structures/SpaceHierarchy.tsx +++ b/src/components/structures/SpaceHierarchy.tsx @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024,2025 New Vector Ltd. Copyright 2021-2023 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial @@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import React, { + type JSX, type Dispatch, type KeyboardEvent, type KeyboardEventHandler, @@ -16,6 +17,7 @@ import React, { useCallback, useContext, useEffect, + useId, useMemo, useRef, useState, @@ -116,6 +118,7 @@ const Tile: React.FC = ({ const [showChildren, toggleShowChildren] = useStateToggle(true); const [onFocus, isActive, ref, nodeRef] = useRovingTabIndex(); const [busy, setBusy] = useState(false); + const checkboxLabelId = useId(); const onPreviewClick = (ev: ButtonEvent): void => { ev.preventDefault(); @@ -172,7 +175,14 @@ const Tile: React.FC = ({ let checkbox: ReactElement | undefined; if (onToggleClick) { if (hasPermissions) { - checkbox = ; + checkbox = ( + + ); } else { checkbox = ( = ({ ev.stopPropagation(); }} > - + ); } @@ -248,7 +263,7 @@ const Tile: React.FC = ({
{avatar}
- {name} + {name} {joinedSection} {suggestedSection}
@@ -330,11 +345,14 @@ const Tile: React.FC = ({ }; } + const shouldToggle = hasPermissions && onToggleClick; + return (
  • = ({ mx_SpaceHierarchy_subspace: room.room_type === RoomType.Space, mx_SpaceHierarchy_joining: busy, })} - onClick={hasPermissions && onToggleClick ? onToggleClick : onPreviewClick} + onClick={shouldToggle ? onToggleClick : onPreviewClick} onKeyDown={onKeyDown} ref={ref} onFocus={onFocus} @@ -619,7 +637,7 @@ const useIntersectionObserver = (callback: () => void): ((element: HTMLDivElemen } }; - const observerRef = useRef(); + const observerRef = useRef(undefined); return (element: HTMLDivElement) => { if (observerRef.current) { observerRef.current.disconnect(); diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index bb4fe19520..59ec657b02 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details. import { EventType, RoomType, JoinRule, Preset, type Room, RoomEvent } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; import { logger } from "matrix-js-sdk/src/logger"; -import React, { useCallback, useContext, useRef, useState } from "react"; +import React, { type JSX, useCallback, useContext, useRef, useState } from "react"; import MatrixClientContext from "../../contexts/MatrixClientContext"; import createRoom, { type IOpts } from "../../createRoom"; diff --git a/src/components/structures/SplashPage.tsx b/src/components/structures/SplashPage.tsx index 7b419bbf47..86d78fcecc 100644 --- a/src/components/structures/SplashPage.tsx +++ b/src/components/structures/SplashPage.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import classNames from "classnames"; -import React, { type DetailedHTMLProps, type HTMLAttributes, type ReactNode } from "react"; +import React, { type JSX, type DetailedHTMLProps, type HTMLAttributes, type ReactNode } from "react"; interface Props extends DetailedHTMLProps, HTMLElement> { className?: string; diff --git a/src/components/structures/TabbedView.tsx b/src/components/structures/TabbedView.tsx index 0778d7c92f..4b842eaff1 100644 --- a/src/components/structures/TabbedView.tsx +++ b/src/components/structures/TabbedView.tsx @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import * as React from "react"; +import React, { type JSX } from "react"; import classNames from "classnames"; import { _t, type TranslationKey } from "../../languageHandler"; diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index 512a1156a5..c1c4b3ff57 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { createRef, type KeyboardEvent } from "react"; +import React, { type JSX, createRef, type KeyboardEvent } from "react"; import { type Thread, THREAD_RELATION_TYPE, @@ -86,8 +86,8 @@ export default class ThreadView extends React.Component { // Set by setEventId in ctor. private eventId!: string; - public constructor(props: IProps, context: React.ContextType) { - super(props, context); + public constructor(props: IProps) { + super(props); this.setEventId(this.props.mxEvent); const thread = this.props.room.getThread(this.eventId) ?? undefined; diff --git a/src/components/structures/ToastContainer.tsx b/src/components/structures/ToastContainer.tsx index 43c12d9cf6..649cd1dc59 100644 --- a/src/components/structures/ToastContainer.tsx +++ b/src/components/structures/ToastContainer.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import * as React from "react"; +import React from "react"; import classNames from "classnames"; import { Text } from "@vector-im/compound-web"; import { type EmptyObject } from "matrix-js-sdk/src/matrix"; diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 08a519e182..67af61f7ac 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { createRef, type ReactNode } from "react"; +import React, { type JSX, createRef, type ReactNode } from "react"; import { type Room } from "matrix-js-sdk/src/matrix"; import { MatrixClientPeg } from "../../MatrixClientPeg"; @@ -81,10 +81,10 @@ export default class UserMenu extends React.Component { private dispatcherRef?: string; private themeWatcherRef?: string; private readonly dndWatcherRef?: string; - private buttonRef: React.RefObject = createRef(); + private buttonRef = createRef(); - public constructor(props: IProps, context: React.ContextType) { - super(props, context); + public constructor(props: IProps) { + super(props); this.state = { contextMenuPosition: null, @@ -370,6 +370,13 @@ export default class UserMenu extends React.Component { ? toRightOf(this.state.contextMenuPosition) : below(this.state.contextMenuPosition); + const userIdentifierString = UserIdentifierCustomisations.getDisplayUserIdentifier( + MatrixClientPeg.safeGet().getSafeUserId(), + { + withDisplayName: true, + }, + ); + return (
    @@ -377,13 +384,8 @@ export default class UserMenu extends React.Component { {OwnProfileStore.instance.displayName} - - {UserIdentifierCustomisations.getDisplayUserIdentifier( - MatrixClientPeg.safeGet().getSafeUserId(), - { - withDisplayName: true, - }, - )} + + {userIdentifierString}
    diff --git a/src/components/structures/UserView.tsx b/src/components/structures/UserView.tsx index be947d9a4b..58aed9932b 100644 --- a/src/components/structures/UserView.tsx +++ b/src/components/structures/UserView.tsx @@ -34,8 +34,8 @@ export default class UserView extends React.Component { public static contextType = MatrixClientContext; declare public context: React.ContextType; - public constructor(props: IProps, context: React.ContextType) { - super(props, context); + public constructor(props: IProps) { + super(props); this.state = { loading: true, }; diff --git a/src/components/structures/ViewSource.tsx b/src/components/structures/ViewSource.tsx index 66bbbe4c0b..b2e003284b 100644 --- a/src/components/structures/ViewSource.tsx +++ b/src/components/structures/ViewSource.tsx @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React from "react"; +import React, { type JSX } from "react"; import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; import SyntaxHighlight from "../views/elements/SyntaxHighlight"; diff --git a/src/components/structures/WaitingForThirdPartyRoomView.tsx b/src/components/structures/WaitingForThirdPartyRoomView.tsx index 0112da4cc0..3983b286f1 100644 --- a/src/components/structures/WaitingForThirdPartyRoomView.tsx +++ b/src/components/structures/WaitingForThirdPartyRoomView.tsx @@ -21,7 +21,7 @@ import SdkConfig from "../../SdkConfig"; import { useScopedRoomContext } from "../../contexts/ScopedRoomContext.tsx"; interface Props { - roomView: RefObject; + roomView: RefObject; resizeNotifier: ResizeNotifier; inviteEvent: MatrixEvent; } diff --git a/src/components/structures/auth/ConfirmSessionLockTheftView.tsx b/src/components/structures/auth/ConfirmSessionLockTheftView.tsx index b71b627a5a..097a6aa5b7 100644 --- a/src/components/structures/auth/ConfirmSessionLockTheftView.tsx +++ b/src/components/structures/auth/ConfirmSessionLockTheftView.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React from "react"; +import React, { type JSX } from "react"; import { _t } from "../../../languageHandler"; import SdkConfig from "../../../SdkConfig"; diff --git a/src/components/structures/auth/ForgotPassword.tsx b/src/components/structures/auth/ForgotPassword.tsx index 9e25f3349c..171bd955a1 100644 --- a/src/components/structures/auth/ForgotPassword.tsx +++ b/src/components/structures/auth/ForgotPassword.tsx @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type ReactNode } from "react"; +import React, { type JSX, type ReactNode } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import { sleep } from "matrix-js-sdk/src/utils"; import { LockSolidIcon, CheckIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; @@ -388,7 +388,9 @@ export default class ForgotPassword extends React.Component { label={_td("auth|change_password_new_label")} value={this.state.password} minScore={PASSWORD_MIN_SCORE} - fieldRef={(field) => (this.fieldPassword = field)} + fieldRef={(field) => { + this.fieldPassword = field; + }} onChange={this.onInputChanged.bind(this, "password")} autoComplete="new-password" /> @@ -399,7 +401,9 @@ export default class ForgotPassword extends React.Component { labelInvalid={_td("auth|reset_password|passwords_mismatch")} value={this.state.password2} password={this.state.password} - fieldRef={(field) => (this.fieldPasswordConfirm = field)} + fieldRef={(field) => { + this.fieldPasswordConfirm = field; + }} onChange={this.onInputChanged.bind(this, "password2")} autoComplete="new-password" /> diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx index 894799b566..9aca0046f2 100644 --- a/src/components/structures/auth/Login.tsx +++ b/src/components/structures/auth/Login.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type ReactNode } from "react"; +import React, { type JSX, type ReactNode } from "react"; import classNames from "classnames"; import { logger } from "matrix-js-sdk/src/logger"; import { type SSOFlow, SSOAction } from "matrix-js-sdk/src/matrix"; diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index 496c687bbf..a5c713b3ea 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -20,7 +20,7 @@ import { SSOAction, type RegisterResponse, } from "matrix-js-sdk/src/matrix"; -import React, { Fragment, type ReactNode } from "react"; +import React, { type JSX, Fragment, type ReactNode } from "react"; import classNames from "classnames"; import { logger } from "matrix-js-sdk/src/logger"; diff --git a/src/components/structures/auth/SessionLockStolenView.tsx b/src/components/structures/auth/SessionLockStolenView.tsx index 01193440fc..bb95f1b44a 100644 --- a/src/components/structures/auth/SessionLockStolenView.tsx +++ b/src/components/structures/auth/SessionLockStolenView.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React from "react"; +import React, { type JSX } from "react"; import SplashPage from "../SplashPage"; import { _t } from "../../../languageHandler"; diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index e290292ea6..b91b99c921 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React from "react"; +import React, { type JSX } from "react"; import { type KeyBackupInfo, type VerificationRequest } from "matrix-js-sdk/src/crypto-api"; import { logger } from "matrix-js-sdk/src/logger"; import { type SecretStorageKeyDescription } from "matrix-js-sdk/src/secret-storage"; diff --git a/src/components/structures/auth/SoftLogout.tsx b/src/components/structures/auth/SoftLogout.tsx index 62cb2801f8..7ebcc7af6a 100644 --- a/src/components/structures/auth/SoftLogout.tsx +++ b/src/components/structures/auth/SoftLogout.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type ChangeEvent, type SyntheticEvent } from "react"; +import React, { type JSX, type ChangeEvent, type SyntheticEvent } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import { type Optional } from "matrix-events-sdk"; import { type LoginFlow, MatrixError, SSOAction, type SSOFlow } from "matrix-js-sdk/src/matrix"; @@ -66,8 +66,8 @@ export default class SoftLogout extends React.Component { public static contextType = SDKContext; declare public context: React.ContextType; - public constructor(props: IProps, context: React.ContextType) { - super(props, context); + public constructor(props: IProps) { + super(props); this.state = { loginView: LoginView.Loading, diff --git a/src/components/structures/auth/header/AuthHeaderDisplay.tsx b/src/components/structures/auth/header/AuthHeaderDisplay.tsx index 17f4c91b8c..515d919db3 100644 --- a/src/components/structures/auth/header/AuthHeaderDisplay.tsx +++ b/src/components/structures/auth/header/AuthHeaderDisplay.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { Fragment, type PropsWithChildren, type ReactNode, useContext } from "react"; +import React, { type JSX, Fragment, type PropsWithChildren, type ReactNode, useContext } from "react"; import { AuthHeaderContext } from "./AuthHeaderContext"; diff --git a/src/components/structures/auth/header/AuthHeaderProvider.tsx b/src/components/structures/auth/header/AuthHeaderProvider.tsx index af48364bd7..ad1ff5b0cc 100644 --- a/src/components/structures/auth/header/AuthHeaderProvider.tsx +++ b/src/components/structures/auth/header/AuthHeaderProvider.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import { isEqual } from "lodash"; -import React, { type ComponentProps, type PropsWithChildren, type Reducer, useReducer } from "react"; +import React, { type JSX, type ComponentProps, type PropsWithChildren, type Reducer, useReducer } from "react"; import { AuthHeaderContext } from "./AuthHeaderContext"; import { type AuthHeaderModifier } from "./AuthHeaderModifier"; diff --git a/src/components/utils/Box.tsx b/src/components/utils/Box.tsx index 2de64ba075..2c5dfa56d2 100644 --- a/src/components/utils/Box.tsx +++ b/src/components/utils/Box.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import classNames from "classnames"; -import React, { useMemo } from "react"; +import React, { type JSX, useMemo } from "react"; type FlexProps = { /** diff --git a/src/components/utils/Flex.tsx b/src/components/utils/Flex.tsx index c4e2f90066..0a8c3c2fa4 100644 --- a/src/components/utils/Flex.tsx +++ b/src/components/utils/Flex.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import classNames from "classnames"; -import React, { type ComponentProps, type JSXElementConstructor, useMemo } from "react"; +import React, { type JSX, type ComponentProps, type JSXElementConstructor, useMemo } from "react"; type FlexProps> = { /** diff --git a/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx b/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx index 9e38e6e8d8..1d5d9aba11 100644 --- a/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx +++ b/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx @@ -5,13 +5,16 @@ * Please see LICENSE files in the repository root for full details. */ -import { useCallback } from "react"; +import { useCallback, useMemo } from "react"; import { type Room } from "matrix-js-sdk/src/matrix"; import dispatcher from "../../../dispatcher/dispatcher"; import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { Action } from "../../../dispatcher/actions"; import { hasAccessToOptionsMenu } from "./utils"; +import { _t } from "../../../languageHandler"; +import { type RoomNotificationState } from "../../../stores/notifications/RoomNotificationState"; +import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; export interface RoomListItemViewState { /** @@ -22,6 +25,14 @@ export interface RoomListItemViewState { * Open the room having given roomId. */ openRoom: () => void; + /** + * The a11y label for the room list item. + */ + a11yLabel: string; + /** + * The notification state of the room. + */ + notificationState: RoomNotificationState; } /** @@ -31,6 +42,8 @@ export interface RoomListItemViewState { export function useRoomListItemViewModel(room: Room): RoomListItemViewState { // incoming: Check notification menu rights const showHoverMenu = hasAccessToOptionsMenu(room); + const notificationState = useMemo(() => RoomNotificationStateStore.instance.getRoomState(room), [room]); + const a11yLabel = getA11yLabel(room, notificationState); // Actions @@ -43,7 +56,38 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState { }, [room]); return { + notificationState, showHoverMenu, openRoom, + a11yLabel, }; } + +/** + * Get the a11y label for the room list item + * @param room + * @param notificationState + */ +function getA11yLabel(room: Room, notificationState: RoomNotificationState): string { + if (notificationState.isUnsetMessage) { + return _t("a11y|room_messsage_not_sent", { + roomName: room.name, + }); + } else if (notificationState.invited) { + return _t("a11y|room_n_unread_invite", { + roomName: room.name, + }); + } else if (notificationState.isMention) { + return _t("a11y|room_n_unread_messages_mentions", { + roomName: room.name, + count: notificationState.count, + }); + } else if (notificationState.hasUnreadCount) { + return _t("a11y|room_n_unread_messages", { + roomName: room.name, + count: notificationState.count, + }); + } else { + return _t("room_list|room|open_room", { roomName: room.name }); + } +} diff --git a/src/components/viewmodels/roomlist/RoomListViewModel.tsx b/src/components/viewmodels/roomlist/RoomListViewModel.tsx index 584e436c57..217eaefbd9 100644 --- a/src/components/viewmodels/roomlist/RoomListViewModel.tsx +++ b/src/components/viewmodels/roomlist/RoomListViewModel.tsx @@ -18,7 +18,7 @@ import SpaceStore from "../../../stores/spaces/SpaceStore"; import dispatcher from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; -import { useIndexForActiveRoom } from "./useIndexForActiveRoom"; +import { useStickyRoomList } from "./useStickyRoomList"; export interface RoomListViewState { /** @@ -97,8 +97,14 @@ export interface RoomListViewState { */ export function useRoomListViewModel(): RoomListViewState { const matrixClient = useMatrixClientContext(); - const { primaryFilters, activePrimaryFilter, rooms, activateSecondaryFilter, activeSecondaryFilter } = - useFilteredRooms(); + const { + primaryFilters, + activePrimaryFilter, + rooms: filteredRooms, + activateSecondaryFilter, + activeSecondaryFilter, + } = useFilteredRooms(); + const { activeIndex, rooms } = useStickyRoomList(filteredRooms); const currentSpace = useEventEmitterState( SpaceStore.instance, @@ -107,7 +113,6 @@ export function useRoomListViewModel(): RoomListViewState { ); const canCreateRoom = hasCreateRoomRights(matrixClient, currentSpace); - const activeIndex = useIndexForActiveRoom(rooms); const { activeSortOption, sort } = useSorter(); const { shouldShowMessagePreview, toggleMessagePreview } = useMessagePreviewToggle(); diff --git a/src/components/viewmodels/roomlist/useFilteredRooms.tsx b/src/components/viewmodels/roomlist/useFilteredRooms.tsx index 68f8e3e380..5e1554fcdc 100644 --- a/src/components/viewmodels/roomlist/useFilteredRooms.tsx +++ b/src/components/viewmodels/roomlist/useFilteredRooms.tsx @@ -145,22 +145,14 @@ export function useFilteredRooms(): FilteredRooms { // SecondaryFilter is an enum for the UI, let's convert it to something // that the store will understand. const secondary = secondaryFiltersToFilterKeyMap.get(filter); - - // Active primary filter may need to be toggled off when applying this secondary filer. - let primary = primaryFilter; - if ( - primaryFilter !== undefined && - secondary !== undefined && - !isPrimaryFilterCompatible(primaryFilter, secondary) - ) { - primary = undefined; - } - setActiveSecondaryFilter(filter); - setPrimaryFilter(primary); - updateRoomsFromStore(filterUndefined([primary, secondary])); + + // Reset any active primary filters. + setPrimaryFilter(undefined); + + updateRoomsFromStore(filterUndefined([secondary])); }, - [activeSecondaryFilter, primaryFilter, updateRoomsFromStore], + [activeSecondaryFilter, updateRoomsFromStore], ); /** diff --git a/src/components/viewmodels/roomlist/useIndexForActiveRoom.tsx b/src/components/viewmodels/roomlist/useIndexForActiveRoom.tsx deleted file mode 100644 index 210e0efd0f..0000000000 --- a/src/components/viewmodels/roomlist/useIndexForActiveRoom.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -import { useCallback, useEffect, useState } from "react"; - -import { SdkContextClass } from "../../../contexts/SDKContext"; -import { useDispatcher } from "../../../hooks/useDispatcher"; -import dispatcher from "../../../dispatcher/dispatcher"; -import { Action } from "../../../dispatcher/actions"; -import type { Room } from "matrix-js-sdk/src/matrix"; - -/** - * Tracks the index of the active room in the given array of rooms. - * @param rooms list of rooms - * @returns index of the active room or undefined otherwise. - */ -export function useIndexForActiveRoom(rooms: Room[]): number | undefined { - const [index, setIndex] = useState(undefined); - - const calculateIndex = useCallback( - (newRoomId?: string) => { - const activeRoomId = newRoomId ?? SdkContextClass.instance.roomViewStore.getRoomId(); - const index = rooms.findIndex((room) => room.roomId === activeRoomId); - setIndex(index === -1 ? undefined : index); - }, - [rooms], - ); - - // Re-calculate the index when the active room has changed. - useDispatcher(dispatcher, (payload) => { - if (payload.action === Action.ActiveRoomChanged) calculateIndex(payload.newRoomId); - }); - - // Re-calculate the index when the list of rooms has changed. - useEffect(() => { - calculateIndex(); - }, [calculateIndex, rooms]); - - return index; -} diff --git a/src/components/viewmodels/roomlist/useStickyRoomList.tsx b/src/components/viewmodels/roomlist/useStickyRoomList.tsx new file mode 100644 index 0000000000..e8234d14ae --- /dev/null +++ b/src/components/viewmodels/roomlist/useStickyRoomList.tsx @@ -0,0 +1,117 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { useCallback, useEffect, useState } from "react"; + +import { SdkContextClass } from "../../../contexts/SDKContext"; +import { useDispatcher } from "../../../hooks/useDispatcher"; +import dispatcher from "../../../dispatcher/dispatcher"; +import { Action } from "../../../dispatcher/actions"; +import type { Room } from "matrix-js-sdk/src/matrix"; +import type { Optional } from "matrix-events-sdk"; + +function getIndexByRoomId(rooms: Room[], roomId: Optional): number | undefined { + const index = rooms.findIndex((room) => room.roomId === roomId); + return index === -1 ? undefined : index; +} + +function getRoomsWithStickyRoom( + rooms: Room[], + oldIndex: number | undefined, + newIndex: number | undefined, + isRoomChange: boolean, +): { newRooms: Room[]; newIndex: number | undefined } { + const updated = { newIndex, newRooms: rooms }; + if (isRoomChange) { + /* + * When opening another room, the index should obviously change. + */ + return updated; + } + if (newIndex === undefined || oldIndex === undefined) { + /* + * If oldIndex is undefined, then there was no active room before. + * So nothing to do in regards to sticky room. + * Similarly, if newIndex is undefined, there's no active room anymore. + */ + return updated; + } + if (newIndex === oldIndex) { + /* + * If the index hasn't changed, we have nothing to do. + */ + return updated; + } + if (oldIndex > rooms.length - 1) { + /* + * If the old index falls out of the bounds of the rooms array + * (usually because rooms were removed), we can no longer place + * the active room in the same old index. + */ + return updated; + } + + /* + * Making the active room sticky is as simple as removing it from + * its new index and placing it in the old index. + */ + const newRooms = [...rooms]; + const [newRoom] = newRooms.splice(newIndex, 1); + newRooms.splice(oldIndex, 0, newRoom); + + return { newIndex: oldIndex, newRooms }; +} + +interface StickyRoomListResult { + /** + * List of rooms with sticky active room. + */ + rooms: Room[]; + /** + * Index of the active room in the room list. + */ + activeIndex: number | undefined; +} + +/** + * - Provides a list of rooms such that the active room is sticky i.e the active room is kept + * in the same index even when the order of rooms in the list changes. + * - Provides the index of the active room. + * @param rooms list of rooms + * @see {@link StickyRoomListResult} details what this hook returns.. + */ +export function useStickyRoomList(rooms: Room[]): StickyRoomListResult { + const [listState, setListState] = useState<{ index: number | undefined; roomsWithStickyRoom: Room[] }>({ + index: undefined, + roomsWithStickyRoom: rooms, + }); + + const updateRoomsAndIndex = useCallback( + (newRoomId?: string, isRoomChange: boolean = false) => { + setListState((current) => { + const activeRoomId = newRoomId ?? SdkContextClass.instance.roomViewStore.getRoomId(); + const newActiveIndex = getIndexByRoomId(rooms, activeRoomId); + const oldIndex = current.index; + const { newIndex, newRooms } = getRoomsWithStickyRoom(rooms, oldIndex, newActiveIndex, isRoomChange); + return { index: newIndex, roomsWithStickyRoom: newRooms }; + }); + }, + [rooms], + ); + + // Re-calculate the index when the active room has changed. + useDispatcher(dispatcher, (payload) => { + if (payload.action === Action.ActiveRoomChanged) updateRoomsAndIndex(payload.newRoomId, true); + }); + + // Re-calculate the index when the list of rooms has changed. + useEffect(() => { + updateRoomsAndIndex(); + }, [rooms, updateRoomsAndIndex]); + + return { activeIndex: listState.index, rooms: listState.roomsWithStickyRoom }; +} diff --git a/src/components/views/audio_messages/AudioPlayerBase.tsx b/src/components/views/audio_messages/AudioPlayerBase.tsx index 97fc49b9db..71bea008a2 100644 --- a/src/components/views/audio_messages/AudioPlayerBase.tsx +++ b/src/components/views/audio_messages/AudioPlayerBase.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { createRef, type ReactNode, type RefObject } from "react"; +import React, { createRef, type ReactNode } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import { type Playback, type PlaybackState } from "../../../audio/Playback"; @@ -31,8 +31,8 @@ interface IState { } export default abstract class AudioPlayerBase extends React.PureComponent { - protected seekRef: RefObject = createRef(); - protected playPauseRef: RefObject = createRef(); + protected seekRef = createRef(); + protected playPauseRef = createRef(); public constructor(props: T) { super(props); diff --git a/src/components/views/auth/AuthBody.tsx b/src/components/views/auth/AuthBody.tsx index ac1950b4a1..98ed548584 100644 --- a/src/components/views/auth/AuthBody.tsx +++ b/src/components/views/auth/AuthBody.tsx @@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details. */ import classNames from "classnames"; -import React, { type PropsWithChildren } from "react"; +import React, { type JSX, type PropsWithChildren } from "react"; interface Props { className?: string; diff --git a/src/components/views/auth/AuthFooter.tsx b/src/components/views/auth/AuthFooter.tsx index f609b4792a..1942bf0431 100644 --- a/src/components/views/auth/AuthFooter.tsx +++ b/src/components/views/auth/AuthFooter.tsx @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type ReactElement } from "react"; +import React, { type JSX, type ReactElement } from "react"; import SdkConfig from "../../../SdkConfig"; import { _t } from "../../../languageHandler"; diff --git a/src/components/views/auth/CaptchaForm.tsx b/src/components/views/auth/CaptchaForm.tsx index d019ba234c..ca25ae180d 100644 --- a/src/components/views/auth/CaptchaForm.tsx +++ b/src/components/views/auth/CaptchaForm.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { createRef } from "react"; +import React, { type JSX, createRef } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "../../../languageHandler"; diff --git a/src/components/views/auth/EmailField.tsx b/src/components/views/auth/EmailField.tsx index 9b330a377d..fb420ed459 100644 --- a/src/components/views/auth/EmailField.tsx +++ b/src/components/views/auth/EmailField.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type ComponentProps, PureComponent, type RefCallback, type RefObject } from "react"; +import React, { type ComponentProps, PureComponent, type Ref } from "react"; import Field, { type IInputProps } from "../elements/Field"; import { _t, _td, type TranslationKey } from "../../../languageHandler"; @@ -15,7 +15,7 @@ import * as Email from "../../../email"; interface IProps extends Omit { id?: string; - fieldRef?: RefCallback | RefObject; + fieldRef?: Ref; value: string; autoFocus?: boolean; diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index cc22ba3c3a..321cb15ec3 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -10,7 +10,7 @@ import classNames from "classnames"; import { type InternationalisedPolicy, type Terms, type MatrixClient } from "matrix-js-sdk/src/matrix"; import { AuthType, type AuthDict, type IInputs, type IStageStatus } from "matrix-js-sdk/src/interactive-auth"; import { logger } from "matrix-js-sdk/src/logger"; -import React, { type ChangeEvent, createRef, type FormEvent, Fragment } from "react"; +import React, { type JSX, type ChangeEvent, createRef, type FormEvent, Fragment } from "react"; import { Button, Text } from "@vector-im/compound-web"; import PopOutIcon from "@vector-im/compound-design-tokens/assets/web/icons/pop-out"; diff --git a/src/components/views/auth/LanguageSelector.tsx b/src/components/views/auth/LanguageSelector.tsx index a3be10eeec..e8dbf0cf4e 100644 --- a/src/components/views/auth/LanguageSelector.tsx +++ b/src/components/views/auth/LanguageSelector.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React from "react"; +import React, { type JSX } from "react"; import SdkConfig from "../../../SdkConfig"; import { getCurrentLanguage } from "../../../languageHandler"; diff --git a/src/components/views/auth/LoginWithQRFlow.tsx b/src/components/views/auth/LoginWithQRFlow.tsx index b246013421..3a5a88f78d 100644 --- a/src/components/views/auth/LoginWithQRFlow.tsx +++ b/src/components/views/auth/LoginWithQRFlow.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { createRef, type ReactNode } from "react"; +import React, { type JSX, createRef, type ReactNode } from "react"; import { ClientRendezvousFailureReason, MSC4108FailureReason } from "matrix-js-sdk/src/rendezvous"; import ChevronLeftIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-left"; import CheckCircleSolidIcon from "@vector-im/compound-design-tokens/assets/web/icons/check-circle-solid"; diff --git a/src/components/views/auth/PassphraseConfirmField.tsx b/src/components/views/auth/PassphraseConfirmField.tsx index bd20b59929..4dc720e8af 100644 --- a/src/components/views/auth/PassphraseConfirmField.tsx +++ b/src/components/views/auth/PassphraseConfirmField.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type ComponentProps, PureComponent, type RefCallback, type RefObject } from "react"; +import React, { type ComponentProps, PureComponent, type Ref } from "react"; import Field, { type IInputProps } from "../elements/Field"; import withValidation, { type IFieldState, type IValidationResult } from "../elements/Validation"; @@ -14,7 +14,7 @@ import { _t, _td, type TranslationKey } from "../../../languageHandler"; interface IProps extends Omit { id?: string; - fieldRef?: RefCallback | RefObject; + fieldRef?: Ref; autoComplete?: string; value: string; password: string; // The password we're confirming diff --git a/src/components/views/auth/PassphraseField.tsx b/src/components/views/auth/PassphraseField.tsx index ce251d4d9b..938e559fd7 100644 --- a/src/components/views/auth/PassphraseField.tsx +++ b/src/components/views/auth/PassphraseField.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type ComponentProps, PureComponent, type RefCallback, type RefObject } from "react"; +import React, { type ComponentProps, PureComponent, type Ref } from "react"; import classNames from "classnames"; import type { ZxcvbnResult } from "@zxcvbn-ts/core"; @@ -22,7 +22,7 @@ interface IProps extends Omit { className?: string; minScore: 0 | 1 | 2 | 3 | 4; value: string; - fieldRef?: RefCallback | RefObject; + fieldRef?: Ref; // Additional strings such as a username used to catch bad passwords userInputs?: string[]; diff --git a/src/components/views/auth/PasswordLogin.tsx b/src/components/views/auth/PasswordLogin.tsx index d394f475a6..a3b77c60f4 100644 --- a/src/components/views/auth/PasswordLogin.tsx +++ b/src/components/views/auth/PasswordLogin.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type SyntheticEvent } from "react"; +import React, { type JSX, type SyntheticEvent } from "react"; import classNames from "classnames"; import { _t } from "../../../languageHandler"; @@ -427,7 +427,9 @@ export default class PasswordLogin extends React.PureComponent { disabled={this.props.busy} autoFocus={autoFocusPassword} onValidate={this.onPasswordValidate} - ref={(field) => (this[LoginField.Password] = field)} + ref={(field) => { + this[LoginField.Password] = field; + }} /> {forgotPasswordJsx} {!this.props.busy && ( diff --git a/src/components/views/auth/RegistrationForm.tsx b/src/components/views/auth/RegistrationForm.tsx index 44dfa64ba5..783c15ed3f 100644 --- a/src/components/views/auth/RegistrationForm.tsx +++ b/src/components/views/auth/RegistrationForm.tsx @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type BaseSyntheticEvent, type ComponentProps, type ReactNode } from "react"; +import React, { type JSX, type BaseSyntheticEvent, type ComponentProps, type ReactNode } from "react"; import { type MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; @@ -456,7 +456,9 @@ export default class RegistrationForm extends React.PureComponent (this[RegistrationField.Email] = field)} + fieldRef={(field) => { + this[RegistrationField.Email] = field; + }} label={emailLabel} value={this.state.email} validationRules={this.validateEmailRules.bind(this)} @@ -471,7 +473,9 @@ export default class RegistrationForm extends React.PureComponent (this[RegistrationField.Password] = field)} + fieldRef={(field) => { + this[RegistrationField.Password] = field; + }} minScore={PASSWORD_MIN_SCORE} value={this.state.password} onChange={this.onPasswordChange} @@ -486,7 +490,9 @@ export default class RegistrationForm extends React.PureComponent (this[RegistrationField.PasswordConfirm] = field)} + fieldRef={(field) => { + this[RegistrationField.PasswordConfirm] = field; + }} autoComplete="new-password" value={this.state.passwordConfirm} password={this.state.password} @@ -514,7 +520,9 @@ export default class RegistrationForm extends React.PureComponent (this[RegistrationField.PhoneNumber] = field)} + ref={(field) => { + this[RegistrationField.PhoneNumber] = field; + }} type="text" label={phoneLabel} value={this.state.phoneNumber} @@ -529,7 +537,9 @@ export default class RegistrationForm extends React.PureComponent (this[RegistrationField.Username] = field)} + ref={(field) => { + this[RegistrationField.Username] = field; + }} type="text" autoFocus={true} label={_t("common|username")} diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx index 89b89af12e..c4b17aea53 100644 --- a/src/components/views/avatars/DecoratedRoomAvatar.tsx +++ b/src/components/views/avatars/DecoratedRoomAvatar.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React from "react"; +import React, { type JSX } from "react"; import classNames from "classnames"; import { EventType, diff --git a/src/components/views/avatars/MemberAvatar.tsx b/src/components/views/avatars/MemberAvatar.tsx index a5bcc211e5..aed1e61180 100644 --- a/src/components/views/avatars/MemberAvatar.tsx +++ b/src/components/views/avatars/MemberAvatar.tsx @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { forwardRef, type ReactNode, type Ref, useContext } from "react"; +import React, { type JSX, forwardRef, type ReactNode, type Ref, useContext } from "react"; import { type RoomMember, type ResizeMethod } from "matrix-js-sdk/src/matrix"; import dis from "../../../dispatcher/dispatcher"; diff --git a/src/components/views/avatars/SearchResultAvatar.tsx b/src/components/views/avatars/SearchResultAvatar.tsx index c17dd21736..45c85717d2 100644 --- a/src/components/views/avatars/SearchResultAvatar.tsx +++ b/src/components/views/avatars/SearchResultAvatar.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React from "react"; +import React, { type JSX } from "react"; import { type RoomMember } from "matrix-js-sdk/src/matrix"; import emailPillAvatar from "../../../../res/img/icon-email-pill-avatar.svg"; diff --git a/src/components/views/avatars/WithPresenceIndicator.tsx b/src/components/views/avatars/WithPresenceIndicator.tsx index 4d60daf494..a08250a17c 100644 --- a/src/components/views/avatars/WithPresenceIndicator.tsx +++ b/src/components/views/avatars/WithPresenceIndicator.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type ReactNode, useEffect, useState } from "react"; +import React, { type JSX, type ReactNode, useEffect, useState } from "react"; import { ClientEvent, type Room, type RoomMember, RoomStateEvent, UserEvent } from "matrix-js-sdk/src/matrix"; import { Tooltip } from "@vector-im/compound-web"; diff --git a/src/components/views/beacon/index.tsx b/src/components/views/beacon/index.tsx index 7cbb6328bf..313e1145a9 100644 --- a/src/components/views/beacon/index.tsx +++ b/src/components/views/beacon/index.tsx @@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details. // Exports beacon components which touch maplibre-gs wrapped in React Suspense to enable code splitting -import React, { type ComponentProps, lazy, Suspense } from "react"; +import React, { type JSX, type ComponentProps, lazy, Suspense } from "react"; import Spinner from "../elements/Spinner"; diff --git a/src/components/views/context_menus/DialpadContextMenu.tsx b/src/components/views/context_menus/DialpadContextMenu.tsx index 12fc19781c..004c86ff72 100644 --- a/src/components/views/context_menus/DialpadContextMenu.tsx +++ b/src/components/views/context_menus/DialpadContextMenu.tsx @@ -6,8 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import * as React from "react"; -import { createRef } from "react"; +import React, { createRef } from "react"; import { type MatrixCall } from "matrix-js-sdk/src/webrtc/call"; import AccessibleButton, { type ButtonEvent } from "../elements/AccessibleButton"; @@ -24,7 +23,7 @@ interface IState { } export default class DialpadContextMenu extends React.Component { - private numberEntryFieldRef: React.RefObject = createRef(); + private numberEntryFieldRef = createRef(); public constructor(props: IProps) { super(props); diff --git a/src/components/views/context_menus/IconizedContextMenu.tsx b/src/components/views/context_menus/IconizedContextMenu.tsx index 4d74ed24b1..b6372a96c6 100644 --- a/src/components/views/context_menus/IconizedContextMenu.tsx +++ b/src/components/views/context_menus/IconizedContextMenu.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type ReactNode } from "react"; +import React, { type JSX, type ReactNode } from "react"; import classNames from "classnames"; import ContextMenu, { diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index 9997fad35b..76f6b31989 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { createRef, useContext } from "react"; +import React, { type JSX, createRef, useContext } from "react"; import { EventStatus, type MatrixEvent, @@ -130,8 +130,8 @@ export default class MessageContextMenu extends React.Component private reactButtonRef = createRef(); // XXX Ref to a functional component - public constructor(props: IProps, context: React.ContextType) { - super(props, context); + public constructor(props: IProps) { + super(props); this.state = { canRedact: false, diff --git a/src/components/views/context_menus/RoomGeneralContextMenu.tsx b/src/components/views/context_menus/RoomGeneralContextMenu.tsx index e6ba9cfc11..7c21d09853 100644 --- a/src/components/views/context_menus/RoomGeneralContextMenu.tsx +++ b/src/components/views/context_menus/RoomGeneralContextMenu.tsx @@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details. import { logger } from "matrix-js-sdk/src/logger"; import { type Room } from "matrix-js-sdk/src/matrix"; -import React, { useContext } from "react"; +import React, { type JSX, useContext } from "react"; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; import RoomListActions from "../../../actions/RoomListActions"; diff --git a/src/components/views/context_menus/RoomNotificationContextMenu.tsx b/src/components/views/context_menus/RoomNotificationContextMenu.tsx index 14e38f9e09..9844f27695 100644 --- a/src/components/views/context_menus/RoomNotificationContextMenu.tsx +++ b/src/components/views/context_menus/RoomNotificationContextMenu.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import { type Room } from "matrix-js-sdk/src/matrix"; -import React from "react"; +import React, { type JSX } from "react"; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; import { useNotificationState } from "../../../hooks/useRoomNotificationState"; diff --git a/src/components/views/context_menus/SpaceContextMenu.tsx b/src/components/views/context_menus/SpaceContextMenu.tsx index 17018da57c..eab9c1d011 100644 --- a/src/components/views/context_menus/SpaceContextMenu.tsx +++ b/src/components/views/context_menus/SpaceContextMenu.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { useContext } from "react"; +import React, { type JSX, useContext } from "react"; import { type Room, EventType, RoomType } from "matrix-js-sdk/src/matrix"; import { type IProps as IContextMenuProps } from "../../structures/ContextMenu"; diff --git a/src/components/views/context_menus/WidgetContextMenu.tsx b/src/components/views/context_menus/WidgetContextMenu.tsx index 2b13f636a6..627e0814e2 100644 --- a/src/components/views/context_menus/WidgetContextMenu.tsx +++ b/src/components/views/context_menus/WidgetContextMenu.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type ComponentProps, useContext } from "react"; +import React, { type JSX, type ComponentProps, useContext } from "react"; import { type ClientWidgetApi, type IWidget, MatrixCapabilities } from "matrix-widget-api"; import { logger } from "matrix-js-sdk/src/logger"; import { type ApprovalOpts, WidgetLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle"; diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index 2397c5f81e..c247c3aea9 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -1,12 +1,12 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024,2025 New Vector Ltd. Copyright 2021 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE files in the repository root for full details. */ -import React, { type ReactElement, type ReactNode, useContext, useMemo, useRef, useState } from "react"; +import React, { type ReactElement, type ReactNode, useContext, useId, useMemo, useRef, useState } from "react"; import classNames from "classnames"; import { type Room, EventType } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; @@ -53,8 +53,9 @@ export const Entry: React.FC<{ checked: boolean; onChange?(value: boolean): void; }> = ({ room, checked, onChange }) => { + const id = useId(); return ( -
  • {room?.isSpaceRoom() ? ( ) : ( @@ -62,11 +63,12 @@ export const Entry: React.FC<{ )} {room.name} onChange(e.currentTarget.checked) : undefined} checked={checked} disabled={!onChange} /> - +
  • ); }; @@ -357,6 +359,7 @@ const defaultRendererFactory =

    {_t(title)}

    { private unmounted: boolean; - private issueRef: React.RefObject; + private issueRef: React.RefObject; public constructor(props: BugReportDialogProps) { super(props); diff --git a/src/components/views/dialogs/BulkRedactDialog.tsx b/src/components/views/dialogs/BulkRedactDialog.tsx index ace7f82861..1b8bf9b07e 100644 --- a/src/components/views/dialogs/BulkRedactDialog.tsx +++ b/src/components/views/dialogs/BulkRedactDialog.tsx @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024,2025 New Vector Ltd. Copyright 2021 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial @@ -113,12 +113,13 @@ const BulkRedactDialog: React.FC = (props) => {

    {_t("user_info|redact|confirm_description_1", { count, user })}

    {_t("user_info|redact|confirm_description_2")}

    - setKeepStateEvents(e.target.checked)}> + setKeepStateEvents(e.target.checked)} + > {_t("user_info|redact|confirm_keep_state_label")} -
    - {_t("user_info|redact|confirm_keep_state_explainer")} -
    = ({ roomId, threadRootId, onFinished }) +
    ); diff --git a/src/components/views/dialogs/ExportDialog.tsx b/src/components/views/dialogs/ExportDialog.tsx index cfb6cca886..a1b6daa721 100644 --- a/src/components/views/dialogs/ExportDialog.tsx +++ b/src/components/views/dialogs/ExportDialog.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { useRef, useState, type Dispatch, type SetStateAction } from "react"; +import React, { type JSX, useRef, useState, type Dispatch, type SetStateAction } from "react"; import { type Room } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; diff --git a/src/components/views/dialogs/FeedbackDialog.tsx b/src/components/views/dialogs/FeedbackDialog.tsx index 27de625920..5e1efba38f 100644 --- a/src/components/views/dialogs/FeedbackDialog.tsx +++ b/src/components/views/dialogs/FeedbackDialog.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { useEffect, useRef, useState } from "react"; +import React, { type JSX, useEffect, useRef, useState } from "react"; import QuestionDialog from "./QuestionDialog"; import { _t } from "../../../languageHandler"; diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index d18b1bac01..512f0ad697 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { useEffect, useMemo, useState } from "react"; +import React, { type JSX, useEffect, useMemo, useState } from "react"; import classnames from "classnames"; import { type IContent, diff --git a/src/components/views/dialogs/GenericFeatureFeedbackDialog.tsx b/src/components/views/dialogs/GenericFeatureFeedbackDialog.tsx index cbfdb3c8b3..3967de7c4d 100644 --- a/src/components/views/dialogs/GenericFeatureFeedbackDialog.tsx +++ b/src/components/views/dialogs/GenericFeatureFeedbackDialog.tsx @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024,2025 New Vector Ltd. Copyright 2021 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial @@ -78,7 +78,6 @@ const GenericFeatureFeedbackDialog: React.FC = ({ }} autoFocus={true} /> - setCanContact((e.target as HTMLInputElement).checked)} diff --git a/src/components/views/dialogs/InteractiveAuthDialog.tsx b/src/components/views/dialogs/InteractiveAuthDialog.tsx index 5800153fed..96abb35ea4 100644 --- a/src/components/views/dialogs/InteractiveAuthDialog.tsx +++ b/src/components/views/dialogs/InteractiveAuthDialog.tsx @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React from "react"; +import React, { type JSX } from "react"; import { type MatrixClient } from "matrix-js-sdk/src/matrix"; import { type AuthType } from "matrix-js-sdk/src/interactive-auth"; diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index d98069fcf9..62536acfd6 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { createRef, type ReactNode, type SyntheticEvent } from "react"; +import React, { type JSX, createRef, type ReactNode, type SyntheticEvent } from "react"; import classNames from "classnames"; import { RoomMember, type Room, MatrixError, EventType } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; @@ -343,7 +343,7 @@ export default class InviteDialog extends React.PureComponent(); - private numberEntryFieldRef: React.RefObject = createRef(); + private numberEntryFieldRef = createRef(); private unmounted = false; private encryptionByDefault = false; private profilesStore: UserProfilesStore; diff --git a/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx b/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx index 7a2e3b35ef..0f16d3d6f6 100644 --- a/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx +++ b/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024,2025 New Vector Ltd. Copyright 2021 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial @@ -44,22 +44,23 @@ const Entry: React.FC<{ } return ( -