diff --git a/.github/workflows/element-web.yaml b/.github/workflows/element-web.yaml deleted file mode 100644 index 8ac5e2da94..0000000000 --- a/.github/workflows/element-web.yaml +++ /dev/null @@ -1,90 +0,0 @@ -# Produce a build of element-web with this version of react-sdk -# and any matching branches of element-web and js-sdk, output it -# as an artifact and run integration tests. -name: Element Web - Build -on: - pull_request: {} - merge_group: - types: [checks_requested] - push: - branches: [develop, master] - repository_dispatch: - types: [upstream-sdk-notify] - - # support triggering from other workflows - workflow_call: - inputs: - react-sdk-repository: - type: string - required: true - description: "The name of the github repository to check out and build." - - matrix-js-sdk-sha: - type: string - required: false - description: "The Git SHA of matrix-js-sdk to build against. By default, will use a matching branch name if it exists, or develop." - element-web-sha: - type: string - required: false - description: "The Git SHA of element-web to build against. By default, will use a matching branch name if it exists, or develop." - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }} - cancel-in-progress: true - -env: - # fetchdep.sh needs to know our PR number - PR_NUMBER: ${{ github.event.pull_request.number }} - -jobs: - build: - name: "Build Element-Web" - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - repository: ${{ inputs.react-sdk-repository || github.repository }} - - - uses: actions/setup-node@v4 - with: - cache: "yarn" - - - name: Fetch layered build - id: layered_build - env: - # tell layered.sh to check out the right sha of the JS-SDK & EW, if they were given one - JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }} - ELEMENT_WEB_GITHUB_BASE_REF: ${{ inputs.element-web-sha }} - run: | - scripts/ci/layered.sh - JSSDK_SHA=$(git -C matrix-js-sdk rev-parse --short=12 HEAD) - REACT_SHA=$(git rev-parse --short=12 HEAD) - VECTOR_SHA=$(git -C element-web rev-parse --short=12 HEAD) - echo "VERSION=$VECTOR_SHA-react-$REACT_SHA-js-$JSSDK_SHA" >> $GITHUB_OUTPUT - - - name: Copy config - run: cp element.io/develop/config.json config.json - working-directory: ./element-web - - - name: Build - env: - CI_PACKAGE: true - VERSION: "${{ steps.layered_build.outputs.VERSION }}" - run: | - yarn build - echo $VERSION > webapp/version - working-directory: ./element-web - - # Record the react-sdk sha so our Playwright tests are from the same sha - - name: Record react-sdk SHA - run: | - git rev-parse HEAD > element-web/webapp/sha - - - name: Upload Artifact - uses: actions/upload-artifact@v4 - with: - name: previewbuild - path: element-web/webapp - # We'll only use this in a triggered job, then we're done with it - retention-days: 1 diff --git a/.github/workflows/end-to-end-tests-netlify.yaml b/.github/workflows/end-to-end-tests-netlify.yaml new file mode 100644 index 0000000000..84c00b9f12 --- /dev/null +++ b/.github/workflows/end-to-end-tests-netlify.yaml @@ -0,0 +1,67 @@ +# Triggers after the playwright tests have finished, +# taking the artifact and uploading it to Netlify for easier viewing +name: Upload End to End Test report to Netlify +on: + workflow_run: + workflows: ["End to End Tests"] + types: + - completed + +concurrency: + group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }} + cancel-in-progress: ${{ github.event.workflow_run.event == 'pull_request' }} + +jobs: + report: + name: Report results + runs-on: ubuntu-latest + environment: Netlify + permissions: + statuses: write + deployments: write + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - uses: actions/setup-node@v4 + with: + cache: "yarn" + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Download blob reports from GitHub Actions Artifacts + uses: actions/download-artifact@v4 + with: + github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} + pattern: all-blob-reports-* + path: all-blob-reports + merge-multiple: true + + - name: Merge into HTML Report + run: yarn playwright merge-reports --reporter=html,github,./playwright/flaky-reporter.ts ./all-blob-reports + env: + # Only pass creds to the flaky-reporter on main branch runs + GITHUB_TOKEN: ${{ github.event.workflow_run.head_branch == 'develop' && secrets.ELEMENT_BOT_TOKEN || '' }} + + - name: Upload HTML report + uses: actions/upload-artifact@v4 + with: + name: html-report--attempt-${{ github.run_attempt }} + path: playwright-report + retention-days: 14 + + - name: 📤 Deploy to Netlify + uses: matrix-org/netlify-pr-preview@v3 + with: + path: playwright-report + owner: ${{ github.event.workflow_run.head_repository.owner.login }} + branch: ${{ github.event.workflow_run.head_branch }} + revision: ${{ github.event.workflow_run.head_sha }} + token: ${{ secrets.NETLIFY_AUTH_TOKEN }} + site_id: ${{ secrets.NETLIFY_SITE_ID }} + desc: Playwright Report + deployment_env: EndToEndTests + prefix: "e2e-" diff --git a/.github/workflows/end-to-end-tests.yaml b/.github/workflows/end-to-end-tests.yaml index 3228fe91b3..391197fb53 100644 --- a/.github/workflows/end-to-end-tests.yaml +++ b/.github/workflows/end-to-end-tests.yaml @@ -1,90 +1,121 @@ -# Triggers after the layered build has finished, taking the artifact and running Playwright on it +# Produce a build of element-web with this version of react-sdk +# and any matching branches of element-web and js-sdk, output it +# as an artifact and run end-to-end tests. name: End to End Tests on: - workflow_run: - workflows: ["Element Web - Build"] - types: - - completed + pull_request: {} + merge_group: + types: [checks_requested] + push: + branches: [develop, master] + repository_dispatch: + types: [upstream-sdk-notify] - # support calls from other workflows for downstream testing + # support triggering from other workflows workflow_call: inputs: + skip: + type: boolean + required: false + default: false + description: "A boolean to skip the playwright check itself while still creating the passing check. Useful when only running in Merge Queues." + react-sdk-repository: type: string required: true description: "The name of the github repository to check out and build." - secrets: - ELEMENT_BOT_TOKEN: - required: true + + matrix-js-sdk-sha: + type: string + required: false + description: "The Git SHA of matrix-js-sdk to build against. By default, will use a matching branch name if it exists, or develop." + element-web-sha: + type: string + required: false + description: "The Git SHA of element-web to build against. By default, will use a matching branch name if it exists, or develop." concurrency: - group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }} - cancel-in-progress: ${{ github.event.workflow_run.event == 'pull_request' }} + group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }} + cancel-in-progress: true + +env: + # fetchdep.sh needs to know our PR number + PR_NUMBER: ${{ github.event.pull_request.number }} jobs: - prepare: - name: Prepare - if: github.event.workflow_run.conclusion == 'success' + build: + name: "Build Element-Web" runs-on: ubuntu-latest - permissions: - statuses: write steps: - # We create the status here and then update it to success/failure in the `report` stage - # This provides an easy link to this workflow_run from the PR before the tests are done. - - uses: Sibz/github-status-action@071b5370da85afbb16637d6eed8524a06bc2053e # v1 + - name: Checkout code + uses: actions/checkout@v4 with: - authToken: ${{ secrets.GITHUB_TOKEN }} - state: pending - context: ${{ github.workflow }} / end-to-end-tests - sha: ${{ github.event.workflow_run.head_sha }} - target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} + repository: ${{ inputs.react-sdk-repository || github.repository }} - tests: + - uses: actions/setup-node@v4 + with: + cache: "yarn" + + - name: Fetch layered build + id: layered_build + env: + # tell layered.sh to check out the right sha of the JS-SDK & EW, if they were given one + JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }} + ELEMENT_WEB_GITHUB_BASE_REF: ${{ inputs.element-web-sha }} + run: | + scripts/ci/layered.sh + JSSDK_SHA=$(git -C matrix-js-sdk rev-parse --short=12 HEAD) + REACT_SHA=$(git rev-parse --short=12 HEAD) + VECTOR_SHA=$(git -C element-web rev-parse --short=12 HEAD) + echo "VERSION=$VECTOR_SHA-react-$REACT_SHA-js-$JSSDK_SHA" >> $GITHUB_OUTPUT + + - name: Copy config + run: cp element.io/develop/config.json config.json + working-directory: ./element-web + + - name: Build + env: + CI_PACKAGE: true + VERSION: "${{ steps.layered_build.outputs.VERSION }}" + run: | + yarn build + echo $VERSION > webapp/version + working-directory: ./element-web + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: webapp + path: element-web/webapp + retention-days: 1 + + playwright: name: "Run Tests ${{ matrix.runner }}/${{ strategy.job-total }}" - needs: prepare + needs: build + if: inputs.skip != true runs-on: ubuntu-latest permissions: actions: read issues: read pull-requests: read - environment: EndToEndTests strategy: fail-fast: false matrix: # Run multiple instances in parallel to speed up the tests runner: [1, 2, 3, 4, 5, 6, 7, 8] steps: - - name: 📥 Download artifact - uses: actions/download-artifact@v4 - with: - github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} - run-id: ${{ github.event.workflow_run.id }} - name: previewbuild - path: webapp - - # The workflow_run.head_sha is the sha of the head commit but the element-web was built using a simulated - # merge commit - https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request - # so use the sha from the tarball for the checkout of the tests - # to make sure we get a matching set of code and tests. - - name: Grab sha from webapp - id: sha - run: | - echo "sha=$(cat webapp/sha)" >> $GITHUB_OUTPUT - - uses: actions/checkout@v4 with: - # XXX: We're checking out untrusted code in a secure context - # We need to be careful to not trust anything this code outputs/may do - # - # Note that (in the absence of a `react-sdk-repository` input), - # we check out from the default repository, which is (for this workflow) the - # *target* repository for the pull request. - # - ref: ${{ steps.sha.outputs.sha }} persist-credentials: false path: matrix-react-sdk repository: ${{ inputs.react-sdk-repository || github.repository }} + - name: 📥 Download artifact + uses: actions/download-artifact@v4 + with: + name: webapp + path: webapp + - uses: actions/setup-node@v4 with: cache: "yarn" @@ -126,66 +157,11 @@ jobs: path: matrix-react-sdk/blob-report retention-days: 1 - report: - name: Report results - needs: tests - runs-on: ubuntu-latest - environment: Netlify + complete: + name: end-to-end-tests + needs: playwright if: always() - permissions: - statuses: write - deployments: write + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - with: - persist-credentials: false - repository: ${{ inputs.react-sdk-repository || github.repository }} - - - uses: actions/setup-node@v4 - with: - cache: "yarn" - - - name: Install dependencies - run: yarn install --frozen-lockfile - - - name: Download blob reports from GitHub Actions Artifacts - uses: actions/download-artifact@v4 - with: - pattern: all-blob-reports-* - path: all-blob-reports - merge-multiple: true - - - name: Merge into HTML Report - run: yarn playwright merge-reports --reporter=html,github,./playwright/flaky-reporter.ts ./all-blob-reports - env: - # Only pass creds to the flaky-reporter on main branch runs - GITHUB_TOKEN: ${{ github.event.workflow_run.head_branch == 'develop' && secrets.ELEMENT_BOT_TOKEN || '' }} - - - name: Upload HTML report - uses: actions/upload-artifact@v4 - with: - name: html-report--attempt-${{ github.run_attempt }} - path: playwright-report - retention-days: 14 - - - uses: Sibz/github-status-action@071b5370da85afbb16637d6eed8524a06bc2053e # v1 - if: always() - with: - authToken: ${{ secrets.GITHUB_TOKEN }} - state: ${{ needs.tests.result == 'success' && 'success' || 'failure' }} - context: ${{ github.workflow }} / end-to-end-tests - sha: ${{ github.event.workflow_run.head_sha }} - target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} - - - name: 📤 Deploy to Netlify - uses: matrix-org/netlify-pr-preview@v3 - with: - path: playwright-report - owner: ${{ github.event.workflow_run.head_repository.owner.login }} - branch: ${{ github.event.workflow_run.head_branch }} - revision: ${{ github.event.workflow_run.head_sha }} - token: ${{ secrets.NETLIFY_AUTH_TOKEN }} - site_id: ${{ secrets.NETLIFY_SITE_ID }} - desc: Playwright Report - deployment_env: EndToEndTests - prefix: "e2e-" + - run: exit 1 + if: failure() || cancelled() diff --git a/.github/workflows/netlify.yaml b/.github/workflows/netlify.yaml index 21c6f22df0..1fe6a9a2cc 100644 --- a/.github/workflows/netlify.yaml +++ b/.github/workflows/netlify.yaml @@ -3,7 +3,7 @@ name: Upload Preview Build to Netlify on: workflow_run: - workflows: ["Element Web - Build"] + workflows: ["End to End Tests"] types: - completed jobs: @@ -29,7 +29,7 @@ jobs: with: github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} run-id: ${{ github.event.workflow_run.id }} - name: previewbuild + name: webapp path: webapp - name: 📤 Deploy to Netlify diff --git a/CHANGELOG.md b/CHANGELOG.md index b21e44fd11..440239db81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,29 @@ +Changes in [3.98.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.98.0) (2024-04-23) +===================================================================================================== +## ✨ Features + +* Make empty state copy for TAC depend on the value of the setting ([#12419](https://github.com/matrix-org/matrix-react-sdk/pull/12419)). Contributed by @dbkr. +* Linkify User Interactive Authentication errors ([#12271](https://github.com/matrix-org/matrix-react-sdk/pull/12271)). Contributed by @t3chguy. +* Add support for device dehydration v2 ([#12316](https://github.com/matrix-org/matrix-react-sdk/pull/12316)). Contributed by @uhoreg. +* Replace `SecurityCustomisations` with `CryptoSetupExtension` ([#12342](https://github.com/matrix-org/matrix-react-sdk/pull/12342)). Contributed by @thoraj. +* Add activity toggle for TAC ([#12413](https://github.com/matrix-org/matrix-react-sdk/pull/12413)). Contributed by @dbkr. +* Humanize spell check language labels ([#12409](https://github.com/matrix-org/matrix-react-sdk/pull/12409)). Contributed by @t3chguy. +* Call Guest Access, give user the option to change the acces level so they can generate a call link. ([#12401](https://github.com/matrix-org/matrix-react-sdk/pull/12401)). Contributed by @toger5. +* TAC: Release Announcement ([#12380](https://github.com/matrix-org/matrix-react-sdk/pull/12380)). Contributed by @florianduros. +* Show the call and share button if the user can create a guest link. ([#12385](https://github.com/matrix-org/matrix-react-sdk/pull/12385)). Contributed by @toger5. +* Add analytics for mark all threads unread ([#12384](https://github.com/matrix-org/matrix-react-sdk/pull/12384)). Contributed by @dbkr. +* Add `EventType.RoomEncryption` to the auto approve capabilities of Element Call widgets ([#12386](https://github.com/matrix-org/matrix-react-sdk/pull/12386)). Contributed by @toger5. + +## 🐛 Bug Fixes + +* Fix link modal not shown after access upgrade ([#12411](https://github.com/matrix-org/matrix-react-sdk/pull/12411)). Contributed by @toger5. +* Fix thread navigation in timeline ([#12412](https://github.com/matrix-org/matrix-react-sdk/pull/12412)). Contributed by @florianduros. +* Fix inability to join a `knock` room via space hierarchy view ([#12404](https://github.com/matrix-org/matrix-react-sdk/pull/12404)). Contributed by @t3chguy. +* Focus the thread panel when clicking on an item in the TAC ([#12410](https://github.com/matrix-org/matrix-react-sdk/pull/12410)). Contributed by @dbkr. +* Fix space hierarchy tile busy state being stuck after join error ([#12405](https://github.com/matrix-org/matrix-react-sdk/pull/12405)). Contributed by @t3chguy. +* Fix room topic in-app links not being handled correctly on topic dialog ([#12406](https://github.com/matrix-org/matrix-react-sdk/pull/12406)). Contributed by @t3chguy. + + Changes in [3.97.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.97.0) (2024-04-09) ===================================================================================================== ## ✨ Features diff --git a/package.json b/package.json index 72cd0fee50..227f1f62c4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.97.0", + "version": "3.98.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -60,14 +60,14 @@ "lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'" }, "resolutions": { - "@types/react-dom": "17.0.21", - "@types/react": "17.0.68", + "@types/react-dom": "17.0.25", + "@types/react": "17.0.80", "oidc-client-ts": "3.0.1", "jwt-decode": "4.0.0" }, "dependencies": { "@babel/runtime": "^7.12.5", - "@matrix-org/analytics-events": "^0.19.0", + "@matrix-org/analytics-events": "^0.20.0", "@matrix-org/emojibase-bindings": "^1.1.2", "@matrix-org/matrix-wysiwyg": "2.17.0", "@matrix-org/olm": "3.2.15", @@ -76,7 +76,7 @@ "@sentry/browser": "^7.0.0", "@testing-library/react-hooks": "^8.0.1", "@vector-im/compound-design-tokens": "^1.2.0", - "@vector-im/compound-web": "^4.0.2", + "@vector-im/compound-web": "^4.1.0", "@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4", "@zxcvbn-ts/language-en": "^3.0.2", @@ -116,7 +116,7 @@ "opus-recorder": "^8.0.3", "pako": "^2.0.3", "png-chunks-extract": "^1.0.0", - "posthog-js": "1.126.0", + "posthog-js": "1.130.1", "proposal-temporal": "^0.9.0", "qrcode": "1.5.3", "re-resizable": "^6.9.0", @@ -176,9 +176,9 @@ "@types/node-fetch": "^2.6.2", "@types/pako": "^2.0.0", "@types/qrcode": "^1.3.5", - "@types/react": "17.0.68", + "@types/react": "17.0.80", "@types/react-beautiful-dnd": "^13.0.0", - "@types/react-dom": "17.0.21", + "@types/react-dom": "17.0.25", "@types/react-transition-group": "^4.4.0", "@types/sanitize-html": "2.11.0", "@types/sdp-transform": "^2.4.6", @@ -223,7 +223,7 @@ "stylelint-config-standard": "^36.0.0", "stylelint-scss": "^6.0.0", "ts-node": "^10.9.1", - "typescript": "5.4.3" + "typescript": "5.4.5" }, "peerDependencies": { "postcss": "^8.4.19", diff --git a/playwright/e2e/crypto/crypto.spec.ts b/playwright/e2e/crypto/crypto.spec.ts index 957be58711..323e1eb703 100644 --- a/playwright/e2e/crypto/crypto.spec.ts +++ b/playwright/e2e/crypto/crypto.spec.ts @@ -15,14 +15,17 @@ limitations under the License. */ import type { Page } from "@playwright/test"; -import { test, expect } from "../../element-web-test"; +import { expect, test } from "../../element-web-test"; import { + copyAndContinue, + createRoom, createSharedRoomWithUser, doTwoWaySasVerification, - copyAndContinue, enableKeyBackup, logIntoElement, logOutOfElement, + sendMessageInCurrentRoom, + verifySession, waitForVerificationRequest, } from "./utils"; import { Bot } from "../../pages/bot"; @@ -453,8 +456,8 @@ test.describe("Cryptography", function () { // no e2e icon await expect(lastTileE2eIcon).not.toBeVisible(); - // It can take up to 10 seconds for the key to be backed up. We don't really have much option other than - // to wait :/ + // Workaround for https://github.com/element-hq/element-web/issues/27267. It can take up to 10 seconds for + // the key to be backed up. await page.waitForTimeout(10000); /* log out, and back in */ @@ -532,4 +535,69 @@ test.describe("Cryptography", function () { ).not.toBeVisible(); }); }); + + test.describe("decryption failure messages", () => { + test("should handle device-relative historical messages", async ({ + homeserver, + page, + app, + credentials, + user, + cryptoBackend, + }) => { + test.skip(cryptoBackend === "legacy", "Not implemented for legacy crypto"); + test.setTimeout(60000); + + // Start with a logged-in session, without key backup, and send a message. + await createRoom(page, "Test room", true); + await sendMessageInCurrentRoom(page, "test test"); + + // Log out, discarding the key for the sent message. + await logOutOfElement(page, true); + + // Log in again, and see how the message looks. + await logIntoElement(page, homeserver, credentials); + await app.viewRoomByName("Test room"); + const lastTile = page.locator(".mx_EventTile").last(); + await expect(lastTile).toContainText("Historical messages are not available on this device"); + await expect(lastTile.locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible(); + + // Now, we set up key backup, and then send another message. + const secretStorageKey = await enableKeyBackup(app); + await app.viewRoomByName("Test room"); + await sendMessageInCurrentRoom(page, "test2 test2"); + + // Workaround for https://github.com/element-hq/element-web/issues/27267. It can take up to 10 seconds for + // the key to be backed up. + await page.waitForTimeout(10000); + + // Finally, log out again, and back in, skipping verification for now, and see what we see. + await logOutOfElement(page); + await logIntoElement(page, homeserver, credentials); + await page.locator(".mx_AuthPage").getByRole("button", { name: "Skip verification for now" }).click(); + await page.locator(".mx_AuthPage").getByRole("button", { name: "I'll verify later" }).click(); + await app.viewRoomByName("Test room"); + + // There should be two historical events in the timeline + const tiles = await page.locator(".mx_EventTile").all(); + expect(tiles.length).toBeGreaterThanOrEqual(2); + // look at the last two tiles only + for (const tile of tiles.slice(-2)) { + await expect(tile).toContainText("You need to verify this device for access to historical messages"); + await expect(tile.locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible(); + } + + // Now verify our device (setting up key backup), and check what happens + await verifySession(app, secretStorageKey); + const tilesAfterVerify = (await page.locator(".mx_EventTile").all()).slice(-2); + + // The first message still cannot be decrypted, because it was never backed up. It's now a regular UTD though. + await expect(tilesAfterVerify[0]).toContainText("Unable to decrypt message"); + await expect(tilesAfterVerify[0].locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible(); + + // The second message should now be decrypted, with a grey shield + await expect(tilesAfterVerify[1]).toContainText("test2 test2"); + await expect(tilesAfterVerify[1].locator(".mx_EventTile_e2eIcon_normal")).toBeVisible(); + }); + }); }); diff --git a/playwright/e2e/crypto/utils.ts b/playwright/e2e/crypto/utils.ts index d43e4c7f94..5b0bf29b97 100644 --- a/playwright/e2e/crypto/utils.ts +++ b/playwright/e2e/crypto/utils.ts @@ -14,15 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { type Page, expect, JSHandle } from "@playwright/test"; +import { expect, JSHandle, type Page } from "@playwright/test"; import type { CryptoEvent, ICreateRoomOpts, MatrixClient } from "matrix-js-sdk/src/matrix"; import type { + EmojiMapping, + ShowSasCallbacks, VerificationRequest, Verifier, - EmojiMapping, VerifierEvent, - ShowSasCallbacks, } from "matrix-js-sdk/src/crypto-api"; import { Credentials, HomeserverInstance } from "../../plugins/homeserver"; import { Client } from "../../pages/client"; @@ -148,7 +148,7 @@ export async function logIntoElement( // select homeserver await page.getByRole("button", { name: "Edit" }).click(); await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl); - await page.getByRole("button", { name: "Continue" }).click(); + await page.getByRole("button", { name: "Continue", exact: true }).click(); // wait for the dialog to go away await expect(page.locator(".mx_ServerPickerDialog")).not.toBeVisible(); @@ -167,15 +167,40 @@ export async function logIntoElement( } } -export async function logOutOfElement(page: Page) { +/** + * Click the "sign out" option in Element, and wait for the login page to load + * + * @param page - Playwright `Page` object. + * @param discardKeys - if true, expect a "You'll lose access to your encrypted messages" dialog, and dismiss it. + */ +export async function logOutOfElement(page: Page, discardKeys: boolean = false) { await page.getByRole("button", { name: "User menu" }).click(); await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Sign out" }).click(); - await page.locator(".mx_Dialog .mx_QuestionDialog").getByRole("button", { name: "Sign out" }).click(); + if (discardKeys) { + await page.getByRole("button", { name: "I don't want my encrypted messages" }).click(); + } else { + await page.locator(".mx_Dialog .mx_QuestionDialog").getByRole("button", { name: "Sign out" }).click(); + } // Wait for the login page to load await page.getByRole("heading", { name: "Sign in" }).click(); } +/** + * Open the security settings, and verify the current session using the security key. + * + * @param app - `ElementAppPage` wrapper for the playwright `Page`. + * @param securityKey - The security key (i.e., 4S key), set up during a previous session. + */ +export async function verifySession(app: ElementAppPage, securityKey: string) { + const settings = await app.settings.openUserSettings("Security & Privacy"); + await settings.getByRole("button", { name: "Verify this session" }).click(); + await app.page.getByRole("button", { name: "Verify with Security Key" }).click(); + await app.page.locator(".mx_Dialog").locator('input[type="password"]').fill(securityKey); + await app.page.getByRole("button", { name: "Continue", disabled: false }).click(); + await app.page.getByRole("button", { name: "Done" }).click(); +} + /** * Given a SAS verifier for a bot client: * - wait for the bot to receive the emojis @@ -289,4 +314,9 @@ export async function createRoom(page: Page, roomName: string, isEncrypted: bool } await dialog.getByRole("button", { name: "Create room" }).click(); + + // Wait for the client to process the encryption event before carrying on (and potentially sending events). + if (isEncrypted) { + await expect(page.getByText("Encryption enabled")).toBeVisible(); + } } diff --git a/playwright/e2e/read-receipts/editing-messages.spec.ts b/playwright/e2e/read-receipts/editing-messages.spec.ts index 49db3bdfbe..5005ad62bf 100644 --- a/playwright/e2e/read-receipts/editing-messages.spec.ts +++ b/playwright/e2e/read-receipts/editing-messages.spec.ts @@ -187,11 +187,11 @@ test.describe("Read receipts", () => { // Given we have read the thread await util.goTo(room1); await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]); - await util.assertUnread(room2, 2); + await util.assertUnread(room2, 1); await util.goTo(room2); await util.openThread("Msg1"); await util.assertRead(room2); - await util.backToThreadsList(); + await util.assertReadThread("Resp1"); await util.goTo(room1); // When a message inside it is edited @@ -202,6 +202,7 @@ test.describe("Read receipts", () => { await util.goTo(room2); await util.assertReadThread("Msg1"); }); + test("Reading an edit of a threaded message makes the room read", async ({ roomAlpha: room1, roomBeta: room2, @@ -211,11 +212,11 @@ test.describe("Read receipts", () => { // Given an edited thread message appears after we read it await util.goTo(room1); await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]); - await util.assertUnread(room2, 2); + await util.assertUnread(room2, 1); await util.goTo(room2); await util.openThread("Msg1"); await util.assertRead(room2); - await util.backToThreadsList(); + await util.assertReadThread("Resp1"); await util.goTo(room1); await util.receiveMessages(room2, [msg.editOf("Resp1", "Edit1")]); await util.assertStillRead(room2); @@ -228,6 +229,7 @@ test.describe("Read receipts", () => { await util.assertStillRead(room2); await util.assertReadThread("Msg1"); }); + test("Marking a room as read after an edit in a thread makes it read", async ({ roomAlpha: room1, roomBeta: room2, @@ -241,14 +243,16 @@ test.describe("Read receipts", () => { msg.threadedOff("Msg1", "Resp1"), msg.editOf("Resp1", "Edit1"), ]); - await util.assertUnread(room2, 2); + await util.assertUnread(room2, 1); // When I mark the room as read await util.markAsRead(room2); // Then it is read await util.assertRead(room2); + await util.assertReadThread("Msg1"); }); + test("Editing a thread message after marking as read leaves the room read", async ({ roomAlpha: room1, roomBeta: room2, @@ -258,7 +262,7 @@ test.describe("Read receipts", () => { // Given a room is marked as read await util.goTo(room1); await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]); - await util.assertUnread(room2, 2); + await util.assertUnread(room2, 1); await util.markAsRead(room2); await util.assertRead(room2); @@ -267,7 +271,9 @@ test.describe("Read receipts", () => { // Then the room remains read await util.assertStillRead(room2); + await util.assertReadThread("Msg1"); }); + test("A room with an edited threaded message is still read after restart", async ({ roomAlpha: room1, roomBeta: room2, @@ -287,6 +293,7 @@ test.describe("Read receipts", () => { // Then is it still read await util.assertRead(room2); }); + test("A room where all threaded edits are read is still read after restart", async ({ roomAlpha: room1, roomBeta: room2, @@ -295,20 +302,23 @@ test.describe("Read receipts", () => { }) => { await util.goTo(room1); await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]); - await util.assertUnread(room2, 2); + await util.assertUnread(room2, 1); await util.receiveMessages(room2, [msg.editOf("Resp1", "Edit1")]); - await util.assertUnread(room2, 2); + await util.assertUnread(room2, 1); await util.goTo(room2); await util.openThread("Msg1"); await util.assertRead(room2); + await util.assertReadThread("Msg1"); await util.goTo(room1); // Make sure we are looking at room1 after reload await util.assertStillRead(room2); await util.saveAndReload(); await util.assertRead(room2); + await util.assertReadThread("Msg1"); }); + test("A room where all threaded edits are marked as read is still read after restart", async ({ roomAlpha: room1, roomBeta: room2, @@ -321,15 +331,17 @@ test.describe("Read receipts", () => { msg.threadedOff("Msg1", "Resp1"), msg.editOf("Resp1", "Edit1"), ]); - await util.assertUnread(room2, 2); + await util.assertUnread(room2, 1); await util.markAsRead(room2); await util.assertRead(room2); + await util.assertReadThread("Msg1"); // When I restart await util.saveAndReload(); // It is still read await util.assertRead(room2); + await util.assertReadThread("Msg1"); }); }); @@ -343,7 +355,7 @@ test.describe("Read receipts", () => { // Given I have read a thread await util.goTo(room1); await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]); - await util.assertUnread(room2, 2); + await util.assertUnread(room2, 1); await util.goTo(room2); await util.openThread("Msg1"); await util.backToThreadsList(); @@ -361,6 +373,7 @@ test.describe("Read receipts", () => { await util.assertStillRead(room2); await util.assertReadThread("Edit1"); }); + test("Reading an edit of a thread root leaves the room read", async ({ roomAlpha: room1, roomBeta: room2, @@ -386,6 +399,7 @@ test.describe("Read receipts", () => { await util.goTo(room1); await util.assertStillRead(room2); }); + test("Editing a thread root after reading leaves the room read", async ({ roomAlpha: room1, roomBeta: room2, @@ -405,6 +419,7 @@ test.describe("Read receipts", () => { // Then the room stays read await util.assertStillRead(room2); }); + test("Marking a room as read after an edit of a thread root keeps it read", async ({ roomAlpha: room1, roomBeta: room2, @@ -431,6 +446,7 @@ test.describe("Read receipts", () => { await util.goTo(room1); await util.assertStillRead(room2); }); + test("Editing a thread root that is a reply after marking as read leaves the room read", async ({ roomAlpha: room1, roomBeta: room2, @@ -444,7 +460,7 @@ test.describe("Read receipts", () => { msg.replyTo("Msg", "Reply"), msg.threadedOff("Reply", "InThread"), ]); - await util.assertUnread(room2, 3); + await util.assertUnread(room2, 2); await util.markAsRead(room2); await util.assertRead(room2); @@ -458,6 +474,7 @@ test.describe("Read receipts", () => { await util.goTo(room2); await util.assertReadThread("Edited Reply"); }); + test("Marking a room as read after an edit of a thread root that is a reply leaves it read", async ({ roomAlpha: room1, roomBeta: room2, @@ -472,7 +489,7 @@ test.describe("Read receipts", () => { msg.threadedOff("Reply", "InThread"), ]); await util.receiveMessages(room2, [msg.editOf("Reply", "Edited Reply")]); - await util.assertUnread(room2, 3); + await util.assertUnread(room2, 2); // When I mark the room as read await util.markAsRead(room2); diff --git a/playwright/e2e/read-receipts/high-level.spec.ts b/playwright/e2e/read-receipts/high-level.spec.ts index 897e752ac4..e237afd64a 100644 --- a/playwright/e2e/read-receipts/high-level.spec.ts +++ b/playwright/e2e/read-receipts/high-level.spec.ts @@ -224,15 +224,15 @@ test.describe("Read receipts", () => { ...msg.manyThreadedOff("Root3", many("T", 20)), ]); await util.goTo(room2); - await util.assertUnread(room2, 60); + await util.assertRead(room2); + await util.assertUnreadThread("Root1"); + await util.assertUnreadThread("Root2"); + await util.assertUnreadThread("Root3"); await util.openThread("Root1"); - await util.assertUnread(room2, 40); await util.assertReadThread("Root1"); await util.openThread("Root2"); - await util.assertUnread(room2, 20); await util.assertReadThread("Root2"); await util.openThread("Root3"); - await util.assertRead(room2); await util.assertReadThread("Root3"); // When I restart and page up to load old thread roots @@ -247,6 +247,7 @@ test.describe("Read receipts", () => { await util.assertReadThread("Root2"); await util.assertReadThread("Root3"); }); + test("Paging up to find old threads that were never read keeps the room unread", async ({ cryptoBackend, roomAlpha: room1, @@ -268,7 +269,7 @@ test.describe("Read receipts", () => { ...many("Msg", 100), ]); await util.goTo(room2); - await util.assertUnread(room2, 6); + await util.assertRead(room2); await util.assertUnreadThread("Root1"); await util.assertUnreadThread("Root2"); await util.assertUnreadThread("Root3"); @@ -278,20 +279,21 @@ test.describe("Read receipts", () => { await util.goTo(room1); await util.saveAndReload(); - // Then the room remembers it's unread + // Then the room remembers it's read // TODO: I (andyb) think this will fall in an encrypted room - await util.assertUnread(room2, 6); + await util.assertRead(room2); // And when I page up to load old thread roots await util.goTo(room2); await util.pageUp(); - // Then the room remains unread - await util.assertUnread(room2, 6); + // Then the room remains read + await util.assertRead(room2); await util.assertUnreadThread("Root1"); await util.assertUnreadThread("Root2"); await util.assertUnreadThread("Root3"); }); + test("Looking in thread view to find old threads that were never read makes the room unread", async ({ roomAlpha: room1, roomBeta: room2, @@ -310,7 +312,7 @@ test.describe("Read receipts", () => { ...many("Msg", 100), ]); await util.goTo(room2); - await util.assertUnread(room2, 6); + await util.assertRead(room2); await util.assertUnreadThread("Root1"); await util.assertUnreadThread("Root2"); await util.assertUnreadThread("Root3"); @@ -320,20 +322,21 @@ test.describe("Read receipts", () => { await util.goTo(room1); await util.saveAndReload(); - // Then the room remembers it's unread + // Then the room remembers it's read // TODO: I (andyb) think this will fall in an encrypted room - await util.assertUnread(room2, 6); + await util.assertRead(room2); // And when I open the threads view await util.goTo(room2); await util.openThreadList(); - // Then the room remains unread - await util.assertUnread(room2, 6); + // Then the room remains read + await util.assertRead(room2); await util.assertUnreadThread("Root1"); await util.assertUnreadThread("Root2"); await util.assertUnreadThread("Root3"); }); + test("After marking room as read, paging up to find old threads that were never read leaves the room read", async ({ cryptoBackend, roomAlpha: room1, diff --git a/playwright/e2e/read-receipts/index.ts b/playwright/e2e/read-receipts/index.ts index 6b9a8381d2..4dd0450fb9 100644 --- a/playwright/e2e/read-receipts/index.ts +++ b/playwright/e2e/read-receipts/index.ts @@ -403,7 +403,7 @@ class Helpers { * tests we only open the threads panel.) */ async closeThreadsPanel() { - await this.page.locator(".mx_RightPanel").getByTitle("Close").click(); + await this.page.locator(".mx_RightPanel").getByLabel("Close").click(); await expect(this.page.locator(".mx_RightPanel")).not.toBeVisible(); } @@ -411,7 +411,7 @@ class Helpers { * Return to the list of threads, given we are viewing a single thread. */ async backToThreadsList() { - await this.page.locator(".mx_RightPanel").getByTitle("Threads").click(); + await this.page.locator(".mx_RightPanel").getByLabel("Threads").click(); } /** @@ -539,7 +539,7 @@ class Helpers { const threadPanel = this.page.locator(".mx_ThreadPanel"); await expect(threadPanel).toBeVisible(); await threadPanel.evaluate(($panel) => { - const $button = $panel.querySelector('.mx_BaseCard_back[title="Threads"]'); + const $button = $panel.querySelector('.mx_BaseCard_back[aria-label="Threads"]'); // If the Threads back button is present then click it - the // threads button can open either threads list or thread panel if ($button) { diff --git a/playwright/e2e/read-receipts/new-messages.spec.ts b/playwright/e2e/read-receipts/new-messages.spec.ts index 14434709ce..97308a4bb2 100644 --- a/playwright/e2e/read-receipts/new-messages.spec.ts +++ b/playwright/e2e/read-receipts/new-messages.spec.ts @@ -183,9 +183,13 @@ test.describe("Read receipts", () => { // When I receive a threaded message await util.receiveMessages(room2, [msg.threadedOff("Msg1", "Resp1")]); - // Then the room becomes unread - await util.assertUnread(room2, 1); + // Then the room stays read + await util.assertRead(room2); + // but the thread is unread + await util.goTo(room2); + await util.assertUnreadThread("Msg1"); }); + test("Reading the last threaded message makes the room read", async ({ roomAlpha: room1, roomBeta: room2, @@ -195,15 +199,16 @@ test.describe("Read receipts", () => { // Given a thread exists and is not read await util.goTo(room1); await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]); - await util.assertUnread(room2, 2); + await util.assertUnread(room2, 1); await util.goTo(room2); // When I read it await util.openThread("Msg1"); - // The room becomes read - await util.assertRead(room2); + // The thread becomes read + await util.assertReadThread("Msg1"); }); + test("Reading a thread message makes the thread read", async ({ roomAlpha: room1, roomBeta: room2, @@ -217,19 +222,20 @@ test.describe("Read receipts", () => { msg.threadedOff("Msg1", "Resp1"), msg.threadedOff("Msg1", "Resp2"), ]); - await util.assertUnread(room2, 3); // (Sanity) + await util.assertUnread(room2, 1); // (Sanity) // When I read the main timeline await util.goTo(room2); - // Then room does appear unread - await util.assertUnread(room2, 2); + // Then room is read + await util.assertRead(room2); - // Until we open the thread + // Reading the thread causes it to become read too await util.openThread("Msg1"); await util.assertReadThread("Msg1"); await util.assertRead(room2); }); + test("Reading an older thread message leaves the thread unread", async ({ roomAlpha: room1, roomBeta: room2, @@ -242,40 +248,19 @@ test.describe("Read receipts", () => { "ThreadRoot", ...msg.manyThreadedOff("ThreadRoot", many("InThread", 20)), ]); - await util.assertUnread(room2, 21); + await util.assertUnread(room2, 1); + await util.goTo(room2); + await util.assertUnreadThread("ThreadRoot"); + await util.goTo(room1); // When I read an older message in the thread await msg.jumpTo(room2.name, "InThread0000", true); - await util.assertUnreadLessThan(room2, 21); // Then the thread is still marked as unread await util.backToThreadsList(); await util.assertUnreadThread("ThreadRoot"); }); - test("Reading only one thread's message does not make the room read", async ({ - roomAlpha: room1, - roomBeta: room2, - util, - msg, - }) => { - // Given two threads are unread - await util.goTo(room1); - await util.receiveMessages(room2, [ - "Msg1", - msg.threadedOff("Msg1", "Resp1"), - "Msg2", - msg.threadedOff("Msg2", "Resp2"), - ]); - await util.assertUnread(room2, 4); - await util.goTo(room2); - await util.assertUnread(room2, 2); - // When I only read one of them - await util.openThread("Msg1"); - - // The room is still unread - await util.assertUnread(room2, 1); - }); test("Reading only one thread's message makes that thread read but not others", async ({ roomAlpha: room1, roomBeta: room2, @@ -290,9 +275,9 @@ test.describe("Read receipts", () => { msg.threadedOff("Msg1", "Resp1"), msg.threadedOff("Msg2", "Resp2"), ]); - await util.assertUnread(room2, 4); // (Sanity) + await util.assertUnread(room2, 2); // (Sanity) await util.goTo(room2); - await util.assertUnread(room2, 2); + await util.assertRead(room2); await util.assertUnreadThread("Msg1"); await util.assertUnreadThread("Msg2"); @@ -303,6 +288,7 @@ test.describe("Read receipts", () => { await util.assertReadThread("Msg1"); await util.assertUnreadThread("Msg2"); }); + test("Reading the main timeline does not mark a thread message as read", async ({ roomAlpha: room1, roomBeta: room2, @@ -316,15 +302,16 @@ test.describe("Read receipts", () => { msg.threadedOff("Msg1", "Resp1"), msg.threadedOff("Msg1", "Resp2"), ]); - await util.assertUnread(room2, 3); // (Sanity) + await util.assertUnread(room2, 1); // (Sanity) // When I read the main timeline await util.goTo(room2); - await util.assertUnread(room2, 2); + await util.assertRead(room2); // Then thread does appear unread await util.assertUnreadThread("Msg1"); }); + test("Marking a room with unread threads as read makes it read", async ({ roomAlpha: room1, roomBeta: room2, @@ -338,14 +325,17 @@ test.describe("Read receipts", () => { msg.threadedOff("Msg1", "Resp1"), msg.threadedOff("Msg1", "Resp2"), ]); - await util.assertUnread(room2, 3); // (Sanity) + await util.assertUnread(room2, 1); // (Sanity) // When I mark the room as read await util.markAsRead(room2); // Then the room is read await util.assertRead(room2); + // and so are the threads + await util.assertReadThread("Msg1"); }); + test("Sending a new thread message after marking as read makes it unread", async ({ roomAlpha: room1, roomBeta: room2, @@ -367,9 +357,11 @@ test.describe("Read receipts", () => { // Then another message appears in the thread await util.receiveMessages(room2, [msg.threadedOff("Msg1", "Resp3")]); - // Then the room becomes unread - await util.assertUnread(room2, 1); + // Then the thread becomes unread + await util.goTo(room2); + await util.assertUnreadThread("Msg1"); }); + test("Sending a new different-thread message after marking as read makes it unread", async ({ roomAlpha: room1, roomBeta: room2, @@ -381,11 +373,8 @@ test.describe("Read receipts", () => { await util.receiveMessages(room2, ["Thread1", "Thread2", msg.threadedOff("Thread1", "t1a")]); // Make sure the message in Thread 1 has definitely arrived, so that we know for sure // that the one in Thread 2 is the latest. - await util.assertUnread(room2, 3); await util.receiveMessages(room2, [msg.threadedOff("Thread2", "t2a")]); - // Make sure the 4th message has arrived before we mark as read. - await util.assertUnread(room2, 4); // When I mark the room as read (making an unthreaded receipt for t2a) await util.markAsRead(room2); @@ -394,9 +383,11 @@ test.describe("Read receipts", () => { // Then another message appears in the other thread await util.receiveMessages(room2, [msg.threadedOff("Thread1", "t1b")]); - // Then the room becomes unread - await util.assertUnread(room2, 1); + // Then the other thread becomes unread + await util.goTo(room2); + await util.assertUnreadThread("Thread1"); }); + test("A room with a new threaded message is still unread after restart", async ({ roomAlpha: room1, roomBeta: room2, @@ -410,21 +401,26 @@ test.describe("Read receipts", () => { msg.threadedOff("Msg1", "Resp1"), msg.threadedOff("Msg1", "Resp2"), ]); - await util.assertUnread(room2, 3); // (Sanity) + await util.assertUnread(room2, 1); // (Sanity) // When I read the main timeline await util.goTo(room2); - // Then room does appear unread - await util.assertUnread(room2, 2); + // Then room appears read + await util.assertRead(room2); + /// but with an unread thread + await util.assertUnreadThread("Msg1"); await util.saveAndReload(); - await util.assertUnread(room2, 2); - - // Until we open the thread - await util.openThread("Msg1"); await util.assertRead(room2); + await util.goTo(room2); + await util.assertUnreadThread("Msg1"); + + // Opening the thread now marks it as read + await util.openThread("Msg1"); + await util.assertReadThread("Msg1"); }); + test("A room where all threaded messages are read is still read after restart", async ({ roomAlpha: room1, roomBeta: room2, @@ -438,17 +434,20 @@ test.describe("Read receipts", () => { msg.threadedOff("Msg1", "Resp1"), msg.threadedOff("Msg1", "Resp2"), ]); - await util.assertUnread(room2, 3); // (Sanity) + await util.assertUnread(room2, 1); // (Sanity) await util.goTo(room2); - await util.assertUnread(room2, 2); - await util.openThread("Msg1"); await util.assertRead(room2); + await util.assertUnreadThread("Msg1"); + await util.openThread("Msg1"); + await util.assertReadThread("Msg1"); // When I restart await util.saveAndReload(); - // Then the room is still read + // Then the room & thread still read await util.assertRead(room2); + await util.goTo(room2); + await util.assertReadThread("Msg1"); }); }); @@ -462,15 +461,16 @@ test.describe("Read receipts", () => { // Given a thread exists await util.goTo(room1); await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]); - await util.assertUnread(room2, 2); // (Sanity) + await util.assertUnread(room2, 1); // (Sanity) // When I read the main timeline await util.goTo(room2); - // Then room does appear unread - await util.assertUnread(room2, 1); + // Then room doesn't appear unread but the thread does + await util.assertRead(room2); await util.assertUnreadThread("Msg1"); }); + test("Reading a thread root within the thread view marks it as read in the main timeline", async ({ roomAlpha: room1, roomBeta: room2, @@ -485,7 +485,7 @@ test.describe("Read receipts", () => { msg.threadedOff("ThreadRoot", "InThread"), ...many("afterThread", 30), ]); - await util.assertUnread(room2, 62); // Sanity + await util.assertUnread(room2, 61); // Sanity // When I jump to an old message and read the thread await msg.jumpTo(room2.name, "beforeThread0000"); @@ -496,6 +496,7 @@ test.describe("Read receipts", () => { // 30 remaining messages are unread - 7 messages are displayed under the thread root await util.assertUnread(room2, 30 - 7); }); + test("Creating a new thread based on a reply makes the room unread", async ({ roomAlpha: room1, roomBeta: room2, @@ -513,10 +514,12 @@ test.describe("Read receipts", () => { // When I receive a thread message created on the reply await util.receiveMessages(room2, [msg.threadedOff("Reply1", "Resp1")]); - // Then the room is unread - await util.assertUnread(room2, 1); + // Then the thread is unread + await util.goTo(room2); + await util.assertUnreadThread("Reply1"); }); - test("Reading a thread whose root is a reply makes the room read", async ({ + + test("Reading a thread whose root is a reply makes the thread read", async ({ roomAlpha: room1, roomBeta: room2, util, @@ -529,9 +532,9 @@ test.describe("Read receipts", () => { msg.replyTo("Msg1", "Reply1"), msg.threadedOff("Reply1", "Resp1"), ]); - await util.assertUnread(room2, 3); + await util.assertUnread(room2, 2); await util.goTo(room2); - await util.assertUnread(room2, 1); + await util.assertRead(room2); await util.assertUnreadThread("Reply1"); // When I read the thread diff --git a/playwright/e2e/read-receipts/reactions.spec.ts b/playwright/e2e/read-receipts/reactions.spec.ts index 1063c7d19e..69208e5fc9 100644 --- a/playwright/e2e/read-receipts/reactions.spec.ts +++ b/playwright/e2e/read-receipts/reactions.spec.ts @@ -107,10 +107,11 @@ test.describe("Read receipts", () => { await util.goTo(room1); await util.assertRead(room2); await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Reply1")]); - await util.assertUnread(room2, 2); + await util.assertUnread(room2, 1); await util.goTo(room2); await util.openThread("Msg1"); await util.assertRead(room2); + await util.assertReadThread("Msg1"); await util.goTo(room1); // When someone reacts to a thread message @@ -118,7 +119,9 @@ test.describe("Read receipts", () => { // Then the room remains read await util.assertStillRead(room2); + await util.assertReadThread("Msg1"); }); + test("Marking a room as read after a reaction in a thread makes it read", async ({ roomAlpha: room1, roomBeta: room2, @@ -133,7 +136,7 @@ test.describe("Read receipts", () => { msg.threadedOff("Msg1", "Reply1"), msg.reactionTo("Reply1", "🪿"), ]); - await util.assertUnread(room2, 2); + await util.assertUnread(room2, 1); // When I mark the room as read await util.markAsRead(room2); @@ -141,6 +144,7 @@ test.describe("Read receipts", () => { // Then it becomes read await util.assertRead(room2); }); + test("Reacting to a thread message after marking as read does not make the room unread", async ({ roomAlpha: room1, roomBeta: room2, @@ -155,7 +159,7 @@ test.describe("Read receipts", () => { msg.threadedOff("Msg1", "Reply1"), msg.reactionTo("Reply1", "🪿"), ]); - await util.assertUnread(room2, 2); + await util.assertUnread(room2, 1); await util.markAsRead(room2); await util.assertRead(room2); @@ -164,7 +168,10 @@ test.describe("Read receipts", () => { // Then the room remains read await util.assertStillRead(room2); + // as does the thread + await util.assertReadThread("Msg1"); }); + test("A room with a reaction to a threaded message is still unread after restart", async ({ roomAlpha: room1, roomBeta: room2, @@ -175,22 +182,25 @@ test.describe("Read receipts", () => { await util.goTo(room1); await util.assertRead(room2); await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Reply1")]); - await util.assertUnread(room2, 2); + await util.assertUnread(room2, 1); await util.goTo(room2); await util.openThread("Msg1"); await util.assertRead(room2); await util.goTo(room1); - // And someone reacted to it, which doesn't stop it being read + // And someone reacted to it, which doesn't make it read await util.receiveMessages(room2, [msg.reactionTo("Reply1", "🪿")]); await util.assertStillRead(room2); + await util.assertReadThread("Msg1"); // When I restart await util.saveAndReload(); // Then the room is still read await util.assertRead(room2); + await util.assertReadThread("Msg1"); }); + test("A room where all reactions in threads are read is still read after restart", async ({ roomAlpha: room1, roomBeta: room2, @@ -213,7 +223,7 @@ test.describe("Read receipts", () => { msg.reactionTo("Reply2b", "c"), msg.reactionTo("Reply1b", "t"), ]); - await util.assertUnread(room2, 6); + await util.assertUnread(room2, 2); await util.goTo(room2); await util.openThread("Msg1"); await util.assertReadThread("Msg1"); @@ -231,6 +241,7 @@ test.describe("Read receipts", () => { await util.assertReadThread("Msg1"); await util.assertReadThread("Msg2"); }); + test("Can remove a reaction in a thread", async ({ page, roomAlpha: room1, @@ -247,7 +258,7 @@ test.describe("Read receipts", () => { await util.goTo(room1); await util.assertRead(room2); await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Reply1a")]); - await util.assertUnread(room2, 2); + await util.assertUnread(room2, 1); // When I react to a thread message await util.goTo(room2); @@ -283,10 +294,11 @@ test.describe("Read receipts", () => { await util.goTo(room1); await util.assertRead(room2); await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Reply1")]); - await util.assertUnread(room2, 2); + await util.assertUnread(room2, 1); await util.goTo(room2); await util.openThread("Msg1"); await util.assertRead(room2); + await util.assertReadThread("Msg1"); // When someone reacts to it await util.goTo(room1); @@ -295,7 +307,10 @@ test.describe("Read receipts", () => { // Then the room is still read await util.assertRead(room2); + // as is the thread + await util.assertReadThread("Msg1"); }); + test("Reading a reaction to a thread root leaves the room read", async ({ page, roomAlpha: room1, @@ -307,7 +322,7 @@ test.describe("Read receipts", () => { await util.goTo(room1); await util.assertRead(room2); await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Reply1")]); - await util.assertUnread(room2, 2); + await util.assertUnread(room2, 1); await util.goTo(room2); await util.openThread("Msg1"); await util.assertRead(room2); @@ -316,6 +331,7 @@ test.describe("Read receipts", () => { await util.goTo(room1); await util.receiveMessages(room2, [msg.reactionTo("Msg1", "🪿")]); await util.assertRead(room2); + await util.assertReadThread("Msg1"); // When we read the reaction and go away again await util.goTo(room2); @@ -326,7 +342,9 @@ test.describe("Read receipts", () => { // Then the room is still read await util.assertRead(room2); + await util.assertReadThread("Msg1"); }); + test("Reacting to a thread root after marking as read makes the room unread but not the thread", async ({ page, roomAlpha: room1, @@ -338,11 +356,12 @@ test.describe("Read receipts", () => { await util.goTo(room1); await util.assertRead(room2); await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Reply1")]); - await util.assertUnread(room2, 2); + await util.assertUnread(room2, 1); // And we have marked the room as read await util.markAsRead(room2); await util.assertRead(room2); + await util.assertReadThread("Msg1"); // When someone reacts to it await util.receiveMessages(room2, [msg.reactionTo("Msg1", "🪿")]); @@ -350,6 +369,8 @@ test.describe("Read receipts", () => { // Then the room is still read await util.assertRead(room2); + // as is the thread + await util.assertReadThread("Msg1"); }); }); }); diff --git a/playwright/e2e/read-receipts/read-receipts.spec.ts b/playwright/e2e/read-receipts/read-receipts.spec.ts index 36f74e2c64..dac679f6a0 100644 --- a/playwright/e2e/read-receipts/read-receipts.spec.ts +++ b/playwright/e2e/read-receipts/read-receipts.spec.ts @@ -16,9 +16,10 @@ limitations under the License. import type { JSHandle } from "@playwright/test"; import type { MatrixEvent, ISendEventResponse, ReceiptType } from "matrix-js-sdk/src/matrix"; -import { test, expect } from "../../element-web-test"; +import { expect } from "../../element-web-test"; import { ElementAppPage } from "../../pages/ElementAppPage"; import { Bot } from "../../pages/bot"; +import { test } from "."; test.describe("Read receipts", () => { test.use({ @@ -189,29 +190,31 @@ test.describe("Read receipts", () => { page, app, bot, + util, }) => { // Given we sent 3 events on the main thread const main1 = await sendMessage(bot); const thread1a = await botSendThreadMessage(bot, main1.event_id); await botSendThreadMessage(bot, main1.event_id); - // 1 unread on the main thread, 2 in the new thread - await expect(page.getByLabel(`${otherRoomName} 3 unread messages.`)).toBeVisible(); + // 1 unread on the main thread, 2 in the new thread that aren't shown + await expect(page.getByLabel(`${otherRoomName} 1 unread message.`)).toBeVisible(); // When we send receipts for main, and the second-last in the thread await sendThreadedReadReceipt(app, main1); await sendThreadedReadReceipt(app, thread1a, main1); // Then the room has only one unread - the one in the thread - await expect(page.getByLabel(`${otherRoomName} 1 unread message.`)).toBeVisible(); + await util.goTo(otherRoomName); + await util.assertUnreadThread("Message 1"); }); - test("Considers room read if there are receipts for main and other thread", async ({ page, app, bot }) => { + test("Considers room read if there are receipts for main and other thread", async ({ page, app, bot, util }) => { // Given we sent 3 events on the main thread const main1 = await sendMessage(bot); await botSendThreadMessage(bot, main1.event_id); const thread1b = await botSendThreadMessage(bot, main1.event_id); - // 1 unread on the main thread, 2 in the new thread - await expect(page.getByLabel(`${otherRoomName} 3 unread messages.`)).toBeVisible(); + // 1 unread on the main thread, 2 in the new thread which don't show + await expect(page.getByLabel(`${otherRoomName} 1 unread message.`)).toBeVisible(); // When we send receipts for main, and the last in the thread await sendThreadedReadReceipt(app, main1); @@ -219,27 +222,33 @@ test.describe("Read receipts", () => { // Then the room has no unreads await expect(page.getByLabel(`${otherRoomName}`)).toBeVisible(); + await util.goTo(otherRoomName); + await util.assertReadThread("Message 1"); }); test("Recognises unread messages on a thread after receiving a unthreaded receipt for earlier ones", async ({ page, app, bot, + util, }) => { // Given we sent 3 events on the main thread const main1 = await sendMessage(bot); const thread1a = await botSendThreadMessage(bot, main1.event_id); await botSendThreadMessage(bot, main1.event_id); - // 1 unread on the main thread, 2 in the new thread - await expect(page.getByLabel(`${otherRoomName} 3 unread messages.`)).toBeVisible(); + // 1 unread on the main thread, 2 in the new thread which don't count + await expect(page.getByLabel(`${otherRoomName} 1 unread message.`)).toBeVisible(); // When we send an unthreaded receipt for the second-last in the thread await sendUnthreadedReadReceipt(app, thread1a); // Then the room has only one unread - the one in the // thread. The one in main is read because the unthreaded - // receipt is for a later event. - await expect(page.getByLabel(`${otherRoomName} 1 unread message.`)).toBeVisible(); + // receipt is for a later event. The room should therefore be + // read, and the thread unread. + await expect(page.getByLabel(`${otherRoomName}`)).toBeVisible(); + await util.goTo(otherRoomName); + await util.assertUnreadThread("Message 1"); }); test("Recognises unread messages on main after receiving a unthreaded receipt for a thread message", async ({ @@ -252,8 +261,8 @@ test.describe("Read receipts", () => { await botSendThreadMessage(bot, main1.event_id); const thread1b = await botSendThreadMessage(bot, main1.event_id); await sendMessage(bot); - // 2 unreads on the main thread, 2 in the new thread - await expect(page.getByLabel(`${otherRoomName} 4 unread messages.`)).toBeVisible(); + // 2 unreads on the main thread, 2 in the new thread which don't count + await expect(page.getByLabel(`${otherRoomName} 2 unread messages.`)).toBeVisible(); // When we send an unthreaded receipt for the last in the thread await sendUnthreadedReadReceipt(app, thread1b); diff --git a/playwright/e2e/read-receipts/redactions.spec.ts b/playwright/e2e/read-receipts/redactions.spec.ts index 1b5751acbc..f7affbed21 100644 --- a/playwright/e2e/read-receipts/redactions.spec.ts +++ b/playwright/e2e/read-receipts/redactions.spec.ts @@ -344,18 +344,23 @@ test.describe("Read receipts", () => { "Root2", msg.threadedOff("Root2", "Root2->A"), ]); - await util.assertUnread(room2, 5); + await util.assertUnread(room2, 2); - // And I have read them await util.goTo(room2); await util.assertUnreadThread("Root1"); + await util.assertUnreadThread("Root2"); + + // And I have read them + await util.assertUnreadThread("Root1"); await util.openThread("Root1"); - await util.assertUnreadLessThan(room2, 4); - await util.openThread("Root2"); await util.assertRead(room2); + await util.backToThreadsList(); + await util.assertReadThread("Root1"); + + await util.openThread("Root2"); + await util.assertReadThread("Root2"); await util.closeThreadsPanel(); await util.goTo(room1); - await util.assertRead(room2); // When the latest message in a thread is redacted await util.receiveMessages(room2, [msg.redactionOf("ThreadMsg2")]); @@ -365,6 +370,7 @@ test.describe("Read receipts", () => { await util.goTo(room2); await util.assertReadThread("Root1"); }); + test("Reading an unread thread after a redaction of the latest message makes it read", async ({ roomAlpha: room1, roomBeta: room2, @@ -378,9 +384,9 @@ test.describe("Read receipts", () => { msg.threadedOff("Root", "ThreadMsg1"), msg.threadedOff("Root", "ThreadMsg2"), ]); - await util.assertUnread(room2, 3); + await util.assertUnread(room2, 1); await util.receiveMessages(room2, [msg.redactionOf("ThreadMsg2")]); - await util.assertUnread(room2, 2); + await util.assertUnread(room2, 1); await util.goTo(room2); await util.assertUnreadThread("Root"); @@ -395,6 +401,7 @@ test.describe("Read receipts", () => { await util.goTo(room2); await util.assertReadThread("Root"); }); + test("Reading an unread thread after a redaction of the latest message makes it read after restart", async ({ roomAlpha: room1, roomBeta: room2, @@ -408,9 +415,9 @@ test.describe("Read receipts", () => { msg.threadedOff("Root", "ThreadMsg1"), msg.threadedOff("Root", "ThreadMsg2"), ]); - await util.assertUnread(room2, 3); + await util.assertUnread(room2, 1); await util.receiveMessages(room2, [msg.redactionOf("ThreadMsg2")]); - await util.assertUnread(room2, 2); + await util.assertUnread(room2, 1); await util.goTo(room2); await util.assertUnreadThread("Root"); await util.openThread("Root"); @@ -424,9 +431,12 @@ test.describe("Read receipts", () => { // When I restart await util.saveAndReload(); - // Then the room is still read + // Then the room and thread are still read await util.assertRead(room2); + await util.openThreadList(); + await util.assertReadThread("Root"); }); + test("Reading an unread thread after a redaction of an older message makes it read", async ({ roomAlpha: room1, roomBeta: room2, @@ -440,9 +450,9 @@ test.describe("Read receipts", () => { msg.threadedOff("Root", "ThreadMsg1"), msg.threadedOff("Root", "ThreadMsg2"), ]); - await util.assertUnread(room2, 3); + await util.assertUnread(room2, 1); await util.receiveMessages(room2, [msg.redactionOf("ThreadMsg1")]); - await util.assertUnread(room2, 2); + await util.assertUnread(room2, 1); await util.goTo(room2); await util.assertUnreadThread("Root"); @@ -457,6 +467,7 @@ test.describe("Read receipts", () => { await util.goTo(room2); await util.assertReadThread("Root"); }); + test("Marking an unread thread as read after a redaction makes it read", async ({ roomAlpha: room1, roomBeta: room2, @@ -470,9 +481,9 @@ test.describe("Read receipts", () => { msg.threadedOff("Root", "ThreadMsg1"), msg.threadedOff("Root", "ThreadMsg2"), ]); - await util.assertUnread(room2, 3); + await util.assertUnread(room2, 1); await util.receiveMessages(room2, [msg.redactionOf("ThreadMsg1")]); - await util.assertUnread(room2, 2); + await util.assertUnread(room2, 1); // When I mark the room as read await util.markAsRead(room2); @@ -483,6 +494,7 @@ test.describe("Read receipts", () => { await util.goTo(room2); await util.assertReadThread("Root"); }); + test("Sending and redacting a message after marking the thread as read leaves it read", async ({ roomAlpha: room1, roomBeta: room2, @@ -496,20 +508,22 @@ test.describe("Read receipts", () => { msg.threadedOff("Root", "ThreadMsg1"), msg.threadedOff("Root", "ThreadMsg2"), ]); - await util.assertUnread(room2, 3); + await util.assertUnread(room2, 1); await util.markAsRead(room2); await util.assertRead(room2); // When I send and redact a message await util.receiveMessages(room2, [msg.threadedOff("Root", "Msg3")]); - await util.assertUnread(room2, 1); + await util.goTo(room2); + await util.openThreadList(); + await util.assertUnreadThread("Root"); await util.receiveMessages(room2, [msg.redactionOf("Msg3")]); // Then the room and thread are read - await util.assertRead(room2); await util.goTo(room2); await util.assertReadThread("Root"); }); + test("Redacting a message after marking the thread as read leaves it read", async ({ roomAlpha: room1, roomBeta: room2, @@ -523,7 +537,7 @@ test.describe("Read receipts", () => { msg.threadedOff("Root", "ThreadMsg1"), msg.threadedOff("Root", "ThreadMsg2"), ]); - await util.assertUnread(room2, 3); + await util.assertUnread(room2, 1); await util.markAsRead(room2); await util.assertRead(room2); @@ -535,6 +549,7 @@ test.describe("Read receipts", () => { await util.goTo(room2); await util.assertReadThread("Root"); }); + test("Reacting to a redacted message leaves the thread read", async ({ roomAlpha: room1, roomBeta: room2, @@ -548,21 +563,27 @@ test.describe("Read receipts", () => { msg.threadedOff("Root", "Msg2"), msg.threadedOff("Root", "Msg3"), ]); - await util.assertUnread(room2, 3); - await util.receiveMessages(room2, [msg.redactionOf("Msg2")]); - await util.assertUnread(room2, 2); - await util.goTo(room2); await util.assertUnread(room2, 1); + await util.receiveMessages(room2, [msg.redactionOf("Msg2")]); + await util.assertUnread(room2, 1); + await util.goTo(room2); + await util.assertRead(room2); await util.openThread("Root"); await util.assertRead(room2); + await util.backToThreadsList(); + await util.assertReadThread("Root"); await util.goTo(room1); // When we receive a reaction to the redacted event await util.receiveMessages(room2, [msg.reactionTo("Msg2", "z")]); - // Then the room is unread + // Then the room is read await util.assertStillRead(room2); + await util.goTo(room2); + await util.openThreadList(); + await util.assertReadThread("Root"); }); + test("Editing a redacted message leaves the thread read", async ({ roomAlpha: room1, roomBeta: room2, @@ -576,13 +597,15 @@ test.describe("Read receipts", () => { msg.threadedOff("Root", "Msg2"), msg.threadedOff("Root", "Msg3"), ]); - await util.assertUnread(room2, 3); - await util.receiveMessages(room2, [msg.redactionOf("Msg2")]); - await util.assertUnread(room2, 2); - await util.goTo(room2); await util.assertUnread(room2, 1); - await util.openThread("Root"); + await util.receiveMessages(room2, [msg.redactionOf("Msg2")]); + await util.assertUnread(room2, 1); + await util.goTo(room2); await util.assertRead(room2); + await util.openThreadList(); + await util.assertUnreadThread("Root"); + await util.openThread("Root"); + await util.assertReadThread("Root"); await util.goTo(room1); // When we receive an edit of the redacted message @@ -590,7 +613,12 @@ test.describe("Read receipts", () => { // Then the room is unread await util.assertStillRead(room2); + // and so is the thread + await util.goTo(room2); + await util.openThreadList(); + await util.assertReadThread("Root"); }); + test("Reading a thread after a reaction to a redacted message marks the thread as read", async ({ roomAlpha: room1, roomBeta: room2, @@ -605,9 +633,9 @@ test.describe("Read receipts", () => { msg.threadedOff("Root", "Msg3"), msg.reactionTo("Msg3", "x"), ]); - await util.assertUnread(room2, 3); + await util.assertUnread(room2, 1); await util.receiveMessages(room2, [msg.redactionOf("Msg3")]); - await util.assertUnread(room2, 2); + await util.assertUnread(room2, 1); // When we read the thread await util.goTo(room2); @@ -617,6 +645,7 @@ test.describe("Read receipts", () => { await util.assertRead(room2); await util.assertReadThread("Root"); }); + test("Reading a thread containing a redacted, edited message marks the thread as read", async ({ roomAlpha: room1, roomBeta: room2, @@ -631,7 +660,7 @@ test.describe("Read receipts", () => { msg.threadedOff("Root", "Msg3"), msg.editOf("Msg3", "Msg3 Edited"), ]); - await util.assertUnread(room2, 3); + await util.assertUnread(room2, 1); await util.receiveMessages(room2, [msg.redactionOf("Msg3")]); // When we read the thread @@ -642,6 +671,7 @@ test.describe("Read receipts", () => { await util.assertRead(room2); await util.assertReadThread("Root"); }); + test("Reading a reply to a redacted message marks the thread as read", async ({ roomAlpha: room1, roomBeta: room2, @@ -656,7 +686,7 @@ test.describe("Read receipts", () => { msg.threadedOff("Root", "Msg3"), msg.replyTo("Msg3", "Msg3Reply"), ]); - await util.assertUnread(room2, 4); + await util.assertUnread(room2, 1); await util.receiveMessages(room2, [msg.redactionOf("Msg3")]); // When we read the thread, creating a receipt that points at the edit @@ -667,6 +697,7 @@ test.describe("Read receipts", () => { await util.assertRead(room2); await util.assertReadThread("Root"); }); + test("Reading a thread root when its only message has been redacted leaves the room read", async ({ roomAlpha: room1, roomBeta: room2, @@ -676,7 +707,7 @@ test.describe("Read receipts", () => { // Given we had a thread await util.goTo(room1); await util.receiveMessages(room2, ["Root", msg.threadedOff("Root", "Msg2")]); - await util.assertUnread(room2, 2); + await util.assertUnread(room2, 1); // And then redacted the message that makes it a thread await util.receiveMessages(room2, [msg.redactionOf("Msg2")]); @@ -687,7 +718,11 @@ test.describe("Read receipts", () => { // Then the room is read await util.assertRead(room2); + // and that thread is read + await util.openThreadList(); + await util.assertReadThread("Root"); }); + test("A thread with a redacted unread is still read after restart", async ({ roomAlpha: room1, roomBeta: room2, @@ -701,13 +736,13 @@ test.describe("Read receipts", () => { msg.threadedOff("Root", "ThreadMsg1"), msg.threadedOff("Root", "ThreadMsg2"), ]); - await util.assertUnread(room2, 3); + await util.assertUnread(room2, 1); await util.goTo(room2); await util.openThread("Root"); await util.assertRead(room2); await util.assertReadThread("Root"); await util.receiveMessages(room2, [msg.threadedOff("Root", "Msg3")]); - await util.assertUnread(room2, 1); + await util.assertRead(room2); await util.receiveMessages(room2, [msg.redactionOf("Msg3")]); await util.assertRead(room2); await util.goTo(room2); @@ -722,7 +757,13 @@ test.describe("Read receipts", () => { await util.goTo(room2); await util.assertReadThread("Root"); }); - test("A thread with a read redaction is still read after restart", async ({ + + /* + * Disabled: this doesn't seem to work as, at some point after syncing from cache, the redaction and redacted + * event get removed from the thread timeline such that we have no record of the events that the read receipt + * points to. I suspect this may have been passing by fluke before. + */ + test.skip("A thread with a read redaction is still read after restart", async ({ roomAlpha: room1, roomBeta: room2, util, @@ -737,11 +778,11 @@ test.describe("Read receipts", () => { "Root2", msg.threadedOff("Root2", "Root2->A"), ]); - await util.assertUnread(room2, 5); + await util.assertUnread(room2, 2); await util.goTo(room2); await util.assertUnreadThread("Root1"); await util.openThread("Root1"); - await util.assertUnreadLessThan(room2, 4); + await util.assertRead(room2); await util.openThread("Root2"); await util.assertRead(room2); await util.closeThreadsPanel(); @@ -757,7 +798,12 @@ test.describe("Read receipts", () => { // Then the room is still read await util.assertRead(room2); + // and so is the thread + await util.openThreadList(); + await util.assertReadThread("Root1"); + await util.assertReadThread("Root2"); }); + test("A thread with an unread reply to a redacted message is still unread after restart", async ({ roomAlpha: room1, roomBeta: room2, @@ -772,7 +818,7 @@ test.describe("Read receipts", () => { msg.threadedOff("Root", "Msg3"), msg.replyTo("Msg3", "Msg3Reply"), ]); - await util.assertUnread(room2, 4); + await util.assertUnread(room2, 1); await util.receiveMessages(room2, [msg.redactionOf("Msg3")]); // And we have read all this @@ -788,6 +834,7 @@ test.describe("Read receipts", () => { await util.assertRead(room2); await util.assertReadThread("Root"); }); + test("A thread with a read reply to a redacted message is still read after restart", async ({ roomAlpha: room1, roomBeta: room2, @@ -802,7 +849,7 @@ test.describe("Read receipts", () => { msg.threadedOff("Root", "Msg3"), msg.replyTo("Msg3", "Msg3Reply"), ]); - await util.assertUnread(room2, 4); + await util.assertUnread(room2, 1); await util.receiveMessages(room2, [msg.redactionOf("Msg3")]); // And I read it, so the room is read @@ -836,7 +883,7 @@ test.describe("Read receipts", () => { msg.threadedOff("Root", "Msg2"), msg.threadedOff("Root", "Msg3"), ]); - await util.assertUnread(room2, 3); + await util.assertUnread(room2, 1); await util.goTo(room2); await util.openThread("Root"); await util.assertRead(room2); @@ -848,7 +895,12 @@ test.describe("Read receipts", () => { // Then the room is still read await util.assertStillRead(room2); }); - test("Redacting a thread root still allows us to read the thread", async ({ + + /* + * Disabled for the same reason as "A thread with a read redaction is still read after restart" + * above + */ + test.skip("Redacting a thread root still allows us to read the thread", async ({ roomAlpha: room1, roomBeta: room2, util, @@ -861,23 +913,24 @@ test.describe("Read receipts", () => { msg.threadedOff("Root", "Msg2"), msg.threadedOff("Root", "Msg3"), ]); - await util.assertUnread(room2, 3); + await util.assertUnread(room2, 1); // When someone redacts the thread root await util.receiveMessages(room2, [msg.redactionOf("Root")]); // Then the room is still unread - await util.assertUnread(room2, 2); + await util.assertUnread(room2, 1); // And I can open the thread and read it await util.goTo(room2); - await util.assertUnread(room2, 2); + await util.assertRead(room2); // The redacted message gets collapsed into, "foo was invited, joined and removed a message" await util.openCollapsedMessage(1); await util.openThread("Message deleted"); await util.assertRead(room2); await util.assertReadThread("Root"); }); + test("Sending a threaded message onto a redacted thread root leaves the room unread", async ({ roomAlpha: room1, roomBeta: room2, @@ -891,7 +944,7 @@ test.describe("Read receipts", () => { msg.threadedOff("Root", "Msg2"), msg.threadedOff("Root", "Msg3"), ]); - await util.assertUnread(room2, 3); + await util.assertUnread(room2, 1); await util.goTo(room2); await util.openThread("Root"); await util.assertRead(room2); @@ -901,11 +954,12 @@ test.describe("Read receipts", () => { // When we receive a new message on it await util.receiveMessages(room2, [msg.threadedOff("Root", "Msg4")]); - // Then the room and thread are unread - await util.assertUnread(room2, 1); + // Then the room is read but the thread is unread + await util.assertRead(room2); await util.goTo(room2); await util.assertUnreadThread("Message deleted"); }); + test("Reacting to a redacted thread root leaves the room read", async ({ roomAlpha: room1, roomBeta: room2, @@ -919,7 +973,7 @@ test.describe("Read receipts", () => { msg.threadedOff("Root", "Msg2"), msg.threadedOff("Root", "Msg3"), ]); - await util.assertUnread(room2, 3); + await util.assertUnread(room2, 1); await util.goTo(room2); await util.openThread("Root"); await util.assertRead(room2); @@ -931,7 +985,9 @@ test.describe("Read receipts", () => { // Then the room is still read await util.assertRead(room2); + await util.assertReadThread("Root"); }); + test("Editing a redacted thread root leaves the room read", async ({ roomAlpha: room1, roomBeta: room2, @@ -945,7 +1001,7 @@ test.describe("Read receipts", () => { msg.threadedOff("Root", "Msg2"), msg.threadedOff("Root", "Msg3"), ]); - await util.assertUnread(room2, 3); + await util.assertUnread(room2, 1); await util.goTo(room2); await util.openThread("Root"); await util.assertRead(room2); @@ -957,7 +1013,10 @@ test.describe("Read receipts", () => { // Then the room is still read await util.assertRead(room2); + // as is the thread + await util.assertReadThread("Root"); }); + test("Replying to a redacted thread root makes the room unread", async ({ roomAlpha: room1, roomBeta: room2, @@ -971,7 +1030,7 @@ test.describe("Read receipts", () => { msg.threadedOff("Root", "Msg2"), msg.threadedOff("Root", "Msg3"), ]); - await util.assertUnread(room2, 3); + await util.assertUnread(room2, 1); await util.goTo(room2); await util.openThread("Root"); await util.assertRead(room2); @@ -984,6 +1043,7 @@ test.describe("Read receipts", () => { // Then the room is unread await util.assertUnread(room2, 1); }); + test("Reading a reply to a redacted thread root makes the room read", async ({ roomAlpha: room1, roomBeta: room2, @@ -998,7 +1058,7 @@ test.describe("Read receipts", () => { msg.threadedOff("Root", "Msg2"), msg.threadedOff("Root", "Msg3"), ]); - await util.assertUnread(room2, 3); + await util.assertUnread(room2, 1); await util.goTo(room2); await util.openThread("Root"); await util.assertRead(room2); diff --git a/playwright/e2e/spaces/threads-activity-centre/index.ts b/playwright/e2e/spaces/threads-activity-centre/index.ts index 7ad477541a..8bafe2e804 100644 --- a/playwright/e2e/spaces/threads-activity-centre/index.ts +++ b/playwright/e2e/spaces/threads-activity-centre/index.ts @@ -341,7 +341,7 @@ export class Helpers { */ assertThreadPanelFocused() { return expect( - this.page.locator(".mx_ThreadPanel").locator(".mx_BaseCard_header").getByTitle("Close"), + this.page.locator(".mx_ThreadPanel").locator(".mx_BaseCard_header").getByLabel("Close"), ).toBeFocused(); } diff --git a/playwright/e2e/threads/threads.spec.ts b/playwright/e2e/threads/threads.spec.ts index 5e32516646..9b5ea46511 100644 --- a/playwright/e2e/threads/threads.spec.ts +++ b/playwright/e2e/threads/threads.spec.ts @@ -495,14 +495,14 @@ test.describe("Threads", () => { await createThread("Hello again Mr. Bot", "Hello again Mr. User in a thread"); // Open thread panel - await page.getByRole("button", { name: "Threads" }).click(); + await page.getByTestId("threadsButton").click(); const threadPanel = page.locator(".mx_ThreadPanel"); await expect( threadPanel.locator(".mx_EventTile_last").getByText("Hello again Mr. User in a thread"), ).toBeVisible(); // Open threads list - await threadPanel.getByRole("button", { name: "Threads" }).click(); + await page.locator(".mx_BaseCard_back").click(); const rightPanel = page.locator(".mx_RightPanel"); // Check that the threads are listed await expect(rightPanel.locator(".mx_EventTile").getByText("Hello Mr. User in a thread")).toBeVisible(); diff --git a/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/window-12px-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/window-12px-linux.png index 23b77fd751..ee9eca2283 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/window-12px-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/window-12px-linux.png differ diff --git a/playwright/snapshots/spaces/spaces.spec.ts/space-panel-collapsed-linux.png b/playwright/snapshots/spaces/spaces.spec.ts/space-panel-collapsed-linux.png index 32e664808e..66b8af0e5b 100644 Binary files a/playwright/snapshots/spaces/spaces.spec.ts/space-panel-collapsed-linux.png and b/playwright/snapshots/spaces/spaces.spec.ts/space-panel-collapsed-linux.png differ diff --git a/playwright/snapshots/spaces/spaces.spec.ts/space-panel-expanded-linux.png b/playwright/snapshots/spaces/spaces.spec.ts/space-panel-expanded-linux.png index a2edd3d88f..6f55f2fd00 100644 Binary files a/playwright/snapshots/spaces/spaces.spec.ts/space-panel-expanded-linux.png and b/playwright/snapshots/spaces/spaces.spec.ts/space-panel-expanded-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-layout-linux.png index 45c43f06fe..98ec9e0cf6 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-layout-linux.png differ diff --git a/res/css/views/auth/_LoginWithQR.pcss b/res/css/views/auth/_LoginWithQR.pcss index 6a112c7c82..c4904952b6 100644 --- a/res/css/views/auth/_LoginWithQR.pcss +++ b/res/css/views/auth/_LoginWithQR.pcss @@ -32,36 +32,10 @@ limitations under the License. margin-top: $spacing-8; } - .mx_LoginWithQR_separator { - display: flex; - align-items: center; - text-align: center; - - &::before, - &::after { - content: ""; - flex: 1; - border-bottom: 1px solid $quinary-content; - } - - &:not(:empty) { - &::before { - margin-right: 1em; - } - &::after { - margin-left: 1em; - } - } - } - font-size: $font-15px; } .mx_UserSettingsDialog .mx_LoginWithQR { - .mx_AccessibleButton + .mx_AccessibleButton { - margin-left: $spacing-12; - } - font: var(--cpd-font-body-md-regular); h1 { @@ -69,18 +43,14 @@ limitations under the License. margin-bottom: 0; } - li { - line-height: 1.8; + h2 { + margin-top: $spacing-24; } .mx_QRCode { margin: $spacing-28 0; } - .mx_LoginWithQR_buttons { - text-align: center; - } - .mx_LoginWithQR_qrWrapper { display: flex; } @@ -91,12 +61,6 @@ limitations under the License. display: flex; flex-direction: column; - .mx_LoginWithQR_centreTitle { - h1 { - text-align: center; - } - } - h1 > svg { &.normal { color: $secondary-content; @@ -137,11 +101,69 @@ limitations under the License. } ol { - list-style-position: inside; padding-inline-start: 0; + list-style: none; /* list markers do not support the outlined number styling we need */ - li::marker { - color: $accent; + li { + position: relative; + padding-left: var(--cpd-space-7x); + color: 1px solid $input-placeholder; + margin-bottom: var(--cpd-space-4x); + line-height: 20px; + text-align: initial; + } + + /* Circled number list item marker */ + li::before { + content: counter(list-item); + position: absolute; + left: 0; + display: inline-block; + width: 20px; + height: 20px; + line-height: 20px; + border-radius: 50%; + border: 1px solid $input-placeholder; + box-sizing: border-box; + text-align: center; + } + } + + label[for="mx_LoginWithQR_checkCode"] { + margin-top: var(--cpd-space-6x); + color: var(--cpd-color-text-primary); + margin-bottom: var(--cpd-space-1x); + } + + .mx_LoginWithQR_icon { + width: 56px; + height: 56px; + border-radius: 8px; + box-sizing: border-box; + padding: var(--cpd-space-3x); + gap: 10px; + + background-color: var(--cpd-color-bg-success-subtle); + svg { + color: var(--cpd-color-icon-success-primary); + } + + &.mx_LoginWithQR_icon--critical { + background-color: var(--cpd-color-bg-critical-subtle); + svg { + color: var(--cpd-color-icon-critical-primary); + } + } + } + + .mx_LoginWithQR_checkCode_input { + margin-bottom: var(--cpd-space-1x); + text-align: initial; + + input { + /* Workaround for one of the input rules in _common.pcss being not specific enough */ + padding: 0; + padding-inline-start: calc(40px / 2 - (1ch / 2)); } } @@ -164,13 +186,39 @@ limitations under the License. .mx_LoginWithQR_breadcrumbs { font-size: $font-13px; - color: var(--cpd-color-text-secondary); + color: $secondary-content; } .mx_LoginWithQR_main { display: flex; flex-direction: column; flex-grow: 1; + align-items: center; + color: $primary-content; + text-align: center; + + p { + color: $secondary-content; + } + } + + &.mx_LoginWithQR_error .mx_LoginWithQR_main { + max-width: 400px; + margin: 0 auto; + } + + .mx_LoginWithQR_buttons { + display: flex; + flex-direction: column; + align-items: center; + gap: $spacing-16; + margin-top: var(--cpd-space-6x); + + .mx_AccessibleButton { + width: 300px; + height: 48px; + box-sizing: border-box; + } } .mx_QRCode { diff --git a/src/DecryptionFailureTracker.ts b/src/DecryptionFailureTracker.ts index 9914704550..c842e55ec4 100644 --- a/src/DecryptionFailureTracker.ts +++ b/src/DecryptionFailureTracker.ts @@ -58,6 +58,8 @@ export class DecryptionFailureTracker { case DecryptionFailureCode.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED: case DecryptionFailureCode.HISTORICAL_MESSAGE_WORKING_BACKUP: return "HistoricalMessage"; + case DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED: + return "ExpectedDueToMembership"; default: return "UnknownError"; } diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 07ff3c7178..d14003dbfa 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -73,22 +73,44 @@ export interface IMatrixClientCreds { * you'll find a `MatrixClient` hanging on the `MatrixClientPeg`. */ export interface IMatrixClientPeg { + /** + * The opts used to start the client + */ opts: IStartClientOpts; /** * Return the server name of the user's homeserver * Throws an error if unable to deduce the homeserver name - * (eg. if the user is not logged in) + * (e.g. if the user is not logged in) * * @returns {string} The homeserver name, if present. */ getHomeserverName(): string; + /** + * Get the current MatrixClient, if any + */ get(): MatrixClient | null; + + /** + * Get the current MatrixClient, throwing an error if there isn't one + */ safeGet(): MatrixClient; + + /** + * Unset the current MatrixClient + */ unset(): void; - assign(): Promise; - start(): Promise; + + /** + * Prepare the MatrixClient for use, including initialising the store and crypto, but do not start it + */ + assign(): Promise; + + /** + * Prepare the MatrixClient for use, including initialising the store and crypto, and start it + */ + start(): Promise; /** * If we've registered a user ID we set this to the ID of the @@ -235,7 +257,7 @@ class MatrixClientPegClass implements IMatrixClientPeg { PlatformPeg.get()?.reload(); }; - public async assign(): Promise { + public async assign(): Promise { if (!this.matrixClient) { throw new Error("createClient must be called first"); } @@ -273,17 +295,9 @@ class MatrixClientPegClass implements IMatrixClientPeg { opts.threadSupport = true; if (SettingsStore.getValue("feature_sliding_sync")) { - const proxyUrl = SettingsStore.getValue("feature_sliding_sync_proxy_url"); - if (proxyUrl) { - logger.log("Activating sliding sync using proxy at ", proxyUrl); - } else { - logger.log("Activating sliding sync"); - } - opts.slidingSync = SlidingSyncManager.instance.configure( - this.matrixClient, - proxyUrl || this.matrixClient.baseUrl, - ); - SlidingSyncManager.instance.startSpidering(100, 50); // 100 rooms at a time, 50ms apart + opts.slidingSync = await SlidingSyncManager.instance.setup(this.matrixClient); + } else { + SlidingSyncManager.instance.checkSupport(this.matrixClient); } // Connect the matrix client to the dispatcher and setting handlers @@ -362,7 +376,7 @@ class MatrixClientPegClass implements IMatrixClientPeg { } } - public async start(): Promise { + public async start(): Promise { const opts = await this.assign(); logger.log(`MatrixClientPeg: really starting MatrixClient`); diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 20c4ff2f33..f8518d925b 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -313,7 +313,7 @@ export class PosthogAnalytics { // No point identifying again return; } - if (this.posthog.persistence?.get_user_state() === "identified") { + if (this.posthog.persistence?.get_property("$user_state") === "identified") { // Analytics ID has changed, reset as Posthog will refuse to merge in this case this.posthog.reset(); } diff --git a/src/SlidingSyncManager.ts b/src/SlidingSyncManager.ts index 5f459c0b9e..e3f420b43d 100644 --- a/src/SlidingSyncManager.ts +++ b/src/SlidingSyncManager.ts @@ -44,7 +44,7 @@ limitations under the License. * list ops) */ -import { MatrixClient, EventType } from "matrix-js-sdk/src/matrix"; +import { MatrixClient, EventType, AutoDiscovery, Method, timeoutSignal } from "matrix-js-sdk/src/matrix"; import { MSC3575Filter, MSC3575List, @@ -56,6 +56,9 @@ import { 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; @@ -323,4 +326,93 @@ export class SlidingSyncManager { firstTime = false; } } + + /** + * Set up the Sliding Sync instance; configures the end point and starts spidering. + * The sliding sync endpoint is derived the following way: + * 1. The user-defined sliding sync proxy URL (legacy, for backwards compatibility) + * 2. The client `well-known` sliding sync proxy URL [declared at the unstable prefix](https://github.com/matrix-org/matrix-spec-proposals/blob/kegan/sync-v3/proposals/3575-sync.md#unstable-prefix) + * 3. The homeserver base url (for native server support) + * @param client The MatrixClient to use + * @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 clientWellKnown = await AutoDiscovery.findClientConfig(client.baseUrl); + proxyUrl = clientWellKnown?.["org.matrix.msc3575.proxy"]?.url; + } catch (e) { + // client.baseUrl is invalid, `AutoDiscovery.findClientConfig` has thrown + } + + if (proxyUrl != undefined) { + logger.log("getProxyFromWellKnown: client well-known declares sliding sync proxy at", proxyUrl); + } + return proxyUrl; + } + + /** + * Check if the server "natively" supports sliding sync (at the unstable endpoint). + * @param client The MatrixClient to use + * @return Whether the "native" (unstable) endpoint is up + */ + public async nativeSlidingSyncSupport(client: MatrixClient): Promise { + try { + await client.http.authedRequest(Method.Post, "/sync", undefined, undefined, { + localTimeoutMs: 10 * 1000, // 10s + prefix: "/_matrix/client/unstable/org.matrix.msc3575", + }); + } catch (e) { + return false; // 404, M_UNRECOGNIZED + } + + logger.log("nativeSlidingSyncSupport: sliding sync endpoint is up"); + return true; // 200, OK + } + + /** + * Check whether our homeserver has sliding sync support, that the endpoint is up, and + * is a sliding sync endpoint. + * + * Sets static member `SlidingSyncController.serverSupportsSlidingSync` + * @param client The MatrixClient to use + */ + public async checkSupport(client: MatrixClient): Promise { + if (await this.nativeSlidingSyncSupport(client)) { + SlidingSyncController.serverSupportsSlidingSync = true; + return; + } + + const proxyUrl = await this.getProxyFromWellKnown(client); + if (proxyUrl != undefined) { + const response = await fetch(proxyUrl + "/client/server.json", { + 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; + } + } + } } diff --git a/src/accessibility/context_menu/ContextMenuButton.tsx b/src/accessibility/context_menu/ContextMenuButton.tsx index 6ef6afef37..35dd986f8e 100644 --- a/src/accessibility/context_menu/ContextMenuButton.tsx +++ b/src/accessibility/context_menu/ContextMenuButton.tsx @@ -28,15 +28,15 @@ type Props = ComponentProps export const ContextMenuButton = forwardRef(function ( - { label, isExpanded, children, onClick, onContextMenu, ...props }: Props, + { label, isExpanded, children, onClick, onContextMenu, element, ...props }: Props, ref: Ref, ) { return ( = ComponentProps export const ContextMenuTooltipButton = forwardRef(function ( - { isExpanded, children, onClick, onContextMenu, ...props }: Props, + { isExpanded, children, onClick, onContextMenu, element, ...props }: Props, ref: Ref, ) { return ( { label?: string; - tooltip?: string; } // Semantic component for representing a role=menuitem -export const MenuItem: React.FC = ({ children, label, tooltip, ...props }) => { +export const MenuItem: React.FC = ({ children, label, ...props }) => { const ariaLabel = props["aria-label"] || label; - if (tooltip) { - return ( - - {children} - - ); - } - return ( {children} diff --git a/src/accessibility/roving/RovingAccessibleButton.tsx b/src/accessibility/roving/RovingAccessibleButton.tsx index 56c9052714..01e126824d 100644 --- a/src/accessibility/roving/RovingAccessibleButton.tsx +++ b/src/accessibility/roving/RovingAccessibleButton.tsx @@ -34,12 +34,14 @@ export const RovingAccessibleButton = ({ onFocus, onMouseOver, focusOnMouseOver, + element, ...props }: Props): JSX.Element => { const [onFocusInternal, isActive, ref] = useRovingTabIndex(inputRef); return ( { onFocusInternal(); onFocus?.(event); diff --git a/src/accessibility/roving/RovingAccessibleTooltipButton.tsx b/src/accessibility/roving/RovingAccessibleTooltipButton.tsx index 5607089c6e..76927c1773 100644 --- a/src/accessibility/roving/RovingAccessibleTooltipButton.tsx +++ b/src/accessibility/roving/RovingAccessibleTooltipButton.tsx @@ -16,27 +16,26 @@ limitations under the License. import React, { ComponentProps } from "react"; -import AccessibleTooltipButton from "../../components/views/elements/AccessibleTooltipButton"; import { useRovingTabIndex } from "../RovingTabIndex"; import { Ref } from "./types"; +import AccessibleButton from "../../components/views/elements/AccessibleButton"; -type Props = Omit< - ComponentProps>, - "tabIndex" -> & { +type Props = Omit>, "tabIndex"> & { inputRef?: Ref; }; -// Wrapper to allow use of useRovingTabIndex for simple AccessibleTooltipButtons outside of React Functional Components. +// Wrapper to allow use of useRovingTabIndex for simple AccessibleButtons outside of React Functional Components. export const RovingAccessibleTooltipButton = ({ inputRef, onFocus, + element, ...props }: Props): JSX.Element => { const [onFocusInternal, isActive, ref] = useRovingTabIndex(inputRef); return ( - { onFocusInternal(); onFocus?.(event); diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index 586cc442d6..f8dbccf28c 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -54,7 +54,6 @@ import dis from "../../dispatcher/dispatcher"; import { Action } from "../../dispatcher/actions"; import Timer from "../../utils/Timer"; import shouldHideEvent from "../../shouldHideEvent"; -import { arrayFastClone } from "../../utils/arrays"; import MessagePanel from "./MessagePanel"; import { IScrollState } from "./ScrollPanel"; import { ActionPayload } from "../../dispatcher/payloads"; @@ -1751,15 +1750,11 @@ export class TimelinePanel extends React.Component { [...mainEvents], ); - // `arrayFastClone` performs a shallow copy of the array - // we want the last event to be decrypted first but displayed last - // `reverse` is destructive and unfortunately mutates the "events" array - arrayFastClone(events) - .reverse() - .forEach((event) => { - const client = MatrixClientPeg.safeGet(); - client.decryptEventIfNeeded(event); - }); + // We want the last event to be decrypted first + const client = MatrixClientPeg.safeGet(); + for (let i = events.length - 1; i >= 0; --i) { + client.decryptEventIfNeeded(events[i]); + } const firstVisibleEventIndex = this.checkForPreJoinUISI(events); diff --git a/src/components/views/auth/LoginWithQR-types.ts b/src/components/views/auth/LoginWithQR-types.ts new file mode 100644 index 0000000000..33f709d79c --- /dev/null +++ b/src/components/views/auth/LoginWithQR-types.ts @@ -0,0 +1,43 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * The intention of this enum is to have a mode that scans a QR code instead of generating one. + */ +export enum Mode { + /** + * A QR code with be generated and shown + */ + Show = "show", +} + +export enum Phase { + Loading, + ShowingQR, + Connecting, + Connected, + WaitingForDevice, + Verifying, + Error, +} + +export enum Click { + Cancel, + Decline, + Approve, + TryAgain, + Back, +} diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 1e2efb5106..feb869deaf 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -24,34 +24,7 @@ import { HTTPError, MatrixClient } from "matrix-js-sdk/src/matrix"; import { _t } from "../../../languageHandler"; import { wrapRequestWithDialog } from "../../../utils/UserInteractiveAuth"; import LoginWithQRFlow from "./LoginWithQRFlow"; - -/** - * The intention of this enum is to have a mode that scans a QR code instead of generating one. - */ -export enum Mode { - /** - * A QR code with be generated and shown - */ - Show = "show", -} - -export enum Phase { - Loading, - ShowingQR, - Connecting, - Connected, - WaitingForDevice, - Verifying, - Error, -} - -export enum Click { - Cancel, - Decline, - Approve, - TryAgain, - Back, -} +import { Click, Mode, Phase } from "./LoginWithQR-types"; interface IProps { client: MatrixClient; diff --git a/src/components/views/auth/LoginWithQRFlow.tsx b/src/components/views/auth/LoginWithQRFlow.tsx index 05c8d95c42..b5daddbe0c 100644 --- a/src/components/views/auth/LoginWithQRFlow.tsx +++ b/src/components/views/auth/LoginWithQRFlow.tsx @@ -1,5 +1,5 @@ /* -Copyright 2022 The Matrix.org Foundation C.I.C. +Copyright 2022 - 2024 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,19 +14,24 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; -import { RendezvousFailureReason } from "matrix-js-sdk/src/rendezvous"; +import React, { ReactNode } from "react"; +import { RendezvousFailureReason as LegacyRendezvousFailureReason } from "matrix-js-sdk/src/rendezvous"; import { Icon as ChevronLeftIcon } from "@vector-im/compound-design-tokens/icons/chevron-left.svg"; +import { Icon as CheckCircleSolidIcon } from "@vector-im/compound-design-tokens/icons/check-circle-solid.svg"; +import { Icon as ErrorIcon } from "@vector-im/compound-design-tokens/icons/error.svg"; +import { Heading, Text } from "@vector-im/compound-web"; +import classNames from "classnames"; import { _t } from "../../../languageHandler"; import AccessibleButton from "../elements/AccessibleButton"; import QRCode from "../elements/QRCode"; import Spinner from "../elements/Spinner"; import { Icon as InfoIcon } from "../../../../res/img/element-icons/i.svg"; -import { Click, FailureReason, LoginWithQRFailureReason, Phase } from "./LoginWithQR"; +import { Click, Phase } from "./LoginWithQR-types"; import SdkConfig from "../../../SdkConfig"; +import { FailureReason, LoginWithQRFailureReason } from "./LoginWithQR"; -interface IProps { +interface Props { phase: Phase; code?: string; onClick(type: Click): Promise; @@ -39,8 +44,8 @@ interface IProps { * * This uses the unstable feature of MSC3906: https://github.com/matrix-org/matrix-spec-proposals/pull/3906 */ -export default class LoginWithQRFlow extends React.Component { - public constructor(props: IProps) { +export default class LoginWithQRFlow extends React.Component { + public constructor(props: Props) { super(props); } @@ -72,49 +77,75 @@ export default class LoginWithQRFlow extends React.Component { let main: JSX.Element | undefined; let buttons: JSX.Element | undefined; let backButton = true; - let cancellationMessage: string | undefined; - let centreTitle = false; + let className = ""; switch (this.props.phase) { - case Phase.Error: + case Phase.Error: { + let success = false; + let title: string | undefined; + let message: ReactNode | undefined; + switch (this.props.failureReason) { - case RendezvousFailureReason.Expired: - cancellationMessage = _t("auth|qr_code_login|error_linking_incomplete"); + case LegacyRendezvousFailureReason.UnsupportedAlgorithm: + case LegacyRendezvousFailureReason.UnsupportedTransport: + case LegacyRendezvousFailureReason.HomeserverLacksSupport: + title = _t("auth|qr_code_login|error_unsupported_protocol_title"); + message = _t("auth|qr_code_login|error_unsupported_protocol"); break; - case RendezvousFailureReason.InvalidCode: - cancellationMessage = _t("auth|qr_code_login|error_invalid_scanned_code"); + + case LegacyRendezvousFailureReason.UserCancelled: + title = _t("auth|qr_code_login|error_user_cancelled_title"); + message = _t("auth|qr_code_login|error_user_cancelled"); break; - case RendezvousFailureReason.UnsupportedAlgorithm: - cancellationMessage = _t("auth|qr_code_login|error_device_unsupported"); + + case LegacyRendezvousFailureReason.Expired: + title = _t("auth|qr_code_login|error_expired_title"); + message = _t("auth|qr_code_login|error_expired"); break; - case RendezvousFailureReason.UserDeclined: - cancellationMessage = _t("auth|qr_code_login|error_request_declined"); + + case LegacyRendezvousFailureReason.InvalidCode: + title = _t("auth|qr_code_login|error_insecure_channel_detected_title"); + message = ( + <> + {_t("auth|qr_code_login|error_insecure_channel_detected")} + + + {_t("auth|qr_code_login|error_insecure_channel_detected_instructions")} + +
    +
  1. {_t("auth|qr_code_login|error_insecure_channel_detected_instructions_1")}
  2. +
  3. {_t("auth|qr_code_login|error_insecure_channel_detected_instructions_2")}
  4. +
  5. {_t("auth|qr_code_login|error_insecure_channel_detected_instructions_3")}
  6. +
