diff --git a/.github/cfp_headers b/.github/cfp_headers index 5eb050e228..497a5ff58d 100644 --- a/.github/cfp_headers +++ b/.github/cfp_headers @@ -11,3 +11,6 @@ /apple-app-site-association Content-Type: application/json + +/.well-known/assetlinks.json + Content-Type: application/json diff --git a/.github/workflows/build_develop.yml b/.github/workflows/build_develop.yml index 13acfef761..7097099250 100644 --- a/.github/workflows/build_develop.yml +++ b/.github/workflows/build_develop.yml @@ -92,7 +92,7 @@ jobs: find bundles -type d -mindepth 1 -maxdepth 1 -exec sed -i "\:{}:d" _redirects \; - name: Wait for other steps to succeed - uses: t3chguy/wait-on-check-action@05861d3a448898eb33dfce34153bd1ecb9422fb9 # fork + uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork with: ref: ${{ github.sha }} running-workflow-name: "Build & Deploy develop.element.io" diff --git a/.github/workflows/dockerhub.yaml b/.github/workflows/dockerhub.yaml index 52e6eb5cde..3c64e4efbc 100644 --- a/.github/workflows/dockerhub.yaml +++ b/.github/workflows/dockerhub.yaml @@ -7,6 +7,9 @@ on: # This job can take a while, and we have usage limits, so just publish develop only twice a day - cron: "0 7/12 * * *" concurrency: ${{ github.workflow }}-${{ github.ref_name }} + +permissions: + id-token: write # needed for signing the images with GitHub OIDC Token jobs: buildx: name: Docker Buildx @@ -26,6 +29,9 @@ jobs: with: fetch-depth: 0 # needed for docker-package to be able to calculate the version + - name: Install Cosign + uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3 + - name: Prepare if: matrix.prepare run: ${{ matrix.prepare }} @@ -34,7 +40,7 @@ jobs: uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@2b51285047da1547ffb1b2203d8be4c0af6b1f20 # v3 + uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3 with: install: true @@ -58,6 +64,7 @@ jobs: ${{ matrix.flavor }} - name: Build and push + id: build-and-push uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5 with: context: . @@ -66,6 +73,17 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + - name: Sign the images with GitHub OIDC Token + env: + DIGEST: ${{ steps.build-and-push.outputs.digest }} + TAGS: ${{ steps.meta.outputs.tags }} + run: | + images="" + for tag in ${TAGS}; do + images+="${tag}@${DIGEST} " + done + cosign sign --yes ${images} + - name: Update repo description if: matrix.variant == 'vanilla' uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae # v4 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 5dea22bbb4..2aefb39a32 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -55,7 +55,7 @@ jobs: echo "- [Automations](automations.md)" >> docs/SUMMARY.md - name: Setup mdBook - uses: peaceiris/actions-mdbook@v1 + uses: peaceiris/actions-mdbook@v2 with: mdbook-version: "0.4.10" diff --git a/.github/workflows/downstream-artifacts.yml b/.github/workflows/downstream-artifacts.yml deleted file mode 100644 index 29d1a1fa93..0000000000 --- a/.github/workflows/downstream-artifacts.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: Build downstream artifacts -on: - merge_group: - types: [checks_requested] - pull_request: {} - push: - branches: [develop, master] -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true -jobs: - build-element-web: - name: Build element-web - uses: matrix-org/matrix-react-sdk/.github/workflows/element-web.yaml@develop - with: - element-web-sha: ${{ github.sha }} - react-sdk-repository: matrix-org/matrix-react-sdk diff --git a/.github/workflows/end-to-end-tests.yaml b/.github/workflows/end-to-end-tests.yaml index 1fcc64272c..782efb43b5 100644 --- a/.github/workflows/end-to-end-tests.yaml +++ b/.github/workflows/end-to-end-tests.yaml @@ -3,10 +3,11 @@ name: matrix-react-sdk End to End Tests on: - workflow_run: - workflows: ["Build downstream artifacts"] - types: - - completed + merge_group: + types: [checks_requested] + pull_request: {} + push: + branches: [develop, master] concurrency: group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }} @@ -15,44 +16,14 @@ concurrency: jobs: playwright: name: Playwright - # We only want to run the playwright tests on merge queue to prevent regressions - # from creeping in. They take a long time to run and consume multiple concurrent runners. - if: github.event.workflow_run.event == 'merge_group' uses: matrix-org/matrix-react-sdk/.github/workflows/end-to-end-tests.yaml@develop permissions: actions: read issues: read - statuses: write pull-requests: read - deployments: write with: + element-web-sha: ${{ github.sha }} react-sdk-repository: matrix-org/matrix-react-sdk - secrets: - ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} - - # We want to make the Playwright tests a required check for the merge queue. - # - # Unfortunately, github doesn't distinguish between "checks needed for branch - # protection" (ie, the things that must pass before the PR will even be added - # to the merge queue) and "checks needed in the merge queue". We just have to add - # the check to the branch protection list. - # - # Ergo, if we know we're not going to run the Playwright tests, we need to add a - # passing status check manually. - mark_skipped: - if: github.event.workflow_run.event != 'merge_group' - permissions: - statuses: write - runs-on: ubuntu-latest - steps: - - uses: Sibz/github-status-action@faaa4d96fecf273bd762985e0e7f9f933c774918 # v1 - with: - authToken: "${{ secrets.GITHUB_TOKEN }}" - state: success - description: Playwright skipped - - # Keep in step with the `context` that is updated by `Sibz/github-status-action` - # in matrix-org/matrix-react-sdk/.github/workflows/end-to-end-tests.yaml. - context: "${{ github.workflow }} / end-to-end-tests" - - sha: "${{ github.event.workflow_run.head_sha }}" + # We only want to run the playwright tests on merge queue to prevent regressions + # from creeping in. They take a long time to run and consume multiple concurrent runners. + skip: ${{ github.event_name != 'merge_group' }} diff --git a/.github/workflows/triage-labelled.yml b/.github/workflows/triage-labelled.yml index 95e1c55005..e57f2bf6d6 100644 --- a/.github/workflows/triage-labelled.yml +++ b/.github/workflows/triage-labelled.yml @@ -148,7 +148,7 @@ jobs: contains(github.event.issue.labels.*.name, 'A-Element-R') steps: - id: add_to_project - uses: actions/add-to-project@v1.0.0 + uses: actions/add-to-project@v1.0.1 with: project-url: ${{ env.PROJECT_URL }} github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} diff --git a/.github/workflows/triage-stale-flaky-tests.yml b/.github/workflows/triage-stale-flaky-tests.yml new file mode 100644 index 0000000000..70d63dd2df --- /dev/null +++ b/.github/workflows/triage-stale-flaky-tests.yml @@ -0,0 +1,17 @@ +name: Close stale flaky issues +on: + schedule: + - cron: "30 1 * * *" +jobs: + close: + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - uses: actions/stale@v9 + with: + only-labels: "Z-Flaky-Test" + days-before-stale: 14 + days-before-close: 0 + close-issue-message: "This flaky test issue has not been updated in 14 days. It is being closed as presumed resolved." + exempt-issue-labels: "Z-Flaky-Test-Disabled" diff --git a/.github/workflows/update-jitsi.yml b/.github/workflows/update-jitsi.yml index 49b56aadca..3a1ea33cbf 100644 --- a/.github/workflows/update-jitsi.yml +++ b/.github/workflows/update-jitsi.yml @@ -21,7 +21,7 @@ jobs: run: "yarn update:jitsi" - name: Create Pull Request - uses: peter-evans/create-pull-request@70a41aba780001da0a30141984ae2a0c95d8704e # v6 + uses: peter-evans/create-pull-request@c55203cfde3e5c11a452d352b4393e68b85b4533 # v6 with: token: ${{ secrets.ELEMENT_BOT_TOKEN }} branch: actions/jitsi-update diff --git a/CHANGELOG.md b/CHANGELOG.md index c1100a6cae..321acf18ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,59 @@ +Changes in [1.11.67](https://github.com/element-hq/element-web/releases/tag/v1.11.67) (2024-05-22) +================================================================================================== +## ✨ Features + +* Tooltip: Improve the accessibility of the composer and the rich text editor ([#12459](https://github.com/matrix-org/matrix-react-sdk/pull/12459)). Contributed by @florianduros. +* Allow explicit configuration of OIDC dynamic registration metadata ([#12514](https://github.com/matrix-org/matrix-react-sdk/pull/12514)). Contributed by @t3chguy. +* Tooltip: improve accessibility for messages ([#12487](https://github.com/matrix-org/matrix-react-sdk/pull/12487)). Contributed by @florianduros. +* Collapse UserSettings tabs to just icons on narrow screens ([#12505](https://github.com/matrix-org/matrix-react-sdk/pull/12505)). Contributed by @dbkr. +* Add room topic to right panel room info ([#12503](https://github.com/matrix-org/matrix-react-sdk/pull/12503)). Contributed by @t3chguy. +* OIDC: pass `id_token` via `id_token_hint` on Manage Account interaction ([#12499](https://github.com/matrix-org/matrix-react-sdk/pull/12499)). Contributed by @t3chguy. +* Tooltip: improve accessibility in room ([#12493](https://github.com/matrix-org/matrix-react-sdk/pull/12493)). Contributed by @florianduros. +* Tooltip: improve accessibility for call and voice messages ([#12489](https://github.com/matrix-org/matrix-react-sdk/pull/12489)). Contributed by @florianduros. +* Move the active tab in user settings to the dialog title ([#12481](https://github.com/matrix-org/matrix-react-sdk/pull/12481)). Contributed by @dbkr. +* Tooltip: improve accessibility of spaces ([#12497](https://github.com/matrix-org/matrix-react-sdk/pull/12497)). Contributed by @florianduros. +* Tooltip: improve accessibility of the right panel ([#12490](https://github.com/matrix-org/matrix-react-sdk/pull/12490)). Contributed by @florianduros. +* MSC3575 (Sliding Sync) add well-known proxy support ([#12307](https://github.com/matrix-org/matrix-react-sdk/pull/12307)). Contributed by @EdGeraghty. + +## 🐛 Bug Fixes + +* Reuse single PlaybackWorker between Playback instances ([#12520](https://github.com/matrix-org/matrix-react-sdk/pull/12520)). Contributed by @t3chguy. +* Fix well-known lookup for sliding sync labs check ([#12519](https://github.com/matrix-org/matrix-react-sdk/pull/12519)). Contributed by @t3chguy. +* Fix `element-desktop-ssoid being` included in OIDC Authorization call ([#12495](https://github.com/matrix-org/matrix-react-sdk/pull/12495)). Contributed by @t3chguy. +* Fix beta notifications reconciliation for intentional mentions push rules ([#12510](https://github.com/matrix-org/matrix-react-sdk/pull/12510)). Contributed by @t3chguy. +* fix avatar stretched on 1:1 call ([#12494](https://github.com/matrix-org/matrix-react-sdk/pull/12494)). Contributed by @I-lander. +* Check native sliding sync support against an unstable feature flag ([#12498](https://github.com/matrix-org/matrix-react-sdk/pull/12498)). Contributed by @turt2live. +* Use OPTIONS for sliding sync detection poke ([#12492](https://github.com/matrix-org/matrix-react-sdk/pull/12492)). Contributed by @turt2live. +* TAC: hide tooltip when the release announcement is displayed ([#12472](https://github.com/matrix-org/matrix-react-sdk/pull/12472)). Contributed by @florianduros. + + + +Changes in [1.11.66](https://github.com/element-hq/element-web/releases/tag/v1.11.66) (2024-05-07) +================================================================================================== +## ✨ Features + +* Use a different error message for UTDs when you weren't in the room. ([#12453](https://github.com/matrix-org/matrix-react-sdk/pull/12453)). Contributed by @uhoreg. +* Take the Threads Activity Centre out of labs ([#12439](https://github.com/matrix-org/matrix-react-sdk/pull/12439)). Contributed by @dbkr. +* Expected UTDs: use a different message for UTDs sent before login ([#12391](https://github.com/matrix-org/matrix-react-sdk/pull/12391)). Contributed by @richvdh. +* Add `Tooltip` to `AccessibleButton` ([#12443](https://github.com/matrix-org/matrix-react-sdk/pull/12443)). Contributed by @florianduros. +* Add analytics to activity toggles ([#12418](https://github.com/matrix-org/matrix-react-sdk/pull/12418)). Contributed by @dbkr. +* Decrypt events in reverse order without copying the array ([#12445](https://github.com/matrix-org/matrix-react-sdk/pull/12445)). Contributed by @Johennes. +* Use new compound tooltip ([#12416](https://github.com/matrix-org/matrix-react-sdk/pull/12416)). Contributed by @florianduros. +* Expected UTDs: report a different Posthog code ([#12389](https://github.com/matrix-org/matrix-react-sdk/pull/12389)). Contributed by @richvdh. + +## 🐛 Bug Fixes + +* TAC: Fix accessibility issue when the Release announcement is displayed ([#12484](https://github.com/matrix-org/matrix-react-sdk/pull/12484)). Contributed by @RiotRobot. +* TAC: Close Release Announcement when TAC button is clicked ([#12485](https://github.com/matrix-org/matrix-react-sdk/pull/12485)). Contributed by @florianduros. +* MenuItem: fix caption usage ([#12455](https://github.com/matrix-org/matrix-react-sdk/pull/12455)). Contributed by @florianduros. +* Show the local echo in previews ([#12451](https://github.com/matrix-org/matrix-react-sdk/pull/12451)). Contributed by @langleyd. +* Fixed the drag and drop of X #27186 ([#12450](https://github.com/matrix-org/matrix-react-sdk/pull/12450)). Contributed by @asimdelvi. +* Move the TAC to above the button ([#12438](https://github.com/matrix-org/matrix-react-sdk/pull/12438)). Contributed by @dbkr. +* Use the same logic in previews as the timeline to hide events that should be hidden ([#12434](https://github.com/matrix-org/matrix-react-sdk/pull/12434)). Contributed by @langleyd. +* Fix selector so maths support doesn't mangle divs ([#12433](https://github.com/matrix-org/matrix-react-sdk/pull/12433)). Contributed by @uhoreg. + + + Changes in [1.11.65](https://github.com/element-hq/element-web/releases/tag/v1.11.65) (2024-04-23) ================================================================================================== ## ✨ Features diff --git a/code_style.md b/code_style.md index 0462f3a4a9..e6ad053111 100644 --- a/code_style.md +++ b/code_style.md @@ -225,6 +225,12 @@ Unless otherwise specified, the following applies to all code: } ``` +37. Avoid functions whose fundamental behaviour varies with different parameter types. + Multiple return types are fine, but if the function's behaviour is going to change significantly, + have two separate functions. For example, `SDKConfig.get()` with a string param which returns the + type according to the param given is ok, but `SDKConfig.get()` with no args returning the whole + config object would not be: this should just be a separate function. + ## React Inheriting all the rules of TypeScript, the following additionally apply: diff --git a/docs/config.md b/docs/config.md index 95972a4fa8..1f6bbabd20 100644 --- a/docs/config.md +++ b/docs/config.md @@ -261,6 +261,47 @@ When Element is deployed alongside a homeserver with SSO-only login, some option ``` It is most common to use the `immediate` flag instead of `on_welcome_page`. +## Native OIDC + +Native OIDC support is currently in labs and is subject to change. + +Static OIDC Client IDs are preferred and can be specified under `oidc_static_clients` as a mapping from `issuer` to configuration object containing `client_id`. +Issuer must have a trailing forward slash. As an example: + +```json +{ + "oidc_static_clients": { + "https://auth.example.com/": { + "client_id": "example-client-id" + } + } +} +``` + +If a matching static client is not found, the app will attempt to dynamically register a client using metadata specified under `oidc_metadata`. +The app has sane defaults for the metadata properties below but on stricter policy identity providers they may not pass muster, e.g. `contacts` may be required. +The following subproperties are available: + +1. `client_uri`: This is the base URI for the OIDC client registration, typically `logo_uri`, `tos_uri`, and `policy_uri` must be either on the same domain or a subdomain of this URI. +2. `logo_uri`: Optional URI for the client logo. +3. `tos_uri`: Optional URI for the client's terms of service. +4. `policy_uri`: Optional URI for the client's privacy policy. +5. `contacts`: Optional list of contact emails for the client. + +As an example: + +```json +{ + "oidc_metadata": { + "client_uri": "https://example.com", + "logo_uri": "https://example.com/logo.png", + "tos_uri": "https://example.com/tos", + "policy_uri": "https://example.com/policy", + "contacts": ["support@example.com"] + } +} +``` + ## VoIP / Jitsi calls Currently, Element uses Jitsi to offer conference calls in rooms, with an experimental Element Call implementation in the works. diff --git a/element.io/app/config.json b/element.io/app/config.json index 2ea4ce7c61..2214dbc7ea 100644 --- a/element.io/app/config.json +++ b/element.io/app/config.json @@ -45,6 +45,6 @@ "privacy_policy_url": "https://element.io/cookie-policy", "map_style_url": "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx", "setting_defaults": { - "RustCrypto.staged_rollout_percent": 10 + "RustCrypto.staged_rollout_percent": 30 } } diff --git a/element.io/develop/config.json b/element.io/develop/config.json index 9a4227c8af..aed546559d 100644 --- a/element.io/develop/config.json +++ b/element.io/develop/config.json @@ -48,6 +48,7 @@ }, "privacy_policy_url": "https://element.io/cookie-policy", "features": { + "threadsActivityCentre": true, "feature_video_rooms": true, "feature_new_room_decoration_ui": true, "feature_element_call_video_rooms": true diff --git a/linked-dependencies/matrix-react-sdk/.eslintrc.js b/linked-dependencies/matrix-react-sdk/.eslintrc.js index 14afc41c07..caeeca403d 100644 --- a/linked-dependencies/matrix-react-sdk/.eslintrc.js +++ b/linked-dependencies/matrix-react-sdk/.eslintrc.js @@ -108,7 +108,6 @@ module.exports = { "!matrix-js-sdk/src/extensible_events_v1/PollEndEvent", "!matrix-js-sdk/src/extensible_events_v1/InvalidEventError", "!matrix-js-sdk/src/crypto", - "!matrix-js-sdk/src/crypto/algorithms", "!matrix-js-sdk/src/crypto/aes", "!matrix-js-sdk/src/crypto/olmlib", "!matrix-js-sdk/src/crypto/crypto", diff --git a/linked-dependencies/matrix-react-sdk/.github/workflows/element-web.yaml b/linked-dependencies/matrix-react-sdk/.github/workflows/element-web.yaml deleted file mode 100644 index 8ac5e2da94..0000000000 --- a/linked-dependencies/matrix-react-sdk/.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/linked-dependencies/matrix-react-sdk/.github/workflows/end-to-end-tests-netlify.yaml b/linked-dependencies/matrix-react-sdk/.github/workflows/end-to-end-tests-netlify.yaml new file mode 100644 index 0000000000..4667bfb02b --- /dev/null +++ b/linked-dependencies/matrix-react-sdk/.github/workflows/end-to-end-tests-netlify.yaml @@ -0,0 +1,68 @@ +# 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: + if: github.event.workflow_run.conclusion != 'cancelled' + 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,./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/linked-dependencies/matrix-react-sdk/.github/workflows/end-to-end-tests.yaml b/linked-dependencies/matrix-react-sdk/.github/workflows/end-to-end-tests.yaml index 3228fe91b3..0e224c04db 100644 --- a/linked-dependencies/matrix-react-sdk/.github/workflows/end-to-end-tests.yaml +++ b/linked-dependencies/matrix-react-sdk/.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 - - # support calls from other workflows for downstream testing + 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: + 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 }} + + - 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 - tests: + - 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-" + - if: needs.playwright.result != 'skipped' && needs.playwright.result != 'success' + run: exit 1 diff --git a/linked-dependencies/matrix-react-sdk/.github/workflows/netlify.yaml b/linked-dependencies/matrix-react-sdk/.github/workflows/netlify.yaml index 21c6f22df0..911bfccbf4 100644 --- a/linked-dependencies/matrix-react-sdk/.github/workflows/netlify.yaml +++ b/linked-dependencies/matrix-react-sdk/.github/workflows/netlify.yaml @@ -3,12 +3,12 @@ name: Upload Preview Build to Netlify on: workflow_run: - workflows: ["Element Web - Build"] + workflows: ["End to End Tests"] types: - completed jobs: deploy: - if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' + if: github.event.workflow_run.conclusion != 'cancelled' && github.event.workflow_run.event == 'pull_request' runs-on: ubuntu-latest environment: Netlify steps: @@ -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/linked-dependencies/matrix-react-sdk/CHANGELOG.md b/linked-dependencies/matrix-react-sdk/CHANGELOG.md index 440239db81..ea324f0e03 100644 --- a/linked-dependencies/matrix-react-sdk/CHANGELOG.md +++ b/linked-dependencies/matrix-react-sdk/CHANGELOG.md @@ -1,3 +1,28 @@ +Changes in [3.99.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.99.0) (2024-05-07) +===================================================================================================== +## ✨ Features + +* Use a different error message for UTDs when you weren't in the room. ([#12453](https://github.com/matrix-org/matrix-react-sdk/pull/12453)). Contributed by @uhoreg. +* Take the Threads Activity Centre out of labs ([#12439](https://github.com/matrix-org/matrix-react-sdk/pull/12439)). Contributed by @dbkr. +* Expected UTDs: use a different message for UTDs sent before login ([#12391](https://github.com/matrix-org/matrix-react-sdk/pull/12391)). Contributed by @richvdh. +* Add `Tooltip` to `AccessibleButton` ([#12443](https://github.com/matrix-org/matrix-react-sdk/pull/12443)). Contributed by @florianduros. +* Add analytics to activity toggles ([#12418](https://github.com/matrix-org/matrix-react-sdk/pull/12418)). Contributed by @dbkr. +* Decrypt events in reverse order without copying the array ([#12445](https://github.com/matrix-org/matrix-react-sdk/pull/12445)). Contributed by @Johennes. +* Use new compound tooltip ([#12416](https://github.com/matrix-org/matrix-react-sdk/pull/12416)). Contributed by @florianduros. +* Expected UTDs: report a different Posthog code ([#12389](https://github.com/matrix-org/matrix-react-sdk/pull/12389)). Contributed by @richvdh. + +## 🐛 Bug Fixes + +* TAC: Fix accessibility issue when the Release announcement is displayed ([#12484](https://github.com/matrix-org/matrix-react-sdk/pull/12484)). Contributed by @RiotRobot. +* TAC: Close Release Announcement when TAC button is clicked ([#12485](https://github.com/matrix-org/matrix-react-sdk/pull/12485)). Contributed by @florianduros. +* MenuItem: fix caption usage ([#12455](https://github.com/matrix-org/matrix-react-sdk/pull/12455)). Contributed by @florianduros. +* Show the local echo in previews ([#12451](https://github.com/matrix-org/matrix-react-sdk/pull/12451)). Contributed by @langleyd. +* Fixed the drag and drop of X #27186 ([#12450](https://github.com/matrix-org/matrix-react-sdk/pull/12450)). Contributed by @asimdelvi. +* Move the TAC to above the button ([#12438](https://github.com/matrix-org/matrix-react-sdk/pull/12438)). Contributed by @dbkr. +* Use the same logic in previews as the timeline to hide events that should be hidden ([#12434](https://github.com/matrix-org/matrix-react-sdk/pull/12434)). Contributed by @langleyd. +* Fix selector so maths support doesn't mangle divs ([#12433](https://github.com/matrix-org/matrix-react-sdk/pull/12433)). Contributed by @uhoreg. + + Changes in [3.98.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.98.0) (2024-04-23) ===================================================================================================== ## ✨ Features diff --git a/linked-dependencies/matrix-react-sdk/package.json b/linked-dependencies/matrix-react-sdk/package.json index eae134822e..01e1ff56c5 100644 --- a/linked-dependencies/matrix-react-sdk/package.json +++ b/linked-dependencies/matrix-react-sdk/package.json @@ -1,7 +1,7 @@ { "name": "matrix-react-sdk", "version": "0.0.0", - "version-matrix": "3.98.0", + "version-matrix": "3.100.0-rc.0", "description": "SDK for matrix.org using React for Tchap", "author": "DINUM", "repository": { @@ -61,14 +61,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", @@ -77,7 +77,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": "^3.3.1", + "@vector-im/compound-web": "^4.2.0", "@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4", "@zxcvbn-ts/language-en": "^3.0.2", @@ -109,7 +109,7 @@ "maplibre-gl": "^2.0.0", "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "0.0.1", - "matrix-js-sdk": "32.1.0", + "matrix-js-sdk": "32.3.0-rc.0", "matrix-widget-api": "^1.5.0", "memoize-one": "^6.0.0", "minimist": "^1.2.5", @@ -117,7 +117,7 @@ "opus-recorder": "^8.0.3", "pako": "^2.0.3", "png-chunks-extract": "^1.0.0", - "posthog-js": "1.116.6", + "posthog-js": "1.130.1", "proposal-temporal": "^0.9.0", "qrcode": "1.5.3", "re-resizable": "^6.9.0", @@ -177,9 +177,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", @@ -196,12 +196,12 @@ "eslint-config-prettier": "^9.0.0", "eslint-plugin-deprecate": "0.8.4", "eslint-plugin-import": "^2.25.4", - "eslint-plugin-jest": "^27.2.1", + "eslint-plugin-jest": "^28.0.0", "eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-matrix-org": "1.2.1", "eslint-plugin-react": "^7.28.0", "eslint-plugin-react-hooks": "^4.3.0", - "eslint-plugin-unicorn": "^51.0.0", + "eslint-plugin-unicorn": "^52.0.0", "express": "^4.18.2", "fake-indexeddb": "^5.0.2", "fetch-mock-jest": "^1.5.1", @@ -224,7 +224,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/linked-dependencies/matrix-react-sdk/playwright.config.ts b/linked-dependencies/matrix-react-sdk/playwright.config.ts index 40065b92c4..96a8dd95ec 100644 --- a/linked-dependencies/matrix-react-sdk/playwright.config.ts +++ b/linked-dependencies/matrix-react-sdk/playwright.config.ts @@ -41,7 +41,7 @@ export default defineConfig({ outputDir: "playwright/test-results", workers: 1, retries: process.env.CI ? 2 : 0, - reporter: process.env.CI ? "blob" : [["html", { outputFolder: "playwright/html-report" }]], + reporter: process.env.CI ? [["blob"], ["github"]] : [["html", { outputFolder: "playwright/html-report" }]], projects: [ { name: "Legacy Crypto", diff --git a/linked-dependencies/matrix-react-sdk/playwright/Dockerfile b/linked-dependencies/matrix-react-sdk/playwright/Dockerfile index f13d7a2c68..46d617ccc2 100644 --- a/linked-dependencies/matrix-react-sdk/playwright/Dockerfile +++ b/linked-dependencies/matrix-react-sdk/playwright/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/playwright:v1.42.1-jammy +FROM mcr.microsoft.com/playwright:v1.43.1-jammy WORKDIR /work/matrix-react-sdk VOLUME ["/work/element-web/node_modules"] diff --git a/linked-dependencies/matrix-react-sdk/playwright/e2e/crypto/crypto.spec.ts b/linked-dependencies/matrix-react-sdk/playwright/e2e/crypto/crypto.spec.ts index 957be58711..326aeaff8e 100644 --- a/linked-dependencies/matrix-react-sdk/playwright/e2e/crypto/crypto.spec.ts +++ b/linked-dependencies/matrix-react-sdk/playwright/e2e/crypto/crypto.spec.ts @@ -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. @@ -15,19 +15,24 @@ limitations under the License. */ import type { Page } from "@playwright/test"; -import { test, expect } from "../../element-web-test"; +import type { EmittedEvents, Preset } from "matrix-js-sdk/src/matrix"; +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"; import { ElementAppPage } from "../../pages/ElementAppPage"; import { Client } from "../../pages/client"; +import { isDendrite } from "../../plugins/homeserver/dendrite"; const openRoomInfo = async (page: Page) => { await page.getByRole("button", { name: "Room info" }).click(); @@ -453,8 +458,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 +537,281 @@ 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(); + }); + + test.describe("non-joined historical messages", () => { + test.skip(isDendrite, "does not yet support membership on events"); + + test("should display undecryptable non-joined historical messages with a different message", async ({ + homeserver, + page, + app, + credentials: aliceCredentials, + user: alice, + cryptoBackend, + bot: bob, + }) => { + test.skip(cryptoBackend === "legacy", "Not implemented for legacy crypto"); + + // Bob creates an encrypted room and sends a message to it. He then invites Alice + const roomId = await bob.evaluate( + async (client, { alice }) => { + const encryptionStatePromise = new Promise((resolve) => { + client.on("RoomState.events" as EmittedEvents, (event, _state, _lastStateEvent) => { + if (event.getType() === "m.room.encryption") { + resolve(); + } + }); + }); + + const { room_id: roomId } = await client.createRoom({ + initial_state: [ + { + type: "m.room.encryption", + content: { + algorithm: "m.megolm.v1.aes-sha2", + }, + }, + ], + name: "Test room", + preset: "private_chat" as Preset, + }); + + // wait for m.room.encryption event, so that when we send a + // message, it will be encrypted + await encryptionStatePromise; + + await client.sendTextMessage(roomId, "This should be undecryptable"); + + await client.invite(roomId, alice.userId); + + return roomId; + }, + { alice }, + ); + + // Alice accepts the invite + await expect( + page.getByRole("group", { name: "Invites" }).locator(".mx_RoomSublist_tiles").getByRole("treeitem"), + ).toHaveCount(1); + await page.getByRole("treeitem", { name: "Test room" }).click(); + await page.locator(".mx_RoomView").getByRole("button", { name: "Accept" }).click(); + + // Bob sends an encrypted event and an undecryptable event + await bob.evaluate( + async (client, { roomId }) => { + await client.sendTextMessage(roomId, "This should be decryptable"); + await client.sendEvent( + roomId, + "m.room.encrypted" as any, + { + algorithm: "m.megolm.v1.aes-sha2", + ciphertext: "this+message+will+be+undecryptable", + device_id: client.getDeviceId()!, + sender_key: (await client.getCrypto()!.getOwnDeviceKeys()).ed25519, + session_id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + } as any, + ); + }, + { roomId }, + ); + + // We wait for the event tiles that we expect from the messages that + // Bob sent, in sequence. + await expect( + page.locator(`.mx_EventTile`).getByText("You don't have access to this message"), + ).toBeVisible(); + await expect(page.locator(`.mx_EventTile`).getByText("This should be decryptable")).toBeVisible(); + await expect(page.locator(`.mx_EventTile`).getByText("Unable to decrypt message")).toBeVisible(); + + // And then we ensure that they are where we expect them to be + // Alice should see these event tiles: + // - first message sent by Bob (undecryptable) + // - Bob invited Alice + // - Alice joined the room + // - second message sent by Bob (decryptable) + // - third message sent by Bob (undecryptable) + const tiles = await page.locator(".mx_EventTile").all(); + expect(tiles.length).toBeGreaterThanOrEqual(5); + + // The first message from Bob was sent before Alice was in the room, so should + // be different from the standard UTD message + await expect(tiles[tiles.length - 5]).toContainText("You don't have access to this message"); + await expect(tiles[tiles.length - 5].locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible(); + + // The second message from Bob should be decryptable + await expect(tiles[tiles.length - 2]).toContainText("This should be decryptable"); + // this tile won't have an e2e icon since we got the key from the sender + + // The third message from Bob is undecryptable, but was sent while Alice was + // in the room and is expected to be decryptable, so this should have the + // standard UTD message + await expect(tiles[tiles.length - 1]).toContainText("Unable to decrypt message"); + await expect(tiles[tiles.length - 1].locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible(); + }); + + test("should be able to jump to a message sent before our last join event", async ({ + homeserver, + page, + app, + credentials: aliceCredentials, + user: alice, + cryptoBackend, + bot: bob, + }) => { + // The old pre-join UTD hiding code would hide events sent + // before our latest join event, even if the event that we're + // jumping to was decryptable. We test that this no longer happens. + + test.skip(cryptoBackend === "legacy", "Not implemented for legacy crypto"); + + // Bob: + // - creates an encrypted room, + // - invites Alice, + // - sends a message to it, + // - kicks Alice, + // - sends a bunch more events + // - invites Alice again + // In this way, there will be an event that Alice can decrypt, + // followed by a bunch of undecryptable events which Alice shouldn't + // expect to be able to decrypt. The old code would have hidden all + // the events, even the decryptable event (which it wouldn't have + // even tried to fetch, if it was far enough back). + const { roomId, eventId } = await bob.evaluate( + async (client, { alice }) => { + const { room_id: roomId } = await client.createRoom({ + initial_state: [ + { + type: "m.room.encryption", + content: { + algorithm: "m.megolm.v1.aes-sha2", + }, + }, + ], + name: "Test room", + preset: "private_chat" as Preset, + }); + + // invite Alice + const inviteAlicePromise = new Promise((resolve) => { + client.on("RoomMember.membership" as EmittedEvents, (_event, member, _oldMembership?) => { + if (member.userId === alice.userId && member.membership === "invite") { + resolve(); + } + }); + }); + await client.invite(roomId, alice.userId); + // wait for the invite to come back so that we encrypt to Alice + await inviteAlicePromise; + + // send a message that Alice should be able to decrypt + const { event_id: eventId } = await client.sendTextMessage( + roomId, + "This should be decryptable", + ); + + // kick Alice + const kickAlicePromise = new Promise((resolve) => { + client.on("RoomMember.membership" as EmittedEvents, (_event, member, _oldMembership?) => { + if (member.userId === alice.userId && member.membership === "leave") { + resolve(); + } + }); + }); + await client.kick(roomId, alice.userId); + await kickAlicePromise; + + // send a bunch of messages that Alice won't be able to decrypt + for (let i = 0; i < 20; i++) { + await client.sendTextMessage(roomId, `${i}`); + } + + // invite Alice again + await client.invite(roomId, alice.userId); + + return { roomId, eventId }; + }, + { alice }, + ); + + // Alice accepts the invite + await expect( + page.getByRole("group", { name: "Invites" }).locator(".mx_RoomSublist_tiles").getByRole("treeitem"), + ).toHaveCount(1); + await page.getByRole("treeitem", { name: "Test room" }).click(); + await page.locator(".mx_RoomView").getByRole("button", { name: "Accept" }).click(); + + // wait until we're joined and see the timeline + await expect(page.locator(`.mx_EventTile`).getByText("Alice joined the room")).toBeVisible(); + + // we should be able to jump to the decryptable message that Bob sent + await page.goto(`#/room/${roomId}/${eventId}`); + + await expect(page.locator(`.mx_EventTile`).getByText("This should be decryptable")).toBeVisible(); + }); + }); + }); }); diff --git a/linked-dependencies/matrix-react-sdk/playwright/e2e/crypto/utils.ts b/linked-dependencies/matrix-react-sdk/playwright/e2e/crypto/utils.ts index d43e4c7f94..5b0bf29b97 100644 --- a/linked-dependencies/matrix-react-sdk/playwright/e2e/crypto/utils.ts +++ b/linked-dependencies/matrix-react-sdk/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/linked-dependencies/matrix-react-sdk/playwright/e2e/login/overwrite_login.spec.ts b/linked-dependencies/matrix-react-sdk/playwright/e2e/login/overwrite_login.spec.ts index b047cfa3dd..7ef8769c9d 100644 --- a/linked-dependencies/matrix-react-sdk/playwright/e2e/login/overwrite_login.spec.ts +++ b/linked-dependencies/matrix-react-sdk/playwright/e2e/login/overwrite_login.spec.ts @@ -18,7 +18,9 @@ import { test, expect } from "../../element-web-test"; import { logIntoElement } from "../crypto/utils"; test.describe("Overwrite login action", () => { - test("Try replace existing login with new one", async ({ page, app, credentials, homeserver }) => { + // This seems terminally flakey: https://github.com/element-hq/element-web/issues/27363 + // I tried verious things to try & deflake it, to no avail: https://github.com/matrix-org/matrix-react-sdk/pull/12506 + test.skip("Try replace existing login with new one", async ({ page, app, credentials, homeserver }) => { await logIntoElement(page, homeserver, credentials); const userMenu = await app.openUserMenu(); @@ -47,7 +49,6 @@ test.describe("Overwrite login action", () => { }, clientCredentials); // It should be now another user!! - const newUserMenu = await app.openUserMenu(); - await expect(newUserMenu.getByText(bobRegister.userId)).toBeVisible(); + await expect(page.getByText("Welcome BOB")).toBeVisible(); }); }); diff --git a/linked-dependencies/matrix-react-sdk/playwright/e2e/one-to-one-chat/one-to-one-chat.spec.ts b/linked-dependencies/matrix-react-sdk/playwright/e2e/one-to-one-chat/one-to-one-chat.spec.ts index 39b30fbab5..287ac77cd4 100644 --- a/linked-dependencies/matrix-react-sdk/playwright/e2e/one-to-one-chat/one-to-one-chat.spec.ts +++ b/linked-dependencies/matrix-react-sdk/playwright/e2e/one-to-one-chat/one-to-one-chat.spec.ts @@ -45,6 +45,7 @@ test.describe("1:1 chat room", () => { await expect( page.getByRole("group", { name: "Rooms" }).locator(".mx_RoomTile").getByText(user2.displayName), ).not.toBeVisible(); + await page.waitForTimeout(500); // avoid race condition with routing // open new 1:1 chat room await page.goto(`/#/user/${user2.userId}?action=chat`); diff --git a/linked-dependencies/matrix-react-sdk/playwright/e2e/read-receipts/editing-messages.spec.ts b/linked-dependencies/matrix-react-sdk/playwright/e2e/read-receipts/editing-messages.spec.ts index 49db3bdfbe..5005ad62bf 100644 --- a/linked-dependencies/matrix-react-sdk/playwright/e2e/read-receipts/editing-messages.spec.ts +++ b/linked-dependencies/matrix-react-sdk/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/linked-dependencies/matrix-react-sdk/playwright/e2e/read-receipts/high-level.spec.ts b/linked-dependencies/matrix-react-sdk/playwright/e2e/read-receipts/high-level.spec.ts index 897e752ac4..e237afd64a 100644 --- a/linked-dependencies/matrix-react-sdk/playwright/e2e/read-receipts/high-level.spec.ts +++ b/linked-dependencies/matrix-react-sdk/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/linked-dependencies/matrix-react-sdk/playwright/e2e/read-receipts/index.ts b/linked-dependencies/matrix-react-sdk/playwright/e2e/read-receipts/index.ts index 6b9a8381d2..4dd0450fb9 100644 --- a/linked-dependencies/matrix-react-sdk/playwright/e2e/read-receipts/index.ts +++ b/linked-dependencies/matrix-react-sdk/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/linked-dependencies/matrix-react-sdk/playwright/e2e/read-receipts/new-messages.spec.ts b/linked-dependencies/matrix-react-sdk/playwright/e2e/read-receipts/new-messages.spec.ts index 14434709ce..97308a4bb2 100644 --- a/linked-dependencies/matrix-react-sdk/playwright/e2e/read-receipts/new-messages.spec.ts +++ b/linked-dependencies/matrix-react-sdk/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); + await util.assertRead(room2); + await util.goTo(room2); + await util.assertUnreadThread("Msg1"); - // Until we open the thread + // Opening the thread now marks it as read await util.openThread("Msg1"); - await util.assertRead(room2); + 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/linked-dependencies/matrix-react-sdk/playwright/e2e/read-receipts/reactions.spec.ts b/linked-dependencies/matrix-react-sdk/playwright/e2e/read-receipts/reactions.spec.ts index 1063c7d19e..69208e5fc9 100644 --- a/linked-dependencies/matrix-react-sdk/playwright/e2e/read-receipts/reactions.spec.ts +++ b/linked-dependencies/matrix-react-sdk/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/linked-dependencies/matrix-react-sdk/playwright/e2e/read-receipts/read-receipts.spec.ts b/linked-dependencies/matrix-react-sdk/playwright/e2e/read-receipts/read-receipts.spec.ts index 36f74e2c64..dac679f6a0 100644 --- a/linked-dependencies/matrix-react-sdk/playwright/e2e/read-receipts/read-receipts.spec.ts +++ b/linked-dependencies/matrix-react-sdk/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/linked-dependencies/matrix-react-sdk/playwright/e2e/read-receipts/redactions.spec.ts b/linked-dependencies/matrix-react-sdk/playwright/e2e/read-receipts/redactions.spec.ts index 1b5751acbc..f7affbed21 100644 --- a/linked-dependencies/matrix-react-sdk/playwright/e2e/read-receipts/redactions.spec.ts +++ b/linked-dependencies/matrix-react-sdk/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.assertUnread(room2, 1); await util.receiveMessages(room2, [msg.redactionOf("Msg2")]); - await util.assertUnread(room2, 2); - await util.goTo(room2); 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.assertUnread(room2, 1); 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.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/linked-dependencies/matrix-react-sdk/playwright/e2e/settings/appearance-user-settings-tab.spec.ts b/linked-dependencies/matrix-react-sdk/playwright/e2e/settings/appearance-user-settings-tab.spec.ts index df091f45a8..7e16d73955 100644 --- a/linked-dependencies/matrix-react-sdk/playwright/e2e/settings/appearance-user-settings-tab.spec.ts +++ b/linked-dependencies/matrix-react-sdk/playwright/e2e/settings/appearance-user-settings-tab.spec.ts @@ -25,8 +25,6 @@ test.describe("Appearance user settings tab", () => { test("should be rendered properly", async ({ page, user, app }) => { const tab = await app.settings.openUserSettings("Appearance"); - await expect(tab.getByRole("heading", { name: "Customise your appearance" })).toBeVisible(); - // Click "Show advanced" link button await tab.getByRole("button", { name: "Show advanced" }).click(); diff --git a/linked-dependencies/matrix-react-sdk/playwright/e2e/settings/general-user-settings-tab.spec.ts b/linked-dependencies/matrix-react-sdk/playwright/e2e/settings/general-user-settings-tab.spec.ts index d6d138db5b..625f1d6bd5 100644 --- a/linked-dependencies/matrix-react-sdk/playwright/e2e/settings/general-user-settings-tab.spec.ts +++ b/linked-dependencies/matrix-react-sdk/playwright/e2e/settings/general-user-settings-tab.spec.ts @@ -125,6 +125,11 @@ test.describe("General user settings tab", () => { ); }); + test("should respond to small screen sizes", async ({ page, uut }) => { + await page.setViewportSize({ width: 700, height: 600 }); + await expect(uut).toMatchScreenshot("general-smallscreen.png"); + }); + test("should support adding and removing a profile picture", async ({ uut }) => { const profileSettings = uut.locator(".mx_ProfileSettings"); // Upload a picture diff --git a/linked-dependencies/matrix-react-sdk/playwright/e2e/spaces/threads-activity-centre/index.ts b/linked-dependencies/matrix-react-sdk/playwright/e2e/spaces/threads-activity-centre/index.ts index 7ad477541a..8bafe2e804 100644 --- a/linked-dependencies/matrix-react-sdk/playwright/e2e/spaces/threads-activity-centre/index.ts +++ b/linked-dependencies/matrix-react-sdk/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/linked-dependencies/matrix-react-sdk/playwright/e2e/threads/threads.spec.ts b/linked-dependencies/matrix-react-sdk/playwright/e2e/threads/threads.spec.ts index 5e32516646..9b5ea46511 100644 --- a/linked-dependencies/matrix-react-sdk/playwright/e2e/threads/threads.spec.ts +++ b/linked-dependencies/matrix-react-sdk/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/linked-dependencies/matrix-react-sdk/playwright/e2e/timeline/timeline.spec.ts b/linked-dependencies/matrix-react-sdk/playwright/e2e/timeline/timeline.spec.ts index 2ca507fc9e..60aa1e2a27 100644 --- a/linked-dependencies/matrix-react-sdk/playwright/e2e/timeline/timeline.spec.ts +++ b/linked-dependencies/matrix-react-sdk/playwright/e2e/timeline/timeline.spec.ts @@ -70,6 +70,22 @@ const sendEvent = async (client: Client, roomId: string, html = false): Promise< return client.sendEvent(roomId, null, "m.room.message" as EventType, content); }; +const sendImage = async ( + client: Client, + roomId: string, + pngBytes: Buffer, + additionalContent?: any, +): Promise => { + const upload = await client.uploadContent(pngBytes, { name: "image.png", type: "image/png" }); + return client.sendEvent(roomId, null, "m.room.message" as EventType, { + ...(additionalContent ?? {}), + + msgtype: "m.image" as MsgType, + body: "image.png", + url: upload.content_uri, + }); +}; + test.describe("Timeline", () => { test.use({ displayName: OLD_NAME, @@ -1136,5 +1152,91 @@ test.describe("Timeline", () => { screenshotOptions, ); }); + + async function testImageRendering(page: Page, app: ElementAppPage, room: { roomId: string }) { + await app.viewRoomById(room.roomId); + + // Reinstall the service workers to clear their implicit caches (global-level stuff) + await page.evaluate(async () => { + const registrations = await window.navigator.serviceWorker.getRegistrations(); + registrations.forEach((r) => r.update()); + }); + + await sendImage(app.client, room.roomId, NEW_AVATAR); + await expect(page.locator(".mx_MImageBody").first()).toBeVisible(); + + // Exclude timestamp and read marker from snapshot + const screenshotOptions = { + mask: [page.locator(".mx_MessageTimestamp")], + css: ` + .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker { + display: none !important; + } + `, + }; + + await expect(page.locator(".mx_ScrollPanel")).toMatchScreenshot( + "image-in-timeline-default-layout.png", + screenshotOptions, + ); + } + + test("should render images in the timeline", async ({ page, app, room, context }) => { + await testImageRendering(page, app, room); + }); + + // XXX: This test doesn't actually work because the service worker relies on IndexedDB, which Playwright forces + // to be a localstorage implementation, which service workers cannot access. + // See https://github.com/microsoft/playwright/issues/11164 + // See https://github.com/microsoft/playwright/issues/15684#issuecomment-2070862042 + // + // In practice, this means this test will *always* succeed because it ends up relying on fallback behaviour tested + // above (unless of course the above tests are also broken). + test.describe("MSC3916 - Authenticated Media", () => { + test("should render authenticated images in the timeline", async ({ page, app, room, context }) => { + // Note: we have to use `context` instead of `page` for routing, otherwise we'll miss Service Worker events. + // See https://playwright.dev/docs/service-workers-experimental#network-events-and-routing + + // Install our mocks and preventative measures + await context.route("**/_matrix/client/versions", async (route) => { + // Force enable MSC3916, which may require the service worker's internal cache to be cleared later. + const json = await (await route.fetch()).json(); + if (!json["unstable_features"]) json["unstable_features"] = {}; + json["unstable_features"]["org.matrix.msc3916"] = true; + await route.fulfill({ json }); + }); + await context.route("**/_matrix/media/*/download/**", async (route) => { + // should not be called. We don't use `abort` so that it's clearer in the logs what happened. + await route.fulfill({ + status: 500, + json: { errcode: "M_UNKNOWN", error: "Unexpected route called." }, + }); + }); + await context.route("**/_matrix/media/*/thumbnail/**", async (route) => { + // should not be called. We don't use `abort` so that it's clearer in the logs what happened. + await route.fulfill({ + status: 500, + json: { errcode: "M_UNKNOWN", error: "Unexpected route called." }, + }); + }); + await context.route("**/_matrix/client/unstable/org.matrix.msc3916/download/**", async (route) => { + expect(route.request().headers()["Authorization"]).toBeDefined(); + // we can't use route.continue() because no configured homeserver supports MSC3916 yet + await route.fulfill({ + body: NEW_AVATAR, + }); + }); + await context.route("**/_matrix/client/unstable/org.matrix.msc3916/thumbnail/**", async (route) => { + expect(route.request().headers()["Authorization"]).toBeDefined(); + // we can't use route.continue() because no configured homeserver supports MSC3916 yet + await route.fulfill({ + body: NEW_AVATAR, + }); + }); + + // We check the same screenshot because there should be no user-visible impact to using authentication. + await testImageRendering(page, app, room); + }); + }); }); }); diff --git a/linked-dependencies/matrix-react-sdk/playwright/e2e/user-onboarding/user-onboarding-new.spec.ts b/linked-dependencies/matrix-react-sdk/playwright/e2e/user-onboarding/user-onboarding-new.spec.ts index 46bca0b78b..09a140d441 100644 --- a/linked-dependencies/matrix-react-sdk/playwright/e2e/user-onboarding/user-onboarding-new.spec.ts +++ b/linked-dependencies/matrix-react-sdk/playwright/e2e/user-onboarding/user-onboarding-new.spec.ts @@ -45,7 +45,17 @@ test.describe("User Onboarding (new user)", () => { await expect( page.getByRole("dialog").getByRole("heading", { level: 1, name: "Download Element" }), ).toBeVisible(); - await expect(page.locator(".mx_Dialog")).toMatchScreenshot(); + await expect(page.locator(".mx_Dialog")).toMatchScreenshot( + "User-Onboarding-new-user-app-download-dialog-1.png", + { + // Set a constant bg behind the modal to ensure screenshot stability + css: ` + .mx_AppDownloadDialog_wrapper { + background: black; + } + `, + }, + ); }); test("using find friends action should increase progress", async ({ page, homeserver }) => { diff --git a/linked-dependencies/matrix-react-sdk/playwright/element-web-test.ts b/linked-dependencies/matrix-react-sdk/playwright/element-web-test.ts index e67cca6ab8..2317978898 100644 --- a/linked-dependencies/matrix-react-sdk/playwright/element-web-test.ts +++ b/linked-dependencies/matrix-react-sdk/playwright/element-web-test.ts @@ -33,6 +33,10 @@ import { Bot, CreateBotOpts } from "./pages/bot"; import { ProxyInstance, SlidingSyncProxy } from "./plugins/sliding-sync-proxy"; import { Webserver } from "./plugins/webserver"; +// Enable experimental service worker support +// See https://playwright.dev/docs/service-workers-experimental#how-to-enable +process.env["PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS"] = "1"; + const CONFIG_JSON: Partial = { // This is deliberately quite a minimal config.json, so that we can test that the default settings // actually work. diff --git a/linked-dependencies/matrix-react-sdk/playwright/pages/settings.ts b/linked-dependencies/matrix-react-sdk/playwright/pages/settings.ts index 916ce26e03..c0efb6770c 100644 --- a/linked-dependencies/matrix-react-sdk/playwright/pages/settings.ts +++ b/linked-dependencies/matrix-react-sdk/playwright/pages/settings.ts @@ -25,8 +25,9 @@ export class Settings { * Open the top left user menu, returning a Locator to the resulting context menu. */ public async openUserMenu(): Promise { - await this.page.getByRole("button", { name: "User menu" }).click(); const locator = this.page.locator(".mx_ContextualMenu"); + if (await locator.locator(".mx_UserMenu_contextMenu_header").isVisible()) return locator; + await this.page.getByRole("button", { name: "User menu" }).click(); await locator.waitFor(); return locator; } diff --git a/linked-dependencies/matrix-react-sdk/playwright/plugins/homeserver/synapse/templates/default/homeserver.yaml b/linked-dependencies/matrix-react-sdk/playwright/plugins/homeserver/synapse/templates/default/homeserver.yaml index c5bea307b4..bc3ecd7c9b 100644 --- a/linked-dependencies/matrix-react-sdk/playwright/plugins/homeserver/synapse/templates/default/homeserver.yaml +++ b/linked-dependencies/matrix-react-sdk/playwright/plugins/homeserver/synapse/templates/default/homeserver.yaml @@ -96,3 +96,9 @@ oidc_providers: background_updates: min_batch_size: 100000 sleep_duration_ms: 100000 + +experimental_features: + # Needed for e2e/crypto/crypto.spec.ts > Cryptography > decryption failure + # messages > non-joined historical messages. + # Can be removed after Synapse enables it by default + msc4115_membership_on_events: true diff --git a/linked-dependencies/matrix-react-sdk/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png b/linked-dependencies/matrix-react-sdk/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png index d3c8961391..98c1ff245d 100644 Binary files a/linked-dependencies/matrix-react-sdk/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png and b/linked-dependencies/matrix-react-sdk/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png differ diff --git a/linked-dependencies/matrix-react-sdk/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png b/linked-dependencies/matrix-react-sdk/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png index 9cc13698cf..c8b8dba45b 100644 Binary files a/linked-dependencies/matrix-react-sdk/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png and b/linked-dependencies/matrix-react-sdk/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png differ diff --git a/linked-dependencies/matrix-react-sdk/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png b/linked-dependencies/matrix-react-sdk/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png index 44d8129404..852cb85518 100644 Binary files a/linked-dependencies/matrix-react-sdk/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png and b/linked-dependencies/matrix-react-sdk/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png differ diff --git a/linked-dependencies/matrix-react-sdk/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png b/linked-dependencies/matrix-react-sdk/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png index 2fc33b1f0b..c85c583a19 100644 Binary files a/linked-dependencies/matrix-react-sdk/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png and b/linked-dependencies/matrix-react-sdk/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png differ diff --git a/linked-dependencies/matrix-react-sdk/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png b/linked-dependencies/matrix-react-sdk/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png index a4c053e7a7..b6990e727e 100644 Binary files a/linked-dependencies/matrix-react-sdk/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png and b/linked-dependencies/matrix-react-sdk/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png differ diff --git a/linked-dependencies/matrix-react-sdk/playwright/snapshots/right-panel/file-panel.spec.ts/file-tiles-list-linux.png b/linked-dependencies/matrix-react-sdk/playwright/snapshots/right-panel/file-panel.spec.ts/file-tiles-list-linux.png index ad49c25abc..b783826727 100644 Binary files a/linked-dependencies/matrix-react-sdk/playwright/snapshots/right-panel/file-panel.spec.ts/file-tiles-list-linux.png and b/linked-dependencies/matrix-react-sdk/playwright/snapshots/right-panel/file-panel.spec.ts/file-tiles-list-linux.png differ diff --git a/linked-dependencies/matrix-react-sdk/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png b/linked-dependencies/matrix-react-sdk/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png index e53470df87..f383a828e2 100644 Binary files a/linked-dependencies/matrix-react-sdk/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png and b/linked-dependencies/matrix-react-sdk/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png differ diff --git a/linked-dependencies/matrix-react-sdk/playwright/snapshots/room/room-header.spec.ts/room-header-highlighted-linux.png b/linked-dependencies/matrix-react-sdk/playwright/snapshots/room/room-header.spec.ts/room-header-highlighted-linux.png index 5fa24a887f..c792c4bcf0 100644 Binary files a/linked-dependencies/matrix-react-sdk/playwright/snapshots/room/room-header.spec.ts/room-header-highlighted-linux.png and b/linked-dependencies/matrix-react-sdk/playwright/snapshots/room/room-header.spec.ts/room-header-highlighted-linux.png differ diff --git a/linked-dependencies/matrix-react-sdk/playwright/snapshots/room/room-header.spec.ts/room-header-with-apps-button-not-highlighted-linux.png b/linked-dependencies/matrix-react-sdk/playwright/snapshots/room/room-header.spec.ts/room-header-with-apps-button-not-highlighted-linux.png index 84eb8fcccc..498853a973 100644 Binary files a/linked-dependencies/matrix-react-sdk/playwright/snapshots/room/room-header.spec.ts/room-header-with-apps-button-not-highlighted-linux.png and b/linked-dependencies/matrix-react-sdk/playwright/snapshots/room/room-header.spec.ts/room-header-with-apps-button-not-highlighted-linux.png differ diff --git a/linked-dependencies/matrix-react-sdk/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png b/linked-dependencies/matrix-react-sdk/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png index e5680339f4..b7fea97192 100644 Binary files a/linked-dependencies/matrix-react-sdk/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png and b/linked-dependencies/matrix-react-sdk/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png differ diff --git a/linked-dependencies/matrix-react-sdk/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/window-12px-linux.png b/linked-dependencies/matrix-react-sdk/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/window-12px-linux.png index 23b77fd751..3247abd7c1 100644 Binary files a/linked-dependencies/matrix-react-sdk/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/window-12px-linux.png and b/linked-dependencies/matrix-react-sdk/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/window-12px-linux.png differ diff --git a/linked-dependencies/matrix-react-sdk/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-linux.png b/linked-dependencies/matrix-react-sdk/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-linux.png index f0b18bc950..e11ef9c410 100644 Binary files a/linked-dependencies/matrix-react-sdk/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-linux.png and b/linked-dependencies/matrix-react-sdk/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-linux.png differ diff --git a/linked-dependencies/matrix-react-sdk/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-smallscreen-linux.png b/linked-dependencies/matrix-react-sdk/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-smallscreen-linux.png new file mode 100644 index 0000000000..75febc97d7 Binary files /dev/null and b/linked-dependencies/matrix-react-sdk/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-smallscreen-linux.png differ diff --git a/linked-dependencies/matrix-react-sdk/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png b/linked-dependencies/matrix-react-sdk/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png index 02ce908efa..e5d1ddef4f 100644 Binary files a/linked-dependencies/matrix-react-sdk/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png and b/linked-dependencies/matrix-react-sdk/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png differ diff --git a/linked-dependencies/matrix-react-sdk/playwright/snapshots/spaces/spaces.spec.ts/space-panel-collapsed-linux.png b/linked-dependencies/matrix-react-sdk/playwright/snapshots/spaces/spaces.spec.ts/space-panel-collapsed-linux.png index 32e664808e..66b8af0e5b 100644 Binary files a/linked-dependencies/matrix-react-sdk/playwright/snapshots/spaces/spaces.spec.ts/space-panel-collapsed-linux.png and b/linked-dependencies/matrix-react-sdk/playwright/snapshots/spaces/spaces.spec.ts/space-panel-collapsed-linux.png differ diff --git a/linked-dependencies/matrix-react-sdk/playwright/snapshots/spaces/spaces.spec.ts/space-panel-expanded-linux.png b/linked-dependencies/matrix-react-sdk/playwright/snapshots/spaces/spaces.spec.ts/space-panel-expanded-linux.png index a2edd3d88f..6f55f2fd00 100644 Binary files a/linked-dependencies/matrix-react-sdk/playwright/snapshots/spaces/spaces.spec.ts/space-panel-expanded-linux.png and b/linked-dependencies/matrix-react-sdk/playwright/snapshots/spaces/spaces.spec.ts/space-panel-expanded-linux.png differ diff --git a/linked-dependencies/matrix-react-sdk/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png b/linked-dependencies/matrix-react-sdk/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png index 7ef5b40543..9fc79671a1 100644 Binary files a/linked-dependencies/matrix-react-sdk/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png and b/linked-dependencies/matrix-react-sdk/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png differ diff --git a/linked-dependencies/matrix-react-sdk/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-layout-linux.png b/linked-dependencies/matrix-react-sdk/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-layout-linux.png index 45c43f06fe..98ec9e0cf6 100644 Binary files a/linked-dependencies/matrix-react-sdk/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-layout-linux.png and b/linked-dependencies/matrix-react-sdk/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-layout-linux.png differ diff --git a/linked-dependencies/matrix-react-sdk/playwright/snapshots/timeline/timeline.spec.ts/image-in-timeline-default-layout-linux.png b/linked-dependencies/matrix-react-sdk/playwright/snapshots/timeline/timeline.spec.ts/image-in-timeline-default-layout-linux.png new file mode 100644 index 0000000000..dfc55550aa Binary files /dev/null and b/linked-dependencies/matrix-react-sdk/playwright/snapshots/timeline/timeline.spec.ts/image-in-timeline-default-layout-linux.png differ diff --git a/linked-dependencies/matrix-react-sdk/playwright/snapshots/user-onboarding/user-onboarding-new.spec.ts/User-Onboarding-new-user-app-download-dialog-1-linux.png b/linked-dependencies/matrix-react-sdk/playwright/snapshots/user-onboarding/user-onboarding-new.spec.ts/User-Onboarding-new-user-app-download-dialog-1-linux.png index 589cb34cb4..0c7fc94a0e 100644 Binary files a/linked-dependencies/matrix-react-sdk/playwright/snapshots/user-onboarding/user-onboarding-new.spec.ts/User-Onboarding-new-user-app-download-dialog-1-linux.png and b/linked-dependencies/matrix-react-sdk/playwright/snapshots/user-onboarding/user-onboarding-new.spec.ts/User-Onboarding-new-user-app-download-dialog-1-linux.png differ diff --git a/linked-dependencies/matrix-react-sdk/playwright/snapshots/user-view/user-view.spec.ts/user-info-linux.png b/linked-dependencies/matrix-react-sdk/playwright/snapshots/user-view/user-view.spec.ts/user-info-linux.png index 9d9b431b0c..61ab660157 100644 Binary files a/linked-dependencies/matrix-react-sdk/playwright/snapshots/user-view/user-view.spec.ts/user-info-linux.png and b/linked-dependencies/matrix-react-sdk/playwright/snapshots/user-view/user-view.spec.ts/user-info-linux.png differ diff --git a/linked-dependencies/matrix-react-sdk/res/css/structures/_TabbedView.pcss b/linked-dependencies/matrix-react-sdk/res/css/structures/_TabbedView.pcss index 756f6ab864..34a1766c19 100644 --- a/linked-dependencies/matrix-react-sdk/res/css/structures/_TabbedView.pcss +++ b/linked-dependencies/matrix-react-sdk/res/css/structures/_TabbedView.pcss @@ -129,6 +129,7 @@ limitations under the License. padding-inline: var(--cpd-space-3x) var(--cpd-space-4x); box-sizing: border-box; min-block-size: 40px; + min-inline-size: 40px; border-radius: 24px; font: var(--cpd-font-body-md-medium); position: relative; @@ -164,3 +165,25 @@ limitations under the License. overflow: auto; min-height: 0; /* firefox */ } + +/* Hide the labels on tabs, showing only the icons, on narrow viewports. */ +@media (max-width: 768px) { + .mx_TabbedView_tabsOnLeft.mx_TabbedView_responsive { + .mx_TabbedView_tabLabel_text { + display: none; + } + .mx_TabbedView_tabPanel { + margin-left: 72px; /* 40px sidebar + 32px padding */ + } + .mx_TabbedView_maskedIcon { + margin-right: auto; + margin-left: auto; + } + .mx_TabbedView_tabLabels { + width: auto; + } + .mx_TabbedView_tabLabel { + padding-inline: 0 0; + } + } +} diff --git a/linked-dependencies/matrix-react-sdk/res/css/views/auth/_LoginWithQR.pcss b/linked-dependencies/matrix-react-sdk/res/css/views/auth/_LoginWithQR.pcss index 6a112c7c82..c4904952b6 100644 --- a/linked-dependencies/matrix-react-sdk/res/css/views/auth/_LoginWithQR.pcss +++ b/linked-dependencies/matrix-react-sdk/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 { + 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; + } - li::marker { - color: $accent; + /* 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/linked-dependencies/matrix-react-sdk/res/css/views/right_panel/_BaseCard.pcss b/linked-dependencies/matrix-react-sdk/res/css/views/right_panel/_BaseCard.pcss index 6d17930fce..67eb9b7e49 100644 --- a/linked-dependencies/matrix-react-sdk/res/css/views/right_panel/_BaseCard.pcss +++ b/linked-dependencies/matrix-react-sdk/res/css/views/right_panel/_BaseCard.pcss @@ -90,6 +90,7 @@ limitations under the License. min-height: 0; width: 100%; height: 100%; + scrollbar-gutter: stable; } .mx_BaseCard_Group { diff --git a/linked-dependencies/matrix-react-sdk/res/css/views/right_panel/_RoomSummaryCard.pcss b/linked-dependencies/matrix-react-sdk/res/css/views/right_panel/_RoomSummaryCard.pcss index 72b23d860e..4c3ff2f888 100644 --- a/linked-dependencies/matrix-react-sdk/res/css/views/right_panel/_RoomSummaryCard.pcss +++ b/linked-dependencies/matrix-react-sdk/res/css/views/right_panel/_RoomSummaryCard.pcss @@ -51,6 +51,52 @@ limitations under the License. } } + .mx_RoomSummaryCard_topic { + padding: 0 12px; + + .mx_Box { + width: 100%; + } + + .mx_RoomSummaryCard_topic_container { + display: flex; + } + + .mx_RoomSummaryCard_topic_edit { + width: max-content; + } + + p { + white-space: pre-wrap; + width: 100%; + min-width: 0; + margin: 0; + } + + a { + cursor: pointer; + } + + .mx_RoomSummaryCard_topic_chevron { + transition: transform 0.3s; + } + + &.mx_RoomSummaryCard_topic_collapsed { + p { + overflow: hidden; + text-overflow: ellipsis; + white-space: normal; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + } + + .mx_RoomSummaryCard_topic_chevron { + transform: rotate(-90deg); + } + } + } + .mx_RoomSummaryCard_appsGroup { .mx_RoomSummaryCard_Button { /* this button is special so we have to override some of the original styling */ diff --git a/linked-dependencies/matrix-react-sdk/res/css/views/rooms/_AppsDrawer.pcss b/linked-dependencies/matrix-react-sdk/res/css/views/rooms/_AppsDrawer.pcss index fc59568322..83888d0b9f 100644 --- a/linked-dependencies/matrix-react-sdk/res/css/views/rooms/_AppsDrawer.pcss +++ b/linked-dependencies/matrix-react-sdk/res/css/views/rooms/_AppsDrawer.pcss @@ -216,6 +216,11 @@ limitations under the License. margin-right: 12px; } + h3 { + font-size: inherit; + margin: 0; + } + > :last-child { margin-left: 9px; display: flex; diff --git a/linked-dependencies/matrix-react-sdk/res/css/views/rooms/_MessageComposerFormatBar.pcss b/linked-dependencies/matrix-react-sdk/res/css/views/rooms/_MessageComposerFormatBar.pcss index 68520cc741..1cbabecc3a 100644 --- a/linked-dependencies/matrix-react-sdk/res/css/views/rooms/_MessageComposerFormatBar.pcss +++ b/linked-dependencies/matrix-react-sdk/res/css/views/rooms/_MessageComposerFormatBar.pcss @@ -102,9 +102,4 @@ limitations under the License. font-weight: var(--cpd-font-weight-semibold); min-width: 54px; text-align: center; - - .mx_MessageComposerFormatBar_tooltipShortcut { - font-size: $font-9px; - opacity: 0.7; - } } diff --git a/linked-dependencies/matrix-react-sdk/res/css/views/rooms/_RoomBreadcrumbs.pcss b/linked-dependencies/matrix-react-sdk/res/css/views/rooms/_RoomBreadcrumbs.pcss index f502c3f470..73c4b27432 100644 --- a/linked-dependencies/matrix-react-sdk/res/css/views/rooms/_RoomBreadcrumbs.pcss +++ b/linked-dependencies/matrix-react-sdk/res/css/views/rooms/_RoomBreadcrumbs.pcss @@ -49,8 +49,3 @@ limitations under the License. height: 32px; } } - -.mx_RoomBreadcrumbs_Tooltip { - margin-left: -42px; - margin-top: -42px; -} diff --git a/linked-dependencies/matrix-react-sdk/res/css/views/rooms/wysiwyg_composer/components/_FormattingButtons.pcss b/linked-dependencies/matrix-react-sdk/res/css/views/rooms/wysiwyg_composer/components/_FormattingButtons.pcss index 8f23227c80..347f475063 100644 --- a/linked-dependencies/matrix-react-sdk/res/css/views/rooms/wysiwyg_composer/components/_FormattingButtons.pcss +++ b/linked-dependencies/matrix-react-sdk/res/css/views/rooms/wysiwyg_composer/components/_FormattingButtons.pcss @@ -64,19 +64,11 @@ limitations under the License. } } -.mx_FormattingButtons_Tooltip { - padding: 0 2px 0 2px; - - .mx_FormattingButtons_Tooltip_KeyboardShortcut { - color: $tertiary-content; - - kbd { - margin-top: 2px; - text-align: center; - display: inline-block; - text-transform: capitalize; - font-size: 12px; - font-family: Inter, sans-serif; - } +.mx_FormattingButtons_Tooltip_KeyboardShortcut { + kbd { + text-align: center; + display: inline-block; + text-transform: capitalize; + font-family: Inter, sans-serif; } } diff --git a/linked-dependencies/matrix-react-sdk/res/css/views/toasts/_IncomingLegacyCallToast.pcss b/linked-dependencies/matrix-react-sdk/res/css/views/toasts/_IncomingLegacyCallToast.pcss index e2092ef006..695b588932 100644 --- a/linked-dependencies/matrix-react-sdk/res/css/views/toasts/_IncomingLegacyCallToast.pcss +++ b/linked-dependencies/matrix-react-sdk/res/css/views/toasts/_IncomingLegacyCallToast.pcss @@ -18,6 +18,7 @@ limitations under the License. .mx_IncomingLegacyCallToast { display: flex; flex-direction: row; + align-items: flex-start; pointer-events: initial; /* restore pointer events so the user can accept/decline */ .mx_IncomingLegacyCallToast_content { diff --git a/linked-dependencies/matrix-react-sdk/src/BasePlatform.ts b/linked-dependencies/matrix-react-sdk/src/BasePlatform.ts index 7150336e45..e7e4ff7e3c 100644 --- a/linked-dependencies/matrix-react-sdk/src/BasePlatform.ts +++ b/linked-dependencies/matrix-react-sdk/src/BasePlatform.ts @@ -34,10 +34,11 @@ import { CheckUpdatesPayload } from "./dispatcher/payloads/CheckUpdatesPayload"; import { Action } from "./dispatcher/actions"; import { hideToast as hideUpdateToast } from "./toasts/UpdateToast"; import { MatrixClientPeg } from "./MatrixClientPeg"; -import { idbLoad, idbSave, idbDelete } from "./utils/StorageManager"; +import { idbLoad, idbSave, idbDelete } from "./utils/StorageAccess"; import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload"; import { IConfigOptions } from "./IConfigOptions"; import SdkConfig from "./SdkConfig"; +import { buildAndEncodePickleKey, getPickleAdditionalData } from "./utils/tokens/pickling"; export const SSO_HOMESERVER_URL_KEY = "mx_sso_hs_url"; export const SSO_ID_SERVER_URL_KEY = "mx_sso_is_url"; @@ -314,7 +315,7 @@ export default abstract class BasePlatform { } /** - * The URL to return to after a successful SSO/OIDC authentication + * The URL to return to after a successful SSO authentication * @param fragmentAfterLogin optional fragment for specific view to return to */ public getSSOCallbackUrl(fragmentAfterLogin = ""): URL { @@ -352,55 +353,21 @@ export default abstract class BasePlatform { /** * Get a previously stored pickle key. The pickle key is used for - * encrypting libolm objects. + * encrypting libolm objects and react-sdk-crypto data. * @param {string} userId the user ID for the user that the pickle key is for. - * @param {string} userId the device ID that the pickle key is for. + * @param {string} deviceId the device ID that the pickle key is for. * @returns {string|null} the previously stored pickle key, or null if no * pickle key has been stored. */ public async getPickleKey(userId: string, deviceId: string): Promise { - if (!window.crypto || !window.crypto.subtle) { - return null; - } - let data; + let data: { encrypted?: BufferSource; iv?: BufferSource; cryptoKey?: CryptoKey } | undefined; try { data = await idbLoad("pickleKey", [userId, deviceId]); } catch (e) { logger.error("idbLoad for pickleKey failed", e); } - if (!data) { - return null; - } - if (!data.encrypted || !data.iv || !data.cryptoKey) { - logger.error("Badly formatted pickle key"); - return null; - } - - const additionalData = this.getPickleAdditionalData(userId, deviceId); - - try { - const key = await crypto.subtle.decrypt( - { name: "AES-GCM", iv: data.iv, additionalData }, - data.cryptoKey, - data.encrypted, - ); - return encodeUnpaddedBase64(key); - } catch (e) { - logger.error("Error decrypting pickle key"); - return null; - } - } - private getPickleAdditionalData(userId: string, deviceId: string): Uint8Array { - const additionalData = new Uint8Array(userId.length + deviceId.length + 1); - for (let i = 0; i < userId.length; i++) { - additionalData[i] = userId.charCodeAt(i); - } - additionalData[userId.length] = 124; // "|" - for (let i = 0; i < deviceId.length; i++) { - additionalData[userId.length + 1 + i] = deviceId.charCodeAt(i); - } - return additionalData; + return (await buildAndEncodePickleKey(data, userId, deviceId)) ?? null; } /** @@ -424,7 +391,7 @@ export default abstract class BasePlatform { const iv = new Uint8Array(32); crypto.getRandomValues(iv); - const additionalData = this.getPickleAdditionalData(userId, deviceId); + const additionalData = getPickleAdditionalData(userId, deviceId); const encrypted = await crypto.subtle.encrypt({ name: "AES-GCM", iv, additionalData }, cryptoKey, randomArray); try { @@ -463,6 +430,13 @@ export default abstract class BasePlatform { return window.location.origin + window.location.pathname; } + /** + * Fallback Client URI to use for OIDC client registration for if one is not specified in config.json + */ + public get defaultOidcClientUri(): string { + return window.location.origin; + } + /** * Metadata to use for dynamic OIDC client registrations */ @@ -470,16 +444,17 @@ export default abstract class BasePlatform { const config = SdkConfig.get(); return { clientName: config.brand, - clientUri: this.baseUrl, - redirectUris: [this.getSSOCallbackUrl().href], - logoUri: new URL("vector-icons/1024.png", this.baseUrl).href, + clientUri: config.oidc_metadata?.client_uri ?? this.defaultOidcClientUri, + redirectUris: [this.getOidcCallbackUrl().href], + logoUri: config.oidc_metadata?.logo_uri ?? new URL("vector-icons/1024.png", this.baseUrl).href, applicationType: "web", // XXX: We break the spec by not consistently supplying these required fields - // contacts: [], // @ts-ignore - tosUri: config.terms_and_conditions_links?.[0]?.url, + contacts: config.oidc_metadata?.contacts, // @ts-ignore - policyUri: config.privacy_policy_url, + tosUri: config.oidc_metadata?.tos_uri ?? config.terms_and_conditions_links?.[0]?.url, + // @ts-ignore + policyUri: config.oidc_metadata?.policy_uri ?? config.privacy_policy_url, }; } @@ -490,4 +465,15 @@ export default abstract class BasePlatform { public getOidcClientState(): string { return ""; } + + /** + * The URL to return to after a successful OIDC authentication + */ + public getOidcCallbackUrl(): URL { + const url = new URL(window.location.href); + // The redirect URL has to exactly match that registered at the OIDC server, so + // ensure that the fragment part of the URL is empty. + url.hash = ""; + return url; + } } diff --git a/linked-dependencies/matrix-react-sdk/src/DecryptionFailureTracker.ts b/linked-dependencies/matrix-react-sdk/src/DecryptionFailureTracker.ts index f9afec0daa..c842e55ec4 100644 --- a/linked-dependencies/matrix-react-sdk/src/DecryptionFailureTracker.ts +++ b/linked-dependencies/matrix-react-sdk/src/DecryptionFailureTracker.ts @@ -14,9 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { DecryptionError } from "matrix-js-sdk/src/crypto/algorithms"; import { MatrixEvent } from "matrix-js-sdk/src/matrix"; import { Error as ErrorEvent } from "@matrix-org/analytics-events/types/typescript/Error"; +import { DecryptionFailureCode } from "matrix-js-sdk/src/crypto-api"; import { PosthogAnalytics } from "./PosthogAnalytics"; @@ -25,17 +25,15 @@ export class DecryptionFailure { public constructor( public readonly failedEventId: string, - public readonly errorCode: string, + public readonly errorCode: DecryptionFailureCode, ) { this.ts = Date.now(); } } -type ErrorCode = "OlmKeysNotSentError" | "OlmIndexError" | "UnknownError" | "OlmUnspecifiedError"; - +type ErrorCode = ErrorEvent["name"]; type TrackingFn = (count: number, trackedErrCode: ErrorCode, rawError: string) => void; - -export type ErrCodeMapFn = (errcode: string) => ErrorCode; +export type ErrCodeMapFn = (errcode: DecryptionFailureCode) => ErrorCode; export class DecryptionFailureTracker { private static internalInstance = new DecryptionFailureTracker( @@ -52,12 +50,16 @@ export class DecryptionFailureTracker { (errorCode) => { // Map JS-SDK error codes to tracker codes for aggregation switch (errorCode) { - case "MEGOLM_UNKNOWN_INBOUND_SESSION_ID": + case DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID: return "OlmKeysNotSentError"; - case "OLM_UNKNOWN_MESSAGE_INDEX": + case DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX: return "OlmIndexError"; - case undefined: - return "OlmUnspecifiedError"; + case DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP: + 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"; } @@ -76,11 +78,11 @@ export class DecryptionFailureTracker { // accumulated in `failureCounts`. public visibleFailures: Map = new Map(); - // A histogram of the number of failures that will be tracked at the next tracking - // interval, split by failure error code. - public failureCounts: Record = { - // [errorCode]: 42 - }; + /** + * A histogram of the number of failures that will be tracked at the next tracking + * interval, split by failure error code. + */ + private failureCounts: Map = new Map(); // Event IDs of failures that were tracked previously public trackedEvents: Set = new Set(); @@ -108,10 +110,10 @@ export class DecryptionFailureTracker { * * @param {function} fn The tracking function, which will be called when failures * are tracked. The function should have a signature `(count, trackedErrorCode) => {...}`, - * where `count` is the number of failures and `errorCode` matches the `.code` of - * provided DecryptionError errors (by default, unless `errorCodeMapFn` is specified. - * @param {function?} errorCodeMapFn The function used to map error codes to the - * trackedErrorCode. If not provided, the `.code` of errors will be used. + * where `count` is the number of failures and `errorCode` matches the output of `errorCodeMapFn`. + * + * @param {function} errorCodeMapFn The function used to map decryption failure reason codes to the + * `trackedErrorCode`. */ private constructor( private readonly fn: TrackingFn, @@ -138,13 +140,15 @@ export class DecryptionFailureTracker { // localStorage.setItem('mx-decryption-failure-event-ids', JSON.stringify([...this.trackedEvents])); // } - public eventDecrypted(e: MatrixEvent, err: DecryptionError): void { - // for now we only track megolm decrytion failures + public eventDecrypted(e: MatrixEvent): void { + // for now we only track megolm decryption failures if (e.getWireContent().algorithm != "m.megolm.v1.aes-sha2") { return; } - if (err) { - this.addDecryptionFailure(new DecryptionFailure(e.getId()!, err.code)); + + const errCode = e.decryptionFailureReason; + if (errCode !== null) { + this.addDecryptionFailure(new DecryptionFailure(e.getId()!, errCode)); } else { // Could be an event in the failures, remove it this.removeDecryptionFailuresForEvent(e); @@ -205,7 +209,7 @@ export class DecryptionFailureTracker { this.failures = new Map(); this.visibleEvents = new Set(); this.visibleFailures = new Map(); - this.failureCounts = {}; + this.failureCounts = new Map(); } /** @@ -236,7 +240,7 @@ export class DecryptionFailureTracker { private aggregateFailures(failures: Set): void { for (const failure of failures) { const errorCode = failure.errorCode; - this.failureCounts[errorCode] = (this.failureCounts[errorCode] || 0) + 1; + this.failureCounts.set(errorCode, (this.failureCounts.get(errorCode) ?? 0) + 1); } } @@ -245,12 +249,12 @@ export class DecryptionFailureTracker { * function with the number of failures that should be tracked. */ public trackFailures(): void { - for (const errorCode of Object.keys(this.failureCounts)) { - if (this.failureCounts[errorCode] > 0) { + for (const [errorCode, count] of this.failureCounts.entries()) { + if (count > 0) { const trackedErrorCode = this.errorCodeMapFn(errorCode); - this.fn(this.failureCounts[errorCode], trackedErrorCode, errorCode); - this.failureCounts[errorCode] = 0; + this.fn(count, trackedErrorCode, errorCode); + this.failureCounts.set(errorCode, 0); } } } diff --git a/linked-dependencies/matrix-react-sdk/src/HtmlUtils.tsx b/linked-dependencies/matrix-react-sdk/src/HtmlUtils.tsx index b518c43973..b63ed1dcf0 100644 --- a/linked-dependencies/matrix-react-sdk/src/HtmlUtils.tsx +++ b/linked-dependencies/matrix-react-sdk/src/HtmlUtils.tsx @@ -348,7 +348,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op isHtmlMessage = !isPlainText; if (isHtmlMessage && SettingsStore.getValue("feature_latex_maths")) { - [...phtml.querySelectorAll("div, span[data-mx-maths]")].forEach((e) => { + [...phtml.querySelectorAll("div[data-mx-maths], span[data-mx-maths]")].forEach((e) => { e.outerHTML = katex.renderToString(decode(e.getAttribute("data-mx-maths")), { throwOnError: false, displayMode: e.tagName == "DIV", @@ -461,9 +461,13 @@ export function topicToHtml( emojiBodyElements = formatEmojis(topic, false); } - return isFormattedTopic ? ( - - ) : ( + if (isFormattedTopic) { + if (!safeTopic) return null; + return ; + } + + if (!emojiBodyElements && !topic) return null; + return ( {emojiBodyElements || topic} diff --git a/linked-dependencies/matrix-react-sdk/src/IConfigOptions.ts b/linked-dependencies/matrix-react-sdk/src/IConfigOptions.ts index 0e9645349b..c3a919648e 100644 --- a/linked-dependencies/matrix-react-sdk/src/IConfigOptions.ts +++ b/linked-dependencies/matrix-react-sdk/src/IConfigOptions.ts @@ -200,12 +200,20 @@ export interface IConfigOptions { * The issuer URL must have a trailing `/`. * OPTIONAL */ - oidc_static_clients?: Record< - string, - { - client_id: string; - } - >; + oidc_static_clients?: { + [issuer: string]: { client_id: string }; + }; + + /** + * Configuration for OIDC dynamic registration where a static OIDC client is not configured. + */ + oidc_metadata?: { + client_uri?: string; + logo_uri?: string; + tos_uri?: string; + policy_uri?: string; + contacts?: string[]; + }; } export interface ISsoRedirectOptions { diff --git a/linked-dependencies/matrix-react-sdk/src/Lifecycle.ts b/linked-dependencies/matrix-react-sdk/src/Lifecycle.ts index 61097c13c2..8b04f74afc 100644 --- a/linked-dependencies/matrix-react-sdk/src/Lifecycle.ts +++ b/linked-dependencies/matrix-react-sdk/src/Lifecycle.ts @@ -37,6 +37,7 @@ import ActiveWidgetStore from "./stores/ActiveWidgetStore"; import PlatformPeg from "./PlatformPeg"; import { sendLoginRequest } from "./Login"; import * as StorageManager from "./utils/StorageManager"; +import * as StorageAccess from "./utils/StorageAccess"; import SettingsStore from "./settings/SettingsStore"; import { SettingLevel } from "./settings/SettingLevel"; import ToastStore from "./stores/ToastStore"; @@ -288,7 +289,7 @@ export async function attemptDelegatedAuthLogin( */ async function attemptOidcNativeLogin(queryParams: QueryDict): Promise { try { - const { accessToken, refreshToken, homeserverUrl, identityServerUrl, idTokenClaims, clientId, issuer } = + const { accessToken, refreshToken, homeserverUrl, identityServerUrl, idToken, clientId, issuer } = await completeOidcLogin(queryParams); const { @@ -310,7 +311,7 @@ async function attemptOidcNativeLogin(queryParams: QueryDict): Promise logger.debug("Logged in via OIDC native flow"); await onSuccessfulDelegatedAuthLogin(credentials); // this needs to happen after success handler which clears storages - persistOidcAuthenticatedSettings(clientId, issuer, idTokenClaims); + persistOidcAuthenticatedSettings(clientId, issuer, idToken); return true; } catch (error) { logger.error("Failed to login via OIDC", error); @@ -493,7 +494,7 @@ export interface IStoredSession { async function getStoredToken(storageKey: string): Promise { let token: string | undefined; try { - token = await StorageManager.idbLoad("account", storageKey); + token = await StorageAccess.idbLoad("account", storageKey); } catch (e) { logger.error(`StorageManager.idbLoad failed for account:${storageKey}`, e); } @@ -502,7 +503,7 @@ async function getStoredToken(storageKey: string): Promise { if (token) { try { // try to migrate access token to IndexedDB if we can - await StorageManager.idbSave("account", storageKey, token); + await StorageAccess.idbSave("account", storageKey, token); localStorage.removeItem(storageKey); } catch (e) { logger.error(`migration of token ${storageKey} to IndexedDB failed`, e); @@ -719,7 +720,7 @@ async function createOidcTokenRefresher(credentials: IMatrixClientCreds): Promis try { const clientId = getStoredOidcClientId(); const idTokenClaims = getStoredOidcIdTokenClaims(); - const redirectUri = PlatformPeg.get()!.getSSOCallbackUrl().href; + const redirectUri = PlatformPeg.get()!.getOidcCallbackUrl().href; const deviceId = credentials.deviceId; if (!deviceId) { throw new Error("Expected deviceId in user credentials."); @@ -1064,7 +1065,7 @@ async function clearStorage(opts?: { deleteEverything?: boolean }): 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/linked-dependencies/matrix-react-sdk/src/Modal.tsx b/linked-dependencies/matrix-react-sdk/src/Modal.tsx index aa4ba691dc..2ac12d280f 100644 --- a/linked-dependencies/matrix-react-sdk/src/Modal.tsx +++ b/linked-dependencies/matrix-react-sdk/src/Modal.tsx @@ -20,7 +20,7 @@ import ReactDOM from "react-dom"; import classNames from "classnames"; import { defer, sleep } from "matrix-js-sdk/src/utils"; import { TypedEventEmitter } from "matrix-js-sdk/src/matrix"; -import { Glass, TooltipProvider } from "@vector-im/compound-web"; +import { Glass } from "@vector-im/compound-web"; import dis from "./dispatcher/dispatcher"; import AsyncWrapper from "./AsyncWrapper"; @@ -374,18 +374,16 @@ export class ModalManager extends TypedEventEmitter -
- -
{this.staticModal.elem}
-
-
-
- +
+ +
{this.staticModal.elem}
+
+
+
); ReactDOM.render(staticDialog, ModalManager.getOrCreateStaticContainer()); @@ -401,18 +399,16 @@ export class ModalManager extends TypedEventEmitter -
- -
{modal.elem}
-
-
-
- +
+ +
{modal.elem}
+
+
+
); setImmediate(() => ReactDOM.render(dialog, ModalManager.getOrCreateContainer())); diff --git a/linked-dependencies/matrix-react-sdk/src/PlaybackEncoder.ts b/linked-dependencies/matrix-react-sdk/src/PlaybackEncoder.ts new file mode 100644 index 0000000000..a08292e01f --- /dev/null +++ b/linked-dependencies/matrix-react-sdk/src/PlaybackEncoder.ts @@ -0,0 +1,34 @@ +/* +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. +*/ + +// @ts-ignore - `.ts` is needed here to make TS happy +import { Request, Response } from "./workers/playback.worker"; +import { WorkerManager } from "./WorkerManager"; +import playbackWorkerFactory from "./workers/playbackWorkerFactory"; + +export class PlaybackEncoder { + private static internalInstance = new PlaybackEncoder(); + + public static get instance(): PlaybackEncoder { + return PlaybackEncoder.internalInstance; + } + + private readonly worker = new WorkerManager(playbackWorkerFactory()); + + public getPlaybackWaveform(input: Float32Array): Promise { + return this.worker.call({ data: Array.from(input) }).then((resp) => resp.waveform); + } +} diff --git a/linked-dependencies/matrix-react-sdk/src/PosthogAnalytics.ts b/linked-dependencies/matrix-react-sdk/src/PosthogAnalytics.ts index 20c4ff2f33..f8518d925b 100644 --- a/linked-dependencies/matrix-react-sdk/src/PosthogAnalytics.ts +++ b/linked-dependencies/matrix-react-sdk/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/linked-dependencies/matrix-react-sdk/src/SlidingSyncManager.ts b/linked-dependencies/matrix-react-sdk/src/SlidingSyncManager.ts index 5f459c0b9e..c4387e85d6 100644 --- a/linked-dependencies/matrix-react-sdk/src/SlidingSyncManager.ts +++ b/linked-dependencies/matrix-react-sdk/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,89 @@ 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.getDomain()!); + proxyUrl = clientWellKnown?.["org.matrix.msc3575.proxy"]?.url; + } catch (e) { + // client.getDomain() 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 (with an unstable endpoint). + * @param client The MatrixClient to use + * @return Whether the "native" (unstable) endpoint is supported + */ + public async nativeSlidingSyncSupport(client: MatrixClient): Promise { + // Per https://github.com/matrix-org/matrix-spec-proposals/pull/3575/files#r1589542561 + // `client` can be undefined/null in tests for some reason. + const support = await client?.doesServerSupportUnstableFeature("org.matrix.msc3575"); + if (support) { + logger.log("nativeSlidingSyncSupport: sliding sync advertised as unstable"); + } + return support; + } + + /** + * 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(new URL("/client/server.json", proxyUrl), { + method: Method.Get, + signal: timeoutSignal(10 * 1000), // 10s + }); + if (response.status === 200) { + logger.log("checkSupport: well-known sliding sync proxy is up at", proxyUrl); + SlidingSyncController.serverSupportsSlidingSync = true; + } + } + } } diff --git a/linked-dependencies/matrix-react-sdk/src/accessibility/context_menu/ContextMenuButton.tsx b/linked-dependencies/matrix-react-sdk/src/accessibility/context_menu/ContextMenuButton.tsx index 6ef6afef37..35dd986f8e 100644 --- a/linked-dependencies/matrix-react-sdk/src/accessibility/context_menu/ContextMenuButton.tsx +++ b/linked-dependencies/matrix-react-sdk/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/linked-dependencies/matrix-react-sdk/src/accessibility/roving/RovingAccessibleButton.tsx b/linked-dependencies/matrix-react-sdk/src/accessibility/roving/RovingAccessibleButton.tsx index 56c9052714..01e126824d 100644 --- a/linked-dependencies/matrix-react-sdk/src/accessibility/roving/RovingAccessibleButton.tsx +++ b/linked-dependencies/matrix-react-sdk/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/linked-dependencies/matrix-react-sdk/src/accessibility/roving/RovingAccessibleTooltipButton.tsx b/linked-dependencies/matrix-react-sdk/src/accessibility/roving/RovingAccessibleTooltipButton.tsx index 5607089c6e..76927c1773 100644 --- a/linked-dependencies/matrix-react-sdk/src/accessibility/roving/RovingAccessibleTooltipButton.tsx +++ b/linked-dependencies/matrix-react-sdk/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/linked-dependencies/matrix-react-sdk/src/audio/Playback.ts b/linked-dependencies/matrix-react-sdk/src/audio/Playback.ts index dc2619d692..957d541732 100644 --- a/linked-dependencies/matrix-react-sdk/src/audio/Playback.ts +++ b/linked-dependencies/matrix-react-sdk/src/audio/Playback.ts @@ -19,17 +19,14 @@ import { SimpleObservable } from "matrix-widget-api"; import { logger } from "matrix-js-sdk/src/logger"; import { defer } from "matrix-js-sdk/src/utils"; -// @ts-ignore - `.ts` is needed here to make TS happy -import { Request, Response } from "../workers/playback.worker.ts"; import { UPDATE_EVENT } from "../stores/AsyncStore"; import { arrayFastResample } from "../utils/arrays"; import { IDestroyable } from "../utils/IDestroyable"; import { PlaybackClock } from "./PlaybackClock"; import { createAudioContext, decodeOgg } from "./compat"; import { clamp } from "../utils/numbers"; -import { WorkerManager } from "../WorkerManager"; import { DEFAULT_WAVEFORM, PLAYBACK_WAVEFORM_SAMPLES } from "./consts"; -import playbackWorkerFactory from "../workers/playbackWorkerFactory"; +import { PlaybackEncoder } from "../PlaybackEncoder"; export enum PlaybackState { Decoding = "decoding", @@ -64,7 +61,6 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte private waveformObservable = new SimpleObservable(); private readonly clock: PlaybackClock; private readonly fileSize: number; - private readonly worker = new WorkerManager(playbackWorkerFactory()); /** * Creates a new playback instance from a buffer. @@ -209,7 +205,9 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte // Update the waveform to the real waveform once we have channel data to use. We don't // exactly trust the user-provided waveform to be accurate... - this.resampledWaveform = await this.makePlaybackWaveform(this.audioBuf.getChannelData(0)); + this.resampledWaveform = await PlaybackEncoder.instance.getPlaybackWaveform( + this.audioBuf.getChannelData(0), + ); } this.waveformObservable.update(this.resampledWaveform); @@ -222,10 +220,6 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte this.emit(PlaybackState.Stopped); // signal that we're not decoding anymore } - private makePlaybackWaveform(input: Float32Array): Promise { - return this.worker.call({ data: Array.from(input) }).then((resp) => resp.waveform); - } - private onPlaybackEnd = async (): Promise => { await this.context.suspend(); this.emit(PlaybackState.Stopped); diff --git a/linked-dependencies/matrix-react-sdk/src/components/structures/ContextMenu.tsx b/linked-dependencies/matrix-react-sdk/src/components/structures/ContextMenu.tsx index 26b52a6e72..0b71c8dd30 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/structures/ContextMenu.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/structures/ContextMenu.tsx @@ -20,7 +20,6 @@ import React, { CSSProperties, RefObject, SyntheticEvent, useRef, useState } fro import ReactDOM from "react-dom"; import classNames from "classnames"; import FocusLock from "react-focus-lock"; -import { TooltipProvider } from "@vector-im/compound-web"; import { Writeable } from "../../@types/common"; import UIStore from "../../stores/UIStore"; @@ -630,17 +629,15 @@ export function createMenu( }; const menu = ( - - - - - + + + ); ReactDOM.render(menu, getOrCreateContainer()); diff --git a/linked-dependencies/matrix-react-sdk/src/components/structures/LeftPanel.tsx b/linked-dependencies/matrix-react-sdk/src/components/structures/LeftPanel.tsx index c2454e04fb..084afdaf8b 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/structures/LeftPanel.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/structures/LeftPanel.tsx @@ -26,7 +26,6 @@ import { HEADER_HEIGHT } from "../views/rooms/RoomSublist"; import { Action } from "../../dispatcher/actions"; import RoomSearch from "./RoomSearch"; import ResizeNotifier from "../../utils/ResizeNotifier"; -import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import SpaceStore from "../../stores/spaces/SpaceStore"; import { MetaSpace, SpaceKey, UPDATE_SELECTED_SPACE } from "../../stores/spaces"; import { getKeyBindingsManager } from "../../KeyBindingsManager"; @@ -41,7 +40,7 @@ import RoomBreadcrumbs from "../views/rooms/RoomBreadcrumbs"; import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts"; import { shouldShowComponent } from "../../customisations/helpers/UIComponents"; import { UIComponent } from "../../settings/UIFeature"; -import { ButtonEvent } from "../views/elements/AccessibleButton"; +import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton"; import PosthogTrackers from "../../PosthogTrackers"; import PageType from "../../PageTypes"; import { UserOnboardingButton } from "../views/user-onboarding/UserOnboardingButton"; @@ -333,7 +332,7 @@ export default class LeftPanel extends React.Component { // to start a new call if (LegacyCallHandler.instance.getSupportsPstnProtocol()) { dialPadButton = ( - { let rightButton: JSX.Element | undefined; if (this.state.activeSpace === MetaSpace.Home && shouldShowComponent(UIComponent.ExploreRooms)) { rightButton = ( - { // When logging out, stop tracking failures and destroy state cli.on(HttpApiEvent.SessionLoggedOut, () => dft.stop()); - cli.on(MatrixEventEvent.Decrypted, (e, err) => dft.eventDecrypted(e, err as DecryptionError)); + cli.on(MatrixEventEvent.Decrypted, (e) => dft.eventDecrypted(e)); cli.on(ClientEvent.Room, (room) => { if (cli.isCryptoEnabled()) { @@ -2148,9 +2146,7 @@ export default class MatrixChat extends React.PureComponent { return ( - - {view} - + {view} ); } diff --git a/linked-dependencies/matrix-react-sdk/src/components/structures/SpaceHierarchy.tsx b/linked-dependencies/matrix-react-sdk/src/components/structures/SpaceHierarchy.tsx index 41c2bd139e..fd82b410a7 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/structures/SpaceHierarchy.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/structures/SpaceHierarchy.tsx @@ -15,7 +15,6 @@ limitations under the License. */ import React, { - ComponentProps, Dispatch, KeyboardEvent, KeyboardEventHandler, @@ -62,7 +61,6 @@ import InfoTooltip from "../views/elements/InfoTooltip"; import TextWithTooltip from "../views/elements/TextWithTooltip"; import { useStateToggle } from "../../hooks/useStateToggle"; import { getChildOrder } from "../../stores/spaces/SpaceStore"; -import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import { Linkify, topicToHtml } from "../../HtmlUtils"; import { useDispatcher } from "../../hooks/useDispatcher"; import { Action } from "../../dispatcher/actions"; @@ -75,7 +73,6 @@ import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; import { JoinRoomReadyPayload } from "../../dispatcher/payloads/JoinRoomReadyPayload"; import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts"; import { getKeyBindingsManager } from "../../KeyBindingsManager"; -import { Alignment } from "../views/elements/Tooltip"; import { getTopic } from "../../hooks/room/useTopic"; import { SdkContextClass } from "../../contexts/SDKContext"; import { getDisplayAliasForAliasSet } from "../../Rooms"; @@ -148,7 +145,7 @@ const Tile: React.FC = ({ let button: ReactElement; if (busy) { button = ( - = ({ title={_t("space|joining_space")} > - + ); } else if (joinedRoom || room.join_rule === JoinRule.Knock) { // If the room is knockable, show the "View" button even if we are not a member; that @@ -670,25 +667,16 @@ const ManageButtons: React.FC = ({ hierarchy, selected, set const disabled = !selectedRelations.length || removing || saving; - let Button: React.ComponentType> = AccessibleButton; - let props: Partial> = {}; - if (!selectedRelations.length) { - Button = AccessibleTooltipButton; - props = { - tooltip: _t("space|select_room_below"), - alignment: Alignment.Top, - }; - } - let buttonText = _t("common|saving"); if (!saving) { buttonText = selectionAllSuggested ? _t("space|unmark_suggested") : _t("space|mark_suggested"); } + const title = !selectedRelations.length ? _t("space|select_room_below") : undefined; + return ( <> - - + ); }; diff --git a/linked-dependencies/matrix-react-sdk/src/components/structures/SpaceRoomView.tsx b/linked-dependencies/matrix-react-sdk/src/components/structures/SpaceRoomView.tsx index edc857edaf..a71970c08d 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/structures/SpaceRoomView.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/structures/SpaceRoomView.tsx @@ -60,7 +60,6 @@ import { defaultRoomsRenderer, } from "../views/dialogs/AddExistingToSpaceDialog"; import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton"; -import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import ErrorBoundary from "../views/elements/ErrorBoundary"; import Field from "../views/elements/Field"; import RoomFacePile from "../views/elements/RoomFacePile"; @@ -248,7 +247,7 @@ const SpaceLanding: React.FC<{ space: Room }> = ({ space }) => { let settingsButton; if (shouldShowSpaceSettings(space)) { settingsButton = ( - { showSpaceSettings(space); diff --git a/linked-dependencies/matrix-react-sdk/src/components/structures/TabbedView.tsx b/linked-dependencies/matrix-react-sdk/src/components/structures/TabbedView.tsx index 61e34d2d0d..c745d9cf5d 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/structures/TabbedView.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/structures/TabbedView.tsx @@ -1,7 +1,7 @@ /* Copyright 2017 Travis Ralston Copyright 2019 New Vector Ltd -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020, 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. @@ -18,7 +18,6 @@ limitations under the License. import * as React from "react"; import classNames from "classnames"; -import { logger } from "matrix-js-sdk/src/logger"; import { _t, TranslationKey } from "../../languageHandler"; import AutoHideScrollbar from "./AutoHideScrollbar"; @@ -47,138 +46,153 @@ export class Tab { ) {} } +export function useActiveTabWithDefault( + tabs: NonEmptyArray>, + defaultTabID: T, + initialTabID?: T, +): [T, (tabId: T) => void] { + const [activeTabId, setActiveTabId] = React.useState( + initialTabID && tabs.some((t) => t.id === initialTabID) ? initialTabID : defaultTabID, + ); + + return [activeTabId, setActiveTabId]; +} + export enum TabLocation { LEFT = "left", TOP = "top", } -interface IProps { - tabs: NonEmptyArray>; - initialTabId?: T; - tabLocation: TabLocation; - onChange?: (tabId: T) => void; - screenName?: ScreenName; +interface ITabPanelProps { + tab: Tab; } -interface IState { - activeTabId: T; +function domIDForTabID(tabId: string): string { + return `mx_tabpanel_${tabId}`; } -export default class TabbedView extends React.Component, IState> { - public constructor(props: IProps) { - super(props); +function TabPanel({ tab }: ITabPanelProps): JSX.Element { + return ( +
+ {tab.body} +
+ ); +} - const initialTabIdIsValid = props.tabs.find((tab) => tab.id === props.initialTabId); - this.state = { - activeTabId: initialTabIdIsValid ? props.initialTabId! : props.tabs[0].id, - }; - } +interface ITabLabelProps { + tab: Tab; + isActive: boolean; + onClick: () => void; +} - public static defaultProps = { - tabLocation: TabLocation.LEFT, - }; +function TabLabel({ tab, isActive, onClick }: ITabLabelProps): JSX.Element { + const classes = classNames("mx_TabbedView_tabLabel", { + mx_TabbedView_tabLabel_active: isActive, + }); - private getTabById(id: T): Tab | undefined { - return this.props.tabs.find((tab) => tab.id === id); + let tabIcon: JSX.Element | undefined; + if (tab.icon) { + tabIcon = ; } + const id = domIDForTabID(tab.id); + + const label = _t(tab.label); + return ( + + {tabIcon} + + {label} + + + ); +} + +interface IProps { + // An array of objects representign tabs that the tabbed view will display. + tabs: NonEmptyArray>; + // The ID of the tab to show + activeTabId: T; + // The location of the tabs, dictating the layout of the TabbedView. + tabLocation?: TabLocation; + // A callback that is called when the active tab should change + onChange: (tabId: T) => void; + // The screen name to report to Posthog. + screenName?: ScreenName; /** - * Shows the given tab - * @param {Tab} tab the tab to show - * @private + * If true, the layout of the tabbed view will be responsive to the viewport size (eg, just showing icons + * instead of names of tabs). + * Only applies if `tabLocation === TabLocation.LEFT`. + * Default: false. */ - private setActiveTab(tab: Tab): void { - // make sure this tab is still in available tabs - if (!!this.getTabById(tab.id)) { - if (this.props.onChange) this.props.onChange(tab.id); - this.setState({ activeTabId: tab.id }); - } else { - logger.error("Could not find tab " + tab.label + " in tabs"); - } - } - - private renderTabLabel(tab: Tab): JSX.Element { - const isActive = this.state.activeTabId === tab.id; - const classes = classNames("mx_TabbedView_tabLabel", { - mx_TabbedView_tabLabel_active: isActive, - }); - - let tabIcon: JSX.Element | undefined; - if (tab.icon) { - tabIcon = ; - } - - const onClickHandler = (): void => this.setActiveTab(tab); - const id = this.getTabId(tab); - - const label = _t(tab.label); - return ( - - {tabIcon} - - {label} - - - ); - } + responsive?: boolean; +} - private getTabId(tab: Tab): string { - return `mx_tabpanel_${tab.id}`; - } +/** + * A tabbed view component. Given objects representing content with titles, displays + * them in a tabbed view where the user can select which one of the items to view at once. + */ +export default function TabbedView(props: IProps): JSX.Element { + const tabLocation = props.tabLocation ?? TabLocation.LEFT; - private renderTabPanel(tab: Tab): React.ReactNode { - const id = this.getTabId(tab); - return ( -
- {tab.body} -
- ); - } + const getTabById = (id: T): Tab | undefined => { + return props.tabs.find((tab) => tab.id === id); + }; - public render(): React.ReactNode { - const labels = this.props.tabs.map((tab) => this.renderTabLabel(tab)); - const tab = this.getTabById(this.state.activeTabId); - const panel = tab ? this.renderTabPanel(tab) : null; - - const tabbedViewClasses = classNames({ - mx_TabbedView: true, - mx_TabbedView_tabsOnLeft: this.props.tabLocation == TabLocation.LEFT, - mx_TabbedView_tabsOnTop: this.props.tabLocation == TabLocation.TOP, - }); - - const screenName = tab?.screenName ?? this.props.screenName; - - return ( -
- {screenName && } - - {({ onKeyDownHandler }) => ( -
    - {labels} -
- )} -
- {panel} -
- ); - } + const labels = props.tabs.map((tab) => ( + props.onChange(tab.id)} + /> + )); + const tab = getTabById(props.activeTabId); + const panel = tab ? : null; + + const tabbedViewClasses = classNames({ + mx_TabbedView: true, + mx_TabbedView_tabsOnLeft: tabLocation == TabLocation.LEFT, + mx_TabbedView_tabsOnTop: tabLocation == TabLocation.TOP, + mx_TabbedView_responsive: props.responsive, + }); + + const screenName = tab?.screenName ?? props.screenName; + + return ( +
+ {screenName && } + + {({ onKeyDownHandler }) => ( +
    + {labels} +
+ )} +
+ {panel} +
+ ); } diff --git a/linked-dependencies/matrix-react-sdk/src/components/structures/TimelinePanel.tsx b/linked-dependencies/matrix-react-sdk/src/components/structures/TimelinePanel.tsx index ba3c4d203b..45198fff74 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/structures/TimelinePanel.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/structures/TimelinePanel.tsx @@ -39,8 +39,7 @@ import { ThreadEvent, ReceiptType, } from "matrix-js-sdk/src/matrix"; -import { KnownMembership, Membership } from "matrix-js-sdk/src/types"; -import { debounce, findLastIndex, throttle } from "lodash"; +import { debounce, findLastIndex } from "lodash"; import { logger } from "matrix-js-sdk/src/logger"; import SettingsStore from "../../settings/SettingsStore"; @@ -54,7 +53,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"; @@ -177,9 +175,6 @@ interface IState { // track whether our room timeline is loading timelineLoading: boolean; - // the index of the first event that is to be shown - firstVisibleEventIndex: number; - // canBackPaginate == false may mean: // // * we haven't (successfully) loaded the timeline yet, or: @@ -297,7 +292,6 @@ class TimelinePanel extends React.Component { events: [], liveEvents: [], timelineLoading: true, - firstVisibleEventIndex: 0, canBackPaginate: false, canForwardPaginate: false, readMarkerVisible: true, @@ -569,12 +563,11 @@ class TimelinePanel extends React.Component { this.overlayTimelineWindow!.unpaginate(overlayCount, backwards); } - const { events, liveEvents, firstVisibleEventIndex } = this.getEvents(); + const { events, liveEvents } = this.getEvents(); this.buildLegacyCallEventGroupers(events); this.setState({ events, liveEvents, - firstVisibleEventIndex, }); // We can now paginate in the unpaginated direction @@ -618,11 +611,6 @@ class TimelinePanel extends React.Component { return Promise.resolve(false); } - if (backwards && this.state.firstVisibleEventIndex !== 0) { - debuglog("won't", dir, "paginate past first visible event"); - return Promise.resolve(false); - } - debuglog("Initiating paginate; backwards:" + backwards); this.setState({ [paginatingKey]: true } as Pick); @@ -637,15 +625,14 @@ class TimelinePanel extends React.Component { debuglog("paginate complete backwards:" + backwards + "; success:" + r); - const { events, liveEvents, firstVisibleEventIndex } = this.getEvents(); + const { events, liveEvents } = this.getEvents(); this.buildLegacyCallEventGroupers(events); const newState = { [paginatingKey]: false, [canPaginateKey]: r, events, liveEvents, - firstVisibleEventIndex, - } as Pick; + } as Pick; // moving the window in this direction may mean that we can now // paginate in the other where we previously could not. @@ -663,11 +650,9 @@ class TimelinePanel extends React.Component { // itself into the right place return new Promise((resolve) => { this.setState(newState, () => { - // we can continue paginating in the given direction if: - // - timelineWindow.paginate says we can - // - we're paginating forwards, or we won't be trying to - // paginate backwards past the first visible event - resolve(r && (!backwards || firstVisibleEventIndex === 0)); + // we can continue paginating in the given direction if + // timelineWindow.paginate says we can + resolve(r); }); }); }); @@ -783,14 +768,13 @@ class TimelinePanel extends React.Component { return; } - const { events, liveEvents, firstVisibleEventIndex } = this.getEvents(); + const { events, liveEvents } = this.getEvents(); this.buildLegacyCallEventGroupers(events); const lastLiveEvent = liveEvents[liveEvents.length - 1]; const updatedState: Partial = { events, liveEvents, - firstVisibleEventIndex, }; let callRMUpdated = false; @@ -968,8 +952,6 @@ class TimelinePanel extends React.Component { if (!this.state.events.includes(ev)) return; - this.recheckFirstVisibleEventIndex(); - // Need to update as we don't display event tiles for events that // haven't yet been decrypted. The event will have just been updated // in place so we just need to re-render. @@ -985,17 +967,6 @@ class TimelinePanel extends React.Component { this.setState({ clientSyncState }); }; - private recheckFirstVisibleEventIndex = throttle( - (): void => { - const firstVisibleEventIndex = this.checkForPreJoinUISI(this.state.events); - if (firstVisibleEventIndex !== this.state.firstVisibleEventIndex) { - this.setState({ firstVisibleEventIndex }); - } - }, - 500, - { leading: true, trailing: true }, - ); - private readMarkerTimeout(readMarkerPosition: number | null): number { return readMarkerPosition === 0 ? this.context?.readMarkerInViewThresholdMs ?? this.state.readMarkerInViewThresholdMs @@ -1722,7 +1693,7 @@ class TimelinePanel extends React.Component { } // get the list of events from the timeline windows and the pending event list - private getEvents(): Pick { + private getEvents(): Pick { const mainEvents = this.timelineWindow!.getEvents(); let overlayEvents = this.overlayTimelineWindow?.getEvents() ?? []; if (this.props.overlayTimelineSetFilter !== undefined) { @@ -1754,17 +1725,11 @@ 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); - }); - - const firstVisibleEventIndex = this.checkForPreJoinUISI(events); + // 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]); + } // Hold onto the live events separately. The read receipt and read marker // should use this list, so that they don't advance into pending events. @@ -1793,87 +1758,9 @@ class TimelinePanel extends React.Component { return { events, liveEvents, - firstVisibleEventIndex, }; } - /** - * Check for undecryptable messages that were sent while the user was not in - * the room. - * - * @param {Array} events The timeline events to check - * - * @return {Number} The index within `events` of the event after the most recent - * undecryptable event that was sent while the user was not in the room. If no - * such events were found, then it returns 0. - */ - private checkForPreJoinUISI(events: MatrixEvent[]): number { - const cli = MatrixClientPeg.safeGet(); - const room = this.props.timelineSet.room; - - const isThreadTimeline = [TimelineRenderingType.Thread, TimelineRenderingType.ThreadsList].includes( - this.context.timelineRenderingType, - ); - if (events.length === 0 || !room || !cli.isRoomEncrypted(room.roomId) || isThreadTimeline) { - logger.debug("checkForPreJoinUISI: showing all messages, skipping check"); - return 0; - } - - const userId = cli.getSafeUserId(); - - // get the user's membership at the last event by getting the timeline - // that the event belongs to, and traversing the timeline looking for - // that event, while keeping track of the user's membership - let i = events.length - 1; - let userMembership: Membership = KnownMembership.Leave; - for (; i >= 0; i--) { - const timeline = this.props.timelineSet.getTimelineForEvent(events[i].getId()!); - if (!timeline) { - // Somehow, it seems to be possible for live events to not have - // a timeline, even though that should not happen. :( - // https://github.com/vector-im/element-web/issues/12120 - logger.warn( - `Event ${events[i].getId()} in room ${room.roomId} is live, ` + `but it does not have a timeline`, - ); - continue; - } - - userMembership = - timeline.getState(EventTimeline.FORWARDS)?.getMember(userId)?.membership ?? KnownMembership.Leave; - const timelineEvents = timeline.getEvents(); - for (let j = timelineEvents.length - 1; j >= 0; j--) { - const event = timelineEvents[j]; - if (event.getId() === events[i].getId()) { - break; - } else if (event.getStateKey() === userId && event.getType() === EventType.RoomMember) { - userMembership = event.getPrevContent().membership || KnownMembership.Leave; - } - } - break; - } - - // now go through the rest of the events and find the first undecryptable - // one that was sent when the user wasn't in the room - for (; i >= 0; i--) { - const event = events[i]; - if (event.getStateKey() === userId && event.getType() === EventType.RoomMember) { - userMembership = event.getPrevContent().membership || KnownMembership.Leave; - } else if ( - userMembership === KnownMembership.Leave && - (event.isDecryptionFailure() || event.isBeingDecrypted()) - ) { - // reached an undecryptable message when the user wasn't in the room -- don't try to load any more - // Note: for now, we assume that events that are being decrypted are - // not decryptable - we will be called once more when it is decrypted. - logger.debug("checkForPreJoinUISI: reached a pre-join UISI at index ", i); - return i + 1; - } - } - - logger.debug("checkForPreJoinUISI: did not find pre-join UISI"); - return 0; - } - private indexForEventId(evId: string | null): number | null { if (evId === null) { return null; @@ -2124,9 +2011,7 @@ class TimelinePanel extends React.Component { // the HS and fetch the latest events, so we are effectively forward paginating. const forwardPaginating = this.state.forwardPaginating || ["PREPARED", "CATCHUP"].includes(this.state.clientSyncState!); - const events = this.state.firstVisibleEventIndex - ? this.state.events.slice(this.state.firstVisibleEventIndex) - : this.state.events; + const events = this.state.events; return ( { highlightedEventId={this.props.highlightedEventId} readMarkerEventId={this.state.readMarkerEventId} readMarkerVisible={this.state.readMarkerVisible} - canBackPaginate={this.state.canBackPaginate && this.state.firstVisibleEventIndex === 0} + canBackPaginate={this.state.canBackPaginate} showUrlPreview={this.props.showUrlPreview} showReadReceipts={this.props.showReadReceipts} ourUserId={MatrixClientPeg.safeGet().getSafeUserId()} diff --git a/linked-dependencies/matrix-react-sdk/src/components/structures/auth/forgot-password/CheckEmail.tsx b/linked-dependencies/matrix-react-sdk/src/components/structures/auth/forgot-password/CheckEmail.tsx index d9d9c4ca80..af3e6cf216 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/structures/auth/forgot-password/CheckEmail.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/structures/auth/forgot-password/CheckEmail.tsx @@ -66,7 +66,7 @@ export const CheckEmail: React.FC = ({
{_t("auth|check_email_resend_prompt")} - + {_t("action|resend")} diff --git a/linked-dependencies/matrix-react-sdk/src/components/structures/auth/forgot-password/VerifyEmailModal.tsx b/linked-dependencies/matrix-react-sdk/src/components/structures/auth/forgot-password/VerifyEmailModal.tsx index 11ede00340..fd461ddc5d 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/structures/auth/forgot-password/VerifyEmailModal.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/structures/auth/forgot-password/VerifyEmailModal.tsx @@ -65,7 +65,7 @@ export const VerifyEmailModal: React.FC = ({
{_t("auth|check_email_resend_prompt")} - + {_t("action|resend")} diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/audio_messages/PlayPauseButton.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/audio_messages/PlayPauseButton.tsx index 63f266fbf7..c49fd2e74c 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/audio_messages/PlayPauseButton.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/audio_messages/PlayPauseButton.tsx @@ -17,14 +17,11 @@ limitations under the License. import React, { ComponentProps, ReactNode } from "react"; import classNames from "classnames"; -import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import { _t } from "../../../languageHandler"; import { Playback, PlaybackState } from "../../../audio/Playback"; +import AccessibleButton from "../elements/AccessibleButton"; -type Props = Omit< - ComponentProps, - "title" | "onClick" | "disabled" | "element" | "ref" -> & { +type Props = Omit, "title" | "onClick" | "disabled" | "element" | "ref"> & { // Playback instance to manipulate. Cannot change during the component lifecycle. playback: Playback; @@ -61,7 +58,7 @@ export default class PlayPauseButton extends React.PureComponent { }); return ( - ; @@ -34,13 +39,17 @@ interface IProps { confirmationDigits?: string; } +// n.b MSC3886/MSC3903/MSC3906 that this is based on are now closed. +// However, we want to keep this implementation around for some time. +// TODO: define an end-of-life date for this implementation. + /** * A component that implements the UI for sign in and E2EE set up with a QR code. * * 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 +81,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"); - break; - case RendezvousFailureReason.InvalidCode: - cancellationMessage = _t("auth|qr_code_login|error_invalid_scanned_code"); + 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.UnsupportedAlgorithm: - cancellationMessage = _t("auth|qr_code_login|error_device_unsupported"); + + 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.UserDeclined: - cancellationMessage = _t("auth|qr_code_login|error_request_declined"); + + case LegacyRendezvousFailureReason.Expired: + title = _t("auth|qr_code_login|error_expired_title"); + message = _t("auth|qr_code_login|error_expired"); break; - case RendezvousFailureReason.OtherDeviceAlreadySignedIn: - cancellationMessage = _t("auth|qr_code_login|error_device_already_signed_in"); + + 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.OtherDeviceNotSignedIn: - cancellationMessage = _t("auth|qr_code_login|error_device_not_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.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 +196,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 +258,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/linked-dependencies/matrix-react-sdk/src/components/views/avatars/DecoratedRoomAvatar.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/avatars/DecoratedRoomAvatar.tsx index dd83d1e969..7e85588907 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/avatars/DecoratedRoomAvatar.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/avatars/DecoratedRoomAvatar.tsx @@ -255,7 +255,7 @@ export default class DecoratedRoomAvatar extends React.PureComponent {icon && ( - + {icon} )} diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/BaseDialog.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/BaseDialog.tsx index 1b160150f7..66f6c9e095 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/BaseDialog.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/BaseDialog.tsx @@ -53,7 +53,7 @@ interface IProps { "top"?: React.ReactNode; // Title for the dialog. - "title"?: JSX.Element | string; + "title"?: React.ReactNode; // Specific aria label to use, if not provided will set aria-labelledBy to mx_Dialog_title "aria-label"?: string; diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/ForwardDialog.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/ForwardDialog.tsx index d59e23fe4c..7ff95edce3 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/ForwardDialog.tsx +++ b/linked-dependencies/matrix-react-sdk/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/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/InviteDialog.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/InviteDialog.tsx index 2b3c7af8db..bb81d7a05f 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/InviteDialog.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/InviteDialog.tsx @@ -1536,9 +1536,9 @@ export default class InviteDialog extends React.PureComponent - tabs={tabs} - initialTabId={this.state.currentTabId} + activeTabId={this.state.currentTabId} tabLocation={TabLocation.TOP} onChange={this.onTabChange} /> diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/RoomSettingsDialog.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/RoomSettingsDialog.tsx index a58cef95a7..213ee94aca 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/RoomSettingsDialog.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/RoomSettingsDialog.tsx @@ -56,11 +56,12 @@ export const enum RoomSettingsTab { interface IProps { roomId: string; onFinished: (success?: boolean) => void; - initialTabId?: string; + initialTabId?: RoomSettingsTab; } interface IState { room: Room; + activeTabId: RoomSettingsTab; } class RoomSettingsDialog extends React.Component { @@ -70,7 +71,7 @@ class RoomSettingsDialog extends React.Component { super(props); const room = this.getRoom(); - this.state = { room }; + this.state = { room, activeTabId: props.initialTabId || RoomSettingsTab.General }; } public componentDidMount(): void { @@ -128,6 +129,10 @@ class RoomSettingsDialog extends React.Component { if (event.getType() === EventType.RoomJoinRules) this.forceUpdate(); }; + private onTabChange = (tabId: RoomSettingsTab): void => { + this.setState({ activeTabId: tabId }); + }; + private getTabs(): NonEmptyArray> { const tabs: Tab[] = []; @@ -246,8 +251,9 @@ class RoomSettingsDialog extends React.Component {
    diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/SlidingSyncOptionsDialog.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/SlidingSyncOptionsDialog.tsx deleted file mode 100644 index 958c8d0876..0000000000 --- a/linked-dependencies/matrix-react-sdk/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/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/SpacePreferencesDialog.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/SpacePreferencesDialog.tsx index f943fc7d3c..881a655076 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/SpacePreferencesDialog.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/SpacePreferencesDialog.tsx @@ -33,7 +33,6 @@ import SettingsSubsection, { SettingsSubsectionText } from "../settings/shared/S interface IProps { space: Room; - initialTabId?: SpacePreferenceTab; onFinished(): void; } @@ -68,7 +67,7 @@ const SpacePreferencesAppearanceTab: React.FC> = ({ space ); }; -const SpacePreferencesDialog: React.FC = ({ space, initialTabId, onFinished }) => { +const SpacePreferencesDialog: React.FC = ({ space, onFinished }) => { const tabs: NonEmptyArray> = [ new Tab( SpacePreferenceTab.Appearance, @@ -90,7 +89,7 @@ const SpacePreferencesDialog: React.FC = ({ space, initialTabId, onFinis
    - + {}} />
    ); diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/SpaceSettingsDialog.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/SpaceSettingsDialog.tsx index 0318e1af62..016307d899 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/SpaceSettingsDialog.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/SpaceSettingsDialog.tsx @@ -82,6 +82,8 @@ const SpaceSettingsDialog: React.FC = ({ matrixClient: cli, space, onFin ].filter(Boolean) as NonEmptyArray>; }, [cli, space, onFinished]); + const [activeTabId, setActiveTabId] = React.useState(SpaceSettingsTab.General); + return ( = ({ matrixClient: cli, space, onFin fixedWidth={false} >
    - +
    ); diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/UserSettingsDialog.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/UserSettingsDialog.tsx index 820617ae96..bb97b36fc9 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/UserSettingsDialog.tsx +++ b/linked-dependencies/matrix-react-sdk/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. @@ -17,10 +17,10 @@ limitations under the License. import React from "react"; -import TabbedView, { Tab } from "../../structures/TabbedView"; +import TabbedView, { Tab, useActiveTabWithDefault } 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,43 @@ interface IProps { onFinished(): void; } -interface IState { - mjolnirEnabled: boolean; -} - -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)); +function titleForTabID(tabId: UserTab): React.ReactNode { + const subs = { + strong: (sub: string) => {sub}, + }; + switch (tabId) { + case UserTab.General: + return _t("settings|general|dialog_title", undefined, subs); + case UserTab.SessionManager: + return _t("settings|sessions|dialog_title", undefined, subs); + case UserTab.Appearance: + return _t("settings|appearance|dialog_title", undefined, subs); + case UserTab.Notifications: + return _t("settings|notifications|dialog_title", undefined, subs); + case UserTab.Preferences: + return _t("settings|preferences|dialog_title", undefined, subs); + case UserTab.Keyboard: + return _t("settings|keyboard|dialog_title", undefined, subs); + case UserTab.Sidebar: + return _t("settings|sidebar|dialog_title", undefined, subs); + case UserTab.Voice: + return _t("settings|voip|dialog_title", undefined, subs); + case UserTab.Security: + return _t("settings|security|dialog_title", undefined, subs); + case UserTab.Labs: + return _t("settings|labs|dialog_title", undefined, subs); + case UserTab.Mjolnir: + return _t("settings|labs_mjolnir|dialog_title", undefined, subs); + case UserTab.Help: + return _t("setting|help_about|dialog_title", undefined, subs); } +} - 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 }); - }; +export default function UserSettingsDialog(props: IProps): JSX.Element { + const voipEnabled = useSettingValue(UIFeature.Voip); + const mjolnirEnabled = useSettingValue("feature_mjolnir"); - private getTabs(): NonEmptyArray> { + const getTabs = (): NonEmptyArray> => { const tabs: Tab[] = []; tabs.push( @@ -80,7 +89,7 @@ export default class UserSettingsDialog extends React.Component UserTab.General, _td("common|general"), "mx_UserSettingsDialog_settingsIcon", - , + , "UserSettingsGeneral", ), ); @@ -90,7 +99,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 +125,7 @@ export default class UserSettingsDialog extends React.Component UserTab.Preferences, _td("common|preferences"), "mx_UserSettingsDialog_preferencesIcon", - , + , "UserSettingsPreferences", ), ); @@ -140,7 +148,7 @@ export default class UserSettingsDialog extends React.Component ), ); - if (SettingsStore.getValue(UIFeature.Voip)) { + if (voipEnabled) { tabs.push( new Tab( UserTab.Voice, @@ -157,11 +165,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 +181,7 @@ export default class UserSettingsDialog extends React.Component ), ); } - if (this.state.mjolnirEnabled) { + if (mjolnirEnabled) { tabs.push( new Tab( UserTab.Mjolnir, @@ -195,29 +203,31 @@ 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. - - -
    - -
    -
    -
    - ); - } + const [activeTabId, setActiveTabId] = useActiveTabWithDefault(getTabs(), UserTab.General, props.initialTabId); + + 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/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/devtools/RoomNotifications.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/devtools/RoomNotifications.tsx index 5d03ee7c3a..397db1fa4b 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/devtools/RoomNotifications.tsx +++ b/linked-dependencies/matrix-react-sdk/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/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/spotlight/SpotlightDialog.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/spotlight/SpotlightDialog.tsx index ee42a59221..2ac7681afa 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/spotlight/SpotlightDialog.tsx +++ b/linked-dependencies/matrix-react-sdk/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/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/spotlight/TooltipOption.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/spotlight/TooltipOption.tsx index 2233e762d4..0deb4b1311 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/dialogs/spotlight/TooltipOption.tsx +++ b/linked-dependencies/matrix-react-sdk/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}. * @@ -85,7 +88,24 @@ type Props = DynamicHtmlElementProps & /** * Event handler for button activation. Should be implemented exactly like a normal `onClick` handler. */ - onClick?: ((e: ButtonEvent) => void | Promise) | null; + onClick: ((e: ButtonEvent) => void | Promise) | null; + /** + * The tooltip to show on hover or focus. + */ + title?: TooltipProps["label"]; + /** + * The caption is a secondary text displayed under the `title` of the tooltip. + * Only valid when used in conjunction with `title`. + */ + caption?: TooltipProps["caption"]; + /** + * 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/linked-dependencies/matrix-react-sdk/src/components/views/elements/AccessibleTooltipButton.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/elements/AccessibleTooltipButton.tsx index 0af5cc9625..759643da1c 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/elements/AccessibleTooltipButton.tsx +++ b/linked-dependencies/matrix-react-sdk/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 { return ( - {name} +

    {name}

    {title ? titleSpacer : ""} {title} diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/elements/CopyableText.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/elements/CopyableText.tsx index 7e92b39564..5d9946d2c1 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/elements/CopyableText.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/elements/CopyableText.tsx @@ -20,8 +20,7 @@ import classNames from "classnames"; import { _t } from "../../../languageHandler"; import { copyPlaintext } from "../../../utils/strings"; -import { ButtonEvent } from "./AccessibleButton"; -import AccessibleTooltipButton from "./AccessibleTooltipButton"; +import AccessibleButton, { ButtonEvent } from "./AccessibleButton"; interface IProps { children?: React.ReactNode; @@ -53,11 +52,13 @@ const CopyableText: React.FC = ({ children, getTextToCopy, border = true return (
    {children} - { + if (!open) onHideTooltip(); + }} />
    ); diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/elements/DesktopCapturerSourcePicker.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/elements/DesktopCapturerSourcePicker.tsx index e4d52a8104..18cb9e6f4e 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/elements/DesktopCapturerSourcePicker.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/elements/DesktopCapturerSourcePicker.tsx @@ -85,8 +85,6 @@ export interface PickerIProps { onFinished(source?: DesktopCapturerSource): void; } -type TabId = "screen" | "window"; - export default class DesktopCapturerSourcePicker extends React.Component { public interval?: number; @@ -127,15 +125,15 @@ export default class DesktopCapturerSourcePicker extends React.Component { - this.setState({ selectedSource: undefined }); + private onTabChange = (tab: Tabs): void => { + this.setState({ selectedSource: undefined, selectedTab: tab }); }; private onCloseClick = (): void => { this.props.onFinished(); }; - private getTab(type: TabId, label: TranslationKey): Tab { + private getTab(type: Tabs, label: TranslationKey): Tab { const sources = this.state.sources .filter((source) => source.id.startsWith(type)) .map((source) => { @@ -153,9 +151,9 @@ export default class DesktopCapturerSourcePicker extends React.Component> = [ - this.getTab("screen", _td("voip|screenshare_monitor")), - this.getTab("window", _td("voip|screenshare_window")), + const tabs: NonEmptyArray> = [ + this.getTab(Tabs.Screens, _td("voip|screenshare_monitor")), + this.getTab(Tabs.Windows, _td("voip|screenshare_window")), ]; return ( @@ -164,7 +162,12 @@ export default class DesktopCapturerSourcePicker extends React.Component - + { } const zoomOutButton = ( - ); const zoomInButton = ( - {
    {zoomOutButton} {zoomInButton} - - - {contextMenuButton} - { // Tooltip are forced on the right for a more natural feel to them on info icons return ( - +
    {children} diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/elements/PersistedElement.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/elements/PersistedElement.tsx index 37e5fc26ad..99730ec344 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/elements/PersistedElement.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/elements/PersistedElement.tsx @@ -17,7 +17,6 @@ limitations under the License. import React, { MutableRefObject, ReactNode } from "react"; import ReactDOM from "react-dom"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; -import { TooltipProvider } from "@vector-im/compound-web"; import dis from "../../../dispatcher/dispatcher"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; @@ -177,11 +176,9 @@ export default class PersistedElement extends React.Component { private renderApp(): void { const content = ( - -
    - {this.props.children} -
    -
    +
    + {this.props.children} +
    ); diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/elements/Pill.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/elements/Pill.tsx index 9f332e29c3..52ad1e2b89 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/elements/Pill.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/elements/Pill.tsx @@ -151,7 +151,7 @@ export const Pill: React.FC = ({ type: propType, url, inMessage, room {isAnchor ? ( diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/elements/RoomTopic.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/elements/RoomTopic.tsx index fa9fc0fd34..f926ef5cf4 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/elements/RoomTopic.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/elements/RoomTopic.tsx @@ -36,6 +36,17 @@ interface IProps extends React.HTMLProps { room: Room; } +export function onRoomTopicLinkClick(e: React.MouseEvent): void { + const anchor = e.target as HTMLLinkElement; + const localHref = tryTransformPermalinkToLocalHref(anchor.href); + + if (localHref !== anchor.href) { + // it could be converted to a localHref -> therefore handle locally + e.preventDefault(); + window.location.hash = localHref; + } +} + export default function RoomTopic({ room, className, ...props }: IProps): JSX.Element { const client = useContext(MatrixClientContext); const ref = useRef(null); @@ -54,14 +65,7 @@ export default function RoomTopic({ room, className, ...props }: IProps): JSX.El return; } - const anchor = e.target as HTMLLinkElement; - const localHref = tryTransformPermalinkToLocalHref(anchor.href); - - if (localHref !== anchor.href) { - // it could be converted to a localHref -> therefore handle locally - e.preventDefault(); - window.location.hash = localHref; - } + onRoomTopicLinkClick(e); }, [props], ); diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/elements/SSOButtons.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/elements/SSOButtons.tsx index 829ffaf3c5..c0647e504f 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/elements/SSOButtons.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/elements/SSOButtons.tsx @@ -30,7 +30,6 @@ import { Signup } from "@matrix-org/analytics-events/types/typescript/Signup"; import PlatformPeg from "../../../PlatformPeg"; import AccessibleButton from "./AccessibleButton"; import { _t } from "../../../languageHandler"; -import AccessibleTooltipButton from "./AccessibleTooltipButton"; import { mediaFromMxc } from "../../../customisations/Media"; import { PosthogAnalytics } from "../../../PosthogAnalytics"; @@ -131,9 +130,9 @@ const SSOButton: React.FC = ({ if (mini) { // TODO fallback icon return ( - + {icon} - + ); } diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/elements/TextWithTooltip.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/elements/TextWithTooltip.tsx index 95f8608dae..b6a3bd0cd2 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/elements/TextWithTooltip.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/elements/TextWithTooltip.tsx @@ -33,7 +33,7 @@ export default class TextWithTooltip extends React.Component { const { className, children, tooltip, tooltipProps } = this.props; return ( - + {children} diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/elements/ToggleSwitch.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/elements/ToggleSwitch.tsx index 588374d17b..8e595ff234 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/elements/ToggleSwitch.tsx +++ b/linked-dependencies/matrix-react-sdk/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/linked-dependencies/matrix-react-sdk/src/components/views/messages/CallEvent.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/messages/CallEvent.tsx index e37217c422..c05b56563f 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/messages/CallEvent.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/messages/CallEvent.tsx @@ -28,13 +28,12 @@ import { import defaultDispatcher from "../../../dispatcher/dispatcher"; import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { Action } from "../../../dispatcher/actions"; -import type { AccessibleButtonKind, ButtonEvent } from "../elements/AccessibleButton"; +import AccessibleButton, { AccessibleButtonKind, ButtonEvent } from "../elements/AccessibleButton"; import MemberAvatar from "../avatars/MemberAvatar"; import { LiveContentSummary, LiveContentType } from "../rooms/LiveContentSummary"; import FacePile from "../elements/FacePile"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { CallDuration, SessionDuration } from "../voip/CallDuration"; -import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; const MAX_FACES = 8; @@ -78,15 +77,15 @@ const ActiveCallEvent = forwardRef(
    {call && } - {buttonText} - +
diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/messages/DecryptionFailureBody.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/messages/DecryptionFailureBody.tsx index b3fa398a2c..1e94e533cd 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/messages/DecryptionFailureBody.tsx +++ b/linked-dependencies/matrix-react-sdk/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,19 +14,36 @@ 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 TchapUrls from "../../../../../../src/tchap/util/TchapUrls"; // :TCHAP: better-text-for-locked-messages import ExternalLink from "../elements/ExternalLink"; // :TCHAP: better-text-for-locked-messages +import { LocalDeviceVerificationStateContext } from "../../../contexts/LocalDeviceVerificationStateContext"; -function getErrorMessage(mxEvent?: MatrixEvent): string { - return mxEvent?.isEncryptedDisabledForUnverifiedDevices - ? _t("timeline|decryption_failure_blocked") - // :TCHAP: better-text-for-locked-messages - : _t("threads|unable_to_decrypt"); - : _t( +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"); + } + // :TCHAP: better-text-for-locked-messages : return _t("timeline|decryption_failure|unable_to_decrypt"); + return _t( "threads|unable_to_decrypt_with_info_message", {}, { @@ -41,10 +58,11 @@ function getErrorMessage(mxEvent?: MatrixEvent): string { } // 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/linked-dependencies/matrix-react-sdk/src/components/views/messages/DownloadActionButton.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/messages/DownloadActionButton.tsx index 852c53f237..4105426bb5 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/messages/DownloadActionButton.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/messages/DownloadActionButton.tsx @@ -98,6 +98,7 @@ export default class DownloadActionButton extends React.PureComponent {spinner} diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/messages/LegacyCallEvent.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/messages/LegacyCallEvent.tsx index 48d69ae094..dd8ccb533b 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/messages/LegacyCallEvent.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/messages/LegacyCallEvent.tsx @@ -24,7 +24,6 @@ import MemberAvatar from "../avatars/MemberAvatar"; import LegacyCallEventGrouper, { LegacyCallEventGrouperEvent } from "../../structures/LegacyCallEventGrouper"; import AccessibleButton from "../elements/AccessibleButton"; import InfoTooltip, { InfoTooltipKind } from "../elements/InfoTooltip"; -import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import { formatPreciseDuration } from "../../../DateUtils"; import Clock from "../audio_messages/Clock"; @@ -118,7 +117,7 @@ export default class LegacyCallEvent extends React.PureComponent }); return ( - = ({ ref={button} onFocus={onFocus} tabIndex={isActive ? 0 : -1} + placement="left" > @@ -187,6 +188,7 @@ const ReactButton: React.FC = ({ mxEvent, reactions, onFocusC ref={button} onFocus={onFocus} tabIndex={isActive ? 0 : -1} + placement="left" > @@ -230,22 +232,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" >
@@ -401,6 +397,7 @@ export default class MessageActionBar extends React.PureComponent , @@ -414,6 +411,7 @@ export default class MessageActionBar extends React.PureComponent @@ -439,6 +437,7 @@ export default class MessageActionBar extends React.PureComponent , @@ -465,6 +464,7 @@ export default class MessageActionBar extends React.PureComponent , @@ -513,18 +513,7 @@ export default class MessageActionBar extends React.PureComponent -
- {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( {this.props.isQuoteExpanded ? : } , diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/messages/TextualBody.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/messages/TextualBody.tsx index a3bac0b9cd..7246f7db57 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/messages/TextualBody.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/messages/TextualBody.tsx @@ -17,7 +17,6 @@ limitations under the License. import React, { createRef, SyntheticEvent, MouseEvent } from "react"; import ReactDOM from "react-dom"; import { MsgType } from "matrix-js-sdk/src/matrix"; -import { TooltipProvider } from "@vector-im/compound-web"; import * as HtmlUtils from "../../../HtmlUtils"; import { formatDate } from "../../../DateUtils"; @@ -32,7 +31,6 @@ import { tooltipifyLinks, unmountTooltips } from "../../../utils/tooltipify"; import { IntegrationManagers } from "../../../integrations/IntegrationManagers"; import { isPermalinkHost, tryTransformPermalinkToLocalHref } from "../../../utils/permalinks/Permalinks"; import { copyPlaintext } from "../../../utils/strings"; -import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import UIStore from "../../../stores/UIStore"; import { Action } from "../../../dispatcher/actions"; import GenericTextContextMenu from "../context_menus/GenericTextContextMenu"; @@ -349,11 +347,7 @@ export default class TextualBody extends React.Component { const reason = node.getAttribute("data-mx-spoiler") ?? undefined; node.removeAttribute("data-mx-spoiler"); // we don't want to recurse - const spoiler = ( - - - - ); + const spoiler = ; ReactDOM.render(spoiler, spoilerContainer); node.parentNode?.replaceChild(spoilerContainer, node); @@ -527,22 +521,16 @@ export default class TextualBody extends React.Component { const date = this.props.mxEvent.replacingEventDate(); const dateString = date && formatDate(date); - const tooltip = ( -
-
{_t("timeline|edits|tooltip_title", { date: dateString })}
-
{_t("timeline|edits|tooltip_sub")}
-
- ); - return ( - {`(${_t("common|edited")})`} - +
); } diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/pips/WidgetPip.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/pips/WidgetPip.tsx index 4a0d5e6618..2ba9e39e25 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/pips/WidgetPip.tsx +++ b/linked-dependencies/matrix-react-sdk/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/linked-dependencies/matrix-react-sdk/src/components/views/polls/pollHistory/PollListItem.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/polls/pollHistory/PollListItem.tsx index 58a2909ea0..7115f3b6a1 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/polls/pollHistory/PollListItem.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/polls/pollHistory/PollListItem.tsx @@ -36,7 +36,7 @@ export const PollListItem: React.FC = ({ event, onClick }) => { const formattedDate = formatLocalDateShort(event.getTs()); return (
  • - +
    {formattedDate} diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/polls/pollHistory/PollListItemEnded.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/polls/pollHistory/PollListItemEnded.tsx index 657d940865..5da7772323 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/polls/pollHistory/PollListItemEnded.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/polls/pollHistory/PollListItemEnded.tsx @@ -99,7 +99,7 @@ export const PollListItemEnded: React.FC = ({ event, poll, onClick }) => return (
  • - +
    diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/right_panel/HeaderButton.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/right_panel/HeaderButton.tsx index 4fb8c1b513..bfe463c6bf 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/right_panel/HeaderButton.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/right_panel/HeaderButton.tsx @@ -21,9 +21,7 @@ limitations under the License. import React, { ReactNode } from "react"; import classNames from "classnames"; -import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; -import { ButtonEvent } from "../elements/AccessibleButton"; -import { Alignment } from "../elements/Tooltip"; +import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; interface IProps { // Whether this button is highlighted @@ -52,11 +50,11 @@ export default class HeaderButton extends React.Component { }); return ( - diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/right_panel/RoomSummaryCard.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/right_panel/RoomSummaryCard.tsx index 72cab396c0..54a6d8ecec 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/right_panel/RoomSummaryCard.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/right_panel/RoomSummaryCard.tsx @@ -14,9 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useCallback, useContext, useEffect, useMemo, useState } from "react"; +import React, { SyntheticEvent, useCallback, useContext, useEffect, useMemo, useState } from "react"; import classNames from "classnames"; -import { MenuItem, Tooltip, Separator, ToggleMenuItem, Text, Badge, Heading } from "@vector-im/compound-web"; +import { + MenuItem, + Tooltip, + Separator, + ToggleMenuItem, + Text, + Badge, + Heading, + IconButton, + Link, +} from "@vector-im/compound-web"; import { Icon as SearchIcon } from "@vector-im/compound-design-tokens/icons/search.svg"; import { Icon as FavouriteIcon } from "@vector-im/compound-design-tokens/icons/favourite.svg"; import { Icon as UserAddIcon } from "@vector-im/compound-design-tokens/icons/user-add.svg"; @@ -32,6 +42,7 @@ import { Icon as LockIcon } from "@vector-im/compound-design-tokens/icons/lock-s import { Icon as LockOffIcon } from "@vector-im/compound-design-tokens/icons/lock-off.svg"; import { Icon as PublicIcon } from "@vector-im/compound-design-tokens/icons/public.svg"; import { Icon as ErrorIcon } from "@vector-im/compound-design-tokens/icons/error.svg"; +import { Icon as ChevronDownIcon } from "@vector-im/compound-design-tokens/icons/chevron-down.svg"; import { EventType, JoinRule, Room, RoomStateEvent } from "matrix-js-sdk/src/matrix"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; @@ -49,7 +60,6 @@ import WidgetUtils from "../../../utils/WidgetUtils"; import { IntegrationManagers } from "../../../integrations/IntegrationManagers"; import SettingsStore from "../../../settings/SettingsStore"; import WidgetAvatar from "../avatars/WidgetAvatar"; -import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import WidgetStore, { IApp } from "../../../stores/WidgetStore"; import { E2EStatus } from "../../../utils/ShieldUtils"; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; @@ -75,6 +85,10 @@ import { canInviteTo } from "../../../utils/room/canInviteTo"; import { inviteToRoom } from "../../../utils/room/inviteToRoom"; import { useAccountData } from "../../../hooks/useAccountData"; import { useRoomState } from "../../../hooks/useRoomState"; +import { useTopic } from "../../../hooks/room/useTopic"; +import { Linkify, topicToHtml } from "../../../HtmlUtils"; +import { Box } from "../../utils/Box"; +import { onRoomTopicLinkClick } from "../elements/RoomTopic"; import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; // :TCHAP: tchap-room-icons @@ -187,18 +201,17 @@ const AppRow: React.FC = ({ app, room }) => { return (
    - {name} {subtitle} - + {canModifyWidget && ( = ({ app, room }) => { /> )} - - { PosthogTrackers.trackInteraction("WebRightPanelRoomInfoSettingsButton", ev); }; +const RoomTopic: React.FC> = ({ room }): JSX.Element | null => { + const [expanded, setExpanded] = useState(false); + + const topic = useTopic(room); + const body = topicToHtml(topic?.text, topic?.html); + + const onEditClick = (e: SyntheticEvent): void => { + e.preventDefault(); + e.stopPropagation(); + defaultDispatcher.dispatch({ action: "open_room_settings" }); + }; + + if (!body) { + return ( + + + + + {_t("right_panel|add_topic")} + + + + + ); + } + + const content = expanded ? {body} : body; + return ( + + + { + if (ev.target instanceof HTMLAnchorElement) { + onRoomTopicLinkClick(ev); + return; + } + setExpanded(!expanded); + }} + > + {content} + + setExpanded(!expanded)} + > + + + + {expanded && ( + + + + {_t("action|edit")} + + + + )} + + ); +}; + const RoomSummaryCard: React.FC = ({ room, permalinkCreator, onClose, onSearchClick }) => { const cli = useContext(MatrixClientContext); @@ -393,6 +484,8 @@ const RoomSummaryCard: React.FC = ({ room, permalinkCreator, onClose, on )} */} + + ); @@ -414,7 +507,7 @@ const RoomSummaryCard: React.FC = ({ room, permalinkCreator, onClose, on align="center" justify="space-between" > - + + {this.renderMenu()} {this.props.isMinimized ? null : badgeContainer} {this.props.isMinimized ? null : addRoomButton} diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/rooms/RoomTile.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/rooms/RoomTile.tsx index 61f865e9fc..aae949858e 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/rooms/RoomTile.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/rooms/RoomTile.tsx @@ -37,7 +37,6 @@ import NotificationBadge from "./NotificationBadge"; import { ActionPayload } from "../../../dispatcher/payloads"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; import { NotificationState, NotificationStateEvents } from "../../../stores/notifications/NotificationState"; -import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import { EchoChamber } from "../../../stores/local-echo/EchoChamber"; import { CachedRoomKey, RoomEchoChamber } from "../../../stores/local-echo/RoomEchoChamber"; import { PROPERTY_UPDATED } from "../../../stores/local-echo/GenericEchoChamber"; @@ -464,21 +463,11 @@ export class RoomTile extends React.PureComponent { ariaDescribedBy = messagePreviewId(this.props.room.roomId); } - const props: Partial> = {}; - let Button: React.ComponentType> = AccessibleButton; - if (this.props.isMinimized) { - Button = AccessibleTooltipButton; - props.title = name; - // force the tooltip to hide whilst we are showing the context menu - props.forceHide = !!this.state.generalMenuPosition; - } - return ( {({ onFocus, isActive, ref }) => ( - + )} diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/rooms/ThreadSummary.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/rooms/ThreadSummary.tsx index 60d7534ca7..2e03262bd7 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/rooms/ThreadSummary.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/rooms/ThreadSummary.tsx @@ -136,7 +136,7 @@ export const ThreadMessagePreview: React.FC = ({ thread, showDisp {lastReply.isDecryptionFailure() ? (
    { /* :TCHAP: better-text-for-locked-messages - {_t("threads|unable_to_decrypt")}*/} diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/rooms/VoiceRecordComposerTile.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/rooms/VoiceRecordComposerTile.tsx index 70cabb474c..1001def386 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -19,7 +19,6 @@ import { Room, IEventRelation, MatrixEvent } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { Optional } from "matrix-events-sdk"; -import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import { _t } from "../../../languageHandler"; import { RecordingState } from "../../../audio/VoiceRecording"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; @@ -44,6 +43,7 @@ import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import RoomContext from "../../../contexts/RoomContext"; import { IUpload, VoiceMessageRecording } from "../../../audio/VoiceMessageRecording"; import { createVoiceMessageContent } from "../../../utils/createVoiceMessageContent"; +import AccessibleButton from "../elements/AccessibleButton"; interface IProps { room: Room; @@ -271,7 +271,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent - {label} - {keyCombo && ( - - )} -
    - ); -} - -interface ButtonProps extends TooltipProps { +interface ButtonProps { icon: ReactNode; actionState: ActionState; onClick: MouseEventHandler; + label: string; + keyCombo?: KeyCombo; } function Button({ label, keyCombo, onClick, actionState, icon }: ButtonProps): JSX.Element { return ( - void} - title={label} + aria-label={label} className={classNames("mx_FormattingButtons_Button", { mx_FormattingButtons_active: actionState === "reversed", mx_FormattingButtons_Button_hover: actionState === "enabled", mx_FormattingButtons_disabled: actionState === "disabled", })} - tooltip={keyCombo && } - forceHide={actionState === "disabled"} - alignment={Alignment.Top} + title={actionState === "disabled" ? undefined : label} + caption={ + keyCombo && ( + + ) + } + placement="top" > {icon} - + ); } diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/settings/JoinRuleSettings.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/settings/JoinRuleSettings.tsx index 9f4bceb02a..0d7cac30d7 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/settings/JoinRuleSettings.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/settings/JoinRuleSettings.tsx @@ -15,14 +15,8 @@ limitations under the License. */ import React, { ReactNode, useEffect, useState } from "react"; -import { - IJoinRuleEventContent, - JoinRule, - RestrictedAllowType, - Room, - EventType, - Visibility, -} from "matrix-js-sdk/src/matrix"; +import { JoinRule, RestrictedAllowType, Room, EventType, Visibility } from "matrix-js-sdk/src/matrix"; +import { RoomJoinRulesEventContent } from "matrix-js-sdk/src/types"; import StyledRadioGroup, { IDefinition } from "../elements/StyledRadioGroup"; import { _t } from "../../../languageHandler"; @@ -72,7 +66,7 @@ const JoinRuleSettings: React.FC = ({ const disabled = !room.currentState.mayClientSendStateEvent(EventType.RoomJoinRules, cli); - const [content, setContent] = useLocalEcho( + const [content, setContent] = useLocalEcho( () => room.currentState.getStateEvents(EventType.RoomJoinRules, "")?.getContent(), (content) => cli.sendStateEvent(room.roomId, EventType.RoomJoinRules, content, ""), onError, @@ -391,7 +385,7 @@ const JoinRuleSettings: React.FC = ({ if (beforeJoinRule === joinRule && !restrictedAllowRoomIds) return; if (beforeChange && !(await beforeChange(joinRule))) return; - const newContent: IJoinRuleEventContent = { + const newContent: RoomJoinRulesEventContent = { join_rule: joinRule, }; diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx index cd3cd4ccec..317afdfca1 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx +++ b/linked-dependencies/matrix-react-sdk/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/linked-dependencies/matrix-react-sdk/src/components/views/settings/devices/FilteredDeviceListHeader.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/settings/devices/FilteredDeviceListHeader.tsx index 551b03c96c..cb7359d448 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/settings/devices/FilteredDeviceListHeader.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/settings/devices/FilteredDeviceListHeader.tsx @@ -40,7 +40,7 @@ const FilteredDeviceListHeader: React.FC = ({ return (
    {!isSelectDisabled && ( - + { - 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; +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; +} - // don't show anything if no method is available - if (!offerShowQr) { - return null; - } +const LoginWithQRSection: React.FC = ({ onShowQr, versions, capabilities, wellKnown }) => { + const offerShowQr = shouldShowQrLegacy(versions, wellKnown, capabilities); - return ( - -
    -

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

    - - - {_t("settings|sessions|sign_in_with_qr_button")} - -
    -
    - ); + // 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/linked-dependencies/matrix-react-sdk/src/components/views/settings/notifications/NotificationSettings2.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/settings/notifications/NotificationSettings2.tsx index a15fdd1d8b..84cd57882c 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/settings/notifications/NotificationSettings2.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/settings/notifications/NotificationSettings2.tsx @@ -123,7 +123,7 @@ export default function NotificationSettings2(): JSX.Element { )} )} - +
    { - heading: string | React.ReactNode; + heading?: string | React.ReactNode; children?: React.ReactNode; } +function renderHeading(heading: string | React.ReactNode | undefined): React.ReactNode | undefined { + switch (typeof heading) { + case "string": + return ( + + {heading} + + ); + case "undefined": + return undefined; + default: + return heading; + } +} + /** * A section of settings content * A SettingsTab may contain one or more SettingsSections @@ -43,13 +58,7 @@ export interface SettingsSectionProps extends HTMLAttributes { */ export const SettingsSection: React.FC = ({ className, heading, children, ...rest }) => (
    - {typeof heading === "string" ? ( - - {heading} - - ) : ( - <>{heading} - )} + {renderHeading(heading)}
    {children}
    ); diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index 9408274e9b..99f5a51c3b 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -32,7 +32,7 @@ import ThemeChoicePanel from "../../ThemeChoicePanel"; import ImageSizePanel from "../../ImageSizePanel"; import SettingsTab from "../SettingsTab"; import { SettingsSection } from "../../shared/SettingsSection"; -import SettingsSubsection, { SettingsSubsectionText } from "../../shared/SettingsSubsection"; +import SettingsSubsection from "../../shared/SettingsSubsection"; import MatrixClientContext from "../../../../../contexts/MatrixClientContext"; interface IProps {} @@ -152,12 +152,9 @@ export default class AppearanceUserSettingsTab extends React.Component - - {_t("settings|appearance|subheading", { brand })} + - + {this.renderAccountSection()} {this.renderLanguageSection()} diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx index c8221b2df4..2bf2c0f604 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx @@ -264,7 +264,7 @@ export default class HelpUserSettingsTab extends React.Component return ( - + {bugReportingSection} diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/settings/tabs/user/KeyboardUserSettingsTab.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/settings/tabs/user/KeyboardUserSettingsTab.tsx index 7a856e0627..ea3a75e8f3 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/settings/tabs/user/KeyboardUserSettingsTab.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/settings/tabs/user/KeyboardUserSettingsTab.tsx @@ -73,7 +73,7 @@ const KeyboardShortcutSection: React.FC = ({ cate const KeyboardUserSettingsTab: React.FC = () => { return ( - + {visibleCategories.map(([categoryName, category]) => { return ( diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx index 7ec29d4366..29466fc57e 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx @@ -254,7 +254,7 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState> return ( - + {_t("labs_mjolnir|advanced_warning")}

    {_t("labs_mjolnir|explainer_1", { brand }, { code: (s) => {s} })}

    diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/settings/tabs/user/NotificationUserSettingsTab.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/settings/tabs/user/NotificationUserSettingsTab.tsx index 0a00c32ca1..841babf979 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/settings/tabs/user/NotificationUserSettingsTab.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/settings/tabs/user/NotificationUserSettingsTab.tsx @@ -16,7 +16,6 @@ limitations under the License. import React from "react"; -import { _t } from "../../../../../languageHandler"; import { Features } from "../../../../../settings/Settings"; import SettingsStore from "../../../../../settings/SettingsStore"; import Notifications from "../../Notifications"; @@ -33,7 +32,7 @@ export default class NotificationUserSettingsTab extends React.Component { {newNotificationSettingsEnabled ? ( ) : ( - + )} diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx index 6758519eaf..6df2a1a03c 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx @@ -145,7 +145,7 @@ export default class PreferencesUserSettingsTab extends React.Component - + {roomListSettings.length > 0 && ( {this.renderGroup(roomListSettings)} diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/settings/tabs/user/SessionManagerTab.tsx index 61c8e85f8d..ec1d658b5b 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/linked-dependencies/matrix-react-sdk/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"; @@ -283,7 +284,13 @@ const SessionManagerTab: React.FC = () => { return ( - + + { /> )} -
    ); diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/settings/tabs/user/SidebarUserSettingsTab.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/settings/tabs/user/SidebarUserSettingsTab.tsx index a000e208eb..91aa63170d 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/settings/tabs/user/SidebarUserSettingsTab.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/settings/tabs/user/SidebarUserSettingsTab.tsx @@ -80,7 +80,7 @@ const SidebarUserSettingsTab: React.FC = () => { return ( - + { return ( - + {requestButton} {speakerDropdown} diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/spaces/QuickSettingsButton.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/spaces/QuickSettingsButton.tsx index 4702d9769b..63024c458b 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/spaces/QuickSettingsButton.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/spaces/QuickSettingsButton.tsx @@ -18,7 +18,6 @@ import React from "react"; import classNames from "classnames"; import { _t } from "../../../languageHandler"; -import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import ContextMenu, { alwaysAboveRightOf, ChevronFace, useContextMenu } from "../../structures/ContextMenu"; import AccessibleButton from "../elements/AccessibleButton"; import StyledCheckbox from "../elements/StyledCheckbox"; @@ -132,16 +131,16 @@ const QuickSettingsButton: React.FC<{ return ( <> - {!isPanelCollapsed ? _t("common|settings") : null} - + {contextMenu} diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/spaces/SpaceCreateMenu.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/spaces/SpaceCreateMenu.tsx index 2ded20912d..a690d3494d 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/spaces/SpaceCreateMenu.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/spaces/SpaceCreateMenu.tsx @@ -38,7 +38,6 @@ import { import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "../../../languageHandler"; -import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import ContextMenu, { ChevronFace } from "../../structures/ContextMenu"; import createRoom, { IOpts as ICreateOpts } from "../../../createRoom"; import MatrixClientContext, { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; @@ -310,7 +309,7 @@ const SpaceCreateMenu: React.FC<{ } else { body = ( - setVisibility(null)} title={_t("action|go_back")} diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/spaces/SpacePanel.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/spaces/SpacePanel.tsx index 429a18e134..a9b7093537 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/spaces/SpacePanel.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/spaces/SpacePanel.tsx @@ -34,7 +34,6 @@ import { _t } from "../../../languageHandler"; import { useContextMenu } from "../../structures/ContextMenu"; import SpaceCreateMenu from "./SpaceCreateMenu"; import { SpaceButton, SpaceItem } from "./SpaceTreeLevel"; -import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import { useEventEmitter, useEventEmitterState } from "../../../hooks/useEventEmitter"; import SpaceStore from "../../../stores/spaces/SpaceStore"; import { @@ -73,6 +72,7 @@ import { ALTERNATE_KEY_NAME } from "../../../accessibility/KeyboardShortcuts"; import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; import { UIComponent } from "../../../settings/UIFeature"; import { ThreadsActivityCentre } from "./threads-activity-centre/"; +import AccessibleButton from "../elements/AccessibleButton"; const useSpaces = (): [Room[], MetaSpace[], Room[], SpaceKey] => { const invites = useEventEmitterState(SpaceStore.instance, UPDATE_INVITED_SPACES, () => { @@ -368,8 +368,6 @@ const SpacePanel: React.FC = () => { } }); - const isThreadsActivityCentreEnabled = useSettingValue("threadsActivityCentre"); - return ( {({ onKeyDownHandler, onDragEndHandler }) => ( @@ -391,24 +389,18 @@ const SpacePanel: React.FC = () => { aria-label={_t("common|spaces")} > - setPanelCollapsed(!isPanelCollapsed)} title={isPanelCollapsed ? _t("action|expand") : _t("action|collapse")} - tooltip={ -
    -
    - {isPanelCollapsed ? _t("action|expand") : _t("action|collapse")} -
    -
    - {IS_MAC - ? "⌘ + ⇧ + D" - : _t(ALTERNATE_KEY_NAME[Key.CONTROL]) + - " + " + - _t(ALTERNATE_KEY_NAME[Key.SHIFT]) + - " + D"} -
    -
    + // TODO should use a kbd element for accessibility https://developer.mozilla.org/en-US/docs/Web/HTML/Element/kbd + caption={ + IS_MAC + ? "⌘ + ⇧ + D" + : _t(ALTERNATE_KEY_NAME[Key.CONTROL]) + + " + " + + _t(ALTERNATE_KEY_NAME[Key.SHIFT]) + + " + D" } />
    @@ -426,9 +418,8 @@ const SpacePanel: React.FC = () => { )} - {isThreadsActivityCentreEnabled && ( - - )} + + diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/spaces/SpaceTreeLevel.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/spaces/SpaceTreeLevel.tsx index 6b43399c28..253e7bf881 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/spaces/SpaceTreeLevel.tsx @@ -45,13 +45,12 @@ import { NotificationLevel } from "../../../stores/notifications/NotificationLev import { getKeyBindingsManager } from "../../../KeyBindingsManager"; import { NotificationState } from "../../../stores/notifications/NotificationState"; import SpaceContextMenu from "../context_menus/SpaceContextMenu"; -import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import { useRovingTabIndex } from "../../../accessibility/RovingTabIndex"; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; type ButtonProps = Omit< - ComponentProps>, - "title" | "onClick" | "size" + ComponentProps>, + "title" | "onClick" | "size" | "element" > & { space?: Room; spaceKey?: SpaceKey; @@ -143,17 +142,17 @@ export const SpaceButton = ({ const onClick = props.onClick ?? (selected && space ? viewSpaceHome : activateSpace); return ( - ({ {contextMenu}
    - + ); }; diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentre.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentre.tsx index 8b0b470f12..fc38326398 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentre.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentre.tsx @@ -37,6 +37,7 @@ import { KeyBindingAction } from "../../../../accessibility/KeyboardShortcuts"; import { ReleaseAnnouncement } from "../../../structures/ReleaseAnnouncement"; import { useIsReleaseAnnouncementOpen } from "../../../../hooks/useIsReleaseAnnouncementOpen"; import { useSettingValue } from "../../../../hooks/useSettings"; +import { ReleaseAnnouncementStore } from "../../../../stores/ReleaseAnnouncementStore"; interface ThreadsActivityCentreProps { /** @@ -82,13 +83,20 @@ export function ThreadsActivityCentre({ displayButtonLabel }: ThreadsActivityCen closeLabel={_t("action|ok")} > { + // Open the TAC after the release announcement closing + setOpen(true); + await ReleaseAnnouncementStore.instance.nextReleaseAnnouncement(); + }} /> ) : ( { // Track only when the Threads Activity Centre is opened @@ -96,7 +104,6 @@ export function ThreadsActivityCentre({ displayButtonLabel }: ThreadsActivityCen setOpen(newOpen); }} - side="right" title={_t("threads_activity_centre|header")} trigger={ { + /** + * Whether to disable the tooltip. + */ + disableTooltip?: boolean; /** * Display the `Threads` label next to the icon. */ @@ -40,12 +44,15 @@ interface ThreadsActivityCentreButtonProps extends ComponentProps( - function ThreadsActivityCentreButton({ displayLabel, notificationLevel, ...props }, ref): React.JSX.Element { + function ThreadsActivityCentreButton( + { displayLabel, notificationLevel, disableTooltip, ...props }, + ref, + ): React.JSX.Element { // Disable tooltip when the label is displayed - const openTooltip = displayLabel ? false : undefined; + const openTooltip = disableTooltip || displayLabel ? false : undefined; return ( - + , "title" | "element"> & { +type ButtonProps = Omit, "title" | "element"> & { state: boolean; onLabel?: string; offLabel?: string; + forceHide?: boolean; + onHover?: (hovering: boolean) => void; }; const LegacyCallViewToggleButton = forwardRef( - ({ children, state: isOn, className, onLabel, offLabel, ...props }, ref) => { + ({ children, state: isOn, className, onLabel, offLabel, forceHide, onHover, ...props }, ref) => { const classes = classNames("mx_LegacyCallViewButtons_button", className, { mx_LegacyCallViewButtons_button_on: isOn, mx_LegacyCallViewButtons_button_off: !isOn, }); + const title = forceHide ? undefined : isOn ? onLabel : offLabel; + return ( - {children} - + ); }, ); @@ -265,7 +268,7 @@ export default class LegacyCallViewButtons extends React.Component )} )} -
    ); diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/voip/LegacyCallView/LegacyCallViewHeader.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/voip/LegacyCallView/LegacyCallViewHeader.tsx index 33484eb3ce..c60b70c932 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/voip/LegacyCallView/LegacyCallViewHeader.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/voip/LegacyCallView/LegacyCallViewHeader.tsx @@ -19,7 +19,7 @@ import React from "react"; import { _t } from "../../../../languageHandler"; import RoomAvatar from "../../avatars/RoomAvatar"; -import AccessibleTooltipButton from "../../elements/AccessibleTooltipButton"; +import AccessibleButton from "../../elements/AccessibleButton"; interface LegacyCallControlsProps { onExpand?: () => void; @@ -31,21 +31,21 @@ const LegacyCallViewHeaderControls: React.FC = ({ onExp return (
    {onMaximize && ( - )} {onPin && ( - )} {onExpand && ( - { - 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/linked-dependencies/matrix-react-sdk/src/i18n/strings/cs.json b/linked-dependencies/matrix-react-sdk/src/i18n/strings/cs.json index eacf7b6762..3a8f2d9812 100644 --- a/linked-dependencies/matrix-react-sdk/src/i18n/strings/cs.json +++ b/linked-dependencies/matrix-react-sdk/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", @@ -2414,13 +2397,11 @@ "custom_theme_url": "URL adresa vlastního vzhledu", "font_size": "Velikost písma", "font_size_default": "%(fontSize)s (výchozí)", - "heading": "Přizpůsobte si vzhled aplikace", "image_size_default": "Výchozí", "image_size_large": "Velký", "layout_bubbles": "Bubliny zpráv", "layout_irc": "IRC (experimentální)", "match_system_theme": "Nastavit podle vzhledu systému", - "subheading": "Nastavení vzhledu působí jen v této relaci programu %(brand)s.", "timeline_image_size": "Velikost obrázku na časové ose", "use_high_contrast": "Použít vysoký kontrast" }, @@ -3148,12 +3129,10 @@ "my_threads_description": "Zobrazit všechna vlákna, kterých jste se zúčastnili", "open_thread": "Otevřít vlákno", "show_all_threads": "Zobrazit všechna vlákna", - "show_thread_filter": "Zobrazit:", - "unable_to_decrypt": "Nepodařilo se dešifrovat zprávu" + "show_thread_filter": "Zobrazit:" }, "threads_activity_centre": { - "header": "Aktivita vláken", - "no_rooms_with_unreads_threads": "Zatím nemáte místnosti s nepřečtenými vlákny." + "header": "Aktivita vláken" }, "time": { "about_day_ago": "před jedním dnem", @@ -3196,7 +3175,6 @@ }, "creation_summary_dm": "%(creator)s vytvořil(a) tuto přímou zprávu.", "creation_summary_room": "%(creator)s vytvořil(a) a nakonfiguroval(a) místnost.", - "decryption_failure_blocked": "Odesílatel vám zablokoval přijetí této zprávy", "disambiguated_profile": "%(displayName)s (%(matrixId)s)", "download_action_decrypting": "Dešifrování", "download_action_downloading": "Stahování", @@ -3205,7 +3183,6 @@ "tooltip_sub": "Klikněte pro zobrazení úprav", "tooltip_title": "Upraveno %(date)s" }, - "encrypted_historical_messages_unavailable": "Šifrované zprávy před tímto bodem nejsou k dispozici.", "error_no_renderer": "Tato událost nemohla být zobrazena", "error_rendering_message": "Tuto zprávu nelze načíst", "historical_messages_unavailable": "Dřívější zprávy nelze zobrazit", diff --git a/linked-dependencies/matrix-react-sdk/src/i18n/strings/de_DE.json b/linked-dependencies/matrix-react-sdk/src/i18n/strings/de_DE.json index 3fc5e49035..1d8beaa14c 100644 --- a/linked-dependencies/matrix-react-sdk/src/i18n/strings/de_DE.json +++ b/linked-dependencies/matrix-react-sdk/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", @@ -2387,13 +2372,11 @@ "custom_theme_success": "Design hinzugefügt!", "custom_theme_url": "URL des selbstdefinierten Designs", "font_size": "Schriftgröße", - "heading": "Verändere das Erscheinungsbild", "image_size_default": "Standard", "image_size_large": "Groß", "layout_bubbles": "Nachrichtenblasen", "layout_irc": "IRC (Experimentell)", "match_system_theme": "An Systemdesign anpassen", - "subheading": "Die %(brand)s Einstellungen zum Erscheinungsbild wirken sich nur auf diese Sitzung aus.", "timeline_image_size": "Bildgröße im Verlauf", "use_high_contrast": "Hohen Kontrast verwenden" }, @@ -3118,8 +3101,7 @@ "my_threads_description": "Zeigt alle Threads, an denen du teilgenommen hast", "open_thread": "Thread anzeigen", "show_all_threads": "Alle Threads anzeigen", - "show_thread_filter": "Zeige:", - "unable_to_decrypt": "Nachrichten-Entschlüsselung nicht möglich" + "show_thread_filter": "Zeige:" }, "time": { "about_day_ago": "vor etwa einem Tag", @@ -3162,7 +3144,6 @@ }, "creation_summary_dm": "%(creator)s hat diese Direktnachricht erstellt.", "creation_summary_room": "%(creator)s hat den Raum erstellt und konfiguriert.", - "decryption_failure_blocked": "Der Absender hat dich vom Erhalt dieser Nachricht ausgeschlossen", "disambiguated_profile": "%(displayName)s (%(matrixId)s)", "download_action_decrypting": "Entschlüsseln", "download_action_downloading": "Herunterladen", @@ -3171,7 +3152,6 @@ "tooltip_sub": "Klicke, um Änderungen anzuzeigen", "tooltip_title": "Geändert am %(date)s" }, - "encrypted_historical_messages_unavailable": "Vor diesem Zeitpunkt sind keine verschlüsselten Nachrichten verfügbar.", "error_no_renderer": "Dieses Ereignis konnte nicht angezeigt werden", "error_rendering_message": "Diese Nachricht kann nicht geladen werden", "historical_messages_unavailable": "Du kannst keine älteren Nachrichten lesen", diff --git a/linked-dependencies/matrix-react-sdk/src/i18n/strings/el.json b/linked-dependencies/matrix-react-sdk/src/i18n/strings/el.json index bd9f43db30..a678188e57 100644 --- a/linked-dependencies/matrix-react-sdk/src/i18n/strings/el.json +++ b/linked-dependencies/matrix-react-sdk/src/i18n/strings/el.json @@ -1913,13 +1913,11 @@ "custom_theme_success": "Το θέμα προστέθηκε!", "custom_theme_url": "URL προσαρμοσμένου θέματος", "font_size": "Μέγεθος γραμματοσειράς", - "heading": "Προσαρμόστε την εμφάνισή σας", "image_size_default": "Προεπιλογή", "image_size_large": "Μεγάλο", "layout_bubbles": "Συννεφάκια μηνυμάτων", "layout_irc": "IRC (Πειραματικό)", "match_system_theme": "Αντιστοίχιση θέματος συστήματος", - "subheading": "Οι ρυθμίσεις εμφάνισης επηρεάζουν μόνο αυτή τη %(brand)s συνεδρία.", "timeline_image_size": "Μέγεθος εικόνας στη γραμμή χρόνου", "use_high_contrast": "Χρησιμοποιήστε υψηλή αντίθεση" }, @@ -2510,7 +2508,6 @@ "tooltip_sub": "Κάντε κλικ για να δείτε τις τροποποιήσεις", "tooltip_title": "Τροποποιήθηκε στις %(date)s" }, - "encrypted_historical_messages_unavailable": "Κρυπτογραφημένα μηνύματα πριν από αυτό το σημείο δεν είναι διαθέσιμα.", "error_no_renderer": "Δεν ήταν δυνατή η εμφάνιση αυτού του συμβάντος", "error_rendering_message": "Δεν είναι δυνατή η φόρτωση αυτού του μηνύματος", "historical_messages_unavailable": "Δεν μπορείτε να δείτε προηγούμενα μηνύματα", diff --git a/linked-dependencies/matrix-react-sdk/src/i18n/strings/en_EN.json b/linked-dependencies/matrix-react-sdk/src/i18n/strings/en_EN.json index f0ee695d5e..7b97049e92 100644 --- a/linked-dependencies/matrix-react-sdk/src/i18n/strings/en_EN.json +++ b/linked-dependencies/matrix-react-sdk/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", @@ -1822,6 +1821,7 @@ }, "right_panel": { "add_integrations": "Add widgets, bridges & bots", + "add_topic": "Add topic", "edit_integrations": "Edit widgets, bridges & bots", "export_chat_button": "Export chat", "files_button": "Files", @@ -2396,6 +2396,7 @@ "brand_version": "%(brand)s version:", "clear_cache_reload": "Clear cache and reload", "crypto_version": "Crypto version:", + "dialog_title": "Settings: Help & About", "help_link": "For help with using %(brand)s, click here.", "homeserver": "Homeserver is %(homeserverUrl)s", "identity_server": "Identity server is %(identityServerUrl)s", @@ -2418,15 +2419,14 @@ "custom_theme_invalid": "Invalid theme schema.", "custom_theme_success": "Theme added!", "custom_theme_url": "Custom theme URL", + "dialog_title": "Settings: Appearance", "font_size": "Font size", "font_size_default": "%(fontSize)s (default)", - "heading": "Customise your appearance", "image_size_default": "Default", "image_size_large": "Large", "layout_bubbles": "Message bubbles", "layout_irc": "IRC (Experimental)", "match_system_theme": "Match system theme", - "subheading": "Appearance Settings only affect this %(brand)s session.", "timeline_image_size": "Image size in the timeline", "use_high_contrast": "Use high contrast" }, @@ -2468,6 +2468,7 @@ "deactivate_confirm_erase_label": "Hide my messages from new joiners", "deactivate_section": "Deactivate Account", "deactivate_warning": "Deactivating your account is a permanent action — be careful!", + "dialog_title": "Settings: General", "discovery_email_empty": "Discovery options will appear once you have added an email above.", "discovery_email_verification_instructions": "Verify the link in your inbox", "discovery_msisdn_empty": "Discovery options will appear once you have added a phone number above.", @@ -2575,12 +2576,20 @@ "phrase_strong_enough": "Great! This passphrase looks strong enough" }, "keyboard": { + "dialog_title": "Settings: Keyboard", "title": "Keyboard" }, + "labs": { + "dialog_title": "Settings: Labs" + }, + "labs_mjolnir": { + "dialog_title": "Settings: Ignored Users" + }, "notifications": { "default_setting_description": "This setting will be applied by default to all your rooms.", "default_setting_section": "I want to be notified for (Default Setting)", "desktop_notification_message_preview": "Show message preview in desktop notification", + "dialog_title": "Settings: Notifications", "email_description": "Receive an email summary of missed notifications", "email_section": "Email summary", "email_select": "Select which emails you want to send summaries to. Manage your emails in .", @@ -2639,6 +2648,7 @@ "code_blocks_heading": "Code blocks", "compact_modern": "Use a more compact 'Modern' layout", "composer_heading": "Composer", + "dialog_title": "Settings: Preferences", "enable_hardware_acceleration": "Enable hardware acceleration", "enable_tray_icon": "Show tray icon and minimise window to it on close", "keyboard_heading": "Keyboard shortcuts", @@ -2688,6 +2698,7 @@ "dehydrated_device_enabled": "Offline device enabled", "delete_backup": "Delete Backup", "delete_backup_confirm_description": "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.", + "dialog_title": "Settings: Security & Privacy", "e2ee_default_disabled_warning": "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.", "enable_message_search": "Enable message search in encrypted rooms", "encryption_individual_verification_mode": "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.", @@ -2767,6 +2778,7 @@ "device_unverified_description_current": "Verify your current session for enhanced secure messaging.", "device_verified_description": "This session is ready for secure messaging.", "device_verified_description_current": "Your current session is ready for secure messaging.", + "dialog_title": "Settings: Sessions", "error_pusher_state": "Failed to set pusher state", "error_set_name": "Failed to set session name", "filter_all": "All", @@ -2850,6 +2862,7 @@ "show_typing_notifications": "Show typing notifications", "showbold": "Show all activity in the room list (dots or number of unread messages)", "sidebar": { + "dialog_title": "Settings: Sidebar", "metaspaces_favourites_description": "Group all your favourite rooms and people in one place.", "metaspaces_home_all_rooms": "Show all rooms", "metaspaces_home_all_rooms_description": "Show all your rooms in Home, even if they're in a space.", @@ -2879,6 +2892,7 @@ "audio_output_empty": "No Audio Outputs detected", "auto_gain_control": "Automatic gain control", "connection_section": "Connection", + "dialog_title": "Settings: Voice & Video", "echo_cancellation": "Echo cancellation", "enable_fallback_ice_server": "Allow fallback call assist server (%(server)s)", "enable_fallback_ice_server_description": "Only applies if your homeserver does not offer one. Your IP address would be shared during a call.", @@ -3162,8 +3176,7 @@ "my_threads_description": "Shows all threads you've participated in", "open_thread": "Open thread", "show_all_threads": "Show all threads", - "show_thread_filter": "Show:", - "unable_to_decrypt": "Unable to decrypt message" + "show_thread_filter": "Show:" }, "threads_activity_centre": { "header": "Threads activity", @@ -3213,7 +3226,13 @@ }, "creation_summary_dm": "%(creator)s created this DM.", "creation_summary_room": "%(creator)s created and configured the room.", - "decryption_failure_blocked": "The sender has blocked you from receiving this message", + "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)", "download_action_decrypting": "Decrypting", "download_action_downloading": "Downloading", @@ -3222,7 +3241,6 @@ "tooltip_sub": "Click to view edits", "tooltip_title": "Edited at %(date)s" }, - "encrypted_historical_messages_unavailable": "Encrypted messages before this point are unavailable.", "error_no_renderer": "This event could not be displayed", "error_rendering_message": "Can't load this message", "historical_messages_unavailable": "You can't see earlier messages", diff --git a/linked-dependencies/matrix-react-sdk/src/i18n/strings/eo.json b/linked-dependencies/matrix-react-sdk/src/i18n/strings/eo.json index 7845e4bfb6..caeae03dbd 100644 --- a/linked-dependencies/matrix-react-sdk/src/i18n/strings/eo.json +++ b/linked-dependencies/matrix-react-sdk/src/i18n/strings/eo.json @@ -1712,11 +1712,9 @@ "custom_theme_success": "Haŭto aldoniĝis!", "custom_theme_url": "Propra URL al haŭto", "font_size": "Grando de tiparo", - "heading": "Adaptu vian aspekton", "image_size_default": "Ordinara", "layout_bubbles": "Mesaĝaj vezikoj", - "match_system_theme": "Similiĝi la sisteman haŭton", - "subheading": "Agordoj de aspekto nur efikos sur ĉi tiun salutaĵon de %(brand)s." + "match_system_theme": "Similiĝi la sisteman haŭton" }, "automatic_language_detection_syntax_highlight": "Ŝalti memagan rekonon de lingvo por sintaksa markado", "autoplay_gifs": "Memage ludi GIF-ojn", diff --git a/linked-dependencies/matrix-react-sdk/src/i18n/strings/es.json b/linked-dependencies/matrix-react-sdk/src/i18n/strings/es.json index 936ecebb6f..39f6c85e1c 100644 --- a/linked-dependencies/matrix-react-sdk/src/i18n/strings/es.json +++ b/linked-dependencies/matrix-react-sdk/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.", @@ -2205,13 +2191,11 @@ "custom_theme_success": "¡Se añadió el tema!", "custom_theme_url": "URL de tema personalizado", "font_size": "Tamaño del texto", - "heading": "Personaliza la apariencia", "image_size_default": "Por defecto", "image_size_large": "Grande", "layout_bubbles": "Burbujas de mensaje", "layout_irc": "IRC (en pruebas)", "match_system_theme": "Usar el mismo tema que el sistema", - "subheading": "Cambiar las opciones de apariencia solo afecta a esta sesión de %(brand)s.", "timeline_image_size": "Tamaño de las imágenes en la línea de tiempo", "use_high_contrast": "Usar un modo con contraste alto" }, @@ -2866,8 +2850,7 @@ "my_threads_description": "Ver todos los hilos en los que has participado", "open_thread": "Abrir hilo", "show_all_threads": "Ver todos los hilos", - "show_thread_filter": "Mostrar:", - "unable_to_decrypt": "No se ha podido descifrar el mensaje" + "show_thread_filter": "Mostrar:" }, "time": { "about_day_ago": "hace aprox. un día", @@ -2907,7 +2890,6 @@ }, "creation_summary_dm": "%(creator)s creó este mensaje directo.", "creation_summary_room": "Sala creada y configurada por %(creator)s.", - "decryption_failure_blocked": "La persona que ha enviado este mensaje te ha bloqueado, no puedes recibir el mensaje", "download_action_decrypting": "Descifrando", "download_action_downloading": "Descargando", "edits": { @@ -2915,7 +2897,6 @@ "tooltip_sub": "Haz clic para ver las ediciones", "tooltip_title": "Última vez editado: %(date)s" }, - "encrypted_historical_messages_unavailable": "Los mensajes cifrados antes de este punto no están disponibles.", "error_no_renderer": "No se ha podido mostrar este evento", "error_rendering_message": "No se ha podido cargar este mensaje", "historical_messages_unavailable": "No puedes ver mensajes anteriores", diff --git a/linked-dependencies/matrix-react-sdk/src/i18n/strings/et.json b/linked-dependencies/matrix-react-sdk/src/i18n/strings/et.json index cdfe16cc66..3116ea709f 100644 --- a/linked-dependencies/matrix-react-sdk/src/i18n/strings/et.json +++ b/linked-dependencies/matrix-react-sdk/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.", @@ -2374,13 +2359,11 @@ "custom_theme_success": "Teema sai lisatud!", "custom_theme_url": "Kohandatud teema URL", "font_size": "Fontide suurus", - "heading": "Kohenda välimust", "image_size_default": "Tavaline", "image_size_large": "Suur", "layout_bubbles": "Jutumullid", "layout_irc": "IRC (katseline)", "match_system_theme": "Kasuta süsteemset teemat", - "subheading": "Välimuse kohendused kehtivad vaid selles %(brand)s'i sessioonis.", "timeline_image_size": "Piltide suurus ajajoonel", "use_high_contrast": "Kasuta kontrastset välimust" }, @@ -3093,11 +3076,7 @@ "my_threads_description": "Näitab kõiki jutulõngasid, kus sa oled osalenud", "open_thread": "Ava jutulõng", "show_all_threads": "Näita kõiki jutulõngasid", - "show_thread_filter": "Näita:", - "unable_to_decrypt": "Sõnumi dekrüptimine ei õnnestunud" - }, - "threads_activity_centre": { - "no_rooms_with_unreads_threads": "Sul veel pole lugemata jutulõngadega jututubasid." + "show_thread_filter": "Näita:" }, "time": { "about_day_ago": "umbes päev tagasi", @@ -3140,7 +3119,6 @@ }, "creation_summary_dm": "%(creator)s alustas seda otsesuhtlust.", "creation_summary_room": "%(creator)s lõi ja seadistas jututoa.", - "decryption_failure_blocked": "Sõnumi saatja on keelanud sul selle sõnumi saamise", "download_action_decrypting": "Dekrüptin sisu", "download_action_downloading": "Laadin alla", "edits": { @@ -3148,7 +3126,6 @@ "tooltip_sub": "Muudatuste nägemiseks klõpsi", "tooltip_title": "Muutmise kuupäev %(date)s" }, - "encrypted_historical_messages_unavailable": "Enne seda ajahetke saadetud krüptitud sõnumid pole saadaval.", "error_no_renderer": "Seda sündmust ei õnnestunud kuvada", "error_rendering_message": "Selle sõnumi laadimine ei õnnestu", "historical_messages_unavailable": "Sa ei saa näha varasemaid sõnumeid", diff --git a/linked-dependencies/matrix-react-sdk/src/i18n/strings/fa.json b/linked-dependencies/matrix-react-sdk/src/i18n/strings/fa.json index aad57928ed..249c72adfd 100644 --- a/linked-dependencies/matrix-react-sdk/src/i18n/strings/fa.json +++ b/linked-dependencies/matrix-react-sdk/src/i18n/strings/fa.json @@ -1503,10 +1503,8 @@ "custom_theme_success": "پوسته اضافه شد!", "custom_theme_url": "آدرس پوسته دلخواه", "font_size": "اندازه فونت", - "heading": "ظاهر پیام‌رسان خود را سفارشی‌سازی کنید", "image_size_default": "پیشفرض", - "match_system_theme": "با پوسته‌ی سیستم تطبیق پیدا کن", - "subheading": "تنظیمات ظاهری برنامه تنها همین نشست %(brand)s را تحت تاثیر قرار می‌دهد." + "match_system_theme": "با پوسته‌ی سیستم تطبیق پیدا کن" }, "automatic_language_detection_syntax_highlight": "فعال‌سازی تشخیص خودکار زبان برای پررنگ‌سازی نحوی", "big_emoji": "نمایش شکلک‌های بزرگ در گفتگوها را فعال کن", diff --git a/linked-dependencies/matrix-react-sdk/src/i18n/strings/fi.json b/linked-dependencies/matrix-react-sdk/src/i18n/strings/fi.json index 21535ba92b..656735aa51 100644 --- a/linked-dependencies/matrix-react-sdk/src/i18n/strings/fi.json +++ b/linked-dependencies/matrix-react-sdk/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ä.", @@ -2091,13 +2079,11 @@ "custom_theme_success": "Teema lisätty!", "custom_theme_url": "Mukautettu teeman osoite", "font_size": "Fontin koko", - "heading": "Mukauta ulkoasua", "image_size_default": "Oletus", "image_size_large": "Suuri", "layout_bubbles": "Viestikuplat", "layout_irc": "IRC (kokeellinen)", "match_system_theme": "Käytä järjestelmän teemaa", - "subheading": "Ulkoasuasetukset vaikuttavat vain tähän %(brand)s-istuntoon.", "timeline_image_size": "Kuvan koko aikajanalla", "use_high_contrast": "Käytä suurta kontrastia" }, @@ -2733,8 +2719,7 @@ "my_threads_description": "Näyttää kaikki ketjut, joissa olet ollut osallinen", "open_thread": "Avaa ketju", "show_all_threads": "Näytä kaikki ketjut", - "show_thread_filter": "Näytä:", - "unable_to_decrypt": "Viestin salauksen purkaminen ei onnistu" + "show_thread_filter": "Näytä:" }, "time": { "about_day_ago": "noin päivä sitten", @@ -2776,7 +2761,6 @@ }, "creation_summary_dm": "%(creator)s loi tämän yksityisviestin.", "creation_summary_room": "%(creator)s loi ja määritti huoneen.", - "decryption_failure_blocked": "Lähettäjä on estänyt sinua saamasta tätä viestiä", "download_action_decrypting": "Puretaan salausta", "download_action_downloading": "Ladataan", "edits": { @@ -2784,7 +2768,6 @@ "tooltip_sub": "Napsauta nähdäksesi muokkaukset", "tooltip_title": "Muokattu %(date)s" }, - "encrypted_historical_messages_unavailable": "Tätä aiemmat salatut viestit eivät ole saatavilla.", "error_no_renderer": "Tätä tapahtumaa ei voitu näyttää", "error_rendering_message": "Tätä viestiä ei voi ladata", "historical_messages_unavailable": "Et voi nähdä aiempia viestejä", diff --git a/linked-dependencies/matrix-react-sdk/src/i18n/strings/fr.json b/linked-dependencies/matrix-react-sdk/src/i18n/strings/fr.json index 30441221cb..26199f9f93 100644 --- a/linked-dependencies/matrix-react-sdk/src/i18n/strings/fr.json +++ b/linked-dependencies/matrix-react-sdk/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", @@ -2402,13 +2385,11 @@ "custom_theme_success": "Thème ajouté !", "custom_theme_url": "URL personnalisée pour le thème", "font_size": "Taille de la police", - "heading": "Personnalisez l’apparence", "image_size_default": "Par défaut", "image_size_large": "Grande", "layout_bubbles": "Message en bulles", "layout_irc": "IRC (Expérimental)", "match_system_theme": "S’adapter au thème du système", - "subheading": "Les paramètres d’apparence affecteront uniquement cette session de %(brand)s.", "timeline_image_size": "Taille d’image dans l’historique", "use_high_contrast": "Utiliser un contraste élevé" }, @@ -3132,8 +3113,7 @@ "my_threads_description": "Affiche tous les fils de discussion auxquels vous avez participé", "open_thread": "Ouvrir le fil de discussion", "show_all_threads": "Afficher tous les fils de discussion", - "show_thread_filter": "Affiche :", - "unable_to_decrypt": "Impossible de déchiffrer le message" + "show_thread_filter": "Affiche :" }, "time": { "about_day_ago": "il y a environ un jour", @@ -3176,7 +3156,6 @@ }, "creation_summary_dm": "%(creator)s a créé cette conversation privée.", "creation_summary_room": "%(creator)s a créé et configuré le salon.", - "decryption_failure_blocked": "L’expéditeur a bloqué la réception de votre message", "disambiguated_profile": "%(displayName)s (%(matrixId)s)", "download_action_decrypting": "Déchiffrement", "download_action_downloading": "Téléchargement en cours", @@ -3185,7 +3164,6 @@ "tooltip_sub": "Cliquez pour voir les modifications", "tooltip_title": "Modifié le %(date)s" }, - "encrypted_historical_messages_unavailable": "Les messages chiffrés avant ce point sont inaccessibles.", "error_no_renderer": "Cet évènement n’a pas pu être affiché", "error_rendering_message": "Impossible de charger ce message", "historical_messages_unavailable": "Vous ne pouvez pas voir les messages plus anciens", diff --git a/linked-dependencies/matrix-react-sdk/src/i18n/strings/gl.json b/linked-dependencies/matrix-react-sdk/src/i18n/strings/gl.json index e58b0ea5ad..1a7cd9d297 100644 --- a/linked-dependencies/matrix-react-sdk/src/i18n/strings/gl.json +++ b/linked-dependencies/matrix-react-sdk/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.", @@ -2033,12 +2027,10 @@ "custom_theme_success": "Decorado engadido!", "custom_theme_url": "URL do decorado personalizado", "font_size": "Tamaño da letra", - "heading": "Personaliza o aspecto", "image_size_default": "Por defecto", "image_size_large": "Grande", "layout_bubbles": "Burbullas con mensaxes", "match_system_theme": "Imitar o aspecto do sistema", - "subheading": "Os axustes da aparencia só lle afectan a esta sesión %(brand)s.", "timeline_image_size": "Tamaño de imaxe na cronoloxía", "use_high_contrast": "Usar alto contraste" }, @@ -2674,7 +2666,6 @@ "tooltip_sub": "Preme para ver as edicións", "tooltip_title": "Editado o %(date)s" }, - "encrypted_historical_messages_unavailable": "Non están dispoñibles as mensaxes cifradas anteriores a este punto.", "error_no_renderer": "Non se puido amosar este evento", "error_rendering_message": "Non se cargou a mensaxe", "historical_messages_unavailable": "Non podes ver mensaxes anteriores", diff --git a/linked-dependencies/matrix-react-sdk/src/i18n/strings/he.json b/linked-dependencies/matrix-react-sdk/src/i18n/strings/he.json index 3a3c2ad736..1de2723275 100644 --- a/linked-dependencies/matrix-react-sdk/src/i18n/strings/he.json +++ b/linked-dependencies/matrix-react-sdk/src/i18n/strings/he.json @@ -1625,12 +1625,10 @@ "custom_theme_success": "ערכת נושא התווספה בהצלחה!", "custom_theme_url": "כתובת ערכת נושא מותאמת אישית", "font_size": "גודל אותיות", - "heading": "התאם את התצוגה שלך", "image_size_default": "ברירת מחדל", "image_size_large": "גדול", "layout_bubbles": "בועות הודעות", "match_system_theme": "התאם לתבנית המערכת", - "subheading": "התאמת תצוגה משפיעה רק על התחברות זו %(brand)s.", "timeline_image_size": "גודל תמונה בציר הזמן" }, "automatic_language_detection_syntax_highlight": "החל זיהוי שפה אוטומטי עבור הדגשת מבנה הכתיבה", diff --git a/linked-dependencies/matrix-react-sdk/src/i18n/strings/hu.json b/linked-dependencies/matrix-react-sdk/src/i18n/strings/hu.json index 69988db429..5d3c86655e 100644 --- a/linked-dependencies/matrix-react-sdk/src/i18n/strings/hu.json +++ b/linked-dependencies/matrix-react-sdk/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.", @@ -2329,13 +2314,11 @@ "custom_theme_success": "Téma hozzáadva!", "custom_theme_url": "Egyéni téma webcíme", "font_size": "Betűméret", - "heading": "A megjelenés testreszabása", "image_size_default": "Alapértelmezett", "image_size_large": "Nagy", "layout_bubbles": "Üzenetbuborékok", "layout_irc": "IRC (kísérleti)", "match_system_theme": "Rendszer témájához megfelelő", - "subheading": "A megjelenés beállításai csak erre az %(brand)s munkamenetre lesznek érvényesek.", "timeline_image_size": "Képméret az idővonalon", "use_high_contrast": "Nagy kontraszt használata" }, @@ -3057,8 +3040,7 @@ "my_threads_description": "Minden üzenetszál megjelenítése, amelyben részt vesz", "open_thread": "Üzenetszál megnyitása", "show_all_threads": "Minden üzenetszál megjelenítése", - "show_thread_filter": "Megjelenítés:", - "unable_to_decrypt": "Üzenet visszafejtése sikertelen" + "show_thread_filter": "Megjelenítés:" }, "time": { "about_day_ago": "egy napja", @@ -3101,7 +3083,6 @@ }, "creation_summary_dm": "%(creator)s hozta létre ezt az üzenetet.", "creation_summary_room": "%(creator)s elkészítette és beállította a szobát.", - "decryption_failure_blocked": "A feladó megtagadta az Ön hozzáférését ehhez az üzenethez", "disambiguated_profile": "%(displayName)s (%(matrixId)s)", "download_action_decrypting": "Visszafejtés", "download_action_downloading": "Letöltés", @@ -3110,7 +3091,6 @@ "tooltip_sub": "A szerkesztések megtekintéséhez kattints", "tooltip_title": "Szerkesztve ekkor: %(date)s" }, - "encrypted_historical_messages_unavailable": "A régebbi titkosított üzenetek elérhetetlenek.", "error_no_renderer": "Az eseményt nem lehet megjeleníteni", "error_rendering_message": "Ezt az üzenetet nem sikerült betölteni", "historical_messages_unavailable": "Nem tekintheted meg a régebbi üzeneteket", diff --git a/linked-dependencies/matrix-react-sdk/src/i18n/strings/id.json b/linked-dependencies/matrix-react-sdk/src/i18n/strings/id.json index 4f942d89da..e35d2e6b46 100644 --- a/linked-dependencies/matrix-react-sdk/src/i18n/strings/id.json +++ b/linked-dependencies/matrix-react-sdk/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", @@ -2362,13 +2347,11 @@ "custom_theme_success": "Tema ditambahkan!", "custom_theme_url": "URL tema kustom", "font_size": "Ukuran font", - "heading": "Ubah tampilan Anda", "image_size_default": "Bawaan", "image_size_large": "Besar", "layout_bubbles": "Gelembung pesan", "layout_irc": "IRC (Eksperimental)", "match_system_theme": "Sesuaikan dengan tema sistem", - "subheading": "Pengaturan Tampilan hanya ditetapkan di sesi %(brand)s ini.", "timeline_image_size": "Ukuran gambar di lini masa", "use_high_contrast": "Gunakan kontras tinggi" }, @@ -3091,8 +3074,7 @@ "my_threads_description": "Menampilkan semua utasan yang Anda berpartisipasi", "open_thread": "Buka utasan", "show_all_threads": "Tampilkan semua utasan", - "show_thread_filter": "Tampilkan:", - "unable_to_decrypt": "Tidak dapat mendekripsi pesan" + "show_thread_filter": "Tampilkan:" }, "time": { "about_day_ago": "1 hari yang lalu", @@ -3135,7 +3117,6 @@ }, "creation_summary_dm": "%(creator)s membuat pesan langsung ini.", "creation_summary_room": "%(creator)s membuat dan mengatur ruangan ini.", - "decryption_failure_blocked": "Pengirim telah memblokir Anda supaya tidak menerima pesan ini", "disambiguated_profile": "%(displayName)s (%(matrixId)s)", "download_action_decrypting": "Mendekripsi", "download_action_downloading": "Mengunduh", @@ -3144,7 +3125,6 @@ "tooltip_sub": "Klik untuk melihat editan", "tooltip_title": "Diedit di %(date)s" }, - "encrypted_historical_messages_unavailable": "Pesan-pesan terenkripsi sebelum titik ini tidak tersedia.", "error_no_renderer": "Peristiwa ini tidak dapat ditampilkan", "error_rendering_message": "Tidak dapat memuat pesan ini", "historical_messages_unavailable": "Anda tidak dapat melihat pesan-pesan awal", diff --git a/linked-dependencies/matrix-react-sdk/src/i18n/strings/is.json b/linked-dependencies/matrix-react-sdk/src/i18n/strings/is.json index 8c8122b30b..99b180dd47 100644 --- a/linked-dependencies/matrix-react-sdk/src/i18n/strings/is.json +++ b/linked-dependencies/matrix-react-sdk/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.", @@ -1929,13 +1926,11 @@ "custom_theme_success": "Þema bætt við!", "custom_theme_url": "Slóð á sérsniðið þema", "font_size": "Leturstærð", - "heading": "Sérsníddu útlitið þitt", "image_size_default": "Sjálfgefið", "image_size_large": "Stórt", "layout_bubbles": "Skilaboðablöðrur", "layout_irc": "IRC (á tilraunastigi)", "match_system_theme": "Samsvara þema kerfis", - "subheading": "Stillingar útlits hafa einungis áhrif á þessa %(brand)s setu.", "timeline_image_size": "Stærð myndar í tímalínunni", "use_high_contrast": "Nota mikil birtuskil" }, @@ -2531,8 +2526,7 @@ "my_threads_description": "Birtir alla spjallþræði sem þú hefur tekið þátt í", "open_thread": "Opna spjallþráð", "show_all_threads": "Birta alla spjallþræði", - "show_thread_filter": "Sýna:", - "unable_to_decrypt": "Tókst ekki að afkóða skilaboð" + "show_thread_filter": "Sýna:" }, "time": { "about_day_ago": "fyrir um degi síðan", diff --git a/linked-dependencies/matrix-react-sdk/src/i18n/strings/it.json b/linked-dependencies/matrix-react-sdk/src/i18n/strings/it.json index 46085e1050..f08b5605af 100644 --- a/linked-dependencies/matrix-react-sdk/src/i18n/strings/it.json +++ b/linked-dependencies/matrix-react-sdk/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", @@ -2410,13 +2393,11 @@ "custom_theme_url": "URL tema personalizzato", "font_size": "Dimensione carattere", "font_size_default": "%(fontSize)s (predefinito)", - "heading": "Personalizza l'aspetto", "image_size_default": "Predefinito", "image_size_large": "Grande", "layout_bubbles": "Messaggi", "layout_irc": "IRC (Sperimentale)", "match_system_theme": "Usa il tema di sistema", - "subheading": "Le impostazioni dell'aspetto hanno effetto solo in questa sessione di %(brand)s.", "timeline_image_size": "Dimensione immagine nella linea temporale", "use_high_contrast": "Usa contrasto alto" }, @@ -3141,12 +3122,10 @@ "my_threads_description": "Mostra tutte le conversazioni a cui hai partecipato", "open_thread": "Apri conversazione", "show_all_threads": "Mostra tutte le conversazioni", - "show_thread_filter": "Mostra:", - "unable_to_decrypt": "Impossibile decifrare il messaggio" + "show_thread_filter": "Mostra:" }, "threads_activity_centre": { - "header": "Attività delle conversazioni", - "no_rooms_with_unreads_threads": "Non hai ancora stanze con conversazioni non lette." + "header": "Attività delle conversazioni" }, "time": { "about_day_ago": "circa un giorno fa", @@ -3189,7 +3168,6 @@ }, "creation_summary_dm": "%(creator)s ha creato questo MD.", "creation_summary_room": "%(creator)s ha creato e configurato la stanza.", - "decryption_failure_blocked": "Il mittente ti ha bloccato dalla ricezione di questo messaggio", "disambiguated_profile": "%(displayName)s (%(matrixId)s)", "download_action_decrypting": "Decifrazione", "download_action_downloading": "Scaricamento", @@ -3198,7 +3176,6 @@ "tooltip_sub": "Clicca per vedere le modifiche", "tooltip_title": "Modificato il %(date)s" }, - "encrypted_historical_messages_unavailable": "I messaggi cifrati prima di questo punto non sono disponibili.", "error_no_renderer": "Questo evento non può essere mostrato", "error_rendering_message": "Impossibile caricare questo messaggio", "historical_messages_unavailable": "Non puoi vedere i messaggi precedenti", diff --git a/linked-dependencies/matrix-react-sdk/src/i18n/strings/ja.json b/linked-dependencies/matrix-react-sdk/src/i18n/strings/ja.json index 22a17aabde..6fe3e3f401 100644 --- a/linked-dependencies/matrix-react-sdk/src/i18n/strings/ja.json +++ b/linked-dependencies/matrix-react-sdk/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で音声と動画により会話する新しい方法です。", @@ -2201,13 +2186,11 @@ "custom_theme_success": "テーマが追加されました!", "custom_theme_url": "ユーザー定義のテーマのURL", "font_size": "フォントの大きさ", - "heading": "外観のカスタマイズ", "image_size_default": "既定値", "image_size_large": "大", "layout_bubbles": "吹き出し", "layout_irc": "IRC(実験的)", "match_system_theme": "システムテーマに合わせる", - "subheading": "外観の設定はこの%(brand)sのセッションにのみ適用されます。", "timeline_image_size": "タイムライン上での画像のサイズ", "use_high_contrast": "高コントラストを使用" }, @@ -2872,8 +2855,7 @@ "my_threads_description": "参加している全スレッドを表示", "open_thread": "スレッドを開く", "show_all_threads": "全てのスレッドを表示", - "show_thread_filter": "表示:", - "unable_to_decrypt": "メッセージを復号化できません" + "show_thread_filter": "表示:" }, "time": { "about_day_ago": "約1日前", @@ -2924,7 +2906,6 @@ "tooltip_sub": "クリックすると変更履歴を表示", "tooltip_title": "%(date)sに編集済" }, - "encrypted_historical_messages_unavailable": "これ以前の暗号化されたメッセージは利用できません。", "error_no_renderer": "このイベントは表示できませんでした", "error_rendering_message": "このメッセージを読み込めません", "historical_messages_unavailable": "以前のメッセージは表示できません", diff --git a/linked-dependencies/matrix-react-sdk/src/i18n/strings/lo.json b/linked-dependencies/matrix-react-sdk/src/i18n/strings/lo.json index 72f90e0f0a..db8dc5a9fd 100644 --- a/linked-dependencies/matrix-react-sdk/src/i18n/strings/lo.json +++ b/linked-dependencies/matrix-react-sdk/src/i18n/strings/lo.json @@ -1940,13 +1940,11 @@ "custom_theme_success": "ເພີ່ມຫົວຂໍ້!", "custom_theme_url": "ການ ກຳນົດເອງຫົວຂໍ້ URL", "font_size": "ຂະໜາດຕົວອັກສອນ", - "heading": "ປັບແຕ່ງຮູບລັກສະນະຂອງທ່ານ", "image_size_default": "ຄ່າເລີ່ມຕົ້ນ", "image_size_large": "ຂະຫນາດໃຫຍ່", "layout_bubbles": "ຟອງຂໍ້ຄວາມ", "layout_irc": "(ທົດລອງ)IRC", "match_system_theme": "ລະບົບຈັບຄູ່ຫົວຂໍ້", - "subheading": "ການຕັ້ງຄ່າຮູບລັກສະນະມີຜົນກະທົບພຽງແຕ່ %(brand)s ໃນລະບົບ ນີ້.", "timeline_image_size": "ຂະຫນາດຮູບພາບຢູ່ໃນທາມລາຍ", "use_high_contrast": "ໃຊ້ຄວາມຄົມຊັດສູງ" }, @@ -2523,7 +2521,6 @@ "tooltip_sub": "ກົດເພື່ອເບິ່ງການແກ້ໄຂ", "tooltip_title": "ແກ້ໄຂເມື່ອ %(date)s" }, - "encrypted_historical_messages_unavailable": "ຂໍ້ຄວາມທີ່ເຂົ້າລະຫັດໄວ້ກ່ອນຈຸດນີ້ບໍ່ສາມາດໃຊ້ໄດ້.", "error_no_renderer": "ເຫດການນີ້ບໍ່ສາມາດສະແດງໄດ້", "error_rendering_message": "ບໍ່ສາມາດໂຫຼດຂໍ້ຄວາມນີ້ໄດ້", "historical_messages_unavailable": "ທ່ານບໍ່ສາມາດເຫັນຂໍ້ຄວາມກ່ອນໜ້ານີ້", diff --git a/linked-dependencies/matrix-react-sdk/src/i18n/strings/lt.json b/linked-dependencies/matrix-react-sdk/src/i18n/strings/lt.json index 66deb29fe1..013f7eb591 100644 --- a/linked-dependencies/matrix-react-sdk/src/i18n/strings/lt.json +++ b/linked-dependencies/matrix-react-sdk/src/i18n/strings/lt.json @@ -1522,13 +1522,11 @@ "custom_theme_success": "Tema pridėta!", "custom_theme_url": "Pasirinktinės temos URL", "font_size": "Šrifto dydis", - "heading": "Tinkinti savo išvaizdą", "image_size_default": "Numatytas", "image_size_large": "Didelis", "layout_bubbles": "Žinučių burbulai", "layout_irc": "IRC (eksperimentinis)", "match_system_theme": "Suderinti su sistemos tema", - "subheading": "Išvaizdos nustatymai įtakoja tik šį %(brand)s seansą.", "timeline_image_size": "Paveikslėlio dydis laiko juostoje", "use_high_contrast": "Naudoti didelį kontrastą" }, @@ -2012,7 +2010,6 @@ "tooltip_sub": "Spustelėkite kad peržiūrėti pakeitimus", "tooltip_title": "Keista %(date)s" }, - "encrypted_historical_messages_unavailable": "Iki šio taško užšifruotos žinutės yra neprieinamos.", "error_no_renderer": "Nepavyko parodyti šio įvykio", "error_rendering_message": "Nepavyko įkelti šios žinutės", "historical_messages_unavailable": "Negalite matyti ankstesnių žinučių", diff --git a/linked-dependencies/matrix-react-sdk/src/i18n/strings/nl.json b/linked-dependencies/matrix-react-sdk/src/i18n/strings/nl.json index 9352a3cf2f..45905e97d8 100644 --- a/linked-dependencies/matrix-react-sdk/src/i18n/strings/nl.json +++ b/linked-dependencies/matrix-react-sdk/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.", @@ -2035,13 +2022,11 @@ "custom_theme_success": "Thema toegevoegd!", "custom_theme_url": "Aangepaste thema-URL", "font_size": "Lettergrootte", - "heading": "Weergave aanpassen", "image_size_default": "Standaard", "image_size_large": "Groot", "layout_bubbles": "Berichtenbubbels", "layout_irc": "IRC (Experimenteel)", "match_system_theme": "Aanpassen aan systeemthema", - "subheading": "Weergave-instellingen zijn alleen van toepassing op deze %(brand)s sessie.", "timeline_image_size": "Afbeeldingformaat in de tijdlijn", "use_high_contrast": "Hoog contrast inschakelen" }, @@ -2701,7 +2686,6 @@ "tooltip_sub": "Klik om bewerkingen te zien", "tooltip_title": "Bewerkt op %(date)s" }, - "encrypted_historical_messages_unavailable": "Versleutelde berichten vóór dit punt zijn niet beschikbaar.", "error_no_renderer": "Deze gebeurtenis kon niet weergegeven worden", "error_rendering_message": "Dit bericht kan niet geladen worden", "historical_messages_unavailable": "Je kan eerdere berichten niet zien", diff --git a/linked-dependencies/matrix-react-sdk/src/i18n/strings/pl.json b/linked-dependencies/matrix-react-sdk/src/i18n/strings/pl.json index 9b349bdd0e..1ff549f5cf 100644 --- a/linked-dependencies/matrix-react-sdk/src/i18n/strings/pl.json +++ b/linked-dependencies/matrix-react-sdk/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", @@ -1419,6 +1411,7 @@ "group_spaces": "Przestrzenie", "group_themes": "Motywy", "group_threads": "Wątki", + "group_ui": "Interfejs użytkownika", "group_voip": "Głos i wideo", "group_widgets": "Widżety", "hidebold": "Ukryj kropkę powiadomienia (wyświetlaj tylko licznik plakietek)", @@ -1442,6 +1435,7 @@ "oidc_native_flow": "Uwierzytelnianie natywne OIDC", "oidc_native_flow_description": "⚠ OSTRZEŻENIE: Funkcja eksperymentalna. Użyj uwierzytelniania natywnego OIDC, gdy jest wspierane przez serwer.", "pinning": "Przypinanie wiadomości", + "release_announcement": "Ogłoszenie o wydaniu", "render_reaction_images": "Renderuj niestandardowe obrazy w reakcjach", "render_reaction_images_description": "Czasami określane jako \"emoji niestandardowe\".", "report_to_moderators": "Zgłoś do moderatorów", @@ -1452,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", @@ -2421,13 +2406,11 @@ "custom_theme_url": "Niestandardowy adres URL motywu", "font_size": "Rozmiar czcionki", "font_size_default": "%(fontSize)s (domyślny)", - "heading": "Dostosuj wygląd", "image_size_default": "Zwykły", "image_size_large": "Duży", "layout_bubbles": "Dymki wiadomości", "layout_irc": "IRC (eksperymentalny)", "match_system_theme": "Dopasuj do motywu systemowego", - "subheading": "Ustawienia wyglądu wpływają tylko na tę sesję %(brand)s.", "timeline_image_size": "Rozmiar obrazu na osi czasu", "use_high_contrast": "Użyj wysokiego kontrastu" }, @@ -2685,6 +2668,8 @@ "cross_signing_self_signing_private_key": "Samo-podpisujący klucz prywatny:", "cross_signing_user_signing_private_key": "Podpisany przez użytkownika klucz prywatny:", "cryptography_section": "Kryptografia", + "dehydrated_device_description": "Funkcja urządzenia offline umożliwia odbieranie wiadomości szyfrowanych, nawet jeśli nie jesteś zalogowany na żadnym urządzeniu", + "dehydrated_device_enabled": "Urządzenie offline włączone", "delete_backup": "Usuń kopię zapasową", "delete_backup_confirm_description": "Czy jesteś pewien? Stracisz dostęp do wszystkich swoich zaszyfrowanych wiadomości, jeżeli nie utworzyłeś poprawnej kopii zapasowej kluczy.", "e2ee_default_disabled_warning": "Twój administrator serwera wyłączył szyfrowanie end-to-end domyślnie w pokojach prywatnych i wiadomościach bezpośrednich.", @@ -2847,6 +2832,7 @@ "show_redaction_placeholder": "Pokaż symbol zastępczy dla usuniętych wiadomości", "show_stickers_button": "Pokaż przycisk naklejek", "show_typing_notifications": "Pokazuj powiadomienia o pisaniu", + "showbold": "Pokaż całą aktywność na liście pomieszczeń (kropki lub liczbę nieprzeczytanych wiadomości)", "sidebar": { "metaspaces_favourites_description": "Pogrupuj wszystkie swoje ulubione pokoje i osoby w jednym miejscu.", "metaspaces_home_all_rooms": "Pokaż wszystkie pokoje", @@ -2863,6 +2849,7 @@ "title": "Pasek boczny" }, "start_automatically": "Uruchom automatycznie po zalogowaniu się do systemu", + "tac_only_notifications": "Pokaż powiadomienia tylko w centrum aktywności wątków", "use_12_hour_format": "Pokaż czas w formacie 12-sto godzinnym (np. 2:30pm)", "use_command_enter_send_message": "Użyj Command + Enter, aby wysłać wiadomość", "use_command_f_search": "Użyj Command + F aby przeszukać oś czasu", @@ -3159,12 +3146,14 @@ "my_threads_description": "Pokazuje wszystkie wątki, w których brałeś udział", "open_thread": "Otwórz wątek", "show_all_threads": "Pokaż wszystkie wątki", - "show_thread_filter": "Pokaż:", - "unable_to_decrypt": "Nie można rozszyfrować wiadomości" + "show_thread_filter": "Pokaż:" }, "threads_activity_centre": { "header": "Aktywność wątków", - "no_rooms_with_unreads_threads": "Nie masz jeszcze pokoi z nieprzeczytanymi wątkami." + "no_rooms_with_threads_notifs": "Nie masz jeszcze pokoi z powiadomieniami w wątku.", + "no_rooms_with_unread_threads": "Nie masz jeszcze pokoi z nieprzeczytanymi wątkami.", + "release_announcement_description": "Powiadomienia w wątkach zostały przeniesione, teraz znajdziesz je tutaj.", + "release_announcement_header": "Centrum aktywności wątków" }, "time": { "about_day_ago": "około dzień temu", @@ -3207,7 +3196,6 @@ }, "creation_summary_dm": "%(creator)s utworzył tę wiadomość prywatną.", "creation_summary_room": "%(creator)s stworzył i skonfigurował pokój.", - "decryption_failure_blocked": "Nadawca zablokował Ci możliwość otrzymania tej wiadomości", "disambiguated_profile": "%(displayName)s (%(matrixId)s)", "download_action_decrypting": "Rozszyfrowuję", "download_action_downloading": "Pobieranie", @@ -3216,7 +3204,6 @@ "tooltip_sub": "Kliknij, aby wyświetlić edycje", "tooltip_title": "Edytowano o %(date)s" }, - "encrypted_historical_messages_unavailable": "Wiadomości szyfrowane przed tym punktem są niedostępne.", "error_no_renderer": "Ten event nie może zostać wyświetlony", "error_rendering_message": "Nie można wczytać tej wiadomości", "historical_messages_unavailable": "Nie możesz widzieć poprzednich wiadomości", @@ -3659,6 +3646,12 @@ "toast_title": "Aktualizuj %(brand)s", "unavailable": "Niedostępny" }, + "update_room_access_modal": { + "description": "Aby utworzyć link udostępniania, musisz zezwolić gościom na dołączenie do tego pokoju. Może to zmniejszyć bezpieczeństwo pokoju. Gdy zakończysz połączenie, możesz ustawić pokój jako prywatny z powrotem.", + "dont_change_description": "Możesz również zadzwonić w innym pokoju.", + "no_change": "Nie chce zmieniać poziomu uprawnień.", + "title": "Zmień poziom dostępu pokoju" + }, "upload_failed_generic": "Nie udało się przesłać pliku '%(fileName)s'.", "upload_failed_size": "Plik '%(fileName)s' przekracza limit rozmiaru dla tego serwera głównego", "upload_failed_title": "Błąd przesyłania", @@ -3694,6 +3687,7 @@ "deactivate_confirm_action": "Dezaktywuj użytkownika", "deactivate_confirm_description": "Dezaktywacja tego użytkownika, wyloguje go i uniemożliwi logowanie ponowne. Dodatkowo, opuści wszystkie pokoje, w których się znajdują. Tej akcji nie można cofnąć. Czy na pewno chcesz dezaktywować tego użytkownika?", "deactivate_confirm_title": "Dezaktywować użytkownika?", + "dehydrated_device_enabled": "Urządzenie offline włączone", "demote_button": "Degraduj", "demote_self_confirm_description_space": "Nie będziesz mógł cofnąć tej zmiany, ponieważ degradujesz swoje uprawnienia. Jeśli jesteś ostatnim użytkownikiem uprzywilejowanym w tej przestrzeni, nie będziesz mógł ich odzyskać.", "demote_self_confirm_room": "Nie będziesz mógł cofnąć tej zmiany, ponieważ degradujesz swoje uprawnienia. Jeśli jesteś ostatnim użytkownikiem uprzywilejowanym w tym pokoju, nie będziesz mógł ich odzyskać.", diff --git a/linked-dependencies/matrix-react-sdk/src/i18n/strings/pt_BR.json b/linked-dependencies/matrix-react-sdk/src/i18n/strings/pt_BR.json index 6d322e1f66..75f39a2ebf 100644 --- a/linked-dependencies/matrix-react-sdk/src/i18n/strings/pt_BR.json +++ b/linked-dependencies/matrix-react-sdk/src/i18n/strings/pt_BR.json @@ -1617,13 +1617,11 @@ "custom_theme_success": "Tema adicionado!", "custom_theme_url": "Link do tema personalizado", "font_size": "Tamanho da fonte", - "heading": "Personalize sua aparência", "image_size_default": "Padrão", "image_size_large": "Grande", "layout_bubbles": "Balões de mensagem", "layout_irc": "IRC (experimental)", "match_system_theme": "Se adaptar ao tema do sistema", - "subheading": "As configurações de aparência afetam apenas esta sessão do %(brand)s.", "timeline_image_size": "Tamanho da imagem na linha do tempo", "use_high_contrast": "Usar alto contraste" }, @@ -2141,7 +2139,6 @@ "tooltip_sub": "Clicar para ver edições", "tooltip_title": "Editado em %(date)s" }, - "encrypted_historical_messages_unavailable": "As mensagens criptografadas antes deste ponto não estão disponíveis.", "error_no_renderer": "Este evento não pôde ser exibido", "error_rendering_message": "Não foi possível carregar esta mensagem", "historical_messages_unavailable": "Você não pode ver as mensagens anteriores", diff --git a/linked-dependencies/matrix-react-sdk/src/i18n/strings/ru.json b/linked-dependencies/matrix-react-sdk/src/i18n/strings/ru.json index f9a84fd00d..fb9db606d2 100644 --- a/linked-dependencies/matrix-react-sdk/src/i18n/strings/ru.json +++ b/linked-dependencies/matrix-react-sdk/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": "Видеокомнаты", @@ -2387,13 +2372,11 @@ "custom_theme_success": "Тема добавлена!", "custom_theme_url": "Ссылка на стороннюю тему", "font_size": "Размер шрифта", - "heading": "Настройка внешнего вида", "image_size_default": "По умолчанию", "image_size_large": "Большой", "layout_bubbles": "Пузыри сообщений", "layout_irc": "IRC (Экспериментально)", "match_system_theme": "Тема системы", - "subheading": "Настройки внешнего вида работают только в этом сеансе %(brand)s.", "timeline_image_size": "Размер изображения в ленте сообщений", "use_high_contrast": "Высокая контрастность" }, @@ -3119,8 +3102,7 @@ "my_threads_description": "Показывает все обсуждения, в которых вы принимали участие", "open_thread": "Открыть ветку", "show_all_threads": "Показать все обсуждения", - "show_thread_filter": "Показать:", - "unable_to_decrypt": "Невозможно расшифровать сообщение" + "show_thread_filter": "Показать:" }, "time": { "about_day_ago": "около суток назад", @@ -3163,7 +3145,6 @@ }, "creation_summary_dm": "%(creator)s начал(а) этот чат.", "creation_summary_room": "%(creator)s создал(а) и настроил(а) комнату.", - "decryption_failure_blocked": "Отправитель заблокировал получение этого сообщения", "disambiguated_profile": "%(displayName)s (%(matrixId)s)", "download_action_decrypting": "Расшифровка", "download_action_downloading": "Загрузка", @@ -3172,7 +3153,6 @@ "tooltip_sub": "Нажмите для просмотра правок", "tooltip_title": "Изменено %(date)s" }, - "encrypted_historical_messages_unavailable": "Зашифрованные сообщения до этого момента недоступны.", "error_no_renderer": "Не удалось отобразить это событие", "error_rendering_message": "Не удалось загрузить это сообщение", "historical_messages_unavailable": "Вы не можете просматривать более старые сообщения", diff --git a/linked-dependencies/matrix-react-sdk/src/i18n/strings/sk.json b/linked-dependencies/matrix-react-sdk/src/i18n/strings/sk.json index ca7b24e476..b158fb30d5 100644 --- a/linked-dependencies/matrix-react-sdk/src/i18n/strings/sk.json +++ b/linked-dependencies/matrix-react-sdk/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", @@ -2391,13 +2376,11 @@ "custom_theme_success": "Vzhľad pridaný!", "custom_theme_url": "URL adresa vlastného vzhľadu", "font_size": "Veľkosť písma", - "heading": "Upravte svoj vzhľad", "image_size_default": "Predvolené", "image_size_large": "Veľký", "layout_bubbles": "Správy v bublinách", "layout_irc": "IRC (experimentálne)", "match_system_theme": "Prispôsobiť sa vzhľadu systému", - "subheading": "Nastavenia vzhľadu ovplyvnia len túto reláciu %(brand)s.", "timeline_image_size": "Veľkosť obrázku na časovej osi", "use_high_contrast": "Použiť vysoký kontrast" }, @@ -3122,8 +3105,7 @@ "my_threads_description": "Zobrazí všetky vlákna, v ktorých ste sa zúčastnili", "open_thread": "Otvoriť vlákno", "show_all_threads": "Zobraziť všetky vlákna", - "show_thread_filter": "Zobraziť:", - "unable_to_decrypt": "Nie je možné dešifrovať správu" + "show_thread_filter": "Zobraziť:" }, "time": { "about_day_ago": "asi pred jedným dňom", @@ -3166,7 +3148,6 @@ }, "creation_summary_dm": "%(creator)s vytvoril/a túto priamu správu.", "creation_summary_room": "%(creator)s vytvoril a nastavil miestnosť.", - "decryption_failure_blocked": "Odosielateľ vám zablokoval príjem tejto správy", "disambiguated_profile": "%(displayName)s (%(matrixId)s)", "download_action_decrypting": "Dešifrovanie", "download_action_downloading": "Preberanie", @@ -3175,7 +3156,6 @@ "tooltip_sub": "Kliknutím zobrazíte úpravy", "tooltip_title": "Upravené %(date)s" }, - "encrypted_historical_messages_unavailable": "Šifrované správy pred týmto bodom nie sú k dispozícii.", "error_no_renderer": "Nie je možné zobraziť túto udalosť", "error_rendering_message": "Nemožno načítať túto správu", "historical_messages_unavailable": "Nemôžete vidieť predchádzajúce správy", diff --git a/linked-dependencies/matrix-react-sdk/src/i18n/strings/sq.json b/linked-dependencies/matrix-react-sdk/src/i18n/strings/sq.json index c75782696c..eadf91d796 100644 --- a/linked-dependencies/matrix-react-sdk/src/i18n/strings/sq.json +++ b/linked-dependencies/matrix-react-sdk/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.", @@ -2256,13 +2241,11 @@ "custom_theme_success": "Tema u shtua!", "custom_theme_url": "URL teme vetjake", "font_size": "Madhësi shkronjash", - "heading": "Përshtatni dukjen tuaj", "image_size_default": "Parazgjedhje", "image_size_large": "E madhe", "layout_bubbles": "Flluska mesazhesh", "layout_irc": "IRC (Eksperimentale)", "match_system_theme": "Përputhe me temën e sistemit", - "subheading": "Rregullimet e Dukjes prekin vetëm këtë sesion %(brand)s.", "timeline_image_size": "Madhësi figure në rrjedhën kohore", "use_high_contrast": "Përdor kontrast të lartë" }, @@ -2939,8 +2922,7 @@ "my_threads_description": "Shfaq krejt rrjedhat ku keni marrë pjesë", "open_thread": "Hape rrjedhën", "show_all_threads": "Shfaqi krejt rrjedhat", - "show_thread_filter": "Shfaq:", - "unable_to_decrypt": "S’arrihet të shfshehtëzohet mesazhi" + "show_thread_filter": "Shfaq:" }, "time": { "about_day_ago": "rreth një ditë më parë", @@ -2976,7 +2958,6 @@ }, "creation_summary_dm": "%(creator)s krijoi këtë DM.", "creation_summary_room": "%(creator)s krijoi dhe formësoi dhomën.", - "decryption_failure_blocked": "Dërguesi ka bllokuar marrjen e këtij mesazhi nga ju", "download_action_decrypting": "Po shfshehtëzohet", "download_action_downloading": "Shkarkim", "edits": { @@ -2984,7 +2965,6 @@ "tooltip_sub": "Klikoni që të shihni përpunime", "tooltip_title": "Përpunuar më %(date)s" }, - "encrypted_historical_messages_unavailable": "S’mund të kihen më mesazhe të fshehtëzuar para kësaj pike.", "error_no_renderer": "Ky akt s’u shfaq dot", "error_rendering_message": "S’ngarkohet dot ky mesazh", "historical_messages_unavailable": "S’mund të shihni mesazhe më të hershëm", diff --git a/linked-dependencies/matrix-react-sdk/src/i18n/strings/sv.json b/linked-dependencies/matrix-react-sdk/src/i18n/strings/sv.json index 722ef247af..8a6cb95a1f 100644 --- a/linked-dependencies/matrix-react-sdk/src/i18n/strings/sv.json +++ b/linked-dependencies/matrix-react-sdk/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", @@ -2409,13 +2392,11 @@ "custom_theme_success": "Tema tillagt!", "custom_theme_url": "Anpassad tema-URL", "font_size": "Teckenstorlek", - "heading": "Anpassa ditt utseende", "image_size_default": "Standard", "image_size_large": "Stor", "layout_bubbles": "Meddelandebubblor", "layout_irc": "IRC (Experimentellt)", "match_system_theme": "Matcha systemtema", - "subheading": "Utseende inställningar påverkar bara den här %(brand)s-sessionen.", "timeline_image_size": "Bildstorlek i tidslinjen", "use_high_contrast": "Använd högkontrast" }, @@ -3140,12 +3121,10 @@ "my_threads_description": "Visar alla trådar du har medverkat i", "open_thread": "Öppna tråd", "show_all_threads": "Visa alla trådar", - "show_thread_filter": "Visa:", - "unable_to_decrypt": "Kunde inte avkryptera meddelande" + "show_thread_filter": "Visa:" }, "threads_activity_centre": { - "header": "Aktivitet för trådar", - "no_rooms_with_unreads_threads": "Du har inte rum med olästa trådar än." + "header": "Aktivitet för trådar" }, "time": { "about_day_ago": "cirka en dag sedan", @@ -3188,7 +3167,6 @@ }, "creation_summary_dm": "%(creator)s skapade den här DM:en.", "creation_summary_room": "%(creator)s skapade och konfigurerade rummet.", - "decryption_failure_blocked": "Avsändaren har blockerat dig från att ta emot det här meddelandet", "disambiguated_profile": "%(displayName)s (%(matrixId)s)", "download_action_decrypting": "Avkrypterar", "download_action_downloading": "Laddar ner", @@ -3197,7 +3175,6 @@ "tooltip_sub": "Klicka för att visa redigeringar", "tooltip_title": "Redigerat vid %(date)s" }, - "encrypted_historical_messages_unavailable": "Krypterade meddelanden innan den här tidpunkten är otillgängliga.", "error_no_renderer": "Den här händelsen kunde inte visas", "error_rendering_message": "Kan inte ladda det här meddelandet", "historical_messages_unavailable": "Du kan inte se tidigare meddelanden", diff --git a/linked-dependencies/matrix-react-sdk/src/i18n/strings/uk.json b/linked-dependencies/matrix-react-sdk/src/i18n/strings/uk.json index b9b307889e..79378c5c75 100644 --- a/linked-dependencies/matrix-react-sdk/src/i18n/strings/uk.json +++ b/linked-dependencies/matrix-react-sdk/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.", @@ -2328,13 +2313,11 @@ "custom_theme_success": "Тему додано!", "custom_theme_url": "Посилання на власну тему", "font_size": "Розмір шрифту", - "heading": "Налаштування вигляду", "image_size_default": "Типовий", "image_size_large": "Великі", "layout_bubbles": "Бульбашки повідомлень", "layout_irc": "IRC (Експериментально)", "match_system_theme": "Тема системи", - "subheading": "Налаштування вигляду впливають тільки на цей сеанс %(brand)s.", "timeline_image_size": "Розмір зображень у стрічці", "use_high_contrast": "Висока контрастність" }, @@ -3054,8 +3037,7 @@ "my_threads_description": "Показує всі гілки, де ви брали участь", "open_thread": "Відкрити гілку", "show_all_threads": "Показати всі гілки", - "show_thread_filter": "Показати:", - "unable_to_decrypt": "Не вдалося розшифрувати повідомлення" + "show_thread_filter": "Показати:" }, "time": { "about_day_ago": "близько доби тому", @@ -3098,7 +3080,6 @@ }, "creation_summary_dm": "%(creator)s створює цю приватну розмову.", "creation_summary_room": "%(creator)s створює й налаштовує кімнату.", - "decryption_failure_blocked": "Відправник заблокував вам отримання цього повідомлення", "download_action_decrypting": "Розшифрування", "download_action_downloading": "Завантаження", "edits": { @@ -3106,7 +3087,6 @@ "tooltip_sub": "Натисніть, щоб переглянути зміни", "tooltip_title": "Змінено %(date)s" }, - "encrypted_historical_messages_unavailable": "Зашифровані повідомлення до цієї точки недоступні.", "error_no_renderer": "Неможливо показати цю подію", "error_rendering_message": "Не вдалося завантажити це повідомлення", "historical_messages_unavailable": "Ви не можете переглядати давніші повідомлення", diff --git a/linked-dependencies/matrix-react-sdk/src/i18n/strings/vi.json b/linked-dependencies/matrix-react-sdk/src/i18n/strings/vi.json index 7fc2c66760..82bba11048 100644 --- a/linked-dependencies/matrix-react-sdk/src/i18n/strings/vi.json +++ b/linked-dependencies/matrix-react-sdk/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.", @@ -2120,13 +2114,11 @@ "custom_theme_success": "Đã thêm chủ đề!", "custom_theme_url": "URL chủ đề tùy chỉnh", "font_size": "Cỡ chữ", - "heading": "Tùy chỉnh diện mạo của bạn", "image_size_default": "Mặc định", "image_size_large": "Lớn", "layout_bubbles": "Bong bóng tin nhắn", "layout_irc": "IRC (thử nghiệm)", "match_system_theme": "Theo chủ đề hệ thống", - "subheading": "Cài đặt Giao diện chỉ ảnh hưởng đến phiên %(brand)s này.", "timeline_image_size": "Kích thước hình ảnh trong timeline", "use_high_contrast": "Sử dụng độ tương phản cao" }, @@ -2798,8 +2790,7 @@ "my_threads": "Các chủ đề của tôi", "my_threads_description": "Hiển thị tất cả các chủ đề bạn đã tham gia", "show_all_threads": "Hiển thị tất cả chủ đề", - "show_thread_filter": "Hiển thị:", - "unable_to_decrypt": "Không thể giải mã tin nhắn" + "show_thread_filter": "Hiển thị:" }, "time": { "about_day_ago": "khoảng một ngày trước", @@ -2837,7 +2828,6 @@ }, "creation_summary_dm": "%(creator)s đã tạo DM này.", "creation_summary_room": "%(creator)s đã tạo và định cấu hình phòng.", - "decryption_failure_blocked": "Người gửi không cho bạn nhận tin nhắn này", "download_action_decrypting": "Đang giải mã", "download_action_downloading": "Đang tải xuống", "edits": { @@ -2845,7 +2835,6 @@ "tooltip_sub": "Nhấp để xem các chỉnh sửa", "tooltip_title": "Đã chỉnh sửa lúc %(date)s" }, - "encrypted_historical_messages_unavailable": "Các tin nhắn được mã hóa trước thời điểm này không có sẵn.", "error_no_renderer": "Sự kiện này không thể được hiển thị", "error_rendering_message": "Không thể tải tin nhắn này", "historical_messages_unavailable": "Bạn khồng thể thấy các tin nhắn trước", diff --git a/linked-dependencies/matrix-react-sdk/src/i18n/strings/zh_Hans.json b/linked-dependencies/matrix-react-sdk/src/i18n/strings/zh_Hans.json index e03a592144..e53f50442c 100644 --- a/linked-dependencies/matrix-react-sdk/src/i18n/strings/zh_Hans.json +++ b/linked-dependencies/matrix-react-sdk/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 中使用语音和视频的新方式。", @@ -2157,13 +2151,11 @@ "custom_theme_success": "主题已添加!", "custom_theme_url": "自定义主题URL", "font_size": "字体大小", - "heading": "自定义你的外观", "image_size_default": "默认", "image_size_large": "大", "layout_bubbles": "消息气泡", "layout_irc": "IRC(实验性)", "match_system_theme": "匹配系统主题", - "subheading": "外观设置仅会影响此 %(brand)s 会话。", "timeline_image_size": "时间线中的图像大小", "use_high_contrast": "使用高对比度" }, @@ -2836,7 +2828,6 @@ "tooltip_sub": "点击查看编辑历史", "tooltip_title": "编辑于 %(date)s" }, - "encrypted_historical_messages_unavailable": "在此之前的加密消息不可用。", "error_no_renderer": "无法显示此事件", "error_rendering_message": "无法加载此消息", "historical_messages_unavailable": "你不能查看更早的消息", diff --git a/linked-dependencies/matrix-react-sdk/src/i18n/strings/zh_Hant.json b/linked-dependencies/matrix-react-sdk/src/i18n/strings/zh_Hant.json index 0f02cb8ed0..16939a2138 100644 --- a/linked-dependencies/matrix-react-sdk/src/i18n/strings/zh_Hant.json +++ b/linked-dependencies/matrix-react-sdk/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 中透過語音及視訊聊天的新方式。", @@ -2330,13 +2315,11 @@ "custom_theme_success": "已新增佈景主題!", "custom_theme_url": "自訂佈景主題網址", "font_size": "字型大小", - "heading": "自訂您的外觀", "image_size_default": "預設", "image_size_large": "大", "layout_bubbles": "訊息泡泡", "layout_irc": "IRC(實驗性)", "match_system_theme": "符合系統佈景主題", - "subheading": "外觀設定僅會影響此 %(brand)s 工作階段。", "timeline_image_size": "時間軸中的圖片大小", "use_high_contrast": "使用高對比" }, @@ -3049,8 +3032,7 @@ "my_threads_description": "顯示您參與的所有討論串", "open_thread": "開啟討論串", "show_all_threads": "顯示所有討論串", - "show_thread_filter": "顯示:", - "unable_to_decrypt": "無法解密訊息" + "show_thread_filter": "顯示:" }, "time": { "about_day_ago": "大約一天前", @@ -3089,7 +3071,6 @@ }, "creation_summary_dm": "%(creator)s 建立了此私人訊息。", "creation_summary_room": "%(creator)s 建立並設定了聊天室。", - "decryption_failure_blocked": "傳送者已封鎖您,因此無法接收此訊息", "download_action_decrypting": "正在解密", "download_action_downloading": "正在下載", "edits": { @@ -3097,7 +3078,6 @@ "tooltip_sub": "點擊以檢視編輯", "tooltip_title": "編輯於 %(date)s" }, - "encrypted_historical_messages_unavailable": "在此之前的加密訊息不可用。", "error_no_renderer": "此活動無法顯示", "error_rendering_message": "無法載入此訊息", "historical_messages_unavailable": "您看不到更早的訊息", diff --git a/linked-dependencies/matrix-react-sdk/src/models/notificationsettings/reconcileNotificationSettings.ts b/linked-dependencies/matrix-react-sdk/src/models/notificationsettings/reconcileNotificationSettings.ts index 510e7ca3af..49b35a6575 100644 --- a/linked-dependencies/matrix-react-sdk/src/models/notificationsettings/reconcileNotificationSettings.ts +++ b/linked-dependencies/matrix-react-sdk/src/models/notificationsettings/reconcileNotificationSettings.ts @@ -107,7 +107,7 @@ function toStandardRules( if (supportsIntentionalMentions) { standardRules.set(RuleId.IsUserMention, { rule_id: RuleId.IsUserMention, - kind: PushRuleKind.ContentSpecific, + kind: PushRuleKind.Override, enabled: true, actions: userMentionActions, }); @@ -129,7 +129,7 @@ function toStandardRules( if (supportsIntentionalMentions) { standardRules.set(RuleId.IsRoomMention, { rule_id: RuleId.IsRoomMention, - kind: PushRuleKind.ContentSpecific, + kind: PushRuleKind.Override, enabled: true, actions: roomMentionActions, }); diff --git a/linked-dependencies/matrix-react-sdk/src/settings/Settings.tsx b/linked-dependencies/matrix-react-sdk/src/settings/Settings.tsx index 6be0a6b46f..3650e51814 100644 --- a/linked-dependencies/matrix-react-sdk/src/settings/Settings.tsx +++ b/linked-dependencies/matrix-react-sdk/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/linked-dependencies/matrix-react-sdk/src/settings/controllers/AnalyticsController.ts b/linked-dependencies/matrix-react-sdk/src/settings/controllers/AnalyticsController.ts new file mode 100644 index 0000000000..5c127ed3b9 --- /dev/null +++ b/linked-dependencies/matrix-react-sdk/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/linked-dependencies/matrix-react-sdk/src/settings/controllers/SlidingSyncController.ts b/linked-dependencies/matrix-react-sdk/src/settings/controllers/SlidingSyncController.ts index 77bdf7f42f..7d7ca78128 100644 --- a/linked-dependencies/matrix-react-sdk/src/settings/controllers/SlidingSyncController.ts +++ b/linked-dependencies/matrix-react-sdk/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/linked-dependencies/matrix-react-sdk/src/stores/notifications/RoomNotificationStateStore.ts b/linked-dependencies/matrix-react-sdk/src/stores/notifications/RoomNotificationStateStore.ts index f2d10ac4fb..502d2dcce7 100644 --- a/linked-dependencies/matrix-react-sdk/src/stores/notifications/RoomNotificationStateStore.ts +++ b/linked-dependencies/matrix-react-sdk/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/linked-dependencies/matrix-react-sdk/src/stores/oidc/OidcClientStore.ts b/linked-dependencies/matrix-react-sdk/src/stores/oidc/OidcClientStore.ts index 57fe1adcd1..ffe6977390 100644 --- a/linked-dependencies/matrix-react-sdk/src/stores/oidc/OidcClientStore.ts +++ b/linked-dependencies/matrix-react-sdk/src/stores/oidc/OidcClientStore.ts @@ -18,7 +18,11 @@ import { MatrixClient, discoverAndValidateOIDCIssuerWellKnown } from "matrix-js- import { logger } from "matrix-js-sdk/src/logger"; import { OidcClient } from "oidc-client-ts"; -import { getStoredOidcTokenIssuer, getStoredOidcClientId } from "../../utils/oidc/persistOidcSettings"; +import { + getStoredOidcTokenIssuer, + getStoredOidcClientId, + getStoredOidcIdToken, +} from "../../utils/oidc/persistOidcSettings"; import PlatformPeg from "../../PlatformPeg"; /** @@ -58,7 +62,7 @@ export class OidcClientStore { const { accountManagementEndpoint, metadata } = await discoverAndValidateOIDCIssuerWellKnown( authIssuer.issuer, ); - this._accountManagementEndpoint = accountManagementEndpoint ?? metadata.issuer; + this.setAccountManagementEndpoint(accountManagementEndpoint, metadata.issuer); } catch (e) { console.log("Auth issuer not found", e); } @@ -72,6 +76,16 @@ export class OidcClientStore { return !!this.authenticatedIssuer; } + private setAccountManagementEndpoint(endpoint: string | undefined, issuer: string): void { + // if no account endpoint is configured default to the issuer + const url = new URL(endpoint ?? issuer); + const idToken = getStoredOidcIdToken(); + if (idToken) { + url.searchParams.set("id_token_hint", idToken); + } + this._accountManagementEndpoint = url.toString(); + } + public get accountManagementEndpoint(): string | undefined { return this._accountManagementEndpoint; } @@ -150,13 +164,12 @@ export class OidcClientStore { const { accountManagementEndpoint, metadata, signingKeys } = await discoverAndValidateOIDCIssuerWellKnown( this.authenticatedIssuer, ); - // if no account endpoint is configured default to the issuer - this._accountManagementEndpoint = accountManagementEndpoint ?? metadata.issuer; + this.setAccountManagementEndpoint(accountManagementEndpoint, metadata.issuer); this.oidcClient = new OidcClient({ ...metadata, authority: metadata.issuer, signingKeys, - redirect_uri: PlatformPeg.get()!.getSSOCallbackUrl().href, + redirect_uri: PlatformPeg.get()!.getOidcCallbackUrl().href, client_id: clientId, }); } catch (error) { diff --git a/linked-dependencies/matrix-react-sdk/src/stores/room-list/MessagePreviewStore.ts b/linked-dependencies/matrix-react-sdk/src/stores/room-list/MessagePreviewStore.ts index 62dfa6f0f1..a3c44084d5 100644 --- a/linked-dependencies/matrix-react-sdk/src/stores/room-list/MessagePreviewStore.ts +++ b/linked-dependencies/matrix-react-sdk/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"; @@ -32,6 +32,7 @@ import { UPDATE_EVENT } from "../AsyncStore"; import { IPreview } from "./previews/IPreview"; import { VoiceBroadcastInfoEventType } from "../../voice-broadcast"; import { VoiceBroadcastPreview } from "./previews/VoiceBroadcastPreview"; +import shouldHideEvent from "../../shouldHideEvent"; // Emitted event for when a room's preview has changed. First argument will the room for which // the change happened. @@ -184,23 +185,8 @@ export class MessagePreviewStore extends AsyncStoreWithClient { return previewDef?.previewer.getTextFor(event, undefined, true) ?? ""; } - private shouldSkipPreview(event: MatrixEvent, previousEvent?: MatrixEvent): boolean { - if (event.isRelation(RelationType.Replace)) { - if (previousEvent !== undefined) { - // Ignore edits if they don't apply to the latest event in the room to keep the preview on the latest event - const room = this.matrixClient?.getRoom(event.getRoomId()!); - const relatedEvent = room?.findEventById(event.relationEventId!); - if (relatedEvent !== previousEvent) { - return true; - } - } - } - - return false; - } - 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 => { @@ -221,8 +207,6 @@ export class MessagePreviewStore extends AsyncStoreWithClient { this.previews.set(room.roomId, map); } - const previousEventInAny = map.get(TAG_ANY)?.event; - // Set the tags so we know what to generate if (!map.has(TAG_ANY)) map.set(TAG_ANY, null); if (tagId && !map.has(tagId)) map.set(tagId, null); @@ -237,7 +221,8 @@ export class MessagePreviewStore extends AsyncStoreWithClient { const event = events[i]; await this.matrixClient?.decryptEventIfNeeded(event); - + const shouldHide = shouldHideEvent(event); + if (shouldHide) continue; const previewDef = PREVIEWS[event.getType()]; if (!previewDef) continue; if (previewDef.isState && isNullOrUndefined(event.getStateKey())) continue; @@ -245,16 +230,11 @@ export class MessagePreviewStore extends AsyncStoreWithClient { const anyPreviewText = previewDef.previewer.getTextFor(event); if (!anyPreviewText) continue; // not previewable for some reason - if (!this.shouldSkipPreview(event, previousEventInAny)) { - changed = changed || anyPreviewText !== map.get(TAG_ANY)?.text; - map.set(TAG_ANY, mkMessagePreview(anyPreviewText, event)); - } + changed = changed || anyPreviewText !== map.get(TAG_ANY)?.text; + map.set(TAG_ANY, mkMessagePreview(anyPreviewText, event)); const tagsToGenerate = Array.from(map.keys()).filter((t) => t !== TAG_ANY); // we did the any tag above for (const genTagId of tagsToGenerate) { - const previousEventInTag = map.get(genTagId)?.event; - if (this.shouldSkipPreview(event, previousEventInTag)) continue; - const realTagId = genTagId === TAG_ANY ? undefined : genTagId; const preview = previewDef.previewer.getTextFor(event, realTagId); @@ -299,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/linked-dependencies/matrix-react-sdk/src/toasts/IncomingCallToast.tsx b/linked-dependencies/matrix-react-sdk/src/toasts/IncomingCallToast.tsx index 5fc64fc3de..dfdf25efe6 100644 --- a/linked-dependencies/matrix-react-sdk/src/toasts/IncomingCallToast.tsx +++ b/linked-dependencies/matrix-react-sdk/src/toasts/IncomingCallToast.tsx @@ -20,7 +20,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/matrix"; import { MatrixRTCSessionManagerEvents } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSessionManager"; // eslint-disable-next-line no-restricted-imports import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; -import { Button, Tooltip, TooltipProvider } from "@vector-im/compound-web"; +import { Button, Tooltip } from "@vector-im/compound-web"; import { Icon as VideoCallIcon } from "@vector-im/compound-design-tokens/icons/video-call-solid.svg"; import { _t } from "../languageHandler"; @@ -36,13 +36,12 @@ import { LiveContentType, } from "../components/views/rooms/LiveContentSummary"; import { useCall, useJoinCallButtonDisabledTooltip } from "../hooks/useCall"; -import { ButtonEvent } from "../components/views/elements/AccessibleButton"; +import AccessibleButton, { ButtonEvent } from "../components/views/elements/AccessibleButton"; import { useDispatcher } from "../hooks/useDispatcher"; import { ActionPayload } from "../dispatcher/payloads"; import { Call } from "../models/Call"; import { AudioID } from "../LegacyCallHandler"; import { useEventEmitter, useTypedEventEmitter } from "../hooks/useEventEmitter"; -import AccessibleTooltipButton from "../components/views/elements/AccessibleTooltipButton"; import { CallStore, CallStoreEvent } from "../stores/CallStore"; export const getIncomingCallToastKey = (callId: string, roomId: string): string => `call_${callId}_${roomId}`; @@ -168,7 +167,7 @@ export function IncomingCallToast({ notifyEvent }: Props): JSX.Element { ); return ( - + <>
    @@ -195,11 +194,11 @@ export function IncomingCallToast({ notifyEvent }: Props): JSX.Element { disabledTooltip={otherCallIsOngoing ? "Ongoing call" : undefined} />
    - - + ); } diff --git a/linked-dependencies/matrix-react-sdk/src/toasts/IncomingLegacyCallToast.tsx b/linked-dependencies/matrix-react-sdk/src/toasts/IncomingLegacyCallToast.tsx index fbe806e543..8b51096264 100644 --- a/linked-dependencies/matrix-react-sdk/src/toasts/IncomingLegacyCallToast.tsx +++ b/linked-dependencies/matrix-react-sdk/src/toasts/IncomingLegacyCallToast.tsx @@ -25,7 +25,6 @@ import LegacyCallHandler, { LegacyCallHandlerEvent } from "../LegacyCallHandler" import { MatrixClientPeg } from "../MatrixClientPeg"; import { _t } from "../languageHandler"; import RoomAvatar from "../components/views/avatars/RoomAvatar"; -import AccessibleTooltipButton from "../components/views/elements/AccessibleTooltipButton"; import AccessibleButton, { ButtonEvent } from "../components/views/elements/AccessibleButton"; export const getIncomingLegacyCallToastKey = (callId: string): string => `call_${callId}`; @@ -136,7 +135,7 @@ export default class IncomingLegacyCallToast extends React.Component
    - { + if (!getIDBFactory()) { + throw new Error("IndexedDB not available"); + } + idb = await new Promise((resolve, reject) => { + const request = getIDBFactory()!.open("matrix-react-sdk", 1); + request.onerror = reject; + request.onsuccess = (): void => { + resolve(request.result); + }; + request.onupgradeneeded = (): void => { + const db = request.result; + db.createObjectStore("pickleKey"); + db.createObjectStore("account"); + }; + }); +} + +/** + * Loads an item from an IndexedDB table within the underlying `matrix-react-sdk` database. + * + * If IndexedDB access is not supported in the environment, an error is thrown. + * + * @param {string} table The name of the object store in IndexedDB. + * @param {string | string[]} key The key where the data is stored. + * @returns {Promise} A promise that resolves with the retrieved item from the table. + */ +export async function idbLoad(table: string, key: string | string[]): Promise { + if (!idb) { + await idbInit(); + } + return new Promise((resolve, reject) => { + const txn = idb!.transaction([table], "readonly"); + txn.onerror = reject; + + const objectStore = txn.objectStore(table); + const request = objectStore.get(key); + request.onerror = reject; + request.onsuccess = (event): void => { + resolve(request.result); + }; + }); +} + +/** + * Saves data to an IndexedDB table within the underlying `matrix-react-sdk` database. + * + * If IndexedDB access is not supported in the environment, an error is thrown. + * + * @param {string} table The name of the object store in the IndexedDB. + * @param {string|string[]} key The key to use for storing the data. + * @param {*} data The data to be saved. + * @returns {Promise} A promise that resolves when the data is saved successfully. + */ +export async function idbSave(table: string, key: string | string[], data: any): Promise { + if (!idb) { + await idbInit(); + } + return new Promise((resolve, reject) => { + const txn = idb!.transaction([table], "readwrite"); + txn.onerror = reject; + + const objectStore = txn.objectStore(table); + const request = objectStore.put(data, key); + request.onerror = reject; + request.onsuccess = (event): void => { + resolve(); + }; + }); +} + +/** + * Deletes a record from an IndexedDB table within the underlying `matrix-react-sdk` database. + * + * If IndexedDB access is not supported in the environment, an error is thrown. + * + * @param {string} table The name of the object store where the record is stored. + * @param {string|string[]} key The key of the record to be deleted. + * @returns {Promise} A Promise that resolves when the record(s) have been successfully deleted. + */ +export async function idbDelete(table: string, key: string | string[]): Promise { + if (!idb) { + await idbInit(); + } + return new Promise((resolve, reject) => { + const txn = idb!.transaction([table], "readwrite"); + txn.onerror = reject; + + const objectStore = txn.objectStore(table); + const request = objectStore.delete(key); + request.onerror = reject; + request.onsuccess = (): void => { + resolve(); + }; + }); +} diff --git a/linked-dependencies/matrix-react-sdk/src/utils/StorageManager.ts b/linked-dependencies/matrix-react-sdk/src/utils/StorageManager.ts index faf5f7d27a..0cee3d9ef5 100644 --- a/linked-dependencies/matrix-react-sdk/src/utils/StorageManager.ts +++ b/linked-dependencies/matrix-react-sdk/src/utils/StorageManager.ts @@ -19,18 +19,10 @@ import { logger } from "matrix-js-sdk/src/logger"; import SettingsStore from "../settings/SettingsStore"; import { Features } from "../settings/Settings"; +import { getIDBFactory } from "./StorageAccess"; const localStorage = window.localStorage; -// make this lazy in order to make testing easier -function getIndexedDb(): IDBFactory | undefined { - // just *accessing* _indexedDB throws an exception in firefox with - // indexeddb disabled. - try { - return window.indexedDB; - } catch (e) {} -} - // The JS SDK will add a prefix of "matrix-js-sdk:" to the sync store name. const SYNC_STORE_NAME = "riot-web-sync"; const LEGACY_CRYPTO_STORE_NAME = "matrix-js-sdk:crypto"; @@ -68,7 +60,7 @@ export async function checkConsistency(): Promise<{ }> { log("Checking storage consistency"); log(`Local storage supported? ${!!localStorage}`); - log(`IndexedDB supported? ${!!getIndexedDb()}`); + log(`IndexedDB supported? ${!!getIDBFactory()}`); let dataInLocalStorage = false; let dataInCryptoStore = false; @@ -86,7 +78,7 @@ export async function checkConsistency(): Promise<{ error("Local storage cannot be used on this browser"); } - if (getIndexedDb() && localStorage) { + if (getIDBFactory() && localStorage) { const results = await checkSyncStore(); if (!results.healthy) { healthy = false; @@ -96,7 +88,7 @@ export async function checkConsistency(): Promise<{ error("Sync store cannot be used on this browser"); } - if (getIndexedDb()) { + if (getIDBFactory()) { const results = await checkCryptoStore(); dataInCryptoStore = results.exists; if (!results.healthy) { @@ -138,7 +130,7 @@ interface StoreCheck { async function checkSyncStore(): Promise { let exists = false; try { - exists = await IndexedDBStore.exists(getIndexedDb()!, SYNC_STORE_NAME); + exists = await IndexedDBStore.exists(getIDBFactory()!, SYNC_STORE_NAME); log(`Sync store using IndexedDB contains data? ${exists}`); return { exists, healthy: true }; } catch (e) { @@ -152,7 +144,7 @@ async function checkCryptoStore(): Promise { if (await SettingsStore.getValue(Features.RustCrypto)) { // check first if there is a rust crypto store try { - const rustDbExists = await IndexedDBCryptoStore.exists(getIndexedDb()!, RUST_CRYPTO_STORE_NAME); + const rustDbExists = await IndexedDBCryptoStore.exists(getIDBFactory()!, RUST_CRYPTO_STORE_NAME); log(`Rust Crypto store using IndexedDB contains data? ${rustDbExists}`); if (rustDbExists) { @@ -162,7 +154,7 @@ async function checkCryptoStore(): Promise { // No rust store, so let's check if there is a legacy store not yet migrated. try { const legacyIdbExists = await IndexedDBCryptoStore.existsAndIsNotMigrated( - getIndexedDb()!, + getIDBFactory()!, LEGACY_CRYPTO_STORE_NAME, ); log(`Legacy Crypto store using IndexedDB contains non migrated data? ${legacyIdbExists}`); @@ -183,7 +175,7 @@ async function checkCryptoStore(): Promise { let exists = false; // legacy checks try { - exists = await IndexedDBCryptoStore.exists(getIndexedDb()!, LEGACY_CRYPTO_STORE_NAME); + exists = await IndexedDBCryptoStore.exists(getIDBFactory()!, LEGACY_CRYPTO_STORE_NAME); log(`Crypto store using IndexedDB contains data? ${exists}`); return { exists, healthy: true }; } catch (e) { @@ -214,77 +206,3 @@ async function checkCryptoStore(): Promise { export function setCryptoInitialised(cryptoInited: boolean): void { localStorage.setItem("mx_crypto_initialised", String(cryptoInited)); } - -/* Simple wrapper functions around IndexedDB. - */ - -let idb: IDBDatabase | null = null; - -async function idbInit(): Promise { - if (!getIndexedDb()) { - throw new Error("IndexedDB not available"); - } - idb = await new Promise((resolve, reject) => { - const request = getIndexedDb()!.open("matrix-react-sdk", 1); - request.onerror = reject; - request.onsuccess = (): void => { - resolve(request.result); - }; - request.onupgradeneeded = (): void => { - const db = request.result; - db.createObjectStore("pickleKey"); - db.createObjectStore("account"); - }; - }); -} - -export async function idbLoad(table: string, key: string | string[]): Promise { - if (!idb) { - await idbInit(); - } - return new Promise((resolve, reject) => { - const txn = idb!.transaction([table], "readonly"); - txn.onerror = reject; - - const objectStore = txn.objectStore(table); - const request = objectStore.get(key); - request.onerror = reject; - request.onsuccess = (event): void => { - resolve(request.result); - }; - }); -} - -export async function idbSave(table: string, key: string | string[], data: any): Promise { - if (!idb) { - await idbInit(); - } - return new Promise((resolve, reject) => { - const txn = idb!.transaction([table], "readwrite"); - txn.onerror = reject; - - const objectStore = txn.objectStore(table); - const request = objectStore.put(data, key); - request.onerror = reject; - request.onsuccess = (event): void => { - resolve(); - }; - }); -} - -export async function idbDelete(table: string, key: string | string[]): Promise { - if (!idb) { - await idbInit(); - } - return new Promise((resolve, reject) => { - const txn = idb!.transaction([table], "readwrite"); - txn.onerror = reject; - - const objectStore = txn.objectStore(table); - const request = objectStore.delete(key); - request.onerror = reject; - request.onsuccess = (): void => { - resolve(); - }; - }); -} diff --git a/linked-dependencies/matrix-react-sdk/src/utils/exportUtils/HtmlExport.tsx b/linked-dependencies/matrix-react-sdk/src/utils/exportUtils/HtmlExport.tsx index 6f17942007..3edf0f3cc0 100644 --- a/linked-dependencies/matrix-react-sdk/src/utils/exportUtils/HtmlExport.tsx +++ b/linked-dependencies/matrix-react-sdk/src/utils/exportUtils/HtmlExport.tsx @@ -20,7 +20,6 @@ import { Room, MatrixEvent, EventType, MsgType } from "matrix-js-sdk/src/matrix" import { renderToStaticMarkup } from "react-dom/server"; import { logger } from "matrix-js-sdk/src/logger"; import escapeHtml from "escape-html"; -import { TooltipProvider } from "@vector-im/compound-web"; import Exporter from "./Exporter"; import { mediaFromMxc } from "../../customisations/Media"; @@ -284,27 +283,25 @@ export default class HTMLExporter extends Exporter { return (
    - - false} - isTwelveHour={false} - last={false} - lastInSection={false} - permalinkCreator={this.permalinkCreator} - lastSuccessful={false} - isSelectedEvent={false} - showReactions={false} - layout={Layout.Group} - showReadReceipts={false} - /> - + false} + isTwelveHour={false} + last={false} + lastInSection={false} + permalinkCreator={this.permalinkCreator} + lastSuccessful={false} + isSelectedEvent={false} + showReactions={false} + layout={Layout.Group} + showReadReceipts={false} + />
    ); diff --git a/linked-dependencies/matrix-react-sdk/src/utils/oidc/authorize.ts b/linked-dependencies/matrix-react-sdk/src/utils/oidc/authorize.ts index 8bbdd9894a..345fb42969 100644 --- a/linked-dependencies/matrix-react-sdk/src/utils/oidc/authorize.ts +++ b/linked-dependencies/matrix-react-sdk/src/utils/oidc/authorize.ts @@ -40,7 +40,7 @@ export const startOidcLogin = async ( identityServerUrl?: string, isRegistration?: boolean, ): Promise => { - const redirectUri = PlatformPeg.get()!.getSSOCallbackUrl().href; + const redirectUri = PlatformPeg.get()!.getOidcCallbackUrl().href; const nonce = randomString(10); @@ -86,6 +86,8 @@ type CompleteOidcLoginResponse = { accessToken: string; // refreshToken gained from OIDC token issuer, when falsy token cannot be refreshed refreshToken?: string; + // idToken gained from OIDC token issuer + idToken: string; // this client's id as registered with the OIDC issuer clientId: string; // issuer used during authentication @@ -109,6 +111,7 @@ export const completeOidcLogin = async (queryParams: QueryDict): Promise { +export const persistOidcAuthenticatedSettings = (clientId: string, issuer: string, idToken: string): void => { localStorage.setItem(clientIdStorageKey, clientId); localStorage.setItem(tokenIssuerStorageKey, issuer); - localStorage.setItem(idTokenClaimsStorageKey, JSON.stringify(idTokenClaims)); + localStorage.setItem(idTokenStorageKey, idToken); }; /** @@ -59,13 +62,26 @@ export const getStoredOidcClientId = (): string => { }; /** - * Retrieve stored id token claims from local storage - * @returns idtokenclaims or undefined + * Retrieve stored id token claims from stored id token or local storage + * @returns idTokenClaims or undefined */ export const getStoredOidcIdTokenClaims = (): IdTokenClaims | undefined => { + const idToken = getStoredOidcIdToken(); + if (idToken) { + return decodeIdToken(idToken); + } + const idTokenClaims = localStorage.getItem(idTokenClaimsStorageKey); if (!idTokenClaims) { return; } return JSON.parse(idTokenClaims) as IdTokenClaims; }; + +/** + * Retrieve stored id token from local storage + * @returns idToken or undefined + */ +export const getStoredOidcIdToken = (): string | undefined => { + return localStorage.getItem(idTokenStorageKey) ?? undefined; +}; diff --git a/linked-dependencies/matrix-react-sdk/src/utils/pillify.tsx b/linked-dependencies/matrix-react-sdk/src/utils/pillify.tsx index 22fcaec99a..38ac08d6ee 100644 --- a/linked-dependencies/matrix-react-sdk/src/utils/pillify.tsx +++ b/linked-dependencies/matrix-react-sdk/src/utils/pillify.tsx @@ -18,7 +18,6 @@ import React from "react"; import ReactDOM from "react-dom"; import { PushProcessor } from "matrix-js-sdk/src/pushprocessor"; import { MatrixClient, MatrixEvent, RuleId } from "matrix-js-sdk/src/matrix"; -import { TooltipProvider } from "@vector-im/compound-web"; import SettingsStore from "../settings/SettingsStore"; import { Pill, pillRoomNotifLen, pillRoomNotifPos, PillType } from "../components/views/elements/Pill"; @@ -84,9 +83,7 @@ export function pillifyLinks( const pillContainer = document.createElement("span"); const pill = ( - - - + ); ReactDOM.render(pill, pillContainer); @@ -141,14 +138,12 @@ export function pillifyLinks( const pillContainer = document.createElement("span"); const pill = ( - - - + ); ReactDOM.render(pill, pillContainer); diff --git a/linked-dependencies/matrix-react-sdk/src/utils/tokens/pickling.ts b/linked-dependencies/matrix-react-sdk/src/utils/tokens/pickling.ts new file mode 100644 index 0000000000..c113559a69 --- /dev/null +++ b/linked-dependencies/matrix-react-sdk/src/utils/tokens/pickling.ts @@ -0,0 +1,88 @@ +/* +Copyright 2016 Aviral Dasgupta +Copyright 2016 OpenMarket Ltd +Copyright 2018 New Vector Ltd +Copyright 2020, 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 { encodeUnpaddedBase64 } from "matrix-js-sdk/src/matrix"; +import { logger } from "matrix-js-sdk/src/logger"; + +/** + * Calculates the `additionalData` for the AES-GCM key used by the pickling processes. This + * additional data is *not* encrypted, but *is* authenticated. The additional data is constructed + * from the user ID and device ID provided. + * + * The later-constructed pickle key is used to decrypt values, such as access tokens, from IndexedDB. + * + * See https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams for more information on + * `additionalData`. + * + * @param {string} userId The user ID who owns the pickle key. + * @param {string} deviceId The device ID which owns the pickle key. + * @return {Uint8Array} The additional data as a Uint8Array. + */ +export function getPickleAdditionalData(userId: string, deviceId: string): Uint8Array { + const additionalData = new Uint8Array(userId.length + deviceId.length + 1); + for (let i = 0; i < userId.length; i++) { + additionalData[i] = userId.charCodeAt(i); + } + additionalData[userId.length] = 124; // "|" + for (let i = 0; i < deviceId.length; i++) { + additionalData[userId.length + 1 + i] = deviceId.charCodeAt(i); + } + return additionalData; +} + +/** + * Decrypts the provided data into a pickle key and base64-encodes it ready for use elsewhere. + * + * If `data` is undefined in part or in full, returns undefined. + * + * If crypto functions are not available, returns undefined regardless of input. + * + * @param data An object containing the encrypted pickle key data: encrypted payload, initialization vector (IV), and crypto key. Typically loaded from indexedDB. + * @param userId The user ID the pickle key belongs to. + * @param deviceId The device ID the pickle key belongs to. + * @returns A promise that resolves to the encoded pickle key, or undefined if the key cannot be built and encoded. + */ +export async function buildAndEncodePickleKey( + data: { encrypted?: BufferSource; iv?: BufferSource; cryptoKey?: CryptoKey } | undefined, + userId: string, + deviceId: string, +): Promise { + if (!crypto?.subtle) { + return undefined; + } + if (!data || !data.encrypted || !data.iv || !data.cryptoKey) { + return undefined; + } + + try { + const additionalData = getPickleAdditionalData(userId, deviceId); + const pickleKeyBuf = await crypto.subtle.decrypt( + { name: "AES-GCM", iv: data.iv, additionalData }, + data.cryptoKey, + data.encrypted, + ); + if (pickleKeyBuf) { + return encodeUnpaddedBase64(pickleKeyBuf); + } + } catch (e) { + logger.error("Error decrypting pickle key"); + } + + return undefined; +} diff --git a/linked-dependencies/matrix-react-sdk/src/utils/tokens/tokens.ts b/linked-dependencies/matrix-react-sdk/src/utils/tokens/tokens.ts index 864b6b2090..f526775e63 100644 --- a/linked-dependencies/matrix-react-sdk/src/utils/tokens/tokens.ts +++ b/linked-dependencies/matrix-react-sdk/src/utils/tokens/tokens.ts @@ -17,7 +17,7 @@ limitations under the License. import { decryptAES, encryptAES, IEncryptedPayload } from "matrix-js-sdk/src/crypto/aes"; import { logger } from "matrix-js-sdk/src/logger"; -import * as StorageManager from "../StorageManager"; +import * as StorageAccess from "../StorageAccess"; /** * Utility functions related to the storage and retrieval of access tokens @@ -50,10 +50,10 @@ async function pickleKeyToAesKey(pickleKey: string): Promise { for (let i = 0; i < pickleKey.length; i++) { pickleKeyBuffer[i] = pickleKey.charCodeAt(i); } - const hkdfKey = await window.crypto.subtle.importKey("raw", pickleKeyBuffer, "HKDF", false, ["deriveBits"]); + const hkdfKey = await crypto.subtle.importKey("raw", pickleKeyBuffer, "HKDF", false, ["deriveBits"]); pickleKeyBuffer.fill(0); return new Uint8Array( - await window.crypto.subtle.deriveBits( + await crypto.subtle.deriveBits( { name: "HKDF", hash: "SHA-256", @@ -142,7 +142,7 @@ export async function persistTokenInStorage( // Save either the encrypted access token, or the plain access // token if there is no token or we were unable to encrypt (e.g. if the browser doesn't // have WebCrypto). - await StorageManager.idbSave("account", storageKey, encryptedToken || token); + await StorageAccess.idbSave("account", storageKey, encryptedToken || token); } catch (e) { // if we couldn't save to indexedDB, fall back to localStorage. We // store the access token unencrypted since localStorage only saves @@ -155,7 +155,7 @@ export async function persistTokenInStorage( } } else { try { - await StorageManager.idbSave("account", storageKey, token); + await StorageAccess.idbSave("account", storageKey, token); } catch (e) { if (!!token) { localStorage.setItem(storageKey, token); diff --git a/linked-dependencies/matrix-react-sdk/src/utils/tooltipify.tsx b/linked-dependencies/matrix-react-sdk/src/utils/tooltipify.tsx index 8f384e59e4..e3280f7fe2 100644 --- a/linked-dependencies/matrix-react-sdk/src/utils/tooltipify.tsx +++ b/linked-dependencies/matrix-react-sdk/src/utils/tooltipify.tsx @@ -16,7 +16,6 @@ limitations under the License. import React from "react"; import ReactDOM from "react-dom"; -import { TooltipProvider } from "@vector-im/compound-web"; import PlatformPeg from "../PlatformPeg"; import LinkWithTooltip from "../components/views/elements/LinkWithTooltip"; @@ -61,11 +60,9 @@ export function tooltipifyLinks(rootNodes: ArrayLike, ignoredNodes: Ele // wrapping the link with the LinkWithTooltip component, keeping the same children. Ideally we'd do this // without the superfluous span but this is not something React trivially supports at this time. const tooltip = ( - - - - - + + + ); ReactDOM.render(tooltip, node); diff --git a/linked-dependencies/matrix-react-sdk/src/voice-broadcast/components/atoms/VoiceBroadcastHeader.tsx b/linked-dependencies/matrix-react-sdk/src/voice-broadcast/components/atoms/VoiceBroadcastHeader.tsx index 1fe579bfbe..34a2a81a20 100644 --- a/linked-dependencies/matrix-react-sdk/src/voice-broadcast/components/atoms/VoiceBroadcastHeader.tsx +++ b/linked-dependencies/matrix-react-sdk/src/voice-broadcast/components/atoms/VoiceBroadcastHeader.tsx @@ -29,7 +29,6 @@ import Spinner from "../../../components/views/elements/Spinner"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { Action } from "../../../dispatcher/actions"; import dis from "../../../dispatcher/dispatcher"; -import AccessibleTooltipButton from "../../../components/views/elements/AccessibleTooltipButton"; interface VoiceBroadcastHeaderProps { linkToRoom?: boolean; @@ -95,14 +94,14 @@ export const VoiceBroadcastHeader: React.FC = ({ }); const microphoneLine = microphoneLabel && ( - {microphoneLabel} - + ); const onRoomAvatarOrNameClick = (): void => { diff --git a/linked-dependencies/matrix-react-sdk/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip.tsx b/linked-dependencies/matrix-react-sdk/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip.tsx index 026cf40ce1..46ca3f9319 100644 --- a/linked-dependencies/matrix-react-sdk/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip.tsx +++ b/linked-dependencies/matrix-react-sdk/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip.tsx @@ -32,7 +32,7 @@ import { Icon as MicrophoneIcon } from "../../../../res/img/compound/mic-16px.sv import { _t } from "../../../languageHandler"; import { useAudioDeviceSelection } from "../../../hooks/useAudioDeviceSelection"; import { DevicesContextMenu } from "../../../components/views/audio_messages/DevicesContextMenu"; -import AccessibleTooltipButton from "../../../components/views/elements/AccessibleTooltipButton"; +import AccessibleButton from "../../../components/views/elements/AccessibleButton"; interface VoiceBroadcastRecordingPipProps { recording: VoiceBroadcastRecording; @@ -92,12 +92,12 @@ export const VoiceBroadcastRecordingPip: React.FC {toggleControl} - setShowDeviceSelect(true)} title={_t("voip|change_input_device")} > - + } label="Stop Recording" diff --git a/linked-dependencies/matrix-react-sdk/test/DecryptionFailureTracker-test.ts b/linked-dependencies/matrix-react-sdk/test/DecryptionFailureTracker-test.ts index 553d4f4d74..305692fce8 100644 --- a/linked-dependencies/matrix-react-sdk/test/DecryptionFailureTracker-test.ts +++ b/linked-dependencies/matrix-react-sdk/test/DecryptionFailureTracker-test.ts @@ -19,21 +19,11 @@ import { DecryptionFailureCode } from "matrix-js-sdk/src/crypto-api"; import { DecryptionFailureTracker } from "../src/DecryptionFailureTracker"; -class MockDecryptionError extends Error { - public readonly code: string; - - constructor(code?: string) { - super(); - - this.code = code || "MOCK_DECRYPTION_ERROR"; - } -} - -async function createFailedDecryptionEvent() { +async function createFailedDecryptionEvent(code?: DecryptionFailureCode) { return await mkDecryptionFailureMatrixEvent({ roomId: "!room:id", sender: "@alice:example.com", - code: DecryptionFailureCode.UNKNOWN_ERROR, + code: code ?? DecryptionFailureCode.UNKNOWN_ERROR, msg: ":(", }); } @@ -50,9 +40,7 @@ describe("DecryptionFailureTracker", function () { ); tracker.addVisibleEvent(failedDecryptionEvent); - - const err = new MockDecryptionError(); - tracker.eventDecrypted(failedDecryptionEvent, err); + tracker.eventDecrypted(failedDecryptionEvent); // Pretend "now" is Infinity tracker.checkFailures(Infinity); @@ -65,7 +53,9 @@ describe("DecryptionFailureTracker", function () { }); it("tracks a failed decryption with expected raw error for a visible event", async function () { - const failedDecryptionEvent = await createFailedDecryptionEvent(); + const failedDecryptionEvent = await createFailedDecryptionEvent( + DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX, + ); let count = 0; let reportedRawCode = ""; @@ -79,9 +69,7 @@ describe("DecryptionFailureTracker", function () { ); tracker.addVisibleEvent(failedDecryptionEvent); - - const err = new MockDecryptionError("INBOUND_SESSION_MISMATCH_ROOM_ID"); - tracker.eventDecrypted(failedDecryptionEvent, err); + tracker.eventDecrypted(failedDecryptionEvent); // Pretend "now" is Infinity tracker.checkFailures(Infinity); @@ -93,7 +81,7 @@ describe("DecryptionFailureTracker", function () { expect(count).not.toBe(0); // Should add the rawCode to the event context - expect(reportedRawCode).toBe("INBOUND_SESSION_MISMATCH_ROOM_ID"); + expect(reportedRawCode).toBe("OLM_UNKNOWN_MESSAGE_INDEX"); }); it("tracks a failed decryption for an event that becomes visible later", async function () { @@ -106,9 +94,7 @@ describe("DecryptionFailureTracker", function () { () => "UnknownError", ); - const err = new MockDecryptionError(); - tracker.eventDecrypted(failedDecryptionEvent, err); - + tracker.eventDecrypted(failedDecryptionEvent); tracker.addVisibleEvent(failedDecryptionEvent); // Pretend "now" is Infinity @@ -131,8 +117,7 @@ describe("DecryptionFailureTracker", function () { () => "UnknownError", ); - const err = new MockDecryptionError(); - tracker.eventDecrypted(failedDecryptionEvent, err); + tracker.eventDecrypted(failedDecryptionEvent); // Pretend "now" is Infinity tracker.checkFailures(Infinity); @@ -156,9 +141,7 @@ describe("DecryptionFailureTracker", function () { ); tracker.addVisibleEvent(decryptedEvent); - - const err = new MockDecryptionError(); - tracker.eventDecrypted(decryptedEvent, err); + tracker.eventDecrypted(decryptedEvent); // Indicate successful decryption. await decryptExistingEvent(decryptedEvent, { @@ -188,15 +171,14 @@ describe("DecryptionFailureTracker", function () { () => "UnknownError", ); - const err = new MockDecryptionError(); - tracker.eventDecrypted(decryptedEvent, err); + tracker.eventDecrypted(decryptedEvent); // Indicate successful decryption. await decryptExistingEvent(decryptedEvent, { plainType: "m.room.message", plainContent: { body: "success" }, }); - tracker.eventDecrypted(decryptedEvent, null); + tracker.eventDecrypted(decryptedEvent); tracker.addVisibleEvent(decryptedEvent); @@ -222,16 +204,15 @@ describe("DecryptionFailureTracker", function () { tracker.addVisibleEvent(decryptedEvent); // Arbitrary number of failed decryptions for both events - const err = new MockDecryptionError(); - tracker.eventDecrypted(decryptedEvent, err); - tracker.eventDecrypted(decryptedEvent, err); - tracker.eventDecrypted(decryptedEvent, err); - tracker.eventDecrypted(decryptedEvent, err); - tracker.eventDecrypted(decryptedEvent, err); - tracker.eventDecrypted(decryptedEvent2, err); - tracker.eventDecrypted(decryptedEvent2, err); + tracker.eventDecrypted(decryptedEvent); + tracker.eventDecrypted(decryptedEvent); + tracker.eventDecrypted(decryptedEvent); + tracker.eventDecrypted(decryptedEvent); + tracker.eventDecrypted(decryptedEvent); + tracker.eventDecrypted(decryptedEvent2); + tracker.eventDecrypted(decryptedEvent2); tracker.addVisibleEvent(decryptedEvent2); - tracker.eventDecrypted(decryptedEvent2, err); + tracker.eventDecrypted(decryptedEvent2); // Pretend "now" is Infinity tracker.checkFailures(Infinity); @@ -259,8 +240,7 @@ describe("DecryptionFailureTracker", function () { tracker.addVisibleEvent(decryptedEvent); // Indicate decryption - const err = new MockDecryptionError(); - tracker.eventDecrypted(decryptedEvent, err); + tracker.eventDecrypted(decryptedEvent); // Pretend "now" is Infinity tracker.checkFailures(Infinity); @@ -268,7 +248,7 @@ describe("DecryptionFailureTracker", function () { tracker.trackFailures(); // Indicate a second decryption, after having tracked the failure - tracker.eventDecrypted(decryptedEvent, err); + tracker.eventDecrypted(decryptedEvent); tracker.trackFailures(); @@ -292,8 +272,7 @@ describe("DecryptionFailureTracker", function () { tracker.addVisibleEvent(decryptedEvent); // Indicate decryption - const err = new MockDecryptionError(); - tracker.eventDecrypted(decryptedEvent, err); + tracker.eventDecrypted(decryptedEvent); // Pretend "now" is Infinity // NB: This saves to localStorage specific to DFT @@ -312,7 +291,7 @@ describe("DecryptionFailureTracker", function () { //secondTracker.loadTrackedEvents(); - secondTracker.eventDecrypted(decryptedEvent, err); + secondTracker.eventDecrypted(decryptedEvent); secondTracker.checkFailures(Infinity); secondTracker.trackFailures(); @@ -326,25 +305,27 @@ describe("DecryptionFailureTracker", function () { // @ts-ignore access to private constructor const tracker = new DecryptionFailureTracker( (total: number, errorCode: string) => (counts[errorCode] = (counts[errorCode] || 0) + total), - (error: string) => (error === "UnknownError" ? "UnknownError" : "OlmKeysNotSentError"), + (error: DecryptionFailureCode) => + error === DecryptionFailureCode.UNKNOWN_ERROR ? "UnknownError" : "OlmKeysNotSentError", ); - const decryptedEvent1 = await createFailedDecryptionEvent(); - const decryptedEvent2 = await createFailedDecryptionEvent(); - const decryptedEvent3 = await createFailedDecryptionEvent(); - - const error1 = new MockDecryptionError("UnknownError"); - const error2 = new MockDecryptionError("OlmKeysNotSentError"); + const decryptedEvent1 = await createFailedDecryptionEvent(DecryptionFailureCode.UNKNOWN_ERROR); + const decryptedEvent2 = await createFailedDecryptionEvent( + DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID, + ); + const decryptedEvent3 = await createFailedDecryptionEvent( + DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID, + ); tracker.addVisibleEvent(decryptedEvent1); tracker.addVisibleEvent(decryptedEvent2); tracker.addVisibleEvent(decryptedEvent3); // One failure of ERROR_CODE_1, and effectively two for ERROR_CODE_2 - tracker.eventDecrypted(decryptedEvent1, error1); - tracker.eventDecrypted(decryptedEvent2, error2); - tracker.eventDecrypted(decryptedEvent2, error2); - tracker.eventDecrypted(decryptedEvent3, error2); + tracker.eventDecrypted(decryptedEvent1); + tracker.eventDecrypted(decryptedEvent2); + tracker.eventDecrypted(decryptedEvent2); + tracker.eventDecrypted(decryptedEvent3); // Pretend "now" is Infinity tracker.checkFailures(Infinity); @@ -364,21 +345,19 @@ describe("DecryptionFailureTracker", function () { (_errorCode: string) => "OlmUnspecifiedError", ); - const decryptedEvent1 = await createFailedDecryptionEvent(); - const decryptedEvent2 = await createFailedDecryptionEvent(); - const decryptedEvent3 = await createFailedDecryptionEvent(); - - const error1 = new MockDecryptionError("ERROR_CODE_1"); - const error2 = new MockDecryptionError("ERROR_CODE_2"); - const error3 = new MockDecryptionError("ERROR_CODE_3"); + const decryptedEvent1 = await createFailedDecryptionEvent( + DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID, + ); + const decryptedEvent2 = await createFailedDecryptionEvent(DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX); + const decryptedEvent3 = await createFailedDecryptionEvent(DecryptionFailureCode.UNKNOWN_ERROR); tracker.addVisibleEvent(decryptedEvent1); tracker.addVisibleEvent(decryptedEvent2); tracker.addVisibleEvent(decryptedEvent3); - tracker.eventDecrypted(decryptedEvent1, error1); - tracker.eventDecrypted(decryptedEvent2, error2); - tracker.eventDecrypted(decryptedEvent3, error3); + tracker.eventDecrypted(decryptedEvent1); + tracker.eventDecrypted(decryptedEvent2); + tracker.eventDecrypted(decryptedEvent3); // Pretend "now" is Infinity tracker.checkFailures(Infinity); @@ -397,20 +376,72 @@ describe("DecryptionFailureTracker", function () { (errorCode: string) => Array.from(errorCode).reverse().join(""), ); - const decryptedEvent = await createFailedDecryptionEvent(); + const decryptedEvent = await createFailedDecryptionEvent(DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX); + tracker.addVisibleEvent(decryptedEvent); + tracker.eventDecrypted(decryptedEvent); - const error = new MockDecryptionError("ERROR_CODE_1"); + // Pretend "now" is Infinity + tracker.checkFailures(Infinity); - tracker.addVisibleEvent(decryptedEvent); + tracker.trackFailures(); + + // should track remapped error code + expect(counts["XEDNI_EGASSEM_NWONKNU_MLO"]).toBe(1); + }); - tracker.eventDecrypted(decryptedEvent, error); + 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(); - // should track remapped error code - expect(counts["1_EDOC_RORRE"]).toBe(1); + expect(errorCodes).toEqual([ + "OlmKeysNotSentError", + "OlmIndexError", + "HistoricalMessage", + "HistoricalMessage", + "HistoricalMessage", + "ExpectedDueToMembership", + "UnknownError", + ]); }); }); diff --git a/linked-dependencies/matrix-react-sdk/test/HtmlUtils-test.tsx b/linked-dependencies/matrix-react-sdk/test/HtmlUtils-test.tsx index f177fc1b47..d9e75faaa9 100644 --- a/linked-dependencies/matrix-react-sdk/test/HtmlUtils-test.tsx +++ b/linked-dependencies/matrix-react-sdk/test/HtmlUtils-test.tsx @@ -166,6 +166,16 @@ describe("bodyToHtml", () => { }); expect(html).toMatchSnapshot(); }); + + it("should not mangle divs", () => { + const html = getHtml({ + body: "hello world", + msgtype: "m.text", + formatted_body: "

    hello

    world
    ", + format: "org.matrix.custom.html", + }); + expect(html).toMatchSnapshot(); + }); }); }); diff --git a/linked-dependencies/matrix-react-sdk/test/Lifecycle-test.ts b/linked-dependencies/matrix-react-sdk/test/Lifecycle-test.ts index fac59b235a..4a6122f470 100644 --- a/linked-dependencies/matrix-react-sdk/test/Lifecycle-test.ts +++ b/linked-dependencies/matrix-react-sdk/test/Lifecycle-test.ts @@ -26,7 +26,7 @@ import StorageEvictedDialog from "../src/components/views/dialogs/StorageEvicted import { logout, restoreFromLocalStorage, setLoggedIn } from "../src/Lifecycle"; import { MatrixClientPeg } from "../src/MatrixClientPeg"; import Modal from "../src/Modal"; -import * as StorageManager from "../src/utils/StorageManager"; +import * as StorageAccess from "../src/utils/StorageAccess"; import { flushPromises, getMockClientWithEventEmitter, mockClientMethodsUser, mockPlatformPeg } from "./test-utils"; import { OidcClientStore } from "../src/stores/oidc/OidcClientStore"; import { makeDelegatedAuthConfig } from "./test-utils/oidc"; @@ -128,13 +128,13 @@ describe("Lifecycle", () => { }; const initIdbMock = (mockStore: Record> = {}): void => { - jest.spyOn(StorageManager, "idbLoad") + jest.spyOn(StorageAccess, "idbLoad") .mockClear() .mockImplementation( // @ts-ignore mock type async (table: string, key: string) => mockStore[table]?.[key] ?? null, ); - jest.spyOn(StorageManager, "idbSave") + jest.spyOn(StorageAccess, "idbSave") .mockClear() .mockImplementation( // @ts-ignore mock type @@ -144,7 +144,7 @@ describe("Lifecycle", () => { mockStore[tableKey] = table; }, ); - jest.spyOn(StorageManager, "idbDelete").mockClear().mockResolvedValue(undefined); + jest.spyOn(StorageAccess, "idbDelete").mockClear().mockResolvedValue(undefined); }; const homeserverUrl = "https://server.org"; @@ -258,16 +258,16 @@ describe("Lifecycle", () => { expect(localStorage.setItem).toHaveBeenCalledWith("mx_is_guest", "false"); expect(localStorage.setItem).toHaveBeenCalledWith("mx_device_id", deviceId); - expect(StorageManager.idbSave).toHaveBeenCalledWith("account", "mx_access_token", accessToken); + expect(StorageAccess.idbSave).toHaveBeenCalledWith("account", "mx_access_token", accessToken); // dont put accessToken in localstorage when we have idb expect(localStorage.setItem).not.toHaveBeenCalledWith("mx_access_token", accessToken); }); it("should persist access token when idb is not available", async () => { - jest.spyOn(StorageManager, "idbSave").mockRejectedValue("oups"); + jest.spyOn(StorageAccess, "idbSave").mockRejectedValue("oups"); expect(await restoreFromLocalStorage()).toEqual(true); - expect(StorageManager.idbSave).toHaveBeenCalledWith("account", "mx_access_token", accessToken); + expect(StorageAccess.idbSave).toHaveBeenCalledWith("account", "mx_access_token", accessToken); // put accessToken in localstorage as fallback expect(localStorage.setItem).toHaveBeenCalledWith("mx_access_token", accessToken); }); @@ -316,11 +316,7 @@ describe("Lifecycle", () => { // refresh token from storage is re-persisted expect(localStorage.setItem).toHaveBeenCalledWith("mx_has_refresh_token", "true"); - expect(StorageManager.idbSave).toHaveBeenCalledWith( - "account", - "mx_refresh_token", - refreshToken, - ); + expect(StorageAccess.idbSave).toHaveBeenCalledWith("account", "mx_refresh_token", refreshToken); }); it("should create new matrix client with credentials", async () => { @@ -359,7 +355,7 @@ describe("Lifecycle", () => { expect(localStorage.setItem).toHaveBeenCalledWith("mx_has_access_token", "true"); // token encrypted and persisted - expect(StorageManager.idbSave).toHaveBeenCalledWith( + expect(StorageAccess.idbSave).toHaveBeenCalledWith( "account", "mx_access_token", encryptedTokenShapedObject, @@ -368,7 +364,7 @@ describe("Lifecycle", () => { it("should persist access token when idb is not available", async () => { // dont fail for pickle key persist - jest.spyOn(StorageManager, "idbSave").mockImplementation( + jest.spyOn(StorageAccess, "idbSave").mockImplementation( async (table: string, key: string | string[]) => { if (table === "account" && key === "mx_access_token") { throw new Error("oups"); @@ -378,7 +374,7 @@ describe("Lifecycle", () => { expect(await restoreFromLocalStorage()).toEqual(true); - expect(StorageManager.idbSave).toHaveBeenCalledWith( + expect(StorageAccess.idbSave).toHaveBeenCalledWith( "account", "mx_access_token", encryptedTokenShapedObject, @@ -422,7 +418,7 @@ describe("Lifecycle", () => { // refresh token from storage is re-persisted expect(localStorage.setItem).toHaveBeenCalledWith("mx_has_refresh_token", "true"); - expect(StorageManager.idbSave).toHaveBeenCalledWith( + expect(StorageAccess.idbSave).toHaveBeenCalledWith( "account", "mx_refresh_token", encryptedTokenShapedObject, @@ -502,7 +498,7 @@ describe("Lifecycle", () => { expect(localStorage.setItem).toHaveBeenCalledWith("mx_is_guest", "false"); expect(localStorage.setItem).toHaveBeenCalledWith("mx_device_id", deviceId); - expect(StorageManager.idbSave).toHaveBeenCalledWith("account", "mx_access_token", accessToken); + expect(StorageAccess.idbSave).toHaveBeenCalledWith("account", "mx_access_token", accessToken); // dont put accessToken in localstorage when we have idb expect(localStorage.setItem).not.toHaveBeenCalledWith("mx_access_token", accessToken); }); @@ -513,14 +509,14 @@ describe("Lifecycle", () => { refreshToken, }); - expect(StorageManager.idbSave).toHaveBeenCalledWith("account", "mx_access_token", accessToken); - expect(StorageManager.idbSave).toHaveBeenCalledWith("account", "mx_refresh_token", refreshToken); + expect(StorageAccess.idbSave).toHaveBeenCalledWith("account", "mx_access_token", accessToken); + expect(StorageAccess.idbSave).toHaveBeenCalledWith("account", "mx_refresh_token", refreshToken); // dont put accessToken in localstorage when we have idb expect(localStorage.setItem).not.toHaveBeenCalledWith("mx_access_token", accessToken); }); it("should remove any access token from storage when there is none in credentials and idb save fails", async () => { - jest.spyOn(StorageManager, "idbSave").mockRejectedValue("oups"); + jest.spyOn(StorageAccess, "idbSave").mockRejectedValue("oups"); await setLoggedIn({ ...credentials, // @ts-ignore @@ -534,7 +530,7 @@ describe("Lifecycle", () => { it("should clear stores", async () => { await setLoggedIn(credentials); - expect(StorageManager.idbDelete).toHaveBeenCalledWith("account", "mx_access_token"); + expect(StorageAccess.idbDelete).toHaveBeenCalledWith("account", "mx_access_token"); expect(sessionStorage.clear).toHaveBeenCalled(); expect(mockClient.clearStores).toHaveBeenCalled(); }); @@ -566,7 +562,7 @@ describe("Lifecycle", () => { }); // unpickled access token saved - expect(StorageManager.idbSave).toHaveBeenCalledWith("account", "mx_access_token", accessToken); + expect(StorageAccess.idbSave).toHaveBeenCalledWith("account", "mx_access_token", accessToken); expect(mockPlatform.createPickleKey).not.toHaveBeenCalled(); }); @@ -585,16 +581,12 @@ describe("Lifecycle", () => { expect(localStorage.setItem).toHaveBeenCalledWith("mx_device_id", deviceId); expect(localStorage.setItem).toHaveBeenCalledWith("mx_has_pickle_key", "true"); - expect(StorageManager.idbSave).toHaveBeenCalledWith( + expect(StorageAccess.idbSave).toHaveBeenCalledWith( "account", "mx_access_token", encryptedTokenShapedObject, ); - expect(StorageManager.idbSave).toHaveBeenCalledWith( - "pickleKey", - [userId, deviceId], - expect.any(Object), - ); + expect(StorageAccess.idbSave).toHaveBeenCalledWith("pickleKey", [userId, deviceId], expect.any(Object)); // dont put accessToken in localstorage when we have idb expect(localStorage.setItem).not.toHaveBeenCalledWith("mx_access_token", accessToken); }); @@ -604,12 +596,12 @@ describe("Lifecycle", () => { await setLoggedIn(credentials); // persist the unencrypted token - expect(StorageManager.idbSave).toHaveBeenCalledWith("account", "mx_access_token", accessToken); + expect(StorageAccess.idbSave).toHaveBeenCalledWith("account", "mx_access_token", accessToken); }); it("should persist token in localStorage when idb fails to save token", async () => { // dont fail for pickle key persist - jest.spyOn(StorageManager, "idbSave").mockImplementation( + jest.spyOn(StorageAccess, "idbSave").mockImplementation( async (table: string, key: string | string[]) => { if (table === "account" && key === "mx_access_token") { throw new Error("oups"); @@ -624,7 +616,7 @@ describe("Lifecycle", () => { it("should remove any access token from storage when there is none in credentials and idb save fails", async () => { // dont fail for pickle key persist - jest.spyOn(StorageManager, "idbSave").mockImplementation( + jest.spyOn(StorageAccess, "idbSave").mockImplementation( async (table: string, key: string | string[]) => { if (table === "account" && key === "mx_access_token") { throw new Error("oups"); @@ -665,13 +657,8 @@ describe("Lifecycle", () => { const issuer = "https://auth.com/"; const delegatedAuthConfig = makeDelegatedAuthConfig(issuer); - const idTokenClaims = { - aud: "123", - iss: issuer, - sub: "123", - exp: 123, - iat: 456, - }; + const idToken = + "eyJhbGciOiJSUzI1NiIsImtpZCI6Imh4ZEhXb0Y5bW4ifQ.eyJzdWIiOiIwMUhQUDJGU0JZREU5UDlFTU04REQ3V1pIUiIsImlzcyI6Imh0dHBzOi8vYXV0aC1vaWRjLmxhYi5lbGVtZW50LmRldi8iLCJpYXQiOjE3MTUwNzE5ODUsImF1dGhfdGltZSI6MTcwNzk5MDMxMiwiY19oYXNoIjoidGt5R1RhUjU5aTk3YXoyTU4yMGdidyIsImV4cCI6MTcxNTA3NTU4NSwibm9uY2UiOiJxaXhwM0hFMmVaIiwiYXVkIjoiMDFIWDk0Mlg3QTg3REgxRUs2UDRaNjI4WEciLCJhdF9oYXNoIjoiNFlFUjdPRlVKTmRTeEVHV2hJUDlnZyJ9.HxODneXvSTfWB5Vc4cf7b8GiN2gdwUuTiyVqZuupWske2HkZiJZUt5Lsxg9BW3gz28POkE0Ln17snlkmy02B_AD3DQxKOOxQCzIIARHdfFvZxgGWsMdFcVQZDW7rtXcqgj-SpVaUQ_8acsgxSrz_DF2o0O4tto0PT6wVUiw8KlBmgWTscWPeAWe-39T-8EiQ8Wi16h6oSPcz2NzOQ7eOM_S9fDkOorgcBkRGLl1nrahrPSdWJSGAeruk5mX4YxN714YThFDyEA2t9YmKpjaiSQ2tT-Xkd7tgsZqeirNs2ni9mIiFX3bRX6t2AhUNzA7MaX9ZyizKGa6go3BESO_oDg"; beforeAll(() => { fetchMock.get( @@ -690,7 +677,7 @@ describe("Lifecycle", () => { beforeEach(() => { initSessionStorageMock(); // set values in session storage as they would be after a successful oidc authentication - persistOidcAuthenticatedSettings(clientId, issuer, idTokenClaims); + persistOidcAuthenticatedSettings(clientId, issuer, idToken); }); it("should not try to create a token refresher without a refresh token", async () => { @@ -720,7 +707,7 @@ describe("Lifecycle", () => { clientId, // @ts-ignore set undefined issuer undefined, - idTokenClaims, + idToken, ); await setLoggedIn({ ...credentials, @@ -752,7 +739,7 @@ describe("Lifecycle", () => { it("should create a client when creating token refresher fails", async () => { // set invalid value in session storage for a malformed oidc authentication - persistOidcAuthenticatedSettings(null as any, issuer, idTokenClaims); + persistOidcAuthenticatedSettings(null as any, issuer, idToken); // succeeded expect( diff --git a/linked-dependencies/matrix-react-sdk/test/PosthogAnalytics-test.ts b/linked-dependencies/matrix-react-sdk/test/PosthogAnalytics-test.ts index 748c8f17f6..c131e536d1 100644 --- a/linked-dependencies/matrix-react-sdk/test/PosthogAnalytics-test.ts +++ b/linked-dependencies/matrix-react-sdk/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/linked-dependencies/matrix-react-sdk/test/SlidingSyncManager-test.ts b/linked-dependencies/matrix-react-sdk/test/SlidingSyncManager-test.ts index 76ebd8f15c..9cb30c5b6f 100644 --- a/linked-dependencies/matrix-react-sdk/test/SlidingSyncManager-test.ts +++ b/linked-dependencies/matrix-react-sdk/test/SlidingSyncManager-test.ts @@ -17,9 +17,12 @@ limitations under the License. import { SlidingSync } from "matrix-js-sdk/src/sliding-sync"; import { mocked } from "jest-mock"; import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; +import fetchMockJest from "fetch-mock-jest"; 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); @@ -37,6 +40,8 @@ describe("SlidingSyncManager", () => { mocked(client.getRoom).mockReturnValue(null); manager.configure(client, "invalid"); manager.slidingSync = slidingSync; + fetchMockJest.reset(); + fetchMockJest.get("https://proxy/client/server.json", {}); }); describe("setRoomVisible", () => { @@ -231,4 +236,94 @@ describe("SlidingSyncManager", () => { ); }); }); + describe("checkSupport", () => { + beforeEach(() => { + SlidingSyncController.serverSupportsSlidingSync = false; + jest.spyOn(manager, "getProxyFromWellKnown").mockResolvedValue("https://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(); + }); + it("should query well-known on server_name not baseUrl", async () => { + fetchMockJest.get("https://matrix.org/.well-known/matrix/client", { + "m.homeserver": { + base_url: "https://matrix-client.matrix.org", + server: "matrix.org", + }, + "org.matrix.msc3575.proxy": { + url: "https://proxy/", + }, + }); + fetchMockJest.get("https://matrix-client.matrix.org/_matrix/client/versions", { versions: ["v1.4"] }); + + mocked(manager.getProxyFromWellKnown).mockRestore(); + jest.spyOn(manager, "nativeSlidingSyncSupport").mockResolvedValue(false); + expect(SlidingSyncController.serverSupportsSlidingSync).toBeFalsy(); + await manager.checkSupport(client); + expect(SlidingSyncController.serverSupportsSlidingSync).toBeTruthy(); + expect(fetchMockJest).not.toHaveFetched("https://matrix-client.matrix.org/.well-known/matrix/client"); + }); + }); + describe("nativeSlidingSyncSupport", () => { + beforeEach(() => { + SlidingSyncController.serverSupportsSlidingSync = false; + }); + it("should make an OPTIONS request to avoid unintended side effects", async () => { + // See https://github.com/element-hq/element-web/issues/27426 + + const unstableSpy = jest + .spyOn(client, "doesServerSupportUnstableFeature") + .mockImplementation(async (feature: string) => { + expect(feature).toBe("org.matrix.msc3575"); + return true; + }); + const proxySpy = jest.spyOn(manager, "getProxyFromWellKnown").mockResolvedValue("https://proxy/"); + + expect(SlidingSyncController.serverSupportsSlidingSync).toBeFalsy(); + await manager.checkSupport(client); // first thing it does is call nativeSlidingSyncSupport + expect(proxySpy).not.toHaveBeenCalled(); + expect(unstableSpy).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("https://proxy/"); + await manager.setup(client); + expect(manager.configure).toHaveBeenCalled(); + expect(manager.configure).toHaveBeenCalledWith(client, "https://proxy/"); + expect(manager.startSpidering).toHaveBeenCalled(); + }); + it("uses the legacy `feature_sliding_sync_proxy_url` if it was set", async () => { + jest.spyOn(manager, "getProxyFromWellKnown").mockResolvedValue("https://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/linked-dependencies/matrix-react-sdk/test/__snapshots__/HtmlUtils-test.tsx.snap b/linked-dependencies/matrix-react-sdk/test/__snapshots__/HtmlUtils-test.tsx.snap index c4d91467c0..c33cc46433 100644 --- a/linked-dependencies/matrix-react-sdk/test/__snapshots__/HtmlUtils-test.tsx.snap +++ b/linked-dependencies/matrix-react-sdk/test/__snapshots__/HtmlUtils-test.tsx.snap @@ -2,6 +2,8 @@ exports[`bodyToHtml feature_latex_maths should not mangle code blocks 1`] = `"

    hello

    $\\xi$

    world

    "`; +exports[`bodyToHtml feature_latex_maths should not mangle divs 1`] = `"

    hello

    world
    "`; + exports[`bodyToHtml feature_latex_maths should render block katex 1`] = `"

    hello

    ξ\\xi

    world

    "`; exports[`bodyToHtml feature_latex_maths should render inline katex 1`] = `"hello ξ\\xi world"`; diff --git a/linked-dependencies/matrix-react-sdk/test/components/structures/MatrixChat-test.tsx b/linked-dependencies/matrix-react-sdk/test/components/structures/MatrixChat-test.tsx index 908c7a7c04..00a44c44bd 100644 --- a/linked-dependencies/matrix-react-sdk/test/components/structures/MatrixChat-test.tsx +++ b/linked-dependencies/matrix-react-sdk/test/components/structures/MatrixChat-test.tsx @@ -29,7 +29,7 @@ import { defer, sleep } from "matrix-js-sdk/src/utils"; import { UserVerificationStatus } from "matrix-js-sdk/src/crypto-api"; import MatrixChat from "../../../src/components/structures/MatrixChat"; -import * as StorageManager from "../../../src/utils/StorageManager"; +import * as StorageAccess from "../../../src/utils/StorageAccess"; import defaultDispatcher from "../../../src/dispatcher/dispatcher"; import { Action } from "../../../src/dispatcher/actions"; import { UserTab } from "../../../src/components/views/dialogs/UserTab"; @@ -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(), @@ -219,8 +220,8 @@ describe("", () => { headers: { "content-type": "application/json" }, }); - jest.spyOn(StorageManager, "idbLoad").mockReset(); - jest.spyOn(StorageManager, "idbSave").mockResolvedValue(undefined); + jest.spyOn(StorageAccess, "idbLoad").mockReset(); + jest.spyOn(StorageAccess, "idbSave").mockResolvedValue(undefined); jest.spyOn(defaultDispatcher, "dispatch").mockClear(); jest.spyOn(defaultDispatcher, "fire").mockClear(); @@ -283,6 +284,7 @@ describe("", () => { const tokenResponse: BearerTokenResponse = { access_token: accessToken, refresh_token: "def456", + id_token: "ghi789", scope: "test", token_type: "Bearer", expires_at: 12345, @@ -458,7 +460,7 @@ describe("", () => { describe("when login succeeds", () => { beforeEach(() => { - jest.spyOn(StorageManager, "idbLoad").mockImplementation( + jest.spyOn(StorageAccess, "idbLoad").mockImplementation( async (_table: string, key: string | string[]) => (key === "mx_access_token" ? accessToken : null), ); loginClient.getProfileInfo.mockResolvedValue({ @@ -552,7 +554,7 @@ describe("", () => { beforeEach(async () => { await populateStorageForSession(); - jest.spyOn(StorageManager, "idbLoad").mockImplementation(async (table, key) => { + jest.spyOn(StorageAccess, "idbLoad").mockImplementation(async (table, key) => { const safeKey = Array.isArray(key) ? key[0] : key; return mockidb[table]?.[safeKey]; }); @@ -627,6 +629,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", () => { @@ -861,7 +869,7 @@ describe("", () => { mockClient.loginFlows.mockResolvedValue({ flows: [{ type: "m.login.password" }] }); - jest.spyOn(StorageManager, "idbLoad").mockImplementation(async (table, key) => { + jest.spyOn(StorageAccess, "idbLoad").mockImplementation(async (table, key) => { const safeKey = Array.isArray(key) ? key[0] : key; return mockidb[table]?.[safeKey]; }); @@ -1157,7 +1165,7 @@ describe("", () => { describe("when login succeeds", () => { beforeEach(() => { - jest.spyOn(StorageManager, "idbLoad").mockImplementation( + jest.spyOn(StorageAccess, "idbLoad").mockImplementation( async (_table: string, key: string | string[]) => { if (key === "mx_access_token") { return accessToken as any; diff --git a/linked-dependencies/matrix-react-sdk/test/components/structures/MessagePanel-test.tsx b/linked-dependencies/matrix-react-sdk/test/components/structures/MessagePanel-test.tsx index 3513cee91f..23f094d4ab 100644 --- a/linked-dependencies/matrix-react-sdk/test/components/structures/MessagePanel-test.tsx +++ b/linked-dependencies/matrix-react-sdk/test/components/structures/MessagePanel-test.tsx @@ -20,7 +20,6 @@ import { EventEmitter } from "events"; import { MatrixEvent, Room, RoomMember, Thread, ReceiptType } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; import { render } from "@testing-library/react"; -import { TooltipProvider } from "@vector-im/compound-web"; import MessagePanel, { shouldFormContinuation } from "../../../src/components/structures/MessagePanel"; import SettingsStore from "../../../src/settings/SettingsStore"; @@ -99,9 +98,7 @@ describe("MessagePanel", function () { const getComponent = (props = {}, roomContext: Partial = {}) => ( - - - + ); diff --git a/linked-dependencies/matrix-react-sdk/test/components/structures/RightPanel-test.tsx b/linked-dependencies/matrix-react-sdk/test/components/structures/RightPanel-test.tsx index 49eea96410..d7f5fbef11 100644 --- a/linked-dependencies/matrix-react-sdk/test/components/structures/RightPanel-test.tsx +++ b/linked-dependencies/matrix-react-sdk/test/components/structures/RightPanel-test.tsx @@ -19,7 +19,6 @@ import { render, screen, waitFor } from "@testing-library/react"; import { jest } from "@jest/globals"; import { mocked, MockedObject } from "jest-mock"; import { MatrixClient } from "matrix-js-sdk/src/matrix"; -import { TooltipProvider } from "@vector-im/compound-web"; import _RightPanel from "../../../src/components/structures/RightPanel"; import { MatrixClientPeg } from "../../../src/MatrixClientPeg"; @@ -123,7 +122,6 @@ describe("RightPanel", () => { resizeNotifier={resizeNotifier} permalinkCreator={new RoomPermalinkCreator(r1, r1.roomId)} />, - { wrapper: TooltipProvider }, ); // Wait for RPS room 1 updates to fire const rpsUpdated = waitForRpsUpdate(); diff --git a/linked-dependencies/matrix-react-sdk/test/components/structures/RoomView-test.tsx b/linked-dependencies/matrix-react-sdk/test/components/structures/RoomView-test.tsx index d0d12d7105..31f5c896ae 100644 --- a/linked-dependencies/matrix-react-sdk/test/components/structures/RoomView-test.tsx +++ b/linked-dependencies/matrix-react-sdk/test/components/structures/RoomView-test.tsx @@ -33,7 +33,6 @@ import { KnownMembership } from "matrix-js-sdk/src/types"; import { MEGOLM_ALGORITHM } from "matrix-js-sdk/src/crypto/olmlib"; import { fireEvent, render, screen, RenderResult, waitForElementToBeRemoved, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import { TooltipProvider } from "@vector-im/compound-web"; import { stubClient, @@ -145,7 +144,6 @@ describe("RoomView", () => { wrappedRef={ref as any} /> , - { wrapper: TooltipProvider }, ); await flushPromises(); return roomView; @@ -183,7 +181,6 @@ describe("RoomView", () => { onRegistered={jest.fn()} /> , - { wrapper: TooltipProvider }, ); await flushPromises(); return roomView; diff --git a/linked-dependencies/matrix-react-sdk/test/components/structures/SpaceHierarchy-test.tsx b/linked-dependencies/matrix-react-sdk/test/components/structures/SpaceHierarchy-test.tsx index 653b24fc53..d25f8a6f12 100644 --- a/linked-dependencies/matrix-react-sdk/test/components/structures/SpaceHierarchy-test.tsx +++ b/linked-dependencies/matrix-react-sdk/test/components/structures/SpaceHierarchy-test.tsx @@ -20,7 +20,6 @@ import { fireEvent, render, screen, waitFor, waitForElementToBeRemoved } from "@ import { HierarchyRoom, JoinRule, MatrixClient, Room } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; import { RoomHierarchy } from "matrix-js-sdk/src/room-hierarchy"; -import { TooltipProvider } from "@vector-im/compound-web"; import { MatrixClientPeg } from "../../../src/MatrixClientPeg"; import { mkStubRoom, stubClient } from "../../test-utils"; @@ -287,9 +286,7 @@ describe("SpaceHierarchy", () => { }; const getComponent = (props = {}): React.ReactElement => ( - - - + ); diff --git a/linked-dependencies/matrix-react-sdk/test/components/structures/TabbedView-test.tsx b/linked-dependencies/matrix-react-sdk/test/components/structures/TabbedView-test.tsx index d624633390..5ecf682afc 100644 --- a/linked-dependencies/matrix-react-sdk/test/components/structures/TabbedView-test.tsx +++ b/linked-dependencies/matrix-react-sdk/test/components/structures/TabbedView-test.tsx @@ -28,8 +28,17 @@ describe("", () => { const defaultProps = { tabLocation: TabLocation.LEFT, tabs: [generalTab, labsTab, securityTab] as NonEmptyArray>, + onChange: () => {}, }; - const getComponent = (props = {}): React.ReactElement => ; + const getComponent = ( + props: { + activeTabId: "GENERAL" | "LABS" | "SECURITY"; + onChange?: () => any; + tabs?: NonEmptyArray>; + } = { + activeTabId: "GENERAL", + }, + ): React.ReactElement => ; const getTabTestId = (tab: Tab): string => `settings-tab-${tab.id}`; const getActiveTab = (container: HTMLElement): Element | undefined => @@ -42,38 +51,15 @@ describe("", () => { expect(container).toMatchSnapshot(); }); - it("renders first tab as active tab when no initialTabId", () => { - const { container } = render(getComponent()); - expect(getActiveTab(container)?.textContent).toEqual(_t(generalTab.label)); - expect(getActiveTabBody(container)?.textContent).toEqual("general"); - }); - - it("renders first tab as active tab when initialTabId is not valid", () => { - const { container } = render(getComponent({ initialTabId: "bad-tab-id" })); - expect(getActiveTab(container)?.textContent).toEqual(_t(generalTab.label)); - expect(getActiveTabBody(container)?.textContent).toEqual("general"); - }); - - it("renders initialTabId tab as active when valid", () => { - const { container } = render(getComponent({ initialTabId: securityTab.id })); - expect(getActiveTab(container)?.textContent).toEqual(_t(securityTab.label)); - expect(getActiveTabBody(container)?.textContent).toEqual("security"); - }); - - it("sets active tab on tab click", () => { - const { container, getByTestId } = render(getComponent()); - - act(() => { - fireEvent.click(getByTestId(getTabTestId(securityTab))); - }); - + it("renders activeTabId tab as active when valid", () => { + const { container } = render(getComponent({ activeTabId: securityTab.id })); expect(getActiveTab(container)?.textContent).toEqual(_t(securityTab.label)); expect(getActiveTabBody(container)?.textContent).toEqual("security"); }); it("calls onchange on on tab click", () => { const onChange = jest.fn(); - const { getByTestId } = render(getComponent({ onChange })); + const { getByTestId } = render(getComponent({ activeTabId: "GENERAL", onChange })); act(() => { fireEvent.click(getByTestId(getTabTestId(securityTab))); @@ -84,31 +70,13 @@ describe("", () => { it("keeps same tab active when order of tabs changes", () => { // start with middle tab active - const { container, rerender } = render(getComponent({ initialTabId: labsTab.id })); + const { container, rerender } = render(getComponent({ activeTabId: labsTab.id })); expect(getActiveTab(container)?.textContent).toEqual(_t(labsTab.label)); - rerender(getComponent({ tabs: [labsTab, generalTab, securityTab] })); + rerender(getComponent({ tabs: [labsTab, generalTab, securityTab], activeTabId: labsTab.id })); // labs tab still active expect(getActiveTab(container)?.textContent).toEqual(_t(labsTab.label)); }); - - it("does not reactivate inititalTabId on rerender", () => { - const { container, getByTestId, rerender } = render(getComponent()); - - expect(getActiveTab(container)?.textContent).toEqual(_t(generalTab.label)); - - // make security tab active - act(() => { - fireEvent.click(getByTestId(getTabTestId(securityTab))); - }); - expect(getActiveTab(container)?.textContent).toEqual(_t(securityTab.label)); - - // rerender with new tab location - rerender(getComponent({ tabLocation: TabLocation.TOP })); - - // still security tab - expect(getActiveTab(container)?.textContent).toEqual(_t(securityTab.label)); - }); }); diff --git a/linked-dependencies/matrix-react-sdk/test/components/structures/ThreadPanel-test.tsx b/linked-dependencies/matrix-react-sdk/test/components/structures/ThreadPanel-test.tsx index 74a1d4023f..4f66379a3d 100644 --- a/linked-dependencies/matrix-react-sdk/test/components/structures/ThreadPanel-test.tsx +++ b/linked-dependencies/matrix-react-sdk/test/components/structures/ThreadPanel-test.tsx @@ -25,7 +25,6 @@ import { FeatureSupport, Thread, } from "matrix-js-sdk/src/matrix"; -import { TooltipProvider } from "@vector-im/compound-web"; import ThreadPanel, { ThreadFilterType, ThreadPanelHeader } from "../../../src/components/structures/ThreadPanel"; import MatrixClientContext from "../../../src/contexts/MatrixClientContext"; @@ -51,7 +50,6 @@ describe("ThreadPanel", () => { filterOption={ThreadFilterType.All} setFilterOption={() => undefined} />, - { wrapper: TooltipProvider }, ); expect(asFragment()).toMatchSnapshot(); }); @@ -63,7 +61,6 @@ describe("ThreadPanel", () => { filterOption={ThreadFilterType.My} setFilterOption={() => undefined} />, - { wrapper: TooltipProvider }, ); expect(asFragment()).toMatchSnapshot(); }); @@ -75,7 +72,6 @@ describe("ThreadPanel", () => { filterOption={ThreadFilterType.All} setFilterOption={() => undefined} />, - { wrapper: TooltipProvider }, ); expect(asFragment()).toMatchSnapshot(); }); @@ -87,7 +83,6 @@ describe("ThreadPanel", () => { filterOption={ThreadFilterType.All} setFilterOption={() => undefined} />, - { wrapper: TooltipProvider }, ); const found = container.querySelector(".mx_ThreadPanel_dropdown"); expect(found).toBeTruthy(); @@ -103,7 +98,6 @@ describe("ThreadPanel", () => { filterOption={ThreadFilterType.All} setFilterOption={() => undefined} />, - { wrapper: TooltipProvider }, ); fireEvent.click(container.querySelector(".mx_ThreadPanel_dropdown")!); const found = screen.queryAllByRole("menuitemradio"); @@ -126,13 +120,11 @@ describe("ThreadPanel", () => { const { container } = render( - - undefined} - /> - + undefined} + /> , ); @@ -146,13 +138,11 @@ describe("ThreadPanel", () => { const mockClient = createTestClient(); const { container } = render( - - undefined} - /> - + undefined} + /> , ); fireEvent.click(getByRole(container, "button", { name: "Mark all as read" })); @@ -308,7 +298,7 @@ describe("ThreadPanel", () => { myThreads!.addLiveEvent(ownThread.rootEvent); let events: EventData[] = []; - const renderResult = render(, { wrapper: TooltipProvider }); + const renderResult = render(); await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy()); await waitFor(() => { events = findEvents(renderResult.container); @@ -354,7 +344,7 @@ describe("ThreadPanel", () => { allThreads!.addLiveEvent(otherThread.rootEvent); let events: EventData[] = []; - const renderResult = render(, { wrapper: TooltipProvider }); + const renderResult = render(); await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy()); await waitFor(() => { events = findEvents(renderResult.container); diff --git a/linked-dependencies/matrix-react-sdk/test/components/structures/ThreadView-test.tsx b/linked-dependencies/matrix-react-sdk/test/components/structures/ThreadView-test.tsx index 83eed5eb9d..d7cbfa1756 100644 --- a/linked-dependencies/matrix-react-sdk/test/components/structures/ThreadView-test.tsx +++ b/linked-dependencies/matrix-react-sdk/test/components/structures/ThreadView-test.tsx @@ -197,7 +197,9 @@ describe("ThreadView", () => { it("sets the correct thread in the room view store", async () => { // expect(SdkContextClass.instance.roomViewStore.getThreadId()).toBeNull(); const { unmount } = await getComponent(); - expect(SdkContextClass.instance.roomViewStore.getThreadId()).toBe(rootEvent.getId()); + waitFor(() => { + expect(SdkContextClass.instance.roomViewStore.getThreadId()).toBe(rootEvent.getId()); + }); unmount(); await waitFor(() => expect(SdkContextClass.instance.roomViewStore.getThreadId()).toBeNull()); diff --git a/linked-dependencies/matrix-react-sdk/test/components/structures/TimelinePanel-test.tsx b/linked-dependencies/matrix-react-sdk/test/components/structures/TimelinePanel-test.tsx index afbe173940..0396ea68a4 100644 --- a/linked-dependencies/matrix-react-sdk/test/components/structures/TimelinePanel-test.tsx +++ b/linked-dependencies/matrix-react-sdk/test/components/structures/TimelinePanel-test.tsx @@ -39,7 +39,6 @@ import { KnownMembership } from "matrix-js-sdk/src/types"; import React, { createRef } from "react"; import { Mocked, mocked } from "jest-mock"; import { forEachRight } from "lodash"; -import { TooltipProvider } from "@vector-im/compound-web"; import TimelinePanel from "../../../src/components/structures/TimelinePanel"; import MatrixClientContext from "../../../src/contexts/MatrixClientContext"; @@ -212,7 +211,6 @@ describe("TimelinePanel", () => { manageReadReceipts={true} ref={ref} />, - { wrapper: TooltipProvider }, ); await flushPromises(); timelinePanel = ref.current!; @@ -392,7 +390,7 @@ describe("TimelinePanel", () => { onEventScrolledIntoView: jest.fn(), }; - const { rerender } = render(, { wrapper: TooltipProvider }); + const { rerender } = render(); expect(props.onEventScrolledIntoView).toHaveBeenCalledWith(undefined); props.eventId = events[1].getId(); rerender(); @@ -409,9 +407,7 @@ describe("TimelinePanel", () => { setupPagination(client, timeline, eventsPage1, null); await withScrollPanelMountSpy(async (mountSpy) => { - const { container } = render(, { - wrapper: TooltipProvider, - }); + const { container } = render(, {}); await waitFor(() => expectEvents(container, [events[1]])); @@ -428,7 +424,7 @@ describe("TimelinePanel", () => { const [, room, events] = setupTestData(); await withScrollPanelMountSpy(async (mountSpy) => { - const { container } = render(, { wrapper: TooltipProvider }); + const { container } = render(); await waitFor(() => expectEvents(container, [events[0], events[1]])); @@ -455,7 +451,7 @@ describe("TimelinePanel", () => { const paginateSpy = jest.spyOn(TimelineWindow.prototype, "paginate").mockClear(); - render(, { wrapper: TooltipProvider }); + render(); const event = new MatrixEvent({ type: RoomEvent.Timeline, origin_server_ts: 0 }); const data = { timeline: otherTimeline, liveEvent: true }; @@ -471,7 +467,7 @@ describe("TimelinePanel", () => { const paginateSpy = jest.spyOn(TimelineWindow.prototype, "paginate").mockClear(); - render(, { wrapper: TooltipProvider }); + render(); const event = new MatrixEvent({ type: RoomEvent.Timeline, origin_server_ts: 0 }); const data = { timeline: props.timelineSet.getLiveTimeline(), liveEvent: false }; @@ -487,7 +483,7 @@ describe("TimelinePanel", () => { const paginateSpy = jest.spyOn(TimelineWindow.prototype, "paginate").mockClear(); - render(, { wrapper: TooltipProvider }); + render(); const event = new MatrixEvent({ type: RoomEvent.Timeline, origin_server_ts: 0 }); const data = { timeline: props.timelineSet.getLiveTimeline(), liveEvent: false }; @@ -504,7 +500,7 @@ describe("TimelinePanel", () => { const paginateSpy = jest.spyOn(TimelineWindow.prototype, "paginate").mockClear(); - render(, { wrapper: TooltipProvider }); + render(); const event = new MatrixEvent({ type: RoomEvent.Timeline, origin_server_ts: 0 }); const data = { timeline: props.timelineSet.getLiveTimeline(), liveEvent: true }; @@ -527,7 +523,7 @@ describe("TimelinePanel", () => { const paginateSpy = jest.spyOn(TimelineWindow.prototype, "paginate").mockClear(); - render(, { wrapper: TooltipProvider }); + render(); await flushPromises(); @@ -568,7 +564,6 @@ describe("TimelinePanel", () => { overlayTimelineSet={overlayTimelineSet} overlayTimelineSetFilter={isCallEvent} />, - { wrapper: TooltipProvider }, ); await waitFor(() => expectEvents(container, [ @@ -608,7 +603,6 @@ describe("TimelinePanel", () => { const { container } = render( , - { wrapper: TooltipProvider }, ); await waitFor(() => @@ -640,7 +634,6 @@ describe("TimelinePanel", () => { const { container } = render( , - { wrapper: TooltipProvider }, ); await waitFor(() => @@ -672,7 +665,6 @@ describe("TimelinePanel", () => { const { container } = render( , - { wrapper: TooltipProvider }, ); await waitFor(() => @@ -707,7 +699,6 @@ describe("TimelinePanel", () => { timelineSet={timelineSet} overlayTimelineSet={overlayTimelineSet} />, - { wrapper: TooltipProvider }, ); await waitFor(() => expectEvents(container, [overlayEvents[0], events[0]])); @@ -781,7 +772,6 @@ describe("TimelinePanel", () => { await withScrollPanelMountSpy(async (mountSpy) => { const { container } = render( , - { wrapper: TooltipProvider }, ); await waitFor(() => @@ -894,7 +884,6 @@ describe("TimelinePanel", () => { , - { wrapper: TooltipProvider }, ); await dom.findByText("RootEvent"); await dom.findByText("ReplyEvent1"); @@ -948,7 +937,6 @@ describe("TimelinePanel", () => { , - { wrapper: TooltipProvider }, ); await dom.findByText("RootEvent"); await dom.findByText("ReplyEvent1"); @@ -1017,7 +1005,6 @@ describe("TimelinePanel", () => { , - { wrapper: TooltipProvider }, ); await waitFor(() => expect(screen.queryByRole("progressbar")).toBeNull()); diff --git a/linked-dependencies/matrix-react-sdk/test/components/structures/__snapshots__/RoomView-test.tsx.snap b/linked-dependencies/matrix-react-sdk/test/components/structures/__snapshots__/RoomView-test.tsx.snap index 80d6ac7c74..91bfd33e83 100644 --- a/linked-dependencies/matrix-react-sdk/test/components/structures/__snapshots__/RoomView-test.tsx.snap +++ b/linked-dependencies/matrix-react-sdk/test/components/structures/__snapshots__/RoomView-test.tsx.snap @@ -47,11 +47,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1 class="mx_LegacyRoomHeader_topic mx_RoomTopic" dir="auto" tabindex="0" - > - -
    + />
  • - -
    + />
    - -
    + />
    - -
    + />
    renders 1`] = ` >
    renders 1`] = `
    renders 1`] = ` Join
    when video broadcast when rendered should render class="mx_AccessibleButton mx_UserMenu_contextMenuButton" role="button" tabindex="0" - title="User menu" >
    ", () => { beforeEach(() => { renderResult = render( , - { wrapper: TooltipProvider }, ); }); diff --git a/linked-dependencies/matrix-react-sdk/test/components/views/avatars/DecoratedRoomAvatar-test.tsx b/linked-dependencies/matrix-react-sdk/test/components/views/avatars/DecoratedRoomAvatar-test.tsx index bf62355ce4..d9a7b9b16e 100644 --- a/linked-dependencies/matrix-react-sdk/test/components/views/avatars/DecoratedRoomAvatar-test.tsx +++ b/linked-dependencies/matrix-react-sdk/test/components/views/avatars/DecoratedRoomAvatar-test.tsx @@ -19,7 +19,6 @@ import { mocked } from "jest-mock"; import { JoinRule, MatrixClient, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix"; import React from "react"; import userEvent from "@testing-library/user-event"; -import { TooltipProvider } from "@vector-im/compound-web"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import { stubClient } from "../../../test-utils"; @@ -39,9 +38,7 @@ describe("DecoratedRoomAvatar", () => { let room: Room; function renderComponent() { - return render(, { - wrapper: TooltipProvider, - }); + return render(); } beforeEach(() => { diff --git a/linked-dependencies/matrix-react-sdk/test/components/views/avatars/__snapshots__/DecoratedRoomAvatar-test.tsx.snap b/linked-dependencies/matrix-react-sdk/test/components/views/avatars/__snapshots__/DecoratedRoomAvatar-test.tsx.snap index c5323de9f6..b060312070 100644 --- a/linked-dependencies/matrix-react-sdk/test/components/views/avatars/__snapshots__/DecoratedRoomAvatar-test.tsx.snap +++ b/linked-dependencies/matrix-react-sdk/test/components/views/avatars/__snapshots__/DecoratedRoomAvatar-test.tsx.snap @@ -16,9 +16,8 @@ exports[`DecoratedRoomAvatar shows an avatar with globe icon and tooltip for pub r
    @@ -41,9 +40,8 @@ exports[`DecoratedRoomAvatar shows the presence indicator in a DM room that also r
    diff --git a/linked-dependencies/matrix-react-sdk/test/components/views/beacon/BeaconListItem-test.tsx b/linked-dependencies/matrix-react-sdk/test/components/views/beacon/BeaconListItem-test.tsx index 6050e2b6cd..10497b369f 100644 --- a/linked-dependencies/matrix-react-sdk/test/components/views/beacon/BeaconListItem-test.tsx +++ b/linked-dependencies/matrix-react-sdk/test/components/views/beacon/BeaconListItem-test.tsx @@ -17,7 +17,6 @@ limitations under the License. import React from "react"; import { act, fireEvent, render } from "@testing-library/react"; import { Beacon, RoomMember, MatrixEvent, LocationAssetType } from "matrix-js-sdk/src/matrix"; -import { TooltipProvider } from "@vector-im/compound-web"; import BeaconListItem from "../../../../src/components/views/beacon/BeaconListItem"; import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; @@ -76,7 +75,6 @@ describe("", () => { , - { wrapper: TooltipProvider }, ); const setupRoomWithBeacons = (beaconInfoEvents: MatrixEvent[], locationEvents?: MatrixEvent[]): Beacon[] => { diff --git a/linked-dependencies/matrix-react-sdk/test/components/views/beacon/BeaconMarker-test.tsx b/linked-dependencies/matrix-react-sdk/test/components/views/beacon/BeaconMarker-test.tsx index 15b1335d90..4a5b5b8711 100644 --- a/linked-dependencies/matrix-react-sdk/test/components/views/beacon/BeaconMarker-test.tsx +++ b/linked-dependencies/matrix-react-sdk/test/components/views/beacon/BeaconMarker-test.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import React from "react"; -import { act, render, screen } from "@testing-library/react"; +import { act, render, screen, waitFor } from "@testing-library/react"; import * as maplibregl from "maplibre-gl"; import { Beacon, Room, RoomMember, MatrixEvent, getBeaconInfoIdentifier } from "matrix-js-sdk/src/matrix"; @@ -111,13 +111,15 @@ describe("", () => { expect(screen.queryByTestId("avatar-img")).not.toBeInTheDocument(); }); - it("renders marker when beacon has location", () => { + it("renders marker when beacon has location", async () => { const room = setupRoom([defaultEvent]); const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent)); beacon?.addLocations([location1]); const { asFragment } = renderComponent({ beacon }); + await waitFor(() => { + expect(screen.getByTestId("avatar-img")).toBeInTheDocument(); + }); expect(asFragment()).toMatchSnapshot(); - expect(screen.getByTestId("avatar-img")).toBeInTheDocument(); }); it("updates with new locations", () => { diff --git a/linked-dependencies/matrix-react-sdk/test/components/views/beacon/BeaconViewDialog-test.tsx b/linked-dependencies/matrix-react-sdk/test/components/views/beacon/BeaconViewDialog-test.tsx index b796253e2a..8347fca18b 100644 --- a/linked-dependencies/matrix-react-sdk/test/components/views/beacon/BeaconViewDialog-test.tsx +++ b/linked-dependencies/matrix-react-sdk/test/components/views/beacon/BeaconViewDialog-test.tsx @@ -15,11 +15,10 @@ limitations under the License. */ import React from "react"; -import { act, fireEvent, render, RenderResult } from "@testing-library/react"; +import { act, fireEvent, render, RenderResult, waitFor } from "@testing-library/react"; import { MatrixClient, MatrixEvent, Room, RoomMember, getBeaconInfoIdentifier } from "matrix-js-sdk/src/matrix"; import * as maplibregl from "maplibre-gl"; import { mocked } from "jest-mock"; -import { TooltipProvider } from "@vector-im/compound-web"; import BeaconViewDialog from "../../../../src/components/views/beacon/BeaconViewDialog"; import { @@ -80,8 +79,7 @@ describe("", () => { matrixClient: mockClient as MatrixClient, }; - const getComponent = (props = {}): RenderResult => - render(, { wrapper: TooltipProvider }); + const getComponent = (props = {}): RenderResult => render(); const openSidebar = (getByTestId: RenderResult["getByTestId"]) => { fireEvent.click(getByTestId("beacon-view-dialog-open-sidebar")); @@ -94,7 +92,7 @@ describe("", () => { jest.clearAllMocks(); }); - it("renders a map with markers", () => { + it("renders a map with markers", async () => { const room = setupRoom([defaultEvent]); const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent))!; beacon.addLocations([location1]); @@ -105,7 +103,9 @@ describe("", () => { lat: 51, }); // marker added - expect(mockMarker.addTo).toHaveBeenCalledWith(mockMap); + await waitFor(() => { + expect(mockMarker.addTo).toHaveBeenCalledWith(mockMap); + }); }); it("does not render any own beacon status when user is not live sharing", () => { diff --git a/linked-dependencies/matrix-react-sdk/test/components/views/beacon/DialogSidebar-test.tsx b/linked-dependencies/matrix-react-sdk/test/components/views/beacon/DialogSidebar-test.tsx index 0c14d334df..3f1e9194eb 100644 --- a/linked-dependencies/matrix-react-sdk/test/components/views/beacon/DialogSidebar-test.tsx +++ b/linked-dependencies/matrix-react-sdk/test/components/views/beacon/DialogSidebar-test.tsx @@ -16,7 +16,6 @@ limitations under the License. import React, { ComponentProps } from "react"; import { act, fireEvent, render } from "@testing-library/react"; -import { TooltipProvider } from "@vector-im/compound-web"; import DialogSidebar from "../../../../src/components/views/beacon/DialogSidebar"; import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; @@ -53,9 +52,7 @@ describe("", () => { const getComponent = (props = {}) => ( - - - + ); diff --git a/linked-dependencies/matrix-react-sdk/test/components/views/beacon/ShareLatestLocation-test.tsx b/linked-dependencies/matrix-react-sdk/test/components/views/beacon/ShareLatestLocation-test.tsx index 279e21671f..654b3dc73a 100644 --- a/linked-dependencies/matrix-react-sdk/test/components/views/beacon/ShareLatestLocation-test.tsx +++ b/linked-dependencies/matrix-react-sdk/test/components/views/beacon/ShareLatestLocation-test.tsx @@ -16,7 +16,6 @@ limitations under the License. import React from "react"; import { fireEvent, render } from "@testing-library/react"; -import { TooltipProvider } from "@vector-im/compound-web"; import ShareLatestLocation from "../../../../src/components/views/beacon/ShareLatestLocation"; import { copyPlaintext } from "../../../../src/utils/strings"; @@ -33,8 +32,7 @@ describe("", () => { timestamp: 123, }, }; - const getComponent = (props = {}) => - render(, { wrapper: TooltipProvider }); + const getComponent = (props = {}) => render(); beforeEach(() => { jest.clearAllMocks(); diff --git a/linked-dependencies/matrix-react-sdk/test/components/views/beacon/__snapshots__/BeaconListItem-test.tsx.snap b/linked-dependencies/matrix-react-sdk/test/components/views/beacon/__snapshots__/BeaconListItem-test.tsx.snap index 1d7a958672..b80d05c1ae 100644 --- a/linked-dependencies/matrix-react-sdk/test/components/views/beacon/__snapshots__/BeaconListItem-test.tsx.snap +++ b/linked-dependencies/matrix-react-sdk/test/components/views/beacon/__snapshots__/BeaconListItem-test.tsx.snap @@ -32,7 +32,6 @@ exports[` when a beacon is live and has locations renders beac class="mx_BeaconListItem_interactions" > renders sidebar correctly with beacons 1`] = ` View list
    renders sidebar correctly with beacons 1`] = ` class="mx_BeaconListItem_interactions" > renders sidebar correctly without beacons 1`] = ` View list
    when user has live location monitor renders correctly when minimized 1`] = `
    when user has live beacons and geolocation is Retry +
    + +
    + + ! + +

    + !room:domain.org +

    +
    +