+ + ); break; - case RendezvousFailureReason.OtherDeviceAlreadySignedIn: - cancellationMessage = _t("auth|qr_code_login|error_device_already_signed_in"); + + case LegacyRendezvousFailureReason.OtherDeviceAlreadySignedIn: + success = true; + title = _t("auth|qr_code_login|error_other_device_already_signed_in_title"); + message = _t("auth|qr_code_login|error_other_device_already_signed_in"); break; - case RendezvousFailureReason.OtherDeviceNotSignedIn: - cancellationMessage = _t("auth|qr_code_login|error_device_not_signed_in"); - break; - case RendezvousFailureReason.UserCancelled: - cancellationMessage = _t("auth|qr_code_login|error_request_cancelled"); + + case LegacyRendezvousFailureReason.UserDeclined: + title = _t("auth|qr_code_login|error_user_declined_title"); + message = _t("auth|qr_code_login|error_user_declined"); break; + case LoginWithQRFailureReason.RateLimited: - cancellationMessage = _t("auth|qr_code_login|error_rate_limited"); - break; - case RendezvousFailureReason.Unknown: - cancellationMessage = _t("auth|qr_code_login|error_unexpected"); - break; - case RendezvousFailureReason.HomeserverLacksSupport: - cancellationMessage = _t("auth|qr_code_login|error_homeserver_lacks_support"); + title = _t("error|something_went_wrong"); + message = _t("auth|qr_code_login|error_rate_limited"); break; + + case LegacyRendezvousFailureReason.OtherDeviceNotSignedIn: + case LegacyRendezvousFailureReason.Unknown: default: - cancellationMessage = _t("auth|qr_code_login|error_request_cancelled"); + title = _t("error|something_went_wrong"); + message = _t("auth|qr_code_login|error_unexpected"); break; } - centreTitle = true; + className = "mx_LoginWithQR_error"; backButton = false; - main =

{cancellationMessage}

; buttons = ( <> { {this.cancelButton()} ); + main = ( + <> +
+ {success ? : } +
+ + {title} + + {typeof message === "object" ? message :

{message}

} + + ); break; + } case Phase.Connected: backButton = false; main = ( @@ -145,13 +192,6 @@ export default class LoginWithQRFlow extends React.Component { buttons = ( <> - - {_t("action|cancel")} - { > {_t("action|approve")} + + {_t("action|cancel")} + ); break; case Phase.ShowingQR: if (this.props.code) { - const code = ( -
- -
- ); + const data = Buffer.from(this.props.code ?? ""); + main = ( <> -

{_t("auth|qr_code_login|scan_code_instruction")}

- {code} + + {_t("auth|qr_code_login|scan_code_instruction")} + +
+ +
  1. {_t("auth|qr_code_login|open_element_other_device", { @@ -209,30 +254,27 @@ export default class LoginWithQRFlow extends React.Component { buttons = this.cancelButton(); break; case Phase.Verifying: - centreTitle = true; main = this.simpleSpinner(_t("auth|qr_code_login|completing_setup")); break; } return ( -
    -
    - {backButton ? ( -
    - - - -
    - {_t("settings|sessions|title")} / {_t("settings|sessions|sign_in_with_qr")} -
    +
    + {backButton ? ( +
    + + + +
    + {_t("settings|sessions|title")} / {_t("settings|sessions|sign_in_with_qr")}
    - ) : null} -
    +
    + ) : null}
    {main}
    {buttons}
    diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index d59e23fe4c..7ff95edce3 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -41,8 +41,6 @@ import { avatarUrlForUser } from "../../../Avatar"; import EventTile from "../rooms/EventTile"; import SearchBox from "../../structures/SearchBox"; import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; -import { Alignment } from "../elements/Tooltip"; -import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; import NotificationBadge from "../rooms/NotificationBadge"; @@ -54,7 +52,7 @@ import EntityTile from "../rooms/EntityTile"; import BaseAvatar from "../avatars/BaseAvatar"; import { Action } from "../../../dispatcher/actions"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; -import { ButtonEvent } from "../elements/AccessibleButton"; +import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; import { isLocationEvent } from "../../../utils/EventUtils"; import { isSelfLocation, locationEventGeoUri } from "../../../utils/location"; import { RoomContextDetails } from "../rooms/RoomContextDetails"; @@ -159,11 +157,11 @@ const Entry: React.FC> = ({ room, type, content, matrixClient: onFocus={onFocus} id={id} > - @@ -171,20 +169,20 @@ const Entry: React.FC> = ({ room, type, content, matrixClient: {room.name} - - +
    {_t("forward|send_label")}
    {icon} -
    +
    ); }; diff --git a/src/components/views/dialogs/SlidingSyncOptionsDialog.tsx b/src/components/views/dialogs/SlidingSyncOptionsDialog.tsx deleted file mode 100644 index 958c8d0876..0000000000 --- a/src/components/views/dialogs/SlidingSyncOptionsDialog.tsx +++ /dev/null @@ -1,142 +0,0 @@ -/* -Copyright 2022 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from "react"; -import { MatrixClient, Method } from "matrix-js-sdk/src/matrix"; -import { logger } from "matrix-js-sdk/src/logger"; - -import { _t } from "../../../languageHandler"; -import SettingsStore from "../../../settings/SettingsStore"; -import TextInputDialog from "./TextInputDialog"; -import withValidation from "../elements/Validation"; -import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import { useAsyncMemo } from "../../../hooks/useAsyncMemo"; -import { SettingLevel } from "../../../settings/SettingLevel"; - -/** - * Check that the server natively supports sliding sync. - * @param cli The MatrixClient of the logged in user. - * @throws if the proxy server is unreachable or not configured to the given homeserver - */ -async function syncHealthCheck(cli: MatrixClient): Promise { - await cli.http.authedRequest(Method.Post, "/sync", undefined, undefined, { - localTimeoutMs: 10 * 1000, // 10s - prefix: "/_matrix/client/unstable/org.matrix.msc3575", - }); - logger.info("server natively support sliding sync OK"); -} - -/** - * Check that the proxy url is in fact a sliding sync proxy endpoint and it is up. - * @param endpoint The proxy endpoint url - * @param hsUrl The homeserver url of the logged in user. - * @throws if the proxy server is unreachable or not configured to the given homeserver - */ -async function proxyHealthCheck(endpoint: string, hsUrl?: string): Promise { - const controller = new AbortController(); - const id = window.setTimeout(() => controller.abort(), 10 * 1000); // 10s - const res = await fetch(endpoint + "/client/server.json", { - signal: controller.signal, - }); - clearTimeout(id); - if (res.status != 200) { - throw new Error(`proxyHealthCheck: proxy server returned HTTP ${res.status}`); - } - const body = await res.json(); - if (body.server !== hsUrl) { - throw new Error(`proxyHealthCheck: client using ${hsUrl} but server is as ${body.server}`); - } - logger.info("sliding sync proxy is OK"); -} - -export const SlidingSyncOptionsDialog: React.FC<{ onFinished(enabled: boolean): void }> = ({ onFinished }) => { - const cli = MatrixClientPeg.safeGet(); - const currentProxy = SettingsStore.getValue("feature_sliding_sync_proxy_url"); - const hasNativeSupport = useAsyncMemo( - () => - syncHealthCheck(cli).then( - () => true, - () => false, - ), - [], - null, - ); - - let nativeSupport: string; - if (hasNativeSupport === null) { - nativeSupport = _t("labs|sliding_sync_checking"); - } else { - nativeSupport = hasNativeSupport - ? _t("labs|sliding_sync_server_support") - : _t("labs|sliding_sync_server_no_support"); - } - - const validProxy = withValidation({ - async deriveData({ value }): Promise<{ error?: unknown }> { - if (!value) return {}; - try { - await proxyHealthCheck(value, MatrixClientPeg.safeGet().baseUrl); - return {}; - } catch (error) { - return { error }; - } - }, - rules: [ - { - key: "required", - test: async ({ value }) => !!value || !!hasNativeSupport, - invalid: () => _t("labs|sliding_sync_server_specify_proxy"), - }, - { - key: "working", - final: true, - test: async (_, { error }) => !error, - valid: () => _t("spotlight|public_rooms|network_dropdown_available_valid"), - invalid: ({ error }) => (error instanceof Error ? error.message : null), - }, - ], - }); - - return ( - -
    - {_t("labs|sliding_sync_disable_warning")} -
    - {nativeSupport} -
    - } - placeholder={ - hasNativeSupport - ? _t("labs|sliding_sync_proxy_url_optional_label") - : _t("labs|sliding_sync_proxy_url_label") - } - value={currentProxy} - button={_t("action|enable")} - validator={validProxy} - onFinished={(enable, proxyUrl) => { - if (enable) { - SettingsStore.setValue("feature_sliding_sync_proxy_url", null, SettingLevel.DEVICE, proxyUrl); - onFinished(true); - } else { - onFinished(false); - } - }} - /> - ); -}; diff --git a/src/components/views/dialogs/UserSettingsDialog.tsx b/src/components/views/dialogs/UserSettingsDialog.tsx index 820617ae96..a6c82d50c2 100644 --- a/src/components/views/dialogs/UserSettingsDialog.tsx +++ b/src/components/views/dialogs/UserSettingsDialog.tsx @@ -1,6 +1,6 @@ /* Copyright 2019 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2024 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ import React from "react"; import TabbedView, { Tab } from "../../structures/TabbedView"; import { _t, _td } from "../../../languageHandler"; import GeneralUserSettingsTab from "../settings/tabs/user/GeneralUserSettingsTab"; -import SettingsStore, { CallbackFn } from "../../../settings/SettingsStore"; +import SettingsStore from "../../../settings/SettingsStore"; import LabsUserSettingsTab, { showLabsFlags } from "../settings/tabs/user/LabsUserSettingsTab"; import AppearanceUserSettingsTab from "../settings/tabs/user/AppearanceUserSettingsTab"; import SecurityUserSettingsTab from "../settings/tabs/user/SecurityUserSettingsTab"; @@ -37,6 +37,7 @@ import SessionManagerTab from "../settings/tabs/user/SessionManagerTab"; import { UserTab } from "./UserTab"; import { NonEmptyArray } from "../../../@types/common"; import { SDKContext, SdkContextClass } from "../../../contexts/SDKContext"; +import { useSettingValue } from "../../../hooks/useSettings"; interface IProps { initialTabId?: UserTab; @@ -44,35 +45,11 @@ interface IProps { onFinished(): void; } -interface IState { - mjolnirEnabled: boolean; -} +export default function UserSettingsDialog(props: IProps): JSX.Element { + const voipEnabled = useSettingValue(UIFeature.Voip); + const mjolnirEnabled = useSettingValue("feature_mjolnir"); -export default class UserSettingsDialog extends React.Component { - private settingsWatchers: string[] = []; - - public constructor(props: IProps) { - super(props); - - this.state = { - mjolnirEnabled: SettingsStore.getValue("feature_mjolnir"), - }; - } - - public componentDidMount(): void { - this.settingsWatchers = [SettingsStore.watchSetting("feature_mjolnir", null, this.mjolnirChanged)]; - } - - public componentWillUnmount(): void { - this.settingsWatchers.forEach((watcherRef) => SettingsStore.unwatchSetting(watcherRef)); - } - - private mjolnirChanged: CallbackFn = (settingName, roomId, atLevel, newValue) => { - // We can cheat because we know what levels a feature is tracked at, and how it is tracked - this.setState({ mjolnirEnabled: newValue }); - }; - - private getTabs(): NonEmptyArray> { + const getTabs = (): NonEmptyArray> => { const tabs: Tab[] = []; tabs.push( @@ -80,7 +57,7 @@ export default class UserSettingsDialog extends React.Component UserTab.General, _td("common|general"), "mx_UserSettingsDialog_settingsIcon", - , + , "UserSettingsGeneral", ), ); @@ -90,7 +67,6 @@ export default class UserSettingsDialog extends React.Component _td("settings|sessions|title"), "mx_UserSettingsDialog_sessionsIcon", , - // don't track with posthog while under construction undefined, ), ); @@ -117,7 +93,7 @@ export default class UserSettingsDialog extends React.Component UserTab.Preferences, _td("common|preferences"), "mx_UserSettingsDialog_preferencesIcon", - , + , "UserSettingsPreferences", ), ); @@ -140,7 +116,7 @@ export default class UserSettingsDialog extends React.Component ), ); - if (SettingsStore.getValue(UIFeature.Voip)) { + if (voipEnabled) { tabs.push( new Tab( UserTab.Voice, @@ -157,11 +133,11 @@ export default class UserSettingsDialog extends React.Component UserTab.Security, _td("room_settings|security|title"), "mx_UserSettingsDialog_securityIcon", - , + , "UserSettingsSecurityPrivacy", ), ); - // Show the Labs tab if enabled or if there are any active betas + if (showLabsFlags() || SettingsStore.getFeatureSettingNames().some((k) => SettingsStore.getBetaInfo(k))) { tabs.push( new Tab( @@ -173,7 +149,7 @@ export default class UserSettingsDialog extends React.Component ), ); } - if (this.state.mjolnirEnabled) { + if (mjolnirEnabled) { tabs.push( new Tab( UserTab.Mjolnir, @@ -195,29 +171,23 @@ export default class UserSettingsDialog extends React.Component ); return tabs as NonEmptyArray>; - } + }; - public render(): React.ReactNode { - return ( - // XXX: SDKContext is provided within the LoggedInView subtree. - // Modals function outside the MatrixChat React tree, so sdkContext is reprovided here to simulate that. - // The longer term solution is to move our ModalManager into the React tree to inherit contexts properly. - - -
    - -
    -
    -
    - ); - } + return ( + // XXX: SDKContext is provided within the LoggedInView subtree. + // Modals function outside the MatrixChat React tree, so sdkContext is reprovided here to simulate that. + // The longer term solution is to move our ModalManager into the React tree to inherit contexts properly. + + +
    + +
    +
    +
    + ); } diff --git a/src/components/views/dialogs/devtools/RoomNotifications.tsx b/src/components/views/dialogs/devtools/RoomNotifications.tsx index 5d03ee7c3a..397db1fa4b 100644 --- a/src/components/views/dialogs/devtools/RoomNotifications.tsx +++ b/src/components/views/dialogs/devtools/RoomNotifications.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import { NotificationCountType, Room, Thread, ReceiptType } from "matrix-js-sdk/src/matrix"; -import React, { useContext, useMemo } from "react"; +import React, { useContext } from "react"; import { ReadReceipt } from "matrix-js-sdk/src/models/read-receipt"; import MatrixClientContext from "../../../../contexts/MatrixClientContext"; @@ -25,7 +25,6 @@ import { determineUnreadState } from "../../../../RoomNotifs"; import { humanReadableNotificationLevel } from "../../../../stores/notifications/NotificationLevel"; import { doesRoomOrThreadHaveUnreadMessages } from "../../../../Unread"; import BaseTool, { DevtoolsContext, IDevtoolsProps } from "./BaseTool"; -import SettingsStore from "../../../../settings/SettingsStore"; function UserReadUpTo({ target }: { target: ReadReceipt }): JSX.Element { const cli = useContext(MatrixClientContext); @@ -66,12 +65,10 @@ function UserReadUpTo({ target }: { target: ReadReceipt }): JSX.Elemen } export default function RoomNotifications({ onBack }: IDevtoolsProps): JSX.Element { - const tacEnabled = useMemo(() => SettingsStore.getValue("threadsActivityCentre"), []); - const { room } = useContext(DevtoolsContext); const cli = useContext(MatrixClientContext); - const { level, count } = determineUnreadState(room, undefined, !tacEnabled); + const { level, count } = determineUnreadState(room, undefined, false); const [notificationState] = useNotificationState(room); return ( diff --git a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx index ee42a59221..2ac7681afa 100644 --- a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx +++ b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx @@ -939,7 +939,9 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n setInviteLinkCopied(true); copyPlaintext(ownInviteLink); }} - onHideTooltip={() => setInviteLinkCopied(false)} + onTooltipOpenChange={(open) => { + if (!open) setInviteLinkCopied(false); + }} title={inviteLinkCopied ? _t("common|copied") : _t("action|copy")} > diff --git a/src/components/views/dialogs/spotlight/TooltipOption.tsx b/src/components/views/dialogs/spotlight/TooltipOption.tsx index 2233e762d4..0deb4b1311 100644 --- a/src/components/views/dialogs/spotlight/TooltipOption.tsx +++ b/src/components/views/dialogs/spotlight/TooltipOption.tsx @@ -17,18 +17,19 @@ limitations under the License. import classNames from "classnames"; import React, { ComponentProps, ReactNode } from "react"; -import { RovingAccessibleTooltipButton } from "../../../../accessibility/roving/RovingAccessibleTooltipButton"; import { useRovingTabIndex } from "../../../../accessibility/RovingTabIndex"; -import AccessibleTooltipButton from "../../elements/AccessibleTooltipButton"; +import AccessibleButton from "../../elements/AccessibleButton"; +import { Ref } from "../../../../accessibility/roving/types"; -interface TooltipOptionProps extends ComponentProps { +interface TooltipOptionProps extends ComponentProps { endAdornment?: ReactNode; + inputRef?: Ref; } export const TooltipOption: React.FC = ({ inputRef, className, ...props }) => { const [onFocus, isActive, ref] = useRovingTabIndex(inputRef); return ( - = Partial< > & Omit, "onClick">; +type TooltipProps = ComponentProps; + /** * Type of props accepted by {@link AccessibleButton}. * @@ -86,6 +89,23 @@ type Props = DynamicHtmlElementProps & * Event handler for button activation. Should be implemented exactly like a normal `onClick` handler. */ onClick: ((e: ButtonEvent) => void | Promise) | null; + /** + * The tooltip to show on hover or focus. + */ + title?: string; + /** + * The caption is a secondary text displayed under the `title` of the tooltip. + * Only valid when used in conjunction with `title`. + */ + caption?: string; + /** + * The placement of the tooltip. + */ + placement?: TooltipProps["placement"]; + /** + * Callback for when the tooltip is opened or closed. + */ + onTooltipOpenChange?: TooltipProps["onOpenChange"]; }; /** @@ -116,11 +136,16 @@ const AccessibleButton = forwardRef(function , ref: Ref, ): JSX.Element { const newProps: RenderedElementProps = restProps; + newProps["aria-label"] = newProps["aria-label"] ?? title; if (disabled) { newProps["aria-disabled"] = true; newProps["disabled"] = true; @@ -182,7 +207,22 @@ const AccessibleButton = forwardRef(function + {button} + + ); + } + return button; }); // Type assertion required due to forwardRef type workaround in react.d.ts diff --git a/src/components/views/elements/AccessibleTooltipButton.tsx b/src/components/views/elements/AccessibleTooltipButton.tsx index 0af5cc9625..759643da1c 100644 --- a/src/components/views/elements/AccessibleTooltipButton.tsx +++ b/src/components/views/elements/AccessibleTooltipButton.tsx @@ -60,8 +60,11 @@ type Props = ComponentProps( - { title, tooltip, children, forceHide, alignment, onHideTooltip, tooltipClassName, ...props }: Props, + { title, tooltip, children, forceHide, alignment, onHideTooltip, tooltipClassName, element, ...props }: Props, ref: Ref, ) { const [hover, setHover] = useState(false); @@ -97,6 +100,7 @@ const AccessibleTooltipButton = forwardRef(function = ({ children, getTextToCopy, border = true return (
    {children} - { + if (!open) onHideTooltip(); + }} />
    ); diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 6f5815e95a..ddc2769c48 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -21,7 +21,6 @@ import FocusLock from "react-focus-lock"; import { MatrixEvent } from "matrix-js-sdk/src/matrix"; import { _t } from "../../../languageHandler"; -import AccessibleTooltipButton from "./AccessibleTooltipButton"; import MemberAvatar from "../avatars/MemberAvatar"; import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton"; import MessageContextMenu from "../context_menus/MessageContextMenu"; @@ -38,6 +37,7 @@ import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; import { getKeyBindingsManager } from "../../../KeyBindingsManager"; import { presentableTextForFile } from "../../../utils/FileUtils"; +import AccessibleButton from "./AccessibleButton"; // Max scale to keep gaps around the image const MAX_SCALE = 0.95; @@ -513,14 +513,14 @@ export default class ImageView extends React.Component { } const zoomOutButton = ( - ); const zoomInButton = ( - {
    {zoomOutButton} {zoomInButton} - - - {contextMenuButton} - = ({ if (mini) { // TODO fallback icon return ( - + {icon} - + ); } diff --git a/src/components/views/elements/ToggleSwitch.tsx b/src/components/views/elements/ToggleSwitch.tsx index 588374d17b..8e595ff234 100644 --- a/src/components/views/elements/ToggleSwitch.tsx +++ b/src/components/views/elements/ToggleSwitch.tsx @@ -18,7 +18,7 @@ limitations under the License. import React from "react"; import classNames from "classnames"; -import AccessibleTooltipButton from "./AccessibleTooltipButton"; +import AccessibleButton from "./AccessibleButton"; interface IProps { // Whether or not this toggle is in the 'on' position. @@ -41,7 +41,7 @@ interface IProps { } // Controlled Toggle Switch element, written with Accessibility in mind -export default ({ checked, disabled = false, onChange, ...props }: IProps): JSX.Element => { +export default ({ checked, disabled = false, onChange, title, tooltip, ...props }: IProps): JSX.Element => { const _onClick = (): void => { if (disabled) return; onChange(!checked); @@ -54,15 +54,17 @@ export default ({ checked, disabled = false, onChange, ...props }: IProps): JSX. }); return ( -
    - + ); }; diff --git a/src/components/views/messages/DecryptionFailureBody.tsx b/src/components/views/messages/DecryptionFailureBody.tsx index d3895e815f..718fa492e2 100644 --- a/src/components/views/messages/DecryptionFailureBody.tsx +++ b/src/components/views/messages/DecryptionFailureBody.tsx @@ -1,5 +1,5 @@ /* -Copyright 2022 The Matrix.org Foundation C.I.C. +Copyright 2022-2024 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,23 +14,41 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { forwardRef, ForwardRefExoticComponent } from "react"; +import React, { forwardRef, ForwardRefExoticComponent, useContext } from "react"; import { MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { DecryptionFailureCode } from "matrix-js-sdk/src/crypto-api"; import { _t } from "../../../languageHandler"; import { IBodyProps } from "./IBodyProps"; +import { LocalDeviceVerificationStateContext } from "../../../contexts/LocalDeviceVerificationStateContext"; -function getErrorMessage(mxEvent?: MatrixEvent): string { - return mxEvent?.isEncryptedDisabledForUnverifiedDevices - ? _t("timeline|decryption_failure|blocked") - : _t("timeline|decryption_failure|unable_to_decrypt"); +function getErrorMessage(mxEvent: MatrixEvent, isVerified: boolean | undefined): string { + if (mxEvent.isEncryptedDisabledForUnverifiedDevices) return _t("timeline|decryption_failure|blocked"); + switch (mxEvent.decryptionFailureReason) { + case DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP: + return _t("timeline|decryption_failure|historical_event_no_key_backup"); + + case DecryptionFailureCode.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED: + if (isVerified === false) { + // The user seems to have a key backup, so prompt them to verify in the hope that doing so will + // mean we can restore from backup and we'll get the key for this message. + return _t("timeline|decryption_failure|historical_event_unverified_device"); + } + // otherwise, use the default. + break; + + case DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED: + return _t("timeline|decryption_failure|historical_event_user_not_joined"); + } + return _t("timeline|decryption_failure|unable_to_decrypt"); } // A placeholder element for messages that could not be decrypted -export const DecryptionFailureBody = forwardRef(({ mxEvent }, ref): JSX.Element => { +export const DecryptionFailureBody = forwardRef(({ mxEvent }, ref): React.JSX.Element => { + const verificationState = useContext(LocalDeviceVerificationStateContext); return (
    - {getErrorMessage(mxEvent)} + {getErrorMessage(mxEvent, verificationState)}
    ); }) as ForwardRefExoticComponent; diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx index fa3df0ca03..b5109ee001 100644 --- a/src/components/views/messages/MessageActionBar.tsx +++ b/src/components/views/messages/MessageActionBar.tsx @@ -229,22 +229,16 @@ const ReplyInThreadButton: React.FC = ({ mxEvent }) => { } }; + const title = !hasARelation ? _t("action|reply_in_thread") : _t("threads|error_start_thread_existing_relation"); + return ( -
    - {!hasARelation - ? _t("action|reply_in_thread") - : _t("threads|error_start_thread_existing_relation")} -
    - - } - title={!hasARelation ? _t("action|reply_in_thread") : _t("threads|error_start_thread_existing_relation")} + title={title} onClick={onClick} onContextMenu={onClick} + placement="left" >
    @@ -508,18 +502,7 @@ class MessageActionBar extends React.PureComponent { mx_MessageActionBar_iconButton: true, mx_MessageActionBar_expandCollapseMessageButton: true, }); - const tooltip = ( - <> -
    - {this.props.isQuoteExpanded - ? _t("timeline|mab|collapse_reply_chain") - : _t("timeline|mab|expand_reply_chain")} -
    -
    - {_t(ALTERNATE_KEY_NAME[Key.SHIFT]) + " + " + _t("action|click")} -
    - - ); + toolbarOpts.push( { ? _t("timeline|mab|collapse_reply_chain") : _t("timeline|mab|expand_reply_chain") } - tooltip={tooltip} + caption={_t(ALTERNATE_KEY_NAME[Key.SHIFT]) + " + " + _t("action|click")} onClick={this.props.toggleThreadExpanded} key="expand" + placement="left" > {this.props.isQuoteExpanded ? : } , diff --git a/src/components/views/pips/WidgetPip.tsx b/src/components/views/pips/WidgetPip.tsx index 4a0d5e6618..2ba9e39e25 100644 --- a/src/components/views/pips/WidgetPip.tsx +++ b/src/components/views/pips/WidgetPip.tsx @@ -34,7 +34,6 @@ import { WidgetType } from "../../../widgets/WidgetType"; import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore"; import WidgetUtils from "../../../utils/WidgetUtils"; import { ElementWidgetActions } from "../../../stores/widgets/ElementWidgetActions"; -import { Alignment } from "../elements/Tooltip"; interface Props { widgetId: string; @@ -128,9 +127,9 @@ export const WidgetPip: FC = ({ widgetId, room, viewingRoom, onStartMovin diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index 2e7b9bc9ab..dbc6acb29b 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -237,7 +237,12 @@ export function DeviceItem({ ); } else { return ( - +
    {deviceName}
    {trustedLabel}
    diff --git a/src/components/views/rooms/LinkPreviewGroup.tsx b/src/components/views/rooms/LinkPreviewGroup.tsx index 0b82b8729a..c940e9fce7 100644 --- a/src/components/views/rooms/LinkPreviewGroup.tsx +++ b/src/components/views/rooms/LinkPreviewGroup.tsx @@ -81,6 +81,7 @@ const LinkPreviewGroup: React.FC = ({ links, mxEvent, onCancelClick, onH src={require("../../../../res/img/cancel.svg").default} width="18" height="18" + draggable="false" /> ) : undefined} diff --git a/src/components/views/rooms/MessageComposerFormatBar.tsx b/src/components/views/rooms/MessageComposerFormatBar.tsx index 0d737c4001..5893540528 100644 --- a/src/components/views/rooms/MessageComposerFormatBar.tsx +++ b/src/components/views/rooms/MessageComposerFormatBar.tsx @@ -127,16 +127,6 @@ interface IFormatButtonProps { class FormatButton extends React.PureComponent { public render(): React.ReactNode { const className = `mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIcon${this.props.icon}`; - let shortcut; - if (this.props.shortcut) { - shortcut =
    {this.props.shortcut}
    ; - } - const tooltip = ( -
    -
    {this.props.label}
    -
    {shortcut}
    -
    - ); // element="button" and type="button" are necessary for the buttons to work on WebKit, // otherwise the text is deselected before onClick can ever be called @@ -145,8 +135,9 @@ class FormatButton extends React.PureComponent { element="button" type="button" onClick={this.props.onClick} + aria-label={this.props.label} title={this.props.label} - tooltip={tooltip} + caption={this.props.shortcut} className={className} /> ); diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index e8c6d1fc8d..8bd5231f4c 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -159,7 +159,7 @@ const DmAuxButton: React.FC = ({ tabIndex, dispatcher = default showSpaceInvite(activeSpace); }} disabled={!canInvite} - tooltip={canInvite ? undefined : _t("spaces|error_no_permission_invite")} + title={canInvite ? undefined : _t("spaces|error_no_permission_invite")} /> )} @@ -249,7 +249,7 @@ const UntaggedAuxButton: React.FC = ({ tabIndex }) => { PosthogTrackers.trackInteraction("WebRoomListRoomsSublistPlusMenuCreateRoomItem", e); }} disabled={!canAddRooms} - tooltip={canAddRooms ? undefined : _t("spaces|error_no_permission_create_room")} + title={canAddRooms ? undefined : _t("spaces|error_no_permission_create_room")} /> {videoRoomsEnabled && ( = ({ tabIndex }) => { ); }} disabled={!canAddRooms} - tooltip={canAddRooms ? undefined : _t("spaces|error_no_permission_create_room")} + title={canAddRooms ? undefined : _t("spaces|error_no_permission_create_room")} > @@ -280,7 +280,7 @@ const UntaggedAuxButton: React.FC = ({ tabIndex }) => { showAddExistingRooms(activeSpace); }} disabled={!canAddRooms} - tooltip={canAddRooms ? undefined : _t("spaces|error_no_permission_add_room")} + title={canAddRooms ? undefined : _t("spaces|error_no_permission_add_room")} /> ) : null} diff --git a/src/components/views/rooms/RoomListHeader.tsx b/src/components/views/rooms/RoomListHeader.tsx index 690300dfa2..bcd918eaf3 100644 --- a/src/components/views/rooms/RoomListHeader.tsx +++ b/src/components/views/rooms/RoomListHeader.tsx @@ -267,7 +267,7 @@ const RoomListHeader: React.FC = ({ onVisibilityChange }) => { closePlusMenu(); }} disabled={!canAddSubRooms} - tooltip={!canAddSubRooms ? _t("spaces|error_no_permission_add_room") : undefined} + title={!canAddSubRooms ? _t("spaces|error_no_permission_add_room") : undefined} /> {canCreateSpaces && ( = ({ onVisibilityChange }) => { closePlusMenu(); }} disabled={!canAddSubSpaces} - tooltip={!canAddSubSpaces ? _t("spaces|error_no_permission_add_space") : undefined} + title={!canAddSubSpaces ? _t("spaces|error_no_permission_add_space") : undefined} > diff --git a/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx b/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx index cd3cd4ccec..317afdfca1 100644 --- a/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx +++ b/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx @@ -23,7 +23,7 @@ import AccessibleTooltipButton from "../../elements/AccessibleTooltipButton"; type Props = Omit< ComponentProps>, - "aria-label" | "title" | "kind" | "className" | "onClick" + "aria-label" | "title" | "kind" | "className" | "onClick" | "element" > & { isExpanded: boolean; onClick: () => void; diff --git a/src/components/views/settings/devices/LoginWithQRSection.tsx b/src/components/views/settings/devices/LoginWithQRSection.tsx index c85b80c3a7..b83668b6b8 100644 --- a/src/components/views/settings/devices/LoginWithQRSection.tsx +++ b/src/components/views/settings/devices/LoginWithQRSection.tsx @@ -35,39 +35,40 @@ interface IProps { wellKnown?: IClientWellKnown; } -export default class LoginWithQRSection extends React.Component { - public constructor(props: IProps) { - super(props); - } - - public render(): JSX.Element | null { - // Needs server support for get_login_token and MSC3886: - // in r0 of MSC3882 it is exposed as a feature flag, but in stable and unstable r1 it is a capability - const capability = GET_LOGIN_TOKEN_CAPABILITY.findIn(this.props.capabilities); - const getLoginTokenSupported = - !!this.props.versions?.unstable_features?.["org.matrix.msc3882"] || !!capability?.enabled; - const msc3886Supported = - !!this.props.versions?.unstable_features?.["org.matrix.msc3886"] || - this.props.wellKnown?.["io.element.rendezvous"]?.server; - const offerShowQr = getLoginTokenSupported && msc3886Supported; - - // don't show anything if no method is available - if (!offerShowQr) { - return null; - } - - return ( - -
    -

    - {_t("settings|sessions|sign_in_with_qr_description")} -

    - - - {_t("settings|sessions|sign_in_with_qr_button")} - -
    -
    - ); - } +function shouldShowQrLegacy( + versions?: IServerVersions, + wellKnown?: IClientWellKnown, + capabilities?: Capabilities, +): boolean { + // Needs server support for (get_login_token or OIDC Device Authorization Grant) and MSC3886: + // in r0 of MSC3882 it is exposed as a feature flag, but in stable and unstable r1 it is a capability + const loginTokenCapability = GET_LOGIN_TOKEN_CAPABILITY.findIn(capabilities); + const getLoginTokenSupported = + !!versions?.unstable_features?.["org.matrix.msc3882"] || !!loginTokenCapability?.enabled; + const msc3886Supported = + !!versions?.unstable_features?.["org.matrix.msc3886"] || !!wellKnown?.["io.element.rendezvous"]?.server; + return getLoginTokenSupported && msc3886Supported; } + +const LoginWithQRSection: React.FC = ({ onShowQr, versions, capabilities, wellKnown }) => { + const offerShowQr = shouldShowQrLegacy(versions, wellKnown, capabilities); + + // don't show anything if no method is available + if (!offerShowQr) { + return null; + } + + return ( + +
    +

    {_t("settings|sessions|sign_in_with_qr_description")}

    + + + {_t("settings|sessions|sign_in_with_qr_button")} + +
    +
    + ); +}; + +export default LoginWithQRSection; diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index 61c8e85f8d..c711b7e39e 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -32,7 +32,8 @@ import { ExtendedDevice } from "../../devices/types"; import { deleteDevicesWithInteractiveAuth } from "../../devices/deleteDevices"; import SettingsTab from "../SettingsTab"; import LoginWithQRSection from "../../devices/LoginWithQRSection"; -import LoginWithQR, { Mode } from "../../../auth/LoginWithQR"; +import LoginWithQR from "../../../auth/LoginWithQR"; +import { Mode } from "../../../auth/LoginWithQR-types"; import { useAsyncMemo } from "../../../../../hooks/useAsyncMemo"; import QuestionDialog from "../../../dialogs/QuestionDialog"; import { FilterVariation } from "../../devices/filter"; @@ -284,6 +285,12 @@ const SessionManagerTab: React.FC = () => { return ( + { /> )} - ); diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index 429a18e134..8d3041ff1e 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -368,8 +368,6 @@ const SpacePanel: React.FC = () => { } }); - const isThreadsActivityCentreEnabled = useSettingValue("threadsActivityCentre"); - return ( {({ onKeyDownHandler, onDragEndHandler }) => ( @@ -426,9 +424,8 @@ const SpacePanel: React.FC = () => { )} - {isThreadsActivityCentreEnabled && ( - - )} + + diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index e7f905068a..d858f0533e 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -51,7 +51,7 @@ import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; type ButtonProps = Omit< ComponentProps>, - "title" | "onClick" | "size" + "title" | "onClick" | "size" | "element" > & { space?: Room; spaceKey?: SpaceKey; diff --git a/src/hooks/useUnreadNotifications.ts b/src/hooks/useUnreadNotifications.ts index e0a2f1eeff..f687ee7f1d 100644 --- a/src/hooks/useUnreadNotifications.ts +++ b/src/hooks/useUnreadNotifications.ts @@ -15,13 +15,12 @@ limitations under the License. */ import { RoomEvent } from "matrix-js-sdk/src/matrix"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import type { NotificationCount, Room } from "matrix-js-sdk/src/matrix"; import { determineUnreadState } from "../RoomNotifs"; import { NotificationLevel } from "../stores/notifications/NotificationLevel"; import { useEventEmitter } from "./useEventEmitter"; -import SettingsStore from "../settings/SettingsStore"; export const useUnreadNotifications = ( room?: Room, @@ -31,8 +30,6 @@ export const useUnreadNotifications = ( count: number; level: NotificationLevel; } => { - const tacEnabled = useMemo(() => SettingsStore.getValue("threadsActivityCentre"), []); - const [symbol, setSymbol] = useState(null); const [count, setCount] = useState(0); const [level, setLevel] = useState(NotificationLevel.None); @@ -53,11 +50,11 @@ export const useUnreadNotifications = ( useEventEmitter(room, RoomEvent.MyMembership, () => updateNotificationState()); const updateNotificationState = useCallback(() => { - const { symbol, count, level } = determineUnreadState(room, threadId, !tacEnabled); + const { symbol, count, level } = determineUnreadState(room, threadId, false); setSymbol(symbol); setCount(count); setLevel(level); - }, [room, threadId, tacEnabled]); + }, [room, threadId]); useEffect(() => { updateNotificationState(); diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index 08d552101a..6967a0c16b 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -249,15 +249,7 @@ "completing_setup": "Dokončování nastavení nového zařízení", "confirm_code_match": "Zkontrolujte, zda se níže uvedený kód shoduje s vaším dalším zařízením:", "connecting": "Připojování…", - "error_device_already_signed_in": "Druhé zařízení je již přihlášeno.", - "error_device_not_signed_in": "Druhé zařízení není přihlášeno.", - "error_device_unsupported": "Propojení s tímto zařízením není podporováno.", - "error_homeserver_lacks_support": "Domovský server nepodporuje přihlášení pomocí jiného zařízení.", - "error_invalid_scanned_code": "Naskenovaný kód je neplatný.", - "error_linking_incomplete": "Propojení nebylo dokončeno v požadovaném čase.", "error_rate_limited": "Příliš mnoho pokusů v krátkém čase. Počkejte chvíli, než to zkusíte znovu.", - "error_request_cancelled": "Požadavek byl zrušen.", - "error_request_declined": "Požadavek byl na druhém zařízení odmítnut.", "error_unexpected": "Došlo k neočekávané chybě.", "scan_code_instruction": "Níže uvedený QR kód naskenujte pomocí přihlašovaného zařízení.", "scan_qr_code": "Skenovat QR kód", @@ -1446,18 +1438,9 @@ "rust_crypto_optin_warning": "Přechod na Rust kryptografii vyžaduje proces migrace, který může trvat několik minut. Pro deaktivaci se budete muset odhlásit a znovu přihlásit; používejte s opatrností!", "rust_crypto_requires_logout": "Jakmile je Rust kryptografie povolena, lze ji vypnout pouze odhlášením a opětovným přihlášením.", "sliding_sync": "Režim klouzavé synchronizace", - "sliding_sync_checking": "Kontrola…", - "sliding_sync_configuration": "Nastavení klouzavé synchronizace", "sliding_sync_description": "V aktivním vývoji, nelze zakázat.", - "sliding_sync_disable_warning": "Pro deaktivaci se musíte odhlásit a znovu přihlásit, používejte s opatrností!", "sliding_sync_disabled_notice": "Pro vypnutí se odhlaste a znovu přihlaste", - "sliding_sync_proxy_url_label": "URL proxy serveru", - "sliding_sync_proxy_url_optional_label": "URL proxy serveru (volitelné)", "sliding_sync_server_no_support": "Váš server nemá nativní podporu", - "sliding_sync_server_specify_proxy": "Váš server nemá nativní podporu, musíte zadat proxy server", - "sliding_sync_server_support": "Váš server má nativní podporu", - "threads_activity_centre": "Centrum aktivit vláken (ve vývoji).", - "threads_activity_centre_description": "Upozornění: V aktivním vývoji; znovu načte %(brand)s.", "under_active_development": "V aktivním vývoji.", "unrealiable_e2e": "Nespolehlivé v šifrovaných místnostech", "video_rooms": "Video místnosti", diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 809ffe6e4b..5704774956 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -245,15 +245,7 @@ "completing_setup": "Schließe Anmeldung deines neuen Gerätes ab", "confirm_code_match": "Überprüfe, dass der unten angezeigte Code mit deinem anderen Gerät übereinstimmt:", "connecting": "Verbinde …", - "error_device_already_signed_in": "Das andere Gerät ist bereits angemeldet.", - "error_device_not_signed_in": "Das andere Gerät ist nicht angemeldet.", - "error_device_unsupported": "Die Verbindung mit diesem Gerät wird nicht unterstützt.", - "error_homeserver_lacks_support": "Der Heim-Server unterstützt die Anmeldung eines anderen Gerätes nicht.", - "error_invalid_scanned_code": "Der gescannte Code ist ungültig.", - "error_linking_incomplete": "Die Verbindung konnte nicht in der erforderlichen Zeit hergestellt werden.", "error_rate_limited": "Zu viele Versuche in zu kurzer Zeit. Warte ein wenig, bevor du es erneut versuchst.", - "error_request_cancelled": "Die Anfrage wurde abgebrochen.", - "error_request_declined": "Die Anfrage wurde auf dem anderen Gerät abgelehnt.", "error_unexpected": "Ein unerwarteter Fehler ist aufgetreten.", "scan_code_instruction": "Lese den folgenden QR-Code mit deinem nicht angemeldeten Gerät ein.", "scan_qr_code": "QR-Code einlesen", @@ -1431,16 +1423,9 @@ "report_to_moderators_description": "In Räumen, die Moderation unterstützen, lässt dich die Schaltfläche „Melden“ missbräuchliche Verwendung an die Raummoderation melden.", "rust_crypto": "Rust-Verschlüsselungsumsetzung", "sliding_sync": "Sliding-Sync-Modus", - "sliding_sync_checking": "Überprüfe …", - "sliding_sync_configuration": "Sliding-Sync-Konfiguration", "sliding_sync_description": "In aktiver Entwicklung, kann nicht deaktiviert werden.", - "sliding_sync_disable_warning": "Zum Deaktivieren musst du dich neu anmelden. Mit Vorsicht verwenden!", "sliding_sync_disabled_notice": "Zum Deaktivieren, melde dich ab und erneut an", - "sliding_sync_proxy_url_label": "Proxy-URL", - "sliding_sync_proxy_url_optional_label": "Proxy-URL (optional)", "sliding_sync_server_no_support": "Dein Server unterstützt dies nicht nativ", - "sliding_sync_server_specify_proxy": "Dein Server unterstützt dies nicht nativ, du musst einen Proxy angeben", - "sliding_sync_server_support": "Dein Server unterstützt dies nativ", "under_active_development": "In aktiver Entwicklung.", "unrealiable_e2e": "Nicht zuverlässig in verschlüsselten Räumen", "video_rooms": "Videoräume", diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 0f353c820a..d1f4016aaf 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -249,21 +249,29 @@ "completing_setup": "Completing set up of your new device", "confirm_code_match": "Check that the code below matches with your other device:", "connecting": "Connecting…", - "error_device_already_signed_in": "The other device is already signed in.", - "error_device_not_signed_in": "The other device isn't signed in.", - "error_device_unsupported": "Linking with this device is not supported.", - "error_homeserver_lacks_support": "The homeserver doesn't support signing in another device.", - "error_invalid_scanned_code": "The scanned code is invalid.", - "error_linking_incomplete": "The linking wasn't completed in the required time.", + "error_expired": "Sign in expired. Please try again.", + "error_expired_title": "The sign in was not completed in time", + "error_insecure_channel_detected": "A secure connection could not be made to the new device. Your existing devices are still safe and you don't need to worry about them.", + "error_insecure_channel_detected_instructions": "Now what?", + "error_insecure_channel_detected_instructions_1": "Try signing in to the other device again with a QR code in case this was a network problem", + "error_insecure_channel_detected_instructions_2": "If you encounter the same problem, try a different wifi network or use your mobile data instead of wifi", + "error_insecure_channel_detected_instructions_3": "If that doesn't work, sign in manually", + "error_insecure_channel_detected_title": "Connection not secure", + "error_other_device_already_signed_in": "You don’t need to do anything else.", + "error_other_device_already_signed_in_title": "Your other device is already signed in", "error_rate_limited": "Too many attempts in a short time. Wait some time before trying again.", - "error_request_cancelled": "The request was cancelled.", - "error_request_declined": "The request was declined on the other device.", - "error_unexpected": "An unexpected error occurred.", - "follow_remaining_instructions": "Follow the remaining instructions to verify your other device", + "error_unexpected": "An unexpected error occurred. The request to connect your other device has been cancelled.", + "error_unsupported_protocol": "This device does not support signing in to the other device with a QR code.", + "error_unsupported_protocol_title": "Other device not compatible", + "error_user_cancelled": "The sign in was cancelled on the other device.", + "error_user_cancelled_title": "Sign in request cancelled", + "error_user_declined": "You declined the request from your other device to sign in.", + "error_user_declined_title": "Sign in declined", + "follow_remaining_instructions": "Follow the instructions to link your other device", "open_element_other_device": "Open %(brand)s on your other device", "point_the_camera": "Point the camera at the QR code shown here", "scan_code_instruction": "Scan the QR code with another device", - "scan_qr_code": "Scan QR code", + "scan_qr_code": "Sign in with QR code", "select_qr_code": "Select \"%(scanQRCode)s\"", "sign_in_new_device": "Sign in new device", "waiting_for_device": "Waiting for device to sign in" @@ -1452,18 +1460,9 @@ "rust_crypto_optin_warning": "Switching to the Rust cryptography requires a migration process that may take several minutes. To disable you will need to log out and back in; use with caution!", "rust_crypto_requires_logout": "Once enabled, Rust cryptography can only be disabled by logging out and in again", "sliding_sync": "Sliding Sync mode", - "sliding_sync_checking": "Checking…", - "sliding_sync_configuration": "Sliding Sync configuration", "sliding_sync_description": "Under active development, cannot be disabled.", - "sliding_sync_disable_warning": "To disable you will need to log out and back in, use with caution!", "sliding_sync_disabled_notice": "Log out and back in to disable", - "sliding_sync_proxy_url_label": "Proxy URL", - "sliding_sync_proxy_url_optional_label": "Proxy URL (optional)", - "sliding_sync_server_no_support": "Your server lacks native support", - "sliding_sync_server_specify_proxy": "Your server lacks native support, you must specify a proxy", - "sliding_sync_server_support": "Your server has native support", - "threads_activity_centre": "Threads Activity Centre (in development)", - "threads_activity_centre_description": "Warning: Under active development; reloads %(brand)s.", + "sliding_sync_server_no_support": "Your server lacks support", "under_active_development": "Under active development.", "unrealiable_e2e": "Unreliable in encrypted rooms", "video_rooms": "Video rooms", @@ -3214,6 +3213,9 @@ "creation_summary_room": "%(creator)s created and configured the room.", "decryption_failure": { "blocked": "The sender has blocked you from receiving this message", + "historical_event_no_key_backup": "Historical messages are not available on this device", + "historical_event_unverified_device": "You need to verify this device for access to historical messages", + "historical_event_user_not_joined": "You don't have access to this message", "unable_to_decrypt": "Unable to decrypt message" }, "disambiguated_profile": "%(displayName)s (%(matrixId)s)", diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json index 95632d2452..81e047f408 100644 --- a/src/i18n/strings/es.json +++ b/src/i18n/strings/es.json @@ -233,14 +233,7 @@ "completing_setup": "Terminando de configurar tu nuevo dispositivo", "confirm_code_match": "Comprueba que el siguiente código también aparece en el otro dispositivo:", "connecting": "Conectando…", - "error_device_already_signed_in": "El otro dispositivo ya tiene una sesión iniciada.", - "error_device_not_signed_in": "El otro dispositivo no tiene una sesión iniciada.", - "error_homeserver_lacks_support": "Tu servidor base no es compatible con el inicio de sesión en otro dispositivo.", - "error_invalid_scanned_code": "El código escaneado no es válido.", - "error_linking_incomplete": "El proceso de enlace ha tardado demasiado tiempo, por lo que no se ha completado.", "error_rate_limited": "Demasiados intentos en poco tiempo. Espera un poco antes de volverlo a intentar.", - "error_request_cancelled": "La solicitud ha sido cancelada.", - "error_request_declined": "El otro dispositivo ha rechazado la solicitud.", "error_unexpected": "Ha ocurrido un error inesperado.", "scan_code_instruction": "Escanea el siguiente código QR con tu dispositivo.", "scan_qr_code": "Escanear código QR", @@ -1323,16 +1316,9 @@ "report_to_moderators_description": "En las salas que sean compatible con la moderación, el botón de «Denunciar» avisará a los moderadores de la sala.", "rust_crypto": "Implementación de la criptografía en Rust", "sliding_sync": "Modo de sincronización progresiva", - "sliding_sync_checking": "Comprobando…", - "sliding_sync_configuration": "Configuración de la sincronización progresiva", "sliding_sync_description": "En desarrollo, no se puede desactivar.", - "sliding_sync_disable_warning": "Para desactivarlo, tendrás que cerrar sesión y volverla a iniciar. ¡Ten cuidado!", "sliding_sync_disabled_notice": "Cierra sesión y vuélvela a abrir para desactivar", - "sliding_sync_proxy_url_label": "URL de servidor proxy", - "sliding_sync_proxy_url_optional_label": "URL de servidor proxy (opcional)", "sliding_sync_server_no_support": "Tu servidor no es compatible", - "sliding_sync_server_specify_proxy": "Tu servidor no es compatible, debes configurar un intermediario (proxy)", - "sliding_sync_server_support": "Tu servidor es compatible", "under_active_development": "Funcionalidad en desarrollo.", "video_rooms": "Salas de vídeo", "video_rooms_a_new_way_to_chat": "Una nueva forma de hablar por voz y vídeo en %(brand)s.", diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index e24b4c846d..a3e5126dab 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -249,15 +249,7 @@ "completing_setup": "Lõpetame uue seadme seadistamise", "confirm_code_match": "Kontrolli, et järgnev kood klapib teises seadmes kuvatava koodiga:", "connecting": "Kõne on ühendamisel…", - "error_device_already_signed_in": "Teine seade on juba võrku loginud.", - "error_device_not_signed_in": "Teine seade ei ole võrku loginud.", - "error_device_unsupported": "Sidumine selle seadmega ei ole toetatud.", - "error_homeserver_lacks_support": "Koduserver ei toeta muude seadmete võrku logimise võimalust.", - "error_invalid_scanned_code": "Skaneeritud QR-kood on vigane.", - "error_linking_incomplete": "Sidumine ei lõppenud etteantud aja jooksul.", "error_rate_limited": "Liiga palju päringuid napis ajavahemikus. Enne uuesti proovimist palun oota veidi.", - "error_request_cancelled": "Päring katkestati.", - "error_request_declined": "Teine seade lükkas päringu tagasi.", "error_unexpected": "Tekkis teadmata viga.", "scan_code_instruction": "Loe QR-koodi seadmega, kus sa oled Matrix'i võrgust välja loginud.", "scan_qr_code": "Loe QR-koodi", @@ -1439,16 +1431,9 @@ "rust_crypto_optin_warning": "Rust'i teekidel põhineva krüptograafia kasutusele võtmine eeldab andmete ümbertõstmist ja selleks võib kuluda õige mitu minutit. Selle funktsionaalsuse väljalülitamiseks pead võrgust välja logima ning seejärel tagasi logima. Palun ole kindel, et tead, mida teed!", "rust_crypto_requires_logout": "Kui Rust'i põhised teegid on kasutusel, siis selle funktsionaalsuse väljalülitamiseks pead võrgust välja logima ning seejärel tagasi logima", "sliding_sync": "Järkjärgulise sünkroniseerimise režiim", - "sliding_sync_checking": "Kontrollin…", - "sliding_sync_configuration": "Sliding Sync konfiguratsioon", "sliding_sync_description": "Aktiivselt arendamisel ega ole võimalik välja lülitada.", - "sliding_sync_disable_warning": "Väljalülitamiseks palun logi välja ning seejärel tagasi, kuid ole sellega ettevaatlik!", "sliding_sync_disabled_notice": "Väljalülitamiseks logi Matrix'i võrgust välja ja seejärel tagasi", - "sliding_sync_proxy_url_label": "Puhverserveri aadress", - "sliding_sync_proxy_url_optional_label": "Puhverserveri aadress (kui vaja)", "sliding_sync_server_no_support": "Selle funktsionaalsuse tugi on sinu koduserveris puudu", - "sliding_sync_server_specify_proxy": "Selle funktsionaalsuse tugi on sinu koduserveris puudu, palun kasuta puhverserverit", - "sliding_sync_server_support": "Selle funktsionaalsuse tugi on sinu koduserveris olemas", "under_active_development": "Aktiivselt arendamisel.", "video_rooms": "Videotoad", "video_rooms_a_new_way_to_chat": "Uus võimalus videovestlusteks rakenduses %(brand)s.", diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index 1fecd1b6d0..acb49fe159 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -230,14 +230,7 @@ "phone_optional_label": "Puhelin (valinnainen)", "qr_code_login": { "connecting": "Yhdistetään…", - "error_device_already_signed_in": "Toinen laite on jo sisäänkirjautunut.", - "error_device_not_signed_in": "Toinen laite ei ole sisäänkirjautunut.", - "error_device_unsupported": "Tämän laitteen kanssa linkittäminen ei ole tuettu.", - "error_invalid_scanned_code": "Skannattu koodi on virheellinen.", - "error_linking_incomplete": "Linkitystä ei suoritettu vaaditussa ajassa.", "error_rate_limited": "Liikaa yrityksiä lyhyessä ajassa. Odota hetki, ennen kuin yrität uudelleen.", - "error_request_cancelled": "Pyyntö peruttiin.", - "error_request_declined": "Pyyntö hylättiin toiselta laitteelta.", "error_unexpected": "Tapahtui odottamaton virhe.", "sign_in_new_device": "Kirjaa sisään uusi laite", "waiting_for_device": "Odotetaan laitteen sisäänkirjautumista" @@ -1253,14 +1246,9 @@ "report_to_moderators_description": "Moderointia tukevissa huoneissa väärinkäytökset voi ilmoittaa Ilmoita-painikkeella huoneen moderaattoreille.", "rust_crypto": "Rust-kryptografiatoteutus", "sliding_sync": "Liukuvan synkronoinnin tila", - "sliding_sync_checking": "Tarkistetaan…", - "sliding_sync_configuration": "Liukuvan synkronoinnin asetukset", "sliding_sync_description": "Työn alla, käytöstä poistaminen ei ole mahdollista.", "sliding_sync_disabled_notice": "Poista käytöstä kirjautumalla ulos ja takaisin sisään", - "sliding_sync_proxy_url_label": "Välityspalvelimen URL-osoite", - "sliding_sync_proxy_url_optional_label": "Välityspalvelimen URL-osoite (valinnainen)", "sliding_sync_server_no_support": "Palvelimellasi ei ole natiivitukea", - "sliding_sync_server_support": "Palvelimellasi on natiivituki", "under_active_development": "Aktiivisen kehityksen kohteena.", "video_rooms": "Videohuoneet", "video_rooms_a_new_way_to_chat": "Uusi tapa keskustella äänen ja videon välityksellä %(brand)sissä.", diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 5e9517bf74..acb2b33490 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -249,15 +249,7 @@ "completing_setup": "Fin de la configuration de votre nouvel appareil", "confirm_code_match": "Vérifiez que le code ci-dessous correspond à celui sur votre autre appareil :", "connecting": "Connexion…", - "error_device_already_signed_in": "L’autre appareil est déjà connecté.", - "error_device_not_signed_in": "L’autre appareil n’est pas connecté.", - "error_device_unsupported": "L’appairage avec cet appareil n’est pas pris en charge.", - "error_homeserver_lacks_support": "Le serveur d’accueil ne prend pas en charge la connexion d’un autre appareil.", - "error_invalid_scanned_code": "Le code scanné est invalide.", - "error_linking_incomplete": "L’appairage n’a pas été effectué dans le temps imparti.", "error_rate_limited": "Trop de tentatives consécutives. Attendez un peu avant de réessayer.", - "error_request_cancelled": "La demande a été annulée.", - "error_request_declined": "La requête a été refusée sur l’autre appareil.", "error_unexpected": "Une erreur inattendue s’est produite.", "scan_code_instruction": "Scannez le QR code ci-dessous avec l’appareil qui n’est pas connecté.", "scan_qr_code": "Scanner le QR code", @@ -1445,18 +1437,9 @@ "rust_crypto_optin_warning": "Si vous passez à la cryptographie Rust, cela démarrera un processus de migration qui peut durer plusieurs minutes. Pour la désactiver, vous devrez vous déconnecter et vous reconnecter; à utiliser prudemment !", "rust_crypto_requires_logout": "Une fois activée, la cryptographie Rust ne peut être désactivée qu'en se déconnectant et se reconnectant", "sliding_sync": "Mode synchronisation progressive", - "sliding_sync_checking": "Vérification…", - "sliding_sync_configuration": "Configuration de la synchronisation progressive", "sliding_sync_description": "En cours de développement, ne peut être désactivé.", - "sliding_sync_disable_warning": "Pour la désactiver, vous devrez vous déconnecter et vous reconnecter, faites attention !", "sliding_sync_disabled_notice": "Déconnectez et revenez pour désactiver", - "sliding_sync_proxy_url_label": "URL du serveur mandataire (proxy)", - "sliding_sync_proxy_url_optional_label": "URL du serveur mandataire (proxy – facultatif)", "sliding_sync_server_no_support": "Votre serveur manque d’un support natif", - "sliding_sync_server_specify_proxy": "Votre serveur manque d’un support natif, vous devez spécifier un serveur mandataire (proxy)", - "sliding_sync_server_support": "Votre serveur a un support natif", - "threads_activity_centre": "Centre d'activité des fils de discussion (en développement)", - "threads_activity_centre_description": "Attention: en cours de développement actif. Recharge %(brand)s", "under_active_development": "En cours de développement.", "unrealiable_e2e": "Non fiable dans les salons chiffrés", "video_rooms": "Salons vidéo", diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index e58b0ea5ad..1150025406 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -1211,13 +1211,7 @@ "leave_beta_reload": "Ao saír da beta volveremos a cargar %(brand)s.", "msc3531_hide_messages_pending_moderation": "Permitir que a moderación agoche mensaxes pendentes de moderar.", "pinning": "Fixando mensaxe", - "sliding_sync_configuration": "Configuración Sliding Sync", - "sliding_sync_disable_warning": "Para desactivalo tes que saír e volver a acceder, usa con precaución!", - "sliding_sync_proxy_url_label": "URL do Proxy", - "sliding_sync_proxy_url_optional_label": "URL do proxy (optativo)", "sliding_sync_server_no_support": "O teu servidor non ten soporte nativo", - "sliding_sync_server_specify_proxy": "O teu servidor non ten servidor nativo, tes que indicar un proxy", - "sliding_sync_server_support": "O teu servidor ten soporte nativo", "video_rooms": "Salas de vídeo", "video_rooms_a_new_way_to_chat": "Un novo xeito de conversar con voz e vídeo en %(brand)s.", "video_rooms_always_on_voip_channels": "As salas de vídeo son canles VoIP sempre activas dentro dunha sala en %(brand)s.", diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 801aa2f69a..7d8744d2a4 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -243,14 +243,6 @@ "completing_setup": "Új eszköz beállításának elvégzése", "confirm_code_match": "Ellenőrizze, hogy az alábbi kód megegyezik a másik eszközödön lévővel:", "connecting": "Kapcsolás…", - "error_device_already_signed_in": "A másik eszköz már bejelentkezett.", - "error_device_not_signed_in": "A másik eszköz még nincs bejelentkezve.", - "error_device_unsupported": "Összekötés ezzel az eszközzel nem támogatott.", - "error_homeserver_lacks_support": "A Matrix-kiszolgáló nem támogatja más eszköz bejelentkeztetését.", - "error_invalid_scanned_code": "A beolvasott kód érvénytelen.", - "error_linking_incomplete": "Az összekötés az elvárt időn belül nem fejeződött be.", - "error_request_cancelled": "A kérés megszakítva.", - "error_request_declined": "A kérést elutasították a másik eszközön.", "error_unexpected": "Nemvárt hiba történt.", "scan_code_instruction": "A kijelentkezett eszközzel olvasd be a QR kódot alább.", "scan_qr_code": "QR kód beolvasása", @@ -1412,16 +1404,9 @@ "report_to_moderators_description": "A moderálást támogató szobákban a problémás tartalmat a „Jelentés” gombbal lehet a moderátorok felé jelezni.", "rust_crypto": "Rust titkosítási implementáció", "sliding_sync": "Csúszó szinkronizációs mód", - "sliding_sync_checking": "Ellenőrzés…", - "sliding_sync_configuration": "Csúszó szinkronizáció beállítása", "sliding_sync_description": "Aktív fejlesztés alatt, nem kapcsolható ki.", - "sliding_sync_disable_warning": "A kikapcsoláshoz ki, majd újra be kell jelentkezni, használja óvatosan.", "sliding_sync_disabled_notice": "A kikapcsoláshoz ki-, és bejelentkezés szükséges", - "sliding_sync_proxy_url_label": "Proxy webcíme", - "sliding_sync_proxy_url_optional_label": "Proxy webcíme (nem kötelező)", "sliding_sync_server_no_support": "A kiszolgálója nem támogatja natívan", - "sliding_sync_server_specify_proxy": "A kiszolgálója nem támogatja natívan, proxy kiszolgálót kell beállítani", - "sliding_sync_server_support": "A kiszolgálója natívan támogatja", "under_active_development": "Aktív fejlesztés alatt.", "video_rooms": "Videószobák", "video_rooms_a_new_way_to_chat": "Új csevegési lehetőség a(z) %(brand)s alkalmazásban, hanggal és videóval.", diff --git a/src/i18n/strings/id.json b/src/i18n/strings/id.json index b70af7df1d..9ec1be1a53 100644 --- a/src/i18n/strings/id.json +++ b/src/i18n/strings/id.json @@ -243,14 +243,6 @@ "completing_setup": "Menyelesaikan penyiapan perangkat baru Anda", "confirm_code_match": "Periksa bahwa kode di bawah cocok dengan perangkat Anda yang lain:", "connecting": "Menghubungkan…", - "error_device_already_signed_in": "Perangkat yang lain sudah masuk.", - "error_device_not_signed_in": "Perangkat yang lain belum masuk.", - "error_device_unsupported": "Penautan dengan perangkat ini tidak didukung.", - "error_homeserver_lacks_support": "Homeserver tidak mendukung masuk ke perangkat lain.", - "error_invalid_scanned_code": "Kode yang dipindai tidak absah.", - "error_linking_incomplete": "Penautan tidak selesai dalam waktu yang dibutuhkan.", - "error_request_cancelled": "Permintaan dibatalkan.", - "error_request_declined": "Permintaan ditolak di perangkat yang lain.", "error_unexpected": "Sebuah kesalahan terjadi secara tidak terduga.", "scan_code_instruction": "Pindai kode QR di bawah dengan perangkat Anda yang sudah keluar dari akun.", "scan_qr_code": "Pindai kode QR", @@ -1418,16 +1410,9 @@ "report_to_moderators_description": "Dalam ruangan yang mendukung moderasi, tombol “Laporkan” memungkinkan Anda untuk melaporkan penyalahgunaan ke moderator ruangan.", "rust_crypto": "Implementasi kriptografi Rust", "sliding_sync": "Mode Sinkronisasi Geser", - "sliding_sync_checking": "Memeriksa…", - "sliding_sync_configuration": "Konfigurasi Penyinkronan Bergeser", "sliding_sync_description": "Dalam pengembangan aktif, tidak dapat dinonaktifkan.", - "sliding_sync_disable_warning": "Untuk menonaktifkan Anda harus keluar dan masuk kembali, gunakan dengan hati-hati!", "sliding_sync_disabled_notice": "Keluar dan masuk kembali ke akun untuk menonaktifkan", - "sliding_sync_proxy_url_label": "URL Proksi", - "sliding_sync_proxy_url_optional_label": "URL Proksi (opsional)", "sliding_sync_server_no_support": "Server Anda belum mendukungnya", - "sliding_sync_server_specify_proxy": "Server Anda belum mendukungnya, Anda harus menetapkan sebuah proksi", - "sliding_sync_server_support": "Server Anda mendukungnya", "under_active_development": "Dalam pengembangan aktif.", "unrealiable_e2e": "Tidak dapat diandalkan di ruangan terenkripsi", "video_rooms": "Ruangan video", diff --git a/src/i18n/strings/is.json b/src/i18n/strings/is.json index e810a7f7fe..3d09037a7f 100644 --- a/src/i18n/strings/is.json +++ b/src/i18n/strings/is.json @@ -218,7 +218,6 @@ "phone_label": "Sími", "phone_optional_label": "Sími (valfrjálst)", "qr_code_login": { - "error_invalid_scanned_code": "Skannaði kóðinn er ógildur.", "sign_in_new_device": "Skrá inn nýtt tæki", "waiting_for_device": "Bíð eftir að tækið skráist inn" }, @@ -1167,8 +1166,6 @@ "pinning": "Festing skilaboða", "report_to_moderators": "Tilkynna til umsjónarmanna", "report_to_moderators_description": "Í spjallrásum sem styðja eftirlit umsjónarmanna, mun 'Kæra'-hnappurinn gefa þér færi á að tilkynna misnotkun til umsjónarmanna spjallrása.", - "sliding_sync_proxy_url_label": "Slóð milliþjóns", - "sliding_sync_proxy_url_optional_label": "Slóð milliþjóns (valfrjálst)", "under_active_development": "Í virkri þróun.", "video_rooms": "Myndspjallrásir", "video_rooms_a_new_way_to_chat": "Ný leið til að spjalla með tali og myndmerki í %(brand)s.", diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 64bdb4223f..4153df8cf2 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -249,15 +249,7 @@ "completing_setup": "Completamento configurazione nuovo dispositivo", "confirm_code_match": "Controlla che il codice sottostante corrisponda nell'altro dispositivo:", "connecting": "In connessione…", - "error_device_already_signed_in": "L'altro dispositivo ha già fatto l'accesso.", - "error_device_not_signed_in": "L'altro dispositivo non ha fatto l'accesso.", - "error_device_unsupported": "Il collegamento con questo dispositivo non è supportato.", - "error_homeserver_lacks_support": "L'homeserver non supporta l'accesso in un altro dispositivo.", - "error_invalid_scanned_code": "Il codice scansionato non è valido.", - "error_linking_incomplete": "Il collegamento non è stato completato nel tempo previsto.", "error_rate_limited": "Troppi tentativi in poco tempo. Attendi un po' prima di riprovare.", - "error_request_cancelled": "La richiesta è stata annullata.", - "error_request_declined": "La richiesta è stata negata sull'altro dispositivo.", "error_unexpected": "Si è verificato un errore imprevisto.", "scan_code_instruction": "Scansiona il codice QR sottostante con il dispositivo che è disconnesso.", "scan_qr_code": "Scansiona codice QR", @@ -1445,18 +1437,9 @@ "rust_crypto_optin_warning": "Il passaggio alla crittografia Rust richiede un processo di migrazione che può impiegare diversi minuti. Per disattivarla dovrai disconnetterti e poi riaccedere, usala con cautela!", "rust_crypto_requires_logout": "Una volta attivata, la crittografia Rust può essere disattivata solo disconnettendoti e riaccedendo.", "sliding_sync": "Modalità di sincr. con slide", - "sliding_sync_checking": "Controllo…", - "sliding_sync_configuration": "Configurazione sincr. Sliding", "sliding_sync_description": "In sviluppo attivo, non può essere disattivato.", - "sliding_sync_disable_warning": "Per disattivarlo dovrai disconnetterti e riaccedere, usare con cautela!", "sliding_sync_disabled_notice": "Disconnettiti e riconnettiti per disattivare", - "sliding_sync_proxy_url_label": "URL proxy", - "sliding_sync_proxy_url_optional_label": "URL proxy (facoltativo)", "sliding_sync_server_no_support": "Il tuo server non ha il supporto nativo", - "sliding_sync_server_specify_proxy": "Il tuo server non ha il supporto nativo, devi specificare un proxy", - "sliding_sync_server_support": "Il tuo server ha il supporto nativo", - "threads_activity_centre": "Centro attività in discussioni (in sviluppo).", - "threads_activity_centre_description": "Attenzione: in fase di sviluppo attivo; ricarica %(brand)s.", "under_active_development": "In sviluppo attivo.", "unrealiable_e2e": "Inaffidabile nelle stanze cifrate", "video_rooms": "Stanze video", diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json index a29dab0e0d..f5aaa80913 100644 --- a/src/i18n/strings/ja.json +++ b/src/i18n/strings/ja.json @@ -233,15 +233,7 @@ "completing_setup": "新しい端末の設定を完了しています", "confirm_code_match": "以下のコードが他の端末と一致していることを確認してください:", "connecting": "接続しています…", - "error_device_already_signed_in": "もう一方のデバイスは既にサインインしています。", - "error_device_not_signed_in": "もう一方の端末はサインインしていません。", - "error_device_unsupported": "この端末とのリンクはサポートしていません。", - "error_homeserver_lacks_support": "ホームサーバーは他の端末でのサインインをサポートしていません。", - "error_invalid_scanned_code": "スキャンされたコードは無効です。", - "error_linking_incomplete": "時間内にリンクが完了しませんでした。", "error_rate_limited": "再試行の数が多すぎます。少し待ってから再度試してください。", - "error_request_cancelled": "リクエストはキャンセルされました。", - "error_request_declined": "リクエストはもう一方の端末で拒否されました。", "error_unexpected": "予期しないエラーが発生しました。", "scan_code_instruction": "サインアウトした端末で以下のQRコードをスキャンしてください。", "scan_qr_code": "QRコードをスキャン", @@ -1338,15 +1330,8 @@ "report_to_moderators_description": "モデレートをサポートするルームで「報告」ボタンを使用すると、ルームのモデレーターに問題を報告できます。", "rust_crypto": "Rustによる暗号の実装", "sliding_sync": "スライド式同期モード", - "sliding_sync_checking": "確認しています…", - "sliding_sync_configuration": "スライド式同期の設定", "sliding_sync_description": "開発中です。無効にできません。", - "sliding_sync_disable_warning": "無効にするにはログアウトして、再度ログインする必要があります。注意して使用してください!", - "sliding_sync_proxy_url_label": "プロクシーのURL", - "sliding_sync_proxy_url_optional_label": "プロクシーのURL(任意)", "sliding_sync_server_no_support": "あなたのサーバーはネイティブでサポートしていません", - "sliding_sync_server_specify_proxy": "あなたのサーバーはネイティブでサポートしていません。プロクシーを指定してください", - "sliding_sync_server_support": "あなたのサーバーはネイティブでサポートしています", "under_active_development": "開発中。", "video_rooms": "ビデオ通話ルーム", "video_rooms_a_new_way_to_chat": "%(brand)sで音声と動画により会話する新しい方法です。", diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index 9352a3cf2f..f354686349 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -221,14 +221,6 @@ "approve_access_warning": "Door de toegang voor dit apparaat goed te keuren, heeft het volledige toegang tot jouw account.", "completing_setup": "De configuratie van je nieuwe apparaat voltooien", "confirm_code_match": "Controleer of de onderstaande code overeenkomt met je andere apparaat:", - "error_device_already_signed_in": "Het andere apparaat is al aangemeld.", - "error_device_not_signed_in": "Het andere apparaat is niet ingelogd.", - "error_device_unsupported": "Koppelen met dit apparaat wordt niet ondersteund.", - "error_homeserver_lacks_support": "De server ondersteunt het inloggen op een ander apparaat niet.", - "error_invalid_scanned_code": "De gescande code is ongeldig.", - "error_linking_incomplete": "De koppeling is niet binnen de vereiste tijd voltooid.", - "error_request_cancelled": "Het verzoek is geannuleerd.", - "error_request_declined": "Het verzoek is afgewezen op het andere apparaat.", "error_unexpected": "Er is een onverwachte fout opgetreden.", "scan_code_instruction": "Scan de onderstaande QR-code met je apparaat dat is uitgelogd.", "sign_in_new_device": "Aanmelden nieuw apparaat", @@ -1203,12 +1195,7 @@ "leave_beta_reload": "Als je de bèta verlaat, wordt %(brand)s opnieuw geladen.", "msc3531_hide_messages_pending_moderation": "Laat moderators berichten verbergen in afwachting van moderatie.", "pinning": "Berichten vastprikken", - "sliding_sync_configuration": "Scrollende Synchronisatie-configuratie", - "sliding_sync_disable_warning": "Om uit te schakelen moet je uitloggen en weer inloggen, wees voorzichtig!", - "sliding_sync_proxy_url_optional_label": "Proxy-URL (optioneel)", "sliding_sync_server_no_support": "Jouw server heeft geen native ondersteuning", - "sliding_sync_server_specify_proxy": "Jouw server heeft geen native ondersteuning, je moet een proxy opgeven", - "sliding_sync_server_support": "Jouw server heeft native ondersteuning", "video_rooms": "Video kamers", "video_rooms_a_new_way_to_chat": "Een nieuwe manier om te chatten via spraak en video in %(brand)s.", "video_rooms_always_on_voip_channels": "Videoruimten zijn altijd-aan VoIP-kanalen die zijn geïntegreerd in een kamer in %(brand)s.", diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index 487860cfe0..58c779e2ed 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -249,15 +249,7 @@ "completing_setup": "Kończenie konfiguracji nowego urządzenia", "confirm_code_match": "Potwierdź, że kod poniżej pasuje z Twoim drugim urządzeniem:", "connecting": "Łączenie…", - "error_device_already_signed_in": "Drugie urządzenie jest już zalogowane.", - "error_device_not_signed_in": "Drugie urządzenie nie jest zalogowane.", - "error_device_unsupported": "Wiązanie z tym urządzeniem nie jest wspierane.", - "error_homeserver_lacks_support": "Serwer domowy nie wspiera logowania innych urządzeń.", - "error_invalid_scanned_code": "Zeskanowany kod jest nieprawidłowy.", - "error_linking_incomplete": "Wiązanie nie zostało zakończone w ustalonym czasie.", "error_rate_limited": "Za dużo prób w krótkim odstępie czasu. Odczekaj trochę, zanim spróbujesz ponownie.", - "error_request_cancelled": "Żądanie zostało anulowane.", - "error_request_declined": "Żądanie zostało odrzucone przez drugie urządzenie.", "error_unexpected": "Wystąpił niespodziewany błąd.", "follow_remaining_instructions": "Podążaj zgodnie z pozostałymi instrukcjami, aby zweryfikować drugie urządzenie", "open_element_other_device": "Otwórz %(brand)s na swoim drugim urządzeniu", @@ -1454,18 +1446,9 @@ "rust_crypto_optin_warning": "Przejście na kryptografię Rust wymaga procesu migracji, która może potrwać kilka minut. Aby ją wyłączyć, będziesz musiał zalogować się ponownie; zachowaj ostrożność!", "rust_crypto_requires_logout": "Po włączeniu, kryptografia Rust może zostać wyłączona tylko po ponownym zalogowaniu.", "sliding_sync": "Tryb synchronizacji przesuwanej", - "sliding_sync_checking": "Sprawdzanie…", - "sliding_sync_configuration": "Konfiguracja synchronizacji przesuwanej", "sliding_sync_description": "W trakcie aktywnego rozwoju, nie można wyłączyć.", - "sliding_sync_disable_warning": "By wyłączyć, będziesz musiał się zalogować ponownie. Korzystaj z rozwagą!", "sliding_sync_disabled_notice": "Zaloguj się ponownie, aby wyłączyć", - "sliding_sync_proxy_url_label": "URL proxy", - "sliding_sync_proxy_url_optional_label": "URL proxy (opcjonalne)", "sliding_sync_server_no_support": "Twój serwer nie posiada wsparcia natywnego", - "sliding_sync_server_specify_proxy": "Twój serwer nie posiada wsparcia natywnego, musisz podać serwer proxy", - "sliding_sync_server_support": "Twój serwer posiada wsparcie natywne", - "threads_activity_centre": "Centrum aktywności wątków (w trakcie rozwoju)", - "threads_activity_centre_description": "Ostrzeżenie: W trakcie aktywnego rozwoju; przeładowuje %(brand)s.", "under_active_development": "W trakcie aktywnego rozwoju.", "unrealiable_e2e": "Problematyczny w pokojach szyfrowanych", "video_rooms": "Pokoje wideo", diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index 9c42600f78..14714fcc71 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -244,14 +244,6 @@ "completing_setup": "Завершение настройки нового устройства", "confirm_code_match": "Проверьте, чтобы код ниже совпадал с тем, что показан на другом устройстве:", "connecting": "Подключение…", - "error_device_already_signed_in": "Уже выполнен вход на другом устройстве.", - "error_device_not_signed_in": "На другом устройстве вход не выполнен.", - "error_device_unsupported": "Соединение с этим устройством не поддерживается.", - "error_homeserver_lacks_support": "Домашний сервер не поддерживает вход с другого устройства.", - "error_invalid_scanned_code": "Отсканированный код недействителен.", - "error_linking_incomplete": "Соединение не было завершено в нужное время.", - "error_request_cancelled": "Запрос был отменён.", - "error_request_declined": "Запрос был отклонен на другом устройстве.", "error_unexpected": "Произошла неожиданная ошибка.", "scan_code_instruction": "Отсканируйте приведенный ниже QR-код на устройстве, которое вышло из системы.", "scan_qr_code": "Сканировать QR-код", @@ -1430,16 +1422,9 @@ "report_to_moderators_description": "В поддерживающих модерирование комнатах, кнопка \"Пожаловаться\" позволит вам сообщить о нарушении модераторам комнаты.", "rust_crypto": "Реализация криптографии Rust", "sliding_sync": "Режим Sliding Sync", - "sliding_sync_checking": "Проверка…", - "sliding_sync_configuration": "Настройка Sliding sync", "sliding_sync_description": "В активной разработке, нельзя отключить.", - "sliding_sync_disable_warning": "Чтобы отключить, вам нужно выйти из системы и снова войти в систему, используйте с осторожностью!", "sliding_sync_disabled_notice": "Выйдите из системы и снова войдите, чтобы отключить", - "sliding_sync_proxy_url_label": "URL-адрес прокси-сервера", - "sliding_sync_proxy_url_optional_label": "URL-адрес прокси-сервера (необязательно)", "sliding_sync_server_no_support": "На вашем сервере отсутствует встроенная поддержка", - "sliding_sync_server_specify_proxy": "На вашем сервере отсутствует встроенная поддержка, необходимо указать прокси-сервер", - "sliding_sync_server_support": "Ваш сервер имеет встроенную поддержку", "under_active_development": "В активной разработке.", "unrealiable_e2e": "Ненадежно в зашифрованных комнатах", "video_rooms": "Видеокомнаты", diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json index ac3c1ab5c0..19acabb910 100644 --- a/src/i18n/strings/sk.json +++ b/src/i18n/strings/sk.json @@ -244,14 +244,6 @@ "completing_setup": "Dokončenie nastavenia nového zariadenia", "confirm_code_match": "Skontrolujte, či sa nižšie uvedený kód zhoduje s vaším druhým zariadením:", "connecting": "Pripájanie…", - "error_device_already_signed_in": "Druhé zariadenie je už prihlásené.", - "error_device_not_signed_in": "Druhé zariadenie nie je prihlásené.", - "error_device_unsupported": "Prepojenie s týmto zariadením nie je podporované.", - "error_homeserver_lacks_support": "Domovský server nepodporuje prihlasovanie do iného zariadenia.", - "error_invalid_scanned_code": "Naskenovaný kód je neplatný.", - "error_linking_incomplete": "Prepojenie nebolo dokončené v požadovanom čase.", - "error_request_cancelled": "Žiadosť bola zrušená.", - "error_request_declined": "Žiadosť bola na druhom zariadení zamietnutá.", "error_unexpected": "Vyskytla sa neočakávaná chyba.", "scan_code_instruction": "Naskenujte nižšie uvedený QR kód pomocou zariadenia, ktoré je odhlásené.", "scan_qr_code": "Skenovať QR kód", @@ -1435,16 +1427,9 @@ "report_to_moderators_description": "V miestnostiach, ktoré podporujú moderovanie, môžete pomocou tlačidla \"Nahlásiť\" nahlásiť porušovanie pravidiel moderátorom miestnosti.", "rust_crypto": "Implementácia kryptografie Rust", "sliding_sync": "Režim kĺzavej synchronizácie", - "sliding_sync_checking": "Kontrolovanie…", - "sliding_sync_configuration": "Konfigurácia kĺzavej synchronizácie", "sliding_sync_description": "V štádiu aktívneho vývoja, nie je možné to vypnúť.", - "sliding_sync_disable_warning": "Pre vypnutie sa musíte odhlásiť a znova prihlásiť, používajte opatrne!", "sliding_sync_disabled_notice": "Odhláste sa a znova sa prihláste, aby sa to vyplo", - "sliding_sync_proxy_url_label": "URL adresa proxy servera", - "sliding_sync_proxy_url_optional_label": "URL adresa proxy servera (voliteľná)", "sliding_sync_server_no_support": "Váš server nemá natívnu podporu", - "sliding_sync_server_specify_proxy": "Váš server nemá natívnu podporu, musíte zadať proxy server", - "sliding_sync_server_support": "Váš server má natívnu podporu", "under_active_development": "V štádiu aktívneho vývoja.", "unrealiable_e2e": "Nespoľahlivé v šifrovaných miestnostiach", "video_rooms": "Video miestnosti", diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 8fd413b041..b875650be7 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -235,15 +235,7 @@ "completing_setup": "Po plotësohet ujdisja e pajisjes tuaj të re", "confirm_code_match": "Kontrolloni se kodi më poshtë përkon me atë në pajisjen tuaj tjetër:", "connecting": "Po lidhet…", - "error_device_already_signed_in": "Nga pajisja tjetër është bërë tashmë hyrja.", - "error_device_not_signed_in": "Te pajisja tjetër s’është bërë hyrja.", - "error_device_unsupported": "Lidhja me këtë pajisje nuk mbulohet.", - "error_homeserver_lacks_support": "Shërbyesi Home nuk mbulon bërje hyrjeje në një pajisje tjetër.", - "error_invalid_scanned_code": "Kodi i skanuar është i pavlefshëm.", - "error_linking_incomplete": "Lidhja s’u plotësua brenda kohës së domosdoshme.", "error_rate_limited": "Shumë përpjekje në një kohë të shkurtër. Prisni ca, para se të riprovoni.", - "error_request_cancelled": "Kërkesa u anulua.", - "error_request_declined": "Kërkesa u hodh poshtë në pajisjen tjetër.", "error_unexpected": "Ndodhi një gabim të papritur.", "scan_code_instruction": "Skanoni kodin QR më poshtë me pajisjen ku është bërë dalja.", "scan_qr_code": "Skanoni kodin QR", @@ -1348,16 +1340,9 @@ "report_to_moderators": "Raportojeni te moderatorët", "report_to_moderators_description": "Në dhoma që mbulojnë moderimin, butoni “Raportojeni” do t’ju lejojë t’u raportoni abuzim moderatorëve të dhomës.", "rust_crypto": "Sendërtim kriptografie Rust", - "sliding_sync_checking": "Po kontrollohet…", - "sliding_sync_configuration": "Formësim Sliding Sync-u", "sliding_sync_description": "Nën zhvillim aktiv, s’mund të çaktivizohet.", - "sliding_sync_disable_warning": "Për ta çaktivizuar do t’ju duhet të bëni daljen dhe ribëni hyrjen, përdoreni me kujdes!", "sliding_sync_disabled_notice": "Që të çaktivizohet, dilni dhe rihyni në llogari", - "sliding_sync_proxy_url_label": "URL Ndërmjetësi", - "sliding_sync_proxy_url_optional_label": "URL ndërmjetësi (opsionale)", "sliding_sync_server_no_support": "Shërbyesit tuaj i mungon mbulim i brendshëm për këtë", - "sliding_sync_server_specify_proxy": "Shërbyesit tuaj i mungon mbulimi së brendshmi, duhet të specifikoni një ndërmjetës", - "sliding_sync_server_support": "Shërbyesi juaj ka mbulim të brendshëm për këtë", "under_active_development": "Nën zhvillim aktiv.", "video_rooms": "Dhoma me video", "video_rooms_a_new_way_to_chat": "Një rrugë e re për të biseduar me zë dhe video në %(brand)s.", diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json index 153aabad63..3b4e66d40a 100644 --- a/src/i18n/strings/sv.json +++ b/src/i18n/strings/sv.json @@ -249,15 +249,7 @@ "completing_setup": "Slutför inställning av din nya enhet", "confirm_code_match": "Kolla att koden nedan matchar din andra enhet:", "connecting": "Kopplar upp …", - "error_device_already_signed_in": "Den andra enheten är redan inloggad.", - "error_device_not_signed_in": "Den andra enheten är inte inloggad.", - "error_device_unsupported": "Länkning med den här enheten stöds inte.", - "error_homeserver_lacks_support": "Hemservern stöder inte inloggning av en annan enhet.", - "error_invalid_scanned_code": "Den skannade koden är ogiltig.", - "error_linking_incomplete": "Länkningen slutfördes inte inom den krävda tiden.", "error_rate_limited": "För många försök under för kort tid. Vänta ett tag innan du försöker igen.", - "error_request_cancelled": "Förfrågan avbröts.", - "error_request_declined": "Förfrågan nekades på den andra enheten.", "error_unexpected": "Ett oväntade fel inträffade.", "scan_code_instruction": "Skanna QR-koden nedan med din andra enhet som är utloggad.", "scan_qr_code": "Skanna QR-kod", @@ -1445,18 +1437,9 @@ "rust_crypto_optin_warning": "Byte till Rust-kryptografi kräver en migreringsprocess som kan ta flera minuter. För att inaktivera måste du logga ut och in igen; använd med försiktighet!", "rust_crypto_requires_logout": "När Rust-kryptografi har aktiverats kan den endast avaktiveras genom att logga ut och logga in igen", "sliding_sync": "Sliding sync-läge", - "sliding_sync_checking": "Kontrollerar …", - "sliding_sync_configuration": "Sliding sync-konfiguration", "sliding_sync_description": "Under aktiv utveckling, kan inte inaktiveras.", - "sliding_sync_disable_warning": "För att inaktivera det här så behöver du logga ut och logga in igen, använd varsamt!", "sliding_sync_disabled_notice": "Logga ut och in igen för att inaktivera", - "sliding_sync_proxy_url_label": "Proxy-URL", - "sliding_sync_proxy_url_optional_label": "Proxy-URL (valfritt)", "sliding_sync_server_no_support": "Din server saknar nativt stöd", - "sliding_sync_server_specify_proxy": "Din server saknar nativt stöd, du måste ange en proxy", - "sliding_sync_server_support": "Din server har nativt stöd", - "threads_activity_centre": "Aktivitetscenter för trådar (under utveckling). För närvarande tar detta bara bort antalet trådaviseringar från det totala antalet i rumslistan", - "threads_activity_centre_description": "Varning: Under aktiv utveckling; laddar om Element.", "under_active_development": "Under aktiv utveckling.", "unrealiable_e2e": "Otillförlitlig i krypterade rum", "video_rooms": "Videorum", diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json index bdd073f27e..92c7f29ec6 100644 --- a/src/i18n/strings/uk.json +++ b/src/i18n/strings/uk.json @@ -241,15 +241,7 @@ "completing_setup": "Завершення налаштування нового пристрою", "confirm_code_match": "Перевірте, чи збігається наведений внизу код з кодом на вашому іншому пристрої:", "connecting": "З'єднання…", - "error_device_already_signed_in": "На іншому пристрої вхід було виконано.", - "error_device_not_signed_in": "На іншому пристрої вхід не виконано.", - "error_device_unsupported": "Зв'язок з цим пристроєм не підтримується.", - "error_homeserver_lacks_support": "Домашній сервер не підтримує вхід на іншому пристрої.", - "error_invalid_scanned_code": "Сканований код недійсний.", - "error_linking_incomplete": "У встановлені терміни з'єднання не було виконано.", "error_rate_limited": "Забагато спроб за короткий час. Зачекайте трохи, перш ніж повторити спробу.", - "error_request_cancelled": "Запит було скасовано.", - "error_request_declined": "На іншому пристрої запит відхилено.", "error_unexpected": "Виникла непередбачувана помилка.", "scan_code_instruction": "Скануйте QR-код знизу своїм пристроєм, на якому ви вийшли.", "scan_qr_code": "Скануйте QR-код", @@ -1396,16 +1388,9 @@ "report_to_moderators_description": "У кімнатах, які підтримують модерацію, кнопка «Поскаржитися» дає змогу повідомити про зловживання модераторам кімнати.", "rust_crypto": "Реалізація криптографії Rust", "sliding_sync": "Режим ковзної синхронізації", - "sliding_sync_checking": "Перевірка…", - "sliding_sync_configuration": "Конфігурація ковзної синхронізації", "sliding_sync_description": "На стадії активної розробки, вимкнути не можна.", - "sliding_sync_disable_warning": "Для вимкнення потрібно буде вийти з системи та зайти знову, користуйтеся з обережністю!", "sliding_sync_disabled_notice": "Вийдіть і знову увійдіть, щоб вимкнути", - "sliding_sync_proxy_url_label": "URL-адреса проксі-сервера", - "sliding_sync_proxy_url_optional_label": "URL-адреса проксі-сервера (необов'язково)", "sliding_sync_server_no_support": "На вашому сервері немає вбудованої підтримки", - "sliding_sync_server_specify_proxy": "На вашому сервері немає вбудованої підтримки, ви повинні вказати проксі", - "sliding_sync_server_support": "Ваш сервер має вбудовану підтримку", "under_active_development": "У стадії активної розробки.", "video_rooms": "Відеокімнати", "video_rooms_a_new_way_to_chat": "Новий спосіб спілкування за допомогою голосового та відеозв’язку в %(brand)s.", diff --git a/src/i18n/strings/vi.json b/src/i18n/strings/vi.json index 69c222ca6b..29f6cc2fd0 100644 --- a/src/i18n/strings/vi.json +++ b/src/i18n/strings/vi.json @@ -229,9 +229,6 @@ "password_field_weak_label": "Mật khẩu được phép, nhưng không an toàn", "phone_label": "Điện thoại", "phone_optional_label": "Điện thoại (tùy chọn)", - "qr_code_login": { - "error_invalid_scanned_code": "Mã vừa quét là không hợp lệ." - }, "register_action": "Tạo tài khoản", "registration": { "continue_without_email_description": "Lưu ý là nếu bạn không thêm địa chỉ thư điện tử và quên mật khẩu, bạn có thể vĩnh viễn mất quyền truy cập vào tài khoản của mình.", @@ -1291,10 +1288,7 @@ "sliding_sync": "Chế độ đồng bộ tối ưu (Sync v3)", "sliding_sync_description": "Đang được phát triển tích cực, không thể vô hiệu.", "sliding_sync_disabled_notice": "Đăng xuất và đăng nhập lại để vô hiệu hóa", - "sliding_sync_proxy_url_label": "Đường dẫn máy chủ ủy nhiệm (proxy)", "sliding_sync_server_no_support": "Máy chủ của bạn không hoàn toàn hỗ trợ", - "sliding_sync_server_specify_proxy": "Máy chủ của bạn không hỗ trợ, bạn cần chỉ định máy chủ ủy nhiệm (proxy)", - "sliding_sync_server_support": "Máy chủ của bạn hoàn toàn hỗ trợ", "under_active_development": "Đang được phát triển tích cực.", "video_rooms": "Phòng video", "video_rooms_a_new_way_to_chat": "Một cách mới để trò chuyện bằng thoại và video trong %(brand)s.", diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json index e03a592144..7a3ce3674d 100644 --- a/src/i18n/strings/zh_Hans.json +++ b/src/i18n/strings/zh_Hans.json @@ -243,8 +243,7 @@ "approve_access_warning": "为此设备批准访问权限后,它对你的帐户有完全的访问权限。", "completing_setup": "完成新设备的设置", "confirm_code_match": "检查以下代码是否与你的其他设备匹配:", - "connecting": "正在连接……", - "error_homeserver_lacks_support": "此服务器不支持多设备登录" + "connecting": "正在连接……" }, "register_action": "创建账户", "registration": { @@ -1309,12 +1308,7 @@ "rust_crypto": "Rust加密实现", "sliding_sync": "滑动同步模式", "sliding_sync_description": "正在积极开发中,不能禁用。", - "sliding_sync_disable_warning": "要停用,你必须登出并重新登录,请小心!", - "sliding_sync_proxy_url_label": "代理URL", - "sliding_sync_proxy_url_optional_label": "代理URL(可选)", "sliding_sync_server_no_support": "你的服务器缺少原生支持", - "sliding_sync_server_specify_proxy": "你的服务器缺少原生支持,你必须指定代理", - "sliding_sync_server_support": "你的服务器有原生支持", "under_active_development": "积极开发中。", "video_rooms": "视频房间", "video_rooms_a_new_way_to_chat": "在 %(brand)s 中使用语音和视频的新方式。", diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 41f63b5b18..8049491f01 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -241,15 +241,7 @@ "completing_setup": "完成您新裝置的設定", "confirm_code_match": "請確認下列代碼與您另一台裝置上的代碼相符:", "connecting": "連線中…", - "error_device_already_signed_in": "其他裝置已登入。", - "error_device_not_signed_in": "其他裝置未登入。", - "error_device_unsupported": "不支援與此裝置連結。", - "error_homeserver_lacks_support": "家伺服器不支援在其他裝置上登入。", - "error_invalid_scanned_code": "掃描的代碼無效。", - "error_linking_incomplete": "未在要求的時間內完成連結。", "error_rate_limited": "短時間內嘗試太多次,請稍待一段時間後再嘗試。", - "error_request_cancelled": "請求已取消。", - "error_request_declined": "請求在另一台裝置上被拒絕。", "error_unexpected": "發生預料之外的錯誤。", "scan_code_instruction": "請用您已登出的裝置掃描下列 QR Code。", "scan_qr_code": "掃描 QR Code", @@ -1399,16 +1391,9 @@ "report_to_moderators_description": "在支援審核的聊天室中,「回報」按鈕讓您可以回報濫用行為給聊天室管理員。", "rust_crypto": "Rust 密碼學實作", "sliding_sync": "滑動同步模式", - "sliding_sync_checking": "正在檢查…", - "sliding_sync_configuration": "滑動同步設定", "sliding_sync_description": "正在積極開發中,無法停用。", - "sliding_sync_disable_warning": "要停用,您必須登出並重新登入,請小心使用!", "sliding_sync_disabled_notice": "登出並重新登入以停用", - "sliding_sync_proxy_url_label": "代理伺服器網址", - "sliding_sync_proxy_url_optional_label": "代理伺服器網址(選填)", "sliding_sync_server_no_support": "您的伺服器缺乏原生支援", - "sliding_sync_server_specify_proxy": "您的伺服器缺乏原生支援,您必須指定代理", - "sliding_sync_server_support": "您的伺服器有原生支援", "under_active_development": "正在積極開發中。", "video_rooms": "視訊聊天室", "video_rooms_a_new_way_to_chat": "在 %(brand)s 中透過語音及視訊聊天的新方式。", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 6be0a6b46f..3650e51814 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -47,6 +47,7 @@ import ServerSupportUnstableFeatureController from "./controllers/ServerSupportU import { WatchManager } from "./WatchManager"; import { CustomTheme } from "../theme"; import SettingsStore from "./SettingsStore"; +import AnalyticsController from "./controllers/AnalyticsController"; export const defaultWatchManager = new WatchManager(); @@ -406,7 +407,7 @@ export const SETTINGS: { [setting: string]: ISetting } = { controller: new SlidingSyncController(), }, "feature_sliding_sync_proxy_url": { - // This is not a distinct feature, it is a setting for feature_sliding_sync above + // This is not a distinct feature, it is a legacy setting for feature_sliding_sync above supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, default: "", }, @@ -597,11 +598,13 @@ export const SETTINGS: { [setting: string]: ISetting } = { displayName: _td("settings|showbold"), default: false, invertedSettingName: "feature_hidebold", + controller: new AnalyticsController("WebSettingsNotificationsShowBoldToggle"), }, "Notifications.tac_only_notifications": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, displayName: _td("settings|tac_only_notifications"), default: true, + controller: new AnalyticsController("WebSettingsNotificationsTACOnlyNotificationsToggle"), }, "feature_ask_to_join": { isFeature: true, @@ -1148,15 +1151,6 @@ export const SETTINGS: { [setting: string]: ISetting } = { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, default: [], }, - "threadsActivityCentre": { - supportedLevels: LEVELS_ACCOUNT_SETTINGS, - labsGroup: LabGroup.Threads, - controller: new ReloadOnChangeController(), - displayName: _td("labs|threads_activity_centre"), - description: () => _t("labs|threads_activity_centre_description", { brand: SdkConfig.get().brand }), - default: false, - isFeature: true, - }, /** * Enable or disable the release announcement feature */ diff --git a/src/settings/controllers/AnalyticsController.ts b/src/settings/controllers/AnalyticsController.ts new file mode 100644 index 0000000000..5c127ed3b9 --- /dev/null +++ b/src/settings/controllers/AnalyticsController.ts @@ -0,0 +1,42 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import SettingController from "./SettingController"; +import { SettingLevel } from "../SettingLevel"; +import PosthogTrackers, { InteractionName } from "../../PosthogTrackers"; + +/** + * Controller that sends events to analytics when a setting is changed. + * Since it will only trigger events when the setting is changed, + * (and the value isn't reported: only the fact that it's been toggled) + * it won't be useful for tracking what percentage of a userbase has a given setting + * enabled, but many of our settings can be set per device and Posthog only supports + * per-user properties, so this isn't straightforward. This is only for seeing how + * often people interact with the settings. + */ +export default class AnalyticsController extends SettingController { + /** + * + * @param interactionName The name of the event to send to analytics + */ + public constructor(private interactionName: InteractionName) { + super(); + } + + public onChange(_level: SettingLevel, _roomId: string | null, _newValue: any): void { + PosthogTrackers.trackInteraction(this.interactionName); + } +} diff --git a/src/settings/controllers/SlidingSyncController.ts b/src/settings/controllers/SlidingSyncController.ts index 77bdf7f42f..7d7ca78128 100644 --- a/src/settings/controllers/SlidingSyncController.ts +++ b/src/settings/controllers/SlidingSyncController.ts @@ -1,5 +1,6 @@ /* Copyright 2022 The Matrix.org Foundation C.I.C. +Copyright 2024 Ed Geraghty Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,18 +17,11 @@ limitations under the License. import SettingController from "./SettingController"; import PlatformPeg from "../../PlatformPeg"; -import { SettingLevel } from "../SettingLevel"; -import { SlidingSyncOptionsDialog } from "../../components/views/dialogs/SlidingSyncOptionsDialog"; -import Modal from "../../Modal"; import SettingsStore from "../SettingsStore"; import { _t } from "../../languageHandler"; export default class SlidingSyncController extends SettingController { - public async beforeChange(level: SettingLevel, roomId: string, newValue: any): Promise { - const { finished } = Modal.createDialog(SlidingSyncOptionsDialog); - const [value] = await finished; - return newValue === value; // abort the operation if we're already in the state the user chose via modal - } + public static serverSupportsSlidingSync: boolean; public async onChange(): Promise { PlatformPeg.get()?.reload(); @@ -38,6 +32,9 @@ export default class SlidingSyncController extends SettingController { if (SettingsStore.getValue("feature_sliding_sync")) { return _t("labs|sliding_sync_disabled_notice"); } + if (!SlidingSyncController.serverSupportsSlidingSync) { + return _t("labs|sliding_sync_server_no_support"); + } return false; } diff --git a/src/stores/notifications/RoomNotificationStateStore.ts b/src/stores/notifications/RoomNotificationStateStore.ts index f2d10ac4fb..502d2dcce7 100644 --- a/src/stores/notifications/RoomNotificationStateStore.ts +++ b/src/stores/notifications/RoomNotificationStateStore.ts @@ -42,8 +42,6 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient { private listMap = new Map(); private _globalState = new SummarizedNotificationState(); - private tacEnabled = SettingsStore.getValue("threadsActivityCentre"); - private constructor(dispatcher = defaultDispatcher) { super(dispatcher, {}); SettingsStore.watchSetting("feature_dynamic_room_predecessors", null, () => { @@ -99,7 +97,7 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient { */ public getRoomState(room: Room): RoomNotificationState { if (!this.roomMap.has(room)) { - this.roomMap.set(room, new RoomNotificationState(room, !this.tacEnabled)); + this.roomMap.set(room, new RoomNotificationState(room, false)); } return this.roomMap.get(room)!; } diff --git a/src/stores/room-list/MessagePreviewStore.ts b/src/stores/room-list/MessagePreviewStore.ts index 647300f99c..a3c44084d5 100644 --- a/src/stores/room-list/MessagePreviewStore.ts +++ b/src/stores/room-list/MessagePreviewStore.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Room, RelationType, MatrixEvent, Thread, M_POLL_START } from "matrix-js-sdk/src/matrix"; +import { Room, RelationType, MatrixEvent, Thread, M_POLL_START, RoomEvent } from "matrix-js-sdk/src/matrix"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import { ActionPayload } from "../../dispatcher/payloads"; @@ -186,7 +186,7 @@ export class MessagePreviewStore extends AsyncStoreWithClient { } private async generatePreview(room: Room, tagId?: TagID): Promise { - const events = [...room.getLiveTimeline().getEvents()]; + const events = [...room.getLiveTimeline().getEvents(), ...room.getPendingEvents()]; // add last reply from each thread room.getThreads().forEach((thread: Thread): void => { @@ -279,4 +279,19 @@ export class MessagePreviewStore extends AsyncStoreWithClient { await this.generatePreview(room, TAG_ANY); } } + + protected async onReady(): Promise { + if (!this.matrixClient) return; + this.matrixClient.on(RoomEvent.LocalEchoUpdated, this.onLocalEchoUpdated); + } + + protected async onNotReady(): Promise { + if (!this.matrixClient) return; + this.matrixClient.off(RoomEvent.LocalEchoUpdated, this.onLocalEchoUpdated); + } + + protected onLocalEchoUpdated = async (ev: MatrixEvent, room: Room): Promise => { + if (!this.previews.has(room.roomId)) return; + await this.generatePreview(room, TAG_ANY); + }; } diff --git a/test/DecryptionFailureTracker-test.ts b/test/DecryptionFailureTracker-test.ts index 7a0bf65f81..305692fce8 100644 --- a/test/DecryptionFailureTracker-test.ts +++ b/test/DecryptionFailureTracker-test.ts @@ -388,4 +388,60 @@ describe("DecryptionFailureTracker", function () { // should track remapped error code expect(counts["XEDNI_EGASSEM_NWONKNU_MLO"]).toBe(1); }); + + it("default error code mapper maps error codes correctly", async () => { + const errorCodes: string[] = []; + + // @ts-ignore access to private constructor + const tracker = new DecryptionFailureTracker( + (total: number, errorCode: string) => { + errorCodes.push(errorCode); + }, + // @ts-ignore access to private member + DecryptionFailureTracker.instance.errorCodeMapFn, + ); + + const event1 = await createFailedDecryptionEvent(DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID); + tracker.addVisibleEvent(event1); + tracker.eventDecrypted(event1); + + const event2 = await createFailedDecryptionEvent(DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX); + tracker.addVisibleEvent(event2); + tracker.eventDecrypted(event2); + + const event3 = await createFailedDecryptionEvent(DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP); + tracker.addVisibleEvent(event3); + tracker.eventDecrypted(event3); + + const event4 = await createFailedDecryptionEvent(DecryptionFailureCode.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED); + tracker.addVisibleEvent(event4); + tracker.eventDecrypted(event4); + + const event5 = await createFailedDecryptionEvent(DecryptionFailureCode.HISTORICAL_MESSAGE_WORKING_BACKUP); + tracker.addVisibleEvent(event5); + tracker.eventDecrypted(event5); + + const event6 = await createFailedDecryptionEvent(DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED); + tracker.addVisibleEvent(event6); + tracker.eventDecrypted(event6); + + const event7 = await createFailedDecryptionEvent(DecryptionFailureCode.UNKNOWN_ERROR); + tracker.addVisibleEvent(event7); + tracker.eventDecrypted(event7); + + // Pretend "now" is Infinity + tracker.checkFailures(Infinity); + + tracker.trackFailures(); + + expect(errorCodes).toEqual([ + "OlmKeysNotSentError", + "OlmIndexError", + "HistoricalMessage", + "HistoricalMessage", + "HistoricalMessage", + "ExpectedDueToMembership", + "UnknownError", + ]); + }); }); diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index 748c8f17f6..c131e536d1 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -36,7 +36,7 @@ const getFakePosthog = (): PostHog => register: jest.fn(), get_distinct_id: jest.fn(), persistence: { - get_user_state: jest.fn(), + get_property: jest.fn(), }, identifyUser: jest.fn(), }) as unknown as PostHog; diff --git a/test/SlidingSyncManager-test.ts b/test/SlidingSyncManager-test.ts index 76ebd8f15c..757a682d84 100644 --- a/test/SlidingSyncManager-test.ts +++ b/test/SlidingSyncManager-test.ts @@ -20,6 +20,8 @@ import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; import { SlidingSyncManager } from "../src/SlidingSyncManager"; import { stubClient } from "./test-utils"; +import SlidingSyncController from "../src/settings/controllers/SlidingSyncController"; +import SettingsStore from "../src/settings/SettingsStore"; jest.mock("matrix-js-sdk/src/sliding-sync"); const MockSlidingSync = >(SlidingSync); @@ -231,4 +233,53 @@ describe("SlidingSyncManager", () => { ); }); }); + describe("checkSupport", () => { + beforeEach(() => { + SlidingSyncController.serverSupportsSlidingSync = false; + jest.spyOn(manager, "getProxyFromWellKnown").mockResolvedValue("proxy"); + }); + it("shorts out if the server has 'native' sliding sync support", async () => { + jest.spyOn(manager, "nativeSlidingSyncSupport").mockResolvedValue(true); + expect(SlidingSyncController.serverSupportsSlidingSync).toBeFalsy(); + await manager.checkSupport(client); + expect(manager.getProxyFromWellKnown).not.toHaveBeenCalled(); // We return earlier + expect(SlidingSyncController.serverSupportsSlidingSync).toBeTruthy(); + }); + it("tries to find a sliding sync proxy url from the client well-known if there's no 'native' support", async () => { + jest.spyOn(manager, "nativeSlidingSyncSupport").mockResolvedValue(false); + expect(SlidingSyncController.serverSupportsSlidingSync).toBeFalsy(); + await manager.checkSupport(client); + expect(manager.getProxyFromWellKnown).toHaveBeenCalled(); + expect(SlidingSyncController.serverSupportsSlidingSync).toBeTruthy(); + }); + }); + describe("setup", () => { + beforeEach(() => { + jest.spyOn(manager, "configure"); + jest.spyOn(manager, "startSpidering"); + }); + it("uses the baseUrl as a proxy if no proxy is set in the client well-known and the server has no native support", async () => { + await manager.setup(client); + expect(manager.configure).toHaveBeenCalled(); + expect(manager.configure).toHaveBeenCalledWith(client, client.baseUrl); + expect(manager.startSpidering).toHaveBeenCalled(); + }); + it("uses the proxy declared in the client well-known", async () => { + jest.spyOn(manager, "getProxyFromWellKnown").mockResolvedValue("proxy"); + await manager.setup(client); + expect(manager.configure).toHaveBeenCalled(); + expect(manager.configure).toHaveBeenCalledWith(client, "proxy"); + expect(manager.startSpidering).toHaveBeenCalled(); + }); + it("uses the legacy `feature_sliding_sync_proxy_url` if it was set", async () => { + jest.spyOn(manager, "getProxyFromWellKnown").mockResolvedValue("proxy"); + jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => { + if (name === "feature_sliding_sync_proxy_url") return "legacy-proxy"; + }); + await manager.setup(client); + expect(manager.configure).toHaveBeenCalled(); + expect(manager.configure).toHaveBeenCalledWith(client, "legacy-proxy"); + expect(manager.startSpidering).toHaveBeenCalled(); + }); + }); }); diff --git a/test/components/structures/MatrixChat-test.tsx b/test/components/structures/MatrixChat-test.tsx index 908c7a7c04..38309b8178 100644 --- a/test/components/structures/MatrixChat-test.tsx +++ b/test/components/structures/MatrixChat-test.tsx @@ -61,6 +61,7 @@ import SettingsStore from "../../../src/settings/SettingsStore"; import { SettingLevel } from "../../../src/settings/SettingLevel"; import { MatrixClientPeg as peg } from "../../../src/MatrixClientPeg"; import DMRoomMap from "../../../src/utils/DMRoomMap"; +import { ReleaseAnnouncementStore } from "../../../src/stores/ReleaseAnnouncementStore"; jest.mock("matrix-js-sdk/src/oidc/authorize", () => ({ completeAuthorizationCodeGrant: jest.fn(), @@ -627,6 +628,12 @@ describe("", () => { (id) => [room, spaceRoom].find((room) => room.roomId === id) || null, ); jest.spyOn(spaceRoom, "isSpaceRoom").mockReturnValue(true); + + jest.spyOn(ReleaseAnnouncementStore.instance, "getReleaseAnnouncement").mockReturnValue(null); + }); + + afterEach(() => { + jest.restoreAllMocks(); }); describe("leave_room", () => { diff --git a/test/components/structures/__snapshots__/RoomView-test.tsx.snap b/test/components/structures/__snapshots__/RoomView-test.tsx.snap index 80d6ac7c74..b6c87aaaef 100644 --- a/test/components/structures/__snapshots__/RoomView-test.tsx.snap +++ b/test/components/structures/__snapshots__/RoomView-test.tsx.snap @@ -397,6 +397,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
    ambiguous display name 1`] = `
    ambiguous display name 1`] = ` exports[` with display name 1`] = `
    with display name 1`] = ` exports[` without display name 1`] = `
    with crypto enabled renders 1`] = ` class="mx_BaseCard_header" >
    @@ -19,6 +20,7 @@ exports[`EventTileThreadToolbar renders 1`] = `
    diff --git a/test/components/views/rooms/MemberList-test.tsx b/test/components/views/rooms/MemberList-test.tsx index dbf5b270df..9098ee1e29 100644 --- a/test/components/views/rooms/MemberList-test.tsx +++ b/test/components/views/rooms/MemberList-test.tsx @@ -92,7 +92,7 @@ describe("MemberList", () => { let prevMember: RoomMember | undefined; for (const tile of memberTiles) { const memberA = prevMember; - const memberB = memberListRoom.currentState.members[tile.getAttribute("title")!.split(" ")[0]]; + const memberB = memberListRoom.currentState.members[tile.getAttribute("aria-label")!.split(" ")[0]]; prevMember = memberB; // just in case an expect fails, set this early if (!memberA) { continue; diff --git a/test/components/views/rooms/__snapshots__/MemberTile-test.tsx.snap b/test/components/views/rooms/__snapshots__/MemberTile-test.tsx.snap index f40db566bb..77c87d93da 100644 --- a/test/components/views/rooms/__snapshots__/MemberTile-test.tsx.snap +++ b/test/components/views/rooms/__snapshots__/MemberTile-test.tsx.snap @@ -4,10 +4,11 @@ exports[`MemberTile should display an verified E2EIcon when the e2E status = Ver
    ", () => { const onClick = jest.fn(); diff --git a/test/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap index a1b700c003..bd3ec55ca6 100644 --- a/test/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap +++ b/test/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap @@ -139,7 +139,9 @@ exports[` handles when device is falsy 1`] = ` aria-disabled="true" aria-expanded="false" aria-haspopup="true" + aria-label="Options" class="mx_AccessibleButton mx_AccessibleButton_disabled" + data-state="closed" data-testid="current-session-menu" disabled="" role="button" @@ -174,7 +176,9 @@ exports[` renders device and correct security card when
  2. - Follow the remaining instructions to verify your other device + Follow the instructions to link your other device
@@ -587,9 +707,6 @@ exports[` renders code when connected 1`] = ` class="mx_LoginWithQR" data-testid="login-with-qr" > -
@@ -615,14 +732,6 @@ exports[` renders code when connected 1`] = `
-
- Cancel -
renders code when connected 1`] = ` > Approve
+
+ Cancel +
@@ -643,27 +760,24 @@ exports[` renders spinner while connecting 1`] = ` data-testid="login-with-qr" >
-
-
-
-
- Sessions - / - Link new device -
+
+
+
+ Sessions + / + Link new device
renders spinner while loading 1`] = ` data-testid="login-with-qr" >
-
-
-
-
- Sessions - / - Link new device -
+
+
+
+ Sessions + / + Link new device
renders spinner while signing in 1`] = ` data-testid="login-with-qr" >
-
-
-
-
- Sessions - / - Link new device -
+
+
+
+ Sessions + / + Link new device
renders spinner while verifying 1`] = ` data-testid="login-with-qr" >
-
-
-
-
- Sessions - / - Link new device -
+
+
+
+ Sessions + / + Link new device
renders spinner whilst QR generating 1`] = ` data-testid="login-with-qr" >
-
-
-
-
- Sessions - / - Link new device -
+
+
+
+ Sessions + / + Link new device
diff --git a/test/components/views/settings/tabs/room/__snapshots__/PeopleRoomSettingsTab-test.tsx.snap b/test/components/views/settings/tabs/room/__snapshots__/PeopleRoomSettingsTab-test.tsx.snap index 81347ce2a6..1c720bf01d 100644 --- a/test/components/views/settings/tabs/room/__snapshots__/PeopleRoomSettingsTab-test.tsx.snap +++ b/test/components/views/settings/tabs/room/__snapshots__/PeopleRoomSettingsTab-test.tsx.snap @@ -68,10 +68,11 @@ exports[`PeopleRoomSettingsTab with requests to join renders requests fully 1`]
", () => { // non-beta labs section expect(screen.getByText("Early previews")).toBeInTheDocument(); const labsSections = container.getElementsByClassName("mx_SettingsSubsection"); - expect(labsSections).toHaveLength(11); + expect(labsSections).toHaveLength(10); }); describe("Rust crypto setting", () => { @@ -113,12 +114,14 @@ describe("", () => { expect(toggle.getAttribute("aria-checked")).toEqual("true"); // Hover over the toggle to make it show the tooltip - fireEvent.mouseOver(toggle); + await userEvent.hover(toggle); - const tooltip = rendered.getByRole("tooltip"); - expect(tooltip).toHaveTextContent( - "Once enabled, Rust cryptography can only be disabled by logging out and in again", - ); + await waitFor(() => { + const tooltip = screen.getByRole("tooltip"); + expect(tooltip).toHaveTextContent( + "Once enabled, Rust cryptography can only be disabled by logging out and in again", + ); + }); }); }); @@ -150,12 +153,14 @@ describe("", () => { expect(toggle.getAttribute("aria-checked")).toEqual("true"); // Hover over the toggle to make it show the tooltip - fireEvent.mouseOver(toggle); + await userEvent.hover(toggle); - const tooltip = rendered.getByRole("tooltip"); - expect(tooltip).toHaveTextContent( - "Rust cryptography cannot be disabled on this deployment of BrandedClient", - ); + await waitFor(() => { + const tooltip = rendered.getByRole("tooltip"); + expect(tooltip).toHaveTextContent( + "Rust cryptography cannot be disabled on this deployment of BrandedClient", + ); + }); }); }); }); diff --git a/test/components/views/settings/tabs/user/__snapshots__/PreferencesUserSettingsTab-test.tsx.snap b/test/components/views/settings/tabs/user/__snapshots__/PreferencesUserSettingsTab-test.tsx.snap index 45e1999af6..244746a909 100644 --- a/test/components/views/settings/tabs/user/__snapshots__/PreferencesUserSettingsTab-test.tsx.snap +++ b/test/components/views/settings/tabs/user/__snapshots__/PreferencesUserSettingsTab-test.tsx.snap @@ -289,6 +289,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` aria-disabled="true" aria-label="Send read receipts" class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on" + data-state="closed" id="mx_SettingsFlag_GQvdMWe954DV" role="switch" tabindex="0" diff --git a/test/components/views/settings/tabs/user/__snapshots__/SessionManagerTab-test.tsx.snap b/test/components/views/settings/tabs/user/__snapshots__/SessionManagerTab-test.tsx.snap index b871fce465..2de6a103bc 100644 --- a/test/components/views/settings/tabs/user/__snapshots__/SessionManagerTab-test.tsx.snap +++ b/test/components/views/settings/tabs/user/__snapshots__/SessionManagerTab-test.tsx.snap @@ -84,7 +84,9 @@ exports[` current session section renders current session s