diff --git a/.eslintrc.js b/.eslintrc.js index f168a87a06..2b0dd2c186 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -42,6 +42,10 @@ module.exports = { name: "setImmediate", message: "Use setTimeout instead.", }, + { + name: "Buffer", + message: "Buffer is not available in the web.", + }, ], "import/no-duplicates": ["error"], @@ -255,6 +259,9 @@ module.exports = { additionalTestBlockFunctions: ["beforeAll", "beforeEach", "oldBackendOnly"], }, ], + + // These are fine in tests + "no-restricted-globals": "off", }, }, { diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e7a976a7be..af52a6b77d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -13,6 +13,7 @@ # Ignore translations as those will be updated by GHA for Localazy download /src/i18n/strings +/src/i18n/strings/en_EN.json @element-hq/element-web-reviewers # Ignore the synapse plugin as this is updated by GHA for docker image updating /playwright/plugins/homeserver/synapse/index.ts diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 16036541f8..ebde627fde 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,7 +2,7 @@ ## Checklist -- [ ] Tests written for new code (and old code if feasible). -- [ ] New or updated `public`/`exported` symbols have accurate [TSDoc](https://tsdoc.org/) documentation. -- [ ] Linter and other CI checks pass. -- [ ] I have licensed the changes to Element by completing the [Contributor License Agreement (CLA)](https://cla-assistant.io/element-hq/element-web) +- [ ] Tests written for new code (and old code if feasible). +- [ ] New or updated `public`/`exported` symbols have accurate [TSDoc](https://tsdoc.org/) documentation. +- [ ] Linter and other CI checks pass. +- [ ] I have licensed the changes to Element by completing the [Contributor License Agreement (CLA)](https://cla-assistant.io/element-hq/element-web) diff --git a/.github/actions/download-verify-element-tarball/action.yml b/.github/actions/download-verify-element-tarball/action.yml new file mode 100644 index 0000000000..75d325c543 --- /dev/null +++ b/.github/actions/download-verify-element-tarball/action.yml @@ -0,0 +1,38 @@ +name: Upload release assets +description: Uploads assets to an existing release and optionally signs them +inputs: + tag: + description: GitHub release tag to fetch assets from. + required: true + out-file-path: + description: Path to where the webapp should be extracted to. + required: true +runs: + using: composite + steps: + - name: Download release tarball + uses: robinraju/release-downloader@a96f54c1b5f5e09e47d9504526e96febd949d4c2 # v1 + with: + tag: ${{ inputs.tag }} + fileName: element-*.tar.gz* + out-file-path: ${{ runner.temp }}/download-verify-element-tarball + + - name: Verify tarball + shell: bash + run: gpg --verify element-*.tar.gz.asc element-*.tar.gz + working-directory: ${{ runner.temp }}/download-verify-element-tarball + + - name: Extract tarball + shell: bash + run: | + mkdir webapp + tar xvzf element-*.tar.gz -C webapp --strip-components=1 + working-directory: ${{ runner.temp }}/download-verify-element-tarball + + - name: Move webapp to out-file-path + shell: bash + run: mv ${{ runner.temp }}/download-verify-element-tarball/webapp ${{ inputs.out-file-path }} + + - name: Clean up temp directory + shell: bash + run: rm -R ${{ runner.temp }}/download-verify-element-tarball diff --git a/.github/workflows/build_develop.yml b/.github/workflows/build_develop.yml index c21ab831e6..8bbcfe726f 100644 --- a/.github/workflows/build_develop.yml +++ b/.github/workflows/build_develop.yml @@ -20,6 +20,7 @@ jobs: permissions: checks: read pages: write + deployments: write env: R2_BUCKET: "element-web-develop" R2_URL: ${{ vars.CF_R2_S3_API }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000000..daac3bfed8 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,92 @@ +# Manual deploy workflow for deploying to app.element.io & staging.element.io +# Runs automatically for staging.element.io when an RC or Release is published +# Note: Does *NOT* run automatically for app.element.io so that it gets tested on staging.element.io beforehand +name: Deploy release +run-name: Deploy ${{ github.ref_name }} to ${{ inputs.site || 'staging.element.io' }} +on: + release: + types: [published] + workflow_dispatch: + inputs: + site: + description: Which site to deploy to + required: true + default: staging.element.io + type: choice + options: + - staging.element.io + - app.element.io +concurrency: ${{ inputs.site || 'staging.element.io' }} +permissions: {} +jobs: + deploy: + name: "Deploy to Cloudflare Pages" + runs-on: ubuntu-24.04 + environment: ${{ inputs.site || 'staging.element.io' }} + permissions: + checks: read + deployments: write + env: + SITE: ${{ inputs.site || 'staging.element.io' }} + steps: + - uses: actions/checkout@v4 + + - name: Load GPG key + run: | + curl https://packages.element.io/element-release-key.gpg | gpg --import + gpg -k "$GPG_FINGERPRINT" + env: + GPG_FINGERPRINT: ${{ vars.GPG_FINGERPRINT }} + + - name: Check current version on deployment + id: current_version + run: | + version=$(curl -s https://$SITE/version) + echo "version=${version#v}" >> $GITHUB_OUTPUT + + # The current version bundle melding dance is skipped if the version we're deploying is the same + # as then we're just doing a re-deploy of the same version with potentially different configs. + - name: Download current version for its old bundles + id: current_download + if: steps.current_version.outputs.version != github.ref_name + uses: ./.github/actions/download-verify-element-tarball + with: + tag: v${{ steps.current_version.outputs.version }} + out-file-path: _current_version + + - name: Download target version + uses: ./.github/actions/download-verify-element-tarball + with: + tag: ${{ github.ref_name }} + out-file-path: _deploy + + - name: Merge current bundles into target + if: steps.current_download.outcome == 'success' + run: cp -vnpr _current_version/bundles/* _deploy/bundles/ + + - name: Copy config + run: cp element.io/app/config.json _deploy/config.json + + - name: Populate 404.html + run: echo "404 Not Found" > _deploy/404.html + + - name: Populate _headers + run: cp .github/cfp_headers _deploy/_headers + + - name: Wait for other steps to succeed + uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork + with: + ref: ${{ github.sha }} + running-workflow-name: "Deploy to Cloudflare Pages" + repo-token: ${{ secrets.GITHUB_TOKEN }} + wait-interval: 10 + check-regexp: ^((?!SonarCloud|SonarQube|issue|board|label|Release|prepare|GitHub Pages).)*$ + + - name: Deploy to Cloudflare Pages + uses: cloudflare/pages-action@f0a1cd58cd66095dee69bfa18fa5efd1dde93bca # v1 + with: + apiToken: ${{ secrets.CF_PAGES_TOKEN }} + accountId: ${{ secrets.CF_PAGES_ACCOUNT_ID }} + projectName: ${{ env.SITE == 'staging.element.io' && 'element-web-staging' || 'element-web' }} + directory: _deploy + gitHubToken: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/dockerhub.yaml b/.github/workflows/dockerhub.yaml index 7911cf794a..8dae6cf5ab 100644 --- a/.github/workflows/dockerhub.yaml +++ b/.github/workflows/dockerhub.yaml @@ -39,7 +39,7 @@ jobs: - name: Docker meta id: meta - uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5 + uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5 with: images: | vectorim/element-web @@ -51,7 +51,7 @@ jobs: - name: Build and push id: build-and-push - uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6 + uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6 with: context: . push: true diff --git a/.github/workflows/end-to-end-tests-netlify.yaml b/.github/workflows/end-to-end-tests-netlify.yaml index f59513ca12..e25994ec9d 100644 --- a/.github/workflows/end-to-end-tests-netlify.yaml +++ b/.github/workflows/end-to-end-tests-netlify.yaml @@ -27,6 +27,7 @@ jobs: - name: Download HTML report uses: actions/download-artifact@v4 with: + github-token: ${{ secrets.GITHUB_TOKEN }} run-id: ${{ github.event.workflow_run.id }} name: html-report path: playwright-report diff --git a/.github/workflows/end-to-end-tests.yaml b/.github/workflows/end-to-end-tests.yaml index 1a31f75065..5a75040866 100644 --- a/.github/workflows/end-to-end-tests.yaml +++ b/.github/workflows/end-to-end-tests.yaml @@ -83,7 +83,7 @@ jobs: name: "Run Tests ${{ matrix.runner }}/${{ strategy.job-total }}" needs: build if: inputs.skip != true - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 permissions: actions: read issues: read @@ -124,14 +124,18 @@ jobs: with: path: | ~/.cache/ms-playwright - key: ${{ runner.os }}-playwright-${{ steps.playwright.outputs.version }} + key: ${{ runner.os }}-playwright-${{ steps.playwright.outputs.version }}-chromium - - name: Install Playwright browsers + - name: Install Playwright browser if: steps.playwright-cache.outputs.cache-hit != 'true' - run: yarn playwright install --with-deps + run: yarn playwright install --with-deps --no-shell chromium + # We skip tests tagged with @mergequeue when running on PRs, but run them in MQ and everywhere else - name: Run Playwright tests - run: yarn playwright test --shard ${{ matrix.runner }}/${{ strategy.job-total }} + run: | + yarn playwright test \ + --shard "${{ matrix.runner }}/${{ strategy.job-total }}" \ + ${{ github.event_name == 'pull_request' && '--grep-invert @mergequeue' || '' }} - name: Upload blob report to GitHub Actions Artifacts if: always() diff --git a/.github/workflows/netlify.yaml b/.github/workflows/netlify.yaml index bcaa9ce810..63bac7d33f 100644 --- a/.github/workflows/netlify.yaml +++ b/.github/workflows/netlify.yaml @@ -30,6 +30,7 @@ jobs: - name: 📥 Download artifact uses: actions/download-artifact@v4 with: + github-token: ${{ secrets.GITHUB_TOKEN }} run-id: ${{ github.event.workflow_run.id }} name: webapp path: webapp diff --git a/.github/workflows/pull_request.yaml b/.github/workflows/pull_request.yaml index 2f97ccbbb4..d48fed1792 100644 --- a/.github/workflows/pull_request.yaml +++ b/.github/workflows/pull_request.yaml @@ -9,6 +9,6 @@ jobs: action: uses: matrix-org/matrix-js-sdk/.github/workflows/pull_request.yaml@develop permissions: - pull-requests: read + pull-requests: write secrets: ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2ecc4a4662..019bc1b9ce 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,6 +18,7 @@ jobs: permissions: contents: write issues: write + pull-requests: read secrets: ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} diff --git a/.github/workflows/release_prepare.yml b/.github/workflows/release_prepare.yml index b655bb4206..031221041a 100644 --- a/.github/workflows/release_prepare.yml +++ b/.github/workflows/release_prepare.yml @@ -19,8 +19,23 @@ on: default: true permissions: {} # Uses ELEMENT_BOT_TOKEN instead jobs: + checks: + name: Sanity checks + strategy: + matrix: + repo: + - matrix-org/matrix-js-sdk + - element-hq/element-web + - element-hq/element-desktop + uses: matrix-org/matrix-js-sdk/.github/workflows/release-checks.yml@develop + secrets: + ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + with: + repository: ${{ matrix.repo }} + prepare: runs-on: ubuntu-24.04 + needs: checks env: # The order is specified bottom-up to avoid any races for allchange REPOS: matrix-js-sdk element-web element-desktop diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index deb1183fd9..35803a60f1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -104,7 +104,7 @@ jobs: - name: Skip SonarCloud in merge queue if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true' - uses: Sibz/github-status-action@faaa4d96fecf273bd762985e0e7f9f933c774918 # v1 + uses: guibranco/github-status-action-v2@d469d49426f5a7b8a1fbcac20ad274d3e4892321 with: authToken: ${{ secrets.GITHUB_TOKEN }} state: success diff --git a/CHANGELOG.md b/CHANGELOG.md index a554890dc6..ebe9f061ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,33 @@ +Changes in [1.11.87](https://github.com/element-hq/element-web/releases/tag/v1.11.87) (2024-12-03) +================================================================================================== +## ✨ Features + +* Send and respect MSC4230 is\_animated flag ([#28513](https://github.com/element-hq/element-web/pull/28513)). Contributed by @t3chguy. +* Display a warning when an unverified user's identity changes ([#28211](https://github.com/element-hq/element-web/pull/28211)). Contributed by @uhoreg. +* Swap out Twitter link for Mastodon on auth footer ([#28508](https://github.com/element-hq/element-web/pull/28508)). Contributed by @t3chguy. +* Consider `org.matrix.msc3417.call` as video room in create room dialog ([#28497](https://github.com/element-hq/element-web/pull/28497)). Contributed by @t3chguy. +* Standardise icons using Compound Design Tokens ([#28217](https://github.com/element-hq/element-web/pull/28217)). Contributed by @t3chguy. +* Start sending stable `m.marked_unread` events ([#28478](https://github.com/element-hq/element-web/pull/28478)). Contributed by @tulir. +* Upgrade to compound-design-tokens v2 ([#28471](https://github.com/element-hq/element-web/pull/28471)). Contributed by @t3chguy. +* Standardise icons using Compound Design Tokens ([#28286](https://github.com/element-hq/element-web/pull/28286)). Contributed by @t3chguy. +* Remove reply fallbacks as per merged MSC2781 ([#28406](https://github.com/element-hq/element-web/pull/28406)). Contributed by @t3chguy. +* Use React Suspense when rendering async modals ([#28386](https://github.com/element-hq/element-web/pull/28386)). Contributed by @t3chguy. + +## 🐛 Bug Fixes + +* Add spinner when room encryption is loading in room settings ([#28535](https://github.com/element-hq/element-web/pull/28535)). Contributed by @florianduros. +* Fix getOidcCallbackUrl for Element Desktop ([#28521](https://github.com/element-hq/element-web/pull/28521)). Contributed by @t3chguy. +* Filter out redacted poll votes to avoid crashing the Poll widget ([#28498](https://github.com/element-hq/element-web/pull/28498)). Contributed by @t3chguy. +* Fix force tab complete not working since switching to React 18 createRoot API ([#28505](https://github.com/element-hq/element-web/pull/28505)). Contributed by @t3chguy. +* Fix media captions in bubble layout ([#28480](https://github.com/element-hq/element-web/pull/28480)). Contributed by @tulir. +* Reset cross-signing before backup when resetting both ([#28402](https://github.com/element-hq/element-web/pull/28402)). Contributed by @uhoreg. +* Listen to events so that encryption icon updates when status changes ([#28407](https://github.com/element-hq/element-web/pull/28407)). Contributed by @uhoreg. +* Check that the file the user chose has a MIME type of `image/*` ([#28467](https://github.com/element-hq/element-web/pull/28467)). Contributed by @t3chguy. +* Fix download button size in message action bar ([#28472](https://github.com/element-hq/element-web/pull/28472)). Contributed by @t3chguy. +* Allow tab completing users in brackets ([#28460](https://github.com/element-hq/element-web/pull/28460)). Contributed by @t3chguy. +* Fix React 18 strict mode breaking spotlight dialog ([#28452](https://github.com/element-hq/element-web/pull/28452)). Contributed by @MidhunSureshR. + + Changes in [1.11.86](https://github.com/element-hq/element-web/releases/tag/v1.11.86) (2024-11-19) ================================================================================================== ## ✨ Features diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 49741b073c..fa887929fb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,26 +20,26 @@ Definitely don't use the GitHub default of "Update file.ts". As for your PR description, it should include these things: -- References to any bugs fixed by the change (in GitHub's `Fixes` notation) -- Describe the why and what is changing in the PR description so it's easy for - onlookers and reviewers to onboard and context switch. This information is - also helpful when we come back to look at this in 6 months and ask "why did - we do it like that?" we have a chance of finding out. - - Why didn't it work before? Why does it work now? What use cases does it - unlock? - - If you find yourself adding information on how the code works or why you - chose to do it the way you did, make sure this information is instead - written as comments in the code itself. - - Sometimes a PR can change considerably as it is developed. In this case, - the description should be updated to reflect the most recent state of - the PR. (It can be helpful to retain the old content under a suitable - heading, for additional context.) -- Include both **before** and **after** screenshots to easily compare and discuss - what's changing. -- Include a step-by-step testing strategy so that a reviewer can check out the - code locally and easily get to the point of testing your change. -- Add comments to the diff for the reviewer that might help them to understand - why the change is necessary or how they might better understand and review it. +- References to any bugs fixed by the change (in GitHub's `Fixes` notation) +- Describe the why and what is changing in the PR description so it's easy for + onlookers and reviewers to onboard and context switch. This information is + also helpful when we come back to look at this in 6 months and ask "why did + we do it like that?" we have a chance of finding out. + - Why didn't it work before? Why does it work now? What use cases does it + unlock? + - If you find yourself adding information on how the code works or why you + chose to do it the way you did, make sure this information is instead + written as comments in the code itself. + - Sometimes a PR can change considerably as it is developed. In this case, + the description should be updated to reflect the most recent state of + the PR. (It can be helpful to retain the old content under a suitable + heading, for additional context.) +- Include both **before** and **after** screenshots to easily compare and discuss + what's changing. +- Include a step-by-step testing strategy so that a reviewer can check out the + code locally and easily get to the point of testing your change. +- Add comments to the diff for the reviewer that might help them to understand + why the change is necessary or how they might better understand and review it. ### Changelogs @@ -79,8 +79,8 @@ element-web notes: Fix a bug where the 'Herd' button only worked on Tuesdays This example is for Element Web. You can specify: -- element-web -- element-desktop +- element-web +- element-desktop If your PR introduces a breaking change, use the `Notes` section in the same way, additionally adding the `X-Breaking-Change` label (see below). There's no need @@ -96,10 +96,10 @@ Notes: Remove legacy `Camelopard` class. `Giraffe` should be used instead. Other metadata can be added using labels. -- `X-Breaking-Change`: A breaking change - adding this label will mean the change causes a _major_ version bump. -- `T-Enhancement`: A new feature - adding this label will mean the change causes a _minor_ version bump. -- `T-Defect`: A bug fix (in either code or docs). -- `T-Task`: No user-facing changes, eg. code comments, CI fixes, refactors or tests. Won't have a changelog entry unless you specify one. +- `X-Breaking-Change`: A breaking change - adding this label will mean the change causes a _major_ version bump. +- `T-Enhancement`: A new feature - adding this label will mean the change causes a _minor_ version bump. +- `T-Defect`: A bug fix (in either code or docs). +- `T-Task`: No user-facing changes, eg. code comments, CI fixes, refactors or tests. Won't have a changelog entry unless you specify one. If you don't have permission to add labels, your PR reviewer(s) can work with you to add them: ask in the PR description or comments. diff --git a/README.md b/README.md index fa4ac89ff9..87e451c9ff 100644 --- a/README.md +++ b/README.md @@ -16,28 +16,28 @@ JS SDK](https://github.com/matrix-org/matrix-js-sdk). Element has several tiers of support for different environments: -- Supported - - Definition: - - Issues **actively triaged**, regressions **block** the release - - Last 2 major versions of Chrome, Firefox, and Edge on desktop OSes - - Last 2 versions of Safari - - Latest release of official Element Desktop app on desktop OSes - - Desktop OSes means macOS, Windows, and Linux versions for desktop devices - that are actively supported by the OS vendor and receive security updates -- Best effort - - Definition: - - Issues **accepted**, regressions **do not block** the release - - The wider Element Products(including Element Call and the Enterprise Server Suite) do still not officially support these browsers. - - The element web project and its contributors should keep the client functioning and gracefully degrade where other sibling features (E.g. Element Call) may not function. - - Last major release of Firefox ESR and Chrome/Edge Extended Stable -- Community Supported - - Definition: - - Issues **accepted**, regressions **do not block** the release - - Community contributions are welcome to support these issues - - Mobile web for current stable version of Chrome, Firefox, and Safari on Android, iOS, and iPadOS -- Not supported - - Definition: Issues only affecting unsupported environments are **closed** - - Everything else +- Supported + - Definition: + - Issues **actively triaged**, regressions **block** the release + - Last 2 major versions of Chrome, Firefox, and Edge on desktop OSes + - Last 2 versions of Safari + - Latest release of official Element Desktop app on desktop OSes + - Desktop OSes means macOS, Windows, and Linux versions for desktop devices + that are actively supported by the OS vendor and receive security updates +- Best effort + - Definition: + - Issues **accepted**, regressions **do not block** the release + - The wider Element Products(including Element Call and the Enterprise Server Suite) do still not officially support these browsers. + - The element web project and its contributors should keep the client functioning and gracefully degrade where other sibling features (E.g. Element Call) may not function. + - Last major release of Firefox ESR and Chrome/Edge Extended Stable +- Community Supported + - Definition: + - Issues **accepted**, regressions **do not block** the release + - Community contributions are welcome to support these issues + - Mobile web for current stable version of Chrome, Firefox, and Safari on Android, iOS, and iPadOS +- Not supported + - Definition: Issues only affecting unsupported environments are **closed** + - Everything else The period of support for these tiers should last until the releases specified above, plus 1 app release cycle(2 weeks). In the case of Firefox ESR this is extended further to allow it land in Debian Stable. @@ -74,16 +74,16 @@ situation, but it's still not good practice to do it in the first place. See Unless you have special requirements, you will want to add the following to your web server configuration when hosting Element Web: -- The `X-Frame-Options: SAMEORIGIN` header, to prevent Element Web from being - framed and protect from [clickjacking][owasp-clickjacking]. -- The `frame-ancestors 'self'` directive to your `Content-Security-Policy` - header, as the modern replacement for `X-Frame-Options` (though both should be - included since not all browsers support it yet, see - [this][owasp-clickjacking-csp]). -- The `X-Content-Type-Options: nosniff` header, to [disable MIME - sniffing][mime-sniffing]. -- The `X-XSS-Protection: 1; mode=block;` header, for basic XSS protection in - legacy browsers. +- The `X-Frame-Options: SAMEORIGIN` header, to prevent Element Web from being + framed and protect from [clickjacking][owasp-clickjacking]. +- The `frame-ancestors 'self'` directive to your `Content-Security-Policy` + header, as the modern replacement for `X-Frame-Options` (though both should be + included since not all browsers support it yet, see + [this][owasp-clickjacking-csp]). +- The `X-Content-Type-Options: nosniff` header, to [disable MIME + sniffing][mime-sniffing]. +- The `X-XSS-Protection: 1; mode=block;` header, for basic XSS protection in + legacy browsers. [mime-sniffing]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#mime_sniffing [owasp-clickjacking-csp]: https://cheatsheetseries.owasp.org/cheatsheets/Clickjacking_Defense_Cheat_Sheet.html#content-security-policy-frame-ancestors-examples diff --git a/__mocks__/FontManager.js b/__mocks__/FontManager.js deleted file mode 100644 index 41eab4bf94..0000000000 --- a/__mocks__/FontManager.js +++ /dev/null @@ -1,6 +0,0 @@ -// Stub out FontManager for tests as it doesn't validate anything we don't already know given -// our fixed test environment and it requires the installation of node-canvas. - -module.exports = { - fixupColorFonts: () => Promise.resolve(), -}; diff --git a/code_style.md b/code_style.md index e5f7485cec..9aa6836442 100644 --- a/code_style.md +++ b/code_style.md @@ -3,9 +3,9 @@ This code style applies to projects which the element-web team directly maintains or is reasonably adjacent to. As of writing, these are: -- element-desktop -- element-web -- matrix-js-sdk +- element-desktop +- element-web +- matrix-js-sdk Other projects might extend this code style for increased strictness. For example, matrix-events-sdk has stricter code organization to reduce the maintenance burden. These projects will declare their code diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 23229bf00a..57b017bc1c 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -1,55 +1,55 @@ # Summary -- [Introduction](../README.md) +- [Introduction](../README.md) # Usage -- [Betas](betas.md) -- [Labs](labs.md) +- [Betas](betas.md) +- [Labs](labs.md) # Setup -- [Install](install.md) -- [Config](config.md) -- [Custom home page](custom-home.md) -- [Kubernetes](kubernetes.md) -- [Jitsi](jitsi.md) -- [Encryption](e2ee.md) +- [Install](install.md) +- [Config](config.md) +- [Custom home page](custom-home.md) +- [Kubernetes](kubernetes.md) +- [Jitsi](jitsi.md) +- [Encryption](e2ee.md) # Build -- [Customisations](customisations.md) -- [Modules](modules.md) -- [Native Node modules](native-node-modules.md) +- [Customisations](customisations.md) +- [Modules](modules.md) +- [Native Node modules](native-node-modules.md) # Contribution -- [Choosing an issue](choosing-an-issue.md) -- [Translation](translating.md) -- [Netlify builds](pr-previews.md) -- [Code review](review.md) +- [Choosing an issue](choosing-an-issue.md) +- [Translation](translating.md) +- [Netlify builds](pr-previews.md) +- [Code review](review.md) # Development -- [App load order](app-load.md) -- [Translation](translating-dev.md) -- [Theming](theming.md) -- [Playwright end to end tests](playwright.md) -- [Memory profiling](memory-profiles-and-leaks.md) -- [Jitsi](jitsi-dev.md) -- [Feature flags](feature-flags.md) -- [OIDC and delegated authentication](oidc.md) -- [Release Process](release.md) +- [App load order](app-load.md) +- [Translation](translating-dev.md) +- [Theming](theming.md) +- [Playwright end to end tests](playwright.md) +- [Memory profiling](memory-profiles-and-leaks.md) +- [Jitsi](jitsi-dev.md) +- [Feature flags](feature-flags.md) +- [OIDC and delegated authentication](oidc.md) +- [Release Process](release.md) # Deep dive -- [Skinning](skinning.md) -- [Cider editor](ciderEditor.md) -- [Iconography](icons.md) -- [Jitsi](jitsi.md) -- [Local echo](local-echo-dev.md) -- [Media](media-handling.md) -- [Room List Store](room-list-store.md) -- [Scrolling](scrolling.md) -- [Usercontent](usercontent.md) -- [Widget layouts](widget-layouts.md) +- [Skinning](skinning.md) +- [Cider editor](ciderEditor.md) +- [Iconography](icons.md) +- [Jitsi](jitsi.md) +- [Local echo](local-echo-dev.md) +- [Media](media-handling.md) +- [Room List Store](room-list-store.md) +- [Scrolling](scrolling.md) +- [Usercontent](usercontent.md) +- [Widget layouts](widget-layouts.md) diff --git a/docs/app-load.md b/docs/app-load.md index 849e95cb8d..7f72b3fea4 100644 --- a/docs/app-load.md +++ b/docs/app-load.md @@ -61,18 +61,18 @@ flowchart TD Key: -- Parallelogram: async/await task -- Box: sync task -- Diamond: conditional branch -- Circle: user interaction -- Blue arrow: async task is allowed to settle but allowed to fail -- Red arrow: async task success is asserted +- Parallelogram: async/await task +- Box: sync task +- Diamond: conditional branch +- Circle: user interaction +- Blue arrow: async task is allowed to settle but allowed to fail +- Red arrow: async task success is asserted Notes: -- A task begins when all its dependencies (arrows going into it) are fulfilled. -- The success of setting up rageshake is never asserted, element-web has a fallback path for running without IDB (and thus rageshake). -- Everything is awaited to be settled before the Modernizr check, to allow it to make use of things like i18n if they are successful. +- A task begins when all its dependencies (arrows going into it) are fulfilled. +- The success of setting up rageshake is never asserted, element-web has a fallback path for running without IDB (and thus rageshake). +- Everything is awaited to be settled before the Modernizr check, to allow it to make use of things like i18n if they are successful. Underlying dependencies: diff --git a/docs/choosing-an-issue.md b/docs/choosing-an-issue.md index 9d008782a1..ca17979367 100644 --- a/docs/choosing-an-issue.md +++ b/docs/choosing-an-issue.md @@ -32,19 +32,19 @@ someone to add something. When you're looking through the list, here are some things that might make an issue a **GOOD** choice: -- It is a problem or feature you care about. -- It concerns a type of code you know a little about. -- You think you can understand what's needed. -- It already has approval from Element Web's designers (look for comments from - members of the - [Product](https://github.com/orgs/element-hq/teams/product/members) or - [Design](https://github.com/orgs/element-hq/teams/design/members) teams). +- It is a problem or feature you care about. +- It concerns a type of code you know a little about. +- You think you can understand what's needed. +- It already has approval from Element Web's designers (look for comments from + members of the + [Product](https://github.com/orgs/element-hq/teams/product/members) or + [Design](https://github.com/orgs/element-hq/teams/design/members) teams). Here are some things that might make it a **BAD** choice: -- You don't understand it (maybe add a comment asking a clarifying question). -- It sounds difficult, or is part of a larger change you don't know about. -- **It is tagged with `X-Needs-Design` or `X-Needs-Product`.** +- You don't understand it (maybe add a comment asking a clarifying question). +- It sounds difficult, or is part of a larger change you don't know about. +- **It is tagged with `X-Needs-Design` or `X-Needs-Product`.** **Element Web's Design and Product teams tend to be very busy**, so if you make changes that require approval from one of those teams, you will probably have diff --git a/docs/config.md b/docs/config.md index cc40179740..8ca4ba4eb8 100644 --- a/docs/config.md +++ b/docs/config.md @@ -455,7 +455,7 @@ If you would like to use Scalar, the integration manager maintained by Element, For widgets in general (from an integration manager or not) there is also: -- `default_widget_container_height` +- `default_widget_container_height` This controls the height that the top widget panel initially appears as and is the height in pixels, default 280. @@ -551,38 +551,38 @@ preferences. Currently, the following UI feature flags are supported: -- `UIFeature.urlPreviews` - Whether URL previews are enabled across the entire application. -- `UIFeature.feedback` - Whether prompts to supply feedback are shown. -- `UIFeature.voip` - Whether or not VoIP is shown readily to the user. When disabled, - Jitsi widgets will still work though they cannot easily be added. -- `UIFeature.widgets` - Whether or not widgets will be shown. -- `UIFeature.advancedSettings` - Whether or not sections titled "advanced" in room and - user settings are shown to the user. -- `UIFeature.shareQrCode` - Whether or not the QR code on the share room/event dialog - is shown. -- `UIFeature.shareSocial` - Whether or not the social icons on the share room/event dialog - are shown. -- `UIFeature.identityServer` - Whether or not functionality requiring an identity server - is shown. When disabled, the user will not be able to interact with the identity - server (sharing email addresses, 3PID invites, etc). -- `UIFeature.thirdPartyId` - Whether or not UI relating to third party identifiers (3PIDs) - is shown. Typically this is considered "contact information" on the homeserver, and is - not directly related to the identity server. -- `UIFeature.registration` - Whether or not the registration page is accessible. Typically - useful if accounts are managed externally. -- `UIFeature.passwordReset` - Whether or not the password reset page is accessible. Typically - useful if accounts are managed externally. -- `UIFeature.deactivate` - Whether or not the deactivate account button is accessible. Typically - useful if accounts are managed externally. -- `UIFeature.advancedEncryption` - Whether or not advanced encryption options are shown to the - user. -- `UIFeature.roomHistorySettings` - Whether or not the room history settings are shown to the user. - This should only be used if the room history visibility options are managed by the server. -- `UIFeature.TimelineEnableRelativeDates` - Display relative date separators (eg: 'Today', 'Yesterday') in the - timeline for recent messages. When false day dates will be used. -- `UIFeature.BulkUnverifiedSessionsReminder` - Display popup reminders to verify or remove unverified sessions. Defaults - to true. -- `UIFeature.locationSharing` - Whether or not location sharing menus will be shown. +- `UIFeature.urlPreviews` - Whether URL previews are enabled across the entire application. +- `UIFeature.feedback` - Whether prompts to supply feedback are shown. +- `UIFeature.voip` - Whether or not VoIP is shown readily to the user. When disabled, + Jitsi widgets will still work though they cannot easily be added. +- `UIFeature.widgets` - Whether or not widgets will be shown. +- `UIFeature.advancedSettings` - Whether or not sections titled "advanced" in room and + user settings are shown to the user. +- `UIFeature.shareQrCode` - Whether or not the QR code on the share room/event dialog + is shown. +- `UIFeature.shareSocial` - Whether or not the social icons on the share room/event dialog + are shown. +- `UIFeature.identityServer` - Whether or not functionality requiring an identity server + is shown. When disabled, the user will not be able to interact with the identity + server (sharing email addresses, 3PID invites, etc). +- `UIFeature.thirdPartyId` - Whether or not UI relating to third party identifiers (3PIDs) + is shown. Typically this is considered "contact information" on the homeserver, and is + not directly related to the identity server. +- `UIFeature.registration` - Whether or not the registration page is accessible. Typically + useful if accounts are managed externally. +- `UIFeature.passwordReset` - Whether or not the password reset page is accessible. Typically + useful if accounts are managed externally. +- `UIFeature.deactivate` - Whether or not the deactivate account button is accessible. Typically + useful if accounts are managed externally. +- `UIFeature.advancedEncryption` - Whether or not advanced encryption options are shown to the + user. +- `UIFeature.roomHistorySettings` - Whether or not the room history settings are shown to the user. + This should only be used if the room history visibility options are managed by the server. +- `UIFeature.TimelineEnableRelativeDates` - Display relative date separators (eg: 'Today', 'Yesterday') in the + timeline for recent messages. When false day dates will be used. +- `UIFeature.BulkUnverifiedSessionsReminder` - Display popup reminders to verify or remove unverified sessions. Defaults + to true. +- `UIFeature.locationSharing` - Whether or not location sharing menus will be shown. ## Undocumented / developer options @@ -592,4 +592,3 @@ The following are undocumented or intended for developer use only. 2. `sync_timeline_limit` 3. `dangerously_allow_unsafe_and_insecure_passwords` 4. `latex_maths_delims`: An optional setting to override the default delimiters used for maths parsing. See https://github.com/matrix-org/matrix-react-sdk/pull/5939 for details. Only used when `feature_latex_maths` is enabled. -5. `voice_broadcast.chunk_length`: Target chunk length in seconds for the Voice Broadcast feature currently under development. diff --git a/docs/customisations.md b/docs/customisations.md index a6f72ab1ab..42cb8c7c5c 100644 --- a/docs/customisations.md +++ b/docs/customisations.md @@ -50,9 +50,9 @@ that properties/state machines won't change. UI for some actions can be hidden via the ComponentVisibility customisation: -- inviting users to rooms and spaces, -- creating rooms, -- creating spaces, +- inviting users to rooms and spaces, +- creating rooms, +- creating spaces, To customise visibility create a customisation module from [ComponentVisibility](https://github.com/element-hq/element-web/blob/master/src/customisations/ComponentVisibility.ts) following the instructions above. diff --git a/docs/e2ee.md b/docs/e2ee.md index 1229f55f38..835c38a1d5 100644 --- a/docs/e2ee.md +++ b/docs/e2ee.md @@ -31,9 +31,9 @@ Set the following on your homeserver's When `force_disable` is true: -- all rooms will be created with encryption disabled, and it will not be possible to enable - encryption from room settings. -- any `io.element.e2ee.default` value will be disregarded. +- all rooms will be created with encryption disabled, and it will not be possible to enable + encryption from room settings. +- any `io.element.e2ee.default` value will be disregarded. Note: If the server is configured to forcibly enable encryption for some or all rooms, this behaviour will be overridden. diff --git a/docs/feature-flags.md b/docs/feature-flags.md index 46e5f1243e..54d54e3b1b 100644 --- a/docs/feature-flags.md +++ b/docs/feature-flags.md @@ -5,10 +5,10 @@ flexibility and control over when and where those features are enabled. For example, flags make the following things possible: -- Extended testing of a feature via labs on develop -- Enabling features when ready instead of the first moment the code is released -- Testing a feature with a specific set of users (by enabling only on a specific - Element instance) +- Extended testing of a feature via labs on develop +- Enabling features when ready instead of the first moment the code is released +- Testing a feature with a specific set of users (by enabling only on a specific + Element instance) The size of the feature controlled by a feature flag may vary widely: it could be a large project like reactions or a smaller change to an existing algorithm. diff --git a/docs/features/composer.md b/docs/features/composer.md index 408c78a8d9..1af4c9c894 100644 --- a/docs/features/composer.md +++ b/docs/features/composer.md @@ -2,37 +2,37 @@ ## Auto Complete -- Hitting tab tries to auto-complete the word before the caret as a room member - - If no matching name is found, a visual bell is shown -- @ + a letter opens auto complete for members starting with the given letter - - When inserting a user pill at the start in the composer, a colon and space is appended to the pill - - When inserting a user pill anywhere else in composer, only a space is appended to the pill -- # + a letter opens auto complete for rooms starting with the given letter -- : open auto complete for emoji -- Pressing arrow-up/arrow-down while the autocomplete is open navigates between auto complete options -- Pressing tab while the autocomplete is open goes to the next autocomplete option, - wrapping around at the end after reverting to the typed text first. +- Hitting tab tries to auto-complete the word before the caret as a room member + - If no matching name is found, a visual bell is shown +- @ + a letter opens auto complete for members starting with the given letter + - When inserting a user pill at the start in the composer, a colon and space is appended to the pill + - When inserting a user pill anywhere else in composer, only a space is appended to the pill +- # + a letter opens auto complete for rooms starting with the given letter +- : open auto complete for emoji +- Pressing arrow-up/arrow-down while the autocomplete is open navigates between auto complete options +- Pressing tab while the autocomplete is open goes to the next autocomplete option, + wrapping around at the end after reverting to the typed text first. ## Formatting -- When selecting text, a formatting bar appears above the selection. -- The formatting bar allows to format the selected test as: - bold, italic, strikethrough, a block quote, and a code block (inline if no linebreak is selected). -- Formatting is applied as markdown syntax. -- Hitting ctrl/cmd+B also marks the selected text as bold -- Hitting ctrl/cmd+I also marks the selected text as italic -- Hitting ctrl/cmd+> also marks the selected text as a blockquote +- When selecting text, a formatting bar appears above the selection. +- The formatting bar allows to format the selected test as: + bold, italic, strikethrough, a block quote, and a code block (inline if no linebreak is selected). +- Formatting is applied as markdown syntax. +- Hitting ctrl/cmd+B also marks the selected text as bold +- Hitting ctrl/cmd+I also marks the selected text as italic +- Hitting ctrl/cmd+> also marks the selected text as a blockquote ## Misc -- When hitting the arrow-up button while having the caret at the start in the composer, - the last message sent by the syncing user is edited. -- Clicking a display name on an event in the timeline inserts a user pill into the composer -- Emoticons (like :-), >:-), :-/, ...) are replaced by emojis while typing if the relevant setting is enabled -- Typing in the composer sends typing notifications in the room -- Pressing ctrl/mod+z and ctrl/mod+y undoes/redoes modifications -- Pressing shift+enter inserts a line break -- Pressing enter sends the message. -- Choosing "Quote" in the context menu of an event inserts a quote of the event body in the composer. -- Choosing "Reply" in the context menu of an event shows a preview above the composer to reply to. -- Pressing alt+arrow up/arrow down navigates in previously sent messages, putting them in the composer. +- When hitting the arrow-up button while having the caret at the start in the composer, + the last message sent by the syncing user is edited. +- Clicking a display name on an event in the timeline inserts a user pill into the composer +- Emoticons (like :-), >:-), :-/, ...) are replaced by emojis while typing if the relevant setting is enabled +- Typing in the composer sends typing notifications in the room +- Pressing ctrl/mod+z and ctrl/mod+y undoes/redoes modifications +- Pressing shift+enter inserts a line break +- Pressing enter sends the message. +- Choosing "Quote" in the context menu of an event inserts a quote of the event body in the composer. +- Choosing "Reply" in the context menu of an event shows a preview above the composer to reply to. +- Pressing alt+arrow up/arrow down navigates in previously sent messages, putting them in the composer. diff --git a/docs/icons.md b/docs/icons.md index b0582356ce..449663e24a 100644 --- a/docs/icons.md +++ b/docs/icons.md @@ -8,9 +8,9 @@ Icons have `role="presentation"` and `aria-hidden` automatically applied. These SVG file recommendations: -- Colours should not be defined absolutely. Use `currentColor` instead. -- SVG files should be taken from the design compound as they are. Some icons contain special padding. - This means that there should be icons for each size, e.g. warning-16px and warning-32px. +- Colours should not be defined absolutely. Use `currentColor` instead. +- SVG files should be taken from the design compound as they are. Some icons contain special padding. + This means that there should be icons for each size, e.g. warning-16px and warning-32px. Example usage: diff --git a/docs/jitsi.md b/docs/jitsi.md index 48d1a7bf3e..20e64db379 100644 --- a/docs/jitsi.md +++ b/docs/jitsi.md @@ -81,27 +81,27 @@ which takes several parameters: _Query string_: -- `widgetId`: The ID of the widget. This is needed for communication back to the - react-sdk. -- `parentUrl`: The URL of the parent window. This is also needed for - communication back to the react-sdk. +- `widgetId`: The ID of the widget. This is needed for communication back to the + react-sdk. +- `parentUrl`: The URL of the parent window. This is also needed for + communication back to the react-sdk. _Hash/fragment (formatted as a query string)_: -- `conferenceDomain`: The domain to connect Jitsi Meet to. -- `conferenceId`: The room or conference ID to connect Jitsi Meet to. -- `isAudioOnly`: Boolean for whether this is a voice-only conference. May not - be present, should default to `false`. -- `startWithAudioMuted`: Boolean for whether the calls start with audio - muted. May not be present. -- `startWithVideoMuted`: Boolean for whether the calls start with video - muted. May not be present. -- `displayName`: The display name of the user viewing the widget. May not - be present or could be null. -- `avatarUrl`: The HTTP(S) URL for the avatar of the user viewing the widget. May - not be present or could be null. -- `userId`: The MXID of the user viewing the widget. May not be present or could - be null. +- `conferenceDomain`: The domain to connect Jitsi Meet to. +- `conferenceId`: The room or conference ID to connect Jitsi Meet to. +- `isAudioOnly`: Boolean for whether this is a voice-only conference. May not + be present, should default to `false`. +- `startWithAudioMuted`: Boolean for whether the calls start with audio + muted. May not be present. +- `startWithVideoMuted`: Boolean for whether the calls start with video + muted. May not be present. +- `displayName`: The display name of the user viewing the widget. May not + be present or could be null. +- `avatarUrl`: The HTTP(S) URL for the avatar of the user viewing the widget. May + not be present or could be null. +- `userId`: The MXID of the user viewing the widget. May not be present or could + be null. The react-sdk will assume that `jitsi.html` is at the path of wherever it is currently being served. For example, `https://develop.element.io/jitsi.html` or `vector://webapp/jitsi.html`. diff --git a/docs/playwright.md b/docs/playwright.md index 7eae8e783d..4af3194220 100644 --- a/docs/playwright.md +++ b/docs/playwright.md @@ -2,10 +2,10 @@ ## Contents -- How to run the tests -- How the tests work -- How to write great Playwright tests -- Visual testing +- How to run the tests +- How the tests work +- How to write great Playwright tests +- Visual testing ## Running the Tests @@ -123,15 +123,15 @@ When a Synapse instance is started, it's given a config generated from one of th templates in `playwright/plugins/homeserver/synapse/templates`. There are a couple of special files in these templates: -- `homeserver.yaml`: - Template substitution happens in this file. Template variables are: - - `REGISTRATION_SECRET`: The secret used to register users via the REST API. - - `MACAROON_SECRET_KEY`: Generated each time for security - - `FORM_SECRET`: Generated each time for security - - `PUBLIC_BASEURL`: The localhost url + port combination the synapse is accessible at -- `localhost.signing.key`: A signing key is auto-generated and saved to this file. - Config templates should not contain a signing key and instead assume that one will exist - in this file. +- `homeserver.yaml`: + Template substitution happens in this file. Template variables are: + - `REGISTRATION_SECRET`: The secret used to register users via the REST API. + - `MACAROON_SECRET_KEY`: Generated each time for security + - `FORM_SECRET`: Generated each time for security + - `PUBLIC_BASEURL`: The localhost url + port combination the synapse is accessible at +- `localhost.signing.key`: A signing key is auto-generated and saved to this file. + Config templates should not contain a signing key and instead assume that one will exist + in this file. All other files in the template are copied recursively to `/data/`, so the file `foo.html` in a template can be referenced in the config as `/data/foo.html`. @@ -217,3 +217,10 @@ instead of the native `toHaveScreenshot`. If you are running Linux and are unfortunate that the screenshots are not rendering identically, you may wish to specify `--ignore-snapshots` and rely on Docker to render them for you. + +## Test Tags + +We use test tags to categorise tests for running subsets more efficiently. + +- `@mergequeue`: Tests that are slow or flaky and cover areas of the app we update seldom, should not be run on every PR commit but will be run in the Merge Queue. +- `@screenshot`: Tests that use `toMatchScreenshot` to speed up a run of `test:playwright:screenshots`. A test with this tag must not also have the `@mergequeue` tag as this would cause false positives in the stale screenshot detection. diff --git a/docs/release.md b/docs/release.md index 5074039374..b2c797b66b 100644 --- a/docs/release.md +++ b/docs/release.md @@ -82,28 +82,28 @@ This label will automagically convert to `X-Release-Blocker` at the conclusion o This release process revolves around our main repositories: -- [Element Desktop](https://github.com/element-hq/element-desktop/) -- [Element Web](https://github.com/element-hq/element-web/) -- [Matrix JS SDK](https://github.com/matrix-org/matrix-js-sdk/) +- [Element Desktop](https://github.com/element-hq/element-desktop/) +- [Element Web](https://github.com/element-hq/element-web/) +- [Matrix JS SDK](https://github.com/matrix-org/matrix-js-sdk/) We own other repositories, but they have more ad-hoc releases and are not part of the bi-weekly cycle: -- https://github.com/matrix-org/matrix-web-i18n/ -- https://github.com/matrix-org/matrix-react-sdk-module-api +- https://github.com/matrix-org/matrix-web-i18n/ +- https://github.com/matrix-org/matrix-react-sdk-module-api </blockquote></details> <details><summary><h1>Prerequisites</h1></summary><blockquote> -- You must be part of the 2 Releasers GitHub groups: - - <https://github.com/orgs/element-hq/teams/element-web-releasers> - - <https://github.com/orgs/matrix-org/teams/element-web-releasers> -- You will need access to the **VPN** ([docs](https://gitlab.matrix.org/new-vector/internal/-/wikis/SRE/Tailscale)) to be able to follow the instructions under Deploy below. -- You will need the ability to **SSH** in to the production machines to be able to follow the instructions under Deploy below. Ensure that your SSH key has a non-empty passphrase, and you registered your SSH key with Ops. Log a ticket at https://github.com/matrix-org/matrix-ansible-private and ask for: - - Two-factor authentication to be set up on your SSH key. (This is needed to get access to production). - - SSH access to `horme` (staging.element.io and app.element.io) - - Permission to sudo on horme as the user `element` -- You need "**jumphost**" configuration in your local `~/.ssh/config`. This should have been set up as part of your onboarding. +- You must be part of the 2 Releasers GitHub groups: + - <https://github.com/orgs/element-hq/teams/element-web-releasers> + - <https://github.com/orgs/matrix-org/teams/element-web-releasers> +- You will need access to the **VPN** ([docs](https://gitlab.matrix.org/new-vector/internal/-/wikis/SRE/Tailscale)) to be able to follow the instructions under Deploy below. +- You will need the ability to **SSH** in to the production machines to be able to follow the instructions under Deploy below. Ensure that your SSH key has a non-empty passphrase, and you registered your SSH key with Ops. Log a ticket at https://github.com/matrix-org/matrix-ansible-private and ask for: + - Two-factor authentication to be set up on your SSH key. (This is needed to get access to production). + - SSH access to `horme` (staging.element.io and app.element.io) + - Permission to sudo on horme as the user `element` +- You need "**jumphost**" configuration in your local `~/.ssh/config`. This should have been set up as part of your onboarding. </blockquote></details> @@ -177,7 +177,7 @@ For security, you may wish to merge the security advisory private fork or apply It is worth noting that at the end of the Final/Hotfix/Security release `staging` is merged to `master` which is merged back into `develop` - this means that any commit which goes to `staging` will eventually make its way back to the default branch. -- [ ] The staging branch is prepared +- [ ] The staging branch is prepared # Releasing @@ -192,21 +192,21 @@ switched back to the version of the dependency from the master branch to not lea ### Matrix JS SDK -- [ ] Check the draft release which has been generated by [the automation](https://github.com/matrix-org/matrix-js-sdk/actions/workflows/release-drafter.yml) -- [ ] Make any changes to the release notes in the draft release as are necessary - **Do not click publish, only save draft** -- [ ] Kick off a release using [the automation](https://github.com/matrix-org/matrix-js-sdk/actions/workflows/release.yml) - making sure to select the right type of release. For anything other than an RC: choose final. You should not need to ever switch off either of the Publishing options. +- [ ] Check the draft release which has been generated by [the automation](https://github.com/matrix-org/matrix-js-sdk/actions/workflows/release-drafter.yml) +- [ ] Make any changes to the release notes in the draft release as are necessary - **Do not click publish, only save draft** +- [ ] Kick off a release using [the automation](https://github.com/matrix-org/matrix-js-sdk/actions/workflows/release.yml) - making sure to select the right type of release. For anything other than an RC: choose final. You should not need to ever switch off either of the Publishing options. ### Element Web -- [ ] Check the draft release which has been generated by [the automation](https://github.com/element-hq/element-web/actions/workflows/release-drafter.yml) -- [ ] Make any changes to the release notes in the draft release as are necessary - **Do not click publish, only save draft** -- [ ] Kick off a release using [the automation](https://github.com/element-hq/element-web/actions/workflows/release.yml) - making sure to select the right type of release. For anything other than an RC: choose final. You should not need to ever switch off either of the Publishing options. +- [ ] Check the draft release which has been generated by [the automation](https://github.com/element-hq/element-web/actions/workflows/release-drafter.yml) +- [ ] Make any changes to the release notes in the draft release as are necessary - **Do not click publish, only save draft** +- [ ] Kick off a release using [the automation](https://github.com/element-hq/element-web/actions/workflows/release.yml) - making sure to select the right type of release. For anything other than an RC: choose final. You should not need to ever switch off either of the Publishing options. ### Element Desktop -- [ ] Check the draft release which has been generated by [the automation](https://github.com/element-hq/element-desktop/actions/workflows/release-drafter.yml) -- [ ] Make any changes to the release notes in the draft release as are necessary - **Do not click publish, only save draft** -- [ ] Kick off a release using [the automation](https://github.com/element-hq/element-desktop/actions/workflows/release.yml) - making sure to select the right type of release. For anything other than an RC: choose final. You should not need to ever switch off either of the Publishing options. +- [ ] Check the draft release which has been generated by [the automation](https://github.com/element-hq/element-desktop/actions/workflows/release-drafter.yml) +- [ ] Make any changes to the release notes in the draft release as are necessary - **Do not click publish, only save draft** +- [ ] Kick off a release using [the automation](https://github.com/element-hq/element-desktop/actions/workflows/release.yml) - making sure to select the right type of release. For anything other than an RC: choose final. You should not need to ever switch off either of the Publishing options. # Deploying @@ -214,23 +214,23 @@ We ship the SDKs to npm, this happens as part of the release process. We ship Element Web to dockerhub, `*.element.io`, and packages.element.io. We ship Element Desktop to packages.element.io. -- [ ] Check that element-web has shipped to dockerhub -- [ ] Deploy staging.element.io. [See docs.](https://handbook.element.io/books/element-web-team/page/deploying-appstagingelementio) -- [ ] Test staging.element.io +- [ ] Check that element-web has shipped to dockerhub +- [ ] Deploy staging.element.io. [See docs.](https://handbook.element.io/books/element-web-team/page/deploying-appstagingelementio) +- [ ] Test staging.element.io For final releases additionally do these steps: -- [ ] Deploy app.element.io. [See docs.](https://handbook.element.io/books/element-web-team/page/deploying-appstagingelementio) -- [ ] Test app.element.io -- [ ] Ensure Element Web package has shipped to packages.element.io -- [ ] Ensure Element Desktop packages have shipped to packages.element.io +- [ ] Deploy app.element.io. [See docs.](https://handbook.element.io/books/element-web-team/page/deploying-appstagingelementio) +- [ ] Test app.element.io +- [ ] Ensure Element Web package has shipped to packages.element.io +- [ ] Ensure Element Desktop packages have shipped to packages.element.io # Housekeeping We have some manual housekeeping to do in order to prepare for the next release. -- [ ] Update topics using [the automation](https://github.com/element-hq/element-web/actions/workflows/update-topics.yaml). It will autodetect the current latest version. Don't forget the date you supply should be e.g. September 5th (including the "th") for the script to work. -- [ ] Announce the release in [#element-web-announcements:matrix.org](https://matrix.to/#/#element-web-announcements:matrix.org) +- [ ] Update topics using [the automation](https://github.com/element-hq/element-web/actions/workflows/update-topics.yaml). It will autodetect the current latest version. Don't forget the date you supply should be e.g. September 5th (including the "th") for the script to work. +- [ ] Announce the release in [#element-web-announcements:matrix.org](https://matrix.to/#/#element-web-announcements:matrix.org) <details><summary>(show)</summary> @@ -246,15 +246,15 @@ With wording like: For the first RC of a given release cycle do these steps: -- [ ] Go to the [matrix-js-sdk Renovate dashboard](https://github.com/matrix-org/matrix-js-sdk/issues/2406) and click the checkbox to create/update its PRs. +- [ ] Go to the [matrix-js-sdk Renovate dashboard](https://github.com/matrix-org/matrix-js-sdk/issues/2406) and click the checkbox to create/update its PRs. -- [ ] Go to the [element-web Renovate dashboard](https://github.com/element-hq/element-web/issues/22941) and click the checkbox to create/update its PRs. +- [ ] Go to the [element-web Renovate dashboard](https://github.com/element-hq/element-web/issues/22941) and click the checkbox to create/update its PRs. -- [ ] Go to the [element-desktop Renovate dashboard](https://github.com/element-hq/element-desktop/issues/465) and click the checkbox to create/update its PRs. +- [ ] Go to the [element-desktop Renovate dashboard](https://github.com/element-hq/element-desktop/issues/465) and click the checkbox to create/update its PRs. -- [ ] Later, check back and merge the PRs that succeeded to build. The ones that failed will get picked up by the [maintainer](https://docs.google.com/document/d/1V5VINWXATMpz9UBw4IKmVVB8aw3CxM0Jt7igtHnDfSk/edit#). +- [ ] Later, check back and merge the PRs that succeeded to build. The ones that failed will get picked up by the [maintainer](https://docs.google.com/document/d/1V5VINWXATMpz9UBw4IKmVVB8aw3CxM0Jt7igtHnDfSk/edit#). For final releases additionally do these steps: -- [ ] Archive done column on the [team board](https://github.com/orgs/element-hq/projects/67/views/34) _Note: this should be automated_ -- [ ] Add entry to the [milestones diary](https://docs.google.com/document/d/1cpRFJdfNCo2Ps6jqzQmatzbYEToSrQpyBug0aP_iwZE/edit#heading=h.6y55fw4t283z). The document says only to add significant releases, but we add all of them just in case. +- [ ] Archive done column on the [team board](https://github.com/orgs/element-hq/projects/67/views/34) _Note: this should be automated_ +- [ ] Add entry to the [milestones diary](https://docs.google.com/document/d/1cpRFJdfNCo2Ps6jqzQmatzbYEToSrQpyBug0aP_iwZE/edit#heading=h.6y55fw4t283z). The document says only to add significant releases, but we add all of them just in case. diff --git a/docs/review.md b/docs/review.md index 8f8dc5f09b..c565db5297 100644 --- a/docs/review.md +++ b/docs/review.md @@ -10,53 +10,53 @@ When reviewing code, here are some things we look for and also things we avoid: ### We review for -- Correctness -- Performance -- Accessibility -- Security -- Quality via automated and manual testing -- Comments and documentation where needed -- Sharing knowledge of different areas among the team -- Ensuring it's something we're comfortable maintaining for the long term -- Progress indicators and local echo where appropriate with network activity +- Correctness +- Performance +- Accessibility +- Security +- Quality via automated and manual testing +- Comments and documentation where needed +- Sharing knowledge of different areas among the team +- Ensuring it's something we're comfortable maintaining for the long term +- Progress indicators and local echo where appropriate with network activity ### We should avoid -- Style nits that are already handled by the linter -- Dramatically increasing scope +- Style nits that are already handled by the linter +- Dramatically increasing scope ### Good practices -- Use empathetic language - - See also [Mindful Communication in Code - Reviews](https://kickstarter.engineering/a-guide-to-mindful-communication-in-code-reviews-48aab5282e5e) - and [How to Do Code Reviews Like a Human](https://mtlynch.io/human-code-reviews-1/) -- Authors should prefer smaller commits for easier reviewing and bisection -- Reviewers should be explicit about required versus optional changes - - Reviews are conversations and the PR author should feel comfortable - discussing and pushing back on changes before making them -- Reviewers are encouraged to ask for tests where they believe it is reasonable -- Core team should lead by example through their tone and language -- Take the time to thank and point out good code changes -- Using softer language like "please" and "what do you think?" goes a long way - towards making others feel like colleagues working towards a common goal +- Use empathetic language + - See also [Mindful Communication in Code + Reviews](https://kickstarter.engineering/a-guide-to-mindful-communication-in-code-reviews-48aab5282e5e) + and [How to Do Code Reviews Like a Human](https://mtlynch.io/human-code-reviews-1/) +- Authors should prefer smaller commits for easier reviewing and bisection +- Reviewers should be explicit about required versus optional changes + - Reviews are conversations and the PR author should feel comfortable + discussing and pushing back on changes before making them +- Reviewers are encouraged to ask for tests where they believe it is reasonable +- Core team should lead by example through their tone and language +- Take the time to thank and point out good code changes +- Using softer language like "please" and "what do you think?" goes a long way + towards making others feel like colleagues working towards a common goal ### Workflow -- Authors should request review from the element-web team by default (if someone on - the team is clearly the expert in an area, a direct review request to them may - be more appropriate) -- Reviewers should remove the team review request and request review from - themselves when starting a review to avoid double review -- If there are multiple related PRs authors should reference each of the PRs in - the others before requesting review. Reviewers might start reviewing from - different places and could miss other required PRs. -- Avoid force pushing to a PR after the first round of review -- Use the GitHub default of merge commits when landing (avoid alternate options - like squash or rebase) -- PR author merges after review (assuming they have write access) -- Assign issues only when in progress to indicate to others what can be picked - up +- Authors should request review from the element-web team by default (if someone on + the team is clearly the expert in an area, a direct review request to them may + be more appropriate) +- Reviewers should remove the team review request and request review from + themselves when starting a review to avoid double review +- If there are multiple related PRs authors should reference each of the PRs in + the others before requesting review. Reviewers might start reviewing from + different places and could miss other required PRs. +- Avoid force pushing to a PR after the first round of review +- Use the GitHub default of merge commits when landing (avoid alternate options + like squash or rebase) +- PR author merges after review (assuming they have write access) +- Assign issues only when in progress to indicate to others what can be picked + up ## Code Quality @@ -64,10 +64,10 @@ In the past, we have occasionally written different kinds of tests for Element and the SDKs, but it hasn't been a consistent focus. Going forward, we'd like to change that. -- For new features, code reviewers will expect some form of automated testing to - be included by default -- For bug fixes, regression tests are of course great to have, but we don't want - to block fixes on this, so we won't require them at this time +- For new features, code reviewers will expect some form of automated testing to + be included by default +- For bug fixes, regression tests are of course great to have, but we don't want + to block fixes on this, so we won't require them at this time The above policy is not a strict rule, but instead it's meant to be a conversation between the author and reviewer. As an author, try to think about @@ -104,10 +104,10 @@ perspective. In more detail, our usual process for changes that affect the UI or alter user functionality is: -- For changes that will go live when merged, always flag Design and Product - teams as appropriate -- For changes guarded by a feature flag, Design and Product review is not - required (though may still be useful) since we can continue tweaking +- For changes that will go live when merged, always flag Design and Product + teams as appropriate +- For changes guarded by a feature flag, Design and Product review is not + required (though may still be useful) since we can continue tweaking As it can be difficult to review design work from looking at just the changed files in a PR, a [preview site](./pr-previews.md) that includes your changes diff --git a/docs/room-list-store.md b/docs/room-list-store.md index b87bf5f7bd..4e131ee309 100644 --- a/docs/room-list-store.md +++ b/docs/room-list-store.md @@ -6,11 +6,11 @@ It's so complicated it needs its own README. Legend: -- Orange = External event. -- Purple = Deterministic flow. -- Green = Algorithm definition. -- Red = Exit condition/point. -- Blue = Process definition. +- Orange = External event. +- Purple = Deterministic flow. +- Green = Algorithm definition. +- Red = Exit condition/point. +- Blue = Process definition. ## Algorithms involved @@ -68,14 +68,14 @@ simply get the manual sorting algorithm applied to them with no further involvem algorithm. There are 4 categories: Red, Grey, Bold, and Idle. Each has their own definition based off relative (perceived) importance to the user: -- **Red**: The room has unread mentions waiting for the user. -- **Grey**: The room has unread notifications waiting for the user. Notifications are simply unread - messages which cause a push notification or badge count. Typically, this is the default as rooms get - set to 'All Messages'. -- **Bold**: The room has unread messages waiting for the user. Essentially this is a grey room without - a badge/notification count (or 'Mentions Only'/'Muted'). -- **Idle**: No useful (see definition of useful above) activity has occurred in the room since the user - last read it. +- **Red**: The room has unread mentions waiting for the user. +- **Grey**: The room has unread notifications waiting for the user. Notifications are simply unread + messages which cause a push notification or badge count. Typically, this is the default as rooms get + set to 'All Messages'. +- **Bold**: The room has unread messages waiting for the user. Essentially this is a grey room without + a badge/notification count (or 'Mentions Only'/'Muted'). +- **Idle**: No useful (see definition of useful above) activity has occurred in the room since the user + last read it. Conveniently, each tag gets ordered by those categories as presented: red rooms appear above grey, grey above bold, etc. diff --git a/docs/settings.md b/docs/settings.md index 3f0636d380..e555cd7c1e 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -10,13 +10,13 @@ of dealing with the different levels and exposes easy to use getters and setters Granular Settings rely on a series of known levels in order to use the correct value for the scenario. These levels, in order of priority, are: -- `device` - The current user's device -- `room-device` - The current user's device, but only when in a specific room -- `room-account` - The current user's account, but only when in a specific room -- `account` - The current user's account -- `room` - A specific room (setting for all members of the room) -- `config` - Values are defined by the `setting_defaults` key (usually) in `config.json` -- `default` - The hardcoded default for the settings +- `device` - The current user's device +- `room-device` - The current user's device, but only when in a specific room +- `room-account` - The current user's account, but only when in a specific room +- `account` - The current user's account +- `room` - A specific room (setting for all members of the room) +- `config` - Values are defined by the `setting_defaults` key (usually) in `config.json` +- `default` - The hardcoded default for the settings Individual settings may control which levels are appropriate for them as part of the defaults. This is often to ensure that room administrators cannot force account-only settings upon participants. diff --git a/docs/translating-dev.md b/docs/translating-dev.md index e2a8e2c82a..fd1ac23294 100644 --- a/docs/translating-dev.md +++ b/docs/translating-dev.md @@ -2,9 +2,9 @@ ## Requirements -- A working [Development Setup](../README.md#setting-up-a-dev-environment) -- Latest LTS version of Node.js installed -- Be able to understand English +- A working [Development Setup](../README.md#setting-up-a-dev-environment) +- Latest LTS version of Node.js installed +- Be able to understand English ## Translating strings vs. marking strings for translation @@ -65,17 +65,17 @@ There you can also require all translations to be redone if the meaning of the s 1. Add it to the array in `_t` for example `_t(TKEY, {variable: this.variable})` 1. Add the variable inside the string. The syntax for variables is `%(variable)s`. Please note the _s_ at the end. The name of the variable has to match the previous used name. -- You can use the special `count` variable to choose between multiple versions of the same string, in order to get the correct pluralization. E.g. `_t('You have %(count)s new messages', { count: 2 })` would show 'You have 2 new messages', while `_t('You have %(count)s new messages', { count: 1 })` would show 'You have one new message' (assuming a singular version of the string has been added to the translation file. See above). Passing in `count` is much preferred over having an if-statement choose the correct string to use, because some languages have much more complicated plural rules than english (e.g. they might need a completely different form if there are three things rather than two). -- If you want to translate text that includes e.g. hyperlinks or other HTML you have to also use tag substitution, e.g. `_t('<a>Click here!</a>', {}, { 'a': (sub) => <a>{sub}</a> })`. If you don't do the tag substitution you will end up showing literally '<a>' rather than making a hyperlink. -- You can also use React components with normal variable substitution if you want to insert HTML markup, e.g. `_t('Your email address is %(emailAddress)s', { emailAddress: <i>{userEmailAddress}</i> })`. +- You can use the special `count` variable to choose between multiple versions of the same string, in order to get the correct pluralization. E.g. `_t('You have %(count)s new messages', { count: 2 })` would show 'You have 2 new messages', while `_t('You have %(count)s new messages', { count: 1 })` would show 'You have one new message' (assuming a singular version of the string has been added to the translation file. See above). Passing in `count` is much preferred over having an if-statement choose the correct string to use, because some languages have much more complicated plural rules than english (e.g. they might need a completely different form if there are three things rather than two). +- If you want to translate text that includes e.g. hyperlinks or other HTML you have to also use tag substitution, e.g. `_t('<a>Click here!</a>', {}, { 'a': (sub) => <a>{sub}</a> })`. If you don't do the tag substitution you will end up showing literally '<a>' rather than making a hyperlink. +- You can also use React components with normal variable substitution if you want to insert HTML markup, e.g. `_t('Your email address is %(emailAddress)s', { emailAddress: <i>{userEmailAddress}</i> })`. ## Things to know/Style Guides -- Do not use `_t()` inside `getDefaultProps`: the translations aren't loaded when `getDefaultProps` is called, leading to missing translations. Use `_td()` to indicate that `_t()` will be called on the string later. -- If using translated strings as constants, translated strings can't be in constants loaded at class-load time since the translations won't be loaded. Mark the strings using `_td()` instead and perform the actual translation later. -- If a string is presented in the UI with punctuation like a full stop, include this in the translation strings, since punctuation varies between languages too. -- Avoid "translation in parts", i.e. concatenating translated strings or using translated strings in variable substitutions. Context is important for translations, and translating partial strings this way is simply not always possible. -- Concatenating strings often also introduces an implicit assumption about word order (e.g. that the subject of the sentence comes first), which is incorrect for many languages. -- Translation 'smell test': If you have a string that does not begin with a capital letter (is not the start of a sentence) or it ends with e.g. ':' or a preposition (e.g. 'to') you should recheck that you are not trying to translate a partial sentence. -- If you have multiple strings, that are almost identical, except some part (e.g. a word or two) it is still better to translate the full sentence multiple times. It may seem like inefficient repetition, but unlike programming where you try to minimize repetition, translation is much faster if you have many, full, clear, sentences to work with, rather than fewer, but incomplete sentence fragments. -- Don't forget curly braces when you assign an expression to JSX attributes in the render method) +- Do not use `_t()` inside `getDefaultProps`: the translations aren't loaded when `getDefaultProps` is called, leading to missing translations. Use `_td()` to indicate that `_t()` will be called on the string later. +- If using translated strings as constants, translated strings can't be in constants loaded at class-load time since the translations won't be loaded. Mark the strings using `_td()` instead and perform the actual translation later. +- If a string is presented in the UI with punctuation like a full stop, include this in the translation strings, since punctuation varies between languages too. +- Avoid "translation in parts", i.e. concatenating translated strings or using translated strings in variable substitutions. Context is important for translations, and translating partial strings this way is simply not always possible. +- Concatenating strings often also introduces an implicit assumption about word order (e.g. that the subject of the sentence comes first), which is incorrect for many languages. +- Translation 'smell test': If you have a string that does not begin with a capital letter (is not the start of a sentence) or it ends with e.g. ':' or a preposition (e.g. 'to') you should recheck that you are not trying to translate a partial sentence. +- If you have multiple strings, that are almost identical, except some part (e.g. a word or two) it is still better to translate the full sentence multiple times. It may seem like inefficient repetition, but unlike programming where you try to minimize repetition, translation is much faster if you have many, full, clear, sentences to work with, rather than fewer, but incomplete sentence fragments. +- Don't forget curly braces when you assign an expression to JSX attributes in the render method) diff --git a/docs/translating.md b/docs/translating.md index 657b8cebbc..2b82453f93 100644 --- a/docs/translating.md +++ b/docs/translating.md @@ -2,9 +2,9 @@ ## Requirements -- Web Browser -- Be able to understand English -- Be able to understand the language you want to translate Element into +- Web Browser +- Be able to understand English +- Be able to understand the language you want to translate Element into ## Join #element-translations:matrix.org diff --git a/jest.config.ts b/jest.config.ts index 04f1a91e77..326f2040d9 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -32,7 +32,6 @@ const config: Config = { "decoderWorker\\.min\\.wasm": "<rootDir>/__mocks__/empty.js", "waveWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js", "context-filter-polyfill": "<rootDir>/__mocks__/empty.js", - "FontManager.ts": "<rootDir>/__mocks__/FontManager.js", "workers/(.+)Factory": "<rootDir>/__mocks__/workerFactoryMock.js", "^!!raw-loader!.*": "jest-raw-loader", "recorderWorkletFactory": "<rootDir>/__mocks__/empty.js", diff --git a/package.json b/package.json index a48284bb97..9cd0163945 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "element-web", - "version": "1.11.86", + "version": "1.11.87", "description": "A feature-rich client for Matrix.org", "author": "New Vector Ltd.", "repository": { @@ -64,7 +64,7 @@ "test:playwright:open": "yarn test:playwright --ui", "test:playwright:screenshots": "yarn test:playwright:screenshots:build && yarn test:playwright:screenshots:run", "test:playwright:screenshots:build": "docker build playwright -t element-web-playwright", - "test:playwright:screenshots:run": "docker run --rm --network host -e BASE_URL -e CI -v $(pwd):/work/ -v $(node -e 'console.log(require(`path`).dirname(require.resolve(`matrix-js-sdk/package.json`)))'):/work/node_modules/matrix-js-sdk -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/:/tmp/ -it element-web-playwright", + "test:playwright:screenshots:run": "docker run --rm --network host -e BASE_URL -e CI -v $(pwd):/work/ -v $(node -e 'console.log(require(`path`).dirname(require.resolve(`matrix-js-sdk/package.json`)))'):/work/node_modules/matrix-js-sdk -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/:/tmp/ -it element-web-playwright --grep @screenshot", "coverage": "yarn test --coverage", "analyse:unused-exports": "ts-node ./scripts/analyse_unused_exports.ts", "analyse:webpack-bundles": "webpack-bundle-analyzer webpack-stats.json webapp", @@ -73,12 +73,14 @@ "resolutions": { "oidc-client-ts": "3.1.0", "jwt-decode": "4.0.0", - "caniuse-lite": "1.0.30001679", + "caniuse-lite": "1.0.30001684", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0", "wrap-ansi": "npm:wrap-ansi@^7.0.0" }, "dependencies": { "@babel/runtime": "^7.12.5", + "@fontsource/inconsolata": "^5", + "@fontsource/inter": "^5", "@formatjs/intl-segmenter": "^11.5.7", "@matrix-org/analytics-events": "^0.29.0", "@matrix-org/emojibase-bindings": "^1.3.3", @@ -86,7 +88,7 @@ "@matrix-org/spec": "^1.7.0", "@sentry/browser": "^8.0.0", "@vector-im/compound-design-tokens": "^2.0.1", - "@vector-im/compound-web": "^7.4.0", + "@vector-im/compound-web": "^7.5.0", "@vector-im/matrix-wysiwyg": "2.37.13", "@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4", @@ -114,10 +116,10 @@ "jsrsasign": "^11.0.0", "jszip": "^3.7.0", "katex": "^0.16.0", - "linkify-element": "4.1.3", - "linkify-react": "4.1.3", - "linkify-string": "4.1.3", - "linkifyjs": "4.1.3", + "linkify-element": "4.2.0", + "linkify-react": "4.2.0", + "linkify-string": "4.2.0", + "linkifyjs": "4.2.0", "lodash": "^4.17.21", "maplibre-gl": "^4.0.0", "matrix-encrypt-attachment": "^1.0.3", @@ -214,7 +216,6 @@ "babel-loader": "^9.0.0", "babel-plugin-jsx-remove-data-test-id": "^3.0.0", "blob-polyfill": "^9.0.0", - "buffer": "^6.0.3", "chokidar": "^4.0.0", "concurrently": "^9.0.0", "copy-webpack-plugin": "^12.0.0", @@ -268,11 +269,12 @@ "postcss-preset-env": "^10.0.0", "postcss-scss": "^4.0.4", "postcss-simple-vars": "^7.0.1", - "prettier": "3.3.3", + "prettier": "3.4.2", "process": "^0.11.10", "raw-loader": "^4.0.2", "rimraf": "^6.0.0", "semver": "^7.5.2", + "source-map-loader": "^5.0.0", "stylelint": "^16.1.0", "stylelint-config-standard": "^36.0.0", "stylelint-scss": "^6.0.0", diff --git a/playwright.config.ts b/playwright.config.ts index 1c1d380042..06c1b05322 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -6,11 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { defineConfig } from "@playwright/test"; +import { defineConfig, devices } from "@playwright/test"; const baseURL = process.env["BASE_URL"] ?? "http://localhost:8080"; export default defineConfig({ + projects: [{ name: "Chrome", use: { ...devices["Desktop Chrome"], channel: "chromium" } }], use: { viewport: { width: 1280, height: 720 }, ignoreHTTPSErrors: true, diff --git a/playwright/Dockerfile b/playwright/Dockerfile index 9d478ff231..7e918e04f7 100644 --- a/playwright/Dockerfile +++ b/playwright/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/playwright:v1.48.2-jammy +FROM mcr.microsoft.com/playwright:v1.49.1-noble WORKDIR /work diff --git a/playwright/e2e/app-loading/feature-detection.spec.ts b/playwright/e2e/app-loading/feature-detection.spec.ts index 16e17a8054..ee61fb5662 100644 --- a/playwright/e2e/app-loading/feature-detection.spec.ts +++ b/playwright/e2e/app-loading/feature-detection.spec.ts @@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details. import { test, expect } from "../../element-web-test"; -test(`shows error page if browser lacks Intl support`, async ({ page }) => { +test(`shows error page if browser lacks Intl support`, { tag: "@screenshot" }, async ({ page }) => { await page.addInitScript({ content: `delete window.Intl;` }); await page.goto("/"); @@ -21,7 +21,7 @@ test(`shows error page if browser lacks Intl support`, async ({ page }) => { await expect(page).toMatchScreenshot("unsupported-browser.png"); }); -test(`shows error page if browser lacks WebAssembly support`, async ({ page }) => { +test(`shows error page if browser lacks WebAssembly support`, { tag: "@screenshot" }, async ({ page }) => { await page.addInitScript({ content: `delete window.WebAssembly;` }); await page.goto("/"); diff --git a/playwright/e2e/audio-player/audio-player.spec.ts b/playwright/e2e/audio-player/audio-player.spec.ts index c2081dfcd8..2bb9ab0be4 100644 --- a/playwright/e2e/audio-player/audio-player.spec.ts +++ b/playwright/e2e/audio-player/audio-player.spec.ts @@ -134,18 +134,22 @@ test.describe("Audio player", () => { ).toBeVisible(); }); - test("should be correctly rendered - light theme", async ({ page, app }) => { + test("should be correctly rendered - light theme", { tag: "@screenshot" }, async ({ page, app }) => { await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg"); await takeSnapshots(page, app, "Selected EventTile of audio player (light theme)"); }); - test("should be correctly rendered - light theme with monospace font", async ({ page, app }) => { - await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg"); + test( + "should be correctly rendered - light theme with monospace font", + { tag: "@screenshot" }, + async ({ page, app }) => { + await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg"); - await takeSnapshots(page, app, "Selected EventTile of audio player (light theme, monospace font)", true); // Enable monospace - }); + await takeSnapshots(page, app, "Selected EventTile of audio player (light theme, monospace font)", true); // Enable monospace + }, + ); - test("should be correctly rendered - high contrast theme", async ({ page, app }) => { + test("should be correctly rendered - high contrast theme", { tag: "@screenshot" }, async ({ page, app }) => { // Disable system theme in case ThemeWatcher enables the theme automatically, // so that the high contrast theme can be enabled await app.settings.setValue("use_system_theme", null, SettingLevel.DEVICE, false); @@ -161,7 +165,7 @@ test.describe("Audio player", () => { await takeSnapshots(page, app, "Selected EventTile of audio player (high contrast)"); }); - test("should be correctly rendered - dark theme", async ({ page, app }) => { + test("should be correctly rendered - dark theme", { tag: "@screenshot" }, async ({ page, app }) => { // Enable dark theme await app.settings.setValue("theme", null, SettingLevel.ACCOUNT, "dark"); @@ -207,93 +211,101 @@ test.describe("Audio player", () => { expect(download.suggestedFilename()).toBe("1sec.ogg"); }); - test("should support replying to audio file with another audio file", async ({ page, app }) => { - await uploadFile(page, "playwright/sample-files/1sec.ogg"); + test( + "should support replying to audio file with another audio file", + { tag: "@screenshot" }, + async ({ page, app }) => { + await uploadFile(page, "playwright/sample-files/1sec.ogg"); - // Assert the audio player is rendered - await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible(); + // Assert the audio player is rendered + await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible(); - // Find and click "Reply" button on MessageActionBar - const tile = page.locator(".mx_EventTile_last"); - await tile.hover(); - await tile.getByRole("button", { name: "Reply", exact: true }).click(); - - // Reply to the player with another audio file - await uploadFile(page, "playwright/sample-files/1sec.ogg"); - - // Assert that the audio player is rendered - await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible(); - - // Assert that replied audio file is rendered as file button inside ReplyChain - const button = tile.locator(".mx_ReplyChain_wrapper .mx_MFileBody_info[role='button']"); - // Assert that the file button has file name - await expect(button.locator(".mx_MFileBody_info_filename")).toBeVisible(); - - await takeSnapshots(page, app, "Selected EventTile of audio player with a reply"); - }); - - test("should support creating a reply chain with multiple audio files", async ({ page, app, user }) => { - // Note: "mx_ReplyChain" element is used not only for replies which - // create a reply chain, but also for a single reply without a replied - // message. This test checks whether a reply chain which consists of - // multiple audio file replies is rendered properly. - - const tile = page.locator(".mx_EventTile_last"); - - // Find and click "Reply" button - const clickButtonReply = async () => { - await tile.scrollIntoViewIfNeeded(); + // Find and click "Reply" button on MessageActionBar + const tile = page.locator(".mx_EventTile_last"); await tile.hover(); await tile.getByRole("button", { name: "Reply", exact: true }).click(); - }; - await uploadFile(page, "playwright/sample-files/upload-first.ogg"); + // Reply to the player with another audio file + await uploadFile(page, "playwright/sample-files/1sec.ogg"); - // Assert that the audio player is rendered - await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible(); + // Assert that the audio player is rendered + await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible(); - await clickButtonReply(); + // Assert that replied audio file is rendered as file button inside ReplyChain + const button = tile.locator(".mx_ReplyChain_wrapper .mx_MFileBody_info[role='button']"); + // Assert that the file button has file name + await expect(button.locator(".mx_MFileBody_info_filename")).toBeVisible(); - // Reply to the player with another audio file - await uploadFile(page, "playwright/sample-files/upload-second.ogg"); + await takeSnapshots(page, app, "Selected EventTile of audio player with a reply"); + }, + ); - // Assert that the audio player is rendered - await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible(); + test( + "should support creating a reply chain with multiple audio files", + { tag: "@screenshot" }, + async ({ page, app, user }) => { + // Note: "mx_ReplyChain" element is used not only for replies which + // create a reply chain, but also for a single reply without a replied + // message. This test checks whether a reply chain which consists of + // multiple audio file replies is rendered properly. - await clickButtonReply(); + const tile = page.locator(".mx_EventTile_last"); - // Reply to the player with yet another audio file to create a reply chain - await uploadFile(page, "playwright/sample-files/upload-third.ogg"); + // Find and click "Reply" button + const clickButtonReply = async () => { + await tile.scrollIntoViewIfNeeded(); + await tile.hover(); + await tile.getByRole("button", { name: "Reply", exact: true }).click(); + }; - // Assert that the audio player is rendered - await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible(); + await uploadFile(page, "playwright/sample-files/upload-first.ogg"); - // Assert that there are two "mx_ReplyChain" elements - await expect(tile.locator(".mx_ReplyChain")).toHaveCount(2); + // Assert that the audio player is rendered + await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible(); - // Assert that one line contains the user name - await expect(tile.locator(".mx_ReplyChain .mx_ReplyTile_sender").getByText(user.displayName)).toBeVisible(); + await clickButtonReply(); - // Assert that the other line contains the file button - await expect(tile.locator(".mx_ReplyChain .mx_MFileBody")).toBeVisible(); + // Reply to the player with another audio file + await uploadFile(page, "playwright/sample-files/upload-second.ogg"); - // Click "In reply to" - await tile.locator(".mx_ReplyChain .mx_ReplyChain_show", { hasText: "In reply to" }).click(); + // Assert that the audio player is rendered + await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible(); - const replyChain = tile.locator(".mx_ReplyChain:first-of-type"); - // Assert that "In reply to" has disappeared - await expect(replyChain.getByText("In reply to")).not.toBeVisible(); + await clickButtonReply(); - // Assert that the file button contains the name of the file sent at first - await expect( - replyChain - .locator(".mx_MFileBody_info[role='button']") - .locator(".mx_MFileBody_info_filename", { hasText: "upload-first.ogg" }), - ).toBeVisible(); + // Reply to the player with yet another audio file to create a reply chain + await uploadFile(page, "playwright/sample-files/upload-third.ogg"); - // Take snapshots - await takeSnapshots(page, app, "Selected EventTile of audio player with a reply chain"); - }); + // Assert that the audio player is rendered + await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible(); + + // Assert that there are two "mx_ReplyChain" elements + await expect(tile.locator(".mx_ReplyChain")).toHaveCount(2); + + // Assert that one line contains the user name + await expect(tile.locator(".mx_ReplyChain .mx_ReplyTile_sender").getByText(user.displayName)).toBeVisible(); + + // Assert that the other line contains the file button + await expect(tile.locator(".mx_ReplyChain .mx_MFileBody")).toBeVisible(); + + // Click "In reply to" + await tile.locator(".mx_ReplyChain .mx_ReplyChain_show", { hasText: "In reply to" }).click(); + + const replyChain = tile.locator(".mx_ReplyChain:first-of-type"); + // Assert that "In reply to" has disappeared + await expect(replyChain.getByText("In reply to")).not.toBeVisible(); + + // Assert that the file button contains the name of the file sent at first + await expect( + replyChain + .locator(".mx_MFileBody_info[role='button']") + .locator(".mx_MFileBody_info_filename", { hasText: "upload-first.ogg" }), + ).toBeVisible(); + + // Take snapshots + await takeSnapshots(page, app, "Selected EventTile of audio player with a reply chain"); + }, + ); test("should be rendered, play, and support replying on a thread", async ({ page, app }) => { await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg"); diff --git a/playwright/e2e/chat-export/html-export.spec.ts b/playwright/e2e/chat-export/html-export.spec.ts index 9a66a4907a..f914cccd96 100644 --- a/playwright/e2e/chat-export/html-export.spec.ts +++ b/playwright/e2e/chat-export/html-export.spec.ts @@ -89,43 +89,47 @@ test.describe("HTML Export", () => { }, }); - test("should export html successfully and match screenshot", async ({ page, app, room }) => { - // Set a fixed time rather than masking off the line with the time in it: we don't need to worry - // about the width changing and we can actually test this line looks correct. - page.clock.setSystemTime(new Date("2024-01-01T00:00:00Z")); + test( + "should export html successfully and match screenshot", + { tag: "@screenshot" }, + async ({ page, app, room }) => { + // Set a fixed time rather than masking off the line with the time in it: we don't need to worry + // about the width changing and we can actually test this line looks correct. + page.clock.setSystemTime(new Date("2024-01-01T00:00:00Z")); - // Send a bunch of messages to populate the room - for (let i = 1; i < 10; i++) { - const respone = await app.client.sendMessage(room.roomId, { body: `Testing ${i}`, msgtype: "m.text" }); - if (i == 1) { - await app.client.reactToMessage(room.roomId, null, respone.event_id, "🙃"); + // Send a bunch of messages to populate the room + for (let i = 1; i < 10; i++) { + const respone = await app.client.sendMessage(room.roomId, { body: `Testing ${i}`, msgtype: "m.text" }); + if (i == 1) { + await app.client.reactToMessage(room.roomId, null, respone.event_id, "🙃"); + } } - } - // Wait for all the messages to be displayed - await expect( - page.locator(".mx_EventTile_last .mx_MTextBody .mx_EventTile_body").getByText("Testing 9"), - ).toBeVisible(); + // Wait for all the messages to be displayed + await expect( + page.locator(".mx_EventTile_last .mx_MTextBody .mx_EventTile_body").getByText("Testing 9"), + ).toBeVisible(); - await app.toggleRoomInfoPanel(); - await page.getByRole("menuitem", { name: "Export Chat" }).click(); + await app.toggleRoomInfoPanel(); + await page.getByRole("menuitem", { name: "Export Chat" }).click(); - const downloadPromise = page.waitForEvent("download"); - await page.getByRole("button", { name: "Export", exact: true }).click(); - const download = await downloadPromise; + const downloadPromise = page.waitForEvent("download"); + await page.getByRole("button", { name: "Export", exact: true }).click(); + const download = await downloadPromise; - const dirPath = path.join(os.tmpdir(), "html-export-test"); - const zipPath = `${dirPath}.zip`; - await download.saveAs(zipPath); + const dirPath = path.join(os.tmpdir(), "html-export-test"); + const zipPath = `${dirPath}.zip`; + await download.saveAs(zipPath); - const zip = await extractZipFileToPath(zipPath, dirPath); - await page.goto(`file://${dirPath}/${Object.keys(zip.files)[0]}/messages.html`); - await expect(page).toMatchScreenshot("html-export.png", { - mask: [ - // We need to mask the whole thing because the width of the time part changes - page.locator(".mx_TimelineSeparator"), - page.locator(".mx_MessageTimestamp"), - ], - }); - }); + const zip = await extractZipFileToPath(zipPath, dirPath); + await page.goto(`file://${dirPath}/${Object.keys(zip.files)[0]}/messages.html`); + await expect(page).toMatchScreenshot("html-export.png", { + mask: [ + // We need to mask the whole thing because the width of the time part changes + page.locator(".mx_TimelineSeparator"), + page.locator(".mx_MessageTimestamp"), + ], + }); + }, + ); }); diff --git a/playwright/e2e/crypto/crypto.spec.ts b/playwright/e2e/crypto/crypto.spec.ts index 2ab49e72ec..668c17d931 100644 --- a/playwright/e2e/crypto/crypto.spec.ts +++ b/playwright/e2e/crypto/crypto.spec.ts @@ -204,30 +204,29 @@ test.describe("Cryptography", function () { await expect(page.locator(".mx_Dialog")).toHaveCount(1); }); - test("creating a DM should work, being e2e-encrypted / user verification", async ({ - page, - app, - bot: bob, - user: aliceCredentials, - }) => { - await app.client.bootstrapCrossSigning(aliceCredentials); - await startDMWithBob(page, bob); - // send first message - await page.getByRole("textbox", { name: "Send a message…" }).fill("Hey!"); - await page.getByRole("textbox", { name: "Send a message…" }).press("Enter"); - await checkDMRoom(page); - const bobRoomId = await bobJoin(page, bob); - await testMessages(page, bob, bobRoomId); - await verify(app, bob); + test( + "creating a DM should work, being e2e-encrypted / user verification", + { tag: "@screenshot" }, + async ({ page, app, bot: bob, user: aliceCredentials }) => { + await app.client.bootstrapCrossSigning(aliceCredentials); + await startDMWithBob(page, bob); + // send first message + await page.getByRole("textbox", { name: "Send a message…" }).fill("Hey!"); + await page.getByRole("textbox", { name: "Send a message…" }).press("Enter"); + await checkDMRoom(page); + const bobRoomId = await bobJoin(page, bob); + await testMessages(page, bob, bobRoomId); + await verify(app, bob); - // Assert that verified icon is rendered - await page.getByTestId("base-card-back-button").click(); - await page.getByLabel("Room info").nth(1).click(); - await expect(page.locator('.mx_RoomSummaryCard_badges [data-kind="green"]')).toContainText("Encrypted"); + // Assert that verified icon is rendered + await page.getByTestId("base-card-back-button").click(); + await page.getByLabel("Room info").nth(1).click(); + await expect(page.locator('.mx_RoomSummaryCard_badges [data-kind="green"]')).toContainText("Encrypted"); - // Take a snapshot of RoomSummaryCard with a verified E2EE icon - await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("RoomSummaryCard-with-verified-e2ee.png"); - }); + // Take a snapshot of RoomSummaryCard with a verified E2EE icon + await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("RoomSummaryCard-with-verified-e2ee.png"); + }, + ); test("should allow verification when there is no existing DM", async ({ page, diff --git a/playwright/e2e/crypto/decryption-failure-messages.spec.ts b/playwright/e2e/crypto/decryption-failure-messages.spec.ts index ce7ca34d8e..b2a1209a70 100644 --- a/playwright/e2e/crypto/decryption-failure-messages.spec.ts +++ b/playwright/e2e/crypto/decryption-failure-messages.spec.ts @@ -67,6 +67,9 @@ test.describe("Cryptography", function () { await page.locator(".mx_AuthPage").getByRole("button", { name: "I'll verify later" }).click(); await app.viewRoomByName("Test room"); + // In this case, the call to cryptoApi.isEncryptionEnabledInRoom is taking a long time to resolve + await page.waitForTimeout(1000); + // There should be two historical events in the timeline const tiles = await page.locator(".mx_EventTile").all(); expect(tiles.length).toBeGreaterThanOrEqual(2); diff --git a/playwright/e2e/crypto/device-verification.spec.ts b/playwright/e2e/crypto/device-verification.spec.ts index e01e5bbd83..83a81c260c 100644 --- a/playwright/e2e/crypto/device-verification.spec.ts +++ b/playwright/e2e/crypto/device-verification.spec.ts @@ -102,7 +102,7 @@ test.describe("Device verification", () => { // feed the QR code into the verification request. const qrData = await readQrCode(infoDialog); const verifier = await verificationRequest.evaluateHandle( - (request, qrData) => request.scanQRCode(new Uint8Array(qrData)), + (request, qrData) => request.scanQRCode(new Uint8ClampedArray(qrData)), [...qrData], ); diff --git a/playwright/e2e/crypto/event-shields.spec.ts b/playwright/e2e/crypto/event-shields.spec.ts index b5d3790aaa..0beb8e3650 100644 --- a/playwright/e2e/crypto/event-shields.spec.ts +++ b/playwright/e2e/crypto/event-shields.spec.ts @@ -6,6 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ +import { Locator } from "@playwright/test"; + import { expect, test } from "../../element-web-test"; import { autoJoin, @@ -16,6 +18,8 @@ import { logOutOfElement, verify, } from "./utils"; +import { bootstrapCrossSigningForClient } from "../../pages/client.ts"; +import { ElementAppPage } from "../../pages/ElementAppPage.ts"; test.describe("Cryptography", function () { test.use({ @@ -276,6 +280,15 @@ test.describe("Cryptography", function () { bot: bob, homeserver, }) => { + // Workaround for https://github.com/element-hq/element-web/issues/28640: + // make sure that Alice has seen Bob's identity before she goes offline. We do this by opening + // his user info. + await app.toggleRoomInfoPanel(); + const rightPanel = page.locator(".mx_RightPanel"); + await rightPanel.getByRole("menuitem", { name: "People" }).click(); + await rightPanel.getByRole("button", { name: bob.credentials!.userId }).click(); + await expect(rightPanel.locator(".mx_UserInfo_devices")).toContainText("1 session"); + // Our app is blocked from syncing while Bob sends his messages. await app.client.network.goOffline(); @@ -305,7 +318,50 @@ test.describe("Cryptography", function () { ); const penultimate = page.locator(".mx_EventTile").filter({ hasText: "test encrypted from verified" }); - await expect(penultimate.locator(".mx_EventTile_e2eIcon")).not.toBeVisible(); + await assertNoE2EIcon(penultimate, app); + }); + + test("should show correct shields on events sent by users with changed identity", async ({ + page, + app, + bot: bob, + homeserver, + }) => { + // Verify Bob + await verify(app, bob); + + // Bob logs in a new device and resets cross-signing + const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob); + await bootstrapCrossSigningForClient(await bobSecondDevice.prepareClient(), bob.credentials, true); + + /* should show an error for a message from a previously verified device */ + await bobSecondDevice.sendMessage(testRoomId, "test encrypted from user that was previously verified"); + const last = page.locator(".mx_EventTile_last"); + await expect(last).toContainText("test encrypted from user that was previously verified"); + const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon"); + await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/); + await lastE2eIcon.focus(); + await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText( + "Sender's verified identity has changed", + ); }); }); }); + +/** + * Check that the given message doesn't have an E2E warning icon. + * + * If it does, throw an error. + */ +async function assertNoE2EIcon(messageLocator: Locator, app: ElementAppPage) { + // Make sure the message itself exists, before we check if it has any icons + await messageLocator.waitFor(); + + const e2eIcon = messageLocator.locator(".mx_EventTile_e2eIcon"); + if ((await e2eIcon.count()) > 0) { + // uh-oh, there is an e2e icon. Let's find out what it's about so that we can throw a helpful error. + await e2eIcon.focus(); + const tooltip = await app.getTooltipForElement(e2eIcon); + throw new Error(`Found an unexpected e2eIcon with tooltip '${await tooltip.textContent()}'`); + } +} diff --git a/playwright/e2e/crypto/user-verification.spec.ts b/playwright/e2e/crypto/user-verification.spec.ts index 4c8d641e6f..bd3d859526 100644 --- a/playwright/e2e/crypto/user-verification.spec.ts +++ b/playwright/e2e/crypto/user-verification.spec.ts @@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details. import { type Preset, type Visibility } from "matrix-js-sdk/src/matrix"; +import type { Page } from "@playwright/test"; import { test, expect } from "../../element-web-test"; import { doTwoWaySasVerification, awaitVerifier } from "./utils"; import { Client } from "../../pages/client"; @@ -38,6 +39,8 @@ test.describe("User verification", () => { toasts, room: { roomId: dmRoomId }, }) => { + await waitForDeviceKeys(page); + // once Alice has joined, Bob starts the verification const bobVerificationRequest = await bob.evaluateHandle( async (client, { dmRoomId, aliceCredentials }) => { @@ -87,6 +90,8 @@ test.describe("User verification", () => { toasts, room: { roomId: dmRoomId }, }) => { + await waitForDeviceKeys(page); + // once Alice has joined, Bob starts the verification const bobVerificationRequest = await bob.evaluateHandle( async (client, { dmRoomId, aliceCredentials }) => { @@ -149,3 +154,15 @@ async function createDMRoom(client: Client, userId: string): Promise<string> { ], }); } + +/** + * Wait until we get the other user's device keys. + * In newer rust-crypto versions, the verification request will be ignored if we + * don't have the sender's device keys. + */ +async function waitForDeviceKeys(page: Page): Promise<void> { + await expect(page.getByRole("button", { name: "Avatar" })).toBeVisible(); + const avatar = await page.getByRole("button", { name: "Avatar" }); + await avatar.click(); + await expect(page.getByText("1 session")).toBeVisible(); +} diff --git a/playwright/e2e/editing/editing.spec.ts b/playwright/e2e/editing/editing.spec.ts index 206d91982e..e22ec250b9 100644 --- a/playwright/e2e/editing/editing.spec.ts +++ b/playwright/e2e/editing/editing.spec.ts @@ -66,126 +66,130 @@ test.describe("Editing", () => { botCreateOpts: { displayName: "Bob" }, }); - test("should render and interact with the message edit history dialog", async ({ page, user, app, room }) => { - // Click the "Remove" button on the message edit history dialog - const clickButtonRemove = async (locator: Locator) => { - const eventTileLine = locator.locator(".mx_EventTile_line"); - await eventTileLine.hover(); - await eventTileLine.getByRole("button", { name: "Remove" }).click(); - }; + test( + "should render and interact with the message edit history dialog", + { tag: "@screenshot" }, + async ({ page, user, app, room }) => { + // Click the "Remove" button on the message edit history dialog + const clickButtonRemove = async (locator: Locator) => { + const eventTileLine = locator.locator(".mx_EventTile_line"); + await eventTileLine.hover(); + await eventTileLine.getByRole("button", { name: "Remove" }).click(); + }; - await page.goto(`#/room/${room.roomId}`); + await page.goto(`#/room/${room.roomId}`); - // Send "Message" - await sendEvent(app, room.roomId); + // Send "Message" + await sendEvent(app, room.roomId); - // Edit "Message" to "Massage" - await editLastMessage(page, "Massage"); + // Edit "Message" to "Massage" + await editLastMessage(page, "Massage"); - // Assert that the edit label is visible - await expect(page.locator(".mx_EventTile_edited")).toBeVisible(); + // Assert that the edit label is visible + await expect(page.locator(".mx_EventTile_edited")).toBeVisible(); - await clickEditedMessage(page, "Massage"); + await clickEditedMessage(page, "Massage"); - // Assert that the message edit history dialog is rendered - const dialog = page.getByRole("dialog"); - const li = dialog.getByRole("listitem").last(); - // Assert CSS styles which are difficult or cannot be detected with snapshots are applied as expected - await expect(li).toHaveCSS("clear", "both"); + // Assert that the message edit history dialog is rendered + const dialog = page.getByRole("dialog"); + const li = dialog.getByRole("listitem").last(); + // Assert CSS styles which are difficult or cannot be detected with snapshots are applied as expected + await expect(li).toHaveCSS("clear", "both"); - const timestamp = li.locator(".mx_EventTile .mx_MessageTimestamp"); - await expect(timestamp).toHaveCSS("position", "absolute"); - await expect(timestamp).toHaveCSS("inset-inline-start", "0px"); - await expect(timestamp).toHaveCSS("text-align", "center"); + const timestamp = li.locator(".mx_EventTile .mx_MessageTimestamp"); + await expect(timestamp).toHaveCSS("position", "absolute"); + await expect(timestamp).toHaveCSS("inset-inline-start", "0px"); + await expect(timestamp).toHaveCSS("text-align", "center"); - // Assert that monospace characters can fill the content line as expected - await expect(li.locator(".mx_EventTile .mx_EventTile_content")).toHaveCSS("margin-inline-end", "0px"); + // Assert that monospace characters can fill the content line as expected + await expect(li.locator(".mx_EventTile .mx_EventTile_content")).toHaveCSS("margin-inline-end", "0px"); - // Assert that zero block start padding is applied to mx_EventTile as expected - // See: .mx_EventTile on _EventTile.pcss - await expect(li.locator(".mx_EventTile")).toHaveCSS("padding-block-start", "0px"); + // Assert that zero block start padding is applied to mx_EventTile as expected + // See: .mx_EventTile on _EventTile.pcss + await expect(li.locator(".mx_EventTile")).toHaveCSS("padding-block-start", "0px"); - // Assert that the date separator is rendered at the top - await expect(dialog.getByRole("listitem").first().locator("h2", { hasText: "today" })).toHaveCSS( - "text-transform", - "capitalize", - ); + // Assert that the date separator is rendered at the top + await expect(dialog.getByRole("listitem").first().locator("h2", { hasText: "today" })).toHaveCSS( + "text-transform", + "capitalize", + ); - { - // Assert that the edited message is rendered under the date separator - const tile = dialog.locator("li:nth-child(2) .mx_EventTile"); - // Assert that the edited message body consists of both deleted character and inserted character - // Above the first "e" of "Message" was replaced with "a" - await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage"); + { + // Assert that the edited message is rendered under the date separator + const tile = dialog.locator("li:nth-child(2) .mx_EventTile"); + // Assert that the edited message body consists of both deleted character and inserted character + // Above the first "e" of "Message" was replaced with "a" + await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage"); - const body = tile.locator(".mx_EventTile_content .mx_EventTile_body"); - await expect(body.locator(".mx_EditHistoryMessage_deletion").getByText("e")).toBeVisible(); - await expect(body.locator(".mx_EditHistoryMessage_insertion").getByText("a")).toBeVisible(); - } + const body = tile.locator(".mx_EventTile_content .mx_EventTile_body"); + await expect(body.locator(".mx_EditHistoryMessage_deletion").getByText("e")).toBeVisible(); + await expect(body.locator(".mx_EditHistoryMessage_insertion").getByText("a")).toBeVisible(); + } - // Assert that the original message is rendered at the bottom - await expect( - dialog - .locator("li:nth-child(3) .mx_EventTile") - .locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Message" }), - ).toBeVisible(); + // Assert that the original message is rendered at the bottom + await expect( + dialog + .locator("li:nth-child(3) .mx_EventTile") + .locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Message" }), + ).toBeVisible(); - // Take a snapshot of the dialog - await expect(dialog).toMatchScreenshot("message-edit-history-dialog.png", { - mask: [page.locator(".mx_MessageTimestamp")], - }); + // Take a snapshot of the dialog + await expect(dialog).toMatchScreenshot("message-edit-history-dialog.png", { + mask: [page.locator(".mx_MessageTimestamp")], + }); - { - const tile = dialog.locator("li:nth-child(2) .mx_EventTile"); - await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage"); - // Click the "Remove" button again - await clickButtonRemove(tile); - } + { + const tile = dialog.locator("li:nth-child(2) .mx_EventTile"); + await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage"); + // Click the "Remove" button again + await clickButtonRemove(tile); + } - // Do nothing and close the dialog to confirm that the message edit history dialog is rendered - await app.closeDialog(); + // Do nothing and close the dialog to confirm that the message edit history dialog is rendered + await app.closeDialog(); - { - // Assert that the message edit history dialog is rendered again after it was closed - const tile = dialog.locator("li:nth-child(2) .mx_EventTile"); - await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage"); - // Click the "Remove" button again - await clickButtonRemove(tile); - } + { + // Assert that the message edit history dialog is rendered again after it was closed + const tile = dialog.locator("li:nth-child(2) .mx_EventTile"); + await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage"); + // Click the "Remove" button again + await clickButtonRemove(tile); + } - // This time remove the message really - const textInputDialog = page.locator(".mx_TextInputDialog"); - await textInputDialog.getByRole("textbox", { name: "Reason (optional)" }).fill("This is a test."); // Reason - await textInputDialog.getByRole("button", { name: "Remove" }).click(); + // This time remove the message really + const textInputDialog = page.locator(".mx_TextInputDialog"); + await textInputDialog.getByRole("textbox", { name: "Reason (optional)" }).fill("This is a test."); // Reason + await textInputDialog.getByRole("button", { name: "Remove" }).click(); - // Assert that the message edit history dialog is rendered again - const messageEditHistoryDialog = page.locator(".mx_MessageEditHistoryDialog"); - // Assert that the date is rendered - await expect( - messageEditHistoryDialog.getByRole("listitem").first().locator("h2", { hasText: "today" }), - ).toHaveCSS("text-transform", "capitalize"); + // Assert that the message edit history dialog is rendered again + const messageEditHistoryDialog = page.locator(".mx_MessageEditHistoryDialog"); + // Assert that the date is rendered + await expect( + messageEditHistoryDialog.getByRole("listitem").first().locator("h2", { hasText: "today" }), + ).toHaveCSS("text-transform", "capitalize"); - // Assert that the original message is rendered under the date on the dialog - await expect( - messageEditHistoryDialog - .locator("li:nth-child(2) .mx_EventTile") - .locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Message" }), - ).toBeVisible(); + // Assert that the original message is rendered under the date on the dialog + await expect( + messageEditHistoryDialog + .locator("li:nth-child(2) .mx_EventTile") + .locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Message" }), + ).toBeVisible(); - // Assert that the edited message is gone - await expect( - messageEditHistoryDialog.locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Meassage" }), - ).not.toBeVisible(); + // Assert that the edited message is gone + await expect( + messageEditHistoryDialog.locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Meassage" }), + ).not.toBeVisible(); - await app.closeDialog(); + await app.closeDialog(); - // Assert that the redaction placeholder is rendered - await expect( - page - .locator(".mx_RoomView_MessageList") - .locator(".mx_EventTile_last .mx_RedactedBody", { hasText: "Message deleted" }), - ).toBeVisible(); - }); + // Assert that the redaction placeholder is rendered + await expect( + page + .locator(".mx_RoomView_MessageList") + .locator(".mx_EventTile_last .mx_RedactedBody", { hasText: "Message deleted" }), + ).toBeVisible(); + }, + ); test("should render 'View Source' button in developer mode on the message edit history dialog", async ({ page, diff --git a/playwright/e2e/file-upload/image-upload.spec.ts b/playwright/e2e/file-upload/image-upload.spec.ts index eb473d83b2..76782e90e8 100644 --- a/playwright/e2e/file-upload/image-upload.spec.ts +++ b/playwright/e2e/file-upload/image-upload.spec.ts @@ -25,7 +25,7 @@ test.describe("Image Upload", () => { ).toBeVisible(); }); - test("should show image preview when uploading an image", async ({ page, app }) => { + test("should show image preview when uploading an image", { tag: "@screenshot" }, async ({ page, app }) => { await page .locator(".mx_MessageComposer_actions input[type='file']") .setInputFiles("playwright/sample-files/riot.png"); diff --git a/playwright/e2e/forgot-password/forgot-password.spec.ts b/playwright/e2e/forgot-password/forgot-password.spec.ts index c148900afd..0a12514d9e 100644 --- a/playwright/e2e/forgot-password/forgot-password.spec.ts +++ b/playwright/e2e/forgot-password/forgot-password.spec.ts @@ -26,7 +26,7 @@ test.describe("Forgot Password", () => { }), }); - test("renders properly", async ({ page, homeserver }) => { + test("renders properly", { tag: "@screenshot" }, async ({ page, homeserver }) => { await page.goto("/"); await page.getByRole("link", { name: "Sign in" }).click(); @@ -39,7 +39,7 @@ test.describe("Forgot Password", () => { await expect(page.getByRole("main")).toMatchScreenshot("forgot-password.png"); }); - test("renders email verification dialog properly", async ({ page, homeserver }) => { + test("renders email verification dialog properly", { tag: "@screenshot" }, async ({ page, homeserver }) => { const user = await homeserver.registerUser(username, password); await homeserver.setThreepid(user.userId, "email", email); diff --git a/playwright/e2e/invite/invite-dialog.spec.ts b/playwright/e2e/invite/invite-dialog.spec.ts index c8bd8eb404..eb434eb5b5 100644 --- a/playwright/e2e/invite/invite-dialog.spec.ts +++ b/playwright/e2e/invite/invite-dialog.spec.ts @@ -19,7 +19,7 @@ test.describe("Invite dialog", function () { const botName = "BotAlice"; - test("should support inviting a user to a room", async ({ page, app, user, bot }) => { + test("should support inviting a user to a room", { tag: "@screenshot" }, async ({ page, app, user, bot }) => { // Create and view a room await app.client.createRoom({ name: "Test Room" }); await app.viewRoomByName("Test Room"); @@ -73,52 +73,63 @@ test.describe("Invite dialog", function () { await expect(page.getByText(`${botName} joined the room`)).toBeVisible(); }); - test("should support inviting a user to Direct Messages", async ({ page, app, user, bot }) => { - await page.locator(".mx_RoomList").getByRole("button", { name: "Start chat" }).click(); + test( + "should support inviting a user to Direct Messages", + { tag: "@screenshot" }, + async ({ page, app, user, bot }) => { + await page.locator(".mx_RoomList").getByRole("button", { name: "Start chat" }).click(); - const other = page.locator(".mx_InviteDialog_other"); - // Assert that the header is rendered - await expect(other.locator(".mx_Dialog_header .mx_Dialog_title").getByText("Direct Messages")).toBeVisible(); + const other = page.locator(".mx_InviteDialog_other"); + // Assert that the header is rendered + await expect( + other.locator(".mx_Dialog_header .mx_Dialog_title").getByText("Direct Messages"), + ).toBeVisible(); - // Assert that the bar is rendered - await expect(other.locator(".mx_InviteDialog_addressBar")).toBeVisible(); + // Assert that the bar is rendered + await expect(other.locator(".mx_InviteDialog_addressBar")).toBeVisible(); - // Take a snapshot of the invite dialog - await expect(page.locator(".mx_Dialog")).toMatchScreenshot("invite-dialog-dm-without-user.png"); + // Take a snapshot of the invite dialog + await expect(page.locator(".mx_Dialog")).toMatchScreenshot("invite-dialog-dm-without-user.png"); - await other.getByTestId("invite-dialog-input").fill(bot.credentials.userId); + await other.getByTestId("invite-dialog-input").fill(bot.credentials.userId); - await expect(other.locator(".mx_InviteDialog_tile_nameStack").getByText(bot.credentials.userId)).toBeVisible(); - await other.locator(".mx_InviteDialog_tile_nameStack").getByText(botName).click(); + await expect( + other.locator(".mx_InviteDialog_tile_nameStack").getByText(bot.credentials.userId), + ).toBeVisible(); + await other.locator(".mx_InviteDialog_tile_nameStack").getByText(botName).click(); - await expect( - other.locator(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name").getByText(botName), - ).toBeVisible(); + await expect( + other.locator(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name").getByText(botName), + ).toBeVisible(); - // Take a snapshot of the invite dialog with a user pill - await expect(page.locator(".mx_Dialog")).toMatchScreenshot("invite-dialog-dm-with-user-pill.png"); + // Take a snapshot of the invite dialog with a user pill + await expect(page.locator(".mx_Dialog")).toMatchScreenshot("invite-dialog-dm-with-user-pill.png"); - // Open a direct message UI - await other.getByRole("button", { name: "Go" }).click(); + // Open a direct message UI + await other.getByRole("button", { name: "Go" }).click(); - // Assert that the invite dialog disappears - await expect(page.locator(".mx_InviteDialog_other")).not.toBeVisible(); + // Assert that the invite dialog disappears + await expect(page.locator(".mx_InviteDialog_other")).not.toBeVisible(); - // Assert that the hovered user name on invitation UI does not have background color - // TODO: implement the test on room-header.spec.ts - const roomHeader = page.locator(".mx_RoomHeader"); - await roomHeader.locator(".mx_RoomHeader_heading").hover(); - await expect(roomHeader.locator(".mx_RoomHeader_heading")).toHaveCSS("background-color", "rgba(0, 0, 0, 0)"); + // Assert that the hovered user name on invitation UI does not have background color + // TODO: implement the test on room-header.spec.ts + const roomHeader = page.locator(".mx_RoomHeader"); + await roomHeader.locator(".mx_RoomHeader_heading").hover(); + await expect(roomHeader.locator(".mx_RoomHeader_heading")).toHaveCSS( + "background-color", + "rgba(0, 0, 0, 0)", + ); - // Send a message to invite the bots - const composer = app.getComposer().locator("[contenteditable]"); - await composer.fill("Hello}"); - await composer.press("Enter"); + // Send a message to invite the bots + const composer = app.getComposer().locator("[contenteditable]"); + await composer.fill("Hello}"); + await composer.press("Enter"); - // Assert that they were invited and joined - await expect(page.getByText(`${botName} joined the room`)).toBeVisible(); + // Assert that they were invited and joined + await expect(page.getByText(`${botName} joined the room`)).toBeVisible(); - // Assert that the message is displayed at the bottom - await expect(page.locator(".mx_EventTile_last").getByText("Hello")).toBeVisible(); - }); + // Assert that the message is displayed at the bottom + await expect(page.locator(".mx_EventTile_last").getByText("Hello")).toBeVisible(); + }, + ); }); diff --git a/playwright/e2e/messages/messages.spec.ts b/playwright/e2e/messages/messages.spec.ts index 0d5a5da472..1c518199a0 100644 --- a/playwright/e2e/messages/messages.spec.ts +++ b/playwright/e2e/messages/messages.spec.ts @@ -63,7 +63,7 @@ test.describe("Message rendering", () => { { direction: "ltr", displayName: "Quentin" }, { direction: "rtl", displayName: "كوينتين" }, ].forEach(({ direction, displayName }) => { - test.describe(`with ${direction} display name`, () => { + test.describe(`with ${direction} display name`, { tag: "@screenshot" }, () => { test.use({ displayName, room: async ({ user, app }, use) => { @@ -72,14 +72,18 @@ test.describe("Message rendering", () => { }, }); - test("should render a basic LTR text message", async ({ page, user, app, room }) => { - await page.goto(`#/room/${room.roomId}`); + test( + "should render a basic LTR text message", + { tag: "@screenshot" }, + async ({ page, user, app, room }) => { + await page.goto(`#/room/${room.roomId}`); - const msgTile = await sendMessage(page, "Hello, world!"); - await expect(msgTile).toMatchScreenshot(`basic-message-ltr-${direction}displayname.png`, { - mask: [page.locator(".mx_MessageTimestamp")], - }); - }); + const msgTile = await sendMessage(page, "Hello, world!"); + await expect(msgTile).toMatchScreenshot(`basic-message-ltr-${direction}displayname.png`, { + mask: [page.locator(".mx_MessageTimestamp")], + }); + }, + ); test("should render an LTR emote", async ({ page, user, app, room }) => { await page.goto(`#/room/${room.roomId}`); diff --git a/playwright/e2e/permalinks/permalinks.spec.ts b/playwright/e2e/permalinks/permalinks.spec.ts index bd7884ea78..746e15d288 100644 --- a/playwright/e2e/permalinks/permalinks.spec.ts +++ b/playwright/e2e/permalinks/permalinks.spec.ts @@ -24,7 +24,7 @@ test.describe("permalinks", () => { displayName: "Alice", }); - test("shoud render permalinks as expected", async ({ page, app, user, homeserver }) => { + test("shoud render permalinks as expected", { tag: "@screenshot" }, async ({ page, app, user, homeserver }) => { const bob = new Bot(page, homeserver, { displayName: "Bob" }); const charlotte = new Bot(page, homeserver, { displayName: "Charlotte" }); await bob.prepareClient(); diff --git a/playwright/e2e/pinned-messages/index.ts b/playwright/e2e/pinned-messages/index.ts index ac50b62294..545d0e3438 100644 --- a/playwright/e2e/pinned-messages/index.ts +++ b/playwright/e2e/pinned-messages/index.ts @@ -129,6 +129,7 @@ export class Helpers { const timelineMessage = this.page.locator(".mx_MTextBody", { hasText: message }); await timelineMessage.click({ button: "right" }); await this.page.getByRole("menuitem", { name: "Pin", exact: true }).click(); + await this.assertMessageInBanner(message); } /** diff --git a/playwright/e2e/pinned-messages/pinned-messages.spec.ts b/playwright/e2e/pinned-messages/pinned-messages.spec.ts index ef2c1b27d4..06d6db8058 100644 --- a/playwright/e2e/pinned-messages/pinned-messages.spec.ts +++ b/playwright/e2e/pinned-messages/pinned-messages.spec.ts @@ -10,35 +10,38 @@ import { test } from "./index"; import { expect } from "../../element-web-test"; test.describe("Pinned messages", () => { - test("should show the empty state when there are no pinned messages", async ({ page, app, room1, util }) => { - await util.goTo(room1); - await util.openRoomInfo(); - await util.assertPinnedCountInRoomInfo(0); - await util.openPinnedMessagesList(); - await util.assertEmptyPinnedMessagesList(); - }); + test( + "should show the empty state when there are no pinned messages", + { tag: "@screenshot" }, + async ({ page, app, room1, util }) => { + await util.goTo(room1); + await util.openRoomInfo(); + await util.assertPinnedCountInRoomInfo(0); + await util.openPinnedMessagesList(); + await util.assertEmptyPinnedMessagesList(); + }, + ); - test("should pin one message and to have the pinned message badge in the timeline", async ({ - page, - app, - room1, - util, - }) => { - await util.goTo(room1); - await util.receiveMessages(room1, ["Msg1"]); - await util.pinMessages(["Msg1"]); + test( + "should pin one message and to have the pinned message badge in the timeline", + { tag: "@screenshot" }, + async ({ page, app, room1, util }) => { + await util.goTo(room1); + await util.receiveMessages(room1, ["Msg1"]); + await util.pinMessages(["Msg1"]); - const tile = util.getEventTile("Msg1"); - await expect(tile).toMatchScreenshot("pinned-message-Msg1.png", { - mask: [tile.locator(".mx_MessageTimestamp")], - // Hide the jump to bottom button in the timeline to avoid flakiness - css: ` + const tile = util.getEventTile("Msg1"); + await expect(tile).toMatchScreenshot("pinned-message-Msg1.png", { + mask: [tile.locator(".mx_MessageTimestamp")], + // Hide the jump to bottom button in the timeline to avoid flakiness + css: ` .mx_JumpToBottomButton { display: none !important; } `, - }); - }); + }); + }, + ); test("should pin messages and show them in the room info panel", async ({ page, app, room1, util }) => { await util.goTo(room1); @@ -73,7 +76,7 @@ test.describe("Pinned messages", () => { await util.assertPinnedCountInRoomInfo(2); }); - test("should unpin all messages", async ({ page, app, room1, util }) => { + test("should unpin all messages", { tag: "@screenshot" }, async ({ page, app, room1, util }) => { await util.goTo(room1); await util.receiveMessages(room1, ["Msg1", "Msg2", "Msg3", "Msg4"]); await util.pinMessages(["Msg1", "Msg2", "Msg4"]); @@ -98,7 +101,7 @@ test.describe("Pinned messages", () => { await util.assertPinnedCountInRoomInfo(0); }); - test("should display one message in the banner", async ({ page, app, room1, util }) => { + test("should display one message in the banner", { tag: "@screenshot" }, async ({ page, app, room1, util }) => { await util.goTo(room1); await util.receiveMessages(room1, ["Msg1"]); await util.pinMessages(["Msg1"]); @@ -106,7 +109,7 @@ test.describe("Pinned messages", () => { await expect(util.getBanner()).toMatchScreenshot("pinned-message-banner-1-Msg1.png"); }); - test("should display 2 messages in the banner", async ({ page, app, room1, util }) => { + test("should display 2 messages in the banner", { tag: "@screenshot" }, async ({ page, app, room1, util }) => { await util.goTo(room1); await util.receiveMessages(room1, ["Msg1", "Msg2"]); await util.pinMessages(["Msg1", "Msg2"]); @@ -123,7 +126,7 @@ test.describe("Pinned messages", () => { await expect(util.getBanner()).toMatchScreenshot("pinned-message-banner-2-Msg2.png"); }); - test("should display 4 messages in the banner", async ({ page, app, room1, util }) => { + test("should display 4 messages in the banner", { tag: "@screenshot" }, async ({ page, app, room1, util }) => { await util.goTo(room1); await util.receiveMessages(room1, ["Msg1", "Msg2", "Msg3", "Msg4"]); await util.pinMessages(["Msg1", "Msg2", "Msg3", "Msg4"]); diff --git a/playwright/e2e/polls/polls.spec.ts b/playwright/e2e/polls/polls.spec.ts index 4fd8195581..e1d3ebe7e3 100644 --- a/playwright/e2e/polls/polls.spec.ts +++ b/playwright/e2e/polls/polls.spec.ts @@ -93,7 +93,7 @@ test.describe("Polls", () => { }); }); - test("should be creatable and votable", async ({ page, app, bot, user }) => { + test("should be creatable and votable", { tag: "@screenshot" }, async ({ page, app, bot, user }) => { const roomId: string = await app.client.createRoom({}); await app.client.inviteUser(roomId, bot.credentials.userId); await page.goto("/#/room/" + roomId); @@ -219,107 +219,121 @@ test.describe("Polls", () => { await expect(page.locator(".mx_ErrorDialog")).toBeAttached(); }); - test("should be displayed correctly in thread panel", async ({ page, app, user, bot, homeserver }) => { - const botCharlie = new Bot(page, homeserver, { displayName: "BotCharlie" }); - await botCharlie.prepareClient(); + test( + "should be displayed correctly in thread panel", + { tag: "@screenshot" }, + async ({ page, app, user, bot, homeserver }) => { + const botCharlie = new Bot(page, homeserver, { displayName: "BotCharlie" }); + await botCharlie.prepareClient(); - const roomId: string = await app.client.createRoom({}); - await app.client.inviteUser(roomId, bot.credentials.userId); - await app.client.inviteUser(roomId, botCharlie.credentials.userId); - await page.goto("/#/room/" + roomId); + const roomId: string = await app.client.createRoom({}); + await app.client.inviteUser(roomId, bot.credentials.userId); + await app.client.inviteUser(roomId, botCharlie.credentials.userId); + await page.goto("/#/room/" + roomId); - // wait until the bots joined - await expect(page.getByText("BotBob and one other were invited and joined")).toBeAttached({ timeout: 10000 }); + // wait until the bots joined + await expect(page.getByText("BotBob and one other were invited and joined")).toBeAttached({ + timeout: 10000, + }); - const locator = await app.openMessageComposerOptions(); - await locator.getByRole("menuitem", { name: "Poll" }).click(); + const locator = await app.openMessageComposerOptions(); + await locator.getByRole("menuitem", { name: "Poll" }).click(); - const pollParams = { - title: "Does the polls feature work?", - options: ["Yes", "No", "Maybe"], - }; - await createPoll(page, pollParams); + const pollParams = { + title: "Does the polls feature work?", + options: ["Yes", "No", "Maybe"], + }; + await createPoll(page, pollParams); - // Wait for message to send, get its ID and save as @pollId - const pollId = await page - .locator(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]") - .filter({ hasText: pollParams.title }) - .getAttribute("data-scroll-tokens"); + // Wait for message to send, get its ID and save as @pollId + const pollId = await page + .locator(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]") + .filter({ hasText: pollParams.title }) + .getAttribute("data-scroll-tokens"); - // Bob starts thread on the poll - await bot.sendMessage( - roomId, - { - body: "Hello there", - msgtype: "m.text", - }, - pollId, - ); + // Bob starts thread on the poll + await bot.sendMessage( + roomId, + { + body: "Hello there", + msgtype: "m.text", + }, + pollId, + ); - // open the thread summary - await page.getByRole("button", { name: "Open thread" }).click(); + // open the thread summary + await page.getByRole("button", { name: "Open thread" }).click(); - // Bob votes 'Maybe' in the poll - await botVoteForOption(page, bot, roomId, pollId, pollParams.options[2]); + // Bob votes 'Maybe' in the poll + await botVoteForOption(page, bot, roomId, pollId, pollParams.options[2]); - // Charlie votes 'No' - await botVoteForOption(page, botCharlie, roomId, pollId, pollParams.options[1]); + // Charlie votes 'No' + await botVoteForOption(page, botCharlie, roomId, pollId, pollParams.options[1]); - // no votes shown until I vote, check votes have arrived in main tl - await expect( - page - .locator(".mx_RoomView_body .mx_MPollBody_totalVotes") - .getByText("2 votes cast. Vote to see the results"), - ).toBeAttached(); + // no votes shown until I vote, check votes have arrived in main tl + await expect( + page + .locator(".mx_RoomView_body .mx_MPollBody_totalVotes") + .getByText("2 votes cast. Vote to see the results"), + ).toBeAttached(); - // and thread view - await expect( - page.locator(".mx_ThreadView .mx_MPollBody_totalVotes").getByText("2 votes cast. Vote to see the results"), - ).toBeAttached(); + // and thread view + await expect( + page + .locator(".mx_ThreadView .mx_MPollBody_totalVotes") + .getByText("2 votes cast. Vote to see the results"), + ).toBeAttached(); - // Take snapshots of poll on ThreadView - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); - await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='bubble']").first()).toBeVisible(); - await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("ThreadView_with_a_poll_on_bubble_layout.png", { - mask: [page.locator(".mx_MessageTimestamp")], - }); + // Take snapshots of poll on ThreadView + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); + await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='bubble']").first()).toBeVisible(); + await expect(page.locator(".mx_ThreadView")).toMatchScreenshot( + "ThreadView_with_a_poll_on_bubble_layout.png", + { + mask: [page.locator(".mx_MessageTimestamp")], + }, + ); - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group); - await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='group']").first()).toBeVisible(); + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group); + await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='group']").first()).toBeVisible(); - await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("ThreadView_with_a_poll_on_group_layout.png", { - mask: [page.locator(".mx_MessageTimestamp")], - }); + await expect(page.locator(".mx_ThreadView")).toMatchScreenshot( + "ThreadView_with_a_poll_on_group_layout.png", + { + mask: [page.locator(".mx_MessageTimestamp")], + }, + ); - const roomViewLocator = page.locator(".mx_RoomView_body"); - // vote 'Maybe' in the main timeline poll - await getPollOption(page, pollId, pollParams.options[2], roomViewLocator).click(); - // both me and bob have voted Maybe - await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 2, roomViewLocator); + const roomViewLocator = page.locator(".mx_RoomView_body"); + // vote 'Maybe' in the main timeline poll + await getPollOption(page, pollId, pollParams.options[2], roomViewLocator).click(); + // both me and bob have voted Maybe + await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 2, roomViewLocator); - const threadViewLocator = page.locator(".mx_ThreadView"); - // votes updated in thread view too - await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 2, threadViewLocator); - // change my vote to 'Yes' - await getPollOption(page, pollId, pollParams.options[0], threadViewLocator).click(); + const threadViewLocator = page.locator(".mx_ThreadView"); + // votes updated in thread view too + await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 2, threadViewLocator); + // change my vote to 'Yes' + await getPollOption(page, pollId, pollParams.options[0], threadViewLocator).click(); - // Bob updates vote to 'No' - await botVoteForOption(page, bot, roomId, pollId, pollParams.options[1]); + // Bob updates vote to 'No' + await botVoteForOption(page, bot, roomId, pollId, pollParams.options[1]); - // me: yes, bob: no, charlie: no - const expectVoteCounts = async (optLocator: Locator) => { - // I voted yes - await expectPollOptionVoteCount(page, pollId, pollParams.options[0], 1, optLocator); - // Bob and Charlie voted no - await expectPollOptionVoteCount(page, pollId, pollParams.options[1], 2, optLocator); - // 0 for maybe - await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 0, optLocator); - }; + // me: yes, bob: no, charlie: no + const expectVoteCounts = async (optLocator: Locator) => { + // I voted yes + await expectPollOptionVoteCount(page, pollId, pollParams.options[0], 1, optLocator); + // Bob and Charlie voted no + await expectPollOptionVoteCount(page, pollId, pollParams.options[1], 2, optLocator); + // 0 for maybe + await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 0, optLocator); + }; - // check counts are correct in main timeline tile - await expectVoteCounts(page.locator(".mx_RoomView_body")); + // check counts are correct in main timeline tile + await expectVoteCounts(page.locator(".mx_RoomView_body")); - // and in thread view tile - await expectVoteCounts(page.locator(".mx_ThreadView")); - }); + // and in thread view tile + await expectVoteCounts(page.locator(".mx_ThreadView")); + }, + ); }); diff --git a/playwright/e2e/read-receipts/editing-messages-in-threads.spec.ts b/playwright/e2e/read-receipts/editing-messages-in-threads.spec.ts index 277de62876..42191831c8 100644 --- a/playwright/e2e/read-receipts/editing-messages-in-threads.spec.ts +++ b/playwright/e2e/read-receipts/editing-messages-in-threads.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.describe("editing messages", () => { test.describe("in threads", () => { test("An edit of a threaded message makes the room unread", async ({ diff --git a/playwright/e2e/read-receipts/editing-messages-main-timeline.spec.ts b/playwright/e2e/read-receipts/editing-messages-main-timeline.spec.ts index 027ab08e2d..a464822305 100644 --- a/playwright/e2e/read-receipts/editing-messages-main-timeline.spec.ts +++ b/playwright/e2e/read-receipts/editing-messages-main-timeline.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.describe("editing messages", () => { test.describe("in the main timeline", () => { test("Editing a message leaves a room read", async ({ roomAlpha: room1, roomBeta: room2, util, msg }) => { diff --git a/playwright/e2e/read-receipts/editing-messages-thread-roots.spec.ts b/playwright/e2e/read-receipts/editing-messages-thread-roots.spec.ts index e653b5d9bd..506ed603bd 100644 --- a/playwright/e2e/read-receipts/editing-messages-thread-roots.spec.ts +++ b/playwright/e2e/read-receipts/editing-messages-thread-roots.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.describe("editing messages", () => { test.describe("thread roots", () => { test("An edit of a thread root leaves the room read", async ({ diff --git a/playwright/e2e/read-receipts/high-level.spec.ts b/playwright/e2e/read-receipts/high-level.spec.ts index 7d4f4eb133..457cf99481 100644 --- a/playwright/e2e/read-receipts/high-level.spec.ts +++ b/playwright/e2e/read-receipts/high-level.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { customEvent, many, test } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.describe("Ignored events", () => { test("If all events after receipt are unimportant, the room is read", async ({ roomAlpha: room1, diff --git a/playwright/e2e/read-receipts/message-ordering.spec.ts b/playwright/e2e/read-receipts/message-ordering.spec.ts index 65875cf4a9..d7f77fae5f 100644 --- a/playwright/e2e/read-receipts/message-ordering.spec.ts +++ b/playwright/e2e/read-receipts/message-ordering.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.describe("Message ordering", () => { test.describe("in the main timeline", () => { test.fixme( diff --git a/playwright/e2e/read-receipts/missing-referents.spec.ts b/playwright/e2e/read-receipts/missing-referents.spec.ts index f798d7d455..a1741851e2 100644 --- a/playwright/e2e/read-receipts/missing-referents.spec.ts +++ b/playwright/e2e/read-receipts/missing-referents.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.describe("messages with missing referents", () => { test.fixme( "A message in an unknown thread is not visible and the room is read", diff --git a/playwright/e2e/read-receipts/new-messages-in-threads.spec.ts b/playwright/e2e/read-receipts/new-messages-in-threads.spec.ts index 91d850fac8..5407f3cb44 100644 --- a/playwright/e2e/read-receipts/new-messages-in-threads.spec.ts +++ b/playwright/e2e/read-receipts/new-messages-in-threads.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { many, test } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.describe("new messages", () => { test.describe("in threads", () => { test("Receiving a message makes a room unread", async ({ diff --git a/playwright/e2e/read-receipts/new-messages-main-timeline.spec.ts b/playwright/e2e/read-receipts/new-messages-main-timeline.spec.ts index 2000e444d6..92f7b10cdd 100644 --- a/playwright/e2e/read-receipts/new-messages-main-timeline.spec.ts +++ b/playwright/e2e/read-receipts/new-messages-main-timeline.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { many, test } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.describe("new messages", () => { test.describe("in the main timeline", () => { test("Receiving a message makes a room unread", async ({ diff --git a/playwright/e2e/read-receipts/new-messages-thread-roots.spec.ts b/playwright/e2e/read-receipts/new-messages-thread-roots.spec.ts index 878d0d4419..3c8ed7849f 100644 --- a/playwright/e2e/read-receipts/new-messages-thread-roots.spec.ts +++ b/playwright/e2e/read-receipts/new-messages-thread-roots.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { many, test } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.describe("new messages", () => { test.describe("thread roots", () => { test("Reading a thread root does not mark the thread as read", async ({ diff --git a/playwright/e2e/read-receipts/notifications.spec.ts b/playwright/e2e/read-receipts/notifications.spec.ts index 3050987be7..46edc9a7a3 100644 --- a/playwright/e2e/read-receipts/notifications.spec.ts +++ b/playwright/e2e/read-receipts/notifications.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.describe("Notifications", () => { test.describe("in the main timeline", () => { test.fixme("A new message that mentions me shows a notification", () => {}); diff --git a/playwright/e2e/read-receipts/reactions-in-threads.spec.ts b/playwright/e2e/read-receipts/reactions-in-threads.spec.ts index bc4a184744..45b5e071ec 100644 --- a/playwright/e2e/read-receipts/reactions-in-threads.spec.ts +++ b/playwright/e2e/read-receipts/reactions-in-threads.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test, expect } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.describe("reactions", () => { test.describe("in threads", () => { test("A reaction to a threaded message does not make the room unread", async ({ diff --git a/playwright/e2e/read-receipts/reactions-main-timeline.spec.ts b/playwright/e2e/read-receipts/reactions-main-timeline.spec.ts index 59d6eaea40..16d5c92eca 100644 --- a/playwright/e2e/read-receipts/reactions-main-timeline.spec.ts +++ b/playwright/e2e/read-receipts/reactions-main-timeline.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.describe("reactions", () => { test.describe("in the main timeline", () => { test("Receiving a reaction to a message does not make a room unread", async ({ diff --git a/playwright/e2e/read-receipts/reactions-thread-roots.spec.ts b/playwright/e2e/read-receipts/reactions-thread-roots.spec.ts index 219a73d5e4..817597a27e 100644 --- a/playwright/e2e/read-receipts/reactions-thread-roots.spec.ts +++ b/playwright/e2e/read-receipts/reactions-thread-roots.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.describe("reactions", () => { test.describe("thread roots", () => { test("A reaction to a thread root does not make the room unread", async ({ diff --git a/playwright/e2e/read-receipts/read-receipts.spec.ts b/playwright/e2e/read-receipts/read-receipts.spec.ts index 3056cc4a54..f6515361f2 100644 --- a/playwright/e2e/read-receipts/read-receipts.spec.ts +++ b/playwright/e2e/read-receipts/read-receipts.spec.ts @@ -13,7 +13,7 @@ import { ElementAppPage } from "../../pages/ElementAppPage"; import { Bot } from "../../pages/bot"; import { test } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.use({ displayName: "Mae", botCreateOpts: { displayName: "Other User" }, diff --git a/playwright/e2e/read-receipts/readme.md b/playwright/e2e/read-receipts/readme.md index 4e4dce297f..33bcfeb93d 100644 --- a/playwright/e2e/read-receipts/readme.md +++ b/playwright/e2e/read-receipts/readme.md @@ -2,19 +2,19 @@ Tips for writing these tests: -- Break up your tests into the smallest test case possible. The purpose of - these tests is to understand hard-to-find bugs, so small tests are necessary. - We know that Playwright recommends combining tests together for performance, but - that will frustrate our goals here. (We will need to find a different way to - reduce CI time.) +- Break up your tests into the smallest test case possible. The purpose of + these tests is to understand hard-to-find bugs, so small tests are necessary. + We know that Playwright recommends combining tests together for performance, but + that will frustrate our goals here. (We will need to find a different way to + reduce CI time.) -- Try to assert something after every action, to make sure it has completed. - E.g.: - markAsRead(room2); - assertRead(room2); - You should especially follow this rule if you are jumping to a different - room or similar straight afterward. +- Try to assert something after every action, to make sure it has completed. + E.g.: + markAsRead(room2); + assertRead(room2); + You should especially follow this rule if you are jumping to a different + room or similar straight afterward. -- Use assertStillRead() if you are asserting something is read when it was - also read before. This waits a little while to make sure you're not getting a - false positive. +- Use assertStillRead() if you are asserting something is read when it was + also read before. This waits a little while to make sure you're not getting a + false positive. diff --git a/playwright/e2e/read-receipts/redactions-in-threads.spec.ts b/playwright/e2e/read-receipts/redactions-in-threads.spec.ts index 715bb4e9fc..25c19a4f97 100644 --- a/playwright/e2e/read-receipts/redactions-in-threads.spec.ts +++ b/playwright/e2e/read-receipts/redactions-in-threads.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.describe("redactions", () => { test.describe("in threads", () => { test("Redacting the threaded message pointed to by my receipt leaves the room read", async ({ diff --git a/playwright/e2e/read-receipts/redactions-main-timeline.spec.ts b/playwright/e2e/read-receipts/redactions-main-timeline.spec.ts index c1dceda6a0..143d9685d8 100644 --- a/playwright/e2e/read-receipts/redactions-main-timeline.spec.ts +++ b/playwright/e2e/read-receipts/redactions-main-timeline.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.describe("redactions", () => { test.describe("in the main timeline", () => { test("Redacting the message pointed to by my receipt leaves the room read", async ({ diff --git a/playwright/e2e/read-receipts/redactions-thread-roots.spec.ts b/playwright/e2e/read-receipts/redactions-thread-roots.spec.ts index a8dc38c47e..01f296075c 100644 --- a/playwright/e2e/read-receipts/redactions-thread-roots.spec.ts +++ b/playwright/e2e/read-receipts/redactions-thread-roots.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.describe("redactions", () => { test.describe("thread roots", () => { test("Redacting a thread root after it was read leaves the room read", async ({ diff --git a/playwright/e2e/read-receipts/room-list-order.spec.ts b/playwright/e2e/read-receipts/room-list-order.spec.ts index 052e2d756a..80dda202a3 100644 --- a/playwright/e2e/read-receipts/room-list-order.spec.ts +++ b/playwright/e2e/read-receipts/room-list-order.spec.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import { test } from "."; -test.describe("Read receipts", () => { +test.describe("Read receipts", { tag: "@mergequeue" }, () => { test.describe("Room list order", () => { test("Rooms with unread messages appear at the top of room list if 'unread first' is selected", async ({ roomAlpha: room1, diff --git a/playwright/e2e/register/email.spec.ts b/playwright/e2e/register/email.spec.ts index 7a80f0bbf7..665e20ef01 100644 --- a/playwright/e2e/register/email.spec.ts +++ b/playwright/e2e/register/email.spec.ts @@ -38,34 +38,33 @@ test.describe("Email Registration", async () => { await page.goto("/#/register"); }); - test("registers an account and lands on the use case selection screen", async ({ - page, - mailhog, - request, - checkA11y, - }) => { - await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible(); - // Hide the server text as it contains the randomly allocated Homeserver port - const screenshotOptions = { mask: [page.locator(".mx_ServerPicker_server")] }; + test( + "registers an account and lands on the use case selection screen", + { tag: "@screenshot" }, + async ({ page, mailhog, request, checkA11y }) => { + await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible(); + // Hide the server text as it contains the randomly allocated Homeserver port + const screenshotOptions = { mask: [page.locator(".mx_ServerPicker_server")] }; - await page.getByRole("textbox", { name: "Username" }).fill("alice"); - await page.getByPlaceholder("Password", { exact: true }).fill("totally a great password"); - await page.getByPlaceholder("Confirm password").fill("totally a great password"); - await page.getByPlaceholder("Email").fill("alice@email.com"); - await page.getByRole("button", { name: "Register" }).click(); + await page.getByRole("textbox", { name: "Username" }).fill("alice"); + await page.getByPlaceholder("Password", { exact: true }).fill("totally a great password"); + await page.getByPlaceholder("Confirm password").fill("totally a great password"); + await page.getByPlaceholder("Email").fill("alice@email.com"); + await page.getByRole("button", { name: "Register" }).click(); - await expect(page.getByText("Check your email to continue")).toBeVisible(); - await expect(page).toMatchScreenshot("registration_check_your_email.png", screenshotOptions); - await checkA11y(); + await expect(page.getByText("Check your email to continue")).toBeVisible(); + await expect(page).toMatchScreenshot("registration_check_your_email.png", screenshotOptions); + await checkA11y(); - await expect(page.getByText("An error was encountered when sending the email")).not.toBeVisible(); + await expect(page.getByText("An error was encountered when sending the email")).not.toBeVisible(); - const messages = await mailhog.api.messages(); - expect(messages.items).toHaveLength(1); - expect(messages.items[0].to).toEqual("alice@email.com"); - const [emailLink] = messages.items[0].text.match(/http.+/); - await request.get(emailLink); // "Click" the link in the email + const messages = await mailhog.api.messages(); + expect(messages.items).toHaveLength(1); + expect(messages.items[0].to).toEqual("alice@email.com"); + const [emailLink] = messages.items[0].text.match(/http.+/); + await request.get(emailLink); // "Click" the link in the email - await expect(page.locator(".mx_UseCaseSelection_skip")).toBeVisible(); - }); + await expect(page.locator(".mx_UseCaseSelection_skip")).toBeVisible(); + }, + ); }); diff --git a/playwright/e2e/register/register.spec.ts b/playwright/e2e/register/register.spec.ts index 2dd3779573..19608ee174 100644 --- a/playwright/e2e/register/register.spec.ts +++ b/playwright/e2e/register/register.spec.ts @@ -15,66 +15,73 @@ test.describe("Registration", () => { await page.goto("/#/register"); }); - test("registers an account and lands on the home screen", async ({ homeserver, page, checkA11y, crypto }) => { - await page.getByRole("button", { name: "Edit", exact: true }).click(); - await expect(page.getByRole("button", { name: "Continue", exact: true })).toBeVisible(); + test( + "registers an account and lands on the home screen", + { tag: "@screenshot" }, + async ({ homeserver, page, checkA11y, crypto }) => { + await page.getByRole("button", { name: "Edit", exact: true }).click(); + await expect(page.getByRole("button", { name: "Continue", exact: true })).toBeVisible(); - await expect(page.locator(".mx_Dialog")).toMatchScreenshot("server-picker.png"); - await checkA11y(); + await expect(page.locator(".mx_Dialog")).toMatchScreenshot("server-picker.png"); + await checkA11y(); - await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl); - await page.getByRole("button", { name: "Continue", exact: true }).click(); - // wait for the dialog to go away - await expect(page.getByRole("dialog")).not.toBeVisible(); + await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl); + await page.getByRole("button", { name: "Continue", exact: true }).click(); + // wait for the dialog to go away + await expect(page.getByRole("dialog")).not.toBeVisible(); - await expect(page.getByRole("textbox", { name: "Username", exact: true })).toBeVisible(); - // Hide the server text as it contains the randomly allocated Homeserver port - const screenshotOptions = { mask: [page.locator(".mx_ServerPicker_server")], includeDialogBackground: true }; - await expect(page).toMatchScreenshot("registration.png", screenshotOptions); - await checkA11y(); + await expect(page.getByRole("textbox", { name: "Username", exact: true })).toBeVisible(); + // Hide the server text as it contains the randomly allocated Homeserver port + const screenshotOptions = { + mask: [page.locator(".mx_ServerPicker_server")], + includeDialogBackground: true, + }; + await expect(page).toMatchScreenshot("registration.png", screenshotOptions); + await checkA11y(); - await page.getByRole("textbox", { name: "Username", exact: true }).fill("alice"); - await page.getByPlaceholder("Password", { exact: true }).fill("totally a great password"); - await page.getByPlaceholder("Confirm password", { exact: true }).fill("totally a great password"); - await page.getByRole("button", { name: "Register", exact: true }).click(); + await page.getByRole("textbox", { name: "Username", exact: true }).fill("alice"); + await page.getByPlaceholder("Password", { exact: true }).fill("totally a great password"); + await page.getByPlaceholder("Confirm password", { exact: true }).fill("totally a great password"); + await page.getByRole("button", { name: "Register", exact: true }).click(); - const dialog = page.getByRole("dialog"); - await expect(dialog).toBeVisible(); - await expect(page).toMatchScreenshot("email-prompt.png", screenshotOptions); - await checkA11y(); - await dialog.getByRole("button", { name: "Continue", exact: true }).click(); + const dialog = page.getByRole("dialog"); + await expect(dialog).toBeVisible(); + await expect(page).toMatchScreenshot("email-prompt.png", screenshotOptions); + await checkA11y(); + await dialog.getByRole("button", { name: "Continue", exact: true }).click(); - await expect(page.locator(".mx_InteractiveAuthEntryComponents_termsPolicy")).toBeVisible(); - await expect(page).toMatchScreenshot("terms-prompt.png", screenshotOptions); - await checkA11y(); + await expect(page.locator(".mx_InteractiveAuthEntryComponents_termsPolicy")).toBeVisible(); + await expect(page).toMatchScreenshot("terms-prompt.png", screenshotOptions); + await checkA11y(); - const termsPolicy = page.locator(".mx_InteractiveAuthEntryComponents_termsPolicy"); - await termsPolicy.getByRole("checkbox").click(); // Click the checkbox before terms of service anchor link - await expect(termsPolicy.getByLabel("Privacy Policy")).toBeVisible(); + const termsPolicy = page.locator(".mx_InteractiveAuthEntryComponents_termsPolicy"); + await termsPolicy.getByRole("checkbox").click(); // Click the checkbox before terms of service anchor link + await expect(termsPolicy.getByLabel("Privacy Policy")).toBeVisible(); - await page.getByRole("button", { name: "Accept", exact: true }).click(); + await page.getByRole("button", { name: "Accept", exact: true }).click(); - await expect(page.locator(".mx_UseCaseSelection_skip")).toBeVisible(); - await expect(page).toMatchScreenshot("use-case-selection.png", screenshotOptions); - await checkA11y(); - await page.getByRole("button", { name: "Skip", exact: true }).click(); + await expect(page.locator(".mx_UseCaseSelection_skip")).toBeVisible(); + await expect(page).toMatchScreenshot("use-case-selection.png", screenshotOptions); + await checkA11y(); + await page.getByRole("button", { name: "Skip", exact: true }).click(); - await expect(page).toHaveURL(/\/#\/home$/); + await expect(page).toHaveURL(/\/#\/home$/); - /* - * Cross-signing checks - */ - // check that the device considers itself verified - await page.getByRole("button", { name: "User menu", exact: true }).click(); - await page.getByRole("menuitem", { name: "All settings", exact: true }).click(); - await page.getByRole("tab", { name: "Sessions", exact: true }).click(); - await expect(page.getByTestId("current-session-section").getByTestId("device-metadata-isVerified")).toHaveText( - "Verified", - ); + /* + * Cross-signing checks + */ + // check that the device considers itself verified + await page.getByRole("button", { name: "User menu", exact: true }).click(); + await page.getByRole("menuitem", { name: "All settings", exact: true }).click(); + await page.getByRole("tab", { name: "Sessions", exact: true }).click(); + await expect( + page.getByTestId("current-session-section").getByTestId("device-metadata-isVerified"), + ).toHaveText("Verified"); - // check that cross-signing keys have been uploaded. - await crypto.assertDeviceIsCrossSigned(); - }); + // check that cross-signing keys have been uploaded. + await crypto.assertDeviceIsCrossSigned(); + }, + ); test("should require username to fulfil requirements and be available", async ({ homeserver, page }) => { await page.getByRole("button", { name: "Edit", exact: true }).click(); diff --git a/playwright/e2e/release-announcement/releaseAnnouncement.spec.ts b/playwright/e2e/release-announcement/releaseAnnouncement.spec.ts index 57c27caf1a..e18d72ddba 100644 --- a/playwright/e2e/release-announcement/releaseAnnouncement.spec.ts +++ b/playwright/e2e/release-announcement/releaseAnnouncement.spec.ts @@ -18,7 +18,7 @@ test.describe("Release announcement", () => { labsFlags: ["threadsActivityCentre"], }); - test("should display the release announcement process", async ({ page, app, util }) => { + test("should display the release announcement process", { tag: "@screenshot" }, async ({ page, app, util }) => { // The TAC release announcement should be displayed await util.assertReleaseAnnouncementIsVisible("Threads Activity Centre"); // Hide the release announcement diff --git a/playwright/e2e/right-panel/file-panel.spec.ts b/playwright/e2e/right-panel/file-panel.spec.ts index 1cb39aad25..c535bcdfbb 100644 --- a/playwright/e2e/right-panel/file-panel.spec.ts +++ b/playwright/e2e/right-panel/file-panel.spec.ts @@ -40,7 +40,7 @@ test.describe("FilePanel", () => { }); test.describe("render", () => { - test("should render empty state", async ({ page }) => { + test("should render empty state", { tag: "@screenshot" }, async ({ page }) => { // Wait until the information about the empty state is rendered await expect(page.locator(".mx_EmptyState")).toBeVisible(); @@ -48,7 +48,7 @@ test.describe("FilePanel", () => { await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("empty.png"); }); - test("should list tiles on the panel", async ({ page }) => { + test("should list tiles on the panel", { tag: "@screenshot" }, async ({ page }) => { // Upload multiple files await uploadFile(page, "playwright/sample-files/riot.png"); // Image await uploadFile(page, "playwright/sample-files/1sec.ogg"); // Audio diff --git a/playwright/e2e/right-panel/notification-panel.spec.ts b/playwright/e2e/right-panel/notification-panel.spec.ts index 1d10af9798..55a6be0450 100644 --- a/playwright/e2e/right-panel/notification-panel.spec.ts +++ b/playwright/e2e/right-panel/notification-panel.spec.ts @@ -21,7 +21,7 @@ test.describe("NotificationPanel", () => { await app.client.createRoom({ name: ROOM_NAME }); }); - test("should render empty state", async ({ page, app }) => { + test("should render empty state", { tag: "@screenshot" }, async ({ page, app }) => { await app.viewRoomByName(ROOM_NAME); await page.getByRole("button", { name: "Notifications" }).click(); diff --git a/playwright/e2e/right-panel/right-panel.spec.ts b/playwright/e2e/right-panel/right-panel.spec.ts index 110f24a501..1e9b8ebe1d 100644 --- a/playwright/e2e/right-panel/right-panel.spec.ts +++ b/playwright/e2e/right-panel/right-panel.spec.ts @@ -38,7 +38,7 @@ test.describe("RightPanel", () => { }); test.describe("in rooms", () => { - test("should handle long room address and long room name", async ({ page, app }) => { + test("should handle long room address and long room name", { tag: "@screenshot" }, async ({ page, app }) => { await app.client.createRoom({ name: ROOM_NAME_LONG }); await viewRoomSummaryByName(page, app, ROOM_NAME_LONG); diff --git a/playwright/e2e/room-directory/room-directory.spec.ts b/playwright/e2e/room-directory/room-directory.spec.ts index f078a858a2..f299a929bb 100644 --- a/playwright/e2e/room-directory/room-directory.spec.ts +++ b/playwright/e2e/room-directory/room-directory.spec.ts @@ -47,34 +47,40 @@ test.describe("Room Directory", () => { expect(resp.chunk[0].room_id).toEqual(roomId); }); - test("should allow finding published rooms in directory", async ({ page, app, user, bot }) => { - const name = "This is a public room"; - await bot.createRoom({ - visibility: "public" as Visibility, - name, - room_alias_name: "test1234", - }); + test( + "should allow finding published rooms in directory", + { tag: "@screenshot" }, + async ({ page, app, user, bot }) => { + const name = "This is a public room"; + await bot.createRoom({ + visibility: "public" as Visibility, + name, + room_alias_name: "test1234", + }); - await page.getByRole("button", { name: "Explore rooms" }).click(); + await page.getByRole("button", { name: "Explore rooms" }).click(); - const dialog = page.locator(".mx_SpotlightDialog"); - await dialog.getByRole("textbox", { name: "Search" }).fill("Unknown Room"); - await expect( - dialog.getByText("If you can't find the room you're looking for, ask for an invite or create a new room."), - ).toHaveClass("mx_SpotlightDialog_otherSearches_messageSearchText"); + const dialog = page.locator(".mx_SpotlightDialog"); + await dialog.getByRole("textbox", { name: "Search" }).fill("Unknown Room"); + await expect( + dialog.getByText( + "If you can't find the room you're looking for, ask for an invite or create a new room.", + ), + ).toHaveClass("mx_SpotlightDialog_otherSearches_messageSearchText"); - await expect(page.locator(".mx_Dialog")).toMatchScreenshot("filtered-no-results.png"); + await expect(page.locator(".mx_Dialog")).toMatchScreenshot("filtered-no-results.png"); - await dialog.getByRole("textbox", { name: "Search" }).fill("test1234"); - await expect(dialog.getByText(name)).toHaveClass("mx_SpotlightDialog_result_publicRoomName"); + await dialog.getByRole("textbox", { name: "Search" }).fill("test1234"); + await expect(dialog.getByText(name)).toHaveClass("mx_SpotlightDialog_result_publicRoomName"); - await expect(page.locator(".mx_Dialog")).toMatchScreenshot("filtered-one-result.png"); + await expect(page.locator(".mx_Dialog")).toMatchScreenshot("filtered-one-result.png"); - await page - .locator(".mx_SpotlightDialog .mx_SpotlightDialog_option") - .getByRole("button", { name: "Join" }) - .click(); + await page + .locator(".mx_SpotlightDialog .mx_SpotlightDialog_option") + .getByRole("button", { name: "Join" }) + .click(); - await expect(page).toHaveURL("/#/room/#test1234:localhost"); - }); + await expect(page).toHaveURL("/#/room/#test1234:localhost"); + }, + ); }); diff --git a/playwright/e2e/room/room-header.spec.ts b/playwright/e2e/room/room-header.spec.ts index 6ecf4b3b33..971508b25b 100644 --- a/playwright/e2e/room/room-header.spec.ts +++ b/playwright/e2e/room/room-header.spec.ts @@ -20,7 +20,7 @@ test.describe("Room Header", () => { test.use({ labsFlags: ["feature_notifications"], }); - test("should render default buttons properly", async ({ page, app, user }) => { + test("should render default buttons properly", { tag: "@screenshot" }, async ({ page, app, user }) => { await app.client.createRoom({ name: "Test Room" }); await app.viewRoomByName("Test Room"); @@ -51,34 +51,38 @@ test.describe("Room Header", () => { await expect(header).toMatchScreenshot("room-header.png"); }); - test("should render a very long room name without collapsing the buttons", async ({ page, app, user }) => { - const LONG_ROOM_NAME = - "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore " + - "et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut " + - "aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum " + - "dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui " + - "officia deserunt mollit anim id est laborum."; + test( + "should render a very long room name without collapsing the buttons", + { tag: "@screenshot" }, + async ({ page, app, user }) => { + const LONG_ROOM_NAME = + "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore " + + "et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut " + + "aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum " + + "dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui " + + "officia deserunt mollit anim id est laborum."; - await app.client.createRoom({ name: LONG_ROOM_NAME }); - await app.viewRoomByName(LONG_ROOM_NAME); + await app.client.createRoom({ name: LONG_ROOM_NAME }); + await app.viewRoomByName(LONG_ROOM_NAME); - const header = page.locator(".mx_RoomHeader"); - // Wait until the room name is set - await expect(page.locator(".mx_RoomHeader_heading").getByText(LONG_ROOM_NAME)).toBeVisible(); + const header = page.locator(".mx_RoomHeader"); + // Wait until the room name is set + await expect(page.locator(".mx_RoomHeader_heading").getByText(LONG_ROOM_NAME)).toBeVisible(); - // Assert the size of buttons on RoomHeader are specified and the buttons are not compressed - // Note these assertions do not check the size of mx_LegacyRoomHeader_name button - const buttons = header.locator(".mx_Flex").getByRole("button"); - await expect(buttons).toHaveCount(5); + // Assert the size of buttons on RoomHeader are specified and the buttons are not compressed + // Note these assertions do not check the size of mx_LegacyRoomHeader_name button + const buttons = header.locator(".mx_Flex").getByRole("button"); + await expect(buttons).toHaveCount(5); - for (const button of await buttons.all()) { - await expect(button).toBeVisible(); - await expect(button).toHaveCSS("height", "32px"); - await expect(button).toHaveCSS("width", "32px"); - } + for (const button of await buttons.all()) { + await expect(button).toBeVisible(); + await expect(button).toHaveCSS("height", "32px"); + await expect(button).toHaveCSS("width", "32px"); + } - await expect(header).toMatchScreenshot("room-header-long-name.png"); - }); + await expect(header).toMatchScreenshot("room-header-long-name.png"); + }, + ); }); test.describe("with a video room", () => { @@ -99,30 +103,34 @@ test.describe("Room Header", () => { test.describe("and with feature_notifications enabled", () => { test.use({ labsFlags: ["feature_video_rooms", "feature_notifications"] }); - test("should render buttons for chat, room info, threads and facepile", async ({ page, app, user }) => { - await createVideoRoom(page, app); + test( + "should render buttons for chat, room info, threads and facepile", + { tag: "@screenshot" }, + async ({ page, app, user }) => { + await createVideoRoom(page, app); - const header = page.locator(".mx_RoomHeader"); + const header = page.locator(".mx_RoomHeader"); - // There's two room info button - the header itself and the i button - const infoButtons = header.getByRole("button", { name: "Room info" }); - await expect(infoButtons).toHaveCount(2); - await expect(infoButtons.first()).toBeVisible(); - await expect(infoButtons.last()).toBeVisible(); + // There's two room info button - the header itself and the i button + const infoButtons = header.getByRole("button", { name: "Room info" }); + await expect(infoButtons).toHaveCount(2); + await expect(infoButtons.first()).toBeVisible(); + await expect(infoButtons.last()).toBeVisible(); - // Facepile - await expect(header.locator(".mx_FacePile")).toBeVisible(); + // Facepile + await expect(header.locator(".mx_FacePile")).toBeVisible(); - // Chat, Threads and Notification buttons - await expect(header.getByRole("button", { name: "Chat" })).toBeVisible(); - await expect(header.getByRole("button", { name: "Threads" })).toBeVisible(); - await expect(header.getByRole("button", { name: "Notifications" })).toBeVisible(); + // Chat, Threads and Notification buttons + await expect(header.getByRole("button", { name: "Chat" })).toBeVisible(); + await expect(header.getByRole("button", { name: "Threads" })).toBeVisible(); + await expect(header.getByRole("button", { name: "Notifications" })).toBeVisible(); - // Assert that there is not a button except those buttons - await expect(header.getByRole("button")).toHaveCount(7); + // Assert that there is not a button except those buttons + await expect(header.getByRole("button")).toHaveCount(7); - await expect(header).toMatchScreenshot("room-header-video-room.png"); - }); + await expect(header).toMatchScreenshot("room-header-video-room.png"); + }, + ); }); test("should render a working chat button which opens the timeline on a right panel", async ({ diff --git a/playwright/e2e/settings/account-user-settings-tab.spec.ts b/playwright/e2e/settings/account-user-settings-tab.spec.ts index 5492094f93..7390ccfd8d 100644 --- a/playwright/e2e/settings/account-user-settings-tab.spec.ts +++ b/playwright/e2e/settings/account-user-settings-tab.spec.ts @@ -23,7 +23,7 @@ test.describe("Account user settings tab", () => { }, }); - test("should be rendered properly", async ({ uut, user }) => { + test("should be rendered properly", { tag: "@screenshot" }, async ({ uut, user }) => { await expect(uut).toMatchScreenshot("account.png"); // Assert that the top heading is rendered @@ -71,7 +71,7 @@ test.describe("Account user settings tab", () => { ); }); - test("should respond to small screen sizes", async ({ page, uut }) => { + test("should respond to small screen sizes", { tag: "@screenshot" }, async ({ page, uut }) => { await page.setViewportSize({ width: 700, height: 600 }); await expect(uut).toMatchScreenshot("account-smallscreen.png"); }); diff --git a/playwright/e2e/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts b/playwright/e2e/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts index de6c9c527a..c60ecb99d2 100644 --- a/playwright/e2e/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts +++ b/playwright/e2e/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts @@ -13,7 +13,7 @@ test.describe("Appearance user settings tab", () => { displayName: "Hanako", }); - test("should be rendered properly", async ({ page, user, app }) => { + test("should be rendered properly", { tag: "@screenshot" }, async ({ page, user, app }) => { const tab = await app.settings.openUserSettings("Appearance"); // Click "Show advanced" link button @@ -25,19 +25,23 @@ test.describe("Appearance user settings tab", () => { await expect(tab).toMatchScreenshot("appearance-tab.png"); }); - test("should support changing font size by using the font size dropdown", async ({ page, app, user }) => { - await app.settings.openUserSettings("Appearance"); + test( + "should support changing font size by using the font size dropdown", + { tag: "@screenshot" }, + async ({ page, app, user }) => { + await app.settings.openUserSettings("Appearance"); - const tab = page.getByTestId("mx_AppearanceUserSettingsTab"); - const fontDropdown = tab.locator(".mx_FontScalingPanel_Dropdown"); - await expect(fontDropdown.getByLabel("Font size")).toBeVisible(); + const tab = page.getByTestId("mx_AppearanceUserSettingsTab"); + const fontDropdown = tab.locator(".mx_FontScalingPanel_Dropdown"); + await expect(fontDropdown.getByLabel("Font size")).toBeVisible(); - // Default browser font size is 16px and the select value is 0 - // -4 value is 12px - await fontDropdown.getByLabel("Font size").selectOption({ value: "-4" }); + // Default browser font size is 16px and the select value is 0 + // -4 value is 12px + await fontDropdown.getByLabel("Font size").selectOption({ value: "-4" }); - await expect(page).toMatchScreenshot("window-12px.png", { includeDialogBackground: true }); - }); + await expect(page).toMatchScreenshot("window-12px.png", { includeDialogBackground: true }); + }, + ); test("should support enabling system font", async ({ page, app, user }) => { await app.settings.openUserSettings("Appearance"); diff --git a/playwright/e2e/settings/appearance-user-settings-tab/message-layout-panel.ts b/playwright/e2e/settings/appearance-user-settings-tab/message-layout-panel.ts index a0288baf1d..157942a585 100644 --- a/playwright/e2e/settings/appearance-user-settings-tab/message-layout-panel.ts +++ b/playwright/e2e/settings/appearance-user-settings-tab/message-layout-panel.ts @@ -20,20 +20,24 @@ test.describe("Appearance user settings tab", () => { await util.openAppearanceTab(); }); - test("should change the message layout from modern to bubble", async ({ page, app, user, util }) => { - await util.assertScreenshot(util.getMessageLayoutPanel(), "message-layout-panel-modern.png"); + test( + "should change the message layout from modern to bubble", + { tag: "@screenshot" }, + async ({ page, app, user, util }) => { + await util.assertScreenshot(util.getMessageLayoutPanel(), "message-layout-panel-modern.png"); - await util.getBubbleLayout().click(); + await util.getBubbleLayout().click(); - // Assert that modern are irc layout are not selected - await expect(util.getBubbleLayout()).toBeChecked(); - await expect(util.getModernLayout()).not.toBeChecked(); - await expect(util.getIRCLayout()).not.toBeChecked(); + // Assert that modern are irc layout are not selected + await expect(util.getBubbleLayout()).toBeChecked(); + await expect(util.getModernLayout()).not.toBeChecked(); + await expect(util.getIRCLayout()).not.toBeChecked(); - // Assert that the room layout is set to bubble layout - await util.assertBubbleLayout(); - await util.assertScreenshot(util.getMessageLayoutPanel(), "message-layout-panel-bubble.png"); - }); + // Assert that the room layout is set to bubble layout + await util.assertBubbleLayout(); + await util.assertScreenshot(util.getMessageLayoutPanel(), "message-layout-panel-bubble.png"); + }, + ); test("should enable compact layout when the modern layout is selected", async ({ page, app, user, util }) => { await expect(util.getCompactLayoutCheckbox()).not.toBeChecked(); diff --git a/playwright/e2e/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts b/playwright/e2e/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts index 4f3b75b5ba..63b53caa23 100644 --- a/playwright/e2e/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts +++ b/playwright/e2e/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts @@ -20,31 +20,39 @@ test.describe("Appearance user settings tab", () => { await util.openAppearanceTab(); }); - test("should be rendered with the light theme selected", async ({ page, app, util }) => { - // Assert that 'Match system theme' is not checked - await expect(util.getMatchSystemThemeCheckbox()).not.toBeChecked(); + test( + "should be rendered with the light theme selected", + { tag: "@screenshot" }, + async ({ page, app, util }) => { + // Assert that 'Match system theme' is not checked + await expect(util.getMatchSystemThemeCheckbox()).not.toBeChecked(); - // Assert that the light theme is selected - await expect(util.getLightTheme()).toBeChecked(); - // Assert that the dark and high contrast themes are not selected - await expect(util.getDarkTheme()).not.toBeChecked(); - await expect(util.getHighContrastTheme()).not.toBeChecked(); + // Assert that the light theme is selected + await expect(util.getLightTheme()).toBeChecked(); + // Assert that the dark and high contrast themes are not selected + await expect(util.getDarkTheme()).not.toBeChecked(); + await expect(util.getHighContrastTheme()).not.toBeChecked(); - await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-light.png"); - }); + await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-light.png"); + }, + ); - test("should disable the themes when the system theme is clicked", async ({ page, app, util }) => { - await util.getMatchSystemThemeCheckbox().click(); + test( + "should disable the themes when the system theme is clicked", + { tag: "@screenshot" }, + async ({ page, app, util }) => { + await util.getMatchSystemThemeCheckbox().click(); - // Assert that the themes are disabled - await expect(util.getLightTheme()).toBeDisabled(); - await expect(util.getDarkTheme()).toBeDisabled(); - await expect(util.getHighContrastTheme()).toBeDisabled(); + // Assert that the themes are disabled + await expect(util.getLightTheme()).toBeDisabled(); + await expect(util.getDarkTheme()).toBeDisabled(); + await expect(util.getHighContrastTheme()).toBeDisabled(); - await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-match-system-enabled.png"); - }); + await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-match-system-enabled.png"); + }, + ); - test("should change the theme to dark", async ({ page, app, util }) => { + test("should change the theme to dark", { tag: "@screenshot" }, async ({ page, app, util }) => { // Assert that the light theme is selected await expect(util.getLightTheme()).toBeChecked(); @@ -63,19 +71,23 @@ test.describe("Appearance user settings tab", () => { labsFlags: ["feature_custom_themes"], }); - test("should render the custom theme section", async ({ page, app, util }) => { + test("should render the custom theme section", { tag: "@screenshot" }, async ({ page, app, util }) => { await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme.png"); }); - test("should be able to add and remove a custom theme", async ({ page, app, util }) => { - await util.addCustomTheme(); + test( + "should be able to add and remove a custom theme", + { tag: "@screenshot" }, + async ({ page, app, util }) => { + await util.addCustomTheme(); - await expect(util.getCustomTheme()).not.toBeChecked(); - await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme-added.png"); + await expect(util.getCustomTheme()).not.toBeChecked(); + await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme-added.png"); - await util.removeCustomTheme(); - await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme-removed.png"); - }); + await util.removeCustomTheme(); + await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme-removed.png"); + }, + ); }); }); }); diff --git a/playwright/e2e/settings/general-room-settings-tab.spec.ts b/playwright/e2e/settings/general-room-settings-tab.spec.ts index 47582bf0c0..828ba5285b 100644 --- a/playwright/e2e/settings/general-room-settings-tab.spec.ts +++ b/playwright/e2e/settings/general-room-settings-tab.spec.ts @@ -20,7 +20,7 @@ test.describe("General room settings tab", () => { await app.viewRoomByName(roomName); }); - test("should be rendered properly", async ({ page, app }) => { + test("should be rendered properly", { tag: "@screenshot" }, async ({ page, app }) => { const settings = await app.settings.openRoomSettings("General"); // Assert that "Show less" details element is rendered diff --git a/playwright/e2e/settings/preferences-user-settings-tab.spec.ts b/playwright/e2e/settings/preferences-user-settings-tab.spec.ts index 0880853ee8..8dc2570b42 100644 --- a/playwright/e2e/settings/preferences-user-settings-tab.spec.ts +++ b/playwright/e2e/settings/preferences-user-settings-tab.spec.ts @@ -23,7 +23,7 @@ test.describe("Preferences user settings tab", () => { }, }); - test("should be rendered properly", async ({ app, page, user }) => { + test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user }) => { page.setViewportSize({ width: 1024, height: 3300 }); const tab = await app.settings.openUserSettings("Preferences"); // Assert that the top heading is rendered diff --git a/playwright/e2e/settings/security-user-settings-tab.spec.ts b/playwright/e2e/settings/security-user-settings-tab.spec.ts index 6eab830623..e7562698c3 100644 --- a/playwright/e2e/settings/security-user-settings-tab.spec.ts +++ b/playwright/e2e/settings/security-user-settings-tab.spec.ts @@ -36,7 +36,7 @@ test.describe("Security user settings tab", () => { }); test.describe("AnalyticsLearnMoreDialog", () => { - test("should be rendered properly", async ({ app, page }) => { + test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page }) => { const tab = await app.settings.openUserSettings("Security"); await tab.getByRole("button", { name: "Learn more" }).click(); await expect(page.locator(".mx_AnalyticsLearnMoreDialog_wrapper .mx_Dialog")).toMatchScreenshot( diff --git a/playwright/e2e/share-dialog/share-dialog.spec.ts b/playwright/e2e/share-dialog/share-dialog.spec.ts new file mode 100644 index 0000000000..e0993dd1bc --- /dev/null +++ b/playwright/e2e/share-dialog/share-dialog.spec.ts @@ -0,0 +1,67 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only + * Please see LICENSE files in the repository root for full details. + */ + +import { test, expect } from "../../element-web-test"; + +test.describe("Share dialog", () => { + test.use({ + displayName: "Alice", + room: async ({ app, user, bot }, use) => { + const roomId = await app.client.createRoom({ name: "Alice room" }); + await use({ roomId }); + }, + }); + + test("should share a room", { tag: "@screenshot" }, async ({ page, app, room }) => { + await app.viewRoomById(room.roomId); + await app.toggleRoomInfoPanel(); + await page.getByRole("menuitem", { name: "Copy link" }).click(); + + const dialog = page.getByRole("dialog", { name: "Share room" }); + await expect(dialog.getByText(`https://matrix.to/#/${room.roomId}`)).toBeVisible(); + expect(dialog).toMatchScreenshot("share-dialog-room.png", { + // QRCode and url changes at every run + mask: [page.locator(".mx_QRCode"), page.locator(".mx_ShareDialog_top > span")], + }); + }); + + test("should share a room member", { tag: "@screenshot" }, async ({ page, app, room, user }) => { + await app.viewRoomById(room.roomId); + await app.client.sendMessage(room.roomId, { body: "hello", msgtype: "m.text" }); + + const rightPanel = await app.toggleRoomInfoPanel(); + await rightPanel.getByRole("menuitem", { name: "People" }).click(); + await rightPanel.getByRole("button", { name: `${user.userId} (power 100)` }).click(); + await rightPanel.getByRole("button", { name: "Share profile" }).click(); + + const dialog = page.getByRole("dialog", { name: "Share User" }); + await expect(dialog.getByText(`https://matrix.to/#/${user.userId}`)).toBeVisible(); + expect(dialog).toMatchScreenshot("share-dialog-user.png", { + // QRCode changes at every run + mask: [page.locator(".mx_QRCode")], + }); + }); + + test("should share an event", { tag: "@screenshot" }, async ({ page, app, room }) => { + await app.viewRoomById(room.roomId); + await app.client.sendMessage(room.roomId, { body: "hello", msgtype: "m.text" }); + + const timelineMessage = page.locator(".mx_MTextBody", { hasText: "hello" }); + await timelineMessage.hover(); + await page.getByRole("button", { name: "Options", exact: true }).click(); + await page.getByRole("menuitem", { name: "Share" }).click(); + + const dialog = page.getByRole("dialog", { name: "Share Room Message" }); + await expect(dialog.getByRole("checkbox", { name: "Link to selected message" })).toBeChecked(); + expect(dialog).toMatchScreenshot("share-dialog-event.png", { + // QRCode and url changes at every run + mask: [page.locator(".mx_QRCode"), page.locator(".mx_ShareDialog_top > span")], + }); + await dialog.getByRole("checkbox", { name: "Link to selected message" }).click(); + await expect(dialog.getByRole("checkbox", { name: "Link to selected message" })).not.toBeChecked(); + }); +}); diff --git a/playwright/e2e/spaces/spaces.spec.ts b/playwright/e2e/spaces/spaces.spec.ts index 575450c641..233cdee3b4 100644 --- a/playwright/e2e/spaces/spaces.spec.ts +++ b/playwright/e2e/spaces/spaces.spec.ts @@ -55,7 +55,7 @@ test.describe("Spaces", () => { botCreateOpts: { displayName: "BotBob" }, }); - test("should allow user to create public space", async ({ page, app, user }) => { + test("should allow user to create public space", { tag: "@screenshot" }, async ({ page, app, user }) => { const contextMenu = await openSpaceCreateMenu(page); await expect(contextMenu).toMatchScreenshot("space-create-menu.png"); @@ -88,7 +88,7 @@ test.describe("Spaces", () => { await expect(page.getByRole("treeitem", { name: "Jokes" })).toBeVisible(); }); - test("should allow user to create private space", async ({ page, app, user }) => { + test("should allow user to create private space", { tag: "@screenshot" }, async ({ page, app, user }) => { const menu = await openSpaceCreateMenu(page); await menu.getByRole("button", { name: "Private" }).click(); @@ -216,49 +216,47 @@ test.describe("Spaces", () => { await expect(hierarchyList.getByRole("treeitem", { name: "Gaming" }).getByRole("button")).toBeVisible(); }); - test("should render subspaces in the space panel only when expanded", async ({ - page, - app, - user, - axe, - checkA11y, - }) => { - axe.disableRules([ - // Disable this check as it triggers on nested roving tab index elements which are in practice fine - "nested-interactive", - // XXX: We have some known contrast issues here - "color-contrast", - ]); + test( + "should render subspaces in the space panel only when expanded", + { tag: "@screenshot" }, + async ({ page, app, user, axe, checkA11y }) => { + axe.disableRules([ + // Disable this check as it triggers on nested roving tab index elements which are in practice fine + "nested-interactive", + // XXX: We have some known contrast issues here + "color-contrast", + ]); - const childSpaceId = await app.client.createSpace({ - name: "Child Space", - initial_state: [], - }); - await app.client.createSpace({ - name: "Root Space", - initial_state: [spaceChildInitialState(childSpaceId)], - }); + const childSpaceId = await app.client.createSpace({ + name: "Child Space", + initial_state: [], + }); + await app.client.createSpace({ + name: "Root Space", + initial_state: [spaceChildInitialState(childSpaceId)], + }); - // Find collapsed Space panel - const spaceTree = page.getByRole("tree", { name: "Spaces" }); - await expect(spaceTree.getByRole("button", { name: "Root Space" })).toBeVisible(); - await expect(spaceTree.getByRole("button", { name: "Child Space" })).not.toBeVisible(); + // Find collapsed Space panel + const spaceTree = page.getByRole("tree", { name: "Spaces" }); + await expect(spaceTree.getByRole("button", { name: "Root Space" })).toBeVisible(); + await expect(spaceTree.getByRole("button", { name: "Child Space" })).not.toBeVisible(); - await checkA11y(); - await expect(page.locator(".mx_SpacePanel")).toMatchScreenshot("space-panel-collapsed.png"); + await checkA11y(); + await expect(page.locator(".mx_SpacePanel")).toMatchScreenshot("space-panel-collapsed.png"); - // This finds the expand button with the class name "mx_SpaceButton_toggleCollapse". Note there is another - // button with the same name with different class name "mx_SpacePanel_toggleCollapse". - await spaceTree.getByRole("button", { name: "Expand" }).click(); - await expect(page.locator(".mx_SpacePanel:not(.collapsed)")).toBeVisible(); // TODO: replace :not() selector + // This finds the expand button with the class name "mx_SpaceButton_toggleCollapse". Note there is another + // button with the same name with different class name "mx_SpacePanel_toggleCollapse". + await spaceTree.getByRole("button", { name: "Expand" }).click(); + await expect(page.locator(".mx_SpacePanel:not(.collapsed)")).toBeVisible(); // TODO: replace :not() selector - const item = page.locator(".mx_SpaceItem", { hasText: "Root Space" }); - await expect(item).toBeVisible(); - await expect(item.locator(".mx_SpaceItem", { hasText: "Child Space" })).toBeVisible(); + const item = page.locator(".mx_SpaceItem", { hasText: "Root Space" }); + await expect(item).toBeVisible(); + await expect(item.locator(".mx_SpaceItem", { hasText: "Child Space" })).toBeVisible(); - await checkA11y(); - await expect(page.locator(".mx_SpacePanel")).toMatchScreenshot("space-panel-expanded.png"); - }); + await checkA11y(); + await expect(page.locator(".mx_SpacePanel")).toMatchScreenshot("space-panel-expanded.png"); + }, + ); test("should not soft crash when joining a room from space hierarchy which has a link in its topic", async ({ page, diff --git a/playwright/e2e/spaces/threads-activity-centre/index.ts b/playwright/e2e/spaces/threads-activity-centre/index.ts index f6ac7f95d6..b2b8473640 100644 --- a/playwright/e2e/spaces/threads-activity-centre/index.ts +++ b/playwright/e2e/spaces/threads-activity-centre/index.ts @@ -276,7 +276,7 @@ export class Helpers { * Assert that the threads activity centre button has no indicator */ async assertNoTacIndicator() { - // Assert by checkng neither of the known indicators are visible first. This will wait + // Assert by checking neither of the known indicators are visible first. This will wait // if it takes a little time to disappear, but the screenshot comparison won't. await expect(this.getTacButton().locator("[data-indicator='success']")).not.toBeVisible(); await expect(this.getTacButton().locator("[data-indicator='critical']")).not.toBeVisible(); @@ -376,7 +376,7 @@ export class Helpers { * Clicks the button to mark all threads as read in the current room */ clickMarkAllThreadsRead() { - return this.page.getByLabel("Mark all as read").click(); + return this.page.locator("#thread-panel").getByRole("button", { name: "Mark all as read" }).click(); } } diff --git a/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts b/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts index fc66c13520..ecf458c060 100644 --- a/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts +++ b/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts @@ -16,16 +16,18 @@ test.describe("Threads Activity Centre", () => { labsFlags: ["threadsActivityCentre"], }); - test("should have the button correctly aligned and displayed in the space panel when expanded", async ({ - util, - }) => { - // Open the space panel - await util.expandSpacePanel(); - // The buttons in the space panel should be aligned when expanded - await expect(util.getSpacePanel()).toMatchScreenshot("tac-button-expanded.png"); - }); + test( + "should have the button correctly aligned and displayed in the space panel when expanded", + { tag: "@screenshot" }, + async ({ util }) => { + // Open the space panel + await util.expandSpacePanel(); + // The buttons in the space panel should be aligned when expanded + await expect(util.getSpacePanel()).toMatchScreenshot("tac-button-expanded.png"); + }, + ); - test("should not show indicator when there is no thread", async ({ room1, util }) => { + test("should not show indicator when there is no thread", { tag: "@screenshot" }, async ({ room1, util }) => { // No indicator should be shown await util.assertNoTacIndicator(); @@ -62,7 +64,7 @@ test.describe("Threads Activity Centre", () => { await util.assertHighlightIndicator(); }); - test("should show the rooms with unread threads", async ({ room1, room2, util, msg }) => { + test("should show the rooms with unread threads", { tag: "@screenshot" }, async ({ room1, room2, util, msg }) => { await util.goTo(room2); await util.populateThreads(room1, room2, msg); // The indicator should be shown @@ -79,7 +81,7 @@ test.describe("Threads Activity Centre", () => { await expect(util.getTacPanel()).toMatchScreenshot("tac-panel-mix-unread.png"); }); - test("should update with a thread is read", async ({ room1, room2, util, msg }) => { + test("should update with a thread is read", { tag: "@screenshot" }, async ({ room1, room2, util, msg }) => { await util.goTo(room2); await util.populateThreads(room1, room2, msg); @@ -128,7 +130,7 @@ test.describe("Threads Activity Centre", () => { await expect(page.locator(".mx_SpotlightDialog")).not.toBeVisible(); }); - test("should have the correct hover state", async ({ util, page }) => { + test("should have the correct hover state", { tag: "@screenshot" }, async ({ util, page }) => { await util.hoverTacButton(); await expect(util.getSpacePanel()).toMatchScreenshot("tac-hovered.png"); @@ -138,7 +140,7 @@ test.describe("Threads Activity Centre", () => { await expect(util.getSpacePanel()).toMatchScreenshot("tac-hovered-expanded.png"); }); - test("should mark all threads as read", async ({ room1, room2, util, msg, page }) => { + test("should mark all threads as read", { tag: "@screenshot" }, async ({ room1, room2, util, msg, page }) => { await util.receiveMessages(room1, ["Msg1", msg.threadedOff("Msg1", "Resp1")]); await util.assertNotificationTac(); @@ -146,7 +148,7 @@ test.describe("Threads Activity Centre", () => { await util.openTac(); await util.clickRoomInTac(room1.name); - util.clickMarkAllThreadsRead(); + await util.clickMarkAllThreadsRead(); await util.assertNoTacIndicator(); }); diff --git a/playwright/e2e/threads/threads.spec.ts b/playwright/e2e/threads/threads.spec.ts index a2642a49d1..06ec57653c 100644 --- a/playwright/e2e/threads/threads.spec.ts +++ b/playwright/e2e/threads/threads.spec.ts @@ -25,7 +25,7 @@ test.describe("Threads", () => { }); // Flaky: https://github.com/vector-im/element-web/issues/26452 - test.skip("should be usable for a conversation", async ({ page, app, bot }) => { + test.skip("should be usable for a conversation", { tag: "@screenshot" }, async ({ page, app, bot }) => { const roomId = await app.client.createRoom({}); await app.client.inviteUser(roomId, bot.credentials.userId); await bot.joinRoom(roomId); @@ -150,7 +150,7 @@ test.describe("Threads", () => { ).toHaveCSS("padding-inline-start", ThreadViewGroupSpacingStart); // Take snapshot of group layout (IRC layout is not available on ThreadView) - expect(page.locator(".mx_ThreadView")).toMatchScreenshot( + await expect(page.locator(".mx_ThreadView")).toMatchScreenshot( "ThreadView_with_reaction_and_a_hidden_event_on_group_layout.png", { mask: mask, @@ -174,7 +174,7 @@ test.describe("Threads", () => { .toHaveCSS("margin-inline-start", "0px"); // Take snapshot of bubble layout - expect(page.locator(".mx_ThreadView")).toMatchScreenshot( + await expect(page.locator(".mx_ThreadView")).toMatchScreenshot( "ThreadView_with_reaction_and_a_hidden_event_on_bubble_layout.png", { mask: mask, @@ -351,57 +351,61 @@ test.describe("Threads", () => { }); }); - test("should send location and reply to the location on ThreadView", async ({ page, app, bot }) => { - const roomId = await app.client.createRoom({}); - await app.client.inviteUser(roomId, bot.credentials.userId); - await bot.joinRoom(roomId); - await page.goto("/#/room/" + roomId); + test( + "should send location and reply to the location on ThreadView", + { tag: "@screenshot" }, + async ({ page, app, bot }) => { + const roomId = await app.client.createRoom({}); + await app.client.inviteUser(roomId, bot.credentials.userId); + await bot.joinRoom(roomId); + await page.goto("/#/room/" + roomId); - // Exclude timestamp, read marker, and maplibregl-map from snapshots - const css = - ".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker, .maplibregl-map { visibility: hidden !important; }"; + // Exclude timestamp, read marker, and maplibregl-map from snapshots + const css = + ".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker, .maplibregl-map { visibility: hidden !important; }"; - let locator = page.locator(".mx_RoomView_body"); - // User sends message - let textbox = locator.getByRole("textbox", { name: "Send a message…" }); - await textbox.fill("Hello Mr. Bot"); - await textbox.press("Enter"); - // Wait for message to send, get its ID and save as @threadId - const threadId = await locator - .locator(".mx_EventTile[data-scroll-tokens]") - .filter({ hasText: "Hello Mr. Bot" }) - .getAttribute("data-scroll-tokens"); + let locator = page.locator(".mx_RoomView_body"); + // User sends message + let textbox = locator.getByRole("textbox", { name: "Send a message…" }); + await textbox.fill("Hello Mr. Bot"); + await textbox.press("Enter"); + // Wait for message to send, get its ID and save as @threadId + const threadId = await locator + .locator(".mx_EventTile[data-scroll-tokens]") + .filter({ hasText: "Hello Mr. Bot" }) + .getAttribute("data-scroll-tokens"); - // Bot starts thread - await bot.sendMessage(roomId, "Hello there", threadId); + // Bot starts thread + await bot.sendMessage(roomId, "Hello there", threadId); - // User clicks thread summary - await page.locator(".mx_RoomView_body .mx_ThreadSummary").click(); + // User clicks thread summary + await page.locator(".mx_RoomView_body .mx_ThreadSummary").click(); - // User sends location on ThreadView - await expect(page.locator(".mx_ThreadView")).toBeAttached(); - await (await app.openMessageComposerOptions(true)).getByRole("menuitem", { name: "Location" }).click(); - await page.getByTestId(`share-location-option-Pin`).click(); - await page.locator("#mx_LocationPicker_map").click(); - await page.getByRole("button", { name: "Share location" }).click(); - await expect(page.locator(".mx_ThreadView .mx_EventTile_last .mx_MLocationBody")).toBeAttached({ - timeout: 10000, - }); + // User sends location on ThreadView + await expect(page.locator(".mx_ThreadView")).toBeAttached(); + await (await app.openMessageComposerOptions(true)).getByRole("menuitem", { name: "Location" }).click(); + await page.getByTestId(`share-location-option-Pin`).click(); + await page.locator("#mx_LocationPicker_map").click(); + await page.getByRole("button", { name: "Share location" }).click(); + await expect(page.locator(".mx_ThreadView .mx_EventTile_last .mx_MLocationBody")).toBeAttached({ + timeout: 10000, + }); - // User replies to the location - locator = page.locator(".mx_ThreadView"); - await locator.locator(".mx_EventTile_last").hover(); - await locator.locator(".mx_EventTile_last").getByRole("button", { name: "Reply" }).click(); - textbox = locator.getByRole("textbox", { name: "Reply to thread…" }); - await textbox.fill("Please come here"); - await textbox.press("Enter"); - // Wait until the reply is sent - await expect(locator.locator(".mx_EventTile_last .mx_EventTile_receiptSent")).toBeVisible(); + // User replies to the location + locator = page.locator(".mx_ThreadView"); + await locator.locator(".mx_EventTile_last").hover(); + await locator.locator(".mx_EventTile_last").getByRole("button", { name: "Reply" }).click(); + textbox = locator.getByRole("textbox", { name: "Reply to thread…" }); + await textbox.fill("Please come here"); + await textbox.press("Enter"); + // Wait until the reply is sent + await expect(locator.locator(".mx_EventTile_last .mx_EventTile_receiptSent")).toBeVisible(); - // Take a snapshot of reply to the shared location - await page.addStyleTag({ content: css }); - await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("Reply_to_the_location_on_ThreadView.png"); - }); + // Take a snapshot of reply to the shared location + await page.addStyleTag({ content: css }); + await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("Reply_to_the_location_on_ThreadView.png"); + }, + ); test("right panel behaves correctly", async ({ page, app, user }) => { // Create room diff --git a/playwright/e2e/timeline/timeline.spec.ts b/playwright/e2e/timeline/timeline.spec.ts index e8ef0e577c..7aaabb9759 100644 --- a/playwright/e2e/timeline/timeline.spec.ts +++ b/playwright/e2e/timeline/timeline.spec.ts @@ -137,182 +137,190 @@ test.describe("Timeline", () => { }); test.describe("configure room", () => { - test("should create and configure a room on IRC layout", async ({ page, app, room }) => { - await page.goto(`/#/room/${room.roomId}`); - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); - await expect( - page.locator( - ".mx_RoomView_body .mx_GenericEventListSummary[data-layout='irc'] .mx_GenericEventListSummary_summary", - { hasText: `${OLD_NAME} created and configured the room.` }, - ), - ).toBeVisible(); + test( + "should create and configure a room on IRC layout", + { tag: "@screenshot" }, + async ({ page, app, room }) => { + await page.goto(`/#/room/${room.roomId}`); + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); + await expect( + page.locator( + ".mx_RoomView_body .mx_GenericEventListSummary[data-layout='irc'] .mx_GenericEventListSummary_summary", + { hasText: `${OLD_NAME} created and configured the room.` }, + ), + ).toBeVisible(); - // wait for the date separator to appear to have a stable screenshot - await expect(page.locator(".mx_TimelineSeparator")).toHaveText("today"); + // wait for the date separator to appear to have a stable screenshot + await expect(page.locator(".mx_TimelineSeparator")).toHaveText("today"); - await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("configured-room-irc-layout.png"); - }); + await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("configured-room-irc-layout.png"); + }, + ); - test("should have an expanded generic event list summary (GELS) on IRC layout", async ({ page, app, room }) => { - await page.goto(`/#/room/${room.roomId}`); - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); + test( + "should have an expanded generic event list summary (GELS) on IRC layout", + { tag: "@screenshot" }, + async ({ page, app, room }) => { + await page.goto(`/#/room/${room.roomId}`); + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); - // Wait until configuration is finished - await expect( - page.locator( - ".mx_RoomView_body .mx_GenericEventListSummary[data-layout='irc'] .mx_GenericEventListSummary_summary", - { hasText: `${OLD_NAME} created and configured the room.` }, - ), - ).toBeVisible(); + // Wait until configuration is finished + await expect( + page.locator( + ".mx_RoomView_body .mx_GenericEventListSummary[data-layout='irc'] .mx_GenericEventListSummary_summary", + { hasText: `${OLD_NAME} created and configured the room.` }, + ), + ).toBeVisible(); - const gels = page.locator(".mx_GenericEventListSummary"); - // Click "expand" link button - await gels.getByRole("button", { name: "Expand" }).click(); - // Assert that the "expand" link button worked - await expect(gels.getByRole("button", { name: "Collapse" })).toBeVisible(); + const gels = page.locator(".mx_GenericEventListSummary"); + // Click "expand" link button + await gels.getByRole("button", { name: "Expand" }).click(); + // Assert that the "expand" link button worked + await expect(gels.getByRole("button", { name: "Collapse" })).toBeVisible(); - await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-irc-layout.png", { - mask: [page.locator(".mx_MessageTimestamp")], - css: ` - .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker { - display: none !important; - } - `, - }); - }); - - test("should have an expanded generic event list summary (GELS) on compact modern/group layout", async ({ - page, - app, - room, - }) => { - await page.goto(`/#/room/${room.roomId}`); - - // Set compact modern layout - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group); - await app.settings.setValue("useCompactLayout", null, SettingLevel.DEVICE, true); - - // Wait until configuration is finished - await expect( - page.locator(".mx_RoomView_body .mx_GenericEventListSummary[data-layout='group']", { - hasText: `${OLD_NAME} created and configured the room.`, - }), - ).toBeVisible(); - - const gels = page.locator(".mx_GenericEventListSummary"); - // Click "expand" link button - await gels.getByRole("button", { name: "Expand" }).click(); - // Assert that the "expand" link button worked - await expect(gels.getByRole("button", { name: "Collapse" })).toBeVisible(); - - await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-modern-layout.png", { - mask: [page.locator(".mx_MessageTimestamp")], - css: ` - .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker { - display: none !important; - } - `, - }); - }); - - test("should click 'collapse' on the first hovered info event line inside GELS on bubble layout", async ({ - page, - app, - room, - }) => { - // This test checks clickability of the "Collapse" link button, which had been covered with - // MessageActionBar's safe area - https://github.com/vector-im/element-web/issues/22864 - - await page.goto(`/#/room/${room.roomId}`); - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); - await expect( - page.locator( - ".mx_RoomView_body .mx_GenericEventListSummary[data-layout='bubble'] .mx_GenericEventListSummary_summary", - { hasText: `${OLD_NAME} created and configured the room.` }, - ), - ).toBeVisible(); - - const gels = page.locator(".mx_GenericEventListSummary"); - // Click "expand" link button - await gels.getByRole("button", { name: "Expand" }).click(); - // Assert that the "expand" link button worked - await expect(gels.getByRole("button", { name: "Collapse" })).toBeVisible(); - - // Make sure spacer is not visible on bubble layout - await expect( - page.locator(".mx_GenericEventListSummary[data-layout=bubble] .mx_GenericEventListSummary_spacer"), - ).not.toBeVisible(); // See: _GenericEventListSummary.pcss - - // Save snapshot of expanded generic event list summary on bubble layout - await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-bubble-layout.png", { - // Exclude timestamp from snapshot - mask: [page.locator(".mx_MessageTimestamp")], - }); - - // Click "collapse" link button on the first hovered info event line - const firstTile = gels.locator(".mx_GenericEventListSummary_unstyledList .mx_EventTile_info:first-of-type"); - await firstTile.hover(); - await expect(firstTile.getByRole("toolbar", { name: "Message Actions" })).toBeVisible(); - await gels.getByRole("button", { name: "Collapse" }).click(); - - // Assert that "collapse" link button worked - await expect(gels.getByRole("button", { name: "Expand" })).toBeVisible(); - - // Save snapshot of collapsed generic event list summary on bubble layout - await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("collapsed-gels-bubble-layout.png", { - mask: [page.locator(".mx_MessageTimestamp")], - }); - }); - - test("should add inline start margin to an event line on IRC layout", async ({ - page, - app, - room, - axe, - checkA11y, - }) => { - axe.disableRules("color-contrast"); - - await page.goto(`/#/room/${room.roomId}`); - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); - - // Wait until configuration is finished - await expect( - page.locator( - ".mx_RoomView_body .mx_GenericEventListSummary[data-layout='irc'] .mx_GenericEventListSummary_summary", - { hasText: `${OLD_NAME} created and configured the room.` }, - ), - ).toBeVisible(); - - // Click "expand" link button - await page.locator(".mx_GenericEventListSummary").getByRole("button", { name: "Expand" }).click(); - - // Check the event line has margin instead of inset property - // cf. _EventTile.pcss - // --EventTile_irc_line_info-margin-inline-start - // = calc(var(--name-width) + var(--icon-width) + 1 * var(--right-padding)) - // = 80 + 14 + 5 = 99px - - const firstEventLineIrc = page.locator( - ".mx_EventTile_info[data-layout=irc]:first-of-type .mx_EventTile_line", - ); - await expect(firstEventLineIrc).toHaveCSS("margin-inline-start", "99px"); - await expect(firstEventLineIrc).toHaveCSS("inset-inline-start", "0px"); - - await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( - "event-line-inline-start-margin-irc-layout.png", - { - // Exclude timestamp and read marker from snapshot + await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-irc-layout.png", { mask: [page.locator(".mx_MessageTimestamp")], css: ` .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker { display: none !important; } `, - }, - ); - await checkA11y(); - }); + }); + }, + ); + + test( + "should have an expanded generic event list summary (GELS) on compact modern/group layout", + { tag: "@screenshot" }, + async ({ page, app, room }) => { + await page.goto(`/#/room/${room.roomId}`); + + // Set compact modern layout + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group); + await app.settings.setValue("useCompactLayout", null, SettingLevel.DEVICE, true); + + // Wait until configuration is finished + await expect( + page.locator(".mx_RoomView_body .mx_GenericEventListSummary[data-layout='group']", { + hasText: `${OLD_NAME} created and configured the room.`, + }), + ).toBeVisible(); + + const gels = page.locator(".mx_GenericEventListSummary"); + // Click "expand" link button + await gels.getByRole("button", { name: "Expand" }).click(); + // Assert that the "expand" link button worked + await expect(gels.getByRole("button", { name: "Collapse" })).toBeVisible(); + + await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-modern-layout.png", { + mask: [page.locator(".mx_MessageTimestamp")], + css: ` + .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker { + display: none !important; + } + `, + }); + }, + ); + + test( + "should click 'collapse' on the first hovered info event line inside GELS on bubble layout", + { tag: "@screenshot" }, + async ({ page, app, room }) => { + // This test checks clickability of the "Collapse" link button, which had been covered with + // MessageActionBar's safe area - https://github.com/vector-im/element-web/issues/22864 + + await page.goto(`/#/room/${room.roomId}`); + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); + await expect( + page.locator( + ".mx_RoomView_body .mx_GenericEventListSummary[data-layout='bubble'] .mx_GenericEventListSummary_summary", + { hasText: `${OLD_NAME} created and configured the room.` }, + ), + ).toBeVisible(); + + const gels = page.locator(".mx_GenericEventListSummary"); + // Click "expand" link button + await gels.getByRole("button", { name: "Expand" }).click(); + // Assert that the "expand" link button worked + await expect(gels.getByRole("button", { name: "Collapse" })).toBeVisible(); + + // Make sure spacer is not visible on bubble layout + await expect( + page.locator(".mx_GenericEventListSummary[data-layout=bubble] .mx_GenericEventListSummary_spacer"), + ).not.toBeVisible(); // See: _GenericEventListSummary.pcss + + // Save snapshot of expanded generic event list summary on bubble layout + await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-bubble-layout.png", { + // Exclude timestamp from snapshot + mask: [page.locator(".mx_MessageTimestamp")], + }); + + // Click "collapse" link button on the first hovered info event line + const firstTile = gels.locator( + ".mx_GenericEventListSummary_unstyledList .mx_EventTile_info:first-of-type", + ); + await firstTile.hover(); + await expect(firstTile.getByRole("toolbar", { name: "Message Actions" })).toBeVisible(); + await gels.getByRole("button", { name: "Collapse" }).click(); + + // Assert that "collapse" link button worked + await expect(gels.getByRole("button", { name: "Expand" })).toBeVisible(); + + // Save snapshot of collapsed generic event list summary on bubble layout + await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("collapsed-gels-bubble-layout.png", { + mask: [page.locator(".mx_MessageTimestamp")], + }); + }, + ); + + test( + "should add inline start margin to an event line on IRC layout", + { tag: "@screenshot" }, + async ({ page, app, room, axe, checkA11y }) => { + axe.disableRules("color-contrast"); + + await page.goto(`/#/room/${room.roomId}`); + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); + + // Wait until configuration is finished + await expect( + page.locator( + ".mx_RoomView_body .mx_GenericEventListSummary[data-layout='irc'] .mx_GenericEventListSummary_summary", + { hasText: `${OLD_NAME} created and configured the room.` }, + ), + ).toBeVisible(); + + // Click "expand" link button + await page.locator(".mx_GenericEventListSummary").getByRole("button", { name: "Expand" }).click(); + + // Check the event line has margin instead of inset property + // cf. _EventTile.pcss + // --EventTile_irc_line_info-margin-inline-start + // = calc(var(--name-width) + var(--icon-width) + 1 * var(--right-padding)) + // = 80 + 14 + 5 = 99px + + const firstEventLineIrc = page.locator( + ".mx_EventTile_info[data-layout=irc]:first-of-type .mx_EventTile_line", + ); + await expect(firstEventLineIrc).toHaveCSS("margin-inline-start", "99px"); + await expect(firstEventLineIrc).toHaveCSS("inset-inline-start", "0px"); + + await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( + "event-line-inline-start-margin-irc-layout.png", + { + // Exclude timestamp and read marker from snapshot + mask: [page.locator(".mx_MessageTimestamp")], + css: ` + .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker { + display: none !important; + } + `, + }, + ); + await checkA11y(); + }, + ); }); test.describe("message displaying", () => { @@ -332,289 +340,311 @@ test.describe("Timeline", () => { ).toBeVisible(); }; - test("should align generic event list summary with messages and emote on IRC layout", async ({ - page, - app, - room, - }) => { - // This test aims to check: - // 1. Alignment of collapsed GELS (generic event list summary) and messages - // 2. Alignment of expanded GELS and messages - // 3. Alignment of expanded GELS and placeholder of deleted message - // 4. Alignment of expanded GELS, placeholder of deleted message, and emote + test( + "should align generic event list summary with messages and emote on IRC layout", + { tag: "@screenshot" }, + async ({ page, app, room }) => { + // This test aims to check: + // 1. Alignment of collapsed GELS (generic event list summary) and messages + // 2. Alignment of expanded GELS and messages + // 3. Alignment of expanded GELS and placeholder of deleted message + // 4. Alignment of expanded GELS, placeholder of deleted message, and emote - await page.goto(`/#/room/${room.roomId}`); - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); + await page.goto(`/#/room/${room.roomId}`); + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); - // Wait until configuration is finished - await expect( - page - .locator(".mx_GenericEventListSummary_summary") - .getByText(`${OLD_NAME} created and configured the room.`), - ).toBeVisible(); + // Wait until configuration is finished + await expect( + page + .locator(".mx_GenericEventListSummary_summary") + .getByText(`${OLD_NAME} created and configured the room.`), + ).toBeVisible(); - // Send messages - const composer = app.getComposerField(); - await composer.fill("Hello Mr. Bot"); - await composer.press("Enter"); - await composer.fill("Hello again, Mr. Bot"); - await composer.press("Enter"); + // Send messages + const composer = app.getComposerField(); + await composer.fill("Hello Mr. Bot"); + await composer.press("Enter"); + await composer.fill("Hello again, Mr. Bot"); + await composer.press("Enter"); - // Make sure the second message was sent - await expect( - page.locator(".mx_RoomView_MessageList > .mx_EventTile_last .mx_EventTile_receiptSent"), - ).toBeVisible(); + // Make sure the second message was sent + await expect( + page.locator(".mx_RoomView_MessageList > .mx_EventTile_last .mx_EventTile_receiptSent"), + ).toBeVisible(); - // 1. Alignment of collapsed GELS (generic event list summary) and messages - // Check inline start spacing of collapsed GELS - // See: _EventTile.pcss - // .mx_GenericEventListSummary[data-layout="irc"] > .mx_EventTile_line - // = var(--name-width) + var(--icon-width) + var(--MessageTimestamp-width) + 2 * var(--right-padding) - // = 80 + 14 + 46 + 2 * 5 - // = 150px - await expect(page.locator(".mx_GenericEventListSummary[data-layout=irc] > .mx_EventTile_line")).toHaveCSS( - "padding-inline-start", - "150px", - ); - // Check width and spacing values of elements in .mx_EventTile, which should be equal to 150px - // --right-padding should be applied - for (const locator of await page.locator(".mx_EventTile > a").all()) { - if (await locator.isVisible()) { - await expect(locator).toHaveCSS("margin-right", "5px"); + // 1. Alignment of collapsed GELS (generic event list summary) and messages + // Check inline start spacing of collapsed GELS + // See: _EventTile.pcss + // .mx_GenericEventListSummary[data-layout="irc"] > .mx_EventTile_line + // = var(--name-width) + var(--icon-width) + var(--MessageTimestamp-width) + 2 * var(--right-padding) + // = 80 + 14 + 46 + 2 * 5 + // = 150px + await expect( + page.locator(".mx_GenericEventListSummary[data-layout=irc] > .mx_EventTile_line"), + ).toHaveCSS("padding-inline-start", "150px"); + // Check width and spacing values of elements in .mx_EventTile, which should be equal to 150px + // --right-padding should be applied + for (const locator of await page.locator(".mx_EventTile > a").all()) { + if (await locator.isVisible()) { + await expect(locator).toHaveCSS("margin-right", "5px"); + } } - } - // --name-width width zero inline end margin should be applied - for (const locator of await page.locator(".mx_EventTile .mx_DisambiguatedProfile").all()) { - await expect(locator).toHaveCSS("width", "80px"); - await expect(locator).toHaveCSS("margin-inline-end", "0px"); - } - // --icon-width should be applied - for (const locator of await page.locator(".mx_EventTile .mx_EventTile_avatar > .mx_BaseAvatar").all()) { - await expect(locator).toHaveCSS("width", "14px"); - } - // var(--MessageTimestamp-width) should be applied - for (const locator of await page.locator(".mx_EventTile > a").all()) { - await expect(locator).toHaveCSS("min-width", "46px"); - } - // Record alignment of collapsed GELS and messages on messagePanel - await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( - "collapsed-gels-and-messages-irc-layout.png", - { + // --name-width width zero inline end margin should be applied + for (const locator of await page.locator(".mx_EventTile .mx_DisambiguatedProfile").all()) { + await expect(locator).toHaveCSS("width", "80px"); + await expect(locator).toHaveCSS("margin-inline-end", "0px"); + } + // --icon-width should be applied + for (const locator of await page.locator(".mx_EventTile .mx_EventTile_avatar > .mx_BaseAvatar").all()) { + await expect(locator).toHaveCSS("width", "14px"); + } + // var(--MessageTimestamp-width) should be applied + for (const locator of await page.locator(".mx_EventTile > a").all()) { + await expect(locator).toHaveCSS("min-width", "46px"); + } + // Record alignment of collapsed GELS and messages on messagePanel + await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( + "collapsed-gels-and-messages-irc-layout.png", + { + // Exclude timestamp from snapshot of mx_MainSplit + mask: [page.locator(".mx_MessageTimestamp")], + }, + ); + + // 2. Alignment of expanded GELS and messages + // Click "expand" link button + await page.locator(".mx_GenericEventListSummary").getByRole("button", { name: "Expand" }).click(); + // Check inline start spacing of info line on expanded GELS + // See: _EventTile.pcss + // --EventTile_irc_line_info-margin-inline-start + // = 80 + 14 + 1 * 5 + await expect( + page.locator(".mx_EventTile[data-layout=irc].mx_EventTile_info:first-of-type .mx_EventTile_line"), + ).toHaveCSS("margin-inline-start", "99px"); + // Record alignment of expanded GELS and messages on messagePanel + await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( + "expanded-gels-and-messages-irc-layout.png", + { + // Exclude timestamp from snapshot of mx_MainSplit + mask: [page.locator(".mx_MessageTimestamp")], + }, + ); + + // 3. Alignment of expanded GELS and placeholder of deleted message + // Delete the second (last) message + const lastTile = page.locator(".mx_RoomView_MessageList > .mx_EventTile_last"); + await lastTile.hover(); + await lastTile.getByRole("button", { name: "Options" }).click(); + await page.getByRole("menuitem", { name: "Remove" }).click(); + // Confirm deletion + await page.locator(".mx_Dialog_buttons").getByRole("button", { name: "Remove" }).click(); + // Make sure the dialog was closed and the second (last) message was redacted + await expect(page.locator(".mx_Dialog")).not.toBeVisible(); + await expect( + page.locator(".mx_GenericEventListSummary .mx_EventTile_last .mx_RedactedBody"), + ).toBeVisible(); + await expect( + page.locator(".mx_GenericEventListSummary .mx_EventTile_last .mx_EventTile_receiptSent"), + ).toBeVisible(); + // Record alignment of expanded GELS and placeholder of deleted message on messagePanel + await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( + "expanded-gels-redaction-placeholder.png", + { + // Exclude timestamp from snapshot of mx_MainSplit + mask: [page.locator(".mx_MessageTimestamp")], + }, + ); + + // 4. Alignment of expanded GELS, placeholder of deleted message, and emote + // Send a emote + await page + .locator(".mx_RoomView_body") + .getByRole("textbox", { name: "Send a message…" }) + .fill("/me says hello to Mr. Bot"); + await page + .locator(".mx_RoomView_body") + .getByRole("textbox", { name: "Send a message…" }) + .press("Enter"); + // Check inline start margin of its avatar + // Here --right-padding is for the avatar on the message line + // See: _IRCLayout.pcss + // .mx_IRCLayout .mx_EventTile_emote .mx_EventTile_avatar + // = calc(var(--name-width) + var(--icon-width) + 1 * var(--right-padding)) + // = 80 + 14 + 1 * 5 + await expect(page.locator(".mx_EventTile_emote .mx_EventTile_avatar")).toHaveCSS("margin-left", "99px"); + // Make sure emote was sent + await expect( + page.locator(".mx_EventTile_last.mx_EventTile_emote .mx_EventTile_receiptSent"), + ).toBeVisible(); + // Record alignment of expanded GELS, placeholder of deleted message, and emote + await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-emote-irc-layout.png", { // Exclude timestamp from snapshot of mx_MainSplit mask: [page.locator(".mx_MessageTimestamp")], - }, - ); + }); + }, + ); - // 2. Alignment of expanded GELS and messages - // Click "expand" link button - await page.locator(".mx_GenericEventListSummary").getByRole("button", { name: "Expand" }).click(); - // Check inline start spacing of info line on expanded GELS - // See: _EventTile.pcss - // --EventTile_irc_line_info-margin-inline-start - // = 80 + 14 + 1 * 5 - await expect( - page.locator(".mx_EventTile[data-layout=irc].mx_EventTile_info:first-of-type .mx_EventTile_line"), - ).toHaveCSS("margin-inline-start", "99px"); - // Record alignment of expanded GELS and messages on messagePanel - await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-and-messages-irc-layout.png", { - // Exclude timestamp from snapshot of mx_MainSplit - mask: [page.locator(".mx_MessageTimestamp")], - }); - - // 3. Alignment of expanded GELS and placeholder of deleted message - // Delete the second (last) message - const lastTile = page.locator(".mx_RoomView_MessageList > .mx_EventTile_last"); - await lastTile.hover(); - await lastTile.getByRole("button", { name: "Options" }).click(); - await page.getByRole("menuitem", { name: "Remove" }).click(); - // Confirm deletion - await page.locator(".mx_Dialog_buttons").getByRole("button", { name: "Remove" }).click(); - // Make sure the dialog was closed and the second (last) message was redacted - await expect(page.locator(".mx_Dialog")).not.toBeVisible(); - await expect(page.locator(".mx_GenericEventListSummary .mx_EventTile_last .mx_RedactedBody")).toBeVisible(); - await expect( - page.locator(".mx_GenericEventListSummary .mx_EventTile_last .mx_EventTile_receiptSent"), - ).toBeVisible(); - // Record alignment of expanded GELS and placeholder of deleted message on messagePanel - await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-redaction-placeholder.png", { - // Exclude timestamp from snapshot of mx_MainSplit - mask: [page.locator(".mx_MessageTimestamp")], - }); - - // 4. Alignment of expanded GELS, placeholder of deleted message, and emote - // Send a emote - await page - .locator(".mx_RoomView_body") - .getByRole("textbox", { name: "Send a message…" }) - .fill("/me says hello to Mr. Bot"); - await page.locator(".mx_RoomView_body").getByRole("textbox", { name: "Send a message…" }).press("Enter"); - // Check inline start margin of its avatar - // Here --right-padding is for the avatar on the message line - // See: _IRCLayout.pcss - // .mx_IRCLayout .mx_EventTile_emote .mx_EventTile_avatar - // = calc(var(--name-width) + var(--icon-width) + 1 * var(--right-padding)) - // = 80 + 14 + 1 * 5 - await expect(page.locator(".mx_EventTile_emote .mx_EventTile_avatar")).toHaveCSS("margin-left", "99px"); - // Make sure emote was sent - await expect(page.locator(".mx_EventTile_last.mx_EventTile_emote .mx_EventTile_receiptSent")).toBeVisible(); - // Record alignment of expanded GELS, placeholder of deleted message, and emote - await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-emote-irc-layout.png", { - // Exclude timestamp from snapshot of mx_MainSplit - mask: [page.locator(".mx_MessageTimestamp")], - }); - }); - - test("should render EventTiles on IRC, modern (group), and bubble layout", async ({ page, app, room }) => { - const screenshotOptions = { - // Hide because flaky - See https://github.com/vector-im/element-web/issues/24957 - mask: [page.locator(".mx_MessageTimestamp")], - css: ` + test( + "should render EventTiles on IRC, modern (group), and bubble layout", + { tag: "@screenshot" }, + async ({ page, app, room }) => { + const screenshotOptions = { + // Hide because flaky - See https://github.com/vector-im/element-web/issues/24957 + mask: [page.locator(".mx_MessageTimestamp")], + css: ` .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker { display: none !important; } `, - }; + }; - await sendEvent(app.client, room.roomId); - await sendEvent(app.client, room.roomId); // check continuation - await sendEvent(app.client, room.roomId); // check the last EventTile + await sendEvent(app.client, room.roomId); + await sendEvent(app.client, room.roomId); // check continuation + await sendEvent(app.client, room.roomId); // check the last EventTile - await page.goto(`/#/room/${room.roomId}`); - const composer = app.getComposerField(); - // Send a plain text message - await composer.fill("Hello"); - await composer.press("Enter"); - // Send a big emoji - await composer.fill("🏀"); - await composer.press("Enter"); - // Send an inline emoji - await composer.fill("This message has an inline emoji 👒"); - await composer.press("Enter"); + await page.goto(`/#/room/${room.roomId}`); + const composer = app.getComposerField(); + // Send a plain text message + await composer.fill("Hello"); + await composer.press("Enter"); + // Send a big emoji + await composer.fill("🏀"); + await composer.press("Enter"); + // Send an inline emoji + await composer.fill("This message has an inline emoji 👒"); + await composer.press("Enter"); - await expect(page.locator(".mx_RoomView").getByText("This message has an inline emoji 👒")).toBeVisible(); + await expect( + page.locator(".mx_RoomView").getByText("This message has an inline emoji 👒"), + ).toBeVisible(); - //////////////////////////////////////////////////////////////////////////////////////////////////////////// - // IRC layout - //////////////////////////////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // IRC layout + //////////////////////////////////////////////////////////////////////////////////////////////////////////// - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); - // Wait until configuration is finished - await expect( - page - .locator(".mx_GenericEventListSummary_summary") - .getByText(`${OLD_NAME} created and configured the room.`), - ).toBeVisible(); + // Wait until configuration is finished + await expect( + page + .locator(".mx_GenericEventListSummary_summary") + .getByText(`${OLD_NAME} created and configured the room.`), + ).toBeVisible(); - await app.timeline.scrollToBottom(); - await expect( - page.locator(".mx_RoomView").getByText("This message has an inline emoji 👒"), - ).toBeInViewport(); - await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( - "event-tiles-irc-layout.png", - screenshotOptions, - ); + await app.timeline.scrollToBottom(); + await expect( + page.locator(".mx_RoomView").getByText("This message has an inline emoji 👒"), + ).toBeInViewport(); + await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( + "event-tiles-irc-layout.png", + screenshotOptions, + ); - //////////////////////////////////////////////////////////////////////////////////////////////////////////// - // Group/modern layout - //////////////////////////////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Group/modern layout + //////////////////////////////////////////////////////////////////////////////////////////////////////////// - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group); + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group); - // Check that the last EventTile is rendered - await app.timeline.scrollToBottom(); - await expect( - page.locator(".mx_RoomView").getByText("This message has an inline emoji 👒"), - ).toBeInViewport(); - await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( - "event-tiles-modern-layout.png", - screenshotOptions, - ); + // Check that the last EventTile is rendered + await app.timeline.scrollToBottom(); + await expect( + page.locator(".mx_RoomView").getByText("This message has an inline emoji 👒"), + ).toBeInViewport(); + await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( + "event-tiles-modern-layout.png", + screenshotOptions, + ); - // Check the same thing for compact layout - await app.settings.setValue("useCompactLayout", null, SettingLevel.DEVICE, true); + // Check the same thing for compact layout + await app.settings.setValue("useCompactLayout", null, SettingLevel.DEVICE, true); - // Check that the last EventTile is rendered - await app.timeline.scrollToBottom(); - await expect( - page.locator(".mx_RoomView").getByText("This message has an inline emoji 👒"), - ).toBeInViewport(); - await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( - "event-tiles-compact-modern-layout.png", - screenshotOptions, - ); + // Check that the last EventTile is rendered + await app.timeline.scrollToBottom(); + await expect( + page.locator(".mx_RoomView").getByText("This message has an inline emoji 👒"), + ).toBeInViewport(); + await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( + "event-tiles-compact-modern-layout.png", + screenshotOptions, + ); - //////////////////////////////////////////////////////////////////////////////////////////////////////////// - // Message bubble layout - //////////////////////////////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Message bubble layout + //////////////////////////////////////////////////////////////////////////////////////////////////////////// - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); - await app.timeline.scrollToBottom(); - await expect( - page.locator(".mx_RoomView").getByText("This message has an inline emoji 👒"), - ).toBeInViewport(); - await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( - "event-tiles-bubble-layout.png", - screenshotOptions, - ); - }); + await app.timeline.scrollToBottom(); + await expect( + page.locator(".mx_RoomView").getByText("This message has an inline emoji 👒"), + ).toBeInViewport(); + await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( + "event-tiles-bubble-layout.png", + screenshotOptions, + ); + }, + ); - test("should set inline start padding to a hidden event line", async ({ page, app, room }) => { - test.skip( - true, - "Disabled due to screenshot test being flaky - https://github.com/element-hq/element-web/issues/26890", - ); - await sendEvent(app.client, room.roomId); - await page.goto(`/#/room/${room.roomId}`); - await app.settings.setValue("showHiddenEventsInTimeline", null, SettingLevel.DEVICE, true); - await expect( - page - .locator(".mx_GenericEventListSummary_summary") - .getByText(`${OLD_NAME} created and configured the room.`), - ).toBeVisible(); + test( + "should set inline start padding to a hidden event line", + { tag: "@screenshot" }, + async ({ page, app, room }) => { + test.skip( + true, + "Disabled due to screenshot test being flaky - https://github.com/element-hq/element-web/issues/26890", + ); + await sendEvent(app.client, room.roomId); + await page.goto(`/#/room/${room.roomId}`); + await app.settings.setValue("showHiddenEventsInTimeline", null, SettingLevel.DEVICE, true); + await expect( + page + .locator(".mx_GenericEventListSummary_summary") + .getByText(`${OLD_NAME} created and configured the room.`), + ).toBeVisible(); - // Edit message - await messageEdit(page); + // Edit message + await messageEdit(page); - // Click timestamp to highlight hidden event line - await page.locator(".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp").click(); + // Click timestamp to highlight hidden event line + await page.locator(".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp").click(); - // should not add inline start padding to a hidden event line on IRC layout - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); - await expect( - page.locator(".mx_EventTile[data-layout=irc].mx_EventTile_info .mx_EventTile_line").first(), - ).toHaveCSS("padding-inline-start", "0px"); + // should not add inline start padding to a hidden event line on IRC layout + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); + await expect( + page.locator(".mx_EventTile[data-layout=irc].mx_EventTile_info .mx_EventTile_line").first(), + ).toHaveCSS("padding-inline-start", "0px"); - // Exclude timestamp and read marker from snapshot - const screenshotOptions = { - mask: [page.locator(".mx_MessageTimestamp")], - css: ` + // 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_MainSplit")).toMatchScreenshot( - "hidden-event-line-zero-padding-irc-layout.png", - screenshotOptions, - ); + await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( + "hidden-event-line-zero-padding-irc-layout.png", + screenshotOptions, + ); - // should add inline start padding to a hidden event line on modern layout - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group); - // calc(var(--EventTile_group_line-spacing-inline-start) + 20px) = 64 + 20 = 84px - await expect( - page.locator(".mx_EventTile[data-layout=group].mx_EventTile_info .mx_EventTile_line").first(), - ).toHaveCSS("padding-inline-start", "84px"); + // should add inline start padding to a hidden event line on modern layout + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group); + // calc(var(--EventTile_group_line-spacing-inline-start) + 20px) = 64 + 20 = 84px + await expect( + page.locator(".mx_EventTile[data-layout=group].mx_EventTile_info .mx_EventTile_line").first(), + ).toHaveCSS("padding-inline-start", "84px"); - await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( - "hidden-event-line-padding-modern-layout.png", - screenshotOptions, - ); - }); + await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( + "hidden-event-line-padding-modern-layout.png", + screenshotOptions, + ); + }, + ); - test("should click view source event toggle", async ({ page, app, room }) => { + test("should click view source event toggle", { tag: "@screenshot" }, async ({ page, app, room }) => { // This test checks: // 1. clickability of top left of view source event toggle // 2. clickability of view source toggle on IRC layout @@ -712,89 +742,97 @@ test.describe("Timeline", () => { ).toBeVisible(); }); - test("should render url previews", async ({ page, app, room, axe, checkA11y, context }) => { - axe.disableRules("color-contrast"); + test( + "should render url previews", + { tag: "@screenshot" }, + async ({ page, app, room, axe, checkA11y, context }) => { + axe.disableRules("color-contrast"); - // Element Web uses a Service Worker to rewrite unauthenticated media requests to authenticated ones, but - // the page can't see this happening. We intercept the route at the BrowserContext to ensure we get it - // post-worker, but we can't waitForResponse on that, so the page context is still used there. Because - // the page doesn't see the rewrite, it waits for the unauthenticated route. This is only confusing until - // the js-sdk (and thus the app as a whole) switches to using authenticated endpoints by default, hopefully. - await context.route( - "**/_matrix/client/v1/media/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*", - async (route) => { - await route.fulfill({ - path: "playwright/sample-files/riot.png", - }); - }, - ); - await page.route( - "**/_matrix/media/v3/preview_url?url=https%3A%2F%2Fcall.element.io%2F&ts=*", - async (route) => { - await route.fulfill({ - json: { - "og:title": "Element Call", - "og:description": null, - "og:image:width": 48, - "og:image:height": 48, - "og:image": "mxc://matrix.org/2022-08-16_yaiSVSRIsNFfxDnV", - "og:image:type": "image/png", - "matrix:image:size": 2121, - }, - }); - }, - ); + // Element Web uses a Service Worker to rewrite unauthenticated media requests to authenticated ones, but + // the page can't see this happening. We intercept the route at the BrowserContext to ensure we get it + // post-worker, but we can't waitForResponse on that, so the page context is still used there. Because + // the page doesn't see the rewrite, it waits for the unauthenticated route. This is only confusing until + // the js-sdk (and thus the app as a whole) switches to using authenticated endpoints by default, hopefully. + await context.route( + "**/_matrix/client/v1/media/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*", + async (route) => { + await route.fulfill({ + path: "playwright/sample-files/riot.png", + }); + }, + ); + await page.route( + "**/_matrix/media/v3/preview_url?url=https%3A%2F%2Fcall.element.io%2F&ts=*", + async (route) => { + await route.fulfill({ + json: { + "og:title": "Element Call", + "og:description": null, + "og:image:width": 48, + "og:image:height": 48, + "og:image": "mxc://matrix.org/2022-08-16_yaiSVSRIsNFfxDnV", + "og:image:type": "image/png", + "matrix:image:size": 2121, + }, + }); + }, + ); - const requestPromises: Promise<any>[] = [ - page.waitForResponse("**/_matrix/media/v3/preview_url?url=https%3A%2F%2Fcall.element.io%2F&ts=*"), - // see context.route above for why we listen for the unauthenticated endpoint - page.waitForResponse("**/_matrix/media/v3/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*"), - ]; + const requestPromises: Promise<any>[] = [ + page.waitForResponse("**/_matrix/media/v3/preview_url?url=https%3A%2F%2Fcall.element.io%2F&ts=*"), + // see context.route above for why we listen for the unauthenticated endpoint + page.waitForResponse("**/_matrix/media/v3/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*"), + ]; - await app.client.sendMessage(room.roomId, "https://call.element.io/"); - await page.goto(`/#/room/${room.roomId}`); + await app.client.sendMessage(room.roomId, "https://call.element.io/"); + await page.goto(`/#/room/${room.roomId}`); - await expect(page.locator(".mx_LinkPreviewWidget").getByText("Element Call")).toBeVisible(); - await Promise.all(requestPromises); + await expect(page.locator(".mx_LinkPreviewWidget").getByText("Element Call")).toBeVisible(); + await Promise.all(requestPromises); - await checkA11y(); + await checkA11y(); - await app.timeline.scrollToBottom(); - await expect(page.locator(".mx_EventTile_last")).toMatchScreenshot("url-preview.png", { - // Exclude timestamp and read marker from snapshot - mask: [page.locator(".mx_MessageTimestamp")], - css: ` + await app.timeline.scrollToBottom(); + await expect(page.locator(".mx_EventTile_last")).toMatchScreenshot("url-preview.png", { + // Exclude timestamp and read marker from snapshot + mask: [page.locator(".mx_MessageTimestamp")], + css: ` .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker { display: none !important; } `, - }); - }); + }); + }, + ); test.describe("on search results panel", () => { - test("should highlight search result words regardless of formatting", async ({ page, app, room }) => { - await sendEvent(app.client, room.roomId); - await sendEvent(app.client, room.roomId, true); - await page.goto(`/#/room/${room.roomId}`); + test( + "should highlight search result words regardless of formatting", + { tag: "@screenshot" }, + async ({ page, app, room }) => { + await sendEvent(app.client, room.roomId); + await sendEvent(app.client, room.roomId, true); + await page.goto(`/#/room/${room.roomId}`); - await app.toggleRoomInfoPanel(); + await app.toggleRoomInfoPanel(); - await page.locator(".mx_RoomSummaryCard_search").getByRole("searchbox").fill("Message"); - await page.locator(".mx_RoomSummaryCard_search").getByRole("searchbox").press("Enter"); + await page.locator(".mx_RoomSummaryCard_search").getByRole("searchbox").fill("Message"); + await page.locator(".mx_RoomSummaryCard_search").getByRole("searchbox").press("Enter"); - await expect(page.locator(".mx_RoomSearchAuxPanel")).toMatchScreenshot("search-aux-panel.png"); + await expect(page.locator(".mx_RoomSearchAuxPanel")).toMatchScreenshot("search-aux-panel.png"); - for (const locator of await page - .locator(".mx_EventTile:not(.mx_EventTile_contextual) .mx_EventTile_searchHighlight") - .all()) { - await expect(locator).toBeVisible(); - } - await expect(page.locator(".mx_RoomView_searchResultsPanel")).toMatchScreenshot( - "highlighted-search-results.png", - ); - }); + for (const locator of await page + .locator(".mx_EventTile:not(.mx_EventTile_contextual) .mx_EventTile_searchHighlight") + .all()) { + await expect(locator).toBeVisible(); + } + await expect(page.locator(".mx_RoomView_searchResultsPanel")).toMatchScreenshot( + "highlighted-search-results.png", + ); + }, + ); - test("should render a fully opaque textual event", async ({ page, app, room }) => { + test("should render a fully opaque textual event", { tag: "@screenshot" }, async ({ page, app, room }) => { const stringToSearch = "Message"; // Same with string sent with sendEvent() await sendEvent(app.client, room.roomId); @@ -918,7 +956,7 @@ test.describe("Timeline", () => { ).toHaveCount(4); }); - test("should display a reply chain", async ({ page, app, room, homeserver }) => { + test("should display a reply chain", { tag: "@screenshot" }, async ({ page, app, room, homeserver }) => { const reply2 = "Reply again"; await page.goto(`/#/room/${room.roomId}`); @@ -1031,122 +1069,121 @@ test.describe("Timeline", () => { ); }); - test("should send, reply, and display long strings without overflowing", async ({ - page, - app, - room, - homeserver, - }) => { - // Max 256 characters for display name - const LONG_STRING = - "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut " + - "et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut " + - "aliquip"; + test( + "should send, reply, and display long strings without overflowing", + { tag: "@screenshot" }, + async ({ page, app, room, homeserver }) => { + // Max 256 characters for display name + const LONG_STRING = + "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut " + + "et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut " + + "aliquip"; - const newDisplayName = `${LONG_STRING} 2`; + const newDisplayName = `${LONG_STRING} 2`; - // Set the display name to "LONG_STRING 2" in order to avoid screenshot tests from failing - // due to the generated random mxid being displayed inside the GELS summary. - // Note that we set it here as the test was failing on CI (but not locally!) if the name - // was changed afterwards. This is quite concerning, but maybe better than just disabling the - // whole test? - // https://github.com/element-hq/element-web/issues/27109 - await app.client.setDisplayName(newDisplayName); + // Set the display name to "LONG_STRING 2" in order to avoid screenshot tests from failing + // due to the generated random mxid being displayed inside the GELS summary. + // Note that we set it here as the test was failing on CI (but not locally!) if the name + // was changed afterwards. This is quite concerning, but maybe better than just disabling the + // whole test? + // https://github.com/element-hq/element-web/issues/27109 + await app.client.setDisplayName(newDisplayName); - // Create a bot with a long display name - const bot = new Bot(page, homeserver, { - displayName: LONG_STRING, - autoAcceptInvites: false, - }); - await bot.prepareClient(); + // Create a bot with a long display name + const bot = new Bot(page, homeserver, { + displayName: LONG_STRING, + autoAcceptInvites: false, + }); + await bot.prepareClient(); - // Create another room with a long name, invite the bot, and open the room - const testRoomId = await app.client.createRoom({ name: LONG_STRING }); - await app.client.inviteUser(testRoomId, bot.credentials.userId); - await bot.joinRoom(testRoomId); - await page.goto(`/#/room/${testRoomId}`); + // Create another room with a long name, invite the bot, and open the room + const testRoomId = await app.client.createRoom({ name: LONG_STRING }); + await app.client.inviteUser(testRoomId, bot.credentials.userId); + await bot.joinRoom(testRoomId); + await page.goto(`/#/room/${testRoomId}`); - // Wait until configuration is finished - await expect( - page - .locator(".mx_GenericEventListSummary_summary") - .getByText(newDisplayName + " created and configured the room."), - ).toBeVisible(); + // Wait until configuration is finished + await expect( + page + .locator(".mx_GenericEventListSummary_summary") + .getByText(newDisplayName + " created and configured the room."), + ).toBeVisible(); - // Have the bot send a long message - await bot.sendMessage(testRoomId, { - body: LONG_STRING, - msgtype: "m.text", - }); + // Have the bot send a long message + await bot.sendMessage(testRoomId, { + body: LONG_STRING, + msgtype: "m.text", + }); - // Wait until the message is rendered - await expect( - page.locator(".mx_EventTile_last .mx_MTextBody .mx_EventTile_body").getByText(LONG_STRING), - ).toBeVisible(); + // Wait until the message is rendered + await expect( + page.locator(".mx_EventTile_last .mx_MTextBody .mx_EventTile_body").getByText(LONG_STRING), + ).toBeVisible(); - // Reply to the message - await clickButtonReply(page); - await app.getComposerField().fill(reply); - await app.getComposerField().press("Enter"); + // Reply to the message + await clickButtonReply(page); + await app.getComposerField().fill(reply); + await app.getComposerField().press("Enter"); - // Make sure the reply tile is rendered - const eventTileLine = page.locator(".mx_EventTile_last .mx_EventTile_line"); - await expect(eventTileLine.locator(".mx_ReplyTile .mx_MTextBody").getByText(LONG_STRING)).toBeVisible(); + // Make sure the reply tile is rendered + const eventTileLine = page.locator(".mx_EventTile_last .mx_EventTile_line"); + await expect(eventTileLine.locator(".mx_ReplyTile .mx_MTextBody").getByText(LONG_STRING)).toBeVisible(); - await expect(eventTileLine.getByText(reply)).toHaveCount(1); + await expect(eventTileLine.getByText(reply)).toHaveCount(1); - // Change the viewport size - await page.setViewportSize({ width: 1600, height: 1200 }); + // Change the viewport size + await page.setViewportSize({ width: 1600, height: 1200 }); - // Exclude timestamp and read marker from snapshot - const screenshotOptions = { - mask: [page.locator(".mx_MessageTimestamp")], - css: ` + // Exclude timestamp and read marker from snapshot + const screenshotOptions = { + mask: [page.locator(".mx_MessageTimestamp")], + css: ` .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker { display: none !important; } `, - }; + }; - // Make sure the strings do not overflow on IRC layout - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); - // Scroll to the bottom to take a snapshot of the whole viewport - await app.timeline.scrollToBottom(); - // Assert that both avatar in the introduction and the last message are visible at the same time - await expect(page.locator(".mx_NewRoomIntro .mx_BaseAvatar")).toBeVisible(); - const lastEventTileIrc = page.locator(".mx_EventTile_last[data-layout='irc']"); - await expect(lastEventTileIrc.locator(".mx_MTextBody").first()).toBeVisible(); - await expect(lastEventTileIrc.locator(".mx_EventTile_receiptSent")).toBeVisible(); // rendered at the bottom of EventTile - // Take a snapshot in IRC layout - await expect(page.locator(".mx_ScrollPanel")).toMatchScreenshot( - "long-strings-with-reply-irc-layout.png", - screenshotOptions, - ); + // Make sure the strings do not overflow on IRC layout + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); + // Scroll to the bottom to take a snapshot of the whole viewport + await app.timeline.scrollToBottom(); + // Assert that both avatar in the introduction and the last message are visible at the same time + await expect(page.locator(".mx_NewRoomIntro .mx_BaseAvatar")).toBeVisible(); + const lastEventTileIrc = page.locator(".mx_EventTile_last[data-layout='irc']"); + await expect(lastEventTileIrc.locator(".mx_MTextBody").first()).toBeVisible(); + await expect(lastEventTileIrc.locator(".mx_EventTile_receiptSent")).toBeVisible(); // rendered at the bottom of EventTile + // Take a snapshot in IRC layout + await expect(page.locator(".mx_ScrollPanel")).toMatchScreenshot( + "long-strings-with-reply-irc-layout.png", + screenshotOptions, + ); - // Make sure the strings do not overflow on modern layout - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group); - await app.timeline.scrollToBottom(); // Scroll again in case - await expect(page.locator(".mx_NewRoomIntro .mx_BaseAvatar")).toBeVisible(); - const lastEventTileGroup = page.locator(".mx_EventTile_last[data-layout='group']"); - await expect(lastEventTileGroup.locator(".mx_MTextBody").first()).toBeVisible(); - await expect(lastEventTileGroup.locator(".mx_EventTile_receiptSent")).toBeVisible(); - await expect(page.locator(".mx_ScrollPanel")).toMatchScreenshot( - "long-strings-with-reply-modern-layout.png", - screenshotOptions, - ); + // Make sure the strings do not overflow on modern layout + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group); + await app.timeline.scrollToBottom(); // Scroll again in case + await expect(page.locator(".mx_NewRoomIntro .mx_BaseAvatar")).toBeVisible(); + const lastEventTileGroup = page.locator(".mx_EventTile_last[data-layout='group']"); + await expect(lastEventTileGroup.locator(".mx_MTextBody").first()).toBeVisible(); + await expect(lastEventTileGroup.locator(".mx_EventTile_receiptSent")).toBeVisible(); + await expect(page.locator(".mx_ScrollPanel")).toMatchScreenshot( + "long-strings-with-reply-modern-layout.png", + screenshotOptions, + ); - // Make sure the strings do not overflow on bubble layout - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); - await app.timeline.scrollToBottom(); // Scroll again in case - await expect(page.locator(".mx_NewRoomIntro .mx_BaseAvatar")).toBeVisible(); - const lastEventTileBubble = page.locator(".mx_EventTile_last[data-layout='bubble']"); - await expect(lastEventTileBubble.locator(".mx_MTextBody").first()).toBeVisible(); - await expect(lastEventTileBubble.locator(".mx_EventTile_receiptSent")).toBeVisible(); - await expect(page.locator(".mx_ScrollPanel")).toMatchScreenshot( - "long-strings-with-reply-bubble-layout.png", - screenshotOptions, - ); - }); + // Make sure the strings do not overflow on bubble layout + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); + await app.timeline.scrollToBottom(); // Scroll again in case + await expect(page.locator(".mx_NewRoomIntro .mx_BaseAvatar")).toBeVisible(); + const lastEventTileBubble = page.locator(".mx_EventTile_last[data-layout='bubble']"); + await expect(lastEventTileBubble.locator(".mx_MTextBody").first()).toBeVisible(); + await expect(lastEventTileBubble.locator(".mx_EventTile_receiptSent")).toBeVisible(); + await expect(page.locator(".mx_ScrollPanel")).toMatchScreenshot( + "long-strings-with-reply-bubble-layout.png", + screenshotOptions, + ); + }, + ); async function testImageRendering(page: Page, app: ElementAppPage, room: { roomId: string }) { await app.viewRoomById(room.roomId); @@ -1176,7 +1213,7 @@ test.describe("Timeline", () => { ); } - test("should render images in the timeline", async ({ page, app, room, context }) => { + test("should render images in the timeline", { tag: "@screenshot" }, async ({ page, app, room, context }) => { await testImageRendering(page, app, room); }); @@ -1188,50 +1225,54 @@ test.describe("Timeline", () => { // 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 + test( + "should render authenticated images in the timeline", + { tag: "@screenshot" }, + 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/Matrix 1.11, which may require the service worker's internal cache to be cleared later. - const json = await (await route.fetch()).json(); - if (!json["versions"]) json["versions"] = []; - json["versions"].push("v1.11"); - 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." }, + // Install our mocks and preventative measures + await context.route("**/_matrix/client/versions", async (route) => { + // Force enable MSC3916/Matrix 1.11, which may require the service worker's internal cache to be cleared later. + const json = await (await route.fetch()).json(); + if (!json["versions"]) json["versions"] = []; + json["versions"].push("v1.11"); + await route.fulfill({ json }); }); - }); - 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/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/client/v1/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/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/v1/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, + await context.route("**/_matrix/client/v1/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/v1/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); - }); + // We check the same screenshot because there should be no user-visible impact to using authentication. + await testImageRendering(page, app, room); + }, + ); }); }); }); diff --git a/playwright/e2e/user-menu/user-menu.spec.ts b/playwright/e2e/user-menu/user-menu.spec.ts index 0ad21dbded..268da00f30 100644 --- a/playwright/e2e/user-menu/user-menu.spec.ts +++ b/playwright/e2e/user-menu/user-menu.spec.ts @@ -11,7 +11,7 @@ import { test, expect } from "../../element-web-test"; test.describe("User Menu", () => { test.use({ displayName: "Jeff" }); - test("should contain our name & userId", async ({ page, user }) => { + test("should contain our name & userId", { tag: "@screenshot" }, async ({ page, user }) => { await page.getByRole("button", { name: "User menu", exact: true }).click(); const menu = page.getByRole("menu"); diff --git a/playwright/e2e/user-onboarding/user-onboarding-new.spec.ts b/playwright/e2e/user-onboarding/user-onboarding-new.spec.ts index f561eb9615..b89fa3ac70 100644 --- a/playwright/e2e/user-onboarding/user-onboarding-new.spec.ts +++ b/playwright/e2e/user-onboarding/user-onboarding-new.spec.ts @@ -26,7 +26,7 @@ test.describe("User Onboarding (new user)", () => { await expect(page.locator(".mx_UserOnboardingList")).toBeVisible(); }); - test("page is shown and preference exists", async ({ page, app }) => { + test("page is shown and preference exists", { tag: "@screenshot" }, async ({ page, app }) => { await expect(page.locator(".mx_UserOnboardingPage")).toMatchScreenshot( "User-Onboarding-new-user-page-is-shown-and-preference-exists-1.png", ); @@ -34,7 +34,7 @@ test.describe("User Onboarding (new user)", () => { await expect(page.getByText("Show shortcut to welcome checklist above the room list")).toBeVisible(); }); - test("app download dialog", async ({ page }) => { + test("app download dialog", { tag: "@screenshot" }, async ({ page }) => { await page.getByRole("button", { name: "Download apps" }).click(); await expect( page.getByRole("dialog").getByRole("heading", { level: 1, name: "Download Element" }), diff --git a/playwright/e2e/user-view/user-view.spec.ts b/playwright/e2e/user-view/user-view.spec.ts index 218c63fe74..ff8e9684e9 100644 --- a/playwright/e2e/user-view/user-view.spec.ts +++ b/playwright/e2e/user-view/user-view.spec.ts @@ -14,7 +14,7 @@ test.describe("UserView", () => { botCreateOpts: { displayName: "Usman" }, }); - test("should render the user view as expected", async ({ page, homeserver, user, bot }) => { + test("should render the user view as expected", { tag: "@screenshot" }, async ({ page, homeserver, user, bot }) => { await page.goto(`/#/user/${bot.credentials.userId}`); const rightPanel = page.locator("#mx_RightPanel"); diff --git a/playwright/e2e/widgets/layout.spec.ts b/playwright/e2e/widgets/layout.spec.ts index 41cfece6e8..c80ea44078 100644 --- a/playwright/e2e/widgets/layout.spec.ts +++ b/playwright/e2e/widgets/layout.spec.ts @@ -70,7 +70,7 @@ test.describe("Widget Layout", () => { await app.viewRoomByName(ROOM_NAME); }); - test("should be set properly", async ({ page }) => { + test("should be set properly", { tag: "@screenshot" }, async ({ page }) => { await expect(page.locator(".mx_AppsDrawer")).toMatchScreenshot("apps-drawer.png"); }); diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts index 8d5229a510..76e57e33f7 100644 --- a/playwright/element-web-test.ts +++ b/playwright/element-web-test.ts @@ -314,6 +314,10 @@ export const expect = baseExpect.extend({ const testInfo = test.info(); if (!testInfo) throw new Error(`toMatchScreenshot() must be called during the test`); + if (!testInfo.tags.includes("@screenshot")) { + throw new Error("toMatchScreenshot() must be used in a test tagged with @screenshot"); + } + const page = "page" in receiver ? receiver.page() : receiver; let css = ` diff --git a/playwright/plugins/homeserver/synapse/index.ts b/playwright/plugins/homeserver/synapse/index.ts index 0a14950b1f..9fb859cc17 100644 --- a/playwright/plugins/homeserver/synapse/index.ts +++ b/playwright/plugins/homeserver/synapse/index.ts @@ -20,7 +20,7 @@ import { randB64Bytes } from "../../utils/rand"; // Docker tag to use for synapse docker image. // We target a specific digest as every now and then a Synapse update will break our CI. // This digest is updated by the playwright-image-updates.yaml workflow periodically. -const DOCKER_TAG = "develop@sha256:127c68d4468019ce363c8b2fd7a42a3ef50710eb3aaf288a2295dd4623ce9f54"; +const DOCKER_TAG = "develop@sha256:14408766bad1274ec077d3fe6f0ae72748962c3303e1a6ee1e78bc46176411bb"; async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise<Omit<HomeserverConfig, "dockerUrl">> { const templateDir = path.join(__dirname, "templates", opts.template); diff --git a/playwright/plugins/oauth_server/README.md b/playwright/plugins/oauth_server/README.md index 541756384f..5260704a66 100644 --- a/playwright/plugins/oauth_server/README.md +++ b/playwright/plugins/oauth_server/README.md @@ -4,16 +4,16 @@ A very simple OAuth identity provider server. The following endpoints are exposed: -- `/oauth/auth.html`: An OAuth2 [authorization endpoint](https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint). - In a proper OAuth2 system, this would prompt the user to log in; we just give a big "Submit" button (and an - auth code that can be changed if we want the next step to fail). It redirects back to the calling application - with a "code". +- `/oauth/auth.html`: An OAuth2 [authorization endpoint](https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint). + In a proper OAuth2 system, this would prompt the user to log in; we just give a big "Submit" button (and an + auth code that can be changed if we want the next step to fail). It redirects back to the calling application + with a "code". -- `/oauth/token`: An OAuth2 [token endpoint](https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint). - Receives the code issued by "auth.html" and, if it is valid, exchanges it for an OAuth2 access token. +- `/oauth/token`: An OAuth2 [token endpoint](https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint). + Receives the code issued by "auth.html" and, if it is valid, exchanges it for an OAuth2 access token. -- `/oauth/userinfo`: An OAuth2 [userinfo endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo). - Returns details about the owner of the offered access token. +- `/oauth/userinfo`: An OAuth2 [userinfo endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo). + Returns details about the owner of the offered access token. To start the server, do: diff --git a/playwright/snapshots/app-loading/feature-detection.spec.ts/unsupported-browser-CompatibilityView-linux.png b/playwright/snapshots/app-loading/feature-detection.spec.ts/unsupported-browser-CompatibilityView-linux.png index dd8b24beea..6524a45a67 100644 Binary files a/playwright/snapshots/app-loading/feature-detection.spec.ts/unsupported-browser-CompatibilityView-linux.png and b/playwright/snapshots/app-loading/feature-detection.spec.ts/unsupported-browser-CompatibilityView-linux.png differ diff --git a/playwright/snapshots/app-loading/feature-detection.spec.ts/unsupported-browser-linux.png b/playwright/snapshots/app-loading/feature-detection.spec.ts/unsupported-browser-linux.png index 96ad96e3e1..9a7760bfd0 100644 Binary files a/playwright/snapshots/app-loading/feature-detection.spec.ts/unsupported-browser-linux.png and b/playwright/snapshots/app-loading/feature-detection.spec.ts/unsupported-browser-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--bubble-layout-linux.png index 0887d95a0c..165033dbe9 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--bubble-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--bubble-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--group-layout-linux.png index 1a082fc9bf..f309d57bc0 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--group-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--group-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--irc-layout-linux.png index 4600e591f8..bd02a2f21a 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--irc-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--irc-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--bubble-layout-linux.png index 4878163232..16e0624b83 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--bubble-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--bubble-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--group-layout-linux.png index 1167c0672f..1e78930256 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--group-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--group-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--irc-layout-linux.png index bdddc786c5..6a43aac7ef 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--irc-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--irc-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--bubble-layout-linux.png index e9feb85d10..014b8dbaec 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--bubble-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--bubble-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--group-layout-linux.png index f59e2d7ab1..156d89053c 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--group-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--group-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--irc-layout-linux.png index 078eae3de4..caf6e1e698 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--irc-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--irc-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--bubble-layout-linux.png index 0f0df017fd..c9591ebf49 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--bubble-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--bubble-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--group-layout-linux.png index 6cc3f5506f..794ac11b01 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--group-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--group-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--irc-layout-linux.png index f92f43c947..2b6475fbdf 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--irc-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--irc-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-bubble-layout-linux.png index 61051aa939..0f643ee43a 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-bubble-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-bubble-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-bubble-layout-linux.png index 3dded6ef82..ffe1b0be50 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-bubble-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-bubble-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-group-layout-linux.png index 6866b16475..2b867170ae 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-group-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-group-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-irc-layout-linux.png index a340f5c104..459ebd3584 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-irc-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-irc-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-group-layout-linux.png index 80927840ae..da97c28029 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-group-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-group-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-irc-layout-linux.png index 346642f84d..009ea38f7b 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-irc-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-irc-layout-linux.png differ diff --git a/playwright/snapshots/chat-export/html-export.spec.ts/html-export-linux.png b/playwright/snapshots/chat-export/html-export.spec.ts/html-export-linux.png index 8519e162f2..465e58ca39 100644 Binary files a/playwright/snapshots/chat-export/html-export.spec.ts/html-export-linux.png and b/playwright/snapshots/chat-export/html-export.spec.ts/html-export-linux.png differ diff --git a/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png b/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png index 3e68415017..14cb5ce372 100644 Binary files a/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png and b/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png differ diff --git a/playwright/snapshots/editing/editing.spec.ts/message-edit-history-dialog-linux.png b/playwright/snapshots/editing/editing.spec.ts/message-edit-history-dialog-linux.png index 3504b1ddfa..81939514ff 100644 Binary files a/playwright/snapshots/editing/editing.spec.ts/message-edit-history-dialog-linux.png and b/playwright/snapshots/editing/editing.spec.ts/message-edit-history-dialog-linux.png differ diff --git a/playwright/snapshots/file-upload/image-upload.spec.ts/image-upload-preview-linux.png b/playwright/snapshots/file-upload/image-upload.spec.ts/image-upload-preview-linux.png index d2afccb990..e3c37e79c9 100644 Binary files a/playwright/snapshots/file-upload/image-upload.spec.ts/image-upload-preview-linux.png and b/playwright/snapshots/file-upload/image-upload.spec.ts/image-upload-preview-linux.png differ diff --git a/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-linux.png b/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-linux.png index 6ce8d95e59..377e1931be 100644 Binary files a/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-linux.png and b/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-linux.png differ diff --git a/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-verify-email-linux.png b/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-verify-email-linux.png index 5fa7969c57..ec7b9a174d 100644 Binary files a/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-verify-email-linux.png and b/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-verify-email-linux.png differ diff --git a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-with-user-pill-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-with-user-pill-linux.png index 554400123f..cc64e8bf78 100644 Binary files a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-with-user-pill-linux.png and b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-with-user-pill-linux.png differ diff --git a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-without-user-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-without-user-linux.png index 9a888fb65c..a634e876bc 100644 Binary files a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-without-user-linux.png and b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-without-user-linux.png differ diff --git a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png index 1342f5bf27..d01aae804c 100644 Binary files a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png and b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png differ diff --git a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png index de476106e9..6e8e2fbddf 100644 Binary files a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png and b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-ltrdisplayname-linux.png index fe92443694..9831051768 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-ltrdisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-ltrdisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-rtldisplayname-linux.png index a0a5dbb8b0..4bf016b825 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-ltrdisplayname-linux.png index cf2da6f023..9f31f518fc 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-ltrdisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-ltrdisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-rtldisplayname-linux.png index e9aded5a5f..ee69990523 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-ltrdisplayname-linux.png index 1e29c40c73..d8b02a028b 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-ltrdisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-ltrdisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-rtldisplayname-linux.png index 104b8f469e..eea2b47469 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-ltrdisplayname-linux.png index f15894f2b3..85a3c69c0e 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-ltrdisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-ltrdisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-rtldisplayname-linux.png index bec538f32d..4d2e9c593d 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-ltr-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-ltr-ltrdisplayname-linux.png index 772bbbbeec..5ab0a7e88f 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/emote-ltr-ltrdisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/emote-ltr-ltrdisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-ltr-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-ltr-rtldisplayname-linux.png index 04f4e0d1f5..1bb3582578 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/emote-ltr-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/emote-ltr-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-ltrdisplayname-linux.png index 8cc8d6e088..c3501583f8 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-ltrdisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-ltrdisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-rtldisplayname-linux.png index feb0651650..a4b7d0a992 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-ltrdisplayname-linux.png index 181e1fb9ca..537f1dd2c4 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-ltrdisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-ltrdisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-rtldisplayname-linux.png index ffae099bdc..26bd9f7523 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-rtl-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-rtl-ltrdisplayname-linux.png index ee9d8b8a43..27357dc503 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/emote-rtl-ltrdisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/emote-rtl-ltrdisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-rtl-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-rtl-rtldisplayname-linux.png index 19075ea869..3f50a1406f 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/emote-rtl-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/emote-rtl-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/reply-message-ltr-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/reply-message-ltr-ltrdisplayname-linux.png index eebd533079..a4de383fba 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/reply-message-ltr-ltrdisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/reply-message-ltr-ltrdisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/reply-message-ltr-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/reply-message-ltr-rtldisplayname-linux.png index ed0c69fb8d..37096f025c 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/reply-message-ltr-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/reply-message-ltr-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-ltrdisplayname-linux.png index 1a0f5577b6..a3acc741f3 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-ltrdisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-ltrdisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-rtldisplayname-linux.png index 587170ee1c..52ced56c83 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/permalinks/permalinks.spec.ts/permalink-rendering-linux.png b/playwright/snapshots/permalinks/permalinks.spec.ts/permalink-rendering-linux.png index 0ec66fa4ef..62ffcead99 100644 Binary files a/playwright/snapshots/permalinks/permalinks.spec.ts/permalink-rendering-linux.png and b/playwright/snapshots/permalinks/permalinks.spec.ts/permalink-rendering-linux.png differ diff --git a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-Msg1-linux.png b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-Msg1-linux.png index 97b751ec6a..f39caf654a 100644 Binary files a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-Msg1-linux.png and b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-Msg1-linux.png differ diff --git a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-1-Msg1-linux.png b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-1-Msg1-linux.png index c3c4d57acf..ac13a2152d 100644 Binary files a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-1-Msg1-linux.png and b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-1-Msg1-linux.png differ diff --git a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-2-Msg1-linux.png b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-2-Msg1-linux.png index 3c87967137..20917ae16f 100644 Binary files a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-2-Msg1-linux.png and b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-2-Msg1-linux.png differ diff --git a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-2-Msg2-linux.png b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-2-Msg2-linux.png index 996c289bcc..aaf8c720eb 100644 Binary files a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-2-Msg2-linux.png and b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-2-Msg2-linux.png differ diff --git a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg1-linux.png b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg1-linux.png index 35c7e6fb25..7ef356c589 100644 Binary files a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg1-linux.png and b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg1-linux.png differ diff --git a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg2-linux.png b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg2-linux.png index 610a44adf1..a62d8182b1 100644 Binary files a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg2-linux.png and b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg2-linux.png differ diff --git a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg3-linux.png b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg3-linux.png index 62d1f3401b..fa548aaadc 100644 Binary files a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg3-linux.png and b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg3-linux.png differ diff --git a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg4-linux.png b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg4-linux.png index 2c27dd0029..bc2062f98d 100644 Binary files a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg4-linux.png and b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-banner-4-Msg4-linux.png differ diff --git a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-messages-list-empty-linux.png b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-messages-list-empty-linux.png index 16c694de95..84ff71a49c 100644 Binary files a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-messages-list-empty-linux.png and b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-messages-list-empty-linux.png differ diff --git a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/unpin-all-dialog-linux.png b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/unpin-all-dialog-linux.png index e6f1005395..39a8905711 100644 Binary files a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/unpin-all-dialog-linux.png and b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/unpin-all-dialog-linux.png differ diff --git a/playwright/snapshots/polls/polls.spec.ts/Polls-Timeline-tile-no-votes-linux.png b/playwright/snapshots/polls/polls.spec.ts/Polls-Timeline-tile-no-votes-linux.png index 1ade373ba8..6818add73e 100644 Binary files a/playwright/snapshots/polls/polls.spec.ts/Polls-Timeline-tile-no-votes-linux.png and b/playwright/snapshots/polls/polls.spec.ts/Polls-Timeline-tile-no-votes-linux.png differ diff --git a/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png b/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png index bbc29d4be3..a842b686dd 100644 Binary files a/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png and b/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png differ diff --git a/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png b/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png index 120b80320b..d9d12951df 100644 Binary files a/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png and b/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png differ diff --git a/playwright/snapshots/register/email.spec.ts/registration-check-your-email-linux.png b/playwright/snapshots/register/email.spec.ts/registration-check-your-email-linux.png index a8709dfd99..4b724d7772 100644 Binary files a/playwright/snapshots/register/email.spec.ts/registration-check-your-email-linux.png and b/playwright/snapshots/register/email.spec.ts/registration-check-your-email-linux.png differ diff --git a/playwright/snapshots/register/register.spec.ts/email-prompt-linux.png b/playwright/snapshots/register/register.spec.ts/email-prompt-linux.png index 5d32d5b619..f78a2a0b16 100644 Binary files a/playwright/snapshots/register/register.spec.ts/email-prompt-linux.png and b/playwright/snapshots/register/register.spec.ts/email-prompt-linux.png differ diff --git a/playwright/snapshots/register/register.spec.ts/registration-linux.png b/playwright/snapshots/register/register.spec.ts/registration-linux.png index a4f3be59a9..481c74fcda 100644 Binary files a/playwright/snapshots/register/register.spec.ts/registration-linux.png and b/playwright/snapshots/register/register.spec.ts/registration-linux.png differ diff --git a/playwright/snapshots/register/register.spec.ts/server-picker-linux.png b/playwright/snapshots/register/register.spec.ts/server-picker-linux.png index c0e398debb..da924bae82 100644 Binary files a/playwright/snapshots/register/register.spec.ts/server-picker-linux.png and b/playwright/snapshots/register/register.spec.ts/server-picker-linux.png differ diff --git a/playwright/snapshots/register/register.spec.ts/terms-prompt-linux.png b/playwright/snapshots/register/register.spec.ts/terms-prompt-linux.png index 310dcf25d9..f5326e9d2d 100644 Binary files a/playwright/snapshots/register/register.spec.ts/terms-prompt-linux.png and b/playwright/snapshots/register/register.spec.ts/terms-prompt-linux.png differ diff --git a/playwright/snapshots/register/register.spec.ts/use-case-selection-linux.png b/playwright/snapshots/register/register.spec.ts/use-case-selection-linux.png index efde30d4fa..1dd98b51e1 100644 Binary files a/playwright/snapshots/register/register.spec.ts/use-case-selection-linux.png and b/playwright/snapshots/register/register.spec.ts/use-case-selection-linux.png differ diff --git a/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-Threads-Activity-Centre-linux.png b/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-Threads-Activity-Centre-linux.png index 8a0b738a1b..fee99165ab 100644 Binary files a/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-Threads-Activity-Centre-linux.png and b/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-Threads-Activity-Centre-linux.png differ diff --git a/playwright/snapshots/right-panel/file-panel.spec.ts/empty-linux.png b/playwright/snapshots/right-panel/file-panel.spec.ts/empty-linux.png index 9edd65a633..0566a21175 100644 Binary files a/playwright/snapshots/right-panel/file-panel.spec.ts/empty-linux.png and b/playwright/snapshots/right-panel/file-panel.spec.ts/empty-linux.png differ diff --git a/playwright/snapshots/right-panel/file-panel.spec.ts/file-tiles-list-linux.png b/playwright/snapshots/right-panel/file-panel.spec.ts/file-tiles-list-linux.png index 8b248e5240..1c75a92373 100644 Binary files a/playwright/snapshots/right-panel/file-panel.spec.ts/file-tiles-list-linux.png and b/playwright/snapshots/right-panel/file-panel.spec.ts/file-tiles-list-linux.png differ diff --git a/playwright/snapshots/right-panel/notification-panel.spec.ts/empty-linux.png b/playwright/snapshots/right-panel/notification-panel.spec.ts/empty-linux.png index 26481bfe5d..cd228be8ca 100644 Binary files a/playwright/snapshots/right-panel/notification-panel.spec.ts/empty-linux.png and b/playwright/snapshots/right-panel/notification-panel.spec.ts/empty-linux.png differ diff --git a/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png b/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png index 4e3c67393b..8ba6f98e11 100644 Binary files a/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png and b/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png differ diff --git a/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-no-results-linux.png b/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-no-results-linux.png index 82779d4cc9..783e468c6e 100644 Binary files a/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-no-results-linux.png and b/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-no-results-linux.png differ diff --git a/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-one-result-linux.png b/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-one-result-linux.png index 666dbe689a..aaa504f4e3 100644 Binary files a/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-one-result-linux.png and b/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-one-result-linux.png differ diff --git a/playwright/snapshots/room/room-header.spec.ts/room-header-linux.png b/playwright/snapshots/room/room-header.spec.ts/room-header-linux.png index ea591089b4..afc5d53fab 100644 Binary files a/playwright/snapshots/room/room-header.spec.ts/room-header-linux.png and b/playwright/snapshots/room/room-header.spec.ts/room-header-linux.png differ diff --git a/playwright/snapshots/room/room-header.spec.ts/room-header-long-name-linux.png b/playwright/snapshots/room/room-header.spec.ts/room-header-long-name-linux.png index 742ed77712..ce15e3e151 100644 Binary files a/playwright/snapshots/room/room-header.spec.ts/room-header-long-name-linux.png and b/playwright/snapshots/room/room-header.spec.ts/room-header-long-name-linux.png differ diff --git a/playwright/snapshots/room/room-header.spec.ts/room-header-video-room-linux.png b/playwright/snapshots/room/room-header.spec.ts/room-header-video-room-linux.png index bb10e28aba..bd31e502d7 100644 Binary files a/playwright/snapshots/room/room-header.spec.ts/room-header-video-room-linux.png and b/playwright/snapshots/room/room-header.spec.ts/room-header-video-room-linux.png differ diff --git a/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-linux.png b/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-linux.png index 702dab55f9..c641f2a0aa 100644 Binary files a/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-linux.png and b/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-linux.png differ diff --git a/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-smallscreen-linux.png b/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-smallscreen-linux.png index d1f28308f1..b9d81c5d5a 100644 Binary files a/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-smallscreen-linux.png and b/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-smallscreen-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png index a30e9969b6..75a4852f9b 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png index ba0b6f24b8..41ffca6c93 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-added-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-added-linux.png index d44c107307..147fcfa057 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-added-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-added-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-linux.png index 76a0befd33..5475f9a537 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-removed-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-removed-linux.png index cdafb6e9ee..23b88c022c 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-removed-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-custom-theme-removed-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-dark-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-dark-linux.png index 3b9c243138..6378098d7a 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-dark-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-dark-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-light-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-light-linux.png index ca90917116..f2269a0532 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-light-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-light-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-match-system-enabled-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-match-system-enabled-linux.png index 1aed777c8d..6b41f30acd 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-match-system-enabled-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-match-system-enabled-linux.png differ diff --git a/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png b/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png index b9b3969000..c08a36a808 100644 Binary files a/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png and b/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png differ diff --git a/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png b/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png index 082650056f..7d95205251 100644 Binary files a/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png and b/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png differ diff --git a/playwright/snapshots/settings/security-user-settings-tab.spec.ts/Security-user-settings-tab-with-posthog-enable-b5d89-csLearnMoreDialog-should-be-rendered-properly-1-linux.png b/playwright/snapshots/settings/security-user-settings-tab.spec.ts/Security-user-settings-tab-with-posthog-enable-b5d89-csLearnMoreDialog-should-be-rendered-properly-1-linux.png index e858838ab9..720cc41548 100644 Binary files a/playwright/snapshots/settings/security-user-settings-tab.spec.ts/Security-user-settings-tab-with-posthog-enable-b5d89-csLearnMoreDialog-should-be-rendered-properly-1-linux.png and b/playwright/snapshots/settings/security-user-settings-tab.spec.ts/Security-user-settings-tab-with-posthog-enable-b5d89-csLearnMoreDialog-should-be-rendered-properly-1-linux.png differ diff --git a/playwright/snapshots/share-dialog/share-dialog.spec.ts/share-dialog-event-linux.png b/playwright/snapshots/share-dialog/share-dialog.spec.ts/share-dialog-event-linux.png new file mode 100644 index 0000000000..541eaa2fa5 Binary files /dev/null and b/playwright/snapshots/share-dialog/share-dialog.spec.ts/share-dialog-event-linux.png differ diff --git a/playwright/snapshots/share-dialog/share-dialog.spec.ts/share-dialog-room-linux.png b/playwright/snapshots/share-dialog/share-dialog.spec.ts/share-dialog-room-linux.png new file mode 100644 index 0000000000..f6fbd2fa43 Binary files /dev/null and b/playwright/snapshots/share-dialog/share-dialog.spec.ts/share-dialog-room-linux.png differ diff --git a/playwright/snapshots/share-dialog/share-dialog.spec.ts/share-dialog-user-linux.png b/playwright/snapshots/share-dialog/share-dialog.spec.ts/share-dialog-user-linux.png new file mode 100644 index 0000000000..2fb39b9f5f Binary files /dev/null and b/playwright/snapshots/share-dialog/share-dialog.spec.ts/share-dialog-user-linux.png differ diff --git a/playwright/snapshots/spaces/spaces.spec.ts/invite-teammates-dialog-linux.png b/playwright/snapshots/spaces/spaces.spec.ts/invite-teammates-dialog-linux.png index ef7536d455..2244dc7cf9 100644 Binary files a/playwright/snapshots/spaces/spaces.spec.ts/invite-teammates-dialog-linux.png and b/playwright/snapshots/spaces/spaces.spec.ts/invite-teammates-dialog-linux.png differ diff --git a/playwright/snapshots/spaces/spaces.spec.ts/space-create-menu-linux.png b/playwright/snapshots/spaces/spaces.spec.ts/space-create-menu-linux.png index 3dae06ad52..d178f257ca 100644 Binary files a/playwright/snapshots/spaces/spaces.spec.ts/space-create-menu-linux.png and b/playwright/snapshots/spaces/spaces.spec.ts/space-create-menu-linux.png differ diff --git a/playwright/snapshots/spaces/spaces.spec.ts/space-panel-collapsed-linux.png b/playwright/snapshots/spaces/spaces.spec.ts/space-panel-collapsed-linux.png index 8485df9e3a..312ca2580c 100644 Binary files a/playwright/snapshots/spaces/spaces.spec.ts/space-panel-collapsed-linux.png and b/playwright/snapshots/spaces/spaces.spec.ts/space-panel-collapsed-linux.png differ diff --git a/playwright/snapshots/spaces/spaces.spec.ts/space-panel-expanded-linux.png b/playwright/snapshots/spaces/spaces.spec.ts/space-panel-expanded-linux.png index 296563e870..f0606ce47a 100644 Binary files a/playwright/snapshots/spaces/spaces.spec.ts/space-panel-expanded-linux.png and b/playwright/snapshots/spaces/spaces.spec.ts/space-panel-expanded-linux.png differ diff --git a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-button-expanded-linux.png b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-button-expanded-linux.png index 85e483aca0..2ad6315d9e 100644 Binary files a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-button-expanded-linux.png and b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-button-expanded-linux.png differ diff --git a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-expanded-linux.png b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-expanded-linux.png index 85e483aca0..2ad6315d9e 100644 Binary files a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-expanded-linux.png and b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-expanded-linux.png differ diff --git a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-linux.png b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-linux.png index 5d9490c1d1..591d22c3c4 100644 Binary files a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-linux.png and b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-linux.png differ diff --git a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-no-indicator-linux.png b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-no-indicator-linux.png index c7a1f9fea1..fedc5cd750 100644 Binary files a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-no-indicator-linux.png and b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-no-indicator-linux.png differ diff --git a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-mix-unread-linux.png b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-mix-unread-linux.png index ec5a8193d2..0785ecbe97 100644 Binary files a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-mix-unread-linux.png and b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-mix-unread-linux.png differ diff --git a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-notification-unread-linux.png b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-notification-unread-linux.png index f0f6cee3e6..8b6af97d6e 100644 Binary files a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-notification-unread-linux.png and b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-notification-unread-linux.png differ diff --git a/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png b/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png index d8a4f542d4..e43e41dd79 100644 Binary files a/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png and b/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png index b0b7efb95b..348db69cfc 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-bubble-layout-linux.png index d05e7432b3..42ee5a0acb 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-bubble-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-bubble-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/configured-room-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/configured-room-irc-layout-linux.png index 375f2e9640..92532e3d9c 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/configured-room-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/configured-room-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-line-inline-start-margin-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-line-inline-start-margin-irc-layout-linux.png index 334221670d..1e50cd3c0f 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-line-inline-start-margin-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-line-inline-start-margin-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-bubble-layout-linux.png index c0a01c99fb..167f9b6855 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-bubble-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-bubble-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-compact-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-compact-modern-layout-linux.png index 87e65a86ae..0682bf760f 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-compact-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-compact-modern-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-layout-linux.png index 98ec9e0cf6..b9899fb177 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-modern-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-modern-linux.png index 445d616ea4..ca4ce5933f 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-modern-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-modern-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-bubble-layout-linux.png index 5be5e1ec05..b0960a1188 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-bubble-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-bubble-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-compact-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-compact-modern-layout-linux.png index 01ed4d5b0c..a7637b6b94 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-compact-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-compact-modern-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-irc-layout-linux.png index 74817fced2..a609a4cd0d 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-modern-layout-linux.png index d2d5b2cf73..fe50abef0c 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-modern-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-and-messages-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-and-messages-irc-layout-linux.png index eaa1e70db5..ac6dadc962 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-and-messages-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-and-messages-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-bubble-layout-linux.png index e8a6062591..8e833be308 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-bubble-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-bubble-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-emote-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-emote-irc-layout-linux.png index 7ce18d56e2..3e9e78ca99 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-emote-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-emote-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-irc-layout-linux.png index 334221670d..1e50cd3c0f 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-modern-layout-linux.png index a98369e7b6..b81a9d68a8 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-modern-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-redaction-placeholder-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-redaction-placeholder-linux.png index 91926e1c15..58ba6c5703 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-redaction-placeholder-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-redaction-placeholder-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/highlighted-search-results-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/highlighted-search-results-linux.png index 008ebc82e3..a99cfb68cf 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/highlighted-search-results-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/highlighted-search-results-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/hovered-hidden-event-line-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/hovered-hidden-event-line-irc-layout-linux.png index 100dc86c7a..5fb0c283b5 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/hovered-hidden-event-line-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/hovered-hidden-event-line-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/image-in-timeline-default-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/image-in-timeline-default-layout-linux.png index 1d05e32105..b8a24fb3a4 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/image-in-timeline-default-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/image-in-timeline-default-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-bubble-layout-linux.png index 7a918a959a..879e647f5e 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-bubble-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-bubble-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-irc-layout-linux.png index e52b371fe7..b52a56a22e 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-modern-layout-linux.png index ec66bf2013..bbaba1dbea 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-modern-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/search-aux-panel-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/search-aux-panel-linux.png index b50f176222..125c47ee35 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/search-aux-panel-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/search-aux-panel-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/search-results-with-TextualEvent-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/search-results-with-TextualEvent-linux.png index cf489b1836..5aa6bcea19 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/search-results-with-TextualEvent-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/search-results-with-TextualEvent-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/url-preview-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/url-preview-linux.png index a0c833115c..56220d88d1 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/url-preview-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/url-preview-linux.png differ diff --git a/playwright/snapshots/user-menu/user-menu.spec.ts/user-menu-linux.png b/playwright/snapshots/user-menu/user-menu.spec.ts/user-menu-linux.png index edc0ad17ad..bfaa18d4c3 100644 Binary files a/playwright/snapshots/user-menu/user-menu.spec.ts/user-menu-linux.png and b/playwright/snapshots/user-menu/user-menu.spec.ts/user-menu-linux.png differ diff --git a/playwright/snapshots/user-onboarding/user-onboarding-new.spec.ts/User-Onboarding-new-user-app-download-dialog-1-linux.png b/playwright/snapshots/user-onboarding/user-onboarding-new.spec.ts/User-Onboarding-new-user-app-download-dialog-1-linux.png index 0c7fc94a0e..024886d01e 100644 Binary files a/playwright/snapshots/user-onboarding/user-onboarding-new.spec.ts/User-Onboarding-new-user-app-download-dialog-1-linux.png and b/playwright/snapshots/user-onboarding/user-onboarding-new.spec.ts/User-Onboarding-new-user-app-download-dialog-1-linux.png differ diff --git a/playwright/snapshots/user-onboarding/user-onboarding-new.spec.ts/User-Onboarding-new-user-page-is-shown-and-preference-exists-1-linux.png b/playwright/snapshots/user-onboarding/user-onboarding-new.spec.ts/User-Onboarding-new-user-page-is-shown-and-preference-exists-1-linux.png index 3112b0fcf2..1042d92e76 100644 Binary files a/playwright/snapshots/user-onboarding/user-onboarding-new.spec.ts/User-Onboarding-new-user-page-is-shown-and-preference-exists-1-linux.png and b/playwright/snapshots/user-onboarding/user-onboarding-new.spec.ts/User-Onboarding-new-user-page-is-shown-and-preference-exists-1-linux.png differ diff --git a/playwright/snapshots/user-view/user-view.spec.ts/user-info-linux.png b/playwright/snapshots/user-view/user-view.spec.ts/user-info-linux.png index 0612fe1ba7..30206f1a25 100644 Binary files a/playwright/snapshots/user-view/user-view.spec.ts/user-info-linux.png and b/playwright/snapshots/user-view/user-view.spec.ts/user-info-linux.png differ diff --git a/playwright/snapshots/widgets/layout.spec.ts/apps-drawer-linux.png b/playwright/snapshots/widgets/layout.spec.ts/apps-drawer-linux.png index 203991bc32..6331d373b3 100644 Binary files a/playwright/snapshots/widgets/layout.spec.ts/apps-drawer-linux.png and b/playwright/snapshots/widgets/layout.spec.ts/apps-drawer-linux.png differ diff --git a/res/css/_common.pcss b/res/css/_common.pcss index 15ba02b6b8..74328af39b 100644 --- a/res/css/_common.pcss +++ b/res/css/_common.pcss @@ -596,7 +596,7 @@ legend { .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button), + ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button), .mx_Dialog input[type="submit"], .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton), .mx_Dialog_buttons input[type="submit"] { @@ -616,14 +616,16 @@ legend { .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):last-child { + ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not( + .mx_ShareDialog button + ):last-child { margin-right: 0px; } .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):focus, + ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button):focus, .mx_Dialog input[type="submit"]:focus, .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):focus, .mx_Dialog_buttons input[type="submit"]:focus { @@ -635,7 +637,7 @@ legend { .mx_Dialog_buttons button.mx_Dialog_primary:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button), + ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button), .mx_Dialog_buttons input[type="submit"].mx_Dialog_primary { color: var(--cpd-color-text-on-solid-primary); background-color: var(--cpd-color-bg-action-primary-rest); @@ -648,7 +650,7 @@ legend { .mx_Dialog_buttons button.danger:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(.mx_UserProfileSettings button):not( .mx_ThemeChoicePanel_CustomTheme button - ):not(.mx_UnpinAllDialog button), + ):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button), .mx_Dialog_buttons input[type="submit"].danger { background-color: var(--cpd-color-bg-critical-primary); border: solid 1px var(--cpd-color-bg-critical-primary); @@ -664,7 +666,7 @@ legend { .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):disabled, + ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button):disabled, .mx_Dialog input[type="submit"]:disabled, .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):disabled, .mx_Dialog_buttons input[type="submit"]:disabled { diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 12239fac2d..e9a53cd43c 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -319,6 +319,7 @@ @import "./views/rooms/_ThirdPartyMemberInfo.pcss"; @import "./views/rooms/_ThreadSummary.pcss"; @import "./views/rooms/_TopUnreadMessagesBar.pcss"; +@import "./views/rooms/_UserIdentityWarning.pcss"; @import "./views/rooms/_VoiceRecordComposerTile.pcss"; @import "./views/rooms/_WhoIsTypingTile.pcss"; @import "./views/rooms/wysiwyg_composer/_EditWysiwygComposer.pcss"; @@ -392,9 +393,3 @@ @import "./views/voip/_LegacyCallViewHeader.pcss"; @import "./views/voip/_LegacyCallViewSidebar.pcss"; @import "./views/voip/_VideoFeed.pcss"; -@import "./voice-broadcast/atoms/_LiveBadge.pcss"; -@import "./voice-broadcast/atoms/_VoiceBroadcastControl.pcss"; -@import "./voice-broadcast/atoms/_VoiceBroadcastHeader.pcss"; -@import "./voice-broadcast/atoms/_VoiceBroadcastRecordingConnectionError.pcss"; -@import "./voice-broadcast/atoms/_VoiceBroadcastRoomSubtitle.pcss"; -@import "./voice-broadcast/molecules/_VoiceBroadcastBody.pcss"; diff --git a/res/css/components/views/settings/shared/_SettingsSubsection.pcss b/res/css/components/views/settings/shared/_SettingsSubsection.pcss index 14261a59c8..ec20b86d98 100644 --- a/res/css/components/views/settings/shared/_SettingsSubsection.pcss +++ b/res/css/components/views/settings/shared/_SettingsSubsection.pcss @@ -13,7 +13,16 @@ Please see LICENSE files in the repository root for full details. &.mx_SettingsSubsection_newUi { display: flex; flex-direction: column; - gap: var(--cpd-space-8x); + gap: var(--cpd-space-6x); + } + + *[role="separator"] { + /** + * The gap between subsections is 32px and inside the subsection is 24px. + * The separator separates the subsections, so it should have the same gap as the subsections. + * We add 12px and the separator spacing to the top margin to make the separator visually centered between the subsections. + */ + margin-top: calc(var(--cpd-space-3x) + var(--cpd-separator-spacing)); } } diff --git a/res/css/structures/_UserMenu.pcss b/res/css/structures/_UserMenu.pcss index 741a4e90dc..d24a6e4ac7 100644 --- a/res/css/structures/_UserMenu.pcss +++ b/res/css/structures/_UserMenu.pcss @@ -22,20 +22,6 @@ Please see LICENSE files in the repository root for full details. pointer-events: none; /* makes the avatar non-draggable */ } } - - .mx_UserMenu_userAvatarLive { - align-items: center; - background-color: $alert; - border-radius: 6px; - color: $live-badge-color; - display: flex; - height: 12px; - justify-content: center; - left: 25px; - position: absolute; - top: 20px; - width: 12px; - } } .mx_UserMenu_contextMenuButton { diff --git a/res/css/views/dialogs/_ShareDialog.pcss b/res/css/views/dialogs/_ShareDialog.pcss index 086222af31..cfede43aae 100644 --- a/res/css/views/dialogs/_ShareDialog.pcss +++ b/res/css/views/dialogs/_ShareDialog.pcss @@ -5,50 +5,73 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -.mx_ShareDialog hr { - margin-top: 25px; - margin-bottom: 25px; - border-color: $light-fg-color; -} +.mx_ShareDialog { + /* Value from figma design */ + width: 416px; -.mx_ShareDialog .mx_ShareDialog_content { - margin: 10px 0; + .mx_Dialog_header { + text-align: center; + margin-bottom: var(--cpd-space-6x); + /* Override dialog header padding to able to center it */ + padding-inline-end: 0; + } - .mx_CopyableText { - width: unset; /* full width */ + .mx_ShareDialog_content { + display: flex; + flex-direction: column; + gap: var(--cpd-space-6x); + align-items: center; - > a { - text-decoration: none; - flex-shrink: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + .mx_ShareDialog_top { + display: flex; + flex-direction: column; + gap: var(--cpd-space-4x); + align-items: center; + width: 100%; + + span { + text-align: center; + font: var(--cpd-font-body-sm-semibold); + color: var(--cpd-color-text-secondary); + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + width: 100%; + } + } + + label { + display: inline-flex; + gap: var(--cpd-space-3x); + justify-content: center; + align-items: center; + font: var(--cpd-font-body-md-medium); + } + + button { + width: 100%; + } + + .mx_ShareDialog_social { + display: flex; + gap: var(--cpd-space-3x); + justify-content: center; + + a { + width: 48px; + height: 48px; + border-radius: 99px; + box-sizing: border-box; + border: 1px solid var(--cpd-color-border-interactive-secondary); + display: flex; + justify-content: center; + align-items: center; + + img { + width: 24px; + height: 24px; + } + } } } } - -.mx_ShareDialog_split { - display: flex; - flex-wrap: wrap; -} - -.mx_ShareDialog_qrcode_container { - float: left; - height: 256px; - width: 256px; - margin-right: 64px; -} - -.mx_ShareDialog_qrcode_container + .mx_ShareDialog_social_container { - width: 299px; -} - -.mx_ShareDialog_social_container { - display: inline-block; -} - -.mx_ShareDialog_social_icon { - display: inline-grid; - margin-right: 10px; - margin-bottom: 10px; -} diff --git a/res/css/views/rooms/_MessageComposer.pcss b/res/css/views/rooms/_MessageComposer.pcss index 3f11e9fa6c..73ac15c9c9 100644 --- a/res/css/views/rooms/_MessageComposer.pcss +++ b/res/css/views/rooms/_MessageComposer.pcss @@ -256,10 +256,6 @@ Please see LICENSE files in the repository root for full details. mask-image: url("@vector-im/compound-design-tokens/icons/mic-on-solid.svg"); } -.mx_MessageComposer_voiceBroadcast::before { - mask-image: url("$(res)/img/element-icons/live.svg"); -} - .mx_MessageComposer_plain_text::before { mask-image: url("$(res)/img/element-icons/room/composer/plain_text.svg"); } diff --git a/res/css/views/rooms/_UserIdentityWarning.pcss b/res/css/views/rooms/_UserIdentityWarning.pcss new file mode 100644 index 0000000000..b294b3fc8c --- /dev/null +++ b/res/css/views/rooms/_UserIdentityWarning.pcss @@ -0,0 +1,28 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +.mx_UserIdentityWarning { + /* 42px is the padding-left of .mx_MessageComposer_wrapper in res/css/views/rooms/_MessageComposer.pcss */ + margin-left: calc(-42px + var(--RoomView_MessageList-padding)); + + .mx_UserIdentityWarning_row { + display: flex; + align-items: center; + + .mx_BaseAvatar { + margin-left: var(--cpd-space-2x); + } + .mx_UserIdentityWarning_main { + margin-left: var(--cpd-space-6x); + flex-grow: 1; + } + } +} + +.mx_MessageComposer.mx_MessageComposer--compact > .mx_UserIdentityWarning { + margin-left: calc(-25px + var(--RoomView_MessageList-padding)); +} diff --git a/res/css/voice-broadcast/atoms/_LiveBadge.pcss b/res/css/voice-broadcast/atoms/_LiveBadge.pcss deleted file mode 100644 index 7d5f23819b..0000000000 --- a/res/css/voice-broadcast/atoms/_LiveBadge.pcss +++ /dev/null @@ -1,23 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -.mx_LiveBadge { - align-items: center; - background-color: $alert; - border-radius: 2px; - color: $live-badge-color; - display: inline-flex; - font-size: $font-12px; - font-weight: var(--cpd-font-weight-semibold); - gap: $spacing-4; - padding: 2px 4px; -} - -.mx_LiveBadge--grey { - background-color: $quaternary-content; -} diff --git a/res/css/voice-broadcast/atoms/_VoiceBroadcastControl.pcss b/res/css/voice-broadcast/atoms/_VoiceBroadcastControl.pcss deleted file mode 100644 index 5bd7bfe098..0000000000 --- a/res/css/voice-broadcast/atoms/_VoiceBroadcastControl.pcss +++ /dev/null @@ -1,28 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -.mx_VoiceBroadcastControl { - align-items: center; - background-color: $background; - border-radius: 50%; - color: $secondary-content; - display: flex; - flex: 0 0 32px; - height: 32px; - justify-content: center; - width: 32px; -} - -.mx_VoiceBroadcastControl-recording { - color: $alert; -} - -.mx_VoiceBroadcastControl-play .mx_Icon { - left: 1px; - position: relative; -} diff --git a/res/css/voice-broadcast/atoms/_VoiceBroadcastHeader.pcss b/res/css/voice-broadcast/atoms/_VoiceBroadcastHeader.pcss deleted file mode 100644 index c5e21233b7..0000000000 --- a/res/css/voice-broadcast/atoms/_VoiceBroadcastHeader.pcss +++ /dev/null @@ -1,60 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -.mx_VoiceBroadcastHeader { - align-items: flex-start; - display: flex; - gap: $spacing-8; - line-height: 20px; - margin-bottom: $spacing-16; - min-width: 0; -} - -.mx_VoiceBroadcastHeader_content { - flex-grow: 1; - min-width: 0; -} - -.mx_VoiceBroadcastHeader_room_wrapper { - align-items: center; - display: flex; - gap: 4px; - justify-content: flex-start; -} - -.mx_VoiceBroadcastHeader_room { - font-size: $font-12px; - font-weight: var(--cpd-font-weight-semibold); - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.mx_VoiceBroadcastHeader_line { - align-items: center; - color: $secondary-content; - font-size: $font-12px; - display: flex; - gap: $spacing-4; - - .mx_Spinner { - flex: 0 0 14px; - padding: 1px; - } - - span { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } -} - -.mx_VoiceBroadcastHeader_mic--clickable { - cursor: pointer; -} diff --git a/res/css/voice-broadcast/atoms/_VoiceBroadcastRecordingConnectionError.pcss b/res/css/voice-broadcast/atoms/_VoiceBroadcastRecordingConnectionError.pcss deleted file mode 100644 index f21c0bb733..0000000000 --- a/res/css/voice-broadcast/atoms/_VoiceBroadcastRecordingConnectionError.pcss +++ /dev/null @@ -1,18 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -.mx_VoiceBroadcastRecordingConnectionError { - align-items: center; - color: $alert; - display: flex; - gap: $spacing-12; - - svg path { - fill: $alert; - } -} diff --git a/res/css/voice-broadcast/atoms/_VoiceBroadcastRoomSubtitle.pcss b/res/css/voice-broadcast/atoms/_VoiceBroadcastRoomSubtitle.pcss deleted file mode 100644 index e0748e7626..0000000000 --- a/res/css/voice-broadcast/atoms/_VoiceBroadcastRoomSubtitle.pcss +++ /dev/null @@ -1,14 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -.mx_RoomTile .mx_RoomTile_titleContainer .mx_RoomTile_subtitle.mx_RoomTile_subtitle--voice-broadcast { - align-items: center; - color: $alert; - display: flex; - gap: $spacing-4; -} diff --git a/res/css/voice-broadcast/molecules/_VoiceBroadcastBody.pcss b/res/css/voice-broadcast/molecules/_VoiceBroadcastBody.pcss deleted file mode 100644 index 45ed0e98f9..0000000000 --- a/res/css/voice-broadcast/molecules/_VoiceBroadcastBody.pcss +++ /dev/null @@ -1,75 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -.mx_VoiceBroadcastBody { - background-color: $quinary-content; - border-radius: 8px; - color: $secondary-content; - display: inline-block; - font-size: $font-12px; - padding: $spacing-12; - width: 271px; - - .mx_Clock { - line-height: 1; - } -} - -.mx_VoiceBroadcastBody--pip { - background-color: $system; - box-shadow: 0 2px 8px 0 #0000004a; -} - -.mx_VoiceBroadcastBody--small { - display: flex; - gap: $spacing-8; - width: 192px; - - .mx_VoiceBroadcastHeader { - margin-bottom: 0; - } - - .mx_VoiceBroadcastControl { - align-self: center; - } - - .mx_LiveBadge { - margin-top: 4px; - } -} - -.mx_VoiceBroadcastBody_divider { - background-color: $quinary-content; - border: 0; - height: 1px; - margin: $spacing-12 0; -} - -.mx_VoiceBroadcastBody_controls { - align-items: center; - display: flex; - gap: $spacing-32; - justify-content: center; - margin-bottom: $spacing-8; -} - -.mx_VoiceBroadcastBody_timerow { - display: flex; - justify-content: space-between; -} - -.mx_AccessibleButton.mx_VoiceBroadcastBody_blockButton { - display: flex; - gap: $spacing-8; -} - -.mx_VoiceBroadcastBody__small-close { - right: 8px; - position: absolute; - top: 8px; -} diff --git a/res/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlX5qhExfHwNJU.woff2 b/res/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlX5qhExfHwNJU.woff2 deleted file mode 100644 index 880f06af78..0000000000 Binary files a/res/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlX5qhExfHwNJU.woff2 and /dev/null differ diff --git a/res/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlZ5qhExfHw.woff2 b/res/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlZ5qhExfHw.woff2 deleted file mode 100644 index 9fe96559d1..0000000000 Binary files a/res/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlZ5qhExfHw.woff2 and /dev/null differ diff --git a/res/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71n5_zaDpwm80E.woff2 b/res/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71n5_zaDpwm80E.woff2 deleted file mode 100644 index cd79590d99..0000000000 Binary files a/res/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71n5_zaDpwm80E.woff2 and /dev/null differ diff --git a/res/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71p5_zaDpwm.woff2 b/res/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71p5_zaDpwm.woff2 deleted file mode 100644 index cf26d38db4..0000000000 Binary files a/res/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71p5_zaDpwm.woff2 and /dev/null differ diff --git a/res/fonts/Inter/Inter-Bold.woff b/res/fonts/Inter/Inter-Bold.woff deleted file mode 100644 index 2ec7ac3d21..0000000000 Binary files a/res/fonts/Inter/Inter-Bold.woff and /dev/null differ diff --git a/res/fonts/Inter/Inter-Bold.woff2 b/res/fonts/Inter/Inter-Bold.woff2 deleted file mode 100644 index 6989c99229..0000000000 Binary files a/res/fonts/Inter/Inter-Bold.woff2 and /dev/null differ diff --git a/res/fonts/Inter/Inter-BoldItalic.woff b/res/fonts/Inter/Inter-BoldItalic.woff deleted file mode 100644 index aa35b79745..0000000000 Binary files a/res/fonts/Inter/Inter-BoldItalic.woff and /dev/null differ diff --git a/res/fonts/Inter/Inter-BoldItalic.woff2 b/res/fonts/Inter/Inter-BoldItalic.woff2 deleted file mode 100644 index 18b4c1ce5e..0000000000 Binary files a/res/fonts/Inter/Inter-BoldItalic.woff2 and /dev/null differ diff --git a/res/fonts/Inter/Inter-Italic.woff b/res/fonts/Inter/Inter-Italic.woff deleted file mode 100644 index 4b765bd592..0000000000 Binary files a/res/fonts/Inter/Inter-Italic.woff and /dev/null differ diff --git a/res/fonts/Inter/Inter-Italic.woff2 b/res/fonts/Inter/Inter-Italic.woff2 deleted file mode 100644 index bd5f255a98..0000000000 Binary files a/res/fonts/Inter/Inter-Italic.woff2 and /dev/null differ diff --git a/res/fonts/Inter/Inter-Medium.woff b/res/fonts/Inter/Inter-Medium.woff deleted file mode 100644 index 7d55f34cca..0000000000 Binary files a/res/fonts/Inter/Inter-Medium.woff and /dev/null differ diff --git a/res/fonts/Inter/Inter-Medium.woff2 b/res/fonts/Inter/Inter-Medium.woff2 deleted file mode 100644 index a916b47fc8..0000000000 Binary files a/res/fonts/Inter/Inter-Medium.woff2 and /dev/null differ diff --git a/res/fonts/Inter/Inter-MediumItalic.woff b/res/fonts/Inter/Inter-MediumItalic.woff deleted file mode 100644 index 422ab0576a..0000000000 Binary files a/res/fonts/Inter/Inter-MediumItalic.woff and /dev/null differ diff --git a/res/fonts/Inter/Inter-MediumItalic.woff2 b/res/fonts/Inter/Inter-MediumItalic.woff2 deleted file mode 100644 index f623924aea..0000000000 Binary files a/res/fonts/Inter/Inter-MediumItalic.woff2 and /dev/null differ diff --git a/res/fonts/Inter/Inter-Regular.woff b/res/fonts/Inter/Inter-Regular.woff deleted file mode 100644 index 7ff51b7d8f..0000000000 Binary files a/res/fonts/Inter/Inter-Regular.woff and /dev/null differ diff --git a/res/fonts/Inter/Inter-Regular.woff2 b/res/fonts/Inter/Inter-Regular.woff2 deleted file mode 100644 index 554aed6612..0000000000 Binary files a/res/fonts/Inter/Inter-Regular.woff2 and /dev/null differ diff --git a/res/fonts/Inter/Inter-SemiBold.woff b/res/fonts/Inter/Inter-SemiBold.woff deleted file mode 100644 index 76e507a515..0000000000 Binary files a/res/fonts/Inter/Inter-SemiBold.woff and /dev/null differ diff --git a/res/fonts/Inter/Inter-SemiBold.woff2 b/res/fonts/Inter/Inter-SemiBold.woff2 deleted file mode 100644 index 9307998993..0000000000 Binary files a/res/fonts/Inter/Inter-SemiBold.woff2 and /dev/null differ diff --git a/res/fonts/Inter/Inter-SemiBoldItalic.woff b/res/fonts/Inter/Inter-SemiBoldItalic.woff deleted file mode 100644 index 382181212d..0000000000 Binary files a/res/fonts/Inter/Inter-SemiBoldItalic.woff and /dev/null differ diff --git a/res/fonts/Inter/Inter-SemiBoldItalic.woff2 b/res/fonts/Inter/Inter-SemiBoldItalic.woff2 deleted file mode 100644 index f19f5505ec..0000000000 Binary files a/res/fonts/Inter/Inter-SemiBoldItalic.woff2 and /dev/null differ diff --git a/res/fonts/Open_Sans/LICENSE.txt b/res/fonts/Open_Sans/LICENSE.txt deleted file mode 100755 index 75b52484ea..0000000000 --- a/res/fonts/Open_Sans/LICENSE.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - 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. diff --git a/res/fonts/Open_Sans/OpenSans-Bold.ttf b/res/fonts/Open_Sans/OpenSans-Bold.ttf deleted file mode 100755 index fd79d43bea..0000000000 Binary files a/res/fonts/Open_Sans/OpenSans-Bold.ttf and /dev/null differ diff --git a/res/fonts/Open_Sans/OpenSans-BoldItalic.ttf b/res/fonts/Open_Sans/OpenSans-BoldItalic.ttf deleted file mode 100755 index 9bc800958a..0000000000 Binary files a/res/fonts/Open_Sans/OpenSans-BoldItalic.ttf and /dev/null differ diff --git a/res/fonts/Open_Sans/OpenSans-Italic.ttf b/res/fonts/Open_Sans/OpenSans-Italic.ttf deleted file mode 100755 index c90da48ff3..0000000000 Binary files a/res/fonts/Open_Sans/OpenSans-Italic.ttf and /dev/null differ diff --git a/res/fonts/Open_Sans/OpenSans-Regular.ttf b/res/fonts/Open_Sans/OpenSans-Regular.ttf deleted file mode 100755 index db433349b7..0000000000 Binary files a/res/fonts/Open_Sans/OpenSans-Regular.ttf and /dev/null differ diff --git a/res/fonts/Open_Sans/OpenSans-Semibold.ttf b/res/fonts/Open_Sans/OpenSans-Semibold.ttf deleted file mode 100755 index 1a7679e394..0000000000 Binary files a/res/fonts/Open_Sans/OpenSans-Semibold.ttf and /dev/null differ diff --git a/res/fonts/Open_Sans/OpenSans-SemiboldItalic.ttf b/res/fonts/Open_Sans/OpenSans-SemiboldItalic.ttf deleted file mode 100755 index 59b6d16b06..0000000000 Binary files a/res/fonts/Open_Sans/OpenSans-SemiboldItalic.ttf and /dev/null differ diff --git a/res/fonts/Twemoji_Mozilla/TwemojiMozilla-sbix.woff2 b/res/fonts/Twemoji_Mozilla/TwemojiMozilla-sbix.woff2 deleted file mode 100644 index 90f444b1a1..0000000000 Binary files a/res/fonts/Twemoji_Mozilla/TwemojiMozilla-sbix.woff2 and /dev/null differ diff --git a/res/themes/dark/css/_dark.pcss b/res/themes/dark/css/_dark.pcss index 8b0673f692..2d3ea2e4f4 100644 --- a/res/themes/dark/css/_dark.pcss +++ b/res/themes/dark/css/_dark.pcss @@ -240,11 +240,6 @@ $location-live-secondary-color: #deddfd; } /* ******************** */ -/* Voice Broadcast */ -/* ******************** */ -$live-badge-color: #ffffff; -/* ******************** */ - /* One-off colors */ /* ******************** */ $progressbar-bg-color: var(--cpd-color-gray-200); diff --git a/res/themes/legacy-dark/css/_legacy-dark.pcss b/res/themes/legacy-dark/css/_legacy-dark.pcss index 45bb1870f1..ea5228b6c7 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.pcss +++ b/res/themes/legacy-dark/css/_legacy-dark.pcss @@ -226,11 +226,6 @@ $location-live-color: #5c56f5; $location-live-secondary-color: #deddfd; /* ******************** */ -/* Voice Broadcast */ -/* ******************** */ -$live-badge-color: #ffffff; -/* ******************** */ - body { color-scheme: dark; } diff --git a/res/themes/legacy-light/css/_legacy-light.pcss b/res/themes/legacy-light/css/_legacy-light.pcss index 76e0eec588..32ca7d3d1a 100644 --- a/res/themes/legacy-light/css/_legacy-light.pcss +++ b/res/themes/legacy-light/css/_legacy-light.pcss @@ -325,11 +325,6 @@ $location-live-color: #5c56f5; $location-live-secondary-color: #deddfd; /* ******************** */ -/* Voice Broadcast */ -/* ******************** */ -$live-badge-color: #ffffff; -/* ******************** */ - body { color-scheme: light; } diff --git a/res/themes/light/css/_fonts.pcss b/res/themes/light/css/_fonts.pcss index 62613fcee5..8044f47b21 100644 --- a/res/themes/light/css/_fonts.pcss +++ b/res/themes/light/css/_fonts.pcss @@ -1,145 +1,20 @@ /* the 'src' links are relative to the bundle.css, which is in a subdirectory. */ -/* Inter unexpectedly contains various codepoints which collide with emoji, even - when variation-16 is applied to request the emoji variant. From eyeballing - the emoji picker, these are: 20e3, 23cf, 24c2, 25a0-25c1, 2665, 2764, 2b06, 2b1c. - Therefore we define a unicode-range to load which excludes the glyphs - (to avoid having to maintain a fork of Inter). */ - -$inter-unicode-range: U+0000-20e2, U+20e4-23ce, U+23d0-24c1, U+24c3-259f, U+25c2-2664, U+2666-2763, U+2765-2b05, - U+2b07-2b1b, U+2b1d-10FFFF; - +/* Twemoji COLR */ @font-face { - font-family: "Inter"; - font-style: normal; + font-family: "Twemoji"; font-weight: 400; - font-display: swap; - unicode-range: $inter-unicode-range; - src: - url("$(res)/fonts/Inter/Inter-Regular.woff2?v=3.18") format("woff2"), - url("$(res)/fonts/Inter/Inter-Regular.woff?v=3.18") format("woff"); + src: url("$(res)/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2") format("woff2"); } +/* For at least Chrome on Windows 10, we have to explictly add extra weights for the emoji to appear in bold messages, etc. */ @font-face { - font-family: "Inter"; - font-style: italic; - font-weight: 400; - font-display: swap; - unicode-range: $inter-unicode-range; - src: - url("$(res)/fonts/Inter/Inter-Italic.woff2?v=3.18") format("woff2"), - url("$(res)/fonts/Inter/Inter-Italic.woff?v=3.18") format("woff"); -} - -@font-face { - font-family: "Inter"; - font-style: normal; - font-weight: 500; - font-display: swap; - unicode-range: $inter-unicode-range; - src: - url("$(res)/fonts/Inter/Inter-Medium.woff2?v=3.18") format("woff2"), - url("$(res)/fonts/Inter/Inter-Medium.woff?v=3.18") format("woff"); -} -@font-face { - font-family: "Inter"; - font-style: italic; - font-weight: 500; - font-display: swap; - unicode-range: $inter-unicode-range; - src: - url("$(res)/fonts/Inter/Inter-MediumItalic.woff2?v=3.18") format("woff2"), - url("$(res)/fonts/Inter/Inter-MediumItalic.woff?v=3.18") format("woff"); -} - -@font-face { - font-family: "Inter"; - font-style: normal; + font-family: "Twemoji"; font-weight: 600; - font-display: swap; - unicode-range: $inter-unicode-range; - src: - url("$(res)/fonts/Inter/Inter-SemiBold.woff2?v=3.18") format("woff2"), - url("$(res)/fonts/Inter/Inter-SemiBold.woff?v=3.18") format("woff"); + src: url("$(res)/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2") format("woff2"); } @font-face { - font-family: "Inter"; - font-style: italic; - font-weight: 600; - font-display: swap; - unicode-range: $inter-unicode-range; - src: - url("$(res)/fonts/Inter/Inter-SemiBoldItalic.woff2?v=3.18") format("woff2"), - url("$(res)/fonts/Inter/Inter-SemiBoldItalic.woff?v=3.18") format("woff"); -} - -@font-face { - font-family: "Inter"; - font-style: normal; + font-family: "Twemoji"; font-weight: 700; - font-display: swap; - unicode-range: $inter-unicode-range; - src: - url("$(res)/fonts/Inter/Inter-Bold.woff2?v=3.18") format("woff2"), - url("$(res)/fonts/Inter/Inter-Bold.woff?v=3.18") format("woff"); -} -@font-face { - font-family: "Inter"; - font-style: italic; - font-weight: 700; - font-display: swap; - unicode-range: $inter-unicode-range; - src: - url("$(res)/fonts/Inter/Inter-BoldItalic.woff2?v=3.18") format("woff2"), - url("$(res)/fonts/Inter/Inter-BoldItalic.woff?v=3.18") format("woff"); -} - -/* latin-ext */ -@font-face { - font-family: "Inconsolata"; - font-style: normal; - font-weight: 400; - src: - local("Inconsolata Regular"), - local("Inconsolata-Regular"), - url("$(res)/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlX5qhExfHwNJU.woff2") format("woff2"); - unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; -} -/* latin */ -@font-face { - font-family: "Inconsolata"; - font-style: normal; - font-weight: 400; - font-display: swap; - src: - local("Inconsolata Regular"), - local("Inconsolata-Regular"), - url("$(res)/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlZ5qhExfHw.woff2") format("woff2"); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, - U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} -/* latin-ext */ -@font-face { - font-family: "Inconsolata"; - font-style: normal; - font-weight: 700; - font-display: swap; - src: - local("Inconsolata Bold"), - local("Inconsolata-Bold"), - url("$(res)/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71n5_zaDpwm80E.woff2") format("woff2"); - unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; -} -/* latin */ -@font-face { - font-family: "Inconsolata"; - font-style: normal; - font-weight: 700; - font-display: swap; - src: - local("Inconsolata Bold"), - local("Inconsolata-Bold"), - url("$(res)/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71p5_zaDpwm.woff2") format("woff2"); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, - U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; + src: url("$(res)/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2") format("woff2"); } diff --git a/res/themes/light/css/_light.pcss b/res/themes/light/css/_light.pcss index 5f278c6f16..1a1705a9c1 100644 --- a/res/themes/light/css/_light.pcss +++ b/res/themes/light/css/_light.pcss @@ -10,8 +10,8 @@ /* Noto Color Emoji contains digits, in fixed-width, therefore causing digits in flowed text to stand out. TODO: Consider putting all emoji fonts to the end rather than the front. */ -$font-family: "Inter", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Arial", "Helvetica", sans-serif, - "Noto Color Emoji"; +$font-family: "Inter", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Arial", "Helvetica", + sans-serif, "Noto Color Emoji"; $monospace-font-family: "Inconsolata", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Courier", monospace, "Noto Color Emoji"; @@ -355,11 +355,6 @@ $location-live-color: var(--cpd-color-purple-900); $location-live-secondary-color: var(--cpd-color-purple-600); /* ******************** */ -/* Voice Broadcast */ -/* ******************** */ -$live-badge-color: var(--cpd-color-icon-on-solid-primary); -/* ******************** */ - body { color-scheme: light; } diff --git a/scripts/copy-i18n.py b/scripts/copy-i18n.py deleted file mode 100755 index 07b1271239..0000000000 --- a/scripts/copy-i18n.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python - -import json -import sys -import os - -if len(sys.argv) < 3: - print "Usage: %s <source> <dest>" % (sys.argv[0],) - print "eg. %s pt_BR.json pt.json" % (sys.argv[0],) - print - print "Adds any translations to <dest> that exist in <source> but not <dest>" - sys.exit(1) - -srcpath = sys.argv[1] -dstpath = sys.argv[2] -tmppath = dstpath + ".tmp" - -with open(srcpath) as f: - src = json.load(f) - -with open(dstpath) as f: - dst = json.load(f) - -toAdd = {} -for k,v in src.iteritems(): - if k not in dst: - print "Adding %s" % (k,) - toAdd[k] = v - -# don't just json.dumps as we'll probably re-order all the keys (and they're -# not in any given order so we can't just sort_keys). Append them to the end. -with open(dstpath) as ifp: - with open(tmppath, 'w') as ofp: - for line in ifp: - strippedline = line.strip() - if strippedline in ('{', '}'): - ofp.write(line) - elif strippedline.endswith(','): - ofp.write(line) - else: - ofp.write(' '+strippedline+',') - toAddStr = json.dumps(toAdd, indent=4, separators=(',', ': '), ensure_ascii=False, encoding="utf8").strip("{}\n") - ofp.write("\n") - ofp.write(toAddStr.encode('utf8')) - ofp.write("\n") - -os.rename(tmppath, dstpath) diff --git a/scripts/fetch-develop.deps.sh b/scripts/fetch-develop.deps.sh deleted file mode 100755 index 5814b43ff7..0000000000 --- a/scripts/fetch-develop.deps.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env bash - -# Fetches the js-sdk dependency for development or testing purposes -# If there exists a branch of that dependency with the same name as -# the branch the current checkout is on, use that branch. Otherwise, -# use develop. - -set -x - -GIT_CLONE_ARGS=("$@") -[ -z "$defbranch" ] && defbranch="develop" - -# clone a specific branch of a github repo -function clone() { - org=$1 - repo=$2 - branch=$3 - - # Chop 'origin' off the start as jenkins ends up using - # branches on the origin, but this doesn't work if we - # specify the branch when cloning. - branch=${branch#origin/} - - if [ -n "$branch" ] - then - echo "Trying to use $org/$repo#$branch" - # Disable auth prompts: https://serverfault.com/a/665959 - GIT_TERMINAL_PROMPT=0 git clone https://github.com/$org/$repo.git $repo --branch $branch \ - "${GIT_CLONE_ARGS[@]}" - return $? - fi - return 1 -} - -function dodep() { - deforg=$1 - defrepo=$2 - rm -rf $defrepo - - # Try the PR author's branch in case it exists on the deps as well. - # Try the target branch of the push or PR. - # Use the default branch as the last resort. - if [[ "$BUILDKITE" == true ]]; then - # If BUILDKITE_BRANCH is set, it will contain either: - # * "branch" when the author's branch and target branch are in the same repo - # * "author:branch" when the author's branch is in their fork - # We can split on `:` into an array to check. - BUILDKITE_BRANCH_ARRAY=(${BUILDKITE_BRANCH//:/ }) - if [[ "${#BUILDKITE_BRANCH_ARRAY[@]}" == "2" ]]; then - prAuthor=${BUILDKITE_BRANCH_ARRAY[0]} - prBranch=${BUILDKITE_BRANCH_ARRAY[1]} - else - prAuthor=$deforg - prBranch=$BUILDKITE_BRANCH - fi - clone $prAuthor $defrepo $prBranch || - clone $deforg $defrepo $BUILDKITE_PULL_REQUEST_BASE_BRANCH || - clone $deforg $defrepo $defbranch || - return $? - else - clone $deforg $defrepo $ghprbSourceBranch || - clone $deforg $defrepo $GIT_BRANCH || - clone $deforg $defrepo `git rev-parse --abbrev-ref HEAD` || - clone $deforg $defrepo $defbranch || - return $? - fi - - echo "$defrepo set to branch "`git -C "$defrepo" rev-parse --abbrev-ref HEAD` -} - -############################## - -echo 'Setting up matrix-js-sdk' - -dodep matrix-org matrix-js-sdk - -pushd matrix-js-sdk -yarn link -yarn install --frozen-lockfile -popd - -yarn link matrix-js-sdk - -############################## diff --git a/scripts/genflags.sh b/scripts/genflags.sh deleted file mode 100755 index aa882a99b4..0000000000 --- a/scripts/genflags.sh +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright 2017-2024 New Vector Ltd. - -# SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -# Please see LICENSE in the repository root for full details. - - -# genflags.sh - Generates pngs for use with CountryDropdown.js -# -# Dependencies: -# - imagemagick --with-rsvg (because default imagemagick SVG -# renderer does not produce accurate results) -# -# on macOS, this is most easily done with: -# brew install imagemagick --with-librsvg -# -# This will clone the googlei18n flag repo before converting -# all phonenumber.js-supported country flags (as SVGs) into -# PNGs that can be used by CountryDropdown.js. - -set -e - -# Allow CTRL+C to terminate the script -trap "echo Exited!; exit;" SIGINT SIGTERM - -# git clone the google repo to get flag SVGs -git clone git@github.com:googlei18n/region-flags -for f in region-flags/svg/*.svg; do - # Skip state flags - if [[ $f =~ [A-Z]{2}-[A-Z]{2,3}.svg ]] ; then - echo "Skipping state flag "$f - continue - fi - - # Skip countries not included in phonenumber.js - if [[ $f =~ (AC|CP|DG|EA|EU|IC|TA|UM|UN|XK).svg ]] ; then - echo "Skipping non-phonenumber supported flag "$f - continue - fi - - # Run imagemagick convert - # -background none : transparent background - # -resize 50x30 : resize the flag to have a height of 15px (2x) - # By default, aspect ratio is respected so the width will - # be correct and not necessarily 25px. - # -filter Lanczos : use sharper resampling to avoid muddiness - # -gravity Center : keep the image central when adding an -extent - # -border 1 : add a 1px border around the flag - # -bordercolor : set the border colour - # -extent 54x54 : surround the image with padding so that it - # has the dimensions 27x27px (2x). - convert $f -background none -filter Lanczos -resize 50x30 \ - -gravity Center -border 1 -bordercolor \#e0e0e0 \ - -extent 54x54 $f.png - - # $f.png will be region-flags/svg/XX.svg.png at this point - - # Extract filename from path $f - newname=${f##*/} - # Replace .svg with .png - newname=${newname%.svg}.png - # Move the file to flags directory - mv $f.png ../res/flags/$newname - echo "Generated res/flags/"$newname -done diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index be36c5b689..f921cd291f 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -44,6 +44,7 @@ import { IConfigOptions } from "../IConfigOptions"; import { MatrixDispatcher } from "../dispatcher/dispatcher"; import { DeepReadonly } from "./common"; import MatrixChat from "../components/structures/MatrixChat"; +import { InitialCryptoSetupStore } from "../stores/InitialCryptoSetupStore"; /* eslint-disable @typescript-eslint/naming-convention */ @@ -117,6 +118,7 @@ declare global { mxPerformanceEntryNames: any; mxUIStore: UIStore; mxSetupEncryptionStore?: SetupEncryptionStore; + mxInitialCryptoStore?: InitialCryptoSetupStore; mxRoomScrollStateStore?: RoomScrollStateStore; mxActiveWidgetStore?: ActiveWidgetStore; mxOnRecaptchaLoaded?: () => void; diff --git a/src/@types/matrix-js-sdk.d.ts b/src/@types/matrix-js-sdk.d.ts index dcef9c2eb9..41ccfcbb3b 100644 --- a/src/@types/matrix-js-sdk.d.ts +++ b/src/@types/matrix-js-sdk.d.ts @@ -10,7 +10,6 @@ import type { IWidget } from "matrix-widget-api"; import type { BLURHASH_FIELD } from "../utils/image-media"; import type { JitsiCallMemberEventType, JitsiCallMemberContent } from "../call-types"; import type { ILayoutStateEvent, WIDGET_LAYOUT_EVENT_TYPE } from "../stores/widgets/types"; -import type { VoiceBroadcastInfoEventContent, VoiceBroadcastInfoEventType } from "../voice-broadcast/types"; import type { EncryptedFile } from "matrix-js-sdk/src/types"; // Extend Matrix JS SDK types via Typescript declaration merging to support unspecced event fields and types @@ -22,6 +21,13 @@ declare module "matrix-js-sdk/src/types" { [BLURHASH_FIELD]?: string; } + export interface ImageInfo { + /** + * @see https://github.com/matrix-org/matrix-spec-proposals/pull/4230 + */ + "org.matrix.msc4230.is_animated"?: boolean; + } + export interface StateEvents { // Jitsi-backed video room state events [JitsiCallMemberEventType]: JitsiCallMemberContent; @@ -30,9 +36,6 @@ declare module "matrix-js-sdk/src/types" { "im.vector.modular.widgets": IWidget | {}; [WIDGET_LAYOUT_EVENT_TYPE]: ILayoutStateEvent; - // Unstable voice broadcast state events - [VoiceBroadcastInfoEventType]: VoiceBroadcastInfoEventContent; - // Element custom state events "im.vector.web.settings": Record<string, any>; "org.matrix.room.preview_urls": { disable: boolean }; @@ -71,7 +74,5 @@ declare module "matrix-js-sdk/src/types" { waveform?: number[]; }; "org.matrix.msc3245.voice"?: {}; - - "io.element.voice_broadcast_chunk"?: { sequence: number }; } } diff --git a/src/@types/png-chunks-extract.d.ts b/src/@types/png-chunks-extract.d.ts index 38ea84c32a..c767b40655 100644 --- a/src/@types/png-chunks-extract.d.ts +++ b/src/@types/png-chunks-extract.d.ts @@ -12,7 +12,7 @@ declare module "png-chunks-extract" { data: Uint8Array; } - function extractPngChunks(data: Uint8Array | Buffer): IChunk[]; + function extractPngChunks(data: Uint8Array): IChunk[]; export default extractPngChunks; } diff --git a/src/ContentMessages.ts b/src/ContentMessages.ts index 895e168f3b..344a2f112c 100644 --- a/src/ContentMessages.ts +++ b/src/ContentMessages.ts @@ -56,6 +56,7 @@ import { createThumbnail } from "./utils/image-media"; import { attachMentions, attachRelation } from "./components/views/rooms/SendMessageComposer"; import { doMaybeLocalRoomAction } from "./utils/local-room"; import { SdkContextClass } from "./contexts/SDKContext"; +import { blobIsAnimated } from "./utils/Image.ts"; // scraped out of a macOS hidpi (5660ppm) screenshot png // 5669 px (x-axis) , 5669 px (y-axis) , per metre @@ -150,15 +151,20 @@ async function infoForImageFile(matrixClient: MatrixClient, roomId: string, imag thumbnailType = "image/jpeg"; } + // We don't await this immediately so it can happen in the background + const isAnimatedPromise = blobIsAnimated(imageFile.type, imageFile); + const imageElement = await loadImageElement(imageFile); const result = await createThumbnail(imageElement.img, imageElement.width, imageElement.height, thumbnailType); const imageInfo = result.info; + imageInfo["org.matrix.msc4230.is_animated"] = await isAnimatedPromise; + // For lesser supported image types, always include the thumbnail even if it is larger if (!ALWAYS_INCLUDE_THUMBNAIL.includes(imageFile.type)) { // we do all sizing checks here because we still rely on thumbnail generation for making a blurhash from. - const sizeDifference = imageFile.size - imageInfo.thumbnail_info!.size; + const sizeDifference = imageFile.size - imageInfo.thumbnail_info!.size!; if ( // image is small enough already imageFile.size <= IMAGE_SIZE_THRESHOLD_THUMBNAIL || diff --git a/src/DecryptionFailureTracker.ts b/src/DecryptionFailureTracker.ts index 1e07ba252b..bfcd126eea 100644 --- a/src/DecryptionFailureTracker.ts +++ b/src/DecryptionFailureTracker.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { ScalableBloomFilter } from "bloom-filters"; +import ScalableBloomFilter from "bloom-filters/dist/bloom/scalable-bloom-filter"; import { HttpApiEvent, MatrixClient, MatrixEventEvent, MatrixEvent } from "matrix-js-sdk/src/matrix"; import { Error as ErrorEvent } from "@matrix-org/analytics-events/types/typescript/Error"; import { DecryptionFailureCode, CryptoEvent } from "matrix-js-sdk/src/crypto-api"; diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index 4f47cd7eac..84d83827da 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -230,12 +230,15 @@ export default class DeviceListener { private async getKeyBackupInfo(): Promise<KeyBackupInfo | null> { if (!this.client) return null; const now = new Date().getTime(); + const crypto = this.client.getCrypto(); + if (!crypto) return null; + if ( !this.keyBackupInfo || !this.keyBackupFetchedAt || this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL ) { - this.keyBackupInfo = await this.client.getKeyBackupVersion(); + this.keyBackupInfo = await crypto.getKeyBackupInfo(); this.keyBackupFetchedAt = now; } return this.keyBackupInfo; diff --git a/src/IConfigOptions.ts b/src/IConfigOptions.ts index 72bee5d0ab..5dd500402d 100644 --- a/src/IConfigOptions.ts +++ b/src/IConfigOptions.ts @@ -175,13 +175,6 @@ export interface IConfigOptions { sync_timeline_limit?: number; dangerously_allow_unsafe_and_insecure_passwords?: boolean; // developer option - voice_broadcast?: { - // length per voice chunk in seconds - chunk_length?: number; - // max voice broadcast length in seconds - max_length?: number; - }; - user_notice?: { title: string; description: string; diff --git a/src/LegacyCallHandler.tsx b/src/LegacyCallHandler.tsx index a06480e9cd..b804ca0084 100644 --- a/src/LegacyCallHandler.tsx +++ b/src/LegacyCallHandler.tsx @@ -55,8 +55,6 @@ import { OpenInviteDialogPayload } from "./dispatcher/payloads/OpenInviteDialogP import { findDMForUser } from "./utils/dm/findDMForUser"; import { getJoinedNonFunctionalMembers } from "./utils/room/getJoinedNonFunctionalMembers"; import { localNotificationsAreSilenced } from "./utils/notifications"; -import { SdkContextClass } from "./contexts/SDKContext"; -import { showCantStartACallDialog } from "./voice-broadcast/utils/showCantStartACallDialog"; import { isNotNull } from "./Typeguards"; import { BackgroundAudio } from "./audio/BackgroundAudio"; import { Jitsi } from "./widgets/Jitsi.ts"; @@ -859,15 +857,6 @@ export default class LegacyCallHandler extends EventEmitter { return; } - // Pause current broadcast, if any - SdkContextClass.instance.voiceBroadcastPlaybacksStore.getCurrent()?.pause(); - - if (SdkContextClass.instance.voiceBroadcastRecordingsStore.getCurrent()) { - // Do not start a call, if recording a broadcast - showCantStartACallDialog(); - return; - } - // We might be using managed hybrid widgets if (isManagedHybridWidgetEnabled(room)) { await addManagedHybridWidget(room); diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 72b1115033..52a3abe82d 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -35,13 +35,11 @@ import IdentityAuthClient from "./IdentityAuthClient"; import { crossSigningCallbacks } from "./SecurityManager"; import { SlidingSyncManager } from "./SlidingSyncManager"; import { _t, UserFriendlyError } from "./languageHandler"; -import { SettingLevel } from "./settings/SettingLevel"; import MatrixClientBackedController from "./settings/controllers/MatrixClientBackedController"; import ErrorDialog from "./components/views/dialogs/ErrorDialog"; import PlatformPeg from "./PlatformPeg"; import { formatList } from "./utils/FormattingUtils"; import SdkConfig from "./SdkConfig"; -import { Features } from "./settings/Settings"; import { setDeviceIsolationMode } from "./settings/controllers/DeviceIsolationModeController.ts"; import SlidingSyncController from "./settings/controllers/SlidingSyncController"; @@ -340,11 +338,6 @@ class MatrixClientPegClass implements IMatrixClientPeg { logger.error("Warning! Not using an encryption key for rust crypto store."); } - // Record the fact that we used the Rust crypto stack with this client. This just guards against people - // rolling back to versions of EW that did not default to Rust crypto (which would lead to an error, since - // we cannot migrate from Rust to Legacy crypto). - await SettingsStore.setValue(Features.RustCrypto, null, SettingLevel.DEVICE, true); - await this.matrixClient.initRustCrypto({ storageKey: rustCryptoStoreKey, storagePassword: rustCryptoStorePassword, diff --git a/src/Notifier.ts b/src/Notifier.ts index 961d2171a8..c724c4780c 100644 --- a/src/Notifier.ts +++ b/src/Notifier.ts @@ -49,8 +49,6 @@ import { SdkContextClass } from "./contexts/SDKContext"; import { localNotificationsAreSilenced, createLocalNotificationSettingsIfNeeded } from "./utils/notifications"; import { getIncomingCallToastKey, IncomingCallToast } from "./toasts/IncomingCallToast"; import ToastStore from "./stores/ToastStore"; -import { VoiceBroadcastChunkEventType, VoiceBroadcastInfoEventType } from "./voice-broadcast"; -import { getSenderName } from "./utils/event/getSenderName"; import { stripPlainReply } from "./utils/Reply"; import { BackgroundAudio } from "./audio/BackgroundAudio"; @@ -81,17 +79,6 @@ const msgTypeHandlers: Record<string, (event: MatrixEvent) => string | null> = { return TextForEvent.textForLocationEvent(event)(); }, [MsgType.Audio]: (event: MatrixEvent): string | null => { - if (event.getContent()?.[VoiceBroadcastChunkEventType]) { - if (event.getContent()?.[VoiceBroadcastChunkEventType]?.sequence === 1) { - // Show a notification for the first broadcast chunk. - // At this point a user received something to listen to. - return _t("notifier|io.element.voice_broadcast_chunk", { senderName: getSenderName(event) }); - } - - // Mute other broadcast chunks - return null; - } - return TextForEvent.textForEvent(event, MatrixClientPeg.safeGet()); }, }; @@ -460,8 +447,6 @@ class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents // XXX: exported for tests public evaluateEvent(ev: MatrixEvent): void { - // Mute notifications for broadcast info events - if (ev.getType() === VoiceBroadcastInfoEventType) return; let roomId = ev.getRoomId()!; if (LegacyCallHandler.instance.getSupportsVirtualRooms()) { // Attempt to translate a virtual room to a native one @@ -513,7 +498,8 @@ class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents const thisUserHasConnectedDevice = room && MatrixRTCSession.callMembershipsForRoom(room).some((m) => m.sender === cli.getUserId()); - if (EventType.CallNotify === ev.getType() && (ev.getAge() ?? 0) < 10000 && !thisUserHasConnectedDevice) { + // Check maximum age (<= 15 seconds) of a call notify event that will trigger a ringing notification + if (EventType.CallNotify === ev.getType() && (ev.getAge() ?? 0) < 15000 && !thisUserHasConnectedDevice) { const content = ev.getContent(); const roomId = ev.getRoomId(); if (typeof content.call_id !== "string") { diff --git a/src/SdkConfig.ts b/src/SdkConfig.ts index cbec03381a..c9a0010b0f 100644 --- a/src/SdkConfig.ts +++ b/src/SdkConfig.ts @@ -46,10 +46,6 @@ export const DEFAULTS: DeepReadonly<IConfigOptions> = { logo: require("../res/img/element-desktop-logo.svg").default, url: "https://element.io/get-started", }, - voice_broadcast: { - chunk_length: 2 * 60, // two minutes - max_length: 4 * 60 * 60, // four hours - }, feedback: { existing_issues_url: diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index f97dff786f..e8122b2dbf 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -7,8 +7,8 @@ Please see LICENSE files in the repository root for full details. */ import { lazy } from "react"; -import { ICryptoCallbacks, SecretStorage } from "matrix-js-sdk/src/matrix"; -import { deriveRecoveryKeyFromPassphrase, decodeRecoveryKey } from "matrix-js-sdk/src/crypto-api"; +import { SecretStorage } from "matrix-js-sdk/src/matrix"; +import { deriveRecoveryKeyFromPassphrase, decodeRecoveryKey, CryptoCallbacks } from "matrix-js-sdk/src/crypto-api"; import { logger } from "matrix-js-sdk/src/logger"; import Modal from "./Modal"; @@ -159,7 +159,7 @@ function cacheSecretStorageKey( } } -export const crossSigningCallbacks: ICryptoCallbacks = { +export const crossSigningCallbacks: CryptoCallbacks = { getSecretStorageKey, cacheSecretStorageKey, }; diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 5fc4d8c357..b739cec12f 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -49,7 +49,6 @@ import VoipUserMapper from "./VoipUserMapper"; import { htmlSerializeFromMdIfNeeded } from "./editor/serialize"; import { leaveRoomBehaviour } from "./utils/leave-behaviour"; import { MatrixClientPeg } from "./MatrixClientPeg"; -import { getDeviceCryptoInfo } from "./utils/crypto/deviceInfo"; import { isCurrentLocalRoom, reject, singleMxcUpload, success, successSync } from "./slash-commands/utils"; import { deop, op } from "./slash-commands/op"; import { CommandCategories } from "./slash-commands/interface"; @@ -658,69 +657,6 @@ export const Commands = [ category: CommandCategories.admin, renderingTypes: [TimelineRenderingType.Room], }), - new Command({ - command: "verify", - args: "<user-id> <device-id> <device-signing-key>", - description: _td("slash_command|verify"), - runFn: function (cli, roomId, threadId, args) { - if (args) { - const matches = args.match(/^(\S+) +(\S+) +(\S+)$/); - if (matches) { - const userId = matches[1]; - const deviceId = matches[2]; - const fingerprint = matches[3]; - - return success( - (async (): Promise<void> => { - const device = await getDeviceCryptoInfo(cli, userId, deviceId); - if (!device) { - throw new UserFriendlyError("slash_command|verify_unknown_pair", { - userId, - deviceId, - cause: undefined, - }); - } - const deviceTrust = await cli.getCrypto()?.getDeviceVerificationStatus(userId, deviceId); - - if (deviceTrust?.isVerified()) { - if (device.getFingerprint() === fingerprint) { - throw new UserFriendlyError("slash_command|verify_nop"); - } else { - throw new UserFriendlyError("slash_command|verify_nop_warning_mismatch"); - } - } - - if (device.getFingerprint() !== fingerprint) { - const fprint = device.getFingerprint(); - throw new UserFriendlyError("slash_command|verify_mismatch", { - fprint, - userId, - deviceId, - fingerprint, - cause: undefined, - }); - } - - await cli.setDeviceVerified(userId, deviceId, true); - - // Tell the user we verified everything - Modal.createDialog(InfoDialog, { - title: _t("slash_command|verify_success_title"), - description: ( - <div> - <p>{_t("slash_command|verify_success_description", { userId, deviceId })}</p> - </div> - ), - }); - })(), - ); - } - } - return reject(this.getUsage()); - }, - category: CommandCategories.advanced, - renderingTypes: [TimelineRenderingType.Room], - }), new Command({ command: "discardsession", description: _td("slash_command|discardsession"), diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx index 1ffae62aea..49d8b739b7 100644 --- a/src/TextForEvent.tsx +++ b/src/TextForEvent.tsx @@ -36,7 +36,6 @@ import AccessibleButton from "./components/views/elements/AccessibleButton"; import RightPanelStore from "./stores/right-panel/RightPanelStore"; import { highlightEvent, isLocationEvent } from "./utils/EventUtils"; import { ElementCall } from "./models/Call"; -import { textForVoiceBroadcastStoppedEvent, VoiceBroadcastInfoEventType } from "./voice-broadcast"; import { getSenderName } from "./utils/event/getSenderName"; import PosthogTrackers from "./PosthogTrackers.ts"; @@ -906,7 +905,6 @@ const stateHandlers: IHandlers = { // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) "im.vector.modular.widgets": textForWidgetEvent, [WIDGET_LAYOUT_EVENT_TYPE]: textForWidgetLayoutEvent, - [VoiceBroadcastInfoEventType]: textForVoiceBroadcastStoppedEvent, }; // Add all the Mjolnir stuff to the renderer diff --git a/src/accessibility/context_menu/ContextMenuButton.tsx b/src/accessibility/context_menu/ContextMenuButton.tsx index d8c7d912c1..9d8b5585e3 100644 --- a/src/accessibility/context_menu/ContextMenuButton.tsx +++ b/src/accessibility/context_menu/ContextMenuButton.tsx @@ -8,25 +8,24 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { ComponentProps, forwardRef, Ref } from "react"; +import React, { forwardRef, Ref } from "react"; -import AccessibleButton from "../../components/views/elements/AccessibleButton"; +import AccessibleButton, { ButtonProps } from "../../components/views/elements/AccessibleButton"; -type Props<T extends keyof JSX.IntrinsicElements> = ComponentProps<typeof AccessibleButton<T>> & { +type Props<T extends keyof HTMLElementTagNameMap> = ButtonProps<T> & { label?: string; // whether the context menu is currently open isExpanded: boolean; }; // Semantic component for representing the AccessibleButton which launches a <ContextMenu /> -export const ContextMenuButton = forwardRef(function <T extends keyof JSX.IntrinsicElements>( - { label, isExpanded, children, onClick, onContextMenu, element, ...props }: Props<T>, - ref: Ref<HTMLElement>, +export const ContextMenuButton = forwardRef(function <T extends keyof HTMLElementTagNameMap>( + { label, isExpanded, children, onClick, onContextMenu, ...props }: Props<T>, + ref: Ref<HTMLElementTagNameMap[T]>, ) { return ( <AccessibleButton {...props} - element={element as keyof JSX.IntrinsicElements} onClick={onClick} onContextMenu={onContextMenu ?? onClick ?? undefined} aria-label={label} diff --git a/src/accessibility/context_menu/ContextMenuTooltipButton.tsx b/src/accessibility/context_menu/ContextMenuTooltipButton.tsx index 11437866bd..8002368083 100644 --- a/src/accessibility/context_menu/ContextMenuTooltipButton.tsx +++ b/src/accessibility/context_menu/ContextMenuTooltipButton.tsx @@ -8,24 +8,23 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { ComponentProps, forwardRef, Ref } from "react"; +import React, { forwardRef, Ref } from "react"; -import AccessibleButton from "../../components/views/elements/AccessibleButton"; +import AccessibleButton, { ButtonProps } from "../../components/views/elements/AccessibleButton"; -type Props<T extends keyof JSX.IntrinsicElements> = ComponentProps<typeof AccessibleButton<T>> & { +type Props<T extends keyof HTMLElementTagNameMap> = ButtonProps<T> & { // whether the context menu is currently open isExpanded: boolean; }; // Semantic component for representing the AccessibleButton which launches a <ContextMenu /> -export const ContextMenuTooltipButton = forwardRef(function <T extends keyof JSX.IntrinsicElements>( - { isExpanded, children, onClick, onContextMenu, element, ...props }: Props<T>, - ref: Ref<HTMLElement>, +export const ContextMenuTooltipButton = forwardRef(function <T extends keyof HTMLElementTagNameMap>( + { isExpanded, children, onClick, onContextMenu, ...props }: Props<T>, + ref: Ref<HTMLElementTagNameMap[T]>, ) { return ( <AccessibleButton {...props} - element={element as keyof JSX.IntrinsicElements} onClick={onClick} onContextMenu={onContextMenu ?? onClick ?? undefined} aria-haspopup={true} diff --git a/src/accessibility/roving/RovingAccessibleButton.tsx b/src/accessibility/roving/RovingAccessibleButton.tsx index f50fb010ae..b53221e1c9 100644 --- a/src/accessibility/roving/RovingAccessibleButton.tsx +++ b/src/accessibility/roving/RovingAccessibleButton.tsx @@ -6,39 +6,33 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { ComponentProps } from "react"; +import React, { RefObject } from "react"; -import AccessibleButton from "../../components/views/elements/AccessibleButton"; +import AccessibleButton, { ButtonProps } from "../../components/views/elements/AccessibleButton"; import { useRovingTabIndex } from "../RovingTabIndex"; -import { Ref } from "./types"; -type Props<T extends keyof JSX.IntrinsicElements> = Omit< - ComponentProps<typeof AccessibleButton<T>>, - "inputRef" | "tabIndex" -> & { - inputRef?: Ref; +type Props<T extends keyof HTMLElementTagNameMap> = Omit<ButtonProps<T>, "tabIndex"> & { + inputRef?: RefObject<HTMLElementTagNameMap[T]>; focusOnMouseOver?: boolean; }; // Wrapper to allow use of useRovingTabIndex for simple AccessibleButtons outside of React Functional Components. -export const RovingAccessibleButton = <T extends keyof JSX.IntrinsicElements>({ +export const RovingAccessibleButton = <T extends keyof HTMLElementTagNameMap>({ inputRef, onFocus, onMouseOver, focusOnMouseOver, - element, ...props }: Props<T>): JSX.Element => { - const [onFocusInternal, isActive, ref] = useRovingTabIndex(inputRef); + const [onFocusInternal, isActive, ref] = useRovingTabIndex<HTMLElementTagNameMap[T]>(inputRef); return ( <AccessibleButton {...props} - element={element as keyof JSX.IntrinsicElements} - onFocus={(event: React.FocusEvent) => { + onFocus={(event: React.FocusEvent<never, never>) => { onFocusInternal(); onFocus?.(event); }} - onMouseOver={(event: React.MouseEvent) => { + onMouseOver={(event: React.MouseEvent<never, never>) => { if (focusOnMouseOver) onFocusInternal(); onMouseOver?.(event); }} diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx index 1258bde2ca..932f6d7fcf 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx @@ -279,7 +279,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp if (!forceReset) { try { this.setState({ phase: Phase.Loading }); - backupInfo = await cli.getKeyBackupVersion(); + backupInfo = await crypto.getKeyBackupInfo(); } catch (e) { logger.error("Error fetching backup data from server", e); this.setState({ phase: Phase.LoadError }); diff --git a/src/components/structures/EmbeddedPage.tsx b/src/components/structures/EmbeddedPage.tsx index 5c7e81caf5..c471565d91 100644 --- a/src/components/structures/EmbeddedPage.tsx +++ b/src/components/structures/EmbeddedPage.tsx @@ -36,7 +36,7 @@ interface IState { export default class EmbeddedPage extends React.PureComponent<IProps, IState> { public static contextType = MatrixClientContext; - public declare context: React.ContextType<typeof MatrixClientContext>; + declare public context: React.ContextType<typeof MatrixClientContext>; private unmounted = false; private dispatcherRef?: string; diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx index 74a91d8cbc..4c580cb9fe 100644 --- a/src/components/structures/FilePanel.tsx +++ b/src/components/structures/FilePanel.tsx @@ -34,6 +34,7 @@ import { Layout } from "../../settings/enums/Layout"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import Measured from "../views/elements/Measured"; import EmptyState from "../views/right_panel/EmptyState"; +import { ScopedRoomContextProvider } from "../../contexts/ScopedRoomContext.tsx"; interface IProps { roomId: string; @@ -51,7 +52,7 @@ interface IState { */ class FilePanel extends React.Component<IProps, IState> { public static contextType = RoomContext; - public declare context: React.ContextType<typeof RoomContext>; + declare public context: React.ContextType<typeof RoomContext>; // This is used to track if a decrypted event was a live event and should be // added to the timeline. @@ -104,7 +105,11 @@ class FilePanel extends React.Component<IProps, IState> { } if (!this.state.timelineSet.eventIdToTimeline(ev.getId()!)) { - this.state.timelineSet.addEventToTimeline(ev, timeline, false); + this.state.timelineSet.addEventToTimeline(ev, timeline, { + fromCache: false, + addToState: false, + toStartOfTimeline: false, + }); } } @@ -269,12 +274,10 @@ class FilePanel extends React.Component<IProps, IState> { if (this.state.timelineSet) { return ( - <RoomContext.Provider - value={{ - ...this.context, - timelineRenderingType: TimelineRenderingType.File, - narrow: this.state.narrow, - }} + <ScopedRoomContextProvider + {...this.context} + timelineRenderingType={TimelineRenderingType.File} + narrow={this.state.narrow} > <BaseCard className="mx_FilePanel" @@ -298,16 +301,11 @@ class FilePanel extends React.Component<IProps, IState> { layout={Layout.Group} /> </BaseCard> - </RoomContext.Provider> + </ScopedRoomContextProvider> ); } else { return ( - <RoomContext.Provider - value={{ - ...this.context, - timelineRenderingType: TimelineRenderingType.File, - }} - > + <ScopedRoomContextProvider {...this.context} timelineRenderingType={TimelineRenderingType.File}> <BaseCard className="mx_FilePanel" onClose={this.props.onClose} @@ -315,7 +313,7 @@ class FilePanel extends React.Component<IProps, IState> { > <Spinner /> </BaseCard> - </RoomContext.Provider> + </ScopedRoomContextProvider> ); } } diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 0042169f45..019f9cd1a8 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -23,7 +23,6 @@ import classNames from "classnames"; import { isOnlyCtrlOrCmdKeyEvent, Key } from "../../Keyboard"; import PageTypes from "../../PageTypes"; import MediaDeviceHandler from "../../MediaDeviceHandler"; -import { fixupColorFonts } from "../../utils/FontManager"; import dis from "../../dispatcher/dispatcher"; import { IMatrixClientCreds } from "../../MatrixClientPeg"; import SettingsStore from "../../settings/SettingsStore"; @@ -149,8 +148,6 @@ class LoggedInView extends React.Component<IProps, IState> { MediaDeviceHandler.loadDevices(); - fixupColorFonts(); - this._roomView = React.createRef(); this._resizeContainer = React.createRef(); this.resizeHandler = React.createRef(); diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index e51dd96647..ee120c430a 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -119,7 +119,6 @@ import { ValidatedServerConfig } from "../../utils/ValidatedServerConfig"; import { isLocalRoom } from "../../utils/localRoom/isLocalRoom"; import { SDKContext, SdkContextClass } from "../../contexts/SDKContext"; import { viewUserDeviceSettings } from "../../actions/handlers/viewUserDeviceSettings"; -import { cleanUpBroadcasts, VoiceBroadcastResumer } from "../../voice-broadcast"; import GenericToast from "../views/toasts/GenericToast"; import RovingSpotlightDialog from "../views/dialogs/spotlight/SpotlightDialog"; import { findDMForUser } from "../../utils/dm/findDMForUser"; @@ -133,6 +132,7 @@ import { SessionLockStolenView } from "./auth/SessionLockStolenView"; import { ConfirmSessionLockTheftView } from "./auth/ConfirmSessionLockTheftView"; import { LoginSplashView } from "./auth/LoginSplashView"; import { cleanUpDraftsIfRequired } from "../../DraftCleaner"; +import { InitialCryptoSetupStore } from "../../stores/InitialCryptoSetupStore"; // legacy export export { default as Views } from "../../Views"; @@ -227,7 +227,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> { private focusNext: FocusNextType; private subTitleStatus: string; private prevWindowWidth: number; - private voiceBroadcastResumer?: VoiceBroadcastResumer; private readonly loggedInView = createRef<LoggedInViewType>(); private dispatcherRef?: string; @@ -430,6 +429,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> { !(await shouldSkipSetupEncryption(cli)) ) { // if cross-signing is not yet set up, do so now if possible. + InitialCryptoSetupStore.sharedInstance().startInitialCryptoSetup( + cli, + Boolean(this.tokenLogin), + this.stores, + this.onCompleteSecurityE2eSetupFinished, + ); this.setStateForNewView({ view: Views.E2E_SETUP }); } else { this.onLoggedIn(); @@ -501,7 +506,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> { window.removeEventListener("resize", this.onWindowResized); this.stores.accountPasswordStore.clearPassword(); - this.voiceBroadcastResumer?.destroy(); } private onWindowResized = (): void => { @@ -651,10 +655,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> { break; case "logout": LegacyCallHandler.instance.hangupAllCalls(); - Promise.all([ - ...[...CallStore.instance.connectedCalls].map((call) => call.disconnect()), - cleanUpBroadcasts(this.stores), - ]).finally(() => Lifecycle.logout(this.stores.oidcClientStore)); + Promise.all([...[...CallStore.instance.connectedCalls].map((call) => call.disconnect())]).finally(() => + Lifecycle.logout(this.stores.oidcClientStore), + ); break; case "require_registration": startAnyRegistrationFlow(payload as any); @@ -1638,7 +1641,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> { } else { // otherwise check the server to see if there's a new one try { - newVersionInfo = await cli.getKeyBackupVersion(); + newVersionInfo = (await cli.getCrypto()?.getKeyBackupInfo()) ?? null; if (newVersionInfo !== null) haveNewVersion = true; } catch (e) { logger.error("Saw key backup error but failed to check backup version!", e); @@ -1679,8 +1682,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> { }); } }); - - this.voiceBroadcastResumer = new VoiceBroadcastResumer(cli); } /** @@ -2079,14 +2080,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> { } else if (this.state.view === Views.COMPLETE_SECURITY) { view = <CompleteSecurity onFinished={this.onCompleteSecurityE2eSetupFinished} />; } else if (this.state.view === Views.E2E_SETUP) { - view = ( - <E2eSetup - matrixClient={MatrixClientPeg.safeGet()} - onFinished={this.onCompleteSecurityE2eSetupFinished} - accountPassword={this.stores.accountPasswordStore.getPassword()} - tokenLogin={!!this.tokenLogin} - /> - ); + view = <E2eSetup onFinished={this.onCompleteSecurityE2eSetupFinished} />; } else if (this.state.view === Views.LOGGED_IN) { // `ready` and `view==LOGGED_IN` may be set before `page_type` (because the // latter is set via the dispatcher). If we don't yet have a `page_type`, diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index b26de2e645..d2133f4f13 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -196,7 +196,7 @@ interface IReadReceiptForUser { */ export default class MessagePanel extends React.Component<IProps, IState> { public static contextType = RoomContext; - public declare context: React.ContextType<typeof RoomContext>; + declare public context: React.ContextType<typeof RoomContext>; public static defaultProps = { disableGrouping: false, diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx index edec675b14..236da25409 100644 --- a/src/components/structures/NotificationPanel.tsx +++ b/src/components/structures/NotificationPanel.tsx @@ -19,6 +19,7 @@ import { Layout } from "../../settings/enums/Layout"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import Measured from "../views/elements/Measured"; import EmptyState from "../views/right_panel/EmptyState"; +import { ScopedRoomContextProvider } from "../../contexts/ScopedRoomContext.tsx"; interface IProps { onClose(): void; @@ -33,7 +34,7 @@ interface IState { */ export default class NotificationPanel extends React.PureComponent<IProps, IState> { public static contextType = RoomContext; - public declare context: React.ContextType<typeof RoomContext>; + declare public context: React.ContextType<typeof RoomContext>; private card = React.createRef<HTMLDivElement>(); @@ -79,12 +80,10 @@ export default class NotificationPanel extends React.PureComponent<IProps, IStat } return ( - <RoomContext.Provider - value={{ - ...this.context, - timelineRenderingType: TimelineRenderingType.Notification, - narrow: this.state.narrow, - }} + <ScopedRoomContextProvider + {...this.context} + timelineRenderingType={TimelineRenderingType.Notification} + narrow={this.state.narrow} > <BaseCard header={_t("notifications|enable_prompt_toast_title")} @@ -99,7 +98,7 @@ export default class NotificationPanel extends React.PureComponent<IProps, IStat {this.card.current && <Measured sensor={this.card.current} onMeasurement={this.onMeasurement} />} {content} </BaseCard> - </RoomContext.Provider> + </ScopedRoomContextProvider> ); } } diff --git a/src/components/structures/PipContainer.tsx b/src/components/structures/PipContainer.tsx index 731e720b12..c9fabfe0c9 100644 --- a/src/components/structures/PipContainer.tsx +++ b/src/components/structures/PipContainer.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { MutableRefObject, ReactNode, useContext, useRef } from "react"; +import React, { MutableRefObject, ReactNode, useRef } from "react"; import { CallEvent, CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call"; import { logger } from "matrix-js-sdk/src/logger"; import { Optional } from "matrix-events-sdk"; @@ -21,19 +21,7 @@ import { WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore"; import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../stores/ActiveWidgetStore"; import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; import { UPDATE_EVENT } from "../../stores/AsyncStore"; -import { SDKContext, SdkContextClass } from "../../contexts/SDKContext"; -import { - useCurrentVoiceBroadcastPreRecording, - useCurrentVoiceBroadcastRecording, - VoiceBroadcastPlayback, - VoiceBroadcastPlaybackBody, - VoiceBroadcastPreRecording, - VoiceBroadcastPreRecordingPip, - VoiceBroadcastRecording, - VoiceBroadcastRecordingPip, - VoiceBroadcastSmallPlaybackBody, -} from "../../voice-broadcast"; -import { useCurrentVoiceBroadcastPlayback } from "../../voice-broadcast/hooks/useCurrentVoiceBroadcastPlayback"; +import { SdkContextClass } from "../../contexts/SDKContext"; import { WidgetPip } from "../views/pips/WidgetPip"; const SHOW_CALL_IN_STATES = [ @@ -46,9 +34,6 @@ const SHOW_CALL_IN_STATES = [ ]; interface IProps { - voiceBroadcastRecording: Optional<VoiceBroadcastRecording>; - voiceBroadcastPreRecording: Optional<VoiceBroadcastPreRecording>; - voiceBroadcastPlayback: Optional<VoiceBroadcastPlayback>; movePersistedElement: MutableRefObject<(() => void) | undefined>; } @@ -245,52 +230,9 @@ class PipContainerInner extends React.Component<IProps, IState> { this.setState({ showWidgetInPip, persistentWidgetId, persistentRoomId }); } - private createVoiceBroadcastPlaybackPipContent(voiceBroadcastPlayback: VoiceBroadcastPlayback): CreatePipChildren { - const content = - this.state.viewedRoomId === voiceBroadcastPlayback.infoEvent.getRoomId() ? ( - <VoiceBroadcastPlaybackBody playback={voiceBroadcastPlayback} pip={true} /> - ) : ( - <VoiceBroadcastSmallPlaybackBody playback={voiceBroadcastPlayback} /> - ); - - return ({ onStartMoving }) => ( - <div key={`vb-playback-${voiceBroadcastPlayback.infoEvent.getId()}`} onMouseDown={onStartMoving}> - {content} - </div> - ); - } - - private createVoiceBroadcastPreRecordingPipContent( - voiceBroadcastPreRecording: VoiceBroadcastPreRecording, - ): CreatePipChildren { - return ({ onStartMoving }) => ( - <div key="vb-pre-recording" onMouseDown={onStartMoving}> - <VoiceBroadcastPreRecordingPip voiceBroadcastPreRecording={voiceBroadcastPreRecording} /> - </div> - ); - } - - private createVoiceBroadcastRecordingPipContent( - voiceBroadcastRecording: VoiceBroadcastRecording, - ): CreatePipChildren { - return ({ onStartMoving }) => ( - <div key={`vb-recording-${voiceBroadcastRecording.infoEvent.getId()}`} onMouseDown={onStartMoving}> - <VoiceBroadcastRecordingPip recording={voiceBroadcastRecording} /> - </div> - ); - } - public render(): ReactNode { const pipMode = true; - let pipContent: Array<CreatePipChildren> = []; - - if (this.props.voiceBroadcastRecording) { - pipContent = [this.createVoiceBroadcastRecordingPipContent(this.props.voiceBroadcastRecording)]; - } else if (this.props.voiceBroadcastPreRecording) { - pipContent = [this.createVoiceBroadcastPreRecordingPipContent(this.props.voiceBroadcastPreRecording)]; - } else if (this.props.voiceBroadcastPlayback) { - pipContent = [this.createVoiceBroadcastPlaybackPipContent(this.props.voiceBroadcastPlayback)]; - } + const pipContent: Array<CreatePipChildren> = []; if (this.state.primaryCall) { // get a ref to call inside the current scope @@ -338,24 +280,7 @@ class PipContainerInner extends React.Component<IProps, IState> { } export const PipContainer: React.FC = () => { - const sdkContext = useContext(SDKContext); - const voiceBroadcastPreRecordingStore = sdkContext.voiceBroadcastPreRecordingStore; - const { currentVoiceBroadcastPreRecording } = useCurrentVoiceBroadcastPreRecording(voiceBroadcastPreRecordingStore); - - const voiceBroadcastRecordingsStore = sdkContext.voiceBroadcastRecordingsStore; - const { currentVoiceBroadcastRecording } = useCurrentVoiceBroadcastRecording(voiceBroadcastRecordingsStore); - - const voiceBroadcastPlaybacksStore = sdkContext.voiceBroadcastPlaybacksStore; - const { currentVoiceBroadcastPlayback } = useCurrentVoiceBroadcastPlayback(voiceBroadcastPlaybacksStore); - const movePersistedElement = useRef<() => void>(); - return ( - <PipContainerInner - voiceBroadcastPlayback={currentVoiceBroadcastPlayback} - voiceBroadcastPreRecording={currentVoiceBroadcastPreRecording} - voiceBroadcastRecording={currentVoiceBroadcastRecording} - movePersistedElement={movePersistedElement} - /> - ); + return <PipContainerInner movePersistedElement={movePersistedElement} />; }; diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index 9a9f29f82e..a1f2016243 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -63,7 +63,7 @@ interface IState { export default class RightPanel extends React.Component<Props, IState> { public static contextType = MatrixClientContext; - public declare context: React.ContextType<typeof MatrixClientContext>; + declare public context: React.ContextType<typeof MatrixClientContext>; public constructor(props: Props, context: React.ContextType<typeof MatrixClientContext>) { super(props, context); @@ -109,10 +109,10 @@ export default class RightPanel extends React.Component<Props, IState> { } // redraw the badge on the membership list - if (this.state.phase === RightPanelPhases.RoomMemberList) { + if (this.state.phase === RightPanelPhases.MemberList) { this.delayedUpdate(); } else if ( - this.state.phase === RightPanelPhases.RoomMemberInfo && + this.state.phase === RightPanelPhases.MemberInfo && member.userId === this.state.cardState?.member?.userId ) { // refresh the member info (e.g. new power level) @@ -157,7 +157,7 @@ export default class RightPanel extends React.Component<Props, IState> { const phase = this.props.overwriteCard?.phase ?? this.state.phase; const cardState = this.props.overwriteCard?.state ?? this.state.cardState; switch (phase) { - case RightPanelPhases.RoomMemberList: + case RightPanelPhases.MemberList: if (!!roomId) { card = ( <MemberList @@ -170,22 +170,8 @@ export default class RightPanel extends React.Component<Props, IState> { ); } break; - case RightPanelPhases.SpaceMemberList: - if (!!cardState?.spaceId || !!roomId) { - card = ( - <MemberList - roomId={cardState?.spaceId ?? roomId!} - key={cardState?.spaceId ?? roomId!} - onClose={this.onClose} - searchQuery={this.state.searchQuery} - onSearchQueryChanged={this.onSearchQueryChanged} - /> - ); - } - break; - case RightPanelPhases.RoomMemberInfo: - case RightPanelPhases.SpaceMemberInfo: + case RightPanelPhases.MemberInfo: case RightPanelPhases.EncryptionPanel: { if (!!cardState?.member) { const roomMember = cardState.member instanceof RoomMember ? cardState.member : undefined; @@ -203,8 +189,7 @@ export default class RightPanel extends React.Component<Props, IState> { } break; } - case RightPanelPhases.Room3pidMemberInfo: - case RightPanelPhases.Space3pidMemberInfo: + case RightPanelPhases.ThreePidMemberInfo: if (!!cardState?.memberInfoEvent) { card = ( <ThirdPartyMemberInfo event={cardState.memberInfoEvent} key={roomId} onClose={this.onClose} /> diff --git a/src/components/structures/RoomSearchView.tsx b/src/components/structures/RoomSearchView.tsx index de2d9d2198..82146bcc5e 100644 --- a/src/components/structures/RoomSearchView.tsx +++ b/src/components/structures/RoomSearchView.tsx @@ -26,7 +26,7 @@ import ErrorDialog from "../views/dialogs/ErrorDialog"; import ResizeNotifier from "../../utils/ResizeNotifier"; import MatrixClientContext from "../../contexts/MatrixClientContext"; import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; -import RoomContext from "../../contexts/RoomContext"; +import { useScopedRoomContext } from "../../contexts/ScopedRoomContext.tsx"; const DEBUG = false; let debuglog = function (msg: string): void {}; @@ -53,7 +53,7 @@ interface Props { export const RoomSearchView = forwardRef<ScrollPanel, Props>( ({ term, scope, promise, abortController, resizeNotifier, className, onUpdate, inProgress }: Props, ref) => { const client = useContext(MatrixClientContext); - const roomContext = useContext(RoomContext); + const roomContext = useScopedRoomContext("showHiddenEvents"); const [highlights, setHighlights] = useState<string[] | null>(null); const [results, setResults] = useState<ISearchResults | null>(null); const aborted = useRef(false); diff --git a/src/components/structures/RoomStatusBar.tsx b/src/components/structures/RoomStatusBar.tsx index 76f3b0c229..3bd69148ae 100644 --- a/src/components/structures/RoomStatusBar.tsx +++ b/src/components/structures/RoomStatusBar.tsx @@ -89,7 +89,7 @@ interface IState { export default class RoomStatusBar extends React.PureComponent<IProps, IState> { private unmounted = false; public static contextType = MatrixClientContext; - public declare context: React.ContextType<typeof MatrixClientContext>; + declare public context: React.ContextType<typeof MatrixClientContext>; public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) { super(props, context); diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 470b73de7c..772d5698a3 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { ChangeEvent, ComponentProps, createRef, ReactElement, ReactNode, RefObject, useContext } from "react"; +import React, { ChangeEvent, ComponentProps, createRef, ReactElement, ReactNode, RefObject, JSX } from "react"; import classNames from "classnames"; import { IRecommendedVersion, @@ -29,6 +29,7 @@ import { MatrixError, ISearchResults, THREAD_RELATION_TYPE, + MatrixClient, } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; import { logger } from "matrix-js-sdk/src/logger"; @@ -54,7 +55,7 @@ import WidgetEchoStore from "../../stores/WidgetEchoStore"; import SettingsStore from "../../settings/SettingsStore"; import { Layout } from "../../settings/enums/Layout"; import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton"; -import RoomContext, { TimelineRenderingType, MainSplitContentType } from "../../contexts/RoomContext"; +import { TimelineRenderingType, MainSplitContentType } from "../../contexts/RoomContext"; import { E2EStatus, shieldStatusForRoom } from "../../utils/ShieldUtils"; import { Action } from "../../dispatcher/actions"; import { IMatrixClientCreds } from "../../MatrixClientPeg"; @@ -126,6 +127,7 @@ import RightPanelStore from "../../stores/right-panel/RightPanelStore"; import { onView3pidInvite } from "../../stores/right-panel/action-handlers"; import RoomSearchAuxPanel from "../views/rooms/RoomSearchAuxPanel"; import { PinnedMessageBanner } from "../views/rooms/PinnedMessageBanner"; +import { ScopedRoomContextProvider, useScopedRoomContext } from "../../contexts/ScopedRoomContext"; const DEBUG = false; const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000; @@ -233,6 +235,11 @@ export interface IRoomState { liveTimeline?: EventTimeline; narrow: boolean; msc3946ProcessDynamicPredecessor: boolean; + /** + * Whether the room is encrypted or not. + * If null, we are still determining the encryption status. + */ + isRoomEncrypted: boolean | null; canAskToJoin: boolean; promptAskToJoin: boolean; @@ -246,6 +253,7 @@ interface LocalRoomViewProps { permalinkCreator: RoomPermalinkCreator; roomView: RefObject<HTMLElement>; onFileDrop: (dataTransfer: DataTransfer) => Promise<void>; + mainSplitContentType: MainSplitContentType; } /** @@ -255,7 +263,7 @@ interface LocalRoomViewProps { * @returns {ReactElement} */ function LocalRoomView(props: LocalRoomViewProps): ReactElement { - const context = useContext(RoomContext); + const context = useScopedRoomContext("room"); const room = context.room as LocalRoom; const encryptionEvent = props.localRoom.currentState.getStateEvents(EventType.RoomEncryption)[0]; let encryptionTile: ReactNode; @@ -323,6 +331,7 @@ interface ILocalRoomCreateLoaderProps { localRoom: LocalRoom; names: string; resizeNotifier: ResizeNotifier; + mainSplitContentType: MainSplitContentType; } /** @@ -363,7 +372,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> { private roomViewBody = createRef<HTMLDivElement>(); public static contextType = SDKContext; - public declare context: React.ContextType<typeof SDKContext>; + declare public context: React.ContextType<typeof SDKContext>; public constructor(props: IRoomProps, context: React.ContextType<typeof SDKContext>) { super(props, context); @@ -417,6 +426,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> { canAskToJoin: this.askToJoinEnabled, promptAskToJoin: false, viewRoomOpts: { buttons: [] }, + isRoomEncrypted: null, }; } @@ -655,6 +665,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> { // the RoomView instance if (initial) { newState.room = this.context.client!.getRoom(newState.roomId) || undefined; + newState.isRoomEncrypted = null; if (newState.room) { newState.showApps = this.shouldShowApps(newState.room); this.onRoomLoaded(newState.room); @@ -697,6 +708,14 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> { if (initial) { this.setupRoom(newState.room, newState.roomId, !!newState.joining, !!newState.shouldPeek); } + + // We don't block the initial setup but we want to make it early to not block the timeline rendering + const isRoomEncrypted = await this.getIsRoomEncrypted(newState.roomId); + this.setState({ + isRoomEncrypted, + ...(isRoomEncrypted && + newState.roomId && { e2eStatus: RoomView.e2eStatusCache.get(newState.roomId) ?? E2EStatus.Warning }), + }); }; private onConnectedCalls = (): void => { @@ -1214,18 +1233,18 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> { if (payload.member) { if (payload.push) { RightPanelStore.instance.pushCard({ - phase: RightPanelPhases.RoomMemberInfo, + phase: RightPanelPhases.MemberInfo, state: { member: payload.member }, }); } else { RightPanelStore.instance.setCards([ { phase: RightPanelPhases.RoomSummary }, - { phase: RightPanelPhases.RoomMemberList }, - { phase: RightPanelPhases.RoomMemberInfo, state: { member: payload.member } }, + { phase: RightPanelPhases.MemberList }, + { phase: RightPanelPhases.MemberInfo, state: { member: payload.member } }, ]); } } else { - RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomMemberList); + RightPanelStore.instance.showOrHidePhase(RightPanelPhases.MemberList); } break; case Action.View3pidInvite: @@ -1342,13 +1361,12 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> { this.context.widgetLayoutStore.on(WidgetLayoutStore.emissionForRoom(room), this.onWidgetLayoutChange); this.calculatePeekRules(room); - this.updatePreviewUrlVisibility(room); this.loadMembersIfJoined(room); this.calculateRecommendedVersion(room); - this.updateE2EStatus(room); this.updatePermissions(room); this.checkWidgets(room); this.loadVirtualRoom(room); + this.updateRoomEncrypted(room); if ( this.getMainSplitContentType(room) !== MainSplitContentType.Timeline && @@ -1377,6 +1395,13 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> { return room?.currentState.getStateEvents(EventType.RoomTombstone, "") ?? undefined; } + private async getIsRoomEncrypted(roomId = this.state.roomId): Promise<boolean> { + const crypto = this.context.client?.getCrypto(); + if (!crypto || !roomId) return false; + + return await crypto.isEncryptionEnabledInRoom(roomId); + } + private async calculateRecommendedVersion(room: Room): Promise<void> { const upgradeRecommendation = await room.getRecommendedVersion(); if (this.unmounted) return; @@ -1409,12 +1434,15 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> { }); } - private updatePreviewUrlVisibility({ roomId }: Room): void { - // URL Previews in E2EE rooms can be a privacy leak so use a different setting which is per-room explicit - const key = this.context.client?.isRoomEncrypted(roomId) ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled"; - this.setState({ - showUrlPreview: SettingsStore.getValue(key, roomId), - }); + private updatePreviewUrlVisibility(room: Room): void { + this.setState(({ isRoomEncrypted }) => ({ + showUrlPreview: this.getPreviewUrlVisibility(room, isRoomEncrypted), + })); + } + + private getPreviewUrlVisibility({ roomId }: Room, isRoomEncrypted: boolean | null): boolean { + const key = isRoomEncrypted ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled"; + return SettingsStore.getValue(key, roomId); } private onRoom = (room: Room): void => { @@ -1456,22 +1484,20 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> { }; private async updateE2EStatus(room: Room): Promise<void> { - if (!this.context.client?.isRoomEncrypted(room.roomId)) return; - - // If crypto is not currently enabled, we aren't tracking devices at all, - // so we don't know what the answer is. Let's error on the safe side and show - // a warning for this case. - let e2eStatus = RoomView.e2eStatusCache.get(room.roomId) ?? E2EStatus.Warning; - // set the state immediately then update, so we don't scare the user into thinking the room is unencrypted + if (!this.context.client || !this.state.isRoomEncrypted) return; + const e2eStatus = await this.cacheAndGetE2EStatus(room, this.context.client); + if (this.unmounted) return; this.setState({ e2eStatus }); + } - if (this.context.client.getCrypto()) { - /* At this point, the user has encryption on and cross-signing on */ - e2eStatus = await shieldStatusForRoom(this.context.client, room); - RoomView.e2eStatusCache.set(room.roomId, e2eStatus); - if (this.unmounted) return; - this.setState({ e2eStatus }); - } + private async cacheAndGetE2EStatus(room: Room, client: MatrixClient): Promise<E2EStatus> { + let e2eStatus = RoomView.e2eStatusCache.get(room.roomId); + // set the state immediately then update, so we don't scare the user into thinking the room is unencrypted + if (e2eStatus) this.setState({ e2eStatus }); + + e2eStatus = await shieldStatusForRoom(client, room); + RoomView.e2eStatusCache.set(room.roomId, e2eStatus); + return e2eStatus; } private onUrlPreviewsEnabledChange = (): void => { @@ -1480,20 +1506,36 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> { } }; - private onRoomStateEvents = (ev: MatrixEvent, state: RoomState): void => { + private onRoomStateEvents = async (ev: MatrixEvent, state: RoomState): Promise<void> => { // ignore if we don't have a room yet - if (!this.state.room || this.state.room.roomId !== state.roomId) return; + if (!this.state.room || this.state.room.roomId !== state.roomId || !this.context.client) return; switch (ev.getType()) { case EventType.RoomTombstone: this.setState({ tombstone: this.getRoomTombstone() }); break; - + case EventType.RoomEncryption: { + await this.updateRoomEncrypted(); + break; + } default: this.updatePermissions(this.state.room); } }; + private async updateRoomEncrypted(room = this.state.room): Promise<void> { + if (!room || !this.context.client) return; + + const isRoomEncrypted = await this.getIsRoomEncrypted(room.roomId); + const newE2EStatus = isRoomEncrypted ? await this.cacheAndGetE2EStatus(room, this.context.client) : null; + + this.setState({ + isRoomEncrypted, + showUrlPreview: this.getPreviewUrlVisibility(room, isRoomEncrypted), + ...(newE2EStatus && { e2eStatus: newE2EStatus }), + }); + } + private onRoomStateUpdate = (state: RoomState): void => { // ignore members in other rooms if (state.roomId !== this.state.room?.roomId) { @@ -1959,35 +2001,41 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> { if (!this.state.room || !this.context?.client) return null; const names = this.state.room.getDefaultRoomName(this.context.client.getSafeUserId()); return ( - <RoomContext.Provider value={this.state}> - <LocalRoomCreateLoader localRoom={localRoom} names={names} resizeNotifier={this.props.resizeNotifier} /> - </RoomContext.Provider> + <ScopedRoomContextProvider {...this.state}> + <LocalRoomCreateLoader + localRoom={localRoom} + names={names} + resizeNotifier={this.props.resizeNotifier} + mainSplitContentType={this.state.mainSplitContentType} + /> + </ScopedRoomContextProvider> ); } private renderLocalRoomView(localRoom: LocalRoom): ReactNode { return ( - <RoomContext.Provider value={this.state}> + <ScopedRoomContextProvider {...this.state}> <LocalRoomView localRoom={localRoom} resizeNotifier={this.props.resizeNotifier} permalinkCreator={this.permalinkCreator} roomView={this.roomView} onFileDrop={this.onFileDrop} + mainSplitContentType={this.state.mainSplitContentType} /> - </RoomContext.Provider> + </ScopedRoomContextProvider> ); } private renderWaitingForThirdPartyRoomView(inviteEvent: MatrixEvent): ReactNode { return ( - <RoomContext.Provider value={this.state}> + <ScopedRoomContextProvider {...this.state}> <WaitingForThirdPartyRoomView resizeNotifier={this.props.resizeNotifier} roomView={this.roomView} inviteEvent={inviteEvent} /> - </RoomContext.Provider> + </ScopedRoomContextProvider> ); } @@ -2027,6 +2075,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> { public render(): ReactNode { if (!this.context.client) return null; + const { isRoomEncrypted } = this.state; + const isRoomEncryptionLoading = isRoomEncrypted === null; if (this.state.room instanceof LocalRoom) { if (this.state.room.state === LocalRoomState.CREATING) { @@ -2242,14 +2292,16 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> { let aux: JSX.Element | undefined; let previewBar; if (this.state.timelineRenderingType === TimelineRenderingType.Search) { - aux = ( - <RoomSearchAuxPanel - searchInfo={this.state.search} - onCancelClick={this.onCancelSearchClick} - onSearchScopeChange={this.onSearchScopeChange} - isRoomEncrypted={this.context.client.isRoomEncrypted(this.state.room.roomId)} - /> - ); + if (!isRoomEncryptionLoading) { + aux = ( + <RoomSearchAuxPanel + searchInfo={this.state.search} + onCancelClick={this.onCancelSearchClick} + onSearchScopeChange={this.onSearchScopeChange} + isRoomEncrypted={isRoomEncrypted} + /> + ); + } } else if (showRoomUpgradeBar) { aux = <RoomUpgradeWarningBar room={this.state.room} />; } else if (myMembership !== KnownMembership.Join) { @@ -2320,13 +2372,19 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> { ); const pinnedMessageBanner = ( - <PinnedMessageBanner room={this.state.room} permalinkCreator={this.permalinkCreator} /> + <PinnedMessageBanner + room={this.state.room} + permalinkCreator={this.permalinkCreator} + resizeNotifier={this.props.resizeNotifier} + /> ); let messageComposer; const showComposer = + !isRoomEncryptionLoading && // joined and not showing search results - myMembership === KnownMembership.Join && !this.state.search; + myMembership === KnownMembership.Join && + !this.state.search; if (showComposer) { messageComposer = ( <MessageComposer @@ -2367,34 +2425,37 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> { highlightedEventId = this.state.initialEventId; } - const messagePanel = ( - <TimelinePanel - ref={this.gatherTimelinePanelRef} - timelineSet={this.state.room.getUnfilteredTimelineSet()} - overlayTimelineSet={this.state.virtualRoom?.getUnfilteredTimelineSet()} - overlayTimelineSetFilter={isCallEvent} - showReadReceipts={this.state.showReadReceipts} - manageReadReceipts={!this.state.isPeeking} - sendReadReceiptOnLoad={!this.state.wasContextSwitch} - manageReadMarkers={!this.state.isPeeking} - hidden={hideMessagePanel} - highlightedEventId={highlightedEventId} - eventId={this.state.initialEventId} - eventScrollIntoView={this.state.initialEventScrollIntoView} - eventPixelOffset={this.state.initialEventPixelOffset} - onScroll={this.onMessageListScroll} - onEventScrolledIntoView={this.resetJumpToEvent} - onReadMarkerUpdated={this.updateTopUnreadMessagesBar} - showUrlPreview={this.state.showUrlPreview} - className={this.messagePanelClassNames} - membersLoaded={this.state.membersLoaded} - permalinkCreator={this.permalinkCreator} - resizeNotifier={this.props.resizeNotifier} - showReactions={true} - layout={this.state.layout} - editState={this.state.editState} - /> - ); + let messagePanel: JSX.Element | undefined; + if (!isRoomEncryptionLoading) { + messagePanel = ( + <TimelinePanel + ref={this.gatherTimelinePanelRef} + timelineSet={this.state.room.getUnfilteredTimelineSet()} + overlayTimelineSet={this.state.virtualRoom?.getUnfilteredTimelineSet()} + overlayTimelineSetFilter={isCallEvent} + showReadReceipts={this.state.showReadReceipts} + manageReadReceipts={!this.state.isPeeking} + sendReadReceiptOnLoad={!this.state.wasContextSwitch} + manageReadMarkers={!this.state.isPeeking} + hidden={hideMessagePanel} + highlightedEventId={highlightedEventId} + eventId={this.state.initialEventId} + eventScrollIntoView={this.state.initialEventScrollIntoView} + eventPixelOffset={this.state.initialEventPixelOffset} + onScroll={this.onMessageListScroll} + onEventScrolledIntoView={this.resetJumpToEvent} + onReadMarkerUpdated={this.updateTopUnreadMessagesBar} + showUrlPreview={this.state.showUrlPreview} + className={this.messagePanelClassNames} + membersLoaded={this.state.membersLoaded} + permalinkCreator={this.permalinkCreator} + resizeNotifier={this.props.resizeNotifier} + showReactions={true} + layout={this.state.layout} + editState={this.state.editState} + /> + ); + } let topUnreadMessagesBar: JSX.Element | undefined; // Do not show TopUnreadMessagesBar if we have search results showing, it makes no sense @@ -2415,7 +2476,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> { ); } - const showRightPanel = this.state.room && this.state.showRightPanel; + const showRightPanel = !isRoomEncryptionLoading && this.state.room && this.state.showRightPanel; const rightPanel = showRightPanel ? ( <RightPanel @@ -2516,7 +2577,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> { } return ( - <RoomContext.Provider value={this.state}> + <ScopedRoomContextProvider {...this.state}> <div className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}> {showChatEffects && this.roomView.current && ( <EffectsOverlay roomWidth={this.roomView.current.offsetWidth} /> @@ -2543,7 +2604,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> { </MainSplit> </ErrorBoundary> </div> - </RoomContext.Provider> + </ScopedRoomContextProvider> ); } } diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 3ea2a03c1a..bf0ddb1fe2 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -208,7 +208,7 @@ const SpaceLanding: React.FC<{ space: Room }> = ({ space }) => { const storeIsShowingSpaceMembers = useCallback( () => RightPanelStore.instance.isOpenForRoom(space.roomId) && - RightPanelStore.instance.currentCardForRoom(space.roomId)?.phase === RightPanelPhases.SpaceMemberList, + RightPanelStore.instance.currentCardForRoom(space.roomId)?.phase === RightPanelPhases.MemberList, [space.roomId], ); const isShowingMembers = useEventEmitterState(RightPanelStore.instance, UPDATE_EVENT, storeIsShowingSpaceMembers); @@ -251,7 +251,7 @@ const SpaceLanding: React.FC<{ space: Room }> = ({ space }) => { } const onMembersClick = (): void => { - RightPanelStore.instance.setCard({ phase: RightPanelPhases.SpaceMemberList }); + RightPanelStore.instance.setCard({ phase: RightPanelPhases.MemberList }); }; return ( @@ -597,7 +597,7 @@ const SpaceSetupPrivateInvite: React.FC<{ export default class SpaceRoomView extends React.PureComponent<IProps, IState> { public static contextType = MatrixClientContext; - public declare context: React.ContextType<typeof MatrixClientContext>; + declare public context: React.ContextType<typeof MatrixClientContext>; private dispatcherRef?: string; diff --git a/src/components/structures/ThreadPanel.tsx b/src/components/structures/ThreadPanel.tsx index eb20b05fdc..0e82baa28b 100644 --- a/src/components/structures/ThreadPanel.tsx +++ b/src/components/structures/ThreadPanel.tsx @@ -20,7 +20,7 @@ import MatrixClientContext, { useMatrixClientContext } from "../../contexts/Matr import { _t } from "../../languageHandler"; import { ContextMenuButton } from "../../accessibility/context_menu/ContextMenuButton"; import ContextMenu, { ChevronFace, MenuItemRadio, useContextMenu } from "./ContextMenu"; -import RoomContext, { TimelineRenderingType, useRoomContext } from "../../contexts/RoomContext"; +import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import TimelinePanel from "./TimelinePanel"; import { Layout } from "../../settings/enums/Layout"; import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; @@ -30,6 +30,7 @@ import { ButtonEvent } from "../views/elements/AccessibleButton"; import Spinner from "../views/elements/Spinner"; import { clearRoomNotification } from "../../utils/notifications"; import EmptyState from "../views/right_panel/EmptyState"; +import { ScopedRoomContextProvider, useScopedRoomContext } from "../../contexts/ScopedRoomContext.tsx"; interface IProps { roomId: string; @@ -68,7 +69,7 @@ export const ThreadPanelHeader: React.FC<{ setFilterOption: (filterOption: ThreadFilterType) => void; }> = ({ filterOption, setFilterOption }) => { const mxClient = useMatrixClientContext(); - const roomContext = useRoomContext(); + const roomContext = useScopedRoomContext("room"); const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu<HTMLElement>(); const options: readonly ThreadPanelHeaderOption[] = [ { @@ -184,13 +185,11 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) => }, [timelineSet, timelinePanel]); return ( - <RoomContext.Provider - value={{ - ...roomContext, - timelineRenderingType: TimelineRenderingType.ThreadsList, - showHiddenEvents: true, - narrow, - }} + <ScopedRoomContextProvider + {...roomContext} + timelineRenderingType={TimelineRenderingType.ThreadsList} + showHiddenEvents={true} + narrow={narrow} > <BaseCard header={ @@ -241,7 +240,7 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) => </div> )} </BaseCard> - </RoomContext.Provider> + </ScopedRoomContextProvider> ); }; export default ThreadPanel; diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index be538a6669..bc1e26d087 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -51,6 +51,7 @@ import { ComposerInsertPayload, ComposerType } from "../../dispatcher/payloads/C import Heading from "../views/typography/Heading"; import { SdkContextClass } from "../../contexts/SDKContext"; import { ThreadPayload } from "../../dispatcher/payloads/ThreadPayload"; +import { ScopedRoomContextProvider } from "../../contexts/ScopedRoomContext.tsx"; interface IProps { room: Room; @@ -75,7 +76,7 @@ interface IState { export default class ThreadView extends React.Component<IProps, IState> { public static contextType = RoomContext; - public declare context: React.ContextType<typeof RoomContext>; + declare public context: React.ContextType<typeof RoomContext>; private dispatcherRef?: string; private layoutWatcherRef?: string; @@ -422,14 +423,12 @@ export default class ThreadView extends React.Component<IProps, IState> { } return ( - <RoomContext.Provider - value={{ - ...this.context, - timelineRenderingType: TimelineRenderingType.Thread, - threadId: this.state.thread?.id, - liveTimeline: this.state?.thread?.timelineSet?.getLiveTimeline(), - narrow: this.state.narrow, - }} + <ScopedRoomContextProvider + {...this.context} + timelineRenderingType={TimelineRenderingType.Thread} + threadId={this.state.thread?.id} + liveTimeline={this.state?.thread?.timelineSet?.getLiveTimeline()} + narrow={this.state.narrow} > <BaseCard className={classNames("mx_ThreadView mx_ThreadPanel", { @@ -463,7 +462,7 @@ export default class ThreadView extends React.Component<IProps, IState> { /> )} </BaseCard> - </RoomContext.Provider> + </ScopedRoomContextProvider> ); } } diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index 93582b419b..29dc0b8860 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -229,7 +229,7 @@ interface IEventIndexOpts { */ class TimelinePanel extends React.Component<IProps, IState> { public static contextType = RoomContext; - public declare context: React.ContextType<typeof RoomContext>; + declare public context: React.ContextType<typeof RoomContext>; // a map from room id to read marker event timestamp public static roomReadMarkerTsMap: Record<string, number> = {}; diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index b2c7990746..c5f8ef841d 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -40,8 +40,6 @@ import { UPDATE_SELECTED_SPACE } from "../../stores/spaces"; import UserIdentifierCustomisations from "../../customisations/UserIdentifier"; import PosthogTrackers from "../../PosthogTrackers"; import { ViewHomePagePayload } from "../../dispatcher/payloads/ViewHomePagePayload"; -import { Icon as LiveIcon } from "../../../res/img/compound/live-8px.svg"; -import { VoiceBroadcastRecording, VoiceBroadcastRecordingsStoreEvent } from "../../voice-broadcast"; import { SDKContext } from "../../contexts/SDKContext"; import { shouldShowFeedback } from "../../utils/Feedback"; import DarkLightModeSvg from "../../../res/img/element-icons/roomlist/dark-light-mode.svg"; @@ -58,7 +56,6 @@ interface IState { isDarkTheme: boolean; isHighContrast: boolean; selectedSpace?: Room | null; - showLiveAvatarAddon: boolean; } const toRightOf = (rect: PartialDOMRect): MenuProps => { @@ -79,7 +76,7 @@ const below = (rect: PartialDOMRect): MenuProps => { export default class UserMenu extends React.Component<IProps, IState> { public static contextType = SDKContext; - public declare context: React.ContextType<typeof SDKContext>; + declare public context: React.ContextType<typeof SDKContext>; private dispatcherRef?: string; private themeWatcherRef?: string; @@ -94,7 +91,6 @@ export default class UserMenu extends React.Component<IProps, IState> { isDarkTheme: this.isUserOnDarkTheme(), isHighContrast: this.isUserOnHighContrastTheme(), selectedSpace: SpaceStore.instance.activeSpaceRoom, - showLiveAvatarAddon: this.context.voiceBroadcastRecordingsStore.hasCurrent(), }; } @@ -102,19 +98,9 @@ export default class UserMenu extends React.Component<IProps, IState> { return !!getHomePageUrl(SdkConfig.get(), this.context.client!); } - private onCurrentVoiceBroadcastRecordingChanged = (recording: VoiceBroadcastRecording | null): void => { - this.setState({ - showLiveAvatarAddon: recording !== null, - }); - }; - public componentDidMount(): void { OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate); SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate); - this.context.voiceBroadcastRecordingsStore.on( - VoiceBroadcastRecordingsStoreEvent.CurrentChanged, - this.onCurrentVoiceBroadcastRecordingChanged, - ); this.dispatcherRef = defaultDispatcher.register(this.onAction); this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged); } @@ -125,10 +111,6 @@ export default class UserMenu extends React.Component<IProps, IState> { defaultDispatcher.unregister(this.dispatcherRef); OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate); SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate); - this.context.voiceBroadcastRecordingsStore.off( - VoiceBroadcastRecordingsStoreEvent.CurrentChanged, - this.onCurrentVoiceBroadcastRecordingChanged, - ); } private isUserOnDarkTheme(): boolean { @@ -435,12 +417,6 @@ export default class UserMenu extends React.Component<IProps, IState> { name = <div className="mx_UserMenu_name">{displayName}</div>; } - const liveAvatarAddon = this.state.showLiveAvatarAddon ? ( - <div className="mx_UserMenu_userAvatarLive" data-testid="user-menu-live-vb"> - <LiveIcon className="mx_Icon_8" /> - </div> - ) : null; - return ( <div className="mx_UserMenu"> <ContextMenuButton @@ -459,7 +435,6 @@ export default class UserMenu extends React.Component<IProps, IState> { size={avatarSize + "px"} className="mx_UserMenu_userAvatar_BaseAvatar" /> - {liveAvatarAddon} </div> {name} {this.renderContextMenu()} diff --git a/src/components/structures/UserView.tsx b/src/components/structures/UserView.tsx index 635115877e..ba905099ba 100644 --- a/src/components/structures/UserView.tsx +++ b/src/components/structures/UserView.tsx @@ -32,7 +32,7 @@ interface IState { export default class UserView extends React.Component<IProps, IState> { public static contextType = MatrixClientContext; - public declare context: React.ContextType<typeof MatrixClientContext>; + declare public context: React.ContextType<typeof MatrixClientContext>; public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) { super(props, context); @@ -82,7 +82,7 @@ export default class UserView extends React.Component<IProps, IState> { } else if (this.state.member) { const panel = ( <RightPanel - overwriteCard={{ phase: RightPanelPhases.RoomMemberInfo, state: { member: this.state.member } }} + overwriteCard={{ phase: RightPanelPhases.MemberInfo, state: { member: this.state.member } }} resizeNotifier={this.props.resizeNotifier} /> ); diff --git a/src/components/structures/WaitingForThirdPartyRoomView.tsx b/src/components/structures/WaitingForThirdPartyRoomView.tsx index fd9afc50f2..cabe92a53d 100644 --- a/src/components/structures/WaitingForThirdPartyRoomView.tsx +++ b/src/components/structures/WaitingForThirdPartyRoomView.tsx @@ -9,7 +9,6 @@ Please see LICENSE files in the repository root for full details. import React, { RefObject } from "react"; import { MatrixEvent } from "matrix-js-sdk/src/matrix"; -import { useRoomContext } from "../../contexts/RoomContext"; import ResizeNotifier from "../../utils/ResizeNotifier"; import ErrorBoundary from "../views/elements/ErrorBoundary"; import RoomHeader from "../views/rooms/RoomHeader"; @@ -19,6 +18,7 @@ import NewRoomIntro from "../views/rooms/NewRoomIntro"; import { UnwrappedEventTile } from "../views/rooms/EventTile"; import { _t } from "../../languageHandler"; import SdkConfig from "../../SdkConfig"; +import { useScopedRoomContext } from "../../contexts/ScopedRoomContext.tsx"; interface Props { roomView: RefObject<HTMLElement>; @@ -32,7 +32,7 @@ interface Props { * To avoid UTDs, users are shown a waiting room until the others have joined. */ export const WaitingForThirdPartyRoomView: React.FC<Props> = ({ roomView, resizeNotifier, inviteEvent }) => { - const context = useRoomContext(); + const context = useScopedRoomContext("room"); const brand = SdkConfig.get().brand; return ( diff --git a/src/components/structures/auth/E2eSetup.tsx b/src/components/structures/auth/E2eSetup.tsx index 80a135fe19..3b064d6134 100644 --- a/src/components/structures/auth/E2eSetup.tsx +++ b/src/components/structures/auth/E2eSetup.tsx @@ -7,17 +7,13 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; -import { MatrixClient } from "matrix-js-sdk/src/matrix"; import AuthPage from "../../views/auth/AuthPage"; import CompleteSecurityBody from "../../views/auth/CompleteSecurityBody"; -import CreateCrossSigningDialog from "../../views/dialogs/security/CreateCrossSigningDialog"; +import { InitialCryptoSetupDialog } from "../../views/dialogs/security/InitialCryptoSetupDialog"; interface IProps { - matrixClient: MatrixClient; onFinished: () => void; - accountPassword?: string; - tokenLogin: boolean; } export default class E2eSetup extends React.Component<IProps> { @@ -25,12 +21,7 @@ export default class E2eSetup extends React.Component<IProps> { return ( <AuthPage> <CompleteSecurityBody> - <CreateCrossSigningDialog - matrixClient={this.props.matrixClient} - onFinished={this.props.onFinished} - accountPassword={this.props.accountPassword} - tokenLogin={this.props.tokenLogin} - /> + <InitialCryptoSetupDialog onFinished={this.props.onFinished} /> </CompleteSecurityBody> </AuthPage> ); diff --git a/src/components/structures/auth/SoftLogout.tsx b/src/components/structures/auth/SoftLogout.tsx index 117485df7e..9c7a900643 100644 --- a/src/components/structures/auth/SoftLogout.tsx +++ b/src/components/structures/auth/SoftLogout.tsx @@ -64,7 +64,7 @@ interface IState { export default class SoftLogout extends React.Component<IProps, IState> { public static contextType = SDKContext; - public declare context: React.ContextType<typeof SDKContext>; + declare public context: React.ContextType<typeof SDKContext>; public constructor(props: IProps, context: React.ContextType<typeof SDKContext>) { super(props, context); @@ -235,12 +235,7 @@ export default class SoftLogout extends React.Component<IProps, IState> { value={this.state.password} disabled={this.state.busy} /> - <AccessibleButton - onClick={this.onPasswordLogin} - kind="primary" - type="submit" - disabled={this.state.busy} - > + <AccessibleButton onClick={this.onPasswordLogin} kind="primary" disabled={this.state.busy}> {_t("action|sign_in")} </AccessibleButton> <AccessibleButton onClick={this.onForgotPassword} kind="link"> diff --git a/src/components/structures/grouper/CreationGrouper.tsx b/src/components/structures/grouper/CreationGrouper.tsx index db4542e836..84982066c3 100644 --- a/src/components/structures/grouper/CreationGrouper.tsx +++ b/src/components/structures/grouper/CreationGrouper.tsx @@ -12,7 +12,6 @@ import { KnownMembership } from "matrix-js-sdk/src/types"; import { BaseGrouper } from "./BaseGrouper"; import MessagePanel, { WrappedEvent } from "../MessagePanel"; -import { VoiceBroadcastInfoEventType } from "../../../voice-broadcast"; import DMRoomMap from "../../../utils/DMRoomMap"; import { _t } from "../../../languageHandler"; import DateSeparator from "../../views/messages/DateSeparator"; @@ -53,11 +52,6 @@ export class CreationGrouper extends BaseGrouper { return false; } - if (VoiceBroadcastInfoEventType === eventType) { - // always show voice broadcast info events in timeline - return false; - } - if (event.isState() && event.getSender() === createEvent.getSender()) { return true; } diff --git a/src/components/views/audio_messages/DevicesContextMenu.tsx b/src/components/views/audio_messages/DevicesContextMenu.tsx deleted file mode 100644 index a88a280242..0000000000 --- a/src/components/views/audio_messages/DevicesContextMenu.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022, 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React, { MutableRefObject } from "react"; - -import { toLeftOrRightOf } from "../../structures/ContextMenu"; -import IconizedContextMenu, { - IconizedContextMenuOptionList, - IconizedContextMenuRadio, -} from "../context_menus/IconizedContextMenu"; - -interface Props { - containerRef: MutableRefObject<HTMLElement | null>; - currentDevice: MediaDeviceInfo | null; - devices: MediaDeviceInfo[]; - onDeviceSelect: (device: MediaDeviceInfo) => void; -} - -export const DevicesContextMenu: React.FC<Props> = ({ containerRef, currentDevice, devices, onDeviceSelect }) => { - const deviceOptions = devices.map((d: MediaDeviceInfo) => { - return ( - <IconizedContextMenuRadio - key={d.deviceId} - active={d.deviceId === currentDevice?.deviceId} - onClick={() => onDeviceSelect(d)} - label={d.label} - /> - ); - }); - - return ( - <IconizedContextMenu - mountAsChild={false} - onFinished={() => {}} - {...(containerRef.current ? toLeftOrRightOf(containerRef.current.getBoundingClientRect(), 0) : {})} - > - <IconizedContextMenuOptionList>{deviceOptions}</IconizedContextMenuOptionList> - </IconizedContextMenu> - ); -}; diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index b1360f5560..ae5c07e348 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -910,7 +910,7 @@ export class SSOAuthEntry extends React.Component<ISSOAuthEntryProps, ISSOAuthEn export class FallbackAuthEntry<T = {}> extends React.Component<IAuthEntryProps & T> { protected popupWindow: Window | null; - protected fallbackButton = createRef<HTMLButtonElement>(); + protected fallbackButton = createRef<HTMLDivElement>(); public constructor(props: IAuthEntryProps & T) { super(props); diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index e931b40a71..76475e1dd3 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -37,7 +37,6 @@ interface IState { userCode?: string; checkCode?: string; failureReason?: FailureReason; - lastScannedCode?: Buffer; } export enum LoginWithQRFailureReason { @@ -108,12 +107,9 @@ export default class LoginWithQR extends React.Component<IProps, IState> { private generateAndShowCode = async (): Promise<void> => { let rendezvous: MSC4108SignInWithQR; try { - const fallbackRzServer = this.props.client?.getClientWellKnown()?.["io.element.rendezvous"]?.server; - const transport = new MSC4108RendezvousSession({ onFailure: this.onFailure, client: this.props.client, - fallbackRzServer, }); await transport.send(""); const channel = new MSC4108SecureChannel(transport, undefined, this.onFailure); @@ -157,7 +153,7 @@ export default class LoginWithQR extends React.Component<IProps, IState> { throw new Error("Rendezvous not found"); } - if (!this.state.lastScannedCode && this.state.rendezvous?.checkCode !== checkCode) { + if (this.state.rendezvous?.checkCode !== checkCode) { this.setState({ failureReason: LoginWithQRFailureReason.CheckCodeMismatch }); return; } @@ -204,7 +200,6 @@ export default class LoginWithQR extends React.Component<IProps, IState> { failureReason: undefined, userCode: undefined, checkCode: undefined, - lastScannedCode: undefined, mediaPermissionError: false, }); } diff --git a/src/components/views/avatars/BaseAvatar.tsx b/src/components/views/avatars/BaseAvatar.tsx index db74d7b95e..766eb561ec 100644 --- a/src/components/views/avatars/BaseAvatar.tsx +++ b/src/components/views/avatars/BaseAvatar.tsx @@ -16,10 +16,10 @@ import { Avatar } from "@vector-im/compound-web"; import SettingsStore from "../../../settings/SettingsStore"; import { ButtonEvent } from "../elements/AccessibleButton"; -import RoomContext from "../../../contexts/RoomContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { useTypedEventEmitter } from "../../../hooks/useEventEmitter"; import { _t } from "../../../languageHandler"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; interface IProps { name?: React.ComponentProps<typeof Avatar>["name"]; // The name (first initial used as default) @@ -57,8 +57,8 @@ const calculateUrls = (url?: string | null, urls?: string[], lowBandwidth = fals const useImageUrl = ({ url, urls }: { url?: string | null; urls?: string[] }): [string, () => void] => { // Since this is a hot code path and the settings store can be slow, we // use the cached lowBandwidth value from the room context if it exists - const roomContext = useContext(RoomContext); - const lowBandwidth = roomContext ? roomContext.lowBandwidth : SettingsStore.getValue("lowBandwidth"); + const roomContext = useScopedRoomContext("lowBandwidth"); + const lowBandwidth = roomContext?.lowBandwidth ?? SettingsStore.getValue("lowBandwidth"); const [imageUrls, setUrls] = useState<string[]>(calculateUrls(url, urls, lowBandwidth)); const [urlsIndex, setIndex] = useState<number>(0); diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index d5749658c9..711ffbe70f 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -38,7 +38,7 @@ import ContextMenu, { toRightOf, MenuProps } from "../../structures/ContextMenu" import ReactionPicker from "../emojipicker/ReactionPicker"; import ViewSource from "../../structures/ViewSource"; import { createRedactEventDialog } from "../dialogs/ConfirmRedactDialog"; -import ShareDialog from "../dialogs/ShareDialog"; +import { ShareDialog } from "../dialogs/ShareDialog"; import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext"; import EndPollDialog from "../dialogs/EndPollDialog"; import { isPollEnded } from "../messages/MPollBody"; @@ -126,7 +126,7 @@ interface IState { export default class MessageContextMenu extends React.Component<IProps, IState> { public static contextType = RoomContext; - public declare context: React.ContextType<typeof RoomContext>; + declare public context: React.ContextType<typeof RoomContext>; private reactButtonRef = createRef<any>(); // XXX Ref to a functional component diff --git a/src/components/views/context_menus/WidgetContextMenu.tsx b/src/components/views/context_menus/WidgetContextMenu.tsx index 21239d0fee..46052de9ff 100644 --- a/src/components/views/context_menus/WidgetContextMenu.tsx +++ b/src/components/views/context_menus/WidgetContextMenu.tsx @@ -18,7 +18,6 @@ import { _t } from "../../../languageHandler"; import { isAppWidget } from "../../../stores/WidgetStore"; import WidgetUtils from "../../../utils/WidgetUtils"; import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore"; -import RoomContext from "../../../contexts/RoomContext"; import dis from "../../../dispatcher/dispatcher"; import SettingsStore from "../../../settings/SettingsStore"; import Modal from "../../../Modal"; @@ -30,6 +29,7 @@ import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayo import { getConfigLivestreamUrl, startJitsiAudioLivestream } from "../../../Livestream"; import { ModuleRunner } from "../../../modules/ModuleRunner"; import { ElementWidget } from "../../../stores/widgets/StopGapWidget"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; interface IProps extends Omit<ComponentProps<typeof IconizedContextMenu>, "children"> { app: IWidget; @@ -114,7 +114,7 @@ export const WidgetContextMenu: React.FC<IProps> = ({ ...props }) => { const cli = useContext(MatrixClientContext); - const { room, roomId } = useContext(RoomContext); + const { room, roomId } = useScopedRoomContext("room", "roomId"); const widgetMessaging = WidgetMessagingStore.instance.getMessagingForUid(WidgetUtils.getWidgetUid(app)); const canModify = userWidget || WidgetUtils.canUserModifyWidgets(cli, roomId); diff --git a/src/components/views/dialogs/BaseDialog.tsx b/src/components/views/dialogs/BaseDialog.tsx index 00da8b4f52..ccb162e1cb 100644 --- a/src/components/views/dialogs/BaseDialog.tsx +++ b/src/components/views/dialogs/BaseDialog.tsx @@ -119,6 +119,7 @@ export default class BaseDialog extends React.Component<IProps> { <AccessibleButton onClick={this.onCancelClick} className="mx_Dialog_cancelButton" + title={_t("action|close")} aria-label={_t("dialog_close_label")} placement="bottom" /> diff --git a/src/components/views/dialogs/CantStartVoiceMessageBroadcastDialog.tsx b/src/components/views/dialogs/CantStartVoiceMessageBroadcastDialog.tsx deleted file mode 100644 index eb565c0072..0000000000 --- a/src/components/views/dialogs/CantStartVoiceMessageBroadcastDialog.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; - -import { _t } from "../../../languageHandler"; -import Modal from "../../../Modal"; -import InfoDialog from "./InfoDialog"; - -export const createCantStartVoiceMessageBroadcastDialog = (): void => { - Modal.createDialog(InfoDialog, { - title: _t("voice_message|cant_start_broadcast_title"), - description: <p>{_t("voice_message|cant_start_broadcast_description")}</p>, - hasCloseButton: true, - }); -}; diff --git a/src/components/views/dialogs/ConfirmRedactDialog.tsx b/src/components/views/dialogs/ConfirmRedactDialog.tsx index 27823ec478..f4258c9d6d 100644 --- a/src/components/views/dialogs/ConfirmRedactDialog.tsx +++ b/src/components/views/dialogs/ConfirmRedactDialog.tsx @@ -6,14 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { Feature, ServerSupport } from "matrix-js-sdk/src/feature"; -import { IRedactOpts, MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix"; +import { IRedactOpts, MatrixEvent } from "matrix-js-sdk/src/matrix"; import React from "react"; import { _t } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import Modal from "../../../Modal"; -import { isVoiceBroadcastStartedEvent } from "../../../voice-broadcast/utils/isVoiceBroadcastStartedEvent"; import ErrorDialog from "./ErrorDialog"; import TextInputDialog from "./TextInputDialog"; @@ -70,18 +68,6 @@ export function createRedactEventDialog({ const cli = MatrixClientPeg.safeGet(); const withRelTypes: Pick<IRedactOpts, "with_rel_types"> = {}; - // redact related events if this is a voice broadcast started event and - // server has support for relation based redactions - if (isVoiceBroadcastStartedEvent(mxEvent)) { - const relationBasedRedactionsSupport = cli.canSupport.get(Feature.RelationBasedRedactions); - if ( - relationBasedRedactionsSupport && - relationBasedRedactionsSupport !== ServerSupport.Unsupported - ) { - withRelTypes.with_rel_types = [RelationType.Reference]; - } - } - try { onCloseDialog?.(); await cli.redactEvent(roomId, eventId, undefined, { diff --git a/src/components/views/dialogs/DevtoolsDialog.tsx b/src/components/views/dialogs/DevtoolsDialog.tsx index 7319685014..7dc683469a 100644 --- a/src/components/views/dialogs/DevtoolsDialog.tsx +++ b/src/components/views/dialogs/DevtoolsDialog.tsx @@ -22,7 +22,6 @@ import { AccountDataExplorer, RoomAccountDataExplorer } from "./devtools/Account import SettingsFlag from "../elements/SettingsFlag"; import { SettingLevel } from "../../../settings/SettingLevel"; import ServerInfo from "./devtools/ServerInfo"; -import { Features } from "../../../settings/Settings"; import CopyableText from "../elements/CopyableText"; import RoomNotifications from "./devtools/RoomNotifications"; @@ -100,7 +99,6 @@ const DevtoolsDialog: React.FC<IProps> = ({ roomId, threadRootId, onFinished }) <SettingsFlag name="developerMode" level={SettingLevel.ACCOUNT} /> <SettingsFlag name="showHiddenEventsInTimeline" level={SettingLevel.DEVICE} /> <SettingsFlag name="enableWidgetScreenshots" level={SettingLevel.ACCOUNT} /> - <SettingsFlag name={Features.VoiceBroadcastForceSmallChunks} level={SettingLevel.DEVICE} /> </div> </BaseTool> ); diff --git a/src/components/views/dialogs/LogoutDialog.tsx b/src/components/views/dialogs/LogoutDialog.tsx index cdfaf0e89b..4731c593bc 100644 --- a/src/components/views/dialogs/LogoutDialog.tsx +++ b/src/components/views/dialogs/LogoutDialog.tsx @@ -109,7 +109,7 @@ export default class LogoutDialog extends React.Component<IProps, IState> { } // backup is not active. see if there is a backup version on the server we ought to back up to. - const backupInfo = await client.getKeyBackupVersion(); + const backupInfo = await crypto.getKeyBackupInfo(); this.setState({ backupStatus: backupInfo ? BackupStatus.SERVER_BACKUP_BUT_DISABLED : BackupStatus.NO_BACKUP }); } diff --git a/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.tsx b/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.tsx deleted file mode 100644 index 76e7580e01..0000000000 --- a/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.tsx +++ /dev/null @@ -1,90 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2020 The Matrix.org Foundation C.I.C. -Copyright 2019 New Vector Ltd -Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2017 Vector Creations Ltd -Copyright 2016 OpenMarket Ltd - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React, { useCallback } from "react"; -import { Device } from "matrix-js-sdk/src/matrix"; - -import * as FormattingUtils from "../../../utils/FormattingUtils"; -import { _t } from "../../../languageHandler"; -import QuestionDialog from "./QuestionDialog"; -import { MatrixClientPeg } from "../../../MatrixClientPeg"; - -interface IManualDeviceKeyVerificationDialogProps { - userId: string; - device: Device; - onFinished(confirm?: boolean): void; -} - -export function ManualDeviceKeyVerificationDialog({ - userId, - device, - onFinished, -}: IManualDeviceKeyVerificationDialogProps): JSX.Element { - const mxClient = MatrixClientPeg.safeGet(); - - const onLegacyFinished = useCallback( - (confirm: boolean) => { - if (confirm) { - mxClient.setDeviceVerified(userId, device.deviceId, true); - } - onFinished(confirm); - }, - [mxClient, userId, device, onFinished], - ); - - let text; - if (mxClient?.getUserId() === userId) { - text = _t("encryption|verification|manual_device_verification_self_text"); - } else { - text = _t("encryption|verification|manual_device_verification_user_text"); - } - - const fingerprint = device.getFingerprint(); - const key = fingerprint && FormattingUtils.formatCryptoKey(fingerprint); - const body = ( - <div> - <p>{text}</p> - <div className="mx_DeviceVerifyDialog_cryptoSection"> - <ul> - <li> - <label>{_t("encryption|verification|manual_device_verification_device_name_label")}:</label>{" "} - <span>{device.displayName}</span> - </li> - <li> - <label>{_t("encryption|verification|manual_device_verification_device_id_label")}:</label>{" "} - <span> - <code>{device.deviceId}</code> - </span> - </li> - <li> - <label>{_t("encryption|verification|manual_device_verification_device_key_label")}:</label>{" "} - <span> - <code> - <strong>{key}</strong> - </code> - </span> - </li> - </ul> - </div> - <p>{_t("encryption|verification|manual_device_verification_footer")}</p> - </div> - ); - - return ( - <QuestionDialog - title={_t("settings|sessions|verify_session")} - description={body} - button={_t("settings|sessions|verify_session")} - onFinished={onLegacyFinished} - /> - ); -} diff --git a/src/components/views/dialogs/ShareDialog.tsx b/src/components/views/dialogs/ShareDialog.tsx index f9382227e4..1796b79239 100644 --- a/src/components/views/dialogs/ShareDialog.tsx +++ b/src/components/views/dialogs/ShareDialog.tsx @@ -7,22 +7,23 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import * as React from "react"; +import React, { JSX, useMemo, useRef, useState } from "react"; import { Room, RoomMember, MatrixEvent, User } from "matrix-js-sdk/src/matrix"; +import { Checkbox, Button } from "@vector-im/compound-web"; +import LinkIcon from "@vector-im/compound-design-tokens/assets/web/icons/link"; +import CheckIcon from "@vector-im/compound-design-tokens/assets/web/icons/check"; import { _t } from "../../../languageHandler"; import QRCode from "../elements/QRCode"; import { RoomPermalinkCreator, makeUserPermalink } from "../../../utils/permalinks/Permalinks"; -import { selectText } from "../../../utils/strings"; -import StyledCheckbox from "../elements/StyledCheckbox"; -import SettingsStore from "../../../settings/SettingsStore"; +import { copyPlaintext } from "../../../utils/strings"; import { UIFeature } from "../../../settings/UIFeature"; import BaseDialog from "./BaseDialog"; -import CopyableText from "../elements/CopyableText"; import { XOR } from "../../../@types/common"; +import { useSettingValue } from "../../../hooks/useSettings.ts"; /* eslint-disable @typescript-eslint/no-require-imports */ -const socials = [ +const SOCIALS = [ { name: "Facebook", img: require("../../../../res/img/social/facebook.png"), @@ -33,11 +34,7 @@ const socials = [ img: require("../../../../res/img/social/twitter-2.png"), url: (url: string) => `https://twitter.com/home?status=${url}`, }, - /* // icon missing - name: 'Google Plus', - img: 'img/social/', - url: (url) => `https://plus.google.com/share?url=${url}`, - },*/ { + { name: "LinkedIn", img: require("../../../../res/img/social/linkedin.png"), url: (url: string) => `https://www.linkedin.com/shareArticle?mini=true&url=${url}`, @@ -78,160 +75,153 @@ interface Props extends BaseProps { * A <u>matrix.to</u> link will be generated out of it if it's not already a url. */ target: Room | User | RoomMember | URL; + /** + * Optional when the target is a Room, User, RoomMember or a URL. + * Mandatory when the target is a MatrixEvent. + */ permalinkCreator?: RoomPermalinkCreator; } interface EventProps extends BaseProps { + /** + * The target to link to. + */ target: MatrixEvent; + /** + * Optional when the target is a Room, User, RoomMember or a URL. + * Mandatory when the target is a MatrixEvent. + */ permalinkCreator: RoomPermalinkCreator; } -interface IState { - linkSpecificEvent: boolean; - permalinkCreator: RoomPermalinkCreator | null; +type ShareDialogProps = XOR<Props, EventProps>; + +/** + * A dialog to share a link to a room, user, room member or a matrix event. + */ +export function ShareDialog({ target, customTitle, onFinished, permalinkCreator }: ShareDialogProps): JSX.Element { + const showQrCode = useSettingValue<boolean>(UIFeature.ShareQRCode); + const showSocials = useSettingValue<boolean>(UIFeature.ShareSocial); + + const timeoutIdRef = useRef<number>(); + const [isCopied, setIsCopied] = useState(false); + + const [linkToSpecificEvent, setLinkToSpecificEvent] = useState(target instanceof MatrixEvent); + const { title, url, checkboxLabel } = useTargetValues(target, linkToSpecificEvent, permalinkCreator); + const newTitle = customTitle ?? title; + + return ( + <BaseDialog + title={newTitle} + className="mx_ShareDialog" + contentId="mx_Dialog_content" + onFinished={onFinished} + fixedWidth={false} + > + <div className="mx_ShareDialog_content"> + <div className="mx_ShareDialog_top"> + {showQrCode && <QRCode data={url} width={200} />} + <span>{url}</span> + </div> + {checkboxLabel && ( + <label> + <Checkbox + defaultChecked={linkToSpecificEvent} + onChange={(evt) => setLinkToSpecificEvent(evt.target.checked)} + /> + {checkboxLabel} + </label> + )} + <Button + Icon={isCopied ? CheckIcon : LinkIcon} + onClick={async () => { + clearTimeout(timeoutIdRef.current); + await copyPlaintext(url); + setIsCopied(true); + timeoutIdRef.current = setTimeout(() => setIsCopied(false), 2000); + }} + > + {isCopied ? _t("share|link_copied") : _t("action|copy_link")} + </Button> + {showSocials && <SocialLinks url={url} />} + </div> + </BaseDialog> + ); } -export default class ShareDialog extends React.PureComponent<XOR<Props, EventProps>, IState> { - public constructor(props: XOR<Props, EventProps>) { - super(props); +/** + * Social links to share the link on different platforms. + */ +interface SocialLinksProps { + /** + * The URL to share. + */ + url: string; +} - let permalinkCreator: RoomPermalinkCreator | null = null; - if (props.target instanceof Room) { - permalinkCreator = new RoomPermalinkCreator(props.target); - permalinkCreator.load(); +/** + * The socials to share the link on. + */ +function SocialLinks({ url }: SocialLinksProps): JSX.Element { + return ( + <div className="mx_ShareDialog_social"> + {SOCIALS.map((social) => ( + <a + key={social.name} + href={social.url(url)} + target="_blank" + rel="noreferrer noopener" + title={social.name} + > + <img src={social.img} alt={social.name} /> + </a> + ))} + </div> + ); +} + +/** + * Get the title, url and checkbox label for the dialog based on the target. + * @param target + * @param linkToSpecificEvent + * @param permalinkCreator + */ +function useTargetValues( + target: ShareDialogProps["target"], + linkToSpecificEvent: boolean, + permalinkCreator?: RoomPermalinkCreator, +): { title: string; url: string; checkboxLabel?: string } { + return useMemo(() => { + if (target instanceof URL) return { title: _t("share|title_link"), url: target.toString() }; + if (target instanceof User || target instanceof RoomMember) + return { + title: _t("share|title_user"), + url: makeUserPermalink(target.userId), + }; + + if (target instanceof Room) { + const title = _t("share|title_room"); + const newPermalinkCreator = new RoomPermalinkCreator(target); + newPermalinkCreator.load(); + + const events = target.getLiveTimeline().getEvents(); + return { + title, + url: linkToSpecificEvent + ? newPermalinkCreator.forEvent(events[events.length - 1].getId()!) + : newPermalinkCreator.forShareableRoom(), + ...(events.length > 0 && { checkboxLabel: _t("share|permalink_most_recent") }), + }; } - this.state = { - // MatrixEvent defaults to share linkSpecificEvent - linkSpecificEvent: this.props.target instanceof MatrixEvent, - permalinkCreator, + // MatrixEvent is remaining and should have a permalinkCreator + const url = linkToSpecificEvent + ? permalinkCreator!.forEvent(target.getId()!) + : permalinkCreator!.forShareableRoom(); + return { + title: _t("share|title_message"), + url, + checkboxLabel: _t("share|permalink_message"), }; - } - - public static onLinkClick(e: React.MouseEvent): void { - e.preventDefault(); - selectText(e.currentTarget); - } - - private onLinkSpecificEventCheckboxClick = (): void => { - this.setState({ - linkSpecificEvent: !this.state.linkSpecificEvent, - }); - }; - - private getUrl(): string { - if (this.props.target instanceof URL) { - return this.props.target.toString(); - } else if (this.props.target instanceof Room) { - if (this.state.linkSpecificEvent) { - const events = this.props.target.getLiveTimeline().getEvents(); - return this.state.permalinkCreator!.forEvent(events[events.length - 1].getId()!); - } else { - return this.state.permalinkCreator!.forShareableRoom(); - } - } else if (this.props.target instanceof User || this.props.target instanceof RoomMember) { - return makeUserPermalink(this.props.target.userId); - } else if (this.state.linkSpecificEvent) { - return this.props.permalinkCreator!.forEvent(this.props.target.getId()!); - } else { - return this.props.permalinkCreator!.forShareableRoom(); - } - } - - public render(): React.ReactNode { - let title: string | undefined; - let checkbox: JSX.Element | undefined; - - if (this.props.target instanceof URL) { - title = this.props.customTitle ?? _t("share|title_link"); - } else if (this.props.target instanceof Room) { - title = this.props.customTitle ?? _t("share|title_room"); - - const events = this.props.target.getLiveTimeline().getEvents(); - if (events.length > 0) { - checkbox = ( - <div> - <StyledCheckbox - checked={this.state.linkSpecificEvent} - onChange={this.onLinkSpecificEventCheckboxClick} - > - {_t("share|permalink_most_recent")} - </StyledCheckbox> - </div> - ); - } - } else if (this.props.target instanceof User || this.props.target instanceof RoomMember) { - title = this.props.customTitle ?? _t("share|title_user"); - } else if (this.props.target instanceof MatrixEvent) { - title = this.props.customTitle ?? _t("share|title_message"); - checkbox = ( - <div> - <StyledCheckbox - checked={this.state.linkSpecificEvent} - onChange={this.onLinkSpecificEventCheckboxClick} - > - {_t("share|permalink_message")} - </StyledCheckbox> - </div> - ); - } - - const matrixToUrl = this.getUrl(); - const encodedUrl = encodeURIComponent(matrixToUrl); - - const showQrCode = SettingsStore.getValue(UIFeature.ShareQRCode); - const showSocials = SettingsStore.getValue(UIFeature.ShareSocial); - - let qrSocialSection; - if (showQrCode || showSocials) { - qrSocialSection = ( - <> - <hr /> - <div className="mx_ShareDialog_split"> - {showQrCode && ( - <div className="mx_ShareDialog_qrcode_container"> - <QRCode data={matrixToUrl} width={256} /> - </div> - )} - {showSocials && ( - <div className="mx_ShareDialog_social_container"> - {socials.map((social) => ( - <a - rel="noreferrer noopener" - target="_blank" - key={social.name} - title={social.name} - href={social.url(encodedUrl)} - className="mx_ShareDialog_social_icon" - > - <img src={social.img} alt={social.name} height={64} width={64} /> - </a> - ))} - </div> - )} - </div> - </> - ); - } - - return ( - <BaseDialog - title={title} - className="mx_ShareDialog" - contentId="mx_Dialog_content" - onFinished={this.props.onFinished} - > - {this.props.subtitle && <p>{this.props.subtitle}</p>} - <div className="mx_ShareDialog_content"> - <CopyableText getTextToCopy={() => matrixToUrl}> - <a title={_t("share|link_title")} href={matrixToUrl} onClick={ShareDialog.onLinkClick}> - {matrixToUrl} - </a> - </CopyableText> - {checkbox} - {qrSocialSection} - </div> - </BaseDialog> - ); - } + }, [target, linkToSpecificEvent, permalinkCreator]); } diff --git a/src/components/views/dialogs/UntrustedDeviceDialog.tsx b/src/components/views/dialogs/UntrustedDeviceDialog.tsx index c2bc92e751..4b37032207 100644 --- a/src/components/views/dialogs/UntrustedDeviceDialog.tsx +++ b/src/components/views/dialogs/UntrustedDeviceDialog.tsx @@ -17,9 +17,20 @@ import BaseDialog from "./BaseDialog"; import { IDevice } from "../right_panel/UserInfo"; interface IProps { + /** + * The user whose device is untrusted. + */ user: User; + /** + * The device that is untrusted. + */ device: IDevice; - onFinished(mode?: "legacy" | "sas" | false): void; + /** + * Callback for when the dialog is dismissed. + * If mode is "sas", the user wants to verify the device with SAS. Otherwise, the dialog was dismissed normally. + * @param mode The mode of dismissal. + */ + onFinished(mode?: "sas"): void; } const UntrustedDeviceDialog: React.FC<IProps> = ({ device, user, onFinished }) => { @@ -56,13 +67,10 @@ const UntrustedDeviceDialog: React.FC<IProps> = ({ device, user, onFinished }) = <p>{askToVerifyText}</p> </div> <div className="mx_Dialog_buttons"> - <AccessibleButton kind="primary_outline" onClick={() => onFinished("legacy")}> - {_t("encryption|udd|manual_verification_button")} - </AccessibleButton> <AccessibleButton kind="primary_outline" onClick={() => onFinished("sas")}> {_t("encryption|udd|interactive_verification_button")} </AccessibleButton> - <AccessibleButton kind="primary" onClick={() => onFinished(false)}> + <AccessibleButton kind="primary" onClick={() => onFinished()}> {_t("action|done")} </AccessibleButton> </div> diff --git a/src/components/views/dialogs/devtools/SettingExplorer.tsx b/src/components/views/dialogs/devtools/SettingExplorer.tsx index ed4b64d870..ae37fa3e1c 100644 --- a/src/components/views/dialogs/devtools/SettingExplorer.tsx +++ b/src/components/views/dialogs/devtools/SettingExplorer.tsx @@ -298,7 +298,7 @@ const SettingsList: React.FC<ISettingsListProps> = ({ onBack, onView, onEdit }) <code>{i}</code> </AccessibleButton> <AccessibleButton - alt={_t("devtools|edit_setting")} + title={_t("devtools|edit_setting")} onClick={() => onEdit(i)} className="mx_DevTools_SettingsExplorer_edit" > diff --git a/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx deleted file mode 100644 index 73da6b178c..0000000000 --- a/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx +++ /dev/null @@ -1,99 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. -Copyright 2018, 2019 New Vector Ltd - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React, { useCallback, useEffect, useState } from "react"; -import { logger } from "matrix-js-sdk/src/logger"; -import { MatrixClient } from "matrix-js-sdk/src/matrix"; - -import { _t } from "../../../../languageHandler"; -import DialogButtons from "../../elements/DialogButtons"; -import BaseDialog from "../BaseDialog"; -import Spinner from "../../elements/Spinner"; -import { createCrossSigning } from "../../../../CreateCrossSigning"; - -interface Props { - matrixClient: MatrixClient; - accountPassword?: string; - tokenLogin: boolean; - onFinished: (success?: boolean) => void; -} - -/* - * Walks the user through the process of creating a cross-signing keys. In most - * cases, only a spinner is shown, but for more complex auth like SSO, the user - * may need to complete some steps to proceed. - */ -const CreateCrossSigningDialog: React.FC<Props> = ({ matrixClient, accountPassword, tokenLogin, onFinished }) => { - const [error, setError] = useState(false); - - const bootstrapCrossSigning = useCallback(async () => { - const cryptoApi = matrixClient.getCrypto(); - if (!cryptoApi) return; - - setError(false); - - try { - await createCrossSigning(matrixClient, tokenLogin, accountPassword); - onFinished(true); - } catch (e) { - if (tokenLogin) { - // ignore any failures, we are relying on grace period here - onFinished(false); - return; - } - - setError(true); - logger.error("Error bootstrapping cross-signing", e); - } - }, [matrixClient, tokenLogin, accountPassword, onFinished]); - - const onCancel = useCallback(() => { - onFinished(false); - }, [onFinished]); - - useEffect(() => { - bootstrapCrossSigning(); - }, [bootstrapCrossSigning]); - - let content; - if (error) { - content = ( - <div> - <p>{_t("encryption|unable_to_setup_keys_error")}</p> - <div className="mx_Dialog_buttons"> - <DialogButtons - primaryButton={_t("action|retry")} - onPrimaryButtonClick={bootstrapCrossSigning} - onCancel={onCancel} - /> - </div> - </div> - ); - } else { - content = ( - <div> - <Spinner /> - </div> - ); - } - - return ( - <BaseDialog - className="mx_CreateCrossSigningDialog" - onFinished={onFinished} - title={_t("encryption|bootstrap_title")} - hasCancel={false} - fixedWidth={false} - > - <div>{content}</div> - </BaseDialog> - ); -}; - -export default CreateCrossSigningDialog; diff --git a/src/components/views/dialogs/security/InitialCryptoSetupDialog.tsx b/src/components/views/dialogs/security/InitialCryptoSetupDialog.tsx new file mode 100644 index 0000000000..22635662ce --- /dev/null +++ b/src/components/views/dialogs/security/InitialCryptoSetupDialog.tsx @@ -0,0 +1,71 @@ +/* +Copyright 2024 New Vector Ltd. +Copyright 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2018, 2019 New Vector Ltd + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import React, { useCallback } from "react"; + +import { _t } from "../../../../languageHandler"; +import DialogButtons from "../../elements/DialogButtons"; +import BaseDialog from "../BaseDialog"; +import Spinner from "../../elements/Spinner"; +import { InitialCryptoSetupStore, useInitialCryptoSetupStatus } from "../../../../stores/InitialCryptoSetupStore"; + +interface Props { + onFinished: (success?: boolean) => void; +} + +/* + * Walks the user through the process of creating a cross-signing keys. + * In most cases, only a spinner is shown, but for more + * complex auth like SSO, the user may need to complete some steps to proceed. + */ +export const InitialCryptoSetupDialog: React.FC<Props> = ({ onFinished }) => { + const onRetryClick = useCallback(() => { + InitialCryptoSetupStore.sharedInstance().retry(); + }, []); + + const onCancelClick = useCallback(() => { + onFinished(false); + }, [onFinished]); + + const status = useInitialCryptoSetupStatus(InitialCryptoSetupStore.sharedInstance()); + + let content; + if (status === "error") { + content = ( + <div> + <p>{_t("encryption|unable_to_setup_keys_error")}</p> + <div className="mx_Dialog_buttons"> + <DialogButtons + primaryButton={_t("action|retry")} + onPrimaryButtonClick={onRetryClick} + onCancel={onCancelClick} + /> + </div> + </div> + ); + } else { + content = ( + <div> + <Spinner /> + </div> + ); + } + + return ( + <BaseDialog + className="mx_CreateCrossSigningDialog" + onFinished={onFinished} + title={_t("encryption|bootstrap_title")} + hasCancel={false} + fixedWidth={false} + > + <div>{content}</div> + </BaseDialog> + ); +}; diff --git a/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx b/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx index ec85e72ac9..fddba10948 100644 --- a/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx +++ b/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx @@ -258,7 +258,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps, }); try { const cli = MatrixClientPeg.safeGet(); - const backupInfo = await cli.getKeyBackupVersion(); + const backupInfo = (await cli.getCrypto()?.getKeyBackupInfo()) ?? null; const has4S = await cli.secretStorage.hasKey(); const backupKeyStored = has4S ? await cli.isKeyBackupKeyStored() : null; this.setState({ diff --git a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx index 20d6825b9b..a87b7341e7 100644 --- a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx +++ b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx @@ -1253,7 +1253,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n <span>{filterToLabel(filter)}</span> <AccessibleButton tabIndex={-1} - alt={_t("spotlight_dialog|remove_filter", { + title={_t("spotlight_dialog|remove_filter", { filter: filterToLabel(filter), })} className="mx_SpotlightDialog_filter--close" diff --git a/src/components/views/dialogs/spotlight/TooltipOption.tsx b/src/components/views/dialogs/spotlight/TooltipOption.tsx index 1d60fed5b3..ebb0b4cf06 100644 --- a/src/components/views/dialogs/spotlight/TooltipOption.tsx +++ b/src/components/views/dialogs/spotlight/TooltipOption.tsx @@ -13,15 +13,15 @@ import { useRovingTabIndex } from "../../../../accessibility/RovingTabIndex"; import AccessibleButton, { ButtonProps } from "../../elements/AccessibleButton"; import { Ref } from "../../../../accessibility/roving/types"; -type TooltipOptionProps<T extends keyof JSX.IntrinsicElements> = ButtonProps<T> & { +type TooltipOptionProps<T extends keyof HTMLElementTagNameMap> = ButtonProps<T> & { + className?: string; endAdornment?: ReactNode; inputRef?: Ref; }; -export const TooltipOption = <T extends keyof JSX.IntrinsicElements>({ +export const TooltipOption = <T extends keyof HTMLElementTagNameMap>({ inputRef, className, - element, ...props }: TooltipOptionProps<T>): JSX.Element => { const [onFocus, isActive, ref] = useRovingTabIndex(inputRef); @@ -34,7 +34,6 @@ export const TooltipOption = <T extends keyof JSX.IntrinsicElements>({ tabIndex={-1} aria-selected={isActive} role="option" - element={element as keyof JSX.IntrinsicElements} /> ); }; diff --git a/src/components/views/directory/NetworkDropdown.tsx b/src/components/views/directory/NetworkDropdown.tsx index 43d123676b..a1b1986f47 100644 --- a/src/components/views/directory/NetworkDropdown.tsx +++ b/src/components/views/directory/NetworkDropdown.tsx @@ -168,7 +168,7 @@ export const NetworkDropdown: React.FC<IProps> = ({ protocols, config, setConfig adornment: ( <AccessibleButton className="mx_NetworkDropdown_removeServer" - alt={_t("spotlight|public_rooms|network_dropdown_remove_server_adornment", { roomServer })} + title={_t("spotlight|public_rooms|network_dropdown_remove_server_adornment", { roomServer })} onClick={() => setUserDefinedServers(without(userDefinedServers, roomServer))} /> ), diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx index b8b5297384..8b58f251c3 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -6,7 +6,15 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { ComponentProps, forwardRef, FunctionComponent, HTMLAttributes, InputHTMLAttributes, Ref } from "react"; +import React, { + ComponentProps, + ComponentPropsWithoutRef, + forwardRef, + FunctionComponent, + ReactElement, + KeyboardEvent, + Ref, +} from "react"; import classnames from "classnames"; import { Tooltip } from "@vector-im/compound-web"; @@ -38,20 +46,8 @@ export type AccessibleButtonKind = | "icon_primary" | "icon_primary_outline"; -/** - * This type construct allows us to specifically pass those props down to the element we’re creating that the element - * actually supports. - * - * e.g., if element is set to "a", we’ll support href and target, if it’s set to "input", we support type. - * - * To remain compatible with existing code, we’ll continue to support InputHTMLAttributes<Element> - */ -type DynamicHtmlElementProps<T extends keyof JSX.IntrinsicElements> = - JSX.IntrinsicElements[T] extends HTMLAttributes<{}> ? DynamicElementProps<T> : DynamicElementProps<"div">; -type DynamicElementProps<T extends keyof JSX.IntrinsicElements> = Partial< - Omit<JSX.IntrinsicElements[T], "ref" | "onClick" | "onMouseDown" | "onKeyUp" | "onKeyDown"> -> & - Omit<InputHTMLAttributes<Element>, "onClick">; +type ElementType = keyof HTMLElementTagNameMap; +const defaultElement = "div"; type TooltipProps = ComponentProps<typeof Tooltip>; @@ -60,7 +56,7 @@ type TooltipProps = ComponentProps<typeof Tooltip>; * * Extends props accepted by the underlying element specified using the `element` prop. */ -type Props<T extends keyof JSX.IntrinsicElements> = DynamicHtmlElementProps<T> & { +type Props<T extends ElementType = "div"> = { /** * The base element type. "div" by default. */ @@ -105,14 +101,12 @@ type Props<T extends keyof JSX.IntrinsicElements> = DynamicHtmlElementProps<T> & disableTooltip?: TooltipProps["disabled"]; }; -export type ButtonProps<T extends keyof JSX.IntrinsicElements> = Props<T>; +export type ButtonProps<T extends ElementType> = Props<T> & Omit<ComponentPropsWithoutRef<T>, keyof Props<T>>; /** * Type of the props passed to the element that is rendered by AccessibleButton. */ -interface RenderedElementProps extends React.InputHTMLAttributes<Element> { - ref?: React.Ref<Element>; -} +type RenderedElementProps<T extends ElementType> = React.InputHTMLAttributes<Element> & RefProp<T>; /** * AccessibleButton is a generic wrapper for any element that should be treated @@ -124,9 +118,9 @@ interface RenderedElementProps extends React.InputHTMLAttributes<Element> { * @param {Object} props react element properties * @returns {Object} rendered react */ -const AccessibleButton = forwardRef(function <T extends keyof JSX.IntrinsicElements>( +const AccessibleButton = forwardRef(function <T extends ElementType = typeof defaultElement>( { - element = "div" as T, + element, onClick, children, kind, @@ -141,10 +135,10 @@ const AccessibleButton = forwardRef(function <T extends keyof JSX.IntrinsicEleme onTooltipOpenChange, disableTooltip, ...restProps - }: Props<T>, - ref: Ref<HTMLElement>, + }: ButtonProps<T>, + ref: Ref<HTMLElementTagNameMap[T]>, ): JSX.Element { - const newProps: RenderedElementProps = restProps; + const newProps = restProps as RenderedElementProps<T>; newProps["aria-label"] = newProps["aria-label"] ?? title; if (disabled) { newProps["aria-disabled"] = true; @@ -162,7 +156,7 @@ const AccessibleButton = forwardRef(function <T extends keyof JSX.IntrinsicEleme // And divs which we report as role button to assistive technologies. // Browsers handle space and enter key presses differently and we are only adjusting to the // inconsistencies here - newProps.onKeyDown = (e) => { + newProps.onKeyDown = (e: KeyboardEvent<never>) => { const action = getKeyBindingsManager().getAccessibilityAction(e); switch (action) { @@ -178,7 +172,7 @@ const AccessibleButton = forwardRef(function <T extends keyof JSX.IntrinsicEleme onKeyDown?.(e); } }; - newProps.onKeyUp = (e) => { + newProps.onKeyUp = (e: KeyboardEvent<never>) => { const action = getKeyBindingsManager().getAccessibilityAction(e); switch (action) { @@ -207,7 +201,7 @@ const AccessibleButton = forwardRef(function <T extends keyof JSX.IntrinsicEleme }); // React.createElement expects InputHTMLAttributes - const button = React.createElement(element, newProps, children); + const button = React.createElement(element ?? defaultElement, newProps, children); if (title) { return ( @@ -233,4 +227,15 @@ const AccessibleButton = forwardRef(function <T extends keyof JSX.IntrinsicEleme }; (AccessibleButton as FunctionComponent).displayName = "AccessibleButton"; -export default AccessibleButton; +interface RefProp<T extends ElementType> { + ref?: Ref<HTMLElementTagNameMap[T]>; +} + +interface ButtonComponent { + // With the explicit `element` prop + <C extends ElementType>(props: { element?: C } & ButtonProps<C> & RefProp<C>): ReactElement; + // Without the explicit `element` prop + (props: ButtonProps<"div"> & RefProp<"div">): ReactElement; +} + +export default AccessibleButton as ButtonComponent; diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx index dae452fd5d..56754f14a6 100644 --- a/src/components/views/elements/AppTile.tsx +++ b/src/components/views/elements/AppTile.tsx @@ -116,7 +116,7 @@ interface IState { export default class AppTile extends React.Component<IProps, IState> { public static contextType = MatrixClientContext; - public declare context: ContextType<typeof MatrixClientContext>; + declare public context: ContextType<typeof MatrixClientContext>; public static defaultProps: Partial<IProps> = { waitForIframeLoad: true, diff --git a/src/components/views/elements/EditableItemList.tsx b/src/components/views/elements/EditableItemList.tsx index dc6e6c09a1..ad2d9aceee 100644 --- a/src/components/views/elements/EditableItemList.tsx +++ b/src/components/views/elements/EditableItemList.tsx @@ -133,12 +133,7 @@ export default class EditableItemList<P = {}> extends React.PureComponent<IProps onChange={this.onNewItemChanged} list={this.props.suggestionsListId} /> - <AccessibleButton - onClick={this.onItemAdded} - kind="primary" - type="submit" - disabled={!this.props.newItem} - > + <AccessibleButton onClick={this.onItemAdded} kind="primary" disabled={!this.props.newItem}> {_t("action|add")} </AccessibleButton> </form> diff --git a/src/components/views/elements/EventListSummary.tsx b/src/components/views/elements/EventListSummary.tsx index 7562f992c1..776908375a 100644 --- a/src/components/views/elements/EventListSummary.tsx +++ b/src/components/views/elements/EventListSummary.tsx @@ -73,7 +73,7 @@ export default class EventListSummary extends React.Component< IProps & Required<Pick<IProps, "summaryLength" | "threshold" | "avatarsMaxLength" | "layout">> > { public static contextType = RoomContext; - public declare context: React.ContextType<typeof RoomContext>; + declare public context: React.ContextType<typeof RoomContext>; public static defaultProps = { summaryLength: 1, diff --git a/src/components/views/elements/MiniAvatarUploader.tsx b/src/components/views/elements/MiniAvatarUploader.tsx index cf5a239814..452b206bef 100644 --- a/src/components/views/elements/MiniAvatarUploader.tsx +++ b/src/components/views/elements/MiniAvatarUploader.tsx @@ -12,12 +12,12 @@ import React, { useContext, useRef, useState, MouseEvent, ReactNode } from "reac import { Tooltip } from "@vector-im/compound-web"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import RoomContext from "../../../contexts/RoomContext"; import { useTimeout } from "../../../hooks/useTimeout"; import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; import AccessibleButton from "./AccessibleButton"; import Spinner from "./Spinner"; import { getFileChanged } from "../settings/AvatarSetting.tsx"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; export const AVATAR_SIZE = "52px"; @@ -56,7 +56,7 @@ const MiniAvatarUploader: React.FC<IProps> = ({ const label = hasAvatar || busy ? hasAvatarLabel : noAvatarLabel; - const { room } = useContext(RoomContext); + const { room } = useScopedRoomContext("room"); const canSetAvatar = isUserAvatar || room?.currentState?.maySendStateEvent(EventType.RoomAvatar, cli.getSafeUserId()); if (!canSetAvatar) return <React.Fragment>{children}</React.Fragment>; diff --git a/src/components/views/elements/PersistentApp.tsx b/src/components/views/elements/PersistentApp.tsx index 6fbf0d31bc..5f720dc85e 100644 --- a/src/components/views/elements/PersistentApp.tsx +++ b/src/components/views/elements/PersistentApp.tsx @@ -25,7 +25,7 @@ interface IProps { export default class PersistentApp extends React.Component<IProps> { public static contextType = MatrixClientContext; - public declare context: ContextType<typeof MatrixClientContext>; + declare public context: ContextType<typeof MatrixClientContext>; private room: Room; public constructor(props: IProps, context: ContextType<typeof MatrixClientContext>) { diff --git a/src/components/views/elements/ReplyChain.tsx b/src/components/views/elements/ReplyChain.tsx index 71846d6065..8e10ca3af9 100644 --- a/src/components/views/elements/ReplyChain.tsx +++ b/src/components/views/elements/ReplyChain.tsx @@ -65,7 +65,7 @@ interface IState { // be low as each event being loaded (after the first) is triggered by an explicit user action. export default class ReplyChain extends React.Component<IProps, IState> { public static contextType = RoomContext; - public declare context: React.ContextType<typeof RoomContext>; + declare public context: React.ContextType<typeof RoomContext>; private unmounted = false; private room: Room; diff --git a/src/components/views/elements/RoomAliasField.tsx b/src/components/views/elements/RoomAliasField.tsx index f092f9d25f..faa0ccf1a6 100644 --- a/src/components/views/elements/RoomAliasField.tsx +++ b/src/components/views/elements/RoomAliasField.tsx @@ -33,7 +33,7 @@ interface IState { // Controlled form component wrapping Field for inputting a room alias scoped to a given domain export default class RoomAliasField extends React.PureComponent<IProps, IState> { public static contextType = MatrixClientContext; - public declare context: React.ContextType<typeof MatrixClientContext>; + declare public context: React.ContextType<typeof MatrixClientContext>; private fieldRef = createRef<Field>(); diff --git a/src/components/views/elements/crypto/VerificationQRCode.tsx b/src/components/views/elements/crypto/VerificationQRCode.tsx index 82f4ca20d3..021457bc00 100644 --- a/src/components/views/elements/crypto/VerificationQRCode.tsx +++ b/src/components/views/elements/crypto/VerificationQRCode.tsx @@ -12,7 +12,7 @@ import QRCode from "../QRCode"; interface IProps { /** The data for the QR code. If `undefined`, a spinner is shown. */ - qrCodeBytes: undefined | Buffer; + qrCodeBytes: undefined | Uint8ClampedArray; } export default class VerificationQRCode extends React.PureComponent<IProps> { diff --git a/src/components/views/emojipicker/Emoji.tsx b/src/components/views/emojipicker/Emoji.tsx index c3dfb24bd1..a852122b75 100644 --- a/src/components/views/emojipicker/Emoji.tsx +++ b/src/components/views/emojipicker/Emoji.tsx @@ -31,7 +31,7 @@ class Emoji extends React.PureComponent<IProps> { return ( <RovingAccessibleButton id={this.props.id} - onClick={(ev) => onClick(ev, emoji)} + onClick={(ev: ButtonEvent) => onClick(ev, emoji)} onMouseEnter={() => onMouseEnter(emoji)} onMouseLeave={() => onMouseLeave(emoji)} className="mx_EmojiPicker_item_wrapper" diff --git a/src/components/views/emojipicker/ReactionPicker.tsx b/src/components/views/emojipicker/ReactionPicker.tsx index b62df99e25..bd16634490 100644 --- a/src/components/views/emojipicker/ReactionPicker.tsx +++ b/src/components/views/emojipicker/ReactionPicker.tsx @@ -29,7 +29,7 @@ interface IState { class ReactionPicker extends React.Component<IProps, IState> { public static contextType = RoomContext; - public declare context: React.ContextType<typeof RoomContext>; + declare public context: React.ContextType<typeof RoomContext>; public constructor(props: IProps, context: React.ContextType<typeof RoomContext>) { super(props, context); diff --git a/src/components/views/emojipicker/Search.tsx b/src/components/views/emojipicker/Search.tsx index 87397b6d4b..bce045cb8c 100644 --- a/src/components/views/emojipicker/Search.tsx +++ b/src/components/views/emojipicker/Search.tsx @@ -23,7 +23,7 @@ interface IProps { class Search extends React.PureComponent<IProps> { public static contextType = RovingTabIndexContext; - public declare context: React.ContextType<typeof RovingTabIndexContext>; + declare public context: React.ContextType<typeof RovingTabIndexContext>; private inputRef = React.createRef<HTMLInputElement>(); diff --git a/src/components/views/location/LocationPicker.tsx b/src/components/views/location/LocationPicker.tsx index c45521830d..e812f1c6bd 100644 --- a/src/components/views/location/LocationPicker.tsx +++ b/src/components/views/location/LocationPicker.tsx @@ -42,7 +42,7 @@ const isSharingOwnLocation = (shareType: LocationShareType): boolean => class LocationPicker extends React.Component<ILocationPickerProps, IState> { public static contextType = MatrixClientContext; - public declare context: React.ContextType<typeof MatrixClientContext>; + declare public context: React.ContextType<typeof MatrixClientContext>; private map?: maplibregl.Map; private geolocate?: maplibregl.GeolocateControl; private marker?: maplibregl.Marker; diff --git a/src/components/views/messages/EditHistoryMessage.tsx b/src/components/views/messages/EditHistoryMessage.tsx index 8316d0835b..fb6f04c08f 100644 --- a/src/components/views/messages/EditHistoryMessage.tsx +++ b/src/components/views/messages/EditHistoryMessage.tsx @@ -45,7 +45,7 @@ interface IState { export default class EditHistoryMessage extends React.PureComponent<IProps, IState> { public static contextType = MatrixClientContext; - public declare context: React.ContextType<typeof MatrixClientContext>; + declare public context: React.ContextType<typeof MatrixClientContext>; private content = createRef<HTMLDivElement>(); private pills = new ReactRootManager(); diff --git a/src/components/views/messages/MAudioBody.tsx b/src/components/views/messages/MAudioBody.tsx index 326b1c38c8..bf0cc9ee54 100644 --- a/src/components/views/messages/MAudioBody.tsx +++ b/src/components/views/messages/MAudioBody.tsx @@ -30,7 +30,7 @@ interface IState { export default class MAudioBody extends React.PureComponent<IBodyProps, IState> { public static contextType = RoomContext; - public declare context: React.ContextType<typeof RoomContext>; + declare public context: React.ContextType<typeof RoomContext>; public state: IState = {}; diff --git a/src/components/views/messages/MFileBody.tsx b/src/components/views/messages/MFileBody.tsx index fde3ea0184..1235b73b4b 100644 --- a/src/components/views/messages/MFileBody.tsx +++ b/src/components/views/messages/MFileBody.tsx @@ -102,7 +102,7 @@ interface IState { export default class MFileBody extends React.Component<IProps, IState> { public static contextType = RoomContext; - public declare context: React.ContextType<typeof RoomContext>; + declare public context: React.ContextType<typeof RoomContext>; public state: IState = {}; diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index d8cac8e28b..c3aeee1a54 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -51,13 +51,14 @@ interface IState { naturalHeight: number; }; hover: boolean; + focus: boolean; showImage: boolean; placeholder: Placeholder; } export default class MImageBody extends React.Component<IBodyProps, IState> { public static contextType = RoomContext; - public declare context: React.ContextType<typeof RoomContext>; + declare public context: React.ContextType<typeof RoomContext>; private unmounted = false; private image = createRef<HTMLImageElement>(); @@ -71,6 +72,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> { imgError: false, imgLoaded: false, hover: false, + focus: false, showImage: SettingsStore.getValue("showImages"), placeholder: Placeholder.NoImage, }; @@ -120,30 +122,29 @@ export default class MImageBody extends React.Component<IBodyProps, IState> { } }; - protected onImageEnter = (e: React.MouseEvent<HTMLImageElement>): void => { - this.setState({ hover: true }); - - if ( + private get shouldAutoplay(): boolean { + return !( !this.state.contentUrl || !this.state.showImage || !this.state.isAnimated || SettingsStore.getValue("autoplayGifs") - ) { - return; - } - const imgElement = e.currentTarget; - imgElement.src = this.state.contentUrl; + ); + } + + protected onImageEnter = (): void => { + this.setState({ hover: true }); }; - protected onImageLeave = (e: React.MouseEvent<HTMLImageElement>): void => { + protected onImageLeave = (): void => { this.setState({ hover: false }); + }; - const url = this.state.thumbUrl ?? this.state.contentUrl; - if (!url || !this.state.showImage || !this.state.isAnimated || SettingsStore.getValue("autoplayGifs")) { - return; - } - const imgElement = e.currentTarget; - imgElement.src = url; + private onFocus = (): void => { + this.setState({ focus: true }); + }; + + private onBlur = (): void => { + this.setState({ focus: false }); }; private reconnectedListener = createReconnectedListener((): void => { @@ -275,7 +276,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> { } const content = this.props.mxEvent.getContent<ImageContent>(); - let isAnimated = mayBeAnimated(content.info?.mimetype); + let isAnimated = content.info?.["org.matrix.msc4230.is_animated"] ?? mayBeAnimated(content.info?.mimetype); // If there is no included non-animated thumbnail then we will generate our own, we can't depend on the server // because 1. encryption and 2. we can't ask the server specifically for a non-animated thumbnail. @@ -298,8 +299,15 @@ export default class MImageBody extends React.Component<IBodyProps, IState> { } try { - const blob = await this.props.mediaEventHelper!.sourceBlob.value; - if (!(await blobIsAnimated(content.info?.mimetype, blob))) { + // If we didn't receive the MSC4230 is_animated flag + // then we need to check if the image is animated by downloading it. + if ( + content.info?.["org.matrix.msc4230.is_animated"] === false || + !(await blobIsAnimated( + content.info?.mimetype, + await this.props.mediaEventHelper!.sourceBlob.value, + )) + ) { isAnimated = false; } @@ -463,14 +471,20 @@ export default class MImageBody extends React.Component<IBodyProps, IState> { let showPlaceholder = Boolean(placeholder); + const hoverOrFocus = this.state.hover || this.state.focus; if (thumbUrl && !this.state.imgError) { + let url = thumbUrl; + if (hoverOrFocus && this.shouldAutoplay) { + url = this.state.contentUrl!; + } + // Restrict the width of the thumbnail here, otherwise it will fill the container // which has the same width as the timeline // mx_MImageBody_thumbnail resizes img to exactly container size img = ( <img className="mx_MImageBody_thumbnail" - src={thumbUrl} + src={url} ref={this.image} alt={content.body} onError={this.onImageError} @@ -486,13 +500,13 @@ export default class MImageBody extends React.Component<IBodyProps, IState> { showPlaceholder = false; // because we're hiding the image, so don't show the placeholder. } - if (this.state.isAnimated && !SettingsStore.getValue("autoplayGifs") && !this.state.hover) { + if (this.state.isAnimated && !SettingsStore.getValue("autoplayGifs") && !hoverOrFocus) { // XXX: Arguably we may want a different label when the animated image is WEBP and not GIF gifLabel = <p className="mx_MImageBody_gifLabel">GIF</p>; } let banner: ReactNode | undefined; - if (this.state.showImage && this.state.hover) { + if (this.state.showImage && hoverOrFocus) { banner = this.getBanner(content); } @@ -561,7 +575,13 @@ export default class MImageBody extends React.Component<IBodyProps, IState> { protected wrapImage(contentUrl: string | null | undefined, children: JSX.Element): ReactNode { if (contentUrl) { return ( - <a href={contentUrl} target={this.props.forExport ? "_blank" : undefined} onClick={this.onClick}> + <a + href={contentUrl} + target={this.props.forExport ? "_blank" : undefined} + onClick={this.onClick} + onFocus={this.onFocus} + onBlur={this.onBlur} + > {children} </a> ); @@ -650,17 +670,14 @@ export default class MImageBody extends React.Component<IBodyProps, IState> { } interface PlaceholderIProps { - hover?: boolean; maxWidth?: number; } export class HiddenImagePlaceholder extends React.PureComponent<PlaceholderIProps> { public render(): React.ReactNode { const maxWidth = this.props.maxWidth ? this.props.maxWidth + "px" : null; - let className = "mx_HiddenImagePlaceholder"; - if (this.props.hover) className += " mx_HiddenImagePlaceholder_hover"; return ( - <div className={className} style={{ maxWidth: `min(100%, ${maxWidth}px)` }}> + <div className="mx_HiddenImagePlaceholder" style={{ maxWidth: `min(100%, ${maxWidth}px)` }}> <div className="mx_HiddenImagePlaceholder_button"> <span className="mx_HiddenImagePlaceholder_eye" /> <span>{_t("timeline|m.image|show_image")}</span> diff --git a/src/components/views/messages/MLocationBody.tsx b/src/components/views/messages/MLocationBody.tsx index b226476fa8..7735e64b03 100644 --- a/src/components/views/messages/MLocationBody.tsx +++ b/src/components/views/messages/MLocationBody.tsx @@ -30,7 +30,7 @@ interface IState { export default class MLocationBody extends React.Component<IBodyProps, IState> { public static contextType = MatrixClientContext; - public declare context: React.ContextType<typeof MatrixClientContext>; + declare public context: React.ContextType<typeof MatrixClientContext>; private unmounted = false; private mapId: string; diff --git a/src/components/views/messages/MPollBody.tsx b/src/components/views/messages/MPollBody.tsx index 9e173e5f4a..ba3962779f 100644 --- a/src/components/views/messages/MPollBody.tsx +++ b/src/components/views/messages/MPollBody.tsx @@ -139,7 +139,7 @@ export function launchPollEditor(mxEvent: MatrixEvent, getRelationsForEvent?: Ge export default class MPollBody extends React.Component<IBodyProps, IState> { public static contextType = MatrixClientContext; - public declare context: React.ContextType<typeof MatrixClientContext>; + declare public context: React.ContextType<typeof MatrixClientContext>; private seenEventIds: string[] = []; // Events we have already seen public constructor(props: IBodyProps, context: React.ContextType<typeof MatrixClientContext>) { diff --git a/src/components/views/messages/MPollEndBody.tsx b/src/components/views/messages/MPollEndBody.tsx index 94671fea12..1129b3538e 100644 --- a/src/components/views/messages/MPollEndBody.tsx +++ b/src/components/views/messages/MPollEndBody.tsx @@ -90,7 +90,7 @@ export const MPollEndBody = React.forwardRef<any, IBodyProps>(({ mxEvent, ...pro const { pollStartEvent, isLoadingPollStartEvent } = usePollStartEvent(mxEvent); if (!pollStartEvent) { - const pollEndFallbackMessage = M_TEXT.findIn(mxEvent.getContent()) || textForEvent(mxEvent, cli); + const pollEndFallbackMessage = M_TEXT.findIn<string>(mxEvent.getContent()) || textForEvent(mxEvent, cli); return ( <> <PollIcon className="mx_MPollEndBody_icon" /> diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx index 4036b9ddec..822d2c3f59 100644 --- a/src/components/views/messages/MVideoBody.tsx +++ b/src/components/views/messages/MVideoBody.tsx @@ -34,7 +34,7 @@ interface IState { export default class MVideoBody extends React.PureComponent<IBodyProps, IState> { public static contextType = RoomContext; - public declare context: React.ContextType<typeof RoomContext>; + declare public context: React.ContextType<typeof RoomContext>; private videoRef = React.createRef<HTMLVideoElement>(); private sizeWatcher?: string; diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx index fdd0200429..579db054e9 100644 --- a/src/components/views/messages/MessageActionBar.tsx +++ b/src/components/views/messages/MessageActionBar.tsx @@ -58,7 +58,6 @@ import { ALTERNATE_KEY_NAME } from "../../../accessibility/KeyboardShortcuts"; import { Action } from "../../../dispatcher/actions"; import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload"; import { GetRelationsForEvent, IEventTileType } from "../rooms/EventTile"; -import { VoiceBroadcastInfoEventType } from "../../../voice-broadcast/types"; import { ButtonEvent } from "../elements/AccessibleButton"; import PinningUtils from "../../../utils/PinningUtils"; import PosthogTrackers from "../../../PosthogTrackers.ts"; @@ -262,7 +261,7 @@ interface IMessageActionBarProps { export default class MessageActionBar extends React.PureComponent<IMessageActionBarProps> { public static contextType = RoomContext; - public declare context: React.ContextType<typeof RoomContext>; + declare public context: React.ContextType<typeof RoomContext>; public componentDidMount(): void { if (this.props.mxEvent.status && this.props.mxEvent.status !== EventStatus.SENT) { @@ -354,8 +353,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction * until cross-platform support * (PSF-1041) */ - !M_BEACON_INFO.matches(this.props.mxEvent.getType()) && - !(this.props.mxEvent.getType() === VoiceBroadcastInfoEventType); + !M_BEACON_INFO.matches(this.props.mxEvent.getType()); return inNotThreadTimeline && isAllowedMessageType; } @@ -437,7 +435,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction <RovingAccessibleButton className="mx_MessageActionBar_iconButton" title={isPinned ? _t("action|unpin") : _t("action|pin")} - onClick={(e) => this.onPinClick(e, isPinned)} + onClick={(e: ButtonEvent) => this.onPinClick(e, isPinned)} onContextMenu={(e: ButtonEvent) => this.onPinClick(e, isPinned)} key="pin" placement="left" diff --git a/src/components/views/messages/MessageEvent.tsx b/src/components/views/messages/MessageEvent.tsx index 60fcce6493..d1a1c59141 100644 --- a/src/components/views/messages/MessageEvent.tsx +++ b/src/components/views/messages/MessageEvent.tsx @@ -41,7 +41,6 @@ import MjolnirBody from "./MjolnirBody"; import MBeaconBody from "./MBeaconBody"; import { DecryptionFailureBody } from "./DecryptionFailureBody"; import { GetRelationsForEvent, IEventTileOps } from "../rooms/EventTile"; -import { VoiceBroadcastBody, VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../../../voice-broadcast"; // onMessageAllowed is handled internally interface IProps extends Omit<IBodyProps, "onMessageAllowed" | "mediaEventHelper"> { @@ -85,7 +84,7 @@ export default class MessageEvent extends React.Component<IProps> implements IMe private evTypes = new Map<string, React.ComponentType<IBodyProps>>(baseEvTypes.entries()); public static contextType = MatrixClientContext; - public declare context: React.ContextType<typeof MatrixClientContext>; + declare public context: React.ContextType<typeof MatrixClientContext>; public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) { super(props, context); @@ -276,10 +275,6 @@ export default class MessageEvent extends React.Component<IProps> implements IMe if (M_LOCATION.matches(type) || (type === EventType.RoomMessage && msgtype === MsgType.Location)) { BodyType = MLocationBody; } - - if (type === VoiceBroadcastInfoEventType && content?.state === VoiceBroadcastInfoState.Started) { - BodyType = VoiceBroadcastBody; - } } if (SettingsStore.getValue("feature_mjolnir")) { diff --git a/src/components/views/messages/ReactionsRow.tsx b/src/components/views/messages/ReactionsRow.tsx index eba9499606..605e6a7dfe 100644 --- a/src/components/views/messages/ReactionsRow.tsx +++ b/src/components/views/messages/ReactionsRow.tsx @@ -75,7 +75,7 @@ interface IState { export default class ReactionsRow extends React.PureComponent<IProps, IState> { public static contextType = RoomContext; - public declare context: React.ContextType<typeof RoomContext>; + declare public context: React.ContextType<typeof RoomContext>; public constructor(props: IProps, context: React.ContextType<typeof RoomContext>) { super(props, context); diff --git a/src/components/views/messages/ReactionsRowButton.tsx b/src/components/views/messages/ReactionsRowButton.tsx index 4a1d8d67fe..709edeffd8 100644 --- a/src/components/views/messages/ReactionsRowButton.tsx +++ b/src/components/views/messages/ReactionsRowButton.tsx @@ -38,7 +38,7 @@ export interface IProps { export default class ReactionsRowButton extends React.PureComponent<IProps> { public static contextType = MatrixClientContext; - public declare context: React.ContextType<typeof MatrixClientContext>; + declare public context: React.ContextType<typeof MatrixClientContext>; public onClick = (): void => { const { mxEvent, myReactionEvent, content } = this.props; diff --git a/src/components/views/messages/ReactionsRowButtonTooltip.tsx b/src/components/views/messages/ReactionsRowButtonTooltip.tsx index 9790356762..5f407e2e20 100644 --- a/src/components/views/messages/ReactionsRowButtonTooltip.tsx +++ b/src/components/views/messages/ReactionsRowButtonTooltip.tsx @@ -28,7 +28,7 @@ interface IProps { export default class ReactionsRowButtonTooltip extends React.PureComponent<PropsWithChildren<IProps>> { public static contextType = MatrixClientContext; - public declare context: React.ContextType<typeof MatrixClientContext>; + declare public context: React.ContextType<typeof MatrixClientContext>; public render(): React.ReactNode { const { content, reactionEvents, mxEvent, children } = this.props; diff --git a/src/components/views/messages/RoomPredecessorTile.tsx b/src/components/views/messages/RoomPredecessorTile.tsx index 2e8633febd..afc8142234 100644 --- a/src/components/views/messages/RoomPredecessorTile.tsx +++ b/src/components/views/messages/RoomPredecessorTile.tsx @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { useCallback, useContext } from "react"; +import React, { useCallback } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import { MatrixEvent, Room, RoomState } from "matrix-js-sdk/src/matrix"; @@ -18,10 +18,10 @@ import { _t } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import EventTileBubble from "./EventTileBubble"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; -import RoomContext from "../../../contexts/RoomContext"; import { useRoomState } from "../../../hooks/useRoomState"; import SettingsStore from "../../../settings/SettingsStore"; import MatrixToPermalinkConstructor from "../../../utils/permalinks/MatrixToPermalinkConstructor"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; interface IProps { /** The m.room.create MatrixEvent that this tile represents */ @@ -40,7 +40,7 @@ export const RoomPredecessorTile: React.FC<IProps> = ({ mxEvent, timestamp }) => // the information inside mxEvent. This allows us the flexibility later to // use a different predecessor (e.g. through MSC3946) and still display it // in the timeline location of the create event. - const roomContext = useContext(RoomContext); + const roomContext = useScopedRoomContext("room"); const predecessor = useRoomState( roomContext.room, useCallback( diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 0c05236176..ae99754cba 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -52,10 +52,8 @@ export default class TextualBody extends React.Component<IBodyProps, IState> { private tooltips = new ReactRootManager(); private reactRoots = new ReactRootManager(); - private ref = createRef<HTMLDivElement>(); - public static contextType = RoomContext; - public declare context: React.ContextType<typeof RoomContext>; + declare public context: React.ContextType<typeof RoomContext>; public state = { links: [], @@ -86,7 +84,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> { if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") { // Handle expansion and add buttons - const pres = this.ref.current?.getElementsByTagName("pre"); + const pres = [...content.getElementsByTagName("pre")]; if (pres && pres.length > 0) { for (let i = 0; i < pres.length; i++) { // If there already is a div wrapping the codeblock we want to skip this. @@ -115,13 +113,14 @@ export default class TextualBody extends React.Component<IBodyProps, IState> { root.className = "mx_EventTile_pre_container"; // Insert containing div in place of <pre> block - pre.parentNode?.replaceChild(root, pre); + pre.replaceWith(root); this.reactRoots.render( <StrictMode> <CodeBlock onHeightChanged={this.props.onHeightChanged}>{pre}</CodeBlock> </StrictMode>, root, + pre, ); } @@ -196,10 +195,9 @@ export default class TextualBody extends React.Component<IBodyProps, IState> { </StrictMode> ); - this.reactRoots.render(spoiler, spoilerContainer); - - node.parentNode?.replaceChild(spoilerContainer, node); + this.reactRoots.render(spoiler, spoilerContainer, node); + node.replaceWith(spoilerContainer); node = spoilerContainer; } @@ -479,12 +477,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> { if (isEmote) { return ( - <div - className="mx_MEmoteBody mx_EventTile_content" - onClick={this.onBodyLinkClick} - dir="auto" - ref={this.ref} - > + <div className="mx_MEmoteBody mx_EventTile_content" onClick={this.onBodyLinkClick} dir="auto"> * <span className="mx_MEmoteBody_sender" onClick={this.onEmoteSenderClick}> {mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()} @@ -497,7 +490,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> { } if (isNotice) { return ( - <div className="mx_MNoticeBody mx_EventTile_content" onClick={this.onBodyLinkClick} ref={this.ref}> + <div className="mx_MNoticeBody mx_EventTile_content" onClick={this.onBodyLinkClick}> {body} {widgets} </div> @@ -505,14 +498,14 @@ export default class TextualBody extends React.Component<IBodyProps, IState> { } if (isCaption) { return ( - <div className="mx_MTextBody mx_EventTile_caption" onClick={this.onBodyLinkClick} ref={this.ref}> + <div className="mx_MTextBody mx_EventTile_caption" onClick={this.onBodyLinkClick}> {body} {widgets} </div> ); } return ( - <div className="mx_MTextBody mx_EventTile_content" onClick={this.onBodyLinkClick} ref={this.ref}> + <div className="mx_MTextBody mx_EventTile_content" onClick={this.onBodyLinkClick}> {body} {widgets} </div> diff --git a/src/components/views/messages/TextualEvent.tsx b/src/components/views/messages/TextualEvent.tsx index 1c1ba26d08..1c54963f76 100644 --- a/src/components/views/messages/TextualEvent.tsx +++ b/src/components/views/messages/TextualEvent.tsx @@ -19,7 +19,7 @@ interface IProps { export default class TextualEvent extends React.Component<IProps> { public static contextType = RoomContext; - public declare context: React.ContextType<typeof RoomContext>; + declare public context: React.ContextType<typeof RoomContext>; public render(): React.ReactNode { const text = TextForEvent.textForEvent( diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx index af7106f9c5..d6161e9434 100644 --- a/src/components/views/right_panel/PinnedMessagesCard.tsx +++ b/src/components/views/right_panel/PinnedMessagesCard.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { useCallback, useEffect, JSX } from "react"; +import React, { useCallback, useEffect, JSX, useContext } from "react"; import { Room, MatrixEvent } from "matrix-js-sdk/src/matrix"; import { Button, Separator } from "@vector-im/compound-web"; import classNames from "classnames"; @@ -18,7 +18,7 @@ import Spinner from "../elements/Spinner"; import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; import { PinnedEventTile } from "../rooms/PinnedEventTile"; import { useRoomState } from "../../../hooks/useRoomState"; -import RoomContext, { TimelineRenderingType, useRoomContext } from "../../../contexts/RoomContext"; +import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext"; import { ReadPinsEventId } from "./types"; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import { filterBoolean } from "../../../utils/arrays"; @@ -27,6 +27,7 @@ import { UnpinAllDialog } from "../dialogs/UnpinAllDialog"; import EmptyState from "./EmptyState"; import { usePinnedEvents, useReadPinnedEvents, useSortedFetchedPinnedEvents } from "../../../hooks/usePinnedEvents"; import PinningUtils from "../../../utils/PinningUtils.ts"; +import { ScopedRoomContextProvider } from "../../../contexts/ScopedRoomContext.tsx"; /** * List the pinned messages in a room inside a Card. @@ -48,7 +49,7 @@ interface PinnedMessagesCardProps { export function PinnedMessagesCard({ room, onClose, permalinkCreator }: PinnedMessagesCardProps): JSX.Element { const cli = useMatrixClientContext(); - const roomContext = useRoomContext(); + const roomContext = useContext(RoomContext); const pinnedEventIds = usePinnedEvents(room); const readPinnedEvents = useReadPinnedEvents(room); const pinnedEvents = useSortedFetchedPinnedEvents(room, pinnedEventIds); @@ -89,14 +90,9 @@ export function PinnedMessagesCard({ room, onClose, permalinkCreator }: PinnedMe className="mx_PinnedMessagesCard" onClose={onClose} > - <RoomContext.Provider - value={{ - ...roomContext, - timelineRenderingType: TimelineRenderingType.Pinned, - }} - > + <ScopedRoomContextProvider {...roomContext} timelineRenderingType={TimelineRenderingType.Pinned}> {content} - </RoomContext.Provider> + </ScopedRoomContextProvider> </BaseCard> ); } diff --git a/src/components/views/right_panel/RoomSummaryCard.tsx b/src/components/views/right_panel/RoomSummaryCard.tsx index 664977bbe2..c8dd0b9738 100644 --- a/src/components/views/right_panel/RoomSummaryCard.tsx +++ b/src/components/views/right_panel/RoomSummaryCard.tsx @@ -47,11 +47,11 @@ import RoomAvatar from "../avatars/RoomAvatar"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases"; import Modal from "../../../Modal"; -import ShareDialog from "../dialogs/ShareDialog"; +import { ShareDialog } from "../dialogs/ShareDialog"; import { useEventEmitterState } from "../../../hooks/useEventEmitter"; import { E2EStatus } from "../../../utils/ShieldUtils"; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; -import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext"; +import { TimelineRenderingType } from "../../../contexts/RoomContext"; import RoomName from "../elements/RoomName"; import ExportDialog from "../dialogs/ExportDialog"; import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; @@ -76,6 +76,7 @@ import { useTransition } from "../../../hooks/useTransition"; import { isVideoRoom as calcIsVideoRoom } from "../../../utils/video-rooms"; import { usePinnedEvents } from "../../../hooks/usePinnedEvents"; import { ReleaseAnnouncement } from "../../structures/ReleaseAnnouncement.tsx"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; interface IProps { room: Room; @@ -86,7 +87,7 @@ interface IProps { } const onRoomMembersClick = (): void => { - RightPanelStore.instance.pushCard({ phase: RightPanelPhases.RoomMemberList }, true); + RightPanelStore.instance.pushCard({ phase: RightPanelPhases.MemberList }, true); }; const onRoomThreadsClick = (): void => { @@ -232,7 +233,7 @@ const RoomSummaryCard: React.FC<IProps> = ({ }; const isRoomEncrypted = useIsEncrypted(cli, room); - const roomContext = useContext(RoomContext); + const roomContext = useScopedRoomContext("e2eStatus", "timelineRenderingType"); const e2eStatus = roomContext.e2eStatus; const isVideoRoom = calcIsVideoRoom(room); diff --git a/src/components/views/right_panel/TimelineCard.tsx b/src/components/views/right_panel/TimelineCard.tsx index e0988eeaa5..f62319f3cd 100644 --- a/src/components/views/right_panel/TimelineCard.tsx +++ b/src/components/views/right_panel/TimelineCard.tsx @@ -38,6 +38,7 @@ import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import Measured from "../elements/Measured"; import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import { SdkContextClass } from "../../../contexts/SDKContext"; +import { ScopedRoomContextProvider } from "../../../contexts/ScopedRoomContext.tsx"; interface IProps { room: Room; @@ -68,7 +69,7 @@ interface IState { export default class TimelineCard extends React.Component<IProps, IState> { public static contextType = RoomContext; - public declare context: React.ContextType<typeof RoomContext>; + declare public context: React.ContextType<typeof RoomContext>; private dispatcherRef?: string; private layoutWatcherRef?: string; @@ -199,13 +200,11 @@ export default class TimelineCard extends React.Component<IProps, IState> { const showComposer = myMembership === KnownMembership.Join; return ( - <RoomContext.Provider - value={{ - ...this.context, - timelineRenderingType: this.props.timelineRenderingType ?? this.context.timelineRenderingType, - liveTimeline: this.props.timelineSet?.getLiveTimeline(), - narrow: this.state.narrow, - }} + <ScopedRoomContextProvider + {...this.context} + timelineRenderingType={this.props.timelineRenderingType ?? this.context.timelineRenderingType} + liveTimeline={this.props.timelineSet?.getLiveTimeline()} + narrow={this.state.narrow} > <BaseCard className={this.props.classNames} @@ -255,7 +254,7 @@ export default class TimelineCard extends React.Component<IProps, IState> { /> )} </BaseCard> - </RoomContext.Provider> + </ScopedRoomContextProvider> ); } } diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index d07b3566e2..591e2327ae 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -63,7 +63,7 @@ import PowerSelector from "../elements/PowerSelector"; import MemberAvatar from "../avatars/MemberAvatar"; import PresenceLabel from "../rooms/PresenceLabel"; import BulkRedactDialog from "../dialogs/BulkRedactDialog"; -import ShareDialog from "../dialogs/ShareDialog"; +import { ShareDialog } from "../dialogs/ShareDialog"; import ErrorDialog from "../dialogs/ErrorDialog"; import QuestionDialog from "../dialogs/QuestionDialog"; import ConfirmUserActionDialog from "../dialogs/ConfirmUserActionDialog"; @@ -1739,13 +1739,13 @@ export const UserInfoHeader: React.FC<{ interface IProps { user: Member; room?: Room; - phase: RightPanelPhases.RoomMemberInfo | RightPanelPhases.SpaceMemberInfo | RightPanelPhases.EncryptionPanel; + phase: RightPanelPhases.MemberInfo | RightPanelPhases.EncryptionPanel; onClose(): void; verificationRequest?: VerificationRequest; verificationRequestPromise?: Promise<VerificationRequest>; } -const UserInfo: React.FC<IProps> = ({ user, room, onClose, phase = RightPanelPhases.RoomMemberInfo, ...props }) => { +const UserInfo: React.FC<IProps> = ({ user, room, onClose, phase = RightPanelPhases.MemberInfo, ...props }) => { const cli = useContext(MatrixClientContext); // fetch latest room member if we have a room, so we don't show historical information, falling back to user @@ -1767,8 +1767,6 @@ const UserInfo: React.FC<IProps> = ({ user, room, onClose, phase = RightPanelPha // We have no previousPhase for when viewing a UserInfo without a Room at this time if (room && phase === RightPanelPhases.EncryptionPanel) { cardState = { member }; - } else if (room?.isSpaceRoom()) { - cardState = { spaceId: room.roomId }; } const onEncryptionPanelClose = (): void => { @@ -1777,8 +1775,7 @@ const UserInfo: React.FC<IProps> = ({ user, room, onClose, phase = RightPanelPha let content: JSX.Element | undefined; switch (phase) { - case RightPanelPhases.RoomMemberInfo: - case RightPanelPhases.SpaceMemberInfo: + case RightPanelPhases.MemberInfo: content = ( <BasicUserInfo room={room as Room} @@ -1823,7 +1820,7 @@ const UserInfo: React.FC<IProps> = ({ user, room, onClose, phase = RightPanelPha closeLabel={closeLabel} cardState={cardState} onBack={(ev: ButtonEvent) => { - if (RightPanelStore.instance.previousCard.phase === RightPanelPhases.RoomMemberList) { + if (RightPanelStore.instance.previousCard.phase === RightPanelPhases.MemberList) { PosthogTrackers.trackInteraction("WebRightPanelRoomUserInfoBackButton", ev); } }} diff --git a/src/components/views/right_panel/VerificationPanel.tsx b/src/components/views/right_panel/VerificationPanel.tsx index 7e04af7824..6f8295dece 100644 --- a/src/components/views/right_panel/VerificationPanel.tsx +++ b/src/components/views/right_panel/VerificationPanel.tsx @@ -46,7 +46,7 @@ interface IState { * We attempt to calculate this once the verification request transitions into the "Ready" phase. If the other * side cannot scan QR codes, it will remain `undefined`. */ - qrCodeBytes: Buffer | undefined; + qrCodeBytes: Uint8ClampedArray | undefined; sasEvent: ShowSasCallbacks | null; emojiButtonClicked?: boolean; diff --git a/src/components/views/room_settings/AliasSettings.tsx b/src/components/views/room_settings/AliasSettings.tsx index 3c1a745530..0bb29b7f89 100644 --- a/src/components/views/room_settings/AliasSettings.tsx +++ b/src/components/views/room_settings/AliasSettings.tsx @@ -94,7 +94,7 @@ interface IState { export default class AliasSettings extends React.Component<IProps, IState> { public static contextType = MatrixClientContext; - public declare context: ContextType<typeof MatrixClientContext>; + declare public context: ContextType<typeof MatrixClientContext>; public static defaultProps = { canSetAliases: false, diff --git a/src/components/views/rooms/Autocomplete.tsx b/src/components/views/rooms/Autocomplete.tsx index 423b5c6272..3ffd6648ea 100644 --- a/src/components/views/rooms/Autocomplete.tsx +++ b/src/components/views/rooms/Autocomplete.tsx @@ -49,7 +49,7 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> { private completionRefs: Record<string, RefObject<HTMLElement>> = {}; public static contextType = RoomContext; - public declare context: React.ContextType<typeof RoomContext>; + declare public context: React.ContextType<typeof RoomContext>; public constructor(props: IProps, context: React.ContextType<typeof RoomContext>) { super(props, context); diff --git a/src/components/views/rooms/EditMessageComposer.tsx b/src/components/views/rooms/EditMessageComposer.tsx index d62a451b8b..f4b5c3698e 100644 --- a/src/components/views/rooms/EditMessageComposer.tsx +++ b/src/components/views/rooms/EditMessageComposer.tsx @@ -43,25 +43,6 @@ import { attachMentions, attachRelation } from "./SendMessageComposer"; import { filterBoolean } from "../../../utils/arrays"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -function getHtmlReplyFallback(mxEvent: MatrixEvent): string { - const html = mxEvent.getContent().formatted_body; - if (!html) { - return ""; - } - const rootNode = new DOMParser().parseFromString(html, "text/html").body; - const mxReply = rootNode.querySelector("mx-reply"); - return (mxReply && mxReply.outerHTML) || ""; -} - -function getTextReplyFallback(mxEvent: MatrixEvent): string { - const body: string = mxEvent.getContent().body; - const lines = body.split("\n").map((l) => l.trim()); - if (lines.length > 2 && lines[0].startsWith("> ") && lines[1].length === 0) { - return `${lines[0]}\n\n`; - } - return ""; -} - // exported for tests export function createEditContent( model: EditorModel, @@ -72,15 +53,6 @@ export function createEditContent( if (isEmote) { model = stripEmoteCommand(model); } - const isReply = !!editedEvent.replyEventId; - let plainPrefix = ""; - let htmlPrefix = ""; - - if (isReply) { - plainPrefix = getTextReplyFallback(editedEvent); - htmlPrefix = getHtmlReplyFallback(editedEvent); - } - const body = textSerialize(model); const newContent: RoomMessageEventContent = { @@ -89,19 +61,18 @@ export function createEditContent( }; const contentBody: RoomMessageTextEventContent & Omit<ReplacementEvent<RoomMessageEventContent>, "m.relates_to"> = { "msgtype": newContent.msgtype, - "body": `${plainPrefix} * ${body}`, + "body": `* ${body}`, "m.new_content": newContent, }; const formattedBody = htmlSerializeIfNeeded(model, { - forceHTML: isReply, useMarkdown: SettingsStore.getValue("MessageComposerInput.useMarkdown"), }); if (formattedBody) { newContent.format = "org.matrix.custom.html"; newContent.formatted_body = formattedBody; contentBody.format = newContent.format; - contentBody.formatted_body = `${htmlPrefix} * ${formattedBody}`; + contentBody.formatted_body = `* ${formattedBody}`; } // Build the mentions properties for both the content and new_content. @@ -121,7 +92,7 @@ interface IState { class EditMessageComposer extends React.Component<IEditMessageComposerProps, IState> { public static contextType = RoomContext; - public declare context: React.ContextType<typeof RoomContext>; + declare public context: React.ContextType<typeof RoomContext>; private readonly editorRef = createRef<BasicMessageComposer>(); private dispatcherRef?: string; diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 22da73bef7..8c755f00bd 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -296,7 +296,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState> }; public static contextType = RoomContext; - public declare context: React.ContextType<typeof RoomContext>; + declare public context: React.ContextType<typeof RoomContext>; private unmounted = false; @@ -757,6 +757,14 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState> case EventShieldReason.MISMATCHED_SENDER_KEY: shieldReasonMessage = _t("encryption|event_shield_reason_mismatched_sender_key"); break; + + case EventShieldReason.SENT_IN_CLEAR: + shieldReasonMessage = _t("common|unencrypted"); + break; + + case EventShieldReason.VERIFICATION_VIOLATION: + shieldReasonMessage = _t("timeline|decryption_failure|sender_identity_previously_verified"); + break; } if (this.state.shieldColour === EventShieldColour.GREY) { @@ -767,7 +775,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState> } } - if (MatrixClientPeg.safeGet().isRoomEncrypted(ev.getRoomId()!)) { + if (this.context.isRoomEncrypted) { // else if room is encrypted // and event is being encrypted or is not_sent (Unknown Devices/Network Error) if (ev.status === EventStatus.ENCRYPTING) { diff --git a/src/components/views/rooms/HistoryTile.tsx b/src/components/views/rooms/HistoryTile.tsx index c52ab044a7..3aa74b8b0c 100644 --- a/src/components/views/rooms/HistoryTile.tsx +++ b/src/components/views/rooms/HistoryTile.tsx @@ -6,15 +6,15 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { useContext } from "react"; +import React from "react"; import { EventTimeline } from "matrix-js-sdk/src/matrix"; import EventTileBubble from "../messages/EventTileBubble"; -import RoomContext from "../../../contexts/RoomContext"; import { _t } from "../../../languageHandler"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; const HistoryTile: React.FC = () => { - const { room } = useContext(RoomContext); + const { room } = useScopedRoomContext("room"); const oldState = room?.getLiveTimeline().getState(EventTimeline.BACKWARDS); const historyState = oldState?.getStateEvents("m.room.history_visibility")[0]?.getContent().history_visibility; diff --git a/src/components/views/rooms/MemberList.tsx b/src/components/views/rooms/MemberList.tsx index e503ce2363..5587b56bf8 100644 --- a/src/components/views/rooms/MemberList.tsx +++ b/src/components/views/rooms/MemberList.tsx @@ -75,7 +75,7 @@ export default class MemberList extends React.Component<IProps, IState> { private unmounted = false; public static contextType = SDKContext; - public declare context: React.ContextType<typeof SDKContext>; + declare public context: React.ContextType<typeof SDKContext>; private tiles: Map<string, MemberTile> = new Map(); public constructor(props: IProps, context: React.ContextType<typeof SDKContext>) { diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 62029f46c3..f5716d728b 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -30,6 +30,7 @@ import E2EIcon from "./E2EIcon"; import SettingsStore from "../../../settings/SettingsStore"; import { aboveLeftOf, MenuProps } from "../../structures/ContextMenu"; import ReplyPreview from "./ReplyPreview"; +import { UserIdentityWarning } from "./UserIdentityWarning"; import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import VoiceRecordComposerTile from "./VoiceRecordComposerTile"; import { VoiceRecordingStore } from "../../../stores/VoiceRecordingStore"; @@ -47,14 +48,9 @@ import MessageComposerButtons from "./MessageComposerButtons"; import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom"; -import { Features } from "../../../settings/Settings"; import { VoiceMessageRecording } from "../../../audio/VoiceMessageRecording"; import { SendWysiwygComposer, sendMessage, getConversionFunctions } from "./wysiwyg_composer/"; import { MatrixClientProps, withMatrixClientHOC } from "../../../contexts/MatrixClientContext"; -import { setUpVoiceBroadcastPreRecording } from "../../../voice-broadcast/utils/setUpVoiceBroadcastPreRecording"; -import { SdkContextClass } from "../../../contexts/SDKContext"; -import { VoiceBroadcastInfoState } from "../../../voice-broadcast"; -import { createCantStartVoiceMessageBroadcastDialog } from "../dialogs/CantStartVoiceMessageBroadcastDialog"; import { UIFeature } from "../../../settings/UIFeature"; import { formatTimeLeft } from "../../../DateUtils"; import RoomReplacedSvg from "../../../../res/img/room_replaced.svg"; @@ -100,7 +96,6 @@ interface IState { isStickerPickerOpen: boolean; showStickersButton: boolean; showPollsButton: boolean; - showVoiceBroadcastButton: boolean; isWysiwygLabEnabled: boolean; isRichTextEnabled: boolean; initialComposerContent: string; @@ -122,11 +117,10 @@ export class MessageComposer extends React.Component<IProps, IState> { private _voiceRecording: Optional<VoiceMessageRecording>; public static contextType = RoomContext; - public declare context: React.ContextType<typeof RoomContext>; + declare public context: React.ContextType<typeof RoomContext>; public static defaultProps = { compact: false, - showVoiceBroadcastButton: false, isRichTextEnabled: true, }; @@ -154,7 +148,6 @@ export class MessageComposer extends React.Component<IProps, IState> { isStickerPickerOpen: false, showStickersButton: SettingsStore.getValue("MessageComposerInput.showStickersButton"), showPollsButton: SettingsStore.getValue("MessageComposerInput.showPollsButton"), - showVoiceBroadcastButton: SettingsStore.getValue(Features.VoiceBroadcast), isWysiwygLabEnabled: isWysiwygLabEnabled, isRichTextEnabled: isRichTextEnabled, initialComposerContent: initialComposerContent, @@ -249,7 +242,6 @@ export class MessageComposer extends React.Component<IProps, IState> { SettingsStore.monitorSetting("MessageComposerInput.showStickersButton", null); SettingsStore.monitorSetting("MessageComposerInput.showPollsButton", null); - SettingsStore.monitorSetting(Features.VoiceBroadcast, null); SettingsStore.monitorSetting("feature_wysiwyg_composer", null); this.dispatcherRef = dis.register(this.onAction); @@ -300,12 +292,6 @@ export class MessageComposer extends React.Component<IProps, IState> { } break; } - case Features.VoiceBroadcast: { - if (this.state.showVoiceBroadcastButton !== settingUpdatedPayload.newValue) { - this.setState({ showVoiceBroadcastButton: !!settingUpdatedPayload.newValue }); - } - break; - } case "feature_wysiwyg_composer": { if (this.state.isWysiwygLabEnabled !== settingUpdatedPayload.newValue) { this.setState({ isWysiwygLabEnabled: Boolean(settingUpdatedPayload.newValue) }); @@ -532,13 +518,7 @@ export class MessageComposer extends React.Component<IProps, IState> { } private onRecordStartEndClick = (): void => { - const currentBroadcastRecording = SdkContextClass.instance.voiceBroadcastRecordingsStore.getCurrent(); - - if (currentBroadcastRecording && currentBroadcastRecording.getState() !== VoiceBroadcastInfoState.Stopped) { - createCantStartVoiceMessageBroadcastDialog(); - } else { - this.voiceRecordingButton.current?.onRecordStartEndClick(); - } + this.voiceRecordingButton.current?.onRecordStartEndClick(); if (this.context.narrow) { this.toggleButtonMenu(); @@ -669,6 +649,7 @@ export class MessageComposer extends React.Component<IProps, IState> { <Tooltip open={isTooltipOpen} description={formatTimeLeft(secondsLeft)} placement="bottom"> <div className={classes} ref={this.ref} role="region" aria-label={_t("a11y|message_composer")}> <div className="mx_MessageComposer_wrapper"> + <UserIdentityWarning room={this.props.room} key={this.props.room.roomId} /> <ReplyPreview replyToEvent={this.props.replyToEvent} permalinkCreator={this.props.permalinkCreator} @@ -696,17 +677,6 @@ export class MessageComposer extends React.Component<IProps, IState> { isRichTextEnabled={this.state.isRichTextEnabled} onComposerModeClick={this.onRichTextToggle} toggleButtonMenu={this.toggleButtonMenu} - showVoiceBroadcastButton={this.state.showVoiceBroadcastButton} - onStartVoiceBroadcastClick={() => { - setUpVoiceBroadcastPreRecording( - this.props.room, - MatrixClientPeg.safeGet(), - SdkContextClass.instance.voiceBroadcastPlaybacksStore, - SdkContextClass.instance.voiceBroadcastRecordingsStore, - SdkContextClass.instance.voiceBroadcastPreRecordingStore, - ); - this.toggleButtonMenu(); - }} /> )} {showSendButton && ( diff --git a/src/components/views/rooms/MessageComposerButtons.tsx b/src/components/views/rooms/MessageComposerButtons.tsx index 003c2afed9..19b86834dd 100644 --- a/src/components/views/rooms/MessageComposerButtons.tsx +++ b/src/components/views/rooms/MessageComposerButtons.tsx @@ -21,7 +21,6 @@ import PollCreateDialog from "../elements/PollCreateDialog"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import ContentMessages from "../../../ContentMessages"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import RoomContext from "../../../contexts/RoomContext"; import { useDispatcher } from "../../../hooks/useDispatcher"; import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; import IconizedContextMenu, { IconizedContextMenuOptionList } from "../context_menus/IconizedContextMenu"; @@ -29,6 +28,7 @@ import { EmojiButton } from "./EmojiButton"; import { filterBoolean } from "../../../utils/arrays"; import { useSettingValue } from "../../../hooks/useSettings"; import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; interface IProps { addEmoji: (emoji: string) => boolean; @@ -43,8 +43,6 @@ interface IProps { showPollsButton: boolean; showStickersButton: boolean; toggleButtonMenu: () => void; - showVoiceBroadcastButton: boolean; - onStartVoiceBroadcastClick: () => void; isRichTextEnabled: boolean; onComposerModeClick: () => void; } @@ -54,7 +52,7 @@ export const OverflowMenuContext = createContext<OverflowMenuCloser | null>(null const MessageComposerButtons: React.FC<IProps> = (props: IProps) => { const matrixClient = useContext(MatrixClientContext); - const { room, narrow } = useContext(RoomContext); + const { room, narrow } = useScopedRoomContext("room", "narrow"); const isWysiwygLabEnabled = useSettingValue<boolean>("feature_wysiwyg_composer"); @@ -80,7 +78,6 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => { uploadButton(), // props passed via UploadButtonContext showStickersButton(props), voiceRecordingButton(props, narrow), - startVoiceBroadcastButton(props), props.showPollsButton ? pollButton(room, props.relation) : null, showLocationButton(props, room, matrixClient), ]; @@ -100,7 +97,6 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => { moreButtons = [ showStickersButton(props), voiceRecordingButton(props, narrow), - startVoiceBroadcastButton(props), props.showPollsButton ? pollButton(room, props.relation) : null, showLocationButton(props, room, matrixClient), ]; @@ -168,7 +164,7 @@ interface IUploadButtonProps { // We put the file input outside the UploadButton component so that it doesn't get killed when the context menu closes. const UploadButtonContextProvider: React.FC<IUploadButtonProps> = ({ roomId, relation, children }) => { const cli = useContext(MatrixClientContext); - const roomContext = useContext(RoomContext); + const roomContext = useScopedRoomContext("timelineRenderingType"); const uploadInput = useRef<HTMLInputElement>(null); const onUploadClick = (): void => { @@ -254,18 +250,6 @@ function showStickersButton(props: IProps): ReactElement | null { ) : null; } -const startVoiceBroadcastButton: React.FC<IProps> = (props: IProps): ReactElement | null => { - return props.showVoiceBroadcastButton ? ( - <CollapsibleButton - key="start_voice_broadcast" - className="mx_MessageComposer_button" - iconClassName="mx_MessageComposer_voiceBroadcast" - onClick={props.onStartVoiceBroadcastClick} - title={_t("voice_broadcast|action")} - /> - ) : null; -}; - function voiceRecordingButton(props: IProps, narrow: boolean): ReactElement | null { // XXX: recording UI does not work well in narrow mode, so hide for now return narrow ? null : ( @@ -290,7 +274,7 @@ interface IPollButtonProps { class PollButton extends React.PureComponent<IPollButtonProps> { public static contextType = OverflowMenuContext; - public declare context: React.ContextType<typeof OverflowMenuContext>; + declare public context: React.ContextType<typeof OverflowMenuContext>; private onCreateClick = (): void => { this.context?.(); // close overflow menu diff --git a/src/components/views/rooms/MessageComposerFormatBar.tsx b/src/components/views/rooms/MessageComposerFormatBar.tsx index 34798cc608..0ab359d9dd 100644 --- a/src/components/views/rooms/MessageComposerFormatBar.tsx +++ b/src/components/views/rooms/MessageComposerFormatBar.tsx @@ -33,6 +33,12 @@ interface IState { export default class MessageComposerFormatBar extends React.PureComponent<IProps, IState> { private readonly formatBarRef = createRef<HTMLDivElement>(); + /** + * The height of the format bar in pixels. + * Height 32px + 2px border + * @private + */ + private readonly BAR_HEIGHT = 34; public constructor(props: IProps) { super(props); @@ -96,7 +102,7 @@ export default class MessageComposerFormatBar extends React.PureComponent<IProps this.setState({ visible: true }); const parentRect = this.formatBarRef.current.parentElement.getBoundingClientRect(); this.formatBarRef.current.style.left = `${selectionRect.left - parentRect.left}px`; - const halfBarHeight = this.formatBarRef.current.clientHeight / 2; // used to center the bar + const halfBarHeight = this.BAR_HEIGHT / 2; // used to center the bar const offset = halfBarHeight + 2; // makes sure the bar won't cover selected text const offsetLimit = halfBarHeight + offset; const position = Math.max(selectionRect.top - parentRect.top - offsetLimit, -offsetLimit); diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx index 05912c482e..a5577ee372 100644 --- a/src/components/views/rooms/NewRoomIntro.tsx +++ b/src/components/views/rooms/NewRoomIntro.tsx @@ -11,7 +11,6 @@ import { EventType, Room, User, MatrixClient } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import RoomContext from "../../../contexts/RoomContext"; import DMRoomMap from "../../../utils/DMRoomMap"; import { _t, _td, TranslationKey } from "../../../languageHandler"; import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; @@ -30,6 +29,7 @@ import { UIComponent } from "../../../settings/UIFeature"; import { privateShouldBeEncrypted } from "../../../utils/rooms"; import { LocalRoom } from "../../../models/LocalRoom"; import { shouldEncryptRoomWithSingle3rdPartyInvite } from "../../../utils/room/shouldEncryptRoomWithSingle3rdPartyInvite"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; function hasExpectedEncryptionSettings(matrixClient: MatrixClient, room: Room): boolean { const isEncrypted: boolean = matrixClient.isRoomEncrypted(room.roomId); @@ -51,7 +51,7 @@ const determineIntroMessage = (room: Room, encryptedSingle3rdPartyInvite: boolea const NewRoomIntro: React.FC = () => { const cli = useContext(MatrixClientContext); - const { room, roomId } = useContext(RoomContext); + const { room, roomId } = useScopedRoomContext("room", "roomId"); if (!room || !roomId) { throw new Error("Unable to create a NewRoomIntro without room and roomId"); diff --git a/src/components/views/rooms/PinnedMessageBanner.tsx b/src/components/views/rooms/PinnedMessageBanner.tsx index f44b4417c9..32000d5792 100644 --- a/src/components/views/rooms/PinnedMessageBanner.tsx +++ b/src/components/views/rooms/PinnedMessageBanner.tsx @@ -6,10 +6,10 @@ * Please see LICENSE files in the repository root for full details. */ -import React, { JSX, useEffect, useState } from "react"; +import React, { JSX, useEffect, useRef, useState } from "react"; import PinIcon from "@vector-im/compound-design-tokens/assets/web/icons/pin-solid"; import { Button } from "@vector-im/compound-web"; -import { Room } from "matrix-js-sdk/src/matrix"; +import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; import classNames from "classnames"; import { usePinnedEvents, useSortedFetchedPinnedEvents } from "../../../hooks/usePinnedEvents"; @@ -25,6 +25,7 @@ import { Action } from "../../../dispatcher/actions"; import MessageEvent from "../messages/MessageEvent"; import PosthogTrackers from "../../../PosthogTrackers.ts"; import { EventPreview } from "./EventPreview.tsx"; +import ResizeNotifier from "../../../utils/ResizeNotifier"; /** * The props for the {@link PinnedMessageBanner} component. @@ -38,12 +39,20 @@ interface PinnedMessageBannerProps { * The room where the banner is displayed */ room: Room; + /** + * The resize notifier to notify the timeline to resize itself when the banner is displayed or hidden. + */ + resizeNotifier: ResizeNotifier; } /** * A banner that displays the pinned messages in a room. */ -export function PinnedMessageBanner({ room, permalinkCreator }: PinnedMessageBannerProps): JSX.Element | null { +export function PinnedMessageBanner({ + room, + permalinkCreator, + resizeNotifier, +}: PinnedMessageBannerProps): JSX.Element | null { const pinnedEventIds = usePinnedEvents(room); const pinnedEvents = useSortedFetchedPinnedEvents(room, pinnedEventIds); const eventCount = pinnedEvents.length; @@ -56,6 +65,8 @@ export function PinnedMessageBanner({ room, permalinkCreator }: PinnedMessageBan }, [eventCount]); const pinnedEvent = pinnedEvents[currentEventIndex]; + useNotifyTimeline(pinnedEvent, resizeNotifier); + if (!pinnedEvent) return null; const shouldUseMessageEvent = pinnedEvent.isRedacted() || pinnedEvent.isDecryptionFailure(); @@ -128,6 +139,23 @@ export function PinnedMessageBanner({ room, permalinkCreator }: PinnedMessageBan ); } +/** + * When the banner is displayed or hidden, we want to notify the timeline to resize itself. + * @param pinnedEvent + * @param resizeNotifier + */ +function useNotifyTimeline(pinnedEvent: MatrixEvent | null, resizeNotifier: ResizeNotifier): void { + const previousEvent = useRef<MatrixEvent | null>(null); + useEffect(() => { + // If we switch from a pinned message to no pinned message or the opposite, we want to resize the timeline + if ((previousEvent.current && !pinnedEvent) || (!previousEvent.current && pinnedEvent)) { + resizeNotifier.notifyTimelineHeightChanged(); + } + + previousEvent.current = pinnedEvent; + }, [pinnedEvent, resizeNotifier]); +} + const MAX_INDICATORS = 3; /** diff --git a/src/components/views/rooms/ReplyPreview.tsx b/src/components/views/rooms/ReplyPreview.tsx index c820154b2b..7851f7914d 100644 --- a/src/components/views/rooms/ReplyPreview.tsx +++ b/src/components/views/rooms/ReplyPreview.tsx @@ -31,7 +31,7 @@ interface IProps { export default class ReplyPreview extends React.Component<IProps> { public static contextType = RoomContext; - public declare context: React.ContextType<typeof RoomContext>; + declare public context: React.ContextType<typeof RoomContext>; public render(): JSX.Element | null { if (!this.props.replyToEvent) return null; diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index c2642ea733..d133587fc9 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { useCallback, useContext, useMemo, useState } from "react"; +import React, { useCallback, useMemo, useState } from "react"; import { Body as BodyText, Button, IconButton, Menu, MenuItem, Tooltip } from "@vector-im/compound-web"; import VideoCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/video-call-solid"; import VoiceCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/voice-call"; @@ -48,10 +48,10 @@ import { CallGuestLinkButton } from "./RoomHeader/CallGuestLinkButton"; import { ButtonEvent } from "../elements/AccessibleButton"; import WithPresenceIndicator, { useDmMember } from "../avatars/WithPresenceIndicator"; import { IOOBData } from "../../../stores/ThreepidInviteStore"; -import RoomContext from "../../../contexts/RoomContext"; import { MainSplitContentType } from "../../structures/RoomView"; import defaultDispatcher from "../../../dispatcher/dispatcher.ts"; import { RoomSettingsTab } from "../dialogs/RoomSettingsDialog.tsx"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; export default function RoomHeader({ room, @@ -229,7 +229,7 @@ export default function RoomHeader({ voiceCallButton = undefined; } - const roomContext = useContext(RoomContext); + const roomContext = useScopedRoomContext("mainSplitContentType"); const isVideoRoom = calcIsVideoRoom(room); const showChatButton = isVideoRoom || @@ -392,7 +392,7 @@ export default function RoomHeader({ viewUserOnClick={false} tooltipLabel={_t("room|header_face_pile_tooltip")} onClick={(e: ButtonEvent) => { - RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomMemberList); + RightPanelStore.instance.showOrHidePhase(RightPanelPhases.MemberList); e.stopPropagation(); }} aria-label={_t("common|n_members", { count: memberCount })} diff --git a/src/components/views/rooms/RoomHeader/CallGuestLinkButton.tsx b/src/components/views/rooms/RoomHeader/CallGuestLinkButton.tsx index ae8e7be16b..8c000bdf3b 100644 --- a/src/components/views/rooms/RoomHeader/CallGuestLinkButton.tsx +++ b/src/components/views/rooms/RoomHeader/CallGuestLinkButton.tsx @@ -12,7 +12,7 @@ import { logger } from "matrix-js-sdk/src/logger"; import { EventType, JoinRule, Room } from "matrix-js-sdk/src/matrix"; import Modal from "../../../../Modal"; -import ShareDialog from "../../dialogs/ShareDialog"; +import { ShareDialog } from "../../dialogs/ShareDialog"; import { _t } from "../../../../languageHandler"; import SettingsStore from "../../../../settings/SettingsStore"; import { calculateRoomVia } from "../../../../utils/permalinks/Permalinks"; diff --git a/src/components/views/rooms/RoomInfoLine.tsx b/src/components/views/rooms/RoomInfoLine.tsx index 710ef61758..1487bfe15b 100644 --- a/src/components/views/rooms/RoomInfoLine.tsx +++ b/src/components/views/rooms/RoomInfoLine.tsx @@ -64,7 +64,7 @@ const RoomInfoLine: FC<IProps> = ({ room }) => { // summary is not still loading const viewMembers = (): void => RightPanelStore.instance.setCard({ - phase: room.isSpaceRoom() ? RightPanelPhases.SpaceMemberList : RightPanelPhases.RoomMemberList, + phase: RightPanelPhases.MemberList, }); members = ( diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index 853bebc4fe..f3bde66af9 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -424,7 +424,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> { private treeRef = createRef<HTMLDivElement>(); public static contextType = MatrixClientContext; - public declare context: React.ContextType<typeof MatrixClientContext>; + declare public context: React.ContextType<typeof MatrixClientContext>; public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) { super(props, context); diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index 8351c176ff..7953c5068d 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -39,7 +39,6 @@ import { getKeyBindingsManager } from "../../../KeyBindingsManager"; import { RoomGeneralContextMenu } from "../context_menus/RoomGeneralContextMenu"; import { CallStore, CallStoreEvent } from "../../../stores/CallStore"; import { SdkContextClass } from "../../../contexts/SDKContext"; -import { useHasRoomLiveVoiceBroadcast } from "../../../voice-broadcast"; import { RoomTileSubtitle } from "./RoomTileSubtitle"; import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; import { UIComponent } from "../../../settings/UIFeature"; @@ -53,10 +52,6 @@ interface Props { tag: TagID; } -interface ClassProps extends Props { - hasLiveVoiceBroadcast: boolean; -} - type PartialDOMRect = Pick<DOMRect, "left" | "bottom">; interface State { @@ -77,13 +72,13 @@ export const contextMenuBelow = (elementRect: PartialDOMRect): MenuProps => { return { left, top, chevronFace }; }; -export class RoomTile extends React.PureComponent<ClassProps, State> { +class RoomTile extends React.PureComponent<Props, State> { private dispatcherRef?: string; private roomTileRef = createRef<HTMLDivElement>(); private notificationState: NotificationState; private roomProps: RoomEchoChamber; - public constructor(props: ClassProps) { + public constructor(props: Props) { super(props); this.state = { @@ -370,15 +365,10 @@ export class RoomTile extends React.PureComponent<ClassProps, State> { /** * RoomTile has a subtile if one of the following applies: * - there is a call - * - there is a live voice broadcast * - message previews are enabled and there is a previewable message */ private get shouldRenderSubtitle(): boolean { - return ( - !!this.state.call || - this.props.hasLiveVoiceBroadcast || - (this.props.showMessagePreview && !!this.state.messagePreview) - ); + return !!this.state.call || (this.props.showMessagePreview && !!this.state.messagePreview); } public render(): React.ReactElement { @@ -409,7 +399,6 @@ export class RoomTile extends React.PureComponent<ClassProps, State> { const subtitle = this.shouldRenderSubtitle ? ( <RoomTileSubtitle call={this.state.call} - hasLiveVoiceBroadcast={this.props.hasLiveVoiceBroadcast} messagePreview={this.state.messagePreview} roomId={this.props.room.roomId} showMessagePreview={this.props.showMessagePreview} @@ -491,9 +480,4 @@ export class RoomTile extends React.PureComponent<ClassProps, State> { } } -const RoomTileHOC: React.FC<Props> = (props: Props) => { - const hasLiveVoiceBroadcast = useHasRoomLiveVoiceBroadcast(props.room); - return <RoomTile {...props} hasLiveVoiceBroadcast={hasLiveVoiceBroadcast} />; -}; - -export default RoomTileHOC; +export default RoomTile; diff --git a/src/components/views/rooms/RoomTileSubtitle.tsx b/src/components/views/rooms/RoomTileSubtitle.tsx index ea4a96d259..479b9c4f71 100644 --- a/src/components/views/rooms/RoomTileSubtitle.tsx +++ b/src/components/views/rooms/RoomTileSubtitle.tsx @@ -13,11 +13,9 @@ import { ThreadsIcon } from "@vector-im/compound-design-tokens/assets/web/icons" import { MessagePreview } from "../../../stores/room-list/MessagePreviewStore"; import { Call } from "../../../models/Call"; import { RoomTileCallSummary } from "./RoomTileCallSummary"; -import { VoiceBroadcastRoomSubtitle } from "../../../voice-broadcast"; interface Props { call: Call | null; - hasLiveVoiceBroadcast: boolean; messagePreview: MessagePreview | null; roomId: string; showMessagePreview: boolean; @@ -25,13 +23,7 @@ interface Props { const messagePreviewId = (roomId: string): string => `mx_RoomTile_messagePreview_${roomId}`; -export const RoomTileSubtitle: React.FC<Props> = ({ - call, - hasLiveVoiceBroadcast, - messagePreview, - roomId, - showMessagePreview, -}) => { +export const RoomTileSubtitle: React.FC<Props> = ({ call, messagePreview, roomId, showMessagePreview }) => { if (call) { return ( <div className="mx_RoomTile_subtitle"> @@ -40,10 +32,6 @@ export const RoomTileSubtitle: React.FC<Props> = ({ ); } - if (hasLiveVoiceBroadcast) { - return <VoiceBroadcastRoomSubtitle />; - } - if (showMessagePreview && messagePreview) { const className = classNames("mx_RoomTile_subtitle", { "mx_RoomTile_subtitle--thread-reply": messagePreview.isThreadReply, diff --git a/src/components/views/rooms/RoomUpgradeWarningBar.tsx b/src/components/views/rooms/RoomUpgradeWarningBar.tsx index 66519fa766..e92be96cb2 100644 --- a/src/components/views/rooms/RoomUpgradeWarningBar.tsx +++ b/src/components/views/rooms/RoomUpgradeWarningBar.tsx @@ -25,7 +25,7 @@ interface IState { export default class RoomUpgradeWarningBar extends React.PureComponent<IProps, IState> { public static contextType = MatrixClientContext; - public declare context: React.ContextType<typeof MatrixClientContext>; + declare public context: React.ContextType<typeof MatrixClientContext>; public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) { super(props, context); diff --git a/src/components/views/rooms/SearchResultTile.tsx b/src/components/views/rooms/SearchResultTile.tsx index 5ebbaffdd9..94f5e6da9d 100644 --- a/src/components/views/rooms/SearchResultTile.tsx +++ b/src/components/views/rooms/SearchResultTile.tsx @@ -36,7 +36,7 @@ interface IProps { export default class SearchResultTile extends React.Component<IProps> { public static contextType = RoomContext; - public declare context: React.ContextType<typeof RoomContext>; + declare public context: React.ContextType<typeof RoomContext>; // A map of <callId, LegacyCallEventGrouper> private callEventGroupers = new Map<string, LegacyCallEventGrouper>(); diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 252957c2c7..b3767cbd2a 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -193,7 +193,6 @@ export function createMessageContent( body: body, }; const formattedBody = htmlSerializeIfNeeded(model, { - forceHTML: !!replyToEvent, useMarkdown: SettingsStore.getValue("MessageComposerInput.useMarkdown"), }); if (formattedBody) { @@ -241,7 +240,7 @@ interface ISendMessageComposerProps extends MatrixClientProps { export class SendMessageComposer extends React.Component<ISendMessageComposerProps> { public static contextType = RoomContext; - public declare context: React.ContextType<typeof RoomContext>; + declare public context: React.ContextType<typeof RoomContext>; private readonly prepareToEncrypt?: DebouncedFunc<() => void>; private readonly editorRef = createRef<BasicMessageComposer>(); diff --git a/src/components/views/rooms/ThreadSummary.tsx b/src/components/views/rooms/ThreadSummary.tsx index 4a3032d641..ac23331f66 100644 --- a/src/components/views/rooms/ThreadSummary.tsx +++ b/src/components/views/rooms/ThreadSummary.tsx @@ -16,7 +16,6 @@ import { CardContext } from "../right_panel/context"; import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; import PosthogTrackers from "../../../PosthogTrackers"; import { useTypedEventEmitterState } from "../../../hooks/useEventEmitter"; -import RoomContext from "../../../contexts/RoomContext"; import MemberAvatar from "../avatars/MemberAvatar"; import { Action } from "../../../dispatcher/actions"; import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload"; @@ -24,6 +23,7 @@ import defaultDispatcher from "../../../dispatcher/dispatcher"; import { useUnreadNotifications } from "../../../hooks/useUnreadNotifications"; import { notificationLevelToIndicator } from "../../../utils/notifications"; import { EventPreviewTile, useEventPreview } from "./EventPreview.tsx"; +import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; interface IProps { mxEvent: MatrixEvent; @@ -31,7 +31,7 @@ interface IProps { } const ThreadSummary: React.FC<IProps> = ({ mxEvent, thread, ...props }) => { - const roomContext = useContext(RoomContext); + const roomContext = useScopedRoomContext("narrow"); const cardContext = useContext(CardContext); const count = useTypedEventEmitterState(thread, ThreadEvent.Update, () => thread.length); const { level } = useUnreadNotifications(thread.room, thread.id); diff --git a/src/components/views/rooms/UserIdentityWarning.tsx b/src/components/views/rooms/UserIdentityWarning.tsx new file mode 100644 index 0000000000..564ab71987 --- /dev/null +++ b/src/components/views/rooms/UserIdentityWarning.tsx @@ -0,0 +1,328 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import React, { useCallback, useRef, useState } from "react"; +import { EventType, KnownMembership, MatrixEvent, Room, RoomStateEvent, RoomMember } from "matrix-js-sdk/src/matrix"; +import { CryptoApi, CryptoEvent, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api"; +import { logger } from "matrix-js-sdk/src/logger"; +import { Button, Separator } from "@vector-im/compound-web"; + +import { _t } from "../../../languageHandler"; +import MemberAvatar from "../avatars/MemberAvatar"; +import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; +import { useTypedEventEmitter } from "../../../hooks/useEventEmitter"; + +interface UserIdentityWarningProps { + /** + * The current room being viewed. + */ + room: Room; + /** + * The ID of the room being viewed. This is used to ensure that the + * component's state and references are cleared when the room changes. + */ + key: string; +} + +/** + * Does the given user's identity need to be approved? + */ +async function userNeedsApproval(crypto: CryptoApi, userId: string): Promise<boolean> { + const verificationStatus = await crypto.getUserVerificationStatus(userId); + return verificationStatus.needsUserApproval; +} + +/** + * Whether the component is uninitialised, is in the process of initialising, or + * has completed initialising. + */ +enum InitialisationStatus { + Uninitialised, + Initialising, + Completed, +} + +/** + * Displays a banner warning when there is an issue with a user's identity. + * + * Warns when an unverified user's identity has changed, and gives the user a + * button to acknowledge the change. + */ +export const UserIdentityWarning: React.FC<UserIdentityWarningProps> = ({ room }) => { + const cli = useMatrixClientContext(); + const crypto = cli.getCrypto(); + + // The current room member that we are prompting the user to approve. + // `undefined` means we are not currently showing a prompt. + const [currentPrompt, setCurrentPrompt] = useState<RoomMember | undefined>(undefined); + + // Whether or not we've already initialised the component by loading the + // room membership. + const initialisedRef = useRef<InitialisationStatus>(InitialisationStatus.Uninitialised); + // Which room members need their identity approved. + const membersNeedingApprovalRef = useRef<Map<string, RoomMember>>(new Map()); + // For each user, we assign a sequence number to each verification status + // that we get, or fetch. + // + // Since fetching a verification status is asynchronous, we could get an + // update in the middle of fetching the verification status, which could + // mean that the status that we fetched is out of date. So if the current + // sequence number is not the same as the sequence number when we started + // the fetch, then we drop our fetched result, under the assumption that the + // update that we received is the most up-to-date version. If it is in fact + // not the most up-to-date version, then we should be receiving a new update + // soon with the newer value, so it will fix itself in the end. + // + // We also assign a sequence number when the user leaves the room, in order + // to prevent prompting about a user who leaves while we are fetching their + // verification status. + const verificationStatusSequencesRef = useRef<Map<string, number>>(new Map()); + const incrementVerificationStatusSequence = (userId: string): number => { + const verificationStatusSequences = verificationStatusSequencesRef.current; + const value = verificationStatusSequences.get(userId); + const newValue = value === undefined ? 1 : value + 1; + verificationStatusSequences.set(userId, newValue); + return newValue; + }; + + // Update the current prompt. Select a new user if needed, or hide the + // warning if we don't have anyone to warn about. + const updateCurrentPrompt = useCallback((): undefined => { + const membersNeedingApproval = membersNeedingApprovalRef.current; + // We have to do this in a callback to `setCurrentPrompt` + // because this function could have been called after an + // `await`, and the `currentPrompt` that this function would + // have may be outdated. + setCurrentPrompt((currentPrompt) => { + // If we're already displaying a warning, and that user still needs + // approval, continue showing that user. + if (currentPrompt && membersNeedingApproval.has(currentPrompt.userId)) return currentPrompt; + + if (membersNeedingApproval.size === 0) { + return undefined; + } + + // We pick the user with the smallest user ID. + const keys = Array.from(membersNeedingApproval.keys()).sort((a, b) => a.localeCompare(b)); + const selection = membersNeedingApproval.get(keys[0]!); + return selection; + }); + }, []); + + // Add a user to the membersNeedingApproval map, and update the current + // prompt if necessary. The user will only be added if they are actually a + // member of the room. If they are not a member, this function will do + // nothing. + const addMemberNeedingApproval = useCallback( + (userId: string, member?: RoomMember): void => { + if (userId === cli.getUserId()) { + // We always skip our own user, because we can't pin our own identity. + return; + } + member = member ?? room.getMember(userId) ?? undefined; + if (!member) return; + + membersNeedingApprovalRef.current.set(userId, member); + // We only select the prompt if we are done initialising, + // because we will select the prompt after we're done + // initialising, and we want to start by displaying a warning + // for the user with the smallest ID. + if (initialisedRef.current === InitialisationStatus.Completed) { + updateCurrentPrompt(); + } + }, + [cli, room, updateCurrentPrompt], + ); + + // For each user in the list check if their identity needs approval, and if + // so, add them to the membersNeedingApproval map and update the prompt if + // needed. + const addMembersWhoNeedApproval = useCallback( + async (members: RoomMember[]): Promise<void> => { + const verificationStatusSequences = verificationStatusSequencesRef.current; + + const promises: Promise<void>[] = []; + + for (const member of members) { + const userId = member.userId; + const sequenceNum = incrementVerificationStatusSequence(userId); + promises.push( + userNeedsApproval(crypto!, userId).then((needsApproval) => { + if (needsApproval) { + // Only actually update the list if we have the most + // recent value. + if (verificationStatusSequences.get(userId) === sequenceNum) { + addMemberNeedingApproval(userId, member); + } + } + }), + ); + } + + await Promise.all(promises); + }, + [crypto, addMemberNeedingApproval], + ); + + // Remove a user from the membersNeedingApproval map, and update the current + // prompt if necessary. + const removeMemberNeedingApproval = useCallback( + (userId: string): void => { + membersNeedingApprovalRef.current.delete(userId); + updateCurrentPrompt(); + }, + [updateCurrentPrompt], + ); + + // Initialise the component. Get the room members, check which ones need + // their identity approved, and pick one to display. + const loadMembers = useCallback(async (): Promise<void> => { + if (!crypto || initialisedRef.current !== InitialisationStatus.Uninitialised) { + return; + } + // If encryption is not enabled in the room, we don't need to do + // anything. If encryption gets enabled later, we will retry, via + // onRoomStateEvent. + if (!(await crypto.isEncryptionEnabledInRoom(room.roomId))) { + return; + } + initialisedRef.current = InitialisationStatus.Initialising; + + const members = await room.getEncryptionTargetMembers(); + await addMembersWhoNeedApproval(members); + + updateCurrentPrompt(); + initialisedRef.current = InitialisationStatus.Completed; + }, [crypto, room, addMembersWhoNeedApproval, updateCurrentPrompt]); + + loadMembers().catch((e) => { + logger.error("Error initialising UserIdentityWarning:", e); + }); + + // When a user's verification status changes, we check if they need to be + // added/removed from the set of members needing approval. + const onUserVerificationStatusChanged = useCallback( + (userId: string, verificationStatus: UserVerificationStatus): void => { + // If we haven't started initialising, that means that we're in a + // room where we don't need to display any warnings. + if (initialisedRef.current === InitialisationStatus.Uninitialised) { + return; + } + + incrementVerificationStatusSequence(userId); + + if (verificationStatus.needsUserApproval) { + addMemberNeedingApproval(userId); + } else { + removeMemberNeedingApproval(userId); + } + }, + [addMemberNeedingApproval, removeMemberNeedingApproval], + ); + useTypedEventEmitter(cli, CryptoEvent.UserTrustStatusChanged, onUserVerificationStatusChanged); + + // We watch for encryption events (since we only display warnings in + // encrypted rooms), and for membership changes (since we only display + // warnings for users in the room). + const onRoomStateEvent = useCallback( + async (event: MatrixEvent): Promise<void> => { + if (!crypto || event.getRoomId() !== room.roomId) { + return; + } + + const eventType = event.getType(); + if (eventType === EventType.RoomEncryption && event.getStateKey() === "") { + // Room is now encrypted, so we can initialise the component. + return loadMembers().catch((e) => { + logger.error("Error initialising UserIdentityWarning:", e); + }); + } else if (eventType !== EventType.RoomMember) { + return; + } + + // We're processing an m.room.member event + + if (initialisedRef.current === InitialisationStatus.Uninitialised) { + return; + } + + const userId = event.getStateKey(); + + if (!userId) return; + + if ( + event.getContent().membership === KnownMembership.Join || + (event.getContent().membership === KnownMembership.Invite && room.shouldEncryptForInvitedMembers()) + ) { + // Someone's membership changed and we will now encrypt to them. If + // their identity needs approval, show a warning. + const member = room.getMember(userId); + if (member) { + await addMembersWhoNeedApproval([member]).catch((e) => { + logger.error("Error adding member in UserIdentityWarning:", e); + }); + } + } else { + // Someone's membership changed and we no longer encrypt to them. + // If we're showing a warning about them, we don't need to any more. + removeMemberNeedingApproval(userId); + incrementVerificationStatusSequence(userId); + } + }, + [crypto, room, addMembersWhoNeedApproval, removeMemberNeedingApproval, loadMembers], + ); + useTypedEventEmitter(cli, RoomStateEvent.Events, onRoomStateEvent); + + if (!crypto || !currentPrompt) return null; + + const confirmIdentity = async (): Promise<void> => { + await crypto.pinCurrentUserIdentity(currentPrompt.userId); + }; + + return ( + <div className="mx_UserIdentityWarning"> + <Separator /> + <div className="mx_UserIdentityWarning_row"> + <MemberAvatar member={currentPrompt} title={currentPrompt.userId} size="30px" /> + <span className="mx_UserIdentityWarning_main"> + {currentPrompt.rawDisplayName === currentPrompt.userId + ? _t( + "encryption|pinned_identity_changed_no_displayname", + { userId: currentPrompt.userId }, + { + a: substituteATag, + b: substituteBTag, + }, + ) + : _t( + "encryption|pinned_identity_changed", + { displayName: currentPrompt.rawDisplayName, userId: currentPrompt.userId }, + { + a: substituteATag, + b: substituteBTag, + }, + )} + </span> + <Button kind="primary" size="sm" onClick={confirmIdentity}> + {_t("action|ok")} + </Button> + </div> + </div> + ); +}; + +function substituteATag(sub: string): React.ReactNode { + return ( + <a href="https://element.io/help#encryption18" target="_blank" rel="noreferrer noopener"> + {sub} + </a> + ); +} + +function substituteBTag(sub: string): React.ReactNode { + return <b>{sub}</b>; +} diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index a6d4a2fc27..a8335a9902 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -53,7 +53,7 @@ interface IState { */ export default class VoiceRecordComposerTile extends React.PureComponent<IProps, IState> { public static contextType = RoomContext; - public declare context: React.ContextType<typeof RoomContext>; + declare public context: React.ContextType<typeof RoomContext>; private voiceRecordingId: string; public constructor(props: IProps, context: React.ContextType<typeof RoomContext>) { diff --git a/src/components/views/rooms/wysiwyg_composer/components/Emoji.tsx b/src/components/views/rooms/wysiwyg_composer/components/Emoji.tsx index b7a6d65e23..9ab3d210ab 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/Emoji.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/Emoji.tsx @@ -13,14 +13,14 @@ import { EmojiButton } from "../../EmojiButton"; import dis from "../../../../../dispatcher/dispatcher"; import { ComposerInsertPayload } from "../../../../../dispatcher/payloads/ComposerInsertPayload"; import { Action } from "../../../../../dispatcher/actions"; -import { useRoomContext } from "../../../../../contexts/RoomContext"; +import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx"; interface EmojiProps { menuPosition: MenuProps; } export function Emoji({ menuPosition }: EmojiProps): JSX.Element { - const roomContext = useRoomContext(); + const roomContext = useScopedRoomContext("timelineRenderingType"); return ( <EmojiButton diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx index e7ff5f169a..4d06adcd4e 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx @@ -10,11 +10,11 @@ import React, { ForwardedRef, forwardRef, FunctionComponent } from "react"; import { FormattingFunctions, MappedSuggestion } from "@vector-im/matrix-wysiwyg"; import { logger } from "matrix-js-sdk/src/logger"; -import { useRoomContext } from "../../../../../contexts/RoomContext"; import Autocomplete from "../../Autocomplete"; import { ICompletion } from "../../../../../autocomplete/Autocompleter"; import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext"; import { getMentionDisplayText, getMentionAttributes, buildQuery } from "../utils/autocomplete"; +import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx"; interface WysiwygAutocompleteProps { /** @@ -53,7 +53,7 @@ const WysiwygAutocomplete = forwardRef( { suggestion, handleMention, handleCommand, handleAtRoomMention }: WysiwygAutocompleteProps, ref: ForwardedRef<Autocomplete>, ): JSX.Element | null => { - const { room } = useRoomContext(); + const { room } = useScopedRoomContext("room"); const client = useMatrixClientContext(); function handleConfirm(completion: ICompletion): void { diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx index 5b6361a58e..f1e42ce091 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx @@ -19,12 +19,12 @@ import { Editor } from "./Editor"; import { useInputEventProcessor } from "../hooks/useInputEventProcessor"; import { useSetCursorPosition } from "../hooks/useSetCursorPosition"; import { useIsFocused } from "../hooks/useIsFocused"; -import { useRoomContext } from "../../../../../contexts/RoomContext"; import defaultDispatcher from "../../../../../dispatcher/dispatcher"; import { Action } from "../../../../../dispatcher/actions"; import { parsePermalink } from "../../../../../utils/permalinks/Permalinks"; import { isNotNull } from "../../../../../Typeguards"; import { useSettingValue } from "../../../../../hooks/useSettings"; +import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx"; interface WysiwygComposerProps { disabled?: boolean; @@ -56,7 +56,7 @@ export const WysiwygComposer = memo(function WysiwygComposer({ children, eventRelation, }: WysiwygComposerProps) { - const { room } = useRoomContext(); + const { room } = useScopedRoomContext("room"); const autocompleteRef = useRef<Autocomplete | null>(null); const inputEventProcessor = useInputEventProcessor(onSend, autocompleteRef, initialContent, eventRelation); diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useEditing.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useEditing.ts index 5d1c3b867e..20f394e8a3 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useEditing.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useEditing.ts @@ -10,10 +10,10 @@ import { ISendEventResponse } from "matrix-js-sdk/src/matrix"; import { useCallback, useState } from "react"; import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext"; -import { useRoomContext } from "../../../../../contexts/RoomContext"; import EditorStateTransfer from "../../../../../utils/EditorStateTransfer"; import { endEditing } from "../utils/editing"; import { editMessage } from "../utils/message"; +import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx"; export function useEditing( editorStateTransfer: EditorStateTransfer, @@ -24,7 +24,7 @@ export function useEditing( editMessage(): Promise<ISendEventResponse | undefined>; endEditing(): void; } { - const roomContext = useRoomContext(); + const roomContext = useScopedRoomContext("timelineRenderingType"); const mxClient = useMatrixClientContext(); const [isSaveDisabled, setIsSaveDisabled] = useState(true); diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useInitialContent.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useInitialContent.ts index 2e0ddd3ccd..3a3799496b 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useInitialContent.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useInitialContent.ts @@ -10,11 +10,11 @@ import { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; import { useMemo } from "react"; import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext"; -import { useRoomContext } from "../../../../../contexts/RoomContext"; import { parseEvent } from "../../../../../editor/deserialize"; import { CommandPartCreator, Part } from "../../../../../editor/parts"; import SettingsStore from "../../../../../settings/SettingsStore"; import EditorStateTransfer from "../../../../../utils/EditorStateTransfer"; +import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx"; function getFormattedContent(editorStateTransfer: EditorStateTransfer): string { return ( @@ -60,12 +60,12 @@ export function parseEditorStateTransfer( } export function useInitialContent(editorStateTransfer: EditorStateTransfer): string | undefined { - const roomContext = useRoomContext(); + const { room } = useScopedRoomContext("room"); const mxClient = useMatrixClientContext(); return useMemo<string | undefined>(() => { - if (editorStateTransfer && roomContext.room && mxClient) { - return parseEditorStateTransfer(editorStateTransfer, roomContext.room, mxClient); + if (editorStateTransfer && room && mxClient) { + return parseEditorStateTransfer(editorStateTransfer, room, mxClient); } - }, [editorStateTransfer, roomContext, mxClient]); + }, [editorStateTransfer, room, mxClient]); } diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts index 8eac63eb36..cab3bdefb8 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts @@ -16,7 +16,6 @@ import { KeyBindingAction } from "../../../../../accessibility/KeyboardShortcuts import { findEditableEvent } from "../../../../../utils/EventUtils"; import dis from "../../../../../dispatcher/dispatcher"; import { Action } from "../../../../../dispatcher/actions"; -import { useRoomContext } from "../../../../../contexts/RoomContext"; import { IRoomState } from "../../../../structures/RoomView"; import { ComposerContextState, useComposerContext } from "../ComposerContext"; import EditorStateTransfer from "../../../../../utils/EditorStateTransfer"; @@ -26,6 +25,7 @@ import { getEventsFromEditorStateTransfer, getEventsFromRoom } from "../utils/ev import { endEditing } from "../utils/editing"; import Autocomplete from "../../Autocomplete"; import { handleClipboardEvent, handleEventWithAutocomplete, isEventToHandleAsClipboardEvent } from "./utils"; +import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx"; export function useInputEventProcessor( onSend: () => void, @@ -33,7 +33,7 @@ export function useInputEventProcessor( initialContent?: string, eventRelation?: IEventRelation, ): (event: WysiwygEvent, composer: Wysiwyg, editor: HTMLElement) => WysiwygEvent | null { - const roomContext = useRoomContext(); + const roomContext = useScopedRoomContext("liveTimeline", "room", "replyToEvent", "timelineRenderingType"); const composerContext = useComposerContext(); const mxClient = useMatrixClientContext(); const isCtrlEnterToSend = useSettingValue<boolean>("MessageComposerInput.ctrlEnterToSend"); @@ -94,7 +94,7 @@ function handleKeyboardEvent( initialContent: string | undefined, composer: Wysiwyg, editor: HTMLElement, - roomContext: IRoomState, + roomContext: Pick<IRoomState, "liveTimeline" | "timelineRenderingType" | "room">, composerContext: ComposerContextState, mxClient: MatrixClient | undefined, autocompleteRef: React.RefObject<Autocomplete>, @@ -175,7 +175,7 @@ function dispatchEditEvent( isForward: boolean, editorStateTransfer: EditorStateTransfer | undefined, composerContext: ComposerContextState, - roomContext: IRoomState, + roomContext: Pick<IRoomState, "liveTimeline" | "timelineRenderingType" | "room">, mxClient: MatrixClient, ): boolean { const foundEvents = editorStateTransfer diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts index 742d24fe34..1dc23cc274 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts @@ -16,8 +16,8 @@ import Autocomplete from "../../Autocomplete"; import { handleClipboardEvent, handleEventWithAutocomplete, isEventToHandleAsClipboardEvent } from "./utils"; import { useSuggestion } from "./useSuggestion"; import { isNotNull, isNotUndefined } from "../../../../../Typeguards"; -import { useRoomContext } from "../../../../../contexts/RoomContext"; import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext"; +import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx"; function isDivElement(target: EventTarget): target is HTMLDivElement { return target instanceof HTMLDivElement; @@ -63,7 +63,7 @@ export function usePlainTextListeners( onSelect: (event: SyntheticEvent<HTMLDivElement>) => void; suggestion: MappedSuggestion | null; } { - const roomContext = useRoomContext(); + const roomContext = useScopedRoomContext("room", "timelineRenderingType", "replyToEvent"); const mxClient = useMatrixClientContext(); const ref = useRef<HTMLDivElement | null>(null); diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts index e1e34623c8..eb76d77af5 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts @@ -11,20 +11,21 @@ import { RefObject, useCallback, useRef } from "react"; import defaultDispatcher from "../../../../../dispatcher/dispatcher"; import { Action } from "../../../../../dispatcher/actions"; import { ActionPayload } from "../../../../../dispatcher/payloads"; -import { TimelineRenderingType, useRoomContext } from "../../../../../contexts/RoomContext"; +import { TimelineRenderingType } from "../../../../../contexts/RoomContext"; import { useDispatcher } from "../../../../../hooks/useDispatcher"; import { focusComposer } from "./utils"; import { ComposerType } from "../../../../../dispatcher/payloads/ComposerInsertPayload"; import { ComposerFunctions } from "../types"; import { setSelection } from "../utils/selection"; import { useComposerContext } from "../ComposerContext"; +import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx"; export function useWysiwygEditActionHandler( disabled: boolean, composerElement: RefObject<HTMLElement>, composerFunctions: ComposerFunctions, ): void { - const roomContext = useRoomContext(); + const roomContext = useScopedRoomContext("timelineRenderingType"); const composerContext = useComposerContext(); const timeoutId = useRef<number | null>(null); diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts index 16e1e608ec..d11f3498fd 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts @@ -11,20 +11,21 @@ import { MutableRefObject, useCallback, useRef } from "react"; import defaultDispatcher from "../../../../../dispatcher/dispatcher"; import { Action } from "../../../../../dispatcher/actions"; import { ActionPayload } from "../../../../../dispatcher/payloads"; -import { TimelineRenderingType, useRoomContext } from "../../../../../contexts/RoomContext"; +import { TimelineRenderingType } from "../../../../../contexts/RoomContext"; import { useDispatcher } from "../../../../../hooks/useDispatcher"; import { focusComposer } from "./utils"; import { ComposerFunctions } from "../types"; import { ComposerType } from "../../../../../dispatcher/payloads/ComposerInsertPayload"; import { useComposerContext } from "../ComposerContext"; import { setSelection } from "../utils/selection"; +import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx"; export function useWysiwygSendActionHandler( disabled: boolean, composerElement: MutableRefObject<HTMLElement>, composerFunctions: ComposerFunctions, ): void { - const roomContext = useRoomContext(); + const roomContext = useScopedRoomContext("timelineRenderingType"); const composerContext = useComposerContext(); const timeoutId = useRef<number | null>(null); diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts b/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts index 39317ea88c..3345c9f474 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts @@ -22,7 +22,7 @@ import { isNotNull } from "../../../../../Typeguards"; export function focusComposer( composerElement: MutableRefObject<HTMLElement | null>, renderingType: TimelineRenderingType, - roomContext: IRoomState, + roomContext: Pick<IRoomState, "timelineRenderingType">, timeoutId: MutableRefObject<number | null>, ): void { if (renderingType === roomContext.timelineRenderingType) { @@ -123,7 +123,7 @@ export function handleEventWithAutocomplete( export function handleClipboardEvent( event: ClipboardEvent | InputEvent, data: DataTransfer | null, - roomContext: IRoomState, + roomContext: Pick<IRoomState, "room" | "timelineRenderingType" | "replyToEvent">, mxClient: MatrixClient, eventRelation?: IEventRelation, ): boolean { diff --git a/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts b/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts index 7f42ed2327..58d09b3d12 100644 --- a/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts +++ b/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts @@ -27,28 +27,6 @@ function attachRelation(content: IContent, relation?: IEventRelation): void { } } -function getHtmlReplyFallback(mxEvent: MatrixEvent): string { - const html = mxEvent.getContent().formatted_body; - if (!html) { - return ""; - } - const rootNode = new DOMParser().parseFromString(html, "text/html").body; - const mxReply = rootNode.querySelector("mx-reply"); - return (mxReply && mxReply.outerHTML) || ""; -} - -function getTextReplyFallback(mxEvent: MatrixEvent): string { - const body = mxEvent.getContent().body; - if (typeof body !== "string") { - return ""; - } - const lines = body.split("\n").map((l) => l.trim()); - if (lines.length > 2 && lines[0].startsWith("> ") && lines[1].length === 0) { - return `${lines[0]}\n\n`; - } - return ""; -} - interface CreateMessageContentParams { relation?: IEventRelation; replyToEvent?: MatrixEvent; @@ -63,8 +41,6 @@ export async function createMessageContent( { relation, replyToEvent, editedEvent }: CreateMessageContentParams, ): Promise<RoomMessageEventContent> { const isEditing = isMatrixEvent(editedEvent); - const isReply = isEditing ? Boolean(editedEvent.replyEventId) : isMatrixEvent(replyToEvent); - const isReplyAndEditing = isEditing && isReply; const isEmote = message.startsWith(EMOTE_PREFIX); if (isEmote) { @@ -82,12 +58,10 @@ export async function createMessageContent( // if we're editing rich text, the message content is pure html // BUT if we're not, the message content will be plain text where we need to convert the mentions const body = isHTML ? await richToPlain(message, false) : convertPlainTextToBody(message); - const bodyPrefix = (isReplyAndEditing && getTextReplyFallback(editedEvent)) || ""; - const formattedBodyPrefix = (isReplyAndEditing && getHtmlReplyFallback(editedEvent)) || ""; const content = { msgtype: isEmote ? MsgType.Emote : MsgType.Text, - body: isEditing ? `${bodyPrefix} * ${body}` : body, + body: isEditing ? `* ${body}` : body, } as RoomMessageTextEventContent & ReplacementEvent<RoomMessageTextEventContent>; // TODO markdown support @@ -97,7 +71,7 @@ export async function createMessageContent( if (formattedBody) { content.format = "org.matrix.custom.html"; - content.formatted_body = isEditing ? `${formattedBodyPrefix} * ${formattedBody}` : formattedBody; + content.formatted_body = isEditing ? `* ${formattedBody}` : formattedBody; } if (isEditing) { diff --git a/src/components/views/rooms/wysiwyg_composer/utils/editing.ts b/src/components/views/rooms/wysiwyg_composer/utils/editing.ts index 58a9e24492..462763b8f4 100644 --- a/src/components/views/rooms/wysiwyg_composer/utils/editing.ts +++ b/src/components/views/rooms/wysiwyg_composer/utils/editing.ts @@ -13,7 +13,7 @@ import dis from "../../../../../dispatcher/dispatcher"; import { Action } from "../../../../../dispatcher/actions"; import EditorStateTransfer from "../../../../../utils/EditorStateTransfer"; -export function endEditing(roomContext: IRoomState): void { +export function endEditing(roomContext: Pick<IRoomState, "timelineRenderingType">): void { // todo local storage // localStorage.removeItem(this.editorRoomKey); // localStorage.removeItem(this.editorStateKey); diff --git a/src/components/views/rooms/wysiwyg_composer/utils/event.ts b/src/components/views/rooms/wysiwyg_composer/utils/event.ts index 5fd37b3665..45c6b1cac3 100644 --- a/src/components/views/rooms/wysiwyg_composer/utils/event.ts +++ b/src/components/views/rooms/wysiwyg_composer/utils/event.ts @@ -15,7 +15,7 @@ import { ComposerContextState } from "../ComposerContext"; // From EditMessageComposer private get events(): MatrixEvent[] export function getEventsFromEditorStateTransfer( editorStateTransfer: EditorStateTransfer, - roomContext: IRoomState, + roomContext: Pick<IRoomState, "liveTimeline">, mxClient: MatrixClient, ): MatrixEvent[] | undefined { const liveTimelineEvents = roomContext.liveTimeline?.getEvents(); @@ -41,7 +41,7 @@ export function getEventsFromEditorStateTransfer( // From SendMessageComposer private onKeyDown = (event: KeyboardEvent): void export function getEventsFromRoom( composerContext: ComposerContextState, - roomContext: IRoomState, + roomContext: Pick<IRoomState, "liveTimeline" | "room">, ): MatrixEvent[] | undefined { const isReplyingToThread = composerContext.eventRelation?.key === THREAD_RELATION_TYPE.name; return roomContext.liveTimeline diff --git a/src/components/views/rooms/wysiwyg_composer/utils/message.ts b/src/components/views/rooms/wysiwyg_composer/utils/message.ts index 44e10e3cc5..b7fca8ecb4 100644 --- a/src/components/views/rooms/wysiwyg_composer/utils/message.ts +++ b/src/components/views/rooms/wysiwyg_composer/utils/message.ts @@ -39,7 +39,7 @@ export interface SendMessageParams { mxClient: MatrixClient; relation?: IEventRelation; replyToEvent?: MatrixEvent; - roomContext: IRoomState; + roomContext: Pick<IRoomState, "timelineRenderingType" | "room">; } export async function sendMessage( @@ -177,7 +177,7 @@ export async function sendMessage( interface EditMessageParams { mxClient: MatrixClient; - roomContext: IRoomState; + roomContext: Pick<IRoomState, "timelineRenderingType">; editorStateTransfer: EditorStateTransfer; } diff --git a/src/components/views/settings/AvatarSetting.tsx b/src/components/views/settings/AvatarSetting.tsx index b6ce541590..ee47094cf9 100644 --- a/src/components/views/settings/AvatarSetting.tsx +++ b/src/components/views/settings/AvatarSetting.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { ReactNode, createRef, useCallback, useEffect, useState } from "react"; +import React, { ReactNode, createRef, useCallback, useEffect, useState, useId } from "react"; import EditIcon from "@vector-im/compound-design-tokens/assets/web/icons/edit"; import UploadIcon from "@vector-im/compound-design-tokens/assets/web/icons/share"; import DeleteIcon from "@vector-im/compound-design-tokens/assets/web/icons/delete"; @@ -16,7 +16,6 @@ import classNames from "classnames"; import { _t } from "../../../languageHandler"; import { mediaFromMxc } from "../../../customisations/Media"; import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; -import { useId } from "../../../utils/useId"; import AccessibleButton from "../elements/AccessibleButton"; import BaseAvatar from "../avatars/BaseAvatar"; import Modal from "../../../Modal.tsx"; diff --git a/src/components/views/settings/CryptographyPanel.tsx b/src/components/views/settings/CryptographyPanel.tsx index b418c0b05d..fbd696f243 100644 --- a/src/components/views/settings/CryptographyPanel.tsx +++ b/src/components/views/settings/CryptographyPanel.tsx @@ -32,7 +32,7 @@ interface IState { export default class CryptographyPanel extends React.Component<IProps, IState> { public static contextType = MatrixClientContext; - public declare context: React.ContextType<typeof MatrixClientContext>; + declare public context: React.ContextType<typeof MatrixClientContext>; public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) { super(props); diff --git a/src/components/views/settings/SecureBackupPanel.tsx b/src/components/views/settings/SecureBackupPanel.tsx index 06c67c7d0b..05c7829644 100644 --- a/src/components/views/settings/SecureBackupPanel.tsx +++ b/src/components/views/settings/SecureBackupPanel.tsx @@ -118,7 +118,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { this.getUpdatedDiagnostics(); try { const cli = MatrixClientPeg.safeGet(); - const backupInfo = await cli.getKeyBackupVersion(); + const backupInfo = (await cli.getCrypto()?.getKeyBackupInfo()) ?? null; const backupTrustInfo = backupInfo ? await cli.getCrypto()?.isKeyBackupTrusted(backupInfo) : undefined; const activeBackupVersion = (await cli.getCrypto()?.getActiveSessionBackupVersion()) ?? null; @@ -192,12 +192,9 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { if (!proceed) return; this.setState({ loading: true }); const versionToDelete = this.state.backupInfo!.version!; - MatrixClientPeg.safeGet() - .getCrypto() - ?.deleteKeyBackupVersion(versionToDelete) - .then(() => { - this.loadBackupStatus(); - }); + // deleteKeyBackupVersion fires a key backup status event + // which will update the UI + MatrixClientPeg.safeGet().getCrypto()?.deleteKeyBackupVersion(versionToDelete); }, }); }; diff --git a/src/components/views/settings/SetIdServer.tsx b/src/components/views/settings/SetIdServer.tsx index 8ed6461d0a..6dc3ae48a2 100644 --- a/src/components/views/settings/SetIdServer.tsx +++ b/src/components/views/settings/SetIdServer.tsx @@ -407,7 +407,6 @@ export default class SetIdServer extends React.Component<IProps, IState> { forceValidity={this.state.error ? false : undefined} /> <AccessibleButton - type="submit" kind="primary_sm" onClick={this.checkIdServer} disabled={!this.idServerChangeEnabled()} diff --git a/src/components/views/settings/UserProfileSettings.tsx b/src/components/views/settings/UserProfileSettings.tsx index 403b6349c9..83a00c122d 100644 --- a/src/components/views/settings/UserProfileSettings.tsx +++ b/src/components/views/settings/UserProfileSettings.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { ChangeEvent, ReactNode, useCallback, useEffect, useMemo, useState } from "react"; +import React, { ChangeEvent, ReactNode, useCallback, useEffect, useMemo, useState, useId } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import { EditInPlace, Alert, ErrorMessage } from "@vector-im/compound-web"; import PopOutIcon from "@vector-im/compound-design-tokens/assets/web/icons/pop-out"; @@ -20,7 +20,6 @@ import { formatBytes } from "../../../utils/FormattingUtils"; import { useToastContext } from "../../../contexts/ToastContext"; import InlineSpinner from "../elements/InlineSpinner"; import UserIdentifierCustomisations from "../../../customisations/UserIdentifier"; -import { useId } from "../../../utils/useId"; import CopyableText from "../elements/CopyableText"; import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; import AccessibleButton from "../elements/AccessibleButton"; diff --git a/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx b/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx index e7839b71da..a04430a0c2 100644 --- a/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx +++ b/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx @@ -7,23 +7,21 @@ Please see LICENSE files in the repository root for full details. */ import classNames from "classnames"; -import React, { ComponentProps } from "react"; +import React from "react"; import { ChevronDownIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { _t } from "../../../../languageHandler"; -import AccessibleButton from "../../elements/AccessibleButton"; +import AccessibleButton, { ButtonProps } from "../../elements/AccessibleButton"; -type Props<T extends keyof JSX.IntrinsicElements> = Omit< - ComponentProps<typeof AccessibleButton<T>>, - "aria-label" | "title" | "kind" | "className" | "onClick" | "element" +type Props<T extends keyof HTMLElementTagNameMap> = Omit< + ButtonProps<T>, + "aria-label" | "title" | "kind" | "className" | "element" > & { isExpanded: boolean; - onClick: () => void; }; -export const DeviceExpandDetailsButton = <T extends keyof JSX.IntrinsicElements>({ +export const DeviceExpandDetailsButton = <T extends keyof HTMLElementTagNameMap>({ isExpanded, - onClick, ...rest }: Props<T>): JSX.Element => { const label = isExpanded ? _t("settings|sessions|hide_details") : _t("settings|sessions|show_details"); @@ -36,7 +34,6 @@ export const DeviceExpandDetailsButton = <T extends keyof JSX.IntrinsicElements> className={classNames("mx_DeviceExpandDetailsButton", { mx_DeviceExpandDetailsButton_expanded: isExpanded, })} - onClick={onClick} > <ChevronDownIcon className="mx_DeviceExpandDetailsButton_icon" /> </AccessibleButton> diff --git a/src/components/views/settings/devices/LoginWithQRSection.tsx b/src/components/views/settings/devices/LoginWithQRSection.tsx index a164ff894b..033aa8e32a 100644 --- a/src/components/views/settings/devices/LoginWithQRSection.tsx +++ b/src/components/views/settings/devices/LoginWithQRSection.tsx @@ -7,13 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; -import { - IServerVersions, - IClientWellKnown, - OidcClientConfig, - MatrixClient, - DEVICE_CODE_SCOPE, -} from "matrix-js-sdk/src/matrix"; +import { IServerVersions, OidcClientConfig, MatrixClient, DEVICE_CODE_SCOPE } from "matrix-js-sdk/src/matrix"; import QrCodeIcon from "@vector-im/compound-design-tokens/assets/web/icons/qr-code"; import { Text } from "@vector-im/compound-web"; @@ -25,7 +19,6 @@ import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext interface IProps { onShowQr: () => void; versions?: IServerVersions; - wellKnown?: IClientWellKnown; oidcClientConfig?: OidcClientConfig; isCrossSigningReady?: boolean; } @@ -35,10 +28,8 @@ export function shouldShowQr( isCrossSigningReady: boolean, oidcClientConfig?: OidcClientConfig, versions?: IServerVersions, - wellKnown?: IClientWellKnown, ): boolean { - const msc4108Supported = - !!versions?.unstable_features?.["org.matrix.msc4108"] || !!wellKnown?.["io.element.rendezvous"]?.server; + const msc4108Supported = !!versions?.unstable_features?.["org.matrix.msc4108"]; const deviceAuthorizationGrantSupported = oidcClientConfig?.metadata?.grant_types_supported.includes(DEVICE_CODE_SCOPE); @@ -51,15 +42,9 @@ export function shouldShowQr( ); } -const LoginWithQRSection: React.FC<IProps> = ({ - onShowQr, - versions, - wellKnown, - oidcClientConfig, - isCrossSigningReady, -}) => { +const LoginWithQRSection: React.FC<IProps> = ({ onShowQr, versions, oidcClientConfig, isCrossSigningReady }) => { const cli = useMatrixClientContext(); - const offerShowQr = shouldShowQr(cli, !!isCrossSigningReady, oidcClientConfig, versions, wellKnown); + const offerShowQr = shouldShowQr(cli, !!isCrossSigningReady, oidcClientConfig, versions); return ( <SettingsSubsection heading={_t("settings|sessions|sign_in_with_qr")}> diff --git a/src/components/views/settings/shared/SettingsSubsection.tsx b/src/components/views/settings/shared/SettingsSubsection.tsx index 3248a5eb90..ba8d9aba6d 100644 --- a/src/components/views/settings/shared/SettingsSubsection.tsx +++ b/src/components/views/settings/shared/SettingsSubsection.tsx @@ -45,7 +45,7 @@ export const SettingsSubsection: React.FC<SettingsSubsectionProps> = ({ mx_SettingsSubsection_newUi: !legacy, })} > - {typeof heading === "string" ? <SettingsSubsectionHeading heading={heading} legacy={legacy} /> : <>{heading}</>} + {typeof heading === "string" ? <SettingsSubsectionHeading heading={heading} /> : <>{heading}</>} {!!description && ( <div className="mx_SettingsSubsection_description"> <SettingsSubsectionText>{description}</SettingsSubsectionText> diff --git a/src/components/views/settings/shared/SettingsSubsectionHeading.tsx b/src/components/views/settings/shared/SettingsSubsectionHeading.tsx index 25439d6a62..8c51eac0c2 100644 --- a/src/components/views/settings/shared/SettingsSubsectionHeading.tsx +++ b/src/components/views/settings/shared/SettingsSubsectionHeading.tsx @@ -12,21 +12,13 @@ import Heading from "../../typography/Heading"; export interface SettingsSubsectionHeadingProps extends HTMLAttributes<HTMLDivElement> { heading: string; - legacy?: boolean; children?: React.ReactNode; } -export const SettingsSubsectionHeading: React.FC<SettingsSubsectionHeadingProps> = ({ - heading, - legacy = true, - children, - ...rest -}) => { - const size = legacy ? "4" : "3"; - +export const SettingsSubsectionHeading: React.FC<SettingsSubsectionHeadingProps> = ({ heading, children, ...rest }) => { return ( <div {...rest} className="mx_SettingsSubsectionHeading"> - <Heading className="mx_SettingsSubsectionHeading_heading" size={size} as="h3"> + <Heading className="mx_SettingsSubsectionHeading_heading" size="4" as="h3"> {heading} </Heading> {children} diff --git a/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx b/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx index d29d82853a..0da257607e 100644 --- a/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx @@ -28,7 +28,7 @@ interface IProps { export default class BridgeSettingsTab extends React.Component<IProps> { public static contextType = MatrixClientContext; - public declare context: React.ContextType<typeof MatrixClientContext>; + declare public context: React.ContextType<typeof MatrixClientContext>; private renderBridgeCard(event: MatrixEvent, room: Room | null): ReactNode { const content = event.getContent(); diff --git a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.tsx index 048fe5df9d..31c361de1b 100644 --- a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.tsx @@ -33,7 +33,7 @@ interface IState { export default class GeneralRoomSettingsTab extends React.Component<IProps, IState> { public static contextType = MatrixClientContext; - public declare context: ContextType<typeof MatrixClientContext>; + declare public context: ContextType<typeof MatrixClientContext>; public constructor(props: IProps, context: ContextType<typeof MatrixClientContext>) { super(props, context); diff --git a/src/components/views/settings/tabs/room/NotificationSettingsTab.tsx b/src/components/views/settings/tabs/room/NotificationSettingsTab.tsx index f668b1ff07..9aabf1edb0 100644 --- a/src/components/views/settings/tabs/room/NotificationSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/NotificationSettingsTab.tsx @@ -42,7 +42,7 @@ export default class NotificationsSettingsTab extends React.Component<IProps, IS private soundUpload = createRef<HTMLInputElement>(); public static contextType = MatrixClientContext; - public declare context: React.ContextType<typeof MatrixClientContext>; + declare public context: React.ContextType<typeof MatrixClientContext>; public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) { super(props, context); diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx index 8261bfd3eb..baf4b41253 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx @@ -19,7 +19,6 @@ import ErrorDialog from "../../../dialogs/ErrorDialog"; import PowerSelector from "../../../elements/PowerSelector"; import SettingsFieldset from "../../SettingsFieldset"; import SettingsStore from "../../../../../settings/SettingsStore"; -import { VoiceBroadcastInfoEventType } from "../../../../../voice-broadcast"; import { ElementCall } from "../../../../../models/Call"; import SdkConfig, { DEFAULTS } from "../../../../../SdkConfig"; import { AddPrivilegedUsers } from "../../AddPrivilegedUsers"; @@ -62,7 +61,6 @@ const plEventsToShow: Record<string, IEventShowOpts> = { // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) "im.vector.modular.widgets": { isState: true, hideForSpace: true }, - [VoiceBroadcastInfoEventType]: { isState: true, hideForSpace: true }, }; // parse a string as an integer; if the input is undefined, or cannot be parsed @@ -81,7 +79,7 @@ interface IBannedUserProps { export class BannedUser extends React.Component<IBannedUserProps> { public static contextType = MatrixClientContext; - public declare context: React.ContextType<typeof MatrixClientContext>; + declare public context: React.ContextType<typeof MatrixClientContext>; private onUnbanClick = (): void => { this.context.unban(this.props.member.roomId, this.props.member.userId).catch((err) => { @@ -134,7 +132,7 @@ interface RolesRoomSettingsTabState { export default class RolesRoomSettingsTab extends React.Component<IProps, RolesRoomSettingsTabState> { public static contextType = MatrixClientContext; - public declare context: React.ContextType<typeof MatrixClientContext>; + declare public context: React.ContextType<typeof MatrixClientContext>; public constructor(props: IProps) { super(props); @@ -289,7 +287,6 @@ export default class RolesRoomSettingsTab extends React.Component<IProps, RolesR // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) "im.vector.modular.widgets": isSpaceRoom ? null : _td("room_settings|permissions|m.widget"), - [VoiceBroadcastInfoEventType]: _td("room_settings|permissions|io.element.voice_broadcast_info"), }; // MSC3401: Native Group VoIP signaling diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx index 1ade503bb2..ece6a7deaf 100644 --- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx @@ -17,6 +17,7 @@ import { EventType, } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; +import { InlineSpinner } from "@vector-im/compound-web"; import { Icon as WarningIcon } from "../../../../../../res/img/warning.svg"; import { _t } from "../../../../../languageHandler"; @@ -53,13 +54,13 @@ interface IState { guestAccess: GuestAccess; history: HistoryVisibility; hasAliases: boolean; - encrypted: boolean; + encrypted: boolean | null; showAdvancedSection: boolean; } export default class SecurityRoomSettingsTab extends React.Component<IProps, IState> { public static contextType = MatrixClientContext; - public declare context: React.ContextType<typeof MatrixClientContext>; + declare public context: React.ContextType<typeof MatrixClientContext>; public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) { super(props, context); @@ -78,7 +79,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt HistoryVisibility.Shared, ), hasAliases: false, // async loaded in componentDidMount - encrypted: false, // async loaded in componentDidMount + encrypted: null, // async loaded in componentDidMount showAdvancedSection: false, }; } @@ -419,6 +420,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt const client = this.context; const room = this.props.room; const isEncrypted = this.state.encrypted; + const isEncryptionLoading = isEncrypted === null; const hasEncryptionPermission = room.currentState.mayClientSendStateEvent(EventType.RoomEncryption, client); const isEncryptionForceDisabled = shouldForceDisableEncryption(client); const canEnableEncryption = !isEncrypted && !isEncryptionForceDisabled && hasEncryptionPermission; @@ -451,18 +453,23 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt : _t("room_settings|security|encryption_permanent") } > - <LabelledToggleSwitch - value={isEncrypted} - onChange={this.onEncryptionChange} - label={_t("common|encrypted")} - disabled={!canEnableEncryption} - /> - {isEncryptionForceDisabled && !isEncrypted && ( - <Caption>{_t("room_settings|security|encryption_forced")}</Caption> + {isEncryptionLoading ? ( + <InlineSpinner /> + ) : ( + <> + <LabelledToggleSwitch + value={isEncrypted} + onChange={this.onEncryptionChange} + label={_t("common|encrypted")} + disabled={!canEnableEncryption} + /> + {isEncryptionForceDisabled && !isEncrypted && ( + <Caption>{_t("room_settings|security|encryption_forced")}</Caption> + )} + {encryptionSettings} + </> )} - {encryptionSettings} </SettingsFieldset> - {this.renderJoinRule()} {historySection} </SettingsSection> diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx index 7866131a01..f19343be20 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx @@ -32,7 +32,7 @@ interface IState { export default class HelpUserSettingsTab extends React.Component<IProps, IState> { public static contextType = MatrixClientContext; - public declare context: React.ContextType<typeof MatrixClientContext>; + declare public context: React.ContextType<typeof MatrixClientContext>; public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) { super(props, context); diff --git a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx index 9ad7df31e9..3e86d779ff 100644 --- a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx @@ -268,7 +268,6 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState> onChange={this.onPersonalRuleChanged} /> <AccessibleButton - type="submit" kind="primary" onClick={this.onAddPersonalRule} disabled={this.state.busy} @@ -295,12 +294,7 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState> value={this.state.newList} onChange={this.onNewListChanged} /> - <AccessibleButton - type="submit" - kind="primary" - onClick={this.onSubscribeList} - disabled={this.state.busy} - > + <AccessibleButton kind="primary" onClick={this.onSubscribeList} disabled={this.state.busy}> {_t("action|subscribe")} </AccessibleButton> </form> diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index 2e16f45762..5e9445bb99 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { lazy, Suspense, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; +import React, { lazy, Suspense, useCallback, useContext, useEffect, useRef, useState } from "react"; import { discoverAndValidateOIDCIssuerWellKnown, MatrixClient } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { defer } from "matrix-js-sdk/src/utils"; @@ -184,7 +184,6 @@ const SessionManagerTab: React.FC<{ const userId = matrixClient?.getUserId(); const currentUserMember = (userId && matrixClient?.getUser(userId)) || undefined; const clientVersions = useAsyncMemo(() => matrixClient.getVersions(), [matrixClient]); - const wellKnown = useMemo(() => matrixClient?.getClientWellKnown(), [matrixClient]); const oidcClientConfig = useAsyncMemo(async () => { try { const authIssuer = await matrixClient?.getAuthIssuer(); @@ -305,7 +304,6 @@ const SessionManagerTab: React.FC<{ <LoginWithQRSection onShowQr={onShowQrClicked} versions={clientVersions} - wellKnown={wellKnown} oidcClientConfig={oidcClientConfig} isCrossSigningReady={isCrossSigningReady} /> diff --git a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx index 36d336faa3..9711159a10 100644 --- a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx @@ -51,7 +51,7 @@ const mapDeviceKindToHandlerValue = (deviceKind: MediaDeviceKindEnum): string | export default class VoiceUserSettingsTab extends React.Component<{}, IState> { public static contextType = MatrixClientContext; - public declare context: React.ContextType<typeof MatrixClientContext>; + declare public context: React.ContextType<typeof MatrixClientContext>; public constructor(props: {}, context: React.ContextType<typeof MatrixClientContext>) { super(props, context); diff --git a/src/components/views/spaces/SpaceBasicSettings.tsx b/src/components/views/spaces/SpaceBasicSettings.tsx index 8311e6728e..63a70a97cd 100644 --- a/src/components/views/spaces/SpaceBasicSettings.tsx +++ b/src/components/views/spaces/SpaceBasicSettings.tsx @@ -71,7 +71,6 @@ export const SpaceAvatar: React.FC<Pick<IProps, "avatarUrl" | "avatarDisabled" | <AccessibleButton className="mx_SpaceBasicSettings_avatar" onClick={() => avatarUploadRef.current?.click()} - alt="" /> <AccessibleButton onClick={() => avatarUploadRef.current?.click()} diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index af484445b4..73bb66af38 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -221,7 +221,7 @@ const CreateSpaceButton: React.FC<Pick<IInnerSpacePanelProps, "isPanelCollapsed" isPanelCollapsed, setPanelCollapsed, }) => { - const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLElement>(); + const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>(); useEffect(() => { if (!isPanelCollapsed && menuDisplayed) { diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index cee4cf54ec..38329c39b7 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -30,7 +30,7 @@ import defaultDispatcher from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton"; import { toRightOf, useContextMenu } from "../../structures/ContextMenu"; -import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; +import AccessibleButton, { ButtonEvent, ButtonProps as AccessibleButtonProps } from "../elements/AccessibleButton"; import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; import { NotificationLevel } from "../../../stores/notifications/NotificationLevel"; import { getKeyBindingsManager } from "../../../KeyBindingsManager"; @@ -39,8 +39,8 @@ import SpaceContextMenu from "../context_menus/SpaceContextMenu"; import { useRovingTabIndex } from "../../../accessibility/RovingTabIndex"; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; -type ButtonProps<T extends keyof JSX.IntrinsicElements> = Omit< - ComponentProps<typeof AccessibleButton<T>>, +type ButtonProps<T extends keyof HTMLElementTagNameMap> = Omit< + AccessibleButtonProps<T>, "title" | "onClick" | "size" | "element" > & { space?: Room; @@ -52,12 +52,12 @@ type ButtonProps<T extends keyof JSX.IntrinsicElements> = Omit< notificationState?: NotificationState; isNarrow?: boolean; size: string; - innerRef?: RefObject<HTMLElement>; + innerRef?: RefObject<HTMLDivElement>; ContextMenuComponent?: ComponentType<ComponentProps<typeof SpaceContextMenu>>; onClick?(ev?: ButtonEvent): void; }; -export const SpaceButton = <T extends keyof JSX.IntrinsicElements>({ +export const SpaceButton = <T extends keyof HTMLElementTagNameMap>({ space, spaceKey: _spaceKey, className, @@ -72,8 +72,8 @@ export const SpaceButton = <T extends keyof JSX.IntrinsicElements>({ ContextMenuComponent, ...props }: ButtonProps<T>): JSX.Element => { - const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLElement>(innerRef); - const [onFocus, isActive, ref] = useRovingTabIndex(handle); + const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>(innerRef); + const [onFocus, isActive, ref] = useRovingTabIndex<HTMLDivElement>(handle); const tabIndex = isActive ? 0 : -1; const spaceKey = _spaceKey ?? space?.roomId; diff --git a/src/components/views/toasts/VerificationRequestToast.tsx b/src/components/views/toasts/VerificationRequestToast.tsx index 54932a12ed..7d31aa6764 100644 --- a/src/components/views/toasts/VerificationRequestToast.tsx +++ b/src/components/views/toasts/VerificationRequestToast.tsx @@ -117,7 +117,7 @@ export default class VerificationRequestToast extends React.PureComponent<IProps RightPanelStore.instance.setCards( [ { phase: RightPanelPhases.RoomSummary }, - { phase: RightPanelPhases.RoomMemberInfo, state: { member } }, + { phase: RightPanelPhases.MemberInfo, state: { member } }, { phase: RightPanelPhases.EncryptionPanel, state: { verificationRequest: request, member } }, ], undefined, diff --git a/src/components/views/verification/VerificationShowSas.tsx b/src/components/views/verification/VerificationShowSas.tsx index 139c5f7286..1e053a7e99 100644 --- a/src/components/views/verification/VerificationShowSas.tsx +++ b/src/components/views/verification/VerificationShowSas.tsx @@ -14,7 +14,6 @@ import SasEmoji from "@matrix-org/spec/sas-emoji.json"; import { _t, getNormalizedLanguageKeys, getUserLanguage } from "../../../languageHandler"; import { PendingActionSpinner } from "../right_panel/EncryptionInfo"; import AccessibleButton from "../elements/AccessibleButton"; -import { fixupColorFonts } from "../../../utils/FontManager"; interface IProps { pending?: boolean; @@ -88,11 +87,6 @@ export default class VerificationShowSas extends React.Component<IProps, IState> this.state = { pending: false, }; - - // As this component is also used before login (during complete security), - // also make sure we have a working emoji font to display the SAS emojis here. - // This is also done from LoggedInView. - fixupColorFonts(); } private onMatchClick = (): void => { diff --git a/src/components/views/voip/LegacyCallView/LegacyCallViewButtons.tsx b/src/components/views/voip/LegacyCallView/LegacyCallViewButtons.tsx index bdcd3713cb..105736d04e 100644 --- a/src/components/views/voip/LegacyCallView/LegacyCallViewButtons.tsx +++ b/src/components/views/voip/LegacyCallView/LegacyCallViewButtons.tsx @@ -69,7 +69,7 @@ interface IDropdownButtonProps extends ButtonProps { } const LegacyCallViewDropdownButton: React.FC<IDropdownButtonProps> = ({ state, deviceKinds, ...props }) => { - const [menuDisplayed, buttonRef, openMenu, closeMenu] = useContextMenu(); + const [menuDisplayed, buttonRef, openMenu, closeMenu] = useContextMenu<HTMLDivElement>(); const [hoveringDropdown, setHoveringDropdown] = useState(false); const classes = classNames("mx_LegacyCallViewButtons_button", "mx_LegacyCallViewButtons_dropdownButton", { diff --git a/src/contexts/RoomContext.ts b/src/contexts/RoomContext.ts index acd4bfa6f4..4303c46a34 100644 --- a/src/contexts/RoomContext.ts +++ b/src/contexts/RoomContext.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { createContext, useContext } from "react"; +import { createContext } from "react"; import { IRoomState } from "../components/structures/RoomView"; import { Layout } from "../settings/enums/Layout"; @@ -75,9 +75,7 @@ const RoomContext = createContext< canAskToJoin: false, promptAskToJoin: false, viewRoomOpts: { buttons: [] }, + isRoomEncrypted: null, }); RoomContext.displayName = "RoomContext"; export default RoomContext; -export function useRoomContext(): IRoomState { - return useContext(RoomContext); -} diff --git a/src/contexts/SDKContext.ts b/src/contexts/SDKContext.ts index 28e7e3aadb..fe73661554 100644 --- a/src/contexts/SDKContext.ts +++ b/src/contexts/SDKContext.ts @@ -25,11 +25,6 @@ import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore"; import { WidgetPermissionStore } from "../stores/widgets/WidgetPermissionStore"; import { OidcClientStore } from "../stores/oidc/OidcClientStore"; import WidgetStore from "../stores/WidgetStore"; -import { - VoiceBroadcastPlaybacksStore, - VoiceBroadcastPreRecordingStore, - VoiceBroadcastRecordingsStore, -} from "../voice-broadcast"; // This context is available to components under MatrixChat, // the context must not be used by components outside a SdkContextClass tree. @@ -68,9 +63,6 @@ export class SdkContextClass { protected _SpaceStore?: SpaceStoreClass; protected _LegacyCallHandler?: LegacyCallHandler; protected _TypingStore?: TypingStore; - protected _VoiceBroadcastRecordingsStore?: VoiceBroadcastRecordingsStore; - protected _VoiceBroadcastPreRecordingStore?: VoiceBroadcastPreRecordingStore; - protected _VoiceBroadcastPlaybacksStore?: VoiceBroadcastPlaybacksStore; protected _AccountPasswordStore?: AccountPasswordStore; protected _UserProfilesStore?: UserProfilesStore; protected _OidcClientStore?: OidcClientStore; @@ -157,27 +149,6 @@ export class SdkContextClass { return this._TypingStore; } - public get voiceBroadcastRecordingsStore(): VoiceBroadcastRecordingsStore { - if (!this._VoiceBroadcastRecordingsStore) { - this._VoiceBroadcastRecordingsStore = new VoiceBroadcastRecordingsStore(); - } - return this._VoiceBroadcastRecordingsStore; - } - - public get voiceBroadcastPreRecordingStore(): VoiceBroadcastPreRecordingStore { - if (!this._VoiceBroadcastPreRecordingStore) { - this._VoiceBroadcastPreRecordingStore = new VoiceBroadcastPreRecordingStore(); - } - return this._VoiceBroadcastPreRecordingStore; - } - - public get voiceBroadcastPlaybacksStore(): VoiceBroadcastPlaybacksStore { - if (!this._VoiceBroadcastPlaybacksStore) { - this._VoiceBroadcastPlaybacksStore = new VoiceBroadcastPlaybacksStore(this.voiceBroadcastRecordingsStore); - } - return this._VoiceBroadcastPlaybacksStore; - } - public get accountPasswordStore(): AccountPasswordStore { if (!this._AccountPasswordStore) { this._AccountPasswordStore = new AccountPasswordStore(); diff --git a/src/contexts/ScopedRoomContext.tsx b/src/contexts/ScopedRoomContext.tsx new file mode 100644 index 0000000000..1222443d29 --- /dev/null +++ b/src/contexts/ScopedRoomContext.tsx @@ -0,0 +1,78 @@ +/* +Copyright 2024 New Vector Ltd. +Copyright 2019 The Matrix.org Foundation C.I.C. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import { TypedEventEmitter } from "matrix-js-sdk/src/matrix"; +import React, { ContextType, createContext, memo, ReactNode, useContext, useEffect, useRef, useState } from "react"; + +import { objectKeyChanges } from "../utils/objects.ts"; +import { useTypedEventEmitter } from "../hooks/useEventEmitter.ts"; +import RoomContext from "./RoomContext.ts"; + +// React Contexts with frequently changing values (like State where the object reference is changed on every update) +// cause performance issues by triggering a re-render on every component subscribed to that context. +// With ScopedRoomContext we're effectively setting up virtual contexts which are a subset of the overall context object +// and subscribers specify which fields they care about, and they will only be awoken on updates to those specific fields. + +type ContextValue = ContextType<typeof RoomContext>; + +export enum NotificationStateEvents { + Update = "update", +} + +type EventHandlerMap<C extends Record<string, any>> = { + [NotificationStateEvents.Update]: (keys: Array<keyof C>) => void; +}; + +class EfficientContext<C extends Record<string, any>> extends TypedEventEmitter< + NotificationStateEvents, + EventHandlerMap<C> +> { + public constructor(public state: C) { + super(); + } + + public setState(state: C): void { + const changedKeys = objectKeyChanges(this.state ?? ({} as C), state); + this.state = state; + this.emit(NotificationStateEvents.Update, changedKeys); + } +} + +const ScopedRoomContext = createContext<EfficientContext<ContextValue> | undefined>(undefined); + +// Uses react memo and leverages splatting the value to ensure that the context is only updated when the state changes (shallow compare) +export const ScopedRoomContextProvider = memo( + ({ children, ...state }: { children: ReactNode } & ContextValue): JSX.Element => { + const contextRef = useRef(new EfficientContext<ContextValue>(state)); + useEffect(() => { + contextRef.current.setState(state); + }, [state]); + + // Includes the legacy RoomContext provider for backwards compatibility with class components + return ( + <RoomContext.Provider value={state}> + <ScopedRoomContext.Provider value={contextRef.current}>{children}</ScopedRoomContext.Provider> + </RoomContext.Provider> + ); + }, +); + +type ScopedRoomContext<K extends Array<keyof ContextValue>> = { [key in K[number]]: ContextValue[key] }; + +export function useScopedRoomContext<K extends Array<keyof ContextValue>>(...keys: K): ScopedRoomContext<K> { + const context = useContext(ScopedRoomContext); + const [state, setState] = useState<ScopedRoomContext<K>>(context?.state ?? ({} as ScopedRoomContext<K>)); + + useTypedEventEmitter(context, NotificationStateEvents.Update, (updatedKeys: K): void => { + if (context?.state && updatedKeys.some((updatedKey) => keys.includes(updatedKey))) { + setState(context.state); + } + }); + + return state; +} diff --git a/src/events/EventTileFactory.tsx b/src/events/EventTileFactory.tsx index ff771b0467..ec1651872a 100644 --- a/src/events/EventTileFactory.tsx +++ b/src/events/EventTileFactory.tsx @@ -41,13 +41,7 @@ import { getMessageModerationState, MessageModerationState } from "../utils/Even import HiddenBody from "../components/views/messages/HiddenBody"; import ViewSourceEvent from "../components/views/messages/ViewSourceEvent"; import { shouldDisplayAsBeaconTile } from "../utils/beacon/timeline"; -import { shouldDisplayAsVoiceBroadcastTile } from "../voice-broadcast/utils/shouldDisplayAsVoiceBroadcastTile"; import { ElementCall } from "../models/Call"; -import { - isRelatedToVoiceBroadcast, - shouldDisplayAsVoiceBroadcastStoppedText, - VoiceBroadcastChunkEventType, -} from "../voice-broadcast"; // Subset of EventTile's IProps plus some mixins export interface EventTileTypeProps @@ -223,12 +217,6 @@ export function pickFactory( return MessageEventFactory; } - if (shouldDisplayAsVoiceBroadcastTile(mxEvent)) { - return MessageEventFactory; - } else if (shouldDisplayAsVoiceBroadcastStoppedText(mxEvent)) { - return TextualEventFactory; - } - if (SINGULAR_STATE_EVENTS.has(evType) && mxEvent.getStateKey() !== "") { return noEventFactoryFactory(); // improper event type to render } @@ -249,16 +237,6 @@ export function pickFactory( return noEventFactoryFactory(); } - if (mxEvent.getContent()[VoiceBroadcastChunkEventType]) { - // hide voice broadcast chunks - return noEventFactoryFactory(); - } - - if (!showHiddenEvents && mxEvent.isDecryptionFailure() && isRelatedToVoiceBroadcast(mxEvent, cli)) { - // hide utd events related to a broadcast - return noEventFactoryFactory(); - } - return EVENT_TILE_TYPES.get(evType) ?? noEventFactoryFactory(); } diff --git a/src/events/forward/getForwardableEvent.ts b/src/events/forward/getForwardableEvent.ts index 000a50f4ee..2d37ebf6e9 100644 --- a/src/events/forward/getForwardableEvent.ts +++ b/src/events/forward/getForwardableEvent.ts @@ -9,7 +9,6 @@ Please see LICENSE files in the repository root for full details. import { M_POLL_END, M_POLL_START, M_BEACON_INFO, MatrixEvent, MatrixClient } from "matrix-js-sdk/src/matrix"; import { getShareableLocationEventForBeacon } from "../../utils/beacon/getShareableLocation"; -import { VoiceBroadcastInfoEventType } from "../../voice-broadcast/types"; /** * Get forwardable event for a given event @@ -20,8 +19,6 @@ export const getForwardableEvent = (event: MatrixEvent, cli: MatrixClient): Matr return null; } - if (event.getType() === VoiceBroadcastInfoEventType) return null; - // Live location beacons should forward their latest location as a static pin location // If the beacon is not live, or doesn't have a location forwarding is not allowed if (M_BEACON_INFO.matches(event.getType())) { diff --git a/src/hooks/room/useRoomMemberProfile.ts b/src/hooks/room/useRoomMemberProfile.ts index 57f72a722e..b8bb44c50d 100644 --- a/src/hooks/room/useRoomMemberProfile.ts +++ b/src/hooks/room/useRoomMemberProfile.ts @@ -7,10 +7,11 @@ Please see LICENSE files in the repository root for full details. */ import { RoomMember } from "matrix-js-sdk/src/matrix"; -import { useContext, useMemo } from "react"; +import { useMemo } from "react"; -import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; +import { TimelineRenderingType } from "../../contexts/RoomContext"; import { useSettingValue } from "../useSettings"; +import { useScopedRoomContext } from "../../contexts/ScopedRoomContext.tsx"; export function useRoomMemberProfile({ userId = "", @@ -21,7 +22,7 @@ export function useRoomMemberProfile({ member?: RoomMember | null; forceHistorical?: boolean; }): RoomMember | undefined | null { - const context = useContext(RoomContext); + const context = useScopedRoomContext("room", "timelineRenderingType"); const useOnlyCurrentProfiles = useSettingValue("useOnlyCurrentProfiles"); const member = useMemo(() => { diff --git a/src/hooks/useAudioDeviceSelection.ts b/src/hooks/useAudioDeviceSelection.ts deleted file mode 100644 index 504eb10ea6..0000000000 --- a/src/hooks/useAudioDeviceSelection.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { useRef, useState } from "react"; - -import { _t } from "../languageHandler"; -import MediaDeviceHandler, { MediaDeviceKindEnum } from "../MediaDeviceHandler"; -import { requestMediaPermissions } from "../utils/media/requestMediaPermissions"; - -interface State { - devices: MediaDeviceInfo[]; - device: MediaDeviceInfo | null; -} - -export const useAudioDeviceSelection = ( - onDeviceChanged?: (device: MediaDeviceInfo) => void, -): { - currentDevice: MediaDeviceInfo | null; - currentDeviceLabel: string; - devices: MediaDeviceInfo[]; - setDevice(device: MediaDeviceInfo): void; -} => { - const shouldRequestPermissionsRef = useRef<boolean>(true); - const [state, setState] = useState<State>({ - devices: [], - device: null, - }); - - if (shouldRequestPermissionsRef.current) { - shouldRequestPermissionsRef.current = false; - requestMediaPermissions(false).then((stream: MediaStream | undefined) => { - MediaDeviceHandler.getDevices().then((devices) => { - if (!devices) return; - const { audioinput } = devices; - MediaDeviceHandler.getDefaultDevice(audioinput); - const deviceFromSettings = MediaDeviceHandler.getAudioInput(); - const device = - audioinput.find((d) => { - return d.deviceId === deviceFromSettings; - }) || audioinput[0]; - setState({ - ...state, - devices: audioinput, - device, - }); - stream?.getTracks().forEach((t) => t.stop()); - }); - }); - } - - const setDevice = (device: MediaDeviceInfo): void => { - const shouldNotify = device.deviceId !== state.device?.deviceId; - MediaDeviceHandler.instance.setDevice(device.deviceId, MediaDeviceKindEnum.AudioInput); - - setState({ - ...state, - device, - }); - - if (shouldNotify) { - onDeviceChanged?.(device); - } - }; - - return { - currentDevice: state.device, - currentDeviceLabel: state.device?.label || _t("voip|default_device"), - devices: state.devices, - setDevice, - }; -}; diff --git a/src/hooks/useEncryptionStatus.ts b/src/hooks/useEncryptionStatus.ts index 686f68f25e..ed8cceb9f8 100644 --- a/src/hooks/useEncryptionStatus.ts +++ b/src/hooks/useEncryptionStatus.ts @@ -6,9 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { CryptoEvent, MatrixClient, Room, RoomStateEvent } from "matrix-js-sdk/src/matrix"; +import { MatrixClient, Room, RoomStateEvent } from "matrix-js-sdk/src/matrix"; import { useEffect, useMemo, useState } from "react"; import { throttle } from "lodash"; +import { CryptoEvent } from "matrix-js-sdk/src/crypto-api"; import { E2EStatus, shieldStatusForRoom } from "../utils/ShieldUtils"; import { useTypedEventEmitter } from "./useEventEmitter"; diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index 7647377196..df8a243c73 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -882,7 +882,6 @@ }, "udd": { "interactive_verification_button": "Interaktivní ověření pomocí emoji", - "manual_verification_button": "Ruční ověření pomocí textu", "other_ask_verify_text": "Požádejte tohoto uživatele, aby ověřil svou relaci, nebo jí níže můžete ověřit manuálně.", "other_new_session_text": "%(name)s (%(userId)s) se přihlásil(a) do nové relace bez ověření:", "own_ask_verify_text": "Ověřte další relaci jedním z následujících způsobů.", @@ -917,12 +916,6 @@ "incoming_sas_dialog_waiting": "Čekání na potvrzení partnerem…", "incoming_sas_user_dialog_text_1": "Po ověření bude uživatel označen jako důvěryhodný. Ověřování uživatelů vám dává větší jistotu, že je komunikace důvěrná.", "incoming_sas_user_dialog_text_2": "Ověření uživatele označí jeho relace za důvěryhodné a vaše relace budou důvěryhodné pro něj.", - "manual_device_verification_device_id_label": "ID sezení", - "manual_device_verification_device_key_label": "Klíč relace", - "manual_device_verification_device_name_label": "Název relace", - "manual_device_verification_footer": "Pokud se neshodují, bezpečnost vaší komunikace může být kompromitována.", - "manual_device_verification_self_text": "Potvrďte porovnáním následujícího s uživatelským nastavením v jiné relaci:", - "manual_device_verification_user_text": "Potvrďte relaci tohoto uživatele porovnáním následujícího s jeho uživatelským nastavením:", "no_key_or_device": "Vypadá to, že nemáte bezpečnostní klíč ani žádné jiné zařízení, které byste mohli ověřit. Toto zařízení nebude mít přístup ke starým šifrovaným zprávám. Abyste mohli na tomto zařízení ověřit svou totožnost, budete muset resetovat ověřovací klíče.", "no_support_qr_emoji": "Zařízení, které se snažíte ověřit, neumožňuje ověření QR kódem ani pomocí emotikonů, které %(brand)s podporuje. Zkuste použít jiného klienta.", "other_party_cancelled": "Druhá strana ověření zrušila.", @@ -1046,10 +1039,6 @@ }, "error_user_not_logged_in": "Uživatel není přihlášen", "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s ukončil(a) hlasové vysílání", - "you": "Ukončili jste hlasové vysílání" - }, "m.call.answer": { "dm": "Probíhá hovor", "user": "%(senderName)s se připojil k hovoru", @@ -1423,8 +1412,6 @@ "video_rooms_faq2_answer": "Ano, časová osa chatu se zobrazuje vedle videa.", "video_rooms_faq2_question": "Mohu vedle videohovoru používat i textový chat?", "video_rooms_feedbackSubheading": "Děkujeme, že jste vyzkoušeli beta verzi, a prosíme vás o co nejpodrobnější informace, abychom ji mohli vylepšit.", - "voice_broadcast": "Hlasové vysílání", - "voice_broadcast_force_small_chunks": "Vynutit 15s délku bloku hlasového vysílání", "wysiwyg_composer": "Editor formátovaného textu" }, "labs_mjolnir": { @@ -1568,7 +1555,6 @@ "mute_description": "Nebudete dostávat žádná oznámení" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s zahájil(a) hlasové vysílání", "m.key.verification.request": "%(name)s žádá o ověření" }, "onboarding": { @@ -2145,7 +2131,6 @@ "error_unbanning": "Zrušení vykázání se nezdařilo", "events_default": "Posílat zprávy", "invite": "Zvát uživatele", - "io.element.voice_broadcast_info": "Hlasová vysílání", "kick": "Odebrat uživatele", "m.call": "Zahájit %(brand)s volání", "m.call.member": "Připojit se k %(brand)s volání", @@ -2788,7 +2773,6 @@ "warning": "<w>UPOZORNĚNÍ:</w> <description/>" }, "share": { - "link_title": "Odkaz na místnost", "permalink_message": "Odkaz na vybranou zprávu", "permalink_most_recent": "Odkaz na nejnovější zprávu", "share_call": "Odkaz na pozvánku na konferenci", @@ -2880,13 +2864,6 @@ "upgraderoom": "Aktualizuje místnost na novou verzi", "upgraderoom_permission_error": "Na provedení tohoto příkazu nemáte dostatečná oprávnění.", "usage": "Použití", - "verify": "Ověří uživatele, relaci a veřejné klíče", - "verify_mismatch": "VAROVÁNÍ: OVĚŘENÍ KLÍČE SE NEZDAŘILO! Podpisový klíč pro uživatele %(userId)s a relaci %(deviceId)s je „%(fprint)s“, což neodpovídá klíči „%(fingerprint)s“. To by mohlo znamenat, že vaše komunikace je zachycována!", - "verify_nop": "Relace je už ověřená!", - "verify_nop_warning_mismatch": "VAROVÁNÍ: relace již byla ověřena, ale klíče se NESHODUJÍ!", - "verify_success_description": "Zadaný podpisový klíč odpovídá klíči relace %(deviceId)s od uživatele %(userId)s. Relace byla označena jako ověřená.", - "verify_success_title": "Ověřený klíč", - "verify_unknown_pair": "Neznámý pár (uživatel, relace): (%(userId)s, %(deviceId)s)", "view": "Zobrazí místnost s danou adresou", "whois": "Zobrazuje informace o uživateli" }, @@ -3104,10 +3081,6 @@ "error_rendering_message": "Tuto zprávu nelze načíst", "historical_messages_unavailable": "Dřívější zprávy nelze zobrazit", "in_room_name": " v <strong>%(room)s</strong>", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s ukončil(a) <a>hlasové vysílání</a>", - "you": "Ukončili jste <a>hlasové vysílání</a>" - }, "io.element.widgets.layout": "%(senderName)s aktualizoval rozvržení místnosti", "late_event_separator": "Původně odesláno %(dateTime)s", "load_error": { @@ -3640,38 +3613,6 @@ "switch_theme_dark": "Přepnout do tmavého režimu", "switch_theme_light": "Přepnout do světlého režimu" }, - "voice_broadcast": { - "30s_backward": "30s zpět", - "30s_forward": "30s vpřed", - "action": "Hlasové vysílání", - "buffering": "Ukládání do vyrovnávací paměti…", - "confirm_listen_affirm": "Ano, ukončit nahrávání", - "confirm_listen_description": "Jakmile začnete poslouchat toto živé vysílání, aktuální záznam živého vysílání bude ukončen.", - "confirm_listen_title": "Poslouchat živé vysílání?", - "confirm_stop_affirm": "Ano, zastavit vysílání", - "confirm_stop_description": "Opravdu chcete ukončit živé vysílání? Tím se vysílání ukončí a v místnosti bude k dispozici celý záznam.", - "confirm_stop_title": "Ukončit živé vysílání?", - "connection_error": "Chyba připojení - nahrávání pozastaveno", - "failed_already_recording_description": "Již nahráváte hlasové vysílání. Ukončete prosím aktuální hlasové vysílání a spusťte nové.", - "failed_already_recording_title": "Nelze spustit nové hlasové vysílání", - "failed_decrypt": "Nelze dešifrovat hlasové vysílání", - "failed_generic": "Nelze přehrát toto hlasové vysílání", - "failed_insufficient_permission_description": "Nemáte potřebná oprávnění ke spuštění hlasového vysílání v této místnosti. Obraťte se na správce místnosti, aby vám zvýšil oprávnění.", - "failed_insufficient_permission_title": "Nelze spustit nové hlasové vysílání", - "failed_no_connection_description": "Bohužel nyní nemůžeme spustit nahrávání. Zkuste to prosím později.", - "failed_no_connection_title": "Chyba připojení", - "failed_others_already_recording_description": "Hlasové vysílání už nahrává někdo jiný. Počkejte, až jeho hlasové vysílání skončí, a spusťte nové.", - "failed_others_already_recording_title": "Nelze spustit nové hlasové vysílání", - "go_live": "Přejít naživo", - "live": "Živě", - "pause": "pozastavit hlasové vysílání", - "play": "přehrát hlasové vysílání", - "resume": "obnovit hlasové vysílání" - }, - "voice_message": { - "cant_start_broadcast_description": "Hlasovou zprávu nelze spustit, protože právě nahráváte živé vysílání. Ukončete prosím živé vysílání, abyste mohli začít nahrávat hlasovou zprávu.", - "cant_start_broadcast_title": "Nelze spustit hlasovou zprávu" - }, "voip": { "already_in_call": "Již máte hovor", "already_in_call_person": "S touto osobou již telefonujete.", @@ -3691,7 +3632,6 @@ "camera_disabled": "Vaše kamera je vypnutá", "camera_enabled": "Vaše kamera je stále zapnutá", "cannot_call_yourself_description": "Nemůžete volat sami sobě.", - "change_input_device": "Změnit vstupní zařízení", "close_lobby": "Zavřít lobby", "connecting": "Spojování", "connection_lost": "Došlo ke ztrátě připojení k serveru", @@ -3710,8 +3650,6 @@ "enable_camera": "Zapnout kameru", "enable_microphone": "Zrušit ztlumení mikrofonu", "expand": "Návrat do hovoru", - "failed_call_live_broadcast_description": "Nemůžete zahájit hovor, protože právě nahráváte živé vysílání. Ukončete prosím živé vysílání, abyste mohli zahájit hovor.", - "failed_call_live_broadcast_title": "Nelze zahájit hovor", "get_call_link": "Sdílet odkaz na hovor", "hangup": "Zavěsit", "hide_sidebar_button": "Skrýt postranní panel", diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index abe4566f8c..5a4a6043fb 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -2,6 +2,7 @@ "a11y": { "emoji_picker": "Emoji-Auswahl", "jump_first_invite": "Zur ersten Einladung springen.", + "message_composer": "Nachrichteneingabe-Feld", "n_unread_messages": { "other": "%(count)s ungelesene Nachrichten.", "one": "1 ungelesene Nachricht." @@ -10,11 +11,14 @@ "other": "%(count)s ungelesene Nachrichten einschließlich Erwähnungen.", "one": "1 ungelesene Erwähnung." }, + "recent_rooms": "Kürzlich besuchte Chatrooms", "room_name": "Raum %(name)s", + "room_status_bar": "Statusleiste des Chatrooms", + "seek_bar_label": "Audio-Suchleiste", "unread_messages": "Ungelesene Nachrichten.", "user_menu": "Benutzermenü" }, - "a11y_jump_first_unread_room": "Zum ersten ungelesenen Raum springen.", + "a11y_jump_first_unread_room": "Springen Sie zum ersten ungelesenen Chatroom.", "action": { "accept": "Annehmen", "add": "Hinzufügen", @@ -106,6 +110,7 @@ "save": "Speichern", "search": "Suchen", "send_report": "Bericht senden", + "set_avatar": "Profilbild festlegen", "share": "Teilen", "show": "Zeigen", "show_advanced": "Erweiterte Einstellungen", @@ -123,12 +128,13 @@ "trust": "Vertrauen", "try_again": "Erneut versuchen", "unban": "Verbannung aufheben", - "unignore": "Nicht mehr blockieren", + "unignore": "Freigeben", "unpin": "Nicht mehr anheften", "unsubscribe": "Deabonnieren", "update": "Aktualisieren", "upgrade": "Aktualisieren", "upload": "Hochladen", + "upload_file": "Datei hochladen", "verify": "Verifizieren", "view": "Ansicht", "view_all": "Alles anzeigen", @@ -143,7 +149,7 @@ "accept_button": "Das ist okay", "bullet_1": "Wir erfassen und analysieren <Bold>keine</Bold> Kontodaten", "bullet_2": "Wir teilen <Bold>keine</Bold> Informationen mit Dritten", - "consent_migration": "Sie haben zuvor zugestimmt, anonymisierte Nutzungsdaten mit uns zu teilen. Wir aktualisieren, wie das funktioniert.", + "consent_migration": "Du hast zugestimmt, anonymisierte Nutzungsdaten mit uns zu teilen. Wir aktualisieren die Funktionsweise.", "disable_prompt": "Du kannst dies jederzeit in den Einstellungen deaktivieren", "enable_prompt": "Hilf mit, %(analyticsOwner)s zu verbessern", "learn_more": "Teile Daten anonymisiert um uns zu helfen Probleme zu identifizieren. Nichts persönliches. Keine Dritten. <LearnMoreLink>Mehr dazu hier</LearnMoreLink>", @@ -204,11 +210,11 @@ "failed_soft_logout_auth": "Erneute Authentifizierung fehlgeschlagen", "failed_soft_logout_homeserver": "Erneute Authentifizierung aufgrund eines Problems des Heim-Servers fehlgeschlagen", "forgot_password_email_invalid": "E-Mail-Adresse scheint ungültig zu sein.", - "forgot_password_email_required": "Es muss die mit dem Benutzerkonto verbundene E-Mail-Adresse eingegeben werden.", + "forgot_password_email_required": "Es muss die mit dem Konto verbundene E-Mail-Adresse eingegeben werden.", "forgot_password_prompt": "Passwort vergessen?", "forgot_password_send_email": "E-Mail senden", "identifier_label": "Anmelden mit", - "incorrect_credentials": "Inkorrekter Nutzername und/oder Passwort.", + "incorrect_credentials": "Benutzername und/oder Passwort falsch.", "incorrect_credentials_detail": "Bitte beachte, dass du dich gerade auf %(hs)s anmeldest, nicht matrix.org.", "incorrect_password": "Ungültiges Passwort", "log_in_new_account": "Mit deinem neuen Konto <a>anmelden</a>.", @@ -223,6 +229,7 @@ }, "misconfigured_body": "Wende dich an deinen %(brand)s-Admin um <a>deine Konfiguration</a> auf ungültige oder doppelte Einträge zu überprüfen.", "misconfigured_title": "Dein %(brand)s ist falsch konfiguriert", + "mobile_create_account_title": "Du bist dabei, auf %(hsName)s ein Konto anzulegen", "msisdn_field_description": "Andere Personen können dich mit deinen Kontaktdaten in Räume einladen", "msisdn_field_label": "Telefon", "msisdn_field_number_invalid": "Diese Telefonummer sieht nicht ganz richtig aus. Bitte überprüfe deine Eingabe und versuche es erneut", @@ -241,12 +248,40 @@ "phone_label": "Telefon", "phone_optional_label": "Telefon (optional)", "qr_code_login": { + "check_code_explainer": "Hierdurch wird überprüft, ob die Verbindung zu Ihrem anderen Gerät sicher ist.", + "check_code_heading": "Gib die Nummer ein, die am anderen Gerät angezeigt wird", + "check_code_input_label": "zweistelliger Code", + "check_code_mismatch": "Die Zahlen stimmen nicht überein", "completing_setup": "Schließe Anmeldung deines neuen Gerätes ab", + "error_etag_missing": "Ein unerwarteter Fehler ist aufgetreten. Dies kann an einer Browsererweiterung, einem Proxyserver oder einer fehlerhaften Serverkonfiguration liegen.", + "error_expired": "Die Anmeldung ist abgelaufen. Bitte versuchen Sie es erneut.", + "error_expired_title": "Die Anmeldung wurde nicht rechtzeitig abgeschlossen", + "error_insecure_channel_detected": "Eine sichere Verbindung zum neuen Gerät konnte nicht hergestellt werden. Ihre vorhandenen Geräte sind weiterhin sicher und Sie müssen sich keine Sorgen um sie machen.", + "error_insecure_channel_detected_instructions": "Was jetzt?", + "error_insecure_channel_detected_instructions_1": "Versuchen Sie erneut, sich mit einem QR-Code auf dem anderen Gerät anzumelden, falls dies ein Netzwerkproblem war", + "error_insecure_channel_detected_instructions_2": "Falls das gleiche Problem auftritt, probiere Sie es bitte mit einem anderen WLAN-Netzwerk oder verwenden Sie Ihre mobilen Daten anstelle von WLAN", + "error_insecure_channel_detected_instructions_3": "Falls das nicht funktioniert, melden Sie sich bitte manuell an.", + "error_insecure_channel_detected_title": "Die Verbindung ist nicht sicher", + "error_other_device_already_signed_in": "Sie brauchen nichts weiter zu tun.", + "error_other_device_already_signed_in_title": "Ihr anderes Gerät ist bereits angemeldet", "error_rate_limited": "Zu viele Versuche in zu kurzer Zeit. Warte ein wenig, bevor du es erneut versuchst.", - "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", - "select_qr_code": "Wähle „%(scanQRCode)s“", + "error_unexpected": "Es ist ein unerwarteter Fehler aufgetreten. Ihre Anfrage zum Verbinden des anderen Geräts wurde abgebrochen.", + "error_unsupported_protocol": "Dieses Gerät unterstützt die Anmeldung auf einem anderen Gerät mit einem QR-Code nicht.", + "error_unsupported_protocol_title": "Anderes Gerät nicht kompatibel", + "error_user_cancelled": "Der Anmeldevorgang wurde am anderen Gerät abgebrochen.", + "error_user_cancelled_title": "Anmeldungsanfrage abgebrochen", + "error_user_declined": "Sie oder der Kontoanbieter haben die Anmeldeanfrage abgelehnt.", + "error_user_declined_title": "Anmeldung abgelehnt", + "follow_remaining_instructions": "Folgen Sie den restlichen Anweisungen.", + "open_element_other_device": "Öffnen Sie %(brand)s auf Ihren anderen Gerät", + "point_the_camera": "Scanne den hier angezeigten QR-Code", + "scan_code_instruction": "Scanne den QR-Code mit einem weiteren Gerät.", + "scan_qr_code": "Anmeldung mit QR-Code", + "security_code": "Sicherheitscode", + "security_code_prompt": "Wenn Sie dazu aufgefordert werden, geben Sie den folgenden Code auf Ihrem anderen Gerät ein.", + "select_qr_code": "Wähle \"%(scanQRCode)s\"", + "unsupported_explainer": "Dein Kontoanbieter unterstützt keine Anmeldung bei einem neuen Gerät per QR-Code.", + "unsupported_heading": "QR-Code nicht unterstützt", "waiting_for_device": "Warte auf Anmeldung des Gerätes" }, "register_action": "Konto erstellen", @@ -258,8 +293,8 @@ "registration_disabled": "Registrierungen wurden auf diesem Heim-Server deaktiviert.", "registration_msisdn_field_required_invalid": "Telefonnummer eingeben (auf diesem Heim-Server erforderlich)", "registration_successful": "Registrierung erfolgreich", - "registration_username_in_use": "Jemand anderes nutzt diesen Benutzernamen schon. Probier einen anderen oder wenn du es bist, melde dich unten an.", - "registration_username_unable_check": "Es kann nicht überprüft werden, ob der Nutzername bereits vergeben ist. Bitte versuche es später erneut.", + "registration_username_in_use": "Dieser Benutzername wird bereits verwendet. Wählen Sie einen anderen Benutzernamen oder melden Sie sich unten an.", + "registration_username_unable_check": "Es kann nicht überprüft werden, ob der Benutzername bereits vergeben ist. Bitte versuchen Sie es später erneut.", "registration_username_validation": "Verwende nur Kleinbuchstaben, Zahlen, Bindestriche und Unterstriche", "reset_password": { "confirm_new_password": "Neues Passwort bestätigen", @@ -335,6 +370,8 @@ "email_resend_prompt": "Nicht angekommen? <a>Erneut senden</a>", "email_resent": "Verschickt!", "fallback_button": "Authentifizierung beginnen", + "mas_cross_signing_reset_cta": "Gehen Sie zu Ihren Konto", + "mas_cross_signing_reset_description": "Setzen Sie Ihre Identität über Ihren Kontoanbieter zurück. Kommen Sie dann zurück und klicken Sie auf „Wiederholen“.", "msisdn": "Eine Textnachricht wurde an %(msisdn)s gesendet", "msisdn_token_incorrect": "Token fehlerhaft", "msisdn_token_prompt": "Bitte gib den darin enthaltenen Code ein:", @@ -355,7 +392,7 @@ "unsupported_auth_email": "Dieser Heim-Server unterstützt die Anmeldung per E-Mail-Adresse nicht.", "unsupported_auth_msisdn": "Dieser Server unterstützt keine Authentifizierung per Telefonnummer.", "username_field_required_invalid": "Benutzername eingeben", - "username_in_use": "Dieser Benutzername wird bereits genutzt, bitte versuche es mit einem anderen.", + "username_in_use": "Dieser Benutzername wird bereits verwendet. Bitte wählen Sie einen anderen Benutzernamen.", "verify_email_explainer": "Wir müssen wissen, dass du es auch wirklich bist, bevor wir dein Passwort zurücksetzen. Klicke auf den Link in der E-Mail, die wir gerade an <b>%(email)s</b> gesendet haben", "verify_email_heading": "Verifiziere deine E-Mail, um fortzufahren" }, @@ -365,12 +402,12 @@ "collecting_information": "App-Versionsinformationen werden abgerufen", "collecting_logs": "Protokolle werden abgerufen", "create_new_issue": "Bitte <newIssueLink>erstelle ein neues Issue</newIssueLink> auf GitHub damit wir diesen Fehler untersuchen können.", - "description": "Fehlerberichte enthalten Nutzungsdaten wie Nutzernamen von dir und anderen Personen, Raum-IDs deiner beigetretenen Räume sowie mit welchen Elementen der Oberfläche du kürzlich interagiert hast. Sie enthalten keine Nachrichten.", + "description": "Fehlerberichte enthalten Applikationsnutzungsdaten wie Ihren Benutzernamen and Ihre Pseudonyme oder jene Ihrer Chatpartner, die IDs und Namen von Chaträumen, in denen Sie Mitglied sind und Elementen der Benutzeroberfläche mit denen Sie kürzlich interagiert haben. Fehlerberichte enthalten keine Nachrichten.", "download_logs": "Protokolle herunterladen", "downloading_logs": "Lade Protokolle herunter", "error_empty": "Bitte teile uns mit, was schief lief - oder besser, beschreibe das Problem auf GitHub in einem \"Issue\".", "failed_send_logs": "Senden von Protokolldateien fehlgeschlagen: ", - "github_issue": "\"Issue\" auf Github", + "github_issue": "GitHub-Problem", "introduction": "Wenn du uns einen Bug auf GitHub gemeldet hast, können uns Debug-Logs helfen, das Problem zu finden. ", "log_request": "Um uns zu helfen, dies in Zukunft zu vermeiden, <a>sende uns bitte die Protokolldateien</a>.", "logs_sent": "Protokolldateien gesendet", @@ -425,6 +462,7 @@ "beta": "Beta", "camera": "Kamera", "cameras": "Kameras", + "cancel": "Abbrechen", "capabilities": "Funktionen", "copied": "Kopiert!", "credits": "Danksagungen", @@ -460,11 +498,13 @@ "legal": "Rechtliches", "light": "Hell", "loading": "Lade …", + "lobby": "Lobby", "location": "Standort", "low_priority": "Niedrige Priorität", "matrix": "Matrix", "message": "Nachricht", "message_layout": "Nachrichtenlayout", + "message_timestamp_invalid": "Ungültiger Zeitstempel", "microphone": "Mikrofon", "model": "Modell", "modern": "Modern", @@ -488,7 +528,7 @@ "orphan_rooms": "Andere Räume", "password": "Passwort", "people": "Personen", - "preferences": "Einstellungen", + "preferences": "Präferenzen", "presence": "Anwesenheit", "preview_message": "Hey du. Du bist großartig!", "privacy": "Privatsphäre", @@ -506,6 +546,8 @@ "room": "Raum", "room_name": "Raumname", "rooms": "Räume", + "save": "Speichern", + "saved": "Gespeichert", "saving": "Speichere …", "secure_backup": "Verschlüsselte Sicherung", "security": "Sicherheit", @@ -516,7 +558,7 @@ "show_more": "Mehr zeigen", "someone": "Jemand", "space": "Raum", - "spaces": "Räume", + "spaces": "Spaces", "sticker": "Sticker", "stickerpack": "Sticker-Paket", "success": "Erfolg", @@ -534,6 +576,7 @@ "unnamed_room": "Unbenannter Raum", "unnamed_space": "Unbenannter Space", "unverified": "Nicht verifiziert", + "updating": "Aktualisieren...", "user": "Benutzer", "user_avatar": "Profilbild", "username": "Benutzername", @@ -661,7 +704,7 @@ "private_personal_heading": "Für wen ist dieser Space gedacht?", "private_space": "Für mich und meine Kollegen", "private_space_description": "Ein privater Space für dich und deine Kollegen", - "public_description": "Öffne den Space für alle - am besten für Communities", + "public_description": "Öffne den Space für alle - am besten für Communitys", "public_heading": "Dein öffentlicher Space", "search_public_button": "Öffentliche Spaces suchen", "setup_rooms_community_description": "Lass uns für jedes einen Raum erstellen.", @@ -687,6 +730,7 @@ "twemoji": "Die <twemoji>Twemoji</twemoji>-Emojis sind © <author>Twitter, Inc und weitere Mitwirkende</author> und wird unter den Bedingungen von <terms>CC-BY 4.0</terms> verwendet.", "twemoji_colr": "Die Schriftart <colr>twemoji-colr</colr> ist © <author>Mozilla Foundation</author> und wird unter den Bedingungen von <terms>Apache 2.0</terms> verwendet." }, + "desktop_default_device_name": "%(brand)s Desktop: %(platformName)s", "devtools": { "active_widgets": "Aktive Widgets", "category_other": "Sonstiges", @@ -732,6 +776,7 @@ "room_notifications_type": "Typ: ", "room_status": "Raumstatus", "room_unread_status_count": { + "one": "Ungelesen Status des ChatRooms: <strong>%(status)s</strong>, Anzahl: <strong>%(count)s</strong>", "other": "Ungelesen-Status im Raum: <strong>%(status)s</strong>, Anzahl: <strong>%(count)s</strong>" }, "save_setting_values": "Einstellungswerte speichern", @@ -761,7 +806,7 @@ "toolbox": "Werkzeugkasten", "use_at_own_risk": "Diese Benutzeroberfläche prüft nicht auf richtige Datentypen. Benutzung auf eigene Gefahr.", "user_read_up_to": "Der Benutzer hat gelesen bis: ", - "user_read_up_to_ignore_synthetic": "Der Benutzer hat gelesen bis (ignoreSynthetic): ", + "user_read_up_to_ignore_synthetic": "Der Benutzer hat bis (ignoreSynthetic) gelesen: ", "user_read_up_to_private": "Benutzer las bis (m.read.private): ", "user_read_up_to_private_ignore_synthetic": "Benutzer las bis (m.read.private;ignoreSynthetic): ", "value": "Wert", @@ -860,6 +905,8 @@ "warning": "Wenn du die neue Wiederherstellungsmethode nicht festgelegt hast, versucht ein Angreifer möglicherweise, auf dein Konto zuzugreifen. Ändere dein Kontopasswort und lege sofort eine neue Wiederherstellungsmethode in den Einstellungen fest." }, "not_supported": "<nicht unterstützt>", + "pinned_identity_changed": "Die Identität von %(displayName)s (<b>%(userId)s)</b>) hat sich geändert. <a>Mehr erfahren</a>", + "pinned_identity_changed_no_displayname": "Die Identität von <b>%(userId)s</b> hat sich geändert. <a>Mehr erfahren</a>", "recovery_method_removed": { "description_1": "In dieser Sitzung wurde festgestellt, dass deine Sicherheitsphrase und dein Schlüssel für sichere Nachrichten entfernt wurden.", "description_2": "Wenn du dies versehentlich getan hast, kannst du in dieser Sitzung \"sichere Nachrichten\" einrichten, die den Nachrichtenverlauf dieser Sitzung mit einer neuen Wiederherstellungsmethode erneut verschlüsseln.", @@ -875,7 +922,6 @@ }, "udd": { "interactive_verification_button": "Interaktiv per Emoji verifizieren", - "manual_verification_button": "Manuell per Text verifizieren", "other_ask_verify_text": "Bitte diesen Nutzer, seine Sitzung zu verifizieren, oder verifiziere diese unten manuell.", "other_new_session_text": "%(name)s (%(userId)s) hat sich zu einer neuen Sitzung angemeldet, ohne sie zu verifizieren:", "own_ask_verify_text": "Verifiziere deine andere Sitzung mit einer der folgenden Optionen.", @@ -910,12 +956,6 @@ "incoming_sas_dialog_waiting": "Warte auf Bestätigung des Gesprächspartners …", "incoming_sas_user_dialog_text_1": "Überprüfe diesen Benutzer, um ihn als vertrauenswürdig zu kennzeichnen. Benutzern zu vertrauen gibt dir zusätzliche Sicherheit bei der Verwendung von Ende-zu-Ende-verschlüsselten Nachrichten.", "incoming_sas_user_dialog_text_2": "Wenn du diesen Benutzer verifizierst werden seine Sitzungen für dich und deine Sitzungen für ihn als vertrauenswürdig markiert.", - "manual_device_verification_device_id_label": "Sitzungs-ID", - "manual_device_verification_device_key_label": "Sitzungsschlüssel", - "manual_device_verification_device_name_label": "Sitzungsname", - "manual_device_verification_footer": "Wenn sie nicht übereinstimmen kann die Sicherheit eurer Kommunikation kompromittiert sein.", - "manual_device_verification_self_text": "Bestätige indem du das folgende mit deinen Benutzereinstellungen in deiner anderen Sitzung vergleichst:", - "manual_device_verification_user_text": "Bestätige die Sitzung dieses Benutzers indem du das folgende mit seinen Benutzereinstellungen vergleichst:", "no_key_or_device": "Es sieht so aus, als hättest du keinen Sicherheitsschlüssel oder andere Geräte, mit denen du dich verifizieren könntest. Dieses Gerät wird keine alten verschlüsselten Nachrichten lesen können. Um deine Identität auf diesem Gerät zu verifizieren musst du deine Verifizierungsschlüssel zurücksetzen.", "no_support_qr_emoji": "Das Gerät unterstützt weder Verifizieren mittels QR-Code noch Emoji-Verifizierung. %(brand)s benötigt dies jedoch. Bitte verwende eine andere Anwendung.", "other_party_cancelled": "Die Gegenstelle hat die Überprüfung abgebrochen.", @@ -929,7 +969,8 @@ "qr_reciprocate_same_shield_device": "Fast geschafft! Zeigen beide Geräte das selbe Wappen an?", "qr_reciprocate_same_shield_user": "Fast geschafft! Wird bei %(displayName)s das gleiche Schild angezeigt?", "request_toast_accept": "Sitzung verifizieren", - "request_toast_decline_counter": "Ignorieren (%(counter)s)", + "request_toast_accept_user": "Benutzer verifizieren", + "request_toast_decline_counter": "Blockiert (%(counter)s)", "request_toast_detail": "%(deviceId)s von %(ip)s", "reset_proceed_prompt": "Mit Zurücksetzen fortfahren", "sas_caption_self": "Verifiziere dieses Gerät, indem du überprüfst, dass die folgende Zahl auf dem Bildschirm erscheint.", @@ -954,7 +995,7 @@ "unverified_sessions_toast_description": "Überprüfe sie, um ein sicheres Konto gewährleisten zu können", "unverified_sessions_toast_reject": "Später", "unverified_sessions_toast_title": "Du hast nicht verifizierte Sitzungen", - "verification_description": "Verifiziere diese Anmeldung, um auf verschlüsselte Nachrichten zuzugreifen und dich anderen gegenüber zu identifizieren.", + "verification_description": "Verifizieren Sie Ihre Identität, um auf verschlüsselte Nachrichten zuzugreifen und sich gegenüber anderen Benutzern zu verifizieren. Falls Sie einen Handy oder ein anderes mobiles Gerät verwenden, öffnen Sie die App dort and fahren sie mit Ihrer Identifikation fort.", "verification_dialog_title_device": "Anderes Gerät verifizieren", "verification_dialog_title_user": "Verifizierungsanfrage", "verification_skip_warning": "Ohne dich zu verifizieren wirst du keinen Zugriff auf alle deine Nachrichten haben und könntest für andere als nicht vertrauenswürdig erscheinen.", @@ -1020,6 +1061,10 @@ "error_app_open_in_another_tab": "Wechsle zu einem anderen Tab um mit %(brand)s zu verbinden. Dieser Tab kann jetzt geschlossen werden.", "error_app_open_in_another_tab_title": "%(brand)s läuft bereits in einem anderen Tab.", "error_app_opened_in_another_window": "%(brand)s läuft bereit in einem anderen Fenster. Klicke \"%(label)s um %(brand)s hier zu nutzen und beende das andere Fenster.", + "error_database_closed_description": { + "for_desktop": "Deine Festplatte scheint voll zu sein. Mache Speicherplatz frei und lade erneut.", + "for_web": "Diese Meldung wird erwartet, wenn Sie die Browserdaten gelöscht haben. %(brand)s ist möglicherweise auch in einem anderen Tab geöffnet oder Ihre Festplatte ist voll. Bitte machen Sie etwas Speicherplatz frei und laden Sie die Seite neu." + }, "error_database_closed_title": "%(brand)s funktioniert nicht mehr", "error_dialog": { "copy_room_link_failed": { @@ -1035,10 +1080,6 @@ }, "error_user_not_logged_in": "Benutzer ist nicht angemeldet", "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s beendete eine Sprachübertragung", - "you": "Du hast eine Sprachübertragung beendet" - }, "m.call.answer": { "dm": "Laufendes Gespräch", "user": "%(senderName)s ist dem Anruf beigetreten", @@ -1060,7 +1101,15 @@ "you": "Du reagiertest mit %(reaction)s auf %(message)s" }, "m.sticker": "%(senderName)s: %(stickerName)s", - "m.text": "%(senderName)s: %(message)s" + "m.text": "%(senderName)s: %(message)s", + "prefix": { + "audio": "Audio", + "file": "Datei", + "image": "Bild", + "poll": "Umfrage", + "video": "Video" + }, + "preview": "<bold>%(prefix)s</bold>%(preview)s" }, "export_chat": { "cancelled": "Exportieren abgebrochen", @@ -1183,7 +1232,19 @@ "other": "In %(spaceName)s und %(count)s weiteren Spaces." }, "incompatible_browser": { - "title": "Nicht unterstützter Browser" + "continue": "Trotzdem fortfahren", + "description": "%(brand)s verwendet einige Browser-Funktionen, die von deinem aktuellen Browser nicht unterstützt werden. %(detail)s", + "detail_can_continue": "Wenn Sie fortfahren, funktionieren einige Funktionen möglicherweise nicht mehr und es besteht das Risiko, dass Sie in Zukunft Daten verlieren.", + "detail_no_continue": "Versuchen Sie, diesen Browser zu aktualisieren, wenn Sie nicht die neueste Version verwenden, und versuchen Sie es erneut.", + "learn_more": "Mehr erfahren", + "linux": "Linux", + "macos": "Mac", + "supported_browsers": "Verwenden Sie <Chrome>Chrome</Chrome>, <Firefox> Firefox</Firefox>, <Edge>Edge</Edge> oder <Safari>Safari</Safari>, um das beste Erlebnis zu erzielen.", + "title": "Nicht unterstützter Browser", + "use_desktop_heading": "Verwende stattdessen %(brand)s Desktop", + "use_mobile_heading": "Stattdessen %(brand)s am Smartphone benutzen", + "use_mobile_heading_after_desktop": "Oder verwende die mobile App", + "windows": "Windows (%(bits)s-bit)" }, "info_tooltip_title": "Information", "integration_manager": { @@ -1198,7 +1259,7 @@ "integrations": { "disabled_dialog_description": "Aktiviere „%(manageIntegrations)s“ in den Einstellungen, um dies zu tun.", "disabled_dialog_title": "Integrationen sind deaktiviert", - "impossible_dialog_description": "%(brand)s erlaubt dir nicht, eine Integrationsverwaltung zu verwenden, um dies zu tun. Bitte kontaktiere einen Administrator.", + "impossible_dialog_description": "%(brand)s erlaubt es Ihnen nicht, eine Integrationsverwaltung zu verwenden. Bitte kontaktieren Sie einen Administrator.", "impossible_dialog_title": "Integrationen sind nicht erlaubt" }, "invite": { @@ -1307,12 +1368,14 @@ "navigate_next_message_edit": "Nächste Nachricht bearbeiten", "navigate_prev_history": "Vorheriger kürzlich besuchter Raum oder Space", "navigate_prev_message_edit": "Vorherige Nachricht bearbeiten", + "next_landmark": "Zur nächsten Landmark springen", "next_room": "Nächste Unterhaltung", "next_unread_room": "Nächste ungelesene Nachricht", "number": "[Nummer]", "open_user_settings": "Benutzereinstellungen öffnen", "page_down": "Bild runter", "page_up": "Bild hoch", + "prev_landmark": "Zur vorherigen Landmark springen", "prev_room": "Vorherige Unterhaltung", "prev_unread_room": "Vorherige ungelesene Nachricht", "room_list_collapse_section": "Raumliste einklappen", @@ -1357,8 +1420,11 @@ "dynamic_room_predecessors": "Veränderbare Raumvorgänger", "dynamic_room_predecessors_description": "MSC3946 aktivieren (zur Verknüpfung von Raumarchiven nach der Raumerstellung)", "element_call_video_rooms": "Element Call-Videoräume", + "exclude_insecure_devices": "Unsichere Geräte ausschließen beim senden/empfangen von Nachrichten", + "exclude_insecure_devices_description": "Bei Aktivierung dieses Modus werden verschlüsselte Nachrichten nicht mehr mit unverifizierten Geräten geteilt und Nachrichten von unverifizierten Geräten werden als Fehler angezeigt. Beachten Sie, dass bei Aktivierung dieses Modus es nicht möglich ist, mit Benutzern, die keine verifizierten Geräte haben, zu kommunizieren.", "experimental_description": "Experimentierfreudig? Probiere unsere neuesten, sich in Entwicklung befindlichen Ideen aus. Diese Funktionen sind nicht final; Sie könnten instabil sein, sich verändern oder sogar ganz entfernt werden. <a>Erfahre mehr</a>.", "experimental_section": "Frühe Vorschauen", + "extended_profiles_msc_support": "Erfordert die Unterstützung von MSC4133 durch den Server", "feature_disable_call_per_sender_encryption": "Verschlüsselung per-sender für Element Anruf abschalten", "feature_wysiwyg_composer_description": "Verwende Textverarbeitung (Rich-Text) statt Markdown im Eingabefeld.", "group_calls": "Neue Gruppenanruf-Erfahrung", @@ -1369,9 +1435,10 @@ "group_moderation": "Moderation", "group_profile": "Profil", "group_rooms": "Räume", - "group_spaces": "Räume", + "group_spaces": "Spaces", "group_themes": "Themen", "group_threads": "Themen", + "group_ui": "Benutzeroberfläche", "group_voip": "Anrufe", "group_widgets": "Widgets", "hidebold": "Benachrichtigungspunkt ausblenden (nur Zähler zeigen)", @@ -1393,6 +1460,7 @@ "notifications": "Benachrichtigungen in der Kopfleiste des Raums anschalten", "oidc_native_flow": "Native OIDC Authentifizierung", "oidc_native_flow_description": "⚠ Warnung: Experimentell. Nutze OIDC native Authentifizierung wenn dies vom Server unterstützt wird.", + "release_announcement": "Release Ankündigung", "render_reaction_images": "Benutzerdefinierte Bilder in Reaktionen anzeigen", "render_reaction_images_description": "Werden manchmal auch als „benutzerdefinierte Emojis“ bezeichnet.", "report_to_moderators": "An Raummoderation melden", @@ -1400,7 +1468,7 @@ "sliding_sync": "Sliding-Sync-Modus", "sliding_sync_description": "In aktiver Entwicklung, kann nicht deaktiviert werden.", "sliding_sync_disabled_notice": "Zum Deaktivieren, melde dich ab und erneut an", - "sliding_sync_server_no_support": "Dein Server unterstützt dies nicht nativ", + "sliding_sync_server_no_support": "Dein Server wird hier nicht unterstützt.", "under_active_development": "In aktiver Entwicklung.", "unrealiable_e2e": "Nicht zuverlässig in verschlüsselten Räumen", "video_rooms": "Videoräume", @@ -1412,8 +1480,6 @@ "video_rooms_faq2_answer": "Ja, der Verlauf wird neben dem Videoanruf angezeigt.", "video_rooms_faq2_question": "Kann ich während Videoanrufen auch Textnachrichten verschicken?", "video_rooms_feedbackSubheading": "Danke, dass Du die Beta ausprobierst. Bitte gehe soweit wie Du kannst ins Detail damit wird genau überprüfen können.", - "voice_broadcast": "Sprachübertragung", - "voice_broadcast_force_small_chunks": "Die Chunk-Länge der Sprachübertragungen auf 15 Sekunden erzwingen", "wysiwyg_composer": "Textverarbeitungs-Editor" }, "labs_mjolnir": { @@ -1454,6 +1520,8 @@ "last_person_warning": "Du bist die einzige Person im Raum. Sobald du ihn verlässt, wird niemand mehr hineingelangen, auch du nicht.", "leave_room_question": "Bist du sicher, dass du den Raum „%(roomName)s“ verlassen möchtest?", "leave_space_question": "Bist du sicher, dass du den Space „%(spaceName)s“ verlassen möchtest?", + "room_leave_admin_warning": "Sie sind der einzige Administrator in diesem Chatroom. Wenn Sie den Chatroom verlassen, wird niemand in der Lage sein, die Chatroommeinstellungen zu ändern oder andere wichtige Maßnahmen zu ergreifen.", + "room_leave_mod_warning": "Sie sind der einzige Moderator in diesem Chatroom. Wenn Sie den Chatroom verlassen, kann niemand die Raumeinstellungen ändern oder andere wichtige Maßnahmen ergreifen.", "room_rejoin_warning": "Dieser Raum ist nicht öffentlich. Du wirst ihn nicht ohne erneute Einladung betreten können.", "space_rejoin_warning": "Du wirst diesen privaten Space nur mit einer Einladung wieder betreten können." }, @@ -1518,6 +1586,7 @@ }, "member_list_back_action_label": "Raummitglieder", "message_edit_dialog_title": "Nachrichtenänderungen", + "migrating_crypto": "Bleib dran. Wir aktualisieren%(brand)s, um die Verschlüsselung schneller und zuverlässiger zu machen.", "mobile_guide": { "toast_accept": "App verwenden", "toast_description": "%(brand)s ist in mobilen Browsern experimentell. Für eine bessere Erfahrung nutze unsere App.", @@ -1543,8 +1612,10 @@ "keyword": "Schlüsselwort", "keyword_new": "Neues Schlüsselwort", "level_activity": "Aktivität", + "level_highlight": "Hervorhebung", "level_muted": "Stumm", "level_none": "Nichts", + "level_notification": "Benachrichtigung", "level_unsent": "Nicht gesendet", "mark_all_read": "Alle als gelesen markieren", "mentions_and_keywords": "@Erwähnungen und Schlüsselwörter", @@ -1554,7 +1625,6 @@ "mute_description": "Du wirst keine Benachrichtigungen erhalten" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s begann eine Sprachübertragung", "m.key.verification.request": "%(name)s fordert eine Verifizierung an" }, "onboarding": { @@ -1572,8 +1642,8 @@ "download_brand_desktop": "%(brand)s Desktop herunterladen", "download_f_droid": "In F-Droid erhältlich", "download_google_play": "In Google Play erhältlich", - "enable_notifications": "Benachrichtigungen einschalten", - "enable_notifications_action": "Benachrichtigungen aktivieren", + "enable_notifications": "Desktopbenachrichtigungen einschalten", + "enable_notifications_action": "Einstellungen öffnen", "enable_notifications_description": "Verpasse keine Antworten oder wichtigen Nachrichten", "explore_rooms": "Öffentliche Räume erkunden", "find_community_members": "Finde deine Community-Mitglieder und lade sie ein", @@ -1711,7 +1781,7 @@ "disagree": "Ablehnen", "error_create_room_moderation_bot": "Erstellen des Raums mit Moderations-Bot nicht möglich", "hide_messages_from_user": "Prüfe, ob du alle aktuellen und zukünftigen Nachrichten dieses Nutzers verstecken willst.", - "ignore_user": "Nutzer ignorieren", + "ignore_user": "Benutzer blockieren", "illegal_content": "Illegale Inhalte", "missing_reason": "Bitte gib an, weshalb du einen Fehler meldest.", "nature": "Bitte wähle eine Kategorie aus und beschreibe, was die Nachricht missbräuchlich macht.", @@ -1752,14 +1822,37 @@ "restore_failed_error": "Konnte Schlüsselsicherung nicht wiederherstellen" }, "right_panel": { - "add_integrations": "Widgets, Brücken und Bots hinzufügen", + "add_integrations": "Erweiterungen hinzufügen", + "add_topic": "Thema hinzufügen", + "extensions_button": "Erweiterungen", + "extensions_empty_description": "Wählen Sie \"%(addIntegrations)s\" um Erweiterungen zu suchen und diesem Chatroom hinzuzufügen", + "extensions_empty_title": "Steigern Sie die Produktivität mit mehr Tools, Widgets und Bots", "files_button": "Dateien", "pinned_messages": { + "empty_description": "Wählen Sie eine Nachricht aus und wählen Sie „%(pinAction)s “, um sie hier einzuschließen.", + "empty_title": "Hefte wichtige Nachrichten an, damit sie leicht gefunden werden können.", + "header": { + "one": "1 angeheftete Nachricht", + "other": "%(count)s angeheftete Nachrichten" + }, "limits": { "other": "Du kannst nur %(count)s Widgets anheften" - } + }, + "menu": "Menü öffnen", + "release_announcement": { + "close": "Ok", + "description": "Alle angepinnten Nachrichten finden Sie hier. Bewegen Sie den Mauszeiger über eine beliebige Nachricht und wählen Sie „Pin“, um die Nachricht hinzuzufügen.", + "title": "Alle neuen angehefteten Nachrichten" + }, + "reply_thread": "Auf <link>Nachricht im Thread</link> antworten", + "unpin_all": { + "button": "Alle Nachrichten lösen", + "content": "Stellen Sie sicher, dass Sie wirklich alle angehefteten Nachrichten entfernen möchten. Diese Aktion kann nicht rückgängig gemacht werden.", + "title": "Alle Nachrichten lösen?" + }, + "view": "Im Nachrichtenverlauf ansehen" }, - "pinned_messages_button": "Angeheftet", + "pinned_messages_button": "Angeheftete Nachrichten", "poll": { "active_heading": "Aktive Umfragen", "empty_active": "In diesem Raum gibt es keine aktiven Umfragen", @@ -1780,11 +1873,11 @@ }, "load_more": "Weitere Umfragen laden", "loading": "Lade Umfragen", - "past_heading": "Vergangene Umfragen", + "past_heading": "Abgeschlossene Umfragen", "view_in_timeline": "Umfrage im Verlauf anzeigen", "view_poll": "Umfrage ansehen" }, - "polls_button": "Umfrageverlauf", + "polls_button": "Umfragen", "room_summary_card": { "title": "Raum-Info" }, @@ -1813,6 +1906,7 @@ "forget": "Raum vergessen", "low_priority": "Niedrige Priorität", "mark_read": "Als gelesen markieren", + "mark_unread": "Als ungelesen markieren", "notifications_default": "Standardeinstellung verwenden", "notifications_mute": "Raum stumm stellen", "title": "Raumoptionen", @@ -1861,6 +1955,8 @@ }, "room_is_public": "Dieser Raum ist öffentlich" }, + "header_avatar_open_settings_label": "Chatroomeinstellungen öffnen", + "header_face_pile_tooltip": "Personen", "header_untrusted_label": "Nicht vertrauenswürdig", "inaccessible": "Dieser Raum oder Space ist im Moment nicht zugänglich.", "inaccessible_name": "Auf %(roomName)s kann momentan nicht zugegriffen werden.", @@ -1884,10 +1980,10 @@ "you_created": "Du hast diesen Raum erstellt." }, "invite_email_mismatch_suggestion": "Teile diese E-Mail-Adresse in den Einstellungen, um Einladungen direkt in %(brand)s zu erhalten.", - "invite_reject_ignore": "Ablehnen und Nutzer blockieren", + "invite_reject_ignore": "Ablehnen und Benutzer blockieren", "invite_sent_to_email": "Einladung an %(email)s gesendet", "invite_sent_to_email_room": "Diese Einladung zu %(roomName)s wurde an %(email)s gesendet", - "invite_subtitle": "<userName/> hat dich eingeladen", + "invite_subtitle": "Eingeladen von <userName/>", "invite_this_room": "In diesen Raum einladen", "invite_title": "Möchtest du %(roomName)s betreten?", "inviter_unknown": "Unbekannt", @@ -1930,11 +2026,24 @@ "not_found_title": "Dieser Raum oder Space existiert nicht.", "not_found_title_name": "%(roomName)s existiert nicht.", "peek_join_prompt": "Du erkundest den Raum %(roomName)s. Willst du ihn betreten?", + "pinned_message_badge": "Fixierte Nachrichten", + "pinned_message_banner": { + "button_close_list": "Liste schließen", + "button_view_all": "Alle anzeigen", + "description": "In diesem Raum sind Nachrichten angeheftet. Klicken Sie hier, um sie anzusehen.", + "go_to_message": "Fixierte Nachrichten im Nachrichtenverlauf anzeigen.", + "title": "<bold>%(index)s of %(length)s</bold> Angeheftete Nachrichten" + }, "read_topic": "Klicke, um das Thema zu lesen", "rejecting": "Lehne Einladung ab …", "rejoin_button": "Erneut betreten", "search": { "all_rooms_button": "Alle Räume durchsuchen", + "placeholder": "Nachrichten durchsuchen...", + "summary": { + "one": "1 Ergebnis für \"<query/>\" gefunden", + "other": "%(count)s Ergebnisse für \"<query/>\" gefunden" + }, "this_room_button": "Diesen Raum durchsuchen" }, "status_bar": { @@ -2070,6 +2179,8 @@ "error_deleting_alias_description": "Beim Entfernen dieser Adresse ist ein Fehler aufgetreten. Vielleicht existiert sie nicht mehr oder es kam zu einem temporären Fehler.", "error_deleting_alias_description_forbidden": "Du hast nicht die Berechtigung, die Adresse zu löschen.", "error_deleting_alias_title": "Fehler beim Löschen der Adresse", + "error_publishing": "Chatroom konnte nicht auf öffentlich gestellt werden", + "error_publishing_detail": "Beim Veröffentlichen dieses Chatrooms ist ein Fehler aufgetreten", "error_save_space_settings": "Spaceeinstellungen konnten nicht gespeichert werden.", "error_updating_alias_description": "Es gab einen Fehler beim Ändern des Raumalias. Entweder erlaubt es der Server nicht oder es gab ein temporäres Problem.", "error_updating_canonical_alias_description": "Es gab ein Problem beim Aktualisieren der Raum-Hauptadresse. Es kann sein, dass der Server dies verbietet oder ein temporäres Problem aufgetreten ist.", @@ -2127,7 +2238,6 @@ "error_unbanning": "Aufheben der Verbannung fehlgeschlagen", "events_default": "Nachrichten senden", "invite": "Person einladen", - "io.element.voice_broadcast_info": "Sprachübertragungen", "kick": "Benutzer entfernen", "m.call": "Beginne %(brand)s-Anrufe", "m.call.member": "Trete %(brand)s-Anrufen bei", @@ -2140,7 +2250,7 @@ "m.room.history_visibility": "Sichtbarkeit des Verlaufs ändern", "m.room.name": "Raumname ändern", "m.room.name_space": "Name des Space ändern", - "m.room.pinned_events": "Angeheftete Ereignisse verwalten", + "m.room.pinned_events": "Angeheftete Nachrichten verwalten", "m.room.power_levels": "Berechtigungen ändern", "m.room.redaction": "Vom mir gesendete Nachrichten löschen", "m.room.server_acl": "Server-ACLs bearbeiten", @@ -2297,25 +2407,39 @@ "brand_version": "Version von %(brand)s:", "clear_cache_reload": "Zwischenspeicher löschen und neu laden", "crypto_version": "Krypto-Version:", + "dialog_title": "<strong>Einstellungen:</strong> Hilfe & Info", "help_link": "<a>Um Hilfe zur Benutzung von %(brand)s zu erhalten, klicke hier</a>.", "homeserver": "Heim-Server ist <code>%(homeserverUrl)s</code>", "identity_server": "Identitäts-Server ist <code>%(identityServerUrl)s</code>", - "title": "Hilfe und Info", + "title": "Hilfe & Info", "versions": "Versionen" } }, "settings": { + "account": { + "dialog_title": "<strong>Einstellunge:</strong> Konto", + "title": "Konto" + }, "all_rooms_home": "Alle Räume auf Startseite anzeigen", "all_rooms_home_description": "Alle Räume, denen du beigetreten bist, werden auf der Startseite erscheinen.", "always_show_message_timestamps": "Nachrichtenzeitstempel immer anzeigen", "appearance": { + "bundled_emoji_font": "Verwenden Sie den mitgelieferten Emoji Font.", + "compact_layout": "Kompakten Text und Nachrichten anzeigen", + "compact_layout_description": "Um diese Funktion nutzen zu können, muss das moderne Nachrichtenlayout ausgewählt sein.", "custom_font": "Systemschriftart verwenden", "custom_font_description": "Setze den Schriftnamen auf eine in deinem System installierte Schriftart und %(brand)s wird versuchen, sie zu verwenden.", "custom_font_name": "Systemschriftart", "custom_font_size": "Andere Schriftgröße verwenden", - "custom_theme_error_downloading": "Fehler beim herunterladen des Themas.", + "custom_theme_add": "Benutzerdefiniertes Design hinzufügen", + "custom_theme_downloading": "Benutzerdefiniertes Design wird heruntergeladen...", + "custom_theme_error_downloading": "Fehler beim Herunterladen des Designs.", + "custom_theme_help": "Geben Sie die URL eines benutzerdefinierten Designs ein, das Sie anwenden möchten.", "custom_theme_invalid": "Ungültiges Designschema.", + "dialog_title": "<strong>Einstellungen:</strong> Erscheinungsbild", "font_size": "Schriftgröße", + "font_size_default": "%(fontSize)s (Standard)", + "high_contrast": "Hochkontrast", "image_size_default": "Standard", "image_size_large": "Groß", "layout_bubbles": "Nachrichtenblasen", @@ -2330,6 +2454,9 @@ "code_block_expand_default": "Quelltextblöcke standardmäßig erweitern", "code_block_line_numbers": "Zeilennummern in Quelltextblöcken", "disable_historical_profile": "Aktuelle Profilbilder und Anzeigenamen im Verlauf anzeigen", + "discovery": { + "title": "Wie man Sie findet" + }, "emoji_autocomplete": "Emoji-Vorschläge während Eingabe", "enable_markdown": "Markdown aktivieren", "enable_markdown_description": "Beginne Nachrichten mit <code>/plain</code>, um sie ohne Markdown zu senden.", @@ -2345,6 +2472,14 @@ "add_msisdn_dialog_title": "Telefonnummer hinzufügen", "add_msisdn_instructions": "Gib den per SMS an +%(msisdn)s gesendeten Bestätigungscode ein.", "add_msisdn_misconfigured": "Das MSISDN-Verknüpfungsverfahren ist falsch konfiguriert", + "allow_spellcheck": "Rechtschreibprüfung zulassen", + "application_language": "Anwendungssprache", + "application_language_reload_hint": "Nach Änderung der Sprache wird die App neu gestartet", + "avatar_remove_progress": "Bild wird entfernt...", + "avatar_save_progress": "Bild wird hochgeladen...", + "avatar_upload_error_text": "Das Dateiformat wird nicht unterstützt oder das Bild ist größer als %(size)s.", + "avatar_upload_error_text_generic": "Das Dateiformat wird möglicherweise nicht unterstützt.", + "avatar_upload_error_title": "Profilbild konnte nicht hochgeladen werden", "confirm_adding_email_body": "Klicke unten auf den Knopf, um die hinzugefügte E-Mail-Adresse zu bestätigen.", "confirm_adding_email_title": "Hinzugefügte E-Mail-Addresse bestätigen", "deactivate_confirm_body": "Willst du dein Konto wirklich deaktivieren? Du kannst dies nicht rückgängig machen.", @@ -2352,7 +2487,7 @@ "deactivate_confirm_content": "Bestätige, dass du dein Konto deaktivieren möchtest. Wenn du fortfährst, tritt folgendes ein:", "deactivate_confirm_content_1": "Du wirst dein Konto nicht reaktivieren können", "deactivate_confirm_content_2": "Du wirst dich nicht mehr anmelden können", - "deactivate_confirm_content_3": "Niemand wird in der Lage sein deinen Benutzernamen (MXID) wiederzuverwenden, dich eingeschlossen: Der Benutzername wird nicht verfügbar bleiben", + "deactivate_confirm_content_3": "Niemand wird in der Lage sein, Ihren Benutzernamen (MXID) wiederzuverwenden, Sie eingeschlossen: Der Benutzername wird insgesamt nur einmal vergeben.", "deactivate_confirm_content_4": "Du wirst alle Unterhaltungen verlassen, in denen du dich befindest", "deactivate_confirm_content_5": "Du wirst vom Identitäts-Server entfernt: Deine Freunde werden nicht mehr in der Lage sein, dich über deine E-Mail-Adresse oder Telefonnummer zu finden", "deactivate_confirm_content_6": "Deine alten Nachrichten werden weiterhin für Personen sichtbar bleiben, die sie erhalten haben, so wie es bei E-Mails der Fall ist. Möchtest du deine Nachrichten vor Personen verbergen, die Räume in der Zukunft betreten?", @@ -2360,10 +2495,13 @@ "deactivate_confirm_erase_label": "Meine Nachrichten vor neuen Teilnehmern verstecken", "deactivate_section": "Benutzerkonto deaktivieren", "deactivate_warning": "Die Deaktivierung deines Kontos ist unwiderruflich — sei vorsichtig!", - "discovery_email_empty": "Entdeckungsoptionen werden angezeigt, sobald du eine E-Mail-Adresse hinzugefügt hast.", + "discovery_email_empty": "Optionen zum Entdecken anderer Benutzer werden angezeigt, sobald Sie Ihre E-Mail Adresse zu Ihrem Konto hinzugefügt haben,", "discovery_email_verification_instructions": "Verifiziere den Link in deinem Posteingang", - "discovery_msisdn_empty": "Entdeckungsoptionen werden angezeigt, sobald du eine Telefonnummer hinzugefügt hast.", + "discovery_msisdn_empty": "Optionen zum Entdecken anderer Benutzer werden angezeigt, sobald Sie Ihre Telefonnummer zu Ihrem Konto hinzugefügt haben", "discovery_needs_terms": "Stimme den Nutzungsbedingungen des Identitäts-Servers %(serverName)s zu, um per E-Mail-Adresse oder Telefonnummer auffindbar zu werden.", + "discovery_needs_terms_title": "Lassen Sie sich von anderen finden", + "display_name": "Anzeigename", + "display_name_error": "Anzeigename konnte nicht gesetzt werden", "email_address_in_use": "Diese E-Mail-Adresse wird bereits verwendet", "email_address_label": "E-Mail-Adresse", "email_not_verified": "Deine E-Mail-Adresse wurde noch nicht verifiziert", @@ -2388,18 +2526,24 @@ "error_share_msisdn_discovery": "Teilen der Telefonnummer nicht möglich", "identity_server_no_token": "Kein Identitäts-Zugangs-Token gefunden", "identity_server_not_set": "Kein Identitäts-Server festgelegt", - "language_section": "Sprache und Region", + "language_section": "Sprache", "msisdn_in_use": "Diese Telefonnummer wird bereits verwendet", "msisdn_label": "Telefonnummer", "msisdn_verification_field_label": "Bestätigungscode", "msisdn_verification_instructions": "Gib den Bestätigungscode ein, den du empfangen hast.", "msisdns_heading": "Telefonnummern", "oidc_manage_button": "Konto verwalten", - "password_change_section": "Setze neues Kontopasswort …", + "password_change_section": "Passwort des Nutzerkontos ändern...", "password_change_success": "Dein Passwort wurde erfolgreich geändert.", + "personal_info": "Persönliche Daten", + "profile_subtitle": "So werden Sie von anderen in der App gesehen", + "profile_subtitle_oidc": "Ihr Konto wird separat durch einen Identitätsanbieter verwaltet. Einige persönliche Daten können deswegen hier nicht geändert werden.", "remove_email_prompt": "%(email)s entfernen?", "remove_msisdn_prompt": "%(phone)s entfernen?", - "spell_check_locale_placeholder": "Wähle ein Gebietsschema" + "spell_check_locale_placeholder": "Wähle ein Gebietsschema", + "unable_to_load_emails": "E-Mail Adresse konnte nicht geladen werden", + "unable_to_load_msisdns": "Telefonnummern können nicht geladen werden", + "username": "Benutzername" }, "image_thumbnails": "Vorschauen für Bilder", "inline_url_previews_default": "URL-Vorschau standardmäßig aktivieren", @@ -2455,12 +2599,20 @@ "phrase_strong_enough": "Super! Diese Passphrase wirkt stark genug" }, "keyboard": { + "dialog_title": "<strong>Einstellungen:</strong> Tastatur", "title": "Tastatur" }, + "labs": { + "dialog_title": "<strong>Einstellungen:</strong> Labore" + }, + "labs_mjolnir": { + "dialog_title": "<strong>Einstellungen:</strong> Blockierte Benutzer" + }, "notifications": { "default_setting_description": "Diese Einstellung wird standardmäßig für all deine Räume übernommen.", "default_setting_section": "Ich möchte benachrichtigt werden für (Standardeinstellung)", "desktop_notification_message_preview": "Nachrichtenvorschau in der Desktopbenachrichtigung anzeigen", + "dialog_title": "<strong>Einstellungen:</strong> Benachrichtigungen", "email_description": "E-Mail-Zusammenfassung für verpasste Benachrichtigungen erhalten", "email_section": "E-Mail-Zusammenfassung", "email_select": "Wähle, an welche E-Mail-Adresse die Zusammenfassungen gesendet werden. Verwalte deine E-Mail-Adressen unter <button>Allgemein</button>.", @@ -2519,12 +2671,15 @@ "code_blocks_heading": "Quelltextblöcke", "compact_modern": "Modernes kompaktes Layout verwenden", "composer_heading": "Nachrichteneingabe", + "default_timezone": "Browser-Standard (%(timezone)s )", + "dialog_title": "<strong>Einstellungen:</strong> Präferenzen", "enable_hardware_acceleration": "Aktiviere die Hardwarebeschleunigung", "enable_tray_icon": "Fenster beim Schließen in die Symbolleiste minimieren", "keyboard_heading": "Tastenkombinationen", "keyboard_view_shortcuts_button": "Um alle Tastenkombinationen anzuzeigen, <a>klicke hier</a>.", "media_heading": "Mediendateien", "presence_description": "Teile anderen deine Aktivität und deinen Status mit.", + "publish_timezone": "Zeitzone auf öffentlichem Profil anzeigen lassen", "rm_lifetime": "Gültigkeitsdauer der Gelesen-Markierung (ms)", "rm_lifetime_offscreen": "Gültigkeitsdauer der Gelesen-Markierung außerhalb des Bildschirms (ms)", "room_directory_heading": "Raumverzeichnis", @@ -2533,7 +2688,8 @@ "show_checklist_shortcuts": "Verknüpfung zu ersten Schritten (Willkommen) anzeigen", "show_polls_button": "Zeige Pol button", "surround_text": "Sonderzeichen automatisch vor und hinter Textauswahl setzen", - "time_heading": "Zeitanzeige" + "time_heading": "Zeitanzeige", + "user_timezone": "Zeitzone festlegen" }, "prompt_invite": "Warnen, bevor du Einladungen zu ungültigen Matrix-IDs sendest", "replace_plain_emoji": "Klartext-Emoji automatisch ersetzen", @@ -2564,14 +2720,17 @@ "cross_signing_self_signing_private_key": "Selbst signierter privater Schlüssel:", "cross_signing_user_signing_private_key": "Privater Benutzerschlüssel:", "cryptography_section": "Verschlüsselung", + "dehydrated_device_description": "Die Offline-Gerätefunktion ermöglicht es Ihnen, verschlüsselte Nachrichten zu empfangen, auch wenn Sie an keinem Gerät angemeldet sind", + "dehydrated_device_enabled": "Offline-Gerät aktiviert", "delete_backup": "Lösche Sicherung", "delete_backup_confirm_description": "Bist du sicher? Du wirst alle deine verschlüsselten Nachrichten verlieren, wenn deine Schlüssel nicht gut gesichert sind.", + "dialog_title": "<strong>Einstellungen </strong> Sicherheit & Datenschutz", "e2ee_default_disabled_warning": "Deine Server-Administration hat die Ende-zu-Ende-Verschlüsselung für private Räume und Direktnachrichten standardmäßig deaktiviert.", "enable_message_search": "Nachrichtensuche in verschlüsselten Räumen aktivieren", "encryption_section": "Verschlüsselung", "error_loading_key_backup_status": "Konnte Status der Schlüsselsicherung nicht laden", "export_megolm_keys": "E2E-Raumschlüssel exportieren", - "ignore_users_empty": "Du ignorierst keine Benutzer.", + "ignore_users_empty": "Sie haben keinen Benutzer blockiert.", "ignore_users_section": "Blockierte Benutzer", "import_megolm_keys": "E2E-Raumschlüssel importieren", "key_backup_active": "Diese Sitzung sichert deine Schlüssel.", @@ -2643,6 +2802,7 @@ "device_unverified_description_current": "Verifiziere deine aktuelle Sitzung für besonders sichere Kommunikation.", "device_verified_description": "Diese Sitzung ist für sichere Kommunikation bereit.", "device_verified_description_current": "Deine aktuelle Sitzung ist für sichere Kommunikation bereit.", + "dialog_title": "<strong>Einstellungen:</strong> Sitzungen", "error_pusher_state": "Konfigurieren des Push-Dienstes fehlgeschlagen", "error_set_name": "Es konnte kein Sitzungsname gesetzt werden", "filter_all": "Alle", @@ -2682,9 +2842,10 @@ "security_recommendations_description": "Verbessere deine Kontosicherheit, indem du diese Empfehlungen beherzigst.", "session_id": "Sitzungs-ID", "show_details": "Details anzeigen", - "sign_in_with_qr": "Mit QR-Code anmelden", + "sign_in_with_qr": "Neues Gerät verknüpfen", "sign_in_with_qr_button": "QR-Code anzeigen", - "sign_in_with_qr_description": "Du kannst dieses Gerät verwenden, um ein neues Gerät per QR-Code anzumelden. Dazu musst du den auf diesem Gerät angezeigten QR-Code mit deinem nicht angemeldeten Gerät einlesen.", + "sign_in_with_qr_description": "Sie können dieses Gerät dazu verwenden, um ein neues Gerät per QR-Code anzumelden und verschlüsselte Nachrichtenübermittlung einzurichten.", + "sign_in_with_qr_unsupported": "Wird von Ihrem Kontoanbieter nicht unterstützt", "sign_out": "Von dieser Sitzung abmelden", "sign_out_all_other_sessions": "Von allen anderen Sitzungen abmelden (%(otherSessionsCount)s)", "sign_out_confirm_description": { @@ -2724,7 +2885,9 @@ "show_redaction_placeholder": "Platzhalter für gelöschte Nachrichten", "show_stickers_button": "Sticker-Schaltfläche", "show_typing_notifications": "Tippbenachrichtigungen anzeigen", + "showbold": "Alle Aktivitäten in der Chatroommliste anzeigen (Ausführungspunkte oder Anzahl ungelesener Nachrichten)", "sidebar": { + "dialog_title": "<strong>Einstellungen:</strong> Seitenleiste", "metaspaces_favourites_description": "Gruppiere all deine favorisierten Unterhaltungen an einem Ort.", "metaspaces_home_all_rooms": "Alle Räume anzeigen", "metaspaces_home_all_rooms_description": "Alle Räume auf der Startseite anzeigen, auch wenn sie Teil eines Space sind.", @@ -2733,10 +2896,14 @@ "metaspaces_orphans_description": "Gruppiere all deine Räume, die nicht Teil eines Spaces sind, an einem Ort.", "metaspaces_people_description": "Gruppiere all deine Direktnachrichten an einem Ort.", "metaspaces_subsection": "Anzuzeigende Spaces", - "spaces_explainer": "Räume sind Möglichkeiten, Personen zu gruppieren. Neben den Räumen, in denen du dich befindest, kannst du auch einige vorgefertigte verwenden.", + "metaspaces_video_rooms": "Videoräume und -konferenzen", + "metaspaces_video_rooms_description": "Gruppiere alle privaten Videoräume und -konferenzen.", + "metaspaces_video_rooms_description_invite_extension": "Sie können Personen außerhalb von Matrix zu Konferenzen einladen.", + "spaces_explainer": "Räume sind eine Möglichkeit, Chatrooms und Personen zu gruppieren. Abgesehen von den Räumen, in denen Sie sich gerade befinden, können Sie auch einige vorgefertigte Räume verwenden.", "title": "Seitenleiste" }, "start_automatically": "Nach Systemstart automatisch starten", + "tac_only_notifications": "Benachrichtigungen nur im Thread Aktivitätszentrum anzeigen", "use_12_hour_format": "Uhrzeiten im 12-Stundenformat (z. B. 2:30 p. m.)", "use_command_enter_send_message": "Benutze Betriebssystemtaste + Eingabe um eine Nachricht zu senden", "use_command_f_search": "Nutze Command + F um den Verlauf zu durchsuchen", @@ -2750,6 +2917,7 @@ "audio_output_empty": "Keine Audioausgabe erkannt", "auto_gain_control": "Automatische Lautstärkeregelung", "connection_section": "Verbindung", + "dialog_title": "<strong>Einstellungen:</strong> Sprache und Video", "echo_cancellation": "Echounterdrückung", "enable_fallback_ice_server": "Ersatz-Anrufassistenz-Server erlauben (%(server)s)", "enable_fallback_ice_server_description": "Dieser wird nur verwendet, sollte dein Heim-Server keinen bieten. Deine IP-Adresse würde während eines Anrufs geteilt werden.", @@ -2768,9 +2936,12 @@ "warning": "<w>WARNUNG:</w> <description/>" }, "share": { - "link_title": "Link zum Raum", + "link_copied": "Link kopiert", "permalink_message": "Link zur ausgewählten Nachricht", "permalink_most_recent": "Link zur aktuellsten Nachricht", + "share_call": "Konferenzeinladungslink", + "share_call_subtitle": "Link für externe Benutzer, um dem Anruf ohne Matrixkonto beizutreten:", + "title_link": "Link teilen", "title_message": "Raumnachricht teilen", "title_room": "Raum teilen", "title_user": "Teile Benutzer" @@ -2803,7 +2974,7 @@ "help_dialog_title": "Befehl Hilfe", "holdcall": "Den aktuellen Anruf halten", "html": "Sendet eine Nachricht als HTML, ohne sie als Markdown darzustellen", - "ignore": "Nutzer blockieren und dessen Nachrichten ausblenden", + "ignore": "Benutzer blockieren und dessen Nachrichten ausblenden", "ignore_dialog_description": "%(userId)s ist jetzt blockiert", "ignore_dialog_title": "Benutzer blockiert", "invite": "Lädt den Benutzer mit der angegebenen ID in den aktuellen Raum ein", @@ -2846,7 +3017,7 @@ "unban": "Entbannt den Benutzer mit der angegebenen ID", "unflip": "Stellt ┬──┬ ノ( ゜-゜ノ) einer Klartextnachricht voran", "unholdcall": "Beendet das Halten des Anrufs", - "unignore": "Benutzer nicht mehr ignorieren und neue Nachrichten wieder anzeigen", + "unignore": "Benutzer freigeben und ihre neuen Nachrichten wieder anzeigen", "unignore_dialog_description": "%(userId)s wird nicht mehr blockiert", "unignore_dialog_title": "Benutzer nicht mehr blockiert", "unknown_command": "Unbekannter Befehl", @@ -2857,13 +3028,6 @@ "upgraderoom": "Aktualisiert den Raum auf eine neue Version", "upgraderoom_permission_error": "Du hast nicht die erforderlichen Berechtigungen, diesen Befehl zu verwenden.", "usage": "Verwendung", - "verify": "Verifiziert Benutzer, Sitzung und öffentlichen Schlüsselpaare", - "verify_mismatch": "ACHTUNG: SCHLÜSSELVERIFIZIERUNG FEHLGESCHLAGEN! Der Signierschlüssel für %(userId)s und Sitzung %(deviceId)s ist \"%(fprint)s\", was nicht mit dem bereitgestellten Schlüssel \"%(fingerprint)s\" übereinstimmt. Das könnte bedeuten, dass deine Kommunikation abgehört wird!", - "verify_nop": "Sitzung bereits verifiziert!", - "verify_nop_warning_mismatch": "ACHTUNG: Sitzung bereits verifiziert, aber die Schlüssel PASSEN NICHT!", - "verify_success_description": "Dein bereitgestellter Signaturschlüssel passt zum von der Sitzung %(deviceId)s von %(userId)s empfangendem Schlüssel. Sitzung wurde als verifiziert markiert.", - "verify_success_title": "Verifizierter Schlüssel", - "verify_unknown_pair": "Unbekanntes Paar (Nutzer, Sitzung): (%(userId)s, %(deviceId)s)", "view": "Raum mit angegebener Adresse betrachten", "whois": "Zeigt Informationen über Benutzer" }, @@ -2971,7 +3135,7 @@ "heading_with_query": "Nutze \"%(query)s\" zum Suchen", "heading_without_query": "Suche nach", "join_button_text": "%(roomAddress)s betreten", - "keyboard_scroll_hint": "Benutze <arrows/> zum scrollen", + "keyboard_scroll_hint": "Benutze <arrows/> zum Scrollen", "message_search_section_title": "Andere Suchen", "other_rooms_in_space": "Andere Räume in %(spaceName)s", "public_rooms_label": "Öffentliche Räume", @@ -3019,12 +3183,22 @@ "one": "%(count)s Antwort", "other": "%(count)s Antworten" }, + "empty_description": "Verwende \"%(replyInThread)s\" beim Hovern über eine Nachricht.", + "empty_title": "Threads helfen Ihnen, Ihre Unterhaltungen beim Thema zu halten, and sie sind auch leichter zu verfolgen.", "error_start_thread_existing_relation": "Du kannst keinen Thread in einem Thread starten", + "mark_all_read": "Alle als gelesen markieren", "my_threads": "Meine Threads", "my_threads_description": "Zeigt alle Threads, an denen du teilgenommen hast", "open_thread": "Thread anzeigen", "show_thread_filter": "Zeige:" }, + "threads_activity_centre": { + "header": "Thread-Aktivität", + "no_rooms_with_threads_notifs": "Sie haben noch keine Chatrooms mit Thread-Benachrichtigungen.", + "no_rooms_with_unread_threads": "Sie haben noch keine Chatrooms mit ungelesenen Threads.", + "release_announcement_description": "Die Thread-Benachrichtigungen wurden verschoben. Sie finden sie ab sofort hier.", + "release_announcement_header": "Thread Aktivitätszentrum" + }, "time": { "about_day_ago": "vor etwa einem Tag", "about_hour_ago": "vor etwa einer Stunde", @@ -3066,9 +3240,20 @@ }, "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 den Empfang dieser Nachricht blockiert, da Ihr Gerät nicht verifiziert ist.", + "historical_event_no_key_backup": "Der historische Nachrichtenverlauf ist auf diesem Gerät nicht verfügbar.", + "historical_event_unverified_device": "Sie müssen dieses Gerät verifizieren, um auf den Nachrichtenverlauf zugreifen zu können", + "historical_event_user_not_joined": "Sie haben keinen Zugriff auf diese Nachricht", + "sender_identity_previously_verified": "Die verifizierte Identität des Absenders hat sich geändert", + "sender_unsigned_device": "Von einem unsicheren Gerät verschickt.", + "unable_to_decrypt": "Entschlüsselung der Nachricht nicht möglich" + }, "disambiguated_profile": "%(displayName)s (%(matrixId)s)", "download_action_decrypting": "Entschlüsseln", "download_action_downloading": "Herunterladen", + "download_failed": "Herunterladen fehlgeschlagen", + "download_failed_description": "Beim Herunterladen dieser Datei ist ein Fehler aufgetreten", "e2e_state": "Status der Ende zu Ende Verschlüssellung", "edits": { "tooltip_label": "Am %(date)s geändert. Klicke, um Änderungen anzuzeigen.", @@ -3079,10 +3264,6 @@ "error_rendering_message": "Diese Nachricht kann nicht geladen werden", "historical_messages_unavailable": "Du kannst keine älteren Nachrichten lesen", "in_room_name": " in <strong>%(room)s</strong>", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s beendete eine <a>Sprachübertragung</a>", - "you": "Du hast eine <a>Sprachübertragung</a> beendet" - }, "io.element.widgets.layout": "%(senderName)s hat das Raumlayout geändert", "late_event_separator": "Ursprünglich gesendet %(dateTime)s", "load_error": { @@ -3127,7 +3308,7 @@ }, "m.file": { "error_decrypting": "Fehler beim Entschlüsseln des Anhangs", - "error_invalid": "Ungültige Datei%(extra)s" + "error_invalid": "Ungültige Datei" }, "m.image": { "error": "Kann Bild aufgrund eines Fehlers nicht anzeigen", @@ -3242,12 +3423,12 @@ "set": "%(senderDisplayName)s hat den Raumnamen geändert zu %(roomName)s." }, "m.room.pinned_events": { - "changed": "%(senderName)s hat die angehefteten Nachrichten für diesen Raum geändert.", - "changed_link": "%(senderName)s hat die <a>angehefteten Nachrichten</a> geändert.", - "pinned": "%(senderName)s hat eine Nachricht angeheftet. Alle angehefteten Nachrichten anzeigen.", - "pinned_link": "%(senderName)s hat <a>eine Nachricht</a> angeheftet. <b>Alle angehefteten Nachrichten anzeigen</b>.", - "unpinned": "%(senderName)s hat eine Nachricht losgelöst. Alle angepinnten Nachrichten anzeigen.", - "unpinned_link": "%(senderName)s hat <a>eine Nachricht</a> losgeheftet. Alle <b>angehefteten Nachrichten anzeigen</b>." + "changed": "%(senderName)s hat die fixierte Nachrichten für diesen Chatroom geändert.", + "changed_link": "%(senderName)s hat die <a>fixierten Nachrichten</a> geändert.", + "pinned": "%(senderName)s hat eine Nachricht fixiert. Alle fixierten Nachrichten anzeigen.", + "pinned_link": "%(senderName)s hat <a>eine Nachricht</a> fixiert. <b>Alle fixierten Nachrichten anzeigen</b>.", + "unpinned": "%(senderName)s hat eine Nachricht losgelöst. Alle fixierten Nachrichten anzeigen.", + "unpinned_link": "%(senderName)s hat <a>eine Nachricht</a> gelöst. Alle <b>fixierten Nachrichten anzeigen</b>." }, "m.room.power_levels": { "changed": "%(senderName)s hat das Berechtigungslevel von %(powerLevelDiffText)s geändert.", @@ -3314,7 +3495,8 @@ "reactions": { "add_reaction_prompt": "Reaktion hinzufügen", "custom_reaction_fallback_label": "Benutzerdefinierte Reaktion", - "label": "%(reactors)s hat mit %(content)s reagiert" + "label": "%(reactors)s hat mit %(content)s reagiert", + "tooltip_caption": "hat reagiert mit %(shortName)s" }, "read_receipt_title": { "one": "Von %(count)s Person gesehen", @@ -3499,6 +3681,10 @@ "truncated_list_n_more": { "other": "Und %(count)s weitere …" }, + "unsupported_browser": { + "description": "Wenn Sie fortfahren, funktionieren einige Funktionen möglicherweise nicht mehr und es besteht das Risiko, dass Sie in Zukunft Daten verlieren. Aktualisieren Sie Ihren Browser, um die Nutzung von %(brand)s fortzusetzen.", + "title": "%(brand)s unterstützt diesen Browser nicht" + }, "unsupported_server_description": "Dieser Server nutzt eine ältere Matrix-Version. Aktualisiere auf Matrix %(version)s, um %(brand)s fehlerfrei nutzen zu können.", "unsupported_server_title": "Dein Server wird nicht unterstützt", "update": { @@ -3516,6 +3702,12 @@ "toast_title": "Aktualisiere %(brand)s", "unavailable": "Nicht verfügbar" }, + "update_room_access_modal": { + "description": "Um einen Link zum Teilen zu erstellen, müssen Sie Gästen erlauben, diesem Chatroom beizutreten. Dadurch kann der Chatroom weniger sicher werden. Wenn Sie mit dem Anruf fertig sind, können Sie den Raum wieder privat machen.", + "dont_change_description": "Sie können den Anruf auch in einem separaten Chatroom führen.", + "no_change": "Ich möchte die Zugriffsebene nicht ändern.", + "title": "Ändern Sie die Zugriffsebene des Chatrooms." + }, "upload_failed_generic": "Die Datei „%(fileName)s“ konnte nicht hochgeladen werden.", "upload_failed_size": "Die Datei „%(fileName)s“ überschreitet das Hochladelimit deines Heim-Servers", "upload_failed_title": "Hochladen fehlgeschlagen", @@ -3525,6 +3717,7 @@ "error_files_too_large": "Die Datei ist <b>zu groß</b>, um hochgeladen zu werden. Die maximale Dateigröße ist %(limit)s.", "error_some_files_too_large": "Einige Dateien sind <b>zu groß</b>, um hochgeladen zu werden. Die maximale Dateigröße ist %(limit)s.", "error_title": "Fehler beim Hochladen", + "not_image": "Die von Ihnen ausgewählte Datei ist keine gültige Bilddatei.", "title": "Dateien hochladen", "title_progress": "Dateien hochladen (%(current)s von %(total)s)", "upload_all_button": "Alle hochladen", @@ -3551,6 +3744,7 @@ "deactivate_confirm_action": "Konto deaktivieren", "deactivate_confirm_description": "Beim Deaktivieren wirst du abgemeldet und ein erneutes Anmelden verhindert. Zusätzlich wirst du aus allen Räumen entfernt. Diese Aktion kann nicht rückgängig gemacht werden. Bist du sicher, dass du dieses Konto deaktivieren willst?", "deactivate_confirm_title": "Konto deaktivieren?", + "dehydrated_device_enabled": "Offline-Gerät aktiviert", "demote_button": "Zurückstufen", "demote_self_confirm_description_space": "Das Entfernen von Rechten kann nicht rückgängig gemacht werden. Falls sie dir niemand anderer zurückgeben kann, kannst du sie nie wieder erhalten.", "demote_self_confirm_room": "Du wirst nicht in der Lage sein, die Änderung zurückzusetzen, da du dich degradierst. Wenn du der letze Nutzer mit Berechtigungen bist, wird es unmöglich sein die Privilegien zurückzubekommen.", @@ -3567,6 +3761,7 @@ "error_revoke_3pid_invite_title": "Einladung konnte nicht zurückgezogen werden", "hide_sessions": "Sitzungen ausblenden", "hide_verified_sessions": "Verifizierte Sitzungen ausblenden", + "ignore_button": "Blockieren", "ignore_confirm_description": "Alle Nachrichten und Einladungen der Person werden verborgen. Bist du sicher, dass du sie ignorieren möchtest?", "ignore_confirm_title": "%(user)s ignorieren", "invited_by": "%(sender)s eingeladen", @@ -3594,59 +3789,30 @@ "no_recent_messages_description": "Versuche nach oben zu scrollen, um zu sehen ob sich dort frühere Nachrichten befinden.", "no_recent_messages_title": "Keine neuen Nachrichten von %(user)s gefunden" }, - "redact_button": "Kürzlich gesendete Nachrichten entfernen", + "redact_button": "Nachrichten entfernen", "revoke_invite": "Einladung zurückziehen", "room_encrypted": "Nachrichten in diesem Raum sind Ende-zu-Ende verschlüsselt.", "room_encrypted_detail": "Diese Nachricht ist verschlüsselt. Nur Sie und der Empfänger haben den Schlüssel, um die Nachricht zu entschlüsseln.", "room_unencrypted": "Nachrichten in diesem Raum sind nicht Ende-zu-Ende verschlüsselt.", "room_unencrypted_detail": "Nachrichten in verschlüsselten Räumen können nur von dir und vom Empfänger gelesen werden.", - "share_button": "Link zu Benutzer teilen", + "send_message": "Nachricht senden", + "share_button": "Profil teilen", "unban_button_room": "Entbannen", "unban_button_space": "Entbannen", "unban_room_confirm_title": "Von %(roomName)s entbannen", "unban_space_everything": "Überall wo ich die Rechte dazu habe, entbannen", "unban_space_specific": "In ausgewählten Räumen und Spaces entbannen", "unban_space_warning": "Die Person wird keinen Zutritt zu Bereichen haben, in denen du nicht administrierst.", + "unignore_button": "Nicht mehr ignorieren", "verify_button": "Nutzer verifizieren", "verify_explainer": "Für zusätzliche Sicherheit, verifiziere diesen Nutzer, durch Vergleichen eines Einmal-Codes auf euren beiden Geräten." }, "user_menu": { + "link_new_device": "Neues Gerät verknüpfen", "settings": "Alle Einstellungen", "switch_theme_dark": "Zum dunklen Thema wechseln", "switch_theme_light": "Zum hellen Thema wechseln" }, - "voice_broadcast": { - "30s_backward": "30s zurückspulen", - "30s_forward": "30s vorspulen", - "action": "Sprachübertragung", - "buffering": "Puffere …", - "confirm_listen_affirm": "Ja, beende meine Aufzeichnung", - "confirm_listen_description": "Wenn du beginnst, diese Echtzeitübertragung anzuhören, wird deine aktuelle Echtzeitübertragungsaufzeichnung beendet.", - "confirm_listen_title": "Echtzeitübertragung anhören?", - "confirm_stop_affirm": "Ja, Übertragung beenden", - "confirm_stop_description": "Möchtest du deine Übertragung wirklich beenden? Dies wird die Übertragung abschließen und die vollständige Aufnahme im Raum bereitstellen.", - "confirm_stop_title": "Live-Übertragung beenden?", - "connection_error": "Verbindungsfehler − Aufnahme pausiert", - "failed_already_recording_description": "Du zeichnest bereits eine Sprachübertragung auf. Bitte beende die laufende Übertragung, um eine neue zu beginnen.", - "failed_already_recording_title": "Sprachübertragung kann nicht gestartet werden", - "failed_decrypt": "Entschlüsseln der Sprachübertragung nicht möglich", - "failed_generic": "Wiedergabe der Sprachübertragung nicht möglich", - "failed_insufficient_permission_description": "Du hast nicht die nötigen Berechtigungen, um eine Sprachübertragung in diesem Raum zu starten. Kontaktiere einen Raumadministrator, um deine Berechtigungen anzupassen.", - "failed_insufficient_permission_title": "Sprachübertragung kann nicht gestartet werden", - "failed_no_connection_description": "Leider ist es aktuell nicht möglich, eine Aufnahme zu beginnen. Bitte versuche es später erneut.", - "failed_no_connection_title": "Verbindungsfehler", - "failed_others_already_recording_description": "Jemand anderes nimmt bereits eine Sprachübertragung auf. Warte auf das Ende der Übertragung, bevor du eine neue startest.", - "failed_others_already_recording_title": "Sprachübertragung kann nicht gestartet werden", - "go_live": "Live schalten", - "live": "Live", - "pause": "Sprachübertragung pausieren", - "play": "Sprachübertragung wiedergeben", - "resume": "Sprachübertragung fortsetzen" - }, - "voice_message": { - "cant_start_broadcast_description": "Du kannst keine Sprachnachricht beginnen, da du im Moment eine Echtzeitübertragung aufzeichnest. Bitte beende deine Sprachübertragung, um ein Gespräch zu beginnen.", - "cant_start_broadcast_title": "Kann Sprachnachricht nicht beginnen" - }, "voip": { "already_in_call": "Schon im Anruf", "already_in_call_person": "Du bist schon in einem Anruf mit dieser Person.", @@ -3666,10 +3832,10 @@ "camera_disabled": "Deine Kamera ist ausgeschaltet", "camera_enabled": "Deine Kamera ist noch aktiv", "cannot_call_yourself_description": "Du kannst keinen Anruf mit dir selbst starten.", - "change_input_device": "Eingabegerät wechseln", + "close_lobby": "Lobby schließen", "connecting": "Verbinden", "connection_lost": "Verbindung zum Server unterbrochen", - "connection_lost_description": "Sie können keine Anrufe starten ohne Verbindung zum Server.", + "connection_lost_description": "Ohne Verbindung zum Server können Sie keine Anrufe tätigen.", "consulting": "%(transferTarget)s wird angefragt. <a>Übertragung zu %(transferee)s</a>", "default_device": "Standardgerät", "dial": "Wählen", @@ -3680,17 +3846,24 @@ "disabled_no_perms_start_video_call": "Dir fehlt die Berechtigung, um Videoanrufe zu beginnen", "disabled_no_perms_start_voice_call": "Dir fehlt die Berechtigung, um Audioanrufe zu beginnen", "disabled_ongoing_call": "laufender Anruf", + "element_call": "Element Anruf", "enable_camera": "Kamera aktivieren", "enable_microphone": "Mikrofon aktivieren", "expand": "Zurück zum Anruf", - "failed_call_live_broadcast_description": "Du kannst keinen Anruf beginnen, da du im Moment eine Sprachübertragung aufzeichnest. Bitte beende deine Sprachübertragung, um ein Gespräch zu beginnen.", - "failed_call_live_broadcast_title": "Kann keinen Anruf beginnen", + "get_call_link": "Anruflink teilen", "hangup": "Auflegen", "hide_sidebar_button": "Seitenleiste verbergen", "input_devices": "Eingabegeräte", + "jitsi_call": "Jitsi-Konferenz", "join_button_tooltip_call_full": "Entschuldigung — dieser Anruf ist aktuell besetzt", "join_button_tooltip_connecting": "Verbinden", + "legacy_call": "Legacy-Anruf", "maximise": "Bildschirm füllen", + "maximise_call": "Anruf maximieren", + "metaspace_video_rooms": { + "conference_room_section": "Konferenzen" + }, + "minimise_call": "Anruf minimieren", "misconfigured_server": "Anruf aufgrund eines falsch konfigurierten Servers fehlgeschlagen", "misconfigured_server_description": "Bitte frage die Administration deines Heim-Servers (<code>%(homeserverDomain)s</code>) darum, einen TURN-Server einzurichten, damit Anrufe zuverlässig funktionieren.", "misconfigured_server_fallback": "Alternativ kannst du versuchen, den öffentlichen Server unter <server/> zu verwenden. Dieser wird nicht so zuverlässig sein und deine IP-Adresse wird mit ihm geteilt. Du kannst dies auch in den Einstellungen konfigurieren.", @@ -3732,12 +3905,13 @@ "unknown_person": "unbekannte Person", "unsilence": "Ton an", "unsupported": "Anrufe werden nicht unterstützt", - "unsupported_browser": "Sie können in diesem Browser keien Anrufe durchführen.", + "unsupported_browser": "Du kannst in diesem Browser keine Anrufe tätigen.", "user_busy": "Person beschäftigt", "user_busy_description": "Die angerufene Person ist momentan beschäftigt.", "user_is_presenting": "%(sharerName)s präsentiert", "video_call": "Videoanruf", "video_call_started": "Videoanruf hat begonnen", + "video_call_using": "Videoanruf mit:", "voice_call": "Sprachanruf", "you_are_presenting": "Du präsentierst" }, @@ -3846,7 +4020,7 @@ "title": "Erlaube diesem Widget deine Identität zu überprüfen" }, "popout": "Widget in eigenem Fenster öffnen", - "set_room_layout": "Dein Raumlayout für alle setzen", + "set_room_layout": "Layout für alle festlegen", "shared_data_avatar": "Deine Profilbild-URL", "shared_data_device_id": "Deine Geräte-ID", "shared_data_lang": "Deine Sprache", @@ -3872,7 +4046,7 @@ "l33t": "Vorhersagbare Ersetzungen wie „@“ anstelle von „a“ helfen nicht besonders", "longerKeyboardPattern": "Nutze ein längeres Tastaturmuster mit mehr Abwechslung", "noNeed": "Kein Bedarf an Symbolen, Zahlen oder Großbuchstaben", - "pwned": "Wenn Sie dieses Passwort woanders verwenden, sollten Sie es ändern.", + "pwned": "Falls Sie dieses Passwort anderweitig verwenden, sollten Sie es ändern.", "recentYears": "Vermeide die letzten Jahre", "repeated": "Vermeide wiederholte Worte und Zeichen", "reverseWords": "Umgedrehte Worte sind nicht schwerer zu erraten", @@ -3894,6 +4068,7 @@ "straightRow": "Gerade Reihen von Tasten sind einfach zu erraten", "topHundred": "Dies ist unter den Top 100 der häufigsten Passwörter", "topTen": "Dies ist unter den Top 10 der häufigsten Passwörter", + "userInputs": "Personenbezogene oder seitenbezogene Daten sollten hier nicht vorhanden sein.", "wordByItself": "Ein einzelnes Wort ist einfach zu erraten" } } diff --git a/src/i18n/strings/el.json b/src/i18n/strings/el.json index 2c042c0dc3..808aeb02eb 100644 --- a/src/i18n/strings/el.json +++ b/src/i18n/strings/el.json @@ -763,12 +763,6 @@ "incoming_sas_dialog_title": "Εισερχόμενο Αίτημα Επαλήθευσης", "incoming_sas_user_dialog_text_1": "Επαληθεύστε αυτόν τον χρήστη για να τον επισημάνετε ως αξιόπιστο. Η εμπιστοσύνη των χρηστών σάς προσφέρει επιπλέον ηρεμία όταν χρησιμοποιείτε μηνύματα με κρυπτογράφηση από άκρο σε άκρο.", "incoming_sas_user_dialog_text_2": "Η επαλήθευση αυτού του χρήστη θα επισημάνει τη συνεδρία του ως αξιόπιστη και θα επισημάνει επίσης τη συνεδρία σας ως αξιόπιστη σε αυτόν.", - "manual_device_verification_device_id_label": "Αναγνωριστικό συνεδρίας", - "manual_device_verification_device_key_label": "Κλειδί συνεδρίας", - "manual_device_verification_device_name_label": "Όνομα συνεδρίας", - "manual_device_verification_footer": "Εάν δεν ταιριάζουν, η ασφάλεια της επικοινωνίας σας μπορεί να τεθεί σε κίνδυνο.", - "manual_device_verification_self_text": "Επιβεβαιώστε συγκρίνοντας τα ακόλουθα με τις Ρυθμίσεις χρήστη στην άλλη συνεδρία σας:", - "manual_device_verification_user_text": "Επιβεβαιώστε την συνεδρία αυτού του χρήστη συγκρίνοντας τα ακόλουθα με τις Ρυθμίσεις του:", "no_key_or_device": "Φαίνεται ότι δε διαθέτετε Κλειδί Ασφαλείας ή άλλες συσκευές με τις οποίες μπορείτε να επαληθεύσετε. Αυτή η συσκευή δε θα έχει πρόσβαση σε παλιά κρυπτογραφημένα μηνύματα. Για να επαληθεύσετε την ταυτότητά σας σε αυτήν τη συσκευή, θα πρέπει να επαναφέρετε τα κλειδιά επαλήθευσης.", "no_support_qr_emoji": "Η συσκευή που προσπαθείτε να επαληθεύσετε δεν υποστηρίζει τη σάρωση κωδικού QR ή επαλήθευσης emoji, κάτι που υποστηρίζει το %(brand)s. Δοκιμάστε με διαφορετικό πρόγραμμα-πελάτη.", "other_party_cancelled": "Το άλλο μέρος ακύρωσε την επαλήθευση.", @@ -2164,7 +2158,6 @@ "warn_quit": "Προειδοποιήστε πριν την παραίτηση" }, "share": { - "link_title": "Σύνδεσμος στο δωμάτιο", "permalink_message": "Σύνδεσμος στο επιλεγμένο μήνυμα", "permalink_most_recent": "Σύνδεσμος προς το πιο πρόσφατο μήνυμα", "title_message": "Κοινή χρήση Μηνύματος Δωματίου", @@ -2246,12 +2239,6 @@ "upgraderoom": "Αναβαθμίζει το δωμάτιο σε μια καινούργια έκδοση", "upgraderoom_permission_error": "Δεν διαθέτετε τις απαιτούμενες άδειες για να χρησιμοποιήσετε αυτήν την εντολή.", "usage": "Χρήση", - "verify": "Επιβεβαιώνει έναν χρήστη, συνεδρία, και pubkey tuple", - "verify_mismatch": "ΠΡΟΕΙΔΟΠΟΙΗΣΗ: Η ΕΠΑΛΗΘΕΥΣΗ ΚΛΕΙΔΙΟΥ ΑΠΕΤΥΧΕ! Το κλειδί σύνδεσης για %(userId)s και συνεδρίας %(deviceId)s είναι \"%(fprint)s\" που δεν ταιριάζει με το παρεχόμενο κλειδί\"%(fingerprint)s\". Αυτό μπορεί να σημαίνει ότι υπάρχει υποκλοπή στις επικοινωνίες σας!", - "verify_nop": "Η συνεδρία έχει ήδη επιβεβαιωθεί!", - "verify_success_description": "Το κλειδί υπογραφής που παρείχατε ταιριάζει με το κλειδί που λάβατε από την συνεδρία %(userId)s's %(deviceId)s. Η συνεδρία σημειώνεται ως επιβεβαιωμένη.", - "verify_success_title": "Επιβεβαιωμένο κλειδί", - "verify_unknown_pair": "Άγνωστο ζευγάρι (χρήστης, συνεδρία): (%(userId)s, %(deviceId)s)", "whois": "Εμφανίζει πληροφορίες για έναν χρήστη" }, "space": { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 625c6a717c..4c075b1567 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -398,7 +398,7 @@ }, "bug_reporting": { "additional_context": "If there is additional context that would help in analysing the issue, such as what you were doing at the time, room IDs, user IDs, etc., please include those things here.", - "before_submitting": "Before submitting logs, you must <a>create a GitHub issue</a> to describe your problem.", + "before_submitting": "We recommend <a>creating a GitHub issue</a> to ensure that your report is reviewed.", "collecting_information": "Collecting app version information", "collecting_logs": "Collecting logs", "create_new_issue": "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.", @@ -905,6 +905,8 @@ "warning": "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings." }, "not_supported": "<not supported>", + "pinned_identity_changed": "%(displayName)s's (<b>%(userId)s</b>) identity appears to have changed. <a>Learn more</a>", + "pinned_identity_changed_no_displayname": "<b>%(userId)s</b>'s identity appears to have changed. <a>Learn more</a>", "recovery_method_removed": { "description_1": "This session has detected that your Security Phrase and key for Secure Messages have been removed.", "description_2": "If you did this accidentally, you can setup Secure Messages on this session which will re-encrypt this session's message history with a new recovery method.", @@ -920,7 +922,6 @@ }, "udd": { "interactive_verification_button": "Interactively verify by emoji", - "manual_verification_button": "Manually verify by text", "other_ask_verify_text": "Ask this user to verify their session, or manually verify it below.", "other_new_session_text": "%(name)s (%(userId)s) signed in to a new session without verifying it:", "own_ask_verify_text": "Verify your other session using one of the options below.", @@ -955,12 +956,6 @@ "incoming_sas_dialog_waiting": "Waiting for partner to confirm…", "incoming_sas_user_dialog_text_1": "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.", "incoming_sas_user_dialog_text_2": "Verifying this user will mark their session as trusted, and also mark your session as trusted to them.", - "manual_device_verification_device_id_label": "Session ID", - "manual_device_verification_device_key_label": "Session key", - "manual_device_verification_device_name_label": "Session name", - "manual_device_verification_footer": "If they don't match, the security of your communication may be compromised.", - "manual_device_verification_self_text": "Confirm by comparing the following with the User Settings in your other session:", - "manual_device_verification_user_text": "Confirm this user's session by comparing the following with their User Settings:", "no_key_or_device": "It looks like you don't have a Security Key or any other devices you can verify against. This device will not be able to access old encrypted messages. In order to verify your identity on this device, you'll need to reset your verification keys.", "no_support_qr_emoji": "The device you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.", "other_party_cancelled": "The other party cancelled the verification.", @@ -1000,7 +995,7 @@ "unverified_sessions_toast_description": "Review to ensure your account is safe", "unverified_sessions_toast_reject": "Later", "unverified_sessions_toast_title": "You have unverified sessions", - "verification_description": "Verify your identity to access encrypted messages and prove your identity to others.", + "verification_description": "Verify your identity to access encrypted messages and prove your identity to others. If you also use a mobile device, please open the app there before you proceed.", "verification_dialog_title_device": "Verify other device", "verification_dialog_title_user": "Verification Request", "verification_skip_warning": "Without verifying, you won't have access to all your messages and may appear as untrusted to others.", @@ -1085,10 +1080,6 @@ }, "error_user_not_logged_in": "User is not logged in", "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s ended a voice broadcast", - "you": "You ended a voice broadcast" - }, "m.call.answer": { "dm": "Call in progress", "user": "%(senderName)s joined the call", @@ -1489,8 +1480,6 @@ "video_rooms_faq2_answer": "Yes, the chat timeline is displayed alongside the video.", "video_rooms_faq2_question": "Can I use text chat alongside the video call?", "video_rooms_feedbackSubheading": "Thank you for trying the beta, please go into as much detail as you can so we can improve it.", - "voice_broadcast": "Voice broadcast", - "voice_broadcast_force_small_chunks": "Force 15s voice broadcast chunk length", "wysiwyg_composer": "Rich text editor" }, "labs_mjolnir": { @@ -1636,7 +1625,6 @@ "mute_description": "You won't get any notifications" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s started a voice broadcast", "m.key.verification.request": "%(name)s is requesting verification" }, "onboarding": { @@ -2251,7 +2239,6 @@ "error_unbanning": "Failed to unban", "events_default": "Send messages", "invite": "Invite users", - "io.element.voice_broadcast_info": "Voice broadcasts", "kick": "Remove users", "m.call": "Start %(brand)s calls", "m.call.member": "Join %(brand)s calls", @@ -2950,7 +2937,7 @@ "warning": "<w>WARNING:</w> <description/>" }, "share": { - "link_title": "Link to room", + "link_copied": "Link copied", "permalink_message": "Link to selected message", "permalink_most_recent": "Link to most recent message", "share_call": "Conference invite link", @@ -3042,13 +3029,6 @@ "upgraderoom": "Upgrades a room to a new version", "upgraderoom_permission_error": "You do not have the required permissions to use this command.", "usage": "Usage", - "verify": "Verifies a user, session, and pubkey tuple", - "verify_mismatch": "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!", - "verify_nop": "Session already verified!", - "verify_nop_warning_mismatch": "WARNING: session already verified, but keys do NOT MATCH!", - "verify_success_description": "The signing key you provided matches the signing key you received from %(userId)s's session %(deviceId)s. Session marked as verified.", - "verify_success_title": "Verified key", - "verify_unknown_pair": "Unknown (user, session) pair: (%(userId)s, %(deviceId)s)", "view": "Views room with given address", "whois": "Displays information about a user" }, @@ -3285,10 +3265,6 @@ "error_rendering_message": "Can't load this message", "historical_messages_unavailable": "You can't see earlier messages", "in_room_name": " in <strong>%(room)s</strong>", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s ended a <a>voice broadcast</a>", - "you": "You ended a <a>voice broadcast</a>" - }, "io.element.widgets.layout": "%(senderName)s has updated the room layout", "late_event_separator": "Originally sent %(dateTime)s", "load_error": { @@ -3838,38 +3814,6 @@ "switch_theme_dark": "Switch to dark mode", "switch_theme_light": "Switch to light mode" }, - "voice_broadcast": { - "30s_backward": "30s backward", - "30s_forward": "30s forward", - "action": "Voice broadcast", - "buffering": "Buffering…", - "confirm_listen_affirm": "Yes, end my recording", - "confirm_listen_description": "If you start listening to this live broadcast, your current live broadcast recording will be ended.", - "confirm_listen_title": "Listen to live broadcast?", - "confirm_stop_affirm": "Yes, stop broadcast", - "confirm_stop_description": "Are you sure you want to stop your live broadcast? This will end the broadcast and the full recording will be available in the room.", - "confirm_stop_title": "Stop live broadcasting?", - "connection_error": "Connection error - Recording paused", - "failed_already_recording_description": "You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.", - "failed_already_recording_title": "Can't start a new voice broadcast", - "failed_decrypt": "Unable to decrypt voice broadcast", - "failed_generic": "Unable to play this voice broadcast", - "failed_insufficient_permission_description": "You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.", - "failed_insufficient_permission_title": "Can't start a new voice broadcast", - "failed_no_connection_description": "Unfortunately we're unable to start a recording right now. Please try again later.", - "failed_no_connection_title": "Connection error", - "failed_others_already_recording_description": "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.", - "failed_others_already_recording_title": "Can't start a new voice broadcast", - "go_live": "Go live", - "live": "Live", - "pause": "pause voice broadcast", - "play": "play voice broadcast", - "resume": "resume voice broadcast" - }, - "voice_message": { - "cant_start_broadcast_description": "You can't start a voice message as you are currently recording a live broadcast. Please end your live broadcast in order to start recording a voice message.", - "cant_start_broadcast_title": "Can't start voice message" - }, "voip": { "already_in_call": "Already in call", "already_in_call_person": "You're already in a call with this person.", @@ -3889,7 +3833,6 @@ "camera_disabled": "Your camera is turned off", "camera_enabled": "Your camera is still enabled", "cannot_call_yourself_description": "You cannot place a call with yourself.", - "change_input_device": "Change input device", "close_lobby": "Close lobby", "connecting": "Connecting", "connection_lost": "Connectivity to the server has been lost", @@ -3908,8 +3851,6 @@ "enable_camera": "Turn on camera", "enable_microphone": "Unmute microphone", "expand": "Return to call", - "failed_call_live_broadcast_description": "You can’t start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call.", - "failed_call_live_broadcast_title": "Can’t start a call", "get_call_link": "Share call link", "hangup": "Hangup", "hide_sidebar_button": "Hide sidebar", diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json index 33d8e57f48..88694a3a58 100644 --- a/src/i18n/strings/eo.json +++ b/src/i18n/strings/eo.json @@ -693,12 +693,6 @@ "incoming_sas_dialog_title": "Venas kontrolpeto", "incoming_sas_user_dialog_text_1": "Kontrolu ĉi tiun uzanton por marki ĝin fidata. Fidado devas vin trankviligi dum uzado de tutvoja ĉifrado.", "incoming_sas_user_dialog_text_2": "Kontrolo de tiu ĉi uzanto markos ĝian salutaĵon fidata, kaj ankaŭ markos vian salutaĵon fidata por ĝi.", - "manual_device_verification_device_id_label": "Identigilo de salutaĵo", - "manual_device_verification_device_key_label": "Ŝlosilo de salutaĵo", - "manual_device_verification_device_name_label": "Nomo de salutaĵo", - "manual_device_verification_footer": "Se ili ne akordas, la sekureco de via komunikado eble estas rompita.", - "manual_device_verification_self_text": "Konfirmu per komparo de la sekva kun la agardoj de uzanto en via alia salutaĵo:", - "manual_device_verification_user_text": "Konfirmu la salutaĵon de ĉi tiu uzanto per komparo de la sekva kun ĝiaj agordoj de uzanto:", "other_party_cancelled": "La alia kontrolano nuligis la kontrolon.", "prompt_encrypted": "Kontrolu ĉiujn uzantojn en ĉambro por certigi, ke ĝi sekuras.", "prompt_self": "Rekomencu kontroladon el la sciigo.", @@ -795,10 +789,6 @@ } }, "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s finis voĉan elsendon", - "you": "Vi finis voĉan elsendon" - }, "m.call.answer": { "dm": "Voko okazas", "user": "%(senderName)s aliĝis al la voko", @@ -1065,7 +1055,6 @@ "video_rooms_always_on_voip_channels": "Videoĉambroj estas ĉiam ŝaltitaj VoIP-kanaloj enkonstruitaj en ĉambro en %(brand)s.", "video_rooms_faq1_answer": "Uzu la \"+\" butonon en la ĉambro sekcio de la maldekstra panelo.", "video_rooms_faq1_question": "Kiel mi povas krei videoĉambron?", - "voice_broadcast": "Voĉan elsendo", "wysiwyg_composer": "Riĉa tekstoredaktilo" }, "labs_mjolnir": { @@ -1167,7 +1156,6 @@ "message_didnt_send": "Mesaĝo ne sendiĝis. Klaku por akiri informojn." }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s komencis voĉan elsendon", "m.key.verification.request": "%(name)s petas kontrolon" }, "onboarding": { @@ -2050,13 +2038,6 @@ "upgraderoom": "Gradaltigas ĉambron al nova versio", "upgraderoom_permission_error": "Vi ne havas sufiĉajn permesojn por uzi ĉi tiun komandon.", "usage": "Uzo", - "verify": "Kontrolas opon de uzanto, salutaĵo, kaj publika ŝlosilo", - "verify_mismatch": "AVERTO: MALSUKCESIS KONTROLO DE ŜLOSILOJ! La subskriba ŝlosilo de %(userId)s kaj session %(deviceId)s estas «%(fprint)s», kiu ne akordas la donitan ŝlosilon «%(fingerprint)s». Tio povus signifi, ke via komunikado estas spionata!", - "verify_nop": "Salutaĵo jam estas kontrolita!", - "verify_nop_warning_mismatch": "AVERTO: Salutaĵo jam estas kontrolita, sed la ŝlosiloj NE AKORDAS!", - "verify_success_description": "La subskriba ŝlosilo, kiun vi donis, akordas la subskribas ŝlosilon, kinu vi ricevis de la salutaĵo %(deviceId)s de la uzanto %(userId)s. Salutaĵo estis markita kontrolita.", - "verify_success_title": "Kontrolita ŝlosilo", - "verify_unknown_pair": "Nekonata (uzanto, salutaĵo) duopo: (%(userId)s, %(deviceId)s)", "whois": "Montras informojn pri uzanto" }, "space": { @@ -2208,10 +2189,6 @@ }, "error_no_renderer": "Ĉi tiu okazo ne povis montriĝi", "error_rendering_message": "Ne povas enlegi ĉi tiun mesaĝon", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s finis <a>voĉan elsendon</a>", - "you": "Vi finis <a>voĉan elsendon</a>" - }, "io.element.widgets.layout": "%(senderName)s ĝisdatigis la aranĝon de ĉambro", "load_error": { "no_permission": "Provis enlegi certan parton de ĉi tiu historio, sed vi ne havas permeson vidi ĝin.", @@ -2626,32 +2603,6 @@ "switch_theme_dark": "Ŝalti malhelan reĝimon", "switch_theme_light": "Ŝalti helan reĝimon" }, - "voice_broadcast": { - "30s_backward": "30s. reen", - "30s_forward": "30s. antaŭen", - "action": "Voĉan elsendo", - "confirm_listen_affirm": "Jes, ĉesigu mian registradon", - "confirm_listen_description": "Se vi komencas aŭskulti ĉi tiun vivan elsendon, via nuna viva elsendo registrado estos finita.", - "confirm_listen_title": "Aŭskulti vivan elsendon?", - "confirm_stop_affirm": "Jes, ĉesu elsendon", - "confirm_stop_description": "Ĉu vi certas, ke vi volas fini la elsendon? Ĉi tio finos la transdonon kaj provizos la plenan registradon en la ĉambro.", - "confirm_stop_title": "Ĉu ĉesi rekta elsendo?", - "failed_already_recording_description": "Vi jam registras voĉan elsendon. Bonvolu fini vian nunan voĉelsendon por komenci novan.", - "failed_already_recording_title": "Ne povas komenci novan voĉan elsendon", - "failed_decrypt": "Ne eblas deĉifri voĉan elsendon", - "failed_generic": "Ne eblas ludi ĉi tiun voĉan elsendon", - "failed_insufficient_permission_description": "Vi ne havas la bezonatajn permesojn por komenci voĉan elsendon en ĉi tiu ĉambro. Kontaktu ĉambran administranton por ĝisdatigi viajn permesojn.", - "failed_insufficient_permission_title": "Ne povas komenci novan voĉan elsendon", - "failed_no_connection_description": "Bedaŭrinde ni ne povas komenci registradon nun. Bonvolu reprovi poste.", - "failed_no_connection_title": "eraro de konekto", - "failed_others_already_recording_description": "Iu alia jam registras voĉan elsendon. Atendu, ke ilia voĉa elsendo finiĝos por komenci novan.", - "failed_others_already_recording_title": "Ne povas komenci novan voĉan elsendon", - "go_live": "Iru vivi", - "live": "Vivi", - "pause": "paŭzi voĉan elsendon", - "play": "ludu voĉan elsendon", - "resume": "rekomenci voĉan elsendon" - }, "voip": { "already_in_call": "Jam vokanta", "already_in_call_person": "Vi jam vokas ĉi tiun personon.", @@ -2671,7 +2622,6 @@ "camera_disabled": "Via filmilo estas malŝaltita", "camera_enabled": "Via filmilo ankoraŭ estas ŝaltita", "cannot_call_yourself_description": "Vi ne povas voki vin mem.", - "change_input_device": "Ŝanĝu enigan aparaton", "connecting": "Konektante", "connection_lost": "Konektebleco al la servilo estas perdita", "connection_lost_description": "Vi ne povas voki sen konektaĵo al la servilo.", @@ -2683,8 +2633,6 @@ "enable_camera": "Ŝalti la filmilon", "enable_microphone": "Malsilentigi la mikrofonon", "expand": "Reveni al voko", - "failed_call_live_broadcast_description": "Vi ne povas komenci vokon ĉar vi nuntempe registras vivan elsendon. Bonvolu fini vian vivan elsendon por komenci vokon.", - "failed_call_live_broadcast_title": "Ne povas komenci vokon", "hangup": "Fini vokon", "hide_sidebar_button": "Kaŝi flankan breton", "join_button_tooltip_connecting": "Konektante", diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json index cb6a8557b3..0f54ccca70 100644 --- a/src/i18n/strings/es.json +++ b/src/i18n/strings/es.json @@ -813,7 +813,6 @@ }, "udd": { "interactive_verification_button": "Verificar interactivamente usando emojis", - "manual_verification_button": "Verificar manualmente usando un texto", "other_ask_verify_text": "Pídele al usuario que verifique su sesión, o verifícala manualmente a continuación.", "other_new_session_text": "%(name)s (%(userId)s) inició una nueva sesión sin verificarla:", "own_ask_verify_text": "Verifica la otra sesión utilizando una de las siguientes opciones.", @@ -848,12 +847,6 @@ "incoming_sas_dialog_waiting": "Esperando a que la otra persona confirme…", "incoming_sas_user_dialog_text_1": "Verifica a este usuario para marcarlo como de confianza. Confiar en usuarios aporta tranquilidad en los mensajes cifrados de extremo a extremo.", "incoming_sas_user_dialog_text_2": "Verificar este usuario marcará su sesión como de confianza, y también marcará tu sesión como de confianza para él.", - "manual_device_verification_device_id_label": "ID de Sesión", - "manual_device_verification_device_key_label": "Código de sesión", - "manual_device_verification_device_name_label": "Nombre de sesión", - "manual_device_verification_footer": "Si no coinciden, la seguridad de su comunicación puede estar comprometida.", - "manual_device_verification_self_text": "Confirme comparando lo siguiente con los ajustes de usuario de su otra sesión:", - "manual_device_verification_user_text": "Confirma la sesión de este usuario comparando lo siguiente con su configuración:", "no_key_or_device": "Parece que no tienes una clave de seguridad u otros dispositivos para la verificación. Este dispositivo no podrá acceder los mensajes cifrados antiguos. Para verificar tu identidad en este dispositivo, tendrás que restablecer tus claves de verificación.", "no_support_qr_emoji": "El dispositivo que estás intentando verificar no es compatible con el escaneo de códigos QR o la verificación con emojis, que son las opciones que %(brand)s ofrece. Prueba con otra aplicación distinta.", "other_party_cancelled": "El otro lado canceló la verificación.", @@ -1304,7 +1297,6 @@ "video_rooms_faq1_question": "Cómo crear una sala de vídeo", "video_rooms_faq2_answer": "Sí, el historial de la sala aparece al lado del vídeo.", "video_rooms_faq2_question": "¿Puedo mandar mensajes de texto en la videollamada?", - "voice_broadcast": "Retransmisión de voz", "wysiwyg_composer": "Editor de texto enriquecido" }, "labs_mjolnir": { @@ -1955,7 +1947,6 @@ "error_unbanning": "No se pudo quitar veto", "events_default": "Enviar mensajes", "invite": "Invitar usuarios", - "io.element.voice_broadcast_info": "Retransmisiones de voz", "kick": "Sacar usuarios", "m.call": "Empezar llamadas de %(brand)s", "m.call.member": "Unirte a llamadas de %(brand)s", @@ -2529,7 +2520,6 @@ "warning": "<w>ADVERTENCIA :</w> <description/>" }, "share": { - "link_title": "Enlace a la sala", "permalink_message": "Enlazar al mensaje seleccionado", "permalink_most_recent": "Enlazar al mensaje más reciente", "title_message": "Compartir un mensaje de esta sala", @@ -2612,13 +2602,6 @@ "upgraderoom": "Actualiza una sala a una nueva versión", "upgraderoom_permission_error": "No tienes los permisos requeridos para usar este comando.", "usage": "Uso", - "verify": "Verifica a un usuario, sesión y tupla de clave pública", - "verify_mismatch": "¡ATENCIÓN: LA VERIFICACIÓN DE LA CLAVE HA FALLADO! La clave de firma para %(userId)s y sesión %(deviceId)s es \"%(fprint)s\", la cual no coincide con la clave proporcionada \"%(fingerprint)s\". ¡Esto podría significar que tus comunicaciones están siendo interceptadas!", - "verify_nop": "¡La sesión ya ha sido verificada!", - "verify_nop_warning_mismatch": "ADVERTENCIA: la sesión ya está verificada, pero las claves NO COINCIDEN", - "verify_success_description": "La clave de firma que proporcionaste coincide con la clave de firma que recibiste de la sesión %(deviceId)s de %(userId)s. Sesión marcada como verificada.", - "verify_success_title": "Clave verificada", - "verify_unknown_pair": "Pareja (usuario, sesión) desconocida: (%(userId)s, %(deviceId)s)", "whois": "Muestra información sobre un usuario" }, "space": { @@ -3336,29 +3319,6 @@ "switch_theme_dark": "Cambiar al tema oscuro", "switch_theme_light": "Cambiar al tema claro" }, - "voice_broadcast": { - "30s_backward": "retroceder 30s", - "30s_forward": "avanzar 30s", - "action": "Retransmisión de voz", - "buffering": "Cargando…", - "confirm_listen_affirm": "Sí, terminar grabación", - "confirm_stop_affirm": "Sí, detener retransmisión", - "confirm_stop_title": "¿Dejar de retransmitir?", - "connection_error": "Error de conexión, grabación detenida", - "failed_already_recording_title": "No se ha podido iniciar una nueva difusión de voz", - "failed_insufficient_permission_title": "No se ha podido iniciar una nueva difusión de voz", - "failed_no_connection_description": "Lamentablemente, no hemos podido empezar a grabar ahora mismo. Inténtalo de nuevo más tarde.", - "failed_no_connection_title": "Error de conexión", - "failed_others_already_recording_title": "No se ha podido iniciar una nueva difusión de voz", - "go_live": "Empezar directo", - "live": "En directo", - "pause": "pausar retransmisión de voz", - "play": "reproducir difusión de voz", - "resume": "reanudar retransmisión de voz" - }, - "voice_message": { - "cant_start_broadcast_title": "No se ha podido empezar el mensaje de voz" - }, "voip": { "already_in_call": "Ya en una llamada", "already_in_call_person": "Ya estás en una llamada con esta persona.", @@ -3378,7 +3338,6 @@ "camera_disabled": "Tu cámara está apagada", "camera_enabled": "Tu cámara todavía está encendida", "cannot_call_yourself_description": "No puedes llamarte a ti mismo.", - "change_input_device": "Cambiar dispositivo de entrada", "connecting": "Conectando", "connection_lost": "Se ha perdido la conexión con el servidor", "connection_lost_description": "No puedes llamar porque no hay conexión con el servidor.", @@ -3395,8 +3354,6 @@ "enable_camera": "Encender cámara", "enable_microphone": "Activar micrófono", "expand": "Volver a la llamada", - "failed_call_live_broadcast_description": "No puedes empezar una llamada, porque estás grabando una retransmisión en directo. Por favor, finaliza tu retransmisión en directo para empezar la llamada.", - "failed_call_live_broadcast_title": "No se ha podido empezar la llamada", "hangup": "Colgar", "hide_sidebar_button": "Ocultar menú lateral", "input_devices": "Dispositivos de entrada", diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index b05d9024c0..093dcf861e 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -880,7 +880,6 @@ }, "udd": { "interactive_verification_button": "Verifitseeri interaktiivselt emoji abil", - "manual_verification_button": "Verifitseeri käsitsi etteantud teksti abil", "other_ask_verify_text": "Palu nimetatud kasutajal verifitseerida see sessioon või tee seda alljärgnevaga käsitsi.", "other_new_session_text": "%(name)s (%(userId)s) logis sisse uude sessiooni ilma seda verifitseerimata:", "own_ask_verify_text": "Verifitseeri oma teine sessioon kasutades üht alljärgnevatest võimalustest.", @@ -915,12 +914,6 @@ "incoming_sas_dialog_waiting": "Ootan teise osapoole kinnitust…", "incoming_sas_user_dialog_text_1": "Selle kasutaja usaldamiseks peaksid ta verifitseerima. Kui sa pruugid läbivalt krüptitud sõnumeid, siis kasutajate verifitseerimine tagab sulle täiendava meelerahu.", "incoming_sas_user_dialog_text_2": "Selle kasutaja verifitseerimisel märgitakse tema sessioon usaldusväärseks ning samuti märgitakse sinu sessioon tema jaoks usaldusväärseks.", - "manual_device_verification_device_id_label": "Sessiooni tunnus", - "manual_device_verification_device_key_label": "Sessiooni võti", - "manual_device_verification_device_name_label": "Sessiooni nimi", - "manual_device_verification_footer": "Kui nad omavahel ei klapi, siis teie suhtluse turvalisus võib olla ohus.", - "manual_device_verification_self_text": "Kinnita seda võrreldes järgnevaid andmeid oma teise sessiooni kasutajaseadetes:", - "manual_device_verification_user_text": "Kinnita selle kasutaja sessioon võrreldes järgnevaid andmeid tema kasutajaseadetes:", "no_key_or_device": "Tundub, et sul ei ole ei turvavõtit ega muid seadmeid, mida saaksid verifitseerimiseks kasutada. Siin seadmes ei saa lugeda vanu krüptitud sõnumeid. Enda tuvastamiseks selles seadmed pead oma vanad verifitseerimisvõtmed kustutama.", "no_support_qr_emoji": "See seade, mida sa tahad verifitseerida ei toeta QR-koodi ega emoji-põhist verifitseerimist, aga just neid %(brand)s oskab kasutada. Proovi mõne muu Matrix'i kliendiga.", "other_party_cancelled": "Teine osapool tühistas verifitseerimise.", @@ -1043,10 +1036,6 @@ }, "error_user_not_logged_in": "Kasutaja pole võrku loginud", "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s lõpetas ringhäälingukõne", - "you": "Sa lõpetasid ringhäälingukõne" - }, "m.call.answer": { "dm": "Kõne on pooleli", "user": "%(senderName)s liitus kõnega", @@ -1415,8 +1404,6 @@ "video_rooms_faq1_question": "Kuidas ma saan luua videotoa?", "video_rooms_faq2_answer": "Jah, tekstivestluse ajajoon on kuvatud videovaate kõrval.", "video_rooms_faq2_question": "Kas ma saan videokõne ajal ka tekstisõnumeid saata?", - "voice_broadcast": "Ringhäälingukõne", - "voice_broadcast_force_small_chunks": "Kasuta ringhäälingusõnumi puhul 15-sekundilist blokipikkust", "wysiwyg_composer": "Kujundatud teksti toimeti" }, "labs_mjolnir": { @@ -1556,7 +1543,6 @@ "mute_description": "Sa ei saa üldse teavitusi" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s alustas ringhäälingukõnet", "m.key.verification.request": "%(name)s soovib verifitseerimist" }, "onboarding": { @@ -2112,7 +2098,6 @@ "error_unbanning": "Ligipääsu taastamine ei õnnestunud", "events_default": "Saada sõnumeid", "invite": "Kutsu kasutajaid", - "io.element.voice_broadcast_info": "Ringhäälingukõned", "kick": "Eemalda kasutajaid", "m.call": "Alusta helistamist %(brand)s abil", "m.call.member": "Liitu %(brand)s kõnedega", @@ -2744,7 +2729,6 @@ "warning": "<w>HOIATUS:</w> <description/>" }, "share": { - "link_title": "Link jututoale", "permalink_message": "Viide valitud sõnumile", "permalink_most_recent": "Viide kõige viimasele sõnumile", "title_message": "Jaga jututoa sõnumit", @@ -2831,13 +2815,6 @@ "upgraderoom": "Uuendab jututoa uue versioonini", "upgraderoom_permission_error": "Sul ei ole piisavalt õigusi selle käsu käivitamiseks.", "usage": "Kasutus", - "verify": "Verifitseerib kasutaja, sessiooni ja avalikud võtmed", - "verify_mismatch": "HOIATUS: VÕTMETE VERIFITSEERIMINE EI ÕNNESTUNUD! Kasutaja %(userId)s ja sessiooni %(deviceId)s allkirjastamise võti on „%(fprint)s“, aga see ei vasta antud sõrmejäljele „%(fingerprint)s“. See võib tähendada, et sinu kasutatavad ühendused võivad olla kolmanda osapoole poolt vahelt lõigatud!", - "verify_nop": "Sessioon on juba verifitseeritud!", - "verify_nop_warning_mismatch": "HOIATUS: Sessioon on juba verifitseeritud, aga võtmed ei klapi!", - "verify_success_description": "Sinu antud allkirjavõti vastab allkirjavõtmele, mille sa said kasutaja %(userId)s sessioonist %(deviceId)s. Sessioon on märgitud verifitseerituks.", - "verify_success_title": "Verifitseeritud võti", - "verify_unknown_pair": "Tundmatu kasutaja ja sessiooni kombinatsioon: (%(userId)s, %(deviceId)s)", "view": "Vaata sellise aadressiga jututuba", "whois": "Näitab teavet kasutaja kohta" }, @@ -3049,10 +3026,6 @@ "error_rendering_message": "Selle sõnumi laadimine ei õnnestu", "historical_messages_unavailable": "Sa ei saa näha varasemaid sõnumeid", "in_room_name": " <strong>%(room)s</strong> jututoas", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s lõpetas <a>ringhäälingukõne</a>", - "you": "Sa lõpetasid <a>ringhäälingukõne</a>" - }, "io.element.widgets.layout": "%(senderName)s on uuendanud jututoa välimust", "load_error": { "no_permission": "Üritasin laadida teatud hetke selle jututoa ajajoonelt, kuid sul ei ole õigusi selle sõnumi nägemiseks.", @@ -3580,38 +3553,6 @@ "switch_theme_dark": "Kasuta tumedat teemat", "switch_theme_light": "Kasuta heledat teemat" }, - "voice_broadcast": { - "30s_backward": "30s tagasi", - "30s_forward": "30s edasi", - "action": "Ringhäälingukõne", - "buffering": "Andmed on puhverdamisel…", - "confirm_listen_affirm": "Jah, lõpeta salvestamine", - "confirm_listen_description": "Kui hakkad kuulama seda ringhäälingukõnet, siis hetkel toimuv ringhäälingukõne salvestamine lõppeb.", - "confirm_listen_title": "Kas soovid kuulata ringhäälingukõnet?", - "confirm_stop_affirm": "Jah, lõpeta", - "confirm_stop_description": "Kas sa oled kindel, et soovid otseeetri lõpetada? Sellega ringhäälingukõne salvestamine lõppeb ja salvestis on kättesaadav kõigile jututoas.", - "confirm_stop_title": "Kas lõpetame otseeetri?", - "connection_error": "Viga võrguühenduses - salvestamine on peatatud", - "failed_already_recording_description": "Sa juba salvestad ringhäälingukõnet. Uue alustamiseks palun lõpeta eelmine salvestus.", - "failed_already_recording_title": "Uue ringhäälingukõne alustamine pole võimalik", - "failed_decrypt": "Ringhäälingukõne dekrüptimine ei õnnestu", - "failed_generic": "Selle ringhäälingukõne esitamine ei õnnestu", - "failed_insufficient_permission_description": "Sul pole piisavalt õigusi selles jututoas ringhäälingukõne algatamiseks. Õiguste lisamiseks palun võta ühendust jututoa haldajaga.", - "failed_insufficient_permission_title": "Uue ringhäälingukõne alustamine pole võimalik", - "failed_no_connection_description": "Kahjuks me ei saa hetkel salvestamist alustada. Palun proovi hiljem uuesti.", - "failed_no_connection_title": "Ühenduse viga", - "failed_others_already_recording_description": "Keegi juba salvestab ringhäälingukõnet. Uue ringhäälingukõne salvestamiseks palun oota, kuni see teine ringhäälingukõne on lõppenud.", - "failed_others_already_recording_title": "Uue ringhäälingukõne alustamine pole võimalik", - "go_live": "Alusta otseeetrit", - "live": "Otseeeter", - "pause": "peata ringhäälingukõne", - "play": "esita ringhäälingukõnet", - "resume": "jätka ringhäälingukõnet" - }, - "voice_message": { - "cant_start_broadcast_description": "Kuna sa hetkel salvestad ringhäälingukõnet, siis häälsõnumi salvestamine või esitamine ei õnnestu. Selleks palun lõpeta ringhäälingukõne.", - "cant_start_broadcast_title": "Häälsõnumi salvestamine või esitamine ei õnnestu" - }, "voip": { "already_in_call": "Kõne on juba pooleli", "already_in_call_person": "Sinul juba kõne käsil selle osapoolega.", @@ -3631,7 +3572,6 @@ "camera_disabled": "Sinu seadme kaamera on välja lülitatud", "camera_enabled": "Sinu seadme kaamera on jätkuvalt kasutusel", "cannot_call_yourself_description": "Sa ei saa iseendale helistada.", - "change_input_device": "Vaheta sisendseadet", "connecting": "Kõne on ühendamisel", "connection_lost": "Ühendus sinu serveriga on katkenud", "connection_lost_description": "Kui ühendus sinu serveriga on katkenud, siis sa ei saa helistada.", @@ -3648,8 +3588,6 @@ "enable_camera": "Lülita kaamera sisse", "enable_microphone": "Eemalda mikrofoni summutamine", "expand": "Pöördu tagasi kõne juurde", - "failed_call_live_broadcast_description": "Kuna sa hetkel salvestad ringhäälingukõnet, siis tavakõne algatamine ei õnnestu. Kõne alustamiseks palun lõpeta ringhäälingukõne.", - "failed_call_live_broadcast_title": "Kõne algatamine ei õnnestu", "hangup": "Katkesta kõne", "hide_sidebar_button": "Peida külgpaan", "input_devices": "Sisendseadmed", diff --git a/src/i18n/strings/fa.json b/src/i18n/strings/fa.json index 5541bbbfbd..4fc031ac43 100644 --- a/src/i18n/strings/fa.json +++ b/src/i18n/strings/fa.json @@ -654,12 +654,6 @@ "incoming_sas_dialog_title": "درخواست تأیید دریافتی", "incoming_sas_user_dialog_text_1": "این کاربر را تأیید کنید تا به عنوان کاربر مورد اعتماد علامتگذاری شود. اعتماد به کاربران آرامش و اطمینان بیشتری به شما در استفاده از رمزنگاری سرتاسر میدهد.", "incoming_sas_user_dialog_text_2": "با تأیید این کاربر ، نشست وی به عنوان مورد اعتماد علامتگذاری شده و همچنین نشست شما به عنوان مورد اعتماد برای وی علامتگذاری خواهد شد.", - "manual_device_verification_device_id_label": "شناسهی نشست", - "manual_device_verification_device_key_label": "کلید نشست", - "manual_device_verification_device_name_label": "نام نشست", - "manual_device_verification_footer": "اگر آنها مطابقت نداشتهباشند ، ممکن است امنیت ارتباطات شما به خطر افتاده باشد.", - "manual_device_verification_self_text": "از طریق مقایسهی این با تنظیمات کاربری در نشستهای دیگرتان، تائيد کنید:", - "manual_device_verification_user_text": "این نشست کاربر را از طریق مقایسهی این با تنظیمات کاربری تائيد کنید:", "other_party_cancelled": "طرف مقابل فرآیند تائید را لغو کرد.", "prompt_encrypted": "برای اطمینان از امنیت اتاق، هویت همهی کاربران حاضر در اتاق را تأیید کنید.", "prompt_self": "از اعلان دوباره تأیید را شروع کنید.", @@ -981,8 +975,7 @@ "latex_maths": "نمایش لاتکس ریاضیات در پیامها", "leave_beta": "ترک نسخهی بتا", "video_rooms": "اتاق های تصویری", - "video_rooms_a_new_way_to_chat": "راهکار جدیدی برای گفتگوی صوتی و تصویری در%(brand)sوجود دارد.", - "voice_broadcast": "صدای جمعی" + "video_rooms_a_new_way_to_chat": "راهکار جدیدی برای گفتگوی صوتی و تصویری در%(brand)sوجود دارد." }, "labs_mjolnir": { "advanced_warning": "⚠ این تنظیمات برای کاربران حرفهای قرار داده شدهاست.", @@ -1073,7 +1066,6 @@ "mentions_keywords": "منشن ها و کلمات کلیدی" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s یک پخش صوتی را شروع کرد", "m.key.verification.request": "%(name)s درخواست تائید دارد" }, "onboarding": { @@ -1806,12 +1798,6 @@ "upgraderoom": "یک اتاق را به نسخه جدید ارتقا دهید", "upgraderoom_permission_error": "شما مجوزهای لازم را برای استفاده از این دستور ندارید.", "usage": "استفاده", - "verify": "یک کاربر، نشست و عبارت کلید عمومی را تائید میکند", - "verify_mismatch": "هشدار: تایید کلید ناموفق بود! کلید امضا کننده %(userId)s در نشست %(deviceId)s برابر %(fprint)s است که با کلید %(fingerprint)s تطابق ندارد. این می تواند به معنی رهگیری ارتباطات شما باشد!", - "verify_nop": "نشست پیش از این تائید شدهاست!", - "verify_success_description": "کلید امضای ارائه شده با کلید امضای دریافت شده از جلسه %(deviceId)s کاربر %(userId)s مطابقت دارد. نشست به عنوان تأیید شده علامت گذاری شد.", - "verify_success_title": "کلید تأیید شده", - "verify_unknown_pair": "دوتایی (کاربر و نشست) ناشناخته : ( %(userId)sو%(deviceId)s )", "whois": "اطلاعات مربوط به کاربر را نمایش می دهد" }, "space": { @@ -2312,22 +2298,6 @@ "switch_theme_dark": "انتخاب حالت تاریک", "switch_theme_light": "انتخاب حالت روشن" }, - "voice_broadcast": { - "action": "صدای جمعی", - "confirm_stop_affirm": "بله، توقف ارسال جمعی", - "confirm_stop_title": "آیا ارسال جمعی زنده متوقف شود؟", - "failed_already_recording_description": "شما در حال ضبط یک صدا برای ارسال جمعی هستید. برای تولید یک صدای جمعی دیگر ضبط فعلی را متوقف نمایید.", - "failed_already_recording_title": "امکان ارسال یک صدای جدید به صورت جمعی نیست", - "failed_insufficient_permission_description": "شما دسترسی لازم برای ارسال صدای جمعی در این اتاق را ندارید. لطفا با مدیر اتاق تماس بگیرید.", - "failed_insufficient_permission_title": "امکان ارسال یک صدای جدید به صورت جمعی نیست", - "failed_others_already_recording_description": "شخص دیگری در حال ضبط صدا برای ارسال جمعی است. برای ارسال صدای جمعی باید منتظر بمانید تا کار ایشان به پایان برسد.", - "failed_others_already_recording_title": "امکان ارسال یک صدای جدید به صورت جمعی نیست", - "go_live": "برو به زنده", - "live": "زنده", - "pause": "توقف صدای جمعی", - "play": "پخش صدای جمعی", - "resume": "بازگشت به صدای جمعی" - }, "voip": { "already_in_call": "هماکنون در تماس هستید", "already_in_call_person": "شما هماکنون با این فرد در تماس هستید.", diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index 091761af4b..48fc132f00 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -778,7 +778,6 @@ }, "udd": { "interactive_verification_button": "Vahvista vuorovaikutteisesti emojilla", - "manual_verification_button": "Vahvista manuaalisesti tekstillä", "other_ask_verify_text": "Pyydä tätä käyttäjää vahvistamaan istuntonsa, tai vahvista se manuaalisesti alla.", "other_new_session_text": "%(name)s (%(userId)s) kirjautui uudella istunnolla varmentamatta sitä:", "own_ask_verify_text": "Varmenna toinen istuntosi käyttämällä yhtä seuraavista tavoista.", @@ -807,10 +806,6 @@ "incoming_sas_dialog_title": "Saapuva varmennuspyyntö", "incoming_sas_user_dialog_text_1": "Varmenna tämä käyttäjä merkitäksesi hänet luotetuksi. Käyttäjiin luottaminen antaa sinulle ylimääräistä mielenrauhaa käyttäessäsi päästä päähän -salausta.", "incoming_sas_user_dialog_text_2": "Tämän käyttäjän varmentaminen merkitsee hänen istuntonsa luotetuksi, ja myös merkkaa sinun istuntosi luotetuksi hänen laitteissaan.", - "manual_device_verification_device_id_label": "Istuntotunniste", - "manual_device_verification_device_key_label": "Istunnon tunnus", - "manual_device_verification_device_name_label": "Istunnon nimi", - "manual_device_verification_footer": "Jos ne eivät täsmää, viestinnän turvallisuus saattaa olla vaarantunut.", "other_party_cancelled": "Toinen osapuoli perui varmennuksen.", "prompt_encrypted": "Varmenna kaikki huoneen käyttäjät varmistaaksesi, että se on turvallinen.", "prompt_self": "Aloita varmennus uudelleen ilmoituksesta.", @@ -917,10 +912,6 @@ }, "error_user_not_logged_in": "Käyttäjä ei ole sisäänkirjautunut", "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s lopetti äänen yleislähetyksen", - "you": "Lopetit äänen yleislähetyksen" - }, "m.call.answer": { "dm": "Puhelu käynnissä", "user": "%(senderName)s liittyi puheluun", @@ -1239,8 +1230,7 @@ "video_rooms_faq1_answer": "Käytä ”+”-painiketta vasemman paneelin huoneosiossa.", "video_rooms_faq1_question": "Miten voin luoda videohuoneen?", "video_rooms_faq2_answer": "Kyllä, keskustelun aikajana esitetään videon yhteydessä.", - "video_rooms_faq2_question": "Voinko käyttää tekstikeskustelua videopuhelussa?", - "voice_broadcast": "Äänen yleislähetys" + "video_rooms_faq2_question": "Voinko käyttää tekstikeskustelua videopuhelussa?" }, "labs_mjolnir": { "advanced_warning": "⚠ Nämä asetukset on tarkoitettu edistyneille käyttäjille.", @@ -1356,7 +1346,6 @@ "mute_description": "Et saa ilmoituksia" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s aloitti äänen yleislähetyksen", "m.key.verification.request": "%(name)s pyytää varmennusta" }, "onboarding": { @@ -1856,7 +1845,6 @@ "error_unbanning": "Porttikiellon poistaminen epäonnistui", "events_default": "Lähetä viestejä", "invite": "Kutsu käyttäjiä", - "io.element.voice_broadcast_info": "Äänen yleislähetykset", "kick": "Poista käyttäjiä", "m.call": "Aloita %(brand)s-puheluja", "m.call.member": "Liity %(brand)s-puheluihin", @@ -2415,7 +2403,6 @@ "warning": "<w>VAROITUS:</w> <description/>" }, "share": { - "link_title": "Linkitä huoneeseen", "permalink_message": "Linkitä valittuun viestiin", "permalink_most_recent": "Linkitä viimeisimpään viestiin", "title_message": "Jaa huoneviesti", @@ -2499,13 +2486,6 @@ "upgraderoom": "Päivittää huoneen uuteen versioon", "upgraderoom_permission_error": "Sinulla ei ole vaadittavia oikeuksia tämän komennon käyttämiseksi.", "usage": "Käyttö", - "verify": "Varmentaa käyttäjän, istunnon ja julkiset avaimet", - "verify_mismatch": "VAROITUS: AVAIMEN VARMENTAMINEN EPÄONNISTUI! Käyttäjän %(userId)s ja laitteen %(deviceId)s istunnon allekirjoitusavain on ”%(fprint)s”, mikä ei täsmää annettuun avaimeen ”%(fingerprint)s”. Tämä voi tarkoittaa, että viestintäänne siepataan!", - "verify_nop": "Istunto on jo vahvistettu!", - "verify_nop_warning_mismatch": "VAROITUS: istunto on jo vahvistettu, mutta avaimet EIVÄT TÄSMÄÄ!", - "verify_success_description": "Antamasi allekirjoitusavain täsmää käyttäjältä %(userId)s saamaasi istunnon %(deviceId)s allekirjoitusavaimeen. Istunto on varmennettu.", - "verify_success_title": "Varmennettu avain", - "verify_unknown_pair": "Tuntematon (käyttäjä, laite) (%(userId)s, %(deviceId)s)", "whois": "Näyttää tietoa käyttäjästä" }, "space": { @@ -2710,10 +2690,6 @@ "error_rendering_message": "Tätä viestiä ei voi ladata", "historical_messages_unavailable": "Et voi nähdä aiempia viestejä", "in_room_name": " huoneessa <strong>%(room)s</strong>", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s lopetti <a>äänen yleislähetyksen</a>", - "you": "Lopetit <a>äänen yleislähetyksen</a>" - }, "io.element.widgets.layout": "%(senderName)s on päivittänyt huoneen asettelun", "load_error": { "no_permission": "Aikajanan tietty hetki yritettiin ladata, mutta sinulla ei ole oikeutta nähdä kyseistä viestiä.", @@ -3202,23 +3178,6 @@ "switch_theme_dark": "Vaihda tummaan teemaan", "switch_theme_light": "Vaihda vaaleaan teemaan" }, - "voice_broadcast": { - "30s_backward": "30 s taaksepäin", - "30s_forward": "30 s eteenpäin", - "action": "Äänen yleislähetys", - "buffering": "Puskuroidaan…", - "confirm_stop_affirm": "Kyllä, pysäytä yleislähetys", - "confirm_stop_title": "Pysäytetäänkö liveyleislähetys?", - "failed_already_recording_description": "Tallennat jo äänen yleislähetystä. Lopeta nykyinen äänen yleislähetys aloittaaksesi uuden.", - "failed_already_recording_title": "Uutta äänen yleislähetystä ei voi käynnistää", - "failed_insufficient_permission_title": "Uutta äänen yleislähetystä ei voi käynnistää", - "failed_no_connection_title": "Yhteysvirhe", - "failed_others_already_recording_description": "Joku toinen tallentaa jo äänen yleislähetystä. Odota äänen yleislähetyksen päättymistä, jotta voit aloittaa uuden.", - "failed_others_already_recording_title": "Uutta äänen yleislähetystä ei voi käynnistää", - "pause": "keskeytä äänen yleislähetys", - "play": "toista äänen yleislähetys", - "resume": "palaa äänen yleislähetykseen" - }, "voip": { "already_in_call": "Olet jo puhelussa", "already_in_call_person": "Olet jo puhelussa tämän henkilön kanssa.", @@ -3238,7 +3197,6 @@ "camera_disabled": "Kamerasi on pois päältä", "camera_enabled": "Kamerasi on edelleen päällä", "cannot_call_yourself_description": "Et voi soittaa itsellesi.", - "change_input_device": "Vaihda sisääntulolaitetta", "connecting": "Yhdistetään", "connection_lost": "Yhteys palvelimeen on katkennut", "connection_lost_description": "Et voi soittaa puheluja ilman yhteyttä palvelimeen.", @@ -3254,7 +3212,6 @@ "enable_camera": "Laita kamera päälle", "enable_microphone": "Poista mikrofonin mykistys", "expand": "Palaa puheluun", - "failed_call_live_broadcast_title": "Puhelua ei voi aloittaa", "hangup": "Lopeta", "hide_sidebar_button": "Piilota sivupalkki", "input_devices": "Sisääntulolaitteet", diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 7d209a8a45..08348ba29d 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -229,6 +229,7 @@ }, "misconfigured_body": "Demandez à votre administrateur %(brand)s de vérifier que <a>votre configuration</a> ne contient pas d’entrées incorrectes ou en double.", "misconfigured_title": "Votre %(brand)s est mal configuré", + "mobile_create_account_title": "Vous êtes sur le point de créer un compte sur %(hsName)s", "msisdn_field_description": "D’autres utilisateurs peuvent vous inviter à des salons grâce à vos informations de contact", "msisdn_field_label": "Numéro de téléphone", "msisdn_field_number_invalid": "Ce numéro de téléphone ne semble pas correct, merci de vérifier et réessayer", @@ -279,6 +280,8 @@ "security_code": "Code de sécurité", "security_code_prompt": "Si vous y êtes invité, saisissez le code ci-dessous sur votre autre appareil.", "select_qr_code": "Sélectionnez « %(scanQRCode)s »", + "unsupported_explainer": "Votre fournisseur de compte ne prend pas en charge la connexion à un nouvel appareil à l’aide d’un code QR.", + "unsupported_heading": "Le code QR n'est pas pris en charge", "waiting_for_device": "En attente de connexion de l’appareil" }, "register_action": "Créer un compte", @@ -367,6 +370,8 @@ "email_resend_prompt": "Vous ne l’avez pas reçu ? <a>Le renvoyer</a>", "email_resent": "Ré-envoyé !", "fallback_button": "Commencer l’authentification", + "mas_cross_signing_reset_cta": "Accédez à votre compte", + "mas_cross_signing_reset_description": "Réinitialisez votre identité par l’intermédiaire de votre fournisseur de compte, puis revenez et cliquez sur « Réessayer ».", "msisdn": "Un message a été envoyé à %(msisdn)s", "msisdn_token_incorrect": "Jeton incorrect", "msisdn_token_prompt": "Merci de saisir le code qu’il contient :", @@ -499,6 +504,7 @@ "matrix": "Matrix", "message": "Message", "message_layout": "Mise en page des messages", + "message_timestamp_invalid": "Horodatage non valide", "microphone": "Micro", "model": "Modèle", "modern": "Moderne", @@ -899,6 +905,8 @@ "warning": "Si vous n’avez pas activé de nouvelle méthode de récupération, un attaquant essaye peut-être d’accéder à votre compte. Changez immédiatement le mot de passe de votre compte et configurez une nouvelle méthode de récupération dans les paramètres." }, "not_supported": "<non pris en charge>", + "pinned_identity_changed": "%(displayName)sL'identité de (<b>%(userId)s</b>) semble avoir changé. <a>En savoir plus </a>", + "pinned_identity_changed_no_displayname": "<b>%(userId)s</b>semble avoir changé d'identité. <a>En savoir plus</a>", "recovery_method_removed": { "description_1": "Cette session a détecté que votre phrase secrète et clé de sécurité pour les messages sécurisés ont été supprimées.", "description_2": "Si vous l’avez fait accidentellement, vous pouvez configurer les messages sécurisés sur cette session ce qui re-chiffrera l’historique des messages de cette session avec une nouvelle méthode de récupération.", @@ -914,7 +922,6 @@ }, "udd": { "interactive_verification_button": "Vérifier de façon interactive avec des émojis", - "manual_verification_button": "Vérifier manuellement avec un texte", "other_ask_verify_text": "Demandez à cet utilisateur de vérifier sa session, ou vérifiez-la manuellement ci-dessous.", "other_new_session_text": "%(name)s (%(userId)s) s’est connecté à une nouvelle session sans la vérifier :", "own_ask_verify_text": "Vérifiez votre autre session en utilisant une des options ci-dessous.", @@ -949,12 +956,6 @@ "incoming_sas_dialog_waiting": "Attente de la confirmation du partenaire…", "incoming_sas_user_dialog_text_1": "Vérifier cet utilisateur pour le marquer comme fiable. Faire confiance aux utilisateurs vous permet d’être tranquille lorsque vous utilisez des messages chiffrés de bout en bout.", "incoming_sas_user_dialog_text_2": "Vérifier cet utilisateur marquera sa session comme fiable, et marquera aussi votre session comme fiable pour lui.", - "manual_device_verification_device_id_label": "Identifiant de session", - "manual_device_verification_device_key_label": "Clé de la session", - "manual_device_verification_device_name_label": "Nom de la session", - "manual_device_verification_footer": "S’ils ne correspondent pas, la sécurité de vos communications est peut-être compromise.", - "manual_device_verification_self_text": "Confirmez en comparant ceci avec les paramètres utilisateurs de votre autre session :", - "manual_device_verification_user_text": "Confirmez la session de cet utilisateur en comparant ceci avec ses paramètres utilisateur :", "no_key_or_device": "Il semblerait que vous n’avez pas de clé de sécurité ou d’autres appareils pour faire la vérification. Cet appareil ne pourra pas accéder aux anciens messages chiffrés. Afin de vérifier votre identité sur cet appareil, vous devrez réinitialiser vos clés de vérifications.", "no_support_qr_emoji": "L’appareil que vous essayez de vérifier ne prend pas en charge les QR codes ou la vérification d’émojis, qui sont les méthodes prises en charge par %(brand)s. Essayez avec un autre client.", "other_party_cancelled": "L’autre personne a annulé la vérification.", @@ -994,7 +995,7 @@ "unverified_sessions_toast_description": "Vérifiez pour assurer la sécurité de votre compte", "unverified_sessions_toast_reject": "Plus tard", "unverified_sessions_toast_title": "Vous avez des sessions non vérifiées", - "verification_description": "Vérifiez votre identité pour accéder aux messages chiffrés et prouver votre identité aux autres.", + "verification_description": "Vérifiez votre identité pour accéder aux messages chiffrés et prouver votre identité aux autres. Si vous utilisez également un appareil mobile, veuillez ouvrir l’application avant de continuer.", "verification_dialog_title_device": "Vérifier un autre appareil", "verification_dialog_title_user": "Demande de vérification", "verification_skip_warning": "Sans vérification, vous n’aurez pas accès à tous vos messages et vous n’apparaîtrez pas comme de confiance aux autres.", @@ -1079,10 +1080,6 @@ }, "error_user_not_logged_in": "L’utilisateur n’est pas identifié", "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s a terminé une diffusion audio", - "you": "Vous avez terminé une diffusion audio" - }, "m.call.answer": { "dm": "Appel en cours", "user": "%(senderName)s a rejoint l’appel", @@ -1104,7 +1101,15 @@ "you": "Vous avez réagi avec %(reaction)s à %(message)s" }, "m.sticker": "%(senderName)s : %(stickerName)s", - "m.text": "%(senderName)s : %(message)s" + "m.text": "%(senderName)s : %(message)s", + "prefix": { + "audio": "Audio", + "file": "Fichier", + "image": "Image", + "poll": "Sondage", + "video": "Vidéo" + }, + "preview": "<bold>%(prefix)s: </bold> %(preview)s" }, "export_chat": { "cancelled": "Export annulé", @@ -1227,7 +1232,19 @@ "other": "Dans %(spaceName)s et %(count)s autres espaces." }, "incompatible_browser": { - "title": "Navigateur non pris en charge" + "continue": "Continuez quand même", + "description": "%(brand)s utilise certaines fonctionnalités du navigateur qui ne sont pas disponibles dans votre navigateur actuel. %(detail)s", + "detail_can_continue": "Si vous continuez, certaines fonctionnalités pourraient cesser de fonctionner et vous risquez de perdre des données à l'avenir.", + "detail_no_continue": "Essayez de mettre à jour ce navigateur si vous n'utilisez pas la dernière version, puis réessayez.", + "learn_more": "En savoir plus", + "linux": "Linux", + "macos": "MAC", + "supported_browsers": "Pour une expérience optimale, utilisez <Chrome>Chrome</Chrome>, <Firefox>Firefox</Firefox>, <Edge>Edge</Edge>, ou <Safari>Safari</Safari>.", + "title": "Navigateur non pris en charge", + "use_desktop_heading": "Utilisez plutôt %(brand)s Desktop", + "use_mobile_heading": "Utilisez plutôt %(brand)s sur votre appareil mobile", + "use_mobile_heading_after_desktop": "Ou utilisez notre application mobile", + "windows": "Windows (%(bits)s-bit)" }, "info_tooltip_title": "Informations", "integration_manager": { @@ -1351,12 +1368,14 @@ "navigate_next_message_edit": "Aller vers le prochain message à modifier", "navigate_prev_history": "Salon ou espace précédemment visité", "navigate_prev_message_edit": "Allez vers le précédent message à modifier", + "next_landmark": "Aller au prochain point de repère", "next_room": "Prochain salon ou conversation privée", "next_unread_room": "Prochain salon ou conversation privée non lu", "number": "[numéro]", "open_user_settings": "Ouvrir les paramètres de l'utilisateur", "page_down": "Page Bas", "page_up": "Page Haut", + "prev_landmark": "Aller au point de repère précédent", "prev_room": "Précédent salon ou conversation privée", "prev_unread_room": "Précédent salon ou conversation privée non lu", "room_list_collapse_section": "Réduire la section de la liste des salons", @@ -1401,8 +1420,11 @@ "dynamic_room_predecessors": "Prédécesseurs de salon dynamique", "dynamic_room_predecessors_description": "Active MSC3946 (pour prendre en charge les archives de salon après création)", "element_call_video_rooms": "Salons vidéo Element Call", + "exclude_insecure_devices": "Exclure les appareils non sécurisés lors de l'envoi/de la réception de messages", + "exclude_insecure_devices_description": "Lorsque ce mode est activé, les messages chiffrés ne seront pas partagés avec des appareils non vérifiés et les messages provenant d'appareils non vérifiés seront affichés comme une erreur. Notez que si vous activez ce mode, il se peut que vous ne puissiez pas communiquer avec les utilisateurs qui n'ont pas vérifié leurs appareils.", "experimental_description": "Envie d’expériences ? Essayez nos dernières idées en développement. Ces fonctionnalités ne sont pas terminées ; elles peuvent changer, être instables, ou être complètement abandonnées. <a>En savoir plus</a>.", "experimental_section": "Avant-premières", + "extended_profiles_msc_support": "Nécessite que votre serveur prenne en charge MSC4133", "feature_disable_call_per_sender_encryption": "Désactiver le chiffrement de chaque expéditeur pour Element Call", "feature_wysiwyg_composer_description": "Utilise le texte formaté au lieu de Markdown dans le compositeur de message.", "group_calls": "Nouvelle expérience d’appel de groupe", @@ -1458,8 +1480,6 @@ "video_rooms_faq2_answer": "Oui, l’historique de conversation est affiché à côté de la vidéo.", "video_rooms_faq2_question": "Est-il possible d’utiliser les messages textuels en même temps que l’appel vidéo ?", "video_rooms_feedbackSubheading": "Merci d’essayer la version bêta. Veuillez l’utiliser au maximum pour que nous puissions l’améliorer.", - "voice_broadcast": "Diffusion audio", - "voice_broadcast_force_small_chunks": "Forcer la diffusion audio à utiliser des morceaux de 15s", "wysiwyg_composer": "Éditeur de texte formaté" }, "labs_mjolnir": { @@ -1605,7 +1625,6 @@ "mute_description": "Vous n’aurez aucune notification" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s a démarré une diffusion audio", "m.key.verification.request": "%(name)s demande une vérification" }, "onboarding": { @@ -1805,8 +1824,12 @@ "right_panel": { "add_integrations": "Ajouter des extensions", "add_topic": "Ajouter un sujet", + "extensions_button": "Extensions", + "extensions_empty_description": "Sélectionnez « %(addIntegrations)s » pour explorer et ajouter des extensions à ce salon", + "extensions_empty_title": "Augmentez votre productivité avec plus d’outils, de widgets et de bots", "files_button": "Fichiers", "pinned_messages": { + "empty_description": "Sélectionnez un message et choisissez « %(pinAction)s » pour l'inclure ici.", "empty_title": "Épingler des messages importants afin qu'ils puissent être facilement découverts", "header": { "one": "1 Message épinglé", @@ -1817,11 +1840,17 @@ }, "menu": "Ouvrir le menu", "release_announcement": { + "close": "Ok", + "description": "Retrouvez tous les messages épinglés ici. Survolez n'importe quel message et sélectionnez « Épingler » pour l'ajouter.", "title": "Tous les nouveaux messages épinglés" }, + "reply_thread": "Répondre à un un <link>message de fil de discussion</link>", "unpin_all": { - "button": "Désépingler tous les messages" - } + "button": "Désépingler tous les messages", + "content": "Assurez-vous que vous voulez vraiment supprimer tous les messages épinglés. Cette action ne peut pas être annulée.", + "title": "Désépingler tous les messages ?" + }, + "view": "Voir dans la discussion" }, "pinned_messages_button": "Messages épinglés", "poll": { @@ -1926,6 +1955,7 @@ }, "room_is_public": "Ce salon est public" }, + "header_avatar_open_settings_label": "Ouvrir les paramètres du salon", "header_face_pile_tooltip": "Personnes", "header_untrusted_label": "Non fiable", "inaccessible": "Ce salon ou cet espace n’est pas accessible en ce moment.", @@ -1996,8 +2026,13 @@ "not_found_title": "Ce salon ou cet espace n’existe pas.", "not_found_title_name": "%(roomName)s n’existe pas.", "peek_join_prompt": "Ceci est un aperçu de %(roomName)s. Voulez-vous rejoindre le salon ?", + "pinned_message_badge": "Message épinglé", "pinned_message_banner": { - "description": "Ce salon contient des messages épinglés. Cliquez pour les consulter." + "button_close_list": "Fermer la liste", + "button_view_all": "Voir tout", + "description": "Ce salon contient des messages épinglés. Cliquez pour les consulter.", + "go_to_message": "Afficher le message épinglé dans la discussion.", + "title": "<bold>%(index)s de %(length)s</bold> messages épinglés" }, "read_topic": "Cliquer pour lire le sujet", "rejecting": "Rejet de l’invitation…", @@ -2005,6 +2040,10 @@ "search": { "all_rooms_button": "Rechercher dans tous les salons", "placeholder": "Rechercher des messages…", + "summary": { + "one": "1 résultat trouvé pour « <query/> »", + "other": "%(count)srésultats trouvés pour « <query/> »" + }, "this_room_button": "Rechercher dans ce salon" }, "status_bar": { @@ -2140,6 +2179,8 @@ "error_deleting_alias_description": "Une erreur est survenue lors de la suppression de cette adresse. Elle n’existe peut-être plus ou une erreur temporaire est survenue.", "error_deleting_alias_description_forbidden": "Vous n’avez pas la permission de supprimer cette adresse.", "error_deleting_alias_title": "Erreur lors de la suppression de l’adresse", + "error_publishing": "Impossible de publier le salon", + "error_publishing_detail": "Une erreur s'est produite lors de la publication du salon", "error_save_space_settings": "Échec de l’enregistrement des paramètres.", "error_updating_alias_description": "Une erreur est survenue lors de la mise à jour des adresses alternatives du salon. Ce n’est peut-être pas permis par le serveur ou une défaillance temporaire est survenue.", "error_updating_canonical_alias_description": "Une erreur est survenue lors de la mise à jour de l’adresse principale de salon. Ce n’est peut-être pas autorisé par le serveur ou une erreur temporaire est survenue.", @@ -2197,7 +2238,6 @@ "error_unbanning": "Échec de la révocation du bannissement", "events_default": "Envoyer des messages", "invite": "Inviter des utilisateurs", - "io.element.voice_broadcast_info": "Diffusions audio", "kick": "Expulser des utilisateurs", "m.call": "Démarrer des appels %(brand)s", "m.call.member": "Rejoindre des appels %(brand)s", @@ -2376,16 +2416,25 @@ } }, "settings": { + "account": { + "dialog_title": "<strong>Paramètres : </strong> Compte", + "title": "Compte" + }, "all_rooms_home": "Afficher tous les salons dans Accueil", "all_rooms_home_description": "Tous les salons dans lesquels vous vous trouvez apparaîtront sur l’Accueil.", "always_show_message_timestamps": "Toujours afficher l’heure des messages", "appearance": { "bundled_emoji_font": "Utilise la police d’émoji interne", + "compact_layout": "Afficher le texte et les messages compacts", + "compact_layout_description": "La mise en page moderne doit être sélectionnée pour utiliser cette fonctionnalité.", "custom_font": "Utiliser une police du système", "custom_font_description": "Définissez le nom d’une police de caractères installée sur votre système et %(brand)s essaiera de l’utiliser.", "custom_font_name": "Nom de la police du système", "custom_font_size": "Utiliser une taille personnalisée", + "custom_theme_add": "Ajouter un thème personnalisé", + "custom_theme_downloading": "Téléchargement du thème personnalisé…", "custom_theme_error_downloading": "Erreur lors du téléchargement du thème", + "custom_theme_help": "Entrez l'URL du thème personnalisé que vous souhaitez appliquer.", "custom_theme_invalid": "Schéma du thème invalide.", "dialog_title": "<strong>Paramètres : </strong> Apparence", "font_size": "Taille de la police", @@ -2405,6 +2454,9 @@ "code_block_expand_default": "Développer les blocs de code par défaut", "code_block_line_numbers": "Afficher les numéros de ligne dans les blocs de code", "disable_historical_profile": "Afficher l’image de profil et le nom actuels des utilisateurs dans l’historique des messages", + "discovery": { + "title": "Comment vous trouver" + }, "emoji_autocomplete": "Activer la suggestion d’émojis lors de la saisie", "enable_markdown": "Activer Markdown", "enable_markdown_description": "Commencez les messages avec <code>/plain</code> pour les envoyer sans markdown.", @@ -2420,10 +2472,14 @@ "add_msisdn_dialog_title": "Ajouter un numéro de téléphone", "add_msisdn_instructions": "Un SMS a été envoyé à +%(msisdn)s. Saisissez le code de vérification qu’il contient.", "add_msisdn_misconfigured": "L’ajout / liaison avec le flux MSISDN est mal configuré", + "allow_spellcheck": "Autoriser la vérification orthographique", + "application_language": "Langue de l'application", "application_language_reload_hint": "L’application se rechargera après avoir sélectionné une autre langue", "avatar_remove_progress": "Suppression de l'image...", "avatar_save_progress": "Chargement de l'image...", + "avatar_upload_error_text": "Le format de fichier n'est pas pris en charge ou l'image est plus grande que%(size)s.", "avatar_upload_error_text_generic": "Le format de fichier n'est peut-être pas pris en charge.", + "avatar_upload_error_title": "L'image de l'avatar n'a pas pu être téléchargée", "confirm_adding_email_body": "Cliquez sur le bouton ci-dessous pour confirmer l’ajout de l’adresse e-mail.", "confirm_adding_email_title": "Confirmer l’ajout de l’adresse e-mail", "deactivate_confirm_body": "Voulez-vous vraiment désactiver votre compte ? Ceci est irréversible.", @@ -2443,6 +2499,7 @@ "discovery_email_verification_instructions": "Vérifiez le lien dans votre boîte de réception", "discovery_msisdn_empty": "Les options de découverte apparaîtront quand vous aurez ajouté un numéro de téléphone ci-dessus.", "discovery_needs_terms": "Acceptez les conditions de service du serveur d’identité (%(serverName)s) pour vous permettre d’être découvrable par votre adresse e-mail ou votre numéro de téléphone.", + "discovery_needs_terms_title": "Laissez les gens vous trouver", "display_name": "Nom d'affichage", "display_name_error": "Impossible de définir le nom d'affichage", "email_address_in_use": "Cette adresse e-mail est déjà utilisée", @@ -2479,10 +2536,13 @@ "password_change_section": "Définir un nouveau mot de passe de compte…", "password_change_success": "Votre mot de passe a été mis à jour.", "personal_info": "Informations personnelles", + "profile_subtitle": "Voici comment vous apparaissez aux autres utilisateurs de l'application.", "profile_subtitle_oidc": "Votre compte est géré séparément par un fournisseur d'identité et certaines de vos informations personnelles ne peuvent donc pas être modifiées ici.", "remove_email_prompt": "Supprimer %(email)s ?", "remove_msisdn_prompt": "Supprimer %(phone)s ?", "spell_check_locale_placeholder": "Choisir une langue", + "unable_to_load_emails": "Impossible de charger les adresses e-mail", + "unable_to_load_msisdns": "Impossible de charger les numéros de téléphone", "username": "Nom d’utilisateur" }, "image_thumbnails": "Afficher les aperçus/vignettes pour les images", @@ -2611,6 +2671,7 @@ "code_blocks_heading": "Blocs de code", "compact_modern": "Utiliser une mise en page « moderne » plus compacte", "composer_heading": "Compositeur", + "default_timezone": "Navigateur par défaut (%(timezone)s)", "dialog_title": "<strong>Paramètres : </strong> Préférences", "enable_hardware_acceleration": "Activer l’accélération matérielle", "enable_tray_icon": "Afficher l’icône dans la barre d’état et minimiser la fenêtre lors de la fermeture", @@ -2618,6 +2679,7 @@ "keyboard_view_shortcuts_button": "Pour voir tous les raccourcis claviers, <a>cliquez ici</a>.", "media_heading": "Images, GIF et vidéos", "presence_description": "Partager votre activité et votre statut avec les autres.", + "publish_timezone": "Publier le fuseau horaire sur le profil public", "rm_lifetime": "Durée de vie du repère de lecture (ms)", "rm_lifetime_offscreen": "Durée de vie du repère de lecture en dehors de l’écran (ms)", "room_directory_heading": "Répertoire des salons", @@ -2626,7 +2688,8 @@ "show_checklist_shortcuts": "Afficher le raccourci vers la liste de vérification de bienvenue au-dessus de la liste des salons", "show_polls_button": "Afficher le bouton des sondages", "surround_text": "Entourer le texte sélectionné lors de la saisie de certains caractères", - "time_heading": "Affichage de l’heure" + "time_heading": "Affichage de l’heure", + "user_timezone": "Définir le fuseau horaire" }, "prompt_invite": "Demander avant d’envoyer des invitations à des identifiants matrix potentiellement non valides", "replace_plain_emoji": "Remplacer automatiquement le texte par des émojis", @@ -2782,6 +2845,7 @@ "sign_in_with_qr": "Associer un nouvel appareil", "sign_in_with_qr_button": "Afficher le QR code", "sign_in_with_qr_description": "Utilisez un code QR pour vous connecter à un autre appareil et configurer votre messagerie sécurisée.", + "sign_in_with_qr_unsupported": "Non pris en charge par votre fournisseur de compte", "sign_out": "Se déconnecter de cette session", "sign_out_all_other_sessions": "Déconnecter toutes les autres sessions (%(otherSessionsCount)s)", "sign_out_confirm_description": { @@ -2872,7 +2936,7 @@ "warning": "<w>ATTENTION :</w> <description/>" }, "share": { - "link_title": "Lien vers le salon", + "link_copied": "Lien copié", "permalink_message": "Lien vers le message sélectionné", "permalink_most_recent": "Lien vers le message le plus récent", "share_call": "Lien d'invitation à la conférence", @@ -2964,13 +3028,6 @@ "upgraderoom": "Met à niveau un salon vers une nouvelle version", "upgraderoom_permission_error": "Vous n’avez pas les autorisations nécessaires pour utiliser cette commande.", "usage": "Utilisation", - "verify": "Vérifie un utilisateur, une session et une collection de clés publiques", - "verify_mismatch": "ATTENTION : ÉCHEC DE LA VÉRIFICATION DE CLÉ ! La clé de signature pour %(userId)s et la session %(deviceId)s est « %(fprint)s ce qui ne correspond pas à la clé fournie « %(fingerprint)s ». Cela pourrait signifier que vos communications sont interceptées !", - "verify_nop": "Session déjà vérifiée !", - "verify_nop_warning_mismatch": "ATTENTION : session déjà vérifiée, mais les clés ne CORRESPONDENT PAS !", - "verify_success_description": "La clé de signature que vous avez fournie correspond à celle que vous avez reçue de la session %(deviceId)s de %(userId)s. Session marquée comme vérifiée.", - "verify_success_title": "Clé vérifiée", - "verify_unknown_pair": "Paire (utilisateur, session) inconnue : (%(userId)s, %(deviceId)s)", "view": "Affiche le salon avec cette adresse", "whois": "Affiche des informations à propos de l’utilisateur" }, @@ -3188,6 +3245,8 @@ "historical_event_no_key_backup": "L'historique des messages n'est pas disponible sur cet appareil", "historical_event_unverified_device": "Vous devez vérifier cet appareil pour accéder à l'historique des messages", "historical_event_user_not_joined": "Vous n'avez pas accès à ce message", + "sender_identity_previously_verified": "L'identité vérifiée de l'expéditeur a changé", + "sender_unsigned_device": "Envoyé depuis un appareil non sécurisé.", "unable_to_decrypt": "Impossible de déchiffrer le message" }, "disambiguated_profile": "%(displayName)s (%(matrixId)s)", @@ -3195,6 +3254,7 @@ "download_action_downloading": "Téléchargement en cours", "download_failed": "Échec du téléchargement", "download_failed_description": "Une erreur s'est produite lors du téléchargement de ce fichier", + "e2e_state": "État du chiffrement de bout en bout", "edits": { "tooltip_label": "Modifié le %(date)s. Cliquer pour voir les modifications.", "tooltip_sub": "Cliquez pour voir les modifications", @@ -3204,10 +3264,6 @@ "error_rendering_message": "Impossible de charger ce message", "historical_messages_unavailable": "Vous ne pouvez pas voir les messages plus anciens", "in_room_name": " dans <strong>%(room)s</strong>", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s a terminé une <a>diffusion audio</a>", - "you": "Vous avez terminé une <a>diffusion audio</a>" - }, "io.element.widgets.layout": "%(senderName)s a mis à jour la mise en page du salon", "late_event_separator": "Initialement envoyé%(dateTime)s", "load_error": { @@ -3439,7 +3495,8 @@ "reactions": { "add_reaction_prompt": "Ajouter une réaction", "custom_reaction_fallback_label": "Réaction personnalisée", - "label": "%(reactors)s ont réagi avec %(content)s" + "label": "%(reactors)s ont réagi avec %(content)s", + "tooltip_caption": "a réagi avec %(shortName)s" }, "read_receipt_title": { "one": "Vu par %(count)s personne", @@ -3624,6 +3681,10 @@ "truncated_list_n_more": { "other": "Et %(count)s autres…" }, + "unsupported_browser": { + "description": "Si vous continuez, certaines fonctionnalités risquent de cesser de fonctionner et vous risquez de perdre des données à l'avenir. Mettez à jour votre navigateur pour continuer à utiliser%(brand)s .", + "title": "%(brand)sne prend pas en charge ce navigateur" + }, "unsupported_server_description": "Ce serveur utilise une ancienne version de Matrix. Mettez-le à jour vers Matrix %(version)s pour utiliser %(brand)s sans erreurs.", "unsupported_server_title": "Votre serveur n’est pas pris en charge", "update": { @@ -3656,6 +3717,7 @@ "error_files_too_large": "Ces fichiers sont <b>trop lourds</b> pour être envoyés. La taille limite des fichiers est de %(limit)s.", "error_some_files_too_large": "Certains fichiers sont <b>trop lourds</b> pour être envoyés. La taille limite des fichiers est de %(limit)s.", "error_title": "Erreur d’envoi", + "not_image": "Le fichier que vous avez choisi n'est pas un fichier image valide.", "title": "Envoyer les fichiers", "title_progress": "Envoi des fichiers (%(current)s sur %(total)s)", "upload_all_button": "Tout envoyer", @@ -3682,6 +3744,7 @@ "deactivate_confirm_action": "Désactiver l’utilisateur", "deactivate_confirm_description": "Désactiver cet utilisateur le déconnectera et l’empêchera de se reconnecter. De plus, il quittera tous les salons qu’il a rejoints. Cette action ne peut pas être annulée. Voulez-vous vraiment désactiver cet utilisateur ?", "deactivate_confirm_title": "Désactiver l’utilisateur ?", + "dehydrated_device_enabled": "Appareil hors ligne activé", "demote_button": "Rétrograder", "demote_self_confirm_description_space": "Vous ne pourrez pas annuler ce changement puisque vous vous rétrogradez. Si vous êtes le dernier utilisateur a privilèges de cet espace, il deviendra impossible d’en reprendre contrôle.", "demote_self_confirm_room": "Vous ne pourrez pas annuler cette modification car vous vous rétrogradez. Si vous êtes le dernier utilisateur privilégié de ce salon, il sera impossible de récupérer les privilèges.", @@ -3698,6 +3761,7 @@ "error_revoke_3pid_invite_title": "Échec de la révocation de l’invitation", "hide_sessions": "Masquer les sessions", "hide_verified_sessions": "Masquer les sessions vérifiées", + "ignore_button": "Ignorer", "ignore_confirm_description": "Tous les messages et invitations de cette utilisateur seront cachés. Êtes-vous sûr de vouloir les ignorer ?", "ignore_confirm_title": "Ignorer %(user)s", "invited_by": "Invité par %(sender)s", @@ -3731,6 +3795,7 @@ "room_encrypted_detail": "Vos messages sont sécurisés et seuls vous et le destinataire avez les clés uniques pour les déchiffrer.", "room_unencrypted": "Les messages dans ce salon ne sont pas chiffrés de bout en bout.", "room_unencrypted_detail": "Dans les salons chiffrés, vos messages sont sécurisés et seuls vous et le destinataire avez les clés uniques pour les déchiffrer.", + "send_message": "Envoyer un message", "share_button": "Partager le profil", "unban_button_room": "Révoquer le bannissement du salon", "unban_button_space": "Révoquer le bannissement de l’espace", @@ -3738,6 +3803,7 @@ "unban_space_everything": "Annuler le bannissement de partout où j’ai le droit de le faire", "unban_space_specific": "Annuler le bannissement de certains endroits où j’ai le droit de le faire", "unban_space_warning": "Ils ne pourront plus accéder aux endroits dans lesquels vous n’êtes pas administrateur.", + "unignore_button": "Ne plus ignorer", "verify_button": "Vérifier l’utilisateur", "verify_explainer": "Pour une sécurité supplémentaire, vérifiez cet utilisateur en comparant un code à usage unique sur vos deux appareils." }, @@ -3747,38 +3813,6 @@ "switch_theme_dark": "Passer au mode sombre", "switch_theme_light": "Passer au mode clair" }, - "voice_broadcast": { - "30s_backward": "30s en arrière", - "30s_forward": "30s en avant", - "action": "Diffusion audio", - "buffering": "Mise en mémoire tampon…", - "confirm_listen_affirm": "Oui, terminer mon enregistrement", - "confirm_listen_description": "En commençant à écouter cette diffusion en direct, votre enregistrement de diffusion en direct actuel sera interrompu.", - "confirm_listen_title": "Écouter la diffusion en direct ?", - "confirm_stop_affirm": "Oui, arrêter la diffusion", - "confirm_stop_description": "Êtes-vous sûr de vouloir arrêter votre diffusion en direct ? Cela terminera la diffusion et l’enregistrement complet sera disponible dans le salon.", - "confirm_stop_title": "Arrêter la diffusion en direct ?", - "connection_error": "Erreur de connexion – Enregistrement en pause", - "failed_already_recording_description": "Vous êtes déjà en train de réaliser une diffusion audio. Veuillez terminer votre diffusion audio actuelle pour en démarrer une nouvelle.", - "failed_already_recording_title": "Impossible de commencer une nouvelle diffusion audio", - "failed_decrypt": "Impossible de décrypter la diffusion audio", - "failed_generic": "Impossible de lire cette diffusion audio", - "failed_insufficient_permission_description": "Vous n’avez pas les permissions requises pour démarrer une nouvelle diffusion audio dans ce salon. Contactez un administrateur du salon pour mettre-à-jour vos permissions.", - "failed_insufficient_permission_title": "Impossible de commencer une nouvelle diffusion audio", - "failed_no_connection_description": "Malheureusement, nous ne pouvons pas démarrer l’enregistrement pour le moment. Veuillez réessayer plus tard.", - "failed_no_connection_title": "Erreur de connexion", - "failed_others_already_recording_description": "Une autre personne est déjà en train de réaliser une diffusion audio. Attendez que sa diffusion audio soit terminée pour en démarrer une nouvelle.", - "failed_others_already_recording_title": "Impossible de commencer une nouvelle diffusion audio", - "go_live": "Passer en direct", - "live": "Direct", - "pause": "mettre en pause la diffusion audio", - "play": "lire la diffusion audio", - "resume": "continuer la diffusion audio" - }, - "voice_message": { - "cant_start_broadcast_description": "Vous ne pouvez pas commencer un message vocal car vous êtes en train d’enregistrer une diffusion en direct. Veuillez terminer cette diffusion pour commencer un message vocal.", - "cant_start_broadcast_title": "Impossible de commencer un message vocal" - }, "voip": { "already_in_call": "Déjà en cours d’appel", "already_in_call_person": "Vous êtes déjà en cours d’appel avec cette personne.", @@ -3798,7 +3832,6 @@ "camera_disabled": "Votre caméra est éteinte", "camera_enabled": "Votre caméra est toujours allumée", "cannot_call_yourself_description": "Vous ne pouvez pas passer d’appel avec vous-même.", - "change_input_device": "Change de périphérique d’entrée", "close_lobby": "Fermer la salle d'attente", "connecting": "Connexion", "connection_lost": "La connexion au serveur a été perdue", @@ -3817,8 +3850,6 @@ "enable_camera": "Activer la caméra", "enable_microphone": "Activer le microphone", "expand": "Revenir à l’appel", - "failed_call_live_broadcast_description": "Vous ne pouvez pas démarrer un appel car vous êtes en train d’enregistrer une diffusion en direct. Veuillez terminer cette diffusion pour démarrer un appel.", - "failed_call_live_broadcast_title": "Impossible de démarrer un appel", "get_call_link": "Partager le lien de l'appel", "hangup": "Raccrocher", "hide_sidebar_button": "Masquer la barre latérale", @@ -3826,6 +3857,7 @@ "jitsi_call": "Conférence Jitsi", "join_button_tooltip_call_full": "Désolé — Cet appel est actuellement complet", "join_button_tooltip_connecting": "Connexion", + "legacy_call": "Appel vidéo", "maximise": "Remplir l’écran", "maximise_call": "Plein écran", "metaspace_video_rooms": { diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index 00277a5f3e..50c551d6a2 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -749,7 +749,6 @@ }, "udd": { "interactive_verification_button": "Verificar interactivamente usando emoji", - "manual_verification_button": "Verificar manualmente con texto", "other_ask_verify_text": "Pídelle a usuaria que verifique a súa sesión, ou verificaa manualmente aquí.", "other_new_session_text": "%(name)s (%(userId)s) conectouse a unha nova sesión sen verificala:", "own_ask_verify_text": "Verifica a túa outra sesión usando unha das opcións inferiores.", @@ -781,12 +780,6 @@ "incoming_sas_dialog_title": "Solicitude entrante de verificación", "incoming_sas_user_dialog_text_1": "Verifica esta usuaria para marcala como confiable. Ao confiar nas usuarias proporcionache tranquilidade extra cando usas cifrado de extremo-a-extremo.", "incoming_sas_user_dialog_text_2": "Ao verificar esta usuaria marcarás a súa sesión como confiable, e tamén marcará a túa sesión como confiable para elas.", - "manual_device_verification_device_id_label": "ID de sesión", - "manual_device_verification_device_key_label": "Chave da sesión", - "manual_device_verification_device_name_label": "Nome da sesión", - "manual_device_verification_footer": "Se non concordan, a seguridade da comunicación podería estar comprometida.", - "manual_device_verification_self_text": "Corfirma comparando o seguinte cos Axustes de Usuaria na outra sesión:", - "manual_device_verification_user_text": "Confirma a sesión desta usuaria comparando o seguinte cos seus Axustes de Usuaria:", "no_key_or_device": "Semella que non tes unha Chave de Seguridade ou outros dispositivos cos que verificar. Este dispositivo non poderá acceder a mensaxes antigas cifradas. Para poder verificar a túa identidade neste dispositivo tes que restablecer as chaves de verificación.", "no_support_qr_emoji": "Este dispositivo que intentas verificar non ten soporte para código QR nin verificación por emoji, que é o que %(brand)s soporta. Inténtao cun cliente diferente.", "other_party_cancelled": "A outra parte cancelou a verificación.", @@ -1200,8 +1193,7 @@ "video_rooms_faq1_answer": "Usa o botón \"+\" na sección da sala do panel esquerdo.", "video_rooms_faq1_question": "Como creo unha sala de vídeo?", "video_rooms_faq2_answer": "Si, a cronoloxía de texto móstrase xunto co vídeo.", - "video_rooms_faq2_question": "Podo usar chat de texto xunto á chamada de vídeo?", - "voice_broadcast": "Emisión de voz" + "video_rooms_faq2_question": "Podo usar chat de texto xunto á chamada de vídeo?" }, "labs_mjolnir": { "advanced_warning": "⚠ Estos axustes van dirixidos a usuarias avanzadas.", @@ -1811,7 +1803,6 @@ "error_unbanning": "Fallou eliminar a prohibición", "events_default": "Enviar mensaxes", "invite": "Convidar usuarias", - "io.element.voice_broadcast_info": "Emisións de voz", "kick": "Eliminar usuarias", "m.reaction": "Enviar reaccións", "m.room.avatar": "Cambiar avatar da sala", @@ -2315,7 +2306,6 @@ "warn_quit": "Aviso antes de saír" }, "share": { - "link_title": "Ligazón á sala", "permalink_message": "Ligazón á mensaxe escollida", "permalink_most_recent": "Ligazón ás mensaxes máis recentes", "title_message": "Compartir unha mensaxe da sala", @@ -2397,12 +2387,6 @@ "upgraderoom": "Subir a sala de versión", "upgraderoom_permission_error": "Non tes os permisos suficientes para usar este comando.", "usage": "Uso", - "verify": "Verifica unha usuaria, sesión e chave pública", - "verify_mismatch": "AVISO: FALLOU A VERIFICACIÓN DAS CHAVES! A chave de firma para %(userId)s na sesión %(deviceId)s é \"%(fprint)s\" que non concordan coa chave proporcionada \"%(fingerprint)s\". Esto podería significar que as túas comunicacións foron interceptadas!", - "verify_nop": "A sesión xa está verificada!", - "verify_success_description": "A chave de firma proporcionada concorda coa chave de firma recibida desde a sesión %(deviceId)s de %(userId)s. Sesión marcada como verificada.", - "verify_success_title": "Chave verificada", - "verify_unknown_pair": "Parella (usuaria, sesión) descoñecida: (%(userId)s, %(deviceId)s)", "whois": "Mostra información acerca da usuaria" }, "space": { @@ -3102,9 +3086,6 @@ "switch_theme_dark": "Cambiar a decorado escuro", "switch_theme_light": "Cambiar a decorado claro" }, - "voice_broadcast": { - "action": "Emisión de voz" - }, "voip": { "already_in_call": "Xa estás nunha chamada", "already_in_call_person": "Xa estás nunha conversa con esta persoa.", diff --git a/src/i18n/strings/he.json b/src/i18n/strings/he.json index c98d59c659..724e17efd9 100644 --- a/src/i18n/strings/he.json +++ b/src/i18n/strings/he.json @@ -662,12 +662,6 @@ "incoming_sas_dialog_title": "בקשת אימות נכנסת", "incoming_sas_user_dialog_text_1": "אמתו את המשתמש הזה כדי לסמן אותו כאמין. אמון במשתמשים מעניק לכם שקט נפשי נוסף בשימוש בהודעות מוצפנות מקצה לקצה.", "incoming_sas_user_dialog_text_2": "אימות משתמש זה יסמן את ההפעלה שלו כאמינה, וגם יסמן את ההפעלה שלכם כאמינה להם.", - "manual_device_verification_device_id_label": "זהות מושב", - "manual_device_verification_device_key_label": "מפתח מושב", - "manual_device_verification_device_name_label": "שם מושב", - "manual_device_verification_footer": "אם הם לא תואמים, אבטחת התקשורת שלך עלולה להיפגע.", - "manual_device_verification_self_text": "אשר על ידי השוואה בין הדברים הבאים להגדרות המשתמש בפגישה האחרת שלך:", - "manual_device_verification_user_text": "אשר את הפעלת המשתמש הזה על ידי השוואה בין הדברים הבאים להגדרות המשתמש שלהם:", "other_party_cancelled": "הצד השני ביטל את האימות.", "prompt_encrypted": "אמת את כל המשתמשים בחדר כדי לוודא שהוא מאובטח.", "prompt_self": "התחל אימות שוב מההודעה.", @@ -1454,7 +1448,6 @@ "error_unbanning": "שגיאה בהסרת חסימה", "events_default": "שלח הודעות", "invite": "הזמנת משתמשים", - "io.element.voice_broadcast_info": "שליחת הקלטות קוליות", "kick": "הסר משתמשים", "m.reaction": "שלח תגובות", "m.room.avatar": "שנה אווטר של החדר", @@ -1958,11 +1951,6 @@ "upgraderoom": "משדרג את החדר לגרסא חדשה", "upgraderoom_permission_error": "אין לכם הרשאות להשתמש בפקודה זו.", "usage": "שימוש", - "verify": "מוודא משתמש, התחברות וצמד מפתח ציבורי", - "verify_mismatch": "אזהרה: אימות מפתח נכשל! חתימת המפתח של %(userId)s ושל ההתחברות של מכשיר %(deviceId)s הינו \"%(fprint)s\" אשר אינו תואם למפתח הנתון \"%(fingerprint)s\". דבר זה יכול להעיר על כך שישנו נסיון להאזין לתקשורת שלכם!", - "verify_nop": "ההתחברות כבר אושרה!", - "verify_success_description": "המפתח החתום שנתתם תואם את המפתח שקבלתם מ %(userId)s מהתחברות %(deviceId)s. ההתחברות סומנה כמאושרת.", - "verify_success_title": "מפתח מאושר", "whois": "מציג מידע אודות משתמש" }, "space": { diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index cf410fb82f..349c4a39cd 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -872,7 +872,6 @@ }, "udd": { "interactive_verification_button": "Interaktív ellenőrzés emodzsikkal", - "manual_verification_button": "Kézi szöveges ellenőrzés", "other_ask_verify_text": "Kérje meg a felhasználót, hogy hitelesítse a munkamenetét, vagy ellenőrizze kézzel lentebb.", "other_new_session_text": "%(name)s (%(userId)s) új munkamenetbe lépett be, anélkül, hogy ellenőrizte volna:", "own_ask_verify_text": "Ellenőrizze a másik munkamenetét a lenti lehetőségek egyikével.", @@ -907,12 +906,6 @@ "incoming_sas_dialog_waiting": "Várakozás a partner megerősítésére…", "incoming_sas_user_dialog_text_1": "Ellenőrizd ezt a felhasználót, hogy megbízhatónak lehessen tekinteni. Megbízható felhasználók további nyugalmat jelenthetnek ha végpontól végpontig titkosítást használsz.", "incoming_sas_user_dialog_text_2": "A felhasználó ellenőrzése által az ő munkamenete megbízhatónak lesz jelölve, és a te munkameneted is megbízhatónak lesz jelölve nála.", - "manual_device_verification_device_id_label": "Kapcsolat azonosító", - "manual_device_verification_device_key_label": "Munkamenetkulcs", - "manual_device_verification_device_name_label": "Munkamenet neve", - "manual_device_verification_footer": "Ha nem egyeznek akkor a kommunikációtok biztonsága veszélyben lehet.", - "manual_device_verification_self_text": "Erősítsd meg a felhasználói beállítások összehasonlításával a többi munkamenetedben:", - "manual_device_verification_user_text": "Ezt a munkamenetet hitelesítsd az ő felhasználói beállításának az összehasonlításával:", "no_key_or_device": "Úgy tűnik, hogy nem rendelkezik biztonsági kulccsal, vagy másik eszközzel, amelyikkel ellenőrizhetné. Ezzel az eszközzel nem fér majd hozzá a régi titkosított üzenetekhez. Ahhoz, hogy a személyazonosságát ezen az eszközön ellenőrizni lehessen, az ellenőrzédi kulcsokat alaphelyzetbe kell állítani.", "no_support_qr_emoji": "Az ellenőrizni kívánt eszköz nem támogatja se a QR kód beolvasást se az emodzsi ellenőrzést, amit a %(brand)s támogat. Próbálja meg egy másik klienssel.", "other_party_cancelled": "A másik fél megszakította az ellenőrzést.", @@ -1029,10 +1022,6 @@ }, "error_user_not_logged_in": "A felhasználó nincs bejelentkezve", "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s befejezte a hangközvetítést", - "you": "A hangközvetítés befejeződött" - }, "m.call.answer": { "dm": "Folyamatban lévő hívás", "user": "%(senderName)s csatlakozott a hívásba", @@ -1392,8 +1381,6 @@ "video_rooms_faq1_question": "Hogy lehet videószobát készíteni?", "video_rooms_faq2_answer": "Igen, a szöveges idővonal a videóval együtt megjelenik.", "video_rooms_faq2_question": "Lehet a videóhívás közben szövegesen is csevegni?", - "voice_broadcast": "Hangközvetítés", - "voice_broadcast_force_small_chunks": "Hangközvetítések 15 másodperces darabolásának kényszerítése", "wysiwyg_composer": "Szövegszerkesztő használata" }, "labs_mjolnir": { @@ -1528,7 +1515,6 @@ "mute_description": "Nem kap semmilyen értesítést" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s hangos közvetítést indított", "m.key.verification.request": "%(name)s ellenőrzést kér" }, "onboarding": { @@ -2077,7 +2063,6 @@ "error_unbanning": "A kitiltás visszavonása sikertelen", "events_default": "Üzenetek küldése", "invite": "Felhasználók meghívása", - "io.element.voice_broadcast_info": "Hangközvetítés", "kick": "Felhasználók eltávolítása", "m.call": "%(brand)s hívás indítása", "m.call.member": "Csatlakozás ebbe a hívásba: %(brand)s", @@ -2711,7 +2696,6 @@ "warning": "<w>FIGYELEM:</w> <description/>" }, "share": { - "link_title": "Hivatkozás a szobához", "permalink_message": "Hivatkozás a kijelölt üzenethez", "permalink_most_recent": "Hivatkozás a legfrissebb üzenethez", "title_message": "Szoba üzenetének megosztása", @@ -2800,13 +2784,6 @@ "upgraderoom": "Új verzióra fejleszti a szobát", "upgraderoom_permission_error": "A parancs használatához nincs meg a megfelelő jogosultsága.", "usage": "Használat", - "verify": "Felhasználó, munkamenet és nyilvános kulcs hármas ellenőrzése", - "verify_mismatch": "FIGYELEM: A KULCSELLENŐRZÉS SIKERTELEN! %(userId)s aláírási kulcsa és a(z) %(deviceId)s munkamenet ujjlenyomata „%(fprint)s”, amely nem egyezik meg a megadott ujjlenyomattal: „%(fingerprint)s”. Ez azt is jelentheti, hogy a kommunikációt lehallgatják.", - "verify_nop": "A munkamenet már ellenőrzött.", - "verify_nop_warning_mismatch": "FIGYELEM: a munkamenet már ellenőrizve van, de a kulcsok NEM EGYEZNEK.", - "verify_success_description": "A megadott aláírási kulcs megegyezik %(userId)s felhasználótól kapott aláírási kulccsal ebben a munkamenetben: %(deviceId)s. A munkamenet ellenőrzöttnek lett jelölve.", - "verify_success_title": "Ellenőrzött kulcs", - "verify_unknown_pair": "Ismeretlen (felhasználó, munkamenet) páros: (%(userId)s, %(deviceId)s)", "view": "Megadott címmel rendelkező szobák megjelenítése", "whois": "Információt jelenít meg a felhasználóról" }, @@ -3018,10 +2995,6 @@ "error_rendering_message": "Ezt az üzenetet nem sikerült betölteni", "historical_messages_unavailable": "Nem tekintheted meg a régebbi üzeneteket", "in_room_name": " itt: <strong>%(room)s</strong>", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s befejezte a <a>hangközvetítést</a>", - "you": "Befejezte a <a>hangközvetítést</a>" - }, "io.element.widgets.layout": "%(senderName)s frissítette a szoba kinézetét", "load_error": { "no_permission": "Megpróbálta betölteni a szoba megadott időpontjának megfelelő adatait, de nincs joga a kérdéses üzenetek megjelenítéséhez.", @@ -3551,38 +3524,6 @@ "switch_theme_dark": "Sötét módra váltás", "switch_theme_light": "Világos módra váltás" }, - "voice_broadcast": { - "30s_backward": "vissza 30 másodperccel", - "30s_forward": "előre 30 másodperccel", - "action": "Hangközvetítés", - "buffering": "Pufferelés…", - "confirm_listen_affirm": "Igen, a felvétel befejezése", - "confirm_listen_description": "Ha hallgatja ezt az élő közvetítést, akkor a jelenlegi élő közvetítésének a felvétele befejeződik.", - "confirm_listen_title": "Élő közvetítés hallgatása?", - "confirm_stop_affirm": "Igen, közvetítés megállítása", - "confirm_stop_description": "Biztos, hogy befejezi az élő közvetítést? Ez megállítja a közvetítést és a felvétel az egész szoba számára elérhető lesz.", - "confirm_stop_title": "Megszakítja az élő közvetítést?", - "connection_error": "Kapcsolódási hiba – Felvétel szüneteltetve", - "failed_already_recording_description": "Egy hangközvetítés már folyamatban van. Először fejezze be a jelenlegi közvetítést egy új indításához.", - "failed_already_recording_title": "Az új hangközvetítés nem indítható el", - "failed_decrypt": "A hangközvetítést nem lehet visszafejteni", - "failed_generic": "A hangközvetítés nem játszható le", - "failed_insufficient_permission_description": "Nincs jogosultsága hangközvetítést indítani ebben a szobában. Vegye fel a kapcsolatot a szoba adminisztrátorával a szükséges jogosultság megszerzéséhez.", - "failed_insufficient_permission_title": "Az új hangközvetítés nem indítható el", - "failed_no_connection_description": "Sajnos most nem lehet elindítani a felvételt. Próbálja meg később.", - "failed_no_connection_title": "Kapcsolati hiba", - "failed_others_already_recording_description": "Valaki már elindított egy hangközvetítést. Várja meg a közvetítés végét az új indításához.", - "failed_others_already_recording_title": "Az új hangközvetítés nem indítható el", - "go_live": "Élő közvetítés indítása", - "live": "Élő közvetítés", - "pause": "hangközvetítés szüneteltetése", - "play": "hangközvetítés lejátszása", - "resume": "hangközvetítés folytatása" - }, - "voice_message": { - "cant_start_broadcast_description": "Nem lehet hang üzenetet indítani élő közvetítés felvétele közben. Az élő közvetítés bejezése szükséges a hang üzenet indításához.", - "cant_start_broadcast_title": "Hang üzenetet nem lehet elindítani" - }, "voip": { "already_in_call": "A hívás már folyamatban van", "already_in_call_person": "Már hívásban van ezzel a személlyel.", @@ -3602,7 +3543,6 @@ "camera_disabled": "A kamerája ki van kapcsolva", "camera_enabled": "A kamerája még mindig be van kapcsolva", "cannot_call_yourself_description": "Nem hívhatja fel saját magát.", - "change_input_device": "Bemeneti eszköz megváltoztatása", "connecting": "Kapcsolódás", "connection_lost": "Megszakadt a kapcsolat a kiszolgálóval", "connection_lost_description": "Nem kezdeményezhet hívást a kiszolgálóval való kapcsolat nélkül.", @@ -3619,8 +3559,6 @@ "enable_camera": "Kamera bekapcsolása", "enable_microphone": "Mikrofon némításának feloldása", "expand": "Visszatérés a híváshoz", - "failed_call_live_broadcast_description": "Nem lehet hívást kezdeményezni élő közvetítés felvétele közben. Az élő közvetítés bejezése szükséges a hívás indításához.", - "failed_call_live_broadcast_title": "Nem sikerült hívást indítani", "hangup": "Bontás", "hide_sidebar_button": "Oldalsáv elrejtése", "input_devices": "Beviteli eszközök", diff --git a/src/i18n/strings/id.json b/src/i18n/strings/id.json index 77cdd8a78f..9eade90bc2 100644 --- a/src/i18n/strings/id.json +++ b/src/i18n/strings/id.json @@ -870,7 +870,6 @@ }, "udd": { "interactive_verification_button": "Verifikasi secara interaktif sengan emoji", - "manual_verification_button": "Verifikasi secara manual dengan teks", "other_ask_verify_text": "Tanyakan pengguna ini untuk memverifikasi sesinya, atau verifikasi secara manual di bawah.", "other_new_session_text": "%(name)s (%(userId)s) masuk ke sesi yang baru tanpa memverifikasinya:", "own_ask_verify_text": "Verifikasi sesi Anda lainnya dengan menggunakan salah satu pilihan di bawah.", @@ -905,12 +904,6 @@ "incoming_sas_dialog_waiting": "Menunggu pengguna untuk konfirmasi…", "incoming_sas_user_dialog_text_1": "Verifikasi pengguna ini untuk menandainya sebagai terpercaya. Mempercayai pengguna memberikan Anda ketenangan saat menggunakan pesan terenkripsi secara ujung ke ujung.", "incoming_sas_user_dialog_text_2": "Memverifikasi pengguna ini akan menandai sesinya sebagai terpercaya, dan juga menandai sesi Anda sebagai terpercaya kepadanya.", - "manual_device_verification_device_id_label": "ID Sesi", - "manual_device_verification_device_key_label": "Kunci sesi", - "manual_device_verification_device_name_label": "Nama sesi", - "manual_device_verification_footer": "Jika mereka tidak cocok, keamanan komunikasi Anda mungkin dikompromikan.", - "manual_device_verification_self_text": "Konfirmasi dengan membandingkan berikut ini dengan Pengaturan Pengguna di sesi Anda yang lain:", - "manual_device_verification_user_text": "Konfirmasi sesi pengguna ini dengan membandingkan berikut ini dengan Pengaturan Pengguna:", "no_key_or_device": "Sepertinya Anda tidak memiliki Kunci Keamanan atau perangkat lainnya yang Anda dapat gunakan untuk memverifikasi. Perangkat ini tidak dapat mengakses ke pesan terenkripsi lama. Untuk membuktikan identitas Anda, kunci verifikasi harus diatur ulang.", "no_support_qr_emoji": "Perangkat yang Anda sedang verifikasi tidak mendukung pemindaian kode QR atau verifikasi emoji, yang didukung oleh %(brand)s. Coba menggunakan klien yang lain.", "other_party_cancelled": "Pengguna yang lain membatalkan proses verifikasi ini.", @@ -1027,10 +1020,6 @@ }, "error_user_not_logged_in": "Pengguna belum masuk", "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s mdngakhiri sebuah siaran suara", - "you": "Anda mengakhiri sebuah siaran suara" - }, "m.call.answer": { "dm": "Panggilan sedang berjalan", "user": "%(senderName)s bergabung dengan panggilan saat ini", @@ -1399,8 +1388,6 @@ "video_rooms_faq2_answer": "Ya, lini masa obrolan akan ditampilkan di sebelah videonya.", "video_rooms_faq2_question": "Bisakah saya mengobrol dengan teks saat ada panggilan video?", "video_rooms_feedbackSubheading": "Terima kasih telah mencoba fitur beta, mohon berikan masukan sedetail mungkin supaya kami dapat menyempurnakannya.", - "voice_broadcast": "Siaran suara", - "voice_broadcast_force_small_chunks": "Paksakan panjang bagian siaran suara 15d", "wysiwyg_composer": "Editor teks kaya" }, "labs_mjolnir": { @@ -1537,7 +1524,6 @@ "mute_description": "Anda tidak akan mendapatkan notifikasi apa pun" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s memulai sebuah siaran suara", "m.key.verification.request": "%(name)s meminta verifikasi" }, "onboarding": { @@ -2104,7 +2090,6 @@ "error_unbanning": "Gagal untuk menghapus cekalan", "events_default": "Kirim pesan", "invite": "Undang pengguna", - "io.element.voice_broadcast_info": "Siaran suara", "kick": "Keluarkan pengguna", "m.call": "Mulai panggilan %(brand)s", "m.call.member": "Bergabung panggilan %(brand)s", @@ -2743,7 +2728,6 @@ "warning": "<w>PERINGATAN:</w> <description/>" }, "share": { - "link_title": "Tautan ke ruangan", "permalink_message": "Tautan ke pesan yang dipilih", "permalink_most_recent": "Tautan ke pesan terkini", "title_message": "Bagikan Pesan Ruangan", @@ -2832,13 +2816,6 @@ "upgraderoom": "Meningkatkan ruangan ke versi yang baru", "upgraderoom_permission_error": "Anda tidak memiliki izin yang dibutuhkan untuk menggunakan perintah ini.", "usage": "Penggunaan", - "verify": "Memverifikasi sebuah pengguna, sesi, dan tupel pubkey", - "verify_mismatch": "PERINGATAN: VERIFIKASI KUNCI GAGAL! Kunci penandatanganan untuk %(userId)s dan sesi %(deviceId)s adalah \"%(fprint)s\" yang tidak cocok dengan kunci \"%(fingerprint)s\" yang disediakan. Ini bisa saja berarti komunikasi Anda sedang disadap!", - "verify_nop": "Sesi telah diverifikasi!", - "verify_nop_warning_mismatch": "PERINGATAN: sesi telah diverifikasi, tetapi kuncinya TIDAK COCOK!", - "verify_success_description": "Kunci penandatanganan yang Anda sediakan cocok dengan kunci penandatanganan yang Anda terima dari sesi %(userId)s %(deviceId)s. Sesi ditandai sebagai terverifikasi.", - "verify_success_title": "Kunci terverifikasi", - "verify_unknown_pair": "Pasangan tidak diketahui (pengguna, sesi): (%(userId)s, %(deviceId)s)", "view": "Menampilkan ruangan dengan alamat yang ditentukan", "whois": "Menampilkan informasi tentang sebuah pengguna" }, @@ -3051,10 +3028,6 @@ "error_rendering_message": "Tidak dapat memuat pesan ini", "historical_messages_unavailable": "Anda tidak dapat melihat pesan-pesan awal", "in_room_name": " di <strong>%(room)s</strong>", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s mengakhiri sebuah <a>siaran suara</a>", - "you": "Anda mengakhiri sebuah <a>siaran suara</a>" - }, "io.element.widgets.layout": "%(senderName)s telah memperbarui tata letak ruangan", "load_error": { "no_permission": "Mencoba memuat titik spesifik di lini masa ruangan ini, tetapi Anda tidak memiliki izin untuk menampilkan pesannya.", @@ -3584,38 +3557,6 @@ "switch_theme_dark": "Ubah ke mode gelap", "switch_theme_light": "Ubah ke mode terang" }, - "voice_broadcast": { - "30s_backward": "30d sebelumnya", - "30s_forward": "30d selanjutnya", - "action": "Siaran suara", - "buffering": "Memuat…", - "confirm_listen_affirm": "Ya, hentikan rekaman saya", - "confirm_listen_description": "Jika Anda mendengarkan siaran langsung ini, rekaman siaran langsung Anda saat ini akan dihentikan.", - "confirm_listen_title": "Dengarkan siaran langsung?", - "confirm_stop_affirm": "Iya, hentikan siaran", - "confirm_stop_description": "Apakah Anda ingin menghentikan siaran langsung Anda? Ini akan mengakhiri siarannya, dan rekamanan lengkap akan tersedia dalam ruangan.", - "confirm_stop_title": "Hentikan siaran langsung?", - "connection_error": "Kesalahan koneksi - Perekaman dijeda", - "failed_already_recording_description": "Anda saat ini merekam sebuah siaran suara. Mohon akhiri siaran suara Anda saat ini untuk memulai yang baru.", - "failed_already_recording_title": "Tidak dapat memulai sebuah siaran suara baru", - "failed_decrypt": "Tidak dapat mendekripsi siaran suara", - "failed_generic": "Tidak dapat memutar siaran suara ini", - "failed_insufficient_permission_description": "Anda tidak memiliki izin untuk memulai sebuah siaran suara di ruangan ini. Hubungi sebuah administrator ruangan untuk meningkatkan izin Anda.", - "failed_insufficient_permission_title": "Tidak dapat memulai sebuah siaran suara baru", - "failed_no_connection_description": "Sayangnya kami saat ini tidak dapat memulai sebuah rekaman. Silakan mencoba lagi nanti.", - "failed_no_connection_title": "Terjadi kesalahan koneksi", - "failed_others_already_recording_description": "Ada orang lain yang saat ini merekam sebuah siaran suara. Tunggu siaran suaranya berakhir untuk memulai yang baru.", - "failed_others_already_recording_title": "Tidak dapat memulai sebuah siaran suara baru", - "go_live": "Mulai siaran langsung", - "live": "Langsung", - "pause": "jeda siaran suara", - "play": "mainkan siaran suara", - "resume": "lanjutkan siaran suara" - }, - "voice_message": { - "cant_start_broadcast_description": "Anda tidak dapat memulai sebuah pesan suara karena Anda saat ini merekam sebuah siaran langsung. Silakan mengakhiri siaran langsung Anda untuk memulai merekam sebuah pesan suara.", - "cant_start_broadcast_title": "Tidak dapat memulai pesan suara" - }, "voip": { "already_in_call": "Sudah ada di panggilan", "already_in_call_person": "Anda sudah ada di panggilan dengan orang itu.", @@ -3635,7 +3576,6 @@ "camera_disabled": "Kamera Anda dimatikan", "camera_enabled": "Kamera Anda masih nyala", "cannot_call_yourself_description": "Anda tidak dapat melakukan panggilan dengan diri sendiri.", - "change_input_device": "Ubah perangkat masukan", "connecting": "Menghubungkan", "connection_lost": "Koneksi ke server telah hilang", "connection_lost_description": "Anda tidak dapat membuat panggilan tanpa terhubung ke server.", @@ -3652,8 +3592,6 @@ "enable_camera": "Nyalakan kamera", "enable_microphone": "Suarakan mikrofon", "expand": "Kembali ke panggilan", - "failed_call_live_broadcast_description": "Anda tidak dapat memulai sebuah panggilan karena Anda saat ini merekam sebuah siaran langsung. Mohon akhiri siaran langsung Anda untuk memulai sebuah panggilan.", - "failed_call_live_broadcast_title": "Tidak dapat memulai panggilan", "hangup": "Akhiri", "hide_sidebar_button": "Sembunyikan sisi bilah", "input_devices": "Perangkat masukan", diff --git a/src/i18n/strings/is.json b/src/i18n/strings/is.json index c746caef1d..21ff743f2d 100644 --- a/src/i18n/strings/is.json +++ b/src/i18n/strings/is.json @@ -737,7 +737,6 @@ }, "udd": { "interactive_verification_button": "Sannprófa gagnvirkt með táknmyndum", - "manual_verification_button": "Sannreyna handvirkt með texta", "other_ask_verify_text": "Biddu þennan notanda að sannreyna setuna sína, eða sannreyndu hana handvirkt hér fyrir neðan.", "other_new_session_text": "%(name)s (%(userId)s) skráði sig inn í nýja setu án þess að sannvotta hana:", "own_new_session_text": "Þú skráðir inn í nýja setu án þess að sannvotta hana:", @@ -764,11 +763,6 @@ "explainer": "Örugg skilaboð við þennan notanda eru enda-í-enda dulrituð þannig að enginn annar getur lesið þau.", "in_person": "Til öryggis, gerðu þetta í eigin persónu eða notaðu einhverja samskiptaleið sem þú treystir.", "incoming_sas_dialog_title": "Innkomin beiðni um sannvottun", - "manual_device_verification_device_id_label": "Auðkenni setu", - "manual_device_verification_device_key_label": "Dulritunarlykill setu", - "manual_device_verification_device_name_label": "Nafn á setu", - "manual_device_verification_footer": "Ef þetta samsvarar ekki, getur verið að samskiptin þín séu berskjölduð.", - "manual_device_verification_self_text": "Staðfestu með því að bera eftirfarandi saman við 'Stillingar notanda' í hinni setunni þinni:", "other_party_cancelled": "Hinn aðilinn hætti við sannvottunina.", "prompt_encrypted": "Sannreyndu alla notendur á spjallrás til að tryggja að hún sé örugg.", "prompt_self": "Hefja sannvottun aftur úr tilkynningunni.", @@ -862,10 +856,6 @@ } }, "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s endaði talútsendingu", - "you": "Þú endaðir talútsendingu" - }, "m.call.answer": { "dm": "Símtal í gangi", "user": "%(senderName)s kom inn í símtalið", @@ -1164,7 +1154,6 @@ "video_rooms_faq1_question": "Hvernig bý ég til myndspjallrás?", "video_rooms_faq2_answer": "Já, tímalína spjallsins birtist við hlið myndmerkisins.", "video_rooms_faq2_question": "Get ég notað textaspjall samhliða myndsímtali?", - "voice_broadcast": "Útvörpun tals", "wysiwyg_composer": "Þróaður textaritill" }, "labs_mjolnir": { @@ -1285,7 +1274,6 @@ "mute_description": "Þú munt ekki fá neinar tilkynningar" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s hóf talútsendingu", "m.key.verification.request": "%(name)s biður um sannvottun" }, "onboarding": { @@ -1730,7 +1718,6 @@ "error_unbanning": "Tókst ekki að taka úr banni", "events_default": "Senda skilaboð", "invite": "Bjóða notendum", - "io.element.voice_broadcast_info": "Útsendingar tals", "kick": "Fjarlægja notendur", "m.call": "Byrja %(brand)s samtal", "m.call.member": "Taka þátt í %(brand)s samtali", @@ -2240,7 +2227,6 @@ "warning": "<w>AÐVÖRUN:</w> <description/>" }, "share": { - "link_title": "Tengill á spjallrás", "permalink_message": "Tengill í valin skilaboð", "permalink_most_recent": "Tengill í nýjustu skilaboðin", "title_message": "Deila skilaboðum spjallrásar", @@ -2321,12 +2307,6 @@ "upgraderoom": "Uppfærir spjallrás í nýja útgáfu", "upgraderoom_permission_error": "Þú hefur ekki nauðsynlegar heimildir til að nota þessa skipun.", "usage": "Notkun", - "verify": "Sannreynir auðkenni notanda, setu og dreifilykils", - "verify_mismatch": "AÐVÖRUN: SANNVOTTUN LYKILS MISTÓKST! Undirritunarlykillinn fyrir %(userId)s og setuna %(deviceId)s er \"%(fprint)s\" sem samsvarar ekki uppgefna lyklinum \"%(fingerprint)s\". Þetta gæti þýtt að einhver hafi komist inn í samskiptin þín!", - "verify_nop": "Seta er þegar sannreynd!", - "verify_success_description": "Undirritunarlykillinn sem þú gafst upp samsvarar lyklinum sem þú fékkst frá %(userId)s og setunni %(deviceId)s. Setan er því merkt sem sannreynd.", - "verify_success_title": "Staðfestur dulritunarlykill", - "verify_unknown_pair": "Óþekkt pörun (notandi, seta): (%(userId)s, %(deviceId)s)", "whois": "Birtir upplýsingar um notanda" }, "space": { @@ -2523,10 +2503,6 @@ "error_rendering_message": "Gat ekki hlaðið inn þessum skilaboðum", "historical_messages_unavailable": "Þú getur ekki séð eldri skilaboð", "in_room_name": " í <strong>%(room)s</strong>", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s endaði <a>talútsendingu</a>", - "you": "Þú endaðir <a>talútsendingu</a>" - }, "io.element.widgets.layout": "%(senderName)s hefur uppfært framsetningu spjallrásarinnar", "load_error": { "no_permission": "Reyndi að hlaða inn tilteknum punkti úr tímalínu þessarar spjallrásar, en þú ert ekki með heimild til að skoða tilteknu skilaboðin.", @@ -2998,27 +2974,6 @@ "switch_theme_dark": "Skiptu yfir í dökkan ham", "switch_theme_light": "Skiptu yfir í ljósan ham" }, - "voice_broadcast": { - "30s_backward": "30s afturábak", - "30s_forward": "30s áfram", - "action": "Útvörpun tals", - "buffering": "Hleð í biðminni…", - "confirm_listen_affirm": "Já, stöðva upptökuna mína", - "confirm_listen_title": "Hlusta á beina útsendingu?", - "confirm_stop_affirm": "Já, stöðva útsendingu", - "confirm_stop_title": "Stöðva beina útsendingu?", - "connection_error": "Villa í tengingu - Upptaka í bið", - "failed_already_recording_title": "Get ekki byrjað nýja talútsendingu", - "failed_insufficient_permission_title": "Get ekki byrjað nýja talútsendingu", - "failed_no_connection_description": "Því miður tókst ekki að setja aðra upptöku í gang. Reyndu aftur síðar.", - "failed_no_connection_title": "Villa í tengingu", - "failed_others_already_recording_title": "Get ekki byrjað nýja talútsendingu", - "go_live": "Fara í beina útsendingu", - "live": "Beint", - "pause": "setja talútsendingu í bið", - "play": "spila talútsendingu", - "resume": "halda áfram með talútsendingu" - }, "voip": { "already_in_call": "Nú þegar í símtali", "already_in_call_person": "Þú ert nú þegar í símtali við þennan aðila.", @@ -3038,7 +2993,6 @@ "camera_disabled": "Slökkt er á myndavélinni þinni", "camera_enabled": "Enn er kveikt á myndavélinni þinni", "cannot_call_yourself_description": "Þú getur ekki byrjað símtal með sjálfum þér.", - "change_input_device": "skipta um inntakstæki", "connecting": "Tengist", "connection_lost": "Tenging við vefþjón hefur rofnað", "connection_lost_description": "Þú getur ekki hringt símtöl án tengingar við netþjóninn.", @@ -3055,7 +3009,6 @@ "enable_camera": "Kveikja á myndavél", "enable_microphone": "Kveikja á hljóðnema", "expand": "Fara til baka í símtal", - "failed_call_live_broadcast_title": "Get ekki hafið símtal", "hangup": "Leggja á", "hide_sidebar_button": "Fela hliðarspjald", "input_devices": "Inntakstæki", diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 9f0ab6d430..1ddf88e832 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -881,7 +881,6 @@ }, "udd": { "interactive_verification_button": "Verifica interattivamente con emoji", - "manual_verification_button": "Verifica manualmente con testo", "other_ask_verify_text": "Chiedi a questo utente di verificare la sua sessione o verificala manualmente sotto.", "other_new_session_text": "%(name)s (%(userId)s) ha fatto l'accesso con una nuova sessione senza verificarla:", "own_ask_verify_text": "Verifica la tua altra sessione usando una delle opzioni sotto.", @@ -916,12 +915,6 @@ "incoming_sas_dialog_waiting": "In attesa che il partner confermi…", "incoming_sas_user_dialog_text_1": "Verifica questo utente per contrassegnarlo come affidabile. La fiducia degli utenti offre una maggiore tranquillità quando si utilizzano messaggi cifrati end-to-end.", "incoming_sas_user_dialog_text_2": "La verifica di questo utente contrassegnerà come fidata la sua sessione a te e viceversa.", - "manual_device_verification_device_id_label": "ID sessione", - "manual_device_verification_device_key_label": "Chiave sessione", - "manual_device_verification_device_name_label": "Nome sessione", - "manual_device_verification_footer": "Se non corrispondono, la sicurezza delle tue comunicazioni potrebbe essere compromessa.", - "manual_device_verification_self_text": "Conferma confrontando il seguente con le impostazioni utente nell'altra sessione:", - "manual_device_verification_user_text": "Conferma questa sessione confrontando il seguente con le sue impostazioni utente:", "no_key_or_device": "Pare che tu non abbia una chiave di sicurezza o altri dispositivi con cui poterti verificare. Questo dispositivo non potrà accedere ai vecchi messaggi cifrati. Per potere verificare la tua ideintità su questo dispositivo, dovrai reimpostare le chiavi di verifica.", "no_support_qr_emoji": "Il dispositivo che stai cercando di verificare non supporta la scansione di un codice QR o la verifica emoji, che sono supportate da %(brand)s. Prova con un client diverso.", "other_party_cancelled": "L'altra parte ha annullato la verifica.", @@ -1045,10 +1038,6 @@ }, "error_user_not_logged_in": "Utente non connesso", "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s ha terminato una trasmissione vocale", - "you": "Hai terminato una trasmissione vocale" - }, "m.call.answer": { "dm": "Chiamata in corso", "user": "%(senderName)s si è unito alla chiamata", @@ -1422,8 +1411,6 @@ "video_rooms_faq2_answer": "Sì, la cronologia della chat viene mostrata assieme al video.", "video_rooms_faq2_question": "Posso usare la chat testuale assieme alla chiamata video?", "video_rooms_feedbackSubheading": "Grazie per aver provato la beta, ti preghiamo di fornire tutti i dettagli possibili in modo da poterla migliorare.", - "voice_broadcast": "Trasmissione vocale", - "voice_broadcast_force_small_chunks": "Forza lunghezza pezzo trasmissione vocale a 15s", "wysiwyg_composer": "Editor in rich text" }, "labs_mjolnir": { @@ -1567,7 +1554,6 @@ "mute_description": "Non riceverai alcuna notifica" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s ha iniziato una trasmissione vocale", "m.key.verification.request": "%(name)s sta richiedendo la verifica" }, "onboarding": { @@ -2141,7 +2127,6 @@ "error_unbanning": "Rimozione ban fallita", "events_default": "Invia messaggi", "invite": "Invita utenti", - "io.element.voice_broadcast_info": "Trasmissioni vocali", "kick": "Rimuovi utenti", "m.call": "Inizia chiamate di %(brand)s", "m.call.member": "Entra in chiamate di %(brand)s", @@ -2784,7 +2769,6 @@ "warning": "<w>ATTENZIONE:</w> <description/>" }, "share": { - "link_title": "Collegamento alla stanza", "permalink_message": "Link al messaggio selezionato", "permalink_most_recent": "Link al messaggio più recente", "title_message": "Condividi messaggio stanza", @@ -2873,13 +2857,6 @@ "upgraderoom": "Aggiorna una stanza ad una nuova versione", "upgraderoom_permission_error": "Non hai l'autorizzazione necessaria per usare questo comando.", "usage": "Utilizzo", - "verify": "Verifica un utente, una sessione e una tupla pubblica", - "verify_mismatch": "ATTENZIONE: VERIFICA CHIAVI FALLITA! La chiave per %(userId)s e per la sessione %(deviceId)s è \"%(fprint)s\" la quale non corriponde con la chiave \"%(fingerprint)s\" fornita. Ciò può significare che le comunicazioni vengono intercettate!", - "verify_nop": "Sessione già verificata!", - "verify_nop_warning_mismatch": "ATTENZIONE: sessione già verificata, ma le chiavi NON CORRISPONDONO!", - "verify_success_description": "La chiave che hai fornito corrisponde alla chiave che hai ricevuto dalla sessione di %(userId)s %(deviceId)s. Sessione contrassegnata come verificata.", - "verify_success_title": "Chiave verificata", - "verify_unknown_pair": "Coppia (utente, sessione) sconosciuta: (%(userId)s, %(deviceId)s)", "view": "Visualizza la stanza con l'indirizzo dato", "whois": "Mostra le informazioni di un utente" }, @@ -3097,10 +3074,6 @@ "error_rendering_message": "Impossibile caricare questo messaggio", "historical_messages_unavailable": "Non puoi vedere i messaggi precedenti", "in_room_name": " in <strong>%(room)s</strong>", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s ha terminato una <a>trasmissione vocale</a>", - "you": "Hai terminato una <a>trasmissione vocale</a>" - }, "io.element.widgets.layout": "%(senderName)s ha aggiornato la disposizione della stanza", "late_event_separator": "Inviato originariamente il %(dateTime)s", "load_error": { @@ -3633,38 +3606,6 @@ "switch_theme_dark": "Passa alla modalità scura", "switch_theme_light": "Passa alla modalità chiara" }, - "voice_broadcast": { - "30s_backward": "30s indietro", - "30s_forward": "30s avanti", - "action": "Trasmissione vocale", - "buffering": "Buffer…", - "confirm_listen_affirm": "Sì, termina la mia registrazione", - "confirm_listen_description": "Se inizi ad ascoltare questa trasmissione in diretta, l'attuale registrazione della tua trasmissione in diretta finirà.", - "confirm_listen_title": "Ascoltare la trasmissione in diretta?", - "confirm_stop_affirm": "Sì, ferma la trasmissione", - "confirm_stop_description": "Vuoi davvero fermare la tua trasmissione in diretta? Verrà terminata la trasmissione e la registrazione completa sarà disponibile nella stanza.", - "confirm_stop_title": "Fermare la trasmissione in diretta?", - "connection_error": "Errore di connessione - Registrazione in pausa", - "failed_already_recording_description": "Stai già registrando una trasmissione vocale. Termina quella in corso per iniziarne una nuova.", - "failed_already_recording_title": "Impossibile iniziare una nuova trasmissione vocale", - "failed_decrypt": "Impossibile decifrare la trasmissione vocale", - "failed_generic": "Impossibile avviare questa trasmissione vocale", - "failed_insufficient_permission_description": "Non hai l'autorizzazione necessaria per iniziare una trasmissione vocale in questa stanza. Contatta un amministratore della stanza per aggiornare le tue autorizzazioni.", - "failed_insufficient_permission_title": "Impossibile iniziare una nuova trasmissione vocale", - "failed_no_connection_description": "Sfortunatamente non riusciamo ad iniziare una registrazione al momento. Riprova più tardi.", - "failed_no_connection_title": "Errore di connessione", - "failed_others_already_recording_description": "Qualcun altro sta già registrando una trasmissione vocale. Aspetta che finisca prima di iniziarne una nuova.", - "failed_others_already_recording_title": "Impossibile iniziare una nuova trasmissione vocale", - "go_live": "Vai in diretta", - "live": "In diretta", - "pause": "sospendi trasmissione vocale", - "play": "avvia trasmissione vocale", - "resume": "riprendi trasmissione vocale" - }, - "voice_message": { - "cant_start_broadcast_description": "Non puoi iniziare un messaggio vocale perché stai registrando una trasmissione in diretta. Termina la trasmissione per potere iniziare un messaggio vocale.", - "cant_start_broadcast_title": "Impossibile iniziare il messaggio vocale" - }, "voip": { "already_in_call": "Già in una chiamata", "already_in_call_person": "Sei già in una chiamata con questa persona.", @@ -3684,7 +3625,6 @@ "camera_disabled": "La tua fotocamera è spenta", "camera_enabled": "La tua fotocamera è ancora attiva", "cannot_call_yourself_description": "Non puoi chiamare te stesso.", - "change_input_device": "Cambia dispositivo di input", "close_lobby": "Chiudi sala d'attesa", "connecting": "In connessione", "connection_lost": "La connessione al server è stata persa", @@ -3703,8 +3643,6 @@ "enable_camera": "Accendi la fotocamera", "enable_microphone": "Riaccendi il microfono", "expand": "Torna alla chiamata", - "failed_call_live_broadcast_description": "Non puoi avviare una chiamata perché stai registrando una trasmissione in diretta. Termina la trasmissione per potere iniziare una chiamata.", - "failed_call_live_broadcast_title": "Impossibile avviare una chiamata", "hangup": "Riaggancia", "hide_sidebar_button": "Nascondi barra laterale", "input_devices": "Dispositivi di input", diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json index 963de355ac..4ac7672e4c 100644 --- a/src/i18n/strings/ja.json +++ b/src/i18n/strings/ja.json @@ -833,7 +833,6 @@ }, "udd": { "interactive_verification_button": "絵文字で認証", - "manual_verification_button": "テキストを使って手動で認証", "other_ask_verify_text": "このユーザーにセッションを認証するよう依頼するか、以下から手動で認証してください。", "other_new_session_text": "%(name)s(%(userId)s)は未認証のセッションにサインインしました:", "own_ask_verify_text": "以下のどれか一つを使って他のセッションを認証します。", @@ -868,12 +867,6 @@ "incoming_sas_dialog_waiting": "相手の承認を待機しています…", "incoming_sas_user_dialog_text_1": "このユーザーを認証すると、信頼済として表示します。ユーザーを信頼すると、より一層安心してエンドツーエンド暗号化を使用することができます。", "incoming_sas_user_dialog_text_2": "このユーザーを認証すると、相手のセッションと自分のセッションを信頼済として表示します。", - "manual_device_verification_device_id_label": "セッションID", - "manual_device_verification_device_key_label": "セッションキー", - "manual_device_verification_device_name_label": "セッション名", - "manual_device_verification_footer": "一致していない場合は、コミュニケーションのセキュリティーが損なわれている可能性があります。", - "manual_device_verification_self_text": "他のセッションのユーザー設定で、以下を比較して承認してください:", - "manual_device_verification_user_text": "ユーザー設定画面で以下を比較し、このユーザーのセッションを承認してください:", "no_key_or_device": "セキュリティーキーもしくは認証可能な端末が設定されていません。この端末では、以前暗号化されたメッセージにアクセスすることができません。この端末で本人確認を行うには、認証用の鍵を再設定する必要があります。", "no_support_qr_emoji": "認証しようとしている端末は、QRコードのスキャンや絵文字による認証をサポートしていませんが、%(brand)sではサポートされています。異なるクライアントで試してください。", "other_party_cancelled": "相手が認証をキャンセルしました。", @@ -983,10 +976,6 @@ } }, "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)sが音声配信を終了しました", - "you": "音声配信を終了しました" - }, "m.call.answer": { "dm": "通話しています", "user": "%(senderName)sが通話に参加しました", @@ -1318,8 +1307,6 @@ "video_rooms_faq1_question": "ビデオ通話ルームの作成方法", "video_rooms_faq2_answer": "はい、会話のタイムラインが動画と並んで表示されます。", "video_rooms_faq2_question": "テキストによる会話も行えますか?", - "voice_broadcast": "音声配信", - "voice_broadcast_force_small_chunks": "音声配信のチャンク長を15秒に強制", "wysiwyg_composer": "リッチテキストエディター" }, "labs_mjolnir": { @@ -1455,7 +1442,6 @@ "mute_description": "通知を送信しません" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)sが音声配信を開始しました", "m.key.verification.request": "%(name)sは認証を要求しています" }, "onboarding": { @@ -1953,7 +1939,6 @@ "error_unbanning": "ブロック解除に失敗しました", "events_default": "メッセージの送信", "invite": "ユーザーの招待", - "io.element.voice_broadcast_info": "音声配信", "kick": "ユーザーの追放", "m.call": "%(brand)s通話を開始", "m.call.member": "%(brand)s通話に参加", @@ -2532,7 +2517,6 @@ "warning": "<w>警告:</w><description/>" }, "share": { - "link_title": "ルームへのリンク", "permalink_message": "選択したメッセージにリンク", "permalink_most_recent": "最新のメッセージにリンク", "title_message": "ルームのメッセージを共有", @@ -2614,13 +2598,6 @@ "upgraderoom": "ルームを新しいバージョンにアップグレード", "upgraderoom_permission_error": "このコマンドを実行するのに必要な権限がありません。", "usage": "用法", - "verify": "ユーザー、セッション、およびpubkeyタプルを認証", - "verify_mismatch": "警告:鍵の認証に失敗しました!提供された鍵「%(fingerprint)s」は、%(userId)sおよびセッション %(deviceId)s の署名鍵「%(fprint)s」と一致しません。通信が傍受されているおそれがあります!", - "verify_nop": "このセッションは認証済です!", - "verify_nop_warning_mismatch": "警告:このセッションは認証済ですが、鍵が一致しません!", - "verify_success_description": "指定された署名鍵は%(userId)sのセッション %(deviceId)s から受け取った鍵と一致します。セッションは認証済です。", - "verify_success_title": "認証済の鍵", - "verify_unknown_pair": "不明な(ユーザー、セッション)ペア:(%(userId)s、%(deviceId)s)", "whois": "ユーザーの情報を表示" }, "space": { @@ -2831,10 +2808,6 @@ "error_rendering_message": "このメッセージを読み込めません", "historical_messages_unavailable": "以前のメッセージは表示できません", "in_room_name": " <strong>%(room)s</strong>内で", - "io.element.voice_broadcast_info": { - "user": "%(senderName)sが<a>音声配信</a>を終了しました", - "you": "<a>音声配信</a>を終了しました" - }, "io.element.widgets.layout": "%(senderName)sがルームのレイアウトを更新しました", "load_error": { "no_permission": "このルームのタイムラインの特定の地点を読み込もうとしましたが、問題のメッセージを閲覧する権限がありません。", @@ -3345,38 +3318,6 @@ "switch_theme_dark": "ダークテーマに切り替える", "switch_theme_light": "ライトテーマに切り替える" }, - "voice_broadcast": { - "30s_backward": "30秒戻す", - "30s_forward": "30秒進める", - "action": "音声配信", - "buffering": "バッファリングしています…", - "confirm_listen_affirm": "はい、録音を終了してください", - "confirm_listen_description": "このライブ配信の視聴を開始すると、現在のライブ配信の録音は終了します。", - "confirm_listen_title": "ライブ配信を視聴しますか?", - "confirm_stop_affirm": "はい、配信を停止します", - "confirm_stop_description": "ライブ配信を終了してよろしいですか?配信を終了し、録音をこのルームで利用できるよう設定します。", - "confirm_stop_title": "ライブ配信を停止しますか?", - "connection_error": "接続エラー - 録音を停止しました", - "failed_already_recording_description": "既に音声配信を録音しています。新しく始めるには現在の音声配信を終了してください。", - "failed_already_recording_title": "新しい音声配信を開始できません", - "failed_decrypt": "音声配信を復号化できません", - "failed_generic": "この音声配信を再生できません", - "failed_insufficient_permission_description": "このルームで音声配信を開始する権限がありません。ルームの管理者に連絡して権限の付与を依頼してください。", - "failed_insufficient_permission_title": "新しい音声配信を開始できません", - "failed_no_connection_description": "録音を開始できません。後でもう一度やり直してください。", - "failed_no_connection_title": "接続エラー", - "failed_others_already_recording_description": "他の人が既に音声配信を録音しています。新しく始めるには音声配信が終わるまで待機してください。", - "failed_others_already_recording_title": "新しい音声配信を開始できません", - "go_live": "ライブ配信", - "live": "ライブ", - "pause": "音声配信を一時停止", - "play": "音声配信を再生", - "resume": "音声配信を再開" - }, - "voice_message": { - "cant_start_broadcast_description": "ライブ配信を録音しているため、音声メッセージを開始できません。音声メッセージの録音を開始するには、ライブ配信を終了してください。", - "cant_start_broadcast_title": "音声メッセージを開始できません" - }, "voip": { "already_in_call": "既に通話中です", "already_in_call_person": "既にこの人と通話中です。", @@ -3396,7 +3337,6 @@ "camera_disabled": "カメラが無効です", "camera_enabled": "カメラがまだ有効です", "cannot_call_yourself_description": "自分自身に通話を発信することはできません。", - "change_input_device": "入力端末を変更", "connecting": "接続しています", "connection_lost": "サーバーとの接続が失われました", "connection_lost_description": "サーバーに接続していないため、通話を発信できません。", @@ -3413,8 +3353,6 @@ "enable_camera": "カメラを有効にする", "enable_microphone": "マイクのミュートを解除", "expand": "通話に戻る", - "failed_call_live_broadcast_description": "ライブ配信を録音しているため、通話を開始できません。通話を開始するには、ライブ配信を終了してください。", - "failed_call_live_broadcast_title": "通話を開始できません", "hangup": "電話を切る", "hide_sidebar_button": "サイドバーを表示しない", "input_devices": "入力装置", diff --git a/src/i18n/strings/lo.json b/src/i18n/strings/lo.json index 6310faf200..ddbe3d17f8 100644 --- a/src/i18n/strings/lo.json +++ b/src/i18n/strings/lo.json @@ -769,12 +769,6 @@ "incoming_sas_dialog_title": "ການຮ້ອງຂໍການຢັ້ງຢືນຂາເຂົ້າ", "incoming_sas_user_dialog_text_1": "ຢັ້ງຢືນຜູ້ໃຊ້ນີ້ເພື່ອສ້າງເຄື່ອງທີ່ເຊື່ອຖືໄດ້. ຜູ້ໃຊ້ທີ່ເຊື່ອຖືໄດ້ ເຮັດໃຫ້ທ່ານອຸ່ນໃຈຂື້ນເມື່ຶຶອເຂົ້າລະຫັດຂໍ້ຄວາມແຕ່ຕົ້ນທາງເຖິງປາຍທາງ.", "incoming_sas_user_dialog_text_2": "ການຢືນຢັນຜູ້ໃຊ້ນີ້ຈະເປັນເຄື່ອງໝາຍໃນລະບົບຂອງເຂົາເຈົ້າໜ້າເຊື່ອຖືໄດ້ ແລະ ເປັນເຄື່ອງໝາຍເຖິງລະບົບຂອງທ່ານ ເປັນທີ່ເຊື່ອຖືໄດ້ຕໍ່ກັບເຂົາເຈົ້າ.", - "manual_device_verification_device_id_label": "ID ລະບົບ", - "manual_device_verification_device_key_label": "ລະຫັດລະບົບ", - "manual_device_verification_device_name_label": "ຊື່ລະບົບ", - "manual_device_verification_footer": "ຖ້າລະຫັດບໍ່ກົງກັນ, ຄວາມປອດໄພຂອງການສື່ສານຂອງທ່ານອາດຈະຖືກທໍາລາຍ.", - "manual_device_verification_self_text": "ຢືນຢັນໂດຍການປຽບທຽບສິ່ງຕໍ່ໄປນີ້ກັບການຕັ້ງຄ່າຜູ້ໃຊ້ໃນລະບົບອື່ນຂອງທ່ານ:", - "manual_device_verification_user_text": "ຢືນຢັນລະບົບຂອງຜູ້ໃຊ້ນີ້ໂດຍການປຽບທຽບສິ່ງຕໍ່ໄປນີ້ກັບການຕັ້ງຄ່າຜູ້ໃຊ້ຂອງເຂົາເຈົ້າ:", "no_key_or_device": "ເບິ່ງຄືວ່າທ່ານບໍ່ມີກະແຈຄວາມປອດໄພ ຫຼື ອຸປະກອນອື່ນໆທີ່ທ່ານສາມາດຢືນຢັນໄດ້. ອຸປະກອນນີ້ຈະບໍ່ສາມາດເຂົ້າເຖິງຂໍ້ຄວາມທີ່ເຂົ້າລະຫັດເກົ່າໄດ້. ເພື່ອຢືນຢັນຕົວຕົນຂອງທ່ານໃນອຸປະກອນນີ້, ທ່ານຈຳເປັນຕ້ອງຕັ້ງລະຫັດຢືນຢັນຂອງທ່ານ.", "no_support_qr_emoji": "ອຸປະກອນທີ່ທ່ານພະຍາຍາມກວດສອບບໍ່ຮອງຮັບການສະແກນລະຫັດ QR ຫຼື ການຢັ້ງຢືນ emoji, ຊຶ່ງເປັນສິ່ງທີ່%(brand)sສະຫນັບສະຫນູນ. ລອງໃຊ້ກັບລູກຄ້າອື່ນ.", "other_party_cancelled": "ອີກຝ່າຍໄດ້ຍົກເລີກການຢັ້ງຢືນ.", @@ -2191,7 +2185,6 @@ "warn_quit": "ເຕືອນກ່ອນຢຸດຕິ" }, "share": { - "link_title": "ເຊື່ອມຕໍ່ທີ່ຫ້ອງ", "permalink_message": "ເຊື່ອມຕໍ່ກັບຂໍ້ຄວາມທີ່ເລືອກ", "permalink_most_recent": "ເຊື່ອມຕໍ່ກັບຂໍ້ຄວາມຫຼ້າສຸດ", "title_message": "ແບ່ງປັນຂໍ້ຄວາມໃນຫ້ອງ", @@ -2273,12 +2266,6 @@ "upgraderoom": "ຍົກລະດັບຫ້ອງເປັນລຸ້ນໃໝ່", "upgraderoom_permission_error": "ທ່ານບໍ່ມີສິດໃຊ້ຄໍາສັ່ງນີ້.", "usage": "ການນໍາໃຊ້", - "verify": "ຢືນຢັນຜູ້ໃຊ້, ລະບົບ, ແລະ pubkey tuple", - "verify_mismatch": "ຄຳເຕືອນ: ການຢືນຢັນບໍ່ສຳເລັັດ! ປຸ່ມເຊັນຊື່ສຳລັບ %(userId)s ແລະ ລະບົບ %(deviceId)s ແມ່ນ \"%(fprint)s\" ບໍ່ກົງກັບລະຫັດທີ່ລະບຸໄວ້ \"%(fingerprint)s\". ນີ້ອາດຈະຫມາຍຄວາມວ່າການສື່ສານຂອງທ່ານຖືກຂັດຂວາງ!", - "verify_nop": "ການຢັ້ງຢືນລະບົບແລ້ວ!", - "verify_success_description": "ກະແຈໄຂລະຫັດທີ່ທ່ານໃຊ້ກົງກັບກະແຈໄຂລະຫັດທີ່ທ່ານໄດ້ຮັບຈາກ %(userId)s ເທິງອຸປະກອນ %(deviceId)s. ລະບົບຢັ້ງຢືນສຳເລັດແລ້ວ.", - "verify_success_title": "ກະແຈທີ່ຢືນຢັນແລ້ວ", - "verify_unknown_pair": "ບໍ່ຮູ້ຈັກ (ຜູ້ໃຊ້, ລະບົບ) ຄູ່: (%(userId)s, %(deviceId)s)", "whois": "ສະແດງຂໍ້ມູນກ່ຽວກັບຜູ້ໃຊ້" }, "space": { diff --git a/src/i18n/strings/lt.json b/src/i18n/strings/lt.json index 8f503bacdd..1e0679bae4 100644 --- a/src/i18n/strings/lt.json +++ b/src/i18n/strings/lt.json @@ -597,12 +597,6 @@ "incoming_sas_dialog_title": "Įeinantis Patikrinimo Prašymas", "incoming_sas_user_dialog_text_1": "Patvirtinkite šį vartotoją, kad pažymėtumėte jį kaip patikimą. Vartotojų pažymėjimas patikimais suteikia jums papildomos ramybės naudojant visapusiškai užšifruotas žinutes.", "incoming_sas_user_dialog_text_2": "Patvirtinant šį vartotoją, jo seansas bus pažymėtas kaip patikimas, taip pat jūsų seansas bus pažymėtas kaip patikimas jam.", - "manual_device_verification_device_id_label": "Seanso ID", - "manual_device_verification_device_key_label": "Seanso raktas", - "manual_device_verification_device_name_label": "Seanso pavadinimas", - "manual_device_verification_footer": "Jei jie nesutampa, gali būti pažeistas jūsų komunikacijos saugumas.", - "manual_device_verification_self_text": "Patvirtinkite, palygindami tai, kas nurodyta toliau, su Vartotojo Nustatymais kitame jūsų seanse:", - "manual_device_verification_user_text": "Patvirtinkite šio vartotojo seansą, palygindami tai, kas nurodyta toliau, su jo Vartotojo Nustatymais:", "no_support_qr_emoji": "Įrenginys, kurį bandote patvirtinti, nepalaiko QR kodo nuskaitymo arba jaustukų patikrinimo, kurį palaiko %(brand)s. Pabandykite naudoti kitą klientą.", "other_party_cancelled": "Kita šalis atšaukė patvirtinimą.", "prompt_encrypted": "Patvirtinkite visus vartotojus kambaryje, kad užtikrintumėte jo saugumą.", @@ -886,8 +880,7 @@ "video_rooms_faq1_answer": "Kairiajame skydelyje esančioje kambarių skiltyje naudokite mygtuką “+”.", "video_rooms_faq1_question": "Kaip galiu sukurti vaizdo kambarį?", "video_rooms_faq2_answer": "Taip, pokalbių laiko juosta rodoma kartu su vaizdu.", - "video_rooms_faq2_question": "Ar galiu naudoti teksto pokalbius kartu su vaizdo skambučiu?", - "voice_broadcast": "Balso transliacija" + "video_rooms_faq2_question": "Ar galiu naudoti teksto pokalbius kartu su vaizdo skambučiu?" }, "labs_mjolnir": { "advanced_warning": "⚠ Šie nustatymai yra skirti pažengusiems vartotojams.", @@ -1859,11 +1852,6 @@ "upgraderoom": "Atnaujina kambarį į naują versiją", "upgraderoom_permission_error": "Jūs neturite reikalingų leidimų naudoti šią komandą.", "usage": "Naudojimas", - "verify": "Patvirtina vartotojo, seanso ir pubkey daugiadalę duomenų struktūrą", - "verify_mismatch": "ĮSPĖJIMAS: RAKTŲ PATIKRINIMAS NEPAVYKO! Pasirašymo raktas vartotojui %(userId)s ir seansui %(deviceId)s yra \"%(fprint)s\", kuris nesutampa su pateiktu raktu \"%(fingerprint)s\". Tai gali reikšti, kad jūsų komunikacijos yra perimamos!", - "verify_nop": "Seansas jau patvirtintas!", - "verify_success_description": "Jūsų pateiktas pasirašymo raktas sutampa su pasirašymo raktu, gautu iš vartotojo %(userId)s seanso %(deviceId)s. Seansas pažymėtas kaip patikrintas.", - "verify_success_title": "Patvirtintas raktas", "whois": "Parodo informaciją apie vartotoją" }, "space": { @@ -2385,15 +2373,6 @@ "user_menu": { "settings": "Visi nustatymai" }, - "voice_broadcast": { - "action": "Balso transliacija", - "confirm_stop_affirm": "Taip, sustabdyti transliaciją", - "confirm_stop_title": "Sustabdyti transliaciją gyvai?", - "live": "Gyvai", - "pause": "pristabdyti balso transliaciją", - "play": "paleisti balso transliaciją", - "resume": "tęsti balso transliaciją" - }, "voip": { "already_in_call": "Jau pokalbyje", "already_in_call_person": "Jūs jau esate pokalbyje su šiuo asmeniu.", diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index 1231a9a330..ebc2f39623 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -774,7 +774,6 @@ }, "udd": { "interactive_verification_button": "Interactief verifiëren door emoji", - "manual_verification_button": "Handmatig verifiëren via tekst", "other_ask_verify_text": "Vraag deze persoon de sessie te verifiëren, of verifieer het handmatig hieronder.", "other_new_session_text": "%(name)s%(userId)s heeft zich aangemeld bij een nieuwe sessie zonder deze te verifiëren:", "own_ask_verify_text": "Verifieer je andere sessie op een van onderstaande wijzen.", @@ -806,12 +805,6 @@ "incoming_sas_dialog_title": "Inkomend verificatieverzoek", "incoming_sas_user_dialog_text_1": "Verifieer deze persoon om als vertrouwd te markeren. Personen vertrouwen geeft je extra zekerheid bij het gebruik van eind-tot-eind-versleutelde berichten.", "incoming_sas_user_dialog_text_2": "Deze persoon verifiëren zal de sessie als vertrouwd markeren voor jullie beide.", - "manual_device_verification_device_id_label": "Sessie-ID", - "manual_device_verification_device_key_label": "Sessiesleutel", - "manual_device_verification_device_name_label": "Sessienaam", - "manual_device_verification_footer": "Als deze niet overeenkomen, dan wordt deze sessie mogelijk door iemand anders onderschept.", - "manual_device_verification_self_text": "Bevestig door het volgende te vergelijken met de persoonsinstellingen in je andere sessie:", - "manual_device_verification_user_text": "Bevestig de sessie van deze persoon door het volgende te vergelijken met zijn persoonsinstellingen:", "no_key_or_device": "Het lijkt erop dat je geen veiligheidssleutel hebt of andere apparaten waarmee je kunt verifiëren. Dit apparaat heeft geen toegang tot oude versleutelde berichten. Om je identiteit op dit apparaat te verifiëren, moet je jouw verificatiesleutels opnieuw instellen.", "no_support_qr_emoji": "Het apparaat dat je probeert te verifiëren ondersteund niet de door %(brand)s ondersteunde methodes: scannen van een QR-code of emoji verificatie. Probeer het met een andere app.", "other_party_cancelled": "De tegenpartij heeft de verificatie geannuleerd.", @@ -1214,8 +1207,7 @@ "video_rooms_faq1_answer": "Gebruik de knop \"+\" in het kamergedeelte van het linkerpaneel.", "video_rooms_faq1_question": "Hoe kan ik een videokamer maken?", "video_rooms_faq2_answer": "Ja, de gesprekstijdslijn wordt naast de video weergegeven.", - "video_rooms_faq2_question": "Kan ik tekstberichten gebruiken naast het videogesprek?", - "voice_broadcast": "Spraakuitzending" + "video_rooms_faq2_question": "Kan ik tekstberichten gebruiken naast het videogesprek?" }, "labs_mjolnir": { "advanced_warning": "⚠ Deze instellingen zijn bedoeld voor gevorderde personen.", @@ -1824,7 +1816,6 @@ "error_unbanning": "Ontbannen mislukt", "events_default": "Berichten versturen", "invite": "Personen uitnodigen", - "io.element.voice_broadcast_info": "Spraakuitzendingen", "kick": "Personen verwijderen", "m.call": "%(brand)s oproepen starten", "m.call.member": "Deelnemen aan %(brand)s gesprekken", @@ -2363,7 +2354,6 @@ "warn_quit": "Waarschuwen voordat je afsluit" }, "share": { - "link_title": "Link naar kamer", "permalink_message": "Koppeling naar geselecteerd bericht", "permalink_most_recent": "Koppeling naar meest recente bericht", "title_message": "Bericht uit kamer delen", @@ -2445,12 +2435,6 @@ "upgraderoom": "Upgrade deze kamer naar een nieuwere versie", "upgraderoom_permission_error": "Je beschikt niet over de vereiste machtigingen om deze opdracht uit te voeren.", "usage": "Gebruik", - "verify": "Verifieert de combinatie van persoon, sessie en publieke sleutel", - "verify_mismatch": "PAS OP: sleutelverificatie MISLUKT! De combinatie %(userId)s + sessie %(deviceId)s is ondertekend met ‘%(fprint)s’ - maar de opgegeven sleutel is ‘%(fingerprint)s’. Wellicht worden jouw berichten onderschept!", - "verify_nop": "Sessie al geverifieerd!", - "verify_success_description": "De door jou verschafte sleutel en de van %(userId)ss sessie %(deviceId)s verkregen sleutels komen overeen. De sessie is daarmee geverifieerd.", - "verify_success_title": "Geverifieerde sleutel", - "verify_unknown_pair": "Onbekend paar (persoon, sessie): (%(userId)s, %(deviceId)s)", "whois": "Geeft informatie weer over een persoon" }, "space": { @@ -3155,20 +3139,6 @@ "switch_theme_dark": "Naar donkere modus wisselen", "switch_theme_light": "Naar lichte modus wisselen" }, - "voice_broadcast": { - "action": "Spraakuitzending", - "confirm_stop_affirm": "Ja, stop uitzending", - "confirm_stop_title": "Live uitzending stoppen?", - "failed_already_recording_description": "U neemt al een spraakuitzending op. Beëindig uw huidige spraakuitzending om een nieuwe te starten.", - "failed_already_recording_title": "Kan geen nieuwe spraakuitzending starten", - "failed_insufficient_permission_description": "U heeft niet de vereiste rechten om een spraakuitzending in deze kamer te starten. Neem contact op met een kamer beheerder om uw machtiging aan te passen.", - "failed_insufficient_permission_title": "Kan geen nieuwe spraakuitzending starten", - "failed_others_already_recording_description": "Iemand anders neemt al een spraakuitzending op. Wacht tot de spraakuitzending is afgelopen om een nieuwe te starten.", - "failed_others_already_recording_title": "Kan geen nieuwe spraakuitzending starten", - "pause": "spraakuitzending pauzeren", - "play": "spraakuitzending afspelen", - "resume": "hervat spraakuitzending" - }, "voip": { "already_in_call": "Al in de oproep", "already_in_call_person": "Je bent al in gesprek met deze persoon.", diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index e148c873f1..66ac807080 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -340,7 +340,7 @@ "set_email_prompt": "Czy chcesz ustawić adres e-mail?", "sign_in_description": "Użyj swojego konta, aby kontynuować.", "sign_in_instead": "Zamiast tego zaloguj się", - "sign_in_instead_prompt": "Zamiast tego zaloguj się", + "sign_in_instead_prompt": "Masz już konto? <a>Zaloguj się tutaj</a>", "sign_in_or_register": "Zaloguj się lub utwórz konto", "sign_in_or_register_description": "Użyj konta lub utwórz nowe, aby kontynuować.", "sign_in_prompt": "Posiadasz już konto? <a>Zaloguj się</a>", @@ -505,6 +505,7 @@ "matrix": "Matrix", "message": "Wiadomość", "message_layout": "Wygląd wiadomości", + "message_timestamp_invalid": "Nieprawidłowy znacznik czasu", "microphone": "Mikrofon", "model": "Model", "modern": "Współczesny", @@ -908,6 +909,8 @@ "warning": "Jeżeli nie ustawiłeś nowej metody odzyskiwania, atakujący może uzyskać dostęp do Twojego konta. Zmień hasło konta i natychmiast ustaw nową metodę odzyskiwania w Ustawieniach." }, "not_supported": "<niewspierany>", + "pinned_identity_changed": "Tożsamość użytkownika %(displayName)s (<b>%(userId)s</b>) uległa zmianie. <a>Dowiedz się więcej</a>", + "pinned_identity_changed_no_displayname": "Tożsamość użytkownika <b>%(userId)s</b> uległa zmianie <a>Dowiedz się więcej</a>", "recovery_method_removed": { "description_1": "Ta sesja wykryła, że Twoja fraza bezpieczeństwa i klucz dla bezpiecznych wiadomości zostały usunięte.", "description_2": "Jeśli zrobiłeś to przez pomyłkę, możesz ustawić bezpieczne wiadomości w tej sesji, co zaszyfruje ponownie historię wiadomości za pomocą nowej metody odzyskiwania.", @@ -923,7 +926,6 @@ }, "udd": { "interactive_verification_button": "Zweryfikuj interaktywnie za pomocą emoji", - "manual_verification_button": "Zweryfikuj ręcznie za pomocą tekstu", "other_ask_verify_text": "Poproś go/ją o zweryfikowanie tej sesji bądź zweryfikuj ją osobiście poniżej.", "other_new_session_text": "%(name)s%(userId)s zalogował się do nowej sesji bez zweryfikowania jej:", "own_ask_verify_text": "Zweryfikuj swoje pozostałe sesje używając jednej z opcji poniżej.", @@ -958,12 +960,6 @@ "incoming_sas_dialog_waiting": "Oczekiwanie na potwierdzenie partnera…", "incoming_sas_user_dialog_text_1": "Zweryfikuj tego użytkownika, aby oznaczyć go jako zaufanego. Użytkownicy zaufani dodają większej pewności, gdy korzystasz z wiadomości szyfrowanych end-to-end.", "incoming_sas_user_dialog_text_2": "Weryfikacja tego użytkownika oznaczy Twoją i jego sesję jako zaufaną.", - "manual_device_verification_device_id_label": "Identyfikator sesji", - "manual_device_verification_device_key_label": "Klucz sesji", - "manual_device_verification_device_name_label": "Nazwa sesji", - "manual_device_verification_footer": "Jeśli nie pasują, bezpieczeństwo twojego konta mogło zostać zdradzone.", - "manual_device_verification_self_text": "Potwierdź porównując następujące elementy w ustawieniach użytkownika w drugiej sesji:", - "manual_device_verification_user_text": "Potwierdź sesję tego użytkownika, porównując następujące elementy w jego ustawieniach użytkownika:", "no_key_or_device": "Wygląda na to, że nie masz klucza bezpieczeństwa ani żadnych innych urządzeń, które mogą weryfikować Twoją tożsamość. To urządzenie nie będzie mogło uzyskać dostępu do wcześniejszych zaszyfrowanych wiadomości. Aby zweryfikować swoją tożsamość na tym urządzeniu, należy zresetować klucze weryfikacyjne.", "no_support_qr_emoji": "Urządzenie, które próbujesz zweryfikować nie wspiera skanowania kodu QR lub weryfikacji emoji, czyli tego co obsługuje %(brand)s. Spróbuj użyć innego klienta.", "other_party_cancelled": "Druga strona anulowała weryfikację.", @@ -1003,7 +999,7 @@ "unverified_sessions_toast_description": "Sprawdź, by upewnić się że Twoje konto jest bezpieczne", "unverified_sessions_toast_reject": "Później", "unverified_sessions_toast_title": "Masz niezweryfikowane sesje", - "verification_description": "Zweryfikuj swoją tożsamość, aby uzyskać dostęp do wiadomości szyfrowanych i potwierdzić swoją tożsamość innym.", + "verification_description": "Zweryfikuj swoją tożsamość, aby uzyskać dostęp do wiadomości szyfrowanych i potwierdzić swoją tożsamość innym. Jeśli korzystasz z urządzenia mobilnego, otwórz na niej aplikację.", "verification_dialog_title_device": "Zweryfikuj drugie urządzenie", "verification_dialog_title_user": "Żądanie weryfikacji", "verification_skip_warning": "Bez weryfikacji, nie będziesz posiadać dostępu do wszystkich swoich wiadomości, a inni będą Cię widzieć jako niezaufanego.", @@ -1088,10 +1084,6 @@ }, "error_user_not_logged_in": "Użytkownik nie jest zalogowany", "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s zakończył transmisję głosową", - "you": "Zakończyłeś transmisje na żywo" - }, "m.call.answer": { "dm": "Połączenie w trakcie", "user": "%(senderName)s dołączył do połączenia", @@ -1113,7 +1105,15 @@ "you": "Dodano reakcję %(reaction)s do %(message)s" }, "m.sticker": "%(senderName)s: %(stickerName)s", - "m.text": "%(senderName)s: %(message)s" + "m.text": "%(senderName)s: %(message)s", + "prefix": { + "audio": "Audio", + "file": "Plik", + "image": "Obraz", + "poll": "Ankieta", + "video": "Wideo" + }, + "preview": "<bold>%(prefix)s:</bold> %(preview)s" }, "export_chat": { "cancelled": "Eksport został anulowany", @@ -1490,8 +1490,6 @@ "video_rooms_faq2_answer": "Tak, oś czasu czatu jest wyświetlana wraz z wideo.", "video_rooms_faq2_question": "Czy mogę używać kanału tekstowego jednocześnie rozmawiając na kanale wideo?", "video_rooms_feedbackSubheading": "Dziękujemy za wypróbowanie wersji beta, opisz wnikliwie swoje doświadczenia i pomóż nam ulepszyć nasz produkt.", - "voice_broadcast": "Transmisja głosowa", - "voice_broadcast_force_small_chunks": "Wymuś 15s długość kawałków dla transmisji głosowej", "wysiwyg_composer": "Bogaty edytor tekstu" }, "labs_mjolnir": { @@ -1637,7 +1635,6 @@ "mute_description": "Nie otrzymasz żadnych powiadomień" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s rozpoczął transmisję głosową", "m.key.verification.request": "%(name)s prosi o weryfikację" }, "onboarding": { @@ -2255,7 +2252,6 @@ "error_unbanning": "Nie udało się odbanować", "events_default": "Wysyłanie wiadomości", "invite": "Zapraszanie użytkowników", - "io.element.voice_broadcast_info": "Transmisje głosowe", "kick": "Usuń użytkowników", "m.call": "Rozpocznij połączenie %(brand)s", "m.call.member": "Dołącz do połączeń %(brand)s", @@ -2954,7 +2950,7 @@ "warning": "<w>OSTRZEŻENIE:</w> <description/>" }, "share": { - "link_title": "Link do pokoju", + "link_copied": "Skopiowano link", "permalink_message": "Link do zaznaczonej wiadomości", "permalink_most_recent": "Link do najnowszej wiadomości", "share_call": "Link zaproszenia do konferencji", @@ -3046,13 +3042,6 @@ "upgraderoom": "Ulepsza pokój do nowej wersji", "upgraderoom_permission_error": "Nie posiadasz wymaganych uprawnień do użycia tego polecenia.", "usage": "Użycie", - "verify": "Weryfikuje użytkownika, sesję oraz klucz publiczny", - "verify_mismatch": "OSTRZEŻENIE: WERYFIKACJA KLUCZY NIE POWIODŁA SIĘ! Klucz podpisujący dla %(userId)s oraz sesji %(deviceId)s to \"%(fprint)s\", nie pasuje on do podanego klucza \"%(fingerprint)s\". To może oznaczać że Twoja komunikacja jest przechwytywana!", - "verify_nop": "Sesja już zweryfikowana!", - "verify_nop_warning_mismatch": "OSTRZEŻENIE: sesja została już zweryfikowana, ale klucze NIE PASUJĄ!", - "verify_success_description": "Klucz podpisujący, który podano jest taki sam jak klucz podpisujący otrzymany od %(userId)s oraz sesji %(deviceId)s. Sesja została oznaczona jako zweryfikowana.", - "verify_success_title": "Zweryfikowany klucz", - "verify_unknown_pair": "Nieznana para (użytkownik, sesja): (%(userId)s, %(deviceId)s)", "view": "Przegląda pokój z podanym adresem", "whois": "Pokazuje informacje na temat użytkownika" }, @@ -3270,8 +3259,8 @@ "historical_event_no_key_backup": "Historia wiadomości nie jest dostępna na tym urządzeniu", "historical_event_unverified_device": "Musisz zweryfikować to urządzenie, aby wyświetlić historię wiadomości", "historical_event_user_not_joined": "Nie masz dostępu do tej wiadomości", - "sender_identity_previously_verified": "Zweryfikowana tożsamość uległa zmianie", - "sender_unsigned_device": "Zaszyfrowano przez urządzenie niezweryfikowane przez właściciela.", + "sender_identity_previously_verified": "Zweryfikowana tożsamość nadawcy uległa zmianie", + "sender_unsigned_device": "Wysłano z niezabezpieczonego urządzenia.", "unable_to_decrypt": "Nie można rozszyfrować wiadomości" }, "disambiguated_profile": "%(displayName)s (%(matrixId)s)", @@ -3289,10 +3278,6 @@ "error_rendering_message": "Nie można wczytać tej wiadomości", "historical_messages_unavailable": "Nie możesz widzieć poprzednich wiadomości", "in_room_name": " w <strong>%(room)s</strong>", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s zakończył <a>transmisję głosową</a>", - "you": "Zakończyłeś <a>transmisje głosową</a>" - }, "io.element.widgets.layout": "%(senderName)s zmienił układ pokoju", "late_event_separator": "Pierwotnie wysłano %(dateTime)s", "load_error": { @@ -3747,6 +3732,7 @@ "error_files_too_large": "Te pliki są <b>zbyt duże</b> do wysłania. Ograniczenie wielkości plików to %(limit)s.", "error_some_files_too_large": "Niektóre pliki są <b>zbyt duże</b> do wysłania. Ograniczenie wielkości plików to %(limit)s.", "error_title": "Błąd wysyłania", + "not_image": "Wybrany plik nie jest prawidłowym plikiem obrazu.", "title": "Prześlij pliki", "title_progress": "Prześlij pliki (%(current)s z %(total)s)", "upload_all_button": "Prześlij wszystko", @@ -3842,38 +3828,6 @@ "switch_theme_dark": "Przełącz na tryb ciemny", "switch_theme_light": "Przełącz na tryb jasny" }, - "voice_broadcast": { - "30s_backward": "30s do tyłu", - "30s_forward": "30s do przodu", - "action": "Transmisja głosowa", - "buffering": "Buforowanie…", - "confirm_listen_affirm": "Tak, zakończ moje nagranie", - "confirm_listen_description": "Jeśli zaczniesz słuchać tej transmisji na żywo, twoja bieżąca transmisja na żywo zostanie zakończona.", - "confirm_listen_title": "Zacząć słuchać transmisji na żywo?", - "confirm_stop_affirm": "Tak, zakończ transmisję", - "confirm_stop_description": "Czy na pewno chcesz zakończyć transmisję na żywo? Transmisja zostanie zakończona, a całe nagranie będzie dostępne w pokoju.", - "confirm_stop_title": "Zakończyć transmisję na żywo?", - "connection_error": "Błąd połączenia - Nagrywanie wstrzymane", - "failed_already_recording_description": "Już nagrywasz transmisję głosową. Zakończ bieżącą transmisję głosową, aby rozpocząć nową.", - "failed_already_recording_title": "Nie można rozpocząć nowej transmisji głosowej", - "failed_decrypt": "Nie można rozszyfrować transmisji głosowej", - "failed_generic": "Nie można odtworzyć tej transmisji głosowej", - "failed_insufficient_permission_description": "Nie posiadasz wymaganych uprawnień, aby rozpocząć transmisję głosową w tym pokoju. Skontaktuj się z administratorem pokoju, aby zwiększyć swoje uprawnienia.", - "failed_insufficient_permission_title": "Nie można rozpocząć nowej transmisji głosowej", - "failed_no_connection_description": "Niestety, nie jesteśmy w stanie rozpocząć nowego nagrania. Spróbuj ponownie później.", - "failed_no_connection_title": "Błąd połączenia", - "failed_others_already_recording_description": "Ktoś już nagrywa transmisję głosową. Aby rozpocząć nową, poczekaj aż bieżąca się skończy.", - "failed_others_already_recording_title": "Nie można rozpocząć nowej transmisji głosowej", - "go_live": "Rozpocznij transmisję", - "live": "Na żywo", - "pause": "wstrzymaj transmisję głosową", - "play": "odtwórz transmisję głosową", - "resume": "wznów transmisję głosową" - }, - "voice_message": { - "cant_start_broadcast_description": "Nie możesz rozpocząć wiadomości głosowej, ponieważ już nagrywasz transmisję na żywo. Zakończ transmisję na żywo, aby rozpocząć nagrywanie wiadomości głosowej.", - "cant_start_broadcast_title": "Nie można rozpocząć wiadomości głosowej" - }, "voip": { "already_in_call": "Już dzwoni", "already_in_call_person": "Prowadzisz już rozmowę z tą osobą.", @@ -3893,7 +3847,6 @@ "camera_disabled": "Twoja kamera jest wyłączona", "camera_enabled": "Twoja kamera jest nadal włączona", "cannot_call_yourself_description": "Nie możesz wykonać połączenia do siebie.", - "change_input_device": "Zmień urządzenie wejściowe", "close_lobby": "Zamknij poczekalnię", "connecting": "Łączenie", "connection_lost": "Połączenie z serwerem zostało przerwane", @@ -3912,8 +3865,6 @@ "enable_camera": "Włącz kamerę", "enable_microphone": "Wyłącz wyciszenie mikrofonu", "expand": "Wróć do połączenia", - "failed_call_live_broadcast_description": "Nie możesz rozpocząć połączenia, ponieważ już nagrywasz transmisję na żywo. Zakończ transmisję na żywo, aby rozpocząć połączenie.", - "failed_call_live_broadcast_title": "Nie można rozpocząć połączenia", "get_call_link": "Udostępnij link do połączenia", "hangup": "Rozłącz", "hide_sidebar_button": "Ukryj pasek boczny", diff --git a/src/i18n/strings/pt_BR.json b/src/i18n/strings/pt_BR.json index 6789fb4ee6..7813d6146c 100644 --- a/src/i18n/strings/pt_BR.json +++ b/src/i18n/strings/pt_BR.json @@ -674,12 +674,6 @@ "incoming_sas_dialog_title": "Recebendo solicitação de confirmação", "incoming_sas_user_dialog_text_1": "Confirme este usuário para torná-lo confiável. Confiar nos usuários fornece segurança adicional ao trocar mensagens criptografadas de ponta a ponta.", "incoming_sas_user_dialog_text_2": "Se você confirmar esse usuário, a sessão será marcada como confiável para você e para ele.", - "manual_device_verification_device_id_label": "Identificador de sessão", - "manual_device_verification_device_key_label": "Chave da sessão", - "manual_device_verification_device_name_label": "Nome da sessão", - "manual_device_verification_footer": "Se eles não corresponderem, a segurança da sua comunicação pode estar comprometida.", - "manual_device_verification_self_text": "Para confirmar, compare a seguinte informação com aquela apresentada em sua outra sessão:", - "manual_device_verification_user_text": "Confirme a sessão deste usuário comparando o seguinte com as configurações deste usuário:", "other_party_cancelled": "Seu contato cancelou a confirmação.", "prompt_encrypted": "Verifique todos os usuários em uma sala para se certificar de que ela está segura.", "prompt_self": "Iniciar a confirmação novamente, após a notificação.", @@ -765,10 +759,6 @@ } }, "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s encerrou uma transmissão de voz", - "you": "Você encerrou uma transmissão de voz" - }, "m.call.answer": { "dm": "Chamada em andamento", "user": "%(senderName)s entrou na chamada", @@ -1082,7 +1072,6 @@ "mute_description": "Você não receberá nenhuma notificação" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s iniciou uma transmissão de voz", "m.key.verification.request": "%(name)s está solicitando confirmação" }, "onboarding": { @@ -1970,12 +1959,6 @@ "upgraderoom": "Atualiza a sala para uma nova versão", "upgraderoom_permission_error": "Você não tem as permissões necessárias para usar este comando.", "usage": "Uso", - "verify": "Confirma um usuário, sessão, e chave criptografada pública", - "verify_mismatch": "ATENÇÃO: A CONFIRMAÇÃO DA CHAVE FALHOU! A chave de assinatura para %(userId)s e sessão %(deviceId)s é \"%(fprint)s\", o que não corresponde à chave fornecida \"%(fingerprint)s\". Isso pode significar que suas comunicações estejam sendo interceptadas por terceiros!", - "verify_nop": "Sessão já confirmada!", - "verify_success_description": "A chave de assinatura que você forneceu corresponde à chave de assinatura que você recebeu da sessão %(deviceId)s do usuário %(userId)s. Esta sessão foi marcada como confirmada.", - "verify_success_title": "Chave confirmada", - "verify_unknown_pair": "Par desconhecido (usuário, sessão): (%(userId)s, %(deviceId)s)", "whois": "Exibe informação sobre um usuário" }, "space": { @@ -2101,10 +2084,6 @@ "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", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s encerrou uma <a>transmissão de voz</a>", - "you": "Você encerrou uma <a>transmissão de voz</a>" - }, "io.element.widgets.layout": "%(senderName)s atualizou o layout da sala", "load_error": { "no_permission": "Não foi possível carregar um trecho específico da conversa desta sala, porque parece que você não tem permissão para ler a mensagem em questão.", @@ -2537,21 +2516,6 @@ "switch_theme_dark": "Alternar para o modo escuro", "switch_theme_light": "Alternar para o modo claro" }, - "voice_broadcast": { - "confirm_listen_description": "Se você começar a ouvir esta tramissão ao vivo, a gravação desta transmissão, será encerrada.", - "confirm_listen_title": "Ouvir transmissão ao vivo?", - "confirm_stop_affirm": "Sim, interromper a transmissão", - "confirm_stop_title": "Parar a transmissão ao vivo?", - "failed_already_recording_description": "Você já está gravando uma transmissão de voz. Encerre sua transmissão de voz atual para iniciar uma nova.", - "failed_already_recording_title": "Não é possível iniciar uma nova transmissão de voz", - "failed_insufficient_permission_description": "Você não tem as permissões necessárias para iniciar uma transmissão de voz nesta sala. Entre em contato com um administrador de sala para atualizar suas permissões.", - "failed_insufficient_permission_title": "Não é possível iniciar uma nova transmissão de voz", - "failed_no_connection_description": "Infelizmente, não podemos iniciar uma gravação agora. Por favor, tente novamente mais tarde.", - "failed_no_connection_title": "Erro de conexão", - "failed_others_already_recording_description": "Outra pessoa já está gravando uma transmissão de voz. Aguarde o término da transmissão de voz para iniciar uma nova.", - "failed_others_already_recording_title": "Não é possível iniciar uma nova transmissão de voz", - "live": "Ao vivo" - }, "voip": { "already_in_call": "Já em um chamada", "already_in_call_person": "Você já está em uma chamada com essa pessoa.", @@ -2582,8 +2546,6 @@ "enable_camera": "Ligar câmera", "enable_microphone": "Habilitar microfone", "expand": "Retornar para a chamada", - "failed_call_live_broadcast_description": "Você não pode iniciar uma chamada porque está gravando uma transmissão ao vivo. Termine sua transmissão ao vivo para iniciar uma chamada.", - "failed_call_live_broadcast_title": "Não é possível iniciar uma chamada", "hangup": "Desligar", "hide_sidebar_button": "Esconder a barra lateral", "join_button_tooltip_connecting": "Conectando", diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index 50a3ee50e9..555cb3d1f9 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -877,7 +877,6 @@ }, "udd": { "interactive_verification_button": "Интерактивная сверка по смайлам", - "manual_verification_button": "Ручная сверка по тексту", "other_ask_verify_text": "Попросите этого пользователя подтвердить сеанс или подтвердите его вручную ниже.", "other_new_session_text": "%(name)s (%(userId)s) произвел(а) вход через новый сеанс без подтверждения:", "own_ask_verify_text": "Подтвердите ваш другой сеанс, используя один из вариантов ниже.", @@ -912,12 +911,6 @@ "incoming_sas_dialog_waiting": "Ожидаем подтверждения от партнера…", "incoming_sas_user_dialog_text_1": "Проверить этого пользователя, чтобы отметить его, как доверенного. Доверенные пользователи дают вам больше уверенности при использовании шифрованных сообщений.", "incoming_sas_user_dialog_text_2": "Подтверждение этого пользователя сделает его сеанс доверенным у вас, а также сделает ваш сеанс доверенным у него.", - "manual_device_verification_device_id_label": "ID сеанса", - "manual_device_verification_device_key_label": "Ключ сеанса", - "manual_device_verification_device_name_label": "Название сеанса", - "manual_device_verification_footer": "Если они не совпадают, безопасность вашего общения может быть поставлена под угрозу.", - "manual_device_verification_self_text": "Сравните следующие параметры с \"Пользовательскими настройками\" в другом вашем сеансе:", - "manual_device_verification_user_text": "Подтвердите сеанс этого пользователя, сравнив следующие параметры с его \"Пользовательскими настройками\":", "no_key_or_device": "Похоже, у вас нет бумажного ключа, или других сеансов, с которыми вы могли бы свериться. В этом сеансе вы не сможете получить доступ к старым зашифрованным сообщениям. Чтобы подтвердить свою личность в этом сеансе, вам нужно будет сбросить свои ключи шифрования.", "no_support_qr_emoji": "Устройство, которое вы пытаетесь проверить, не поддерживает сканирование QR-кода или проверку смайликов, которые поддерживает %(brand)s. Попробуйте использовать другой клиент.", "other_party_cancelled": "Другая сторона отменила проверку.", @@ -1036,10 +1029,6 @@ }, "error_user_not_logged_in": "Пользователь не вошел в систему", "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s завершил(а) голосовую трансляцию", - "you": "Вы завершили голосовую трансляцию" - }, "m.call.answer": { "dm": "Звонок в процессе", "user": "%(senderName)s присоединился(лась) к звонку", @@ -1412,8 +1401,6 @@ "video_rooms_faq2_answer": "Да, лента сообщений отображается рядом с видео.", "video_rooms_faq2_question": "Можно ли использовать текстовый чат одновременно с видеозвонком?", "video_rooms_feedbackSubheading": "Спасибо, что попробовали бета-версию. Пожалуйста, расскажите как можно подробнее, чтобы мы могли ее улучшить.", - "voice_broadcast": "Голосовая трансляция", - "voice_broadcast_force_small_chunks": "Длина фрагмента голосовой трансляции 15s", "wysiwyg_composer": "Наглядный текстовый редактор" }, "labs_mjolnir": { @@ -1550,7 +1537,6 @@ "mute_description": "Вы не будете получать никаких уведомлений" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s начал(а) голосовую трансляцию", "m.key.verification.request": "%(name)s запрашивает проверку" }, "onboarding": { @@ -2126,7 +2112,6 @@ "error_unbanning": "Не удалось разблокировать", "events_default": "Отправить сообщения", "invite": "Пригласить пользователей", - "io.element.voice_broadcast_info": "Голосовые трансляции", "kick": "Удалять пользователей", "m.call": "Начать %(brand)s звонок", "m.call.member": "Присоединяйтесь к %(brand)s звонку", @@ -2769,7 +2754,6 @@ "warning": "<w>ВНИМАНИЕ:</w> <description/>" }, "share": { - "link_title": "Ссылка на комнату", "permalink_message": "Ссылка на выбранное сообщение", "permalink_most_recent": "Ссылка на последнее сообщение", "title_message": "Поделиться сообщением", @@ -2858,13 +2842,6 @@ "upgraderoom": "Обновляет комнату до новой версии", "upgraderoom_permission_error": "У вас нет необходимых разрешений для использования этой команды.", "usage": "Использование", - "verify": "Проверяет пользователя, сеанс и публичные ключи", - "verify_mismatch": "ВНИМАНИЕ: ПРОВЕРКА КЛЮЧА НЕ ПРОШЛА! Ключом подписи для %(userId)s и сеанса %(deviceId)s является \"%(fprint)s\", что не соответствует указанному ключу \"%(fingerprint)s\". Это может означать, что ваши сообщения перехватываются!", - "verify_nop": "Сеанс уже подтверждён!", - "verify_nop_warning_mismatch": "ВНИМАНИЕ: сеанс уже заверен, но ключи НЕ СОВПАДАЮТ!", - "verify_success_description": "Ключ подписи, который вы предоставили, соответствует ключу подписи, который вы получили от пользователя %(userId)s через сеанс %(deviceId)s. Сеанс отмечен как подтверждённый.", - "verify_success_title": "Ключ проверен", - "verify_unknown_pair": "Неизвестная пара (пользователь, сеанс): (%(userId)s, %(deviceId)s)", "view": "Просмотр комнаты с указанным адресом", "whois": "Показать информацию о пользователе" }, @@ -3080,10 +3057,6 @@ "error_rendering_message": "Не удалось загрузить это сообщение", "historical_messages_unavailable": "Вы не можете просматривать более старые сообщения", "in_room_name": " в <strong>%(room)s</strong>", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s завершил(а) <a>голосовую трансляцию</a>", - "you": "Вы завершили <a>голосовую трансляцию</a>" - }, "io.element.widgets.layout": "%(senderName)s обновил(а) макет комнаты", "late_event_separator": "Первоначально отправлено %(dateTime)s", "load_error": { @@ -3618,38 +3591,6 @@ "switch_theme_dark": "Переключить в тёмный режим", "switch_theme_light": "Переключить в светлый режим" }, - "voice_broadcast": { - "30s_backward": "30с назад", - "30s_forward": "30с вперёд", - "action": "Голосовая трансляция", - "buffering": "Буферизация…", - "confirm_listen_affirm": "Да, закончить мою запись", - "confirm_listen_description": "Если вы начнете слушать данную трансляцию, то текущая запись прямой трансляции будет завершена.", - "confirm_listen_title": "Слушать прямой эфир?", - "confirm_stop_affirm": "Да, остановить трансляцию", - "confirm_stop_description": "Вы действительно хотите прекратить прямую трансляцию? На этом трансляция закончится и полная запись будет доступна в комнате.", - "confirm_stop_title": "Закончить голосовую трансляцию?", - "connection_error": "Ошибка подключения — запись приостановлена", - "failed_already_recording_description": "Вы уже записываете голосовую трансляцию. Пожалуйста, завершите текущую голосовую трансляцию, чтобы начать новую.", - "failed_already_recording_title": "Не получилось начать новую голосовую трансляцию", - "failed_decrypt": "Невозможно расшифровать голосовую трансляцию", - "failed_generic": "Невозможно воспроизвести эту голосовую трансляцию", - "failed_insufficient_permission_description": "У вас нет необходимых разрешений, чтобы начать голосовую трансляцию в этой комнате. Свяжитесь с администратором комнаты для получения разрешений.", - "failed_insufficient_permission_title": "Не получилось начать новую голосовую трансляцию", - "failed_no_connection_description": "К сожалению, сейчас мы не можем начать запись. Пожалуйста, попробуйте позже.", - "failed_no_connection_title": "Ошибка подключения", - "failed_others_already_recording_description": "Кто-то уже записывает голосовую трансляцию. Ждите окончания их голосовой трансляции, чтобы начать новую.", - "failed_others_already_recording_title": "Не получилось начать новую голосовую трансляцию", - "go_live": "Начать эфир", - "live": "В эфире", - "pause": "приостановить голосовую трансляцию", - "play": "проиграть голосовую трансляцию", - "resume": "продолжить голосовую трансляцию" - }, - "voice_message": { - "cant_start_broadcast_description": "Вы не можете начать голосовое сообщение, так как вы сейчас записываете прямую трансляцию. Завершите прямую трансляцию, чтобы начать запись голосового сообщения.", - "cant_start_broadcast_title": "Не удается запустить голосовое сообщение" - }, "voip": { "already_in_call": "Уже в вызове", "already_in_call_person": "Вы уже разговариваете с этим человеком.", @@ -3669,7 +3610,6 @@ "camera_disabled": "Ваша камера выключена", "camera_enabled": "Ваша камера всё ещё включена", "cannot_call_yourself_description": "Вы не можете позвонить самому себе.", - "change_input_device": "Смените устройство ввода", "connecting": "Подключение", "connection_lost": "Соединение с сервером потеряно", "connection_lost_description": "Вы не можете совершать вызовы без подключения к серверу.", @@ -3686,8 +3626,6 @@ "enable_camera": "Включить камеру", "enable_microphone": "Включить микрофон", "expand": "Вернуться к звонку", - "failed_call_live_broadcast_description": "Вы не можете начать звонок, так как вы производите живое вещание. Пожалуйста, остановите вещание, чтобы начать звонок.", - "failed_call_live_broadcast_title": "Невозможно начать звонок", "hangup": "Повесить трубку", "hide_sidebar_button": "Скрыть боковую панель", "input_devices": "Устройства ввода", diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json index 34ba4789bf..eab25be92b 100644 --- a/src/i18n/strings/sk.json +++ b/src/i18n/strings/sk.json @@ -877,7 +877,6 @@ }, "udd": { "interactive_verification_button": "Interaktívne overte pomocou emotikonov", - "manual_verification_button": "Manuálne overte pomocou textu", "other_ask_verify_text": "Poproste tohto používateľa, aby si overil svoju reláciu alebo ju nižšie manuálne overte.", "other_new_session_text": "%(name)s (%(userId)s) sa prihlásil do novej relácie bez jej overenia:", "own_ask_verify_text": "Overte svoje ostatné relácie pomocou jednej z nižšie uvedených možností.", @@ -912,12 +911,6 @@ "incoming_sas_dialog_waiting": "Čakanie na potvrdenie od partnera…", "incoming_sas_user_dialog_text_1": "Overte tohto používateľa a označte ho ako dôveryhodného. Dôveryhodní používatelia vám poskytujú dodatočný pokoj na duši pri používaní end-to-end šifrovaných správ.", "incoming_sas_user_dialog_text_2": "Overenie tohto používateľa označí jeho reláciu ako dôveryhodnú a zároveň označí vašu reláciu ako dôveryhodnú pre neho.", - "manual_device_verification_device_id_label": "ID relácie", - "manual_device_verification_device_key_label": "Kľúč relácie", - "manual_device_verification_device_name_label": "Názov relácie", - "manual_device_verification_footer": "Ak sa nezhodujú, môže byť ohrozená bezpečnosť vašej komunikácie.", - "manual_device_verification_self_text": "Potvrďte to porovnaním nasledujúcich údajov s nastaveniami používateľa v inej vašej relácii:", - "manual_device_verification_user_text": "Potvrďte reláciu tohto používateľa porovnaním nasledujúcich údajov s jeho nastaveniami používateľa:", "no_key_or_device": "Vyzerá to, že nemáte bezpečnostný kľúč ani žiadne iné zariadenie, pomocou ktorého by ste to mohli overiť. Toto zariadenie nebude mať prístup k starým zašifrovaným správam. Ak chcete overiť svoju totožnosť na tomto zariadení, budete musieť obnoviť svoje overovacie kľúče.", "no_support_qr_emoji": "Zariadenie, ktoré sa snažíte overiť, nepodporuje overenie skenovaním QR kódu ani overenie pomocou emotikonov, ktoré podporuje aplikácia %(brand)s. Skúste použiť iného klienta.", "other_party_cancelled": "Proti strana zrušila overovanie.", @@ -1037,10 +1030,6 @@ }, "error_user_not_logged_in": "Používateľ nie je prihlásený", "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s ukončil/a hlasové vysielanie", - "you": "Ukončili ste hlasové vysielanie" - }, "m.call.answer": { "dm": "Práve prebieha hovor", "user": "%(senderName)s sa pridal/a do hovoru", @@ -1417,8 +1406,6 @@ "video_rooms_faq2_answer": "Áno, časová os konverzácie sa zobrazuje spolu s videom.", "video_rooms_faq2_question": "Môžem popri videohovore používať aj textovú konverzáciu?", "video_rooms_feedbackSubheading": "Ďakujeme, že ste vyskúšali beta verziu, prosím, uveďte čo najviac podrobností, aby sme ju mohli vylepšiť.", - "voice_broadcast": "Hlasové vysielanie", - "voice_broadcast_force_small_chunks": "Vynútiť 15s dĺžku sekcie hlasového vysielania", "wysiwyg_composer": "Rozšírený textový editor" }, "labs_mjolnir": { @@ -1555,7 +1542,6 @@ "mute_description": "Nebudete dostávať žiadne oznámenia" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s začal/a hlasové vysielanie", "m.key.verification.request": "%(name)s žiada o overenie" }, "onboarding": { @@ -2130,7 +2116,6 @@ "error_unbanning": "Nepodarilo sa povoliť vstup", "events_default": "Odoslať správy", "invite": "Pozvať používateľov", - "io.element.voice_broadcast_info": "Hlasové vysielania", "kick": "Odstrániť používateľov", "m.call": "Spustiť %(brand)s hovory", "m.call.member": "Pripojiť sa k %(brand)s hovorom", @@ -2772,7 +2757,6 @@ "warning": "<w>UPOZORNENIE:</w> <description/>" }, "share": { - "link_title": "Odkaz na miestnosť", "permalink_message": "Odkaz na vybratú správu", "permalink_most_recent": "Odkaz na najnovšiu správu", "title_message": "Zdieľať správu z miestnosti", @@ -2861,13 +2845,6 @@ "upgraderoom": "Aktualizuje miestnosť na novšiu verziu", "upgraderoom_permission_error": "Na použitie tohoto príkazu nemáte dostatočné povolenia.", "usage": "Použitie", - "verify": "Overí používateľa, reláciu a verejné kľúče", - "verify_mismatch": "VAROVANIE: OVERENIE KĽÚČOV ZLYHALO! Podpisový kľúč používateľa %(userId)s a relácia %(deviceId)s je \"%(fprint)s\" čo nezodpovedá zadanému kľúču \"%(fingerprint)s\". Môže to znamenať, že vaša komunikácia je odpočúvaná!", - "verify_nop": "Relácia je už overená!", - "verify_nop_warning_mismatch": "VAROVANIE: Relácia je už overená, ale kľúče sa NEZHODUJÚ!", - "verify_success_description": "Zadaný podpisový kľúč sa zhoduje s podpisovým kľúčom, ktorý ste dostali z relácie používateľa %(userId)s %(deviceId)s. Relácia označená ako overená.", - "verify_success_title": "Kľúč overený", - "verify_unknown_pair": "Neznámy pár (používateľ, relácia): (%(userId)s, %(deviceId)s)", "view": "Zobrazí miestnosti s danou adresou", "whois": "Zobrazuje informácie o používateľovi" }, @@ -3082,10 +3059,6 @@ "error_rendering_message": "Nemožno načítať túto správu", "historical_messages_unavailable": "Nemôžete vidieť predchádzajúce správy", "in_room_name": " v <strong>%(room)s</strong>", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s ukončil/a <a>hlasové vysielanie</a>", - "you": "Ukončili ste <a>hlasové vysielanie</a>" - }, "io.element.widgets.layout": "%(senderName)s aktualizoval usporiadanie miestnosti", "late_event_separator": "Pôvodne odoslané %(dateTime)s", "load_error": { @@ -3647,38 +3620,6 @@ "switch_theme_dark": "Prepnúť na tmavý režim", "switch_theme_light": "Prepnúť na svetlý režim" }, - "voice_broadcast": { - "30s_backward": "30s späť", - "30s_forward": "30s dopredu", - "action": "Hlasové vysielanie", - "buffering": "Načítavanie do vyrovnávacej pamäte…", - "confirm_listen_affirm": "Áno, ukončiť moje nahrávanie", - "confirm_listen_description": "Ak začnete počúvať toto živé vysielanie, váš aktuálny záznam živého vysielania sa ukončí.", - "confirm_listen_title": "Počúvať živé vysielanie?", - "confirm_stop_affirm": "Áno, zastaviť vysielanie", - "confirm_stop_description": "Určite chcete zastaviť vysielanie naživo? Tým sa vysielanie ukončí a v miestnosti bude k dispozícii celý záznam.", - "confirm_stop_title": "Zastaviť vysielanie naživo?", - "connection_error": "Chyba pripojenia - nahrávanie pozastavené", - "failed_already_recording_description": "Už nahrávate hlasové vysielanie. Ukončite aktuálne hlasové vysielanie a spustite nové.", - "failed_already_recording_title": "Nemôžete spustiť nové hlasové vysielanie", - "failed_decrypt": "Hlasové vysielanie sa nedá dešifrovať", - "failed_generic": "Toto hlasové vysielanie nie je možné prehrať", - "failed_insufficient_permission_description": "Nemáte požadované oprávnenia na spustenie hlasového vysielania v tejto miestnosti. Obráťte sa na správcu miestnosti, aby vám rozšíril oprávnenia.", - "failed_insufficient_permission_title": "Nemôžete spustiť nové hlasové vysielanie", - "failed_no_connection_description": "Bohužiaľ teraz nemôžeme spustiť nahrávanie. Skúste to prosím neskôr.", - "failed_no_connection_title": "Chyba pripojenia", - "failed_others_already_recording_description": "Niekto iný už nahráva hlasové vysielanie. Počkajte, kým sa skončí jeho hlasové vysielanie, a potom spustite nové.", - "failed_others_already_recording_title": "Nemôžete spustiť nové hlasové vysielanie", - "go_live": "Prejsť naživo", - "live": "Naživo", - "pause": "pozastaviť hlasové vysielanie", - "play": "spustiť hlasové vysielanie", - "resume": "obnoviť hlasové vysielanie" - }, - "voice_message": { - "cant_start_broadcast_description": "Nemôžete spustiť hlasovú správu, pretože práve nahrávate živé vysielanie. Ukončite prosím živé vysielanie, aby ste mohli začať nahrávať hlasovú správu.", - "cant_start_broadcast_title": "Nemožno spustiť hlasovú správu" - }, "voip": { "already_in_call": "Hovor už prebieha", "already_in_call_person": "S touto osobou už hovor prebieha.", @@ -3698,7 +3639,6 @@ "camera_disabled": "Váš fotoaparát je vypnutý", "camera_enabled": "Fotoaparát je stále zapnutý", "cannot_call_yourself_description": "Nemôžete zavolať samému sebe.", - "change_input_device": "Zmeniť vstupné zariadenie", "connecting": "Pripájanie", "connection_lost": "Spojenie so serverom bolo prerušené", "connection_lost_description": "Bez pripojenia k serveru nie je možné uskutočňovať hovory.", @@ -3715,8 +3655,6 @@ "enable_camera": "Zapnúť kameru", "enable_microphone": "Zrušiť stlmenie mikrofónu", "expand": "Návrat k hovoru", - "failed_call_live_broadcast_description": "Nemôžete spustiť hovor, pretože práve nahrávate živé vysielanie. Ukončite živé vysielanie, aby ste mohli začať hovor.", - "failed_call_live_broadcast_title": "Nie je možné začať hovor", "hangup": "Zavesiť", "hide_sidebar_button": "Skryť bočný panel", "input_devices": "Vstupné zariadenia", diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index b7258c26cb..356480535f 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -835,7 +835,6 @@ }, "udd": { "interactive_verification_button": "Verifikojeni në mënyrë ndërvepruese përmes emoji-sh", - "manual_verification_button": "Verifikojeni dorazi përmes teksti", "other_ask_verify_text": "Kërkojini këtij përdoruesi të verifikojë sesionin e vet, ose ta verifikojë më poshtë dorazi.", "other_new_session_text": "%(name)s (%(userId)s) bëri hyrjen në një sesion të ri pa e verifikuar:", "own_ask_verify_text": "Verifikoni sesionit tuaj tjetër duke përdorur një nga mundësitë më poshtë.", @@ -870,12 +869,6 @@ "incoming_sas_dialog_waiting": "Po pritet ripohimi nga partneri…", "incoming_sas_user_dialog_text_1": "Verifikojeni këtë përdorues që t’i vihet shenjë si i besuar. Përdoruesit e besuar ju më tepër siguri kur përdorni mesazhe të fshehtëzuar skaj-më-skaj.", "incoming_sas_user_dialog_text_2": "Verifikimi i këtij përdoruesi do t’i vërë shenjë sesionit të tij si të besuar dhe sesionit tuaj si të besuar për ta.", - "manual_device_verification_device_id_label": "ID sesioni", - "manual_device_verification_device_key_label": "Kyç sesioni", - "manual_device_verification_device_name_label": "Emër sesioni", - "manual_device_verification_footer": "Nëse s’përputhen, siguria e komunikimeve tuaja mund të jetë komprometuar.", - "manual_device_verification_self_text": "Ripohojeni duke krahasuar sa vijon me Rregullimet e Përdoruesit te sesioni juaj tjetër:", - "manual_device_verification_user_text": "Ripohojeni këtë sesion përdoruesi duke krahasuar sa vijon me Rregullimet e tij të Përdoruesit:", "no_key_or_device": "Duket sikur s’keni Kyç Sigurie ose ndonjë pajisje tjetër nga e cila mund të bëni verifikimin. Kjo pajisje s’do të jetë në gjendje të hyjë te mesazhe të dikurshëm të fshehtëzuar. Që të mund të verifikohet identiteti juaj në këtë pajisje, ju duhet të riujdisni kyçet tuaj të verifikimit.", "no_support_qr_emoji": "Pajisja që po provoni të verifikoni nuk mbulon skanim të një kodi QR, apo verifikim me emoji, çka janë ato që mbulohen prej %(brand)s. Provoni me një klient tjetër.", "other_party_cancelled": "Pala tjetër e anuloi verifikimin.", @@ -991,10 +984,6 @@ } }, "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s përfundoi një transmetim zanor", - "you": "Përfunduat një transmetim zanor" - }, "m.call.answer": { "dm": "Thirrje në ecuri e sipër", "user": "%(senderName)s u bë pjesë e thirrjes", @@ -1329,8 +1318,6 @@ "video_rooms_faq1_question": "Si mund të krijoj një dhomë me video?", "video_rooms_faq2_answer": "Po, rrjedha kohore e fjalosjes shfaqet tok me videon.", "video_rooms_faq2_question": "A mund të përdor fjalosje me tekst në krah të thirrjes video?", - "voice_broadcast": "Transmetim zanor", - "voice_broadcast_force_small_chunks": "Detyro gjatësi copëzash transmetimi zanor prej 15s", "wysiwyg_composer": "Përpunues teksti të pasur" }, "labs_mjolnir": { @@ -1465,7 +1452,6 @@ "mute_description": "S’do të merrni ndonjë njoftim" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s nisi një transmetim zanor", "m.key.verification.request": "%(name)s po kërkon verifikim" }, "onboarding": { @@ -2005,7 +1991,6 @@ "error_unbanning": "S’u arrit t’i hiqej dëbimi", "events_default": "Dërgoni mesazhe", "invite": "Ftoni përdorues", - "io.element.voice_broadcast_info": "Transmetime zanore", "kick": "Hiqni përdorues", "m.call": "Nisni thirrje %(brand)s", "m.call.member": "Merrni pjesë në thirrje %(brand)s", @@ -2599,7 +2584,6 @@ "warning": "<w>KUJDES:</w> <description/>" }, "share": { - "link_title": "Lidhje për te dhoma", "permalink_message": "Lidhje për te mesazhi i përzgjedhur", "permalink_most_recent": "Lidhje për te mesazhet më të freskët", "title_message": "Ndani Me të Tjerë Mesazh Dhome", @@ -2684,13 +2668,6 @@ "upgraderoom": "E kalon një dhomë te një version i ri i përmirësuar", "upgraderoom_permission_error": "S’keni lejet e domosdoshme për përdorimin e këtij urdhri.", "usage": "Përdorim", - "verify": "Verifikon një përdorues, sesion dhe një set kyçesh publikë", - "verify_mismatch": "KUJDES: VERIFIKIMI I KYÇIT DËSHTOI! Kyçi i nënshkrimit për %(userId)s dhe sesionin %(deviceId)s është \"%(fprint)s\", që nuk përputhet me kyçin e dhënë \"%(fingerprint)s\". Kjo mund të jetë shenjë se komunikimet tuaja po përgjohen!", - "verify_nop": "Sesion i tashmë i verifikuar!", - "verify_nop_warning_mismatch": "KUJDES: sesion tashmë i verifikuar, por kyçet NUK PËRPUTHEN!", - "verify_success_description": "Kyçi i nënshkrimit që dhatë përputhet me kyçin e nënshkrimit që morët nga sesioni i %(userId)s %(deviceId)s. Sesionit iu vu shenjë si i verifikuar.", - "verify_success_title": "Kyç i verifikuar", - "verify_unknown_pair": "Çift (përdorues, sesion) i pavlefshëm: (%(userId)s, %(deviceId)s)", "whois": "Shfaq të dhëna rreth një përdoruesi" }, "space": { @@ -2893,10 +2870,6 @@ "error_rendering_message": "S’ngarkohet dot ky mesazh", "historical_messages_unavailable": "S’mund të shihni mesazhe më të hershëm", "in_room_name": " në <strong>%(room)s</strong>", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s përfundoi një <a>transmetim zanor</a>", - "you": "Përfunduat një <a>transmetim zanor</a>" - }, "io.element.widgets.layout": "%(senderName)s ka përditësuar skemën e dhomës", "load_error": { "no_permission": "U provua të ngarkohej një pikë e caktuar në kronologjinë e kësaj dhome, por nuk keni leje për ta parë mesazhin në fjalë.", @@ -3411,36 +3384,6 @@ "switch_theme_dark": "Kalo nën mënyrën e errët", "switch_theme_light": "Kalo nën mënyrën e çelët" }, - "voice_broadcast": { - "30s_backward": "30s mbrapsht", - "30s_forward": "30s përpara", - "action": "Transmetim zanor", - "confirm_listen_affirm": "Po, përfundoje regjistrimin tim", - "confirm_listen_description": "Nëse filloni të dëgjoni te ky transmetim i drejtpërdrejtë, regjistrimi juaj i tanishëm i një transmetimi të drejtpërdrejtë do të përfundojë.", - "confirm_listen_title": "Të dëgjohet te transmetimi i drejtpërdrejtë?", - "confirm_stop_affirm": "Po, ndale transmetimin zanor", - "confirm_stop_description": "Jeni i sigurt se doni të ndalet transmetimi juaj i drejtpërdrejtë? Kjo do të përfundojë transmetimin dhe regjistrimi i plotë do të jetë i passhëm te dhoma.", - "confirm_stop_title": "Të ndalet transmetimi i drejtpërdrejtë?", - "connection_error": "Gabim lidhjeje - Regjistrimi u ndal", - "failed_already_recording_description": "Po incizoni tashmë një transmetim zanor. Ju lutemi, që të nisni një të ri, përfundoni transmetimin tuaj zanor të tanishëm.", - "failed_already_recording_title": "S’niset dot një transmetim zanor i ri", - "failed_decrypt": "S’arrihet të shfshehtëzohet transmetim zanor", - "failed_generic": "S’arrihet të luhet ky transmetim zanor", - "failed_insufficient_permission_description": "S’keni lejet e domosdoshme që të nisni një transmetim zanor në këtë dhomë. Lidhuni me një përgjegjës dhome që të përmirësoni lejet tuaja.", - "failed_insufficient_permission_title": "S’niset dot një transmetim zanor i ri", - "failed_no_connection_description": "Mjerisht, s’qemë në gjendje të nisnim tani një regjistrim. Ju lutemi, riprovoni më vonë.", - "failed_no_connection_title": "Gabim lidhjeje", - "failed_others_already_recording_description": "Dikush tjetër është duke incizuar një transmetim zanor. Që të nisni një të ri, prisni të përfundojë incizimi zanor i tij.", - "failed_others_already_recording_title": "S’niset dot një transmetim zanor i ri", - "live": "Drejtpërdrejt", - "pause": "ndal transmetim zanor", - "play": "luaj transmetim zanor", - "resume": "vazhdo transmetim zanor" - }, - "voice_message": { - "cant_start_broadcast_description": "S’mund të niset mesazh zanor, ngaqë aktualisht po incizoni një transmetim të drejtpërdrejtë. Ju lutemi, përfundoni transmetimin e drejtpërdrejtë, që të mund të nisni incizimin e një mesazhi zanor.", - "cant_start_broadcast_title": "S’niset dot mesazh zanor" - }, "voip": { "already_in_call": "Tashmë në thirrje", "already_in_call_person": "Gjendeni tashmë në thirrje me këtë person.", @@ -3460,7 +3403,6 @@ "camera_disabled": "Kamera juaj është e fikur", "camera_enabled": "Kamera juaj është ende e aktivizuar", "cannot_call_yourself_description": "S’mund të bëni thirrje me vetveten.", - "change_input_device": "Ndryshoni pajisje dhëniesh", "connecting": "Po lidhet", "connection_lost": "Humbi lidhja me shërbyesin", "connection_lost_description": "S’mund të bëni thirrje pa një lidhje te shërbyesi.", @@ -3476,8 +3418,6 @@ "enable_camera": "Aktivizo kamerën", "enable_microphone": "Çheshto mikrofonin", "expand": "Kthehu te thirrja", - "failed_call_live_broadcast_description": "S’mund të nisni një thirrje, ngaqë aktualisht jeni duke regjistruar një transmetim të drejtpërdrejtë. Që të mund të nisni një thirrje, ju lutemi, përfundoni transmetimin tuaj të drejtpërdrejtë.", - "failed_call_live_broadcast_title": "S’fillohet dot thirrje", "hangup": "Mbylle Thirrjen", "hide_sidebar_button": "Fshihe anështyllën", "input_devices": "Pajisje input-i", diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json index bb4e489952..e24878b531 100644 --- a/src/i18n/strings/sv.json +++ b/src/i18n/strings/sv.json @@ -882,7 +882,6 @@ }, "udd": { "interactive_verification_button": "Verifiera interaktivt med emoji", - "manual_verification_button": "Verifiera manuellt med text", "other_ask_verify_text": "Be den här användaren att verifiera sin session, eller verifiera den manuellt nedan.", "other_new_session_text": "%(name)s (%(userId)s) loggade in i en ny session utan att verifiera den:", "own_ask_verify_text": "Verifiera din andra session med ett av alternativen nedan.", @@ -917,12 +916,6 @@ "incoming_sas_dialog_waiting": "Väntar på att andra parten ska bekräfta …", "incoming_sas_user_dialog_text_1": "Verifiera denna användare för att markera den som betrodd. Att lita på användare ger en extra sinnesfrid när man använder totalsträckskrypterade meddelanden.", "incoming_sas_user_dialog_text_2": "Att verifiera den här användaren kommer att markera dess session som betrodd, och markera din session som betrodd för denne.", - "manual_device_verification_device_id_label": "Sessions-ID", - "manual_device_verification_device_key_label": "Sessionsnyckel", - "manual_device_verification_device_name_label": "Sessionsnamn", - "manual_device_verification_footer": "Om de inte matchar så kan din kommunikations säkerhet vara äventyrad.", - "manual_device_verification_self_text": "Bekräfta genom att jämföra följande med användarinställningarna i din andra session:", - "manual_device_verification_user_text": "Bekräfta den här användarens session genom att jämföra följande med deras användarinställningar:", "no_key_or_device": "Det ser ut som att du inte har någon säkerhetsnyckel eller några andra enheter du kan verifiera mot. Den här enheten kommer inte kunna komma åt gamla krypterad meddelanden. För att verifiera din identitet på den här enheten så behöver du återställa dina verifieringsnycklar.", "no_support_qr_emoji": "Enheten du försöker verifiera stöder inte att skanna en QR-kod eller verifiera med emoji, vilket är var %(brand)s stöder. Pröva en annan klient.", "other_party_cancelled": "Den andra parten avbröt verifieringen.", @@ -1046,10 +1039,6 @@ }, "error_user_not_logged_in": "Användaren är inte inloggad", "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s avslutade en röstsändning", - "you": "Du avslutade en röstsändning" - }, "m.call.answer": { "dm": "Samtal pågår", "user": "%(senderName)s gick med i samtalet", @@ -1423,8 +1412,6 @@ "video_rooms_faq2_answer": "Ja, chattidslinjen visas tillsammans med videon.", "video_rooms_faq2_question": "Kan jag använda textchatt tillsammans med videosamtalet?", "video_rooms_feedbackSubheading": "Tack för att du prövar betan, vänligen ge så många detaljer du kan så att vi kan förbättra den.", - "voice_broadcast": "Röstsändning", - "voice_broadcast_force_small_chunks": "Tvinga dellängd på 15s för röstsändning", "wysiwyg_composer": "Riktextredigerare" }, "labs_mjolnir": { @@ -1568,7 +1555,6 @@ "mute_description": "Du får inga aviseringar" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s startade en röstsändning", "m.key.verification.request": "%(name)s begär verifiering" }, "onboarding": { @@ -2142,7 +2128,6 @@ "error_unbanning": "Misslyckades att avbanna", "events_default": "Skicka meddelanden", "invite": "Bjuda in användare", - "io.element.voice_broadcast_info": "Röstsändning", "kick": "Ta bort användare", "m.call": "Starta %(brand)s samtal", "m.call.member": "Gå med i %(brand)s samtal", @@ -2784,7 +2769,6 @@ "warning": "<w>VARNING:</w> <description/>" }, "share": { - "link_title": "Länk till rum", "permalink_message": "Länk till valt meddelande", "permalink_most_recent": "Länk till senaste meddelandet", "title_message": "Dela rumsmeddelande", @@ -2873,13 +2857,6 @@ "upgraderoom": "Uppgraderar ett rum till en ny version", "upgraderoom_permission_error": "Du har inte de behörigheter som krävs för att använda det här kommandot.", "usage": "Användande", - "verify": "Verifierar en användar-, sessions- och pubkey-tupel", - "verify_mismatch": "VARNING: NYCKELVERIFIERING MISSLYCKADES! Den signerade nyckeln för %(userId)s och sessionen %(deviceId)s är \"%(fprint)s\" vilket inte matchar den givna nyckeln \"%(fingerprint)s\". Detta kan betyda att kommunikationen är övervakad!", - "verify_nop": "Sessionen är redan verifierad!", - "verify_nop_warning_mismatch": "VARNING: sessionen är redan verifierad, men nycklarna MATCHAR INTE!", - "verify_success_description": "Signeringsnyckeln du gav matchar signeringsnyckeln du fick av %(userId)ss session %(deviceId)s. Sessionen markerades som verifierad.", - "verify_success_title": "Verifierade nyckeln", - "verify_unknown_pair": "Okänt (användare, session)-par: (%(userId)s, %(deviceId)s)", "view": "Visar rum med den angivna adressen", "whois": "Visar information om en användare" }, @@ -3097,10 +3074,6 @@ "error_rendering_message": "Kan inte ladda det här meddelandet", "historical_messages_unavailable": "Du kan inte se tidigare meddelanden", "in_room_name": " i <strong>%(room)s</strong>", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s avslutade en <a>röstsändning</a>", - "you": "Du avslutade en <a>röstsändning</a>" - }, "io.element.widgets.layout": "%(senderName)s har uppdaterat rummets arrangemang", "late_event_separator": "Ursprungligen skickad %(dateTime)s", "load_error": { @@ -3633,38 +3606,6 @@ "switch_theme_dark": "Byt till mörkt läge", "switch_theme_light": "Byt till ljust läge" }, - "voice_broadcast": { - "30s_backward": "30s bakåt", - "30s_forward": "30s framåt", - "action": "Röstsändning", - "buffering": "Buffrar…", - "confirm_listen_affirm": "Ja, avsluta min inspelning", - "confirm_listen_description": "Om du börjar lyssna på den här direktsändningen så kommer din nuvarande direktsändningsinspelning att avslutas.", - "confirm_listen_title": "Lyssna på direktsändning?", - "confirm_stop_affirm": "Ja, avsluta sändning", - "confirm_stop_description": "Är du säker på att du vill avsluta din direktsändning? Det här kommer att avsluta sändningen och den fulla inspelningen kommer att bli tillgänglig i rummet.", - "confirm_stop_title": "Avsluta livesändning?", - "connection_error": "Anslutningsfel - Inspelning pausad", - "failed_already_recording_description": "Du spelar redan in en röstsändning. Avsluta din nuvarande röstsändning för att påbörja en ny.", - "failed_already_recording_title": "Kan inte starta en ny röstsändning", - "failed_decrypt": "Kunde inte kryptera röstsändning", - "failed_generic": "Kan inte spela den här röstsändningen", - "failed_insufficient_permission_description": "Du är inte behörig att starta en röstsändning i det här rummet. Kontakta en rumsadministratör för att uppgradera dina behörigheter.", - "failed_insufficient_permission_title": "Kan inte starta en ny röstsändning", - "failed_no_connection_description": "Tyvärr kunde vi inte starta en inspelning just nu. Vänligen pröva igen senare.", - "failed_no_connection_title": "Anslutningsfel", - "failed_others_already_recording_description": "Någon annan spelar redan in en röstsändning. Vänta på att deras röstsändning tar slut för att starta en ny.", - "failed_others_already_recording_title": "Kan inte starta en ny röstsändning", - "go_live": "Börja sända", - "live": "Sänder", - "pause": "pausa röstsändning", - "play": "spela röstsändning", - "resume": "återuppta röstsändning" - }, - "voice_message": { - "cant_start_broadcast_description": "Du kan inte starta ett röstmeddelande eftersom du spelar in en direktsändning. Vänligen avsluta din direktsändning för att starta inspelning av ett röstmeddelande.", - "cant_start_broadcast_title": "Kan inte starta röstmeddelanden" - }, "voip": { "already_in_call": "Redan i samtal", "already_in_call_person": "Du är redan i ett samtal med den här personen.", @@ -3684,7 +3625,6 @@ "camera_disabled": "Din kamera är av", "camera_enabled": "Din kamera är fortfarande på", "cannot_call_yourself_description": "Du kan inte ringa till dig själv.", - "change_input_device": "Byt ingångsenhet", "close_lobby": "Stäng lobbyn", "connecting": "Ansluter", "connection_lost": "Anslutningen till servern har förlorats", @@ -3703,8 +3643,6 @@ "enable_camera": "Sätt på kamera", "enable_microphone": "Slå på mikrofonen", "expand": "Återgå till samtal", - "failed_call_live_broadcast_description": "Du kan inte starta ett samtal eftersom att du spelar in en direktsändning. Vänligen avsluta din direktsändning för att starta ett samtal.", - "failed_call_live_broadcast_title": "Kunde inte starta ett samtal", "hangup": "Lägg på", "hide_sidebar_button": "Göm sidopanel", "input_devices": "Ingångsenheter", diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json index 7d438ec2a4..8af8d0b2ea 100644 --- a/src/i18n/strings/uk.json +++ b/src/i18n/strings/uk.json @@ -864,7 +864,6 @@ }, "udd": { "interactive_verification_button": "Звірити інтерактивно за допомогою емоджі", - "manual_verification_button": "Звірити вручну за допомогою тексту", "other_ask_verify_text": "Попросіть цього користувача звірити сеанс, або звірте його власноруч унизу.", "other_new_session_text": "%(name)s (%(userId)s) починає новий сеанс без його звірення:", "own_ask_verify_text": "Звірте інший сеанс за допомогою одного з варіантів знизу.", @@ -899,12 +898,6 @@ "incoming_sas_dialog_waiting": "Очікування підтвердження партнером…", "incoming_sas_user_dialog_text_1": "Звірте цього користувача щоб позначити його довіреним. Довіряння користувачам додає спокою якщо ви користуєтесь наскрізно зашифрованими повідомленнями.", "incoming_sas_user_dialog_text_2": "Звірка цього користувача позначить його сеанс довіреним вам, а ваш йому.", - "manual_device_verification_device_id_label": "ID сеансу", - "manual_device_verification_device_key_label": "Ключ сеансу", - "manual_device_verification_device_name_label": "Назва сеансу", - "manual_device_verification_footer": "Якщо вони не збігаються, безпека вашого спілкування ймовірно скомпрометована.", - "manual_device_verification_self_text": "Підтвердьте шляхом порівняння наступного рядка з рядком у користувацьких налаштуваннях вашого іншого сеансу:", - "manual_device_verification_user_text": "Підтвердьте сеанс цього користувача шляхом порівняння наступного рядка з рядком з їхніх користувацьких налаштувань:", "no_key_or_device": "Схоже, у вас немає ключа безпеки або будь-яких інших пристроїв, які ви можете підтвердити. Цей пристрій не зможе отримати доступ до старих зашифрованих повідомлень. Щоб підтвердити свою справжність на цьому пристрої, вам потрібно буде скинути ключі перевірки.", "no_support_qr_emoji": "Пристрій, який ви намагаєтесь звірити, не підтримує сканування QR-коду або звірення за допомогою емоджі, що є підтримувані %(brand)s. Спробуйте використати інший клієнт.", "other_party_cancelled": "Друга сторона скасувала звірення.", @@ -1021,10 +1014,6 @@ }, "error_user_not_logged_in": "Користувач не увійшов", "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s завершує голосову трансляцію", - "you": "Ви завершили голосову трансляцію" - }, "m.call.answer": { "dm": "Виклик триває", "user": "%(senderName)s приєднується до виклику", @@ -1376,8 +1365,6 @@ "video_rooms_faq1_question": "Як створити відеокімнату?", "video_rooms_faq2_answer": "Так, стрічка бесіди показана поряд із відео.", "video_rooms_faq2_question": "Чи можу я писати текстові повідомлення під час відеовиклику?", - "voice_broadcast": "Голосові трансляції", - "voice_broadcast_force_small_chunks": "Примусово обмежити тривалість голосових трансляцій до 15 с", "wysiwyg_composer": "Розширений текстовий редактор" }, "labs_mjolnir": { @@ -1516,7 +1503,6 @@ "mute_description": "Ви не отримуватимете жодних сповіщень" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s розпочинає голосову трансляцію", "m.key.verification.request": "%(name)s робить запит на звірення" }, "onboarding": { @@ -2072,7 +2058,6 @@ "error_unbanning": "Не вдалося розблокувати", "events_default": "Надіслати повідомлення", "invite": "Запросити користувачів", - "io.element.voice_broadcast_info": "Голосові трансляції", "kick": "Вилучити користувачів", "m.call": "Розпочати %(brand)s викликів", "m.call.member": "Приєднатися до %(brand)s викликів", @@ -2709,7 +2694,6 @@ "warning": "<w>ПОПЕРЕДЖЕННЯ:</w> <description/>" }, "share": { - "link_title": "Посилання на кімнату", "permalink_message": "Посилання на вибране повідомлення", "permalink_most_recent": "Посилання на останнє повідомлення", "title_message": "Поділитися повідомленням кімнати", @@ -2796,13 +2780,6 @@ "upgraderoom": "Поліпшує кімнату до нової версії", "upgraderoom_permission_error": "Вам бракує дозволу на використання цієї команди.", "usage": "Використання", - "verify": "Звіряє користувача, сеанс та супровід відкритого ключа", - "verify_mismatch": "УВАГА: НЕ ВДАЛОСЯ ЗВІРИТИ КЛЮЧ! Ключем для %(userId)s та сеансу %(deviceId)s є «%(fprint)s», що не збігається з наданим ключем «%(fingerprint)s». Це може означати, що ваші повідомлення перехоплюють!", - "verify_nop": "Сеанс вже звірено!", - "verify_nop_warning_mismatch": "ПОПЕРЕДЖЕННЯ: сеанс вже звірено, але ключі НЕ ЗБІГАЮТЬСЯ!", - "verify_success_description": "Наданий вами ключ підпису збігається з ключем підпису, що ви отримали від сеансу %(deviceId)s %(userId)s. Сеанс позначено звіреним.", - "verify_success_title": "Звірений ключ", - "verify_unknown_pair": "Невідома пара (користувач, сеанс): (%(userId)s, %(deviceId)s)", "view": "Перегляд кімнати з вказаною адресою", "whois": "Показує відомості про користувача" }, @@ -3014,10 +2991,6 @@ "error_rendering_message": "Не вдалося завантажити це повідомлення", "historical_messages_unavailable": "Ви не можете переглядати давніші повідомлення", "in_room_name": " в <strong>%(room)s</strong>", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s завершує <a>голосову трансляцію</a>", - "you": "Ви завершили <a>голосову трансляцію</a>" - }, "io.element.widgets.layout": "%(senderName)s оновлює макет кімнати", "load_error": { "no_permission": "У вас нема дозволу на перегляд повідомлення за вказаною позицією в стрічці цієї кімнати.", @@ -3545,38 +3518,6 @@ "switch_theme_dark": "Темна тема", "switch_theme_light": "Світла тема" }, - "voice_broadcast": { - "30s_backward": "Назад на 30 с", - "30s_forward": "Уперед на 30 с", - "action": "Голосові трансляції", - "buffering": "Буферизація…", - "confirm_listen_affirm": "Так, завершити мій запис", - "confirm_listen_description": "Якщо ви почнете слухати цю трансляцію наживо, ваш поточний запис трансляції наживо завершиться.", - "confirm_listen_title": "Слухати трансляцію наживо?", - "confirm_stop_affirm": "Так, припинити трансляцію", - "confirm_stop_description": "Ви впевнені, що хочете припинити пряму трансляцію? Це призведе до завершення трансляції, а повний запис буде доступний у кімнаті.", - "confirm_stop_title": "Припинити голосову трансляцію?", - "connection_error": "Помилка з'єднання - Запис призупинено", - "failed_already_recording_description": "Ви вже записуєте голосову трансляцію. Завершіть поточний запис, щоб розпочати новий.", - "failed_already_recording_title": "Не вдалося розпочати нову голосову трансляцію", - "failed_decrypt": "Невдалося розшифрувати голосову трансляцію", - "failed_generic": "Неможливо відтворити цю голосову трансляцію", - "failed_insufficient_permission_description": "Ви не маєте необхідних дозволів для початку голосової трансляції в цю кімнату. Зверніться до адміністратора кімнати, щоб розширити ваші дозволи.", - "failed_insufficient_permission_title": "Не вдалося розпочати нову голосову трансляцію", - "failed_no_connection_description": "На жаль, ми не можемо розпочати запис прямо зараз. Будь ласка, спробуйте пізніше.", - "failed_no_connection_title": "Помилка з'єднання", - "failed_others_already_recording_description": "Хтось інший вже записує голосову трансляцію. Зачекайте, поки запис завершиться, щоб розпочати новий.", - "failed_others_already_recording_title": "Не вдалося розпочати нову голосову трансляцію", - "go_live": "Слухати", - "live": "Наживо", - "pause": "призупинити голосову трансляцію", - "play": "відтворити голосову трансляцію", - "resume": "поновити голосову трансляцію" - }, - "voice_message": { - "cant_start_broadcast_description": "Ви не можете розпочати запис голосового повідомлення, оскільки зараз відбувається запис трансляції наживо. Завершіть трансляцію, щоб розпочати запис голосового повідомлення.", - "cant_start_broadcast_title": "Не можливо запустити запис голосового повідомлення" - }, "voip": { "already_in_call": "Вже у виклику", "already_in_call_person": "Ви вже спілкуєтесь із цією особою.", @@ -3596,7 +3537,6 @@ "camera_disabled": "Вашу камеру вимкнено", "camera_enabled": "Ваша камера досі увімкнена", "cannot_call_yourself_description": "Ви не можете подзвонити самим собі.", - "change_input_device": "Змінити пристрій вводу", "connecting": "З'єднання", "connection_lost": "Втрачено зʼєднання з сервером", "connection_lost_description": "Неможливо здійснювати виклики без з'єднання з сервером.", @@ -3613,8 +3553,6 @@ "enable_camera": "Увімкнути камеру", "enable_microphone": "Увімкнути мікрофон", "expand": "Повернутися до виклику", - "failed_call_live_broadcast_description": "Ви не можете розпочати виклик, оскільки зараз ведеться запис прямої трансляції. Будь ласка, заверште її, щоб розпочати виклик.", - "failed_call_live_broadcast_title": "Не вдалося розпочати виклик", "hangup": "Покласти слухавку", "hide_sidebar_button": "Сховати бічну панель", "input_devices": "Пристрої вводу", diff --git a/src/i18n/strings/vi.json b/src/i18n/strings/vi.json index 5ce36aed05..3e1eaeaec1 100644 --- a/src/i18n/strings/vi.json +++ b/src/i18n/strings/vi.json @@ -792,7 +792,6 @@ }, "udd": { "interactive_verification_button": "Xác thực có tương tác bằng biểu tượng cảm xúc", - "manual_verification_button": "Xác thực thủ công bằng văn bản", "other_ask_verify_text": "Yêu cầu người dùng này xác thực phiên của họ hoặc xác minh theo cách thủ công bên dưới.", "other_new_session_text": "%(name)s (%(userId)s) đã đăng nhập vào một phiên mới mà không xác thực:", "own_ask_verify_text": "Xác minh phiên khác của bạn bằng một trong các tùy chọn bên dưới.", @@ -827,12 +826,6 @@ "incoming_sas_dialog_waiting": "Đang đợi bên kia xác nhận…", "incoming_sas_user_dialog_text_1": "Xác thực người dùng này để đánh dấu họ là đáng tin cậy. Người dùng đáng tin cậy giúp bạn yên tâm hơn khi sử dụng các tin nhắn được mã hóa end-to-end.", "incoming_sas_user_dialog_text_2": "Việc xác thực người dùng này sẽ đánh dấu phiên của họ là đáng tin cậy và cũng đánh dấu phiên của bạn là đáng tin cậy đối với họ.", - "manual_device_verification_device_id_label": "Định danh (ID) phiên", - "manual_device_verification_device_key_label": "Khóa phiên", - "manual_device_verification_device_name_label": "Tên phiên", - "manual_device_verification_footer": "Nếu chúng không khớp, sự bảo mật của việc giao tiếp của bạn có thể bị can thiệp.", - "manual_device_verification_self_text": "Xác nhận bằng cách so sánh những điều sau đây với Cài đặt người dùng trong phiên làm việc kia của bạn:", - "manual_device_verification_user_text": "Xác nhận phiên của người dùng này bằng cách so sánh phần sau với Cài đặt người dùng của họ:", "no_key_or_device": "Có vẻ như bạn không có Khóa Bảo mật hoặc bất kỳ thiết bị nào bạn có thể xác thực. Thiết bị này sẽ không thể truy cập vào các tin nhắn mã hóa cũ. Để xác minh danh tính của bạn trên thiết bị này, bạn sẽ cần đặt lại các khóa xác thực của mình.", "no_support_qr_emoji": "Thiết bị bạn đang cố xác thực không hỗ trợ quét mã QR hoặc xác minh biểu tượng cảm xúc, đó là những gì %(brand)s hỗ trợ. Hãy thử với một thiết bị đầu cuối khác.", "other_party_cancelled": "Người kia đã hủy xác thực.", @@ -949,10 +942,6 @@ }, "error_user_not_logged_in": "Người dùng đang không đăng nhập", "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s đã kết thúc một cuộc phát thanh", - "you": "Bạn đã kết thúc một cuộc phát thanh" - }, "m.call.answer": { "dm": "Cuộc gọi đang diễn ra", "user": "%(senderName)s đã tham gia cuộc gọi", @@ -1288,7 +1277,6 @@ "video_rooms_faq1_question": "Tôi có thể tạo một phòng video bằng cách nào?", "video_rooms_faq2_answer": "Vâng, dòng thời gian trò chuyện được hiển thị cùng với video.", "video_rooms_faq2_question": "Tôi có thể sử dụng nhắn tin cùng lúc với gọi video không?", - "voice_broadcast": "Phát thanh", "wysiwyg_composer": "Trình soạn thảo văn bản giàu tính chất" }, "labs_mjolnir": { @@ -1400,7 +1388,6 @@ "mute_description": "Bạn sẽ không nhận bất kỳ thông báo nào" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s đã bắt đầu phát thanh", "m.key.verification.request": "%(name)s đang yêu cầu xác thực" }, "onboarding": { @@ -1895,7 +1882,6 @@ "error_unbanning": "Không thể bỏ cấm", "events_default": "Gửi tin nhắn", "invite": "Mời người dùng", - "io.element.voice_broadcast_info": "Phát thanh", "kick": "Loại bỏ người dùng", "m.call": "Bắt đầu %(brand)s cuộc gọi", "m.call.member": "Tham gia %(brand)s cuộc gọi", @@ -2496,7 +2482,6 @@ "warn_quit": "Cảnh báo trước khi bỏ thuốc lá" }, "share": { - "link_title": "Liên kết đến phòng", "permalink_message": "Liên kết đến tin nhắn đã chọn", "permalink_most_recent": "Liên kết đến tin nhắn gần đây nhất", "title_message": "Chia sẻ tin nhắn trong phòng", @@ -2583,13 +2568,6 @@ "upgraderoom": "Nâng cấp phòng lên phiên bản mới", "upgraderoom_permission_error": "Bạn không có quyền để dùng lệnh này.", "usage": "Cách sử dụng", - "verify": "Xác thực người dùng, thiết bị và tuple pubkey", - "verify_mismatch": "CẢNH BÁO: XÁC THỰC KHÓA THẤT BẠI! Khóa đăng nhập cho %(userId)s và thiết bị %(deviceId)s là \"%(fprint)s\" không khớp với khóa được cung cấp \"%(fingerprint)s\". Điều này có nghĩa là các thông tin liên lạc của bạn đang bị chặn!", - "verify_nop": "Thiết bị đã được xác thực rồi!", - "verify_nop_warning_mismatch": "CẢNH BÁO: phiên đã được xác thực, nhưng các khóa KHÔNG KHỚP!", - "verify_success_description": "Khóa đăng nhập bạn cung cấp khớp với khóa đăng nhập bạn nhận từ thiết bị %(deviceId)s của %(userId)s. Thiết bị được đánh dấu là đã được xác minh.", - "verify_success_title": "Khóa được xác thực", - "verify_unknown_pair": "Cặp (người dùng, phiên) không xác định: (%(userId)s, %(deviceId)s)", "view": "Phòng truyền hình với địa chỉ đã cho", "whois": "Hiển thị thông tin về người dùng" }, @@ -2786,10 +2764,6 @@ "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", "in_room_name": " ở <strong>%(room)s</strong>", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s đã kết thúc một <a>cuộc phát thanh</a>", - "you": "Bạn đã kết thúc một <a>cuộc phát thanh</a>" - }, "io.element.widgets.layout": "%(senderName)s đã cập nhật bố trí của phòng", "load_error": { "no_permission": "Đã cố gắng tải một điểm cụ thể trong dòng thời gian của phòng này, nhưng bạn không có quyền xem tin nhắn được đề cập.", @@ -3280,34 +3254,6 @@ "switch_theme_dark": "Chuyển sang chế độ tối", "switch_theme_light": "Chuyển sang chế độ ánh sáng" }, - "voice_broadcast": { - "30s_backward": "30 giây trước", - "30s_forward": "30 giây kế tiếp", - "action": "Phát thanh", - "buffering": "Đang khởi tạo bộ đệm…", - "confirm_listen_affirm": "Vâng, ngừng ghi âm tôi", - "confirm_listen_description": "Nếu bạn bắt đầu nghe chương trình phát thanh trực tiếp này, quá trình ghi chương trình phát thanh trực tiếp hiện tại của bạn sẽ kết thúc.", - "confirm_listen_title": "Nghe phát thanh trực tiếp không?", - "confirm_stop_affirm": "Đúng rồi, dừng phát thanh", - "confirm_stop_description": "Bạn có chắc chắn muốn dừng phát sóng trực tiếp của mình không? Điều này sẽ kết thúc chương trình phát sóng và bản ghi đầy đủ sẽ có sẵn trong phòng.", - "confirm_stop_title": "Ngừng phát thanh trực tiếp?", - "connection_error": "Lỗi kết nối - Đã tạm dừng ghi âm", - "failed_already_recording_description": "Bạn hiện đang ghi một cuộc phát thanh. Kết thúc phát thanh để thực hiện một cái mới.", - "failed_already_recording_title": "Không thể bắt đầu cuộc phát thanh mới", - "failed_decrypt": "Không thể giải mã cuộc phát thanh", - "failed_generic": "Không thể nghe phát thanh", - "failed_insufficient_permission_description": "Bạn không có quyền để phát thanh trong phòng này. Hỏi một quản trị viên của phòng để nâng quyền của bạn.", - "failed_insufficient_permission_title": "Không thể bắt đầu cuộc phát thanh mới", - "failed_no_connection_description": "Thật không may là chúng tôi không thể bắt đầu ghi âm. Vui lòng thử lại.", - "failed_no_connection_title": "Lỗi kết nối", - "failed_others_already_recording_description": "Một người khác đang phát thanh. Hãy chờ cho đến khi họ ngừng rồi bạn mới bắt đầu phát thanh.", - "failed_others_already_recording_title": "Không thể bắt đầu cuộc phát thanh mới", - "go_live": "Phát trực tiếp", - "live": "Trực tiếp", - "pause": "Tạm dừng phát thanh", - "play": "nghe phát thanh", - "resume": "Tiếp tục phát thanh" - }, "voip": { "already_in_call": "Đang trong cuộc gọi", "already_in_call_person": "Bạn đang trong cuộc gọi với người này rồi.", @@ -3327,7 +3273,6 @@ "camera_disabled": "Camera của bạn đã tắt", "camera_enabled": "Camera của bạn vẫn đang được bật", "cannot_call_yourself_description": "Bạn không thể tự gọi chính mình.", - "change_input_device": "Đổi thiết bị đầu vào", "connecting": "Đang kết nối", "connection_lost": "Mất kết nối đến máy chủ", "connection_lost_description": "Bạn không thể gọi khi không có kết nối tới máy chủ.", @@ -3344,8 +3289,6 @@ "enable_camera": "Bật máy ghi hình", "enable_microphone": "Mở âm micrô", "expand": "Quay về cuộc gọi", - "failed_call_live_broadcast_description": "Bạn không thể bắt đầu gọi vì bạn đang ghi âm để cuộc phát thanh trực tiếp. Hãy ngừng phát thanh để bắt đầu gọi.", - "failed_call_live_broadcast_title": "Không thể bắt đầu cuộc gọi", "hangup": "Dập máy", "hide_sidebar_button": "Ẩn thanh bên", "input_devices": "Thiết bị đầu vào", diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json index 99d5586a5c..1ddb47fcc4 100644 --- a/src/i18n/strings/zh_Hans.json +++ b/src/i18n/strings/zh_Hans.json @@ -809,7 +809,6 @@ }, "udd": { "interactive_verification_button": "用表情符号交互式验证", - "manual_verification_button": "用文本手动验证", "other_ask_verify_text": "要求此用户验证其会话,或在下面手动进行验证。", "other_new_session_text": "%(name)s(%(userId)s)登录到未验证的新会话:", "own_ask_verify_text": "使用以下选项之一验证你的其他会话。", @@ -841,12 +840,6 @@ "incoming_sas_dialog_title": "收到验证请求", "incoming_sas_user_dialog_text_1": "验证此用户并将其标记为已信任。在收发端到端加密消息时,信任用户可让你更加放心。", "incoming_sas_user_dialog_text_2": "验证此用户会将其会话标记为已信任,与此同时,你的会话也会被此用户标记为已信任。", - "manual_device_verification_device_id_label": "会话 ID", - "manual_device_verification_device_key_label": "会话密钥", - "manual_device_verification_device_name_label": "会话名称", - "manual_device_verification_footer": "如果它们不匹配,你通讯的安全性可能已受损。", - "manual_device_verification_self_text": "通过比较下方内容和你别的会话中的用户设置来确认:", - "manual_device_verification_user_text": "通过比较下方内容和对方用户设置来确认此用户会话:", "no_key_or_device": "看起来你没有安全密钥或者任何其他可以验证的设备。 此设备将无法访问旧的加密消息。为了在这个设备上验证你的身份,你需要重置你的验证密钥。", "no_support_qr_emoji": "你正在尝试验证的设备不支持扫码QR码或表情符号验证,这是%(brand)s所支持的。用不同的客户端试试。", "other_party_cancelled": "另一方取消了验证。", @@ -1296,7 +1289,6 @@ "video_rooms_faq1_question": "我如何创建视频房间?", "video_rooms_faq2_answer": "是的,聊天时间线显示在视频旁。", "video_rooms_faq2_question": "我能在视频通话的同时使用文字聊天吗?", - "voice_broadcast": "语音广播", "wysiwyg_composer": "富文本编辑器" }, "labs_mjolnir": { @@ -1426,7 +1418,6 @@ "mute_description": "你不会收到任何通知" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s开始了语音广播", "m.key.verification.request": "%(name)s 正在请求验证" }, "onboarding": { @@ -1917,7 +1908,6 @@ "error_unbanning": "解除封禁失败", "events_default": "发送消息", "invite": "邀请用户", - "io.element.voice_broadcast_info": "语音广播", "kick": "移除用户", "m.call": "开始%(brand)s呼叫", "m.call.member": "加入%(brand)s呼叫", @@ -2457,7 +2447,6 @@ "warning": "<w>警告:</w><description/>" }, "share": { - "link_title": "房间链接", "permalink_message": "选中消息的链接", "permalink_most_recent": "最新消息的链接", "title_message": "分享房间消息", @@ -2544,13 +2533,6 @@ "upgraderoom": "将房间升级到新版本", "upgraderoom_permission_error": "你没有权限使用此命令。", "usage": "用法", - "verify": "验证用户、会话和公钥元组", - "verify_mismatch": "警告:密钥验证失败!%(userId)s 的会话 %(deviceId)s 的签名密钥为 %(fprint)s,与提供的密钥 %(fingerprint)s 不符。这可能表示你的通讯已被截获!", - "verify_nop": "会话已验证!", - "verify_nop_warning_mismatch": "警告:会话已验证,然而密钥不匹配!", - "verify_success_description": "你提供的签名密钥与你从 %(userId)s 的会话 %(deviceId)s 获取的一致。此会话被标为已验证。", - "verify_success_title": "已验证的密钥", - "verify_unknown_pair": "未知用户会话配对:(%(userId)s:%(deviceId)s)", "whois": "显示关于用户的信息" }, "space": { @@ -3256,27 +3238,6 @@ "switch_theme_dark": "切换到深色模式", "switch_theme_light": "切换到浅色模式" }, - "voice_broadcast": { - "30s_backward": "后退30秒", - "30s_forward": "前进30秒", - "action": "语音广播", - "buffering": "正在缓冲……", - "confirm_stop_affirm": "是的,停止广播", - "confirm_stop_title": "停止直播吗?", - "failed_already_recording_description": "你已经在录制一个语音广播。请结束你当前的语音广播以开始新的语音广播。", - "failed_already_recording_title": "无法开始新的语音广播", - "failed_insufficient_permission_description": "你没有必要的权限在这个房间开始语音广播。请联系房间管理员以提升你的权限。", - "failed_insufficient_permission_title": "无法开始新的语音广播", - "failed_no_connection_description": "很遗憾,我们现在无法开始录音。请稍后再试。", - "failed_no_connection_title": "连接错误", - "failed_others_already_recording_description": "别人已经在录制语音广播了。等到他们的语音广播结束后再开始新的广播。", - "failed_others_already_recording_title": "无法开始新的语音广播", - "go_live": "开始直播", - "live": "实时", - "pause": "暂停语音广播", - "play": "播放语音广播", - "resume": "恢复语音广播" - }, "voip": { "already_in_call": "正在通话中", "already_in_call_person": "你正在与其通话。", @@ -3296,7 +3257,6 @@ "camera_disabled": "你的摄像头已关闭", "camera_enabled": "你的摄像头仍然处于启用状态", "cannot_call_yourself_description": "你不能打给自己。", - "change_input_device": "变更输入设备", "connecting": "连接中", "connection_lost": "已丢失与服务器的连接", "connection_lost_description": "你不能在未连接到服务器时进行呼叫。", @@ -3313,7 +3273,6 @@ "enable_camera": "启动相机", "enable_microphone": "取消静音麦克风", "expand": "返回通话", - "failed_call_live_broadcast_title": "无法开始通话", "hangup": "挂断", "hide_sidebar_button": "隐藏侧边栏", "input_devices": "输入设备", diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 68b3694ee8..584852748b 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -865,7 +865,6 @@ }, "udd": { "interactive_verification_button": "透過表情符號互動來驗證", - "manual_verification_button": "透過文字手動驗證", "other_ask_verify_text": "要求此使用者驗證他們的工作階段,或在下方手動驗證。", "other_new_session_text": "%(name)s (%(userId)s)登入到未驗證的新工作階段:", "own_ask_verify_text": "使用下方的其中一個選項來驗證您其他工作階段。", @@ -900,12 +899,6 @@ "incoming_sas_dialog_waiting": "正在等待夥伴確認…", "incoming_sas_user_dialog_text_1": "驗證此工作階段,並標記為可受信任。由您將工作階段標記為可受信任後,可讓聊天夥伴傳送端到端加密訊息時能更加放心。", "incoming_sas_user_dialog_text_2": "驗證此使用者將會把他們的工作階段標記為受信任,並同時為他們標記您的工作階段為可信任。", - "manual_device_verification_device_id_label": "工作階段 ID", - "manual_device_verification_device_key_label": "工作階段金鑰", - "manual_device_verification_device_name_label": "工作階段名稱", - "manual_device_verification_footer": "如果它們不相符,則可能會威脅到您的通訊安全。", - "manual_device_verification_self_text": "透過將下列內容與您其他工作階段中的「使用者設定」所顯示的內容來確認:", - "manual_device_verification_user_text": "將以下內容與對方的「使用者設定」當中顯示的內容進行比對,來確認對方的工作階段:", "no_key_or_device": "您似乎沒有安全金鑰或其他可以驗證的裝置。此裝置將無法存取舊的加密訊息。為了在此裝置上驗證您的身分,您必須重設您的驗證金鑰。", "no_support_qr_emoji": "您正在嘗試驗證的裝置不支援掃描 QR Code 或表情符號驗證,這是 %(brand)s 所支援的。請嘗試使用其他客戶端。", "other_party_cancelled": "另一方取消了驗證。", @@ -1022,10 +1015,6 @@ }, "error_user_not_logged_in": "使用者未登入", "event_preview": { - "io.element.voice_broadcast_info": { - "user": "%(senderName)s 已結束語音廣播", - "you": "您結束了語音廣播" - }, "m.call.answer": { "dm": "通話進行中", "user": "%(senderName)s 已加入通話", @@ -1379,8 +1368,6 @@ "video_rooms_faq1_question": "我要如何建立視訊聊天室?", "video_rooms_faq2_answer": "可以,會在視訊畫面旁顯示聊天時間軸。", "video_rooms_faq2_question": "我可以在視訊通話的同時使用文字聊天嗎?", - "voice_broadcast": "語音廣播", - "voice_broadcast_force_small_chunks": "強制 15 秒語音廣播區塊長度", "wysiwyg_composer": "格式化文字編輯器" }, "labs_mjolnir": { @@ -1519,7 +1506,6 @@ "mute_description": "不會收到任何通知" }, "notifier": { - "io.element.voice_broadcast_chunk": "%(senderName)s 開始了語音廣播", "m.key.verification.request": "%(name)s 正在要求驗證" }, "onboarding": { @@ -2074,7 +2060,6 @@ "error_unbanning": "無法解除封鎖", "events_default": "傳送訊息", "invite": "邀請使用者", - "io.element.voice_broadcast_info": "語音廣播", "kick": "移除使用者", "m.call": "開始 %(brand)s 通話", "m.call.member": "加入 %(brand)s 通話", @@ -2704,7 +2689,6 @@ "warning": "<w>警告:</w> <description/>" }, "share": { - "link_title": "連結到聊天室", "permalink_message": "連結到選定的訊息", "permalink_most_recent": "連結到最近的訊息", "title_message": "分享聊天室訊息", @@ -2791,13 +2775,6 @@ "upgraderoom": "升級聊天室到新版本", "upgraderoom_permission_error": "您沒有使用此指令的必要權限。", "usage": "使用方法", - "verify": "驗證使用者、工作階段與公開金鑰組合", - "verify_mismatch": "警告:無法驗證金鑰!%(userId)s 與工作階段 %(deviceId)s 簽署的金鑰是「%(fprint)s」,並不符合提供的金鑰「%(fingerprint)s」。這可能代表您的通訊已被攔截!", - "verify_nop": "工作階段已驗證!", - "verify_nop_warning_mismatch": "警告:工作階段已驗證,但金鑰不相符!", - "verify_success_description": "您提供的簽署金鑰符合您從 %(userId)s 的工作階段收到的簽署金鑰 %(deviceId)s。工作階段標記為已驗證。", - "verify_success_title": "已驗證的金鑰", - "verify_unknown_pair": "未知(使用者,工作階段)配對:(%(userId)s, %(deviceId)s)", "view": "檢視指定聊天室的地址", "whois": "顯示關於使用者的資訊" }, @@ -3005,10 +2982,6 @@ "error_rendering_message": "無法載入此訊息", "historical_messages_unavailable": "您看不到更早的訊息", "in_room_name": " 在 <strong>%(room)s</strong>", - "io.element.voice_broadcast_info": { - "user": "%(senderName)s 結束了<a>語音廣播</a>", - "you": "您結束了<a>語音廣播</a>" - }, "io.element.widgets.layout": "%(senderName)s 已更新聊天室佈局", "load_error": { "no_permission": "嘗試載入此聊天室時間軸上的特定時間點,但您沒有權限檢視相關的訊息。", @@ -3536,38 +3509,6 @@ "switch_theme_dark": "切換至深色模式", "switch_theme_light": "切換至淺色模式" }, - "voice_broadcast": { - "30s_backward": "快退30秒", - "30s_forward": "快轉30秒", - "action": "語音廣播", - "buffering": "正在緩衝…", - "confirm_listen_affirm": "是的,結束我的錄製", - "confirm_listen_description": "若您開始收聽本次直播,您目前的直播錄製將會結束。", - "confirm_listen_title": "聆聽直播?", - "confirm_stop_affirm": "是的,停止廣播", - "confirm_stop_description": "您真的要停止即時廣播嗎?將會結束廣播,完整錄音存檔稍後將在聊天室中提供。", - "confirm_stop_title": "停止即時廣播?", - "connection_error": "連線錯誤 - 已暫停錄音", - "failed_already_recording_description": "您已經開始錄製語音廣播。請結束您目前的語音廣播以開始新的語音廣播。", - "failed_already_recording_title": "無法啟動新的語音廣播", - "failed_decrypt": "無法解密語音廣播", - "failed_generic": "無法播放此語音廣播", - "failed_insufficient_permission_description": "您沒有權限在此聊天室內開始語音廣播。請聯絡聊天室管理員升級您的權限。", - "failed_insufficient_permission_title": "無法啟動新的語音廣播", - "failed_no_connection_description": "很抱歉,現在無法錄音。請稍後再試。", - "failed_no_connection_title": "連線錯誤", - "failed_others_already_recording_description": "其他人已在錄製語音廣播。等待他們的語音廣播結束以開始新的。", - "failed_others_already_recording_title": "無法啟動新的語音廣播", - "go_live": "開始直播", - "live": "直播", - "pause": "暫停語音廣播", - "play": "播放語音廣播", - "resume": "恢復語音廣播" - }, - "voice_message": { - "cant_start_broadcast_description": "您無法開始語音訊息,因為您目前正在錄製直播。請結束您的直播以開始錄製語音訊息。", - "cant_start_broadcast_title": "無法開始語音訊息" - }, "voip": { "already_in_call": "已在通話中", "already_in_call_person": "您正在與此人通話。", @@ -3587,7 +3528,6 @@ "camera_disabled": "您的相機已關閉", "camera_enabled": "您的相機開啟中", "cannot_call_yourself_description": "您不能打電話給自己。", - "change_input_device": "變更輸入裝置", "connecting": "連線中", "connection_lost": "與伺服器的連線已遺失", "connection_lost_description": "您無法在未連線至伺服器的情況下通話。", @@ -3604,8 +3544,6 @@ "enable_camera": "開啟相機", "enable_microphone": "取消麥克風靜音", "expand": "回到通話", - "failed_call_live_broadcast_description": "您無法開始通話,因為您正在錄製直播。請結束您的直播以便開始通話。", - "failed_call_live_broadcast_title": "無法開始通話", "hangup": "掛斷", "hide_sidebar_button": "隱藏側邊欄", "input_devices": "輸入裝置", diff --git a/src/indexing/EventIndex.ts b/src/indexing/EventIndex.ts index fc1be4eba5..da2eee995c 100644 --- a/src/indexing/EventIndex.ts +++ b/src/indexing/EventIndex.ts @@ -819,7 +819,11 @@ export default class EventIndex extends EventEmitter { // Add the events to the timeline of the file panel. matrixEvents.forEach((e) => { if (!timelineSet.eventIdToTimeline(e.getId()!)) { - timelineSet.addEventToTimeline(e, timeline, direction == EventTimeline.BACKWARDS); + timelineSet.addEventToTimeline(e, timeline, { + toStartOfTimeline: direction == EventTimeline.BACKWARDS, + fromCache: false, + addToState: false, + }); } }); diff --git a/src/rageshake/rageshake.ts b/src/rageshake/rageshake.ts index 763df51d95..c68fa8503c 100644 --- a/src/rageshake/rageshake.ts +++ b/src/rageshake/rageshake.ts @@ -97,6 +97,7 @@ export class ConsoleLogger { // run. // Example line: // 2017-01-18T11:23:53.214Z W Failed to set badge count + // eslint-disable-next-line @typescript-eslint/no-base-to-string let line = `${ts} ${level} ${args.join(" ")}\n`; // Do some cleanup line = line.replace(/token=[a-zA-Z0-9-]+/gm, "token=xxxxx"); diff --git a/src/rageshake/submit-rageshake.ts b/src/rageshake/submit-rageshake.ts index ec529336d1..0c00cc777a 100644 --- a/src/rageshake/submit-rageshake.ts +++ b/src/rageshake/submit-rageshake.ts @@ -9,7 +9,8 @@ Please see LICENSE files in the repository root for full details. */ import { logger } from "matrix-js-sdk/src/logger"; -import { Method, MatrixClient, Crypto } from "matrix-js-sdk/src/matrix"; +import { Method, MatrixClient } from "matrix-js-sdk/src/matrix"; +import { CryptoApi } from "matrix-js-sdk/src/crypto-api"; import type * as Pako from "pako"; import { MatrixClientPeg } from "../MatrixClientPeg"; @@ -169,7 +170,7 @@ async function collectSynapseSpecific(client: MatrixClient, body: FormData): Pro /** * Collects crypto related information. */ -async function collectCryptoInfo(cryptoApi: Crypto.CryptoApi, body: FormData): Promise<void> { +async function collectCryptoInfo(cryptoApi: CryptoApi, body: FormData): Promise<void> { body.append("crypto_version", cryptoApi.getVersion()); const ownDeviceKeys = await cryptoApi.getOwnDeviceKeys(); @@ -198,7 +199,7 @@ async function collectCryptoInfo(cryptoApi: Crypto.CryptoApi, body: FormData): P /** * Collects information about secret storage and backup. */ -async function collectRecoveryInfo(client: MatrixClient, cryptoApi: Crypto.CryptoApi, body: FormData): Promise<void> { +async function collectRecoveryInfo(client: MatrixClient, cryptoApi: CryptoApi, body: FormData): Promise<void> { const secretStorage = client.secretStorage; body.append("secret_storage_ready", String(await cryptoApi.isSecretStorageReady())); body.append("secret_storage_key_in_account", String(await secretStorage.hasKey())); diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 3442227760..1551805376 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -85,18 +85,9 @@ export enum LabGroup { } export enum Features { - VoiceBroadcast = "feature_voice_broadcast", - VoiceBroadcastForceSmallChunks = "feature_voice_broadcast_force_small_chunks", NotificationSettings2 = "feature_notification_settings2", OidcNativeFlow = "feature_oidc_native_flow", ReleaseAnnouncement = "feature_release_announcement", - - /** If true, use the Rust crypto implementation. - * - * This is no longer read, but we continue to populate it on all devices, to guard against people rolling back to - * old versions of EW that do not use rust crypto by default. - */ - RustCrypto = "feature_rust_crypto", } export const labGroupNames: Record<LabGroup, TranslationKey> = { @@ -442,19 +433,6 @@ export const SETTINGS: { [setting: string]: ISetting } = { shouldWarn: true, default: false, }, - [Features.VoiceBroadcast]: { - isFeature: true, - labsGroup: LabGroup.Messaging, - supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG_PRIORITISED, - supportedLevelsAreOrdered: true, - displayName: _td("labs|voice_broadcast"), - default: false, - }, - [Features.VoiceBroadcastForceSmallChunks]: { - supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, - displayName: _td("labs|voice_broadcast_force_small_chunks"), - default: false, - }, [Features.OidcNativeFlow]: { isFeature: true, labsGroup: LabGroup.Developer, @@ -464,10 +442,6 @@ export const SETTINGS: { [setting: string]: ISetting } = { description: _td("labs|oidc_native_flow_description"), default: false, }, - [Features.RustCrypto]: { - supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, - default: true, - }, /** * @deprecated in favor of {@link fontSizeDelta} */ diff --git a/src/stores/InitialCryptoSetupStore.ts b/src/stores/InitialCryptoSetupStore.ts new file mode 100644 index 0000000000..0c2e49f5ca --- /dev/null +++ b/src/stores/InitialCryptoSetupStore.ts @@ -0,0 +1,140 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import EventEmitter from "events"; +import { MatrixClient } from "matrix-js-sdk/src/matrix"; +import { logger } from "matrix-js-sdk/src/logger"; +import { useEffect, useState } from "react"; + +import { createCrossSigning } from "../CreateCrossSigning"; +import { SdkContextClass } from "../contexts/SDKContext"; + +type Status = "in_progress" | "complete" | "error" | undefined; + +export const useInitialCryptoSetupStatus = (store: InitialCryptoSetupStore): Status => { + const [status, setStatus] = useState<Status>(store.getStatus()); + + useEffect(() => { + const update = (): void => { + setStatus(store.getStatus()); + }; + + store.on("update", update); + + return () => { + store.off("update", update); + }; + }, [store]); + + return status; +}; + +/** + * Logic for setting up crypto state that's done immediately after + * a user registers. Should be transparent to the user, not requiring + * interaction in most cases. + * As distinct from SetupEncryptionStore which is for setting up + * 4S or verifying the device, will always require interaction + * from the user in some form. + */ +export class InitialCryptoSetupStore extends EventEmitter { + private status: Status = undefined; + + private client?: MatrixClient; + private isTokenLogin?: boolean; + private stores?: SdkContextClass; + private onFinished?: (success: boolean) => void; + + public static sharedInstance(): InitialCryptoSetupStore { + if (!window.mxInitialCryptoStore) window.mxInitialCryptoStore = new InitialCryptoSetupStore(); + return window.mxInitialCryptoStore; + } + + public getStatus(): Status { + return this.status; + } + + /** + * Start the initial crypto setup process. + * + * @param {MatrixClient} client The client to use for the setup + * @param {boolean} isTokenLogin True if the user logged in via a token login, otherwise false + * @param {SdkContextClass} stores The stores to use for the setup + */ + public startInitialCryptoSetup( + client: MatrixClient, + isTokenLogin: boolean, + stores: SdkContextClass, + onFinished: (success: boolean) => void, + ): void { + this.client = client; + this.isTokenLogin = isTokenLogin; + this.stores = stores; + this.onFinished = onFinished; + + // We just start this process: it's progress is tracked by the events rather + // than returning a promise, so we don't bother. + this.doSetup().catch(() => logger.error("Initial crypto setup failed")); + } + + /** + * Retry the initial crypto setup process. + * + * If no crypto setup is currently in process, this will return false. + * + * @returns {boolean} True if a retry was initiated, otherwise false + */ + public retry(): boolean { + if (this.client === undefined || this.isTokenLogin === undefined || this.stores == undefined) return false; + + this.doSetup().catch(() => logger.error("Initial crypto setup failed")); + + return true; + } + + private reset(): void { + this.client = undefined; + this.isTokenLogin = undefined; + this.stores = undefined; + } + + private async doSetup(): Promise<void> { + if (this.client === undefined || this.isTokenLogin === undefined || this.stores == undefined) { + throw new Error("No setup is in progress"); + } + + const cryptoApi = this.client.getCrypto(); + if (!cryptoApi) throw new Error("No crypto module found!"); + + this.status = "in_progress"; + this.emit("update"); + + try { + await createCrossSigning(this.client, this.isTokenLogin, this.stores.accountPasswordStore.getPassword()); + + this.reset(); + + this.status = "complete"; + this.emit("update"); + this.onFinished?.(true); + } catch (e) { + if (this.isTokenLogin) { + // ignore any failures, we are relying on grace period here + this.reset(); + + this.status = "complete"; + this.emit("update"); + this.onFinished?.(true); + + return; + } + logger.error("Error bootstrapping cross-signing", e); + this.status = "error"; + this.emit("update"); + } + } +} diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx index 53e25736f0..66644c06a1 100644 --- a/src/stores/RoomViewStore.tsx +++ b/src/stores/RoomViewStore.tsx @@ -42,15 +42,6 @@ import { UPDATE_EVENT } from "./AsyncStore"; import { SdkContextClass } from "../contexts/SDKContext"; import { CallStore } from "./CallStore"; import { ThreadPayload } from "../dispatcher/payloads/ThreadPayload"; -import { - doClearCurrentVoiceBroadcastPlaybackIfStopped, - doMaybeSetCurrentVoiceBroadcastPlayback, - VoiceBroadcastRecording, - VoiceBroadcastRecordingsStoreEvent, -} from "../voice-broadcast"; -import { IRoomStateEventsActionPayload } from "../actions/MatrixActionCreators"; -import { showCantStartACallDialog } from "../voice-broadcast/utils/showCantStartACallDialog"; -import { pauseNonLiveBroadcastFromOtherRoom } from "../voice-broadcast/utils/pauseNonLiveBroadcastFromOtherRoom"; import { ActionPayload } from "../dispatcher/payloads"; import { CancelAskToJoinPayload } from "../dispatcher/payloads/CancelAskToJoinPayload"; import { SubmitAskToJoinPayload } from "../dispatcher/payloads/SubmitAskToJoinPayload"; @@ -164,10 +155,6 @@ export class RoomViewStore extends EventEmitter { ) { super(); this.resetDispatcher(dis); - this.stores.voiceBroadcastRecordingsStore.addListener( - VoiceBroadcastRecordingsStoreEvent.CurrentChanged, - this.onCurrentBroadcastRecordingChanged, - ); } public addRoomListener(roomId: string, fn: Listener): void { @@ -182,16 +169,6 @@ export class RoomViewStore extends EventEmitter { this.emit(roomId, isActive); } - private onCurrentBroadcastRecordingChanged = (recording: VoiceBroadcastRecording | null): void => { - if (recording === null) { - const room = this.stores.client?.getRoom(this.state.roomId || undefined); - - if (room) { - this.doMaybeSetCurrentVoiceBroadcastPlayback(room); - } - } - }; - private setState(newState: Partial<State>): void { // If values haven't changed, there's nothing to do. // This only tries a shallow comparison, so unchanged objects will slip @@ -207,16 +184,6 @@ export class RoomViewStore extends EventEmitter { return; } - if (newState.viewingCall) { - // Pause current broadcast, if any - this.stores.voiceBroadcastPlaybacksStore.getCurrent()?.pause(); - - if (this.stores.voiceBroadcastRecordingsStore.getCurrent()) { - showCantStartACallDialog(); - newState.viewingCall = false; - } - } - const lastRoomId = this.state.roomId; this.state = Object.assign(this.state, newState); if (lastRoomId !== this.state.roomId) { @@ -235,29 +202,6 @@ export class RoomViewStore extends EventEmitter { this.emit(UPDATE_EVENT); } - private doMaybeSetCurrentVoiceBroadcastPlayback(room: Room): void { - if (!this.stores.client) return; - doMaybeSetCurrentVoiceBroadcastPlayback( - room, - this.stores.client, - this.stores.voiceBroadcastPlaybacksStore, - this.stores.voiceBroadcastRecordingsStore, - ); - } - - private onRoomStateEvents(event: MatrixEvent): void { - const roomId = event.getRoomId?.(); - - // no room or not current room - if (!roomId || roomId !== this.state.roomId) return; - - const room = this.stores.client?.getRoom(roomId); - - if (room) { - this.doMaybeSetCurrentVoiceBroadcastPlayback(room); - } - } - private onDispatch(payload: ActionPayload): void { // eslint-disable-line @typescript-eslint/naming-convention switch (payload.action) { @@ -283,10 +227,6 @@ export class RoomViewStore extends EventEmitter { wasContextSwitch: false, viewingCall: false, }); - doClearCurrentVoiceBroadcastPlaybackIfStopped(this.stores.voiceBroadcastPlaybacksStore); - break; - case "MatrixActions.RoomState.events": - this.onRoomStateEvents((payload as IRoomStateEventsActionPayload).event); break; case Action.ViewRoomError: this.viewRoomError(payload as ViewRoomErrorPayload); @@ -489,9 +429,6 @@ export class RoomViewStore extends EventEmitter { } if (room) { - pauseNonLiveBroadcastFromOtherRoom(room, this.stores.voiceBroadcastPlaybacksStore); - this.doMaybeSetCurrentVoiceBroadcastPlayback(room); - await setMarkedUnreadState(room, MatrixClientPeg.safeGet(), false); } } else if (payload.room_alias) { diff --git a/src/stores/SetupEncryptionStore.ts b/src/stores/SetupEncryptionStore.ts index 2fb9c6a9ca..a13ba26f72 100644 --- a/src/stores/SetupEncryptionStore.ts +++ b/src/stores/SetupEncryptionStore.ts @@ -33,6 +33,11 @@ export enum Phase { ConfirmReset = 6, } +/** + * Logic for setting up 4S and/or verifying the user's device: a process requiring + * ongoing interaction with the user, as distinct from InitialCryptoSetupStore which + * a (usually) non-interactive process that happens immediately after registration. + */ export class SetupEncryptionStore extends EventEmitter { private started?: boolean; public phase?: Phase; @@ -125,7 +130,7 @@ export class SetupEncryptionStore extends EventEmitter { this.emit("update"); try { const cli = MatrixClientPeg.safeGet(); - const backupInfo = await cli.getKeyBackupVersion(); + const backupInfo = (await cli.getCrypto()?.getKeyBackupInfo()) ?? null; this.backupInfo = backupInfo; this.emit("update"); diff --git a/src/stores/ThreepidInviteStore.ts b/src/stores/ThreepidInviteStore.ts index 23c6d450d6..bdb1c8faa2 100644 --- a/src/stores/ThreepidInviteStore.ts +++ b/src/stores/ThreepidInviteStore.ts @@ -99,7 +99,7 @@ export default class ThreepidInviteStore extends EventEmitter { private generateIdOf(persisted: IPersistedThreepidInvite): string { // Use a consistent "hash" to form an ID. - return base32.stringify(Buffer.from(JSON.stringify(persisted))); + return base32.stringify(new TextEncoder().encode(JSON.stringify(persisted))); } private translateInvite(persisted: IPersistedThreepidInvite): IThreepidInvite { diff --git a/src/stores/right-panel/RightPanelStore.ts b/src/stores/right-panel/RightPanelStore.ts index 9da06580dc..99b2d7fe50 100644 --- a/src/stores/right-panel/RightPanelStore.ts +++ b/src/stores/right-panel/RightPanelStore.ts @@ -304,15 +304,13 @@ export default class RightPanelStore extends ReadyWatchingStore { logger.warn("removed card from right panel because of missing threadHeadEvent in card state"); } return !!card.state?.threadHeadEvent; - case RightPanelPhases.RoomMemberInfo: - case RightPanelPhases.SpaceMemberInfo: + case RightPanelPhases.MemberInfo: case RightPanelPhases.EncryptionPanel: if (!card.state?.member) { logger.warn("removed card from right panel because of missing member in card state"); } return !!card.state?.member; - case RightPanelPhases.Room3pidMemberInfo: - case RightPanelPhases.Space3pidMemberInfo: + case RightPanelPhases.ThreePidMemberInfo: if (!card.state?.memberInfoEvent) { logger.warn("removed card from right panel because of missing memberInfoEvent in card state"); } @@ -327,7 +325,7 @@ export default class RightPanelStore extends ReadyWatchingStore { } private getVerificationRedirect(card: IRightPanelCard): IRightPanelCard | null { - if (card.phase === RightPanelPhases.RoomMemberInfo && card.state) { + if (card.phase === RightPanelPhases.MemberInfo && card.state) { // RightPanelPhases.RoomMemberInfo -> needs to be changed to RightPanelPhases.EncryptionPanel if there is a pending verification request const { member } = card.state; const pendingRequest = member @@ -385,8 +383,7 @@ export default class RightPanelStore extends ReadyWatchingStore { if (panel?.history) { panel.history = panel.history.filter( (card: IRightPanelCard) => - card.phase != RightPanelPhases.RoomMemberInfo && - card.phase != RightPanelPhases.Room3pidMemberInfo, + card.phase != RightPanelPhases.MemberInfo && card.phase != RightPanelPhases.ThreePidMemberInfo, ); } } diff --git a/src/stores/right-panel/RightPanelStoreIPanelState.ts b/src/stores/right-panel/RightPanelStoreIPanelState.ts index afb7442563..0d205abd2f 100644 --- a/src/stores/right-panel/RightPanelStoreIPanelState.ts +++ b/src/stores/right-panel/RightPanelStoreIPanelState.ts @@ -16,7 +16,6 @@ export interface IRightPanelCardState { verificationRequest?: VerificationRequest; verificationRequestPromise?: Promise<VerificationRequest>; widgetId?: string; - spaceId?: string; // Room3pidMemberInfo, Space3pidMemberInfo, memberInfoEvent?: MatrixEvent; // threads @@ -32,7 +31,6 @@ export interface IRightPanelCardStateStored { memberId?: string; // we do not store the things associated with verification widgetId?: string; - spaceId?: string; // 3pidMemberInfo memberInfoEventId?: string; // threads @@ -80,7 +78,6 @@ export function convertCardToStore(panelState: IRightPanelCard): IRightPanelCard const state = panelState.state ?? {}; const stateStored: IRightPanelCardStateStored = { widgetId: state.widgetId, - spaceId: state.spaceId, isInitialEventHighlighted: state.isInitialEventHighlighted, initialEventScrollIntoView: state.initialEventScrollIntoView, threadHeadEventId: !!state?.threadHeadEvent?.getId() ? state.threadHeadEvent.getId() : undefined, @@ -97,7 +94,6 @@ function convertStoreToCard(panelStateStore: IRightPanelCardStored, room: Room): const stateStored = panelStateStore.state ?? {}; const state: IRightPanelCardState = { widgetId: stateStored.widgetId, - spaceId: stateStored.spaceId, isInitialEventHighlighted: stateStored.isInitialEventHighlighted, initialEventScrollIntoView: stateStored.initialEventScrollIntoView, threadHeadEvent: !!stateStored?.threadHeadEventId diff --git a/src/stores/right-panel/RightPanelStorePhases.ts b/src/stores/right-panel/RightPanelStorePhases.ts index 60b9e50baf..9e7a5697bf 100644 --- a/src/stores/right-panel/RightPanelStorePhases.ts +++ b/src/stores/right-panel/RightPanelStorePhases.ts @@ -10,11 +10,14 @@ import { _t } from "../../languageHandler"; // These are in their own file because of circular imports being a problem. export enum RightPanelPhases { + // Room & Space stuff + MemberList = "MemberList", + MemberInfo = "MemberInfo", + ThreePidMemberInfo = "ThreePidMemberInfo", + // Room stuff - RoomMemberList = "RoomMemberList", FilePanel = "FilePanel", NotificationPanel = "NotificationPanel", - RoomMemberInfo = "RoomMemberInfo", EncryptionPanel = "EncryptionPanel", RoomSummary = "RoomSummary", Widget = "Widget", @@ -22,13 +25,6 @@ export enum RightPanelPhases { Timeline = "Timeline", Extensions = "Extensions", - Room3pidMemberInfo = "Room3pidMemberInfo", - - // Space stuff - SpaceMemberList = "SpaceMemberList", - SpaceMemberInfo = "SpaceMemberInfo", - Space3pidMemberInfo = "Space3pidMemberInfo", - // Thread stuff ThreadView = "ThreadView", ThreadPanel = "ThreadPanel", @@ -42,7 +38,7 @@ export function backLabelForPhase(phase: RightPanelPhases | null): string | null return _t("chat_card_back_action_label"); case RightPanelPhases.RoomSummary: return _t("room_summary_card_back_action_label"); - case RightPanelPhases.RoomMemberList: + case RightPanelPhases.MemberList: return _t("member_list_back_action_label"); case RightPanelPhases.ThreadView: return _t("thread_view_back_action_label"); diff --git a/src/stores/right-panel/action-handlers/View3pidInvite.ts b/src/stores/right-panel/action-handlers/View3pidInvite.ts index e2aa191acb..0f6661819f 100644 --- a/src/stores/right-panel/action-handlers/View3pidInvite.ts +++ b/src/stores/right-panel/action-handlers/View3pidInvite.ts @@ -20,10 +20,10 @@ import { RightPanelPhases } from "../RightPanelStorePhases"; export const onView3pidInvite = (payload: ActionPayload, rightPanelStore: RightPanelStore): void => { if (payload.event) { rightPanelStore.pushCard({ - phase: RightPanelPhases.Room3pidMemberInfo, + phase: RightPanelPhases.ThreePidMemberInfo, state: { memberInfoEvent: payload.event }, }); } else { - rightPanelStore.showOrHidePhase(RightPanelPhases.RoomMemberList); + rightPanelStore.showOrHidePhase(RightPanelPhases.MemberList); } }; diff --git a/src/stores/room-list/MessagePreviewStore.ts b/src/stores/room-list/MessagePreviewStore.ts index e0e06ec980..2577b2ba23 100644 --- a/src/stores/room-list/MessagePreviewStore.ts +++ b/src/stores/room-list/MessagePreviewStore.ts @@ -22,8 +22,6 @@ import { StickerEventPreview } from "./previews/StickerEventPreview"; import { ReactionEventPreview } from "./previews/ReactionEventPreview"; 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 @@ -69,10 +67,6 @@ const PREVIEWS: Record< isState: false, previewer: new PollStartEventPreview(), }, - [VoiceBroadcastInfoEventType]: { - isState: true, - previewer: new VoiceBroadcastPreview(), - }, }; // The maximum number of events we're willing to look back on to get a preview. diff --git a/src/stores/room-list/previews/MessageEventPreview.ts b/src/stores/room-list/previews/MessageEventPreview.ts index 2873320cf3..20631f1425 100644 --- a/src/stores/room-list/previews/MessageEventPreview.ts +++ b/src/stores/room-list/previews/MessageEventPreview.ts @@ -14,15 +14,11 @@ import { _t, sanitizeForTranslation } from "../../../languageHandler"; import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils"; import { getHtmlText } from "../../../HtmlUtils"; import { stripHTMLReply, stripPlainReply } from "../../../utils/Reply"; -import { VoiceBroadcastChunkEventType } from "../../../voice-broadcast/types"; export class MessageEventPreview implements IPreview { public getTextFor(event: MatrixEvent, tagId?: TagID, isThread?: boolean): string | null { let eventContent = event.getContent(); - // no preview for broadcast chunks - if (eventContent[VoiceBroadcastChunkEventType]) return null; - if (event.isRelation(RelationType.Replace)) { // It's an edit, generate the preview on the new text eventContent = event.getContent()["m.new_content"]; diff --git a/src/stores/room-list/previews/PollStartEventPreview.ts b/src/stores/room-list/previews/PollStartEventPreview.ts index bb005f4a94..7548cf12f7 100644 --- a/src/stores/room-list/previews/PollStartEventPreview.ts +++ b/src/stores/room-list/previews/PollStartEventPreview.ts @@ -18,7 +18,7 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext"; export class PollStartEventPreview implements IPreview { public static contextType = MatrixClientContext; - public declare context: React.ContextType<typeof MatrixClientContext>; + declare public context: React.ContextType<typeof MatrixClientContext>; public getTextFor(event: MatrixEvent, tagId?: TagID, isThread?: boolean): string | null { let eventContent = event.getContent(); diff --git a/src/stores/room-list/previews/VoiceBroadcastPreview.ts b/src/stores/room-list/previews/VoiceBroadcastPreview.ts deleted file mode 100644 index 94116692a6..0000000000 --- a/src/stores/room-list/previews/VoiceBroadcastPreview.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixEvent } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastInfoState } from "../../../voice-broadcast/types"; -import { textForVoiceBroadcastStoppedEventWithoutLink } from "../../../voice-broadcast/utils/textForVoiceBroadcastStoppedEventWithoutLink"; -import { IPreview } from "./IPreview"; - -export class VoiceBroadcastPreview implements IPreview { - public getTextFor(event: MatrixEvent, tagId?: string, isThread?: boolean): string | null { - if (!event.isRedacted() && event.getContent()?.state === VoiceBroadcastInfoState.Stopped) { - return textForVoiceBroadcastStoppedEventWithoutLink(event); - } - - return null; - } -} diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index 8362f1048a..0472b1664b 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -284,10 +284,6 @@ export class StopGapWidget extends EventEmitter { }); this.messaging.on("capabilitiesNotified", () => this.emit("capabilitiesNotified")); this.messaging.on(`action:${WidgetApiFromWidgetAction.OpenModalWidget}`, this.onOpenModal); - this.messaging.on(`action:${ElementWidgetActions.JoinCall}`, () => { - // pause voice broadcast recording when any widget sends a "join" - SdkContextClass.instance.voiceBroadcastRecordingsStore.getCurrent()?.pause(); - }); // Always attach a handler for ViewRoom, but permission check it internally this.messaging.on(`action:${ElementWidgetActions.ViewRoom}`, (ev: CustomEvent<IViewRoomApiRequest>) => { diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index 5bc2ac7fc0..de7a71fa80 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -194,6 +194,7 @@ export class StopGapWidgetDriver extends WidgetDriver { EventType.CallSDPStreamMetadataChanged, EventType.CallSDPStreamMetadataChangedPrefix, EventType.CallReplaces, + EventType.CallEncryptionKeysPrefix, ]; for (const eventType of sendRecvToDevice) { this.allowedCapabilities.add( diff --git a/src/theme.ts b/src/theme.ts index c0fb087159..94ae7cb6e4 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -7,6 +7,20 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ +import "@fontsource/inter/400.css"; +import "@fontsource/inter/400-italic.css"; +import "@fontsource/inter/500.css"; +import "@fontsource/inter/500-italic.css"; +import "@fontsource/inter/600.css"; +import "@fontsource/inter/600-italic.css"; +import "@fontsource/inter/700.css"; +import "@fontsource/inter/700-italic.css"; + +import "@fontsource/inconsolata/latin-ext-400.css"; +import "@fontsource/inconsolata/latin-400.css"; +import "@fontsource/inconsolata/latin-ext-700.css"; +import "@fontsource/inconsolata/latin-700.css"; + import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "./languageHandler"; diff --git a/src/toasts/IncomingCallToast.tsx b/src/toasts/IncomingCallToast.tsx index bdad2d4565..58e6980733 100644 --- a/src/toasts/IncomingCallToast.tsx +++ b/src/toasts/IncomingCallToast.tsx @@ -33,7 +33,7 @@ import { useEventEmitter } from "../hooks/useEventEmitter"; import { CallStore, CallStoreEvent } from "../stores/CallStore"; export const getIncomingCallToastKey = (callId: string, roomId: string): string => `call_${callId}_${roomId}`; -const MAX_RING_TIME_MS = 10 * 1000; +const MAX_RING_TIME_MS = 90 * 1000; interface JoinCallButtonWithCallProps { onClick: (e: ButtonEvent) => void; diff --git a/src/utils/EventRenderingUtils.ts b/src/utils/EventRenderingUtils.ts index 099bf768d8..ed8d4af101 100644 --- a/src/utils/EventRenderingUtils.ts +++ b/src/utils/EventRenderingUtils.ts @@ -21,7 +21,6 @@ import SettingsStore from "../settings/SettingsStore"; import { haveRendererForEvent, JitsiEventFactory, JSONEventFactory, pickFactory } from "../events/EventTileFactory"; import { getMessageModerationState, isLocationEvent, MessageModerationState } from "./EventUtils"; import { ElementCall } from "../models/Call"; -import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../voice-broadcast"; const calcIsInfoMessage = ( eventType: EventType | string, @@ -38,8 +37,7 @@ const calcIsInfoMessage = ( eventType !== EventType.RoomCreate && !M_POLL_START.matches(eventType) && !M_POLL_END.matches(eventType) && - !M_BEACON_INFO.matches(eventType) && - !(eventType === VoiceBroadcastInfoEventType && content?.state === VoiceBroadcastInfoState.Started) + !M_BEACON_INFO.matches(eventType) ); }; @@ -91,8 +89,7 @@ export function getEventDisplayInfo( (eventType === EventType.RoomMessage && msgtype === MsgType.Emote) || M_POLL_START.matches(eventType) || M_BEACON_INFO.matches(eventType) || - isLocationEvent(mxEvent) || - eventType === VoiceBroadcastInfoEventType; + isLocationEvent(mxEvent); // If we're showing hidden events in the timeline, we should use the // source tile when there's no regular tile for an event and also for diff --git a/src/utils/EventUtils.ts b/src/utils/EventUtils.ts index 7c5b80697b..d57cefa1b5 100644 --- a/src/utils/EventUtils.ts +++ b/src/utils/EventUtils.ts @@ -30,7 +30,6 @@ import { TimelineRenderingType } from "../contexts/RoomContext"; import { launchPollEditor } from "../components/views/messages/MPollBody"; import { Action } from "../dispatcher/actions"; import { ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload"; -import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../voice-broadcast/types"; /** * Returns whether an event should allow actions like reply, reactions, edit, etc. @@ -56,9 +55,7 @@ export function isContentActionable(mxEvent: MatrixEvent): boolean { mxEvent.getType() === "m.sticker" || M_POLL_START.matches(mxEvent.getType()) || M_POLL_END.matches(mxEvent.getType()) || - M_BEACON_INFO.matches(mxEvent.getType()) || - (mxEvent.getType() === VoiceBroadcastInfoEventType && - mxEvent.getContent()?.state === VoiceBroadcastInfoState.Started) + M_BEACON_INFO.matches(mxEvent.getType()) ) { return true; } diff --git a/src/utils/FontManager.ts b/src/utils/FontManager.ts deleted file mode 100644 index 1ffa653a27..0000000000 --- a/src/utils/FontManager.ts +++ /dev/null @@ -1,124 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2019-2021 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -/* - * Based on... - * ChromaCheck 1.16 - * author Roel Nieskens, https://pixelambacht.nl - * MIT license - */ -import { logger } from "matrix-js-sdk/src/logger"; - -function safariVersionCheck(ua: string): boolean { - logger.log("Browser is Safari - checking version for COLR support"); - try { - const safariVersionMatch = ua.match(/Mac OS X ([\d|_]+).*Version\/([\d|.]+).*Safari/); - if (safariVersionMatch) { - const macOSVersionStr = safariVersionMatch[1]; - const safariVersionStr = safariVersionMatch[2]; - const macOSVersion = macOSVersionStr.split("_").map((n) => parseInt(n, 10)); - const safariVersion = safariVersionStr.split(".").map((n) => parseInt(n, 10)); - const colrFontSupported = - macOSVersion[0] >= 10 && macOSVersion[1] >= 14 && safariVersion[0] >= 12 && safariVersion[0] < 17; - // https://www.colorfonts.wtf/ states Safari supports COLR fonts from this version on but Safari 17 breaks it - logger.log( - `COLR support on Safari requires macOS 10.14 and Safari 12-16, ` + - `detected Safari ${safariVersionStr} on macOS ${macOSVersionStr}, ` + - `COLR supported: ${colrFontSupported}`, - ); - return colrFontSupported; - } - } catch (err) { - logger.error("Error in Safari COLR version check", err); - } - logger.warn("Couldn't determine Safari version to check COLR font support, assuming no."); - return false; -} - -async function isColrFontSupported(): Promise<boolean> { - logger.log("Checking for COLR support"); - - const { userAgent } = navigator; - // Firefox has supported COLR fonts since version 26 - // but doesn't support the check below without - // "Extract canvas data" permissions - // when content blocking is enabled. - if (userAgent.includes("Firefox")) { - logger.log("Browser is Firefox - assuming COLR is supported"); - return true; - } - // Safari doesn't wait for the font to load (if it doesn't have it in cache) - // to emit the load event on the image, so there is no way to not make the check - // reliable. Instead sniff the version. - // Excluding "Chrome", as it's user agent unhelpfully also contains Safari... - if (!userAgent.includes("Chrome") && userAgent.includes("Safari")) { - return safariVersionCheck(userAgent); - } - - try { - const canvas = document.createElement("canvas"); - const context = canvas.getContext("2d")!; - const img = new Image(); - // eslint-disable-next-line - const fontCOLR = - "d09GRgABAAAAAAKAAAwAAAAAAowAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABDT0xSAAACVAAAABYAAAAYAAIAJUNQQUwAAAJsAAAAEgAAABLJAAAQT1MvMgAAAYAAAAA6AAAAYBfxJ0pjbWFwAAABxAAAACcAAAAsAAzpM2dseWYAAAH0AAAAGgAAABoNIh0kaGVhZAAAARwAAAAvAAAANgxLumdoaGVhAAABTAAAABUAAAAkCAEEAmhtdHgAAAG8AAAABgAAAAYEAAAAbG9jYQAAAewAAAAGAAAABgANAABtYXhwAAABZAAAABsAAAAgAg4AHW5hbWUAAAIQAAAAOAAAAD4C5wsecG9zdAAAAkgAAAAMAAAAIAADAAB4AWNgZGAAYQ5+qdB4fpuvDNIsDCBwaQGTAIi+VlscBaJZGMDiHAxMIAoAtjIF/QB4AWNgZGBgYQACOAkUQQWMAAGRABAAAAB4AWNgZGBgYGJgAdMMUJILJMQgAWICAAH3AC4AeAFjYGFhYJzAwMrAwDST6QwDA0M/hGZ8zWDMyMmAChgFkDgKQMBw4CXDSwYWEBdIYgAFBgYA/8sIdAAABAAAAAAAAAB4AWNgYGBkYAZiBgYeBhYGBSDNAoRA/kuG//8hpDgjWJ4BAFVMBiYAAAAAAAANAAAAAQAAAAAEAAQAAAMAABEhESEEAPwABAD8AAAAeAEtxgUNgAAAAMHHIQTShTlOAty9/4bf7AARCwlBNhBw4L/43qXjYGUmf19TMuLcj/BJL3XfBg54AWNgZsALAAB9AAR4AWNgYGAEYj4gFgGygGwICQACOwAoAAAAAAABAAEAAQAAAA4AAAAAyP8AAA=="; - const svg = ` - <svg xmlns="http://www.w3.org/2000/svg" width="20" height="100" style="background:#fff;fill:#000;"> - <style type="text/css"> - @font-face { - font-family: "chromacheck-colr"; - src: url(data:application/x-font-woff;base64,${fontCOLR}) format("woff"); - } - </style> - <text x="0" y="0" font-size="20"> - <tspan font-family="chromacheck-colr" x="0" dy="20"></tspan> - </text> - </svg>`; - canvas.width = 20; - canvas.height = 100; - - img.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg); - - logger.log("Waiting for COLR SVG to load"); - await new Promise((resolve) => (img.onload = resolve)); - logger.log("Drawing canvas to detect COLR support"); - context.drawImage(img, 0, 0); - const colrFontSupported = context.getImageData(10, 10, 1, 1).data[0] === 200; - logger.log("Canvas check revealed COLR is supported? " + colrFontSupported); - return colrFontSupported; - } catch (e) { - logger.error("Couldn't load COLR font", e); - return false; - } -} - -let colrFontCheckStarted = false; -export async function fixupColorFonts(): Promise<void> { - if (colrFontCheckStarted) { - return; - } - colrFontCheckStarted = true; - - if (await isColrFontSupported()) { - // eslint-disable-next-line @typescript-eslint/no-require-imports - const path = `url('${require("../../res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2")}')`; - document.fonts.add(new FontFace("Twemoji", path, {})); - // For at least Chrome on Windows 10, we have to explictly add extra - // weights for the emoji to appear in bold messages, etc. - document.fonts.add(new FontFace("Twemoji", path, { weight: "600" })); - document.fonts.add(new FontFace("Twemoji", path, { weight: "700" })); - } else { - // fall back to SBIX, generated via https://github.com/matrix-org/twemoji-colr/tree/matthew/sbix - // eslint-disable-next-line @typescript-eslint/no-require-imports - const path = `url('${require("../../res/fonts/Twemoji_Mozilla/TwemojiMozilla-sbix.woff2")}')`; - document.fonts.add(new FontFace("Twemoji", path, {})); - document.fonts.add(new FontFace("Twemoji", path, { weight: "600" })); - document.fonts.add(new FontFace("Twemoji", path, { weight: "700" })); - } - // ...and if SBIX is not supported, the browser will fall back to one of the native fonts specified. -} diff --git a/src/utils/WidgetUtils.ts b/src/utils/WidgetUtils.ts index de158efcef..ad2ed63ba1 100644 --- a/src/utils/WidgetUtils.ts +++ b/src/utils/WidgetUtils.ts @@ -445,7 +445,7 @@ export default class WidgetUtils { // For compatibility with Jitsi, use base32 without padding. // More details here: // https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification - confId = base32.stringify(Buffer.from(roomId), { pad: false }); + confId = base32.stringify(new TextEncoder().encode(roomId), { pad: false }); } else { // Create a random conference ID confId = `Jitsi${randomUppercaseString(1)}${randomLowercaseString(23)}`; diff --git a/src/utils/device/dehydration.ts b/src/utils/device/dehydration.ts index f18c4c5c7c..b27b3c54c2 100644 --- a/src/utils/device/dehydration.ts +++ b/src/utils/device/dehydration.ts @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import { logger } from "matrix-js-sdk/src/logger"; -import { Crypto } from "matrix-js-sdk/src/matrix"; +import { CryptoApi } from "matrix-js-sdk/src/crypto-api"; import { MatrixClientPeg } from "../../MatrixClientPeg"; @@ -21,7 +21,7 @@ import { MatrixClientPeg } from "../../MatrixClientPeg"; * * Dehydration can currently only be enabled by setting a flag in the .well-known file. */ -async function deviceDehydrationEnabled(crypto: Crypto.CryptoApi | undefined): Promise<boolean> { +async function deviceDehydrationEnabled(crypto: CryptoApi | undefined): Promise<boolean> { if (!crypto) { return false; } diff --git a/src/utils/dm/createDmLocalRoom.ts b/src/utils/dm/createDmLocalRoom.ts index 6d6cf0712b..0a3d312368 100644 --- a/src/utils/dm/createDmLocalRoom.ts +++ b/src/utils/dm/createDmLocalRoom.ts @@ -109,7 +109,7 @@ export async function createDmLocalRoom(client: MatrixClient, targets: Member[]) localRoom.targets = targets; localRoom.updateMyMembership(KnownMembership.Join); - localRoom.addLiveEvents(events); + localRoom.addLiveEvents(events, { addToState: true }); localRoom.currentState.setStateEvents(events); localRoom.name = localRoom.getDefaultRoomName(client.getUserId()!); client.store.storeRoom(localRoom); diff --git a/src/utils/exportUtils/HtmlExport.tsx b/src/utils/exportUtils/HtmlExport.tsx index 9a6bb93bba..e5b4667fc2 100644 --- a/src/utils/exportUtils/HtmlExport.tsx +++ b/src/utils/exportUtils/HtmlExport.tsx @@ -431,7 +431,7 @@ export default class HTMLExporter extends Exporter { !this.needsDateSeparator(event, prevEvent) && shouldFormContinuation(prevEvent, event, this.room.client, false); const body = await this.createMessageBody(event, shouldBeJoined); - this.totalSize += Buffer.byteLength(body); + this.totalSize += new TextEncoder().encode(body).byteLength; content += body; prevEvent = event; } diff --git a/src/utils/image-media.ts b/src/utils/image-media.ts index 5e0fb07678..5c013c7b1a 100644 --- a/src/utils/image-media.ts +++ b/src/utils/image-media.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { EncryptedFile } from "matrix-js-sdk/src/types"; +import { ImageInfo } from "matrix-js-sdk/src/types"; import { BlurhashEncoder } from "../BlurhashEncoder"; @@ -15,19 +15,7 @@ type ThumbnailableElement = HTMLImageElement | HTMLVideoElement; export const BLURHASH_FIELD = "xyz.amorgan.blurhash"; // MSC2448 interface IThumbnail { - info: { - thumbnail_info?: { - w: number; - h: number; - mimetype: string; - size: number; - }; - w: number; - h: number; - [BLURHASH_FIELD]?: string; - thumbnail_url?: string; - thumbnail_file?: EncryptedFile; - }; + info: ImageInfo; thumbnail: Blob; } diff --git a/src/utils/react.tsx b/src/utils/react.tsx index 164d704d91..b78f574fa9 100644 --- a/src/utils/react.tsx +++ b/src/utils/react.tsx @@ -15,23 +15,38 @@ import { createRoot, Root } from "react-dom/client"; export class ReactRootManager { private roots: Root[] = []; private rootElements: Element[] = []; + private revertElements: Array<null | Element> = []; public get elements(): Element[] { return this.rootElements; } - public render(children: ReactNode, element: Element): void { - const root = createRoot(element); + /** + * Render a React component into a new root based on the given root element + * @param children the React component to render + * @param rootElement the root element to render the component into + * @param revertElement the element to replace the root element with when unmounting + */ + public render(children: ReactNode, rootElement: Element, revertElement?: Element): void { + const root = createRoot(rootElement); this.roots.push(root); - this.rootElements.push(element); + this.rootElements.push(rootElement); + this.revertElements.push(revertElement ?? null); root.render(children); } + /** + * Unmount all roots and revert the elements they were rendered into + */ public unmount(): void { while (this.roots.length) { const root = this.roots.pop()!; - this.rootElements.pop(); + const rootElement = this.rootElements.pop(); + const revertElement = this.revertElements.pop(); root.unmount(); + if (revertElement) { + rootElement?.replaceWith(revertElement); + } } } } diff --git a/src/utils/tokens/pickling.ts b/src/utils/tokens/pickling.ts index a56915a488..5fc82c16a5 100644 --- a/src/utils/tokens/pickling.ts +++ b/src/utils/tokens/pickling.ts @@ -118,7 +118,7 @@ export async function buildAndEncodePickleKey( data.encrypted, ); if (pickleKeyBuf) { - return encodeUnpaddedBase64(pickleKeyBuf); + return encodeUnpaddedBase64(new Uint8Array(pickleKeyBuf)); } } catch { logger.error("Error decrypting pickle key"); diff --git a/src/utils/useId.ts b/src/utils/useId.ts deleted file mode 100644 index 6f7cf79598..0000000000 --- a/src/utils/useId.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2024 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; - -const getUniqueId = (() => { - return () => `:r${Math.random()}:`; -})(); - -// Replace this with React's own useId once we switch to React 18 -export const useId = (): string => React.useMemo(getUniqueId, []); diff --git a/src/vector/index.ts b/src/vector/index.ts index 60f0868eeb..42b69af70e 100644 --- a/src/vector/index.ts +++ b/src/vector/index.ts @@ -67,6 +67,10 @@ function checkBrowserFeatures(): boolean { // although this would start to make (more) assumptions about how rust-crypto loads its wasm. window.Modernizr.addTest("wasm", () => typeof WebAssembly === "object" && typeof WebAssembly.Module === "function"); + // Check that the session is in a secure context otherwise most Crypto & WebRTC APIs will be unavailable + // https://developer.mozilla.org/en-US/docs/Web/API/Window/isSecureContext + window.Modernizr.addTest("securecontext", () => window.isSecureContext); + const featureList = Object.keys(window.Modernizr) as Array<keyof ModernizrStatic>; let featureComplete = true; diff --git a/src/vector/platform/ElectronPlatform.tsx b/src/vector/platform/ElectronPlatform.tsx index d7ebd94bb2..ac6e7a7feb 100644 --- a/src/vector/platform/ElectronPlatform.tsx +++ b/src/vector/platform/ElectronPlatform.tsx @@ -474,10 +474,8 @@ export default class ElectronPlatform extends BasePlatform { const url = super.getOidcCallbackUrl(); url.protocol = "io.element.desktop"; // Trim the double slash into a single slash to comply with https://datatracker.ietf.org/doc/html/rfc8252#section-7.1 - // Chrome seems to have a strange issue where non-standard protocols prevent URL object mutations on pathname - // field, so we cannot mutate `pathname` reliably and instead have to rewrite the href manually. - if (url.pathname.startsWith("//")) { - url.href = url.href.replace(url.pathname, url.pathname.slice(1)); + if (url.href.startsWith(`${url.protocol}://`)) { + url.href = url.href.replace("://", ":/"); } return url; } diff --git a/src/verification.ts b/src/verification.ts index e446186f80..9f77496499 100644 --- a/src/verification.ts +++ b/src/verification.ts @@ -15,7 +15,6 @@ import { RightPanelPhases } from "./stores/right-panel/RightPanelStorePhases"; import { accessSecretStorage } from "./SecurityManager"; import UntrustedDeviceDialog from "./components/views/dialogs/UntrustedDeviceDialog"; import { IDevice } from "./components/views/right_panel/UserInfo"; -import { ManualDeviceKeyVerificationDialog } from "./components/views/dialogs/ManualDeviceKeyVerificationDialog"; import RightPanelStore from "./stores/right-panel/RightPanelStore"; import { IRightPanelCardState } from "./stores/right-panel/RightPanelStoreIPanelState"; import { findDMForUser } from "./utils/dm/findDMForUser"; @@ -53,11 +52,6 @@ export async function verifyDevice(matrixClient: MatrixClient, user: User, devic .getCrypto() ?.requestDeviceVerification(user.userId, device.deviceId); setRightPanel({ member: user, verificationRequestPromise }); - } else if (action === "legacy") { - Modal.createDialog(ManualDeviceKeyVerificationDialog, { - userId: user.userId, - device, - }); } }, }); @@ -81,7 +75,7 @@ function setRightPanel(state: IRightPanelCardState): void { } else { RightPanelStore.instance.setCards([ { phase: RightPanelPhases.RoomSummary }, - { phase: RightPanelPhases.RoomMemberInfo, state: { member: state.member } }, + { phase: RightPanelPhases.MemberInfo, state: { member: state.member } }, { phase: RightPanelPhases.EncryptionPanel, state }, ]); } diff --git a/src/voice-broadcast/audio/VoiceBroadcastRecorder.ts b/src/voice-broadcast/audio/VoiceBroadcastRecorder.ts deleted file mode 100644 index 8a6e17a1a5..0000000000 --- a/src/voice-broadcast/audio/VoiceBroadcastRecorder.ts +++ /dev/null @@ -1,181 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { isEqual } from "lodash"; -import { Optional } from "matrix-events-sdk"; -import { logger } from "matrix-js-sdk/src/logger"; -import { TypedEventEmitter } from "matrix-js-sdk/src/matrix"; - -import { getChunkLength } from ".."; -import { IRecordingUpdate, VoiceRecording } from "../../audio/VoiceRecording"; -import { concat } from "../../utils/arrays"; -import { IDestroyable } from "../../utils/IDestroyable"; -import { Singleflight } from "../../utils/Singleflight"; - -export enum VoiceBroadcastRecorderEvent { - ChunkRecorded = "chunk_recorded", - CurrentChunkLengthUpdated = "current_chunk_length_updated", -} - -interface EventMap { - [VoiceBroadcastRecorderEvent.ChunkRecorded]: (chunk: ChunkRecordedPayload) => void; - [VoiceBroadcastRecorderEvent.CurrentChunkLengthUpdated]: (length: number) => void; -} - -export interface ChunkRecordedPayload { - buffer: Uint8Array; - length: number; -} - -// char sequence of "OpusHead" -const OpusHead = [79, 112, 117, 115, 72, 101, 97, 100]; - -// char sequence of "OpusTags" -const OpusTags = [79, 112, 117, 115, 84, 97, 103, 115]; - -/** - * This class provides the function to seamlessly record fixed length chunks. - * Subscribe with on(VoiceBroadcastRecordingEvents.ChunkRecorded, (payload: ChunkRecordedPayload) => {}) - * to retrieve chunks while recording. - */ -export class VoiceBroadcastRecorder - extends TypedEventEmitter<VoiceBroadcastRecorderEvent, EventMap> - implements IDestroyable -{ - private opusHead?: Uint8Array; - private opusTags?: Uint8Array; - private chunkBuffer = new Uint8Array(0); - // position of the previous chunk in seconds - private previousChunkEndTimePosition = 0; - // current chunk length in seconds - private currentChunkLength = 0; - - public constructor( - private voiceRecording: VoiceRecording, - public readonly targetChunkLength: number, - ) { - super(); - this.voiceRecording.onDataAvailable = this.onDataAvailable; - } - - public async start(): Promise<void> { - await this.voiceRecording.start(); - this.voiceRecording.liveData.onUpdate((data: IRecordingUpdate) => { - this.setCurrentChunkLength(data.timeSeconds - this.previousChunkEndTimePosition); - }); - } - - /** - * Stops the recording and returns the remaining chunk (if any). - */ - public async stop(): Promise<Optional<ChunkRecordedPayload>> { - try { - await this.voiceRecording.stop(); - } catch { - // Ignore if the recording raises any error. - } - - // forget about that call, so that we can stop it again later - Singleflight.forgetAllFor(this.voiceRecording); - const chunk = this.extractChunk(); - this.currentChunkLength = 0; - this.previousChunkEndTimePosition = 0; - return chunk; - } - - public get contentType(): string { - return this.voiceRecording.contentType; - } - - private setCurrentChunkLength(currentChunkLength: number): void { - if (this.currentChunkLength === currentChunkLength) return; - - this.currentChunkLength = currentChunkLength; - this.emit(VoiceBroadcastRecorderEvent.CurrentChunkLengthUpdated, currentChunkLength); - } - - public getCurrentChunkLength(): number { - return this.currentChunkLength; - } - - private onDataAvailable = (data: ArrayBuffer): void => { - const dataArray = new Uint8Array(data); - - // extract the part, that contains the header type info - const headerType = Array.from(dataArray.slice(28, 36)); - - if (isEqual(OpusHead, headerType)) { - // data seems to be an "OpusHead" header - this.opusHead = dataArray; - return; - } - - if (isEqual(OpusTags, headerType)) { - // data seems to be an "OpusTags" header - this.opusTags = dataArray; - return; - } - - this.setCurrentChunkLength(this.voiceRecording.recorderSeconds! - this.previousChunkEndTimePosition); - this.handleData(dataArray); - }; - - private handleData(data: Uint8Array): void { - this.chunkBuffer = concat(this.chunkBuffer, data); - this.emitChunkIfTargetLengthReached(); - } - - private emitChunkIfTargetLengthReached(): void { - if (this.getCurrentChunkLength() >= this.targetChunkLength) { - this.emitAndResetChunk(); - } - } - - /** - * Extracts the current chunk and resets the buffer. - */ - private extractChunk(): Optional<ChunkRecordedPayload> { - if (this.chunkBuffer.length === 0) { - return null; - } - - if (!this.opusHead || !this.opusTags) { - logger.warn("Broadcast chunk cannot be extracted. OpusHead or OpusTags is missing."); - return null; - } - - const currentRecorderTime = this.voiceRecording.recorderSeconds!; - const payload: ChunkRecordedPayload = { - buffer: concat(this.opusHead!, this.opusTags!, this.chunkBuffer), - length: this.getCurrentChunkLength(), - }; - this.chunkBuffer = new Uint8Array(0); - this.setCurrentChunkLength(0); - this.previousChunkEndTimePosition = currentRecorderTime; - return payload; - } - - private emitAndResetChunk(): void { - if (this.chunkBuffer.length === 0) { - return; - } - - this.emit(VoiceBroadcastRecorderEvent.ChunkRecorded, this.extractChunk()!); - } - - public destroy(): void { - this.removeAllListeners(); - this.voiceRecording.destroy(); - } -} - -export const createVoiceBroadcastRecorder = (): VoiceBroadcastRecorder => { - const voiceRecording = new VoiceRecording(); - voiceRecording.disableMaxLength(); - return new VoiceBroadcastRecorder(voiceRecording, getChunkLength()); -}; diff --git a/src/voice-broadcast/components/VoiceBroadcastBody.tsx b/src/voice-broadcast/components/VoiceBroadcastBody.tsx deleted file mode 100644 index 916ee9f907..0000000000 --- a/src/voice-broadcast/components/VoiceBroadcastBody.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React, { useContext, useEffect, useState } from "react"; -import { MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix"; - -import { - VoiceBroadcastRecordingBody, - shouldDisplayAsVoiceBroadcastRecordingTile, - VoiceBroadcastInfoEventType, - VoiceBroadcastPlaybackBody, - VoiceBroadcastInfoState, -} from ".."; -import { IBodyProps } from "../../components/views/messages/IBodyProps"; -import { RelationsHelper, RelationsHelperEvent } from "../../events/RelationsHelper"; -import { SDKContext } from "../../contexts/SDKContext"; -import { useMatrixClientContext } from "../../contexts/MatrixClientContext"; - -export const VoiceBroadcastBody: React.FC<IBodyProps> = ({ mxEvent }) => { - const sdkContext = useContext(SDKContext); - const client = useMatrixClientContext(); - const [infoState, setInfoState] = useState(mxEvent.getContent()?.state || VoiceBroadcastInfoState.Stopped); - - useEffect(() => { - const onInfoEvent = (event: MatrixEvent): void => { - if (event.getContent()?.state === VoiceBroadcastInfoState.Stopped) { - // only a stopped event can change the tile state - setInfoState(VoiceBroadcastInfoState.Stopped); - } - }; - - const relationsHelper = new RelationsHelper( - mxEvent, - RelationType.Reference, - VoiceBroadcastInfoEventType, - client, - ); - relationsHelper.on(RelationsHelperEvent.Add, onInfoEvent); - relationsHelper.emitCurrent(); - - return () => { - relationsHelper.destroy(); - }; - }); - - if (shouldDisplayAsVoiceBroadcastRecordingTile(infoState, client, mxEvent)) { - const recording = sdkContext.voiceBroadcastRecordingsStore.getByInfoEvent(mxEvent, client); - return <VoiceBroadcastRecordingBody recording={recording} />; - } - - const playback = sdkContext.voiceBroadcastPlaybacksStore.getByInfoEvent(mxEvent, client); - return <VoiceBroadcastPlaybackBody playback={playback} />; -}; diff --git a/src/voice-broadcast/components/atoms/LiveBadge.tsx b/src/voice-broadcast/components/atoms/LiveBadge.tsx deleted file mode 100644 index 2591fee435..0000000000 --- a/src/voice-broadcast/components/atoms/LiveBadge.tsx +++ /dev/null @@ -1,30 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import classNames from "classnames"; -import React from "react"; - -import { Icon as LiveIcon } from "../../../../res/img/compound/live-16px.svg"; -import { _t } from "../../../languageHandler"; - -interface Props { - grey?: boolean; -} - -export const LiveBadge: React.FC<Props> = ({ grey = false }) => { - const liveBadgeClasses = classNames("mx_LiveBadge", { - "mx_LiveBadge--grey": grey, - }); - - return ( - <div className={liveBadgeClasses}> - <LiveIcon className="mx_Icon mx_Icon_16" /> - {_t("voice_broadcast|live")} - </div> - ); -}; diff --git a/src/voice-broadcast/components/atoms/SeekButton.tsx b/src/voice-broadcast/components/atoms/SeekButton.tsx deleted file mode 100644 index 5ee0826488..0000000000 --- a/src/voice-broadcast/components/atoms/SeekButton.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; - -import AccessibleButton from "../../../components/views/elements/AccessibleButton"; - -interface Props { - icon: React.FC<React.SVGProps<SVGSVGElement>>; - label: string; - onClick: () => void; -} - -export const SeekButton: React.FC<Props> = ({ onClick, icon: Icon, label }) => { - return ( - <AccessibleButton kind="secondary_content" onClick={onClick} aria-label={label}> - <Icon className="mx_Icon mx_Icon_24" /> - </AccessibleButton> - ); -}; diff --git a/src/voice-broadcast/components/atoms/VoiceBroadcastControl.tsx b/src/voice-broadcast/components/atoms/VoiceBroadcastControl.tsx deleted file mode 100644 index 177b8fd732..0000000000 --- a/src/voice-broadcast/components/atoms/VoiceBroadcastControl.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import classNames from "classnames"; -import React, { ReactElement } from "react"; - -import AccessibleButton from "../../../components/views/elements/AccessibleButton"; - -interface Props { - className?: string; - icon: ReactElement; - label: string; - onClick: () => void; -} - -export const VoiceBroadcastControl: React.FC<Props> = ({ className = "", icon, label, onClick }) => { - return ( - <AccessibleButton - className={classNames("mx_VoiceBroadcastControl", className)} - onClick={onClick} - aria-label={label} - > - {icon} - </AccessibleButton> - ); -}; diff --git a/src/voice-broadcast/components/atoms/VoiceBroadcastError.tsx b/src/voice-broadcast/components/atoms/VoiceBroadcastError.tsx deleted file mode 100644 index d326853f4e..0000000000 --- a/src/voice-broadcast/components/atoms/VoiceBroadcastError.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { WarningIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; - -interface Props { - message: string; -} - -export const VoiceBroadcastError: React.FC<Props> = ({ message }) => { - return ( - <div className="mx_VoiceBroadcastRecordingConnectionError"> - <WarningIcon className="mx_Icon mx_Icon_16" /> - {message} - </div> - ); -}; diff --git a/src/voice-broadcast/components/atoms/VoiceBroadcastHeader.tsx b/src/voice-broadcast/components/atoms/VoiceBroadcastHeader.tsx deleted file mode 100644 index 52c0251c5e..0000000000 --- a/src/voice-broadcast/components/atoms/VoiceBroadcastHeader.tsx +++ /dev/null @@ -1,139 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { Room } from "matrix-js-sdk/src/matrix"; -import classNames from "classnames"; -import CloseIcon from "@vector-im/compound-design-tokens/assets/web/icons/close"; -import MicrophoneIcon from "@vector-im/compound-design-tokens/assets/web/icons/mic-on-solid"; - -import { LiveBadge, VoiceBroadcastLiveness } from "../.."; -import { Icon as LiveIcon } from "../../../../res/img/compound/live-16px.svg"; -import { Icon as TimerIcon } from "../../../../res/img/compound/timer-16px.svg"; -import { _t } from "../../../languageHandler"; -import RoomAvatar from "../../../components/views/avatars/RoomAvatar"; -import AccessibleButton, { ButtonEvent } from "../../../components/views/elements/AccessibleButton"; -import Clock from "../../../components/views/audio_messages/Clock"; -import { formatTimeLeft } from "../../../DateUtils"; -import Spinner from "../../../components/views/elements/Spinner"; -import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; -import { Action } from "../../../dispatcher/actions"; -import dis from "../../../dispatcher/dispatcher"; - -interface VoiceBroadcastHeaderProps { - linkToRoom?: boolean; - live?: VoiceBroadcastLiveness; - liveBadgePosition?: "middle" | "right"; - onCloseClick?: () => void; - onMicrophoneLineClick?: ((e: ButtonEvent) => void | Promise<void>) | null; - room: Room; - microphoneLabel?: string; - showBroadcast?: boolean; - showBuffering?: boolean; - bufferingPosition?: "line" | "title"; - timeLeft?: number; - showClose?: boolean; -} - -export const VoiceBroadcastHeader: React.FC<VoiceBroadcastHeaderProps> = ({ - linkToRoom = false, - live = "not-live", - liveBadgePosition = "right", - onCloseClick = (): void => {}, - onMicrophoneLineClick = null, - room, - microphoneLabel, - showBroadcast = false, - showBuffering = false, - bufferingPosition = "line", - showClose = false, - timeLeft, -}) => { - const broadcast = showBroadcast && ( - <div className="mx_VoiceBroadcastHeader_line"> - <LiveIcon className="mx_Icon mx_Icon_16" /> - {_t("voice_broadcast|action")} - </div> - ); - - const liveBadge = live !== "not-live" && <LiveBadge grey={live === "grey"} />; - - const closeButton = showClose && ( - <AccessibleButton onClick={onCloseClick}> - <CloseIcon className="mx_Icon mx_Icon_16" /> - </AccessibleButton> - ); - - const timeLeftLine = timeLeft && ( - <div className="mx_VoiceBroadcastHeader_line"> - <TimerIcon className="mx_Icon mx_Icon_16" /> - <Clock formatFn={formatTimeLeft} seconds={timeLeft} /> - </div> - ); - - const bufferingLine = showBuffering && bufferingPosition === "line" && ( - <div className="mx_VoiceBroadcastHeader_line"> - <Spinner w={14} h={14} /> - {_t("voice_broadcast|buffering")} - </div> - ); - - const microphoneLineClasses = classNames({ - mx_VoiceBroadcastHeader_line: true, - ["mx_VoiceBroadcastHeader_mic--clickable"]: onMicrophoneLineClick, - }); - - const microphoneLine = microphoneLabel && ( - <AccessibleButton - className={microphoneLineClasses} - onClick={onMicrophoneLineClick} - title={_t("voip|change_input_device")} - > - <MicrophoneIcon className="mx_Icon mx_Icon_16" /> - <span>{microphoneLabel}</span> - </AccessibleButton> - ); - - const onRoomAvatarOrNameClick = (): void => { - dis.dispatch<ViewRoomPayload>({ - action: Action.ViewRoom, - room_id: room.roomId, - metricsTrigger: undefined, // other - }); - }; - - let roomAvatar = <RoomAvatar room={room} size="32px" />; - let roomName = ( - <div className="mx_VoiceBroadcastHeader_room_wrapper"> - <div className="mx_VoiceBroadcastHeader_room">{room.name}</div> - {showBuffering && bufferingPosition === "title" && <Spinner w={12} h={12} />} - </div> - ); - - if (linkToRoom) { - roomAvatar = <AccessibleButton onClick={onRoomAvatarOrNameClick}>{roomAvatar}</AccessibleButton>; - - roomName = <AccessibleButton onClick={onRoomAvatarOrNameClick}>{roomName}</AccessibleButton>; - } - - return ( - <div className="mx_VoiceBroadcastHeader"> - {roomAvatar} - <div className="mx_VoiceBroadcastHeader_content"> - {roomName} - {microphoneLine} - {timeLeftLine} - {broadcast} - {bufferingLine} - {liveBadgePosition === "middle" && liveBadge} - </div> - {liveBadgePosition === "right" && liveBadge} - {closeButton} - </div> - ); -}; diff --git a/src/voice-broadcast/components/atoms/VoiceBroadcastPlaybackControl.tsx b/src/voice-broadcast/components/atoms/VoiceBroadcastPlaybackControl.tsx deleted file mode 100644 index 08531b8afd..0000000000 --- a/src/voice-broadcast/components/atoms/VoiceBroadcastPlaybackControl.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022, 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React, { ReactElement } from "react"; -import PauseIcon from "@vector-im/compound-design-tokens/assets/web/icons/pause-solid"; -import PlayIcon from "@vector-im/compound-design-tokens/assets/web/icons/play-solid"; - -import { _t } from "../../../languageHandler"; -import { VoiceBroadcastControl, VoiceBroadcastPlaybackState } from "../.."; - -interface Props { - onClick: () => void; - state: VoiceBroadcastPlaybackState; -} - -export const VoiceBroadcastPlaybackControl: React.FC<Props> = ({ onClick, state }) => { - let controlIcon: ReactElement | null = null; - let controlLabel: string | null = null; - let className = ""; - - switch (state) { - case VoiceBroadcastPlaybackState.Stopped: - controlIcon = <PlayIcon className="mx_Icon mx_Icon_16" />; - className = "mx_VoiceBroadcastControl-play"; - controlLabel = _t("voice_broadcast|play"); - break; - case VoiceBroadcastPlaybackState.Paused: - controlIcon = <PlayIcon className="mx_Icon mx_Icon_16" />; - className = "mx_VoiceBroadcastControl-play"; - controlLabel = _t("voice_broadcast|resume"); - break; - case VoiceBroadcastPlaybackState.Buffering: - case VoiceBroadcastPlaybackState.Playing: - controlIcon = <PauseIcon className="mx_Icon mx_Icon_12" />; - controlLabel = _t("voice_broadcast|pause"); - break; - } - - if (controlIcon && controlLabel) { - return ( - <VoiceBroadcastControl className={className} label={controlLabel} icon={controlIcon} onClick={onClick} /> - ); - } - - return null; -}; diff --git a/src/voice-broadcast/components/atoms/VoiceBroadcastRecordingConnectionError.tsx b/src/voice-broadcast/components/atoms/VoiceBroadcastRecordingConnectionError.tsx deleted file mode 100644 index 250d71f2f3..0000000000 --- a/src/voice-broadcast/components/atoms/VoiceBroadcastRecordingConnectionError.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { WarningIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; - -import { _t } from "../../../languageHandler"; - -export const VoiceBroadcastRecordingConnectionError: React.FC = () => { - return ( - <div className="mx_VoiceBroadcastRecordingConnectionError"> - <WarningIcon className="mx_Icon mx_Icon_16" /> - {_t("voice_broadcast|connection_error")} - </div> - ); -}; diff --git a/src/voice-broadcast/components/atoms/VoiceBroadcastRoomSubtitle.tsx b/src/voice-broadcast/components/atoms/VoiceBroadcastRoomSubtitle.tsx deleted file mode 100644 index 20b7379789..0000000000 --- a/src/voice-broadcast/components/atoms/VoiceBroadcastRoomSubtitle.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; - -import { Icon as LiveIcon } from "../../../../res/img/compound/live-16px.svg"; -import { _t } from "../../../languageHandler"; - -export const VoiceBroadcastRoomSubtitle: React.FC = () => { - return ( - <div className="mx_RoomTile_subtitle mx_RoomTile_subtitle--voice-broadcast"> - <LiveIcon className="mx_Icon mx_Icon_16" /> - {_t("voice_broadcast|live")} - </div> - ); -}; diff --git a/src/voice-broadcast/components/molecules/ConfirmListenBroadcastStopCurrent.tsx b/src/voice-broadcast/components/molecules/ConfirmListenBroadcastStopCurrent.tsx deleted file mode 100644 index 3dadfeba60..0000000000 --- a/src/voice-broadcast/components/molecules/ConfirmListenBroadcastStopCurrent.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; - -import BaseDialog from "../../../components/views/dialogs/BaseDialog"; -import DialogButtons from "../../../components/views/elements/DialogButtons"; -import { _t } from "../../../languageHandler"; -import Modal from "../../../Modal"; - -interface Props { - onFinished: (confirmed?: boolean) => void; -} - -export const ConfirmListenBroadcastStopCurrentDialog: React.FC<Props> = ({ onFinished }) => { - return ( - <BaseDialog title={_t("voice_broadcast|confirm_listen_title")} hasCancel={true} onFinished={onFinished}> - <p>{_t("voice_broadcast|confirm_listen_description")}</p> - <DialogButtons - onPrimaryButtonClick={() => onFinished(true)} - primaryButton={_t("voice_broadcast|confirm_listen_affirm")} - cancelButton={_t("action|no")} - onCancel={() => onFinished(false)} - /> - </BaseDialog> - ); -}; - -export const showConfirmListenBroadcastStopCurrentDialog = async (): Promise<boolean> => { - const { finished } = Modal.createDialog(ConfirmListenBroadcastStopCurrentDialog); - const [confirmed] = await finished; - return !!confirmed; -}; diff --git a/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx b/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx deleted file mode 100644 index 913b144960..0000000000 --- a/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx +++ /dev/null @@ -1,102 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React, { ReactElement } from "react"; -import classNames from "classnames"; - -import { - VoiceBroadcastError, - VoiceBroadcastHeader, - VoiceBroadcastPlayback, - VoiceBroadcastPlaybackControl, - VoiceBroadcastPlaybackState, -} from "../.."; -import { useVoiceBroadcastPlayback } from "../../hooks/useVoiceBroadcastPlayback"; -import { Icon as Back30sIcon } from "../../../../res/img/compound/back-30s-24px.svg"; -import { Icon as Forward30sIcon } from "../../../../res/img/compound/forward-30s-24px.svg"; -import { _t } from "../../../languageHandler"; -import Clock from "../../../components/views/audio_messages/Clock"; -import SeekBar from "../../../components/views/audio_messages/SeekBar"; -import { SeekButton } from "../atoms/SeekButton"; - -const SEEK_TIME = 30; - -interface VoiceBroadcastPlaybackBodyProps { - pip?: boolean; - playback: VoiceBroadcastPlayback; -} - -export const VoiceBroadcastPlaybackBody: React.FC<VoiceBroadcastPlaybackBodyProps> = ({ pip = false, playback }) => { - const { times, liveness, playbackState, room, sender, toggle } = useVoiceBroadcastPlayback(playback); - - let seekBackwardButton: ReactElement | null = null; - let seekForwardButton: ReactElement | null = null; - - if (playbackState !== VoiceBroadcastPlaybackState.Stopped) { - const onSeekBackwardButtonClick = (): void => { - playback.skipTo(Math.max(0, times.position - SEEK_TIME)); - }; - - seekBackwardButton = ( - <SeekButton - icon={Back30sIcon} - label={_t("voice_broadcast|30s_backward")} - onClick={onSeekBackwardButtonClick} - /> - ); - - const onSeekForwardButtonClick = (): void => { - playback.skipTo(Math.min(times.duration, times.position + SEEK_TIME)); - }; - - seekForwardButton = ( - <SeekButton - icon={Forward30sIcon} - label={_t("voice_broadcast|30s_forward")} - onClick={onSeekForwardButtonClick} - /> - ); - } - - const classes = classNames({ - mx_VoiceBroadcastBody: true, - ["mx_VoiceBroadcastBody--pip"]: pip, - }); - - const content = - playbackState === VoiceBroadcastPlaybackState.Error ? ( - <VoiceBroadcastError message={playback.errorMessage} /> - ) : ( - <> - <div className="mx_VoiceBroadcastBody_controls"> - {seekBackwardButton} - <VoiceBroadcastPlaybackControl state={playbackState} onClick={toggle} /> - {seekForwardButton} - </div> - <SeekBar playback={playback} /> - <div className="mx_VoiceBroadcastBody_timerow"> - <Clock seconds={times.position} /> - <Clock seconds={-times.timeLeft} /> - </div> - </> - ); - - return ( - <div className={classes}> - <VoiceBroadcastHeader - linkToRoom={pip} - live={liveness} - microphoneLabel={sender?.name} - room={room} - showBroadcast={playbackState !== VoiceBroadcastPlaybackState.Buffering} - showBuffering={playbackState === VoiceBroadcastPlaybackState.Buffering} - /> - {content} - </div> - ); -}; diff --git a/src/voice-broadcast/components/molecules/VoiceBroadcastPreRecordingPip.tsx b/src/voice-broadcast/components/molecules/VoiceBroadcastPreRecordingPip.tsx deleted file mode 100644 index ac742e0fd8..0000000000 --- a/src/voice-broadcast/components/molecules/VoiceBroadcastPreRecordingPip.tsx +++ /dev/null @@ -1,82 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React, { useRef, useState } from "react"; - -import { VoiceBroadcastHeader } from "../.."; -import AccessibleButton from "../../../components/views/elements/AccessibleButton"; -import { VoiceBroadcastPreRecording } from "../../models/VoiceBroadcastPreRecording"; -import { Icon as LiveIcon } from "../../../../res/img/compound/live-16px.svg"; -import { _t } from "../../../languageHandler"; -import { useAudioDeviceSelection } from "../../../hooks/useAudioDeviceSelection"; -import { DevicesContextMenu } from "../../../components/views/audio_messages/DevicesContextMenu"; - -interface Props { - voiceBroadcastPreRecording: VoiceBroadcastPreRecording; -} - -interface State { - showDeviceSelect: boolean; - disableStartButton: boolean; -} - -export const VoiceBroadcastPreRecordingPip: React.FC<Props> = ({ voiceBroadcastPreRecording }) => { - const pipRef = useRef<HTMLDivElement | null>(null); - const { currentDevice, currentDeviceLabel, devices, setDevice } = useAudioDeviceSelection(); - const [state, setState] = useState<State>({ - showDeviceSelect: false, - disableStartButton: false, - }); - - const onDeviceSelect = (device: MediaDeviceInfo): void => { - setState((state) => ({ - ...state, - showDeviceSelect: false, - })); - setDevice(device); - }; - - const onStartBroadcastClick = (): void => { - setState((state) => ({ - ...state, - disableStartButton: true, - })); - - voiceBroadcastPreRecording.start(); - }; - - return ( - <div className="mx_VoiceBroadcastBody mx_VoiceBroadcastBody--pip" ref={pipRef}> - <VoiceBroadcastHeader - linkToRoom={true} - onCloseClick={voiceBroadcastPreRecording.cancel} - onMicrophoneLineClick={(): void => setState({ ...state, showDeviceSelect: true })} - room={voiceBroadcastPreRecording.room} - microphoneLabel={currentDeviceLabel} - showClose={true} - /> - <AccessibleButton - className="mx_VoiceBroadcastBody_blockButton" - kind="danger" - onClick={onStartBroadcastClick} - disabled={state.disableStartButton} - > - <LiveIcon className="mx_Icon mx_Icon_16" /> - {_t("voice_broadcast|go_live")} - </AccessibleButton> - {state.showDeviceSelect && ( - <DevicesContextMenu - containerRef={pipRef} - currentDevice={currentDevice} - devices={devices} - onDeviceSelect={onDeviceSelect} - /> - )} - </div> - ); -}; diff --git a/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody.tsx b/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody.tsx deleted file mode 100644 index 15547792db..0000000000 --- a/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; - -import { - useVoiceBroadcastRecording, - VoiceBroadcastHeader, - VoiceBroadcastRecording, - VoiceBroadcastRecordingConnectionError, -} from "../.."; - -interface VoiceBroadcastRecordingBodyProps { - recording: VoiceBroadcastRecording; -} - -export const VoiceBroadcastRecordingBody: React.FC<VoiceBroadcastRecordingBodyProps> = ({ recording }) => { - const { live, room, sender, recordingState } = useVoiceBroadcastRecording(recording); - - return ( - <div className="mx_VoiceBroadcastBody"> - <VoiceBroadcastHeader live={live ? "live" : "grey"} microphoneLabel={sender?.name} room={room} /> - {recordingState === "connection_error" && <VoiceBroadcastRecordingConnectionError />} - </div> - ); -}; diff --git a/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip.tsx b/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip.tsx deleted file mode 100644 index d04132b220..0000000000 --- a/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip.tsx +++ /dev/null @@ -1,116 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React, { useRef, useState } from "react"; -import PauseIcon from "@vector-im/compound-design-tokens/assets/web/icons/pause-solid"; -import MicrophoneIcon from "@vector-im/compound-design-tokens/assets/web/icons/mic-on-solid"; - -import { - VoiceBroadcastControl, - VoiceBroadcastInfoState, - VoiceBroadcastRecording, - VoiceBroadcastRecordingConnectionError, - VoiceBroadcastRecordingState, -} from "../.."; -import { useVoiceBroadcastRecording } from "../../hooks/useVoiceBroadcastRecording"; -import { VoiceBroadcastHeader } from "../atoms/VoiceBroadcastHeader"; -import { Icon as StopIcon } from "../../../../res/img/compound/stop-16.svg"; -import { Icon as RecordIcon } from "../../../../res/img/compound/record-10px.svg"; -import { _t } from "../../../languageHandler"; -import { useAudioDeviceSelection } from "../../../hooks/useAudioDeviceSelection"; -import { DevicesContextMenu } from "../../../components/views/audio_messages/DevicesContextMenu"; -import AccessibleButton from "../../../components/views/elements/AccessibleButton"; - -interface VoiceBroadcastRecordingPipProps { - recording: VoiceBroadcastRecording; -} - -export const VoiceBroadcastRecordingPip: React.FC<VoiceBroadcastRecordingPipProps> = ({ recording }) => { - const pipRef = useRef<HTMLDivElement | null>(null); - const { live, timeLeft, recordingState, room, stopRecording, toggleRecording } = - useVoiceBroadcastRecording(recording); - const { currentDevice, devices, setDevice } = useAudioDeviceSelection(); - - const onDeviceSelect = async (device: MediaDeviceInfo): Promise<void> => { - setShowDeviceSelect(false); - - if (currentDevice?.deviceId === device.deviceId) { - // device unchanged - return; - } - - setDevice(device); - - if ( - ( - [VoiceBroadcastInfoState.Paused, VoiceBroadcastInfoState.Stopped] as VoiceBroadcastRecordingState[] - ).includes(recordingState) - ) { - // Nothing to do in these cases. Resume will use the selected device. - return; - } - - // pause and resume to switch the input device - await recording.pause(); - await recording.resume(); - }; - - const [showDeviceSelect, setShowDeviceSelect] = useState<boolean>(false); - - const toggleControl = - recordingState === VoiceBroadcastInfoState.Paused ? ( - <VoiceBroadcastControl - className="mx_VoiceBroadcastControl-recording" - onClick={toggleRecording} - icon={<RecordIcon className="mx_Icon mx_Icon_12" />} - label={_t("voice_broadcast|resume")} - /> - ) : ( - <VoiceBroadcastControl - onClick={toggleRecording} - icon={<PauseIcon className="mx_Icon mx_Icon_12" />} - label={_t("voice_broadcast|pause")} - /> - ); - - const controls = - recordingState === "connection_error" ? ( - <VoiceBroadcastRecordingConnectionError /> - ) : ( - <div className="mx_VoiceBroadcastBody_controls"> - {toggleControl} - <AccessibleButton - onClick={(): void => setShowDeviceSelect(true)} - title={_t("voip|change_input_device")} - > - <MicrophoneIcon className="mx_Icon mx_Icon_16 mx_Icon_alert" /> - </AccessibleButton> - <VoiceBroadcastControl - icon={<StopIcon className="mx_Icon mx_Icon_16" />} - label="Stop Recording" - onClick={stopRecording} - /> - </div> - ); - - return ( - <div className="mx_VoiceBroadcastBody mx_VoiceBroadcastBody--pip" ref={pipRef}> - <VoiceBroadcastHeader linkToRoom={true} live={live ? "live" : "grey"} room={room} timeLeft={timeLeft} /> - <hr className="mx_VoiceBroadcastBody_divider" /> - {controls} - {showDeviceSelect && ( - <DevicesContextMenu - containerRef={pipRef} - currentDevice={currentDevice} - devices={devices} - onDeviceSelect={onDeviceSelect} - /> - )} - </div> - ); -}; diff --git a/src/voice-broadcast/components/molecules/VoiceBroadcastSmallPlaybackBody.tsx b/src/voice-broadcast/components/molecules/VoiceBroadcastSmallPlaybackBody.tsx deleted file mode 100644 index a791ac75d7..0000000000 --- a/src/voice-broadcast/components/molecules/VoiceBroadcastSmallPlaybackBody.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import CloseIcon from "@vector-im/compound-design-tokens/assets/web/icons/close"; - -import { - VoiceBroadcastHeader, - VoiceBroadcastPlayback, - VoiceBroadcastPlaybackControl, - VoiceBroadcastPlaybackState, -} from "../.."; -import AccessibleButton from "../../../components/views/elements/AccessibleButton"; -import { useVoiceBroadcastPlayback } from "../../hooks/useVoiceBroadcastPlayback"; - -interface VoiceBroadcastSmallPlaybackBodyProps { - playback: VoiceBroadcastPlayback; -} - -export const VoiceBroadcastSmallPlaybackBody: React.FC<VoiceBroadcastSmallPlaybackBodyProps> = ({ playback }) => { - const { liveness, playbackState, room, sender, toggle } = useVoiceBroadcastPlayback(playback); - return ( - <div className="mx_VoiceBroadcastBody mx_VoiceBroadcastBody--pip mx_VoiceBroadcastBody--small"> - <VoiceBroadcastHeader - linkToRoom={true} - live={liveness} - liveBadgePosition="middle" - microphoneLabel={sender?.name} - room={room} - showBuffering={playbackState === VoiceBroadcastPlaybackState.Buffering} - bufferingPosition="title" - /> - <VoiceBroadcastPlaybackControl state={playbackState} onClick={toggle} /> - <AccessibleButton onClick={() => playback.stop()}> - <CloseIcon className="mx_Icon mx_Icon_8 mx_VoiceBroadcastBody__small-close" /> - </AccessibleButton> - </div> - ); -}; diff --git a/src/voice-broadcast/hooks/useCurrentVoiceBroadcastPlayback.ts b/src/voice-broadcast/hooks/useCurrentVoiceBroadcastPlayback.ts deleted file mode 100644 index 3ff4081a9f..0000000000 --- a/src/voice-broadcast/hooks/useCurrentVoiceBroadcastPlayback.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { useTypedEventEmitterState } from "../../hooks/useEventEmitter"; -import { VoiceBroadcastPlayback } from "../models/VoiceBroadcastPlayback"; -import { - VoiceBroadcastPlaybacksStore, - VoiceBroadcastPlaybacksStoreEvent, -} from "../stores/VoiceBroadcastPlaybacksStore"; - -export const useCurrentVoiceBroadcastPlayback = ( - voiceBroadcastPlaybackStore: VoiceBroadcastPlaybacksStore, -): { - currentVoiceBroadcastPlayback: VoiceBroadcastPlayback | null; -} => { - const currentVoiceBroadcastPlayback = useTypedEventEmitterState( - voiceBroadcastPlaybackStore, - VoiceBroadcastPlaybacksStoreEvent.CurrentChanged, - (playback?: VoiceBroadcastPlayback) => { - return playback ?? voiceBroadcastPlaybackStore.getCurrent(); - }, - ); - - return { - currentVoiceBroadcastPlayback, - }; -}; diff --git a/src/voice-broadcast/hooks/useCurrentVoiceBroadcastPreRecording.ts b/src/voice-broadcast/hooks/useCurrentVoiceBroadcastPreRecording.ts deleted file mode 100644 index bb14e38640..0000000000 --- a/src/voice-broadcast/hooks/useCurrentVoiceBroadcastPreRecording.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { useTypedEventEmitterState } from "../../hooks/useEventEmitter"; -import { VoiceBroadcastPreRecordingStore } from "../stores/VoiceBroadcastPreRecordingStore"; -import { VoiceBroadcastPreRecording } from "../models/VoiceBroadcastPreRecording"; - -export const useCurrentVoiceBroadcastPreRecording = ( - voiceBroadcastPreRecordingStore: VoiceBroadcastPreRecordingStore, -): { - currentVoiceBroadcastPreRecording: VoiceBroadcastPreRecording | null; -} => { - const currentVoiceBroadcastPreRecording = useTypedEventEmitterState( - voiceBroadcastPreRecordingStore, - "changed", - (preRecording?: VoiceBroadcastPreRecording) => { - return preRecording ?? voiceBroadcastPreRecordingStore.getCurrent(); - }, - ); - - return { - currentVoiceBroadcastPreRecording, - }; -}; diff --git a/src/voice-broadcast/hooks/useCurrentVoiceBroadcastRecording.ts b/src/voice-broadcast/hooks/useCurrentVoiceBroadcastRecording.ts deleted file mode 100644 index 1d4abe3f10..0000000000 --- a/src/voice-broadcast/hooks/useCurrentVoiceBroadcastRecording.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { VoiceBroadcastRecording, VoiceBroadcastRecordingsStore, VoiceBroadcastRecordingsStoreEvent } from ".."; -import { useTypedEventEmitterState } from "../../hooks/useEventEmitter"; - -export const useCurrentVoiceBroadcastRecording = ( - voiceBroadcastRecordingsStore: VoiceBroadcastRecordingsStore, -): { - currentVoiceBroadcastRecording: VoiceBroadcastRecording | null; -} => { - const currentVoiceBroadcastRecording = useTypedEventEmitterState( - voiceBroadcastRecordingsStore, - VoiceBroadcastRecordingsStoreEvent.CurrentChanged, - (recording?: VoiceBroadcastRecording) => { - return recording ?? voiceBroadcastRecordingsStore.getCurrent(); - }, - ); - - return { - currentVoiceBroadcastRecording, - }; -}; diff --git a/src/voice-broadcast/hooks/useHasRoomLiveVoiceBroadcast.ts b/src/voice-broadcast/hooks/useHasRoomLiveVoiceBroadcast.ts deleted file mode 100644 index a298f4dc83..0000000000 --- a/src/voice-broadcast/hooks/useHasRoomLiveVoiceBroadcast.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { useContext, useEffect, useMemo, useState } from "react"; -import { Room, RoomStateEvent } from "matrix-js-sdk/src/matrix"; - -import { hasRoomLiveVoiceBroadcast } from "../utils/hasRoomLiveVoiceBroadcast"; -import { useTypedEventEmitter } from "../../hooks/useEventEmitter"; -import { SDKContext } from "../../contexts/SDKContext"; - -export const useHasRoomLiveVoiceBroadcast = (room: Room): boolean => { - const sdkContext = useContext(SDKContext); - const [hasLiveVoiceBroadcast, setHasLiveVoiceBroadcast] = useState(false); - - const update = useMemo(() => { - return sdkContext?.client - ? () => { - hasRoomLiveVoiceBroadcast(sdkContext.client!, room).then( - ({ hasBroadcast }) => { - setHasLiveVoiceBroadcast(hasBroadcast); - }, - () => {}, // no update on error - ); - } - : () => {}; // noop without client - }, [room, sdkContext, setHasLiveVoiceBroadcast]); - - useEffect(() => { - update(); - }, [update]); - - useTypedEventEmitter(room.currentState, RoomStateEvent.Update, () => update()); - return hasLiveVoiceBroadcast; -}; diff --git a/src/voice-broadcast/hooks/useVoiceBroadcastPlayback.ts b/src/voice-broadcast/hooks/useVoiceBroadcastPlayback.ts deleted file mode 100644 index eb50b0de08..0000000000 --- a/src/voice-broadcast/hooks/useVoiceBroadcastPlayback.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022, 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { Room, RoomMember } from "matrix-js-sdk/src/matrix"; - -import { useTypedEventEmitterState } from "../../hooks/useEventEmitter"; -import { MatrixClientPeg } from "../../MatrixClientPeg"; -import { - VoiceBroadcastLiveness, - VoiceBroadcastPlayback, - VoiceBroadcastPlaybackEvent, - VoiceBroadcastPlaybackState, - VoiceBroadcastPlaybackTimes, -} from ".."; - -export const useVoiceBroadcastPlayback = ( - playback: VoiceBroadcastPlayback, -): { - times: { - duration: number; - position: number; - timeLeft: number; - }; - sender: RoomMember | null; - liveness: VoiceBroadcastLiveness; - playbackState: VoiceBroadcastPlaybackState; - toggle(): void; - room: Room; -} => { - const client = MatrixClientPeg.safeGet(); - const room = client.getRoom(playback.infoEvent.getRoomId()); - - if (!room) { - throw new Error(`Voice Broadcast room not found (event ${playback.infoEvent.getId()})`); - } - - const sender = playback.infoEvent.sender; - - if (!sender) { - throw new Error(`Voice Broadcast sender not found (event ${playback.infoEvent.getId()})`); - } - - const playbackToggle = (): void => { - playback.toggle(); - }; - - const playbackState = useTypedEventEmitterState( - playback, - VoiceBroadcastPlaybackEvent.StateChanged, - (state?: VoiceBroadcastPlaybackState) => { - return state ?? playback.getState(); - }, - ); - - const times = useTypedEventEmitterState( - playback, - VoiceBroadcastPlaybackEvent.TimesChanged, - (t?: VoiceBroadcastPlaybackTimes) => { - return ( - t ?? { - duration: playback.durationSeconds, - position: playback.timeSeconds, - timeLeft: playback.timeLeftSeconds, - } - ); - }, - ); - - const liveness = useTypedEventEmitterState( - playback, - VoiceBroadcastPlaybackEvent.LivenessChanged, - (l?: VoiceBroadcastLiveness) => { - return l ?? playback.getLiveness(); - }, - ); - - return { - times, - liveness: liveness, - playbackState, - room: room, - sender, - toggle: playbackToggle, - }; -}; diff --git a/src/voice-broadcast/hooks/useVoiceBroadcastRecording.tsx b/src/voice-broadcast/hooks/useVoiceBroadcastRecording.tsx deleted file mode 100644 index fa3c635bc9..0000000000 --- a/src/voice-broadcast/hooks/useVoiceBroadcastRecording.tsx +++ /dev/null @@ -1,96 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022, 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { Room, RoomMember } from "matrix-js-sdk/src/matrix"; -import React from "react"; - -import { - VoiceBroadcastInfoState, - VoiceBroadcastRecording, - VoiceBroadcastRecordingEvent, - VoiceBroadcastRecordingState, -} from ".."; -import QuestionDialog from "../../components/views/dialogs/QuestionDialog"; -import { useTypedEventEmitterState } from "../../hooks/useEventEmitter"; -import { _t } from "../../languageHandler"; -import { MatrixClientPeg } from "../../MatrixClientPeg"; -import Modal from "../../Modal"; - -const showStopBroadcastingDialog = async (): Promise<boolean> => { - const { finished } = Modal.createDialog(QuestionDialog, { - title: _t("voice_broadcast|confirm_stop_title"), - description: <p>{_t("voice_broadcast|confirm_stop_description")}</p>, - button: _t("voice_broadcast|confirm_stop_affirm"), - }); - const [confirmed] = await finished; - return !!confirmed; -}; - -export const useVoiceBroadcastRecording = ( - recording: VoiceBroadcastRecording, -): { - live: boolean; - timeLeft: number; - recordingState: VoiceBroadcastRecordingState; - room: Room; - sender: RoomMember | null; - stopRecording(): void; - toggleRecording(): void; -} => { - const client = MatrixClientPeg.safeGet(); - const roomId = recording.infoEvent.getRoomId(); - const room = client.getRoom(roomId); - - if (!room) { - throw new Error("Unable to find voice broadcast room with Id: " + roomId); - } - - const sender = recording.infoEvent.sender; - - if (!sender) { - throw new Error(`Voice Broadcast sender not found (event ${recording.infoEvent.getId()})`); - } - - const stopRecording = async (): Promise<void> => { - const confirmed = await showStopBroadcastingDialog(); - - if (confirmed) { - await recording.stop(); - } - }; - - const recordingState = useTypedEventEmitterState( - recording, - VoiceBroadcastRecordingEvent.StateChanged, - (state?: VoiceBroadcastRecordingState) => { - return state ?? recording.getState(); - }, - ); - - const timeLeft = useTypedEventEmitterState( - recording, - VoiceBroadcastRecordingEvent.TimeLeftChanged, - (t?: number) => { - return t ?? recording.getTimeLeft(); - }, - ); - - const live = ( - [VoiceBroadcastInfoState.Started, VoiceBroadcastInfoState.Resumed] as VoiceBroadcastRecordingState[] - ).includes(recordingState); - - return { - live, - timeLeft, - recordingState, - room, - sender, - stopRecording, - toggleRecording: recording.toggle, - }; -}; diff --git a/src/voice-broadcast/index.ts b/src/voice-broadcast/index.ts deleted file mode 100644 index 712c25fdc2..0000000000 --- a/src/voice-broadcast/index.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -/** - * Voice Broadcast module - * {@link https://github.com/vector-im/element-meta/discussions/632} - */ - -export * from "./types"; -export * from "./models/VoiceBroadcastPlayback"; -export * from "./models/VoiceBroadcastPreRecording"; -export * from "./models/VoiceBroadcastRecording"; -export * from "./audio/VoiceBroadcastRecorder"; -export * from "./components/VoiceBroadcastBody"; -export * from "./components/atoms/LiveBadge"; -export * from "./components/atoms/VoiceBroadcastControl"; -export * from "./components/atoms/VoiceBroadcastError"; -export * from "./components/atoms/VoiceBroadcastHeader"; -export * from "./components/atoms/VoiceBroadcastPlaybackControl"; -export * from "./components/atoms/VoiceBroadcastRecordingConnectionError"; -export * from "./components/atoms/VoiceBroadcastRoomSubtitle"; -export * from "./components/molecules/ConfirmListenBroadcastStopCurrent"; -export * from "./components/molecules/VoiceBroadcastPlaybackBody"; -export * from "./components/molecules/VoiceBroadcastSmallPlaybackBody"; -export * from "./components/molecules/VoiceBroadcastPreRecordingPip"; -export * from "./components/molecules/VoiceBroadcastRecordingBody"; -export * from "./components/molecules/VoiceBroadcastRecordingPip"; -export * from "./hooks/useCurrentVoiceBroadcastPreRecording"; -export * from "./hooks/useCurrentVoiceBroadcastRecording"; -export * from "./hooks/useHasRoomLiveVoiceBroadcast"; -export * from "./hooks/useVoiceBroadcastRecording"; -export * from "./stores/VoiceBroadcastPlaybacksStore"; -export * from "./stores/VoiceBroadcastPreRecordingStore"; -export * from "./stores/VoiceBroadcastRecordingsStore"; -export * from "./utils/checkVoiceBroadcastPreConditions"; -export * from "./utils/cleanUpBroadcasts"; -export * from "./utils/doClearCurrentVoiceBroadcastPlaybackIfStopped"; -export * from "./utils/doMaybeSetCurrentVoiceBroadcastPlayback"; -export * from "./utils/getChunkLength"; -export * from "./utils/getMaxBroadcastLength"; -export * from "./utils/hasRoomLiveVoiceBroadcast"; -export * from "./utils/isRelatedToVoiceBroadcast"; -export * from "./utils/isVoiceBroadcastStartedEvent"; -export * from "./utils/findRoomLiveVoiceBroadcastFromUserAndDevice"; -export * from "./utils/retrieveStartedInfoEvent"; -export * from "./utils/shouldDisplayAsVoiceBroadcastRecordingTile"; -export * from "./utils/shouldDisplayAsVoiceBroadcastTile"; -export * from "./utils/shouldDisplayAsVoiceBroadcastStoppedText"; -export * from "./utils/startNewVoiceBroadcastRecording"; -export * from "./utils/textForVoiceBroadcastStoppedEvent"; -export * from "./utils/textForVoiceBroadcastStoppedEventWithoutLink"; -export * from "./utils/VoiceBroadcastResumer"; diff --git a/src/voice-broadcast/models/VoiceBroadcastPlayback.ts b/src/voice-broadcast/models/VoiceBroadcastPlayback.ts deleted file mode 100644 index ce6215312f..0000000000 --- a/src/voice-broadcast/models/VoiceBroadcastPlayback.ts +++ /dev/null @@ -1,651 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { - EventType, - MatrixClient, - MatrixEvent, - MatrixEventEvent, - MsgType, - RelationType, - TypedEventEmitter, -} from "matrix-js-sdk/src/matrix"; -import { SimpleObservable } from "matrix-widget-api"; -import { logger } from "matrix-js-sdk/src/logger"; -import { defer, IDeferred } from "matrix-js-sdk/src/utils"; - -import { Playback, PlaybackInterface, PlaybackState } from "../../audio/Playback"; -import { PlaybackManager } from "../../audio/PlaybackManager"; -import { UPDATE_EVENT } from "../../stores/AsyncStore"; -import { MediaEventHelper } from "../../utils/MediaEventHelper"; -import { IDestroyable } from "../../utils/IDestroyable"; -import { - VoiceBroadcastLiveness, - VoiceBroadcastInfoEventType, - VoiceBroadcastInfoState, - VoiceBroadcastInfoEventContent, - VoiceBroadcastRecordingsStore, - showConfirmListenBroadcastStopCurrentDialog, -} from ".."; -import { RelationsHelper, RelationsHelperEvent } from "../../events/RelationsHelper"; -import { VoiceBroadcastChunkEvents } from "../utils/VoiceBroadcastChunkEvents"; -import { determineVoiceBroadcastLiveness } from "../utils/determineVoiceBroadcastLiveness"; -import { _t } from "../../languageHandler"; - -export enum VoiceBroadcastPlaybackState { - Paused = "pause", - Playing = "playing", - Stopped = "stopped", - Buffering = "buffering", - Error = "error", -} - -export enum VoiceBroadcastPlaybackEvent { - TimesChanged = "times_changed", - LivenessChanged = "liveness_changed", - StateChanged = "state_changed", - InfoStateChanged = "info_state_changed", -} - -export type VoiceBroadcastPlaybackTimes = { - duration: number; - position: number; - timeLeft: number; -}; - -interface EventMap { - [VoiceBroadcastPlaybackEvent.TimesChanged]: (times: VoiceBroadcastPlaybackTimes) => void; - [VoiceBroadcastPlaybackEvent.LivenessChanged]: (liveness: VoiceBroadcastLiveness) => void; - [VoiceBroadcastPlaybackEvent.StateChanged]: ( - state: VoiceBroadcastPlaybackState, - playback: VoiceBroadcastPlayback, - ) => void; - [VoiceBroadcastPlaybackEvent.InfoStateChanged]: (state: VoiceBroadcastInfoState) => void; -} - -export class VoiceBroadcastPlayback - extends TypedEventEmitter<VoiceBroadcastPlaybackEvent, EventMap> - implements IDestroyable, PlaybackInterface -{ - private state = VoiceBroadcastPlaybackState.Stopped; - private chunkEvents = new VoiceBroadcastChunkEvents(); - /** @var Map: event Id → undecryptable event */ - private utdChunkEvents: Map<string, MatrixEvent> = new Map(); - private playbacks = new Map<string, Playback>(); - private currentlyPlaying: MatrixEvent | null = null; - /** @var total duration of all chunks in milliseconds */ - private duration = 0; - /** @var current playback position in milliseconds */ - private position = 0; - public readonly liveData = new SimpleObservable<number[]>(); - private liveness: VoiceBroadcastLiveness = "not-live"; - - // set via addInfoEvent() in constructor - private infoState!: VoiceBroadcastInfoState; - private lastInfoEvent!: MatrixEvent; - - // set via setUpRelationsHelper() in constructor - private chunkRelationHelper!: RelationsHelper; - private infoRelationHelper!: RelationsHelper; - - private skipToNext?: number; - private skipToDeferred?: IDeferred<void>; - - public constructor( - public readonly infoEvent: MatrixEvent, - private client: MatrixClient, - private recordings: VoiceBroadcastRecordingsStore, - ) { - super(); - this.addInfoEvent(this.infoEvent); - this.infoEvent.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction); - this.setUpRelationsHelper(); - } - - private async setUpRelationsHelper(): Promise<void> { - this.infoRelationHelper = new RelationsHelper( - this.infoEvent, - RelationType.Reference, - VoiceBroadcastInfoEventType, - this.client, - ); - this.infoRelationHelper.getCurrent().forEach(this.addInfoEvent); - - if (this.infoState !== VoiceBroadcastInfoState.Stopped) { - // Only required if not stopped. Stopped is the final state. - this.infoRelationHelper.on(RelationsHelperEvent.Add, this.addInfoEvent); - - try { - await this.infoRelationHelper.emitFetchCurrent(); - } catch (err) { - logger.warn("error fetching server side relation for voice broadcast info", err); - // fall back to local events - this.infoRelationHelper.emitCurrent(); - } - } - - this.chunkRelationHelper = new RelationsHelper( - this.infoEvent, - RelationType.Reference, - EventType.RoomMessage, - this.client, - ); - this.chunkRelationHelper.on(RelationsHelperEvent.Add, this.addChunkEvent); - - try { - // TODO Michael W: only fetch events if needed, blocked by PSF-1708 - await this.chunkRelationHelper.emitFetchCurrent(); - } catch (err) { - logger.warn("error fetching server side relation for voice broadcast chunks", err); - // fall back to local events - this.chunkRelationHelper.emitCurrent(); - } - } - - private addChunkEvent = async (event: MatrixEvent): Promise<boolean> => { - if (!event.getId() && !event.getTxnId()) { - // skip events without id and txn id - return false; - } - - if (event.isDecryptionFailure()) { - this.onChunkEventDecryptionFailure(event); - return false; - } - - if (event.getContent()?.msgtype !== MsgType.Audio) { - // skip non-audio event - return false; - } - - this.chunkEvents.addEvent(event); - this.setDuration(this.chunkEvents.getLength()); - - if (this.getState() === VoiceBroadcastPlaybackState.Buffering) { - await this.startOrPlayNext(); - } - - return true; - }; - - private onChunkEventDecryptionFailure = (event: MatrixEvent): void => { - const eventId = event.getId(); - - if (!eventId) { - // This should not happen, as the existence of the Id is checked before the call. - // Log anyway and return. - logger.warn("Broadcast chunk decryption failure for event without Id", { - broadcast: this.infoEvent.getId(), - }); - return; - } - - if (!this.utdChunkEvents.has(eventId)) { - event.once(MatrixEventEvent.Decrypted, this.onChunkEventDecrypted); - } - - this.utdChunkEvents.set(eventId, event); - this.setError(); - }; - - private onChunkEventDecrypted = async (event: MatrixEvent): Promise<void> => { - const eventId = event.getId(); - - if (!eventId) { - // This should not happen, as the existence of the Id is checked before the call. - // Log anyway and return. - logger.warn("Broadcast chunk decrypted for event without Id", { broadcast: this.infoEvent.getId() }); - return; - } - - this.utdChunkEvents.delete(eventId); - await this.addChunkEvent(event); - - if (this.utdChunkEvents.size === 0) { - // no more UTD events, recover from error to paused - this.setState(VoiceBroadcastPlaybackState.Paused); - } - }; - - private startOrPlayNext = async (): Promise<void> => { - if (this.currentlyPlaying) { - return this.playNext(); - } - - return await this.start(); - }; - - private addInfoEvent = (event: MatrixEvent): void => { - if (this.lastInfoEvent && this.lastInfoEvent.getTs() >= event.getTs()) { - // Only handle newer events - return; - } - - const state = event.getContent()?.state; - - if (!Object.values(VoiceBroadcastInfoState).includes(state)) { - // Do not handle unknown voice broadcast states - return; - } - - this.lastInfoEvent = event; - this.setInfoState(state); - }; - - private onBeforeRedaction = (): void => { - if (this.getState() !== VoiceBroadcastPlaybackState.Stopped) { - this.stop(); - // destroy cleans up everything - this.destroy(); - } - }; - - private async tryLoadPlayback(chunkEvent: MatrixEvent): Promise<void> { - try { - return await this.loadPlayback(chunkEvent); - } catch (err: any) { - logger.warn("Unable to load broadcast playback", { - message: err.message, - broadcastId: this.infoEvent.getId(), - chunkId: chunkEvent.getId(), - }); - this.setError(); - } - } - - private async loadPlayback(chunkEvent: MatrixEvent): Promise<void> { - const eventId = chunkEvent.getId(); - - if (!eventId) { - throw new Error("Broadcast chunk event without Id occurred"); - } - - const helper = new MediaEventHelper(chunkEvent); - const blob = await helper.sourceBlob.value; - const buffer = await blob.arrayBuffer(); - const playback = PlaybackManager.instance.createPlaybackInstance(buffer); - await playback.prepare(); - playback.clockInfo.populatePlaceholdersFrom(chunkEvent); - this.playbacks.set(eventId, playback); - playback.on(UPDATE_EVENT, (state) => this.onPlaybackStateChange(chunkEvent, state)); - playback.clockInfo.liveData.onUpdate(([position]) => { - this.onPlaybackPositionUpdate(chunkEvent, position); - }); - } - - private unloadPlayback(event: MatrixEvent): void { - const playback = this.playbacks.get(event.getId()!); - if (!playback) return; - - playback.destroy(); - this.playbacks.delete(event.getId()!); - } - - private onPlaybackPositionUpdate = (event: MatrixEvent, position: number): void => { - if (event !== this.currentlyPlaying) return; - - const newPosition = this.chunkEvents.getLengthTo(event) + position * 1000; // observable sends seconds - - // do not jump backwards - this can happen when transiting from one to another chunk - if (newPosition < this.position) return; - - this.setPosition(newPosition); - }; - - private setDuration(duration: number): void { - if (this.duration === duration) return; - - this.duration = duration; - this.emitTimesChanged(); - this.liveData.update([this.timeSeconds, this.durationSeconds]); - } - - private setPosition(position: number): void { - if (this.position === position) return; - - this.position = position; - this.emitTimesChanged(); - this.liveData.update([this.timeSeconds, this.durationSeconds]); - } - - private emitTimesChanged(): void { - this.emit(VoiceBroadcastPlaybackEvent.TimesChanged, { - duration: this.durationSeconds, - position: this.timeSeconds, - timeLeft: this.timeLeftSeconds, - }); - } - - private onPlaybackStateChange = async (event: MatrixEvent, newState: PlaybackState): Promise<void> => { - if (event !== this.currentlyPlaying) return; - if (newState !== PlaybackState.Stopped) return; - - await this.playNext(); - this.unloadPlayback(event); - }; - - private async playNext(): Promise<void> { - if (!this.currentlyPlaying) return; - - const next = this.chunkEvents.getNext(this.currentlyPlaying); - - if (next) { - return this.playEvent(next); - } - - if ( - this.getInfoState() === VoiceBroadcastInfoState.Stopped && - this.chunkEvents.getSequenceForEvent(this.currentlyPlaying) === this.lastChunkSequence - ) { - this.stop(); - } else { - // No more chunks available, although the broadcast is not finished → enter buffering state. - this.setState(VoiceBroadcastPlaybackState.Buffering); - } - } - - /** - * @returns {number} The last chunk sequence from the latest info event. - * Falls back to the length of received chunks if the info event does not provide the number. - */ - private get lastChunkSequence(): number { - return ( - this.lastInfoEvent.getContent<VoiceBroadcastInfoEventContent>()?.last_chunk_sequence || - this.chunkEvents.getNumberOfEvents() - ); - } - - private async playEvent(event: MatrixEvent): Promise<void> { - this.setState(VoiceBroadcastPlaybackState.Playing); - this.currentlyPlaying = event; - const playback = await this.tryGetOrLoadPlaybackForEvent(event); - playback?.play(); - } - - private async tryGetOrLoadPlaybackForEvent(event: MatrixEvent): Promise<Playback | undefined> { - try { - return await this.getOrLoadPlaybackForEvent(event); - } catch (err: any) { - logger.warn("Unable to load broadcast playback", { - message: err.message, - broadcastId: this.infoEvent.getId(), - chunkId: event.getId(), - }); - this.setError(); - } - } - - private async getOrLoadPlaybackForEvent(event: MatrixEvent): Promise<Playback | undefined> { - const eventId = event.getId(); - - if (!eventId) { - throw new Error("Broadcast chunk event without Id occurred"); - } - - if (!this.playbacks.has(eventId)) { - // set to buffering while loading the chunk data - const currentState = this.getState(); - this.setState(VoiceBroadcastPlaybackState.Buffering); - await this.loadPlayback(event); - this.setState(currentState); - } - - const playback = this.playbacks.get(eventId); - - if (!playback) { - throw new Error(`Unable to find playback for event ${event.getId()}`); - } - - // try to load the playback for the next event for a smooth(er) playback - const nextEvent = this.chunkEvents.getNext(event); - if (nextEvent) this.tryLoadPlayback(nextEvent); - - return playback; - } - - private getCurrentPlayback(): Playback | undefined { - if (!this.currentlyPlaying) return; - return this.playbacks.get(this.currentlyPlaying.getId()!); - } - - public getLiveness(): VoiceBroadcastLiveness { - return this.liveness; - } - - private setLiveness(liveness: VoiceBroadcastLiveness): void { - if (this.liveness === liveness) return; - - this.liveness = liveness; - this.emit(VoiceBroadcastPlaybackEvent.LivenessChanged, liveness); - } - - public get currentState(): PlaybackState { - return PlaybackState.Playing; - } - - public get timeSeconds(): number { - return this.position / 1000; - } - - public get durationSeconds(): number { - return this.duration / 1000; - } - - public get timeLeftSeconds(): number { - // Sometimes the meta data and the audio files are a little bit out of sync. - // Be sure it never returns a negative value. - return Math.max(0, Math.round(this.durationSeconds) - this.timeSeconds); - } - - public async skipTo(timeSeconds: number): Promise<void> { - this.skipToNext = timeSeconds; - - if (this.skipToDeferred) { - // Skip to position is already in progress. Return the promise for that. - return this.skipToDeferred.promise; - } - - this.skipToDeferred = defer(); - - while (this.skipToNext !== undefined) { - // Skip to position until skipToNext is undefined. - // skipToNext can be set if skipTo is called while already skipping. - const skipToNext = this.skipToNext; - this.skipToNext = undefined; - await this.doSkipTo(skipToNext); - } - - this.skipToDeferred.resolve(); - this.skipToDeferred = undefined; - } - - private async doSkipTo(timeSeconds: number): Promise<void> { - const time = timeSeconds * 1000; - const event = this.chunkEvents.findByTime(time); - - if (!event) { - logger.warn("voice broadcast chunk event to skip to not found"); - return; - } - - const currentPlayback = this.getCurrentPlayback(); - const skipToPlayback = await this.tryGetOrLoadPlaybackForEvent(event); - const currentPlaybackEvent = this.currentlyPlaying; - - if (!skipToPlayback) { - logger.warn("voice broadcast chunk to skip to not found", event); - return; - } - - this.currentlyPlaying = event; - - if (currentPlayback && currentPlaybackEvent && currentPlayback !== skipToPlayback) { - // only stop and unload the playback here without triggering other effects, e.g. play next - currentPlayback.off(UPDATE_EVENT, this.onPlaybackStateChange); - await currentPlayback.stop(); - currentPlayback.on(UPDATE_EVENT, this.onPlaybackStateChange); - this.unloadPlayback(currentPlaybackEvent); - } - - const offsetInChunk = time - this.chunkEvents.getLengthTo(event); - await skipToPlayback.skipTo(offsetInChunk / 1000); - - if (this.state === VoiceBroadcastPlaybackState.Playing && !skipToPlayback.isPlaying) { - await skipToPlayback.play(); - } - - this.setPosition(time); - } - - public async start(): Promise<void> { - if (this.state === VoiceBroadcastPlaybackState.Playing) return; - - const currentRecording = this.recordings.getCurrent(); - - if (currentRecording && currentRecording.getState() !== VoiceBroadcastInfoState.Stopped) { - const shouldStopRecording = await showConfirmListenBroadcastStopCurrentDialog(); - - if (!shouldStopRecording) { - // keep recording - return; - } - - await this.recordings.getCurrent()?.stop(); - } - - const chunkEvents = this.chunkEvents.getEvents(); - - const toPlay = - this.getInfoState() === VoiceBroadcastInfoState.Stopped - ? chunkEvents[0] // start at the beginning for an ended voice broadcast - : chunkEvents[chunkEvents.length - 1]; // start at the current chunk for an ongoing voice broadcast - - if (toPlay) { - return this.playEvent(toPlay); - } - - this.setState(VoiceBroadcastPlaybackState.Buffering); - } - - public stop(): void { - // error is a final state - if (this.getState() === VoiceBroadcastPlaybackState.Error) return; - - this.setState(VoiceBroadcastPlaybackState.Stopped); - this.getCurrentPlayback()?.stop(); - this.currentlyPlaying = null; - this.setPosition(0); - } - - public pause(): void { - // error is a final state - if (this.getState() === VoiceBroadcastPlaybackState.Error) return; - - // stopped voice broadcasts cannot be paused - if (this.getState() === VoiceBroadcastPlaybackState.Stopped) return; - - this.setState(VoiceBroadcastPlaybackState.Paused); - this.getCurrentPlayback()?.pause(); - } - - public resume(): void { - // error is a final state - if (this.getState() === VoiceBroadcastPlaybackState.Error) return; - - if (!this.currentlyPlaying) { - // no playback to resume, start from the beginning - this.start(); - return; - } - - this.setState(VoiceBroadcastPlaybackState.Playing); - this.getCurrentPlayback()?.play(); - } - - /** - * Toggles the playback: - * stopped → playing - * playing → paused - * paused → playing - */ - public async toggle(): Promise<void> { - // error is a final state - if (this.getState() === VoiceBroadcastPlaybackState.Error) return; - - if (this.state === VoiceBroadcastPlaybackState.Stopped) { - await this.start(); - return; - } - - if (this.state === VoiceBroadcastPlaybackState.Paused) { - this.resume(); - return; - } - - this.pause(); - } - - public getState(): VoiceBroadcastPlaybackState { - return this.state; - } - - private setState(state: VoiceBroadcastPlaybackState): void { - if (this.state === state) { - return; - } - - this.state = state; - this.emit(VoiceBroadcastPlaybackEvent.StateChanged, state, this); - } - - /** - * Set error state. Stop current playback, if any. - */ - private setError(): void { - this.setState(VoiceBroadcastPlaybackState.Error); - this.getCurrentPlayback()?.stop(); - this.currentlyPlaying = null; - this.setPosition(0); - } - - public getInfoState(): VoiceBroadcastInfoState { - return this.infoState; - } - - private setInfoState(state: VoiceBroadcastInfoState): void { - if (this.infoState === state) { - return; - } - - this.infoState = state; - this.emit(VoiceBroadcastPlaybackEvent.InfoStateChanged, state); - this.setLiveness(determineVoiceBroadcastLiveness(this.infoState)); - } - - public get errorMessage(): string { - if (this.getState() !== VoiceBroadcastPlaybackState.Error) return ""; - if (this.utdChunkEvents.size) return _t("voice_broadcast|failed_decrypt"); - return _t("voice_broadcast|failed_generic"); - } - - public destroy(): void { - for (const [, utdEvent] of this.utdChunkEvents) { - utdEvent.off(MatrixEventEvent.Decrypted, this.onChunkEventDecrypted); - } - - this.utdChunkEvents.clear(); - - this.chunkRelationHelper.destroy(); - this.infoRelationHelper.destroy(); - this.removeAllListeners(); - - this.chunkEvents = new VoiceBroadcastChunkEvents(); - this.playbacks.forEach((p) => p.destroy()); - this.playbacks = new Map<string, Playback>(); - } -} diff --git a/src/voice-broadcast/models/VoiceBroadcastPreRecording.ts b/src/voice-broadcast/models/VoiceBroadcastPreRecording.ts deleted file mode 100644 index 0cf47c6f21..0000000000 --- a/src/voice-broadcast/models/VoiceBroadcastPreRecording.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixClient, Room, RoomMember, TypedEventEmitter } from "matrix-js-sdk/src/matrix"; - -import { IDestroyable } from "../../utils/IDestroyable"; -import { VoiceBroadcastPlaybacksStore } from "../stores/VoiceBroadcastPlaybacksStore"; -import { VoiceBroadcastRecordingsStore } from "../stores/VoiceBroadcastRecordingsStore"; -import { startNewVoiceBroadcastRecording } from "../utils/startNewVoiceBroadcastRecording"; - -type VoiceBroadcastPreRecordingEvent = "dismiss"; - -interface EventMap { - dismiss: (voiceBroadcastPreRecording: VoiceBroadcastPreRecording) => void; -} - -export class VoiceBroadcastPreRecording - extends TypedEventEmitter<VoiceBroadcastPreRecordingEvent, EventMap> - implements IDestroyable -{ - public constructor( - public room: Room, - public sender: RoomMember, - private client: MatrixClient, - private playbacksStore: VoiceBroadcastPlaybacksStore, - private recordingsStore: VoiceBroadcastRecordingsStore, - ) { - super(); - } - - public start = async (): Promise<void> => { - await startNewVoiceBroadcastRecording(this.room, this.client, this.playbacksStore, this.recordingsStore); - this.emit("dismiss", this); - }; - - public cancel = (): void => { - this.emit("dismiss", this); - }; - - public destroy(): void { - this.removeAllListeners(); - } -} diff --git a/src/voice-broadcast/models/VoiceBroadcastRecording.ts b/src/voice-broadcast/models/VoiceBroadcastRecording.ts deleted file mode 100644 index ebf8ee697f..0000000000 --- a/src/voice-broadcast/models/VoiceBroadcastRecording.ts +++ /dev/null @@ -1,441 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { logger } from "matrix-js-sdk/src/logger"; -import { - ClientEvent, - ClientEventHandlerMap, - EventType, - MatrixClient, - MatrixEvent, - MatrixEventEvent, - MsgType, - RelationType, - TypedEventEmitter, -} from "matrix-js-sdk/src/matrix"; -import { AudioContent, EncryptedFile } from "matrix-js-sdk/src/types"; - -import { - ChunkRecordedPayload, - createVoiceBroadcastRecorder, - getMaxBroadcastLength, - VoiceBroadcastInfoEventContent, - VoiceBroadcastInfoEventType, - VoiceBroadcastInfoState, - VoiceBroadcastRecorder, - VoiceBroadcastRecorderEvent, -} from ".."; -import { uploadFile } from "../../ContentMessages"; -import { createVoiceMessageContent } from "../../utils/createVoiceMessageContent"; -import { IDestroyable } from "../../utils/IDestroyable"; -import dis from "../../dispatcher/dispatcher"; -import { ActionPayload } from "../../dispatcher/payloads"; -import { VoiceBroadcastChunkEvents } from "../utils/VoiceBroadcastChunkEvents"; -import { RelationsHelper, RelationsHelperEvent } from "../../events/RelationsHelper"; -import { createReconnectedListener } from "../../utils/connection"; -import { localNotificationsAreSilenced } from "../../utils/notifications"; -import { BackgroundAudio } from "../../audio/BackgroundAudio"; - -export enum VoiceBroadcastRecordingEvent { - StateChanged = "liveness_changed", - TimeLeftChanged = "time_left_changed", -} - -export type VoiceBroadcastRecordingState = VoiceBroadcastInfoState | "connection_error"; - -interface EventMap { - [VoiceBroadcastRecordingEvent.StateChanged]: (state: VoiceBroadcastRecordingState) => void; - [VoiceBroadcastRecordingEvent.TimeLeftChanged]: (timeLeft: number) => void; -} - -export class VoiceBroadcastRecording - extends TypedEventEmitter<VoiceBroadcastRecordingEvent, EventMap> - implements IDestroyable -{ - private state: VoiceBroadcastRecordingState; - private recorder: VoiceBroadcastRecorder | null = null; - private dispatcherRef: string; - private chunkEvents = new VoiceBroadcastChunkEvents(); - private chunkRelationHelper: RelationsHelper; - private maxLength: number; - private timeLeft: number; - private toRetry: Array<() => Promise<void>> = []; - private reconnectedListener: ClientEventHandlerMap[ClientEvent.Sync]; - private roomId: string; - private infoEventId: string; - private backgroundAudio = new BackgroundAudio(); - - /** - * Broadcast chunks have a sequence number to bring them in the correct order and to know if a message is missing. - * This variable holds the last sequence number. - * Starts with 0 because there is no chunk at the beginning of a broadcast. - * Will be incremented when a chunk message is created. - */ - private sequence = 0; - - public constructor( - public readonly infoEvent: MatrixEvent, - private client: MatrixClient, - initialState?: VoiceBroadcastInfoState, - ) { - super(); - this.maxLength = getMaxBroadcastLength(); - this.timeLeft = this.maxLength; - this.infoEventId = this.determineEventIdFromInfoEvent(); - this.roomId = this.determineRoomIdFromInfoEvent(); - - if (initialState) { - this.state = initialState; - } else { - this.state = this.determineInitialStateFromInfoEvent(); - } - - // TODO Michael W: listen for state updates - - this.infoEvent.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction); - this.dispatcherRef = dis.register(this.onAction); - this.chunkRelationHelper = this.initialiseChunkEventRelation(); - this.reconnectedListener = createReconnectedListener(this.onReconnect); - this.client.on(ClientEvent.Sync, this.reconnectedListener); - } - - private initialiseChunkEventRelation(): RelationsHelper { - const relationsHelper = new RelationsHelper( - this.infoEvent, - RelationType.Reference, - EventType.RoomMessage, - this.client, - ); - relationsHelper.on(RelationsHelperEvent.Add, this.onChunkEvent); - - relationsHelper.emitFetchCurrent().catch((err) => { - logger.warn("error fetching server side relation for voice broadcast chunks", err); - // fall back to local events - relationsHelper.emitCurrent(); - }); - - return relationsHelper; - } - - private onChunkEvent = (event: MatrixEvent): void => { - if ( - (!event.getId() && !event.getTxnId()) || - event.getContent()?.msgtype !== MsgType.Audio // don't add non-audio event - ) { - return; - } - - this.chunkEvents.addEvent(event); - }; - - private determineEventIdFromInfoEvent(): string { - const infoEventId = this.infoEvent.getId(); - - if (!infoEventId) { - throw new Error("Cannot create broadcast for info event without Id."); - } - - return infoEventId; - } - - private determineRoomIdFromInfoEvent(): string { - const roomId = this.infoEvent.getRoomId(); - - if (!roomId) { - throw new Error(`Cannot create broadcast for unknown room (info event ${this.infoEventId})`); - } - - return roomId; - } - - /** - * Determines the initial broadcast state. - * Checks all related events. If one has the "stopped" state → stopped, else started. - */ - private determineInitialStateFromInfoEvent(): VoiceBroadcastRecordingState { - const room = this.client.getRoom(this.roomId); - const relations = room - ?.getUnfilteredTimelineSet() - ?.relations?.getChildEventsForEvent(this.infoEventId, RelationType.Reference, VoiceBroadcastInfoEventType); - const relatedEvents = relations?.getRelations(); - return !relatedEvents?.find((event: MatrixEvent) => { - return event.getContent()?.state === VoiceBroadcastInfoState.Stopped; - }) - ? VoiceBroadcastInfoState.Started - : VoiceBroadcastInfoState.Stopped; - } - - public getTimeLeft(): number { - return this.timeLeft; - } - - /** - * Retries failed actions on reconnect. - */ - private onReconnect = async (): Promise<void> => { - // Do nothing if not in connection_error state. - if (this.state !== "connection_error") return; - - // Copy the array, so that it is possible to remove elements from it while iterating over the original. - const toRetryCopy = [...this.toRetry]; - - for (const retryFn of this.toRetry) { - try { - await retryFn(); - // Successfully retried. Remove from array copy. - toRetryCopy.splice(toRetryCopy.indexOf(retryFn), 1); - } catch { - // The current retry callback failed. Stop the loop. - break; - } - } - - this.toRetry = toRetryCopy; - - if (this.toRetry.length === 0) { - // Everything has been successfully retried. Recover from error state to paused. - await this.pause(); - } - }; - - private async setTimeLeft(timeLeft: number): Promise<void> { - if (timeLeft <= 0) { - // time is up - stop the recording - return await this.stop(); - } - - // do never increase time left; no action if equals - if (timeLeft >= this.timeLeft) return; - - this.timeLeft = timeLeft; - this.emit(VoiceBroadcastRecordingEvent.TimeLeftChanged, timeLeft); - } - - public async start(): Promise<void> { - return this.getRecorder().start(); - } - - public async stop(): Promise<void> { - if (this.state === VoiceBroadcastInfoState.Stopped) return; - - this.setState(VoiceBroadcastInfoState.Stopped); - await this.stopRecorder(); - await this.sendInfoStateEvent(VoiceBroadcastInfoState.Stopped); - } - - public async pause(): Promise<void> { - // stopped or already paused recordings cannot be paused - if ( - ( - [VoiceBroadcastInfoState.Stopped, VoiceBroadcastInfoState.Paused] as VoiceBroadcastRecordingState[] - ).includes(this.state) - ) - return; - - this.setState(VoiceBroadcastInfoState.Paused); - await this.stopRecorder(); - await this.sendInfoStateEvent(VoiceBroadcastInfoState.Paused); - } - - public async resume(): Promise<void> { - if (this.state !== VoiceBroadcastInfoState.Paused) return; - - this.setState(VoiceBroadcastInfoState.Resumed); - await this.getRecorder().start(); - await this.sendInfoStateEvent(VoiceBroadcastInfoState.Resumed); - } - - public toggle = async (): Promise<void> => { - if (this.getState() === VoiceBroadcastInfoState.Paused) return this.resume(); - - if ( - ( - [VoiceBroadcastInfoState.Started, VoiceBroadcastInfoState.Resumed] as VoiceBroadcastRecordingState[] - ).includes(this.getState()) - ) { - return this.pause(); - } - }; - - public getState(): VoiceBroadcastRecordingState { - return this.state; - } - - private getRecorder(): VoiceBroadcastRecorder { - if (!this.recorder) { - this.recorder = createVoiceBroadcastRecorder(); - this.recorder.on(VoiceBroadcastRecorderEvent.ChunkRecorded, this.onChunkRecorded); - this.recorder.on(VoiceBroadcastRecorderEvent.CurrentChunkLengthUpdated, this.onCurrentChunkLengthUpdated); - } - - return this.recorder; - } - - public async destroy(): Promise<void> { - if (this.recorder) { - this.recorder.stop(); - this.recorder.destroy(); - } - - this.infoEvent.off(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction); - this.removeAllListeners(); - dis.unregister(this.dispatcherRef); - this.chunkEvents = new VoiceBroadcastChunkEvents(); - this.chunkRelationHelper.destroy(); - this.client.off(ClientEvent.Sync, this.reconnectedListener); - } - - private onBeforeRedaction = (): void => { - if (this.getState() !== VoiceBroadcastInfoState.Stopped) { - this.setState(VoiceBroadcastInfoState.Stopped); - // destroy cleans up everything - this.destroy(); - } - }; - - private onAction = (payload: ActionPayload): void => { - if (payload.action !== "call_state") return; - - // pause on any call action - this.pause(); - }; - - private setState(state: VoiceBroadcastRecordingState): void { - this.state = state; - this.emit(VoiceBroadcastRecordingEvent.StateChanged, this.state); - } - - private onCurrentChunkLengthUpdated = (currentChunkLength: number): void => { - this.setTimeLeft(this.maxLength - this.chunkEvents.getLengthSeconds() - currentChunkLength); - }; - - private onChunkRecorded = async (chunk: ChunkRecordedPayload): Promise<void> => { - const uploadAndSendFn = async (): Promise<void> => { - const { url, file } = await this.uploadFile(chunk); - await this.sendVoiceMessage(chunk, url, file); - }; - - await this.callWithRetry(uploadAndSendFn); - }; - - /** - * This function is called on connection errors. - * It sets the connection error state and stops the recorder. - */ - private async onConnectionError(): Promise<void> { - this.playConnectionErrorAudioNotification().catch(() => { - // Error logged in playConnectionErrorAudioNotification(). - }); - await this.stopRecorder(false); - this.setState("connection_error"); - } - - private async playConnectionErrorAudioNotification(): Promise<void> { - if (localNotificationsAreSilenced(this.client)) { - return; - } - - await this.backgroundAudio.pickFormatAndPlay("./media/error", ["mp3", "ogg"]); - } - - private async uploadFile(chunk: ChunkRecordedPayload): ReturnType<typeof uploadFile> { - return uploadFile( - this.client, - this.roomId, - new Blob([chunk.buffer], { - type: this.getRecorder().contentType, - }), - ); - } - - private async sendVoiceMessage(chunk: ChunkRecordedPayload, url?: string, file?: EncryptedFile): Promise<void> { - /** - * Increment the last sequence number and use it for this message. - * Done outside of the sendMessageFn to get a scoped value. - * Also see {@link VoiceBroadcastRecording.sequence}. - */ - const sequence = ++this.sequence; - - const sendMessageFn = async (): Promise<void> => { - const content = createVoiceMessageContent( - url, - this.getRecorder().contentType, - Math.round(chunk.length * 1000), - chunk.buffer.length, - file, - ); - content["m.relates_to"] = { - rel_type: RelationType.Reference, - event_id: this.infoEventId, - }; - (<AudioContent>content)["io.element.voice_broadcast_chunk"] = { - sequence, - }; - - await this.client.sendMessage(this.roomId, content); - }; - - await this.callWithRetry(sendMessageFn); - } - - /** - * Sends an info state event with given state. - * On error stores a resend function and setState(state) in {@link toRetry} and - * sets the broadcast state to connection_error. - */ - private async sendInfoStateEvent(state: VoiceBroadcastInfoState): Promise<void> { - const sendEventFn = async (): Promise<void> => { - await this.client.sendStateEvent( - this.roomId, - VoiceBroadcastInfoEventType, - { - device_id: this.client.getDeviceId(), - state, - last_chunk_sequence: this.sequence, - ["m.relates_to"]: { - rel_type: RelationType.Reference, - event_id: this.infoEventId, - }, - } as VoiceBroadcastInfoEventContent, - this.client.getSafeUserId(), - ); - }; - - await this.callWithRetry(sendEventFn); - } - - /** - * Calls the function. - * On failure adds it to the retry list and triggers connection error. - * {@link toRetry} - * {@link onConnectionError} - */ - private async callWithRetry(retryAbleFn: () => Promise<void>): Promise<void> { - try { - await retryAbleFn(); - } catch { - this.toRetry.push(retryAbleFn); - this.onConnectionError(); - } - } - - private async stopRecorder(emit = true): Promise<void> { - if (!this.recorder) { - return; - } - - try { - const lastChunk = await this.recorder.stop(); - if (lastChunk && emit) { - await this.onChunkRecorded(lastChunk); - } - } catch (err) { - logger.warn("error stopping voice broadcast recorder", err); - } - } -} diff --git a/src/voice-broadcast/stores/VoiceBroadcastPlaybacksStore.ts b/src/voice-broadcast/stores/VoiceBroadcastPlaybacksStore.ts deleted file mode 100644 index 69b8c21d90..0000000000 --- a/src/voice-broadcast/stores/VoiceBroadcastPlaybacksStore.ts +++ /dev/null @@ -1,113 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixClient, MatrixEvent, TypedEventEmitter } from "matrix-js-sdk/src/matrix"; - -import { - VoiceBroadcastPlayback, - VoiceBroadcastPlaybackEvent, - VoiceBroadcastPlaybackState, - VoiceBroadcastRecordingsStore, -} from ".."; -import { IDestroyable } from "../../utils/IDestroyable"; - -export enum VoiceBroadcastPlaybacksStoreEvent { - CurrentChanged = "current_changed", -} - -interface EventMap { - [VoiceBroadcastPlaybacksStoreEvent.CurrentChanged]: (recording: VoiceBroadcastPlayback | null) => void; -} - -/** - * This store manages VoiceBroadcastPlaybacks: - * - access the currently playing voice broadcast - * - ensures that only once broadcast is playing at a time - */ -export class VoiceBroadcastPlaybacksStore - extends TypedEventEmitter<VoiceBroadcastPlaybacksStoreEvent, EventMap> - implements IDestroyable -{ - private current: VoiceBroadcastPlayback | null = null; - - /** Playbacks indexed by their info event id. */ - private playbacks = new Map<string, VoiceBroadcastPlayback>(); - - public constructor(private recordings: VoiceBroadcastRecordingsStore) { - super(); - } - - public setCurrent(current: VoiceBroadcastPlayback): void { - if (this.current === current) return; - - this.current = current; - this.addPlayback(current); - this.emit(VoiceBroadcastPlaybacksStoreEvent.CurrentChanged, current); - } - - public clearCurrent(): void { - if (this.current === null) return; - - this.current = null; - this.emit(VoiceBroadcastPlaybacksStoreEvent.CurrentChanged, null); - } - - public getCurrent(): VoiceBroadcastPlayback | null { - return this.current; - } - - public getByInfoEvent(infoEvent: MatrixEvent, client: MatrixClient): VoiceBroadcastPlayback { - const infoEventId = infoEvent.getId()!; - - if (!this.playbacks.has(infoEventId)) { - this.addPlayback(new VoiceBroadcastPlayback(infoEvent, client, this.recordings)); - } - - return this.playbacks.get(infoEventId)!; - } - - private addPlayback(playback: VoiceBroadcastPlayback): void { - const infoEventId = playback.infoEvent.getId()!; - - if (this.playbacks.has(infoEventId)) return; - - this.playbacks.set(infoEventId, playback); - playback.on(VoiceBroadcastPlaybackEvent.StateChanged, this.onPlaybackStateChanged); - } - - private onPlaybackStateChanged = (state: VoiceBroadcastPlaybackState, playback: VoiceBroadcastPlayback): void => { - switch (state) { - case VoiceBroadcastPlaybackState.Buffering: - case VoiceBroadcastPlaybackState.Playing: - this.pauseExcept(playback); - this.setCurrent(playback); - break; - case VoiceBroadcastPlaybackState.Stopped: - this.clearCurrent(); - break; - } - }; - - private pauseExcept(playbackNotToPause: VoiceBroadcastPlayback): void { - for (const playback of this.playbacks.values()) { - if (playback !== playbackNotToPause) { - playback.pause(); - } - } - } - - public destroy(): void { - this.removeAllListeners(); - - for (const playback of this.playbacks.values()) { - playback.off(VoiceBroadcastPlaybackEvent.StateChanged, this.onPlaybackStateChanged); - } - - this.playbacks = new Map(); - } -} diff --git a/src/voice-broadcast/stores/VoiceBroadcastPreRecordingStore.ts b/src/voice-broadcast/stores/VoiceBroadcastPreRecordingStore.ts deleted file mode 100644 index 3552930687..0000000000 --- a/src/voice-broadcast/stores/VoiceBroadcastPreRecordingStore.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { TypedEventEmitter } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastPreRecording } from ".."; -import { IDestroyable } from "../../utils/IDestroyable"; - -export type VoiceBroadcastPreRecordingEvent = "changed"; - -interface EventMap { - changed: (preRecording: VoiceBroadcastPreRecording | null) => void; -} - -export class VoiceBroadcastPreRecordingStore - extends TypedEventEmitter<VoiceBroadcastPreRecordingEvent, EventMap> - implements IDestroyable -{ - private current: VoiceBroadcastPreRecording | null = null; - - public setCurrent(current: VoiceBroadcastPreRecording): void { - if (this.current === current) return; - - if (this.current) { - this.current.off("dismiss", this.onCancel); - } - - this.current = current; - current.on("dismiss", this.onCancel); - this.emit("changed", current); - } - - public clearCurrent(): void { - if (this.current === null) return; - - this.current.off("dismiss", this.onCancel); - this.current = null; - this.emit("changed", null); - } - - public getCurrent(): VoiceBroadcastPreRecording | null { - return this.current; - } - - public destroy(): void { - this.removeAllListeners(); - - if (this.current) { - this.current.off("dismiss", this.onCancel); - } - } - - private onCancel = (voiceBroadcastPreRecording: VoiceBroadcastPreRecording): void => { - if (this.current === voiceBroadcastPreRecording) { - this.clearCurrent(); - } - }; -} diff --git a/src/voice-broadcast/stores/VoiceBroadcastRecordingsStore.ts b/src/voice-broadcast/stores/VoiceBroadcastRecordingsStore.ts deleted file mode 100644 index ff0f67b910..0000000000 --- a/src/voice-broadcast/stores/VoiceBroadcastRecordingsStore.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022, 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixClient, MatrixEvent, TypedEventEmitter } from "matrix-js-sdk/src/matrix"; - -import { - VoiceBroadcastInfoState, - VoiceBroadcastRecording, - VoiceBroadcastRecordingEvent, - VoiceBroadcastRecordingState, -} from ".."; - -export enum VoiceBroadcastRecordingsStoreEvent { - CurrentChanged = "current_changed", -} - -interface EventMap { - [VoiceBroadcastRecordingsStoreEvent.CurrentChanged]: (recording: VoiceBroadcastRecording | null) => void; -} - -/** - * This store provides access to the current and specific Voice Broadcast recordings. - */ -export class VoiceBroadcastRecordingsStore extends TypedEventEmitter<VoiceBroadcastRecordingsStoreEvent, EventMap> { - private current: VoiceBroadcastRecording | null = null; - private recordings = new Map<string, VoiceBroadcastRecording>(); - - public constructor() { - super(); - } - - public setCurrent(current: VoiceBroadcastRecording): void { - if (this.current === current) return; - - const infoEventId = current.infoEvent.getId(); - - if (!infoEventId) { - throw new Error("Got broadcast info event without Id"); - } - - if (this.current) { - this.current.off(VoiceBroadcastRecordingEvent.StateChanged, this.onCurrentStateChanged); - } - - this.current = current; - this.current.on(VoiceBroadcastRecordingEvent.StateChanged, this.onCurrentStateChanged); - this.recordings.set(infoEventId, current); - this.emit(VoiceBroadcastRecordingsStoreEvent.CurrentChanged, current); - } - - public getCurrent(): VoiceBroadcastRecording | null { - return this.current; - } - - public hasCurrent(): boolean { - return this.current !== null; - } - - public clearCurrent(): void { - if (!this.current) return; - - this.current.off(VoiceBroadcastRecordingEvent.StateChanged, this.onCurrentStateChanged); - this.current = null; - this.emit(VoiceBroadcastRecordingsStoreEvent.CurrentChanged, null); - } - - public getByInfoEvent(infoEvent: MatrixEvent, client: MatrixClient): VoiceBroadcastRecording { - const infoEventId = infoEvent.getId(); - - if (!infoEventId) { - throw new Error("Got broadcast info event without Id"); - } - - const recording = this.recordings.get(infoEventId) || new VoiceBroadcastRecording(infoEvent, client); - this.recordings.set(infoEventId, recording); - return recording; - } - - private onCurrentStateChanged = (state: VoiceBroadcastRecordingState): void => { - if (state === VoiceBroadcastInfoState.Stopped) { - this.clearCurrent(); - } - }; -} diff --git a/src/voice-broadcast/types.ts b/src/voice-broadcast/types.ts deleted file mode 100644 index 8191a0be16..0000000000 --- a/src/voice-broadcast/types.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { RelationType } from "matrix-js-sdk/src/matrix"; - -export const VoiceBroadcastInfoEventType = "io.element.voice_broadcast_info"; -export const VoiceBroadcastChunkEventType = "io.element.voice_broadcast_chunk"; - -export type VoiceBroadcastLiveness = "live" | "not-live" | "grey"; - -export enum VoiceBroadcastInfoState { - Started = "started", - Paused = "paused", - Resumed = "resumed", - Stopped = "stopped", -} - -export interface VoiceBroadcastInfoEventContent { - device_id: string; - state: VoiceBroadcastInfoState; - chunk_length?: number; - last_chunk_sequence?: number; - ["m.relates_to"]?: { - rel_type: RelationType; - event_id: string; - }; -} diff --git a/src/voice-broadcast/utils/VoiceBroadcastChunkEvents.ts b/src/voice-broadcast/utils/VoiceBroadcastChunkEvents.ts deleted file mode 100644 index 039749cf8d..0000000000 --- a/src/voice-broadcast/utils/VoiceBroadcastChunkEvents.ts +++ /dev/null @@ -1,147 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixEvent } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastChunkEventType } from ".."; - -/** - * Voice broadcast chunk collection. - * Orders chunks by sequence (if available) or timestamp. - */ -export class VoiceBroadcastChunkEvents { - private events: MatrixEvent[] = []; - - public getEvents(): MatrixEvent[] { - return [...this.events]; - } - - public getNext(event: MatrixEvent): MatrixEvent | undefined { - return this.events[this.events.indexOf(event) + 1]; - } - - public addEvent(event: MatrixEvent): void { - if (this.addOrReplaceEvent(event)) { - this.sort(); - } - } - - public addEvents(events: MatrixEvent[]): void { - const atLeastOneNew = events.reduce((newSoFar: boolean, event: MatrixEvent): boolean => { - return this.addOrReplaceEvent(event) || newSoFar; - }, false); - - if (atLeastOneNew) { - this.sort(); - } - } - - public includes(event: MatrixEvent): boolean { - return !!this.events.find((e) => this.equalByTxnIdOrId(event, e)); - } - - /** - * @returns {number} Length in milliseconds - */ - public getLength(): number { - return this.events.reduce((length: number, event: MatrixEvent) => { - return length + this.calculateChunkLength(event); - }, 0); - } - - public getLengthSeconds(): number { - return this.getLength() / 1000; - } - - /** - * Returns the accumulated length to (excl.) a chunk event. - */ - public getLengthTo(event: MatrixEvent): number { - let length = 0; - - for (let i = 0; i < this.events.indexOf(event); i++) { - length += this.calculateChunkLength(this.events[i]); - } - - return length; - } - - public findByTime(time: number): MatrixEvent | null { - let lengthSoFar = 0; - - for (let i = 0; i < this.events.length; i++) { - lengthSoFar += this.calculateChunkLength(this.events[i]); - - if (lengthSoFar >= time) { - return this.events[i]; - } - } - - return null; - } - - public isLast(event: MatrixEvent): boolean { - return this.events.indexOf(event) >= this.events.length - 1; - } - - public getSequenceForEvent(event: MatrixEvent): number | null { - const sequence = parseInt(event.getContent()?.[VoiceBroadcastChunkEventType]?.sequence, 10); - if (!isNaN(sequence)) return sequence; - - if (this.events.includes(event)) return this.events.indexOf(event) + 1; - - return null; - } - - public getNumberOfEvents(): number { - return this.events.length; - } - - private calculateChunkLength(event: MatrixEvent): number { - return event.getContent()?.["org.matrix.msc1767.audio"]?.duration || event.getContent()?.info?.duration || 0; - } - - private addOrReplaceEvent = (event: MatrixEvent): boolean => { - this.events = this.events.filter((e) => !this.equalByTxnIdOrId(event, e)); - this.events.push(event); - return true; - }; - - private equalByTxnIdOrId(eventA: MatrixEvent, eventB: MatrixEvent): boolean { - return ( - (eventA.getTxnId() && eventB.getTxnId() && eventA.getTxnId() === eventB.getTxnId()) || - eventA.getId() === eventB.getId() - ); - } - - /** - * Sort by sequence, if available for all events. - * Else fall back to timestamp. - */ - private sort(): void { - const compareFn = this.allHaveSequence() ? this.compareBySequence : this.compareByTimestamp; - this.events.sort(compareFn); - } - - private compareBySequence = (a: MatrixEvent, b: MatrixEvent): number => { - const aSequence = a.getContent()?.[VoiceBroadcastChunkEventType]?.sequence || 0; - const bSequence = b.getContent()?.[VoiceBroadcastChunkEventType]?.sequence || 0; - return aSequence - bSequence; - }; - - private compareByTimestamp = (a: MatrixEvent, b: MatrixEvent): number => { - return a.getTs() - b.getTs(); - }; - - private allHaveSequence(): boolean { - return !this.events.some((event: MatrixEvent) => { - const sequence = event.getContent()?.[VoiceBroadcastChunkEventType]?.sequence; - return parseInt(sequence, 10) !== sequence; - }); - } -} diff --git a/src/voice-broadcast/utils/VoiceBroadcastResumer.ts b/src/voice-broadcast/utils/VoiceBroadcastResumer.ts deleted file mode 100644 index 963b6ef3a6..0000000000 --- a/src/voice-broadcast/utils/VoiceBroadcastResumer.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { ClientEvent, MatrixClient, MatrixEvent, RelationType, Room, SyncState } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastInfoEventContent, VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from ".."; -import { IDestroyable } from "../../utils/IDestroyable"; -import { findRoomLiveVoiceBroadcastFromUserAndDevice } from "./findRoomLiveVoiceBroadcastFromUserAndDevice"; - -/** - * Handles voice broadcasts on app resume (after logging in, reload, crash…). - */ -export class VoiceBroadcastResumer implements IDestroyable { - public constructor(private client: MatrixClient) { - if (client.isInitialSyncComplete()) { - this.resume(); - } else { - // wait for initial sync - client.on(ClientEvent.Sync, this.onClientSync); - } - } - - private onClientSync = (): void => { - if (this.client.getSyncState() === SyncState.Syncing) { - this.client.off(ClientEvent.Sync, this.onClientSync); - this.resume(); - } - }; - - private resume(): void { - const userId = this.client.getUserId(); - const deviceId = this.client.getDeviceId(); - - if (!userId || !deviceId) { - // Resuming a voice broadcast only makes sense if there is a user. - return; - } - - this.client.getRooms().forEach((room: Room) => { - const infoEvent = findRoomLiveVoiceBroadcastFromUserAndDevice(room, userId, deviceId); - - if (infoEvent) { - // Found a live broadcast event from current device; stop it. - // Stopping it is a temporary solution (see PSF-1669). - this.sendStopVoiceBroadcastStateEvent(infoEvent); - return false; - } - }); - } - - private sendStopVoiceBroadcastStateEvent(infoEvent: MatrixEvent): void { - const userId = this.client.getUserId(); - const deviceId = this.client.getDeviceId(); - const roomId = infoEvent.getRoomId(); - - if (!userId || !deviceId || !roomId) { - // We can only send a state event if we know all the IDs. - return; - } - - const content: VoiceBroadcastInfoEventContent = { - device_id: deviceId, - state: VoiceBroadcastInfoState.Stopped, - }; - - // all events should reference the started event - const referencedEventId = - infoEvent.getContent()?.state === VoiceBroadcastInfoState.Started - ? infoEvent.getId() - : infoEvent.getContent()?.["m.relates_to"]?.event_id; - - if (referencedEventId) { - content["m.relates_to"] = { - rel_type: RelationType.Reference, - event_id: referencedEventId, - }; - } - - this.client.sendStateEvent(roomId, VoiceBroadcastInfoEventType, content, userId); - } - - public destroy(): void { - this.client.off(ClientEvent.Sync, this.onClientSync); - } -} diff --git a/src/voice-broadcast/utils/checkVoiceBroadcastPreConditions.tsx b/src/voice-broadcast/utils/checkVoiceBroadcastPreConditions.tsx deleted file mode 100644 index ae96bc0b14..0000000000 --- a/src/voice-broadcast/utils/checkVoiceBroadcastPreConditions.tsx +++ /dev/null @@ -1,86 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { MatrixClient, Room, SyncState } from "matrix-js-sdk/src/matrix"; - -import { hasRoomLiveVoiceBroadcast, VoiceBroadcastInfoEventType, VoiceBroadcastRecordingsStore } from ".."; -import InfoDialog from "../../components/views/dialogs/InfoDialog"; -import { _t } from "../../languageHandler"; -import Modal from "../../Modal"; - -const showAlreadyRecordingDialog = (): void => { - Modal.createDialog(InfoDialog, { - title: _t("voice_broadcast|failed_already_recording_title"), - description: <p>{_t("voice_broadcast|failed_already_recording_description")}</p>, - hasCloseButton: true, - }); -}; - -const showInsufficientPermissionsDialog = (): void => { - Modal.createDialog(InfoDialog, { - title: _t("voice_broadcast|failed_insufficient_permission_title"), - description: <p>{_t("voice_broadcast|failed_insufficient_permission_description")}</p>, - hasCloseButton: true, - }); -}; - -const showOthersAlreadyRecordingDialog = (): void => { - Modal.createDialog(InfoDialog, { - title: _t("voice_broadcast|failed_others_already_recording_title"), - description: <p>{_t("voice_broadcast|failed_others_already_recording_description")}</p>, - hasCloseButton: true, - }); -}; - -const showNoConnectionDialog = (): void => { - Modal.createDialog(InfoDialog, { - title: _t("voice_broadcast|failed_no_connection_title"), - description: <p>{_t("voice_broadcast|failed_no_connection_description")}</p>, - hasCloseButton: true, - }); -}; - -export const checkVoiceBroadcastPreConditions = async ( - room: Room, - client: MatrixClient, - recordingsStore: VoiceBroadcastRecordingsStore, -): Promise<boolean> => { - if (recordingsStore.getCurrent()) { - showAlreadyRecordingDialog(); - return false; - } - - const currentUserId = client.getUserId(); - - if (!currentUserId) return false; - - if (!room.currentState.maySendStateEvent(VoiceBroadcastInfoEventType, currentUserId)) { - showInsufficientPermissionsDialog(); - return false; - } - - if (client.getSyncState() === SyncState.Error) { - showNoConnectionDialog(); - return false; - } - - const { hasBroadcast, startedByUser } = await hasRoomLiveVoiceBroadcast(client, room, currentUserId); - - if (hasBroadcast && startedByUser) { - showAlreadyRecordingDialog(); - return false; - } - - if (hasBroadcast) { - showOthersAlreadyRecordingDialog(); - return false; - } - - return true; -}; diff --git a/src/voice-broadcast/utils/cleanUpBroadcasts.ts b/src/voice-broadcast/utils/cleanUpBroadcasts.ts deleted file mode 100644 index 50133274b0..0000000000 --- a/src/voice-broadcast/utils/cleanUpBroadcasts.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { SdkContextClass } from "../../contexts/SDKContext"; - -export const cleanUpBroadcasts = async (stores: SdkContextClass): Promise<void> => { - stores.voiceBroadcastPlaybacksStore.getCurrent()?.stop(); - stores.voiceBroadcastPlaybacksStore.clearCurrent(); - - await stores.voiceBroadcastRecordingsStore.getCurrent()?.stop(); - stores.voiceBroadcastRecordingsStore.clearCurrent(); - - stores.voiceBroadcastPreRecordingStore.getCurrent()?.cancel(); - stores.voiceBroadcastPreRecordingStore.clearCurrent(); -}; diff --git a/src/voice-broadcast/utils/determineVoiceBroadcastLiveness.ts b/src/voice-broadcast/utils/determineVoiceBroadcastLiveness.ts deleted file mode 100644 index 8d9660c572..0000000000 --- a/src/voice-broadcast/utils/determineVoiceBroadcastLiveness.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { VoiceBroadcastInfoState, VoiceBroadcastLiveness } from ".."; - -const stateLivenessMap: Map<VoiceBroadcastInfoState, VoiceBroadcastLiveness> = new Map([ - ["started", "live"], - ["resumed", "live"], - ["paused", "grey"], - ["stopped", "not-live"], -] as Array<[VoiceBroadcastInfoState, VoiceBroadcastLiveness]>); - -export const determineVoiceBroadcastLiveness = (infoState: VoiceBroadcastInfoState): VoiceBroadcastLiveness => { - return stateLivenessMap.get(infoState) ?? "not-live"; -}; diff --git a/src/voice-broadcast/utils/doClearCurrentVoiceBroadcastPlaybackIfStopped.ts b/src/voice-broadcast/utils/doClearCurrentVoiceBroadcastPlaybackIfStopped.ts deleted file mode 100644 index ef0e1e7aed..0000000000 --- a/src/voice-broadcast/utils/doClearCurrentVoiceBroadcastPlaybackIfStopped.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { VoiceBroadcastPlaybacksStore, VoiceBroadcastPlaybackState } from ".."; - -export const doClearCurrentVoiceBroadcastPlaybackIfStopped = ( - voiceBroadcastPlaybacksStore: VoiceBroadcastPlaybacksStore, -): void => { - if (voiceBroadcastPlaybacksStore.getCurrent()?.getState() === VoiceBroadcastPlaybackState.Stopped) { - // clear current if stopped - return; - } -}; diff --git a/src/voice-broadcast/utils/doMaybeSetCurrentVoiceBroadcastPlayback.ts b/src/voice-broadcast/utils/doMaybeSetCurrentVoiceBroadcastPlayback.ts deleted file mode 100644 index 2ec4ab185d..0000000000 --- a/src/voice-broadcast/utils/doMaybeSetCurrentVoiceBroadcastPlayback.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; - -import { - hasRoomLiveVoiceBroadcast, - VoiceBroadcastPlaybacksStore, - VoiceBroadcastPlaybackState, - VoiceBroadcastRecordingsStore, -} from ".."; - -/** - * When a live voice broadcast is in the room and - * another voice broadcast is not currently being listened to or recorded - * the live broadcast in the room is set as the current broadcast to listen to. - * When there is no live broadcast in the room: clear current broadcast. - * - * @param {Room} room The room to check for a live voice broadcast - * @param {MatrixClient} client - * @param {VoiceBroadcastPlaybacksStore} voiceBroadcastPlaybacksStore - * @param {VoiceBroadcastRecordingsStore} voiceBroadcastRecordingsStore - */ -export const doMaybeSetCurrentVoiceBroadcastPlayback = async ( - room: Room, - client: MatrixClient, - voiceBroadcastPlaybacksStore: VoiceBroadcastPlaybacksStore, - voiceBroadcastRecordingsStore: VoiceBroadcastRecordingsStore, -): Promise<void> => { - // do not disturb the current recording - if (voiceBroadcastRecordingsStore.hasCurrent()) return; - - const currentPlayback = voiceBroadcastPlaybacksStore.getCurrent(); - - if (currentPlayback && currentPlayback.getState() !== VoiceBroadcastPlaybackState.Stopped) { - // do not disturb the current playback - return; - } - - const { infoEvent } = await hasRoomLiveVoiceBroadcast(client, room); - - if (infoEvent) { - // live broadcast in the room + no recording + not listening yet: set the current broadcast - const voiceBroadcastPlayback = voiceBroadcastPlaybacksStore.getByInfoEvent(infoEvent, client); - voiceBroadcastPlaybacksStore.setCurrent(voiceBroadcastPlayback); - return; - } - - // no broadcast; not listening: clear current - voiceBroadcastPlaybacksStore.clearCurrent(); -}; diff --git a/src/voice-broadcast/utils/findRoomLiveVoiceBroadcastFromUserAndDevice.ts b/src/voice-broadcast/utils/findRoomLiveVoiceBroadcastFromUserAndDevice.ts deleted file mode 100644 index fbd02d44fb..0000000000 --- a/src/voice-broadcast/utils/findRoomLiveVoiceBroadcastFromUserAndDevice.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from ".."; - -export const findRoomLiveVoiceBroadcastFromUserAndDevice = ( - room: Room, - userId: string, - deviceId: string, -): MatrixEvent | null => { - const stateEvent = room.currentState.getStateEvents(VoiceBroadcastInfoEventType, userId); - - // no broadcast from that user - if (!stateEvent) return null; - - const content = stateEvent.getContent() || {}; - - // stopped broadcast - if (content.state === VoiceBroadcastInfoState.Stopped) return null; - - return content.device_id === deviceId ? stateEvent : null; -}; diff --git a/src/voice-broadcast/utils/getChunkLength.ts b/src/voice-broadcast/utils/getChunkLength.ts deleted file mode 100644 index b3fe2f557d..0000000000 --- a/src/voice-broadcast/utils/getChunkLength.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import SdkConfig, { DEFAULTS } from "../../SdkConfig"; -import { Features } from "../../settings/Settings"; -import SettingsStore from "../../settings/SettingsStore"; - -/** - * Returns the target chunk length for voice broadcasts: - * - If {@see Features.VoiceBroadcastForceSmallChunks} is enabled uses 15s chunk length - * - Otherwise to get the value from the voice_broadcast.chunk_length config - * - If that fails from DEFAULTS - * - If that fails fall back to 120 (two minutes) - */ -export const getChunkLength = (): number => { - if (SettingsStore.getValue(Features.VoiceBroadcastForceSmallChunks)) return 15; - return SdkConfig.get("voice_broadcast")?.chunk_length || DEFAULTS.voice_broadcast?.chunk_length || 120; -}; diff --git a/src/voice-broadcast/utils/getMaxBroadcastLength.ts b/src/voice-broadcast/utils/getMaxBroadcastLength.ts deleted file mode 100644 index e5df83ef05..0000000000 --- a/src/voice-broadcast/utils/getMaxBroadcastLength.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import SdkConfig, { DEFAULTS } from "../../SdkConfig"; - -/** - * Returns the max length for voice broadcasts: - * - Tries to get the value from the voice_broadcast.max_length config - * - If that fails from DEFAULTS - * - If that fails fall back to four hours - */ -export const getMaxBroadcastLength = (): number => { - return SdkConfig.get("voice_broadcast")?.max_length || DEFAULTS.voice_broadcast?.max_length || 4 * 60 * 60; -}; diff --git a/src/voice-broadcast/utils/hasRoomLiveVoiceBroadcast.ts b/src/voice-broadcast/utils/hasRoomLiveVoiceBroadcast.ts deleted file mode 100644 index 939eb1a0c2..0000000000 --- a/src/voice-broadcast/utils/hasRoomLiveVoiceBroadcast.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; - -import { retrieveStartedInfoEvent, VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from ".."; -import { asyncEvery } from "../../utils/arrays"; - -interface Result { - // whether there is a live broadcast in the room - hasBroadcast: boolean; - // info event of any live broadcast in the room - infoEvent: MatrixEvent | null; - // whether the broadcast was started by the user - startedByUser: boolean; -} - -export const hasRoomLiveVoiceBroadcast = async (client: MatrixClient, room: Room, userId?: string): Promise<Result> => { - let hasBroadcast = false; - let startedByUser = false; - let infoEvent: MatrixEvent | null = null; - - const stateEvents = room.currentState.getStateEvents(VoiceBroadcastInfoEventType); - await asyncEvery(stateEvents, async (event: MatrixEvent) => { - const state = event.getContent()?.state; - - if (state && state !== VoiceBroadcastInfoState.Stopped) { - const startEvent = await retrieveStartedInfoEvent(event, client); - - // skip if started voice broadcast event is redacted - if (startEvent?.isRedacted()) return true; - - hasBroadcast = true; - infoEvent = startEvent; - - // state key = sender's MXID - if (event.getStateKey() === userId) { - startedByUser = true; - // break here, because more than true / true is not possible - return false; - } - } - - return true; - }); - - return { - hasBroadcast, - infoEvent, - startedByUser, - }; -}; diff --git a/src/voice-broadcast/utils/isRelatedToVoiceBroadcast.ts b/src/voice-broadcast/utils/isRelatedToVoiceBroadcast.ts deleted file mode 100644 index eca8f890e0..0000000000 --- a/src/voice-broadcast/utils/isRelatedToVoiceBroadcast.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixClient, MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastInfoEventType } from "../types"; - -export const isRelatedToVoiceBroadcast = (event: MatrixEvent, client: MatrixClient): boolean => { - const relation = event.getRelation(); - - return ( - relation?.rel_type === RelationType.Reference && - !!relation.event_id && - client.getRoom(event.getRoomId())?.findEventById(relation.event_id)?.getType() === VoiceBroadcastInfoEventType - ); -}; diff --git a/src/voice-broadcast/utils/isVoiceBroadcastStartedEvent.ts b/src/voice-broadcast/utils/isVoiceBroadcastStartedEvent.ts deleted file mode 100644 index fffe45850e..0000000000 --- a/src/voice-broadcast/utils/isVoiceBroadcastStartedEvent.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixEvent } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../types"; - -export const isVoiceBroadcastStartedEvent = (event: MatrixEvent): boolean => { - return ( - event.getType() === VoiceBroadcastInfoEventType && event.getContent()?.state === VoiceBroadcastInfoState.Started - ); -}; diff --git a/src/voice-broadcast/utils/pauseNonLiveBroadcastFromOtherRoom.ts b/src/voice-broadcast/utils/pauseNonLiveBroadcastFromOtherRoom.ts deleted file mode 100644 index e854ba9bac..0000000000 --- a/src/voice-broadcast/utils/pauseNonLiveBroadcastFromOtherRoom.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { Room } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastPlaybacksStore } from ".."; - -export const pauseNonLiveBroadcastFromOtherRoom = ( - room: Room, - voiceBroadcastPlaybacksStore: VoiceBroadcastPlaybacksStore, -): void => { - const playingBroadcast = voiceBroadcastPlaybacksStore.getCurrent(); - - if ( - !playingBroadcast || - playingBroadcast?.getLiveness() === "live" || - playingBroadcast?.infoEvent.getRoomId() === room.roomId - ) { - return; - } - - voiceBroadcastPlaybacksStore.clearCurrent(); - playingBroadcast.pause(); -}; diff --git a/src/voice-broadcast/utils/retrieveStartedInfoEvent.ts b/src/voice-broadcast/utils/retrieveStartedInfoEvent.ts deleted file mode 100644 index cc5be144c9..0000000000 --- a/src/voice-broadcast/utils/retrieveStartedInfoEvent.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastInfoState } from ".."; - -export const retrieveStartedInfoEvent = async ( - event: MatrixEvent, - client: MatrixClient, -): Promise<MatrixEvent | null> => { - // started event passed as argument - if (event.getContent()?.state === VoiceBroadcastInfoState.Started) return event; - - const relatedEventId = event.getRelation()?.event_id; - - // no related event - if (!relatedEventId) return null; - - const roomId = event.getRoomId() || ""; - const relatedEventFromRoom = client.getRoom(roomId)?.findEventById(relatedEventId); - - // event found - if (relatedEventFromRoom) return relatedEventFromRoom; - - try { - const relatedEventData = await client.fetchRoomEvent(roomId, relatedEventId); - return new MatrixEvent(relatedEventData); - } catch {} - - return null; -}; diff --git a/src/voice-broadcast/utils/setUpVoiceBroadcastPreRecording.ts b/src/voice-broadcast/utils/setUpVoiceBroadcastPreRecording.ts deleted file mode 100644 index c50607c58f..0000000000 --- a/src/voice-broadcast/utils/setUpVoiceBroadcastPreRecording.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; - -import { - checkVoiceBroadcastPreConditions, - VoiceBroadcastPlaybacksStore, - VoiceBroadcastPreRecording, - VoiceBroadcastPreRecordingStore, - VoiceBroadcastRecordingsStore, -} from ".."; - -export const setUpVoiceBroadcastPreRecording = async ( - room: Room, - client: MatrixClient, - playbacksStore: VoiceBroadcastPlaybacksStore, - recordingsStore: VoiceBroadcastRecordingsStore, - preRecordingStore: VoiceBroadcastPreRecordingStore, -): Promise<VoiceBroadcastPreRecording | null> => { - if (!(await checkVoiceBroadcastPreConditions(room, client, recordingsStore))) { - return null; - } - - const userId = client.getUserId(); - if (!userId) return null; - - const sender = room.getMember(userId); - if (!sender) return null; - - // pause and clear current playback (if any) - playbacksStore.getCurrent()?.pause(); - playbacksStore.clearCurrent(); - - const preRecording = new VoiceBroadcastPreRecording(room, sender, client, playbacksStore, recordingsStore); - preRecordingStore.setCurrent(preRecording); - return preRecording; -}; diff --git a/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastRecordingTile.ts b/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastRecordingTile.ts deleted file mode 100644 index d729d9e1ca..0000000000 --- a/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastRecordingTile.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastInfoEventContent, VoiceBroadcastInfoState } from ".."; - -export const shouldDisplayAsVoiceBroadcastRecordingTile = ( - state: VoiceBroadcastInfoState, - client: MatrixClient, - event: MatrixEvent, -): boolean => { - const userId = client.getUserId(); - return ( - !!userId && - userId === event.getSender() && - client.getDeviceId() === event.getContent<VoiceBroadcastInfoEventContent>()?.device_id && - state !== VoiceBroadcastInfoState.Stopped - ); -}; diff --git a/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastStoppedText.ts b/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastStoppedText.ts deleted file mode 100644 index 2179aff3b7..0000000000 --- a/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastStoppedText.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixEvent } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from ".."; - -export const shouldDisplayAsVoiceBroadcastStoppedText = (event: MatrixEvent): boolean => - event.getType() === VoiceBroadcastInfoEventType && - event.getContent()?.state === VoiceBroadcastInfoState.Stopped && - !event.isRedacted(); diff --git a/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastTile.ts b/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastTile.ts deleted file mode 100644 index 9a51b33c9a..0000000000 --- a/src/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastTile.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixEvent } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from ".."; - -export const shouldDisplayAsVoiceBroadcastTile = (event: MatrixEvent): boolean => - event.getType?.() === VoiceBroadcastInfoEventType && - (event.getContent?.()?.state === VoiceBroadcastInfoState.Started || event.isRedacted()); diff --git a/src/voice-broadcast/utils/showCantStartACallDialog.tsx b/src/voice-broadcast/utils/showCantStartACallDialog.tsx deleted file mode 100644 index eeeb86ee07..0000000000 --- a/src/voice-broadcast/utils/showCantStartACallDialog.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; - -import InfoDialog from "../../components/views/dialogs/InfoDialog"; -import { _t } from "../../languageHandler"; -import Modal from "../../Modal"; - -export const showCantStartACallDialog = (): void => { - Modal.createDialog(InfoDialog, { - title: _t("voip|failed_call_live_broadcast_title"), - description: <p>{_t("voip|failed_call_live_broadcast_description")}</p>, - hasCloseButton: true, - }); -}; diff --git a/src/voice-broadcast/utils/startNewVoiceBroadcastRecording.ts b/src/voice-broadcast/utils/startNewVoiceBroadcastRecording.ts deleted file mode 100644 index f0c5a91932..0000000000 --- a/src/voice-broadcast/utils/startNewVoiceBroadcastRecording.ts +++ /dev/null @@ -1,92 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { ISendEventResponse, MatrixClient, Room, RoomStateEvent } from "matrix-js-sdk/src/matrix"; -import { defer } from "matrix-js-sdk/src/utils"; - -import { - VoiceBroadcastInfoEventContent, - VoiceBroadcastInfoEventType, - VoiceBroadcastInfoState, - VoiceBroadcastRecordingsStore, - VoiceBroadcastRecording, - getChunkLength, - VoiceBroadcastPlaybacksStore, -} from ".."; -import { checkVoiceBroadcastPreConditions } from "./checkVoiceBroadcastPreConditions"; - -const startBroadcast = async ( - room: Room, - client: MatrixClient, - recordingsStore: VoiceBroadcastRecordingsStore, -): Promise<VoiceBroadcastRecording> => { - const { promise, resolve, reject } = defer<VoiceBroadcastRecording>(); - - const userId = client.getUserId(); - - if (!userId) { - reject("unable to start voice broadcast if current user is unknown"); - return promise; - } - - let result: ISendEventResponse | null = null; - - const onRoomStateEvents = (): void => { - if (!result) return; - - const voiceBroadcastEvent = room.currentState.getStateEvents(VoiceBroadcastInfoEventType, userId); - - if (voiceBroadcastEvent?.getId() === result.event_id) { - room.off(RoomStateEvent.Events, onRoomStateEvents); - const recording = new VoiceBroadcastRecording(voiceBroadcastEvent, client); - recordingsStore.setCurrent(recording); - recording.start(); - resolve(recording); - } - }; - - room.on(RoomStateEvent.Events, onRoomStateEvents); - - // XXX Michael W: refactor to live event - result = await client.sendStateEvent( - room.roomId, - VoiceBroadcastInfoEventType, - { - device_id: client.getDeviceId(), - state: VoiceBroadcastInfoState.Started, - chunk_length: getChunkLength(), - } as VoiceBroadcastInfoEventContent, - userId, - ); - - return promise; -}; - -/** - * Starts a new Voice Broadcast Recording, if - * - the user has the permissions to do so in the room - * - the user is not already recording a voice broadcast - * - there is no other broadcast being recorded in the room, yet - * Sends a voice_broadcast_info state event and waits for the event to actually appear in the room state. - */ -export const startNewVoiceBroadcastRecording = async ( - room: Room, - client: MatrixClient, - playbacksStore: VoiceBroadcastPlaybacksStore, - recordingsStore: VoiceBroadcastRecordingsStore, -): Promise<VoiceBroadcastRecording | null> => { - if (!(await checkVoiceBroadcastPreConditions(room, client, recordingsStore))) { - return null; - } - - // pause and clear current playback (if any) - playbacksStore.getCurrent()?.pause(); - playbacksStore.clearCurrent(); - - return startBroadcast(room, client, recordingsStore); -}; diff --git a/src/voice-broadcast/utils/textForVoiceBroadcastStoppedEvent.tsx b/src/voice-broadcast/utils/textForVoiceBroadcastStoppedEvent.tsx deleted file mode 100644 index bc2aa412a5..0000000000 --- a/src/voice-broadcast/utils/textForVoiceBroadcastStoppedEvent.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React, { ReactNode } from "react"; -import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; - -import { MatrixClientPeg } from "../../MatrixClientPeg"; -import AccessibleButton from "../../components/views/elements/AccessibleButton"; -import { highlightEvent } from "../../utils/EventUtils"; -import { _t } from "../../languageHandler"; -import { getSenderName } from "../../utils/event/getSenderName"; - -export const textForVoiceBroadcastStoppedEvent = (event: MatrixEvent, client: MatrixClient): (() => ReactNode) => { - return (): ReactNode => { - const ownUserId = MatrixClientPeg.get()?.getUserId(); - const startEventId = event.getRelation()?.event_id; - const roomId = event.getRoomId(); - - const templateTags = { - a: (text: string) => - startEventId && roomId ? ( - <AccessibleButton kind="link_inline" onClick={(): void => highlightEvent(roomId, startEventId)}> - {text} - </AccessibleButton> - ) : ( - text - ), - }; - - if (ownUserId && ownUserId === event.getSender()) { - return _t("timeline|io.element.voice_broadcast_info|you", {}, templateTags); - } - - return _t("timeline|io.element.voice_broadcast_info|user", { senderName: getSenderName(event) }, templateTags); - }; -}; diff --git a/src/voice-broadcast/utils/textForVoiceBroadcastStoppedEventWithoutLink.ts b/src/voice-broadcast/utils/textForVoiceBroadcastStoppedEventWithoutLink.ts deleted file mode 100644 index 13d7f47c48..0000000000 --- a/src/voice-broadcast/utils/textForVoiceBroadcastStoppedEventWithoutLink.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixEvent } from "matrix-js-sdk/src/matrix"; - -import { _t } from "../../languageHandler"; -import { MatrixClientPeg } from "../../MatrixClientPeg"; -import { getSenderName } from "../../utils/event/getSenderName"; - -export const textForVoiceBroadcastStoppedEventWithoutLink = (event: MatrixEvent): string => { - const ownUserId = MatrixClientPeg.get()?.getUserId(); - - if (ownUserId && ownUserId === event.getSender()) { - return _t("event_preview|io.element.voice_broadcast_info|you", {}); - } - - return _t("event_preview|io.element.voice_broadcast_info|user", { senderName: getSenderName(event) }); -}; diff --git a/test/components/views/dialogs/security/CreateCrossSigningDialog-test.tsx b/test/components/views/dialogs/security/CreateCrossSigningDialog-test.tsx deleted file mode 100644 index 3e5dc4eb94..0000000000 --- a/test/components/views/dialogs/security/CreateCrossSigningDialog-test.tsx +++ /dev/null @@ -1,131 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2018-2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { render, screen, waitFor } from "jest-matrix-react"; -import { mocked } from "jest-mock"; -import { MatrixClient } from "matrix-js-sdk/src/matrix"; - -import { createCrossSigning } from "../../../../../src/CreateCrossSigning"; -import CreateCrossSigningDialog from "../../../../../src/components/views/dialogs/security/CreateCrossSigningDialog"; -import { createTestClient } from "../../../../test-utils"; - -jest.mock("../../../../../src/CreateCrossSigning", () => ({ - createCrossSigning: jest.fn(), -})); - -describe("CreateCrossSigningDialog", () => { - let client: MatrixClient; - let createCrossSigningResolve: () => void; - let createCrossSigningReject: (e: Error) => void; - - beforeEach(() => { - client = createTestClient(); - mocked(createCrossSigning).mockImplementation(() => { - return new Promise((resolve, reject) => { - createCrossSigningResolve = resolve; - createCrossSigningReject = reject; - }); - }); - }); - - afterEach(() => { - jest.resetAllMocks(); - jest.restoreAllMocks(); - }); - - it("should call createCrossSigning and show a spinner while it runs", async () => { - const onFinished = jest.fn(); - - render( - <CreateCrossSigningDialog - matrixClient={client} - accountPassword="hunter2" - tokenLogin={false} - onFinished={onFinished} - />, - ); - - expect(createCrossSigning).toHaveBeenCalledWith(client, false, "hunter2"); - expect(screen.getByTestId("spinner")).toBeInTheDocument(); - - createCrossSigningResolve!(); - - await waitFor(() => expect(onFinished).toHaveBeenCalledWith(true)); - }); - - it("should display an error if createCrossSigning fails", async () => { - render( - <CreateCrossSigningDialog - matrixClient={client} - accountPassword="hunter2" - tokenLogin={false} - onFinished={jest.fn()} - />, - ); - - createCrossSigningReject!(new Error("generic error message")); - - await expect(await screen.findByRole("button", { name: "Retry" })).toBeInTheDocument(); - }); - - it("ignores failures when tokenLogin is true", async () => { - const onFinished = jest.fn(); - - render( - <CreateCrossSigningDialog - matrixClient={client} - accountPassword="hunter2" - tokenLogin={true} - onFinished={onFinished} - />, - ); - - createCrossSigningReject!(new Error("generic error message")); - - await waitFor(() => expect(onFinished).toHaveBeenCalledWith(false)); - }); - - it("cancels the dialog when the cancel button is clicked", async () => { - const onFinished = jest.fn(); - - render( - <CreateCrossSigningDialog - matrixClient={client} - accountPassword="hunter2" - tokenLogin={false} - onFinished={onFinished} - />, - ); - - createCrossSigningReject!(new Error("generic error message")); - - const cancelButton = await screen.findByRole("button", { name: "Cancel" }); - cancelButton.click(); - - expect(onFinished).toHaveBeenCalledWith(false); - }); - - it("should retry when the retry button is clicked", async () => { - render( - <CreateCrossSigningDialog - matrixClient={client} - accountPassword="hunter2" - tokenLogin={false} - onFinished={jest.fn()} - />, - ); - - createCrossSigningReject!(new Error("generic error message")); - - const retryButton = await screen.findByRole("button", { name: "Retry" }); - retryButton.click(); - - expect(createCrossSigning).toHaveBeenCalledTimes(2); - }); -}); diff --git a/test/components/views/dialogs/security/InitialCryptoSetupDialog-test.tsx b/test/components/views/dialogs/security/InitialCryptoSetupDialog-test.tsx new file mode 100644 index 0000000000..a589b55289 --- /dev/null +++ b/test/components/views/dialogs/security/InitialCryptoSetupDialog-test.tsx @@ -0,0 +1,61 @@ +/* +Copyright 2024 New Vector Ltd. +Copyright 2018-2022 The Matrix.org Foundation C.I.C. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import React from "react"; +import { render, screen } from "jest-matrix-react"; +import userEvent from "@testing-library/user-event"; + +import { InitialCryptoSetupDialog } from "../../../../../src/components/views/dialogs/security/InitialCryptoSetupDialog"; +import { InitialCryptoSetupStore } from "../../../../../src/stores/InitialCryptoSetupStore"; + +describe("InitialCryptoSetupDialog", () => { + const storeMock = { + getStatus: jest.fn(), + retry: jest.fn(), + on: jest.fn(), + off: jest.fn(), + }; + + beforeEach(() => { + jest.spyOn(InitialCryptoSetupStore, "sharedInstance").mockReturnValue(storeMock as any); + }); + + afterEach(() => { + jest.resetAllMocks(); + jest.restoreAllMocks(); + }); + + it("should show a spinner while the setup is in progress", async () => { + const onFinished = jest.fn(); + + storeMock.getStatus.mockReturnValue("in_progress"); + + render(<InitialCryptoSetupDialog onFinished={onFinished} />); + + expect(screen.getByTestId("spinner")).toBeInTheDocument(); + }); + + it("should display an error if setup has failed", async () => { + storeMock.getStatus.mockReturnValue("error"); + + render(<InitialCryptoSetupDialog onFinished={jest.fn()} />); + + await expect(await screen.findByRole("button", { name: "Retry" })).toBeInTheDocument(); + }); + + it("calls retry when retry button pressed", async () => { + const onFinished = jest.fn(); + storeMock.getStatus.mockReturnValue("error"); + + render(<InitialCryptoSetupDialog onFinished={onFinished} />); + + await userEvent.click(await screen.findByRole("button", { name: "Retry" })); + + expect(storeMock.retry).toHaveBeenCalled(); + }); +}); diff --git a/test/test-utils/call.ts b/test/test-utils/call.ts index 0262e5537f..df87fcaa55 100644 --- a/test/test-utils/call.ts +++ b/test/test-utils/call.ts @@ -44,17 +44,20 @@ export class MockedCall extends Call { } public static create(room: Room, id: string) { - room.addLiveEvents([ - mkEvent({ - event: true, - type: this.EVENT_TYPE, - room: room.roomId, - user: "@alice:example.org", - content: { "m.type": "m.video", "m.intent": "m.prompt" }, - skey: id, - ts: Date.now(), - }), - ]); + room.addLiveEvents( + [ + mkEvent({ + event: true, + type: this.EVENT_TYPE, + room: room.roomId, + user: "@alice:example.org", + content: { "m.type": "m.video", "m.intent": "m.prompt" }, + skey: id, + ts: Date.now(), + }), + ], + { addToState: true }, + ); // @ts-ignore deliberately calling a private method // Let CallStore know that a call might now exist CallStore.instance.updateRoom(room); @@ -81,17 +84,20 @@ export class MockedCall extends Call { public destroy() { // Terminate the call for good measure - this.room.addLiveEvents([ - mkEvent({ - event: true, - type: MockedCall.EVENT_TYPE, - room: this.room.roomId, - user: "@alice:example.org", - content: { ...this.event.getContent(), "m.terminated": "Call ended" }, - skey: this.widget.id, - ts: Date.now(), - }), - ]); + this.room.addLiveEvents( + [ + mkEvent({ + event: true, + type: MockedCall.EVENT_TYPE, + room: this.room.roomId, + user: "@alice:example.org", + content: { ...this.event.getContent(), "m.terminated": "Call ended" }, + skey: this.widget.id, + ts: Date.now(), + }), + ], + { addToState: true }, + ); super.destroy(); } diff --git a/test/test-utils/client.ts b/test/test-utils/client.ts index 7842afbfe5..a2347f9058 100644 --- a/test/test-utils/client.ts +++ b/test/test-utils/client.ts @@ -143,7 +143,6 @@ export const mockClientMethodsCrypto = (): Partial< > => ({ isKeyBackupKeyStored: jest.fn(), getCrossSigningCacheCallbacks: jest.fn().mockReturnValue({ getCrossSigningKeyCache: jest.fn() }), - getKeyBackupVersion: jest.fn().mockResolvedValue(null), secretStorage: { hasKey: jest.fn() }, getCrypto: jest.fn().mockReturnValue({ getUserDeviceInfo: jest.fn(), @@ -163,6 +162,7 @@ export const mockClientMethodsCrypto = (): Partial< getOwnDeviceKeys: jest.fn().mockReturnValue(new Promise(() => {})), getCrossSigningKeyId: jest.fn(), isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(false), + getKeyBackupInfo: jest.fn().mockResolvedValue(null), }), }); diff --git a/test/test-utils/room.ts b/test/test-utils/room.ts index d618932726..bfd1f9004c 100644 --- a/test/test-utils/room.ts +++ b/test/test-utils/room.ts @@ -85,7 +85,7 @@ export function getRoomContext(room: Room, override: Partial<IRoomState>): IRoom canAskToJoin: false, promptAskToJoin: false, viewRoomOpts: { buttons: [] }, - + isRoomEncrypted: false, ...override, }; } diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index 78481c2fd0..f9aee512a3 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -99,7 +99,6 @@ export function createTestClient(): MatrixClient { getDevices: jest.fn().mockResolvedValue({ devices: [{ device_id: "ABCDEFGHI" }] }), getSessionId: jest.fn().mockReturnValue("iaszphgvfku"), credentials: { userId: "@userId:matrix.org" }, - getKeyBackupVersion: jest.fn(), secretStorage: { get: jest.fn(), @@ -117,7 +116,7 @@ export function createTestClient(): MatrixClient { getCrypto: jest.fn().mockReturnValue({ getOwnDeviceKeys: jest.fn(), - getUserDeviceInfo: jest.fn(), + getUserDeviceInfo: jest.fn().mockResolvedValue(new Map()), getUserVerificationStatus: jest.fn(), getDeviceVerificationStatus: jest.fn(), resetKeyBackup: jest.fn(), @@ -135,6 +134,8 @@ export function createTestClient(): MatrixClient { restoreKeyBackupWithPassphrase: jest.fn(), loadSessionBackupPrivateKeyFromSecretStorage: jest.fn(), storeSessionBackupPrivateKey: jest.fn(), + getKeyBackupInfo: jest.fn().mockResolvedValue(null), + getEncryptionInfoForEvent: jest.fn().mockResolvedValue(null), }), getPushActionsForEvent: jest.fn(), diff --git a/test/test-utils/threads.ts b/test/test-utils/threads.ts index d2459653e5..83313b1b8d 100644 --- a/test/test-utils/threads.ts +++ b/test/test-utils/threads.ts @@ -157,6 +157,6 @@ export const populateThread = async ({ // that it is already loaded, and send the events again to the room // so they are added to the thread timeline. ret.thread.initialEventsFetched = true; - await room.addLiveEvents(ret.events); + await room.addLiveEvents(ret.events, { addToState: false }); return ret; }; diff --git a/test/unit-tests/DeviceListener-test.ts b/test/unit-tests/DeviceListener-test.ts index 906826e456..ad7f14e119 100644 --- a/test/unit-tests/DeviceListener-test.ts +++ b/test/unit-tests/DeviceListener-test.ts @@ -96,12 +96,12 @@ describe("DeviceListener", () => { }), getSessionBackupPrivateKey: jest.fn(), isEncryptionEnabledInRoom: jest.fn(), + getKeyBackupInfo: jest.fn().mockResolvedValue(null), } as unknown as Mocked<CryptoApi>; mockClient = getMockClientWithEventEmitter({ isGuest: jest.fn(), getUserId: jest.fn().mockReturnValue(userId), getSafeUserId: jest.fn().mockReturnValue(userId), - getKeyBackupVersion: jest.fn().mockResolvedValue(undefined), getRooms: jest.fn().mockReturnValue([]), isVersionSupported: jest.fn().mockResolvedValue(true), isInitialSyncComplete: jest.fn().mockReturnValue(true), @@ -354,7 +354,7 @@ describe("DeviceListener", () => { it("shows set up encryption toast when user has a key backup available", async () => { // non falsy response - mockClient!.getKeyBackupVersion.mockResolvedValue({} as unknown as KeyBackupInfo); + mockCrypto.getKeyBackupInfo.mockResolvedValue({} as unknown as KeyBackupInfo); await createAndStart(); expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith( @@ -673,7 +673,7 @@ describe("DeviceListener", () => { describe("When Room Key Backup is not enabled", () => { beforeEach(() => { // no backup - mockClient.getKeyBackupVersion.mockResolvedValue(null); + mockCrypto.getKeyBackupInfo.mockResolvedValue(null); }); it("Should report recovery state as Enabled", async () => { @@ -722,7 +722,7 @@ describe("DeviceListener", () => { }); // no backup - mockClient.getKeyBackupVersion.mockResolvedValue(null); + mockCrypto.getKeyBackupInfo.mockResolvedValue(null); await createAndStart(); @@ -872,7 +872,7 @@ describe("DeviceListener", () => { describe("When Room Key Backup is enabled", () => { beforeEach(() => { // backup enabled - just need a mock object - mockClient.getKeyBackupVersion.mockResolvedValue({} as KeyBackupInfo); + mockCrypto.getKeyBackupInfo.mockResolvedValue({} as KeyBackupInfo); }); const testCases = [ diff --git a/test/unit-tests/LegacyCallHandler-test.ts b/test/unit-tests/LegacyCallHandler-test.ts index c3e64dcf94..476d89a1f0 100644 --- a/test/unit-tests/LegacyCallHandler-test.ts +++ b/test/unit-tests/LegacyCallHandler-test.ts @@ -39,10 +39,6 @@ import { Action } from "../../src/dispatcher/actions"; import { getFunctionalMembers } from "../../src/utils/room/getFunctionalMembers"; import SettingsStore from "../../src/settings/SettingsStore"; import { UIFeature } from "../../src/settings/UIFeature"; -import { VoiceBroadcastInfoState, VoiceBroadcastPlayback, VoiceBroadcastRecording } from "../../src/voice-broadcast"; -import { mkVoiceBroadcastInfoStateEvent } from "./voice-broadcast/utils/test-utils"; -import { SdkContextClass } from "../../src/contexts/SDKContext"; -import Modal from "../../src/Modal"; import { createAudioContext } from "../../src/audio/compat"; import * as ManagedHybrid from "../../src/widgets/ManagedHybrid"; @@ -403,53 +399,6 @@ describe("LegacyCallHandler", () => { await callHandler.placeCall(NATIVE_ROOM_ALICE, CallType.Voice); expect(spy).toHaveBeenCalledWith(MatrixClientPeg.safeGet().getRoom(NATIVE_ROOM_ALICE)); }); - - describe("when listening to a voice broadcast", () => { - let voiceBroadcastPlayback: VoiceBroadcastPlayback; - - beforeEach(() => { - voiceBroadcastPlayback = new VoiceBroadcastPlayback( - mkVoiceBroadcastInfoStateEvent( - "!room:example.com", - VoiceBroadcastInfoState.Started, - MatrixClientPeg.safeGet().getSafeUserId(), - "d42", - ), - MatrixClientPeg.safeGet(), - SdkContextClass.instance.voiceBroadcastRecordingsStore, - ); - SdkContextClass.instance.voiceBroadcastPlaybacksStore.setCurrent(voiceBroadcastPlayback); - jest.spyOn(voiceBroadcastPlayback, "pause").mockImplementation(); - }); - - it("and placing a call should pause the broadcast", async () => { - callHandler.placeCall(NATIVE_ROOM_ALICE, CallType.Voice); - await untilCallHandlerEvent(callHandler, LegacyCallHandlerEvent.CallState); - - expect(voiceBroadcastPlayback.pause).toHaveBeenCalled(); - }); - }); - - describe("when recording a voice broadcast", () => { - beforeEach(() => { - SdkContextClass.instance.voiceBroadcastRecordingsStore.setCurrent( - new VoiceBroadcastRecording( - mkVoiceBroadcastInfoStateEvent( - "!room:example.com", - VoiceBroadcastInfoState.Started, - MatrixClientPeg.safeGet().getSafeUserId(), - "d42", - ), - MatrixClientPeg.safeGet(), - ), - ); - }); - - it("and placing a call should show the info dialog", async () => { - callHandler.placeCall(NATIVE_ROOM_ALICE, CallType.Voice); - expect(Modal.createDialog).toMatchSnapshot(); - }); - }); }); describe("LegacyCallHandler without third party protocols", () => { @@ -528,9 +477,6 @@ describe("LegacyCallHandler without third party protocols", () => { audioElement.id = "remoteAudio"; document.body.appendChild(audioElement); - SdkContextClass.instance.voiceBroadcastPlaybacksStore.clearCurrent(); - SdkContextClass.instance.voiceBroadcastRecordingsStore.clearCurrent(); - fetchMock.get( "/media/ring.mp3", { body: new Blob(["1", "2", "3", "4"], { type: "audio/mpeg" }) }, diff --git a/test/unit-tests/Lifecycle-test.ts b/test/unit-tests/Lifecycle-test.ts index 67ac7c7637..04c3459bdf 100644 --- a/test/unit-tests/Lifecycle-test.ts +++ b/test/unit-tests/Lifecycle-test.ts @@ -418,7 +418,7 @@ describe("Lifecycle", () => { undefined, ); - expect(MatrixClientPeg.start).toHaveBeenCalledWith({ rustCryptoStoreKey: expect.any(Buffer) }); + expect(MatrixClientPeg.start).toHaveBeenCalledWith({ rustCryptoStoreKey: expect.any(Uint8Array) }); }); describe("with a refresh token", () => { diff --git a/test/unit-tests/MatrixClientPeg-test.ts b/test/unit-tests/MatrixClientPeg-test.ts index 5a19b568c0..4653340574 100644 --- a/test/unit-tests/MatrixClientPeg-test.ts +++ b/test/unit-tests/MatrixClientPeg-test.ts @@ -11,8 +11,6 @@ import fetchMockJest from "fetch-mock-jest"; import { advanceDateAndTime, stubClient } from "../test-utils"; import { IMatrixClientPeg, MatrixClientPeg as peg } from "../../src/MatrixClientPeg"; -import SettingsStore from "../../src/settings/SettingsStore"; -import { SettingLevel } from "../../src/settings/SettingLevel"; jest.useFakeTimers(); @@ -81,27 +79,18 @@ describe("MatrixClientPeg", () => { }); it("should initialise the rust crypto library by default", async () => { - const mockSetValue = jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined); - const mockInitRustCrypto = jest.spyOn(testPeg.safeGet(), "initRustCrypto").mockResolvedValue(undefined); const cryptoStoreKey = new Uint8Array([1, 2, 3, 4]); await testPeg.start({ rustCryptoStoreKey: cryptoStoreKey }); expect(mockInitRustCrypto).toHaveBeenCalledWith({ storageKey: cryptoStoreKey }); - - // we should have stashed the setting in the settings store - expect(mockSetValue).toHaveBeenCalledWith("feature_rust_crypto", null, SettingLevel.DEVICE, true); }); it("Should migrate existing login", async () => { - const mockSetValue = jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined); const mockInitRustCrypto = jest.spyOn(testPeg.safeGet(), "initRustCrypto").mockResolvedValue(undefined); await testPeg.start(); expect(mockInitRustCrypto).toHaveBeenCalledTimes(1); - - // we should have stashed the setting in the settings store - expect(mockSetValue).toHaveBeenCalledWith("feature_rust_crypto", null, SettingLevel.DEVICE, true); }); }); }); diff --git a/test/unit-tests/Notifier-test.ts b/test/unit-tests/Notifier-test.ts index 2fe7fdec0b..f94f50724d 100644 --- a/test/unit-tests/Notifier-test.ts +++ b/test/unit-tests/Notifier-test.ts @@ -43,8 +43,6 @@ import { mkThread } from "../test-utils/threads"; import dis from "../../src/dispatcher/dispatcher"; import { ThreadPayload } from "../../src/dispatcher/payloads/ThreadPayload"; import { Action } from "../../src/dispatcher/actions"; -import { VoiceBroadcastChunkEventType, VoiceBroadcastInfoState } from "../../src/voice-broadcast"; -import { mkVoiceBroadcastInfoStateEvent } from "./voice-broadcast/utils/test-utils"; import { addReplyToMessageContent } from "../../src/utils/Reply"; jest.mock("../../src/utils/notifications", () => ({ @@ -85,16 +83,13 @@ describe("Notifier", () => { }); }; - const mkAudioEvent = (broadcastChunkContent?: object): MatrixEvent => { - const chunkContent = broadcastChunkContent ? { [VoiceBroadcastChunkEventType]: broadcastChunkContent } : {}; - + const mkAudioEvent = (): MatrixEvent => { return mkEvent({ event: true, type: EventType.RoomMessage, user: "@user:example.com", room: "!room:example.com", content: { - ...chunkContent, msgtype: MsgType.Audio, body: "test audio message", }, @@ -320,24 +315,6 @@ describe("Notifier", () => { ); }); - it("should display the expected notification for a broadcast chunk with sequence = 1", () => { - const audioEvent = mkAudioEvent({ sequence: 1 }); - Notifier.displayPopupNotification(audioEvent, testRoom); - expect(MockPlatform.displayNotification).toHaveBeenCalledWith( - "@user:example.com (!room1:server)", - "@user:example.com started a voice broadcast", - "data:image/png;base64,00", - testRoom, - audioEvent, - ); - }); - - it("should display the expected notification for a broadcast chunk with sequence = 2", () => { - const audioEvent = mkAudioEvent({ sequence: 2 }); - Notifier.displayPopupNotification(audioEvent, testRoom); - expect(MockPlatform.displayNotification).not.toHaveBeenCalled(); - }); - it("should strip reply fallback", () => { const event = mkMessage({ msg: "Test", @@ -581,24 +558,6 @@ describe("Notifier", () => { Notifier.evaluateEvent(mkAudioEvent()); expect(Notifier.displayPopupNotification).toHaveBeenCalledTimes(1); }); - - it("should not show a notification for broadcast info events in any case", () => { - // Let client decide to show a notification - mockClient.getPushActionsForEvent.mockReturnValue({ - notify: true, - tweaks: {}, - }); - - const broadcastStartedEvent = mkVoiceBroadcastInfoStateEvent( - "!other:example.org", - VoiceBroadcastInfoState.Started, - "@user:example.com", - "ABC123", - ); - - Notifier.evaluateEvent(broadcastStartedEvent); - expect(Notifier.displayPopupNotification).not.toHaveBeenCalled(); - }); }); describe("setPromptHidden", () => { @@ -624,8 +583,7 @@ describe("Notifier", () => { content: { body: "this is a thread root" }, }), testRoom.threadsTimelineSets[0]!.getLiveTimeline(), - false, - false, + { toStartOfTimeline: false, fromCache: false, addToState: true }, ); expect(fn).not.toHaveBeenCalled(); diff --git a/test/unit-tests/RoomNotifs-test.ts b/test/unit-tests/RoomNotifs-test.ts index 51416ab7fd..65089eba94 100644 --- a/test/unit-tests/RoomNotifs-test.ts +++ b/test/unit-tests/RoomNotifs-test.ts @@ -147,7 +147,7 @@ describe("RoomNotifs test", () => { const itShouldCountPredecessorHighlightWhenThereIsAPredecessorInTheCreateEvent = (): void => { it("and there is a predecessor in the create event, it should count predecessor highlight", () => { - room.addLiveEvents([mkCreateEvent(OLD_ROOM_ID)]); + room.addLiveEvents([mkCreateEvent(OLD_ROOM_ID)], { addToState: true }); expect(getUnreadNotificationCount(room, NotificationCountType.Total, false)).toBe(8); expect(getUnreadNotificationCount(room, NotificationCountType.Highlight, false)).toBe(7); @@ -157,7 +157,7 @@ describe("RoomNotifs test", () => { const itShouldCountPredecessorHighlightWhenThereIsAPredecessorEvent = (): void => { it("and there is a predecessor event, it should count predecessor highlight", () => { client.getVisibleRooms(); - room.addLiveEvents([mkCreateEvent(OLD_ROOM_ID)]); + room.addLiveEvents([mkCreateEvent(OLD_ROOM_ID)], { addToState: true }); upsertRoomStateEvents(room, [mkPredecessorEvent(OLD_ROOM_ID)]); expect(getUnreadNotificationCount(room, NotificationCountType.Total, false)).toBe(8); @@ -185,7 +185,7 @@ describe("RoomNotifs test", () => { itShouldCountPredecessorHighlightWhenThereIsAPredecessorEvent(); it("and there is only a predecessor event, it should not count predecessor highlight", () => { - room.addLiveEvents([mkCreateEvent()]); + room.addLiveEvents([mkCreateEvent()], { addToState: true }); upsertRoomStateEvents(room, [mkPredecessorEvent(OLD_ROOM_ID)]); expect(getUnreadNotificationCount(room, NotificationCountType.Total, false)).toBe(2); @@ -204,7 +204,7 @@ describe("RoomNotifs test", () => { itShouldCountPredecessorHighlightWhenThereIsAPredecessorEvent(); it("and there is only a predecessor event, it should count predecessor highlight", () => { - room.addLiveEvents([mkCreateEvent()]); + room.addLiveEvents([mkCreateEvent()], { addToState: true }); upsertRoomStateEvents(room, [mkPredecessorEvent(OLD_ROOM_ID)]); expect(getUnreadNotificationCount(room, NotificationCountType.Total, false)).toBe(8); @@ -212,7 +212,7 @@ describe("RoomNotifs test", () => { }); it("and there is an unknown room in the predecessor event, it should not count predecessor highlight", () => { - room.addLiveEvents([mkCreateEvent()]); + room.addLiveEvents([mkCreateEvent()], { addToState: true }); upsertRoomStateEvents(room, [mkPredecessorEvent("!unknon:example.com")]); expect(getUnreadNotificationCount(room, NotificationCountType.Total, false)).toBe(2); diff --git a/test/unit-tests/SdkConfig-test.ts b/test/unit-tests/SdkConfig-test.ts index 4204a698fa..19d0eec9c3 100644 --- a/test/unit-tests/SdkConfig-test.ts +++ b/test/unit-tests/SdkConfig-test.ts @@ -18,10 +18,6 @@ describe("SdkConfig", () => { describe("with custom values", () => { beforeEach(() => { SdkConfig.put({ - voice_broadcast: { - chunk_length: 42, - max_length: 1337, - }, feedback: { existing_issues_url: "https://existing", } as any, @@ -30,8 +26,6 @@ describe("SdkConfig", () => { it("should return the custom config", () => { const customConfig = JSON.parse(JSON.stringify(DEFAULTS)); - customConfig.voice_broadcast.chunk_length = 42; - customConfig.voice_broadcast.max_length = 1337; customConfig.feedback.existing_issues_url = "https://existing"; expect(SdkConfig.get()).toEqual(customConfig); }); diff --git a/test/unit-tests/SupportedBrowser-test.ts b/test/unit-tests/SupportedBrowser-test.ts index ccf75e0dab..a116ab1f9f 100644 --- a/test/unit-tests/SupportedBrowser-test.ts +++ b/test/unit-tests/SupportedBrowser-test.ts @@ -66,10 +66,10 @@ describe("SupportedBrowser", () => { "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15", // Firefox 131 on macOS Sonoma "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:131.0) Gecko/20100101 Firefox/131.0", - // Edge 129 on Windows - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/129.0.2792.79", - // Edge 129 on macOS - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/129.0.2792.79", + // Edge 131 on Windows + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.2903.70", + // Edge 131 on macOS + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.2903.70", // Firefox 131 on Windows "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0", // Firefox 131 on Linux diff --git a/test/unit-tests/TestSdkContext.ts b/test/unit-tests/TestSdkContext.ts index d4bda4f889..f60b083bef 100644 --- a/test/unit-tests/TestSdkContext.ts +++ b/test/unit-tests/TestSdkContext.ts @@ -16,29 +16,21 @@ import { SpaceStoreClass } from "../../src/stores/spaces/SpaceStore"; import { WidgetLayoutStore } from "../../src/stores/widgets/WidgetLayoutStore"; import { WidgetPermissionStore } from "../../src/stores/widgets/WidgetPermissionStore"; import WidgetStore from "../../src/stores/WidgetStore"; -import { - VoiceBroadcastPlaybacksStore, - VoiceBroadcastPreRecordingStore, - VoiceBroadcastRecordingsStore, -} from "../../src/voice-broadcast"; /** * A class which provides the same API as SdkContextClass but adds additional unsafe setters which can * replace individual stores. This is useful for tests which need to mock out stores. */ export class TestSdkContext extends SdkContextClass { - public declare _RightPanelStore?: RightPanelStore; - public declare _RoomNotificationStateStore?: RoomNotificationStateStore; - public declare _RoomViewStore?: RoomViewStore; - public declare _WidgetPermissionStore?: WidgetPermissionStore; - public declare _WidgetLayoutStore?: WidgetLayoutStore; - public declare _WidgetStore?: WidgetStore; - public declare _PosthogAnalytics?: PosthogAnalytics; - public declare _SlidingSyncManager?: SlidingSyncManager; - public declare _SpaceStore?: SpaceStoreClass; - public declare _VoiceBroadcastRecordingsStore?: VoiceBroadcastRecordingsStore; - public declare _VoiceBroadcastPreRecordingStore?: VoiceBroadcastPreRecordingStore; - public declare _VoiceBroadcastPlaybacksStore?: VoiceBroadcastPlaybacksStore; + declare public _RightPanelStore?: RightPanelStore; + declare public _RoomNotificationStateStore?: RoomNotificationStateStore; + declare public _RoomViewStore?: RoomViewStore; + declare public _WidgetPermissionStore?: WidgetPermissionStore; + declare public _WidgetLayoutStore?: WidgetLayoutStore; + declare public _WidgetStore?: WidgetStore; + declare public _PosthogAnalytics?: PosthogAnalytics; + declare public _SlidingSyncManager?: SlidingSyncManager; + declare public _SpaceStore?: SpaceStoreClass; constructor() { super(); diff --git a/test/unit-tests/Unread-test.ts b/test/unit-tests/Unread-test.ts index 8719da06ef..15d3dab8f5 100644 --- a/test/unit-tests/Unread-test.ts +++ b/test/unit-tests/Unread-test.ts @@ -138,7 +138,7 @@ describe("Unread", () => { room: roomId, content: {}, }); - room.addLiveEvents([event]); + room.addLiveEvents([event], { addToState: true }); // Don't care about the code path of hidden events. mocked(haveRendererForEvent).mockClear().mockReturnValue(true); @@ -157,7 +157,7 @@ describe("Unread", () => { content: {}, }); // Only for timeline events. - room.addLiveEvents([event]); + room.addLiveEvents([event], { addToState: true }); expect(doesRoomHaveUnreadMessages(room, false)).toBe(false); }); @@ -201,7 +201,7 @@ describe("Unread", () => { content: {}, }); // Only for timeline events. - room.addLiveEvents([event2]); + room.addLiveEvents([event2], { addToState: true }); expect(doesRoomHaveUnreadMessages(room, false)).toBe(true); }); @@ -403,7 +403,7 @@ describe("Unread", () => { redactedEvent.makeRedacted(redactedEvent, room); console.log("Event Id", redactedEvent.getId()); // Only for timeline events. - room.addLiveEvents([redactedEvent]); + room.addLiveEvents([redactedEvent], { addToState: true }); expect(doesRoomHaveUnreadMessages(room, true)).toBe(true); expect(logger.warn).toHaveBeenCalledWith( @@ -448,7 +448,7 @@ describe("Unread", () => { room: roomId, content: {}, }); - room.addLiveEvents([event]); + room.addLiveEvents([event], { addToState: true }); }); it("an unthreaded receipt for the event makes the room read", () => { @@ -502,7 +502,7 @@ describe("Unread", () => { ts: 100, currentUserId: myId, }); - room.addLiveEvents(events); + room.addLiveEvents(events, { addToState: true }); threadEvent = events[1]; }); @@ -555,7 +555,7 @@ describe("Unread", () => { room: roomId, content: {}, }); - room.addLiveEvents([event]); + room.addLiveEvents([event], { addToState: true }); // It still returns false expect(doesRoomHaveUnreadThreads(room)).toBe(false); diff --git a/test/unit-tests/__snapshots__/LegacyCallHandler-test.ts.snap b/test/unit-tests/__snapshots__/LegacyCallHandler-test.ts.snap deleted file mode 100644 index aaf4d78758..0000000000 --- a/test/unit-tests/__snapshots__/LegacyCallHandler-test.ts.snap +++ /dev/null @@ -1,24 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LegacyCallHandler when recording a voice broadcast and placing a call should show the info dialog 1`] = ` -[MockFunction] { - "calls": [ - [ - [Function], - { - "description": <p> - You can’t start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call. - </p>, - "hasCloseButton": true, - "title": "Can’t start a call", - }, - ], - ], - "results": [ - { - "type": "return", - "value": undefined, - }, - ], -} -`; diff --git a/test/unit-tests/components/structures/FilePanel-test.tsx b/test/unit-tests/components/structures/FilePanel-test.tsx index 1dce220682..25bdd99676 100644 --- a/test/unit-tests/components/structures/FilePanel-test.tsx +++ b/test/unit-tests/components/structures/FilePanel-test.tsx @@ -7,13 +7,13 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; -import { EventTimelineSet, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix"; +import { EventTimelineSet, PendingEventOrdering, Room, RoomEvent } from "matrix-js-sdk/src/matrix"; import { screen, render, waitFor } from "jest-matrix-react"; import { mocked } from "jest-mock"; import FilePanel from "../../../../src/components/structures/FilePanel"; import ResizeNotifier from "../../../../src/utils/ResizeNotifier"; -import { stubClient } from "../../../test-utils"; +import { mkEvent, stubClient } from "../../../test-utils"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; jest.mock("matrix-js-sdk/src/matrix", () => ({ @@ -47,4 +47,43 @@ describe("FilePanel", () => { }); expect(asFragment()).toMatchSnapshot(); }); + + describe("addEncryptedLiveEvent", () => { + it("should add file msgtype event to filtered timelineSet", async () => { + const cli = MatrixClientPeg.safeGet(); + const room = new Room("!room:server", cli, cli.getSafeUserId(), { + pendingEventOrdering: PendingEventOrdering.Detached, + }); + cli.reEmitter.reEmit(room, [RoomEvent.Timeline]); + const timelineSet = new EventTimelineSet(room); + room.getOrCreateFilteredTimelineSet = jest.fn().mockReturnValue(timelineSet); + mocked(cli.getRoom).mockReturnValue(room); + + let filePanel: FilePanel | null; + render( + <FilePanel + roomId={room.roomId} + onClose={jest.fn()} + resizeNotifier={new ResizeNotifier()} + ref={(ref) => (filePanel = ref)} + />, + ); + await screen.findByText("No files visible in this room"); + + const event = mkEvent({ + type: "m.room.message", + user: cli.getSafeUserId(), + room: room.roomId, + content: { + body: "hello", + url: "mxc://matrix.org/1234", + msgtype: "m.file", + }, + event: true, + }); + filePanel!.addEncryptedLiveEvent(event); + + expect(timelineSet.getLiveTimeline().getEvents()).toContain(event); + }); + }); }); diff --git a/test/unit-tests/components/structures/MatrixChat-test.tsx b/test/unit-tests/components/structures/MatrixChat-test.tsx index 5cee50ef29..fd17ccf583 100644 --- a/test/unit-tests/components/structures/MatrixChat-test.tsx +++ b/test/unit-tests/components/structures/MatrixChat-test.tsx @@ -44,7 +44,6 @@ import { } from "../../../test-utils"; import * as leaveRoomUtils from "../../../../src/utils/leave-behaviour"; import { OidcClientError } from "../../../../src/utils/oidc/error"; -import * as voiceBroadcastUtils from "../../../../src/voice-broadcast/utils/cleanUpBroadcasts"; import LegacyCallHandler from "../../../../src/LegacyCallHandler"; import { CallStore } from "../../../../src/stores/CallStore"; import { Call } from "../../../../src/models/Call"; @@ -139,6 +138,7 @@ describe("<MatrixChat />", () => { globalBlacklistUnverifiedDevices: false, // This needs to not finish immediately because we need to test the screen appears bootstrapCrossSigning: jest.fn().mockImplementation(() => bootstrapDeferred.promise), + getKeyBackupInfo: jest.fn().mockResolvedValue(null), }), secretStorage: { isStored: jest.fn().mockReturnValue(null), @@ -148,7 +148,6 @@ describe("<MatrixChat />", () => { whoami: jest.fn(), logout: jest.fn(), getDeviceId: jest.fn(), - getKeyBackupVersion: jest.fn().mockResolvedValue(null), }); let mockClient: Mocked<MatrixClient>; const serverConfig = { @@ -811,7 +810,6 @@ describe("<MatrixChat />", () => { jest.spyOn(LegacyCallHandler.instance, "hangupAllCalls") .mockClear() .mockImplementation(() => {}); - jest.spyOn(voiceBroadcastUtils, "cleanUpBroadcasts").mockImplementation(async () => {}); jest.spyOn(PosthogAnalytics.instance, "logout").mockImplementation(() => {}); jest.spyOn(EventIndexPeg, "deleteEventIndex").mockImplementation(async () => {}); @@ -831,22 +829,12 @@ describe("<MatrixChat />", () => { jest.spyOn(logger, "warn").mockClear(); }); - afterAll(() => { - jest.spyOn(voiceBroadcastUtils, "cleanUpBroadcasts").mockRestore(); - }); - it("should hangup all legacy calls", async () => { await getComponentAndWaitForReady(); await dispatchLogoutAndWait(); expect(LegacyCallHandler.instance.hangupAllCalls).toHaveBeenCalled(); }); - it("should cleanup broadcasts", async () => { - await getComponentAndWaitForReady(); - await dispatchLogoutAndWait(); - expect(voiceBroadcastUtils.cleanUpBroadcasts).toHaveBeenCalled(); - }); - it("should disconnect all calls", async () => { await getComponentAndWaitForReady(); await dispatchLogoutAndWait(); diff --git a/test/unit-tests/components/structures/MessagePanel-test.tsx b/test/unit-tests/components/structures/MessagePanel-test.tsx index cf44716ba9..dbb83da312 100644 --- a/test/unit-tests/components/structures/MessagePanel-test.tsx +++ b/test/unit-tests/components/structures/MessagePanel-test.tsx @@ -30,6 +30,7 @@ import { import ResizeNotifier from "../../../../src/utils/ResizeNotifier"; import { IRoomState } from "../../../../src/components/structures/RoomView"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; +import { ScopedRoomContextProvider } from "../../../../src/contexts/ScopedRoomContext.tsx"; jest.mock("../../../../src/utils/beacon", () => ({ useBeacon: jest.fn(), @@ -91,9 +92,9 @@ describe("MessagePanel", function () { const getComponent = (props = {}, roomContext: Partial<IRoomState> = {}) => ( <MatrixClientContext.Provider value={client}> - <RoomContext.Provider value={{ ...defaultRoomContext, ...roomContext }}> + <ScopedRoomContextProvider {...defaultRoomContext} {...roomContext}> <MessagePanel {...defaultProps} {...props} /> - </RoomContext.Provider> + </ScopedRoomContextProvider> </MatrixClientContext.Provider> ); diff --git a/test/unit-tests/components/structures/PipContainer-test.tsx b/test/unit-tests/components/structures/PipContainer-test.tsx index 446727c74e..f573b0a0cd 100644 --- a/test/unit-tests/components/structures/PipContainer-test.tsx +++ b/test/unit-tests/components/structures/PipContainer-test.tsx @@ -10,7 +10,7 @@ import React from "react"; import { mocked, Mocked } from "jest-mock"; import { screen, render, act, cleanup } from "jest-matrix-react"; import userEvent from "@testing-library/user-event"; -import { MatrixClient, PendingEventOrdering, Room, MatrixEvent, RoomStateEvent } from "matrix-js-sdk/src/matrix"; +import { MatrixClient, PendingEventOrdering, Room, RoomStateEvent } from "matrix-js-sdk/src/matrix"; import { Widget, ClientWidgetApi } from "matrix-widget-api"; import { UserEvent } from "@testing-library/user-event/dist/types/setup/setup"; @@ -26,7 +26,6 @@ import { wrapInSdkContext, mkRoomCreateEvent, mockPlatformPeg, - flushPromises, useMockMediaDevices, } from "../../../test-utils"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; @@ -39,17 +38,7 @@ import defaultDispatcher from "../../../../src/dispatcher/dispatcher"; import { Action } from "../../../../src/dispatcher/actions"; import { ViewRoomPayload } from "../../../../src/dispatcher/payloads/ViewRoomPayload"; import { TestSdkContext } from "../../TestSdkContext"; -import { - VoiceBroadcastInfoState, - VoiceBroadcastPlaybacksStore, - VoiceBroadcastPreRecording, - VoiceBroadcastPreRecordingStore, - VoiceBroadcastRecording, - VoiceBroadcastRecordingsStore, -} from "../../../../src/voice-broadcast"; -import { mkVoiceBroadcastInfoStateEvent } from "../../voice-broadcast/utils/test-utils"; import { RoomViewStore } from "../../../../src/stores/RoomViewStore"; -import { IRoomStateEventsActionPayload } from "../../../../src/actions/MatrixActionCreators"; import { Container, WidgetLayoutStore } from "../../../../src/stores/widgets/WidgetLayoutStore"; import WidgetStore from "../../../../src/stores/WidgetStore"; import { WidgetType } from "../../../../src/widgets/WidgetType"; @@ -76,13 +65,6 @@ describe("PipContainer", () => { let room: Room; let room2: Room; let alice: RoomMember; - let voiceBroadcastRecordingsStore: VoiceBroadcastRecordingsStore; - let voiceBroadcastPreRecordingStore: VoiceBroadcastPreRecordingStore; - let voiceBroadcastPlaybacksStore: VoiceBroadcastPlaybacksStore; - - const actFlushPromises = async () => { - await flushPromises(); - }; beforeEach(async () => { useMockMediaDevices(); @@ -125,13 +107,7 @@ describe("PipContainer", () => { sdkContext = new TestSdkContext(); // @ts-ignore PipContainer uses SDKContext in the constructor SdkContextClass.instance = sdkContext; - voiceBroadcastRecordingsStore = new VoiceBroadcastRecordingsStore(); - voiceBroadcastPreRecordingStore = new VoiceBroadcastPreRecordingStore(); - voiceBroadcastPlaybacksStore = new VoiceBroadcastPlaybacksStore(voiceBroadcastRecordingsStore); sdkContext.client = client; - sdkContext._VoiceBroadcastRecordingsStore = voiceBroadcastRecordingsStore; - sdkContext._VoiceBroadcastPreRecordingStore = voiceBroadcastPreRecordingStore; - sdkContext._VoiceBroadcastPlaybacksStore = voiceBroadcastPlaybacksStore; }); afterEach(async () => { @@ -190,51 +166,10 @@ describe("PipContainer", () => { ActiveWidgetStore.instance.destroyPersistentWidget("1", room.roomId); }; - const makeVoiceBroadcastInfoStateEvent = (): MatrixEvent => { - return mkVoiceBroadcastInfoStateEvent( - room.roomId, - VoiceBroadcastInfoState.Started, - alice.userId, - client.getDeviceId() || "", - ); - }; - - const setUpVoiceBroadcastRecording = () => { - const infoEvent = makeVoiceBroadcastInfoStateEvent(); - const voiceBroadcastRecording = new VoiceBroadcastRecording(infoEvent, client); - voiceBroadcastRecordingsStore.setCurrent(voiceBroadcastRecording); - }; - - const setUpVoiceBroadcastPreRecording = () => { - const voiceBroadcastPreRecording = new VoiceBroadcastPreRecording( - room, - alice, - client, - voiceBroadcastPlaybacksStore, - voiceBroadcastRecordingsStore, - ); - voiceBroadcastPreRecordingStore.setCurrent(voiceBroadcastPreRecording); - }; - const setUpRoomViewStore = () => { sdkContext._RoomViewStore = new RoomViewStore(defaultDispatcher, sdkContext); }; - const mkVoiceBroadcast = (room: Room): MatrixEvent => { - const infoEvent = makeVoiceBroadcastInfoStateEvent(); - room.currentState.setStateEvents([infoEvent]); - defaultDispatcher.dispatch<IRoomStateEventsActionPayload>( - { - action: "MatrixActions.RoomState.events", - event: infoEvent, - state: room.currentState, - lastStateEvent: null, - }, - true, - ); - return infoEvent; - }; - it("hides if there's no content", () => { renderPip(); expect(screen.queryByRole("complementary")).toBeNull(); @@ -339,138 +274,4 @@ describe("PipContainer", () => { WidgetStore.instance.removeVirtualWidget("1", room.roomId); }); - - describe("when there is a voice broadcast recording and pre-recording", () => { - beforeEach(async () => { - setUpVoiceBroadcastPreRecording(); - setUpVoiceBroadcastRecording(); - renderPip(); - await actFlushPromises(); - }); - - it("should render the voice broadcast recording PiP", () => { - // check for the „Live“ badge to be present - expect(screen.queryByText("Live")).toBeInTheDocument(); - }); - - it("and a call it should show both, the call and the recording", async () => { - await withCall(async () => { - // Broadcast: Check for the „Live“ badge to be present - expect(screen.queryByText("Live")).toBeInTheDocument(); - // Call: Check for the „Leave“ button to be present - screen.getByRole("button", { name: "Leave" }); - }); - }); - }); - - describe("when there is a voice broadcast playback and pre-recording", () => { - beforeEach(async () => { - mkVoiceBroadcast(room); - setUpVoiceBroadcastPreRecording(); - renderPip(); - await actFlushPromises(); - }); - - it("should render the voice broadcast pre-recording PiP", () => { - // check for the „Go live“ button - expect(screen.queryByText("Go live")).toBeInTheDocument(); - }); - }); - - describe("when there is a voice broadcast pre-recording", () => { - beforeEach(async () => { - setUpVoiceBroadcastPreRecording(); - renderPip(); - await actFlushPromises(); - }); - - it("should render the voice broadcast pre-recording PiP", () => { - // check for the „Go live“ button - expect(screen.queryByText("Go live")).toBeInTheDocument(); - }); - }); - - describe("when listening to a voice broadcast in a room and then switching to another room", () => { - beforeEach(async () => { - setUpRoomViewStore(); - viewRoom(room.roomId); - mkVoiceBroadcast(room); - await actFlushPromises(); - - expect(voiceBroadcastPlaybacksStore.getCurrent()).toBeTruthy(); - - await voiceBroadcastPlaybacksStore.getCurrent()?.start(); - viewRoom(room2.roomId); - renderPip(); - }); - - it("should render the small voice broadcast playback PiP", () => { - // check for the „pause voice broadcast“ button - expect(screen.getByLabelText("pause voice broadcast")).toBeInTheDocument(); - // check for the absence of the „30s forward“ button - expect(screen.queryByLabelText("30s forward")).not.toBeInTheDocument(); - }); - }); - - describe("when viewing a room with a live voice broadcast", () => { - let startEvent!: MatrixEvent; - - beforeEach(async () => { - setUpRoomViewStore(); - viewRoom(room.roomId); - startEvent = mkVoiceBroadcast(room); - renderPip(); - await actFlushPromises(); - }); - - it("should render the voice broadcast playback pip", () => { - // check for the „resume voice broadcast“ button - expect(screen.queryByLabelText("play voice broadcast")).toBeInTheDocument(); - }); - - describe("and the broadcast stops", () => { - beforeEach(async () => { - const stopEvent = mkVoiceBroadcastInfoStateEvent( - room.roomId, - VoiceBroadcastInfoState.Stopped, - alice.userId, - client.getDeviceId() || "", - startEvent, - ); - - await act(async () => { - room.currentState.setStateEvents([stopEvent]); - defaultDispatcher.dispatch<IRoomStateEventsActionPayload>( - { - action: "MatrixActions.RoomState.events", - event: stopEvent, - state: room.currentState, - lastStateEvent: stopEvent, - }, - true, - ); - await flushPromises(); - }); - }); - - it("should not render the voice broadcast playback pip", () => { - // check for the „resume voice broadcast“ button - expect(screen.queryByLabelText("play voice broadcast")).not.toBeInTheDocument(); - }); - }); - - describe("and leaving the room", () => { - beforeEach(async () => { - await act(async () => { - viewRoom(room2.roomId); - await flushPromises(); - }); - }); - - it("should not render the voice broadcast playback pip", () => { - // check for the „resume voice broadcast“ button - expect(screen.queryByLabelText("play voice broadcast")).not.toBeInTheDocument(); - }); - }); - }); }); diff --git a/test/unit-tests/components/structures/RightPanel-test.tsx b/test/unit-tests/components/structures/RightPanel-test.tsx index e569369db5..ad29791ee9 100644 --- a/test/unit-tests/components/structures/RightPanel-test.tsx +++ b/test/unit-tests/components/structures/RightPanel-test.tsx @@ -91,7 +91,7 @@ describe("RightPanel", () => { if (name !== "RightPanel.phases") return realGetValue(name, roomId); if (roomId === "r1") { return { - history: [{ phase: RightPanelPhases.RoomMemberList }], + history: [{ phase: RightPanelPhases.MemberList }], isOpen: true, }; } @@ -123,7 +123,7 @@ describe("RightPanel", () => { await rpsUpdated; await waitFor(() => expect(screen.queryByTestId("spinner")).not.toBeInTheDocument()); - // room one will be in the RoomMemberList phase - confirm this is rendered + // room one will be in the MemberList phase - confirm this is rendered expect(container.getElementsByClassName("mx_MemberList")).toHaveLength(1); // wait for RPS room 2 updates to fire, then rerender diff --git a/test/unit-tests/components/structures/RoomView-test.tsx b/test/unit-tests/components/structures/RoomView-test.tsx index b6fbd2e850..385204c01b 100644 --- a/test/unit-tests/components/structures/RoomView-test.tsx +++ b/test/unit-tests/components/structures/RoomView-test.tsx @@ -10,18 +10,19 @@ import React, { createRef, RefObject } from "react"; import { mocked, MockedObject } from "jest-mock"; import { ClientEvent, + EventTimeline, + EventType, + IEvent, + JoinRule, MatrixClient, + MatrixError, + MatrixEvent, Room, RoomEvent, - EventType, - JoinRule, - MatrixError, RoomStateEvent, - MatrixEvent, SearchResult, - IEvent, } from "matrix-js-sdk/src/matrix"; -import { CryptoApi, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api"; +import { CryptoApi, UserVerificationStatus, CryptoEvent } from "matrix-js-sdk/src/crypto-api"; import { KnownMembership } from "matrix-js-sdk/src/types"; import { fireEvent, @@ -34,6 +35,7 @@ import { cleanup, } from "jest-matrix-react"; import userEvent from "@testing-library/user-event"; +import { defer } from "matrix-js-sdk/src/utils"; import { stubClient, @@ -73,6 +75,7 @@ import { ViewRoomErrorPayload } from "../../../../src/dispatcher/payloads/ViewRo import { SearchScope } from "../../../../src/Searching"; import { MEGOLM_ENCRYPTION_ALGORITHM } from "../../../../src/utils/crypto"; import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; +import { ViewUserPayload } from "../../../../src/dispatcher/payloads/ViewUserPayload.ts"; describe("RoomView", () => { let cli: MockedObject<MatrixClient>; @@ -87,8 +90,7 @@ describe("RoomView", () => { beforeEach(() => { mockPlatformPeg({ reload: () => {} }); - stubClient(); - cli = mocked(MatrixClientPeg.safeGet()); + cli = mocked(stubClient()); room = new Room(`!${roomCount++}:example.org`, cli, "@alice:example.org"); jest.spyOn(room, "findPredecessor"); @@ -201,6 +203,21 @@ describe("RoomView", () => { return ref.current!; }; + it("should show member list right panel phase on Action.ViewUser without `payload.member`", async () => { + const spy = jest.spyOn(stores.rightPanelStore, "showOrHidePhase"); + await renderRoomView(false); + + defaultDispatcher.dispatch<ViewUserPayload>( + { + action: Action.ViewUser, + member: undefined, + }, + true, + ); + + expect(spy).toHaveBeenCalledWith(RightPanelPhases.MemberList); + }); + it("when there is no room predecessor, getHiddenHighlightCount should return 0", async () => { const instance = await getRoomViewInstance(); expect(instance.getHiddenHighlightCount()).toBe(0); @@ -247,8 +264,9 @@ describe("RoomView", () => { it("updates url preview visibility on encryption state change", async () => { room.getMyMembership = jest.fn().mockReturnValue(KnownMembership.Join); + jest.spyOn(cli, "getCrypto").mockReturnValue(crypto); // we should be starting unencrypted - expect(cli.isRoomEncrypted(room.roomId)).toEqual(false); + expect(await cli.getCrypto()?.isEncryptionEnabledInRoom(room.roomId)).toEqual(false); const roomViewInstance = await getRoomViewInstance(); @@ -263,23 +281,38 @@ describe("RoomView", () => { expect(roomViewInstance.state.showUrlPreview).toBe(true); // now enable encryption - cli.isRoomEncrypted.mockReturnValue(true); + jest.spyOn(cli.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true); // and fake an encryption event into the room to prompt it to re-check - await act(() => - room.addLiveEvents([ - new MatrixEvent({ - type: "m.room.encryption", - sender: cli.getUserId()!, - content: {}, - event_id: "someid", - room_id: room.roomId, - }), - ]), - ); + act(() => { + const encryptionEvent = new MatrixEvent({ + type: EventType.RoomEncryption, + sender: cli.getUserId()!, + content: {}, + event_id: "someid", + room_id: room.roomId, + }); + const roomState = room.getLiveTimeline().getState(EventTimeline.FORWARDS)!; + cli.emit(RoomStateEvent.Events, encryptionEvent, roomState, null); + }); // URL previews should now be disabled - expect(roomViewInstance.state.showUrlPreview).toBe(false); + await waitFor(() => expect(roomViewInstance.state.showUrlPreview).toBe(false)); + }); + + it("should not display the timeline when the room encryption is loading", async () => { + jest.spyOn(room, "getMyMembership").mockReturnValue(KnownMembership.Join); + jest.spyOn(cli, "getCrypto").mockReturnValue(crypto); + const deferred = defer<boolean>(); + jest.spyOn(cli.getCrypto()!, "isEncryptionEnabledInRoom").mockImplementation(() => deferred.promise); + + const { asFragment, container } = await mountRoomView(); + expect(container.querySelector(".mx_RoomView_messagePanel")).toBeNull(); + expect(asFragment()).toMatchSnapshot(); + + deferred.resolve(true); + await waitFor(() => expect(container.querySelector(".mx_RoomView_messagePanel")).not.toBeNull()); + expect(asFragment()).toMatchSnapshot(); }); it("updates live timeline when a timeline reset happens", async () => { @@ -290,6 +323,32 @@ describe("RoomView", () => { expect(roomViewInstance.state.liveTimeline).not.toEqual(oldTimeline); }); + it("should update when the e2e status when the user verification changed", async () => { + room.currentState.setStateEvents([ + mkRoomMemberJoinEvent(cli.getSafeUserId(), room.roomId), + mkRoomMemberJoinEvent("user@example.com", room.roomId), + ]); + room.getMyMembership = jest.fn().mockReturnValue(KnownMembership.Join); + // Not all the calls to cli.isRoomEncrypted are migrated, so we need to mock both. + mocked(cli.isRoomEncrypted).mockReturnValue(true); + jest.spyOn(cli, "getCrypto").mockReturnValue(crypto); + jest.spyOn(cli.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true); + jest.spyOn(cli.getCrypto()!, "getUserVerificationStatus").mockResolvedValue( + new UserVerificationStatus(false, false, false), + ); + jest.spyOn(cli.getCrypto()!, "getUserDeviceInfo").mockResolvedValue( + new Map([["user@example.com", new Map<string, any>()]]), + ); + + const { container } = await renderRoomView(); + await waitFor(() => expect(container.querySelector(".mx_E2EIcon_normal")).toBeInTheDocument()); + + const verificationStatus = new UserVerificationStatus(true, true, false); + jest.spyOn(cli.getCrypto()!, "getUserVerificationStatus").mockResolvedValue(verificationStatus); + cli.emit(CryptoEvent.UserTrustStatusChanged, cli.getSafeUserId(), verificationStatus); + await waitFor(() => expect(container.querySelector(".mx_E2EIcon_verified")).toBeInTheDocument()); + }); + describe("with virtual rooms", () => { it("checks for a virtual room on initial load", async () => { const { container } = await renderRoomView(); @@ -427,7 +486,8 @@ describe("RoomView", () => { ]); jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(cli.getSafeUserId()); jest.spyOn(DMRoomMap.shared(), "getRoomIds").mockReturnValue(new Set([room.roomId])); - mocked(cli).isRoomEncrypted.mockReturnValue(true); + jest.spyOn(cli, "getCrypto").mockReturnValue(crypto); + jest.spyOn(cli.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true); await renderRoomView(); }); @@ -653,7 +713,7 @@ describe("RoomView", () => { skey: id, ts, }); - room.addLiveEvents([widgetEvent]); + room.addLiveEvents([widgetEvent], { addToState: false }); room.currentState.setStateEvents([widgetEvent]); cli.emit(RoomStateEvent.Events, widgetEvent, room.currentState, null); await flushPromises(); diff --git a/test/unit-tests/components/structures/SpaceRoomView-test.tsx b/test/unit-tests/components/structures/SpaceRoomView-test.tsx new file mode 100644 index 0000000000..fb24603283 --- /dev/null +++ b/test/unit-tests/components/structures/SpaceRoomView-test.tsx @@ -0,0 +1,117 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import React from "react"; +import { mocked, MockedObject } from "jest-mock"; +import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; +import { render, cleanup, screen, fireEvent } from "jest-matrix-react"; + +import { stubClient, mockPlatformPeg, unmockPlatformPeg, withClientContextRenderOptions } from "../../../test-utils"; +import { RightPanelPhases } from "../../../../src/stores/right-panel/RightPanelStorePhases"; +import SpaceRoomView from "../../../../src/components/structures/SpaceRoomView.tsx"; +import ResizeNotifier from "../../../../src/utils/ResizeNotifier.ts"; +import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks.ts"; +import RightPanelStore from "../../../../src/stores/right-panel/RightPanelStore.ts"; +import DMRoomMap from "../../../../src/utils/DMRoomMap.ts"; + +describe("SpaceRoomView", () => { + let cli: MockedObject<MatrixClient>; + let space: Room; + + beforeEach(() => { + mockPlatformPeg({ reload: () => {} }); + cli = mocked(stubClient()); + + space = new Room(`!space:example.org`, cli, cli.getSafeUserId()); + space.currentState.setStateEvents([ + new MatrixEvent({ + type: "m.room.create", + room_id: space.roomId, + sender: cli.getSafeUserId(), + state_key: "", + content: { + creator: cli.getSafeUserId(), + type: "m.space", + }, + }), + new MatrixEvent({ + type: "m.room.member", + room_id: space.roomId, + sender: cli.getSafeUserId(), + state_key: cli.getSafeUserId(), + content: { + membership: "join", + }, + }), + new MatrixEvent({ + type: "m.room.member", + room_id: space.roomId, + sender: "@userA:server", + state_key: "@userA:server", + content: { + membership: "join", + }, + }), + new MatrixEvent({ + type: "m.room.member", + room_id: space.roomId, + sender: "@userB:server", + state_key: "@userB:server", + content: { + membership: "join", + }, + }), + new MatrixEvent({ + type: "m.room.member", + room_id: space.roomId, + sender: "@userC:server", + state_key: "@userC:server", + content: { + membership: "join", + }, + }), + ]); + space.updateMyMembership("join"); + + DMRoomMap.makeShared(cli); + }); + + afterEach(() => { + unmockPlatformPeg(); + jest.clearAllMocks(); + cleanup(); + }); + + const renderSpaceRoomView = async (): Promise<ReturnType<typeof render>> => { + const resizeNotifier = new ResizeNotifier(); + const permalinkCreator = new RoomPermalinkCreator(space); + + const spaceRoomView = render( + <SpaceRoomView + space={space} + resizeNotifier={resizeNotifier} + permalinkCreator={permalinkCreator} + onJoinButtonClicked={jest.fn()} + onRejectButtonClicked={jest.fn()} + />, + withClientContextRenderOptions(cli), + ); + return spaceRoomView; + }; + + describe("SpaceLanding", () => { + it("should show member list right panel phase on members click on landing", async () => { + const spy = jest.spyOn(RightPanelStore.instance, "setCard"); + const { container } = await renderSpaceRoomView(); + + await expect(screen.findByText("Welcome to")).resolves.toBeVisible(); + fireEvent.click(container.querySelector(".mx_FacePile")!); + + expect(spy).toHaveBeenCalledWith({ phase: RightPanelPhases.MemberList }); + }); + }); +}); diff --git a/test/unit-tests/components/structures/ThreadPanel-test.tsx b/test/unit-tests/components/structures/ThreadPanel-test.tsx index c19127de25..20fc708103 100644 --- a/test/unit-tests/components/structures/ThreadPanel-test.tsx +++ b/test/unit-tests/components/structures/ThreadPanel-test.tsx @@ -20,7 +20,6 @@ import { import ThreadPanel, { ThreadFilterType, ThreadPanelHeader } from "../../../../src/components/structures/ThreadPanel"; import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; -import RoomContext from "../../../../src/contexts/RoomContext"; import { _t } from "../../../../src/languageHandler"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks"; @@ -28,6 +27,7 @@ import ResizeNotifier from "../../../../src/utils/ResizeNotifier"; import { createTestClient, getRoomContext, mkRoom, mockPlatformPeg, stubClient } from "../../../test-utils"; import { mkThread } from "../../../test-utils/threads"; import { IRoomState } from "../../../../src/components/structures/RoomView"; +import { ScopedRoomContextProvider } from "../../../../src/contexts/ScopedRoomContext.tsx"; jest.mock("../../../../src/utils/Feedback"); @@ -81,11 +81,11 @@ describe("ThreadPanel", () => { room: mockRoom, } as unknown as IRoomState; const { container } = render( - <RoomContext.Provider value={roomContextObject}> + <ScopedRoomContextProvider {...roomContextObject}> <MatrixClientContext.Provider value={mockClient}> <ThreadPanelHeader filterOption={ThreadFilterType.All} setFilterOption={() => undefined} /> </MatrixClientContext.Provider> - </RoomContext.Provider>, + </ScopedRoomContextProvider>, ); fireEvent.click(getByRole(container, "button", { name: "Mark all as read" })); await waitFor(() => @@ -114,8 +114,8 @@ describe("ThreadPanel", () => { const TestThreadPanel = () => ( <MatrixClientContext.Provider value={mockClient}> - <RoomContext.Provider - value={getRoomContext(room, { + <ScopedRoomContextProvider + {...getRoomContext(room, { canSendMessages: true, })} > @@ -125,7 +125,7 @@ describe("ThreadPanel", () => { resizeNotifier={new ResizeNotifier()} permalinkCreator={new RoomPermalinkCreator(room)} /> - </RoomContext.Provider> + </ScopedRoomContextProvider> </MatrixClientContext.Provider> ); @@ -209,11 +209,11 @@ describe("ThreadPanel", () => { return event ? Promise.resolve(event) : Promise.reject(); }); const [allThreads, myThreads] = room.threadsTimelineSets; - allThreads!.addLiveEvent(otherThread.rootEvent); - allThreads!.addLiveEvent(mixedThread.rootEvent); - allThreads!.addLiveEvent(ownThread.rootEvent); - myThreads!.addLiveEvent(mixedThread.rootEvent); - myThreads!.addLiveEvent(ownThread.rootEvent); + allThreads!.addLiveEvent(otherThread.rootEvent, { addToState: true }); + allThreads!.addLiveEvent(mixedThread.rootEvent, { addToState: true }); + allThreads!.addLiveEvent(ownThread.rootEvent, { addToState: true }); + myThreads!.addLiveEvent(mixedThread.rootEvent, { addToState: true }); + myThreads!.addLiveEvent(ownThread.rootEvent, { addToState: true }); const renderResult = render(<TestThreadPanel />); await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy()); @@ -258,7 +258,7 @@ describe("ThreadPanel", () => { return event ? Promise.resolve(event) : Promise.reject(); }); const [allThreads] = room.threadsTimelineSets; - allThreads!.addLiveEvent(otherThread.rootEvent); + allThreads!.addLiveEvent(otherThread.rootEvent, { addToState: true }); const renderResult = render(<TestThreadPanel />); await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy()); diff --git a/test/unit-tests/components/structures/ThreadView-test.tsx b/test/unit-tests/components/structures/ThreadView-test.tsx index 697fd25181..ee4afff525 100644 --- a/test/unit-tests/components/structures/ThreadView-test.tsx +++ b/test/unit-tests/components/structures/ThreadView-test.tsx @@ -23,7 +23,6 @@ import React, { useState } from "react"; import ThreadView from "../../../../src/components/structures/ThreadView"; import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; -import RoomContext from "../../../../src/contexts/RoomContext"; import { SdkContextClass } from "../../../../src/contexts/SDKContext"; import { Action } from "../../../../src/dispatcher/actions"; import dispatcher from "../../../../src/dispatcher/dispatcher"; @@ -34,6 +33,7 @@ import { mockPlatformPeg } from "../../../test-utils/platform"; import { getRoomContext } from "../../../test-utils/room"; import { mkMessage, stubClient } from "../../../test-utils/test-utils"; import { mkThread } from "../../../test-utils/threads"; +import { ScopedRoomContextProvider } from "../../../../src/contexts/ScopedRoomContext.tsx"; describe("ThreadView", () => { const ROOM_ID = "!roomId:example.org"; @@ -51,8 +51,8 @@ describe("ThreadView", () => { return ( <MatrixClientContext.Provider value={mockClient}> - <RoomContext.Provider - value={getRoomContext(room, { + <ScopedRoomContextProvider + {...getRoomContext(room, { canSendMessages: true, })} > @@ -63,7 +63,7 @@ describe("ThreadView", () => { initialEvent={initialEvent} resizeNotifier={new ResizeNotifier()} /> - </RoomContext.Provider> + </ScopedRoomContextProvider> , </MatrixClientContext.Provider> ); diff --git a/test/unit-tests/components/structures/TimelinePanel-test.tsx b/test/unit-tests/components/structures/TimelinePanel-test.tsx index 2f85843000..442ed1c1d2 100644 --- a/test/unit-tests/components/structures/TimelinePanel-test.tsx +++ b/test/unit-tests/components/structures/TimelinePanel-test.tsx @@ -66,7 +66,7 @@ const mkTimeline = (room: Room, events: MatrixEvent[]): [EventTimeline, EventTim getPendingEvents: () => [] as MatrixEvent[], } as unknown as EventTimelineSet; const timeline = new EventTimeline(timelineSet); - events.forEach((event) => timeline.addEvent(event, { toStartOfTimeline: false })); + events.forEach((event) => timeline.addEvent(event, { toStartOfTimeline: false, addToState: true })); return [timeline, timelineSet]; }; @@ -150,9 +150,11 @@ const setupPagination = ( mocked(client).paginateEventTimeline.mockImplementation(async (tl, { backwards }) => { if (tl === timeline) { if (backwards) { - forEachRight(previousPage ?? [], (event) => tl.addEvent(event, { toStartOfTimeline: true })); + forEachRight(previousPage ?? [], (event) => + tl.addEvent(event, { toStartOfTimeline: true, addToState: true }), + ); } else { - (nextPage ?? []).forEach((event) => tl.addEvent(event, { toStartOfTimeline: false })); + (nextPage ?? []).forEach((event) => tl.addEvent(event, { toStartOfTimeline: false, addToState: true })); } // Prevent any further pagination attempts in this direction tl.setPaginationToken(null, backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS); @@ -256,7 +258,7 @@ describe("TimelinePanel", () => { describe("and reading the timeline", () => { beforeEach(async () => { await renderTimelinePanel(); - timelineSet.addLiveEvent(ev1, {}); + timelineSet.addLiveEvent(ev1, { addToState: true }); await flushPromises(); // @ts-ignore await timelinePanel.sendReadReceipts(); @@ -284,11 +286,11 @@ describe("TimelinePanel", () => { }); it("and forgetting the read markers, should send the stored marker again", async () => { - timelineSet.addLiveEvent(ev2, {}); + timelineSet.addLiveEvent(ev2, { addToState: true }); // Add the event to the room as well as the timeline, so we can find it when we // call findEventById in getEventReadUpTo. This is odd because in our test // setup, timelineSet is not actually the timelineSet of the room. - await room.addLiveEvents([ev2], {}); + await room.addLiveEvents([ev2], { addToState: true }); room.addEphemeralEvents([newReceipt(ev2.getId()!, userId, 222, 200)]); await timelinePanel!.forgetReadMarker(); expect(client.setRoomReadMarkers).toHaveBeenCalledWith(roomId, ev2.getId()); @@ -314,7 +316,7 @@ describe("TimelinePanel", () => { it("should send a fully read marker and a private receipt", async () => { await renderTimelinePanel(); - act(() => timelineSet.addLiveEvent(ev1, {})); + act(() => timelineSet.addLiveEvent(ev1, { addToState: true })); await flushPromises(); // @ts-ignore @@ -361,7 +363,7 @@ describe("TimelinePanel", () => { it("should send receipts but no fully_read when reading the thread timeline", async () => { await renderTimelinePanel(); - act(() => timelineSet.addLiveEvent(threadEv1, {})); + act(() => timelineSet.addLiveEvent(threadEv1, { addToState: true })); await flushPromises(); // @ts-ignore @@ -871,7 +873,9 @@ describe("TimelinePanel", () => { // @ts-ignore thread.fetchEditsWhereNeeded = () => Promise.resolve(); await thread.addEvent(reply1, false, true); - await allThreads.getLiveTimeline().addEvent(thread.rootEvent!, { toStartOfTimeline: true }); + await allThreads + .getLiveTimeline() + .addEvent(thread.rootEvent!, { toStartOfTimeline: true, addToState: true }); const replyToEvent = jest.spyOn(thread, "replyToEvent", "get"); const dom = render( @@ -907,7 +911,9 @@ describe("TimelinePanel", () => { // @ts-ignore realThread.fetchEditsWhereNeeded = () => Promise.resolve(); await realThread.addEvent(reply1, true); - await allThreads.getLiveTimeline().addEvent(realThread.rootEvent!, { toStartOfTimeline: true }); + await allThreads + .getLiveTimeline() + .addEvent(realThread.rootEvent!, { toStartOfTimeline: true, addToState: true }); const replyToEvent = jest.spyOn(realThread, "replyToEvent", "get"); // @ts-ignore @@ -968,7 +974,9 @@ describe("TimelinePanel", () => { events.push(rootEvent); - events.forEach((event) => timelineSet.getLiveTimeline().addEvent(event, { toStartOfTimeline: true })); + events.forEach((event) => + timelineSet.getLiveTimeline().addEvent(event, { toStartOfTimeline: true, addToState: true }), + ); const roomMembership = mkMembership({ mship: KnownMembership.Join, @@ -988,7 +996,10 @@ describe("TimelinePanel", () => { jest.spyOn(roomState, "getMember").mockReturnValue(member); jest.spyOn(timelineSet.getLiveTimeline(), "getState").mockReturnValue(roomState); - timelineSet.addEventToTimeline(roomMembership, timelineSet.getLiveTimeline(), { toStartOfTimeline: false }); + timelineSet.addEventToTimeline(roomMembership, timelineSet.getLiveTimeline(), { + toStartOfTimeline: false, + addToState: true, + }); for (const event of events) { jest.spyOn(event, "isDecryptionFailure").mockReturnValue(true); diff --git a/test/unit-tests/components/structures/UserMenu-test.tsx b/test/unit-tests/components/structures/UserMenu-test.tsx index ac76aba2ad..907bf664b7 100644 --- a/test/unit-tests/components/structures/UserMenu-test.tsx +++ b/test/unit-tests/components/structures/UserMenu-test.tsx @@ -7,20 +7,14 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; -import { act, render, RenderResult, screen, waitFor } from "jest-matrix-react"; -import { DEVICE_CODE_SCOPE, MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; +import { render, screen, waitFor } from "jest-matrix-react"; +import { DEVICE_CODE_SCOPE, MatrixClient, Room } from "matrix-js-sdk/src/matrix"; import { CryptoApi } from "matrix-js-sdk/src/crypto-api"; import { mocked } from "jest-mock"; import fetchMock from "fetch-mock-jest"; import UnwrappedUserMenu from "../../../../src/components/structures/UserMenu"; import { stubClient, wrapInSdkContext } from "../../../test-utils"; -import { - VoiceBroadcastInfoState, - VoiceBroadcastRecording, - VoiceBroadcastRecordingsStore, -} from "../../../../src/voice-broadcast"; -import { mkVoiceBroadcastInfoStateEvent } from "../../voice-broadcast/utils/test-utils"; import { TestSdkContext } from "../../TestSdkContext"; import defaultDispatcher from "../../../../src/dispatcher/dispatcher"; import LogoutDialog from "../../../../src/components/views/dialogs/LogoutDialog"; @@ -34,71 +28,12 @@ import { UserTab } from "../../../../src/components/views/dialogs/UserTab"; describe("<UserMenu>", () => { let client: MatrixClient; - let renderResult: RenderResult; let sdkContext: TestSdkContext; beforeEach(() => { sdkContext = new TestSdkContext(); }); - describe("<UserMenu> when video broadcast", () => { - let voiceBroadcastInfoEvent: MatrixEvent; - let voiceBroadcastRecording: VoiceBroadcastRecording; - let voiceBroadcastRecordingsStore: VoiceBroadcastRecordingsStore; - - beforeAll(() => { - client = stubClient(); - voiceBroadcastInfoEvent = mkVoiceBroadcastInfoStateEvent( - "!room:example.com", - VoiceBroadcastInfoState.Started, - client.getUserId() || "", - client.getDeviceId() || "", - ); - }); - - beforeEach(() => { - voiceBroadcastRecordingsStore = new VoiceBroadcastRecordingsStore(); - sdkContext._VoiceBroadcastRecordingsStore = voiceBroadcastRecordingsStore; - - voiceBroadcastRecording = new VoiceBroadcastRecording(voiceBroadcastInfoEvent, client); - }); - - describe("when rendered", () => { - beforeEach(() => { - const UserMenu = wrapInSdkContext(UnwrappedUserMenu, sdkContext); - renderResult = render(<UserMenu isPanelCollapsed={true} />); - }); - - it("should render as expected", () => { - expect(renderResult.container).toMatchSnapshot(); - }); - - describe("and a live voice broadcast starts", () => { - beforeEach(() => { - act(() => { - voiceBroadcastRecordingsStore.setCurrent(voiceBroadcastRecording); - }); - }); - - it("should render the live voice broadcast avatar addon", () => { - expect(renderResult.queryByTestId("user-menu-live-vb")).toBeInTheDocument(); - }); - - describe("and the broadcast ends", () => { - beforeEach(() => { - act(() => { - voiceBroadcastRecordingsStore.clearCurrent(); - }); - }); - - it("should not render the live voice broadcast avatar addon", () => { - expect(renderResult.queryByTestId("user-menu-live-vb")).not.toBeInTheDocument(); - }); - }); - }); - }); - }); - describe("<UserMenu> logout", () => { beforeEach(() => { client = stubClient(); @@ -106,7 +41,7 @@ describe("<UserMenu>", () => { it("should logout directly if no crypto", async () => { const UserMenu = wrapInSdkContext(UnwrappedUserMenu, sdkContext); - renderResult = render(<UserMenu isPanelCollapsed={true} />); + render(<UserMenu isPanelCollapsed={true} />); mocked(client.getRooms).mockReturnValue([ { @@ -128,7 +63,7 @@ describe("<UserMenu>", () => { it("should logout directly if no encrypted rooms", async () => { const UserMenu = wrapInSdkContext(UnwrappedUserMenu, sdkContext); - renderResult = render(<UserMenu isPanelCollapsed={true} />); + render(<UserMenu isPanelCollapsed={true} />); mocked(client.getRooms).mockReturnValue([ { @@ -152,7 +87,7 @@ describe("<UserMenu>", () => { it("should show dialog if some encrypted rooms", async () => { const UserMenu = wrapInSdkContext(UnwrappedUserMenu, sdkContext); - renderResult = render(<UserMenu isPanelCollapsed={true} />); + render(<UserMenu isPanelCollapsed={true} />); mocked(client.getRooms).mockReturnValue([ { diff --git a/test/unit-tests/components/structures/__snapshots__/MatrixChat-test.tsx.snap b/test/unit-tests/components/structures/__snapshots__/MatrixChat-test.tsx.snap index 94c2678388..1bdbe016d4 100644 --- a/test/unit-tests/components/structures/__snapshots__/MatrixChat-test.tsx.snap +++ b/test/unit-tests/components/structures/__snapshots__/MatrixChat-test.tsx.snap @@ -314,7 +314,6 @@ exports[`<MatrixChat /> with a soft-logged-out session should show the soft-logo class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary" role="button" tabindex="0" - type="submit" > Sign in </div> diff --git a/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap b/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap index 626f7f2b9b..1e0ed2248b 100644 --- a/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap +++ b/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap @@ -62,7 +62,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1 style="--cpd-icon-button-size: 100%;" > <svg - aria-labelledby=":rbc:" + aria-labelledby=":rg4:" fill="currentColor" height="1em" viewBox="0 0 24 24" @@ -78,7 +78,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1 <button aria-disabled="false" aria-label="Voice call" - aria-labelledby=":rbh:" + aria-labelledby=":rg9:" class="_icon-button_bh2qc_17" role="button" style="--cpd-icon-button-size: 32px;" @@ -103,7 +103,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1 </button> <button aria-label="Room info" - aria-labelledby=":rbm:" + aria-labelledby=":rge:" class="_icon-button_bh2qc_17" role="button" style="--cpd-icon-button-size: 32px;" @@ -128,7 +128,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1 </button> <button aria-label="Threads" - aria-labelledby=":rbr:" + aria-labelledby=":rgj:" class="_icon-button_bh2qc_17" role="button" style="--cpd-icon-button-size: 32px;" @@ -157,7 +157,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1 > <div aria-label="2 members" - aria-labelledby=":rc0:" + aria-labelledby=":rgo:" class="mx_AccessibleButton mx_FacePile" role="button" tabindex="0" @@ -280,7 +280,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`] style="--cpd-icon-button-size: 100%;" > <svg - aria-labelledby=":rca:" + aria-labelledby=":rh2:" fill="currentColor" height="1em" viewBox="0 0 24 24" @@ -296,7 +296,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`] <button aria-disabled="false" aria-label="Voice call" - aria-labelledby=":rcf:" + aria-labelledby=":rh7:" class="_icon-button_bh2qc_17" role="button" style="--cpd-icon-button-size: 32px;" @@ -321,7 +321,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`] </button> <button aria-label="Room info" - aria-labelledby=":rck:" + aria-labelledby=":rhc:" class="_icon-button_bh2qc_17" role="button" style="--cpd-icon-button-size: 32px;" @@ -346,7 +346,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`] </button> <button aria-label="Threads" - aria-labelledby=":rcp:" + aria-labelledby=":rhh:" class="_icon-button_bh2qc_17" role="button" style="--cpd-icon-button-size: 32px;" @@ -375,7 +375,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`] > <div aria-label="2 members" - aria-labelledby=":rcu:" + aria-labelledby=":rhm:" class="mx_AccessibleButton mx_FacePile" role="button" tabindex="0" @@ -583,7 +583,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] = style="--cpd-icon-button-size: 100%;" > <svg - aria-labelledby=":r70:" + aria-labelledby=":rbo:" fill="currentColor" height="1em" viewBox="0 0 24 24" @@ -599,7 +599,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] = <button aria-disabled="false" aria-label="Voice call" - aria-labelledby=":r75:" + aria-labelledby=":rbt:" class="_icon-button_bh2qc_17" role="button" style="--cpd-icon-button-size: 32px;" @@ -624,7 +624,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] = </button> <button aria-label="Room info" - aria-labelledby=":r7a:" + aria-labelledby=":rc2:" class="_icon-button_bh2qc_17" role="button" style="--cpd-icon-button-size: 32px;" @@ -649,7 +649,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] = </button> <button aria-label="Threads" - aria-labelledby=":r7f:" + aria-labelledby=":rc7:" class="_icon-button_bh2qc_17" role="button" style="--cpd-icon-button-size: 32px;" @@ -678,7 +678,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] = > <div aria-label="2 members" - aria-labelledby=":r7k:" + aria-labelledby=":rcc:" class="mx_AccessibleButton mx_FacePile" role="button" tabindex="0" @@ -963,7 +963,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t style="--cpd-icon-button-size: 100%;" > <svg - aria-labelledby=":r96:" + aria-labelledby=":rdu:" fill="currentColor" height="1em" viewBox="0 0 24 24" @@ -979,7 +979,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t <button aria-disabled="false" aria-label="Voice call" - aria-labelledby=":r9b:" + aria-labelledby=":re3:" class="_icon-button_bh2qc_17" role="button" style="--cpd-icon-button-size: 32px;" @@ -1004,7 +1004,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t </button> <button aria-label="Room info" - aria-labelledby=":r9g:" + aria-labelledby=":re8:" class="_icon-button_bh2qc_17" role="button" style="--cpd-icon-button-size: 32px;" @@ -1029,7 +1029,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t </button> <button aria-label="Threads" - aria-labelledby=":r9l:" + aria-labelledby=":red:" class="_icon-button_bh2qc_17" role="button" style="--cpd-icon-button-size: 32px;" @@ -1058,7 +1058,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t > <div aria-label="2 members" - aria-labelledby=":r9q:" + aria-labelledby=":rei:" class="mx_AccessibleButton mx_FacePile" role="button" tabindex="0" @@ -1276,6 +1276,571 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t </div> `; +exports[`RoomView should not display the timeline when the room encryption is loading 1`] = ` +<DocumentFragment> + <div + class="mx_RoomView" + > + <canvas + aria-hidden="true" + height="768" + style="display: block; z-index: 999999; pointer-events: none; position: fixed; top: 0px; right: 0px;" + width="0" + /> + <div + class="mx_MainSplit" + > + <div + class="mx_RoomView_body mx_MainSplit_timeline" + data-layout="group" + > + <header + class="mx_Flex mx_RoomHeader light-panel" + style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);" + > + <button + aria-label="Open room settings" + aria-live="off" + class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61" + data-color="2" + data-testid="avatar-img" + data-type="round" + role="button" + style="--cpd-avatar-size: 40px;" + tabindex="-1" + > + ! + </button> + <button + aria-label="Room info" + class="mx_RoomHeader_infoWrapper" + tabindex="0" + > + <div + class="mx_Box mx_RoomHeader_info mx_Box--flex" + style="--mx-box-flex: 1;" + > + <div + aria-level="1" + class="_typography_yh5dq_162 _font-body-lg-semibold_yh5dq_83 mx_RoomHeader_heading" + dir="auto" + role="heading" + > + <span + class="mx_RoomHeader_truncated mx_lineClamp" + > + !6:example.org + </span> + </div> + </div> + </button> + <div + class="mx_Flex" + style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x);" + > + <button + aria-disabled="true" + aria-label="There's no one here to call" + class="_icon-button_bh2qc_17" + role="button" + style="--cpd-icon-button-size: 32px;" + tabindex="0" + > + <div + class="_indicator-icon_133tf_26" + style="--cpd-icon-button-size: 100%; --cpd-color-icon-tertiary: var(--cpd-color-icon-disabled);" + > + <svg + aria-labelledby=":r2c:" + fill="currentColor" + height="1em" + viewBox="0 0 24 24" + width="1em" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4Z" + /> + </svg> + </div> + </button> + <button + aria-disabled="true" + aria-label="There's no one here to call" + aria-labelledby=":r2h:" + class="_icon-button_bh2qc_17" + role="button" + style="--cpd-icon-button-size: 32px;" + tabindex="0" + > + <div + class="_indicator-icon_133tf_26" + style="--cpd-icon-button-size: 100%; --cpd-color-icon-tertiary: var(--cpd-color-icon-disabled);" + > + <svg + fill="currentColor" + height="1em" + viewBox="0 0 24 24" + width="1em" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="m20.958 16.374.039 3.527c0 .285-.11.537-.33.756-.22.22-.472.33-.756.33a15.97 15.97 0 0 1-6.57-1.105 16.223 16.223 0 0 1-5.563-3.663 16.084 16.084 0 0 1-3.653-5.573 16.313 16.313 0 0 1-1.115-6.56c0-.285.11-.537.33-.757.22-.22.471-.329.755-.329l3.528.039a1.069 1.069 0 0 1 1.085.93l.543 3.954c.026.181.013.349-.039.504a1.088 1.088 0 0 1-.271.426l-1.64 1.64c.337.672.721 1.308 1.154 1.909.433.6 1.444 1.696 1.444 1.696s1.095 1.01 1.696 1.444c.6.433 1.237.817 1.909 1.153l1.64-1.64a1.08 1.08 0 0 1 .426-.27c.155-.052.323-.065.504-.04l3.954.543a1.069 1.069 0 0 1 .93 1.085Z" + /> + </svg> + </div> + </button> + <button + aria-label="Room info" + aria-labelledby=":r2m:" + class="_icon-button_bh2qc_17" + role="button" + style="--cpd-icon-button-size: 32px;" + tabindex="0" + > + <div + class="_indicator-icon_133tf_26" + style="--cpd-icon-button-size: 100%;" + > + <svg + fill="currentColor" + height="1em" + viewBox="0 0 24 24" + width="1em" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16v-4a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 11a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 12v4c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-8c.283 0 .52-.096.713-.287A.967.967 0 0 0 13 8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 13a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z" + /> + </svg> + </div> + </button> + <button + aria-label="Threads" + aria-labelledby=":r2r:" + class="_icon-button_bh2qc_17" + role="button" + style="--cpd-icon-button-size: 32px;" + tabindex="0" + > + <div + class="_indicator-icon_133tf_26" + style="--cpd-icon-button-size: 100%;" + > + <svg + fill="currentColor" + height="1em" + viewBox="0 0 24 24" + width="1em" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M4 3h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6l-2.293 2.293c-.63.63-1.707.184-1.707-.707V5a2 2 0 0 1 2-2Zm3 7h10a.97.97 0 0 0 .712-.287A.967.967 0 0 0 18 9a.967.967 0 0 0-.288-.713A.968.968 0 0 0 17 8H7a.968.968 0 0 0-.713.287A.968.968 0 0 0 6 9c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 4h6c.283 0 .52-.096.713-.287A.968.968 0 0 0 14 13a.968.968 0 0 0-.287-.713A.968.968 0 0 0 13 12H7a.967.967 0 0 0-.713.287A.968.968 0 0 0 6 13c0 .283.096.52.287.713.192.191.43.287.713.287Z" + /> + </svg> + </div> + </button> + </div> + <div + class="_typography_yh5dq_162 _font-body-sm-medium_yh5dq_50" + > + <div + aria-label="0 members" + aria-labelledby=":r30:" + class="mx_AccessibleButton mx_FacePile" + role="button" + tabindex="0" + > + <div + class="_stacked-avatars_mcap2_111" + /> + 0 + </div> + </div> + </header> + <div + class="mx_AutoHideScrollbar mx_AuxPanel" + role="region" + tabindex="-1" + > + <div /> + </div> + <main + class="mx_RoomView_timeline mx_RoomView_timeline_rr_enabled" + /> + <div + aria-label="Room status bar" + class="mx_RoomView_statusArea" + role="region" + > + <div + class="mx_RoomView_statusAreaBox" + > + <div + class="mx_RoomView_statusAreaBox_line" + /> + </div> + </div> + </div> + </div> + </div> +</DocumentFragment> +`; + +exports[`RoomView should not display the timeline when the room encryption is loading 2`] = ` +<DocumentFragment> + <div + class="mx_RoomView" + > + <canvas + aria-hidden="true" + height="768" + style="display: block; z-index: 999999; pointer-events: none; position: fixed; top: 0px; right: 0px;" + width="0" + /> + <div + class="mx_MainSplit" + > + <div + class="mx_RoomView_body mx_MainSplit_timeline" + data-layout="group" + > + <header + class="mx_Flex mx_RoomHeader light-panel" + style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);" + > + <button + aria-label="Open room settings" + aria-live="off" + class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61" + data-color="2" + data-testid="avatar-img" + data-type="round" + role="button" + style="--cpd-avatar-size: 40px;" + tabindex="-1" + > + ! + </button> + <button + aria-label="Room info" + class="mx_RoomHeader_infoWrapper" + tabindex="0" + > + <div + class="mx_Box mx_RoomHeader_info mx_Box--flex" + style="--mx-box-flex: 1;" + > + <div + aria-level="1" + class="_typography_yh5dq_162 _font-body-lg-semibold_yh5dq_83 mx_RoomHeader_heading" + dir="auto" + role="heading" + > + <span + class="mx_RoomHeader_truncated mx_lineClamp" + > + !6:example.org + </span> + </div> + </div> + </button> + <div + class="mx_Flex" + style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x);" + > + <button + aria-disabled="true" + aria-label="There's no one here to call" + class="_icon-button_bh2qc_17" + role="button" + style="--cpd-icon-button-size: 32px;" + tabindex="0" + > + <div + class="_indicator-icon_133tf_26" + style="--cpd-icon-button-size: 100%; --cpd-color-icon-tertiary: var(--cpd-color-icon-disabled);" + > + <svg + aria-labelledby=":r2c:" + fill="currentColor" + height="1em" + viewBox="0 0 24 24" + width="1em" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4Z" + /> + </svg> + </div> + </button> + <button + aria-disabled="true" + aria-label="There's no one here to call" + aria-labelledby=":r2h:" + class="_icon-button_bh2qc_17" + role="button" + style="--cpd-icon-button-size: 32px;" + tabindex="0" + > + <div + class="_indicator-icon_133tf_26" + style="--cpd-icon-button-size: 100%; --cpd-color-icon-tertiary: var(--cpd-color-icon-disabled);" + > + <svg + fill="currentColor" + height="1em" + viewBox="0 0 24 24" + width="1em" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="m20.958 16.374.039 3.527c0 .285-.11.537-.33.756-.22.22-.472.33-.756.33a15.97 15.97 0 0 1-6.57-1.105 16.223 16.223 0 0 1-5.563-3.663 16.084 16.084 0 0 1-3.653-5.573 16.313 16.313 0 0 1-1.115-6.56c0-.285.11-.537.33-.757.22-.22.471-.329.755-.329l3.528.039a1.069 1.069 0 0 1 1.085.93l.543 3.954c.026.181.013.349-.039.504a1.088 1.088 0 0 1-.271.426l-1.64 1.64c.337.672.721 1.308 1.154 1.909.433.6 1.444 1.696 1.444 1.696s1.095 1.01 1.696 1.444c.6.433 1.237.817 1.909 1.153l1.64-1.64a1.08 1.08 0 0 1 .426-.27c.155-.052.323-.065.504-.04l3.954.543a1.069 1.069 0 0 1 .93 1.085Z" + /> + </svg> + </div> + </button> + <button + aria-label="Room info" + aria-labelledby=":r2m:" + class="_icon-button_bh2qc_17" + role="button" + style="--cpd-icon-button-size: 32px;" + tabindex="0" + > + <div + class="_indicator-icon_133tf_26" + style="--cpd-icon-button-size: 100%;" + > + <svg + fill="currentColor" + height="1em" + viewBox="0 0 24 24" + width="1em" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16v-4a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 11a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 12v4c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-8c.283 0 .52-.096.713-.287A.967.967 0 0 0 13 8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 13a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z" + /> + </svg> + </div> + </button> + <button + aria-label="Threads" + aria-labelledby=":r2r:" + class="_icon-button_bh2qc_17" + role="button" + style="--cpd-icon-button-size: 32px;" + tabindex="0" + > + <div + class="_indicator-icon_133tf_26" + style="--cpd-icon-button-size: 100%;" + > + <svg + fill="currentColor" + height="1em" + viewBox="0 0 24 24" + width="1em" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M4 3h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6l-2.293 2.293c-.63.63-1.707.184-1.707-.707V5a2 2 0 0 1 2-2Zm3 7h10a.97.97 0 0 0 .712-.287A.967.967 0 0 0 18 9a.967.967 0 0 0-.288-.713A.968.968 0 0 0 17 8H7a.968.968 0 0 0-.713.287A.968.968 0 0 0 6 9c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 4h6c.283 0 .52-.096.713-.287A.968.968 0 0 0 14 13a.968.968 0 0 0-.287-.713A.968.968 0 0 0 13 12H7a.967.967 0 0 0-.713.287A.968.968 0 0 0 6 13c0 .283.096.52.287.713.192.191.43.287.713.287Z" + /> + </svg> + </div> + </button> + </div> + <div + class="_typography_yh5dq_162 _font-body-sm-medium_yh5dq_50" + > + <div + aria-label="0 members" + aria-labelledby=":r30:" + class="mx_AccessibleButton mx_FacePile" + role="button" + tabindex="0" + > + <div + class="_stacked-avatars_mcap2_111" + /> + 0 + </div> + </div> + </header> + <div + class="mx_AutoHideScrollbar mx_AuxPanel" + role="region" + tabindex="-1" + > + <div /> + </div> + <main + class="mx_RoomView_timeline mx_RoomView_timeline_rr_enabled" + > + <div + class="mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel" + tabindex="-1" + > + <div + class="mx_RoomView_messageListWrapper" + > + <ol + aria-live="polite" + class="mx_RoomView_MessageList" + style="height: 400px;" + /> + </div> + </div> + </main> + <div + aria-label="Room status bar" + class="mx_RoomView_statusArea" + role="region" + > + <div + class="mx_RoomView_statusAreaBox" + > + <div + class="mx_RoomView_statusAreaBox_line" + /> + </div> + </div> + <div + aria-label="Message composer" + class="mx_MessageComposer mx_MessageComposer_e2eStatus" + role="region" + > + <div + class="mx_MessageComposer_wrapper" + > + <div + class="mx_MessageComposer_row" + > + <div + class="mx_MessageComposer_e2eIconWrapper" + > + <span + tabindex="0" + > + <div + aria-labelledby=":r3e:" + class="mx_E2EIcon mx_E2EIcon_verified mx_MessageComposer_e2eIcon" + /> + </span> + </div> + <div + class="mx_SendMessageComposer" + > + <div + class="mx_BasicMessageComposer" + > + <div + aria-label="Formatting" + class="mx_MessageComposerFormatBar" + role="toolbar" + > + <button + aria-label="Bold" + class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconBold" + role="button" + tabindex="0" + type="button" + /> + <button + aria-label="Italics" + class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconItalic" + role="button" + tabindex="-1" + type="button" + /> + <button + aria-label="Strikethrough" + class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconStrikethrough" + role="button" + tabindex="-1" + type="button" + /> + <button + aria-label="Code block" + class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconCode" + role="button" + tabindex="-1" + type="button" + /> + <button + aria-label="Quote" + class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconQuote" + role="button" + tabindex="-1" + type="button" + /> + <button + aria-label="Insert link" + class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconInsertLink" + role="button" + tabindex="-1" + type="button" + /> + </div> + <div + aria-autocomplete="list" + aria-disabled="false" + aria-haspopup="listbox" + aria-label="Send an encrypted message…" + aria-multiline="true" + class="mx_BasicMessageComposer_input mx_BasicMessageComposer_input_shouldShowPillAvatar mx_BasicMessageComposer_inputEmpty" + contenteditable="true" + data-testid="basicmessagecomposer" + dir="auto" + role="textbox" + style="--placeholder: 'Send\\ an\\ encrypted\\ message…';" + tabindex="0" + translate="no" + > + <div> + <br /> + </div> + </div> + </div> + </div> + <div + class="mx_MessageComposer_actions" + > + <div + aria-label="Emoji" + class="mx_AccessibleButton mx_EmojiButton mx_MessageComposer_button mx_EmojiButton_icon" + role="button" + tabindex="0" + /> + <div + aria-label="Attachment" + class="mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_upload" + role="button" + tabindex="0" + /> + <div + aria-label="More options" + class="mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_buttonMenu" + role="button" + tabindex="0" + /> + <input + multiple="" + style="display: none;" + type="file" + /> + </div> + </div> + </div> + </div> + </div> + </div> + </div> +</DocumentFragment> +`; + exports[`RoomView should show error view if failed to look up room alias 1`] = ` <DocumentFragment> <div @@ -1332,7 +1897,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = ` aria-label="Open room settings" aria-live="off" class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61" - data-color="3" + data-color="6" data-testid="avatar-img" data-type="round" role="button" @@ -1359,7 +1924,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = ` <span class="mx_RoomHeader_truncated mx_lineClamp" > - !10:example.org + !13:example.org </span> </div> </div> @@ -1370,7 +1935,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = ` > <button aria-label="Room info" - aria-labelledby=":r2k:" + aria-labelledby=":r7c:" class="_icon-button_bh2qc_17" role="button" style="--cpd-icon-button-size: 32px;" @@ -1395,7 +1960,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = ` </button> <button aria-label="Chat" - aria-labelledby=":r2p:" + aria-labelledby=":r7h:" class="_icon-button_bh2qc_17" role="button" style="--cpd-icon-button-size: 32px;" @@ -1420,7 +1985,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = ` </button> <button aria-label="Threads" - aria-labelledby=":r2u:" + aria-labelledby=":r7m:" class="_icon-button_bh2qc_17" role="button" style="--cpd-icon-button-size: 32px;" @@ -1449,7 +2014,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = ` > <div aria-label="0 members" - aria-labelledby=":r33:" + aria-labelledby=":r7r:" class="mx_AccessibleButton mx_FacePile" role="button" tabindex="0" @@ -1487,7 +2052,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = ` </p> </div> <button - aria-labelledby=":r3c:" + aria-labelledby=":r84:" class="_icon-button_bh2qc_17 _subtle-bg_bh2qc_38" data-testid="base-card-close-button" role="button" diff --git a/test/unit-tests/components/structures/__snapshots__/UserMenu-test.tsx.snap b/test/unit-tests/components/structures/__snapshots__/UserMenu-test.tsx.snap deleted file mode 100644 index 029db9bfd4..0000000000 --- a/test/unit-tests/components/structures/__snapshots__/UserMenu-test.tsx.snap +++ /dev/null @@ -1,33 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`<UserMenu> <UserMenu> when video broadcast when rendered should render as expected 1`] = ` -<div> - <div - class="mx_UserMenu" - > - <div - aria-expanded="false" - aria-haspopup="true" - aria-label="User menu" - class="mx_AccessibleButton mx_UserMenu_contextMenuButton" - role="button" - tabindex="0" - > - <div - class="mx_UserMenu_userAvatar" - > - <span - class="_avatar_mcap2_17 mx_BaseAvatar mx_UserMenu_userAvatar_BaseAvatar _avatar-imageless_mcap2_61" - data-color="2" - data-testid="avatar-img" - data-type="round" - role="presentation" - style="--cpd-avatar-size: 32px;" - > - u - </span> - </div> - </div> - </div> -</div> -`; diff --git a/test/unit-tests/components/views/audio_messages/RecordingPlayback-test.tsx b/test/unit-tests/components/views/audio_messages/RecordingPlayback-test.tsx index 1f193d6883..419357e5c8 100644 --- a/test/unit-tests/components/views/audio_messages/RecordingPlayback-test.tsx +++ b/test/unit-tests/components/views/audio_messages/RecordingPlayback-test.tsx @@ -15,10 +15,11 @@ import RecordingPlayback, { PlaybackLayout, } from "../../../../../src/components/views/audio_messages/RecordingPlayback"; import { Playback } from "../../../../../src/audio/Playback"; -import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext"; +import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext"; import { createAudioContext } from "../../../../../src/audio/compat"; import { flushPromises } from "../../../../test-utils"; import { IRoomState } from "../../../../../src/components/structures/RoomView"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; jest.mock("../../../../../src/WorkerManager", () => ({ WorkerManager: jest.fn(() => ({ @@ -56,9 +57,9 @@ describe("<RecordingPlayback />", () => { const defaultRoom = { roomId: "!room:server.org", timelineRenderingType: TimelineRenderingType.File } as IRoomState; const getComponent = (props: React.ComponentProps<typeof RecordingPlayback>, room = defaultRoom) => render( - <RoomContext.Provider value={room}> + <ScopedRoomContextProvider {...room}> <RecordingPlayback {...props} /> - </RoomContext.Provider>, + </ScopedRoomContextProvider>, ); beforeEach(() => { diff --git a/test/unit-tests/components/views/avatars/MemberAvatar-test.tsx b/test/unit-tests/components/views/avatars/MemberAvatar-test.tsx index eaa3a308d5..1c5bf3a44f 100644 --- a/test/unit-tests/components/views/avatars/MemberAvatar-test.tsx +++ b/test/unit-tests/components/views/avatars/MemberAvatar-test.tsx @@ -12,11 +12,11 @@ import { MatrixClient, PendingEventOrdering, Room, RoomMember } from "matrix-js- import React, { ComponentProps } from "react"; import MemberAvatar from "../../../../../src/components/views/avatars/MemberAvatar"; -import RoomContext from "../../../../../src/contexts/RoomContext"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; import SettingsStore from "../../../../../src/settings/SettingsStore"; import { getRoomContext } from "../../../../test-utils/room"; import { stubClient } from "../../../../test-utils/test-utils"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; describe("MemberAvatar", () => { const ROOM_ID = "roomId"; @@ -27,9 +27,9 @@ describe("MemberAvatar", () => { function getComponent(props: Partial<ComponentProps<typeof MemberAvatar>>) { return ( - <RoomContext.Provider value={getRoomContext(room, {})}> + <ScopedRoomContextProvider {...getRoomContext(room, {})}> <MemberAvatar member={null} size="35px" {...props} /> - </RoomContext.Provider> + </ScopedRoomContextProvider> ); } diff --git a/test/unit-tests/components/views/context_menus/MessageContextMenu-test.tsx b/test/unit-tests/components/views/context_menus/MessageContextMenu-test.tsx index 54e8fa434b..142840fd5a 100644 --- a/test/unit-tests/components/views/context_menus/MessageContextMenu-test.tsx +++ b/test/unit-tests/components/views/context_menus/MessageContextMenu-test.tsx @@ -27,7 +27,7 @@ import { mocked } from "jest-mock"; import userEvent from "@testing-library/user-event"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; -import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext"; +import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext"; import { IRoomState } from "../../../../../src/components/structures/RoomView"; import { canEditContent } from "../../../../../src/utils/EventUtils"; import { copyPlaintext, getSelectedText } from "../../../../../src/utils/strings"; @@ -37,9 +37,8 @@ import dispatcher from "../../../../../src/dispatcher/dispatcher"; import SettingsStore from "../../../../../src/settings/SettingsStore"; import { ReadPinsEventId } from "../../../../../src/components/views/right_panel/types"; import { Action } from "../../../../../src/dispatcher/actions"; -import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils"; -import { VoiceBroadcastInfoState } from "../../../../../src/voice-broadcast"; import { createMessageEventContent } from "../../../../test-utils/events"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; jest.mock("../../../../../src/utils/strings", () => ({ copyPlaintext: jest.fn(), @@ -233,17 +232,6 @@ describe("MessageContextMenu", () => { expect(document.querySelector('li[aria-label="Forward"]')).toBeFalsy(); }); - it("should not allow forwarding a voice broadcast", () => { - const broadcastStartEvent = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Started, - "@user:example.com", - "ABC123", - ); - createMenu(broadcastStartEvent); - expect(document.querySelector('li[aria-label="Forward"]')).toBeFalsy(); - }); - describe("forwarding beacons", () => { const aliceId = "@alice:server.org"; @@ -546,8 +534,8 @@ function createMenu( client.getRoom = jest.fn().mockReturnValue(room); return render( - <RoomContext.Provider value={context as IRoomState}> + <ScopedRoomContextProvider {...(context as IRoomState)}> <MessageContextMenu mxEvent={mxEvent} onFinished={jest.fn()} {...props} /> - </RoomContext.Provider>, + </ScopedRoomContextProvider>, ); } diff --git a/test/unit-tests/components/views/context_menus/RoomGeneralContextMenu-test.tsx b/test/unit-tests/components/views/context_menus/RoomGeneralContextMenu-test.tsx index 9fc32dda29..7ef2062d87 100644 --- a/test/unit-tests/components/views/context_menus/RoomGeneralContextMenu-test.tsx +++ b/test/unit-tests/components/views/context_menus/RoomGeneralContextMenu-test.tsx @@ -127,7 +127,7 @@ describe("RoomGeneralContextMenu", () => { user: "@user:id", ts: 1000, }); - room.addLiveEvents([event], {}); + room.addLiveEvents([event], { addToState: true }); const { container } = getComponent({}); diff --git a/test/unit-tests/components/views/dialogs/ConfirmRedactDialog-test.tsx b/test/unit-tests/components/views/dialogs/ConfirmRedactDialog-test.tsx index c648f416f9..ed5b545ed5 100644 --- a/test/unit-tests/components/views/dialogs/ConfirmRedactDialog-test.tsx +++ b/test/unit-tests/components/views/dialogs/ConfirmRedactDialog-test.tsx @@ -6,14 +6,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { Feature, ServerSupport } from "matrix-js-sdk/src/feature"; -import { MatrixClient, MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix"; +import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; import { screen, act } from "jest-matrix-react"; import userEvent from "@testing-library/user-event"; import { flushPromises, mkEvent, stubClient } from "../../../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils"; -import { VoiceBroadcastInfoState } from "../../../../../src/voice-broadcast"; import { createRedactEventDialog } from "../../../../../src/components/views/dialogs/ConfirmRedactDialog"; describe("ConfirmRedactDialog", () => { @@ -21,15 +18,6 @@ describe("ConfirmRedactDialog", () => { let client: MatrixClient; let mxEvent: MatrixEvent; - const setUpVoiceBroadcastStartedEvent = () => { - mxEvent = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Started, - client.getUserId()!, - client.deviceId!, - ); - }; - const confirmDeleteVoiceBroadcastStartedEvent = async () => { act(() => createRedactEventDialog({ mxEvent })); // double-flush promises required for the dialog to show up @@ -68,44 +56,4 @@ describe("ConfirmRedactDialog", () => { `cannot redact event ${mxEvent.getId()} without room ID`, ); }); - - describe("when redacting a voice broadcast started event", () => { - beforeEach(() => { - setUpVoiceBroadcastStartedEvent(); - }); - - describe("and the server does not support relation based redactions", () => { - beforeEach(() => { - client.canSupport.set(Feature.RelationBasedRedactions, ServerSupport.Unsupported); - }); - - describe("and displaying and confirm the dialog for a voice broadcast", () => { - beforeEach(async () => { - await confirmDeleteVoiceBroadcastStartedEvent(); - }); - - it("should call redact without `with_rel_types`", () => { - expect(client.redactEvent).toHaveBeenCalledWith(roomId, mxEvent.getId(), undefined, {}); - }); - }); - }); - - describe("and the server supports relation based redactions", () => { - beforeEach(() => { - client.canSupport.set(Feature.RelationBasedRedactions, ServerSupport.Unstable); - }); - - describe("and displaying and confirm the dialog for a voice broadcast", () => { - beforeEach(async () => { - await confirmDeleteVoiceBroadcastStartedEvent(); - }); - - it("should call redact with `with_rel_types`", () => { - expect(client.redactEvent).toHaveBeenCalledWith(roomId, mxEvent.getId(), undefined, { - with_rel_types: [RelationType.Reference], - }); - }); - }); - }); - }); }); diff --git a/test/unit-tests/components/views/dialogs/ForwardDialog-test.tsx b/test/unit-tests/components/views/dialogs/ForwardDialog-test.tsx index 6942f76cda..7307417b07 100644 --- a/test/unit-tests/components/views/dialogs/ForwardDialog-test.tsx +++ b/test/unit-tests/components/views/dialogs/ForwardDialog-test.tsx @@ -67,7 +67,6 @@ describe("ForwardDialog", () => { getAccountData: jest.fn().mockReturnValue(accountDataEvent), getPushActionsForEvent: jest.fn(), mxcUrlToHttp: jest.fn().mockReturnValue(""), - isRoomEncrypted: jest.fn().mockReturnValue(false), getProfileInfo: jest.fn().mockResolvedValue({ displayname: "Alice", }), diff --git a/test/unit-tests/components/views/dialogs/InviteDialog-test.tsx b/test/unit-tests/components/views/dialogs/InviteDialog-test.tsx index 62cb01b46e..7ed57a7d70 100644 --- a/test/unit-tests/components/views/dialogs/InviteDialog-test.tsx +++ b/test/unit-tests/components/views/dialogs/InviteDialog-test.tsx @@ -141,16 +141,19 @@ describe("InviteDialog", () => { jest.clearAllMocks(); room = new Room(roomId, mockClient, mockClient.getSafeUserId()); - room.addLiveEvents([ - mkMessage({ - msg: "Hello", - relatesTo: undefined, - event: true, - room: roomId, - user: mockClient.getSafeUserId(), - ts: Date.now(), - }), - ]); + room.addLiveEvents( + [ + mkMessage({ + msg: "Hello", + relatesTo: undefined, + event: true, + room: roomId, + user: mockClient.getSafeUserId(), + ts: Date.now(), + }), + ], + { addToState: true }, + ); room.currentState.setStateEvents([ mkRoomCreateEvent(bobId, roomId), mkMembership({ diff --git a/test/unit-tests/components/views/dialogs/LogoutDialog-test.tsx b/test/unit-tests/components/views/dialogs/LogoutDialog-test.tsx index 46fe519b47..0557e538d0 100644 --- a/test/unit-tests/components/views/dialogs/LogoutDialog-test.tsx +++ b/test/unit-tests/components/views/dialogs/LogoutDialog-test.tsx @@ -22,7 +22,6 @@ describe("LogoutDialog", () => { beforeEach(() => { mockClient = getMockClientWithEventEmitter({ ...mockClientMethodsCrypto(), - getKeyBackupVersion: jest.fn(), }); mockCrypto = mocked(mockClient.getCrypto()!); @@ -50,14 +49,14 @@ describe("LogoutDialog", () => { }); it("Prompts user to connect backup if there is a backup on the server", async () => { - mockClient.getKeyBackupVersion.mockResolvedValue({} as KeyBackupInfo); + mockCrypto.getKeyBackupInfo.mockResolvedValue({} as KeyBackupInfo); const rendered = renderComponent(); await rendered.findByText("Connect this session to Key Backup"); expect(rendered.container).toMatchSnapshot(); }); it("Prompts user to set up backup if there is no backup on the server", async () => { - mockClient.getKeyBackupVersion.mockResolvedValue(null); + mockCrypto.getKeyBackupInfo.mockResolvedValue(null); const rendered = renderComponent(); await rendered.findByText("Start using Key Backup"); expect(rendered.container).toMatchSnapshot(); @@ -69,7 +68,7 @@ describe("LogoutDialog", () => { describe("when there is an error fetching backups", () => { filterConsole("Unable to fetch key backup status"); it("prompts user to set up backup", async () => { - mockClient.getKeyBackupVersion.mockImplementation(async () => { + mockCrypto.getKeyBackupInfo.mockImplementation(async () => { throw new Error("beep"); }); const rendered = renderComponent(); diff --git a/test/unit-tests/components/views/dialogs/ManualDeviceKeyVerificationDialog-test.tsx b/test/unit-tests/components/views/dialogs/ManualDeviceKeyVerificationDialog-test.tsx deleted file mode 100644 index cb01b346de..0000000000 --- a/test/unit-tests/components/views/dialogs/ManualDeviceKeyVerificationDialog-test.tsx +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2024 New Vector Ltd. - * Copyright 2023 The Matrix.org Foundation C.I.C. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only - * Please see LICENSE files in the repository root for full details. - */ - -import React from "react"; -import { render, screen } from "jest-matrix-react"; -import { Device, MatrixClient } from "matrix-js-sdk/src/matrix"; - -import { stubClient } from "../../../../test-utils"; -import { ManualDeviceKeyVerificationDialog } from "../../../../../src/components/views/dialogs/ManualDeviceKeyVerificationDialog"; -import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; - -describe("ManualDeviceKeyVerificationDialog", () => { - let mockClient: MatrixClient; - - function renderDialog(userId: string, device: Device, onLegacyFinished: (confirm: boolean) => void) { - return render( - <MatrixClientContext.Provider value={mockClient}> - <ManualDeviceKeyVerificationDialog userId={userId} device={device} onFinished={onLegacyFinished} /> - </MatrixClientContext.Provider>, - ); - } - - beforeEach(() => { - mockClient = stubClient(); - }); - - it("should display the device", () => { - // When - const deviceId = "XYZ"; - const device = new Device({ - userId: mockClient.getUserId()!, - deviceId, - displayName: "my device", - algorithms: [], - keys: new Map([[`ed25519:${deviceId}`, "ABCDEFGH"]]), - }); - const { container } = renderDialog(mockClient.getUserId()!, device, jest.fn()); - - // Then - expect(container).toMatchSnapshot(); - }); - - it("should display the device of another user", () => { - // When - const userId = "@alice:example.com"; - const deviceId = "XYZ"; - const device = new Device({ - userId, - deviceId, - displayName: "my device", - algorithms: [], - keys: new Map([[`ed25519:${deviceId}`, "ABCDEFGH"]]), - }); - const { container } = renderDialog(userId, device, jest.fn()); - - // Then - expect(container).toMatchSnapshot(); - }); - - it("should call onFinished and matrixClient.setDeviceVerified", () => { - // When - const deviceId = "XYZ"; - const device = new Device({ - userId: mockClient.getUserId()!, - deviceId, - displayName: "my device", - algorithms: [], - keys: new Map([[`ed25519:${deviceId}`, "ABCDEFGH"]]), - }); - const onFinished = jest.fn(); - renderDialog(mockClient.getUserId()!, device, onFinished); - - screen.getByRole("button", { name: "Verify session" }).click(); - - // Then - expect(onFinished).toHaveBeenCalledWith(true); - expect(mockClient.setDeviceVerified).toHaveBeenCalledWith(mockClient.getUserId(), deviceId, true); - }); - - it("should call onFinished and not matrixClient.setDeviceVerified", () => { - // When - const deviceId = "XYZ"; - const device = new Device({ - userId: mockClient.getUserId()!, - deviceId, - displayName: "my device", - algorithms: [], - keys: new Map([[`ed25519:${deviceId}`, "ABCDEFGH"]]), - }); - const onFinished = jest.fn(); - renderDialog(mockClient.getUserId()!, device, onFinished); - - screen.getByRole("button", { name: "Cancel" }).click(); - - // Then - expect(onFinished).toHaveBeenCalledWith(false); - expect(mockClient.setDeviceVerified).not.toHaveBeenCalled(); - }); -}); diff --git a/test/unit-tests/components/views/dialogs/ShareDialog-test.tsx b/test/unit-tests/components/views/dialogs/ShareDialog-test.tsx index cb7d556235..c1d9883b7f 100644 --- a/test/unit-tests/components/views/dialogs/ShareDialog-test.tsx +++ b/test/unit-tests/components/views/dialogs/ShareDialog-test.tsx @@ -7,111 +7,139 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; -import { EventTimeline, MatrixEvent, Room, RoomMember } from "matrix-js-sdk/src/matrix"; -import { render, RenderOptions } from "jest-matrix-react"; +import { MatrixClient, MatrixEvent, Room, RoomMember } from "matrix-js-sdk/src/matrix"; +import { render, screen, act } from "jest-matrix-react"; +import userEvent from "@testing-library/user-event"; +import { waitFor } from "@testing-library/dom"; -import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; import SettingsStore from "../../../../../src/settings/SettingsStore"; -import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; -import { _t } from "../../../../../src/languageHandler"; -import ShareDialog from "../../../../../src/components/views/dialogs/ShareDialog"; +import { ShareDialog } from "../../../../../src/components/views/dialogs/ShareDialog"; import { UIFeature } from "../../../../../src/settings/UIFeature"; -import { stubClient } from "../../../../test-utils"; -jest.mock("../../../../../src/utils/ShieldUtils"); - -function getWrapper(): RenderOptions { - return { - wrapper: ({ children }) => ( - <MatrixClientContext.Provider value={MatrixClientPeg.safeGet()}>{children}</MatrixClientContext.Provider> - ), - }; -} +import { stubClient, withClientContextRenderOptions } from "../../../../test-utils"; +import * as StringsModule from "../../../../../src/utils/strings"; +import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks.ts"; describe("ShareDialog", () => { + let client: MatrixClient; let room: Room; - - const ROOM_ID = "!1:example.org"; + const copyTextFunc = jest.fn(); beforeEach(async () => { - stubClient(); - room = new Room(ROOM_ID, MatrixClientPeg.get()!, "@alice:example.org"); + client = stubClient(); + room = new Room("!1:example.org", client, "@alice:example.org"); + jest.spyOn(StringsModule, "copyPlaintext").mockImplementation(copyTextFunc); }); afterEach(() => { jest.restoreAllMocks(); + copyTextFunc.mockClear(); }); - it("renders room share dialog", () => { - const { container: withoutEvents } = render(<ShareDialog target={room} onFinished={jest.fn()} />, getWrapper()); - expect(withoutEvents).toHaveTextContent(_t("share|title_room")); + function renderComponent(target: Room | RoomMember | URL) { + return render(<ShareDialog target={target} onFinished={jest.fn()} />, withClientContextRenderOptions(client)); + } - jest.spyOn(room, "getLiveTimeline").mockReturnValue({ getEvents: () => [{} as MatrixEvent] } as EventTimeline); - const { container: withEvents } = render(<ShareDialog target={room} onFinished={jest.fn()} />, getWrapper()); - expect(withEvents).toHaveTextContent(_t("share|permalink_most_recent")); + const getUrl = () => new URL("https://matrix.org/"); + const getRoomMember = () => new RoomMember(room.roomId, "@alice:example.org"); + + test.each([ + { name: "an URL", title: "Share Link", url: "https://matrix.org/", getTarget: getUrl }, + { + name: "a room member", + title: "Share User", + url: "https://matrix.to/#/@alice:example.org", + getTarget: getRoomMember, + }, + ])("should render a share dialog for $name", async ({ title, url, getTarget }) => { + const { asFragment } = renderComponent(getTarget()); + + expect(screen.getByRole("heading", { name: title })).toBeInTheDocument(); + expect(screen.getByText(url)).toBeInTheDocument(); + expect(asFragment()).toMatchSnapshot(); + + await userEvent.click(screen.getByRole("button", { name: "Copy link" })); + expect(copyTextFunc).toHaveBeenCalledWith(url); }); - it("renders user share dialog", () => { - mockRoomMembers(room, 1); - const { container } = render( - <ShareDialog target={room.getJoinedMembers()[0]} onFinished={jest.fn()} />, - getWrapper(), + it("should render a share dialog for a room", async () => { + const expectedURL = "https://matrix.to/#/!1:example.org"; + jest.spyOn(room.getLiveTimeline(), "getEvents").mockReturnValue([new MatrixEvent({ event_id: "!eventId" })]); + + const { asFragment } = renderComponent(room); + expect(screen.getByRole("heading", { name: "Share Room" })).toBeInTheDocument(); + expect(screen.getByText(expectedURL)).toBeInTheDocument(); + expect(screen.getByRole("checkbox", { name: "Link to most recent message" })).toBeInTheDocument(); + expect(asFragment()).toMatchSnapshot(); + + await userEvent.click(screen.getByRole("button", { name: "Copy link" })); + expect(copyTextFunc).toHaveBeenCalledWith(expectedURL); + + // Click on the checkbox to link to the most recent message + await userEvent.click(screen.getByRole("checkbox", { name: "Link to most recent message" })); + const newExpectedURL = "https://matrix.to/#/!1:example.org/!eventId"; + expect(screen.getByText(newExpectedURL)).toBeInTheDocument(); + }); + + it("should render a share dialog for a matrix event", async () => { + const matrixEvent = new MatrixEvent({ event_id: "!eventId" }); + const permalinkCreator = new RoomPermalinkCreator(room); + const expectedURL = "https://matrix.to/#/!1:example.org/!eventId"; + + const { asFragment } = render( + <ShareDialog target={matrixEvent} permalinkCreator={permalinkCreator} onFinished={jest.fn()} />, + withClientContextRenderOptions(client), ); - expect(container).toHaveTextContent(_t("share|title_user")); + expect(screen.getByRole("heading", { name: "Share Room Message" })).toBeInTheDocument(); + expect(screen.getByText(expectedURL)).toBeInTheDocument(); + expect(screen.getByRole("checkbox", { name: "Link to selected message" })).toBeChecked(); + expect(asFragment()).toMatchSnapshot(); + + await userEvent.click(screen.getByRole("button", { name: "Copy link" })); + expect(copyTextFunc).toHaveBeenCalledWith(expectedURL); + + // Click on the checkbox to link to the room + await userEvent.click(screen.getByRole("checkbox", { name: "Link to selected message" })); + expect(screen.getByText("https://matrix.to/#/!1:example.org")).toBeInTheDocument(); }); - it("renders link share dialog", () => { - mockRoomMembers(room, 1); - const { container } = render( - <ShareDialog target={new URL("https://matrix.org")} onFinished={jest.fn()} />, - getWrapper(), - ); - expect(container).toHaveTextContent(_t("share|title_link")); + it("should change the copy button text when clicked", async () => { + jest.useFakeTimers(); + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + // To not be bother with rtl warnings about QR code state update + jest.spyOn(SettingsStore, "getValue").mockReturnValue(false); + + renderComponent(room); + await user.click(screen.getByRole("button", { name: "Copy link" })); + // Move after `copyPlaintext` + await jest.advanceTimersToNextTimerAsync(); + expect(screen.getByRole("button", { name: "Link copied" })).toBeInTheDocument(); + + // 2 sec after the button should be back to normal + act(() => jest.advanceTimersByTime(2000)); + await waitFor(() => expect(screen.getByRole("button", { name: "Copy link" })).toBeInTheDocument()); }); - it("renders the QR code if configured", () => { + it("should not render the QR code if disabled", () => { const originalGetValue = SettingsStore.getValue; jest.spyOn(SettingsStore, "getValue").mockImplementation((feature) => { - if (feature === UIFeature.ShareQRCode) return true; + if (feature === UIFeature.ShareQRCode) return false; return originalGetValue(feature); }); - const { container } = render(<ShareDialog target={room} onFinished={jest.fn()} />, getWrapper()); - const qrCodesVisible = container.getElementsByClassName("mx_ShareDialog_qrcode_container").length > 0; - expect(qrCodesVisible).toBe(true); + + const { asFragment } = renderComponent(room); + expect(screen.queryByRole("img", { name: "QR code" })).toBeNull(); + expect(asFragment()).toMatchSnapshot(); }); - it("renders the social button if configured", () => { + it("should not render the socials if disabled", () => { const originalGetValue = SettingsStore.getValue; jest.spyOn(SettingsStore, "getValue").mockImplementation((feature) => { - if (feature === UIFeature.ShareSocial) return true; + if (feature === UIFeature.ShareSocial) return false; return originalGetValue(feature); }); - const { container } = render(<ShareDialog target={room} onFinished={jest.fn()} />, getWrapper()); - const qrCodesVisible = container.getElementsByClassName("mx_ShareDialog_social_container").length > 0; - expect(qrCodesVisible).toBe(true); - }); - it("renders custom title and subtitle", () => { - const { container } = render( - <ShareDialog - target={room} - customTitle="test_title_123" - subtitle="custom_subtitle_1234" - onFinished={jest.fn()} - />, - getWrapper(), - ); - expect(container).toHaveTextContent("test_title_123"); - expect(container).toHaveTextContent("custom_subtitle_1234"); + + const { asFragment } = renderComponent(room); + expect(screen.queryByRole("link", { name: "Reddit" })).toBeNull(); + expect(asFragment()).toMatchSnapshot(); }); }); -/** - * - * @param count the number of users to create - */ -function mockRoomMembers(room: Room, count: number) { - const members = Array(count) - .fill(0) - .map((_, index) => new RoomMember(room.roomId, "@alice:example.org")); - - room.currentState.setJoinedMemberCount(members.length); - room.getJoinedMembers = jest.fn().mockReturnValue(members); -} diff --git a/test/unit-tests/components/views/dialogs/UntrustedDeviceDialog-test.tsx b/test/unit-tests/components/views/dialogs/UntrustedDeviceDialog-test.tsx new file mode 100644 index 0000000000..3bfafa394b --- /dev/null +++ b/test/unit-tests/components/views/dialogs/UntrustedDeviceDialog-test.tsx @@ -0,0 +1,59 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only + * Please see LICENSE files in the repository root for full details. + */ + +import React from "react"; +import { Device, MatrixClient, User } from "matrix-js-sdk/src/matrix"; +import { render, screen } from "jest-matrix-react"; + +import { stubClient } from "../../../../test-utils"; +import UntrustedDeviceDialog from "../../../../../src/components/views/dialogs/UntrustedDeviceDialog.tsx"; + +describe("<UntrustedDeviceDialog />", () => { + let client: MatrixClient; + let user: User; + let device: Device; + const onFinished = jest.fn(); + + beforeEach(() => { + client = stubClient(); + user = User.createUser("@alice:example.org", client); + user.setDisplayName("Alice"); + device = new Device({ deviceId: "device_id", userId: user.userId, algorithms: [], keys: new Map() }); + }); + + afterEach(() => { + onFinished.mockReset(); + }); + + function renderComponent() { + return render(<UntrustedDeviceDialog user={user} device={device} onFinished={onFinished} />); + } + + it("should display the dialog for the device of another user", () => { + const { asFragment } = renderComponent(); + expect(asFragment()).toMatchSnapshot(); + }); + + it("should display the dialog for the device of the current user", () => { + jest.spyOn(client, "getUserId").mockReturnValue(user.userId); + + const { asFragment } = renderComponent(); + expect(asFragment()).toMatchSnapshot(); + }); + + it("should call onFinished without parameter when Done is clicked", () => { + renderComponent(); + screen.getByRole("button", { name: "Done" }).click(); + expect(onFinished).toHaveBeenCalledWith(); + }); + + it("should call onFinished with sas when Interactively verify by emoji is clicked", () => { + renderComponent(); + screen.getByRole("button", { name: "Interactively verify by emoji" }).click(); + expect(onFinished).toHaveBeenCalledWith("sas"); + }); +}); diff --git a/test/unit-tests/components/views/dialogs/__snapshots__/DevtoolsDialog-test.tsx.snap b/test/unit-tests/components/views/dialogs/__snapshots__/DevtoolsDialog-test.tsx.snap index 910569c112..622ed32065 100644 --- a/test/unit-tests/components/views/dialogs/__snapshots__/DevtoolsDialog-test.tsx.snap +++ b/test/unit-tests/components/views/dialogs/__snapshots__/DevtoolsDialog-test.tsx.snap @@ -185,33 +185,6 @@ exports[`DevtoolsDialog renders the devtools dialog 1`] = ` /> </div> </div> - <div - class="mx_SettingsFlag" - > - <label - class="mx_SettingsFlag_label" - for="mx_SettingsFlag_4yVCeEefiPqp" - > - <span - class="mx_SettingsFlag_labelText" - > - Force 15s voice broadcast chunk length - </span> - </label> - <div - aria-checked="false" - aria-disabled="false" - aria-label="Force 15s voice broadcast chunk length" - class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled" - id="mx_SettingsFlag_4yVCeEefiPqp" - role="switch" - tabindex="0" - > - <div - class="mx_ToggleSwitch_ball" - /> - </div> - </div> </div> </div> <div diff --git a/test/unit-tests/components/views/dialogs/__snapshots__/ManualDeviceKeyVerificationDialog-test.tsx.snap b/test/unit-tests/components/views/dialogs/__snapshots__/ManualDeviceKeyVerificationDialog-test.tsx.snap deleted file mode 100644 index 1186f06f38..0000000000 --- a/test/unit-tests/components/views/dialogs/__snapshots__/ManualDeviceKeyVerificationDialog-test.tsx.snap +++ /dev/null @@ -1,231 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ManualDeviceKeyVerificationDialog should display the device 1`] = ` -<div> - <div - data-focus-guard="true" - style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;" - tabindex="0" - /> - <div - aria-describedby="mx_Dialog_content" - aria-labelledby="mx_BaseDialog_title" - class="mx_QuestionDialog mx_Dialog_fixedWidth" - data-focus-lock-disabled="false" - role="dialog" - > - <div - class="mx_Dialog_header" - > - <h1 - class="mx_Heading_h3 mx_Dialog_title" - id="mx_BaseDialog_title" - > - Verify session - </h1> - </div> - <div - class="mx_Dialog_content" - id="mx_Dialog_content" - > - <div> - <p> - Confirm by comparing the following with the User Settings in your other session: - </p> - <div - class="mx_DeviceVerifyDialog_cryptoSection" - > - <ul> - <li> - <label> - Session name - : - </label> - - <span> - my device - </span> - </li> - <li> - <label> - Session ID - : - </label> - - <span> - <code> - XYZ - </code> - </span> - </li> - <li> - <label> - Session key - : - </label> - - <span> - <code> - <strong> - ABCD EFGH - </strong> - </code> - </span> - </li> - </ul> - </div> - <p> - If they don't match, the security of your communication may be compromised. - </p> - </div> - </div> - <div - class="mx_Dialog_buttons" - > - <span - class="mx_Dialog_buttons_row" - > - <button - data-testid="dialog-cancel-button" - type="button" - > - Cancel - </button> - <button - class="mx_Dialog_primary" - data-testid="dialog-primary-button" - type="button" - > - Verify session - </button> - </span> - </div> - <div - aria-label="Close dialog" - class="mx_AccessibleButton mx_Dialog_cancelButton" - role="button" - tabindex="0" - /> - </div> - <div - data-focus-guard="true" - style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;" - tabindex="0" - /> -</div> -`; - -exports[`ManualDeviceKeyVerificationDialog should display the device of another user 1`] = ` -<div> - <div - data-focus-guard="true" - style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;" - tabindex="0" - /> - <div - aria-describedby="mx_Dialog_content" - aria-labelledby="mx_BaseDialog_title" - class="mx_QuestionDialog mx_Dialog_fixedWidth" - data-focus-lock-disabled="false" - role="dialog" - > - <div - class="mx_Dialog_header" - > - <h1 - class="mx_Heading_h3 mx_Dialog_title" - id="mx_BaseDialog_title" - > - Verify session - </h1> - </div> - <div - class="mx_Dialog_content" - id="mx_Dialog_content" - > - <div> - <p> - Confirm this user's session by comparing the following with their User Settings: - </p> - <div - class="mx_DeviceVerifyDialog_cryptoSection" - > - <ul> - <li> - <label> - Session name - : - </label> - - <span> - my device - </span> - </li> - <li> - <label> - Session ID - : - </label> - - <span> - <code> - XYZ - </code> - </span> - </li> - <li> - <label> - Session key - : - </label> - - <span> - <code> - <strong> - ABCD EFGH - </strong> - </code> - </span> - </li> - </ul> - </div> - <p> - If they don't match, the security of your communication may be compromised. - </p> - </div> - </div> - <div - class="mx_Dialog_buttons" - > - <span - class="mx_Dialog_buttons_row" - > - <button - data-testid="dialog-cancel-button" - type="button" - > - Cancel - </button> - <button - class="mx_Dialog_primary" - data-testid="dialog-primary-button" - type="button" - > - Verify session - </button> - </span> - </div> - <div - aria-label="Close dialog" - class="mx_AccessibleButton mx_Dialog_cancelButton" - role="button" - tabindex="0" - /> - </div> - <div - data-focus-guard="true" - style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;" - tabindex="0" - /> -</div> -`; diff --git a/test/unit-tests/components/views/dialogs/__snapshots__/ShareDialog-test.tsx.snap b/test/unit-tests/components/views/dialogs/__snapshots__/ShareDialog-test.tsx.snap new file mode 100644 index 0000000000..ab8b8ffb58 --- /dev/null +++ b/test/unit-tests/components/views/dialogs/__snapshots__/ShareDialog-test.tsx.snap @@ -0,0 +1,852 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ShareDialog should not render the QR code if disabled 1`] = ` +<DocumentFragment> + <div + data-focus-guard="true" + style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;" + tabindex="0" + /> + <div + aria-describedby="mx_Dialog_content" + aria-labelledby="mx_BaseDialog_title" + class="mx_ShareDialog" + data-focus-lock-disabled="false" + role="dialog" + > + <div + class="mx_Dialog_header" + > + <h1 + class="mx_Heading_h3 mx_Dialog_title" + id="mx_BaseDialog_title" + > + Share Room + </h1> + </div> + <div + class="mx_ShareDialog_content" + > + <div + class="mx_ShareDialog_top" + > + <span> + https://matrix.to/#/!1:example.org + </span> + </div> + <button + class="_button_i91xf_17 _has-icon_i91xf_66" + data-kind="primary" + data-size="lg" + role="button" + tabindex="0" + > + <svg + aria-hidden="true" + fill="currentColor" + height="20" + viewBox="0 0 24 24" + width="20" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M12 19.071c-.978.978-2.157 1.467-3.536 1.467-1.378 0-2.557-.489-3.535-1.467-.978-.978-1.467-2.157-1.467-3.536 0-1.378.489-2.557 1.467-3.535L7.05 9.879c.2-.2.436-.3.707-.3.271 0 .507.1.707.3.2.2.301.436.301.707 0 .27-.1.506-.3.707l-2.122 2.121a2.893 2.893 0 0 0-.884 2.122c0 .824.295 1.532.884 2.12.59.59 1.296.885 2.121.885s1.533-.295 2.122-.884l2.121-2.121c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707L12 19.07Zm-1.414-4.243c-.2.2-.436.3-.707.3a.967.967 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l4.243-4.242c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707l-4.242 4.242Zm6.364-.707c-.2.2-.436.3-.707.3a.968.968 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l2.122-2.121c.59-.59.884-1.297.884-2.122s-.295-1.532-.884-2.12a2.893 2.893 0 0 0-2.121-.885c-.825 0-1.532.295-2.122.884l-2.121 2.121c-.2.2-.436.301-.707.301a.968.968 0 0 1-.707-.3.97.97 0 0 1-.3-.708c0-.27.1-.506.3-.707L12 4.93c.978-.978 2.157-1.467 3.536-1.467 1.378 0 2.557.489 3.535 1.467.978.978 1.467 2.157 1.467 3.535 0 1.38-.489 2.558-1.467 3.536l-2.121 2.121Z" + /> + </svg> + Copy link + </button> + <div + class="mx_ShareDialog_social" + > + <a + href="https://www.facebook.com/sharer/sharer.php?u=https://matrix.to/#/!1:example.org" + rel="noreferrer noopener" + target="_blank" + title="Facebook" + > + <img + alt="Facebook" + src="image-file-stub" + /> + </a> + <a + href="https://twitter.com/home?status=https://matrix.to/#/!1:example.org" + rel="noreferrer noopener" + target="_blank" + title="Twitter" + > + <img + alt="Twitter" + src="image-file-stub" + /> + </a> + <a + href="https://www.linkedin.com/shareArticle?mini=true&url=https://matrix.to/#/!1:example.org" + rel="noreferrer noopener" + target="_blank" + title="LinkedIn" + > + <img + alt="LinkedIn" + src="image-file-stub" + /> + </a> + <a + href="https://www.reddit.com/submit?url=https://matrix.to/#/!1:example.org" + rel="noreferrer noopener" + target="_blank" + title="Reddit" + > + <img + alt="Reddit" + src="image-file-stub" + /> + </a> + <a + href="mailto:?body=https://matrix.to/#/!1:example.org" + rel="noreferrer noopener" + target="_blank" + title="email" + > + <img + alt="email" + src="image-file-stub" + /> + </a> + </div> + </div> + <div + aria-label="Close dialog" + class="mx_AccessibleButton mx_Dialog_cancelButton" + role="button" + tabindex="0" + /> + </div> + <div + data-focus-guard="true" + style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;" + tabindex="0" + /> +</DocumentFragment> +`; + +exports[`ShareDialog should not render the socials if disabled 1`] = ` +<DocumentFragment> + <div + data-focus-guard="true" + style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;" + tabindex="0" + /> + <div + aria-describedby="mx_Dialog_content" + aria-labelledby="mx_BaseDialog_title" + class="mx_ShareDialog" + data-focus-lock-disabled="false" + role="dialog" + > + <div + class="mx_Dialog_header" + > + <h1 + class="mx_Heading_h3 mx_Dialog_title" + id="mx_BaseDialog_title" + > + Share Room + </h1> + </div> + <div + class="mx_ShareDialog_content" + > + <div + class="mx_ShareDialog_top" + > + <div + class="mx_QRCode" + > + <div + class="mx_Spinner" + > + <div + aria-label="Loading…" + class="mx_Spinner_icon" + data-testid="spinner" + role="progressbar" + style="width: 32px; height: 32px;" + /> + </div> + </div> + <span> + https://matrix.to/#/!1:example.org + </span> + </div> + <button + class="_button_i91xf_17 _has-icon_i91xf_66" + data-kind="primary" + data-size="lg" + role="button" + tabindex="0" + > + <svg + aria-hidden="true" + fill="currentColor" + height="20" + viewBox="0 0 24 24" + width="20" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M12 19.071c-.978.978-2.157 1.467-3.536 1.467-1.378 0-2.557-.489-3.535-1.467-.978-.978-1.467-2.157-1.467-3.536 0-1.378.489-2.557 1.467-3.535L7.05 9.879c.2-.2.436-.3.707-.3.271 0 .507.1.707.3.2.2.301.436.301.707 0 .27-.1.506-.3.707l-2.122 2.121a2.893 2.893 0 0 0-.884 2.122c0 .824.295 1.532.884 2.12.59.59 1.296.885 2.121.885s1.533-.295 2.122-.884l2.121-2.121c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707L12 19.07Zm-1.414-4.243c-.2.2-.436.3-.707.3a.967.967 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l4.243-4.242c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707l-4.242 4.242Zm6.364-.707c-.2.2-.436.3-.707.3a.968.968 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l2.122-2.121c.59-.59.884-1.297.884-2.122s-.295-1.532-.884-2.12a2.893 2.893 0 0 0-2.121-.885c-.825 0-1.532.295-2.122.884l-2.121 2.121c-.2.2-.436.301-.707.301a.968.968 0 0 1-.707-.3.97.97 0 0 1-.3-.708c0-.27.1-.506.3-.707L12 4.93c.978-.978 2.157-1.467 3.536-1.467 1.378 0 2.557.489 3.535 1.467.978.978 1.467 2.157 1.467 3.535 0 1.38-.489 2.558-1.467 3.536l-2.121 2.121Z" + /> + </svg> + Copy link + </button> + </div> + <div + aria-label="Close dialog" + class="mx_AccessibleButton mx_Dialog_cancelButton" + role="button" + tabindex="0" + /> + </div> + <div + data-focus-guard="true" + style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;" + tabindex="0" + /> +</DocumentFragment> +`; + +exports[`ShareDialog should render a share dialog for a matrix event 1`] = ` +<DocumentFragment> + <div + data-focus-guard="true" + style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;" + tabindex="0" + /> + <div + aria-describedby="mx_Dialog_content" + aria-labelledby="mx_BaseDialog_title" + class="mx_ShareDialog" + data-focus-lock-disabled="false" + role="dialog" + > + <div + class="mx_Dialog_header" + > + <h1 + class="mx_Heading_h3 mx_Dialog_title" + id="mx_BaseDialog_title" + > + Share Room Message + </h1> + </div> + <div + class="mx_ShareDialog_content" + > + <div + class="mx_ShareDialog_top" + > + <div + class="mx_QRCode" + > + <div + class="mx_Spinner" + > + <div + aria-label="Loading…" + class="mx_Spinner_icon" + data-testid="spinner" + role="progressbar" + style="width: 32px; height: 32px;" + /> + </div> + </div> + <span> + https://matrix.to/#/!1:example.org/!eventId + </span> + </div> + <label> + <div + class="_container_1wloq_18" + > + <input + checked="" + class="_input_1wloq_26" + type="checkbox" + /> + <div + class="_ui_1wloq_27" + > + <svg + aria-hidden="true" + fill="currentColor" + height="1em" + viewBox="0 0 24 24" + width="1em" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M9.55 17.575c-.133 0-.258-.02-.375-.063a.876.876 0 0 1-.325-.212L4.55 13c-.183-.183-.27-.42-.263-.713.009-.291.105-.529.288-.712a.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275L9.55 15.15l8.475-8.475c.183-.183.42-.275.713-.275.291 0 .529.092.712.275.183.183.275.42.275.713 0 .291-.092.529-.275.712l-9.2 9.2c-.1.1-.208.17-.325.212a1.106 1.106 0 0 1-.375.063Z" + /> + </svg> + </div> + </div> + Link to selected message + </label> + <button + class="_button_i91xf_17 _has-icon_i91xf_66" + data-kind="primary" + data-size="lg" + role="button" + tabindex="0" + > + <svg + aria-hidden="true" + fill="currentColor" + height="20" + viewBox="0 0 24 24" + width="20" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M12 19.071c-.978.978-2.157 1.467-3.536 1.467-1.378 0-2.557-.489-3.535-1.467-.978-.978-1.467-2.157-1.467-3.536 0-1.378.489-2.557 1.467-3.535L7.05 9.879c.2-.2.436-.3.707-.3.271 0 .507.1.707.3.2.2.301.436.301.707 0 .27-.1.506-.3.707l-2.122 2.121a2.893 2.893 0 0 0-.884 2.122c0 .824.295 1.532.884 2.12.59.59 1.296.885 2.121.885s1.533-.295 2.122-.884l2.121-2.121c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707L12 19.07Zm-1.414-4.243c-.2.2-.436.3-.707.3a.967.967 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l4.243-4.242c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707l-4.242 4.242Zm6.364-.707c-.2.2-.436.3-.707.3a.968.968 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l2.122-2.121c.59-.59.884-1.297.884-2.122s-.295-1.532-.884-2.12a2.893 2.893 0 0 0-2.121-.885c-.825 0-1.532.295-2.122.884l-2.121 2.121c-.2.2-.436.301-.707.301a.968.968 0 0 1-.707-.3.97.97 0 0 1-.3-.708c0-.27.1-.506.3-.707L12 4.93c.978-.978 2.157-1.467 3.536-1.467 1.378 0 2.557.489 3.535 1.467.978.978 1.467 2.157 1.467 3.535 0 1.38-.489 2.558-1.467 3.536l-2.121 2.121Z" + /> + </svg> + Copy link + </button> + <div + class="mx_ShareDialog_social" + > + <a + href="https://www.facebook.com/sharer/sharer.php?u=https://matrix.to/#/!1:example.org/!eventId" + rel="noreferrer noopener" + target="_blank" + title="Facebook" + > + <img + alt="Facebook" + src="image-file-stub" + /> + </a> + <a + href="https://twitter.com/home?status=https://matrix.to/#/!1:example.org/!eventId" + rel="noreferrer noopener" + target="_blank" + title="Twitter" + > + <img + alt="Twitter" + src="image-file-stub" + /> + </a> + <a + href="https://www.linkedin.com/shareArticle?mini=true&url=https://matrix.to/#/!1:example.org/!eventId" + rel="noreferrer noopener" + target="_blank" + title="LinkedIn" + > + <img + alt="LinkedIn" + src="image-file-stub" + /> + </a> + <a + href="https://www.reddit.com/submit?url=https://matrix.to/#/!1:example.org/!eventId" + rel="noreferrer noopener" + target="_blank" + title="Reddit" + > + <img + alt="Reddit" + src="image-file-stub" + /> + </a> + <a + href="mailto:?body=https://matrix.to/#/!1:example.org/!eventId" + rel="noreferrer noopener" + target="_blank" + title="email" + > + <img + alt="email" + src="image-file-stub" + /> + </a> + </div> + </div> + <div + aria-label="Close dialog" + class="mx_AccessibleButton mx_Dialog_cancelButton" + role="button" + tabindex="0" + /> + </div> + <div + data-focus-guard="true" + style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;" + tabindex="0" + /> +</DocumentFragment> +`; + +exports[`ShareDialog should render a share dialog for a room 1`] = ` +<DocumentFragment> + <div + data-focus-guard="true" + style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;" + tabindex="0" + /> + <div + aria-describedby="mx_Dialog_content" + aria-labelledby="mx_BaseDialog_title" + class="mx_ShareDialog" + data-focus-lock-disabled="false" + role="dialog" + > + <div + class="mx_Dialog_header" + > + <h1 + class="mx_Heading_h3 mx_Dialog_title" + id="mx_BaseDialog_title" + > + Share Room + </h1> + </div> + <div + class="mx_ShareDialog_content" + > + <div + class="mx_ShareDialog_top" + > + <div + class="mx_QRCode" + > + <div + class="mx_Spinner" + > + <div + aria-label="Loading…" + class="mx_Spinner_icon" + data-testid="spinner" + role="progressbar" + style="width: 32px; height: 32px;" + /> + </div> + </div> + <span> + https://matrix.to/#/!1:example.org + </span> + </div> + <label> + <div + class="_container_1wloq_18" + > + <input + class="_input_1wloq_26" + type="checkbox" + /> + <div + class="_ui_1wloq_27" + > + <svg + aria-hidden="true" + fill="currentColor" + height="1em" + viewBox="0 0 24 24" + width="1em" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M9.55 17.575c-.133 0-.258-.02-.375-.063a.876.876 0 0 1-.325-.212L4.55 13c-.183-.183-.27-.42-.263-.713.009-.291.105-.529.288-.712a.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275L9.55 15.15l8.475-8.475c.183-.183.42-.275.713-.275.291 0 .529.092.712.275.183.183.275.42.275.713 0 .291-.092.529-.275.712l-9.2 9.2c-.1.1-.208.17-.325.212a1.106 1.106 0 0 1-.375.063Z" + /> + </svg> + </div> + </div> + Link to most recent message + </label> + <button + class="_button_i91xf_17 _has-icon_i91xf_66" + data-kind="primary" + data-size="lg" + role="button" + tabindex="0" + > + <svg + aria-hidden="true" + fill="currentColor" + height="20" + viewBox="0 0 24 24" + width="20" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M12 19.071c-.978.978-2.157 1.467-3.536 1.467-1.378 0-2.557-.489-3.535-1.467-.978-.978-1.467-2.157-1.467-3.536 0-1.378.489-2.557 1.467-3.535L7.05 9.879c.2-.2.436-.3.707-.3.271 0 .507.1.707.3.2.2.301.436.301.707 0 .27-.1.506-.3.707l-2.122 2.121a2.893 2.893 0 0 0-.884 2.122c0 .824.295 1.532.884 2.12.59.59 1.296.885 2.121.885s1.533-.295 2.122-.884l2.121-2.121c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707L12 19.07Zm-1.414-4.243c-.2.2-.436.3-.707.3a.967.967 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l4.243-4.242c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707l-4.242 4.242Zm6.364-.707c-.2.2-.436.3-.707.3a.968.968 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l2.122-2.121c.59-.59.884-1.297.884-2.122s-.295-1.532-.884-2.12a2.893 2.893 0 0 0-2.121-.885c-.825 0-1.532.295-2.122.884l-2.121 2.121c-.2.2-.436.301-.707.301a.968.968 0 0 1-.707-.3.97.97 0 0 1-.3-.708c0-.27.1-.506.3-.707L12 4.93c.978-.978 2.157-1.467 3.536-1.467 1.378 0 2.557.489 3.535 1.467.978.978 1.467 2.157 1.467 3.535 0 1.38-.489 2.558-1.467 3.536l-2.121 2.121Z" + /> + </svg> + Copy link + </button> + <div + class="mx_ShareDialog_social" + > + <a + href="https://www.facebook.com/sharer/sharer.php?u=https://matrix.to/#/!1:example.org" + rel="noreferrer noopener" + target="_blank" + title="Facebook" + > + <img + alt="Facebook" + src="image-file-stub" + /> + </a> + <a + href="https://twitter.com/home?status=https://matrix.to/#/!1:example.org" + rel="noreferrer noopener" + target="_blank" + title="Twitter" + > + <img + alt="Twitter" + src="image-file-stub" + /> + </a> + <a + href="https://www.linkedin.com/shareArticle?mini=true&url=https://matrix.to/#/!1:example.org" + rel="noreferrer noopener" + target="_blank" + title="LinkedIn" + > + <img + alt="LinkedIn" + src="image-file-stub" + /> + </a> + <a + href="https://www.reddit.com/submit?url=https://matrix.to/#/!1:example.org" + rel="noreferrer noopener" + target="_blank" + title="Reddit" + > + <img + alt="Reddit" + src="image-file-stub" + /> + </a> + <a + href="mailto:?body=https://matrix.to/#/!1:example.org" + rel="noreferrer noopener" + target="_blank" + title="email" + > + <img + alt="email" + src="image-file-stub" + /> + </a> + </div> + </div> + <div + aria-label="Close dialog" + class="mx_AccessibleButton mx_Dialog_cancelButton" + role="button" + tabindex="0" + /> + </div> + <div + data-focus-guard="true" + style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;" + tabindex="0" + /> +</DocumentFragment> +`; + +exports[`ShareDialog should render a share dialog for a room member 1`] = ` +<DocumentFragment> + <div + data-focus-guard="true" + style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;" + tabindex="0" + /> + <div + aria-describedby="mx_Dialog_content" + aria-labelledby="mx_BaseDialog_title" + class="mx_ShareDialog" + data-focus-lock-disabled="false" + role="dialog" + > + <div + class="mx_Dialog_header" + > + <h1 + class="mx_Heading_h3 mx_Dialog_title" + id="mx_BaseDialog_title" + > + Share User + </h1> + </div> + <div + class="mx_ShareDialog_content" + > + <div + class="mx_ShareDialog_top" + > + <div + class="mx_QRCode" + > + <div + class="mx_Spinner" + > + <div + aria-label="Loading…" + class="mx_Spinner_icon" + data-testid="spinner" + role="progressbar" + style="width: 32px; height: 32px;" + /> + </div> + </div> + <span> + https://matrix.to/#/@alice:example.org + </span> + </div> + <button + class="_button_i91xf_17 _has-icon_i91xf_66" + data-kind="primary" + data-size="lg" + role="button" + tabindex="0" + > + <svg + aria-hidden="true" + fill="currentColor" + height="20" + viewBox="0 0 24 24" + width="20" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M12 19.071c-.978.978-2.157 1.467-3.536 1.467-1.378 0-2.557-.489-3.535-1.467-.978-.978-1.467-2.157-1.467-3.536 0-1.378.489-2.557 1.467-3.535L7.05 9.879c.2-.2.436-.3.707-.3.271 0 .507.1.707.3.2.2.301.436.301.707 0 .27-.1.506-.3.707l-2.122 2.121a2.893 2.893 0 0 0-.884 2.122c0 .824.295 1.532.884 2.12.59.59 1.296.885 2.121.885s1.533-.295 2.122-.884l2.121-2.121c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707L12 19.07Zm-1.414-4.243c-.2.2-.436.3-.707.3a.967.967 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l4.243-4.242c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707l-4.242 4.242Zm6.364-.707c-.2.2-.436.3-.707.3a.968.968 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l2.122-2.121c.59-.59.884-1.297.884-2.122s-.295-1.532-.884-2.12a2.893 2.893 0 0 0-2.121-.885c-.825 0-1.532.295-2.122.884l-2.121 2.121c-.2.2-.436.301-.707.301a.968.968 0 0 1-.707-.3.97.97 0 0 1-.3-.708c0-.27.1-.506.3-.707L12 4.93c.978-.978 2.157-1.467 3.536-1.467 1.378 0 2.557.489 3.535 1.467.978.978 1.467 2.157 1.467 3.535 0 1.38-.489 2.558-1.467 3.536l-2.121 2.121Z" + /> + </svg> + Copy link + </button> + <div + class="mx_ShareDialog_social" + > + <a + href="https://www.facebook.com/sharer/sharer.php?u=https://matrix.to/#/@alice:example.org" + rel="noreferrer noopener" + target="_blank" + title="Facebook" + > + <img + alt="Facebook" + src="image-file-stub" + /> + </a> + <a + href="https://twitter.com/home?status=https://matrix.to/#/@alice:example.org" + rel="noreferrer noopener" + target="_blank" + title="Twitter" + > + <img + alt="Twitter" + src="image-file-stub" + /> + </a> + <a + href="https://www.linkedin.com/shareArticle?mini=true&url=https://matrix.to/#/@alice:example.org" + rel="noreferrer noopener" + target="_blank" + title="LinkedIn" + > + <img + alt="LinkedIn" + src="image-file-stub" + /> + </a> + <a + href="https://www.reddit.com/submit?url=https://matrix.to/#/@alice:example.org" + rel="noreferrer noopener" + target="_blank" + title="Reddit" + > + <img + alt="Reddit" + src="image-file-stub" + /> + </a> + <a + href="mailto:?body=https://matrix.to/#/@alice:example.org" + rel="noreferrer noopener" + target="_blank" + title="email" + > + <img + alt="email" + src="image-file-stub" + /> + </a> + </div> + </div> + <div + aria-label="Close dialog" + class="mx_AccessibleButton mx_Dialog_cancelButton" + role="button" + tabindex="0" + /> + </div> + <div + data-focus-guard="true" + style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;" + tabindex="0" + /> +</DocumentFragment> +`; + +exports[`ShareDialog should render a share dialog for an URL 1`] = ` +<DocumentFragment> + <div + data-focus-guard="true" + style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;" + tabindex="0" + /> + <div + aria-describedby="mx_Dialog_content" + aria-labelledby="mx_BaseDialog_title" + class="mx_ShareDialog" + data-focus-lock-disabled="false" + role="dialog" + > + <div + class="mx_Dialog_header" + > + <h1 + class="mx_Heading_h3 mx_Dialog_title" + id="mx_BaseDialog_title" + > + Share Link + </h1> + </div> + <div + class="mx_ShareDialog_content" + > + <div + class="mx_ShareDialog_top" + > + <div + class="mx_QRCode" + > + <div + class="mx_Spinner" + > + <div + aria-label="Loading…" + class="mx_Spinner_icon" + data-testid="spinner" + role="progressbar" + style="width: 32px; height: 32px;" + /> + </div> + </div> + <span> + https://matrix.org/ + </span> + </div> + <button + class="_button_i91xf_17 _has-icon_i91xf_66" + data-kind="primary" + data-size="lg" + role="button" + tabindex="0" + > + <svg + aria-hidden="true" + fill="currentColor" + height="20" + viewBox="0 0 24 24" + width="20" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M12 19.071c-.978.978-2.157 1.467-3.536 1.467-1.378 0-2.557-.489-3.535-1.467-.978-.978-1.467-2.157-1.467-3.536 0-1.378.489-2.557 1.467-3.535L7.05 9.879c.2-.2.436-.3.707-.3.271 0 .507.1.707.3.2.2.301.436.301.707 0 .27-.1.506-.3.707l-2.122 2.121a2.893 2.893 0 0 0-.884 2.122c0 .824.295 1.532.884 2.12.59.59 1.296.885 2.121.885s1.533-.295 2.122-.884l2.121-2.121c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707L12 19.07Zm-1.414-4.243c-.2.2-.436.3-.707.3a.967.967 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l4.243-4.242c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707l-4.242 4.242Zm6.364-.707c-.2.2-.436.3-.707.3a.968.968 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l2.122-2.121c.59-.59.884-1.297.884-2.122s-.295-1.532-.884-2.12a2.893 2.893 0 0 0-2.121-.885c-.825 0-1.532.295-2.122.884l-2.121 2.121c-.2.2-.436.301-.707.301a.968.968 0 0 1-.707-.3.97.97 0 0 1-.3-.708c0-.27.1-.506.3-.707L12 4.93c.978-.978 2.157-1.467 3.536-1.467 1.378 0 2.557.489 3.535 1.467.978.978 1.467 2.157 1.467 3.535 0 1.38-.489 2.558-1.467 3.536l-2.121 2.121Z" + /> + </svg> + Copy link + </button> + <div + class="mx_ShareDialog_social" + > + <a + href="https://www.facebook.com/sharer/sharer.php?u=https://matrix.org/" + rel="noreferrer noopener" + target="_blank" + title="Facebook" + > + <img + alt="Facebook" + src="image-file-stub" + /> + </a> + <a + href="https://twitter.com/home?status=https://matrix.org/" + rel="noreferrer noopener" + target="_blank" + title="Twitter" + > + <img + alt="Twitter" + src="image-file-stub" + /> + </a> + <a + href="https://www.linkedin.com/shareArticle?mini=true&url=https://matrix.org/" + rel="noreferrer noopener" + target="_blank" + title="LinkedIn" + > + <img + alt="LinkedIn" + src="image-file-stub" + /> + </a> + <a + href="https://www.reddit.com/submit?url=https://matrix.org/" + rel="noreferrer noopener" + target="_blank" + title="Reddit" + > + <img + alt="Reddit" + src="image-file-stub" + /> + </a> + <a + href="mailto:?body=https://matrix.org/" + rel="noreferrer noopener" + target="_blank" + title="email" + > + <img + alt="email" + src="image-file-stub" + /> + </a> + </div> + </div> + <div + aria-label="Close dialog" + class="mx_AccessibleButton mx_Dialog_cancelButton" + role="button" + tabindex="0" + /> + </div> + <div + data-focus-guard="true" + style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;" + tabindex="0" + /> +</DocumentFragment> +`; diff --git a/test/unit-tests/components/views/dialogs/__snapshots__/UntrustedDeviceDialog-test.tsx.snap b/test/unit-tests/components/views/dialogs/__snapshots__/UntrustedDeviceDialog-test.tsx.snap new file mode 100644 index 0000000000..2fd5fdd249 --- /dev/null +++ b/test/unit-tests/components/views/dialogs/__snapshots__/UntrustedDeviceDialog-test.tsx.snap @@ -0,0 +1,149 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`<UntrustedDeviceDialog /> should display the dialog for the device of another user 1`] = ` +<DocumentFragment> + <div + data-focus-guard="true" + style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;" + tabindex="0" + /> + <div + aria-labelledby="mx_BaseDialog_title" + class="mx_UntrustedDeviceDialog mx_Dialog_fixedWidth" + data-focus-lock-disabled="false" + role="dialog" + > + <div + class="mx_Dialog_header" + > + <h1 + class="mx_Heading_h3 mx_Dialog_title" + id="mx_BaseDialog_title" + > + <div + class="mx_E2EIcon mx_E2EIcon_warning" + style="width: 24px; height: 24px;" + /> + Not Trusted + </h1> + </div> + <div + class="mx_Dialog_content" + id="mx_Dialog_content" + > + <p> + Alice (@alice:example.org) signed in to a new session without verifying it: + </p> + <p> + (device_id) + </p> + <p> + Ask this user to verify their session, or manually verify it below. + </p> + </div> + <div + class="mx_Dialog_buttons" + > + <div + class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline" + role="button" + tabindex="0" + > + Interactively verify by emoji + </div> + <div + class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary" + role="button" + tabindex="0" + > + Done + </div> + </div> + <div + aria-label="Close dialog" + class="mx_AccessibleButton mx_Dialog_cancelButton" + role="button" + tabindex="0" + /> + </div> + <div + data-focus-guard="true" + style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;" + tabindex="0" + /> +</DocumentFragment> +`; + +exports[`<UntrustedDeviceDialog /> should display the dialog for the device of the current user 1`] = ` +<DocumentFragment> + <div + data-focus-guard="true" + style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;" + tabindex="0" + /> + <div + aria-labelledby="mx_BaseDialog_title" + class="mx_UntrustedDeviceDialog mx_Dialog_fixedWidth" + data-focus-lock-disabled="false" + role="dialog" + > + <div + class="mx_Dialog_header" + > + <h1 + class="mx_Heading_h3 mx_Dialog_title" + id="mx_BaseDialog_title" + > + <div + class="mx_E2EIcon mx_E2EIcon_warning" + style="width: 24px; height: 24px;" + /> + Not Trusted + </h1> + </div> + <div + class="mx_Dialog_content" + id="mx_Dialog_content" + > + <p> + You signed in to a new session without verifying it: + </p> + <p> + (device_id) + </p> + <p> + Verify your other session using one of the options below. + </p> + </div> + <div + class="mx_Dialog_buttons" + > + <div + class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline" + role="button" + tabindex="0" + > + Interactively verify by emoji + </div> + <div + class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary" + role="button" + tabindex="0" + > + Done + </div> + </div> + <div + aria-label="Close dialog" + class="mx_AccessibleButton mx_Dialog_cancelButton" + role="button" + tabindex="0" + /> + </div> + <div + data-focus-guard="true" + style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;" + tabindex="0" + /> +</DocumentFragment> +`; diff --git a/test/unit-tests/components/views/dialogs/security/CreateSecretStorageDialog-test.tsx b/test/unit-tests/components/views/dialogs/security/CreateSecretStorageDialog-test.tsx index 9e792a48f3..814fe8a954 100644 --- a/test/unit-tests/components/views/dialogs/security/CreateSecretStorageDialog-test.tsx +++ b/test/unit-tests/components/views/dialogs/security/CreateSecretStorageDialog-test.tsx @@ -77,7 +77,7 @@ describe("CreateSecretStorageDialog", () => { filterConsole("Error fetching backup data from server"); it("shows an error", async () => { - mockClient.getKeyBackupVersion.mockImplementation(async () => { + jest.spyOn(mockClient.getCrypto()!, "getKeyBackupInfo").mockImplementation(async () => { throw new Error("bleh bleh"); }); @@ -92,7 +92,7 @@ describe("CreateSecretStorageDialog", () => { expect(result.container).toMatchSnapshot(); // Now we can get the backup and we retry - mockClient.getKeyBackupVersion.mockRestore(); + jest.spyOn(mockClient.getCrypto()!, "getKeyBackupInfo").mockRestore(); await userEvent.click(screen.getByRole("button", { name: "Retry" })); await screen.findByText("Your keys are now being backed up from this device."); }); diff --git a/test/unit-tests/components/views/dialogs/security/ExportE2eKeysDialog-test.tsx b/test/unit-tests/components/views/dialogs/security/ExportE2eKeysDialog-test.tsx index 6e8837c50d..df95482850 100644 --- a/test/unit-tests/components/views/dialogs/security/ExportE2eKeysDialog-test.tsx +++ b/test/unit-tests/components/views/dialogs/security/ExportE2eKeysDialog-test.tsx @@ -9,7 +9,8 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import { screen, fireEvent, render, waitFor, act } from "jest-matrix-react"; import userEvent from "@testing-library/user-event"; -import { Crypto, IMegolmSessionData } from "matrix-js-sdk/src/matrix"; +import { IMegolmSessionData } from "matrix-js-sdk/src/matrix"; +import { CryptoApi } from "matrix-js-sdk/src/crypto-api"; import * as MegolmExportEncryption from "../../../../../../src/utils/MegolmExportEncryption"; import ExportE2eKeysDialog from "../../../../../../src/async-components/views/dialogs/security/ExportE2eKeysDialog"; @@ -62,7 +63,7 @@ describe("ExportE2eKeysDialog", () => { cli.getCrypto = () => { return { exportRoomKeysAsJson, - } as unknown as Crypto.CryptoApi; + } as unknown as CryptoApi; }; // Mock the result of encrypting the sessions. If we don't do this, the diff --git a/test/unit-tests/components/views/dialogs/security/ImportE2eKeysDialog-test.tsx b/test/unit-tests/components/views/dialogs/security/ImportE2eKeysDialog-test.tsx index 1b6200dcee..57f374c252 100644 --- a/test/unit-tests/components/views/dialogs/security/ImportE2eKeysDialog-test.tsx +++ b/test/unit-tests/components/views/dialogs/security/ImportE2eKeysDialog-test.tsx @@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import { fireEvent, render, waitFor } from "jest-matrix-react"; import userEvent from "@testing-library/user-event"; -import { Crypto } from "matrix-js-sdk/src/matrix"; +import { CryptoApi } from "matrix-js-sdk/src/crypto-api"; import ImportE2eKeysDialog from "../../../../../../src/async-components/views/dialogs/security/ImportE2eKeysDialog"; import * as MegolmExportEncryption from "../../../../../../src/utils/MegolmExportEncryption"; @@ -67,7 +67,7 @@ describe("ImportE2eKeysDialog", () => { cli.getCrypto = () => { return { importRoomKeysAsJson, - } as unknown as Crypto.CryptoApi; + } as unknown as CryptoApi; }; // Mock the result of decrypting the sessions, to avoid needing to diff --git a/test/unit-tests/components/views/dialogs/security/RestoreKeyBackupDialog-test.tsx b/test/unit-tests/components/views/dialogs/security/RestoreKeyBackupDialog-test.tsx index 4cfa74073b..c19010a089 100644 --- a/test/unit-tests/components/views/dialogs/security/RestoreKeyBackupDialog-test.tsx +++ b/test/unit-tests/components/views/dialogs/security/RestoreKeyBackupDialog-test.tsx @@ -28,7 +28,7 @@ describe("<RestoreKeyBackupDialog />", () => { beforeEach(() => { matrixClient = stubClient(); jest.spyOn(recoveryKeyModule, "decodeRecoveryKey").mockReturnValue(new Uint8Array(32)); - jest.spyOn(matrixClient, "getKeyBackupVersion").mockResolvedValue({ version: "1" } as KeyBackupInfo); + jest.spyOn(matrixClient.getCrypto()!, "getKeyBackupInfo").mockResolvedValue({ version: "1" } as KeyBackupInfo); }); it("should render", async () => { @@ -99,7 +99,7 @@ describe("<RestoreKeyBackupDialog />", () => { test("should restore key backup when passphrase is filled", async () => { // Determine that the passphrase is required - jest.spyOn(matrixClient, "getKeyBackupVersion").mockResolvedValue({ + jest.spyOn(matrixClient.getCrypto()!, "getKeyBackupInfo").mockResolvedValue({ version: "1", auth_data: { private_key_salt: "salt", diff --git a/test/unit-tests/components/views/elements/Pill-test.tsx b/test/unit-tests/components/views/elements/Pill-test.tsx index 716b4513ce..db075b3b6f 100644 --- a/test/unit-tests/components/views/elements/Pill-test.tsx +++ b/test/unit-tests/components/views/elements/Pill-test.tsx @@ -86,7 +86,7 @@ describe("<Pill>", () => { room: room1Id, msg: "Room 1 Message", }); - room1.addLiveEvents([room1Message]); + room1.addLiveEvents([room1Message], { addToState: true }); room2 = new Room(room2Id, client, user1Id); room2.currentState.setStateEvents([mkRoomMemberJoinEvent(user2Id, room2Id)]); diff --git a/test/unit-tests/components/views/elements/RoomTopic-test.tsx b/test/unit-tests/components/views/elements/RoomTopic-test.tsx index dcf80a95b9..0fe833fe4e 100644 --- a/test/unit-tests/components/views/elements/RoomTopic-test.tsx +++ b/test/unit-tests/components/views/elements/RoomTopic-test.tsx @@ -41,7 +41,7 @@ describe("<RoomTopic/>", () => { ts: 123, event: true, }); - room.addLiveEvents([topicEvent]); + room.addLiveEvents([topicEvent], { addToState: true }); return room; } diff --git a/test/unit-tests/components/views/elements/crypto/VerificationQRCode-test.tsx b/test/unit-tests/components/views/elements/crypto/VerificationQRCode-test.tsx index a62834bd9c..a0db4bfdad 100644 --- a/test/unit-tests/components/views/elements/crypto/VerificationQRCode-test.tsx +++ b/test/unit-tests/components/views/elements/crypto/VerificationQRCode-test.tsx @@ -17,7 +17,9 @@ describe("<VerificationQRCode />", () => { }); it("renders a QR code", async () => { - const { container, getAllByAltText } = render(<VerificationQRCode qrCodeBytes={Buffer.from("asd")} />); + const { container, getAllByAltText } = render( + <VerificationQRCode qrCodeBytes={new Uint8ClampedArray(Buffer.from("asd"))} />, + ); // wait for the spinner to go away await waitFor(() => getAllByAltText("QR Code").length === 1); expect(container).toMatchSnapshot(); diff --git a/test/unit-tests/components/views/messages/MFileBody-test.tsx b/test/unit-tests/components/views/messages/MFileBody-test.tsx index 60795babde..e124e3a2ff 100644 --- a/test/unit-tests/components/views/messages/MFileBody-test.tsx +++ b/test/unit-tests/components/views/messages/MFileBody-test.tsx @@ -20,7 +20,8 @@ import { import { MediaEventHelper } from "../../../../../src/utils/MediaEventHelper"; import SettingsStore from "../../../../../src/settings/SettingsStore"; import MFileBody from "../../../../../src/components/views/messages/MFileBody.tsx"; -import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext.ts"; +import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext.ts"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; jest.mock("matrix-encrypt-attachment", () => ({ decryptAttachment: jest.fn(), @@ -72,14 +73,14 @@ describe("<MFileBody/>", () => { it("should show a download button in file rendering type", async () => { const { container, getByRole } = render( - <RoomContext.Provider value={{ timelineRenderingType: TimelineRenderingType.File } as any}> + <ScopedRoomContextProvider {...({ timelineRenderingType: TimelineRenderingType.File } as any)}> <MFileBody {...props} mxEvent={mediaEvent} mediaEventHelper={new MediaEventHelper(mediaEvent)} showGenericPlaceholder={false} /> - </RoomContext.Provider>, + </ScopedRoomContextProvider>, ); expect(getByRole("link", { name: "Download" })).toBeInTheDocument(); diff --git a/test/unit-tests/components/views/messages/MessageActionBar-test.tsx b/test/unit-tests/components/views/messages/MessageActionBar-test.tsx index 8b639eb94e..dda5b348a9 100644 --- a/test/unit-tests/components/views/messages/MessageActionBar-test.tsx +++ b/test/unit-tests/components/views/messages/MessageActionBar-test.tsx @@ -35,6 +35,7 @@ import dispatcher from "../../../../../src/dispatcher/dispatcher"; import SettingsStore from "../../../../../src/settings/SettingsStore"; import { Action } from "../../../../../src/dispatcher/actions"; import PinningUtils from "../../../../../src/utils/PinningUtils"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; jest.mock("../../../../../src/dispatcher/dispatcher"); @@ -117,9 +118,9 @@ describe("<MessageActionBar />", () => { } as unknown as IRoomState; const getComponent = (props = {}, roomContext: Partial<IRoomState> = {}) => render( - <RoomContext.Provider value={{ ...defaultRoomContext, ...roomContext }}> + <ScopedRoomContextProvider {...defaultRoomContext} {...roomContext}> <MessageActionBar {...defaultProps} {...props} /> - </RoomContext.Provider>, + </ScopedRoomContextProvider>, ); beforeEach(() => { diff --git a/test/unit-tests/components/views/messages/MessageEvent-test.tsx b/test/unit-tests/components/views/messages/MessageEvent-test.tsx index 9ee323f175..4ff574f752 100644 --- a/test/unit-tests/components/views/messages/MessageEvent-test.tsx +++ b/test/unit-tests/components/views/messages/MessageEvent-test.tsx @@ -14,7 +14,6 @@ import fs from "fs"; import path from "path"; import SettingsStore from "../../../../../src/settings/SettingsStore"; -import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../../../../../src/voice-broadcast"; import { mkEvent, mkRoom, stubClient } from "../../../../test-utils"; import MessageEvent from "../../../../../src/components/views/messages/MessageEvent"; import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks"; @@ -24,10 +23,6 @@ jest.mock("../../../../../src/components/views/messages/UnknownBody", () => ({ default: () => <div data-testid="unknown-body" />, })); -jest.mock("../../../../../src/voice-broadcast/components/VoiceBroadcastBody", () => ({ - VoiceBroadcastBody: () => <div data-testid="voice-broadcast-body" />, -})); - jest.mock("../../../../../src/components/views/messages/MImageBody", () => ({ __esModule: true, default: () => <div data-testid="image-body" />, @@ -81,27 +76,6 @@ describe("MessageEvent", () => { jest.spyOn(SettingsStore, "unwatchSetting").mockImplementation(jest.fn()); }); - describe("when a voice broadcast start event occurs", () => { - let result: RenderResult; - - beforeEach(() => { - event = mkEvent({ - event: true, - type: VoiceBroadcastInfoEventType, - user: client.getUserId()!, - room: room.roomId, - content: { - state: VoiceBroadcastInfoState.Started, - }, - }); - result = renderMessageEvent(); - }); - - it("should render a VoiceBroadcast component", () => { - result.getByTestId("voice-broadcast-body"); - }); - }); - describe("when an image with a caption is sent", () => { let result: RenderResult; diff --git a/test/unit-tests/components/views/messages/RoomPredecessorTile-test.tsx b/test/unit-tests/components/views/messages/RoomPredecessorTile-test.tsx index f666b13b33..3ed7d7eb2b 100644 --- a/test/unit-tests/components/views/messages/RoomPredecessorTile-test.tsx +++ b/test/unit-tests/components/views/messages/RoomPredecessorTile-test.tsx @@ -20,9 +20,9 @@ import { } from "../../../../../src/components/views/messages/RoomPredecessorTile"; import { stubClient, upsertRoomStateEvents } from "../../../../test-utils/test-utils"; import { Action } from "../../../../../src/dispatcher/actions"; -import RoomContext from "../../../../../src/contexts/RoomContext"; import { filterConsole, getRoomContext } from "../../../../test-utils"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; jest.mock("../../../../../src/dispatcher/dispatcher"); @@ -99,9 +99,9 @@ describe("<RoomPredecessorTile />", () => { expect(createEvent).toBeTruthy(); return render( - <RoomContext.Provider value={getRoomContext(room, {})}> + <ScopedRoomContextProvider {...getRoomContext(room, {})}> <RoomPredecessorTile mxEvent={createEvent} /> - </RoomContext.Provider>, + </ScopedRoomContextProvider>, ); } diff --git a/test/unit-tests/components/views/right_panel/RoomSummaryCard-test.tsx b/test/unit-tests/components/views/right_panel/RoomSummaryCard-test.tsx index 4026149f98..80d2609577 100644 --- a/test/unit-tests/components/views/right_panel/RoomSummaryCard-test.tsx +++ b/test/unit-tests/components/views/right_panel/RoomSummaryCard-test.tsx @@ -15,7 +15,7 @@ import userEvent from "@testing-library/user-event"; import DMRoomMap from "../../../../../src/utils/DMRoomMap"; import RoomSummaryCard from "../../../../../src/components/views/right_panel/RoomSummaryCard"; -import ShareDialog from "../../../../../src/components/views/dialogs/ShareDialog"; +import { ShareDialog } from "../../../../../src/components/views/dialogs/ShareDialog"; import ExportDialog from "../../../../../src/components/views/dialogs/ExportDialog"; import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; import defaultDispatcher from "../../../../../src/dispatcher/dispatcher"; @@ -30,7 +30,8 @@ import { _t } from "../../../../../src/languageHandler"; import { tagRoom } from "../../../../../src/utils/room/tagRoom"; import { DefaultTagID } from "../../../../../src/stores/room-list/models"; import { Action } from "../../../../../src/dispatcher/actions"; -import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext"; +import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; jest.mock("../../../../../src/utils/room/tagRoom"); @@ -172,14 +173,14 @@ describe("<RoomSummaryCard />", () => { const onSearchChange = jest.fn(); const { rerender } = render( <MatrixClientContext.Provider value={mockClient}> - <RoomContext.Provider value={{ timelineRenderingType: TimelineRenderingType.Search } as any}> + <ScopedRoomContextProvider {...({ timelineRenderingType: TimelineRenderingType.Search } as any)}> <RoomSummaryCard room={room} permalinkCreator={new RoomPermalinkCreator(room)} onSearchChange={onSearchChange} focusRoomSearch={true} /> - </RoomContext.Provider> + </ScopedRoomContextProvider> </MatrixClientContext.Provider>, ); @@ -188,13 +189,13 @@ describe("<RoomSummaryCard />", () => { rerender( <MatrixClientContext.Provider value={mockClient}> - <RoomContext.Provider value={{ timelineRenderingType: TimelineRenderingType.Room } as any}> + <ScopedRoomContextProvider {...({ timelineRenderingType: TimelineRenderingType.Room } as any)}> <RoomSummaryCard room={room} permalinkCreator={new RoomPermalinkCreator(room)} onSearchChange={onSearchChange} /> - </RoomContext.Provider> + </ScopedRoomContextProvider> </MatrixClientContext.Provider>, ); expect(screen.getByPlaceholderText("Search messages…")).toHaveValue(""); @@ -254,10 +255,7 @@ describe("<RoomSummaryCard />", () => { fireEvent.click(getByText("People")); - expect(RightPanelStore.instance.pushCard).toHaveBeenCalledWith( - { phase: RightPanelPhases.RoomMemberList }, - true, - ); + expect(RightPanelStore.instance.pushCard).toHaveBeenCalledWith({ phase: RightPanelPhases.MemberList }, true); }); it("opens room threads list on button click", () => { diff --git a/test/unit-tests/components/views/right_panel/UserInfo-test.tsx b/test/unit-tests/components/views/right_panel/UserInfo-test.tsx index 7e23679fc2..172b04a77c 100644 --- a/test/unit-tests/components/views/right_panel/UserInfo-test.tsx +++ b/test/unit-tests/components/views/right_panel/UserInfo-test.tsx @@ -49,7 +49,7 @@ import ErrorDialog from "../../../../../src/components/views/dialogs/ErrorDialog import { shouldShowComponent } from "../../../../../src/customisations/helpers/UIComponents"; import { UIComponent } from "../../../../../src/settings/UIFeature"; import { Action } from "../../../../../src/dispatcher/actions"; -import ShareDialog from "../../../../../src/components/views/dialogs/ShareDialog"; +import { ShareDialog } from "../../../../../src/components/views/dialogs/ShareDialog"; import BulkRedactDialog from "../../../../../src/components/views/dialogs/BulkRedactDialog"; jest.mock("../../../../../src/utils/direct-messages", () => ({ @@ -188,7 +188,7 @@ describe("<UserInfo />", () => { const defaultProps = { user: defaultUser, // idk what is wrong with this type - phase: RightPanelPhases.RoomMemberInfo as RightPanelPhases.RoomMemberInfo, + phase: RightPanelPhases.MemberInfo as RightPanelPhases.MemberInfo, onClose: jest.fn(), }; @@ -455,7 +455,7 @@ describe("<UserInfo />", () => { mockCrypto.getUserVerificationStatus.mockResolvedValue(new UserVerificationStatus(false, false, false)); const { container } = renderComponent({ - phase: RightPanelPhases.SpaceMemberInfo, + phase: RightPanelPhases.MemberInfo, verificationRequest, room: mockRoom, }); @@ -649,7 +649,7 @@ describe("<UserInfo />", () => { mockClient.getDomain.mockReturnValue("example.com"); const { container } = renderComponent({ - phase: RightPanelPhases.RoomMemberInfo, + phase: RightPanelPhases.MemberInfo, room: mockRoom, }); diff --git a/test/unit-tests/components/views/right_panel/VerificationPanel-test.tsx b/test/unit-tests/components/views/right_panel/VerificationPanel-test.tsx index adc496e197..2adfc1eeb6 100644 --- a/test/unit-tests/components/views/right_panel/VerificationPanel-test.tsx +++ b/test/unit-tests/components/views/right_panel/VerificationPanel-test.tsx @@ -46,7 +46,7 @@ describe("<VerificationPanel />", () => { const request = makeMockVerificationRequest({ phase: Phase.Ready, }); - request.generateQRCode.mockResolvedValue(Buffer.from("test", "utf-8")); + request.generateQRCode.mockResolvedValue(new Uint8ClampedArray(Buffer.from("test", "utf-8"))); const container = renderComponent({ request: request, layout: "dialog", @@ -71,7 +71,7 @@ describe("<VerificationPanel />", () => { const request = makeMockVerificationRequest({ phase: Phase.Ready, }); - request.generateQRCode.mockResolvedValue(Buffer.from("test", "utf-8")); + request.generateQRCode.mockResolvedValue(new Uint8ClampedArray(Buffer.from("test", "utf-8"))); const container = renderComponent({ request: request, member: new User("@other:user"), diff --git a/test/unit-tests/components/views/right_panel/__snapshots__/RoomSummaryCard-test.tsx.snap b/test/unit-tests/components/views/right_panel/__snapshots__/RoomSummaryCard-test.tsx.snap index 5fb1e66115..a4496312f3 100644 --- a/test/unit-tests/components/views/right_panel/__snapshots__/RoomSummaryCard-test.tsx.snap +++ b/test/unit-tests/components/views/right_panel/__snapshots__/RoomSummaryCard-test.tsx.snap @@ -135,8 +135,9 @@ exports[`<RoomSummaryCard /> has button to edit topic 1`] = ` style="--mx-box-flex: 1;" > <a - class="_link_1mzip_17" + class="_link_ue21z_17" data-kind="primary" + data-size="medium" rel="noreferrer noopener" > <p @@ -752,8 +753,9 @@ exports[`<RoomSummaryCard /> renders the room summary 1`] = ` style="--mx-box-flex: 1;" > <a - class="_link_1mzip_17" + class="_link_ue21z_17" data-kind="primary" + data-size="medium" rel="noreferrer noopener" > <p @@ -1406,8 +1408,9 @@ exports[`<RoomSummaryCard /> renders the room topic in the summary 1`] = ` style="--mx-box-flex: 1;" > <a - class="_link_1mzip_17" + class="_link_ue21z_17" data-kind="primary" + data-size="medium" rel="noreferrer noopener" > <p diff --git a/test/unit-tests/components/views/right_panel/__snapshots__/UserInfo-test.tsx.snap b/test/unit-tests/components/views/right_panel/__snapshots__/UserInfo-test.tsx.snap index 06a3c3a12b..1744081389 100644 --- a/test/unit-tests/components/views/right_panel/__snapshots__/UserInfo-test.tsx.snap +++ b/test/unit-tests/components/views/right_panel/__snapshots__/UserInfo-test.tsx.snap @@ -88,7 +88,7 @@ exports[`<UserInfo /> with crypto enabled renders <BasicUserInfo /> 1`] = ` </p> </div> <button - aria-labelledby=":r6s:" + aria-labelledby=":r74:" class="_icon-button_bh2qc_17 _subtle-bg_bh2qc_38" data-testid="base-card-close-button" role="button" @@ -402,7 +402,7 @@ exports[`<UserInfo /> with crypto enabled should render a deactivate button for </p> </div> <button - aria-labelledby=":r92:" + aria-labelledby=":r9a:" class="_icon-button_bh2qc_17 _subtle-bg_bh2qc_38" data-testid="base-card-close-button" role="button" diff --git a/test/unit-tests/components/views/rooms/EditMessageComposer-test.tsx b/test/unit-tests/components/views/rooms/EditMessageComposer-test.tsx index a34e2e895b..beac0c8e1d 100644 --- a/test/unit-tests/components/views/rooms/EditMessageComposer-test.tsx +++ b/test/unit-tests/components/views/rooms/EditMessageComposer-test.tsx @@ -27,12 +27,12 @@ import { import DocumentOffset from "../../../../../src/editor/offset"; import SettingsStore from "../../../../../src/settings/SettingsStore"; import EditorStateTransfer from "../../../../../src/utils/EditorStateTransfer"; -import RoomContext from "../../../../../src/contexts/RoomContext"; import { IRoomState } from "../../../../../src/components/structures/RoomView"; import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; import Autocompleter, { IProviderCompletions } from "../../../../../src/autocomplete/Autocompleter"; import NotifProvider from "../../../../../src/autocomplete/NotifProvider"; import DMRoomMap from "../../../../../src/utils/DMRoomMap"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; describe("<EditMessageComposer/>", () => { const userId = "@alice:server.org"; @@ -79,7 +79,7 @@ describe("<EditMessageComposer/>", () => { render(<EditMessageComposerWithMatrixClient editState={editState} />, { wrapper: ({ children }) => ( <MatrixClientContext.Provider value={mockClient}> - <RoomContext.Provider value={roomContext}>{children}</RoomContext.Provider> + <ScopedRoomContextProvider {...roomContext}>{children}</ScopedRoomContextProvider> </MatrixClientContext.Provider> ), }); @@ -128,7 +128,7 @@ describe("<EditMessageComposer/>", () => { const expectedBody = { ...editedEvent.getContent(), - "body": " * original message + edit", + "body": "* original message + edit", "m.new_content": { "body": "original message + edit", "msgtype": "m.text", @@ -160,7 +160,7 @@ describe("<EditMessageComposer/>", () => { const content = createEditContent(model, editedEvent); expect(content).toEqual({ - "body": " * hello world", + "body": "* hello world", "msgtype": "m.text", "m.new_content": { "body": "hello world", @@ -183,10 +183,10 @@ describe("<EditMessageComposer/>", () => { const content = createEditContent(model, editedEvent); expect(content).toEqual({ - "body": " * hello *world*", + "body": "* hello *world*", "msgtype": "m.text", "format": "org.matrix.custom.html", - "formatted_body": " * hello <em>world</em>", + "formatted_body": "* hello <em>world</em>", "m.new_content": { "body": "hello *world*", "msgtype": "m.text", @@ -210,10 +210,10 @@ describe("<EditMessageComposer/>", () => { const content = createEditContent(model, editedEvent); expect(content).toEqual({ - "body": " * blinks __quickly__", + "body": "* blinks __quickly__", "msgtype": "m.emote", "format": "org.matrix.custom.html", - "formatted_body": " * blinks <strong>quickly</strong>", + "formatted_body": "* blinks <strong>quickly</strong>", "m.new_content": { "body": "blinks __quickly__", "msgtype": "m.emote", @@ -238,7 +238,7 @@ describe("<EditMessageComposer/>", () => { const content = createEditContent(model, editedEvent); expect(content).toEqual({ - "body": " * ✨sparkles✨", + "body": "* ✨sparkles✨", "msgtype": "m.emote", "m.new_content": { "body": "✨sparkles✨", @@ -264,7 +264,7 @@ describe("<EditMessageComposer/>", () => { // TODO Edits do not properly strip the double slash used to skip // command processing. expect(content).toEqual({ - "body": " * //dev/null is my favourite place", + "body": "* //dev/null is my favourite place", "msgtype": "m.text", "m.new_content": { "body": "//dev/null is my favourite place", diff --git a/test/unit-tests/components/views/rooms/EventTile-test.tsx b/test/unit-tests/components/views/rooms/EventTile-test.tsx index 4cb2296760..93445efbe3 100644 --- a/test/unit-tests/components/views/rooms/EventTile-test.tsx +++ b/test/unit-tests/components/views/rooms/EventTile-test.tsx @@ -30,7 +30,7 @@ import { mkEncryptedMatrixEvent } from "matrix-js-sdk/src/testing"; import EventTile, { EventTileProps } from "../../../../../src/components/views/rooms/EventTile"; import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; -import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext"; +import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; import { filterConsole, flushPromises, getRoomContext, mkEvent, mkMessage, stubClient } from "../../../../test-utils"; import { mkThread } from "../../../../test-utils/threads"; @@ -40,6 +40,7 @@ import { Action } from "../../../../../src/dispatcher/actions"; import { IRoomState } from "../../../../../src/components/structures/RoomView"; import PinningUtils from "../../../../../src/utils/PinningUtils"; import { Layout } from "../../../../../src/settings/enums/Layout"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; describe("EventTile", () => { const ROOM_ID = "!roomId:example.org"; @@ -56,13 +57,13 @@ describe("EventTile", () => { }) { return ( <MatrixClientContext.Provider value={client}> - <RoomContext.Provider value={props.roomContext}> + <ScopedRoomContextProvider {...props.roomContext}> <EventTile mxEvent={mxEvent} replacingEventId={mxEvent.replacingEventId()} {...(props.eventTilePropertyOverrides ?? {})} /> - </RoomContext.Provider> + </ScopedRoomContextProvider> </MatrixClientContext.Provider> ); } @@ -70,9 +71,11 @@ describe("EventTile", () => { function getComponent( overrides: Partial<EventTileProps> = {}, renderingType: TimelineRenderingType = TimelineRenderingType.Room, + roomContext: Partial<IRoomState> = {}, ) { const context = getRoomContext(room, { timelineRenderingType: renderingType, + ...roomContext, }); return render(<WrappedEventTile roomContext={context} eventTilePropertyOverrides={overrides} />); } @@ -301,6 +304,8 @@ describe("EventTile", () => { [EventShieldReason.UNKNOWN_DEVICE, "unknown or deleted device"], [EventShieldReason.AUTHENTICITY_NOT_GUARANTEED, "can't be guaranteed"], [EventShieldReason.MISMATCHED_SENDER_KEY, "Encrypted by an unverified session"], + [EventShieldReason.SENT_IN_CLEAR, "Not encrypted"], + [EventShieldReason.VERIFICATION_VIOLATION, "Sender's verified identity has changed"], ])("shows the correct reason code for %i (%s)", async (reasonCode: EventShieldReason, expectedText: string) => { mxEvent = await mkEncryptedMatrixEvent({ plainContent: { msgtype: "m.text", body: "msg1" }, @@ -434,8 +439,6 @@ describe("EventTile", () => { }); it("should update the warning when the event is replaced with an unencrypted one", async () => { - jest.spyOn(client, "isRoomEncrypted").mockReturnValue(true); - // we start out with an event from the trusted device mxEvent = await mkEncryptedMatrixEvent({ plainContent: { msgtype: "m.text", body: "msg1" }, @@ -449,7 +452,7 @@ describe("EventTile", () => { shieldReason: null, } as EventEncryptionInfo); - const roomContext = getRoomContext(room, {}); + const roomContext = getRoomContext(room, { isRoomEncrypted: true }); const { container, rerender } = render(<WrappedEventTile roomContext={roomContext} />); await flushPromises(); @@ -578,4 +581,28 @@ describe("EventTile", () => { }); }); }); + + it("should display the not encrypted status for an unencrypted event when the room becomes encrypted", async () => { + jest.spyOn(client.getCrypto()!, "getEncryptionInfoForEvent").mockResolvedValue({ + shieldColour: EventShieldColour.NONE, + shieldReason: null, + }); + + const { rerender } = getComponent(); + await flushPromises(); + // The room and the event are unencrypted, the tile should not show the not encrypted status + expect(screen.queryByText("Not encrypted")).toBeNull(); + + // The room is now encrypted + rerender( + <WrappedEventTile + roomContext={getRoomContext(room, { + isRoomEncrypted: true, + })} + />, + ); + + // The event tile should now show the not encrypted status + await waitFor(() => expect(screen.getByText("Not encrypted")).toBeInTheDocument()); + }); }); diff --git a/test/unit-tests/components/views/rooms/MessageComposer-test.tsx b/test/unit-tests/components/views/rooms/MessageComposer-test.tsx index 3bd9a6cf62..6849612fee 100644 --- a/test/unit-tests/components/views/rooms/MessageComposer-test.tsx +++ b/test/unit-tests/components/views/rooms/MessageComposer-test.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import * as React from "react"; -import { EventType, MatrixEvent, Room, RoomMember, THREAD_RELATION_TYPE } from "matrix-js-sdk/src/matrix"; +import { EventType, MatrixEvent, RoomMember, THREAD_RELATION_TYPE } from "matrix-js-sdk/src/matrix"; import { act, fireEvent, render, screen, waitFor } from "jest-matrix-react"; import userEvent from "@testing-library/user-event"; @@ -19,17 +19,14 @@ import { mkStubRoom, mockPlatformPeg, stubClient, - waitEnoughCyclesForModal, } from "../../../../test-utils"; import MessageComposer from "../../../../../src/components/views/rooms/MessageComposer"; import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; -import RoomContext from "../../../../../src/contexts/RoomContext"; import { IRoomState } from "../../../../../src/components/structures/RoomView"; import ResizeNotifier from "../../../../../src/utils/ResizeNotifier"; import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks"; import { LocalRoom } from "../../../../../src/models/LocalRoom"; -import { Features } from "../../../../../src/settings/Settings"; import SettingsStore from "../../../../../src/settings/SettingsStore"; import { SettingLevel } from "../../../../../src/settings/SettingLevel"; import dis from "../../../../../src/dispatcher/dispatcher"; @@ -37,9 +34,7 @@ import { E2EStatus } from "../../../../../src/utils/ShieldUtils"; import { addTextToComposerRTL } from "../../../../test-utils/composer"; import UIStore, { UI_EVENTS } from "../../../../../src/stores/UIStore"; import { Action } from "../../../../../src/dispatcher/actions"; -import { VoiceBroadcastInfoState, VoiceBroadcastRecording } from "../../../../../src/voice-broadcast"; -import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils"; -import { SdkContextClass } from "../../../../../src/contexts/SDKContext"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; const openStickerPicker = async (): Promise<void> => { await userEvent.click(screen.getByLabelText("More options")); @@ -51,15 +46,6 @@ const startVoiceMessage = async (): Promise<void> => { await userEvent.click(screen.getByLabelText("Voice Message")); }; -const setCurrentBroadcastRecording = (room: Room, state: VoiceBroadcastInfoState): void => { - const recording = new VoiceBroadcastRecording( - mkVoiceBroadcastInfoStateEvent(room.roomId, state, "@user:example.com", "ABC123"), - MatrixClientPeg.safeGet(), - state, - ); - act(() => SdkContextClass.instance.voiceBroadcastRecordingsStore.setCurrent(recording)); -}; - const expectVoiceMessageRecordingTriggered = (): void => { // Checking for the voice message dialog text, if no mic can be found. // By this we know at least that starting a voice message was triggered. @@ -78,14 +64,11 @@ describe("MessageComposer", () => { await clearAllModals(); jest.useRealTimers(); - SdkContextClass.instance.voiceBroadcastRecordingsStore.clearCurrent(); - // restore settings act(() => { [ "MessageComposerInput.showStickersButton", "MessageComposerInput.showPollsButton", - Features.VoiceBroadcast, "feature_wysiwyg_composer", ].forEach((setting: string): void => { SettingsStore.setValue(setting, null, SettingLevel.DEVICE, SettingsStore.getDefaultValue(setting)); @@ -212,10 +195,6 @@ describe("MessageComposer", () => { setting: "MessageComposerInput.showPollsButton", buttonLabel: "Poll", }, - { - setting: Features.VoiceBroadcast, - buttonLabel: "Voice broadcast", - }, ].forEach(({ setting, buttonLabel }) => { [true, false].forEach((value: boolean) => { describe(`when ${setting} = ${value}`, () => { @@ -437,34 +416,6 @@ describe("MessageComposer", () => { expectVoiceMessageRecordingTriggered(); }); }); - - describe("when recording a voice broadcast and trying to start a voice message", () => { - beforeEach(async () => { - setCurrentBroadcastRecording(room, VoiceBroadcastInfoState.Started); - wrapAndRender({ room }); - await startVoiceMessage(); - await waitEnoughCyclesForModal(); - }); - - it("should not start a voice message and display the info dialog", async () => { - expect(screen.queryByLabelText("Stop recording")).not.toBeInTheDocument(); - expect(screen.getByText("Can't start voice message")).toBeInTheDocument(); - }); - }); - - describe("when there is a stopped voice broadcast recording and trying to start a voice message", () => { - beforeEach(async () => { - setCurrentBroadcastRecording(room, VoiceBroadcastInfoState.Stopped); - wrapAndRender({ room }); - await startVoiceMessage(); - await waitEnoughCyclesForModal(); - }); - - it("should try to start a voice message and should not display the info dialog", async () => { - expect(screen.queryByText("Can't start voice message")).not.toBeInTheDocument(); - expectVoiceMessageRecordingTriggered(); - }); - }); }); describe("for a LocalRoom", () => { @@ -512,9 +463,9 @@ function wrapAndRender( const getRawComponent = (props = {}, context = roomContext, client = mockClient) => ( <MatrixClientContext.Provider value={client}> - <RoomContext.Provider value={context}> + <ScopedRoomContextProvider {...context}> <MessageComposer {...defaultProps} {...props} /> - </RoomContext.Provider> + </ScopedRoomContextProvider> </MatrixClientContext.Provider> ); return { diff --git a/test/unit-tests/components/views/rooms/MessageComposerButtons-test.tsx b/test/unit-tests/components/views/rooms/MessageComposerButtons-test.tsx index a66dc0d925..c2f56dc968 100644 --- a/test/unit-tests/components/views/rooms/MessageComposerButtons-test.tsx +++ b/test/unit-tests/components/views/rooms/MessageComposerButtons-test.tsx @@ -10,11 +10,11 @@ import React from "react"; import { render, screen, waitFor } from "jest-matrix-react"; import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; -import RoomContext from "../../../../../src/contexts/RoomContext"; import { createTestClient, getRoomContext, mkStubRoom } from "../../../../test-utils"; import { IRoomState } from "../../../../../src/components/structures/RoomView"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; import MessageComposerButtons from "../../../../../src/components/views/rooms/MessageComposerButtons"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; describe("MessageComposerButtons", () => { // @ts-ignore - we're deliberately not implementing the whole interface here, but @@ -54,7 +54,7 @@ describe("MessageComposerButtons", () => { return render( <MatrixClientContext.Provider value={mockClient}> - <RoomContext.Provider value={defaultRoomContext}>{component}</RoomContext.Provider> + <ScopedRoomContextProvider {...defaultRoomContext}>{component}</ScopedRoomContextProvider> </MatrixClientContext.Provider>, ); } @@ -168,27 +168,4 @@ describe("MessageComposerButtons", () => { ]); }); }); - - describe("with showVoiceBroadcastButton = true", () => { - it("should render the »Voice broadcast« button", () => { - wrapAndRender( - <MessageComposerButtons - {...mockProps} - isMenuOpen={true} - showLocationButton={true} - showPollsButton={true} - showStickersButton={true} - showVoiceBroadcastButton={true} - />, - false, - ); - - expect(getButtonLabels()).toEqual([ - "Emoji", - "Attachment", - "More options", - ["Sticker", "Voice Message", "Voice broadcast", "Poll", "Location"], - ]); - }); - }); }); diff --git a/test/unit-tests/components/views/rooms/NewRoomIntro-test.tsx b/test/unit-tests/components/views/rooms/NewRoomIntro-test.tsx index 291d28c967..a1cd452610 100644 --- a/test/unit-tests/components/views/rooms/NewRoomIntro-test.tsx +++ b/test/unit-tests/components/views/rooms/NewRoomIntro-test.tsx @@ -13,19 +13,19 @@ import { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; import { LocalRoom } from "../../../../../src/models/LocalRoom"; import { filterConsole, mkRoomMemberJoinEvent, mkThirdPartyInviteEvent, stubClient } from "../../../../test-utils"; -import RoomContext from "../../../../../src/contexts/RoomContext"; import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; import NewRoomIntro from "../../../../../src/components/views/rooms/NewRoomIntro"; import { IRoomState } from "../../../../../src/components/structures/RoomView"; import DMRoomMap from "../../../../../src/utils/DMRoomMap"; import { DirectoryMember } from "../../../../../src/utils/direct-messages"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; const renderNewRoomIntro = (client: MatrixClient, room: Room | LocalRoom) => { render( <MatrixClientContext.Provider value={client}> - <RoomContext.Provider value={{ room, roomId: room.roomId } as unknown as IRoomState}> + <ScopedRoomContextProvider {...({ room, roomId: room.roomId } as unknown as IRoomState)}> <NewRoomIntro /> - </RoomContext.Provider> + </ScopedRoomContextProvider> </MatrixClientContext.Provider>, ); }; diff --git a/test/unit-tests/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx b/test/unit-tests/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx index ea54dc1085..5b2dddc73a 100644 --- a/test/unit-tests/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx +++ b/test/unit-tests/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx @@ -165,7 +165,7 @@ describe("UnreadNotificationBadge", () => { }, ts: 5, }); - room.addLiveEvents([event]); + room.addLiveEvents([event], { addToState: true }); const { container } = render(getComponent(THREAD_ID)); expect(container.querySelector(".mx_NotificationBadge_dot")).toBeTruthy(); diff --git a/test/unit-tests/components/views/rooms/PinnedMessageBanner-test.tsx b/test/unit-tests/components/views/rooms/PinnedMessageBanner-test.tsx index f59b9f3a9f..8717762e0d 100644 --- a/test/unit-tests/components/views/rooms/PinnedMessageBanner-test.tsx +++ b/test/unit-tests/components/views/rooms/PinnedMessageBanner-test.tsx @@ -20,6 +20,7 @@ import RightPanelStore from "../../../../../src/stores/right-panel/RightPanelSto import { RightPanelPhases } from "../../../../../src/stores/right-panel/RightPanelStorePhases"; import { UPDATE_EVENT } from "../../../../../src/stores/AsyncStore"; import { Action } from "../../../../../src/dispatcher/actions"; +import ResizeNotifier from "../../../../../src/utils/ResizeNotifier.ts"; describe("<PinnedMessageBanner />", () => { const userId = "@alice:server.org"; @@ -28,10 +29,12 @@ describe("<PinnedMessageBanner />", () => { let mockClient: MatrixClient; let room: Room; let permalinkCreator: RoomPermalinkCreator; + let resizeNotifier: ResizeNotifier; beforeEach(() => { mockClient = stubClient(); room = new Room(roomId, mockClient, userId); permalinkCreator = new RoomPermalinkCreator(room); + resizeNotifier = new ResizeNotifier(); jest.spyOn(dis, "dispatch").mockReturnValue(undefined); }); @@ -77,7 +80,7 @@ describe("<PinnedMessageBanner />", () => { */ function renderBanner() { return render( - <PinnedMessageBanner permalinkCreator={permalinkCreator} room={room} />, + <PinnedMessageBanner permalinkCreator={permalinkCreator} room={room} resizeNotifier={resizeNotifier} />, withClientContextRenderOptions(mockClient), ); } @@ -145,7 +148,9 @@ describe("<PinnedMessageBanner />", () => { event3.getId()!, ]); jest.spyOn(pinnedEventHooks, "useSortedFetchedPinnedEvents").mockReturnValue([event1, event2, event3]); - rerender(<PinnedMessageBanner permalinkCreator={permalinkCreator} room={room} />); + rerender( + <PinnedMessageBanner permalinkCreator={permalinkCreator} room={room} resizeNotifier={resizeNotifier} />, + ); await expect(screen.findByText("Third pinned message")).resolves.toBeVisible(); expect(asFragment()).toMatchSnapshot(); }); @@ -206,6 +211,42 @@ describe("<PinnedMessageBanner />", () => { expect(asFragment()).toMatchSnapshot(); }); + describe("Notify the timeline to resize", () => { + beforeEach(() => { + jest.spyOn(resizeNotifier, "notifyTimelineHeightChanged"); + jest.spyOn(pinnedEventHooks, "usePinnedEvents").mockReturnValue([event1.getId()!, event2.getId()!]); + jest.spyOn(pinnedEventHooks, "useSortedFetchedPinnedEvents").mockReturnValue([event1, event2]); + }); + + it("should notify the timeline to resize when we display the banner", async () => { + renderBanner(); + await expect(screen.findByText("Second pinned message")).resolves.toBeVisible(); + // The banner is displayed, so we need to resize the timeline + expect(resizeNotifier.notifyTimelineHeightChanged).toHaveBeenCalledTimes(1); + + await userEvent.click(screen.getByRole("button", { name: "View the pinned message in the timeline." })); + await expect(screen.findByText("First pinned message")).resolves.toBeVisible(); + // The banner is already displayed, so we don't need to resize the timeline + expect(resizeNotifier.notifyTimelineHeightChanged).toHaveBeenCalledTimes(1); + }); + + it("should notify the timeline to resize when we hide the banner", async () => { + const { rerender } = renderBanner(); + await expect(screen.findByText("Second pinned message")).resolves.toBeVisible(); + // The banner is displayed, so we need to resize the timeline + expect(resizeNotifier.notifyTimelineHeightChanged).toHaveBeenCalledTimes(1); + + // The banner has no event to display and is hidden + jest.spyOn(pinnedEventHooks, "usePinnedEvents").mockReturnValue([]); + jest.spyOn(pinnedEventHooks, "useSortedFetchedPinnedEvents").mockReturnValue([]); + rerender( + <PinnedMessageBanner permalinkCreator={permalinkCreator} room={room} resizeNotifier={resizeNotifier} />, + ); + // The timeline should be resized + expect(resizeNotifier.notifyTimelineHeightChanged).toHaveBeenCalledTimes(2); + }); + }); + describe("Right button", () => { beforeEach(() => { jest.spyOn(pinnedEventHooks, "usePinnedEvents").mockReturnValue([event1.getId()!, event2.getId()!]); @@ -217,6 +258,8 @@ describe("<PinnedMessageBanner />", () => { jest.spyOn(RightPanelStore.instance, "isOpenForRoom").mockReturnValue(false); renderBanner(); + await expect(screen.findByText("Second pinned message")).resolves.toBeVisible(); + expect(screen.getByRole("button", { name: "View all" })).toBeVisible(); }); @@ -224,10 +267,12 @@ describe("<PinnedMessageBanner />", () => { // The Right panel is opened on another card jest.spyOn(RightPanelStore.instance, "isOpenForRoom").mockReturnValue(true); jest.spyOn(RightPanelStore.instance, "currentCard", "get").mockReturnValue({ - phase: RightPanelPhases.RoomMemberList, + phase: RightPanelPhases.MemberList, }); renderBanner(); + await expect(screen.findByText("Second pinned message")).resolves.toBeVisible(); + expect(screen.getByRole("button", { name: "View all" })).toBeVisible(); }); @@ -239,6 +284,8 @@ describe("<PinnedMessageBanner />", () => { }); renderBanner(); + await expect(screen.findByText("Second pinned message")).resolves.toBeVisible(); + expect(screen.getByRole("button", { name: "Close list" })).toBeVisible(); }); @@ -263,6 +310,7 @@ describe("<PinnedMessageBanner />", () => { }); renderBanner(); + await expect(screen.findByText("Second pinned message")).resolves.toBeVisible(); expect(screen.getByRole("button", { name: "Close list" })).toBeVisible(); jest.spyOn(RightPanelStore.instance, "isOpenForRoom").mockReturnValue(false); diff --git a/test/unit-tests/components/views/rooms/ReadReceiptGroup-test.tsx b/test/unit-tests/components/views/rooms/ReadReceiptGroup-test.tsx index ff89aa6942..13500f9b1f 100644 --- a/test/unit-tests/components/views/rooms/ReadReceiptGroup-test.tsx +++ b/test/unit-tests/components/views/rooms/ReadReceiptGroup-test.tsx @@ -10,6 +10,7 @@ import React, { ComponentProps } from "react"; import { render, screen, waitFor } from "jest-matrix-react"; import { RoomMember } from "matrix-js-sdk/src/matrix"; import userEvent from "@testing-library/user-event"; +import { mocked } from "jest-mock"; import { determineAvatarPosition, @@ -20,6 +21,9 @@ import * as languageHandler from "../../../../../src/languageHandler"; import { stubClient } from "../../../../test-utils"; import dispatcher from "../../../../../src/dispatcher/dispatcher"; import { Action } from "../../../../../src/dispatcher/actions"; +import { formatDate } from "../../../../../src/DateUtils"; + +jest.mock("../../../../../src/DateUtils"); describe("ReadReceiptGroup", () => { describe("TooltipText", () => { @@ -87,6 +91,10 @@ describe("ReadReceiptGroup", () => { describe("<ReadReceiptPerson />", () => { stubClient(); + // We pick a fixed time but this can still vary depending on the locale + // the tests are run in. We are not testing date formatting here, so stub it out. + mocked(formatDate).mockReturnValue("==MOCK FORMATTED DATE=="); + const ROOM_ID = "roomId"; const USER_ID = "@alice:example.org"; diff --git a/test/unit-tests/components/views/rooms/RoomHeader-test.tsx b/test/unit-tests/components/views/rooms/RoomHeader-test.tsx index 1be9c77713..8b9b0d9848 100644 --- a/test/unit-tests/components/views/rooms/RoomHeader-test.tsx +++ b/test/unit-tests/components/views/rooms/RoomHeader-test.tsx @@ -158,7 +158,7 @@ describe("RoomHeader", () => { fireEvent.click(facePile); - expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.RoomMemberList }); + expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.MemberList }); }); it("has room info icon that opens the room info panel", async () => { @@ -589,7 +589,7 @@ describe("RoomHeader", () => { state_key: "", room_id: room.roomId, }); - room.addLiveEvents([joinRuleEvent]); + room.addLiveEvents([joinRuleEvent], { addToState: true }); render(<RoomHeader room={room} />, getWrapper()); diff --git a/test/unit-tests/components/views/rooms/RoomHeader/CallGuestLinkButton-test.tsx b/test/unit-tests/components/views/rooms/RoomHeader/CallGuestLinkButton-test.tsx index cc28a4ec08..c77114fa96 100644 --- a/test/unit-tests/components/views/rooms/RoomHeader/CallGuestLinkButton-test.tsx +++ b/test/unit-tests/components/views/rooms/RoomHeader/CallGuestLinkButton-test.tsx @@ -19,7 +19,7 @@ import { } from "../../../../../../src/components/views/rooms/RoomHeader/CallGuestLinkButton"; import Modal from "../../../../../../src/Modal"; import SdkConfig from "../../../../../../src/SdkConfig"; -import ShareDialog from "../../../../../../src/components/views/dialogs/ShareDialog"; +import { ShareDialog } from "../../../../../../src/components/views/dialogs/ShareDialog"; import { _t } from "../../../../../../src/languageHandler"; import SettingsStore from "../../../../../../src/settings/SettingsStore"; diff --git a/test/unit-tests/components/views/rooms/RoomTile-test.tsx b/test/unit-tests/components/views/rooms/RoomTile-test.tsx index 7aa9ad8462..c14c699699 100644 --- a/test/unit-tests/components/views/rooms/RoomTile-test.tsx +++ b/test/unit-tests/components/views/rooms/RoomTile-test.tsx @@ -9,14 +9,7 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import { render, screen, act, RenderResult } from "jest-matrix-react"; import { mocked, Mocked } from "jest-mock"; -import { - MatrixClient, - PendingEventOrdering, - Room, - MatrixEvent, - RoomStateEvent, - Thread, -} from "matrix-js-sdk/src/matrix"; +import { MatrixClient, PendingEventOrdering, Room, RoomStateEvent, Thread } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; import { Widget } from "matrix-widget-api"; @@ -40,8 +33,6 @@ import DMRoomMap from "../../../../../src/utils/DMRoomMap"; import PlatformPeg from "../../../../../src/PlatformPeg"; import BasePlatform from "../../../../../src/BasePlatform"; import { WidgetMessagingStore } from "../../../../../src/stores/widgets/WidgetMessagingStore"; -import { VoiceBroadcastInfoState } from "../../../../../src/voice-broadcast"; -import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils"; import { TestSdkContext } from "../../../TestSdkContext"; import { SDKContext } from "../../../../../src/contexts/SDKContext"; import { shouldShowComponent } from "../../../../../src/customisations/helpers/UIComponents"; @@ -61,20 +52,6 @@ describe("RoomTile", () => { } as unknown as BasePlatform); useMockedCalls(); - const setUpVoiceBroadcast = async (state: VoiceBroadcastInfoState): Promise<void> => { - voiceBroadcastInfoEvent = mkVoiceBroadcastInfoStateEvent( - room.roomId, - state, - client.getSafeUserId(), - client.getDeviceId()!, - ); - - await act(async () => { - room.currentState.setStateEvents([voiceBroadcastInfoEvent]); - await flushPromises(); - }); - }; - const renderRoomTile = (): RenderResult => { return render( <SDKContext.Provider value={sdkContext}> @@ -89,7 +66,6 @@ describe("RoomTile", () => { }; let client: Mocked<MatrixClient>; - let voiceBroadcastInfoEvent: MatrixEvent; let room: Room; let sdkContext: TestSdkContext; let showMessagePreview = false; @@ -303,49 +279,6 @@ describe("RoomTile", () => { }); expect(screen.queryByLabelText(/participant/)).toBe(null); }); - - describe("and a live broadcast starts", () => { - beforeEach(async () => { - renderRoomTile(); - await setUpVoiceBroadcast(VoiceBroadcastInfoState.Started); - }); - - it("should still render the call subtitle", () => { - expect(screen.queryByText("Video")).toBeInTheDocument(); - expect(screen.queryByText("Live")).not.toBeInTheDocument(); - }); - }); - }); - - describe("when a live voice broadcast starts", () => { - beforeEach(async () => { - renderRoomTile(); - await setUpVoiceBroadcast(VoiceBroadcastInfoState.Started); - }); - - it("should render the »Live« subtitle", () => { - expect(screen.queryByText("Live")).toBeInTheDocument(); - }); - - describe("and the broadcast stops", () => { - beforeEach(async () => { - const stopEvent = mkVoiceBroadcastInfoStateEvent( - room.roomId, - VoiceBroadcastInfoState.Stopped, - client.getSafeUserId(), - client.getDeviceId()!, - voiceBroadcastInfoEvent, - ); - await act(async () => { - room.currentState.setStateEvents([stopEvent]); - await flushPromises(); - }); - }); - - it("should not render the »Live« subtitle", () => { - expect(screen.queryByText("Live")).not.toBeInTheDocument(); - }); - }); }); }); diff --git a/test/unit-tests/components/views/rooms/SendMessageComposer-test.tsx b/test/unit-tests/components/views/rooms/SendMessageComposer-test.tsx index f3a0168833..c372819ce2 100644 --- a/test/unit-tests/components/views/rooms/SendMessageComposer-test.tsx +++ b/test/unit-tests/components/views/rooms/SendMessageComposer-test.tsx @@ -18,7 +18,7 @@ import SendMessageComposer, { isQuickReaction, } from "../../../../../src/components/views/rooms/SendMessageComposer"; import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; -import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext"; +import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext"; import EditorModel from "../../../../../src/editor/model"; import { createPartCreator } from "../../../editor/mock"; import { createTestClient, mkEvent, mkStubRoom, stubClient } from "../../../../test-utils"; @@ -30,6 +30,7 @@ import { IRoomState, MainSplitContentType } from "../../../../../src/components/ import { mockPlatformPeg } from "../../../../test-utils/platform"; import { doMaybeLocalRoomAction } from "../../../../../src/utils/local-room"; import { addTextToComposer } from "../../../../test-utils/composer"; +import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; jest.mock("../../../../../src/utils/local-room", () => ({ doMaybeLocalRoomAction: jest.fn(), @@ -77,6 +78,7 @@ describe("<SendMessageComposer/>", () => { canAskToJoin: false, promptAskToJoin: false, viewRoomOpts: { buttons: [] }, + isRoomEncrypted: false, }; describe("createMessageContent", () => { it("sends plaintext messages correctly", () => { @@ -364,9 +366,9 @@ describe("<SendMessageComposer/>", () => { }; const getRawComponent = (props = {}, roomContext = defaultRoomContext, client = mockClient) => ( <MatrixClientContext.Provider value={client}> - <RoomContext.Provider value={roomContext}> + <ScopedRoomContextProvider {...roomContext}> <SendMessageComposer {...defaultProps} {...props} /> - </RoomContext.Provider> + </ScopedRoomContextProvider> </MatrixClientContext.Provider> ); const getComponent = (props = {}, roomContext = defaultRoomContext, client = mockClient) => { diff --git a/test/unit-tests/components/views/rooms/UserIdentityWarning-test.tsx b/test/unit-tests/components/views/rooms/UserIdentityWarning-test.tsx new file mode 100644 index 0000000000..9a70a88768 --- /dev/null +++ b/test/unit-tests/components/views/rooms/UserIdentityWarning-test.tsx @@ -0,0 +1,534 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import React from "react"; +import { sleep } from "matrix-js-sdk/src/utils"; +import { + EventType, + MatrixClient, + MatrixEvent, + Room, + RoomState, + RoomStateEvent, + RoomMember, +} from "matrix-js-sdk/src/matrix"; +import { CryptoEvent, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api"; +import { act, render, screen, waitFor } from "jest-matrix-react"; +import userEvent from "@testing-library/user-event"; + +import { stubClient } from "../../../../test-utils"; +import { UserIdentityWarning } from "../../../../../src/components/views/rooms/UserIdentityWarning"; +import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; + +const ROOM_ID = "!room:id"; + +function mockRoom(): Room { + const room = { + getEncryptionTargetMembers: jest.fn(async () => []), + getMember: jest.fn((userId) => {}), + roomId: ROOM_ID, + shouldEncryptForInvitedMembers: jest.fn(() => true), + } as unknown as Room; + + return room; +} + +function mockRoomMember(userId: string, name?: string): RoomMember { + return { + userId, + name: name ?? userId, + rawDisplayName: name ?? userId, + roomId: ROOM_ID, + getMxcAvatarUrl: jest.fn(), + } as unknown as RoomMember; +} + +function dummyRoomState(): RoomState { + return new RoomState(ROOM_ID); +} + +/** + * Get the warning element, given the warning text (excluding the "Learn more" + * link). This is needed because the warning text contains a `<b>` tag, so the + * normal `getByText` doesn't work. + */ +function getWarningByText(text: string): Element { + return screen.getByText((content?: string, element?: Element | null): boolean => { + return ( + !!element && + element.classList.contains("mx_UserIdentityWarning_main") && + element.textContent === text + " Learn more" + ); + }); +} + +function renderComponent(client: MatrixClient, room: Room) { + return render(<UserIdentityWarning room={room} key={ROOM_ID} />, { + wrapper: ({ ...rest }) => <MatrixClientContext.Provider value={client} {...rest} />, + }); +} + +describe("UserIdentityWarning", () => { + let client: MatrixClient; + let room: Room; + + beforeEach(async () => { + client = stubClient(); + room = mockRoom(); + jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + // This tests the basic functionality of the component. If we have a room + // member whose identity needs accepting, we should display a warning. When + // the "OK" button gets pressed, it should call `pinCurrentUserIdentity`. + it("displays a warning when a user's identity needs approval", async () => { + jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([ + mockRoomMember("@alice:example.org", "Alice"), + ]); + const crypto = client.getCrypto()!; + jest.spyOn(crypto, "getUserVerificationStatus").mockResolvedValue( + new UserVerificationStatus(false, false, false, true), + ); + crypto.pinCurrentUserIdentity = jest.fn(); + renderComponent(client, room); + + await waitFor(() => + expect( + getWarningByText("Alice's (@alice:example.org) identity appears to have changed."), + ).toBeInTheDocument(), + ); + await userEvent.click(screen.getByRole("button")!); + await waitFor(() => expect(crypto.pinCurrentUserIdentity).toHaveBeenCalledWith("@alice:example.org")); + }); + + // We don't display warnings in non-encrypted rooms, but if encryption is + // enabled, then we should display a warning if there are any users whose + // identity need accepting. + it("displays pending warnings when encryption is enabled", async () => { + jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([ + mockRoomMember("@alice:example.org", "Alice"), + ]); + // Start the room off unencrypted. We shouldn't display anything. + const crypto = client.getCrypto()!; + jest.spyOn(crypto, "isEncryptionEnabledInRoom").mockResolvedValue(false); + jest.spyOn(crypto, "getUserVerificationStatus").mockResolvedValue( + new UserVerificationStatus(false, false, false, true), + ); + + renderComponent(client, room); + await sleep(10); // give it some time to finish initialising + expect(() => getWarningByText("Alice's (@alice:example.org) identity appears to have changed.")).toThrow(); + + // Encryption gets enabled in the room. We should now warn that Alice's + // identity changed. + jest.spyOn(crypto, "isEncryptionEnabledInRoom").mockResolvedValue(true); + client.emit( + RoomStateEvent.Events, + new MatrixEvent({ + event_id: "$event_id", + type: EventType.RoomEncryption, + state_key: "", + content: { + algorithm: "m.megolm.v1.aes-sha2", + }, + room_id: ROOM_ID, + sender: "@alice:example.org", + }), + dummyRoomState(), + null, + ); + await waitFor(() => + expect( + getWarningByText("Alice's (@alice:example.org) identity appears to have changed."), + ).toBeInTheDocument(), + ); + }); + + // When a user's identity needs approval, or has been approved, the display + // should update appropriately. + it("updates the display when identity changes", async () => { + jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([ + mockRoomMember("@alice:example.org", "Alice"), + ]); + jest.spyOn(room, "getMember").mockReturnValue(mockRoomMember("@alice:example.org", "Alice")); + const crypto = client.getCrypto()!; + jest.spyOn(crypto, "getUserVerificationStatus").mockResolvedValue( + new UserVerificationStatus(false, false, false, false), + ); + renderComponent(client, room); + await sleep(10); // give it some time to finish initialising + expect(() => getWarningByText("Alice's (@alice:example.org) identity appears to have changed.")).toThrow(); + + // The user changes their identity, so we should show the warning. + act(() => { + client.emit( + CryptoEvent.UserTrustStatusChanged, + "@alice:example.org", + new UserVerificationStatus(false, false, false, true), + ); + }); + await waitFor(() => + expect( + getWarningByText("Alice's (@alice:example.org) identity appears to have changed."), + ).toBeInTheDocument(), + ); + + // Simulate the user's new identity having been approved, so we no + // longer show the warning. + act(() => { + client.emit( + CryptoEvent.UserTrustStatusChanged, + "@alice:example.org", + new UserVerificationStatus(false, false, false, false), + ); + }); + await waitFor(() => + expect(() => getWarningByText("Alice's (@alice:example.org) identity appears to have changed.")).toThrow(), + ); + }); + + // We only display warnings about users in the room. When someone + // joins/leaves, we should update the warning appropriately. + describe("updates the display when a member joins/leaves", () => { + it("when invited users can see encrypted messages", async () => { + // Nobody in the room yet + jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([]); + jest.spyOn(room, "getMember").mockImplementation((userId) => mockRoomMember(userId)); + jest.spyOn(room, "shouldEncryptForInvitedMembers").mockReturnValue(true); + const crypto = client.getCrypto()!; + jest.spyOn(crypto, "getUserVerificationStatus").mockResolvedValue( + new UserVerificationStatus(false, false, false, true), + ); + renderComponent(client, room); + await sleep(10); // give it some time to finish initialising + + // Alice joins. Her identity needs approval, so we should show a warning. + client.emit( + RoomStateEvent.Events, + new MatrixEvent({ + event_id: "$event_id", + type: EventType.RoomMember, + state_key: "@alice:example.org", + content: { + membership: "join", + }, + room_id: ROOM_ID, + sender: "@alice:example.org", + }), + dummyRoomState(), + null, + ); + await waitFor(() => + expect(getWarningByText("@alice:example.org's identity appears to have changed.")).toBeInTheDocument(), + ); + + // Bob is invited. His identity needs approval, so we should show a + // warning for him after Alice's warning is resolved by her leaving. + client.emit( + RoomStateEvent.Events, + new MatrixEvent({ + event_id: "$event_id", + type: EventType.RoomMember, + state_key: "@bob:example.org", + content: { + membership: "invite", + }, + room_id: ROOM_ID, + sender: "@carol:example.org", + }), + dummyRoomState(), + null, + ); + + // Alice leaves, so we no longer show her warning, but we will show + // a warning for Bob. + act(() => { + client.emit( + RoomStateEvent.Events, + new MatrixEvent({ + event_id: "$event_id", + type: EventType.RoomMember, + state_key: "@alice:example.org", + content: { + membership: "leave", + }, + room_id: ROOM_ID, + sender: "@alice:example.org", + }), + dummyRoomState(), + null, + ); + }); + await waitFor(() => + expect(() => getWarningByText("@alice:example.org's identity appears to have changed.")).toThrow(), + ); + await waitFor(() => + expect(getWarningByText("@bob:example.org's identity appears to have changed.")).toBeInTheDocument(), + ); + }); + + it("when invited users cannot see encrypted messages", async () => { + // Nobody in the room yet + jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([]); + jest.spyOn(room, "getMember").mockImplementation((userId) => mockRoomMember(userId)); + jest.spyOn(room, "shouldEncryptForInvitedMembers").mockReturnValue(false); + const crypto = client.getCrypto()!; + jest.spyOn(crypto, "getUserVerificationStatus").mockResolvedValue( + new UserVerificationStatus(false, false, false, true), + ); + renderComponent(client, room); + await sleep(10); // give it some time to finish initialising + + // Alice joins. Her identity needs approval, so we should show a warning. + client.emit( + RoomStateEvent.Events, + new MatrixEvent({ + event_id: "$event_id", + type: EventType.RoomMember, + state_key: "@alice:example.org", + content: { + membership: "join", + }, + room_id: ROOM_ID, + sender: "@alice:example.org", + }), + dummyRoomState(), + null, + ); + await waitFor(() => + expect(getWarningByText("@alice:example.org's identity appears to have changed.")).toBeInTheDocument(), + ); + + // Bob is invited. His identity needs approval, but we don't encrypt + // to him, so we won't show a warning. (When Alice leaves, the + // display won't be updated to show a warningfor Bob.) + client.emit( + RoomStateEvent.Events, + new MatrixEvent({ + event_id: "$event_id", + type: EventType.RoomMember, + state_key: "@bob:example.org", + content: { + membership: "invite", + }, + room_id: ROOM_ID, + sender: "@carol:example.org", + }), + dummyRoomState(), + null, + ); + + // Alice leaves, so we no longer show her warning, and we don't show + // a warning for Bob. + act(() => { + client.emit( + RoomStateEvent.Events, + new MatrixEvent({ + event_id: "$event_id", + type: EventType.RoomMember, + state_key: "@alice:example.org", + content: { + membership: "leave", + }, + room_id: ROOM_ID, + sender: "@alice:example.org", + }), + dummyRoomState(), + null, + ); + }); + await waitFor(() => + expect(() => getWarningByText("@alice:example.org's identity appears to have changed.")).toThrow(), + ); + await waitFor(() => + expect(() => getWarningByText("@bob:example.org's identity appears to have changed.")).toThrow(), + ); + }); + + it("when member leaves immediately after component is loaded", async () => { + jest.spyOn(room, "getEncryptionTargetMembers").mockImplementation(async () => { + setTimeout(() => { + // Alice immediately leaves after we get the room + // membership, so we shouldn't show the warning any more + client.emit( + RoomStateEvent.Events, + new MatrixEvent({ + event_id: "$event_id", + type: EventType.RoomMember, + state_key: "@alice:example.org", + content: { + membership: "leave", + }, + room_id: ROOM_ID, + sender: "@alice:example.org", + }), + dummyRoomState(), + null, + ); + }); + return [mockRoomMember("@alice:example.org")]; + }); + jest.spyOn(room, "getMember").mockImplementation((userId) => mockRoomMember(userId)); + jest.spyOn(room, "shouldEncryptForInvitedMembers").mockReturnValue(false); + const crypto = client.getCrypto()!; + jest.spyOn(crypto, "getUserVerificationStatus").mockResolvedValue( + new UserVerificationStatus(false, false, false, true), + ); + renderComponent(client, room); + + await sleep(10); + expect(() => getWarningByText("@alice:example.org's identity appears to have changed.")).toThrow(); + }); + + it("when member leaves immediately after joining", async () => { + // Nobody in the room yet + jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([]); + jest.spyOn(room, "getMember").mockImplementation((userId) => mockRoomMember(userId)); + jest.spyOn(room, "shouldEncryptForInvitedMembers").mockReturnValue(false); + const crypto = client.getCrypto()!; + jest.spyOn(crypto, "getUserVerificationStatus").mockResolvedValue( + new UserVerificationStatus(false, false, false, true), + ); + renderComponent(client, room); + await sleep(10); // give it some time to finish initialising + + // Alice joins. Her identity needs approval, so we should show a warning. + client.emit( + RoomStateEvent.Events, + new MatrixEvent({ + event_id: "$event_id", + type: EventType.RoomMember, + state_key: "@alice:example.org", + content: { + membership: "join", + }, + room_id: ROOM_ID, + sender: "@alice:example.org", + }), + dummyRoomState(), + null, + ); + // ... but she immediately leaves, so we shouldn't show the warning any more + client.emit( + RoomStateEvent.Events, + new MatrixEvent({ + event_id: "$event_id", + type: EventType.RoomMember, + state_key: "@alice:example.org", + content: { + membership: "leave", + }, + room_id: ROOM_ID, + sender: "@alice:example.org", + }), + dummyRoomState(), + null, + ); + await sleep(10); // give it some time to finish + expect(() => getWarningByText("@alice:example.org's identity appears to have changed.")).toThrow(); + }); + }); + + // When we have multiple users whose identity needs approval, one user's + // identity no longer needs approval (e.g. their identity was approved), + // then we show the next one. + it("displays the next user when the current user's identity is approved", async () => { + jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([ + mockRoomMember("@alice:example.org", "Alice"), + mockRoomMember("@bob:example.org"), + ]); + const crypto = client.getCrypto()!; + jest.spyOn(crypto, "getUserVerificationStatus").mockResolvedValue( + new UserVerificationStatus(false, false, false, true), + ); + + renderComponent(client, room); + // We should warn about Alice's identity first. + await waitFor(() => + expect( + getWarningByText("Alice's (@alice:example.org) identity appears to have changed."), + ).toBeInTheDocument(), + ); + + // Simulate Alice's new identity having been approved, so now we warn + // about Bob's identity. + act(() => { + client.emit( + CryptoEvent.UserTrustStatusChanged, + "@alice:example.org", + new UserVerificationStatus(false, false, false, false), + ); + }); + await waitFor(() => + expect(getWarningByText("@bob:example.org's identity appears to have changed.")).toBeInTheDocument(), + ); + }); + + // If we get an update for a user's verification status while we're fetching + // that user's verification status, we should display based on the updated + // value. + describe("handles races between fetching verification status and receiving updates", () => { + // First case: check that if the update says that the user identity + // needs approval, but the fetch says it doesn't, we show the warning. + it("update says identity needs approval", async () => { + jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([ + mockRoomMember("@alice:example.org", "Alice"), + ]); + jest.spyOn(room, "getMember").mockReturnValue(mockRoomMember("@alice:example.org", "Alice")); + const crypto = client.getCrypto()!; + jest.spyOn(crypto, "getUserVerificationStatus").mockImplementation(async () => { + act(() => { + client.emit( + CryptoEvent.UserTrustStatusChanged, + "@alice:example.org", + new UserVerificationStatus(false, false, false, true), + ); + }); + return Promise.resolve(new UserVerificationStatus(false, false, false, false)); + }); + renderComponent(client, room); + await sleep(10); // give it some time to finish initialising + await waitFor(() => + expect( + getWarningByText("Alice's (@alice:example.org) identity appears to have changed."), + ).toBeInTheDocument(), + ); + }); + + // Second case: check that if the update says that the user identity + // doesn't needs approval, but the fetch says it does, we don't show the + // warning. + it("update says identity doesn't need approval", async () => { + jest.spyOn(room, "getEncryptionTargetMembers").mockResolvedValue([ + mockRoomMember("@alice:example.org", "Alice"), + ]); + jest.spyOn(room, "getMember").mockReturnValue(mockRoomMember("@alice:example.org", "Alice")); + const crypto = client.getCrypto()!; + jest.spyOn(crypto, "getUserVerificationStatus").mockImplementation(async () => { + act(() => { + client.emit( + CryptoEvent.UserTrustStatusChanged, + "@alice:example.org", + new UserVerificationStatus(false, false, false, false), + ); + }); + return Promise.resolve(new UserVerificationStatus(false, false, false, true)); + }); + renderComponent(client, room); + await sleep(10); // give it some time to finish initialising + await waitFor(() => + expect(() => + getWarningByText("Alice's (@alice:example.org) identity appears to have changed."), + ).toThrow(), + ); + }); + }); +}); diff --git a/test/unit-tests/components/views/rooms/__snapshots__/ReadReceiptGroup-test.tsx.snap b/test/unit-tests/components/views/rooms/__snapshots__/ReadReceiptGroup-test.tsx.snap index b0ba944a66..60e8f844af 100644 --- a/test/unit-tests/components/views/rooms/__snapshots__/ReadReceiptGroup-test.tsx.snap +++ b/test/unit-tests/components/views/rooms/__snapshots__/ReadReceiptGroup-test.tsx.snap @@ -84,7 +84,7 @@ exports[`ReadReceiptGroup <ReadReceiptPerson /> should render 1`] = ` <p class="mx_ReadReceiptGroup_secondary" > - Wed, 15 May, 0:00 + ==MOCK FORMATTED DATE== </p> </div> </div> diff --git a/test/unit-tests/components/views/rooms/__snapshots__/RoomHeader-test.tsx.snap b/test/unit-tests/components/views/rooms/__snapshots__/RoomHeader-test.tsx.snap index af0b884a38..a6f412a3ac 100644 --- a/test/unit-tests/components/views/rooms/__snapshots__/RoomHeader-test.tsx.snap +++ b/test/unit-tests/components/views/rooms/__snapshots__/RoomHeader-test.tsx.snap @@ -47,7 +47,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = ` style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x);" > <button - aria-labelledby=":r154:" + aria-labelledby=":r16c:" class="_icon-button_bh2qc_17" role="button" style="--cpd-icon-button-size: 32px;" @@ -73,7 +73,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = ` <button aria-disabled="true" aria-label="There's no one here to call" - aria-labelledby=":r159:" + aria-labelledby=":r16h:" class="_icon-button_bh2qc_17" role="button" style="--cpd-icon-button-size: 32px;" @@ -98,7 +98,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = ` </button> <button aria-label="Room info" - aria-labelledby=":r15e:" + aria-labelledby=":r16m:" class="_icon-button_bh2qc_17" role="button" style="--cpd-icon-button-size: 32px;" @@ -123,7 +123,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = ` </button> <button aria-label="Threads" - aria-labelledby=":r15j:" + aria-labelledby=":r16r:" class="_icon-button_bh2qc_17" role="button" style="--cpd-icon-button-size: 32px;" diff --git a/test/unit-tests/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx b/test/unit-tests/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx index 5d3c455288..a76c7a8b90 100644 --- a/test/unit-tests/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx +++ b/test/unit-tests/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx @@ -11,7 +11,6 @@ import React from "react"; import { fireEvent, render, screen, waitFor } from "jest-matrix-react"; import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext"; -import RoomContext from "../../../../../../src/contexts/RoomContext"; import defaultDispatcher from "../../../../../../src/dispatcher/dispatcher"; import { Action } from "../../../../../../src/dispatcher/actions"; import { flushPromises, mkEvent } from "../../../../../test-utils"; @@ -23,6 +22,7 @@ import { ComposerInsertPayload, ComposerType } from "../../../../../../src/dispa import { ActionPayload } from "../../../../../../src/dispatcher/payloads"; import * as EmojiButton from "../../../../../../src/components/views/rooms/EmojiButton"; import { createMocks } from "./utils"; +import { ScopedRoomContextProvider } from "../../../../../../src/contexts/ScopedRoomContext.tsx"; describe("EditWysiwygComposer", () => { afterEach(() => { @@ -39,9 +39,9 @@ describe("EditWysiwygComposer", () => { ) => { return render( <MatrixClientContext.Provider value={client}> - <RoomContext.Provider value={roomContext}> + <ScopedRoomContextProvider {...roomContext}> <EditWysiwygComposer disabled={disabled} editorStateTransfer={_editorStateTransfer} /> - </RoomContext.Provider> + </ScopedRoomContextProvider> </MatrixClientContext.Provider>, ); }; @@ -64,9 +64,9 @@ describe("EditWysiwygComposer", () => { rerender( <MatrixClientContext.Provider value={mockClient}> - <RoomContext.Provider value={{ ...defaultRoomContext, room: undefined }}> + <ScopedRoomContextProvider {...defaultRoomContext} room={undefined}> <EditWysiwygComposer disabled={false} editorStateTransfer={editorStateTransfer} /> - </RoomContext.Provider> + </ScopedRoomContextProvider> </MatrixClientContext.Provider>, ); @@ -196,9 +196,9 @@ describe("EditWysiwygComposer", () => { // Then screen.getByText("Save").click(); const expectedContent = { - "body": ` * foo bar`, + "body": `* foo bar`, "format": "org.matrix.custom.html", - "formatted_body": ` * foo bar`, + "formatted_body": `* foo bar`, "m.new_content": { body: "foo bar", format: "org.matrix.custom.html", @@ -275,10 +275,10 @@ describe("EditWysiwygComposer", () => { ); render( <MatrixClientContext.Provider value={mockClient}> - <RoomContext.Provider value={defaultRoomContext}> + <ScopedRoomContextProvider {...defaultRoomContext}> <EditWysiwygComposer editorStateTransfer={editorStateTransfer} /> <Emoji menuPosition={{ chevronFace: ChevronFace.Top }} /> - </RoomContext.Provider> + </ScopedRoomContextProvider> </MatrixClientContext.Provider>, ); // Same behavior as in RoomView.tsx diff --git a/test/unit-tests/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx b/test/unit-tests/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx index 27110149f5..89415448b7 100644 --- a/test/unit-tests/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx +++ b/test/unit-tests/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx @@ -11,7 +11,6 @@ import React from "react"; import { act, fireEvent, render, screen, waitFor } from "jest-matrix-react"; import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext"; -import RoomContext from "../../../../../../src/contexts/RoomContext"; import defaultDispatcher from "../../../../../../src/dispatcher/dispatcher"; import { Action } from "../../../../../../src/dispatcher/actions"; import { flushPromises } from "../../../../../test-utils"; @@ -20,6 +19,7 @@ import { aboveLeftOf } from "../../../../../../src/components/structures/Context import { ComposerInsertPayload, ComposerType } from "../../../../../../src/dispatcher/payloads/ComposerInsertPayload"; import { setSelection } from "../../../../../../src/components/views/rooms/wysiwyg_composer/utils/selection"; import { createMocks } from "./utils"; +import { ScopedRoomContextProvider } from "../../../../../../src/contexts/ScopedRoomContext.tsx"; jest.mock("../../../../../../src/components/views/rooms/EmojiButton", () => ({ EmojiButton: ({ addEmoji }: { addEmoji: (emoji: string) => void }) => { @@ -66,7 +66,7 @@ describe("SendWysiwygComposer", () => { ) => { return render( <MatrixClientContext.Provider value={mockClient}> - <RoomContext.Provider value={defaultRoomContext}> + <ScopedRoomContextProvider {...defaultRoomContext}> <SendWysiwygComposer onChange={onChange} onSend={onSend} @@ -75,7 +75,7 @@ describe("SendWysiwygComposer", () => { menuPosition={aboveLeftOf({ top: 0, bottom: 0, right: 0 })} placeholder={placeholder} /> - </RoomContext.Provider> + </ScopedRoomContextProvider> </MatrixClientContext.Provider>, ); }; diff --git a/test/unit-tests/components/views/rooms/wysiwyg_composer/components/PlainTextComposer-test.tsx b/test/unit-tests/components/views/rooms/wysiwyg_composer/components/PlainTextComposer-test.tsx index 1ca6e9736c..ae37afe860 100644 --- a/test/unit-tests/components/views/rooms/wysiwyg_composer/components/PlainTextComposer-test.tsx +++ b/test/unit-tests/components/views/rooms/wysiwyg_composer/components/PlainTextComposer-test.tsx @@ -14,7 +14,7 @@ import { PlainTextComposer } from "../../../../../../../src/components/views/roo import * as mockUseSettingsHook from "../../../../../../../src/hooks/useSettings"; import * as mockKeyboard from "../../../../../../../src/Keyboard"; import { createMocks } from "../utils"; -import RoomContext from "../../../../../../../src/contexts/RoomContext"; +import { ScopedRoomContextProvider } from "../../../../../../../src/contexts/ScopedRoomContext.tsx"; describe("PlainTextComposer", () => { const customRender = ( @@ -275,9 +275,9 @@ describe("PlainTextComposer", () => { const { defaultRoomContext } = createMocks(); render( - <RoomContext.Provider value={defaultRoomContext}> + <ScopedRoomContextProvider {...defaultRoomContext}> <PlainTextComposer onChange={jest.fn()} onSend={jest.fn()} disabled={false} initialContent="" /> - </RoomContext.Provider>, + </ScopedRoomContextProvider>, ); expect(screen.getByTestId("autocomplete-wrapper")).toBeInTheDocument(); diff --git a/test/unit-tests/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx b/test/unit-tests/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx index 68ce88ce0c..b0f4bbb5bd 100644 --- a/test/unit-tests/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx +++ b/test/unit-tests/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete-test.tsx @@ -11,12 +11,12 @@ import React, { createRef } from "react"; import { render, screen, waitFor } from "jest-matrix-react"; import MatrixClientContext from "../../../../../../../src/contexts/MatrixClientContext"; -import RoomContext from "../../../../../../../src/contexts/RoomContext"; import { WysiwygAutocomplete } from "../../../../../../../src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete"; import { getRoomContext, mkStubRoom, stubClient } from "../../../../../../test-utils"; import Autocomplete from "../../../../../../../src/components/views/rooms/Autocomplete"; import Autocompleter, { ICompletion } from "../../../../../../../src/autocomplete/Autocompleter"; import AutocompleteProvider from "../../../../../../../src/autocomplete/AutocompleteProvider"; +import { ScopedRoomContextProvider } from "../../../../../../../src/contexts/ScopedRoomContext.tsx"; const mockCompletion: ICompletion[] = [ { @@ -71,7 +71,7 @@ describe("WysiwygAutocomplete", () => { return render( <MatrixClientContext.Provider value={mockClient}> - <RoomContext.Provider value={mockRoomContext}> + <ScopedRoomContextProvider {...mockRoomContext}> <WysiwygAutocomplete ref={autocompleteRef} suggestion={null} @@ -80,7 +80,7 @@ describe("WysiwygAutocomplete", () => { handleAtRoomMention={mockHandleAtRoomMention} {...props} /> - </RoomContext.Provider> + </ScopedRoomContextProvider> </MatrixClientContext.Provider>, ); }; diff --git a/test/unit-tests/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx b/test/unit-tests/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx index adfd1412a7..c3c480eb82 100644 --- a/test/unit-tests/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx +++ b/test/unit-tests/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx @@ -18,7 +18,6 @@ import defaultDispatcher from "../../../../../../../src/dispatcher/dispatcher"; import * as EventUtils from "../../../../../../../src/utils/EventUtils"; import { Action } from "../../../../../../../src/dispatcher/actions"; import MatrixClientContext from "../../../../../../../src/contexts/MatrixClientContext"; -import RoomContext from "../../../../../../../src/contexts/RoomContext"; import { ComposerContext, getDefaultContextValue, @@ -32,20 +31,21 @@ import Autocompleter, { ICompletion } from "../../../../../../../src/autocomplet import AutocompleteProvider from "../../../../../../../src/autocomplete/AutocompleteProvider"; import * as Permalinks from "../../../../../../../src/utils/permalinks/Permalinks"; import { PermalinkParts } from "../../../../../../../src/utils/permalinks/PermalinkConstructor"; +import { ScopedRoomContextProvider } from "../../../../../../../src/contexts/ScopedRoomContext.tsx"; describe("WysiwygComposer", () => { const customRender = (onChange = jest.fn(), onSend = jest.fn(), disabled = false, initialContent?: string) => { const { mockClient, defaultRoomContext } = createMocks(); return render( <MatrixClientContext.Provider value={mockClient}> - <RoomContext.Provider value={defaultRoomContext}> + <ScopedRoomContextProvider {...defaultRoomContext}> <WysiwygComposer onChange={onChange} onSend={onSend} disabled={disabled} initialContent={initialContent} /> - </RoomContext.Provider> + </ScopedRoomContextProvider> </MatrixClientContext.Provider>, ); }; @@ -523,7 +523,7 @@ describe("WysiwygComposer", () => { ) => { return render( <MatrixClientContext.Provider value={client}> - <RoomContext.Provider value={roomContext}> + <ScopedRoomContextProvider {...roomContext}> <ComposerContext.Provider value={getDefaultContextValue({ editorStateTransfer: _editorStateTransfer })} > @@ -537,7 +537,7 @@ describe("WysiwygComposer", () => { } /> </ComposerContext.Provider> - </RoomContext.Provider> + </ScopedRoomContextProvider> </MatrixClientContext.Provider>, ); }; diff --git a/test/unit-tests/components/views/rooms/wysiwyg_composer/utils/createMessageContent-test.ts b/test/unit-tests/components/views/rooms/wysiwyg_composer/utils/createMessageContent-test.ts index f0d9ed73c1..74852e9c6d 100644 --- a/test/unit-tests/components/views/rooms/wysiwyg_composer/utils/createMessageContent-test.ts +++ b/test/unit-tests/components/views/rooms/wysiwyg_composer/utils/createMessageContent-test.ts @@ -88,9 +88,9 @@ describe("createMessageContent", () => { // Then expect(content).toEqual({ - "body": " * *__hello__ world*", + "body": "* *__hello__ world*", "format": "org.matrix.custom.html", - "formatted_body": ` * ${message}`, + "formatted_body": `* ${message}`, "msgtype": "m.text", "m.new_content": { body: "*__hello__ world*", diff --git a/test/unit-tests/components/views/rooms/wysiwyg_composer/utils/message-test.ts b/test/unit-tests/components/views/rooms/wysiwyg_composer/utils/message-test.ts index c339978f92..43320b0444 100644 --- a/test/unit-tests/components/views/rooms/wysiwyg_composer/utils/message-test.ts +++ b/test/unit-tests/components/views/rooms/wysiwyg_composer/utils/message-test.ts @@ -418,8 +418,8 @@ describe("message", () => { // Then const { msgtype, format } = mockEvent.getContent(); const expectedContent = { - "body": ` * ${newMessage}`, - "formatted_body": ` * ${newMessage}`, + "body": `* ${newMessage}`, + "formatted_body": `* ${newMessage}`, "m.new_content": { body: "Replying to this new content", format: "org.matrix.custom.html", diff --git a/test/unit-tests/components/views/settings/Notifications-test.tsx b/test/unit-tests/components/views/settings/Notifications-test.tsx index 69b45135cb..e3d9716901 100644 --- a/test/unit-tests/components/views/settings/Notifications-test.tsx +++ b/test/unit-tests/components/views/settings/Notifications-test.tsx @@ -915,7 +915,7 @@ describe("<Notifications />", () => { user: "@alice:example.org", ts: 1, }); - await room.addLiveEvents([message]); + await room.addLiveEvents([message], { addToState: true }); const { container } = await getComponentAndWait(); const clearNotificationEl = getByTestId(container, "clear-notifications"); diff --git a/test/unit-tests/components/views/settings/SecureBackupPanel-test.tsx b/test/unit-tests/components/views/settings/SecureBackupPanel-test.tsx index f2aa15f355..63490bf915 100644 --- a/test/unit-tests/components/views/settings/SecureBackupPanel-test.tsx +++ b/test/unit-tests/components/views/settings/SecureBackupPanel-test.tsx @@ -28,14 +28,13 @@ describe("<SecureBackupPanel />", () => { const client = getMockClientWithEventEmitter({ ...mockClientMethodsUser(userId), ...mockClientMethodsCrypto(), - getKeyBackupVersion: jest.fn().mockReturnValue("1"), getClientWellKnown: jest.fn(), }); const getComponent = () => render(<SecureBackupPanel />); beforeEach(() => { - client.getKeyBackupVersion.mockResolvedValue({ + jest.spyOn(client.getCrypto()!, "getKeyBackupInfo").mockResolvedValue({ version: "1", algorithm: "test", auth_data: { @@ -52,7 +51,6 @@ describe("<SecureBackupPanel />", () => { }); mocked(client.secretStorage.hasKey).mockClear().mockResolvedValue(false); - client.getKeyBackupVersion.mockClear(); mocked(accessSecretStorage).mockClear().mockResolvedValue(); }); @@ -65,8 +63,8 @@ describe("<SecureBackupPanel />", () => { }); it("handles error fetching backup", async () => { - // getKeyBackupVersion can fail for various reasons - client.getKeyBackupVersion.mockImplementation(async () => { + // getKeyBackupInfo can fail for various reasons + jest.spyOn(client.getCrypto()!, "getKeyBackupInfo").mockImplementation(async () => { throw new Error("beep beep"); }); const renderResult = getComponent(); @@ -75,9 +73,9 @@ describe("<SecureBackupPanel />", () => { }); it("handles absence of backup", async () => { - client.getKeyBackupVersion.mockResolvedValue(null); + jest.spyOn(client.getCrypto()!, "getKeyBackupInfo").mockResolvedValue(null); getComponent(); - // flush getKeyBackupVersion promise + // flush getKeyBackupInfo promise await flushPromises(); expect(screen.getByText("Back up your keys before signing out to avoid losing them.")).toBeInTheDocument(); }); @@ -120,7 +118,7 @@ describe("<SecureBackupPanel />", () => { }); it("deletes backup after confirmation", async () => { - client.getKeyBackupVersion + jest.spyOn(client.getCrypto()!, "getKeyBackupInfo") .mockResolvedValueOnce({ version: "1", algorithm: "test", @@ -157,7 +155,7 @@ describe("<SecureBackupPanel />", () => { // flush checkKeyBackup promise await flushPromises(); - client.getKeyBackupVersion.mockClear(); + jest.spyOn(client.getCrypto()!, "getKeyBackupInfo").mockClear(); mocked(client.getCrypto()!).isKeyBackupTrusted.mockClear(); fireEvent.click(screen.getByText("Reset")); @@ -167,7 +165,7 @@ describe("<SecureBackupPanel />", () => { await flushPromises(); // backup status refreshed - expect(client.getKeyBackupVersion).toHaveBeenCalled(); + expect(client.getCrypto()!.getKeyBackupInfo).toHaveBeenCalled(); expect(client.getCrypto()!.isKeyBackupTrusted).toHaveBeenCalled(); }); }); diff --git a/test/unit-tests/components/views/settings/SetIntegrationManager-test.tsx b/test/unit-tests/components/views/settings/SetIntegrationManager-test.tsx index 888499d524..5c77e88d93 100644 --- a/test/unit-tests/components/views/settings/SetIntegrationManager-test.tsx +++ b/test/unit-tests/components/views/settings/SetIntegrationManager-test.tsx @@ -35,7 +35,7 @@ describe("SetIntegrationManager", () => { deleteThreePid: jest.fn(), }); - let stores: SdkContextClass; + let stores!: SdkContextClass; const getComponent = () => ( <MatrixClientContext.Provider value={mockClient}> diff --git a/test/unit-tests/components/views/settings/__snapshots__/LayoutSwitcher-test.tsx.snap b/test/unit-tests/components/views/settings/__snapshots__/LayoutSwitcher-test.tsx.snap index 63c786873c..2aa08adb94 100644 --- a/test/unit-tests/components/views/settings/__snapshots__/LayoutSwitcher-test.tsx.snap +++ b/test/unit-tests/components/views/settings/__snapshots__/LayoutSwitcher-test.tsx.snap @@ -10,7 +10,7 @@ exports[`<LayoutSwitcher /> should render 1`] = ` class="mx_SettingsSubsectionHeading" > <h3 - class="mx_Heading_h3 mx_SettingsSubsectionHeading_heading" + class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" > Message layout </h3> @@ -19,14 +19,14 @@ exports[`<LayoutSwitcher /> should render 1`] = ` class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi" > <form - class="_root_dgy0u_24 mx_LayoutSwitcher_LayoutSelector" + class="_root_ssths_24 mx_LayoutSwitcher_LayoutSelector" > <div - class="_field_dgy0u_34 mxLayoutSwitcher_LayoutSelector_LayoutRadio" + class="_field_ssths_34 mxLayoutSwitcher_LayoutSelector_LayoutRadio" > <label aria-label="Modern" - class="_label_dgy0u_67" + class="_label_ssths_67" for="radix-:r0:" > <div @@ -149,11 +149,11 @@ exports[`<LayoutSwitcher /> should render 1`] = ` </label> </div> <div - class="_field_dgy0u_34 mxLayoutSwitcher_LayoutSelector_LayoutRadio" + class="_field_ssths_34 mxLayoutSwitcher_LayoutSelector_LayoutRadio" > <label aria-label="Message bubbles" - class="_label_dgy0u_67" + class="_label_ssths_67" for="radix-:r9:" > <div @@ -275,11 +275,11 @@ exports[`<LayoutSwitcher /> should render 1`] = ` </label> </div> <div - class="_field_dgy0u_34 mxLayoutSwitcher_LayoutSelector_LayoutRadio" + class="_field_ssths_34 mxLayoutSwitcher_LayoutSelector_LayoutRadio" > <label aria-label="IRC (experimental)" - class="_label_dgy0u_67" + class="_label_ssths_67" for="radix-:ri:" > <div @@ -402,13 +402,13 @@ exports[`<LayoutSwitcher /> should render 1`] = ` </div> </form> <form - class="_root_dgy0u_24" + class="_root_ssths_24" > <div - class="_inline-field_dgy0u_40" + class="_inline-field_ssths_40" > <div - class="_inline-field-control_dgy0u_52" + class="_inline-field-control_ssths_52" > <div class="_container_qnvru_18" @@ -427,16 +427,16 @@ exports[`<LayoutSwitcher /> should render 1`] = ` </div> </div> <div - class="_inline-field-body_dgy0u_46" + class="_inline-field-body_ssths_46" > <label - class="_label_dgy0u_67" + class="_label_ssths_67" for="radix-:rr:" > Show compact text and messages </label> <span - class="_message_dgy0u_98 _help-message_dgy0u_104" + class="_message_ssths_93 _help-message_ssths_99" id="radix-:rs:" > Modern layout must be selected to use this feature. diff --git a/test/unit-tests/components/views/settings/__snapshots__/ThemeChoicePanel-test.tsx.snap b/test/unit-tests/components/views/settings/__snapshots__/ThemeChoicePanel-test.tsx.snap index 319af317eb..6cfb0c7f29 100644 --- a/test/unit-tests/components/views/settings/__snapshots__/ThemeChoicePanel-test.tsx.snap +++ b/test/unit-tests/components/views/settings/__snapshots__/ThemeChoicePanel-test.tsx.snap @@ -10,7 +10,7 @@ exports[`<ThemeChoicePanel /> custom theme should display custom theme 1`] = ` class="mx_SettingsSubsectionHeading" > <h3 - class="mx_Heading_h3 mx_SettingsSubsectionHeading_heading" + class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" > Theme </h3> @@ -19,13 +19,13 @@ exports[`<ThemeChoicePanel /> custom theme should display custom theme 1`] = ` class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi" > <form - class="_root_dgy0u_24" + class="_root_ssths_24" > <div - class="_inline-field_dgy0u_40" + class="_inline-field_ssths_40" > <div - class="_inline-field-control_dgy0u_52" + class="_inline-field-control_ssths_52" > <div class="_container_qnvru_18" @@ -43,10 +43,10 @@ exports[`<ThemeChoicePanel /> custom theme should display custom theme 1`] = ` </div> </div> <div - class="_inline-field-body_dgy0u_46" + class="_inline-field-body_ssths_46" > <label - class="_label_dgy0u_67" + class="_label_ssths_67" for="radix-:r28:" > Match system theme @@ -55,13 +55,13 @@ exports[`<ThemeChoicePanel /> custom theme should display custom theme 1`] = ` </div> </form> <form - class="_root_dgy0u_24 mx_ThemeChoicePanel_ThemeSelectors" + class="_root_ssths_24 mx_ThemeChoicePanel_ThemeSelectors" > <div - class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector mx_ThemeChoicePanel_themeSelector_enabled cpd-theme-light" + class="_inline-field_ssths_40 mx_ThemeChoicePanel_themeSelector mx_ThemeChoicePanel_themeSelector_enabled cpd-theme-light" > <div - class="_inline-field-control_dgy0u_52" + class="_inline-field-control_ssths_52" > <div class="_container_1vw5h_18" @@ -81,10 +81,10 @@ exports[`<ThemeChoicePanel /> custom theme should display custom theme 1`] = ` </div> </div> <div - class="_inline-field-body_dgy0u_46" + class="_inline-field-body_ssths_46" > <label - class="_label_dgy0u_67 mx_ThemeChoicePanel_themeSelector_Label" + class="_label_ssths_67 mx_ThemeChoicePanel_themeSelector_Label" for="radix-:r29:" > Light @@ -92,10 +92,10 @@ exports[`<ThemeChoicePanel /> custom theme should display custom theme 1`] = ` </div> </div> <div - class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector cpd-theme-dark" + class="_inline-field_ssths_40 mx_ThemeChoicePanel_themeSelector cpd-theme-dark" > <div - class="_inline-field-control_dgy0u_52" + class="_inline-field-control_ssths_52" > <div class="_container_1vw5h_18" @@ -114,10 +114,10 @@ exports[`<ThemeChoicePanel /> custom theme should display custom theme 1`] = ` </div> </div> <div - class="_inline-field-body_dgy0u_46" + class="_inline-field-body_ssths_46" > <label - class="_label_dgy0u_67 mx_ThemeChoicePanel_themeSelector_Label" + class="_label_ssths_67 mx_ThemeChoicePanel_themeSelector_Label" for="radix-:r2a:" > Dark @@ -125,10 +125,10 @@ exports[`<ThemeChoicePanel /> custom theme should display custom theme 1`] = ` </div> </div> <div - class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector cpd-theme-light" + class="_inline-field_ssths_40 mx_ThemeChoicePanel_themeSelector cpd-theme-light" > <div - class="_inline-field-control_dgy0u_52" + class="_inline-field-control_ssths_52" > <div class="_container_1vw5h_18" @@ -147,10 +147,10 @@ exports[`<ThemeChoicePanel /> custom theme should display custom theme 1`] = ` </div> </div> <div - class="_inline-field-body_dgy0u_46" + class="_inline-field-body_ssths_46" > <label - class="_label_dgy0u_67 mx_ThemeChoicePanel_themeSelector_Label" + class="_label_ssths_67 mx_ThemeChoicePanel_themeSelector_Label" for="radix-:r2b:" > High contrast @@ -158,10 +158,10 @@ exports[`<ThemeChoicePanel /> custom theme should display custom theme 1`] = ` </div> </div> <div - class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector cpd-theme-dark" + class="_inline-field_ssths_40 mx_ThemeChoicePanel_themeSelector cpd-theme-dark" > <div - class="_inline-field-control_dgy0u_52" + class="_inline-field-control_ssths_52" > <div class="_container_1vw5h_18" @@ -180,10 +180,10 @@ exports[`<ThemeChoicePanel /> custom theme should display custom theme 1`] = ` </div> </div> <div - class="_inline-field-body_dgy0u_46" + class="_inline-field-body_ssths_46" > <label - class="_label_dgy0u_67 mx_ThemeChoicePanel_themeSelector_Label" + class="_label_ssths_67 mx_ThemeChoicePanel_themeSelector_Label" for="radix-:r2c:" > Alice theme @@ -195,13 +195,13 @@ exports[`<ThemeChoicePanel /> custom theme should display custom theme 1`] = ` class="mx_ThemeChoicePanel_CustomTheme" > <form - class="_root_dgy0u_24 mx_ThemeChoicePanel_CustomTheme_EditInPlace" + class="_root_ssths_24 mx_ThemeChoicePanel_CustomTheme_EditInPlace" > <div - class="_field_dgy0u_34" + class="_field_ssths_34" > <label - class="_label_dgy0u_67" + class="_label_ssths_67" for="radix-:r2d:" > Add custom theme @@ -219,7 +219,7 @@ exports[`<ThemeChoicePanel /> custom theme should display custom theme 1`] = ` /> </div> <span - class="_message_dgy0u_98 _help-message_dgy0u_104" + class="_message_ssths_93 _help-message_ssths_99" id="radix-:r2e:" > Enter the URL of a custom theme you want to apply. @@ -287,7 +287,7 @@ exports[`<ThemeChoicePanel /> custom theme should render the custom theme sectio class="mx_SettingsSubsectionHeading" > <h3 - class="mx_Heading_h3 mx_SettingsSubsectionHeading_heading" + class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" > Theme </h3> @@ -296,13 +296,13 @@ exports[`<ThemeChoicePanel /> custom theme should render the custom theme sectio class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi" > <form - class="_root_dgy0u_24" + class="_root_ssths_24" > <div - class="_inline-field_dgy0u_40" + class="_inline-field_ssths_40" > <div - class="_inline-field-control_dgy0u_52" + class="_inline-field-control_ssths_52" > <div class="_container_qnvru_18" @@ -320,10 +320,10 @@ exports[`<ThemeChoicePanel /> custom theme should render the custom theme sectio </div> </div> <div - class="_inline-field-body_dgy0u_46" + class="_inline-field-body_ssths_46" > <label - class="_label_dgy0u_67" + class="_label_ssths_67" for="radix-:r10:" > Match system theme @@ -332,13 +332,13 @@ exports[`<ThemeChoicePanel /> custom theme should render the custom theme sectio </div> </form> <form - class="_root_dgy0u_24 mx_ThemeChoicePanel_ThemeSelectors" + class="_root_ssths_24 mx_ThemeChoicePanel_ThemeSelectors" > <div - class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector mx_ThemeChoicePanel_themeSelector_enabled cpd-theme-light" + class="_inline-field_ssths_40 mx_ThemeChoicePanel_themeSelector mx_ThemeChoicePanel_themeSelector_enabled cpd-theme-light" > <div - class="_inline-field-control_dgy0u_52" + class="_inline-field-control_ssths_52" > <div class="_container_1vw5h_18" @@ -358,10 +358,10 @@ exports[`<ThemeChoicePanel /> custom theme should render the custom theme sectio </div> </div> <div - class="_inline-field-body_dgy0u_46" + class="_inline-field-body_ssths_46" > <label - class="_label_dgy0u_67 mx_ThemeChoicePanel_themeSelector_Label" + class="_label_ssths_67 mx_ThemeChoicePanel_themeSelector_Label" for="radix-:r11:" > Light @@ -369,10 +369,10 @@ exports[`<ThemeChoicePanel /> custom theme should render the custom theme sectio </div> </div> <div - class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector cpd-theme-dark" + class="_inline-field_ssths_40 mx_ThemeChoicePanel_themeSelector cpd-theme-dark" > <div - class="_inline-field-control_dgy0u_52" + class="_inline-field-control_ssths_52" > <div class="_container_1vw5h_18" @@ -391,10 +391,10 @@ exports[`<ThemeChoicePanel /> custom theme should render the custom theme sectio </div> </div> <div - class="_inline-field-body_dgy0u_46" + class="_inline-field-body_ssths_46" > <label - class="_label_dgy0u_67 mx_ThemeChoicePanel_themeSelector_Label" + class="_label_ssths_67 mx_ThemeChoicePanel_themeSelector_Label" for="radix-:r12:" > Dark @@ -402,10 +402,10 @@ exports[`<ThemeChoicePanel /> custom theme should render the custom theme sectio </div> </div> <div - class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector cpd-theme-light" + class="_inline-field_ssths_40 mx_ThemeChoicePanel_themeSelector cpd-theme-light" > <div - class="_inline-field-control_dgy0u_52" + class="_inline-field-control_ssths_52" > <div class="_container_1vw5h_18" @@ -424,10 +424,10 @@ exports[`<ThemeChoicePanel /> custom theme should render the custom theme sectio </div> </div> <div - class="_inline-field-body_dgy0u_46" + class="_inline-field-body_ssths_46" > <label - class="_label_dgy0u_67 mx_ThemeChoicePanel_themeSelector_Label" + class="_label_ssths_67 mx_ThemeChoicePanel_themeSelector_Label" for="radix-:r13:" > High contrast @@ -435,10 +435,10 @@ exports[`<ThemeChoicePanel /> custom theme should render the custom theme sectio </div> </div> <div - class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector cpd-theme-dark" + class="_inline-field_ssths_40 mx_ThemeChoicePanel_themeSelector cpd-theme-dark" > <div - class="_inline-field-control_dgy0u_52" + class="_inline-field-control_ssths_52" > <div class="_container_1vw5h_18" @@ -457,10 +457,10 @@ exports[`<ThemeChoicePanel /> custom theme should render the custom theme sectio </div> </div> <div - class="_inline-field-body_dgy0u_46" + class="_inline-field-body_ssths_46" > <label - class="_label_dgy0u_67 mx_ThemeChoicePanel_themeSelector_Label" + class="_label_ssths_67 mx_ThemeChoicePanel_themeSelector_Label" for="radix-:r14:" > Alice theme @@ -472,13 +472,13 @@ exports[`<ThemeChoicePanel /> custom theme should render the custom theme sectio class="mx_ThemeChoicePanel_CustomTheme" > <form - class="_root_dgy0u_24 mx_ThemeChoicePanel_CustomTheme_EditInPlace" + class="_root_ssths_24 mx_ThemeChoicePanel_CustomTheme_EditInPlace" > <div - class="_field_dgy0u_34" + class="_field_ssths_34" > <label - class="_label_dgy0u_67" + class="_label_ssths_67" for="radix-:r15:" > Add custom theme @@ -496,7 +496,7 @@ exports[`<ThemeChoicePanel /> custom theme should render the custom theme sectio /> </div> <span - class="_message_dgy0u_98 _help-message_dgy0u_104" + class="_message_ssths_93 _help-message_ssths_99" id="radix-:r16:" > Enter the URL of a custom theme you want to apply. @@ -564,7 +564,7 @@ exports[`<ThemeChoicePanel /> renders the theme choice UI 1`] = ` class="mx_SettingsSubsectionHeading" > <h3 - class="mx_Heading_h3 mx_SettingsSubsectionHeading_heading" + class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" > Theme </h3> @@ -573,13 +573,13 @@ exports[`<ThemeChoicePanel /> renders the theme choice UI 1`] = ` class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi" > <form - class="_root_dgy0u_24" + class="_root_ssths_24" > <div - class="_inline-field_dgy0u_40" + class="_inline-field_ssths_40" > <div - class="_inline-field-control_dgy0u_52" + class="_inline-field-control_ssths_52" > <div class="_container_qnvru_18" @@ -597,10 +597,10 @@ exports[`<ThemeChoicePanel /> renders the theme choice UI 1`] = ` </div> </div> <div - class="_inline-field-body_dgy0u_46" + class="_inline-field-body_ssths_46" > <label - class="_label_dgy0u_67" + class="_label_ssths_67" for="radix-:r0:" > Match system theme @@ -609,13 +609,13 @@ exports[`<ThemeChoicePanel /> renders the theme choice UI 1`] = ` </div> </form> <form - class="_root_dgy0u_24 mx_ThemeChoicePanel_ThemeSelectors" + class="_root_ssths_24 mx_ThemeChoicePanel_ThemeSelectors" > <div - class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector mx_ThemeChoicePanel_themeSelector_enabled cpd-theme-light" + class="_inline-field_ssths_40 mx_ThemeChoicePanel_themeSelector mx_ThemeChoicePanel_themeSelector_enabled cpd-theme-light" > <div - class="_inline-field-control_dgy0u_52" + class="_inline-field-control_ssths_52" > <div class="_container_1vw5h_18" @@ -635,10 +635,10 @@ exports[`<ThemeChoicePanel /> renders the theme choice UI 1`] = ` </div> </div> <div - class="_inline-field-body_dgy0u_46" + class="_inline-field-body_ssths_46" > <label - class="_label_dgy0u_67 mx_ThemeChoicePanel_themeSelector_Label" + class="_label_ssths_67 mx_ThemeChoicePanel_themeSelector_Label" for="radix-:r1:" > Light @@ -646,10 +646,10 @@ exports[`<ThemeChoicePanel /> renders the theme choice UI 1`] = ` </div> </div> <div - class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector cpd-theme-dark" + class="_inline-field_ssths_40 mx_ThemeChoicePanel_themeSelector cpd-theme-dark" > <div - class="_inline-field-control_dgy0u_52" + class="_inline-field-control_ssths_52" > <div class="_container_1vw5h_18" @@ -668,10 +668,10 @@ exports[`<ThemeChoicePanel /> renders the theme choice UI 1`] = ` </div> </div> <div - class="_inline-field-body_dgy0u_46" + class="_inline-field-body_ssths_46" > <label - class="_label_dgy0u_67 mx_ThemeChoicePanel_themeSelector_Label" + class="_label_ssths_67 mx_ThemeChoicePanel_themeSelector_Label" for="radix-:r2:" > Dark @@ -679,10 +679,10 @@ exports[`<ThemeChoicePanel /> renders the theme choice UI 1`] = ` </div> </div> <div - class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector cpd-theme-light" + class="_inline-field_ssths_40 mx_ThemeChoicePanel_themeSelector cpd-theme-light" > <div - class="_inline-field-control_dgy0u_52" + class="_inline-field-control_ssths_52" > <div class="_container_1vw5h_18" @@ -701,10 +701,10 @@ exports[`<ThemeChoicePanel /> renders the theme choice UI 1`] = ` </div> </div> <div - class="_inline-field-body_dgy0u_46" + class="_inline-field-body_ssths_46" > <label - class="_label_dgy0u_67 mx_ThemeChoicePanel_themeSelector_Label" + class="_label_ssths_67 mx_ThemeChoicePanel_themeSelector_Label" for="radix-:r3:" > High contrast diff --git a/test/unit-tests/components/views/settings/devices/LoginWithQRSection-test.tsx b/test/unit-tests/components/views/settings/devices/LoginWithQRSection-test.tsx index dd6caef1ce..edaf7b39d1 100644 --- a/test/unit-tests/components/views/settings/devices/LoginWithQRSection-test.tsx +++ b/test/unit-tests/components/views/settings/devices/LoginWithQRSection-test.tsx @@ -56,7 +56,6 @@ describe("<LoginWithQRSection />", () => { const defaultProps = { onShowQr: () => {}, versions: makeVersions({ "org.matrix.msc4108": true }), - wellKnown: {}, }; const getComponent = (props = {}) => <LoginWithQRSection {...defaultProps} {...props} />; diff --git a/test/unit-tests/components/views/settings/notifications/Notifications2-test.tsx b/test/unit-tests/components/views/settings/notifications/Notifications2-test.tsx index 029777c153..2507aa173f 100644 --- a/test/unit-tests/components/views/settings/notifications/Notifications2-test.tsx +++ b/test/unit-tests/components/views/settings/notifications/Notifications2-test.tsx @@ -716,7 +716,7 @@ describe("<Notifications />", () => { user: "@alice:example.org", ts: 1, }); - room.addLiveEvents([message]); + room.addLiveEvents([message], { addToState: true }); room.setUnreadNotificationCount(NotificationCountType.Total, 1); const user = userEvent.setup(); diff --git a/test/unit-tests/components/views/settings/tabs/room/RolesRoomSettingsTab-test.tsx b/test/unit-tests/components/views/settings/tabs/room/RolesRoomSettingsTab-test.tsx index 000c38c771..e1a451c9d5 100644 --- a/test/unit-tests/components/views/settings/tabs/room/RolesRoomSettingsTab-test.tsx +++ b/test/unit-tests/components/views/settings/tabs/room/RolesRoomSettingsTab-test.tsx @@ -17,7 +17,6 @@ import userEvent from "@testing-library/user-event"; import RolesRoomSettingsTab from "../../../../../../../src/components/views/settings/tabs/room/RolesRoomSettingsTab"; import { mkStubRoom, withClientContextRenderOptions, stubClient } from "../../../../../../test-utils"; import { MatrixClientPeg } from "../../../../../../../src/MatrixClientPeg"; -import { VoiceBroadcastInfoEventType } from "../../../../../../../src/voice-broadcast"; import SettingsStore from "../../../../../../../src/settings/SettingsStore"; import { ElementCall } from "../../../../../../../src/models/Call"; @@ -34,14 +33,6 @@ describe("RolesRoomSettingsTab", () => { return renderResult; }; - const getVoiceBroadcastsSelect = async (): Promise<Element> => { - return (await renderTab()).container.querySelector("select[label='Voice broadcasts']")!; - }; - - const getVoiceBroadcastsSelectedOption = async (): Promise<Element> => { - return (await renderTab()).container.querySelector("select[label='Voice broadcasts'] option:checked")!; - }; - beforeEach(() => { stubClient(); cli = MatrixClientPeg.safeGet(); @@ -76,26 +67,6 @@ describe("RolesRoomSettingsTab", () => { expect(container.querySelector(`[placeholder="@admin:server"]`)).toBeDisabled(); }); - it("should initially show »Moderator« permission for »Voice broadcasts«", async () => { - expect((await getVoiceBroadcastsSelectedOption()).textContent).toBe("Moderator"); - }); - - describe("when setting »Default« permission for »Voice broadcasts«", () => { - beforeEach(async () => { - fireEvent.change(await getVoiceBroadcastsSelect(), { - target: { value: 0 }, - }); - }); - - it("should update the power levels", () => { - expect(cli.sendStateEvent).toHaveBeenCalledWith(roomId, EventType.RoomPowerLevels, { - events: { - [VoiceBroadcastInfoEventType]: 0, - }, - }); - }); - }); - describe("Element Call", () => { const setGroupCallsEnabled = (val: boolean): void => { jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => { diff --git a/test/unit-tests/components/views/settings/tabs/room/SecurityRoomSettingsTab-test.tsx b/test/unit-tests/components/views/settings/tabs/room/SecurityRoomSettingsTab-test.tsx index 6534a34004..5fc10714e6 100644 --- a/test/unit-tests/components/views/settings/tabs/room/SecurityRoomSettingsTab-test.tsx +++ b/test/unit-tests/components/views/settings/tabs/room/SecurityRoomSettingsTab-test.tsx @@ -75,7 +75,7 @@ describe("<SecurityRoomSettingsTab />", () => { beforeEach(async () => { client.sendStateEvent.mockReset().mockResolvedValue({ event_id: "test" }); - client.isRoomEncrypted.mockReturnValue(false); + jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(false); client.getClientWellKnown.mockReturnValue(undefined); jest.spyOn(SettingsStore, "getValue").mockRestore(); @@ -313,7 +313,7 @@ describe("<SecurityRoomSettingsTab />", () => { setRoomStateEvents(room); getComponent(room); - expect(screen.getByLabelText("Encrypted")).not.toBeChecked(); + await waitFor(() => expect(screen.getByLabelText("Encrypted")).not.toBeChecked()); fireEvent.click(screen.getByLabelText("Encrypted")); @@ -330,7 +330,7 @@ describe("<SecurityRoomSettingsTab />", () => { setRoomStateEvents(room); getComponent(room); - expect(screen.getByLabelText("Encrypted")).not.toBeChecked(); + await waitFor(() => expect(screen.getByLabelText("Encrypted")).not.toBeChecked()); fireEvent.click(screen.getByLabelText("Encrypted")); @@ -416,12 +416,12 @@ describe("<SecurityRoomSettingsTab />", () => { expect(screen.getByText("Once enabled, encryption cannot be disabled.")).toBeInTheDocument(); }); - it("displays unencrypted rooms with toggle disabled", () => { + it("displays unencrypted rooms with toggle disabled", async () => { const room = new Room(roomId, client, userId); setRoomStateEvents(room); getComponent(room); - expect(screen.getByLabelText("Encrypted")).not.toBeChecked(); + await waitFor(() => expect(screen.getByLabelText("Encrypted")).not.toBeChecked()); expect(screen.getByLabelText("Encrypted").getAttribute("aria-disabled")).toEqual("true"); expect(screen.queryByText("Once enabled, encryption cannot be disabled.")).not.toBeInTheDocument(); expect(screen.getByText("Your server requires encryption to be disabled.")).toBeInTheDocument(); diff --git a/test/unit-tests/components/views/settings/tabs/user/SecurityUserSettingsTab-test.tsx b/test/unit-tests/components/views/settings/tabs/user/SecurityUserSettingsTab-test.tsx index 0ee882767b..27403919b6 100644 --- a/test/unit-tests/components/views/settings/tabs/user/SecurityUserSettingsTab-test.tsx +++ b/test/unit-tests/components/views/settings/tabs/user/SecurityUserSettingsTab-test.tsx @@ -34,7 +34,6 @@ describe("<SecurityUserSettingsTab />", () => { ...mockClientMethodsCrypto(), getRooms: jest.fn().mockReturnValue([]), getIgnoredUsers: jest.fn(), - getKeyBackupVersion: jest.fn(), }); const sdkContext = new SdkContextClass(); diff --git a/test/unit-tests/components/views/settings/tabs/user/__snapshots__/AppearanceUserSettingsTab-test.tsx.snap b/test/unit-tests/components/views/settings/tabs/user/__snapshots__/AppearanceUserSettingsTab-test.tsx.snap index 00c97672ab..0f0e193088 100644 --- a/test/unit-tests/components/views/settings/tabs/user/__snapshots__/AppearanceUserSettingsTab-test.tsx.snap +++ b/test/unit-tests/components/views/settings/tabs/user/__snapshots__/AppearanceUserSettingsTab-test.tsx.snap @@ -23,7 +23,7 @@ exports[`AppearanceUserSettingsTab should render 1`] = ` class="mx_SettingsSubsectionHeading" > <h3 - class="mx_Heading_h3 mx_SettingsSubsectionHeading_heading" + class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" > Theme </h3> @@ -32,13 +32,13 @@ exports[`AppearanceUserSettingsTab should render 1`] = ` class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi" > <form - class="_root_dgy0u_24 mx_ThemeChoicePanel_ThemeSelectors" + class="_root_ssths_24 mx_ThemeChoicePanel_ThemeSelectors" > <div - class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector mx_ThemeChoicePanel_themeSelector_disabled cpd-theme-light" + class="_inline-field_ssths_40 mx_ThemeChoicePanel_themeSelector mx_ThemeChoicePanel_themeSelector_disabled cpd-theme-light" > <div - class="_inline-field-control_dgy0u_52" + class="_inline-field-control_ssths_52" > <div class="_container_1vw5h_18" @@ -58,10 +58,10 @@ exports[`AppearanceUserSettingsTab should render 1`] = ` </div> </div> <div - class="_inline-field-body_dgy0u_46" + class="_inline-field-body_ssths_46" > <label - class="_label_dgy0u_67 mx_ThemeChoicePanel_themeSelector_Label" + class="_label_ssths_67 mx_ThemeChoicePanel_themeSelector_Label" for="radix-:r0:" > Light @@ -69,10 +69,10 @@ exports[`AppearanceUserSettingsTab should render 1`] = ` </div> </div> <div - class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector mx_ThemeChoicePanel_themeSelector_disabled cpd-theme-dark" + class="_inline-field_ssths_40 mx_ThemeChoicePanel_themeSelector mx_ThemeChoicePanel_themeSelector_disabled cpd-theme-dark" > <div - class="_inline-field-control_dgy0u_52" + class="_inline-field-control_ssths_52" > <div class="_container_1vw5h_18" @@ -92,10 +92,10 @@ exports[`AppearanceUserSettingsTab should render 1`] = ` </div> </div> <div - class="_inline-field-body_dgy0u_46" + class="_inline-field-body_ssths_46" > <label - class="_label_dgy0u_67 mx_ThemeChoicePanel_themeSelector_Label" + class="_label_ssths_67 mx_ThemeChoicePanel_themeSelector_Label" for="radix-:r1:" > Dark @@ -103,10 +103,10 @@ exports[`AppearanceUserSettingsTab should render 1`] = ` </div> </div> <div - class="_inline-field_dgy0u_40 mx_ThemeChoicePanel_themeSelector mx_ThemeChoicePanel_themeSelector_disabled cpd-theme-light" + class="_inline-field_ssths_40 mx_ThemeChoicePanel_themeSelector mx_ThemeChoicePanel_themeSelector_disabled cpd-theme-light" > <div - class="_inline-field-control_dgy0u_52" + class="_inline-field-control_ssths_52" > <div class="_container_1vw5h_18" @@ -126,10 +126,10 @@ exports[`AppearanceUserSettingsTab should render 1`] = ` </div> </div> <div - class="_inline-field-body_dgy0u_46" + class="_inline-field-body_ssths_46" > <label - class="_label_dgy0u_67 mx_ThemeChoicePanel_themeSelector_Label" + class="_label_ssths_67 mx_ThemeChoicePanel_themeSelector_Label" for="radix-:r2:" > High contrast @@ -153,7 +153,7 @@ exports[`AppearanceUserSettingsTab should render 1`] = ` class="mx_SettingsSubsectionHeading" > <h3 - class="mx_Heading_h3 mx_SettingsSubsectionHeading_heading" + class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading" > Message layout </h3> @@ -162,14 +162,14 @@ exports[`AppearanceUserSettingsTab should render 1`] = ` class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi" > <form - class="_root_dgy0u_24 mx_LayoutSwitcher_LayoutSelector" + class="_root_ssths_24 mx_LayoutSwitcher_LayoutSelector" > <div - class="_field_dgy0u_34 mxLayoutSwitcher_LayoutSelector_LayoutRadio" + class="_field_ssths_34 mxLayoutSwitcher_LayoutSelector_LayoutRadio" > <label aria-label="Modern" - class="_label_dgy0u_67" + class="_label_ssths_67" for="radix-:r3:" > <div @@ -292,11 +292,11 @@ exports[`AppearanceUserSettingsTab should render 1`] = ` </label> </div> <div - class="_field_dgy0u_34 mxLayoutSwitcher_LayoutSelector_LayoutRadio" + class="_field_ssths_34 mxLayoutSwitcher_LayoutSelector_LayoutRadio" > <label aria-label="Message bubbles" - class="_label_dgy0u_67" + class="_label_ssths_67" for="radix-:rc:" > <div @@ -418,11 +418,11 @@ exports[`AppearanceUserSettingsTab should render 1`] = ` </label> </div> <div - class="_field_dgy0u_34 mxLayoutSwitcher_LayoutSelector_LayoutRadio" + class="_field_ssths_34 mxLayoutSwitcher_LayoutSelector_LayoutRadio" > <label aria-label="IRC (experimental)" - class="_label_dgy0u_67" + class="_label_ssths_67" for="radix-:rl:" > <div @@ -545,13 +545,13 @@ exports[`AppearanceUserSettingsTab should render 1`] = ` </div> </form> <form - class="_root_dgy0u_24" + class="_root_ssths_24" > <div - class="_inline-field_dgy0u_40" + class="_inline-field_ssths_40" > <div - class="_inline-field-control_dgy0u_52" + class="_inline-field-control_ssths_52" > <div class="_container_qnvru_18" @@ -570,16 +570,16 @@ exports[`AppearanceUserSettingsTab should render 1`] = ` </div> </div> <div - class="_inline-field-body_dgy0u_46" + class="_inline-field-body_ssths_46" > <label - class="_label_dgy0u_67" + class="_label_ssths_67" for="radix-:ru:" > Show compact text and messages </label> <span - class="_message_dgy0u_98 _help-message_dgy0u_104" + class="_message_ssths_93 _help-message_ssths_99" id="radix-:rv:" > Modern layout must be selected to use this feature. diff --git a/test/unit-tests/components/views/settings/tabs/user/__snapshots__/MjolnirUserSettingsTab-test.tsx.snap b/test/unit-tests/components/views/settings/tabs/user/__snapshots__/MjolnirUserSettingsTab-test.tsx.snap index 4bdaf3275c..17986bd3bc 100644 --- a/test/unit-tests/components/views/settings/tabs/user/__snapshots__/MjolnirUserSettingsTab-test.tsx.snap +++ b/test/unit-tests/components/views/settings/tabs/user/__snapshots__/MjolnirUserSettingsTab-test.tsx.snap @@ -85,7 +85,6 @@ exports[`<MjolnirUserSettingsTab /> renders correctly when user has no ignored u class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary" role="button" tabindex="0" - type="submit" > Ignore </div> @@ -150,7 +149,6 @@ exports[`<MjolnirUserSettingsTab /> renders correctly when user has no ignored u class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary" role="button" tabindex="0" - type="submit" > Subscribe </div> diff --git a/test/unit-tests/components/views/spaces/__snapshots__/SpacePanel-test.tsx.snap b/test/unit-tests/components/views/spaces/__snapshots__/SpacePanel-test.tsx.snap index bfb888af28..6aee8d43c4 100644 --- a/test/unit-tests/components/views/spaces/__snapshots__/SpacePanel-test.tsx.snap +++ b/test/unit-tests/components/views/spaces/__snapshots__/SpacePanel-test.tsx.snap @@ -8,6 +8,7 @@ exports[`<SpacePanel /> should show all activated MetaSpaces in the correct orde > <div class="mx_UserMenu" + data-floating-ui-inert="" > <div aria-expanded="false" @@ -42,6 +43,7 @@ exports[`<SpacePanel /> should show all activated MetaSpaces in the correct orde <ul aria-label="Spaces" class="mx_AutoHideScrollbar mx_SpaceTreeLevel" + data-floating-ui-inert="" data-rbd-droppable-context-id="0" data-rbd-droppable-id="top-level-spaces" role="tree" @@ -236,6 +238,7 @@ exports[`<SpacePanel /> should show all activated MetaSpaces in the correct orde aria-label="Threads" aria-labelledby=":r14:" class="_icon-button_bh2qc_17 mx_ThreadsActivityCentreButton" + data-floating-ui-inert="" role="button" style="--cpd-icon-button-size: 32px;" tabindex="0" @@ -260,6 +263,7 @@ exports[`<SpacePanel /> should show all activated MetaSpaces in the correct orde </button> <span aria-hidden="true" + data-floating-ui-inert="" style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;" tabindex="-1" /> @@ -272,6 +276,7 @@ exports[`<SpacePanel /> should show all activated MetaSpaces in the correct orde /> <span aria-owns=":r19:" + data-floating-ui-inert="" style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;" /> <span @@ -286,6 +291,7 @@ exports[`<SpacePanel /> should show all activated MetaSpaces in the correct orde aria-expanded="false" aria-label="Quick settings" class="mx_AccessibleButton mx_QuickSettingsButton" + data-floating-ui-inert="" role="button" tabindex="0" /> diff --git a/test/unit-tests/components/views/spaces/__snapshots__/ThreadsActivityCentre-test.tsx.snap b/test/unit-tests/components/views/spaces/__snapshots__/ThreadsActivityCentre-test.tsx.snap index 05e7eabf99..b383932993 100644 --- a/test/unit-tests/components/views/spaces/__snapshots__/ThreadsActivityCentre-test.tsx.snap +++ b/test/unit-tests/components/views/spaces/__snapshots__/ThreadsActivityCentre-test.tsx.snap @@ -477,9 +477,7 @@ exports[`ThreadsActivityCentre should order the room with the same notification exports[`ThreadsActivityCentre should render the release announcement 1`] = ` <body> - <div - data-floating-ui-inert="" - > + <div> <div class="mx_ThreadsActivityCentre_container" > @@ -491,6 +489,7 @@ exports[`ThreadsActivityCentre should render the release announcement 1`] = ` aria-label="Threads" aria-labelledby=":rc:" class="_icon-button_bh2qc_17 mx_ThreadsActivityCentreButton" + data-floating-ui-inert="" role="button" style="--cpd-icon-button-size: 32px;" tabindex="0" @@ -515,6 +514,7 @@ exports[`ThreadsActivityCentre should render the release announcement 1`] = ` </button> <span aria-hidden="true" + data-floating-ui-inert="" style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;" tabindex="-1" /> @@ -527,6 +527,7 @@ exports[`ThreadsActivityCentre should render the release announcement 1`] = ` /> <span aria-owns=":rh:" + data-floating-ui-inert="" style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;" /> <span @@ -585,7 +586,6 @@ exports[`ThreadsActivityCentre should render the release announcement 1`] = ` > <span data-floating-ui-focus-guard="" - data-floating-ui-inert="" data-type="inside" role="button" style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;" @@ -648,7 +648,6 @@ exports[`ThreadsActivityCentre should render the release announcement 1`] = ` </div> <span data-floating-ui-focus-guard="" - data-floating-ui-inert="" data-type="inside" role="button" style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;" diff --git a/test/unit-tests/contexts/SdkContext-test.ts b/test/unit-tests/contexts/SdkContext-test.ts index 21f356ed94..340fabdd3d 100644 --- a/test/unit-tests/contexts/SdkContext-test.ts +++ b/test/unit-tests/contexts/SdkContext-test.ts @@ -11,11 +11,8 @@ import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { SdkContextClass } from "../../../src/contexts/SDKContext"; import { OidcClientStore } from "../../../src/stores/oidc/OidcClientStore"; import { UserProfilesStore } from "../../../src/stores/UserProfilesStore"; -import { VoiceBroadcastPreRecordingStore } from "../../../src/voice-broadcast"; import { createTestClient } from "../../test-utils"; -jest.mock("../../../src/voice-broadcast/stores/VoiceBroadcastPreRecordingStore"); - describe("SdkContextClass", () => { let sdkContext = SdkContextClass.instance; let client: MatrixClient; @@ -33,12 +30,6 @@ describe("SdkContextClass", () => { expect(SdkContextClass.instance).toBe(globalInstance); }); - it("voiceBroadcastPreRecordingStore should always return the same VoiceBroadcastPreRecordingStore", () => { - const first = sdkContext.voiceBroadcastPreRecordingStore; - expect(first).toBeInstanceOf(VoiceBroadcastPreRecordingStore); - expect(sdkContext.voiceBroadcastPreRecordingStore).toBe(first); - }); - it("userProfilesStore should raise an error without a client", () => { expect(() => sdkContext.userProfilesStore).toThrow("Unable to create UserProfilesStore without a client"); }); diff --git a/test/unit-tests/events/EventTileFactory-test.ts b/test/unit-tests/events/EventTileFactory-test.ts index 9c8f7718af..8a7d09c434 100644 --- a/test/unit-tests/events/EventTileFactory-test.ts +++ b/test/unit-tests/events/EventTileFactory-test.ts @@ -7,19 +7,16 @@ Please see LICENSE files in the repository root for full details. */ import { mocked } from "jest-mock"; -import { EventType, MatrixClient, MatrixEvent, MsgType, RelationType, Room } from "matrix-js-sdk/src/matrix"; +import { EventType, MatrixClient, MatrixEvent, MsgType, Room } from "matrix-js-sdk/src/matrix"; import { JSONEventFactory, MessageEventFactory, pickFactory, RoomCreateEventFactory, - TextualEventFactory, } from "../../../src/events/EventTileFactory"; import SettingsStore from "../../../src/settings/SettingsStore"; -import { VoiceBroadcastChunkEventType, VoiceBroadcastInfoState } from "../../../src/voice-broadcast"; import { createTestClient, mkEvent } from "../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "../voice-broadcast/utils/test-utils"; const roomId = "!room:example.com"; @@ -31,11 +28,7 @@ describe("pickFactory", () => { let createEventWithoutPredecessor: MatrixEvent; let dynamicPredecessorEvent: MatrixEvent; - let voiceBroadcastStartedEvent: MatrixEvent; - let voiceBroadcastStoppedEvent: MatrixEvent; - let voiceBroadcastChunkEvent: MatrixEvent; let utdEvent: MatrixEvent; - let utdBroadcastChunkEvent: MatrixEvent; let audioMessageEvent: MatrixEvent; beforeAll(() => { @@ -82,29 +75,6 @@ describe("pickFactory", () => { last_known_event_id: null, }, }); - voiceBroadcastStartedEvent = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Started, - client.getUserId()!, - client.deviceId!, - ); - room.addLiveEvents([voiceBroadcastStartedEvent]); - voiceBroadcastStoppedEvent = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Stopped, - client.getUserId()!, - client.deviceId!, - ); - voiceBroadcastChunkEvent = mkEvent({ - event: true, - type: EventType.RoomMessage, - user: client.getUserId()!, - room: roomId, - content: { - msgtype: MsgType.Audio, - [VoiceBroadcastChunkEventType]: {}, - }, - }); audioMessageEvent = mkEvent({ event: true, type: EventType.RoomMessage, @@ -123,20 +93,6 @@ describe("pickFactory", () => { msgtype: "m.bad.encrypted", }, }); - utdBroadcastChunkEvent = mkEvent({ - event: true, - type: EventType.RoomMessage, - user: client.getUserId()!, - room: roomId, - content: { - "msgtype": "m.bad.encrypted", - "m.relates_to": { - rel_type: RelationType.Reference, - event_id: voiceBroadcastStartedEvent.getId(), - }, - }, - }); - jest.spyOn(utdBroadcastChunkEvent, "isDecryptionFailure").mockReturnValue(true); }); it("should return JSONEventFactory for a no-op m.room.power_levels event", () => { @@ -151,10 +107,6 @@ describe("pickFactory", () => { }); describe("when showing hidden events", () => { - it("should return a JSONEventFactory for a voice broadcast event", () => { - expect(pickFactory(voiceBroadcastChunkEvent, client, true)).toBe(JSONEventFactory); - }); - it("should return a JSONEventFactory for a room create event without predecessor", () => { room.currentState.events.set( EventType.RoomCreate, @@ -164,17 +116,9 @@ describe("pickFactory", () => { expect(pickFactory(createEventWithoutPredecessor, client, true)).toBe(JSONEventFactory); }); - it("should return a TextualEventFactory for a voice broadcast stopped event", () => { - expect(pickFactory(voiceBroadcastStoppedEvent, client, true)).toBe(TextualEventFactory); - }); - it("should return a MessageEventFactory for an audio message event", () => { expect(pickFactory(audioMessageEvent, client, true)).toBe(MessageEventFactory); }); - - it("should return a MessageEventFactory for a UTD broadcast chunk event", () => { - expect(pickFactory(utdBroadcastChunkEvent, client, true)).toBe(MessageEventFactory); - }); }); describe("when not showing hidden events", () => { @@ -252,14 +196,6 @@ describe("pickFactory", () => { }); }); - it("should return undefined for a voice broadcast event", () => { - expect(pickFactory(voiceBroadcastChunkEvent, client, false)).toBeUndefined(); - }); - - it("should return a TextualEventFactory for a voice broadcast stopped event", () => { - expect(pickFactory(voiceBroadcastStoppedEvent, client, false)).toBe(TextualEventFactory); - }); - it("should return a MessageEventFactory for an audio message event", () => { expect(pickFactory(audioMessageEvent, client, false)).toBe(MessageEventFactory); }); @@ -267,9 +203,5 @@ describe("pickFactory", () => { it("should return a MessageEventFactory for a UTD event", () => { expect(pickFactory(utdEvent, client, false)).toBe(MessageEventFactory); }); - - it("should return undefined for a UTD broadcast chunk event", () => { - expect(pickFactory(utdBroadcastChunkEvent, client, false)).toBeUndefined(); - }); }); }); diff --git a/test/unit-tests/linkify-matrix-test.ts b/test/unit-tests/linkify-matrix-test.ts index 97548c62d4..50edff2535 100644 --- a/test/unit-tests/linkify-matrix-test.ts +++ b/test/unit-tests/linkify-matrix-test.ts @@ -332,7 +332,7 @@ describe("linkify-matrix", () => { const event = new MouseEvent("mousedown"); event.preventDefault = jest.fn(); - handlers.click(event); + handlers!.click(event); expect(event.preventDefault).toHaveBeenCalled(); expect(dispatchSpy).toHaveBeenCalledWith( expect.objectContaining({ @@ -372,7 +372,7 @@ describe("linkify-matrix", () => { const event = new MouseEvent("mousedown"); event.preventDefault = jest.fn(); - handlers.click(event); + handlers!.click(event); expect(event.preventDefault).toHaveBeenCalled(); expect(dispatchSpy).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/test/unit-tests/models/Call-test.ts b/test/unit-tests/models/Call-test.ts index 40e929fb4a..316adbbea9 100644 --- a/test/unit-tests/models/Call-test.ts +++ b/test/unit-tests/models/Call-test.ts @@ -119,7 +119,7 @@ const setUpClientRoomAndStores = (): { skey: stateKey, content: content as IContent, }); - room.addLiveEvents([event]); + room.addLiveEvents([event], { addToState: true }); return { event_id: event.getId()! }; }); diff --git a/test/unit-tests/stores/InitialCryptoSetupStore-test.ts b/test/unit-tests/stores/InitialCryptoSetupStore-test.ts new file mode 100644 index 0000000000..64b81bade2 --- /dev/null +++ b/test/unit-tests/stores/InitialCryptoSetupStore-test.ts @@ -0,0 +1,85 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import { mocked } from "jest-mock"; +import { MatrixClient } from "matrix-js-sdk/src/matrix"; +import { waitFor } from "jest-matrix-react"; + +import { createCrossSigning } from "../../../src/CreateCrossSigning"; +import { InitialCryptoSetupStore } from "../../../src/stores/InitialCryptoSetupStore"; +import { SdkContextClass } from "../../../src/contexts/SDKContext"; +import { createTestClient } from "../../test-utils"; +import { AccountPasswordStore } from "../../../src/stores/AccountPasswordStore"; + +jest.mock("../../../src/CreateCrossSigning", () => ({ + createCrossSigning: jest.fn(), +})); + +describe("InitialCryptoSetupStore", () => { + let testStore: InitialCryptoSetupStore; + let client: MatrixClient; + let stores: SdkContextClass; + + let createCrossSigningResolve: () => void; + let createCrossSigningReject: (e: Error) => void; + + beforeEach(() => { + testStore = new InitialCryptoSetupStore(); + client = createTestClient(); + stores = { + accountPasswordStore: { + getPassword: jest.fn(), + } as unknown as AccountPasswordStore, + } as unknown as SdkContextClass; + + mocked(createCrossSigning).mockImplementation(() => { + return new Promise<void>((resolve, reject) => { + createCrossSigningResolve = resolve; + createCrossSigningReject = reject; + }); + }); + }); + + it("should call createCrossSigning when startInitialCryptoSetup is called", async () => { + testStore.startInitialCryptoSetup(client, false, stores, jest.fn()); + + await waitFor(() => expect(createCrossSigning).toHaveBeenCalled()); + }); + + it("emits an update event when createCrossSigning resolves", async () => { + const updateSpy = jest.fn(); + testStore.on("update", updateSpy); + + testStore.startInitialCryptoSetup(client, false, stores, jest.fn()); + createCrossSigningResolve(); + + await waitFor(() => expect(updateSpy).toHaveBeenCalled()); + expect(testStore.getStatus()).toBe("complete"); + }); + + it("emits an update event when createCrossSigning rejects", async () => { + const updateSpy = jest.fn(); + testStore.on("update", updateSpy); + + testStore.startInitialCryptoSetup(client, false, stores, jest.fn()); + createCrossSigningReject(new Error("Test error")); + + await waitFor(() => expect(updateSpy).toHaveBeenCalled()); + expect(testStore.getStatus()).toBe("error"); + }); + + it("should ignore failures if tokenLogin is true", async () => { + const updateSpy = jest.fn(); + testStore.on("update", updateSpy); + + testStore.startInitialCryptoSetup(client, true, stores, jest.fn()); + createCrossSigningReject(new Error("Test error")); + + await waitFor(() => expect(updateSpy).toHaveBeenCalled()); + expect(testStore.getStatus()).toBe("complete"); + }); +}); diff --git a/test/unit-tests/stores/MemberListStore-test.ts b/test/unit-tests/stores/MemberListStore-test.ts index 815dea8758..470087031a 100644 --- a/test/unit-tests/stores/MemberListStore-test.ts +++ b/test/unit-tests/stores/MemberListStore-test.ts @@ -201,6 +201,7 @@ describe("MemberListStore", () => { function addEventToRoom(room: Room, ev: MatrixEvent) { room.getLiveTimeline().addEvent(ev, { toStartOfTimeline: false, + addToState: true, }); } diff --git a/test/unit-tests/stores/RoomViewStore-test.ts b/test/unit-tests/stores/RoomViewStore-test.ts index c9b80553e5..782bb79738 100644 --- a/test/unit-tests/stores/RoomViewStore-test.ts +++ b/test/unit-tests/stores/RoomViewStore-test.ts @@ -24,13 +24,6 @@ import { ActiveRoomChangedPayload } from "../../../src/dispatcher/payloads/Activ import { SpaceStoreClass } from "../../../src/stores/spaces/SpaceStore"; import { TestSdkContext } from "../TestSdkContext"; import { ViewRoomPayload } from "../../../src/dispatcher/payloads/ViewRoomPayload"; -import { - VoiceBroadcastInfoState, - VoiceBroadcastPlayback, - VoiceBroadcastPlaybacksStore, - VoiceBroadcastRecording, -} from "../../../src/voice-broadcast"; -import { mkVoiceBroadcastInfoStateEvent } from "../voice-broadcast/utils/test-utils"; import Modal from "../../../src/Modal"; import ErrorDialog from "../../../src/components/views/dialogs/ErrorDialog"; import { CancelAskToJoinPayload } from "../../../src/dispatcher/payloads/CancelAskToJoinPayload"; @@ -160,7 +153,6 @@ describe("RoomViewStore", function () { stores._SlidingSyncManager = slidingSyncManager; stores._PosthogAnalytics = new MockPosthogAnalytics(); stores._SpaceStore = new MockSpaceStore(); - stores._VoiceBroadcastPlaybacksStore = new VoiceBroadcastPlaybacksStore(stores.voiceBroadcastRecordingsStore); roomViewStore = new RoomViewStore(dis, stores); stores._RoomViewStore = roomViewStore; }); @@ -343,88 +335,6 @@ describe("RoomViewStore", function () { }); }); - describe("when listening to a voice broadcast", () => { - let voiceBroadcastPlayback: VoiceBroadcastPlayback; - - beforeEach(() => { - voiceBroadcastPlayback = new VoiceBroadcastPlayback( - mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Started, - mockClient.getSafeUserId(), - "d42", - ), - mockClient, - stores.voiceBroadcastRecordingsStore, - ); - stores.voiceBroadcastPlaybacksStore.setCurrent(voiceBroadcastPlayback); - jest.spyOn(voiceBroadcastPlayback, "pause").mockImplementation(); - }); - - it("and viewing a call it should pause the current broadcast", async () => { - await viewCall(); - expect(voiceBroadcastPlayback.pause).toHaveBeenCalled(); - expect(roomViewStore.isViewingCall()).toBe(true); - }); - }); - - describe("when recording a voice broadcast", () => { - beforeEach(() => { - stores.voiceBroadcastRecordingsStore.setCurrent( - new VoiceBroadcastRecording( - mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Started, - mockClient.getSafeUserId(), - "d42", - ), - mockClient, - ), - ); - }); - - it("and trying to view a call, it should not actually view it and show the info dialog", async () => { - await viewCall(); - expect(Modal.createDialog).toMatchSnapshot(); - expect(roomViewStore.isViewingCall()).toBe(false); - }); - - describe("and viewing a room with a broadcast", () => { - beforeEach(async () => { - const broadcastEvent = mkVoiceBroadcastInfoStateEvent( - roomId2, - VoiceBroadcastInfoState.Started, - mockClient.getSafeUserId(), - "ABC123", - ); - room2.addLiveEvents([broadcastEvent]); - - stores.voiceBroadcastPlaybacksStore.getByInfoEvent(broadcastEvent, mockClient); - dis.dispatch({ action: Action.ViewRoom, room_id: roomId2 }); - await untilDispatch(Action.ActiveRoomChanged, dis); - }); - - it("should continue recording", () => { - expect(stores.voiceBroadcastPlaybacksStore.getCurrent()).toBeNull(); - expect(stores.voiceBroadcastRecordingsStore.getCurrent()?.getState()).toBe( - VoiceBroadcastInfoState.Started, - ); - }); - - describe("and stopping the recording", () => { - beforeEach(async () => { - await stores.voiceBroadcastRecordingsStore.getCurrent()?.stop(); - // check test precondition - expect(stores.voiceBroadcastRecordingsStore.getCurrent()).toBeNull(); - }); - - it("should view the broadcast", () => { - expect(stores.voiceBroadcastPlaybacksStore.getCurrent()?.infoEvent.getRoomId()).toBe(roomId2); - }); - }); - }); - }); - describe("Sliding Sync", function () { beforeEach(() => { jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName, roomId, value) => { diff --git a/test/unit-tests/stores/SetupEncryptionStore-test.ts b/test/unit-tests/stores/SetupEncryptionStore-test.ts index b9ab29b94b..d3d0300a21 100644 --- a/test/unit-tests/stores/SetupEncryptionStore-test.ts +++ b/test/unit-tests/stores/SetupEncryptionStore-test.ts @@ -37,6 +37,7 @@ describe("SetupEncryptionStore", () => { getDeviceVerificationStatus: jest.fn(), isDehydrationSupported: jest.fn().mockResolvedValue(false), startDehydration: jest.fn(), + getKeyBackupInfo: jest.fn().mockResolvedValue(null), } as unknown as Mocked<CryptoApi>; client.getCrypto.mockReturnValue(mockCrypto); diff --git a/test/unit-tests/stores/__snapshots__/RoomViewStore-test.ts.snap b/test/unit-tests/stores/__snapshots__/RoomViewStore-test.ts.snap index a6b7953697..77ee50df5b 100644 --- a/test/unit-tests/stores/__snapshots__/RoomViewStore-test.ts.snap +++ b/test/unit-tests/stores/__snapshots__/RoomViewStore-test.ts.snap @@ -18,26 +18,3 @@ exports[`RoomViewStore should display the generic error message when the roomId "title": "Failed to join", } `; - -exports[`RoomViewStore when recording a voice broadcast and trying to view a call, it should not actually view it and show the info dialog 1`] = ` -[MockFunction] { - "calls": [ - [ - [Function], - { - "description": <p> - You can’t start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call. - </p>, - "hasCloseButton": true, - "title": "Can’t start a call", - }, - ], - ], - "results": [ - { - "type": "return", - "value": undefined, - }, - ], -} -`; diff --git a/test/unit-tests/stores/right-panel/RightPanelStore-test.ts b/test/unit-tests/stores/right-panel/RightPanelStore-test.ts index 92299426ba..27c4d730a9 100644 --- a/test/unit-tests/stores/right-panel/RightPanelStore-test.ts +++ b/test/unit-tests/stores/right-panel/RightPanelStore-test.ts @@ -18,6 +18,9 @@ import { ActiveRoomChangedPayload } from "../../../../src/dispatcher/payloads/Ac import RightPanelStore from "../../../../src/stores/right-panel/RightPanelStore"; import { RightPanelPhases } from "../../../../src/stores/right-panel/RightPanelStorePhases"; import SettingsStore from "../../../../src/settings/SettingsStore"; +import { pendingVerificationRequestForUser } from "../../../../src/verification.ts"; + +jest.mock("../../../../src/verification"); describe("RightPanelStore", () => { // Mock out the settings store so the right panel store can't persist values between tests @@ -97,7 +100,7 @@ describe("RightPanelStore", () => { it("does nothing if given an invalid state", async () => { await viewRoom("!1:example.org"); // Needs a member specified to be valid - store.setCard({ phase: RightPanelPhases.RoomMemberInfo }, true, "!1:example.org"); + store.setCard({ phase: RightPanelPhases.MemberInfo }, true, "!1:example.org"); expect(store.roomPhaseHistory).toEqual([]); }); it("only creates a single history entry if given the same card twice", async () => { @@ -114,15 +117,15 @@ describe("RightPanelStore", () => { it("overwrites history if changing the phase", async () => { await viewRoom("!1:example.org"); store.setCard({ phase: RightPanelPhases.RoomSummary }, true, "!1:example.org"); - store.setCard({ phase: RightPanelPhases.RoomMemberList }, true, "!1:example.org"); - expect(store.roomPhaseHistory).toEqual([{ phase: RightPanelPhases.RoomMemberList, state: {} }]); + store.setCard({ phase: RightPanelPhases.MemberList }, true, "!1:example.org"); + expect(store.roomPhaseHistory).toEqual([{ phase: RightPanelPhases.MemberList, state: {} }]); }); }); describe("setCards", () => { it("overwrites history", async () => { await viewRoom("!1:example.org"); - store.setCard({ phase: RightPanelPhases.RoomMemberList }, true, "!1:example.org"); + store.setCard({ phase: RightPanelPhases.MemberList }, true, "!1:example.org"); store.setCards( [{ phase: RightPanelPhases.RoomSummary }, { phase: RightPanelPhases.PinnedMessages }], true, @@ -200,21 +203,40 @@ describe("RightPanelStore", () => { store.setCards( [ { - phase: RightPanelPhases.RoomMemberList, + phase: RightPanelPhases.MemberList, }, { - phase: RightPanelPhases.RoomMemberInfo, + phase: RightPanelPhases.MemberInfo, state: { member: new RoomMember("!1:example.org", "@alice:example.org") }, }, ], true, "!1:example.org", ); - expect(store.currentCardForRoom("!1:example.org").phase).toEqual(RightPanelPhases.RoomMemberInfo); + expect(store.currentCardForRoom("!1:example.org").phase).toEqual(RightPanelPhases.MemberInfo); // Switch away and back await viewRoom("!2:example.org"); await viewRoom("!1:example.org"); - expect(store.currentCardForRoom("!1:example.org").phase).toEqual(RightPanelPhases.RoomMemberList); + expect(store.currentCardForRoom("!1:example.org").phase).toEqual(RightPanelPhases.MemberList); + }); + + it("should redirect to verification if set to phase MemberInfo for a user with a pending verification", async () => { + const member = new RoomMember("!1:example.org", "@alice:example.org"); + const verificationRequest = { mockVerificationRequest: true } as any; + mocked(pendingVerificationRequestForUser).mockReturnValue(verificationRequest); + await viewRoom("!1:example.org"); + store.setCard( + { + phase: RightPanelPhases.MemberInfo, + state: { member }, + }, + true, + "!1:example.org", + ); + expect(store.currentCard).toEqual({ + phase: RightPanelPhases.EncryptionPanel, + state: { member, verificationRequest }, + }); }); }); diff --git a/test/unit-tests/stores/right-panel/action-handlers/View3pidInvite-test.ts b/test/unit-tests/stores/right-panel/action-handlers/View3pidInvite-test.ts index d0b721243f..9503bb4ae3 100644 --- a/test/unit-tests/stores/right-panel/action-handlers/View3pidInvite-test.ts +++ b/test/unit-tests/stores/right-panel/action-handlers/View3pidInvite-test.ts @@ -30,7 +30,7 @@ describe("onView3pidInvite()", () => { }; onView3pidInvite(payload, rightPanelStore); - expect(rightPanelStore.showOrHidePhase).toHaveBeenCalledWith(RightPanelPhases.RoomMemberList); + expect(rightPanelStore.showOrHidePhase).toHaveBeenCalledWith(RightPanelPhases.MemberList); expect(rightPanelStore.pushCard).not.toHaveBeenCalled(); }); @@ -43,7 +43,7 @@ describe("onView3pidInvite()", () => { expect(rightPanelStore.showOrHidePhase).not.toHaveBeenCalled(); expect(rightPanelStore.pushCard).toHaveBeenCalledWith({ - phase: RightPanelPhases.Room3pidMemberInfo, + phase: RightPanelPhases.ThreePidMemberInfo, state: { memberInfoEvent: payload.event }, }); }); diff --git a/test/unit-tests/stores/room-list/MessagePreviewStore-test.ts b/test/unit-tests/stores/room-list/MessagePreviewStore-test.ts index 75013bd4e1..861b6dcd11 100644 --- a/test/unit-tests/stores/room-list/MessagePreviewStore-test.ts +++ b/test/unit-tests/stores/room-list/MessagePreviewStore-test.ts @@ -35,7 +35,7 @@ describe("MessagePreviewStore", () => { event: MatrixEvent, fireAction = true, ): Promise<void> { - room.addLiveEvents([event]); + room.addLiveEvents([event], { addToState: true }); if (fireAction) { // @ts-ignore private access await store.onAction({ diff --git a/test/unit-tests/stores/room-list/algorithms/RecentAlgorithm-test.ts b/test/unit-tests/stores/room-list/algorithms/RecentAlgorithm-test.ts index 18a073b632..e0e06bbaf7 100644 --- a/test/unit-tests/stores/room-list/algorithms/RecentAlgorithm-test.ts +++ b/test/unit-tests/stores/room-list/algorithms/RecentAlgorithm-test.ts @@ -47,11 +47,11 @@ describe("RecentAlgorithm", () => { room.getMyMembership = () => KnownMembership.Join; - room.addLiveEvents([event1]); + room.addLiveEvents([event1], { addToState: true }); expect(algorithm.getLastTs(room, "@jane:matrix.org")).toBe(5); expect(algorithm.getLastTs(room, "@john:matrix.org")).toBe(5); - room.addLiveEvents([event2]); + room.addLiveEvents([event2], { addToState: true }); expect(algorithm.getLastTs(room, "@jane:matrix.org")).toBe(10); expect(algorithm.getLastTs(room, "@john:matrix.org")).toBe(10); @@ -94,8 +94,8 @@ describe("RecentAlgorithm", () => { event: true, }); - room1.addLiveEvents([evt]); - room2.addLiveEvents([evt2]); + room1.addLiveEvents([evt], { addToState: true }); + room2.addLiveEvents([evt2], { addToState: true }); expect(algorithm.sortRooms([room2, room1], DefaultTagID.Untagged)).toEqual([room1, room2]); }); @@ -115,7 +115,7 @@ describe("RecentAlgorithm", () => { event: true, }); - room1.addLiveEvents([evt]); + room1.addLiveEvents([evt], { addToState: true }); expect(algorithm.sortRooms([room2, room1], DefaultTagID.Untagged)).toEqual([room2, room1]); @@ -127,7 +127,7 @@ describe("RecentAlgorithm", () => { ts: 12, }); - room1.addLiveEvents(events); + room1.addLiveEvents(events, { addToState: true }); }); it("orders rooms based on thread replies too", () => { @@ -145,7 +145,7 @@ describe("RecentAlgorithm", () => { ts: 12, length: 5, }); - room1.addLiveEvents(events1); + room1.addLiveEvents(events1, { addToState: true }); const { events: events2 } = mkThread({ room: room2, @@ -155,7 +155,7 @@ describe("RecentAlgorithm", () => { ts: 14, length: 10, }); - room2.addLiveEvents(events2); + room2.addLiveEvents(events2, { addToState: true }); expect(algorithm.sortRooms([room1, room2], DefaultTagID.Untagged)).toEqual([room2, room1]); @@ -169,7 +169,7 @@ describe("RecentAlgorithm", () => { // replies are 1ms after each other ts: 50, }); - room1.addLiveEvents([threadReply]); + room1.addLiveEvents([threadReply], { addToState: true }); expect(algorithm.sortRooms([room1, room2], DefaultTagID.Untagged)).toEqual([room1, room2]); }); diff --git a/test/unit-tests/stores/room-list/previews/MessageEventPreview-test.ts b/test/unit-tests/stores/room-list/previews/MessageEventPreview-test.ts index fd6626e6b6..ec97cdd20e 100644 --- a/test/unit-tests/stores/room-list/previews/MessageEventPreview-test.ts +++ b/test/unit-tests/stores/room-list/previews/MessageEventPreview-test.ts @@ -71,18 +71,5 @@ describe("MessageEventPreview", () => { }); expect(preview.getTextFor(event)).toBe(`${userId}: test new content body`); }); - - it("when called with a broadcast chunk event it should return null", () => { - const event = mkEvent({ - event: true, - content: { - body: "test body", - ["io.element.voice_broadcast_chunk"]: {}, - }, - user: userId, - type: "m.room.message", - }); - expect(preview.getTextFor(event)).toBeNull(); - }); }); }); diff --git a/test/unit-tests/stores/room-list/previews/ReactionEventPreview-test.ts b/test/unit-tests/stores/room-list/previews/ReactionEventPreview-test.ts index b3facccb06..7a7ce9e2b3 100644 --- a/test/unit-tests/stores/room-list/previews/ReactionEventPreview-test.ts +++ b/test/unit-tests/stores/room-list/previews/ReactionEventPreview-test.ts @@ -70,7 +70,7 @@ describe("ReactionEventPreview", () => { room: roomId, }); - room.getUnfilteredTimelineSet().addLiveEvent(message, {}); + room.getUnfilteredTimelineSet().addLiveEvent(message, { addToState: true }); const event = mkEvent({ event: true, @@ -107,7 +107,7 @@ describe("ReactionEventPreview", () => { room: roomId, }); - room.getUnfilteredTimelineSet().addLiveEvent(message, {}); + room.getUnfilteredTimelineSet().addLiveEvent(message, { addToState: true }); const event = mkEvent({ event: true, diff --git a/test/unit-tests/stores/room-list/previews/VoiceBroadcastPreview-test.ts b/test/unit-tests/stores/room-list/previews/VoiceBroadcastPreview-test.ts deleted file mode 100644 index a96f3c11bd..0000000000 --- a/test/unit-tests/stores/room-list/previews/VoiceBroadcastPreview-test.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { Room } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastPreview } from "../../../../../src/stores/room-list/previews/VoiceBroadcastPreview"; -import { VoiceBroadcastInfoState } from "../../../../../src/voice-broadcast"; -import { mkEvent, stubClient } from "../../../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils"; - -describe("VoiceBroadcastPreview.getTextFor", () => { - const roomId = "!room:example.com"; - const userId = "@user:example.com"; - const deviceId = "d42"; - let preview: VoiceBroadcastPreview; - - beforeAll(() => { - preview = new VoiceBroadcastPreview(); - }); - - it("when passing an event with empty content, it should return null", () => { - const event = mkEvent({ - event: true, - content: {}, - user: userId, - type: "m.room.message", - }); - expect(preview.getTextFor(event)).toBeNull(); - }); - - it("when passing a broadcast started event, it should return null", () => { - const event = mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Started, userId, deviceId); - expect(preview.getTextFor(event)).toBeNull(); - }); - - it("when passing a broadcast stopped event, it should return the expected text", () => { - const event = mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Stopped, userId, deviceId); - expect(preview.getTextFor(event)).toBe("@user:example.com ended a voice broadcast"); - }); - - it("when passing a redacted broadcast stopped event, it should return null", () => { - const event = mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Stopped, userId, deviceId); - event.makeRedacted( - mkEvent({ event: true, content: {}, user: userId, type: "m.room.redaction" }), - new Room(roomId, stubClient(), userId), - ); - expect(preview.getTextFor(event)).toBeNull(); - }); -}); diff --git a/test/unit-tests/stores/widgets/StopGapWidget-test.ts b/test/unit-tests/stores/widgets/StopGapWidget-test.ts index 397c289d22..1416711017 100644 --- a/test/unit-tests/stores/widgets/StopGapWidget-test.ts +++ b/test/unit-tests/stores/widgets/StopGapWidget-test.ts @@ -22,9 +22,6 @@ import { waitFor } from "jest-matrix-react"; import { stubClient, mkRoom, mkEvent } from "../../../test-utils"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import { StopGapWidget } from "../../../../src/stores/widgets/StopGapWidget"; -import { ElementWidgetActions } from "../../../../src/stores/widgets/ElementWidgetActions"; -import { VoiceBroadcastInfoEventType, VoiceBroadcastRecording } from "../../../../src/voice-broadcast"; -import { SdkContextClass } from "../../../../src/contexts/SDKContext"; import ActiveWidgetStore from "../../../../src/stores/ActiveWidgetStore"; import SettingsStore from "../../../../src/settings/SettingsStore"; @@ -225,41 +222,6 @@ describe("StopGapWidget", () => { expect(messaging.feedEvent).toHaveBeenLastCalledWith(event.getEffectiveEvent(), "!1:example.org"); }); }); - - describe("when there is a voice broadcast recording", () => { - let voiceBroadcastInfoEvent: MatrixEvent; - let voiceBroadcastRecording: VoiceBroadcastRecording; - - beforeEach(() => { - voiceBroadcastInfoEvent = mkEvent({ - event: true, - room: client.getRoom("x")?.roomId, - user: client.getUserId()!, - type: VoiceBroadcastInfoEventType, - content: {}, - }); - voiceBroadcastRecording = new VoiceBroadcastRecording(voiceBroadcastInfoEvent, client); - jest.spyOn(voiceBroadcastRecording, "pause"); - jest.spyOn(SdkContextClass.instance.voiceBroadcastRecordingsStore, "getCurrent").mockReturnValue( - voiceBroadcastRecording, - ); - }); - - describe(`and receiving a action:${ElementWidgetActions.JoinCall} message`, () => { - beforeEach(async () => { - messaging.on.mock.calls.find(([event, listener]) => { - if (event === `action:${ElementWidgetActions.JoinCall}`) { - listener(); - return true; - } - }); - }); - - it("should pause the current voice broadcast recording", () => { - expect(voiceBroadcastRecording.pause).toHaveBeenCalled(); - }); - }); - }); }); describe("StopGapWidget with stickyPromise", () => { let client: MockedObject<MatrixClient>; diff --git a/test/unit-tests/useTopic-test.tsx b/test/unit-tests/useTopic-test.tsx index cbef9dba60..81afaae985 100644 --- a/test/unit-tests/useTopic-test.tsx +++ b/test/unit-tests/useTopic-test.tsx @@ -29,7 +29,7 @@ describe("useTopic", () => { event: true, }); - room.addLiveEvents([topic]); + room.addLiveEvents([topic], { addToState: true }); function RoomTopic() { const topic = useTopic(room); @@ -52,7 +52,7 @@ describe("useTopic", () => { }); act(() => { - room.addLiveEvents([updatedTopic]); + room.addLiveEvents([updatedTopic], { addToState: true }); }); expect(screen.queryByText("New topic")).toBeInTheDocument(); diff --git a/test/unit-tests/utils/DateUtils-test.ts b/test/unit-tests/utils/DateUtils-test.ts index 6112c19e4a..2007c8a05a 100644 --- a/test/unit-tests/utils/DateUtils-test.ts +++ b/test/unit-tests/utils/DateUtils-test.ts @@ -178,22 +178,26 @@ describe("formatDate", () => { it("should return time string if date is within same day", () => { const date = new Date(REPEATABLE_DATE.getTime() + 2 * HOUR_MS + 12 * MINUTE_MS); - expect(formatDate(date, false, "en-GB")).toMatchInlineSnapshot(`"19:10"`); + // We use en-US for these tests because there was a change in Node 22.12 which removed + // the comma after the weekday for en-GB which makes the test output different things + // on different node versions. I'm not sure what a better fix would be, so let's just use + // a locale that happens to have a more stable formatting right now. + expect(formatDate(date, false, "en-US")).toMatchInlineSnapshot(`"19:10"`); }); it("should return time string with weekday if date is within last 6 days", () => { const date = new Date(REPEATABLE_DATE.getTime() - 6 * DAY_MS + 2 * HOUR_MS + 12 * MINUTE_MS); - expect(formatDate(date, false, "en-GB")).toMatchInlineSnapshot(`"Fri 19:10"`); + expect(formatDate(date, false, "en-US")).toMatchInlineSnapshot(`"Fri 19:10"`); }); it("should return time & date string without year if it is within the same year", () => { const date = new Date(REPEATABLE_DATE.getTime() - 66 * DAY_MS + 2 * HOUR_MS + 12 * MINUTE_MS); - expect(formatDate(date, false, "en-GB")).toMatchInlineSnapshot(`"Mon, 12 Sept, 19:10"`); + expect(formatDate(date, false, "en-US")).toMatchInlineSnapshot(`"Mon, Sep 12, 19:10"`); }); it("should return full time & date string otherwise", () => { const date = new Date(REPEATABLE_DATE.getTime() - 666 * DAY_MS + 2 * HOUR_MS + 12 * MINUTE_MS); - expect(formatDate(date, false, "en-GB")).toMatchInlineSnapshot(`"Wed, 20 Jan 2021, 19:10"`); + expect(formatDate(date, false, "en-US")).toMatchInlineSnapshot(`"Wed, Jan 20, 2021, 19:10"`); }); }); diff --git a/test/unit-tests/utils/EventRenderingUtils-test.ts b/test/unit-tests/utils/EventRenderingUtils-test.ts deleted file mode 100644 index 8a3cfded4a..0000000000 --- a/test/unit-tests/utils/EventRenderingUtils-test.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { getEventDisplayInfo } from "../../../src/utils/EventRenderingUtils"; -import { VoiceBroadcastInfoState } from "../../../src/voice-broadcast"; -import { mkVoiceBroadcastInfoStateEvent } from "../voice-broadcast/utils/test-utils"; -import { createTestClient } from "../../test-utils"; - -describe("getEventDisplayInfo", () => { - const mkBroadcastInfoEvent = (state: VoiceBroadcastInfoState) => { - return mkVoiceBroadcastInfoStateEvent("!room:example.com", state, "@user:example.com", "ASD123"); - }; - - it("should return the expected value for a broadcast started event", () => { - expect(getEventDisplayInfo(createTestClient(), mkBroadcastInfoEvent(VoiceBroadcastInfoState.Started), false)) - .toMatchInlineSnapshot(` - { - "hasRenderer": true, - "isBubbleMessage": false, - "isInfoMessage": false, - "isLeftAlignedBubbleMessage": false, - "isSeeingThroughMessageHiddenForModeration": false, - "noBubbleEvent": true, - } - `); - }); - - it("should return the expected value for a broadcast stopped event", () => { - expect(getEventDisplayInfo(createTestClient(), mkBroadcastInfoEvent(VoiceBroadcastInfoState.Stopped), false)) - .toMatchInlineSnapshot(` - { - "hasRenderer": true, - "isBubbleMessage": false, - "isInfoMessage": true, - "isLeftAlignedBubbleMessage": false, - "isSeeingThroughMessageHiddenForModeration": false, - "noBubbleEvent": true, - } - `); - }); -}); diff --git a/test/unit-tests/utils/EventUtils-test.ts b/test/unit-tests/utils/EventUtils-test.ts index 2fc13b507c..c7828c3a2f 100644 --- a/test/unit-tests/utils/EventUtils-test.ts +++ b/test/unit-tests/utils/EventUtils-test.ts @@ -35,8 +35,6 @@ import { import { getMockClientWithEventEmitter, makeBeaconInfoEvent, makePollStartEvent, stubClient } from "../../test-utils"; import dis from "../../../src/dispatcher/dispatcher"; import { Action } from "../../../src/dispatcher/actions"; -import { mkVoiceBroadcastInfoStateEvent } from "../voice-broadcast/utils/test-utils"; -import { VoiceBroadcastInfoState } from "../../../src/voice-broadcast/types"; jest.mock("../../../src/dispatcher/dispatcher"); @@ -148,20 +146,6 @@ describe("EventUtils", () => { }, }); - const voiceBroadcastStart = mkVoiceBroadcastInfoStateEvent( - "!room:example.com", - VoiceBroadcastInfoState.Started, - "@user:example.com", - "ABC123", - ); - - const voiceBroadcastStop = mkVoiceBroadcastInfoStateEvent( - "!room:example.com", - VoiceBroadcastInfoState.Stopped, - "@user:example.com", - "ABC123", - ); - describe("isContentActionable()", () => { type TestCase = [string, MatrixEvent]; it.each<TestCase>([ @@ -172,7 +156,6 @@ describe("EventUtils", () => { ["room member event", roomMemberEvent], ["event without msgtype", noMsgType], ["event without content body property", noContentBody], - ["broadcast stop event", voiceBroadcastStop], ])("returns false for %s", (_description, event) => { expect(isContentActionable(event)).toBe(false); }); @@ -183,7 +166,6 @@ describe("EventUtils", () => { ["event with empty content body", emptyContentBody], ["event with a content body", niceTextMessage], ["beacon_info event", beaconInfoEvent], - ["broadcast start event", voiceBroadcastStart], ])("returns true for %s", (_description, event) => { expect(isContentActionable(event)).toBe(true); }); diff --git a/test/unit-tests/utils/exportUtils/HTMLExport-test.ts b/test/unit-tests/utils/exportUtils/HTMLExport-test.ts index 0fc96e4db7..d169b55a7c 100644 --- a/test/unit-tests/utils/exportUtils/HTMLExport-test.ts +++ b/test/unit-tests/utils/exportUtils/HTMLExport-test.ts @@ -593,18 +593,21 @@ describe("HTMLExport", () => { it("should not make /messages requests when exporting 'Current Timeline'", async () => { client.createMessagesRequest.mockRejectedValue(new Error("Should never be called")); - room.addLiveEvents([ - new MatrixEvent({ - event_id: `$eventId`, - type: EventType.RoomMessage, - sender: client.getSafeUserId(), - origin_server_ts: 123456789, - content: { - msgtype: "m.text", - body: `testing testing`, - }, - }), - ]); + room.addLiveEvents( + [ + new MatrixEvent({ + event_id: `$eventId`, + type: EventType.RoomMessage, + sender: client.getSafeUserId(), + origin_server_ts: 123456789, + content: { + msgtype: "m.text", + body: `testing testing`, + }, + }), + ], + { addToState: true }, + ); const exporter = new HTMLExporter( room, diff --git a/test/unit-tests/utils/notifications-test.ts b/test/unit-tests/utils/notifications-test.ts index 8e33575fec..6cf6e3496b 100644 --- a/test/unit-tests/utils/notifications-test.ts +++ b/test/unit-tests/utils/notifications-test.ts @@ -121,7 +121,7 @@ describe("notifications", () => { user: USER_ID, msg: "Hello", }); - room.addLiveEvents([message]); + room.addLiveEvents([message], { addToState: true }); sendReadReceiptSpy = jest.spyOn(client, "sendReadReceipt").mockResolvedValue({}); jest.spyOn(client, "getRooms").mockReturnValue([room]); jest.spyOn(SettingsStore, "getValue").mockImplementation((name) => { @@ -187,7 +187,7 @@ describe("notifications", () => { user: USER_ID, ts: 1, }); - room.addLiveEvents([message]); + room.addLiveEvents([message], { addToState: true }); room.setUnreadNotificationCount(NotificationCountType.Total, 1); await clearAllNotifications(client); @@ -202,7 +202,7 @@ describe("notifications", () => { user: USER_ID, ts: 1, }); - room.addLiveEvents([message]); + room.addLiveEvents([message], { addToState: true }); room.setUnreadNotificationCount(NotificationCountType.Total, 1); jest.spyOn(SettingsStore, "getValue").mockReset().mockReturnValue(false); diff --git a/test/unit-tests/voice-broadcast/audio/VoiceBroadcastRecorder-test.ts b/test/unit-tests/voice-broadcast/audio/VoiceBroadcastRecorder-test.ts deleted file mode 100644 index 5718fc118c..0000000000 --- a/test/unit-tests/voice-broadcast/audio/VoiceBroadcastRecorder-test.ts +++ /dev/null @@ -1,251 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { mocked } from "jest-mock"; -import { Optional } from "matrix-events-sdk"; - -import { VoiceRecording } from "../../../../src/audio/VoiceRecording"; -import SdkConfig from "../../../../src/SdkConfig"; -import { concat } from "../../../../src/utils/arrays"; -import { - ChunkRecordedPayload, - createVoiceBroadcastRecorder, - VoiceBroadcastRecorder, - VoiceBroadcastRecorderEvent, -} from "../../../../src/voice-broadcast"; - -// mock VoiceRecording because it contains all the audio APIs -jest.mock("../../../../src/audio/VoiceRecording", () => ({ - VoiceRecording: jest.fn().mockReturnValue({ - disableMaxLength: jest.fn(), - emit: jest.fn(), - liveData: { - onUpdate: jest.fn(), - }, - start: jest.fn(), - stop: jest.fn(), - destroy: jest.fn(), - }), -})); - -jest.mock("../../../../src/settings/SettingsStore"); - -describe("VoiceBroadcastRecorder", () => { - describe("createVoiceBroadcastRecorder", () => { - beforeEach(() => { - jest.spyOn(SdkConfig, "get").mockImplementation((key: string) => { - if (key === "voice_broadcast") { - return { - chunk_length: 1337, - }; - } - }); - }); - - afterEach(() => { - mocked(SdkConfig.get).mockRestore(); - }); - - it("should return a VoiceBroadcastRecorder instance with targetChunkLength from config", () => { - const voiceBroadcastRecorder = createVoiceBroadcastRecorder(); - expect(voiceBroadcastRecorder).toBeInstanceOf(VoiceBroadcastRecorder); - expect(voiceBroadcastRecorder.targetChunkLength).toBe(1337); - }); - }); - - describe("instance", () => { - const chunkLength = 30; - // 0... OpusHead - const headers1 = new Uint8Array([...Array(28).fill(0), 79, 112, 117, 115, 72, 101, 97, 100]); - // 0... OpusTags - const headers2 = new Uint8Array([...Array(28).fill(0), 79, 112, 117, 115, 84, 97, 103, 115]); - const chunk1 = new Uint8Array([5, 6]); - const chunk2a = new Uint8Array([7, 8]); - const chunk2b = new Uint8Array([9, 10]); - const contentType = "test content type"; - - let voiceRecording: VoiceRecording; - let voiceBroadcastRecorder: VoiceBroadcastRecorder; - let onChunkRecorded: (chunk: ChunkRecordedPayload) => void; - - const simulateFirstChunk = (): void => { - // send headers in wrong order and multiple times to test robustness for that - voiceRecording.onDataAvailable!(headers2); - voiceRecording.onDataAvailable!(headers1); - voiceRecording.onDataAvailable!(headers1); - voiceRecording.onDataAvailable!(headers2); - // set recorder seconds to something greater than the test chunk length of 30 - // @ts-ignore - voiceRecording.recorderSeconds = 42; - voiceRecording.onDataAvailable!(chunk1); - voiceRecording.onDataAvailable!(headers1); - }; - - const expectOnFirstChunkRecorded = (): void => { - expect(onChunkRecorded).toHaveBeenNthCalledWith(1, { - buffer: concat(headers1, headers2, chunk1), - length: 42, - }); - }; - - const itShouldNotEmitAChunkRecordedEvent = (): void => { - it("should not emit a ChunkRecorded event", (): void => { - expect(voiceRecording.emit).not.toHaveBeenCalledWith( - VoiceBroadcastRecorderEvent.ChunkRecorded, - expect.anything(), - ); - }); - }; - - beforeEach(() => { - voiceRecording = new VoiceRecording(); - // @ts-ignore - voiceRecording.recorderSeconds = 23; - // @ts-ignore - voiceRecording.contentType = contentType; - - voiceBroadcastRecorder = new VoiceBroadcastRecorder(voiceRecording, chunkLength); - jest.spyOn(voiceBroadcastRecorder, "removeAllListeners"); - onChunkRecorded = jest.fn(); - voiceBroadcastRecorder.on(VoiceBroadcastRecorderEvent.ChunkRecorded, onChunkRecorded); - }); - - afterEach(() => { - voiceBroadcastRecorder.destroy(); - }); - - it("start should forward the call to VoiceRecording.start", async () => { - await voiceBroadcastRecorder.start(); - expect(voiceRecording.start).toHaveBeenCalled(); - }); - - describe("stop", () => { - beforeEach(async () => { - await voiceBroadcastRecorder.stop(); - }); - - it("should forward the call to VoiceRecording.stop", async () => { - expect(voiceRecording.stop).toHaveBeenCalled(); - }); - - itShouldNotEmitAChunkRecordedEvent(); - }); - - describe("when calling destroy", () => { - beforeEach(() => { - voiceBroadcastRecorder.destroy(); - }); - - it("should call VoiceRecording.destroy", () => { - expect(voiceRecording.destroy).toHaveBeenCalled(); - }); - - it("should remove all listeners", () => { - expect(voiceBroadcastRecorder.removeAllListeners).toHaveBeenCalled(); - }); - }); - - it("contentType should return the value from VoiceRecording", () => { - expect(voiceBroadcastRecorder.contentType).toBe(contentType); - }); - - describe("when the first header from recorder has been received", () => { - beforeEach(() => { - voiceRecording.onDataAvailable!(headers1); - }); - - itShouldNotEmitAChunkRecordedEvent(); - }); - - describe("when the second header from recorder has been received", () => { - beforeEach(() => { - voiceRecording.onDataAvailable!(headers1); - voiceRecording.onDataAvailable!(headers2); - }); - - itShouldNotEmitAChunkRecordedEvent(); - }); - - describe("when a third page from recorder has been received", () => { - beforeEach(() => { - voiceRecording.onDataAvailable!(headers1); - voiceRecording.onDataAvailable!(headers2); - voiceRecording.onDataAvailable!(chunk1); - }); - - itShouldNotEmitAChunkRecordedEvent(); - - describe("and calling stop", () => { - let stopPayload: Optional<ChunkRecordedPayload>; - - beforeEach(async () => { - stopPayload = await voiceBroadcastRecorder.stop(); - }); - - it("should return the remaining chunk", () => { - expect(stopPayload).toEqual({ - buffer: concat(headers1, headers2, chunk1), - length: 23, - }); - }); - - describe("and calling start again and receiving some data", () => { - beforeEach(() => { - simulateFirstChunk(); - }); - - it("should emit the ChunkRecorded event for the first chunk", () => { - expectOnFirstChunkRecorded(); - }); - }); - }); - - describe("and calling stop() with recording.stop error)", () => { - let stopPayload: Optional<ChunkRecordedPayload>; - - beforeEach(async () => { - mocked(voiceRecording.stop).mockRejectedValue("Error"); - stopPayload = await voiceBroadcastRecorder.stop(); - }); - - it("should return the remaining chunk", () => { - expect(stopPayload).toEqual({ - buffer: concat(headers1, headers2, chunk1), - length: 23, - }); - }); - }); - }); - - describe("when some chunks have been received", () => { - beforeEach(() => { - simulateFirstChunk(); - - // simulate a second chunk - voiceRecording.onDataAvailable!(chunk2a); - - // send headers again to test robustness for that - voiceRecording.onDataAvailable!(headers2); - - // add another 30 seconds for the next chunk - // @ts-ignore - voiceRecording.recorderSeconds = 72; - voiceRecording.onDataAvailable!(chunk2b); - }); - - it("should emit ChunkRecorded events", () => { - expectOnFirstChunkRecorded(); - - expect(onChunkRecorded).toHaveBeenNthCalledWith(2, { - buffer: concat(headers1, headers2, chunk2a, chunk2b), - length: 72 - 42, // 72 (position at second chunk) - 42 (position of first chunk) - }); - }); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/components/VoiceBroadcastBody-test.tsx b/test/unit-tests/voice-broadcast/components/VoiceBroadcastBody-test.tsx deleted file mode 100644 index bf55c45b68..0000000000 --- a/test/unit-tests/voice-broadcast/components/VoiceBroadcastBody-test.tsx +++ /dev/null @@ -1,171 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React, { ReactElement } from "react"; -import { act, render, screen } from "jest-matrix-react"; -import { mocked } from "jest-mock"; -import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; - -import { - VoiceBroadcastBody as UnwrappedVoiceBroadcastBody, - VoiceBroadcastInfoState, - VoiceBroadcastRecordingBody, - VoiceBroadcastRecording, - VoiceBroadcastPlaybackBody, - VoiceBroadcastPlayback, - VoiceBroadcastRecordingsStore, -} from "../../../../src/voice-broadcast"; -import { withClientContextRenderOptions, stubClient, wrapInSdkContext } from "../../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils"; -import { MediaEventHelper } from "../../../../src/utils/MediaEventHelper"; -import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks"; -import { SdkContextClass } from "../../../../src/contexts/SDKContext"; - -jest.mock("../../../../src/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody", () => ({ - VoiceBroadcastRecordingBody: jest.fn(), -})); - -jest.mock("../../../../src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody", () => ({ - VoiceBroadcastPlaybackBody: jest.fn(), -})); - -jest.mock("../../../../src/utils/permalinks/Permalinks"); -jest.mock("../../../../src/utils/MediaEventHelper"); -jest.mock("../../../../src/stores/WidgetStore"); -jest.mock("../../../../src/stores/widgets/WidgetLayoutStore"); - -describe("VoiceBroadcastBody", () => { - const roomId = "!room:example.com"; - let userId: string; - let deviceId: string; - let client: MatrixClient; - let room: Room; - let infoEvent: MatrixEvent; - let stoppedEvent: MatrixEvent; - let testRecording: VoiceBroadcastRecording; - let testPlayback: VoiceBroadcastPlayback; - - const renderVoiceBroadcast = () => { - const VoiceBroadcastBody = wrapInSdkContext(UnwrappedVoiceBroadcastBody, SdkContextClass.instance); - render( - <VoiceBroadcastBody - mxEvent={infoEvent} - mediaEventHelper={new MediaEventHelper(infoEvent)} - onHeightChanged={() => {}} - onMessageAllowed={() => {}} - permalinkCreator={new RoomPermalinkCreator(room)} - />, - withClientContextRenderOptions(client), - ); - testRecording = SdkContextClass.instance.voiceBroadcastRecordingsStore.getByInfoEvent(infoEvent, client); - }; - - beforeEach(() => { - client = stubClient(); - userId = client.getUserId() || ""; - deviceId = client.getDeviceId() || ""; - mocked(client.relations).mockClear(); - mocked(client.relations).mockResolvedValue({ events: [] }); - room = new Room(roomId, client, userId); - mocked(client.getRoom).mockImplementation((getRoomId?: string) => { - if (getRoomId === roomId) return room; - return null; - }); - - infoEvent = mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Started, userId, deviceId); - stoppedEvent = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Stopped, - userId, - deviceId, - infoEvent, - ); - room.addEventsToTimeline([infoEvent], true, room.getLiveTimeline()); - testRecording = new VoiceBroadcastRecording(infoEvent, client); - testPlayback = new VoiceBroadcastPlayback(infoEvent, client, new VoiceBroadcastRecordingsStore()); - mocked(VoiceBroadcastRecordingBody).mockImplementation(({ recording }): ReactElement | null => { - if (testRecording === recording) { - return <div data-testid="voice-broadcast-recording-body" />; - } - - return null; - }); - - mocked(VoiceBroadcastPlaybackBody).mockImplementation(({ playback }): ReactElement | null => { - if (testPlayback === playback) { - return <div data-testid="voice-broadcast-playback-body" />; - } - - return null; - }); - - jest.spyOn(SdkContextClass.instance.voiceBroadcastRecordingsStore, "getByInfoEvent").mockImplementation( - (getEvent: MatrixEvent, getClient: MatrixClient): VoiceBroadcastRecording => { - if (getEvent === infoEvent && getClient === client) { - return testRecording; - } - - throw new Error("unexpected event"); - }, - ); - - jest.spyOn(SdkContextClass.instance.voiceBroadcastPlaybacksStore, "getByInfoEvent").mockImplementation( - (getEvent: MatrixEvent): VoiceBroadcastPlayback => { - if (getEvent === infoEvent) { - return testPlayback; - } - - throw new Error("unexpected event"); - }, - ); - }); - - describe("when there is a stopped voice broadcast", () => { - beforeEach(() => { - room.addEventsToTimeline([stoppedEvent], true, room.getLiveTimeline()); - renderVoiceBroadcast(); - }); - - it("should render a voice broadcast playback body", () => { - screen.getByTestId("voice-broadcast-playback-body"); - }); - }); - - describe("when there is a started voice broadcast from the current user", () => { - beforeEach(() => { - renderVoiceBroadcast(); - }); - - it("should render a voice broadcast recording body", () => { - screen.getByTestId("voice-broadcast-recording-body"); - }); - - describe("and the recordings ends", () => { - beforeEach(() => { - act(() => { - room.addEventsToTimeline([stoppedEvent], true, room.getLiveTimeline()); - }); - }); - - it("should render a voice broadcast playback body", () => { - screen.getByTestId("voice-broadcast-playback-body"); - }); - }); - }); - - describe("when displaying a voice broadcast playback", () => { - beforeEach(() => { - mocked(client).getUserId.mockReturnValue("@other:example.com"); - renderVoiceBroadcast(); - }); - - it("should render a voice broadcast playback body", () => { - screen.getByTestId("voice-broadcast-playback-body"); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/components/atoms/LiveBadge-test.tsx b/test/unit-tests/voice-broadcast/components/atoms/LiveBadge-test.tsx deleted file mode 100644 index 2d630fca9d..0000000000 --- a/test/unit-tests/voice-broadcast/components/atoms/LiveBadge-test.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { render } from "jest-matrix-react"; - -import { LiveBadge } from "../../../../../src/voice-broadcast"; - -describe("LiveBadge", () => { - it("should render as expected with default props", () => { - const { container } = render(<LiveBadge />); - expect(container).toMatchSnapshot(); - }); - - it("should render in grey as expected", () => { - const { container } = render(<LiveBadge grey={true} />); - expect(container).toMatchSnapshot(); - }); -}); diff --git a/test/unit-tests/voice-broadcast/components/atoms/VoiceBroadcastControl-test.tsx b/test/unit-tests/voice-broadcast/components/atoms/VoiceBroadcastControl-test.tsx deleted file mode 100644 index 8c52062b5e..0000000000 --- a/test/unit-tests/voice-broadcast/components/atoms/VoiceBroadcastControl-test.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { render, RenderResult, screen } from "jest-matrix-react"; -import userEvent from "@testing-library/user-event"; - -import { VoiceBroadcastControl } from "../../../../../src/voice-broadcast"; -import { Icon as StopIcon } from "../../../../res/img/compound/stop-16.svg"; - -describe("VoiceBroadcastControl", () => { - let result: RenderResult; - let onClick: () => void; - - beforeEach(() => { - onClick = jest.fn(); - }); - - describe("when rendering it", () => { - beforeEach(() => { - const stopIcon = <StopIcon className="mx_Icon mx_Icon_16" />; - result = render(<VoiceBroadcastControl onClick={onClick} label="test label" icon={stopIcon} />); - }); - - it("should render as expected", () => { - expect(result.container).toMatchSnapshot(); - }); - - describe("when clicking it", () => { - beforeEach(async () => { - await userEvent.click(screen.getByLabelText("test label")); - }); - - it("should call onClick", () => { - expect(onClick).toHaveBeenCalled(); - }); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/components/atoms/VoiceBroadcastHeader-test.tsx b/test/unit-tests/voice-broadcast/components/atoms/VoiceBroadcastHeader-test.tsx deleted file mode 100644 index 6a0e7190d8..0000000000 --- a/test/unit-tests/voice-broadcast/components/atoms/VoiceBroadcastHeader-test.tsx +++ /dev/null @@ -1,89 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix"; -import { render, RenderResult } from "jest-matrix-react"; - -import { VoiceBroadcastHeader, VoiceBroadcastLiveness } from "../../../../../src/voice-broadcast"; -import { mkRoom, stubClient } from "../../../../test-utils"; - -// mock RoomAvatar, because it is doing too much fancy stuff -jest.mock("../../../../../src/components/views/avatars/RoomAvatar", () => ({ - __esModule: true, - default: jest.fn().mockImplementation(({ room }) => { - return <div data-testid="room-avatar">room avatar: {room.name}</div>; - }), -})); - -describe("VoiceBroadcastHeader", () => { - const userId = "@user:example.com"; - const roomId = "!room:example.com"; - let client: MatrixClient; - let room: Room; - const sender = new RoomMember(roomId, userId); - let container: RenderResult["container"]; - - const renderHeader = (live: VoiceBroadcastLiveness, showBroadcast?: boolean, buffering?: boolean): RenderResult => { - return render( - <VoiceBroadcastHeader - live={live} - microphoneLabel={sender.name} - room={room} - showBroadcast={showBroadcast} - showBuffering={buffering} - />, - ); - }; - - beforeAll(() => { - client = stubClient(); - room = mkRoom(client, roomId); - sender.name = "test user"; - }); - - describe("when rendering a live broadcast header with broadcast info", () => { - beforeEach(() => { - container = renderHeader("live", true, true).container; - }); - - it("should render the header with a red live badge", () => { - expect(container).toMatchSnapshot(); - }); - }); - - describe("when rendering a buffering live broadcast header with broadcast info", () => { - beforeEach(() => { - container = renderHeader("live", true).container; - }); - - it("should render the header with a red live badge", () => { - expect(container).toMatchSnapshot(); - }); - }); - - describe("when rendering a live (grey) broadcast header with broadcast info", () => { - beforeEach(() => { - container = renderHeader("grey", true).container; - }); - - it("should render the header with a grey live badge", () => { - expect(container).toMatchSnapshot(); - }); - }); - - describe("when rendering a non-live broadcast header", () => { - beforeEach(() => { - container = renderHeader("not-live").container; - }); - - it("should render the header without a live badge", () => { - expect(container).toMatchSnapshot(); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/components/atoms/VoiceBroadcastPlaybackControl-test.tsx b/test/unit-tests/voice-broadcast/components/atoms/VoiceBroadcastPlaybackControl-test.tsx deleted file mode 100644 index 9eef863eb4..0000000000 --- a/test/unit-tests/voice-broadcast/components/atoms/VoiceBroadcastPlaybackControl-test.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022, 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { render, RenderResult, screen } from "jest-matrix-react"; -import userEvent from "@testing-library/user-event"; - -import { VoiceBroadcastPlaybackControl, VoiceBroadcastPlaybackState } from "../../../../../src/voice-broadcast"; - -describe("<VoiceBroadcastPlaybackControl />", () => { - const renderControl = (state: VoiceBroadcastPlaybackState): { result: RenderResult; onClick: () => void } => { - const onClick = jest.fn(); - return { - onClick, - result: render(<VoiceBroadcastPlaybackControl state={state} onClick={onClick} />), - }; - }; - - it.each([ - VoiceBroadcastPlaybackState.Stopped, - VoiceBroadcastPlaybackState.Paused, - VoiceBroadcastPlaybackState.Buffering, - VoiceBroadcastPlaybackState.Playing, - ])("should render state %s as expected", (state: VoiceBroadcastPlaybackState) => { - expect(renderControl(state).result.container).toMatchSnapshot(); - }); - - it("should not render for error state", () => { - expect(renderControl(VoiceBroadcastPlaybackState.Error).result.asFragment()).toMatchInlineSnapshot( - `<DocumentFragment />`, - ); - }); - - describe("when clicking the control", () => { - let onClick: () => void; - - beforeEach(async () => { - onClick = renderControl(VoiceBroadcastPlaybackState.Playing).onClick; - await userEvent.click(screen.getByLabelText("pause voice broadcast")); - }); - - it("should invoke the onClick callback", () => { - expect(onClick).toHaveBeenCalled(); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/components/atoms/__snapshots__/LiveBadge-test.tsx.snap b/test/unit-tests/voice-broadcast/components/atoms/__snapshots__/LiveBadge-test.tsx.snap deleted file mode 100644 index bd4b8d2bcc..0000000000 --- a/test/unit-tests/voice-broadcast/components/atoms/__snapshots__/LiveBadge-test.tsx.snap +++ /dev/null @@ -1,27 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LiveBadge should render as expected with default props 1`] = ` -<div> - <div - class="mx_LiveBadge" - > - <div - class="mx_Icon mx_Icon_16" - /> - Live - </div> -</div> -`; - -exports[`LiveBadge should render in grey as expected 1`] = ` -<div> - <div - class="mx_LiveBadge mx_LiveBadge--grey" - > - <div - class="mx_Icon mx_Icon_16" - /> - Live - </div> -</div> -`; diff --git a/test/unit-tests/voice-broadcast/components/atoms/__snapshots__/VoiceBroadcastControl-test.tsx.snap b/test/unit-tests/voice-broadcast/components/atoms/__snapshots__/VoiceBroadcastControl-test.tsx.snap deleted file mode 100644 index a28e105b4c..0000000000 --- a/test/unit-tests/voice-broadcast/components/atoms/__snapshots__/VoiceBroadcastControl-test.tsx.snap +++ /dev/null @@ -1,16 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`VoiceBroadcastControl when rendering it should render as expected 1`] = ` -<div> - <div - aria-label="test label" - class="mx_AccessibleButton mx_VoiceBroadcastControl" - role="button" - tabindex="0" - > - <div - class="mx_Icon mx_Icon_16" - /> - </div> -</div> -`; diff --git a/test/unit-tests/voice-broadcast/components/atoms/__snapshots__/VoiceBroadcastHeader-test.tsx.snap b/test/unit-tests/voice-broadcast/components/atoms/__snapshots__/VoiceBroadcastHeader-test.tsx.snap deleted file mode 100644 index 0c1c966f73..0000000000 --- a/test/unit-tests/voice-broadcast/components/atoms/__snapshots__/VoiceBroadcastHeader-test.tsx.snap +++ /dev/null @@ -1,277 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`VoiceBroadcastHeader when rendering a buffering live broadcast header with broadcast info should render the header with a red live badge 1`] = ` -<div> - <div - class="mx_VoiceBroadcastHeader" - > - <div - data-testid="room-avatar" - > - room avatar: - !room:example.com - </div> - <div - class="mx_VoiceBroadcastHeader_content" - > - <div - class="mx_VoiceBroadcastHeader_room_wrapper" - > - <div - class="mx_VoiceBroadcastHeader_room" - > - !room:example.com - </div> - </div> - <div - aria-label="Change input device" - class="mx_AccessibleButton mx_VoiceBroadcastHeader_line" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_16" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z" - /> - <path - d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z" - /> - </svg> - <span> - test user - </span> - </div> - <div - class="mx_VoiceBroadcastHeader_line" - > - <div - class="mx_Icon mx_Icon_16" - /> - Voice broadcast - </div> - </div> - <div - class="mx_LiveBadge" - > - <div - class="mx_Icon mx_Icon_16" - /> - Live - </div> - </div> -</div> -`; - -exports[`VoiceBroadcastHeader when rendering a live (grey) broadcast header with broadcast info should render the header with a grey live badge 1`] = ` -<div> - <div - class="mx_VoiceBroadcastHeader" - > - <div - data-testid="room-avatar" - > - room avatar: - !room:example.com - </div> - <div - class="mx_VoiceBroadcastHeader_content" - > - <div - class="mx_VoiceBroadcastHeader_room_wrapper" - > - <div - class="mx_VoiceBroadcastHeader_room" - > - !room:example.com - </div> - </div> - <div - aria-label="Change input device" - class="mx_AccessibleButton mx_VoiceBroadcastHeader_line" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_16" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z" - /> - <path - d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z" - /> - </svg> - <span> - test user - </span> - </div> - <div - class="mx_VoiceBroadcastHeader_line" - > - <div - class="mx_Icon mx_Icon_16" - /> - Voice broadcast - </div> - </div> - <div - class="mx_LiveBadge mx_LiveBadge--grey" - > - <div - class="mx_Icon mx_Icon_16" - /> - Live - </div> - </div> -</div> -`; - -exports[`VoiceBroadcastHeader when rendering a live broadcast header with broadcast info should render the header with a red live badge 1`] = ` -<div> - <div - class="mx_VoiceBroadcastHeader" - > - <div - data-testid="room-avatar" - > - room avatar: - !room:example.com - </div> - <div - class="mx_VoiceBroadcastHeader_content" - > - <div - class="mx_VoiceBroadcastHeader_room_wrapper" - > - <div - class="mx_VoiceBroadcastHeader_room" - > - !room:example.com - </div> - </div> - <div - aria-label="Change input device" - class="mx_AccessibleButton mx_VoiceBroadcastHeader_line" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_16" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z" - /> - <path - d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z" - /> - </svg> - <span> - test user - </span> - </div> - <div - class="mx_VoiceBroadcastHeader_line" - > - <div - class="mx_Icon mx_Icon_16" - /> - Voice broadcast - </div> - <div - class="mx_VoiceBroadcastHeader_line" - > - <div - class="mx_Spinner" - > - <div - aria-label="Loading…" - class="mx_Spinner_icon" - data-testid="spinner" - role="progressbar" - style="width: 14px; height: 14px;" - /> - </div> - Buffering… - </div> - </div> - <div - class="mx_LiveBadge" - > - <div - class="mx_Icon mx_Icon_16" - /> - Live - </div> - </div> -</div> -`; - -exports[`VoiceBroadcastHeader when rendering a non-live broadcast header should render the header without a live badge 1`] = ` -<div> - <div - class="mx_VoiceBroadcastHeader" - > - <div - data-testid="room-avatar" - > - room avatar: - !room:example.com - </div> - <div - class="mx_VoiceBroadcastHeader_content" - > - <div - class="mx_VoiceBroadcastHeader_room_wrapper" - > - <div - class="mx_VoiceBroadcastHeader_room" - > - !room:example.com - </div> - </div> - <div - aria-label="Change input device" - class="mx_AccessibleButton mx_VoiceBroadcastHeader_line" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_16" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z" - /> - <path - d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z" - /> - </svg> - <span> - test user - </span> - </div> - </div> - </div> -</div> -`; diff --git a/test/unit-tests/voice-broadcast/components/atoms/__snapshots__/VoiceBroadcastPlaybackControl-test.tsx.snap b/test/unit-tests/voice-broadcast/components/atoms/__snapshots__/VoiceBroadcastPlaybackControl-test.tsx.snap deleted file mode 100644 index 1ce23ca8b5..0000000000 --- a/test/unit-tests/voice-broadcast/components/atoms/__snapshots__/VoiceBroadcastPlaybackControl-test.tsx.snap +++ /dev/null @@ -1,97 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`<VoiceBroadcastPlaybackControl /> should render state buffering as expected 1`] = ` -<div> - <div - aria-label="pause voice broadcast" - class="mx_AccessibleButton mx_VoiceBroadcastControl" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_12" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M8 4a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Zm8 0a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Z" - /> - </svg> - </div> -</div> -`; - -exports[`<VoiceBroadcastPlaybackControl /> should render state pause as expected 1`] = ` -<div> - <div - aria-label="resume voice broadcast" - class="mx_AccessibleButton mx_VoiceBroadcastControl mx_VoiceBroadcastControl-play" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_16" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="m8.98 4.677 9.921 5.58c1.36.764 1.36 2.722 0 3.486l-9.92 5.58C7.647 20.073 6 19.11 6 17.58V6.42c0-1.53 1.647-2.493 2.98-1.743Z" - /> - </svg> - </div> -</div> -`; - -exports[`<VoiceBroadcastPlaybackControl /> should render state playing as expected 1`] = ` -<div> - <div - aria-label="pause voice broadcast" - class="mx_AccessibleButton mx_VoiceBroadcastControl" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_12" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M8 4a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Zm8 0a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Z" - /> - </svg> - </div> -</div> -`; - -exports[`<VoiceBroadcastPlaybackControl /> should render state stopped as expected 1`] = ` -<div> - <div - aria-label="play voice broadcast" - class="mx_AccessibleButton mx_VoiceBroadcastControl mx_VoiceBroadcastControl-play" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_16" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="m8.98 4.677 9.921 5.58c1.36.764 1.36 2.722 0 3.486l-9.92 5.58C7.647 20.073 6 19.11 6 17.58V6.42c0-1.53 1.647-2.493 2.98-1.743Z" - /> - </svg> - </div> -</div> -`; diff --git a/test/unit-tests/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody-test.tsx b/test/unit-tests/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody-test.tsx deleted file mode 100644 index a6dd10e5c1..0000000000 --- a/test/unit-tests/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody-test.tsx +++ /dev/null @@ -1,249 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022, 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; -import { act, render, RenderResult, screen } from "jest-matrix-react"; -import userEvent from "@testing-library/user-event"; -import { mocked } from "jest-mock"; - -import { - VoiceBroadcastInfoState, - VoiceBroadcastLiveness, - VoiceBroadcastPlayback, - VoiceBroadcastPlaybackBody, - VoiceBroadcastPlaybackEvent, - VoiceBroadcastPlaybackState, -} from "../../../../../src/voice-broadcast"; -import { filterConsole, stubClient } from "../../../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "../../utils/test-utils"; -import dis from "../../../../../src/dispatcher/dispatcher"; -import { Action } from "../../../../../src/dispatcher/actions"; -import { SdkContextClass } from "../../../../../src/contexts/SDKContext"; - -jest.mock("../../../../../src/dispatcher/dispatcher"); - -// mock RoomAvatar, because it is doing too much fancy stuff -jest.mock("../../../../../src/components/views/avatars/RoomAvatar", () => ({ - __esModule: true, - default: jest.fn().mockImplementation(({ room }) => { - return <div data-testid="room-avatar">room avatar: {room.name}</div>; - }), -})); - -describe("VoiceBroadcastPlaybackBody", () => { - const userId = "@user:example.com"; - const roomId = "!room:example.com"; - const duration = 23 * 60 + 42; // 23:42 - let client: MatrixClient; - let infoEvent: MatrixEvent; - let playback: VoiceBroadcastPlayback; - let renderResult: RenderResult; - - filterConsole( - // expected for some tests - "voice broadcast chunk event to skip to not found", - ); - - beforeAll(() => { - client = stubClient(); - mocked(client.relations).mockClear(); - mocked(client.relations).mockResolvedValue({ events: [] }); - - infoEvent = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Stopped, - userId, - client.getDeviceId(), - ); - }); - - beforeEach(() => { - playback = new VoiceBroadcastPlayback( - infoEvent, - client, - SdkContextClass.instance.voiceBroadcastRecordingsStore, - ); - jest.spyOn(playback, "toggle").mockImplementation(() => Promise.resolve()); - jest.spyOn(playback, "getLiveness"); - jest.spyOn(playback, "getState"); - jest.spyOn(playback, "skipTo"); - jest.spyOn(playback, "durationSeconds", "get").mockReturnValue(duration); - }); - - describe("when rendering a buffering voice broadcast", () => { - beforeEach(() => { - mocked(playback.getState).mockReturnValue(VoiceBroadcastPlaybackState.Buffering); - mocked(playback.getLiveness).mockReturnValue("live"); - renderResult = render(<VoiceBroadcastPlaybackBody playback={playback} />); - }); - - it("should render as expected", () => { - expect(renderResult.container).toMatchSnapshot(); - }); - }); - - describe("when rendering a playing broadcast", () => { - beforeEach(() => { - mocked(playback.getState).mockReturnValue(VoiceBroadcastPlaybackState.Playing); - mocked(playback.getLiveness).mockReturnValue("not-live"); - renderResult = render(<VoiceBroadcastPlaybackBody playback={playback} />); - }); - - it("should render as expected", () => { - expect(renderResult.container).toMatchSnapshot(); - }); - - describe("and being in the middle of the playback", () => { - beforeEach(() => { - act(() => { - playback.emit(VoiceBroadcastPlaybackEvent.TimesChanged, { - duration, - position: 10 * 60, - timeLeft: duration - 10 * 60, - }); - }); - }); - - describe("and clicking 30s backward", () => { - beforeEach(async () => { - await act(async () => { - await userEvent.click(screen.getByLabelText("30s backward")); - }); - }); - - it("should seek 30s backward", () => { - expect(playback.skipTo).toHaveBeenCalledWith(9 * 60 + 30); - }); - }); - - describe("and clicking 30s forward", () => { - beforeEach(async () => { - await act(async () => { - await userEvent.click(screen.getByLabelText("30s forward")); - }); - }); - - it("should seek 30s forward", () => { - expect(playback.skipTo).toHaveBeenCalledWith(10 * 60 + 30); - }); - }); - }); - - describe("and clicking the room name", () => { - beforeEach(async () => { - await userEvent.click(screen.getByText("My room")); - }); - - it("should not view the room", () => { - expect(dis.dispatch).not.toHaveBeenCalled(); - }); - }); - }); - - describe("when rendering a playing broadcast in pip mode", () => { - beforeEach(() => { - mocked(playback.getState).mockReturnValue(VoiceBroadcastPlaybackState.Playing); - mocked(playback.getLiveness).mockReturnValue("not-live"); - renderResult = render(<VoiceBroadcastPlaybackBody pip={true} playback={playback} />); - }); - - it("should render as expected", () => { - expect(renderResult.container).toMatchSnapshot(); - }); - - describe("and clicking the room name", () => { - beforeEach(async () => { - await userEvent.click(screen.getByText("My room")); - }); - - it("should view the room", () => { - expect(dis.dispatch).toHaveBeenCalledWith({ - action: Action.ViewRoom, - room_id: roomId, - metricsTrigger: undefined, - }); - }); - }); - }); - - describe(`when rendering a stopped broadcast`, () => { - beforeEach(() => { - mocked(playback.getState).mockReturnValue(VoiceBroadcastPlaybackState.Stopped); - mocked(playback.getLiveness).mockReturnValue("not-live"); - renderResult = render(<VoiceBroadcastPlaybackBody playback={playback} />); - }); - - it("should render as expected", () => { - expect(renderResult.container).toMatchSnapshot(); - }); - - describe("and clicking the play button", () => { - beforeEach(async () => { - await userEvent.click(renderResult.getByLabelText("play voice broadcast")); - }); - - it("should toggle the recording", () => { - expect(playback.toggle).toHaveBeenCalled(); - }); - }); - - describe("and the times update", () => { - beforeEach(() => { - act(() => { - playback.emit(VoiceBroadcastPlaybackEvent.TimesChanged, { - duration, - position: 5 * 60 + 13, - timeLeft: 7 * 60 + 5, - }); - }); - }); - - it("should render the times", async () => { - expect(await screen.findByText("05:13")).toBeInTheDocument(); - expect(await screen.findByText("-07:05")).toBeInTheDocument(); - }); - }); - }); - - describe("when rendering an error broadcast", () => { - beforeEach(() => { - mocked(playback.getState).mockReturnValue(VoiceBroadcastPlaybackState.Error); - renderResult = render(<VoiceBroadcastPlaybackBody playback={playback} />); - }); - - it("should render as expected", () => { - expect(renderResult.container).toMatchSnapshot(); - }); - }); - - describe.each([ - [VoiceBroadcastPlaybackState.Paused, "not-live"], - [VoiceBroadcastPlaybackState.Playing, "live"], - ] satisfies [VoiceBroadcastPlaybackState, VoiceBroadcastLiveness][])( - "when rendering a %s/%s broadcast", - (state: VoiceBroadcastPlaybackState, liveness: VoiceBroadcastLiveness) => { - beforeEach(() => { - mocked(playback.getState).mockReturnValue(state); - mocked(playback.getLiveness).mockReturnValue(liveness); - renderResult = render(<VoiceBroadcastPlaybackBody playback={playback} />); - }); - - it("should render as expected", () => { - expect(renderResult.container).toMatchSnapshot(); - }); - }, - ); - - it("when there is a broadcast without sender, it should raise an error", () => { - infoEvent.sender = null; - expect(() => { - render(<VoiceBroadcastPlaybackBody playback={playback} />); - }).toThrow(`Voice Broadcast sender not found (event ${playback.infoEvent.getId()})`); - }); -}); diff --git a/test/unit-tests/voice-broadcast/components/molecules/VoiceBroadcastPreRecordingPip-test.tsx b/test/unit-tests/voice-broadcast/components/molecules/VoiceBroadcastPreRecordingPip-test.tsx deleted file mode 100644 index c21cc8d7e4..0000000000 --- a/test/unit-tests/voice-broadcast/components/molecules/VoiceBroadcastPreRecordingPip-test.tsx +++ /dev/null @@ -1,163 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { mocked } from "jest-mock"; -import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix"; -import { act, render, RenderResult, screen } from "jest-matrix-react"; -import userEvent from "@testing-library/user-event"; - -import { - VoiceBroadcastPlaybacksStore, - VoiceBroadcastPreRecording, - VoiceBroadcastPreRecordingPip, - VoiceBroadcastRecordingsStore, -} from "../../../../../src/voice-broadcast"; -import { flushPromises, stubClient } from "../../../../test-utils"; -import { requestMediaPermissions } from "../../../../../src/utils/media/requestMediaPermissions"; -import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../../../src/MediaDeviceHandler"; -import dis from "../../../../../src/dispatcher/dispatcher"; -import { Action } from "../../../../../src/dispatcher/actions"; - -jest.mock("../../../../../src/dispatcher/dispatcher"); -jest.mock("../../../../../src/utils/media/requestMediaPermissions"); - -// mock RoomAvatar, because it is doing too much fancy stuff -jest.mock("../../../../../src/components/views/avatars/RoomAvatar", () => ({ - __esModule: true, - default: jest.fn().mockImplementation(({ room }) => { - return <div data-testid="room-avatar">room avatar: {room.name}</div>; - }), -})); - -describe("VoiceBroadcastPreRecordingPip", () => { - let renderResult: RenderResult; - let preRecording: VoiceBroadcastPreRecording; - let playbacksStore: VoiceBroadcastPlaybacksStore; - let recordingsStore: VoiceBroadcastRecordingsStore; - let client: MatrixClient; - let room: Room; - let sender: RoomMember; - - const itShouldShowTheBroadcastRoom = () => { - it("should show the broadcast room", () => { - expect(dis.dispatch).toHaveBeenCalledWith({ - action: Action.ViewRoom, - room_id: room.roomId, - metricsTrigger: undefined, - }); - }); - }; - - beforeEach(() => { - client = stubClient(); - room = new Room("!room@example.com", client, client.getUserId() || ""); - sender = new RoomMember(room.roomId, client.getUserId() || ""); - recordingsStore = new VoiceBroadcastRecordingsStore(); - playbacksStore = new VoiceBroadcastPlaybacksStore(recordingsStore); - mocked(requestMediaPermissions).mockResolvedValue({ - getTracks: (): Array<MediaStreamTrack> => [], - } as unknown as MediaStream); - jest.spyOn(MediaDeviceHandler, "getDevices").mockResolvedValue({ - [MediaDeviceKindEnum.AudioInput]: [ - { - deviceId: "d1", - label: "Device 1", - } as MediaDeviceInfo, - { - deviceId: "d2", - label: "Device 2", - } as MediaDeviceInfo, - ], - [MediaDeviceKindEnum.AudioOutput]: [], - [MediaDeviceKindEnum.VideoInput]: [], - }); - jest.spyOn(MediaDeviceHandler.instance, "setDevice").mockImplementation(); - preRecording = new VoiceBroadcastPreRecording(room, sender, client, playbacksStore, recordingsStore); - jest.spyOn(preRecording, "start").mockResolvedValue(); - }); - - afterAll(() => { - jest.resetAllMocks(); - }); - - describe("when rendered", () => { - beforeEach(async () => { - renderResult = render(<VoiceBroadcastPreRecordingPip voiceBroadcastPreRecording={preRecording} />); - - await flushPromises(); - }); - - it("should match the snapshot", () => { - expect(renderResult.container).toMatchSnapshot(); - }); - - describe("and double clicking »Go live«", () => { - beforeEach(async () => { - await userEvent.click(screen.getByText("Go live")); - await userEvent.click(screen.getByText("Go live")); - }); - - it("should call start once", () => { - expect(preRecording.start).toHaveBeenCalledTimes(1); - }); - }); - - describe("and clicking the room name", () => { - beforeEach(async () => { - await userEvent.click(screen.getByText(room.name)); - }); - - itShouldShowTheBroadcastRoom(); - }); - - describe("and clicking the room avatar", () => { - beforeEach(async () => { - await userEvent.click(screen.getByText(`room avatar: ${room.name}`)); - }); - - itShouldShowTheBroadcastRoom(); - }); - - describe("and clicking the device label", () => { - beforeEach(async () => { - await act(async () => { - await userEvent.click(screen.getByText("Default Device")); - }); - }); - - it("should display the device selection", () => { - expect(screen.queryAllByText("Default Device").length).toBe(2); - expect(screen.queryByText("Device 1")).toBeInTheDocument(); - expect(screen.queryByText("Device 2")).toBeInTheDocument(); - }); - - describe("and selecting a device", () => { - beforeEach(async () => { - await act(async () => { - await userEvent.click(screen.getByText("Device 1")); - }); - }); - - it("should set it as current device", () => { - expect(MediaDeviceHandler.instance.setDevice).toHaveBeenCalledWith( - "d1", - MediaDeviceKindEnum.AudioInput, - ); - }); - - it("should not show the device selection", () => { - expect(screen.queryByText("Default Device")).not.toBeInTheDocument(); - // expected to be one in the document, displayed in the pip directly - expect(screen.queryByText("Device 1")).toBeInTheDocument(); - expect(screen.queryByText("Device 2")).not.toBeInTheDocument(); - }); - }); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody-test.tsx b/test/unit-tests/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody-test.tsx deleted file mode 100644 index 6c4d035373..0000000000 --- a/test/unit-tests/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody-test.tsx +++ /dev/null @@ -1,79 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022, 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { render, RenderResult } from "jest-matrix-react"; -import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; - -import { - VoiceBroadcastInfoEventType, - VoiceBroadcastInfoState, - VoiceBroadcastRecording, - VoiceBroadcastRecordingBody, -} from "../../../../../src/voice-broadcast"; -import { mkEvent, stubClient } from "../../../../test-utils"; - -// mock RoomAvatar, because it is doing too much fancy stuff -jest.mock("../../../../../src/components/views/avatars/RoomAvatar", () => ({ - __esModule: true, - default: jest.fn().mockImplementation(({ room }) => { - return <div data-testid="room-avatar">room avatar: {room.name}</div>; - }), -})); - -describe("VoiceBroadcastRecordingBody", () => { - const userId = "@user:example.com"; - const roomId = "!room:example.com"; - let client: MatrixClient; - let infoEvent: MatrixEvent; - let recording: VoiceBroadcastRecording; - - beforeAll(() => { - client = stubClient(); - infoEvent = mkEvent({ - event: true, - type: VoiceBroadcastInfoEventType, - content: {}, - room: roomId, - user: userId, - }); - recording = new VoiceBroadcastRecording(infoEvent, client, VoiceBroadcastInfoState.Resumed); - }); - - describe("when rendering a live broadcast", () => { - let renderResult: RenderResult; - - beforeEach(() => { - renderResult = render(<VoiceBroadcastRecordingBody recording={recording} />); - }); - - it("should render with a red live badge", () => { - expect(renderResult.container).toMatchSnapshot(); - }); - }); - - describe("when rendering a paused broadcast", () => { - let renderResult: RenderResult; - - beforeEach(async () => { - await recording.pause(); - renderResult = render(<VoiceBroadcastRecordingBody recording={recording} />); - }); - - it("should render with a grey live badge", () => { - expect(renderResult.container).toMatchSnapshot(); - }); - }); - - it("when there is a broadcast without sender, it should raise an error", () => { - infoEvent.sender = null; - expect(() => { - render(<VoiceBroadcastRecordingBody recording={recording} />); - }).toThrow(`Voice Broadcast sender not found (event ${recording.infoEvent.getId()})`); - }); -}); diff --git a/test/unit-tests/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip-test.tsx b/test/unit-tests/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip-test.tsx deleted file mode 100644 index eafa0d0af6..0000000000 --- a/test/unit-tests/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip-test.tsx +++ /dev/null @@ -1,217 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ -// - -import React from "react"; -import { render, RenderResult, screen } from "jest-matrix-react"; -import userEvent from "@testing-library/user-event"; -import { ClientEvent, MatrixClient, MatrixEvent, SyncState } from "matrix-js-sdk/src/matrix"; -import { sleep } from "matrix-js-sdk/src/utils"; -import { mocked } from "jest-mock"; - -import { - VoiceBroadcastInfoState, - VoiceBroadcastRecording, - VoiceBroadcastRecordingPip, -} from "../../../../../src/voice-broadcast"; -import { flushPromises, stubClient } from "../../../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "../../utils/test-utils"; -import { requestMediaPermissions } from "../../../../../src/utils/media/requestMediaPermissions"; -import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../../../src/MediaDeviceHandler"; -import dis from "../../../../../src/dispatcher/dispatcher"; -import { Action } from "../../../../../src/dispatcher/actions"; - -jest.mock("../../../../../src/dispatcher/dispatcher"); -jest.mock("../../../../../src/utils/media/requestMediaPermissions"); - -// mock RoomAvatar, because it is doing too much fancy stuff -jest.mock("../../../../../src/components/views/avatars/RoomAvatar", () => ({ - __esModule: true, - default: jest.fn().mockImplementation(({ room }) => { - return <div data-testid="room-avatar">room avatar: {room.name}</div>; - }), -})); - -// mock VoiceRecording because it contains all the audio APIs -jest.mock("../../../../../src/audio/VoiceRecording", () => ({ - VoiceRecording: jest.fn().mockReturnValue({ - disableMaxLength: jest.fn(), - liveData: { - onUpdate: jest.fn(), - }, - start: jest.fn(), - }), -})); - -describe("VoiceBroadcastRecordingPip", () => { - const roomId = "!room:example.com"; - let client: MatrixClient; - let infoEvent: MatrixEvent; - let recording: VoiceBroadcastRecording; - let renderResult: RenderResult; - - const renderPip = async (state: VoiceBroadcastInfoState) => { - infoEvent = mkVoiceBroadcastInfoStateEvent(roomId, state, client.getUserId() || "", client.getDeviceId() || ""); - recording = new VoiceBroadcastRecording(infoEvent, client, state); - jest.spyOn(recording, "pause"); - jest.spyOn(recording, "resume"); - renderResult = render(<VoiceBroadcastRecordingPip recording={recording} />); - await flushPromises(); - }; - - const itShouldShowTheBroadcastRoom = () => { - it("should show the broadcast room", () => { - expect(dis.dispatch).toHaveBeenCalledWith({ - action: Action.ViewRoom, - room_id: roomId, - metricsTrigger: undefined, - }); - }); - }; - - beforeAll(() => { - client = stubClient(); - mocked(requestMediaPermissions).mockResolvedValue({ - getTracks: (): Array<MediaStreamTrack> => [], - } as unknown as MediaStream); - jest.spyOn(MediaDeviceHandler, "getDevices").mockResolvedValue({ - [MediaDeviceKindEnum.AudioInput]: [ - { - deviceId: "d1", - label: "Device 1", - } as MediaDeviceInfo, - { - deviceId: "d2", - label: "Device 2", - } as MediaDeviceInfo, - ], - [MediaDeviceKindEnum.AudioOutput]: [], - [MediaDeviceKindEnum.VideoInput]: [], - }); - jest.spyOn(MediaDeviceHandler.instance, "setDevice").mockImplementation(); - }); - - describe("when rendering a started recording", () => { - beforeEach(async () => { - await renderPip(VoiceBroadcastInfoState.Started); - }); - - it("should render as expected", () => { - expect(renderResult.container).toMatchSnapshot(); - }); - - describe("and selecting another input device", () => { - beforeEach(async () => { - await userEvent.click(screen.getByLabelText("Change input device")); - await userEvent.click(screen.getByText("Device 1")); - }); - - it("should select the device and pause and resume the broadcast", () => { - expect(MediaDeviceHandler.instance.setDevice).toHaveBeenCalledWith( - "d1", - MediaDeviceKindEnum.AudioInput, - ); - expect(recording.pause).toHaveBeenCalled(); - expect(recording.resume).toHaveBeenCalled(); - }); - }); - - describe("and clicking the room name", () => { - beforeEach(async () => { - await userEvent.click(screen.getByText("My room")); - }); - - itShouldShowTheBroadcastRoom(); - }); - - describe("and clicking the room avatar", () => { - beforeEach(async () => { - await userEvent.click(screen.getByText("room avatar: My room")); - }); - - itShouldShowTheBroadcastRoom(); - }); - - describe("and clicking the pause button", () => { - beforeEach(async () => { - await userEvent.click(screen.getByLabelText("pause voice broadcast")); - }); - - it("should pause the recording", () => { - expect(recording.getState()).toBe(VoiceBroadcastInfoState.Paused); - }); - }); - - describe("and clicking the stop button", () => { - beforeEach(async () => { - await userEvent.click(screen.getByLabelText("Stop Recording")); - await screen.findByText("Stop live broadcasting?"); - // modal rendering has some weird sleeps - await sleep(200); - }); - - it("should display the confirm end dialog", () => { - screen.getByText("Stop live broadcasting?"); - }); - - describe("and confirming the dialog", () => { - beforeEach(async () => { - await userEvent.click(screen.getByText("Yes, stop broadcast")); - }); - - it("should end the recording", () => { - expect(recording.getState()).toBe(VoiceBroadcastInfoState.Stopped); - }); - }); - }); - - describe("and there is no connection and clicking the pause button", () => { - beforeEach(async () => { - mocked(client.sendStateEvent).mockImplementation(() => { - throw new Error(); - }); - await userEvent.click(screen.getByLabelText("pause voice broadcast")); - }); - - it("should show a connection error info", () => { - expect(screen.getByText("Connection error - Recording paused")).toBeInTheDocument(); - }); - - describe("and the connection is back", () => { - beforeEach(() => { - mocked(client.sendStateEvent).mockResolvedValue({ event_id: "e1" }); - client.emit(ClientEvent.Sync, SyncState.Catchup, SyncState.Error); - }); - - it("should render a paused recording", async () => { - await expect(screen.findByLabelText("resume voice broadcast")).resolves.toBeInTheDocument(); - }); - }); - }); - }); - - describe("when rendering a paused recording", () => { - beforeEach(async () => { - await renderPip(VoiceBroadcastInfoState.Paused); - }); - - it("should render as expected", () => { - expect(renderResult.container).toMatchSnapshot(); - }); - - describe("and clicking the resume button", () => { - beforeEach(async () => { - await userEvent.click(screen.getByLabelText("resume voice broadcast")); - }); - - it("should resume the recording", () => { - expect(recording.getState()).toBe(VoiceBroadcastInfoState.Resumed); - }); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/components/molecules/VoiceBroadcastSmallPlaybackBody-test.tsx b/test/unit-tests/voice-broadcast/components/molecules/VoiceBroadcastSmallPlaybackBody-test.tsx deleted file mode 100644 index b65bbcf583..0000000000 --- a/test/unit-tests/voice-broadcast/components/molecules/VoiceBroadcastSmallPlaybackBody-test.tsx +++ /dev/null @@ -1,129 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; -import { render, RenderResult } from "jest-matrix-react"; -import userEvent from "@testing-library/user-event"; -import { mocked } from "jest-mock"; - -import { - VoiceBroadcastInfoState, - VoiceBroadcastLiveness, - VoiceBroadcastPlayback, - VoiceBroadcastSmallPlaybackBody, - VoiceBroadcastPlaybackState, -} from "../../../../../src/voice-broadcast"; -import { stubClient } from "../../../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "../../utils/test-utils"; -import { SdkContextClass } from "../../../../../src/contexts/SDKContext"; - -// mock RoomAvatar, because it is doing too much fancy stuff -jest.mock("../../../../../src/components/views/avatars/RoomAvatar", () => ({ - __esModule: true, - default: jest.fn().mockImplementation(({ room }) => { - return <div data-testid="room-avatar">room avatar: {room.name}</div>; - }), -})); - -describe("<VoiceBroadcastSmallPlaybackBody />", () => { - const userId = "@user:example.com"; - const roomId = "!room:example.com"; - let client: MatrixClient; - let infoEvent: MatrixEvent; - let playback: VoiceBroadcastPlayback; - let renderResult: RenderResult; - - beforeAll(() => { - client = stubClient(); - mocked(client.relations).mockClear(); - mocked(client.relations).mockResolvedValue({ events: [] }); - - infoEvent = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Stopped, - userId, - client.getDeviceId()!, - ); - }); - - beforeEach(() => { - playback = new VoiceBroadcastPlayback( - infoEvent, - client, - SdkContextClass.instance.voiceBroadcastRecordingsStore, - ); - jest.spyOn(playback, "toggle").mockImplementation(() => Promise.resolve()); - jest.spyOn(playback, "getLiveness"); - jest.spyOn(playback, "getState"); - }); - - describe("when rendering a buffering broadcast", () => { - beforeEach(() => { - mocked(playback.getState).mockReturnValue(VoiceBroadcastPlaybackState.Buffering); - mocked(playback.getLiveness).mockReturnValue("live"); - renderResult = render(<VoiceBroadcastSmallPlaybackBody playback={playback} />); - }); - - it("should render as expected", () => { - expect(renderResult.container).toMatchSnapshot(); - }); - }); - - describe("when rendering a playing broadcast", () => { - beforeEach(() => { - mocked(playback.getState).mockReturnValue(VoiceBroadcastPlaybackState.Playing); - mocked(playback.getLiveness).mockReturnValue("not-live"); - renderResult = render(<VoiceBroadcastSmallPlaybackBody playback={playback} />); - }); - - it("should render as expected", () => { - expect(renderResult.container).toMatchSnapshot(); - }); - }); - - describe(`when rendering a stopped broadcast`, () => { - beforeEach(() => { - mocked(playback.getState).mockReturnValue(VoiceBroadcastPlaybackState.Stopped); - mocked(playback.getLiveness).mockReturnValue("not-live"); - renderResult = render(<VoiceBroadcastSmallPlaybackBody playback={playback} />); - }); - - it("should render as expected", () => { - expect(renderResult.container).toMatchSnapshot(); - }); - - describe("and clicking the play button", () => { - beforeEach(async () => { - await userEvent.click(renderResult.getByLabelText("play voice broadcast")); - }); - - it("should toggle the playback", () => { - expect(playback.toggle).toHaveBeenCalled(); - }); - }); - }); - - describe.each([ - { state: VoiceBroadcastPlaybackState.Paused, liveness: "not-live" }, - { state: VoiceBroadcastPlaybackState.Playing, liveness: "live" }, - ] as Array<{ state: VoiceBroadcastPlaybackState; liveness: VoiceBroadcastLiveness }>)( - "when rendering a %s/%s broadcast", - ({ state, liveness }) => { - beforeEach(() => { - mocked(playback.getState).mockReturnValue(state); - mocked(playback.getLiveness).mockReturnValue(liveness); - renderResult = render(<VoiceBroadcastSmallPlaybackBody playback={playback} />); - }); - - it("should render as expected", () => { - expect(renderResult.container).toMatchSnapshot(); - }); - }, - ); -}); diff --git a/test/unit-tests/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastPlaybackBody-test.tsx.snap b/test/unit-tests/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastPlaybackBody-test.tsx.snap deleted file mode 100644 index cb063c395c..0000000000 --- a/test/unit-tests/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastPlaybackBody-test.tsx.snap +++ /dev/null @@ -1,914 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`VoiceBroadcastPlaybackBody when rendering a buffering voice broadcast should render as expected 1`] = ` -<div> - <div - class="mx_VoiceBroadcastBody" - > - <div - class="mx_VoiceBroadcastHeader" - > - <div - data-testid="room-avatar" - > - room avatar: - My room - </div> - <div - class="mx_VoiceBroadcastHeader_content" - > - <div - class="mx_VoiceBroadcastHeader_room_wrapper" - > - <div - class="mx_VoiceBroadcastHeader_room" - > - My room - </div> - </div> - <div - aria-label="Change input device" - class="mx_AccessibleButton mx_VoiceBroadcastHeader_line" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_16" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z" - /> - <path - d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z" - /> - </svg> - <span> - @user:example.com - </span> - </div> - <div - class="mx_VoiceBroadcastHeader_line" - > - <div - class="mx_Spinner" - > - <div - aria-label="Loading…" - class="mx_Spinner_icon" - data-testid="spinner" - role="progressbar" - style="width: 14px; height: 14px;" - /> - </div> - Buffering… - </div> - </div> - <div - class="mx_LiveBadge" - > - <div - class="mx_Icon mx_Icon_16" - /> - Live - </div> - </div> - <div - class="mx_VoiceBroadcastBody_controls" - > - <div - aria-label="30s backward" - class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary_content" - role="button" - tabindex="0" - > - <div - class="mx_Icon mx_Icon_24" - /> - </div> - <div - aria-label="pause voice broadcast" - class="mx_AccessibleButton mx_VoiceBroadcastControl" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_12" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M8 4a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Zm8 0a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Z" - /> - </svg> - </div> - <div - aria-label="30s forward" - class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary_content" - role="button" - tabindex="0" - > - <div - class="mx_Icon mx_Icon_24" - /> - </div> - </div> - <input - aria-label="Audio seek bar" - class="mx_SeekBar" - max="1" - min="0" - step="0.001" - style="--fillTo: 0;" - tabindex="0" - type="range" - value="0" - /> - <div - class="mx_VoiceBroadcastBody_timerow" - > - <time - class="mx_Clock" - datetime="PT0S" - > - 00:00 - </time> - <time - class="mx_Clock" - datetime="-PT23M42S" - > - -23:42 - </time> - </div> - </div> -</div> -`; - -exports[`VoiceBroadcastPlaybackBody when rendering a pause/not-live broadcast should render as expected 1`] = ` -<div> - <div - class="mx_VoiceBroadcastBody" - > - <div - class="mx_VoiceBroadcastHeader" - > - <div - data-testid="room-avatar" - > - room avatar: - My room - </div> - <div - class="mx_VoiceBroadcastHeader_content" - > - <div - class="mx_VoiceBroadcastHeader_room_wrapper" - > - <div - class="mx_VoiceBroadcastHeader_room" - > - My room - </div> - </div> - <div - aria-label="Change input device" - class="mx_AccessibleButton mx_VoiceBroadcastHeader_line" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_16" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z" - /> - <path - d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z" - /> - </svg> - <span> - @user:example.com - </span> - </div> - <div - class="mx_VoiceBroadcastHeader_line" - > - <div - class="mx_Icon mx_Icon_16" - /> - Voice broadcast - </div> - </div> - </div> - <div - class="mx_VoiceBroadcastBody_controls" - > - <div - aria-label="30s backward" - class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary_content" - role="button" - tabindex="0" - > - <div - class="mx_Icon mx_Icon_24" - /> - </div> - <div - aria-label="resume voice broadcast" - class="mx_AccessibleButton mx_VoiceBroadcastControl mx_VoiceBroadcastControl-play" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_16" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="m8.98 4.677 9.921 5.58c1.36.764 1.36 2.722 0 3.486l-9.92 5.58C7.647 20.073 6 19.11 6 17.58V6.42c0-1.53 1.647-2.493 2.98-1.743Z" - /> - </svg> - </div> - <div - aria-label="30s forward" - class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary_content" - role="button" - tabindex="0" - > - <div - class="mx_Icon mx_Icon_24" - /> - </div> - </div> - <input - aria-label="Audio seek bar" - class="mx_SeekBar" - max="1" - min="0" - step="0.001" - style="--fillTo: 0;" - tabindex="0" - type="range" - value="0" - /> - <div - class="mx_VoiceBroadcastBody_timerow" - > - <time - class="mx_Clock" - datetime="PT0S" - > - 00:00 - </time> - <time - class="mx_Clock" - datetime="-PT23M42S" - > - -23:42 - </time> - </div> - </div> -</div> -`; - -exports[`VoiceBroadcastPlaybackBody when rendering a playing broadcast in pip mode should render as expected 1`] = ` -<div> - <div - class="mx_VoiceBroadcastBody mx_VoiceBroadcastBody--pip" - > - <div - class="mx_VoiceBroadcastHeader" - > - <div - class="mx_AccessibleButton" - role="button" - tabindex="0" - > - <div - data-testid="room-avatar" - > - room avatar: - My room - </div> - </div> - <div - class="mx_VoiceBroadcastHeader_content" - > - <div - class="mx_AccessibleButton" - role="button" - tabindex="0" - > - <div - class="mx_VoiceBroadcastHeader_room_wrapper" - > - <div - class="mx_VoiceBroadcastHeader_room" - > - My room - </div> - </div> - </div> - <div - aria-label="Change input device" - class="mx_AccessibleButton mx_VoiceBroadcastHeader_line" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_16" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z" - /> - <path - d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z" - /> - </svg> - <span> - @user:example.com - </span> - </div> - <div - class="mx_VoiceBroadcastHeader_line" - > - <div - class="mx_Icon mx_Icon_16" - /> - Voice broadcast - </div> - </div> - </div> - <div - class="mx_VoiceBroadcastBody_controls" - > - <div - aria-label="30s backward" - class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary_content" - role="button" - tabindex="0" - > - <div - class="mx_Icon mx_Icon_24" - /> - </div> - <div - aria-label="pause voice broadcast" - class="mx_AccessibleButton mx_VoiceBroadcastControl" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_12" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M8 4a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Zm8 0a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Z" - /> - </svg> - </div> - <div - aria-label="30s forward" - class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary_content" - role="button" - tabindex="0" - > - <div - class="mx_Icon mx_Icon_24" - /> - </div> - </div> - <input - aria-label="Audio seek bar" - class="mx_SeekBar" - max="1" - min="0" - step="0.001" - style="--fillTo: 0;" - tabindex="0" - type="range" - value="0" - /> - <div - class="mx_VoiceBroadcastBody_timerow" - > - <time - class="mx_Clock" - datetime="PT0S" - > - 00:00 - </time> - <time - class="mx_Clock" - datetime="-PT23M42S" - > - -23:42 - </time> - </div> - </div> -</div> -`; - -exports[`VoiceBroadcastPlaybackBody when rendering a playing broadcast should render as expected 1`] = ` -<div> - <div - class="mx_VoiceBroadcastBody" - > - <div - class="mx_VoiceBroadcastHeader" - > - <div - data-testid="room-avatar" - > - room avatar: - My room - </div> - <div - class="mx_VoiceBroadcastHeader_content" - > - <div - class="mx_VoiceBroadcastHeader_room_wrapper" - > - <div - class="mx_VoiceBroadcastHeader_room" - > - My room - </div> - </div> - <div - aria-label="Change input device" - class="mx_AccessibleButton mx_VoiceBroadcastHeader_line" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_16" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z" - /> - <path - d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z" - /> - </svg> - <span> - @user:example.com - </span> - </div> - <div - class="mx_VoiceBroadcastHeader_line" - > - <div - class="mx_Icon mx_Icon_16" - /> - Voice broadcast - </div> - </div> - </div> - <div - class="mx_VoiceBroadcastBody_controls" - > - <div - aria-label="30s backward" - class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary_content" - role="button" - tabindex="0" - > - <div - class="mx_Icon mx_Icon_24" - /> - </div> - <div - aria-label="pause voice broadcast" - class="mx_AccessibleButton mx_VoiceBroadcastControl" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_12" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M8 4a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Zm8 0a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Z" - /> - </svg> - </div> - <div - aria-label="30s forward" - class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary_content" - role="button" - tabindex="0" - > - <div - class="mx_Icon mx_Icon_24" - /> - </div> - </div> - <input - aria-label="Audio seek bar" - class="mx_SeekBar" - max="1" - min="0" - step="0.001" - style="--fillTo: 0;" - tabindex="0" - type="range" - value="0" - /> - <div - class="mx_VoiceBroadcastBody_timerow" - > - <time - class="mx_Clock" - datetime="PT0S" - > - 00:00 - </time> - <time - class="mx_Clock" - datetime="-PT23M42S" - > - -23:42 - </time> - </div> - </div> -</div> -`; - -exports[`VoiceBroadcastPlaybackBody when rendering a playing/live broadcast should render as expected 1`] = ` -<div> - <div - class="mx_VoiceBroadcastBody" - > - <div - class="mx_VoiceBroadcastHeader" - > - <div - data-testid="room-avatar" - > - room avatar: - My room - </div> - <div - class="mx_VoiceBroadcastHeader_content" - > - <div - class="mx_VoiceBroadcastHeader_room_wrapper" - > - <div - class="mx_VoiceBroadcastHeader_room" - > - My room - </div> - </div> - <div - aria-label="Change input device" - class="mx_AccessibleButton mx_VoiceBroadcastHeader_line" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_16" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z" - /> - <path - d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z" - /> - </svg> - <span> - @user:example.com - </span> - </div> - <div - class="mx_VoiceBroadcastHeader_line" - > - <div - class="mx_Icon mx_Icon_16" - /> - Voice broadcast - </div> - </div> - <div - class="mx_LiveBadge" - > - <div - class="mx_Icon mx_Icon_16" - /> - Live - </div> - </div> - <div - class="mx_VoiceBroadcastBody_controls" - > - <div - aria-label="30s backward" - class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary_content" - role="button" - tabindex="0" - > - <div - class="mx_Icon mx_Icon_24" - /> - </div> - <div - aria-label="pause voice broadcast" - class="mx_AccessibleButton mx_VoiceBroadcastControl" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_12" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M8 4a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Zm8 0a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Z" - /> - </svg> - </div> - <div - aria-label="30s forward" - class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary_content" - role="button" - tabindex="0" - > - <div - class="mx_Icon mx_Icon_24" - /> - </div> - </div> - <input - aria-label="Audio seek bar" - class="mx_SeekBar" - max="1" - min="0" - step="0.001" - style="--fillTo: 0;" - tabindex="0" - type="range" - value="0" - /> - <div - class="mx_VoiceBroadcastBody_timerow" - > - <time - class="mx_Clock" - datetime="PT0S" - > - 00:00 - </time> - <time - class="mx_Clock" - datetime="-PT23M42S" - > - -23:42 - </time> - </div> - </div> -</div> -`; - -exports[`VoiceBroadcastPlaybackBody when rendering a stopped broadcast should render as expected 1`] = ` -<div> - <div - class="mx_VoiceBroadcastBody" - > - <div - class="mx_VoiceBroadcastHeader" - > - <div - data-testid="room-avatar" - > - room avatar: - My room - </div> - <div - class="mx_VoiceBroadcastHeader_content" - > - <div - class="mx_VoiceBroadcastHeader_room_wrapper" - > - <div - class="mx_VoiceBroadcastHeader_room" - > - My room - </div> - </div> - <div - aria-label="Change input device" - class="mx_AccessibleButton mx_VoiceBroadcastHeader_line" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_16" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z" - /> - <path - d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z" - /> - </svg> - <span> - @user:example.com - </span> - </div> - <div - class="mx_VoiceBroadcastHeader_line" - > - <div - class="mx_Icon mx_Icon_16" - /> - Voice broadcast - </div> - </div> - </div> - <div - class="mx_VoiceBroadcastBody_controls" - > - <div - aria-label="play voice broadcast" - class="mx_AccessibleButton mx_VoiceBroadcastControl mx_VoiceBroadcastControl-play" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_16" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="m8.98 4.677 9.921 5.58c1.36.764 1.36 2.722 0 3.486l-9.92 5.58C7.647 20.073 6 19.11 6 17.58V6.42c0-1.53 1.647-2.493 2.98-1.743Z" - /> - </svg> - </div> - </div> - <input - aria-label="Audio seek bar" - class="mx_SeekBar" - max="1" - min="0" - step="0.001" - style="--fillTo: 0;" - tabindex="0" - type="range" - value="0" - /> - <div - class="mx_VoiceBroadcastBody_timerow" - > - <time - class="mx_Clock" - datetime="PT0S" - > - 00:00 - </time> - <time - class="mx_Clock" - datetime="-PT23M42S" - > - -23:42 - </time> - </div> - </div> -</div> -`; - -exports[`VoiceBroadcastPlaybackBody when rendering an error broadcast should render as expected 1`] = ` -<div> - <div - class="mx_VoiceBroadcastBody" - > - <div - class="mx_VoiceBroadcastHeader" - > - <div - data-testid="room-avatar" - > - room avatar: - My room - </div> - <div - class="mx_VoiceBroadcastHeader_content" - > - <div - class="mx_VoiceBroadcastHeader_room_wrapper" - > - <div - class="mx_VoiceBroadcastHeader_room" - > - My room - </div> - </div> - <div - aria-label="Change input device" - class="mx_AccessibleButton mx_VoiceBroadcastHeader_line" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_16" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z" - /> - <path - d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z" - /> - </svg> - <span> - @user:example.com - </span> - </div> - <div - class="mx_VoiceBroadcastHeader_line" - > - <div - class="mx_Icon mx_Icon_16" - /> - Voice broadcast - </div> - </div> - </div> - <div - class="mx_VoiceBroadcastRecordingConnectionError" - > - <svg - class="mx_Icon mx_Icon_16" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M12.713 17.713A.968.968 0 0 1 12 18a.968.968 0 0 1-.713-.287A.967.967 0 0 1 11 17a.97.97 0 0 1 .287-.712A.968.968 0 0 1 12 16a.97.97 0 0 1 .713.288A.968.968 0 0 1 13 17a.97.97 0 0 1-.287.713Zm0-4A.968.968 0 0 1 12 14a.968.968 0 0 1-.713-.287A.967.967 0 0 1 11 13V9a.97.97 0 0 1 .287-.712A.968.968 0 0 1 12 8a.97.97 0 0 1 .713.288A.968.968 0 0 1 13 9v4a.97.97 0 0 1-.287.713Z" - /> - <path - clip-rule="evenodd" - d="M10.264 3.039c.767-1.344 2.705-1.344 3.472 0l8.554 14.969c.762 1.333-.2 2.992-1.736 2.992H3.446c-1.535 0-2.498-1.659-1.736-2.992l8.553-14.969ZM3.446 19 12 4.031l8.554 14.97H3.446Z" - fill-rule="evenodd" - /> - </svg> - Unable to play this voice broadcast - </div> - </div> -</div> -`; diff --git a/test/unit-tests/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastPreRecordingPip-test.tsx.snap b/test/unit-tests/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastPreRecordingPip-test.tsx.snap deleted file mode 100644 index f50cdc3be4..0000000000 --- a/test/unit-tests/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastPreRecordingPip-test.tsx.snap +++ /dev/null @@ -1,98 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`VoiceBroadcastPreRecordingPip when rendered should match the snapshot 1`] = ` -<div> - <div - class="mx_VoiceBroadcastBody mx_VoiceBroadcastBody--pip" - > - <div - class="mx_VoiceBroadcastHeader" - > - <div - class="mx_AccessibleButton" - role="button" - tabindex="0" - > - <div - data-testid="room-avatar" - > - room avatar: - !room@example.com - </div> - </div> - <div - class="mx_VoiceBroadcastHeader_content" - > - <div - class="mx_AccessibleButton" - role="button" - tabindex="0" - > - <div - class="mx_VoiceBroadcastHeader_room_wrapper" - > - <div - class="mx_VoiceBroadcastHeader_room" - > - !room@example.com - </div> - </div> - </div> - <div - aria-label="Change input device" - class="mx_AccessibleButton mx_VoiceBroadcastHeader_line mx_VoiceBroadcastHeader_mic--clickable" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_16" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z" - /> - <path - d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z" - /> - </svg> - <span> - Default Device - </span> - </div> - </div> - <div - class="mx_AccessibleButton" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_16" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414Z" - /> - </svg> - </div> - </div> - <div - class="mx_AccessibleButton mx_VoiceBroadcastBody_blockButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger" - role="button" - tabindex="0" - > - <div - class="mx_Icon mx_Icon_16" - /> - Go live - </div> - </div> -</div> -`; diff --git a/test/unit-tests/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastRecordingBody-test.tsx.snap b/test/unit-tests/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastRecordingBody-test.tsx.snap deleted file mode 100644 index c2e6fdcd54..0000000000 --- a/test/unit-tests/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastRecordingBody-test.tsx.snap +++ /dev/null @@ -1,131 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`VoiceBroadcastRecordingBody when rendering a live broadcast should render with a red live badge 1`] = ` -<div> - <div - class="mx_VoiceBroadcastBody" - > - <div - class="mx_VoiceBroadcastHeader" - > - <div - data-testid="room-avatar" - > - room avatar: - My room - </div> - <div - class="mx_VoiceBroadcastHeader_content" - > - <div - class="mx_VoiceBroadcastHeader_room_wrapper" - > - <div - class="mx_VoiceBroadcastHeader_room" - > - My room - </div> - </div> - <div - aria-label="Change input device" - class="mx_AccessibleButton mx_VoiceBroadcastHeader_line" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_16" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z" - /> - <path - d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z" - /> - </svg> - <span> - @user:example.com - </span> - </div> - </div> - <div - class="mx_LiveBadge" - > - <div - class="mx_Icon mx_Icon_16" - /> - Live - </div> - </div> - </div> -</div> -`; - -exports[`VoiceBroadcastRecordingBody when rendering a paused broadcast should render with a grey live badge 1`] = ` -<div> - <div - class="mx_VoiceBroadcastBody" - > - <div - class="mx_VoiceBroadcastHeader" - > - <div - data-testid="room-avatar" - > - room avatar: - My room - </div> - <div - class="mx_VoiceBroadcastHeader_content" - > - <div - class="mx_VoiceBroadcastHeader_room_wrapper" - > - <div - class="mx_VoiceBroadcastHeader_room" - > - My room - </div> - </div> - <div - aria-label="Change input device" - class="mx_AccessibleButton mx_VoiceBroadcastHeader_line" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_16" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z" - /> - <path - d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z" - /> - </svg> - <span> - @user:example.com - </span> - </div> - </div> - <div - class="mx_LiveBadge mx_LiveBadge--grey" - > - <div - class="mx_Icon mx_Icon_16" - /> - Live - </div> - </div> - </div> -</div> -`; diff --git a/test/unit-tests/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastRecordingPip-test.tsx.snap b/test/unit-tests/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastRecordingPip-test.tsx.snap deleted file mode 100644 index 2fc2334575..0000000000 --- a/test/unit-tests/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastRecordingPip-test.tsx.snap +++ /dev/null @@ -1,238 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`VoiceBroadcastRecordingPip when rendering a paused recording should render as expected 1`] = ` -<div> - <div - class="mx_VoiceBroadcastBody mx_VoiceBroadcastBody--pip" - > - <div - class="mx_VoiceBroadcastHeader" - > - <div - class="mx_AccessibleButton" - role="button" - tabindex="0" - > - <div - data-testid="room-avatar" - > - room avatar: - My room - </div> - </div> - <div - class="mx_VoiceBroadcastHeader_content" - > - <div - class="mx_AccessibleButton" - role="button" - tabindex="0" - > - <div - class="mx_VoiceBroadcastHeader_room_wrapper" - > - <div - class="mx_VoiceBroadcastHeader_room" - > - My room - </div> - </div> - </div> - <div - class="mx_VoiceBroadcastHeader_line" - > - <div - class="mx_Icon mx_Icon_16" - /> - <time - class="mx_Clock" - datetime="PT4H" - > - 4h 0m 0s left - </time> - </div> - </div> - <div - class="mx_LiveBadge mx_LiveBadge--grey" - > - <div - class="mx_Icon mx_Icon_16" - /> - Live - </div> - </div> - <hr - class="mx_VoiceBroadcastBody_divider" - /> - <div - class="mx_VoiceBroadcastBody_controls" - > - <div - aria-label="resume voice broadcast" - class="mx_AccessibleButton mx_VoiceBroadcastControl mx_VoiceBroadcastControl-recording" - role="button" - tabindex="0" - > - <div - class="mx_Icon mx_Icon_12" - /> - </div> - <div - aria-label="Change input device" - class="mx_AccessibleButton" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_16 mx_Icon_alert" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z" - /> - <path - d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z" - /> - </svg> - </div> - <div - aria-label="Stop Recording" - class="mx_AccessibleButton mx_VoiceBroadcastControl" - role="button" - tabindex="0" - > - <div - class="mx_Icon mx_Icon_16" - /> - </div> - </div> - </div> -</div> -`; - -exports[`VoiceBroadcastRecordingPip when rendering a started recording should render as expected 1`] = ` -<div> - <div - class="mx_VoiceBroadcastBody mx_VoiceBroadcastBody--pip" - > - <div - class="mx_VoiceBroadcastHeader" - > - <div - class="mx_AccessibleButton" - role="button" - tabindex="0" - > - <div - data-testid="room-avatar" - > - room avatar: - My room - </div> - </div> - <div - class="mx_VoiceBroadcastHeader_content" - > - <div - class="mx_AccessibleButton" - role="button" - tabindex="0" - > - <div - class="mx_VoiceBroadcastHeader_room_wrapper" - > - <div - class="mx_VoiceBroadcastHeader_room" - > - My room - </div> - </div> - </div> - <div - class="mx_VoiceBroadcastHeader_line" - > - <div - class="mx_Icon mx_Icon_16" - /> - <time - class="mx_Clock" - datetime="PT4H" - > - 4h 0m 0s left - </time> - </div> - </div> - <div - class="mx_LiveBadge" - > - <div - class="mx_Icon mx_Icon_16" - /> - Live - </div> - </div> - <hr - class="mx_VoiceBroadcastBody_divider" - /> - <div - class="mx_VoiceBroadcastBody_controls" - > - <div - aria-label="pause voice broadcast" - class="mx_AccessibleButton mx_VoiceBroadcastControl" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_12" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M8 4a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Zm8 0a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Z" - /> - </svg> - </div> - <div - aria-label="Change input device" - class="mx_AccessibleButton" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_16 mx_Icon_alert" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z" - /> - <path - d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z" - /> - </svg> - </div> - <div - aria-label="Stop Recording" - class="mx_AccessibleButton mx_VoiceBroadcastControl" - role="button" - tabindex="0" - > - <div - class="mx_Icon mx_Icon_16" - /> - </div> - </div> - </div> -</div> -`; diff --git a/test/unit-tests/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastSmallPlaybackBody-test.tsx.snap b/test/unit-tests/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastSmallPlaybackBody-test.tsx.snap deleted file mode 100644 index 088151158b..0000000000 --- a/test/unit-tests/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastSmallPlaybackBody-test.tsx.snap +++ /dev/null @@ -1,558 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`<VoiceBroadcastSmallPlaybackBody /> when rendering a { state: 'pause', liveness: 'not-live' }/%s broadcast should render as expected 1`] = ` -<div> - <div - class="mx_VoiceBroadcastBody mx_VoiceBroadcastBody--pip mx_VoiceBroadcastBody--small" - > - <div - class="mx_VoiceBroadcastHeader" - > - <div - class="mx_AccessibleButton" - role="button" - tabindex="0" - > - <div - data-testid="room-avatar" - > - room avatar: - My room - </div> - </div> - <div - class="mx_VoiceBroadcastHeader_content" - > - <div - class="mx_AccessibleButton" - role="button" - tabindex="0" - > - <div - class="mx_VoiceBroadcastHeader_room_wrapper" - > - <div - class="mx_VoiceBroadcastHeader_room" - > - My room - </div> - </div> - </div> - <div - aria-label="Change input device" - class="mx_AccessibleButton mx_VoiceBroadcastHeader_line" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_16" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z" - /> - <path - d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z" - /> - </svg> - <span> - @user:example.com - </span> - </div> - </div> - </div> - <div - aria-label="resume voice broadcast" - class="mx_AccessibleButton mx_VoiceBroadcastControl mx_VoiceBroadcastControl-play" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_16" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="m8.98 4.677 9.921 5.58c1.36.764 1.36 2.722 0 3.486l-9.92 5.58C7.647 20.073 6 19.11 6 17.58V6.42c0-1.53 1.647-2.493 2.98-1.743Z" - /> - </svg> - </div> - <div - class="mx_AccessibleButton" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_8 mx_VoiceBroadcastBody__small-close" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414Z" - /> - </svg> - </div> - </div> -</div> -`; - -exports[`<VoiceBroadcastSmallPlaybackBody /> when rendering a { state: 'playing', liveness: 'live' }/%s broadcast should render as expected 1`] = ` -<div> - <div - class="mx_VoiceBroadcastBody mx_VoiceBroadcastBody--pip mx_VoiceBroadcastBody--small" - > - <div - class="mx_VoiceBroadcastHeader" - > - <div - class="mx_AccessibleButton" - role="button" - tabindex="0" - > - <div - data-testid="room-avatar" - > - room avatar: - My room - </div> - </div> - <div - class="mx_VoiceBroadcastHeader_content" - > - <div - class="mx_AccessibleButton" - role="button" - tabindex="0" - > - <div - class="mx_VoiceBroadcastHeader_room_wrapper" - > - <div - class="mx_VoiceBroadcastHeader_room" - > - My room - </div> - </div> - </div> - <div - aria-label="Change input device" - class="mx_AccessibleButton mx_VoiceBroadcastHeader_line" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_16" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z" - /> - <path - d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z" - /> - </svg> - <span> - @user:example.com - </span> - </div> - <div - class="mx_LiveBadge" - > - <div - class="mx_Icon mx_Icon_16" - /> - Live - </div> - </div> - </div> - <div - aria-label="pause voice broadcast" - class="mx_AccessibleButton mx_VoiceBroadcastControl" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_12" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M8 4a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Zm8 0a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Z" - /> - </svg> - </div> - <div - class="mx_AccessibleButton" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_8 mx_VoiceBroadcastBody__small-close" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414Z" - /> - </svg> - </div> - </div> -</div> -`; - -exports[`<VoiceBroadcastSmallPlaybackBody /> when rendering a buffering broadcast should render as expected 1`] = ` -<div> - <div - class="mx_VoiceBroadcastBody mx_VoiceBroadcastBody--pip mx_VoiceBroadcastBody--small" - > - <div - class="mx_VoiceBroadcastHeader" - > - <div - class="mx_AccessibleButton" - role="button" - tabindex="0" - > - <div - data-testid="room-avatar" - > - room avatar: - My room - </div> - </div> - <div - class="mx_VoiceBroadcastHeader_content" - > - <div - class="mx_AccessibleButton" - role="button" - tabindex="0" - > - <div - class="mx_VoiceBroadcastHeader_room_wrapper" - > - <div - class="mx_VoiceBroadcastHeader_room" - > - My room - </div> - <div - class="mx_Spinner" - > - <div - aria-label="Loading…" - class="mx_Spinner_icon" - data-testid="spinner" - role="progressbar" - style="width: 12px; height: 12px;" - /> - </div> - </div> - </div> - <div - aria-label="Change input device" - class="mx_AccessibleButton mx_VoiceBroadcastHeader_line" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_16" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z" - /> - <path - d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z" - /> - </svg> - <span> - @user:example.com - </span> - </div> - <div - class="mx_LiveBadge" - > - <div - class="mx_Icon mx_Icon_16" - /> - Live - </div> - </div> - </div> - <div - aria-label="pause voice broadcast" - class="mx_AccessibleButton mx_VoiceBroadcastControl" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_12" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M8 4a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Zm8 0a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Z" - /> - </svg> - </div> - <div - class="mx_AccessibleButton" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_8 mx_VoiceBroadcastBody__small-close" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414Z" - /> - </svg> - </div> - </div> -</div> -`; - -exports[`<VoiceBroadcastSmallPlaybackBody /> when rendering a playing broadcast should render as expected 1`] = ` -<div> - <div - class="mx_VoiceBroadcastBody mx_VoiceBroadcastBody--pip mx_VoiceBroadcastBody--small" - > - <div - class="mx_VoiceBroadcastHeader" - > - <div - class="mx_AccessibleButton" - role="button" - tabindex="0" - > - <div - data-testid="room-avatar" - > - room avatar: - My room - </div> - </div> - <div - class="mx_VoiceBroadcastHeader_content" - > - <div - class="mx_AccessibleButton" - role="button" - tabindex="0" - > - <div - class="mx_VoiceBroadcastHeader_room_wrapper" - > - <div - class="mx_VoiceBroadcastHeader_room" - > - My room - </div> - </div> - </div> - <div - aria-label="Change input device" - class="mx_AccessibleButton mx_VoiceBroadcastHeader_line" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_16" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z" - /> - <path - d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z" - /> - </svg> - <span> - @user:example.com - </span> - </div> - </div> - </div> - <div - aria-label="pause voice broadcast" - class="mx_AccessibleButton mx_VoiceBroadcastControl" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_12" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M8 4a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Zm8 0a2 2 0 0 0-2 2v12a2 2 0 1 0 4 0V6a2 2 0 0 0-2-2Z" - /> - </svg> - </div> - <div - class="mx_AccessibleButton" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_8 mx_VoiceBroadcastBody__small-close" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414Z" - /> - </svg> - </div> - </div> -</div> -`; - -exports[`<VoiceBroadcastSmallPlaybackBody /> when rendering a stopped broadcast should render as expected 1`] = ` -<div> - <div - class="mx_VoiceBroadcastBody mx_VoiceBroadcastBody--pip mx_VoiceBroadcastBody--small" - > - <div - class="mx_VoiceBroadcastHeader" - > - <div - class="mx_AccessibleButton" - role="button" - tabindex="0" - > - <div - data-testid="room-avatar" - > - room avatar: - My room - </div> - </div> - <div - class="mx_VoiceBroadcastHeader_content" - > - <div - class="mx_AccessibleButton" - role="button" - tabindex="0" - > - <div - class="mx_VoiceBroadcastHeader_room_wrapper" - > - <div - class="mx_VoiceBroadcastHeader_room" - > - My room - </div> - </div> - </div> - <div - aria-label="Change input device" - class="mx_AccessibleButton mx_VoiceBroadcastHeader_line" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_16" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M8 6a4 4 0 1 1 8 0v6a4 4 0 0 1-8 0V6Z" - /> - <path - d="M5 11a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8.001 8.001 0 0 1-7 7.938V21a1 1 0 1 1-2 0v-1.062A8.001 8.001 0 0 1 4 12a1 1 0 0 1 1-1Z" - /> - </svg> - <span> - @user:example.com - </span> - </div> - </div> - </div> - <div - aria-label="play voice broadcast" - class="mx_AccessibleButton mx_VoiceBroadcastControl mx_VoiceBroadcastControl-play" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_16" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="m8.98 4.677 9.921 5.58c1.36.764 1.36 2.722 0 3.486l-9.92 5.58C7.647 20.073 6 19.11 6 17.58V6.42c0-1.53 1.647-2.493 2.98-1.743Z" - /> - </svg> - </div> - <div - class="mx_AccessibleButton" - role="button" - tabindex="0" - > - <svg - class="mx_Icon mx_Icon_8 mx_VoiceBroadcastBody__small-close" - fill="currentColor" - height="1em" - viewBox="0 0 24 24" - width="1em" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414Z" - /> - </svg> - </div> - </div> -</div> -`; diff --git a/test/unit-tests/voice-broadcast/models/VoiceBroadcastPlayback-test.tsx b/test/unit-tests/voice-broadcast/models/VoiceBroadcastPlayback-test.tsx deleted file mode 100644 index c967189271..0000000000 --- a/test/unit-tests/voice-broadcast/models/VoiceBroadcastPlayback-test.tsx +++ /dev/null @@ -1,747 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { mocked } from "jest-mock"; -import { screen } from "jest-matrix-react"; -import userEvent from "@testing-library/user-event"; -import { MatrixClient, MatrixEvent, MatrixEventEvent, Room } from "matrix-js-sdk/src/matrix"; -import { defer } from "matrix-js-sdk/src/utils"; - -import { Playback, PlaybackState } from "../../../../src/audio/Playback"; -import { PlaybackManager } from "../../../../src/audio/PlaybackManager"; -import { SdkContextClass } from "../../../../src/contexts/SDKContext"; -import { MediaEventHelper } from "../../../../src/utils/MediaEventHelper"; -import { - VoiceBroadcastInfoState, - VoiceBroadcastLiveness, - VoiceBroadcastPlayback, - VoiceBroadcastPlaybackEvent, - VoiceBroadcastPlaybackState, - VoiceBroadcastRecording, -} from "../../../../src/voice-broadcast"; -import { - filterConsole, - flushPromises, - flushPromisesWithFakeTimers, - stubClient, - waitEnoughCyclesForModal, -} from "../../../test-utils"; -import { createTestPlayback } from "../../../test-utils/audio"; -import { mkVoiceBroadcastChunkEvent, mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils"; -import { LazyValue } from "../../../../src/utils/LazyValue"; - -jest.mock("../../../../src/utils/MediaEventHelper", () => ({ - MediaEventHelper: jest.fn(), -})); - -describe("VoiceBroadcastPlayback", () => { - const userId = "@user:example.com"; - let deviceId: string; - const roomId = "!room:example.com"; - let room: Room; - let client: MatrixClient; - let infoEvent: MatrixEvent; - let playback: VoiceBroadcastPlayback; - let onStateChanged: (state: VoiceBroadcastPlaybackState) => void; - let chunk1Event: MatrixEvent; - let deplayedChunk1Event: MatrixEvent; - let chunk2Event: MatrixEvent; - let chunk2BEvent: MatrixEvent; - let chunk3Event: MatrixEvent; - const chunk1Length = 2300; - const chunk2Length = 4200; - const chunk3Length = 6900; - const chunk1Data = new ArrayBuffer(2); - const chunk2Data = new ArrayBuffer(3); - const chunk3Data = new ArrayBuffer(3); - let delayedChunk1Helper: MediaEventHelper; - let chunk1Helper: MediaEventHelper; - let chunk2Helper: MediaEventHelper; - let chunk3Helper: MediaEventHelper; - let chunk1Playback: Playback; - let chunk2Playback: Playback; - let chunk3Playback: Playback; - let middleOfSecondChunk!: number; - let middleOfThirdChunk!: number; - - const queryConfirmListeningDialog = () => { - return screen.queryByText( - "If you start listening to this live broadcast, your current live broadcast recording will be ended.", - ); - }; - - const itShouldSetTheStateTo = (state: VoiceBroadcastPlaybackState) => { - it(`should set the state to ${state}`, () => { - expect(playback.getState()).toBe(state); - }); - }; - - const itShouldEmitAStateChangedEvent = (state: VoiceBroadcastPlaybackState) => { - it(`should emit a ${state} state changed event`, () => { - expect(mocked(onStateChanged)).toHaveBeenCalledWith(state, playback); - }); - }; - - const itShouldHaveLiveness = (liveness: VoiceBroadcastLiveness): void => { - it(`should have liveness ${liveness}`, () => { - expect(playback.getLiveness()).toBe(liveness); - }); - }; - - const startPlayback = () => { - beforeEach(() => { - playback.start(); - }); - }; - - const pausePlayback = () => { - beforeEach(() => { - playback.pause(); - }); - }; - - const stopPlayback = () => { - beforeEach(() => { - playback.stop(); - }); - }; - - const mkChunkHelper = (data: ArrayBuffer): MediaEventHelper => { - return { - sourceBlob: { - cachedValue: new Blob(), - done: false, - value: { - // @ts-ignore - arrayBuffer: jest.fn().mockResolvedValue(data), - }, - }, - }; - }; - - const mkDeplayedChunkHelper = (data: ArrayBuffer): MediaEventHelper => { - const deferred = defer<LazyValue<Blob>>(); - - setTimeout(() => { - deferred.resolve({ - // @ts-ignore - arrayBuffer: jest.fn().mockResolvedValue(data), - }); - }, 7500); - - return { - sourceBlob: { - cachedValue: new Blob(), - done: false, - // @ts-ignore - value: deferred.promise, - }, - }; - }; - - const simulateFirstChunkArrived = async (): Promise<void> => { - jest.advanceTimersByTime(10000); - await flushPromisesWithFakeTimers(); - }; - - const mkInfoEvent = (state: VoiceBroadcastInfoState) => { - return mkVoiceBroadcastInfoStateEvent(roomId, state, userId, deviceId); - }; - - const mkPlayback = async (fakeTimers = false): Promise<VoiceBroadcastPlayback> => { - const playback = new VoiceBroadcastPlayback( - infoEvent, - client, - SdkContextClass.instance.voiceBroadcastRecordingsStore, - ); - jest.spyOn(playback, "removeAllListeners"); - jest.spyOn(playback, "destroy"); - playback.on(VoiceBroadcastPlaybackEvent.StateChanged, onStateChanged); - if (fakeTimers) { - await flushPromisesWithFakeTimers(); - } else { - await flushPromises(); - } - return playback; - }; - - const setUpChunkEvents = (chunkEvents: MatrixEvent[]) => { - mocked(client.relations).mockResolvedValueOnce({ - events: chunkEvents, - }); - }; - - const createChunkEvents = () => { - chunk1Event = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk1Length, 1); - deplayedChunk1Event = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk1Length, 1); - chunk2Event = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk2Length, 2); - chunk2Event.setTxnId("tx-id-1"); - chunk2BEvent = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk2Length, 2); - chunk2BEvent.setTxnId("tx-id-1"); - chunk3Event = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk3Length, 3); - - chunk1Helper = mkChunkHelper(chunk1Data); - delayedChunk1Helper = mkDeplayedChunkHelper(chunk1Data); - chunk2Helper = mkChunkHelper(chunk2Data); - chunk3Helper = mkChunkHelper(chunk3Data); - - chunk1Playback = createTestPlayback(); - chunk2Playback = createTestPlayback(); - chunk3Playback = createTestPlayback(); - - middleOfSecondChunk = (chunk1Length + chunk2Length / 2) / 1000; - middleOfThirdChunk = (chunk1Length + chunk2Length + chunk3Length / 2) / 1000; - - jest.spyOn(PlaybackManager.instance, "createPlaybackInstance").mockImplementation( - (buffer: ArrayBuffer, _waveForm?: number[]) => { - if (buffer === chunk1Data) return chunk1Playback; - if (buffer === chunk2Data) return chunk2Playback; - if (buffer === chunk3Data) return chunk3Playback; - - throw new Error("unexpected buffer"); - }, - ); - - mocked(MediaEventHelper).mockImplementation((event: MatrixEvent): any => { - if (event === chunk1Event) return chunk1Helper; - if (event === deplayedChunk1Event) return delayedChunk1Helper; - if (event === chunk2Event) return chunk2Helper; - if (event === chunk3Event) return chunk3Helper; - }); - }; - - filterConsole( - // expected for some tests - "Unable to load broadcast playback", - ); - - beforeEach(() => { - client = stubClient(); - deviceId = client.getDeviceId() || ""; - room = new Room(roomId, client, client.getSafeUserId()); - mocked(client.getRoom).mockImplementation((roomId: string): Room | null => { - if (roomId === room.roomId) return room; - return null; - }); - onStateChanged = jest.fn(); - }); - - afterEach(async () => { - SdkContextClass.instance.voiceBroadcastPlaybacksStore.getCurrent()?.stop(); - SdkContextClass.instance.voiceBroadcastPlaybacksStore.clearCurrent(); - await SdkContextClass.instance.voiceBroadcastRecordingsStore.getCurrent()?.stop(); - SdkContextClass.instance.voiceBroadcastRecordingsStore.clearCurrent(); - playback.destroy(); - }); - - describe(`when there is a ${VoiceBroadcastInfoState.Resumed} broadcast without chunks yet`, () => { - beforeEach(async () => { - infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Resumed); - createChunkEvents(); - room.addLiveEvents([infoEvent]); - playback = await mkPlayback(); - }); - - describe("and calling start", () => { - startPlayback(); - - itShouldHaveLiveness("live"); - - it("should be in buffering state", () => { - expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Buffering); - }); - - it("should have duration 0", () => { - expect(playback.durationSeconds).toBe(0); - }); - - it("should be at time 0", () => { - expect(playback.timeSeconds).toBe(0); - }); - - describe("and calling stop", () => { - stopPlayback(); - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped); - - describe("and calling pause", () => { - pausePlayback(); - // stopped voice broadcasts cannot be paused - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped); - }); - }); - - describe("and calling pause", () => { - pausePlayback(); - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Paused); - }); - - describe("and receiving the first chunk", () => { - beforeEach(() => { - room.relations.aggregateChildEvent(chunk1Event); - }); - - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing); - itShouldHaveLiveness("live"); - - it("should update the duration", () => { - expect(playback.durationSeconds).toBe(2.3); - }); - - it("should play the first chunk", () => { - expect(chunk1Playback.play).toHaveBeenCalled(); - }); - }); - - describe("and receiving the first undecryptable chunk", () => { - beforeEach(() => { - jest.spyOn(chunk1Event, "isDecryptionFailure").mockReturnValue(true); - room.relations.aggregateChildEvent(chunk1Event); - }); - - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Error); - - it("should not update the duration", () => { - expect(playback.durationSeconds).toBe(0); - }); - - describe("and the chunk is decrypted", () => { - beforeEach(() => { - mocked(chunk1Event.isDecryptionFailure).mockReturnValue(false); - chunk1Event.emit(MatrixEventEvent.Decrypted, chunk1Event); - }); - - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Paused); - - it("should not update the duration", () => { - expect(playback.durationSeconds).toBe(2.3); - }); - }); - }); - }); - }); - - describe(`when there is a ${VoiceBroadcastInfoState.Resumed} voice broadcast with some chunks`, () => { - beforeEach(async () => { - mocked(client.relations).mockResolvedValueOnce({ events: [] }); - infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Resumed); - createChunkEvents(); - setUpChunkEvents([chunk2Event, chunk1Event]); - room.addLiveEvents([infoEvent, chunk1Event, chunk2Event]); - room.relations.aggregateChildEvent(chunk2Event); - room.relations.aggregateChildEvent(chunk1Event); - playback = await mkPlayback(); - }); - - it("durationSeconds should have the length of the known chunks", () => { - expect(playback.durationSeconds).toEqual(6.5); - }); - - describe("and starting a playback with a broken chunk", () => { - beforeEach(async () => { - mocked(chunk2Playback.prepare).mockRejectedValue("Error decoding chunk"); - await playback.start(); - }); - - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Error); - - it("start() should keep it in the error state)", async () => { - await playback.start(); - expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Error); - }); - - it("stop() should keep it in the error state)", () => { - playback.stop(); - expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Error); - }); - - it("toggle() should keep it in the error state)", async () => { - await playback.toggle(); - expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Error); - }); - - it("pause() should keep it in the error state)", () => { - playback.pause(); - expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Error); - }); - }); - - describe("and an event with the same transaction Id occurs", () => { - beforeEach(() => { - room.addLiveEvents([chunk2BEvent]); - room.relations.aggregateChildEvent(chunk2BEvent); - }); - - it("durationSeconds should not change", () => { - expect(playback.durationSeconds).toEqual(6.5); - }); - }); - - describe("and calling start", () => { - startPlayback(); - - it("should play the last chunk", () => { - expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Playing); - // assert that the last chunk is played first - expect(chunk2Playback.play).toHaveBeenCalled(); - expect(chunk1Playback.play).not.toHaveBeenCalled(); - }); - - describe( - "and receiving a stop info event with last_chunk_sequence = 2 and " + - "the playback of the last available chunk ends", - () => { - beforeEach(() => { - const stoppedEvent = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Stopped, - client.getSafeUserId(), - client.deviceId!, - infoEvent, - 2, - ); - room.addLiveEvents([stoppedEvent]); - room.relations.aggregateChildEvent(stoppedEvent); - chunk2Playback.emit(PlaybackState.Stopped); - }); - - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped); - }, - ); - - describe( - "and receiving a stop info event with last_chunk_sequence = 3 and " + - "the playback of the last available chunk ends", - () => { - beforeEach(() => { - const stoppedEvent = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Stopped, - client.getSafeUserId(), - client.deviceId!, - infoEvent, - 3, - ); - room.addLiveEvents([stoppedEvent]); - room.relations.aggregateChildEvent(stoppedEvent); - chunk2Playback.emit(PlaybackState.Stopped); - }); - - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Buffering); - - describe("and the next chunk arrives", () => { - beforeEach(() => { - room.addLiveEvents([chunk3Event]); - room.relations.aggregateChildEvent(chunk3Event); - }); - - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing); - - it("should play the next chunk", () => { - expect(chunk3Playback.play).toHaveBeenCalled(); - }); - }); - }, - ); - - describe("and the info event is deleted", () => { - beforeEach(() => { - infoEvent.makeRedacted(new MatrixEvent({}), room); - }); - - it("should stop and destroy the playback", () => { - expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Stopped); - expect(playback.destroy).toHaveBeenCalled(); - }); - }); - }); - - describe("and currently recording a broadcast", () => { - let recording: VoiceBroadcastRecording; - - beforeEach(async () => { - recording = new VoiceBroadcastRecording( - mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Started, - client.getSafeUserId(), - client.deviceId, - ), - client, - ); - jest.spyOn(recording, "stop"); - SdkContextClass.instance.voiceBroadcastRecordingsStore.setCurrent(recording); - playback.start(); - await waitEnoughCyclesForModal(); - }); - - it("should display a confirm modal", () => { - expect(queryConfirmListeningDialog()).toBeInTheDocument(); - }); - - describe("when confirming the dialog", () => { - beforeEach(async () => { - await userEvent.click(screen.getByText("Yes, end my recording")); - }); - - it("should stop the recording", () => { - expect(recording.stop).toHaveBeenCalled(); - expect(SdkContextClass.instance.voiceBroadcastRecordingsStore.getCurrent()).toBeNull(); - }); - - it("should not start the playback", () => { - expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Playing); - }); - }); - - describe("when not confirming the dialog", () => { - beforeEach(async () => { - await userEvent.click(screen.getByText("No")); - }); - - it("should not stop the recording", () => { - expect(recording.stop).not.toHaveBeenCalled(); - }); - - it("should start the playback", () => { - expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Stopped); - }); - }); - }); - }); - - describe("when there is a stopped voice broadcast", () => { - beforeEach(async () => { - jest.useFakeTimers(); - infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Stopped); - createChunkEvents(); - // use delayed first chunk here to simulate loading time - setUpChunkEvents([chunk2Event, deplayedChunk1Event, chunk3Event]); - room.addLiveEvents([infoEvent, deplayedChunk1Event, chunk2Event, chunk3Event]); - playback = await mkPlayback(true); - }); - - afterEach(() => { - jest.useRealTimers(); - }); - - it("should expose the info event", () => { - expect(playback.infoEvent).toBe(infoEvent); - }); - - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped); - - describe("and calling start", () => { - startPlayback(); - - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Buffering); - - describe("and the first chunk data has been loaded", () => { - beforeEach(async () => { - await simulateFirstChunkArrived(); - }); - - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing); - - it("should play the chunks beginning with the first one", () => { - // assert that the first chunk is being played - expect(chunk1Playback.play).toHaveBeenCalled(); - expect(chunk2Playback.play).not.toHaveBeenCalled(); - }); - - describe("and calling start again", () => { - it("should not play the first chunk a second time", () => { - expect(chunk1Playback.play).toHaveBeenCalledTimes(1); - }); - }); - - describe("and the chunk playback progresses", () => { - beforeEach(() => { - chunk1Playback.clockInfo.liveData.update([11]); - }); - - it("should update the time", () => { - expect(playback.timeSeconds).toBe(11); - }); - }); - - describe("and the chunk playback progresses across the actual time", () => { - // This can be the case if the meta data is out of sync with the actual audio data. - - beforeEach(() => { - chunk1Playback.clockInfo.liveData.update([15]); - }); - - it("should update the time", () => { - expect(playback.timeSeconds).toBe(15); - expect(playback.timeLeftSeconds).toBe(0); - }); - }); - - describe("and skipping to the middle of the second chunk", () => { - const middleOfSecondChunk = (chunk1Length + chunk2Length / 2) / 1000; - - beforeEach(async () => { - await playback.skipTo(middleOfSecondChunk); - }); - - it("should play the second chunk", () => { - expect(chunk1Playback.stop).toHaveBeenCalled(); - expect(chunk1Playback.destroy).toHaveBeenCalled(); - expect(chunk2Playback.play).toHaveBeenCalled(); - }); - - it("should update the time", () => { - expect(playback.timeSeconds).toBe(middleOfSecondChunk); - }); - - describe("and skipping to the start", () => { - beforeEach(async () => { - await playback.skipTo(0); - }); - - it("should play the first chunk", () => { - expect(chunk2Playback.stop).toHaveBeenCalled(); - expect(chunk2Playback.destroy).toHaveBeenCalled(); - expect(chunk1Playback.play).toHaveBeenCalled(); - }); - - it("should update the time", () => { - expect(playback.timeSeconds).toBe(0); - }); - }); - }); - - describe("and skipping multiple times", () => { - beforeEach(async () => { - return Promise.all([ - playback.skipTo(middleOfSecondChunk), - playback.skipTo(middleOfThirdChunk), - playback.skipTo(0), - ]); - }); - - it("should only skip to the first and last position", () => { - expect(chunk1Playback.stop).toHaveBeenCalled(); - expect(chunk1Playback.destroy).toHaveBeenCalled(); - expect(chunk2Playback.play).toHaveBeenCalled(); - - expect(chunk3Playback.play).not.toHaveBeenCalled(); - - expect(chunk2Playback.stop).toHaveBeenCalled(); - expect(chunk2Playback.destroy).toHaveBeenCalled(); - expect(chunk1Playback.play).toHaveBeenCalled(); - }); - }); - - describe("and the first chunk ends", () => { - beforeEach(() => { - chunk1Playback.emit(PlaybackState.Stopped); - }); - - it("should play until the end", () => { - // assert first chunk was unloaded - expect(chunk1Playback.destroy).toHaveBeenCalled(); - - // assert that the second chunk is being played - expect(chunk2Playback.play).toHaveBeenCalled(); - - // simulate end of second and third chunk - chunk2Playback.emit(PlaybackState.Stopped); - chunk3Playback.emit(PlaybackState.Stopped); - - // assert that the entire playback is now in stopped state - expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Stopped); - }); - }); - - describe("and calling pause", () => { - pausePlayback(); - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Paused); - itShouldEmitAStateChangedEvent(VoiceBroadcastPlaybackState.Paused); - }); - - describe("and calling stop", () => { - stopPlayback(); - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped); - - it("should stop the playback", () => { - expect(chunk1Playback.stop).toHaveBeenCalled(); - }); - - describe("and skipping to somewhere in the middle of the first chunk", () => { - beforeEach(async () => { - mocked(chunk1Playback.play).mockClear(); - await playback.skipTo(1); - }); - - it("should not start the playback", () => { - expect(chunk1Playback.play).not.toHaveBeenCalled(); - }); - }); - }); - - describe("and calling destroy", () => { - beforeEach(() => { - playback.destroy(); - }); - - it("should call removeAllListeners", () => { - expect(playback.removeAllListeners).toHaveBeenCalled(); - }); - - it("should call destroy on the playbacks", () => { - expect(chunk1Playback.destroy).toHaveBeenCalled(); - expect(chunk2Playback.destroy).toHaveBeenCalled(); - }); - }); - }); - }); - - describe("and calling toggle for the first time", () => { - beforeEach(async () => { - playback.toggle(); - await simulateFirstChunkArrived(); - }); - - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing); - - describe("and calling toggle a second time", () => { - beforeEach(async () => { - await playback.toggle(); - }); - - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Paused); - - describe("and calling toggle a third time", () => { - beforeEach(async () => { - await playback.toggle(); - }); - - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing); - }); - }); - }); - - describe("and calling stop", () => { - stopPlayback(); - - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped); - - describe("and calling toggle", () => { - beforeEach(async () => { - mocked(onStateChanged).mockReset(); - playback.toggle(); - await simulateFirstChunkArrived(); - }); - - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing); - itShouldEmitAStateChangedEvent(VoiceBroadcastPlaybackState.Playing); - }); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/models/VoiceBroadcastPreRecording-test.ts b/test/unit-tests/voice-broadcast/models/VoiceBroadcastPreRecording-test.ts deleted file mode 100644 index f8b9e89d3e..0000000000 --- a/test/unit-tests/voice-broadcast/models/VoiceBroadcastPreRecording-test.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix"; - -import { - startNewVoiceBroadcastRecording, - VoiceBroadcastPlaybacksStore, - VoiceBroadcastPreRecording, - VoiceBroadcastRecordingsStore, -} from "../../../../src/voice-broadcast"; -import { stubClient } from "../../../test-utils"; - -jest.mock("../../../../src/voice-broadcast/utils/startNewVoiceBroadcastRecording"); - -describe("VoiceBroadcastPreRecording", () => { - const roomId = "!room:example.com"; - let client: MatrixClient; - let room: Room; - let sender: RoomMember; - let playbacksStore: VoiceBroadcastPlaybacksStore; - let recordingsStore: VoiceBroadcastRecordingsStore; - let preRecording: VoiceBroadcastPreRecording; - let onDismiss: (voiceBroadcastPreRecording: VoiceBroadcastPreRecording) => void; - - beforeAll(() => { - client = stubClient(); - room = new Room(roomId, client, client.getUserId() || ""); - sender = new RoomMember(roomId, client.getUserId() || ""); - recordingsStore = new VoiceBroadcastRecordingsStore(); - playbacksStore = new VoiceBroadcastPlaybacksStore(recordingsStore); - }); - - beforeEach(() => { - onDismiss = jest.fn(); - preRecording = new VoiceBroadcastPreRecording(room, sender, client, playbacksStore, recordingsStore); - preRecording.on("dismiss", onDismiss); - }); - - describe("start", () => { - beforeEach(() => { - preRecording.start(); - }); - - it("should start a new voice broadcast recording", () => { - expect(startNewVoiceBroadcastRecording).toHaveBeenCalledWith(room, client, playbacksStore, recordingsStore); - }); - - it("should emit a dismiss event", () => { - expect(onDismiss).toHaveBeenCalledWith(preRecording); - }); - }); - - describe("cancel", () => { - beforeEach(() => { - preRecording.cancel(); - }); - - it("should emit a dismiss event", () => { - expect(onDismiss).toHaveBeenCalledWith(preRecording); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/models/VoiceBroadcastRecording-test.ts b/test/unit-tests/voice-broadcast/models/VoiceBroadcastRecording-test.ts deleted file mode 100644 index 4f6bd8f47b..0000000000 --- a/test/unit-tests/voice-broadcast/models/VoiceBroadcastRecording-test.ts +++ /dev/null @@ -1,660 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { mocked } from "jest-mock"; -import { - ClientEvent, - EventTimelineSet, - EventType, - LOCAL_NOTIFICATION_SETTINGS_PREFIX, - MatrixClient, - MatrixEvent, - MatrixEventEvent, - MsgType, - RelationType, - Room, - Relations, - SyncState, -} from "matrix-js-sdk/src/matrix"; -import { EncryptedFile } from "matrix-js-sdk/src/types"; -import fetchMock from "fetch-mock-jest"; - -import { uploadFile } from "../../../../src/ContentMessages"; -import { createVoiceMessageContent } from "../../../../src/utils/createVoiceMessageContent"; -import { - createVoiceBroadcastRecorder, - getChunkLength, - getMaxBroadcastLength, - VoiceBroadcastInfoEventContent, - VoiceBroadcastInfoEventType, - VoiceBroadcastInfoState, - VoiceBroadcastRecorder, - VoiceBroadcastRecorderEvent, - VoiceBroadcastRecording, - VoiceBroadcastRecordingEvent, - VoiceBroadcastRecordingState, -} from "../../../../src/voice-broadcast"; -import { mkEvent, mkStubRoom, stubClient } from "../../../test-utils"; -import dis from "../../../../src/dispatcher/dispatcher"; -import { VoiceRecording } from "../../../../src/audio/VoiceRecording"; -import { createAudioContext } from "../../../../src/audio/compat"; - -jest.mock("../../../../src/voice-broadcast/audio/VoiceBroadcastRecorder", () => ({ - ...(jest.requireActual("../../../../src/voice-broadcast/audio/VoiceBroadcastRecorder") as object), - createVoiceBroadcastRecorder: jest.fn(), -})); - -// mock VoiceRecording because it contains all the audio APIs -jest.mock("../../../../src/audio/VoiceRecording", () => ({ - VoiceRecording: jest.fn().mockReturnValue({ - disableMaxLength: jest.fn(), - liveData: { - onUpdate: jest.fn(), - }, - off: jest.fn(), - on: jest.fn(), - start: jest.fn(), - stop: jest.fn(), - destroy: jest.fn(), - contentType: "audio/ogg", - }), -})); - -jest.mock("../../../../src/ContentMessages", () => ({ - uploadFile: jest.fn(), -})); - -jest.mock("../../../../src/utils/createVoiceMessageContent", () => ({ - createVoiceMessageContent: jest.fn(), -})); - -jest.mock("../../../../src/audio/compat", () => ({ - ...jest.requireActual("../../../../src/audio/compat"), - createAudioContext: jest.fn(), -})); - -describe("VoiceBroadcastRecording", () => { - const roomId = "!room:example.com"; - const uploadedUrl = "mxc://example.com/vb"; - const uploadedFile = { file: true } as unknown as EncryptedFile; - const maxLength = getMaxBroadcastLength(); - let room: Room; - let client: MatrixClient; - let infoEvent: MatrixEvent; - let voiceBroadcastRecording: VoiceBroadcastRecording; - let onStateChanged: (state: VoiceBroadcastRecordingState) => void; - let voiceBroadcastRecorder: VoiceBroadcastRecorder; - let audioElement: HTMLAudioElement; - - const mkVoiceBroadcastInfoEvent = (content: VoiceBroadcastInfoEventContent) => { - return mkEvent({ - event: true, - type: VoiceBroadcastInfoEventType, - user: client.getSafeUserId(), - room: roomId, - content, - }); - }; - - const setUpVoiceBroadcastRecording = () => { - voiceBroadcastRecording = new VoiceBroadcastRecording(infoEvent, client); - voiceBroadcastRecording.on(VoiceBroadcastRecordingEvent.StateChanged, onStateChanged); - jest.spyOn(voiceBroadcastRecording, "destroy"); - jest.spyOn(voiceBroadcastRecording, "emit"); - jest.spyOn(voiceBroadcastRecording, "removeAllListeners"); - }; - - const itShouldBeInState = (state: VoiceBroadcastRecordingState) => { - it(`should be in state stopped ${state}`, () => { - expect(voiceBroadcastRecording.getState()).toBe(state); - }); - }; - - const emitFirsChunkRecorded = () => { - voiceBroadcastRecorder.emit(VoiceBroadcastRecorderEvent.ChunkRecorded, { - buffer: new Uint8Array([1, 2, 3]), - length: 23, - }); - }; - - const itShouldSendAnInfoEvent = (state: VoiceBroadcastInfoState, lastChunkSequence: number) => { - it(`should send a ${state} info event`, () => { - expect(client.sendStateEvent).toHaveBeenCalledWith( - roomId, - VoiceBroadcastInfoEventType, - { - device_id: client.getDeviceId(), - state, - last_chunk_sequence: lastChunkSequence, - ["m.relates_to"]: { - rel_type: RelationType.Reference, - event_id: infoEvent.getId(), - }, - } as VoiceBroadcastInfoEventContent, - client.getUserId()!, - ); - }); - }; - - const itShouldSendAVoiceMessage = (data: number[], size: number, duration: number, sequence: number) => { - // events contain milliseconds - duration *= 1000; - - it("should send a voice message", () => { - expect(uploadFile).toHaveBeenCalledWith( - client, - roomId, - new Blob([new Uint8Array(data)], { type: voiceBroadcastRecorder.contentType }), - ); - - expect(mocked(client.sendMessage)).toHaveBeenCalledWith(roomId, { - body: "Voice message", - file: { - file: true, - }, - info: { - duration, - mimetype: "audio/ogg", - size, - }, - ["m.relates_to"]: { - event_id: infoEvent.getId(), - rel_type: "m.reference", - }, - msgtype: "m.audio", - ["org.matrix.msc1767.audio"]: { - duration, - waveform: undefined, - }, - ["org.matrix.msc1767.file"]: { - file: { - file: true, - }, - mimetype: "audio/ogg", - name: "Voice message.ogg", - size, - url: "mxc://example.com/vb", - }, - ["org.matrix.msc1767.text"]: "Voice message", - ["org.matrix.msc3245.voice"]: {}, - url: "mxc://example.com/vb", - ["io.element.voice_broadcast_chunk"]: { - sequence, - }, - }); - }); - }; - - const setUpUploadFileMock = () => { - mocked(uploadFile).mockResolvedValue({ - url: uploadedUrl, - file: uploadedFile, - }); - }; - - const mockAudioBufferSourceNode = { - addEventListener: jest.fn(), - connect: jest.fn(), - start: jest.fn(), - }; - const mockAudioContext = { - decodeAudioData: jest.fn(), - suspend: jest.fn(), - resume: jest.fn(), - createBufferSource: jest.fn().mockReturnValue(mockAudioBufferSourceNode), - currentTime: 1337, - }; - - beforeEach(() => { - client = stubClient(); - room = mkStubRoom(roomId, "Test Room", client); - mocked(client.getRoom).mockImplementation((getRoomId: string | undefined): Room | null => { - if (getRoomId === roomId) { - return room; - } - - return null; - }); - onStateChanged = jest.fn(); - voiceBroadcastRecorder = new VoiceBroadcastRecorder(new VoiceRecording(), getChunkLength()); - jest.spyOn(voiceBroadcastRecorder, "start"); - jest.spyOn(voiceBroadcastRecorder, "stop"); - jest.spyOn(voiceBroadcastRecorder, "destroy"); - mocked(createVoiceBroadcastRecorder).mockReturnValue(voiceBroadcastRecorder); - - setUpUploadFileMock(); - - mocked(createVoiceMessageContent).mockImplementation( - ( - mxc: string | undefined, - mimetype: string, - duration: number, - size: number, - file?: EncryptedFile, - waveform?: number[], - ) => { - return { - body: "Voice message", - msgtype: MsgType.Audio, - url: mxc, - file, - info: { - duration, - mimetype, - size, - }, - ["org.matrix.msc1767.text"]: "Voice message", - ["org.matrix.msc1767.file"]: { - url: mxc, - file, - name: "Voice message.ogg", - mimetype, - size, - }, - ["org.matrix.msc1767.audio"]: { - duration, - // https://github.com/matrix-org/matrix-doc/pull/3246 - waveform, - }, - ["org.matrix.msc3245.voice"]: {}, // No content, this is a rendering hint - }; - }, - ); - - audioElement = { - play: jest.fn(), - } as any as HTMLAudioElement; - - jest.spyOn(document, "querySelector").mockImplementation((selector: string) => { - if (selector === "audio#errorAudio") { - return audioElement; - } - - return null; - }); - - mocked(createAudioContext).mockReturnValue(mockAudioContext as unknown as AudioContext); - }); - - afterEach(() => { - voiceBroadcastRecording?.off(VoiceBroadcastRecordingEvent.StateChanged, onStateChanged); - }); - - describe("when there is an info event without id", () => { - beforeEach(() => { - infoEvent = mkVoiceBroadcastInfoEvent({ - device_id: client.getDeviceId()!, - state: VoiceBroadcastInfoState.Started, - }); - jest.spyOn(infoEvent, "getId").mockReturnValue(undefined); - }); - - it("should raise an error when creating a broadcast", () => { - expect(() => { - setUpVoiceBroadcastRecording(); - }).toThrow("Cannot create broadcast for info event without Id."); - }); - }); - - describe("when there is an info event without room", () => { - beforeEach(() => { - infoEvent = mkVoiceBroadcastInfoEvent({ - device_id: client.getDeviceId()!, - state: VoiceBroadcastInfoState.Started, - }); - jest.spyOn(infoEvent, "getRoomId").mockReturnValue(undefined); - }); - - it("should raise an error when creating a broadcast", () => { - expect(() => { - setUpVoiceBroadcastRecording(); - }).toThrow(`Cannot create broadcast for unknown room (info event ${infoEvent.getId()})`); - }); - }); - - describe("when created for a Voice Broadcast Info without relations", () => { - beforeEach(() => { - infoEvent = mkVoiceBroadcastInfoEvent({ - device_id: client.getDeviceId()!, - state: VoiceBroadcastInfoState.Started, - }); - setUpVoiceBroadcastRecording(); - }); - - it("should be in Started state", () => { - expect(voiceBroadcastRecording.getState()).toBe(VoiceBroadcastInfoState.Started); - }); - - describe("and calling stop", () => { - beforeEach(() => { - voiceBroadcastRecording.stop(); - }); - - itShouldSendAnInfoEvent(VoiceBroadcastInfoState.Stopped, 0); - itShouldBeInState(VoiceBroadcastInfoState.Stopped); - - it("should emit a stopped state changed event", () => { - expect(onStateChanged).toHaveBeenCalledWith(VoiceBroadcastInfoState.Stopped); - }); - }); - - describe("and calling start", () => { - beforeEach(async () => { - await voiceBroadcastRecording.start(); - }); - - it("should start the recorder", () => { - expect(voiceBroadcastRecorder.start).toHaveBeenCalled(); - }); - - describe("and the info event is redacted", () => { - beforeEach(() => { - infoEvent.emit( - MatrixEventEvent.BeforeRedaction, - infoEvent, - mkEvent({ - event: true, - type: EventType.RoomRedaction, - user: client.getSafeUserId(), - content: {}, - }), - ); - }); - - itShouldBeInState(VoiceBroadcastInfoState.Stopped); - - it("should destroy the recording", () => { - expect(voiceBroadcastRecording.destroy).toHaveBeenCalled(); - }); - }); - - describe("and receiving a call action", () => { - beforeEach(() => { - dis.dispatch( - { - action: "call_state", - }, - true, - ); - }); - - itShouldBeInState(VoiceBroadcastInfoState.Paused); - }); - - describe("and a chunk time update occurs", () => { - beforeEach(() => { - voiceBroadcastRecorder.emit(VoiceBroadcastRecorderEvent.CurrentChunkLengthUpdated, 10); - }); - - it("should update time left", () => { - expect(voiceBroadcastRecording.getTimeLeft()).toBe(maxLength - 10); - expect(voiceBroadcastRecording.emit).toHaveBeenCalledWith( - VoiceBroadcastRecordingEvent.TimeLeftChanged, - maxLength - 10, - ); - }); - - describe("and a chunk time update occurs, that would increase time left", () => { - beforeEach(() => { - mocked(voiceBroadcastRecording.emit).mockClear(); - voiceBroadcastRecorder.emit(VoiceBroadcastRecorderEvent.CurrentChunkLengthUpdated, 5); - }); - - it("should not change time left", () => { - expect(voiceBroadcastRecording.getTimeLeft()).toBe(maxLength - 10); - expect(voiceBroadcastRecording.emit).not.toHaveBeenCalled(); - }); - }); - }); - - describe("and a chunk has been recorded", () => { - beforeEach(async () => { - emitFirsChunkRecorded(); - }); - - itShouldSendAVoiceMessage([1, 2, 3], 3, 23, 1); - - describe("and another chunk has been recorded, that exceeds the max time", () => { - beforeEach(() => { - mocked(voiceBroadcastRecorder.stop).mockResolvedValue({ - buffer: new Uint8Array([23, 24, 25]), - length: getMaxBroadcastLength(), - }); - voiceBroadcastRecorder.emit( - VoiceBroadcastRecorderEvent.CurrentChunkLengthUpdated, - getMaxBroadcastLength(), - ); - }); - - itShouldBeInState(VoiceBroadcastInfoState.Stopped); - itShouldSendAVoiceMessage([23, 24, 25], 3, getMaxBroadcastLength(), 2); - itShouldSendAnInfoEvent(VoiceBroadcastInfoState.Stopped, 2); - }); - }); - - describe("and calling stop", () => { - beforeEach(async () => { - mocked(voiceBroadcastRecorder.stop).mockResolvedValue({ - buffer: new Uint8Array([4, 5, 6]), - length: 42, - }); - await voiceBroadcastRecording.stop(); - }); - - itShouldSendAVoiceMessage([4, 5, 6], 3, 42, 1); - itShouldSendAnInfoEvent(VoiceBroadcastInfoState.Stopped, 1); - }); - - describe.each([ - ["pause", async () => voiceBroadcastRecording.pause()], - ["toggle", async () => voiceBroadcastRecording.toggle()], - ])("and calling %s", (_case: string, action: () => Promise<void>) => { - beforeEach(async () => { - await action(); - }); - - itShouldBeInState(VoiceBroadcastInfoState.Paused); - itShouldSendAnInfoEvent(VoiceBroadcastInfoState.Paused, 0); - - it("should stop the recorder", () => { - expect(mocked(voiceBroadcastRecorder.stop)).toHaveBeenCalled(); - }); - - it("should emit a paused state changed event", () => { - expect(onStateChanged).toHaveBeenCalledWith(VoiceBroadcastInfoState.Paused); - }); - }); - - describe("and there is no connection", () => { - beforeEach(() => { - mocked(client.sendStateEvent).mockImplementation(() => { - throw new Error(); - }); - }); - - describe.each([ - ["pause", async () => voiceBroadcastRecording.pause()], - ["toggle", async () => voiceBroadcastRecording.toggle()], - ])("and calling %s", (_case: string, action: () => Promise<void>) => { - beforeEach(async () => { - await action(); - }); - - itShouldBeInState("connection_error"); - - describe("and the connection is back", () => { - beforeEach(() => { - mocked(client.sendStateEvent).mockResolvedValue({ event_id: "e1" }); - client.emit(ClientEvent.Sync, SyncState.Catchup, SyncState.Error); - }); - - itShouldBeInState(VoiceBroadcastInfoState.Paused); - }); - }); - }); - - describe("and calling destroy", () => { - beforeEach(() => { - voiceBroadcastRecording.destroy(); - }); - - it("should stop the recorder and remove all listeners", () => { - expect(mocked(voiceBroadcastRecorder.stop)).toHaveBeenCalled(); - expect(mocked(voiceBroadcastRecorder.destroy)).toHaveBeenCalled(); - expect(mocked(voiceBroadcastRecording.removeAllListeners)).toHaveBeenCalled(); - }); - }); - - describe("and a chunk has been recorded and the upload fails", () => { - beforeEach(() => { - mocked(uploadFile).mockRejectedValue("Error"); - emitFirsChunkRecorded(); - }); - - itShouldBeInState("connection_error"); - - describe("and the connection is back", () => { - beforeEach(() => { - setUpUploadFileMock(); - client.emit(ClientEvent.Sync, SyncState.Catchup, SyncState.Error); - }); - - itShouldBeInState(VoiceBroadcastInfoState.Paused); - itShouldSendAVoiceMessage([1, 2, 3], 3, 23, 1); - }); - }); - - describe("and audible notifications are disabled", () => { - beforeEach(() => { - const notificationSettings = mkEvent({ - event: true, - type: `${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${client.getDeviceId()}`, - user: client.getSafeUserId(), - content: { - is_silenced: true, - }, - }); - mocked(client.getAccountData).mockReturnValue(notificationSettings); - }); - - describe("and a chunk has been recorded and sending the voice message fails", () => { - beforeEach(() => { - mocked(client.sendMessage).mockRejectedValue("Error"); - emitFirsChunkRecorded(); - }); - - itShouldBeInState("connection_error"); - - it("should not play a notification", () => { - expect(audioElement.play).not.toHaveBeenCalled(); - }); - }); - }); - - describe("and a chunk has been recorded and sending the voice message fails", () => { - beforeEach(() => { - mocked(client.sendMessage).mockRejectedValue("Error"); - emitFirsChunkRecorded(); - fetchMock.get("media/error.mp3", 200); - }); - - itShouldBeInState("connection_error"); - - it("should play a notification", () => { - expect(mockAudioBufferSourceNode.start).toHaveBeenCalled(); - }); - - describe("and the connection is back", () => { - beforeEach(() => { - mocked(client.sendMessage).mockClear(); - mocked(client.sendMessage).mockResolvedValue({ event_id: "e23" }); - client.emit(ClientEvent.Sync, SyncState.Catchup, SyncState.Error); - }); - - itShouldBeInState(VoiceBroadcastInfoState.Paused); - itShouldSendAVoiceMessage([1, 2, 3], 3, 23, 1); - }); - }); - }); - - describe("and it is in paused state", () => { - beforeEach(async () => { - await voiceBroadcastRecording.pause(); - }); - - describe.each([ - ["resume", async () => voiceBroadcastRecording.resume()], - ["toggle", async () => voiceBroadcastRecording.toggle()], - ])("and calling %s", (_case: string, action: () => Promise<void>) => { - beforeEach(async () => { - await action(); - }); - - itShouldBeInState(VoiceBroadcastInfoState.Resumed); - itShouldSendAnInfoEvent(VoiceBroadcastInfoState.Resumed, 0); - - it("should start the recorder", () => { - expect(mocked(voiceBroadcastRecorder.start)).toHaveBeenCalled(); - }); - - it(`should emit a ${VoiceBroadcastInfoState.Resumed} state changed event`, () => { - expect(onStateChanged).toHaveBeenCalledWith(VoiceBroadcastInfoState.Resumed); - }); - }); - }); - }); - - describe("when created for a Voice Broadcast Info with a Stopped relation", () => { - beforeEach(() => { - infoEvent = mkVoiceBroadcastInfoEvent({ - device_id: client.getDeviceId()!, - state: VoiceBroadcastInfoState.Started, - chunk_length: 120, - }); - - const relationsContainer = { - getRelations: jest.fn(), - } as unknown as Relations; - mocked(relationsContainer.getRelations).mockReturnValue([ - mkVoiceBroadcastInfoEvent({ - device_id: client.getDeviceId()!, - state: VoiceBroadcastInfoState.Stopped, - ["m.relates_to"]: { - rel_type: RelationType.Reference, - event_id: infoEvent.getId()!, - }, - }), - ]); - - const timelineSet = { - relations: { - getChildEventsForEvent: jest - .fn() - .mockImplementation( - (eventId: string, relationType: RelationType | string, eventType: EventType | string) => { - if ( - eventId === infoEvent.getId() && - relationType === RelationType.Reference && - eventType === VoiceBroadcastInfoEventType - ) { - return relationsContainer; - } - }, - ), - }, - } as unknown as EventTimelineSet; - mocked(room.getUnfilteredTimelineSet).mockReturnValue(timelineSet); - - setUpVoiceBroadcastRecording(); - }); - - it("should be in Stopped state", () => { - expect(voiceBroadcastRecording.getState()).toBe(VoiceBroadcastInfoState.Stopped); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/stores/VoiceBroadcastPlaybacksStore-test.ts b/test/unit-tests/voice-broadcast/stores/VoiceBroadcastPlaybacksStore-test.ts deleted file mode 100644 index 29026345cf..0000000000 --- a/test/unit-tests/voice-broadcast/stores/VoiceBroadcastPlaybacksStore-test.ts +++ /dev/null @@ -1,162 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { mocked } from "jest-mock"; -import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; - -import { - VoiceBroadcastInfoState, - VoiceBroadcastPlayback, - VoiceBroadcastPlaybackEvent, - VoiceBroadcastPlaybacksStore, - VoiceBroadcastPlaybacksStoreEvent, - VoiceBroadcastPlaybackState, - VoiceBroadcastRecordingsStore, -} from "../../../../src/voice-broadcast"; -import { mkStubRoom, stubClient } from "../../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils"; - -describe("VoiceBroadcastPlaybacksStore", () => { - const roomId = "!room:example.com"; - let client: MatrixClient; - let userId: string; - let deviceId: string; - let room: Room; - let infoEvent1: MatrixEvent; - let infoEvent2: MatrixEvent; - let playback1: VoiceBroadcastPlayback; - let playback2: VoiceBroadcastPlayback; - let playbacks: VoiceBroadcastPlaybacksStore; - let onCurrentChanged: (playback: VoiceBroadcastPlayback | null) => void; - - beforeEach(() => { - client = stubClient(); - userId = client.getUserId() || ""; - deviceId = client.getDeviceId() || ""; - mocked(client.relations).mockClear(); - mocked(client.relations).mockResolvedValue({ events: [] }); - - room = mkStubRoom(roomId, "test room", client); - mocked(client.getRoom).mockImplementation((roomId: string): Room | null => { - if (roomId === room.roomId) { - return room; - } - - return null; - }); - - infoEvent1 = mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Started, userId, deviceId); - infoEvent2 = mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Started, userId, deviceId); - const recordings = new VoiceBroadcastRecordingsStore(); - playback1 = new VoiceBroadcastPlayback(infoEvent1, client, recordings); - jest.spyOn(playback1, "off"); - playback2 = new VoiceBroadcastPlayback(infoEvent2, client, recordings); - jest.spyOn(playback2, "off"); - - playbacks = new VoiceBroadcastPlaybacksStore(recordings); - jest.spyOn(playbacks, "removeAllListeners"); - onCurrentChanged = jest.fn(); - playbacks.on(VoiceBroadcastPlaybacksStoreEvent.CurrentChanged, onCurrentChanged); - }); - - afterEach(() => { - playbacks.off(VoiceBroadcastPlaybacksStoreEvent.CurrentChanged, onCurrentChanged); - }); - - describe("when setting a current Voice Broadcast playback", () => { - beforeEach(() => { - playbacks.setCurrent(playback1); - }); - - it("should return it as current", () => { - expect(playbacks.getCurrent()).toBe(playback1); - }); - - it("should return it by id", () => { - expect(playbacks.getByInfoEvent(infoEvent1, client)).toBe(playback1); - }); - - it("should emit a CurrentChanged event", () => { - expect(onCurrentChanged).toHaveBeenCalledWith(playback1); - }); - - describe("and setting the same again", () => { - beforeEach(() => { - mocked(onCurrentChanged).mockClear(); - playbacks.setCurrent(playback1); - }); - - it("should not emit a CurrentChanged event", () => { - expect(onCurrentChanged).not.toHaveBeenCalled(); - }); - }); - - describe("and setting another playback and start both", () => { - beforeEach(() => { - playbacks.setCurrent(playback2); - playback1.start(); - playback2.start(); - }); - - it("should set playback1 to paused", () => { - expect(playback1.getState()).toBe(VoiceBroadcastPlaybackState.Paused); - }); - - it("should set playback2 to buffering", () => { - // buffering because there are no chunks, yet - expect(playback2.getState()).toBe(VoiceBroadcastPlaybackState.Buffering); - }); - - describe("and calling destroy", () => { - beforeEach(() => { - playbacks.destroy(); - }); - - it("should remove all listeners", () => { - expect(playbacks.removeAllListeners).toHaveBeenCalled(); - }); - - it("should deregister the listeners on the playbacks", () => { - expect(playback1.off).toHaveBeenCalledWith( - VoiceBroadcastPlaybackEvent.StateChanged, - expect.any(Function), - ); - expect(playback2.off).toHaveBeenCalledWith( - VoiceBroadcastPlaybackEvent.StateChanged, - expect.any(Function), - ); - }); - }); - }); - }); - - describe("getByInfoEventId", () => { - let returnedPlayback: VoiceBroadcastPlayback; - - describe("when retrieving a known playback", () => { - beforeEach(() => { - playbacks.setCurrent(playback1); - returnedPlayback = playbacks.getByInfoEvent(infoEvent1, client); - }); - - it("should return the playback", () => { - expect(returnedPlayback).toBe(playback1); - }); - }); - - describe("when retrieving an unknown playback", () => { - beforeEach(() => { - returnedPlayback = playbacks.getByInfoEvent(infoEvent1, client); - }); - - it("should return the playback", () => { - expect(returnedPlayback.infoEvent).toBe(infoEvent1); - }); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/stores/VoiceBroadcastPreRecordingStore-test.ts b/test/unit-tests/voice-broadcast/stores/VoiceBroadcastPreRecordingStore-test.ts deleted file mode 100644 index 53d3a4c0f2..0000000000 --- a/test/unit-tests/voice-broadcast/stores/VoiceBroadcastPreRecordingStore-test.ts +++ /dev/null @@ -1,130 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { mocked } from "jest-mock"; -import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix"; - -import { - VoiceBroadcastPlaybacksStore, - VoiceBroadcastPreRecording, - VoiceBroadcastPreRecordingStore, - VoiceBroadcastRecordingsStore, -} from "../../../../src/voice-broadcast"; -import { stubClient } from "../../../test-utils"; - -describe("VoiceBroadcastPreRecordingStore", () => { - const roomId = "!room:example.com"; - let client: MatrixClient; - let room: Room; - let sender: RoomMember; - let playbacksStore: VoiceBroadcastPlaybacksStore; - let recordingsStore: VoiceBroadcastRecordingsStore; - let store: VoiceBroadcastPreRecordingStore; - let preRecording1: VoiceBroadcastPreRecording; - - beforeAll(() => { - client = stubClient(); - room = new Room(roomId, client, client.getUserId() || ""); - sender = new RoomMember(roomId, client.getUserId() || ""); - recordingsStore = new VoiceBroadcastRecordingsStore(); - playbacksStore = new VoiceBroadcastPlaybacksStore(recordingsStore); - }); - - beforeEach(() => { - store = new VoiceBroadcastPreRecordingStore(); - jest.spyOn(store, "emit"); - jest.spyOn(store, "removeAllListeners"); - preRecording1 = new VoiceBroadcastPreRecording(room, sender, client, playbacksStore, recordingsStore); - jest.spyOn(preRecording1, "off"); - }); - - it("getCurrent() should return null", () => { - expect(store.getCurrent()).toBeNull(); - }); - - it("clearCurrent() should work", () => { - store.clearCurrent(); - expect(store.getCurrent()).toBeNull(); - }); - - describe("when setting a current recording", () => { - beforeEach(() => { - store.setCurrent(preRecording1); - }); - - it("getCurrent() should return the recording", () => { - expect(store.getCurrent()).toBe(preRecording1); - }); - - it("should emit a changed event with the recording", () => { - expect(store.emit).toHaveBeenCalledWith("changed", preRecording1); - }); - - describe("and calling destroy()", () => { - beforeEach(() => { - store.destroy(); - }); - - it("should remove all listeners", () => { - expect(store.removeAllListeners).toHaveBeenCalled(); - }); - - it("should deregister from the pre-recordings", () => { - expect(preRecording1.off).toHaveBeenCalledWith("dismiss", expect.any(Function)); - }); - }); - - describe("and cancelling the pre-recording", () => { - beforeEach(() => { - preRecording1.cancel(); - }); - - it("should clear the current recording", () => { - expect(store.getCurrent()).toBeNull(); - }); - - it("should emit a changed event with null", () => { - expect(store.emit).toHaveBeenCalledWith("changed", null); - }); - }); - - describe("and setting the same pre-recording again", () => { - beforeEach(() => { - mocked(store.emit).mockClear(); - store.setCurrent(preRecording1); - }); - - it("should not emit a changed event", () => { - expect(store.emit).not.toHaveBeenCalled(); - }); - }); - - describe("and setting another pre-recording", () => { - let preRecording2: VoiceBroadcastPreRecording; - - beforeEach(() => { - mocked(store.emit).mockClear(); - mocked(preRecording1.off).mockClear(); - preRecording2 = new VoiceBroadcastPreRecording(room, sender, client, playbacksStore, recordingsStore); - store.setCurrent(preRecording2); - }); - - it("should deregister from the current pre-recording", () => { - expect(preRecording1.off).toHaveBeenCalledWith("dismiss", expect.any(Function)); - }); - - it("getCurrent() should return the new recording", () => { - expect(store.getCurrent()).toBe(preRecording2); - }); - - it("should emit a changed event with the new recording", () => { - expect(store.emit).toHaveBeenCalledWith("changed", preRecording2); - }); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/stores/VoiceBroadcastRecordingsStore-test.ts b/test/unit-tests/voice-broadcast/stores/VoiceBroadcastRecordingsStore-test.ts deleted file mode 100644 index 97b756fce1..0000000000 --- a/test/unit-tests/voice-broadcast/stores/VoiceBroadcastRecordingsStore-test.ts +++ /dev/null @@ -1,167 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { mocked } from "jest-mock"; -import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; - -import { - VoiceBroadcastRecordingsStore, - VoiceBroadcastRecordingsStoreEvent, - VoiceBroadcastRecording, - VoiceBroadcastInfoState, -} from "../../../../src/voice-broadcast"; -import { mkStubRoom, stubClient } from "../../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils"; - -describe("VoiceBroadcastRecordingsStore", () => { - const roomId = "!room:example.com"; - let client: MatrixClient; - let room: Room; - let infoEvent: MatrixEvent; - let otherInfoEvent: MatrixEvent; - let recording: VoiceBroadcastRecording; - let otherRecording: VoiceBroadcastRecording; - let recordings: VoiceBroadcastRecordingsStore; - let onCurrentChanged: (recording: VoiceBroadcastRecording | null) => void; - - beforeEach(() => { - client = stubClient(); - room = mkStubRoom(roomId, "test room", client); - mocked(client.getRoom).mockImplementation((roomId: string) => { - if (roomId === room.roomId) { - return room; - } - return null; - }); - infoEvent = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Started, - client.getUserId()!, - client.getDeviceId()!, - ); - otherInfoEvent = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Started, - client.getUserId()!, - client.getDeviceId()!, - ); - recording = new VoiceBroadcastRecording(infoEvent, client); - otherRecording = new VoiceBroadcastRecording(otherInfoEvent, client); - recordings = new VoiceBroadcastRecordingsStore(); - onCurrentChanged = jest.fn(); - recordings.on(VoiceBroadcastRecordingsStoreEvent.CurrentChanged, onCurrentChanged); - }); - - afterEach(() => { - recording.destroy(); - recordings.off(VoiceBroadcastRecordingsStoreEvent.CurrentChanged, onCurrentChanged); - }); - - it("when setting a recording without info event Id, it should raise an error", () => { - infoEvent.event.event_id = undefined; - expect(() => { - recordings.setCurrent(recording); - }).toThrow("Got broadcast info event without Id"); - }); - - describe("when setting a current Voice Broadcast recording", () => { - beforeEach(() => { - recordings.setCurrent(recording); - }); - - it("should return it as current", () => { - expect(recordings.hasCurrent()).toBe(true); - expect(recordings.getCurrent()).toBe(recording); - }); - - it("should return it by id", () => { - expect(recordings.getByInfoEvent(infoEvent, client)).toBe(recording); - }); - - it("should emit a CurrentChanged event", () => { - expect(onCurrentChanged).toHaveBeenCalledWith(recording); - }); - - describe("and setting the same again", () => { - beforeEach(() => { - mocked(onCurrentChanged).mockClear(); - recordings.setCurrent(recording); - }); - - it("should not emit a CurrentChanged event", () => { - expect(onCurrentChanged).not.toHaveBeenCalled(); - }); - }); - - describe("and calling clearCurrent()", () => { - beforeEach(() => { - recordings.clearCurrent(); - }); - - it("should clear the current recording", () => { - expect(recordings.hasCurrent()).toBe(false); - expect(recordings.getCurrent()).toBeNull(); - }); - - it("should emit a current changed event", () => { - expect(onCurrentChanged).toHaveBeenCalledWith(null); - }); - - it("and calling it again should work", () => { - recordings.clearCurrent(); - expect(recordings.getCurrent()).toBeNull(); - }); - }); - - describe("and setting another recording and stopping the previous recording", () => { - beforeEach(() => { - recordings.setCurrent(otherRecording); - recording.stop(); - }); - - it("should keep the current recording", () => { - expect(recordings.getCurrent()).toBe(otherRecording); - }); - }); - - describe("and the recording stops", () => { - beforeEach(() => { - recording.stop(); - }); - - it("should clear the current recording", () => { - expect(recordings.getCurrent()).toBeNull(); - }); - }); - }); - - describe("getByInfoEventId", () => { - let returnedRecording: VoiceBroadcastRecording; - - describe("when retrieving a known recording", () => { - beforeEach(() => { - recordings.setCurrent(recording); - returnedRecording = recordings.getByInfoEvent(infoEvent, client); - }); - - it("should return the recording", () => { - expect(returnedRecording).toBe(recording); - }); - }); - - describe("when retrieving an unknown recording", () => { - beforeEach(() => { - returnedRecording = recordings.getByInfoEvent(infoEvent, client); - }); - - it("should return the recording", () => { - expect(returnedRecording.infoEvent).toBe(infoEvent); - }); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/utils/VoiceBroadcastChunkEvents-test.ts b/test/unit-tests/voice-broadcast/utils/VoiceBroadcastChunkEvents-test.ts deleted file mode 100644 index 817538ef17..0000000000 --- a/test/unit-tests/voice-broadcast/utils/VoiceBroadcastChunkEvents-test.ts +++ /dev/null @@ -1,142 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixEvent } from "matrix-js-sdk/src/matrix"; - -import { VoiceBroadcastChunkEvents } from "../../../../src/voice-broadcast/utils/VoiceBroadcastChunkEvents"; -import { mkVoiceBroadcastChunkEvent } from "./test-utils"; - -describe("VoiceBroadcastChunkEvents", () => { - const userId = "@user:example.com"; - const roomId = "!room:example.com"; - const txnId = "txn-id"; - let eventSeq1Time1: MatrixEvent; - let eventSeq2Time4: MatrixEvent; - let eventSeq3Time2: MatrixEvent; - let eventSeq3Time2T: MatrixEvent; - let eventSeq4Time1: MatrixEvent; - let eventSeqUTime3: MatrixEvent; - let eventSeq2Time4Dup: MatrixEvent; - let chunkEvents: VoiceBroadcastChunkEvents; - - beforeEach(() => { - eventSeq1Time1 = mkVoiceBroadcastChunkEvent("info1", userId, roomId, 7, 1, 1); - eventSeq2Time4 = mkVoiceBroadcastChunkEvent("info1", userId, roomId, 23, 2, 4); - eventSeq2Time4Dup = mkVoiceBroadcastChunkEvent("info1", userId, roomId, 3141, 2, 4); - jest.spyOn(eventSeq2Time4Dup, "getId").mockReturnValue(eventSeq2Time4.getId()); - eventSeq3Time2 = mkVoiceBroadcastChunkEvent("info1", userId, roomId, 42, 3, 2); - eventSeq3Time2.setTxnId(txnId); - eventSeq3Time2T = mkVoiceBroadcastChunkEvent("info1", userId, roomId, 42, 3, 2); - eventSeq3Time2T.setTxnId(txnId); - eventSeq4Time1 = mkVoiceBroadcastChunkEvent("info1", userId, roomId, 69, 4, 1); - eventSeqUTime3 = mkVoiceBroadcastChunkEvent("info1", userId, roomId, 314, undefined, 3); - chunkEvents = new VoiceBroadcastChunkEvents(); - }); - - describe("when adding events that all have a sequence", () => { - beforeEach(() => { - chunkEvents.addEvent(eventSeq2Time4); - chunkEvents.addEvent(eventSeq1Time1); - chunkEvents.addEvents([eventSeq4Time1, eventSeq2Time4Dup, eventSeq3Time2]); - }); - - it("should provide the events sort by sequence", () => { - expect(chunkEvents.getEvents()).toEqual([ - eventSeq1Time1, - eventSeq2Time4Dup, - eventSeq3Time2, - eventSeq4Time1, - ]); - }); - - it("getNumberOfEvents should return 4", () => { - expect(chunkEvents.getNumberOfEvents()).toBe(4); - }); - - it("getLength should return the total length of all chunks", () => { - expect(chunkEvents.getLength()).toBe(3259); - }); - - it("getLengthTo(first event) should return 0", () => { - expect(chunkEvents.getLengthTo(eventSeq1Time1)).toBe(0); - }); - - it("getLengthTo(some event) should return the time excl. that event", () => { - expect(chunkEvents.getLengthTo(eventSeq3Time2)).toBe(7 + 3141); - }); - - it("getLengthTo(last event) should return the time excl. that event", () => { - expect(chunkEvents.getLengthTo(eventSeq4Time1)).toBe(7 + 3141 + 42); - }); - - it("should return the expected next chunk", () => { - expect(chunkEvents.getNext(eventSeq2Time4Dup)).toBe(eventSeq3Time2); - }); - - it("should return undefined for next last chunk", () => { - expect(chunkEvents.getNext(eventSeq4Time1)).toBeUndefined(); - }); - - it("findByTime(0) should return the first chunk", () => { - expect(chunkEvents.findByTime(0)).toBe(eventSeq1Time1); - }); - - it("findByTime(some time) should return the chunk with this time", () => { - expect(chunkEvents.findByTime(7 + 3141 + 21)).toBe(eventSeq3Time2); - }); - - it("findByTime(entire duration) should return the last chunk", () => { - expect(chunkEvents.findByTime(7 + 3141 + 42 + 69)).toBe(eventSeq4Time1); - }); - - describe("and adding an event with a known transaction Id", () => { - beforeEach(() => { - chunkEvents.addEvent(eventSeq3Time2T); - }); - - it("should replace the previous event", () => { - expect(chunkEvents.getEvents()).toEqual([ - eventSeq1Time1, - eventSeq2Time4Dup, - eventSeq3Time2T, - eventSeq4Time1, - ]); - expect(chunkEvents.getNumberOfEvents()).toBe(4); - }); - }); - }); - - describe("when adding events where at least one does not have a sequence", () => { - beforeEach(() => { - chunkEvents.addEvent(eventSeq2Time4); - chunkEvents.addEvent(eventSeq1Time1); - chunkEvents.addEvents([eventSeq4Time1, eventSeqUTime3, eventSeq2Time4Dup, eventSeq3Time2]); - }); - - it("should provide the events sort by timestamp without duplicates", () => { - expect(chunkEvents.getEvents()).toEqual([ - eventSeq1Time1, - eventSeq4Time1, - eventSeq3Time2, - eventSeqUTime3, - eventSeq2Time4Dup, - ]); - expect(chunkEvents.getNumberOfEvents()).toBe(5); - }); - - describe("getSequenceForEvent", () => { - it("should return the sequence if provided by the event", () => { - expect(chunkEvents.getSequenceForEvent(eventSeq3Time2)).toBe(3); - }); - - it("should return the index if no sequence provided by event", () => { - expect(chunkEvents.getSequenceForEvent(eventSeqUTime3)).toBe(4); - }); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/utils/VoiceBroadcastResumer-test.ts b/test/unit-tests/voice-broadcast/utils/VoiceBroadcastResumer-test.ts deleted file mode 100644 index 81bcce5f3f..0000000000 --- a/test/unit-tests/voice-broadcast/utils/VoiceBroadcastResumer-test.ts +++ /dev/null @@ -1,179 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { mocked } from "jest-mock"; -import { ClientEvent, MatrixClient, MatrixEvent, RelationType, Room, SyncState } from "matrix-js-sdk/src/matrix"; - -import { - VoiceBroadcastInfoEventContent, - VoiceBroadcastInfoEventType, - VoiceBroadcastInfoState, - VoiceBroadcastResumer, -} from "../../../../src/voice-broadcast"; -import { stubClient } from "../../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "./test-utils"; - -describe("VoiceBroadcastResumer", () => { - const roomId = "!room:example.com"; - let client: MatrixClient; - let room: Room; - let resumer: VoiceBroadcastResumer; - let startedInfoEvent: MatrixEvent; - let pausedInfoEvent: MatrixEvent; - - const itShouldNotSendAStateEvent = (): void => { - it("should not send a state event", () => { - expect(client.sendStateEvent).not.toHaveBeenCalled(); - }); - }; - - const itShouldSendAStoppedStateEvent = (): void => { - it("should send a stopped state event", () => { - expect(client.sendStateEvent).toHaveBeenCalledWith( - startedInfoEvent.getRoomId(), - VoiceBroadcastInfoEventType, - { - "device_id": client.getDeviceId(), - "state": VoiceBroadcastInfoState.Stopped, - "m.relates_to": { - rel_type: RelationType.Reference, - event_id: startedInfoEvent.getId(), - }, - } as VoiceBroadcastInfoEventContent, - client.getUserId()!, - ); - }); - }; - - const itShouldDeregisterFromTheClient = () => { - it("should deregister from the client", () => { - expect(client.off).toHaveBeenCalledWith(ClientEvent.Sync, expect.any(Function)); - }); - }; - - beforeEach(() => { - client = stubClient(); - jest.spyOn(client, "off"); - room = new Room(roomId, client, client.getUserId()!); - mocked(client.getRoom).mockImplementation((getRoomId: string | undefined) => { - if (getRoomId === roomId) return room; - - return null; - }); - mocked(client.getRooms).mockReturnValue([room]); - startedInfoEvent = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Started, - client.getUserId()!, - client.getDeviceId()!, - ); - pausedInfoEvent = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Paused, - client.getUserId()!, - client.getDeviceId()!, - startedInfoEvent, - ); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe("when the initial sync is completed", () => { - beforeEach(() => { - mocked(client.isInitialSyncComplete).mockReturnValue(true); - }); - - describe("and there is no info event", () => { - beforeEach(() => { - resumer = new VoiceBroadcastResumer(client); - }); - - itShouldNotSendAStateEvent(); - - describe("and calling destroy", () => { - beforeEach(() => { - resumer.destroy(); - }); - - itShouldDeregisterFromTheClient(); - }); - }); - - describe("and there is a started info event", () => { - beforeEach(() => { - room.currentState.setStateEvents([startedInfoEvent]); - }); - - describe("and the client knows about the user and device", () => { - beforeEach(() => { - resumer = new VoiceBroadcastResumer(client); - }); - - itShouldSendAStoppedStateEvent(); - }); - - describe("and the client doesn't know about the user", () => { - beforeEach(() => { - mocked(client.getUserId).mockReturnValue(null); - resumer = new VoiceBroadcastResumer(client); - }); - - itShouldNotSendAStateEvent(); - }); - - describe("and the client doesn't know about the device", () => { - beforeEach(() => { - mocked(client.getDeviceId).mockReturnValue(null); - resumer = new VoiceBroadcastResumer(client); - }); - - itShouldNotSendAStateEvent(); - }); - }); - - describe("and there is a paused info event", () => { - beforeEach(() => { - room.currentState.setStateEvents([pausedInfoEvent]); - resumer = new VoiceBroadcastResumer(client); - }); - - itShouldSendAStoppedStateEvent(); - }); - }); - - describe("when the initial sync is not completed", () => { - beforeEach(() => { - room.currentState.setStateEvents([pausedInfoEvent]); - mocked(client.isInitialSyncComplete).mockReturnValue(false); - mocked(client.getSyncState).mockReturnValue(SyncState.Prepared); - resumer = new VoiceBroadcastResumer(client); - }); - - itShouldNotSendAStateEvent(); - - describe("and a sync event appears", () => { - beforeEach(() => { - client.emit(ClientEvent.Sync, SyncState.Prepared, SyncState.Stopped); - }); - - itShouldNotSendAStateEvent(); - - describe("and the initial sync completed and a sync event appears", () => { - beforeEach(() => { - mocked(client.getSyncState).mockReturnValue(SyncState.Syncing); - client.emit(ClientEvent.Sync, SyncState.Syncing, SyncState.Prepared); - }); - - itShouldSendAStoppedStateEvent(); - itShouldDeregisterFromTheClient(); - }); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/utils/__snapshots__/setUpVoiceBroadcastPreRecording-test.ts.snap b/test/unit-tests/voice-broadcast/utils/__snapshots__/setUpVoiceBroadcastPreRecording-test.ts.snap deleted file mode 100644 index 2d6ba0a409..0000000000 --- a/test/unit-tests/voice-broadcast/utils/__snapshots__/setUpVoiceBroadcastPreRecording-test.ts.snap +++ /dev/null @@ -1,24 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`setUpVoiceBroadcastPreRecording when trying to start a broadcast if there is no connection should show an info dialog and not set up a pre-recording 1`] = ` -[MockFunction] { - "calls": [ - [ - [Function], - { - "description": <p> - Unfortunately we're unable to start a recording right now. Please try again later. - </p>, - "hasCloseButton": true, - "title": "Connection error", - }, - ], - ], - "results": [ - { - "type": "return", - "value": undefined, - }, - ], -} -`; diff --git a/test/unit-tests/voice-broadcast/utils/__snapshots__/startNewVoiceBroadcastRecording-test.ts.snap b/test/unit-tests/voice-broadcast/utils/__snapshots__/startNewVoiceBroadcastRecording-test.ts.snap deleted file mode 100644 index dd5aa15305..0000000000 --- a/test/unit-tests/voice-broadcast/utils/__snapshots__/startNewVoiceBroadcastRecording-test.ts.snap +++ /dev/null @@ -1,116 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`startNewVoiceBroadcastRecording when the current user is allowed to send voice broadcast info state events when there already is a live broadcast of another user should show an info dialog 1`] = ` -[MockFunction] { - "calls": [ - [ - [Function], - { - "description": <p> - Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one. - </p>, - "hasCloseButton": true, - "title": "Can't start a new voice broadcast", - }, - ], - ], - "results": [ - { - "type": "return", - "value": undefined, - }, - ], -} -`; - -exports[`startNewVoiceBroadcastRecording when the current user is allowed to send voice broadcast info state events when there already is a live broadcast of the current user in the room should show an info dialog 1`] = ` -[MockFunction] { - "calls": [ - [ - [Function], - { - "description": <p> - You are already recording a voice broadcast. Please end your current voice broadcast to start a new one. - </p>, - "hasCloseButton": true, - "title": "Can't start a new voice broadcast", - }, - ], - ], - "results": [ - { - "type": "return", - "value": undefined, - }, - ], -} -`; - -exports[`startNewVoiceBroadcastRecording when the current user is allowed to send voice broadcast info state events when there is already a current voice broadcast should show an info dialog 1`] = ` -[MockFunction] { - "calls": [ - [ - [Function], - { - "description": <p> - You are already recording a voice broadcast. Please end your current voice broadcast to start a new one. - </p>, - "hasCloseButton": true, - "title": "Can't start a new voice broadcast", - }, - ], - ], - "results": [ - { - "type": "return", - "value": undefined, - }, - ], -} -`; - -exports[`startNewVoiceBroadcastRecording when the current user is not allowed to send voice broadcast info state events should show an info dialog 1`] = ` -[MockFunction] { - "calls": [ - [ - [Function], - { - "description": <p> - You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions. - </p>, - "hasCloseButton": true, - "title": "Can't start a new voice broadcast", - }, - ], - ], - "results": [ - { - "type": "return", - "value": undefined, - }, - ], -} -`; - -exports[`startNewVoiceBroadcastRecording when trying to start a broadcast if there is no connection should show an info dialog and not start a recording 1`] = ` -[MockFunction] { - "calls": [ - [ - [Function], - { - "description": <p> - Unfortunately we're unable to start a recording right now. Please try again later. - </p>, - "hasCloseButton": true, - "title": "Connection error", - }, - ], - ], - "results": [ - { - "type": "return", - "value": undefined, - }, - ], -} -`; diff --git a/test/unit-tests/voice-broadcast/utils/__snapshots__/textForVoiceBroadcastStoppedEvent-test.tsx.snap b/test/unit-tests/voice-broadcast/utils/__snapshots__/textForVoiceBroadcastStoppedEvent-test.tsx.snap deleted file mode 100644 index cf1e93db13..0000000000 --- a/test/unit-tests/voice-broadcast/utils/__snapshots__/textForVoiceBroadcastStoppedEvent-test.tsx.snap +++ /dev/null @@ -1,42 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`textForVoiceBroadcastStoppedEvent should render other users broadcast as expected 1`] = ` -<div> - <div> - @other:example.com ended a voice broadcast - </div> -</div> -`; - -exports[`textForVoiceBroadcastStoppedEvent should render own broadcast as expected 1`] = ` -<div> - <div> - You ended a voice broadcast - </div> -</div> -`; - -exports[`textForVoiceBroadcastStoppedEvent should render without login as expected 1`] = ` -<div> - <div> - @other:example.com ended a voice broadcast - </div> -</div> -`; - -exports[`textForVoiceBroadcastStoppedEvent when rendering an event with relation to the start event should render events with relation to the start event 1`] = ` -<div> - <div> - <span> - You ended a - <div - class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline" - role="button" - tabindex="0" - > - voice broadcast - </div> - </span> - </div> -</div> -`; diff --git a/test/unit-tests/voice-broadcast/utils/cleanUpBroadcasts-test.ts b/test/unit-tests/voice-broadcast/utils/cleanUpBroadcasts-test.ts deleted file mode 100644 index ebf0de390c..0000000000 --- a/test/unit-tests/voice-broadcast/utils/cleanUpBroadcasts-test.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { - cleanUpBroadcasts, - VoiceBroadcastPlayback, - VoiceBroadcastPreRecording, - VoiceBroadcastRecording, -} from "../../../../src/voice-broadcast"; -import { stubClient } from "../../../test-utils"; -import { TestSdkContext } from "../../TestSdkContext"; -import { mkVoiceBroadcastPlayback, mkVoiceBroadcastPreRecording, mkVoiceBroadcastRecording } from "./test-utils"; - -describe("cleanUpBroadcasts", () => { - let playback: VoiceBroadcastPlayback; - let recording: VoiceBroadcastRecording; - let preRecording: VoiceBroadcastPreRecording; - let stores: TestSdkContext; - - beforeEach(() => { - stores = new TestSdkContext(); - stores.client = stubClient(); - - playback = mkVoiceBroadcastPlayback(stores); - jest.spyOn(playback, "stop").mockReturnValue(); - stores.voiceBroadcastPlaybacksStore.setCurrent(playback); - - recording = mkVoiceBroadcastRecording(stores); - jest.spyOn(recording, "stop").mockResolvedValue(); - stores.voiceBroadcastRecordingsStore.setCurrent(recording); - - preRecording = mkVoiceBroadcastPreRecording(stores); - jest.spyOn(preRecording, "cancel").mockReturnValue(); - stores.voiceBroadcastPreRecordingStore.setCurrent(preRecording); - }); - - it("should stop and clear all broadcast related stuff", async () => { - await cleanUpBroadcasts(stores); - expect(playback.stop).toHaveBeenCalled(); - expect(stores.voiceBroadcastPlaybacksStore.getCurrent()).toBeNull(); - expect(recording.stop).toHaveBeenCalled(); - expect(stores.voiceBroadcastRecordingsStore.getCurrent()).toBeNull(); - expect(preRecording.cancel).toHaveBeenCalled(); - expect(stores.voiceBroadcastPreRecordingStore.getCurrent()).toBeNull(); - }); -}); diff --git a/test/unit-tests/voice-broadcast/utils/determineVoiceBroadcastLiveness-test.ts b/test/unit-tests/voice-broadcast/utils/determineVoiceBroadcastLiveness-test.ts deleted file mode 100644 index c1a9b2db13..0000000000 --- a/test/unit-tests/voice-broadcast/utils/determineVoiceBroadcastLiveness-test.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { VoiceBroadcastInfoState, VoiceBroadcastLiveness } from "../../../../src/voice-broadcast"; -import { determineVoiceBroadcastLiveness } from "../../../../src/voice-broadcast/utils/determineVoiceBroadcastLiveness"; - -const testData: Array<{ state: VoiceBroadcastInfoState; expected: VoiceBroadcastLiveness }> = [ - { state: VoiceBroadcastInfoState.Started, expected: "live" }, - { state: VoiceBroadcastInfoState.Resumed, expected: "live" }, - { state: VoiceBroadcastInfoState.Paused, expected: "grey" }, - { state: VoiceBroadcastInfoState.Stopped, expected: "not-live" }, -]; - -describe("determineVoiceBroadcastLiveness", () => { - it.each(testData)("should return the expected value for a %s broadcast", ({ state, expected }) => { - expect(determineVoiceBroadcastLiveness(state)).toBe(expected); - }); - - it("should return »non-live« for an undefined state", () => { - // @ts-ignore - expect(determineVoiceBroadcastLiveness(undefined)).toBe("not-live"); - }); - - it("should return »non-live« for an unknown state", () => { - // @ts-ignore - expect(determineVoiceBroadcastLiveness("unknown test state")).toBe("not-live"); - }); -}); diff --git a/test/unit-tests/voice-broadcast/utils/findRoomLiveVoiceBroadcastFromUserAndDevice-test.ts b/test/unit-tests/voice-broadcast/utils/findRoomLiveVoiceBroadcastFromUserAndDevice-test.ts deleted file mode 100644 index 3120e2b517..0000000000 --- a/test/unit-tests/voice-broadcast/utils/findRoomLiveVoiceBroadcastFromUserAndDevice-test.ts +++ /dev/null @@ -1,116 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { mocked } from "jest-mock"; -import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; - -import { - findRoomLiveVoiceBroadcastFromUserAndDevice, - VoiceBroadcastInfoEventType, - VoiceBroadcastInfoState, -} from "../../../../src/voice-broadcast"; -import { mkEvent, stubClient } from "../../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "./test-utils"; - -describe("findRoomLiveVoiceBroadcastFromUserAndDevice", () => { - const roomId = "!room:example.com"; - let client: MatrixClient; - let room: Room; - - const itShouldReturnNull = () => { - it("should return null", () => { - expect( - findRoomLiveVoiceBroadcastFromUserAndDevice(room, client.getUserId()!, client.getDeviceId()!), - ).toBeNull(); - }); - }; - - beforeAll(() => { - client = stubClient(); - room = new Room(roomId, client, client.getUserId()!); - jest.spyOn(room.currentState, "getStateEvents"); - mocked(client.getRoom).mockImplementation((getRoomId: string) => { - if (getRoomId === roomId) return room; - return null; - }); - }); - - describe("when there is no info event", () => { - itShouldReturnNull(); - }); - - describe("when there is an info event without content", () => { - beforeEach(() => { - room.currentState.setStateEvents([ - mkEvent({ - event: true, - type: VoiceBroadcastInfoEventType, - room: roomId, - user: client.getUserId()!, - content: {}, - }), - ]); - }); - - itShouldReturnNull(); - }); - - describe("when there is a stopped info event", () => { - beforeEach(() => { - room.currentState.setStateEvents([ - mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Stopped, - client.getUserId()!, - client.getDeviceId(), - ), - ]); - }); - - itShouldReturnNull(); - }); - - describe("when there is a started info event from another device", () => { - beforeEach(() => { - const event = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Stopped, - client.getUserId()!, - "JKL123", - ); - room.currentState.setStateEvents([event]); - }); - - itShouldReturnNull(); - }); - - describe("when there is a started info event", () => { - let event: MatrixEvent; - - beforeEach(() => { - event = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Started, - client.getUserId()!, - client.getDeviceId(), - ); - room.currentState.setStateEvents([event]); - }); - - it("should return this event", () => { - expect(room.currentState.getStateEvents).toHaveBeenCalledWith( - VoiceBroadcastInfoEventType, - client.getUserId()!, - ); - - expect(findRoomLiveVoiceBroadcastFromUserAndDevice(room, client.getUserId()!, client.getDeviceId()!)).toBe( - event, - ); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/utils/getChunkLength-test.ts b/test/unit-tests/voice-broadcast/utils/getChunkLength-test.ts deleted file mode 100644 index b2b41ae577..0000000000 --- a/test/unit-tests/voice-broadcast/utils/getChunkLength-test.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import SdkConfig from "../../../../src/SdkConfig"; -import { SettingLevel } from "../../../../src/settings/SettingLevel"; -import { Features } from "../../../../src/settings/Settings"; -import SettingsStore from "../../../../src/settings/SettingsStore"; -import { getChunkLength } from "../../../../src/voice-broadcast/utils/getChunkLength"; - -describe("getChunkLength", () => { - afterEach(() => { - SdkConfig.reset(); - }); - - describe("when there is a value provided by Sdk config", () => { - beforeEach(() => { - SdkConfig.add({ - voice_broadcast: { - chunk_length: 42, - }, - }); - }); - - it("should return this value", () => { - expect(getChunkLength()).toBe(42); - }); - }); - - describe("when Sdk config does not provide a value", () => { - beforeEach(() => { - SdkConfig.add({ - voice_broadcast: { - chunk_length: 23, - }, - }); - }); - - it("should return this value", () => { - expect(getChunkLength()).toBe(23); - }); - }); - - describe("when there are no defaults", () => { - it("should return the fallback value", () => { - expect(getChunkLength()).toBe(120); - }); - }); - - describe("when the Features.VoiceBroadcastForceSmallChunks is enabled", () => { - beforeEach(async () => { - await SettingsStore.setValue(Features.VoiceBroadcastForceSmallChunks, null, SettingLevel.DEVICE, true); - }); - - it("should return a chunk length of 15 seconds", () => { - expect(getChunkLength()).toBe(15); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/utils/getMaxBroadcastLength-test.ts b/test/unit-tests/voice-broadcast/utils/getMaxBroadcastLength-test.ts deleted file mode 100644 index 6bc38bee98..0000000000 --- a/test/unit-tests/voice-broadcast/utils/getMaxBroadcastLength-test.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import SdkConfig, { DEFAULTS } from "../../../../src/SdkConfig"; -import { getMaxBroadcastLength } from "../../../../src/voice-broadcast"; - -describe("getMaxBroadcastLength", () => { - afterEach(() => { - SdkConfig.reset(); - }); - - describe("when there is a value provided by Sdk config", () => { - beforeEach(() => { - SdkConfig.put({ - voice_broadcast: { - max_length: 42, - }, - }); - }); - - it("should return this value", () => { - expect(getMaxBroadcastLength()).toBe(42); - }); - }); - - describe("when Sdk config does not provide a value", () => { - it("should return this value", () => { - expect(getMaxBroadcastLength()).toBe(DEFAULTS.voice_broadcast!.max_length); - }); - }); - - describe("if there are no defaults", () => { - it("should return the fallback value", () => { - expect(DEFAULTS.voice_broadcast!.max_length).toBe(4 * 60 * 60); - expect(getMaxBroadcastLength()).toBe(4 * 60 * 60); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/utils/hasRoomLiveVoiceBroadcast-test.ts b/test/unit-tests/voice-broadcast/utils/hasRoomLiveVoiceBroadcast-test.ts deleted file mode 100644 index 589e1c46a7..0000000000 --- a/test/unit-tests/voice-broadcast/utils/hasRoomLiveVoiceBroadcast-test.ts +++ /dev/null @@ -1,195 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { mocked } from "jest-mock"; -import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; - -import { - hasRoomLiveVoiceBroadcast, - VoiceBroadcastInfoEventType, - VoiceBroadcastInfoState, -} from "../../../../src/voice-broadcast"; -import { mkEvent, stubClient } from "../../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "./test-utils"; - -describe("hasRoomLiveVoiceBroadcast", () => { - const otherUserId = "@other:example.com"; - const otherDeviceId = "ASD123"; - const roomId = "!room:example.com"; - let client: MatrixClient; - let room: Room; - let expectedEvent: MatrixEvent | null = null; - - const addVoiceBroadcastInfoEvent = ( - state: VoiceBroadcastInfoState, - userId: string, - deviceId: string, - startedEvent?: MatrixEvent, - ): MatrixEvent => { - const infoEvent = mkVoiceBroadcastInfoStateEvent(room.roomId, state, userId, deviceId, startedEvent); - room.addLiveEvents([infoEvent]); - room.currentState.setStateEvents([infoEvent]); - room.relations.aggregateChildEvent(infoEvent); - return infoEvent; - }; - - const itShouldReturnTrueTrue = () => { - it("should return true/true", async () => { - expect(await hasRoomLiveVoiceBroadcast(client, room, client.getSafeUserId())).toEqual({ - hasBroadcast: true, - infoEvent: expectedEvent, - startedByUser: true, - }); - }); - }; - - const itShouldReturnTrueFalse = () => { - it("should return true/false", async () => { - expect(await hasRoomLiveVoiceBroadcast(client, room, client.getSafeUserId())).toEqual({ - hasBroadcast: true, - infoEvent: expectedEvent, - startedByUser: false, - }); - }); - }; - - const itShouldReturnFalseFalse = () => { - it("should return false/false", async () => { - expect(await hasRoomLiveVoiceBroadcast(client, room, client.getSafeUserId())).toEqual({ - hasBroadcast: false, - infoEvent: null, - startedByUser: false, - }); - }); - }; - - beforeEach(() => { - client = stubClient(); - room = new Room(roomId, client, client.getSafeUserId()); - mocked(client.getRoom).mockImplementation((roomId: string): Room | null => { - return roomId === room.roomId ? room : null; - }); - expectedEvent = null; - }); - - describe("when there is no voice broadcast info at all", () => { - itShouldReturnFalseFalse(); - }); - - describe("when the »state« prop is missing", () => { - beforeEach(() => { - room.currentState.setStateEvents([ - mkEvent({ - event: true, - room: room.roomId, - user: client.getSafeUserId(), - type: VoiceBroadcastInfoEventType, - skey: client.getSafeUserId(), - content: {}, - }), - ]); - }); - itShouldReturnFalseFalse(); - }); - - describe("when there is a live broadcast from the current and another user", () => { - beforeEach(() => { - expectedEvent = addVoiceBroadcastInfoEvent( - VoiceBroadcastInfoState.Started, - client.getSafeUserId(), - client.getDeviceId()!, - ); - addVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Started, otherUserId, otherDeviceId); - }); - - itShouldReturnTrueTrue(); - }); - - describe("when there are only stopped info events", () => { - beforeEach(() => { - addVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Stopped, client.getSafeUserId(), client.getDeviceId()!); - addVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Stopped, otherUserId, otherDeviceId); - }); - - itShouldReturnFalseFalse(); - }); - - describe("when there is a live, started broadcast from the current user", () => { - beforeEach(() => { - expectedEvent = addVoiceBroadcastInfoEvent( - VoiceBroadcastInfoState.Started, - client.getSafeUserId(), - client.getDeviceId()!, - ); - }); - - itShouldReturnTrueTrue(); - }); - - describe("when there is a live, paused broadcast from the current user", () => { - beforeEach(() => { - expectedEvent = addVoiceBroadcastInfoEvent( - VoiceBroadcastInfoState.Started, - client.getSafeUserId(), - client.getDeviceId()!, - ); - addVoiceBroadcastInfoEvent( - VoiceBroadcastInfoState.Paused, - client.getSafeUserId(), - client.getDeviceId()!, - expectedEvent, - ); - }); - - itShouldReturnTrueTrue(); - }); - - describe("when there is a live, resumed broadcast from the current user", () => { - beforeEach(() => { - expectedEvent = addVoiceBroadcastInfoEvent( - VoiceBroadcastInfoState.Started, - client.getSafeUserId(), - client.getDeviceId()!, - ); - addVoiceBroadcastInfoEvent( - VoiceBroadcastInfoState.Resumed, - client.getSafeUserId(), - client.getDeviceId()!, - expectedEvent, - ); - }); - - itShouldReturnTrueTrue(); - }); - - describe("when there was a live broadcast, that has been stopped", () => { - beforeEach(() => { - const startedEvent = addVoiceBroadcastInfoEvent( - VoiceBroadcastInfoState.Started, - client.getSafeUserId(), - client.getDeviceId()!, - ); - addVoiceBroadcastInfoEvent( - VoiceBroadcastInfoState.Stopped, - client.getSafeUserId(), - client.getDeviceId()!, - startedEvent, - ); - }); - - itShouldReturnFalseFalse(); - }); - - describe("when there is a live broadcast from another user", () => { - beforeEach(() => { - expectedEvent = addVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Started, otherUserId, otherDeviceId); - }); - - itShouldReturnTrueFalse(); - }); -}); diff --git a/test/unit-tests/voice-broadcast/utils/isRelatedToVoiceBroadcast-test.ts b/test/unit-tests/voice-broadcast/utils/isRelatedToVoiceBroadcast-test.ts deleted file mode 100644 index 17160b8b2b..0000000000 --- a/test/unit-tests/voice-broadcast/utils/isRelatedToVoiceBroadcast-test.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { EventType, MatrixEvent, RelationType, Room, MatrixClient } from "matrix-js-sdk/src/matrix"; -import { mocked } from "jest-mock"; - -import { isRelatedToVoiceBroadcast, VoiceBroadcastInfoState } from "../../../../src/voice-broadcast"; -import { mkEvent, stubClient } from "../../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "./test-utils"; - -const mkRelatedEvent = ( - room: Room, - relationType: RelationType, - relatesTo: MatrixEvent | undefined, - client: MatrixClient, -): MatrixEvent => { - const event = mkEvent({ - event: true, - type: EventType.RoomMessage, - room: room.roomId, - content: { - "m.relates_to": { - rel_type: relationType, - event_id: relatesTo?.getId(), - }, - }, - user: client.getSafeUserId(), - }); - room.addLiveEvents([event]); - return event; -}; - -describe("isRelatedToVoiceBroadcast", () => { - const roomId = "!room:example.com"; - let client: MatrixClient; - let room: Room; - let broadcastEvent: MatrixEvent; - let nonBroadcastEvent: MatrixEvent; - - beforeAll(() => { - client = stubClient(); - room = new Room(roomId, client, client.getSafeUserId()); - - mocked(client.getRoom).mockImplementation((getRoomId: string): Room | null => { - if (getRoomId === roomId) return room; - return null; - }); - - broadcastEvent = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Started, - client.getSafeUserId(), - "ABC123", - ); - nonBroadcastEvent = mkEvent({ - event: true, - type: EventType.RoomMessage, - room: roomId, - content: {}, - user: client.getSafeUserId(), - }); - - room.addLiveEvents([broadcastEvent, nonBroadcastEvent]); - }); - - it("should return true if related (reference) to a broadcast event", () => { - expect( - isRelatedToVoiceBroadcast(mkRelatedEvent(room, RelationType.Reference, broadcastEvent, client), client), - ).toBe(true); - }); - - it("should return false if related (reference) is undefeind", () => { - expect(isRelatedToVoiceBroadcast(mkRelatedEvent(room, RelationType.Reference, undefined, client), client)).toBe( - false, - ); - }); - - it("should return false if related (referenireplace) to a broadcast event", () => { - expect( - isRelatedToVoiceBroadcast(mkRelatedEvent(room, RelationType.Replace, broadcastEvent, client), client), - ).toBe(false); - }); - - it("should return false if the event has no relation", () => { - const noRelationEvent = mkEvent({ - event: true, - type: EventType.RoomMessage, - room: room.roomId, - content: {}, - user: client.getSafeUserId(), - }); - expect(isRelatedToVoiceBroadcast(noRelationEvent, client)).toBe(false); - }); - - it("should return false for an unknown room", () => { - const otherRoom = new Room("!other:example.com", client, client.getSafeUserId()); - expect( - isRelatedToVoiceBroadcast( - mkRelatedEvent(otherRoom, RelationType.Reference, broadcastEvent, client), - client, - ), - ).toBe(false); - }); -}); diff --git a/test/unit-tests/voice-broadcast/utils/pauseNonLiveBroadcastFromOtherRoom-test.ts b/test/unit-tests/voice-broadcast/utils/pauseNonLiveBroadcastFromOtherRoom-test.ts deleted file mode 100644 index 565ec77053..0000000000 --- a/test/unit-tests/voice-broadcast/utils/pauseNonLiveBroadcastFromOtherRoom-test.ts +++ /dev/null @@ -1,98 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; - -import { - VoiceBroadcastInfoState, - VoiceBroadcastPlayback, - VoiceBroadcastPlaybacksStore, - VoiceBroadcastRecordingsStore, -} from "../../../../src/voice-broadcast"; -import { pauseNonLiveBroadcastFromOtherRoom } from "../../../../src/voice-broadcast/utils/pauseNonLiveBroadcastFromOtherRoom"; -import { stubClient } from "../../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "./test-utils"; - -describe("pauseNonLiveBroadcastFromOtherRoom", () => { - const roomId = "!room:example.com"; - const roomId2 = "!room2@example.com"; - let room: Room; - let client: MatrixClient; - let playback: VoiceBroadcastPlayback; - let playbacks: VoiceBroadcastPlaybacksStore; - let recordings: VoiceBroadcastRecordingsStore; - - const mkPlayback = (infoState: VoiceBroadcastInfoState, roomId: string): VoiceBroadcastPlayback => { - const infoEvent = mkVoiceBroadcastInfoStateEvent( - roomId, - infoState, - client.getSafeUserId(), - client.getDeviceId()!, - ); - const playback = new VoiceBroadcastPlayback(infoEvent, client, recordings); - jest.spyOn(playback, "pause"); - playbacks.setCurrent(playback); - return playback; - }; - - beforeEach(() => { - client = stubClient(); - room = new Room(roomId, client, client.getSafeUserId()); - recordings = new VoiceBroadcastRecordingsStore(); - playbacks = new VoiceBroadcastPlaybacksStore(recordings); - jest.spyOn(playbacks, "clearCurrent"); - }); - - afterEach(() => { - playback?.destroy(); - playbacks.destroy(); - }); - - describe("when there is no current playback", () => { - it("should not clear the current playback", () => { - pauseNonLiveBroadcastFromOtherRoom(room, playbacks); - expect(playbacks.clearCurrent).not.toHaveBeenCalled(); - }); - }); - - describe("when listening to a live broadcast in another room", () => { - beforeEach(() => { - playback = mkPlayback(VoiceBroadcastInfoState.Started, roomId2); - }); - - it("should not clear current / pause the playback", () => { - pauseNonLiveBroadcastFromOtherRoom(room, playbacks); - expect(playbacks.clearCurrent).not.toHaveBeenCalled(); - expect(playback.pause).not.toHaveBeenCalled(); - }); - }); - - describe("when listening to a non-live broadcast in the same room", () => { - beforeEach(() => { - playback = mkPlayback(VoiceBroadcastInfoState.Stopped, roomId); - }); - - it("should not clear current / pause the playback", () => { - pauseNonLiveBroadcastFromOtherRoom(room, playbacks); - expect(playbacks.clearCurrent).not.toHaveBeenCalled(); - expect(playback.pause).not.toHaveBeenCalled(); - }); - }); - - describe("when listening to a non-live broadcast in another room", () => { - beforeEach(() => { - playback = mkPlayback(VoiceBroadcastInfoState.Stopped, roomId2); - }); - - it("should clear current and pause the playback", () => { - pauseNonLiveBroadcastFromOtherRoom(room, playbacks); - expect(playbacks.getCurrent()).toBeNull(); - expect(playback.pause).toHaveBeenCalled(); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/utils/retrieveStartedInfoEvent-test.ts b/test/unit-tests/voice-broadcast/utils/retrieveStartedInfoEvent-test.ts deleted file mode 100644 index 9bac7aed3e..0000000000 --- a/test/unit-tests/voice-broadcast/utils/retrieveStartedInfoEvent-test.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { mocked } from "jest-mock"; -import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; - -import { - retrieveStartedInfoEvent, - VoiceBroadcastInfoEventType, - VoiceBroadcastInfoState, -} from "../../../../src/voice-broadcast"; -import { mkEvent, stubClient } from "../../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "./test-utils"; - -describe("retrieveStartedInfoEvent", () => { - let client: MatrixClient; - let room: Room; - - const mkStartEvent = () => { - return mkVoiceBroadcastInfoStateEvent( - room.roomId, - VoiceBroadcastInfoState.Started, - client.getUserId()!, - client.deviceId!, - ); - }; - - const mkStopEvent = (startEvent: MatrixEvent) => { - return mkVoiceBroadcastInfoStateEvent( - room.roomId, - VoiceBroadcastInfoState.Stopped, - client.getUserId()!, - client.deviceId!, - startEvent, - ); - }; - - beforeEach(() => { - client = stubClient(); - room = new Room("!room:example.com", client, client.getUserId()!); - mocked(client.getRoom).mockImplementation((roomId: string): Room | null => { - if (roomId === room.roomId) return room; - return null; - }); - }); - - it("when passing a started event, it should return the event", async () => { - const event = mkStartEvent(); - expect(await retrieveStartedInfoEvent(event, client)).toBe(event); - }); - - it("when passing an event without relation, it should return null", async () => { - const event = mkEvent({ - event: true, - type: VoiceBroadcastInfoEventType, - user: client.getUserId()!, - content: {}, - }); - expect(await retrieveStartedInfoEvent(event, client)).toBeNull(); - }); - - it("when the room contains the event, it should return it", async () => { - const startEvent = mkStartEvent(); - const stopEvent = mkStopEvent(startEvent); - room.addLiveEvents([startEvent]); - expect(await retrieveStartedInfoEvent(stopEvent, client)).toBe(startEvent); - }); - - it("when the room not contains the event, it should fetch it", async () => { - const startEvent = mkStartEvent(); - const stopEvent = mkStopEvent(startEvent); - mocked(client.fetchRoomEvent).mockResolvedValue(startEvent.event); - expect((await retrieveStartedInfoEvent(stopEvent, client))?.getId()).toBe(startEvent.getId()); - expect(client.fetchRoomEvent).toHaveBeenCalledWith(room.roomId, startEvent.getId()); - }); -}); diff --git a/test/unit-tests/voice-broadcast/utils/setUpVoiceBroadcastPreRecording-test.ts b/test/unit-tests/voice-broadcast/utils/setUpVoiceBroadcastPreRecording-test.ts deleted file mode 100644 index 40c94aa371..0000000000 --- a/test/unit-tests/voice-broadcast/utils/setUpVoiceBroadcastPreRecording-test.ts +++ /dev/null @@ -1,123 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { mocked } from "jest-mock"; -import { MatrixClient, MatrixEvent, Room, SyncState } from "matrix-js-sdk/src/matrix"; - -import Modal from "../../../../src/Modal"; -import { - VoiceBroadcastInfoState, - VoiceBroadcastPlayback, - VoiceBroadcastPlaybacksStore, - VoiceBroadcastPreRecording, - VoiceBroadcastPreRecordingStore, - VoiceBroadcastRecordingsStore, -} from "../../../../src/voice-broadcast"; -import { setUpVoiceBroadcastPreRecording } from "../../../../src/voice-broadcast/utils/setUpVoiceBroadcastPreRecording"; -import { mkRoomMemberJoinEvent, stubClient } from "../../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "./test-utils"; - -jest.mock("../../../../src/Modal"); - -describe("setUpVoiceBroadcastPreRecording", () => { - const roomId = "!room:example.com"; - let client: MatrixClient; - let userId: string; - let room: Room; - let preRecordingStore: VoiceBroadcastPreRecordingStore; - let infoEvent: MatrixEvent; - let playback: VoiceBroadcastPlayback; - let playbacksStore: VoiceBroadcastPlaybacksStore; - let recordingsStore: VoiceBroadcastRecordingsStore; - let preRecording: VoiceBroadcastPreRecording | null; - - const itShouldNotCreateAPreRecording = () => { - it("should return null", () => { - expect(preRecording).toBeNull(); - }); - - it("should not create a broadcast pre recording", () => { - expect(preRecordingStore.getCurrent()).toBeNull(); - }); - }; - - const setUpPreRecording = async () => { - preRecording = await setUpVoiceBroadcastPreRecording( - room, - client, - playbacksStore, - recordingsStore, - preRecordingStore, - ); - }; - - beforeEach(() => { - client = stubClient(); - userId = client.getSafeUserId(); - room = new Room(roomId, client, userId); - infoEvent = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Started, - client.getUserId()!, - client.getDeviceId()!, - ); - preRecording = null; - preRecordingStore = new VoiceBroadcastPreRecordingStore(); - recordingsStore = new VoiceBroadcastRecordingsStore(); - playback = new VoiceBroadcastPlayback(infoEvent, client, recordingsStore); - jest.spyOn(playback, "pause"); - playbacksStore = new VoiceBroadcastPlaybacksStore(recordingsStore); - }); - - describe("when trying to start a broadcast if there is no connection", () => { - beforeEach(async () => { - mocked(client.getSyncState).mockReturnValue(SyncState.Error); - await setUpPreRecording(); - }); - - it("should show an info dialog and not set up a pre-recording", () => { - expect(preRecordingStore.getCurrent()).toBeNull(); - expect(Modal.createDialog).toMatchSnapshot(); - }); - }); - - describe("when setting up a pre-recording", () => { - describe("and there is no user id", () => { - beforeEach(async () => { - mocked(client.getUserId).mockReturnValue(null); - await setUpPreRecording(); - }); - - itShouldNotCreateAPreRecording(); - }); - - describe("and there is no room member", () => { - beforeEach(async () => { - // check test precondition - expect(room.getMember(userId)).toBeNull(); - await setUpPreRecording(); - }); - - itShouldNotCreateAPreRecording(); - }); - - describe("and there is a room member and listening to another broadcast", () => { - beforeEach(async () => { - playbacksStore.setCurrent(playback); - room.currentState.setStateEvents([mkRoomMemberJoinEvent(userId, roomId)]); - await setUpPreRecording(); - }); - - it("should pause the current playback and create a voice broadcast pre-recording", () => { - expect(playback.pause).toHaveBeenCalled(); - expect(playbacksStore.getCurrent()).toBeNull(); - expect(preRecording).toBeInstanceOf(VoiceBroadcastPreRecording); - }); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastRecordingTile-test.ts b/test/unit-tests/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastRecordingTile-test.ts deleted file mode 100644 index 6448e3d2e7..0000000000 --- a/test/unit-tests/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastRecordingTile-test.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { mocked } from "jest-mock"; -import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; - -import { shouldDisplayAsVoiceBroadcastRecordingTile, VoiceBroadcastInfoState } from "../../../../src/voice-broadcast"; -import { createTestClient } from "../../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "./test-utils"; - -type TestTuple = [string | null, string, string, string, VoiceBroadcastInfoState, boolean]; - -const testCases: TestTuple[] = [ - [ - "@user1:example.com", // own MXID - "@user1:example.com", // sender MXID - "ABC123", // own device ID - "ABC123", // sender device ID - VoiceBroadcastInfoState.Started, - true, // expected return value - ], - ["@user1:example.com", "@user1:example.com", "ABC123", "ABC123", VoiceBroadcastInfoState.Paused, true], - ["@user1:example.com", "@user1:example.com", "ABC123", "ABC123", VoiceBroadcastInfoState.Resumed, true], - ["@user1:example.com", "@user1:example.com", "ABC123", "ABC123", VoiceBroadcastInfoState.Stopped, false], - ["@user2:example.com", "@user1:example.com", "ABC123", "ABC123", VoiceBroadcastInfoState.Started, false], - [null, "@user1:example.com", "ABC123", "ABC123", VoiceBroadcastInfoState.Started, false], - // other device - ["@user1:example.com", "@user1:example.com", "ABC123", "JKL123", VoiceBroadcastInfoState.Started, false], - ["@user1:example.com", "@user1:example.com", "ABC123", "JKL123", VoiceBroadcastInfoState.Paused, false], - ["@user1:example.com", "@user1:example.com", "ABC123", "JKL123", VoiceBroadcastInfoState.Resumed, false], -]; - -describe("shouldDisplayAsVoiceBroadcastRecordingTile", () => { - let event: MatrixEvent; - let client: MatrixClient; - - beforeAll(() => { - client = createTestClient(); - }); - - describe.each<TestTuple>(testCases)( - "when called with user »%s«, sender »%s«, device »%s«, sender device »%s« state »%s«", - (userId, senderId, deviceId, senderDeviceId, state, expected) => { - beforeEach(() => { - event = mkVoiceBroadcastInfoStateEvent("!room:example.com", state, senderId, senderDeviceId); - mocked(client.getUserId).mockReturnValue(userId); - mocked(client.getDeviceId).mockReturnValue(deviceId); - }); - - it(`should return ${expected}`, () => { - expect(shouldDisplayAsVoiceBroadcastRecordingTile(state, client, event)).toBe(expected); - }); - }, - ); - - it("should return false, when all params are null", () => { - event = mkVoiceBroadcastInfoStateEvent("!room:example.com", null, null, null); - // @ts-ignore Simulate null state received for any reason. - expect(shouldDisplayAsVoiceBroadcastRecordingTile(null, client, event)).toBe(false); - }); - - it("should return false, when all params are undefined", () => { - event = mkVoiceBroadcastInfoStateEvent("!room:example.com", undefined, undefined, undefined); - // @ts-ignore Simulate undefined state received for any reason. - expect(shouldDisplayAsVoiceBroadcastRecordingTile(undefined, client, event)).toBe(false); - }); -}); diff --git a/test/unit-tests/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastTile-test.ts b/test/unit-tests/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastTile-test.ts deleted file mode 100644 index fd3d1f91c4..0000000000 --- a/test/unit-tests/voice-broadcast/utils/shouldDisplayAsVoiceBroadcastTile-test.ts +++ /dev/null @@ -1,138 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { EventType, IEvent, MatrixEvent } from "matrix-js-sdk/src/matrix"; - -import { - shouldDisplayAsVoiceBroadcastTile, - VoiceBroadcastInfoEventType, - VoiceBroadcastInfoState, -} from "../../../../src/voice-broadcast"; -import { mkEvent } from "../../../test-utils"; - -describe("shouldDisplayAsVoiceBroadcastTile", () => { - let event: MatrixEvent; - const roomId = "!room:example.com"; - const senderId = "@user:example.com"; - - const itShouldReturnFalse = () => { - it("should return false", () => { - expect(shouldDisplayAsVoiceBroadcastTile(event)).toBe(false); - }); - }; - - const itShouldReturnTrue = () => { - it("should return true", () => { - expect(shouldDisplayAsVoiceBroadcastTile(event)).toBe(true); - }); - }; - - describe("when a broken event occurs", () => { - beforeEach(() => { - event = 23 as unknown as MatrixEvent; - }); - - itShouldReturnFalse(); - }); - - describe("when a non-voice broadcast info event occurs", () => { - beforeEach(() => { - event = mkEvent({ - event: true, - type: EventType.RoomMessage, - room: roomId, - user: senderId, - content: {}, - }); - }); - - itShouldReturnFalse(); - }); - - describe("when a voice broadcast info event with empty content occurs", () => { - beforeEach(() => { - event = mkEvent({ - event: true, - type: VoiceBroadcastInfoEventType, - room: roomId, - user: senderId, - content: {}, - }); - }); - - itShouldReturnFalse(); - }); - - describe("when a voice broadcast info event with undefined content occurs", () => { - beforeEach(() => { - event = mkEvent({ - event: true, - type: VoiceBroadcastInfoEventType, - room: roomId, - user: senderId, - content: {}, - }); - event.getContent = () => ({}) as any; - }); - - itShouldReturnFalse(); - }); - - describe("when a voice broadcast info event in state started occurs", () => { - beforeEach(() => { - event = mkEvent({ - event: true, - type: VoiceBroadcastInfoEventType, - room: roomId, - user: senderId, - content: { - state: VoiceBroadcastInfoState.Started, - }, - }); - }); - - itShouldReturnTrue(); - }); - - describe("when a redacted event occurs", () => { - beforeEach(() => { - event = mkEvent({ - event: true, - type: VoiceBroadcastInfoEventType, - room: roomId, - user: senderId, - content: {}, - unsigned: { - redacted_because: {} as unknown as IEvent, - }, - }); - event.getContent = () => ({}) as any; - }); - - itShouldReturnTrue(); - }); - - describe.each([VoiceBroadcastInfoState.Paused, VoiceBroadcastInfoState.Resumed, VoiceBroadcastInfoState.Stopped])( - "when a voice broadcast info event in state %s occurs", - (state: VoiceBroadcastInfoState) => { - beforeEach(() => { - event = mkEvent({ - event: true, - type: VoiceBroadcastInfoEventType, - room: roomId, - user: senderId, - content: { - state, - }, - }); - }); - - itShouldReturnFalse(); - }, - ); -}); diff --git a/test/unit-tests/voice-broadcast/utils/startNewVoiceBroadcastRecording-test.ts b/test/unit-tests/voice-broadcast/utils/startNewVoiceBroadcastRecording-test.ts deleted file mode 100644 index c22920c174..0000000000 --- a/test/unit-tests/voice-broadcast/utils/startNewVoiceBroadcastRecording-test.ts +++ /dev/null @@ -1,234 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { mocked } from "jest-mock"; -import { EventType, ISendEventResponse, MatrixClient, MatrixEvent, Room, SyncState } from "matrix-js-sdk/src/matrix"; - -import Modal from "../../../../src/Modal"; -import { - startNewVoiceBroadcastRecording, - VoiceBroadcastInfoEventType, - VoiceBroadcastInfoState, - VoiceBroadcastRecordingsStore, - VoiceBroadcastRecording, - VoiceBroadcastPlaybacksStore, - VoiceBroadcastPlayback, -} from "../../../../src/voice-broadcast"; -import { mkEvent, stubClient } from "../../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "./test-utils"; - -jest.mock("../../../../src/voice-broadcast/models/VoiceBroadcastRecording", () => ({ - VoiceBroadcastRecording: jest.fn(), -})); - -jest.mock("../../../../src/Modal"); - -describe("startNewVoiceBroadcastRecording", () => { - const roomId = "!room:example.com"; - const otherUserId = "@other:example.com"; - let client: MatrixClient; - let playbacksStore: VoiceBroadcastPlaybacksStore; - let recordingsStore: VoiceBroadcastRecordingsStore; - let room: Room; - let infoEvent: MatrixEvent; - let otherEvent: MatrixEvent; - let result: VoiceBroadcastRecording | null; - - beforeEach(() => { - client = stubClient(); - room = new Room(roomId, client, client.getUserId()!); - jest.spyOn(room.currentState, "maySendStateEvent"); - - mocked(client.getRoom).mockImplementation((getRoomId: string) => { - if (getRoomId === roomId) { - return room; - } - - return null; - }); - mocked(client.sendStateEvent).mockImplementation( - (sendRoomId: string, eventType: string, content: any, stateKey: string): Promise<ISendEventResponse> => { - if (sendRoomId === roomId && eventType === VoiceBroadcastInfoEventType) { - return Promise.resolve({ event_id: infoEvent.getId()! }); - } - - throw new Error("Unexpected sendStateEvent call"); - }, - ); - - infoEvent = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Started, - client.getUserId()!, - client.getDeviceId()!, - ); - otherEvent = mkEvent({ - event: true, - type: EventType.RoomMember, - content: {}, - user: client.getUserId()!, - room: roomId, - skey: "", - }); - - playbacksStore = new VoiceBroadcastPlaybacksStore(recordingsStore); - recordingsStore = { - setCurrent: jest.fn(), - getCurrent: jest.fn(), - } as unknown as VoiceBroadcastRecordingsStore; - - mocked(VoiceBroadcastRecording).mockImplementation((infoEvent: MatrixEvent, client: MatrixClient): any => { - return { - infoEvent, - client, - start: jest.fn(), - } as unknown as VoiceBroadcastRecording; - }); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe("when trying to start a broadcast if there is no connection", () => { - beforeEach(async () => { - mocked(client.getSyncState).mockReturnValue(SyncState.Error); - result = await startNewVoiceBroadcastRecording(room, client, playbacksStore, recordingsStore); - }); - - it("should show an info dialog and not start a recording", () => { - expect(result).toBeNull(); - expect(Modal.createDialog).toMatchSnapshot(); - }); - }); - - describe("when the current user is allowed to send voice broadcast info state events", () => { - beforeEach(() => { - mocked(room.currentState.maySendStateEvent).mockReturnValue(true); - }); - - describe("when currently listening to a broadcast and there is no recording", () => { - let playback: VoiceBroadcastPlayback; - - beforeEach(() => { - playback = new VoiceBroadcastPlayback(infoEvent, client, recordingsStore); - jest.spyOn(playback, "pause"); - playbacksStore.setCurrent(playback); - }); - - it("should stop listen to the current broadcast and create a new recording", async () => { - mocked(client.sendStateEvent).mockImplementation( - async ( - _roomId: string, - _eventType: string, - _content: any, - _stateKey = "", - ): Promise<ISendEventResponse> => { - window.setTimeout(() => { - // emit state events after resolving the promise - room.currentState.setStateEvents([otherEvent]); - room.currentState.setStateEvents([infoEvent]); - }, 0); - return { event_id: infoEvent.getId()! }; - }, - ); - const recording = await startNewVoiceBroadcastRecording(room, client, playbacksStore, recordingsStore); - expect(recording).not.toBeNull(); - - // expect to stop and clear the current playback - expect(playback.pause).toHaveBeenCalled(); - expect(playbacksStore.getCurrent()).toBeNull(); - - expect(client.sendStateEvent).toHaveBeenCalledWith( - roomId, - VoiceBroadcastInfoEventType, - { - chunk_length: 120, - device_id: client.getDeviceId(), - state: VoiceBroadcastInfoState.Started, - }, - client.getUserId()!, - ); - expect(recording!.infoEvent).toBe(infoEvent); - expect(recording!.start).toHaveBeenCalled(); - }); - }); - - describe("when there is already a current voice broadcast", () => { - beforeEach(async () => { - mocked(recordingsStore.getCurrent).mockReturnValue(new VoiceBroadcastRecording(infoEvent, client)); - - result = await startNewVoiceBroadcastRecording(room, client, playbacksStore, recordingsStore); - }); - - it("should not start a voice broadcast", () => { - expect(result).toBeNull(); - }); - - it("should show an info dialog", () => { - expect(Modal.createDialog).toMatchSnapshot(); - }); - }); - - describe("when there already is a live broadcast of the current user in the room", () => { - beforeEach(async () => { - room.currentState.setStateEvents([ - mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Resumed, - client.getUserId()!, - client.getDeviceId()!, - ), - ]); - - result = await startNewVoiceBroadcastRecording(room, client, playbacksStore, recordingsStore); - }); - - it("should not start a voice broadcast", () => { - expect(result).toBeNull(); - }); - - it("should show an info dialog", () => { - expect(Modal.createDialog).toMatchSnapshot(); - }); - }); - - describe("when there already is a live broadcast of another user", () => { - beforeEach(async () => { - room.currentState.setStateEvents([ - mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Resumed, otherUserId, "ASD123"), - ]); - - result = await startNewVoiceBroadcastRecording(room, client, playbacksStore, recordingsStore); - }); - - it("should not start a voice broadcast", () => { - expect(result).toBeNull(); - }); - - it("should show an info dialog", () => { - expect(Modal.createDialog).toMatchSnapshot(); - }); - }); - }); - - describe("when the current user is not allowed to send voice broadcast info state events", () => { - beforeEach(async () => { - mocked(room.currentState.maySendStateEvent).mockReturnValue(false); - result = await startNewVoiceBroadcastRecording(room, client, playbacksStore, recordingsStore); - }); - - it("should not start a voice broadcast", () => { - expect(result).toBeNull(); - }); - - it("should show an info dialog", () => { - expect(Modal.createDialog).toMatchSnapshot(); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/utils/test-utils.ts b/test/unit-tests/voice-broadcast/utils/test-utils.ts deleted file mode 100644 index 4d465e84dc..0000000000 --- a/test/unit-tests/voice-broadcast/utils/test-utils.ts +++ /dev/null @@ -1,131 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { Optional } from "matrix-events-sdk"; -import { EventType, IContent, MatrixEvent, MsgType, RelationType, Room, RoomMember } from "matrix-js-sdk/src/matrix"; - -import { SdkContextClass } from "../../../../src/contexts/SDKContext"; -import { - VoiceBroadcastPlayback, - VoiceBroadcastPreRecording, - VoiceBroadcastRecording, -} from "../../../../src/voice-broadcast"; -import { - VoiceBroadcastChunkEventType, - VoiceBroadcastInfoEventType, - VoiceBroadcastInfoState, -} from "../../../../src/voice-broadcast/types"; -import { mkEvent } from "../../../test-utils"; - -// timestamp incremented on each call to prevent duplicate timestamp -let timestamp = new Date().getTime(); - -export const mkVoiceBroadcastInfoStateEvent = ( - roomId: Optional<string>, - state: Optional<VoiceBroadcastInfoState>, - senderId: Optional<string>, - senderDeviceId: Optional<string>, - startedInfoEvent?: MatrixEvent, - lastChunkSequence?: number, -): MatrixEvent => { - const relationContent: IContent = {}; - - if (startedInfoEvent) { - relationContent["m.relates_to"] = { - event_id: startedInfoEvent.getId(), - rel_type: "m.reference", - }; - } - - const lastChunkSequenceContent = lastChunkSequence ? { last_chunk_sequence: lastChunkSequence } : {}; - - return mkEvent({ - event: true, - // @ts-ignore allow everything here for edge test cases - room: roomId, - // @ts-ignore allow everything here for edge test cases - user: senderId, - type: VoiceBroadcastInfoEventType, - // @ts-ignore allow everything here for edge test cases - skey: senderId, - content: { - state, - device_id: senderDeviceId, - ...relationContent, - ...lastChunkSequenceContent, - }, - ts: timestamp++, - }); -}; - -export const mkVoiceBroadcastChunkEvent = ( - infoEventId: string, - userId: string, - roomId: string, - duration: number, - sequence?: number, - timestamp?: number, -): MatrixEvent => { - return mkEvent({ - event: true, - user: userId, - room: roomId, - type: EventType.RoomMessage, - content: { - msgtype: MsgType.Audio, - ["org.matrix.msc1767.audio"]: { - duration, - }, - info: { - duration, - }, - [VoiceBroadcastChunkEventType]: { - ...(sequence ? { sequence } : {}), - }, - ["m.relates_to"]: { - rel_type: RelationType.Reference, - event_id: infoEventId, - }, - }, - ts: timestamp, - }); -}; - -export const mkVoiceBroadcastPlayback = (stores: SdkContextClass): VoiceBroadcastPlayback => { - const infoEvent = mkVoiceBroadcastInfoStateEvent( - "!room:example.com", - VoiceBroadcastInfoState.Started, - "@user:example.com", - "ASD123", - ); - return new VoiceBroadcastPlayback(infoEvent, stores.client!, stores.voiceBroadcastRecordingsStore); -}; - -export const mkVoiceBroadcastRecording = (stores: SdkContextClass): VoiceBroadcastRecording => { - const infoEvent = mkVoiceBroadcastInfoStateEvent( - "!room:example.com", - VoiceBroadcastInfoState.Started, - "@user:example.com", - "ASD123", - ); - return new VoiceBroadcastRecording(infoEvent, stores.client!); -}; - -export const mkVoiceBroadcastPreRecording = (stores: SdkContextClass): VoiceBroadcastPreRecording => { - const roomId = "!room:example.com"; - const userId = "@user:example.com"; - const room = new Room(roomId, stores.client!, userId); - const roomMember = new RoomMember(roomId, userId); - return new VoiceBroadcastPreRecording( - room, - roomMember, - stores.client!, - stores.voiceBroadcastPlaybacksStore, - stores.voiceBroadcastRecordingsStore, - ); -}; diff --git a/test/unit-tests/voice-broadcast/utils/textForVoiceBroadcastStoppedEvent-test.tsx b/test/unit-tests/voice-broadcast/utils/textForVoiceBroadcastStoppedEvent-test.tsx deleted file mode 100644 index 37871993d0..0000000000 --- a/test/unit-tests/voice-broadcast/utils/textForVoiceBroadcastStoppedEvent-test.tsx +++ /dev/null @@ -1,90 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { render, RenderResult, screen } from "jest-matrix-react"; -import userEvent from "@testing-library/user-event"; -import { mocked } from "jest-mock"; -import { MatrixClient, RelationType } from "matrix-js-sdk/src/matrix"; - -import { textForVoiceBroadcastStoppedEvent, VoiceBroadcastInfoState } from "../../../../src/voice-broadcast"; -import { stubClient } from "../../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "./test-utils"; -import dis from "../../../../src/dispatcher/dispatcher"; -import { Action } from "../../../../src/dispatcher/actions"; - -jest.mock("../../../../src/dispatcher/dispatcher"); - -describe("textForVoiceBroadcastStoppedEvent", () => { - const otherUserId = "@other:example.com"; - const roomId = "!room:example.com"; - let client: MatrixClient; - - const renderText = (senderId: string, startEventId?: string) => { - const event = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Stopped, - senderId, - client.deviceId!, - ); - - if (startEventId) { - event.getContent()["m.relates_to"] = { - rel_type: RelationType.Reference, - event_id: startEventId, - }; - } - - return render(<div>{textForVoiceBroadcastStoppedEvent(event, client)()}</div>); - }; - - beforeEach(() => { - client = stubClient(); - }); - - it("should render own broadcast as expected", () => { - expect(renderText(client.getUserId()!).container).toMatchSnapshot(); - }); - - it("should render other users broadcast as expected", () => { - expect(renderText(otherUserId).container).toMatchSnapshot(); - }); - - it("should render without login as expected", () => { - mocked(client.getUserId).mockReturnValue(null); - expect(renderText(otherUserId).container).toMatchSnapshot(); - }); - - describe("when rendering an event with relation to the start event", () => { - let result: RenderResult; - - beforeEach(() => { - result = renderText(client.getUserId()!, "$start-id"); - }); - - it("should render events with relation to the start event", () => { - expect(result.container).toMatchSnapshot(); - }); - - describe("and clicking the link", () => { - beforeEach(async () => { - await userEvent.click(screen.getByRole("button")); - }); - - it("should dispatch an action to highlight the event", () => { - expect(dis.dispatch).toHaveBeenCalledWith({ - action: Action.ViewRoom, - event_id: "$start-id", - highlighted: true, - room_id: roomId, - metricsTrigger: undefined, // room doesn't change - }); - }); - }); - }); -}); diff --git a/test/unit-tests/voice-broadcast/utils/textForVoiceBroadcastStoppedEventWithoutLink-test.ts b/test/unit-tests/voice-broadcast/utils/textForVoiceBroadcastStoppedEventWithoutLink-test.ts deleted file mode 100644 index a010a6ec7c..0000000000 --- a/test/unit-tests/voice-broadcast/utils/textForVoiceBroadcastStoppedEventWithoutLink-test.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { mocked } from "jest-mock"; -import { MatrixClient } from "matrix-js-sdk/src/matrix"; - -import { textForVoiceBroadcastStoppedEventWithoutLink, VoiceBroadcastInfoState } from "../../../../src/voice-broadcast"; -import { stubClient } from "../../../test-utils"; -import { mkVoiceBroadcastInfoStateEvent } from "./test-utils"; - -describe("textForVoiceBroadcastStoppedEventWithoutLink", () => { - const otherUserId = "@other:example.com"; - const roomId = "!room:example.com"; - let client: MatrixClient; - - beforeAll(() => { - client = stubClient(); - }); - - const getText = (senderId: string, startEventId?: string) => { - const event = mkVoiceBroadcastInfoStateEvent( - roomId, - VoiceBroadcastInfoState.Stopped, - senderId, - client.deviceId!, - ); - return textForVoiceBroadcastStoppedEventWithoutLink(event); - }; - - it("when called for an own broadcast it should return the expected text", () => { - expect(getText(client.getUserId()!)).toBe("You ended a voice broadcast"); - }); - - it("when called for other ones broadcast it should return the expected text", () => { - expect(getText(otherUserId)).toBe(`${otherUserId} ended a voice broadcast`); - }); - - it("when not logged in it should return the exptected text", () => { - mocked(client.getUserId).mockReturnValue(null); - expect(getText(otherUserId)).toBe(`${otherUserId} ended a voice broadcast`); - }); -}); diff --git a/webpack.config.js b/webpack.config.js index 14f472a189..91606e34d0 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -120,6 +120,10 @@ module.exports = (env, argv) => { return { ...development, + experiments: { + asyncWebAssembly: true, + }, + bail: true, entry: { @@ -187,18 +191,6 @@ module.exports = (env, argv) => { }, resolve: { - // We define an alternative import path so we can safely use src/ across the react-sdk - // and js-sdk. We already import from src/ where possible to ensure our source maps are - // extremely accurate (and because we're capable of compiling the layers manually rather - // than relying on partially-mangled output from babel), though we do need to fix the - // package level import (stuff like `import {Thing} from "matrix-js-sdk"` for example). - // We can't use the aliasing down below to point at src/ because that'll fail to resolve - // the package.json for the dependency. Instead, we rely on the package.json of each - // layer to have our custom alternate fields to load things in the right order. These are - // the defaults of webpack prepended with `matrix_src_`. - mainFields: ["matrix_src_browser", "matrix_src_main", "browser", "main"], - aliasFields: ["matrix_src_browser", "browser"], - // We need to specify that TS can be resolved without an extension extensions: [".js", ".json", ".ts", ".tsx"], alias: { @@ -231,13 +223,24 @@ module.exports = (env, argv) => { // Polyfill needed by counterpart "util": require.resolve("util/"), - // Polyfill needed by matrix-js-sdk/src/crypto - "buffer": require.resolve("buffer/"), // Polyfill needed by sentry "process/browser": require.resolve("process/browser"), }, + + // Enable the custom "wasm-esm" export condition [1] to indicate to + // matrix-sdk-crypto-wasm that we support the ES Module Integration + // Proposal for WebAssembly [2]. The "..." magic value means "the + // default conditions" [3]. + // + // [1]: https://nodejs.org/api/packages.html#conditional-exports + // [2]: https://github.com/webassembly/esm-integration + // [3]: https://github.com/webpack/webpack/issues/17692#issuecomment-1866272674. + conditionNames: ["matrix-org:wasm-esm", "..."], }, + // Some of our deps have broken source maps, so we have to ignore warnings or exclude them one-by-one + ignoreWarnings: [/Failed to parse source map/], + module: { noParse: [ // for cross platform compatibility use [\\\/] as the path separator @@ -250,6 +253,11 @@ module.exports = (env, argv) => { /highlight\.js[\\/]lib[\\/]languages/, ], rules: [ + { + test: /\.js$/, + enforce: "pre", + use: ["source-map-loader"], + }, { test: /\.(ts|js)x?$/, include: (f) => { @@ -671,7 +679,6 @@ module.exports = (env, argv) => { // Automatically load buffer & process modules as we use them without explicitly // importing them new webpack.ProvidePlugin({ - Buffer: ["buffer", "Buffer"], process: "process/browser", }), @@ -688,13 +695,21 @@ module.exports = (env, argv) => { output: { path: path.join(__dirname, "webapp"), - // The generated JS (and CSS, from the extraction plugin) are put in a - // unique subdirectory for the build. There will only be one such - // 'bundle' directory in the generated tarball; however, hosting - // servers can collect 'bundles' from multiple versions into one - // directory and symlink it into place - this allows users who loaded - // an older version of the application to continue to access webpack - // chunks even after the app is redeployed. + // There are a lot of assets that need to be kept in sync with each other + // (once a user loads one version of the app, they need to keep being served + // assets for that version). + // + // To deal with this, we try to put as many as possible of the referenced assets + // into a build-specific subdirectory. This includes generated javascript, as well + // as CSS extracted by the MiniCssExtractPlugin (see config above) and WASM modules + // referenced via `import` statements. + // + // Hosting servers can then collect 'bundles' from multiple versions + // into one directory, and continue to serve them even after a new version is deployed. + // This allows users who loaded an older version of the application to continue to + // access assets even after the app is redeployed. + // + // See `scripts/deploy.py` for a script which manages the deployment in this way. filename: "bundles/[fullhash]/[name].js", chunkFilename: "bundles/[fullhash]/[name].js", webassemblyModuleFilename: "bundles/[fullhash]/[modulehash].wasm", @@ -742,9 +757,11 @@ module.exports = (env, argv) => { */ function getAssetOutputPath(url, resourcePath) { const isKaTeX = resourcePath.includes("KaTeX"); + const isFontSource = resourcePath.includes("@fontsource"); // `res` is the parent dir for our own assets in various layers // `dist` is the parent dir for KaTeX assets - const prefix = /^.*[/\\](dist|res)[/\\]/; + // `files` is the parent dir for @fontsource assets + const prefix = /^.*[/\\](dist|res|files)[/\\]/; /** * Only needed for https://github.com/element-hq/element-web/pull/15939 @@ -770,6 +787,10 @@ function getAssetOutputPath(url, resourcePath) { outputDir = outputDir.substring(compoundMatch.index + compoundMatch[0].length); } + if (isFontSource) { + outputDir = "fonts"; + } + if (isKaTeX) { // Add a clearly named directory segment, rather than leaving the KaTeX // assets loose in each asset type directory. diff --git a/yarn.lock b/yarn.lock index a2bf286f1b..82c2d0491f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -34,15 +34,7 @@ dependencies: axe-core "~4.10.2" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.25.7.tgz#438f2c524071531d643c6f0188e1e28f130cebc7" - integrity sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g== - dependencies: - "@babel/highlight" "^7.25.7" - picocolors "^1.0.0" - -"@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0", "@babel/code-frame@^7.26.2": version "7.26.2" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== @@ -51,11 +43,24 @@ js-tokens "^4.0.0" picocolors "^1.0.0" -"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.25.9", "@babel/compat-data@^7.26.0": +"@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.25.7.tgz#438f2c524071531d643c6f0188e1e28f130cebc7" + integrity sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g== + dependencies: + "@babel/highlight" "^7.25.7" + picocolors "^1.0.0" + +"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.26.0": version "7.26.2" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.2.tgz#278b6b13664557de95b8f35b90d96785850bb56e" integrity sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg== +"@babel/compat-data@^7.25.9": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.3.tgz#99488264a56b2aded63983abd6a417f03b92ed02" + integrity sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g== + "@babel/core@^7.0.0", "@babel/core@^7.11.6", "@babel/core@^7.12.10", "@babel/core@^7.12.3", "@babel/core@^7.18.5", "@babel/core@^7.21.3", "@babel/core@^7.23.9": version "7.26.0" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.0.tgz#d78b6023cc8f3114ccf049eb219613f74a747b40" @@ -103,13 +108,13 @@ "@jridgewell/trace-mapping" "^0.3.25" jsesc "^3.0.2" -"@babel/generator@^7.25.9", "@babel/generator@^7.26.0": - version "7.26.2" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.2.tgz#87b75813bec87916210e5e01939a4c823d6bb74f" - integrity sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw== +"@babel/generator@^7.26.0", "@babel/generator@^7.26.3": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.3.tgz#ab8d4360544a425c90c248df7059881f4b2ce019" + integrity sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ== dependencies: - "@babel/parser" "^7.26.2" - "@babel/types" "^7.26.0" + "@babel/parser" "^7.26.3" + "@babel/types" "^7.26.3" "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.25" jsesc "^3.0.2" @@ -254,12 +259,12 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== -"@babel/helper-validator-identifier@^7.24.5", "@babel/helper-validator-identifier@^7.24.7": +"@babel/helper-validator-identifier@^7.24.5": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz#77b7f60c40b15c97df735b38a66ba1d7c3e93da5" integrity sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg== -"@babel/helper-validator-identifier@^7.25.7", "@babel/helper-validator-identifier@^7.25.9": +"@babel/helper-validator-identifier@^7.24.7", "@babel/helper-validator-identifier@^7.25.7", "@babel/helper-validator-identifier@^7.25.9": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== @@ -287,11 +292,11 @@ "@babel/types" "^7.26.0" "@babel/highlight@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.25.7.tgz#20383b5f442aa606e7b5e3043b0b1aafe9f37de5" - integrity sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw== + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.25.9.tgz#8141ce68fc73757946f983b343f1231f4691acc6" + integrity sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw== dependencies: - "@babel/helper-validator-identifier" "^7.25.7" + "@babel/helper-validator-identifier" "^7.25.9" chalk "^2.4.2" js-tokens "^4.0.0" picocolors "^1.0.0" @@ -303,12 +308,12 @@ dependencies: "@babel/types" "^7.25.8" -"@babel/parser@^7.25.9", "@babel/parser@^7.26.0", "@babel/parser@^7.26.2": - version "7.26.2" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.2.tgz#fd7b6f487cfea09889557ef5d4eeb9ff9a5abd11" - integrity sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ== +"@babel/parser@^7.25.9", "@babel/parser@^7.26.0", "@babel/parser@^7.26.3": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.3.tgz#8c51c5db6ddf08134af1ddbacf16aaab48bac234" + integrity sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA== dependencies: - "@babel/types" "^7.26.0" + "@babel/types" "^7.26.3" "@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.25.9": version "7.25.9" @@ -1060,9 +1065,9 @@ esutils "^2.0.2" "@babel/preset-react@^7.12.10", "@babel/preset-react@^7.18.6": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.25.9.tgz#5f473035dc2094bcfdbc7392d0766bd42dce173e" - integrity sha512-D3to0uSPiWE7rBrdIICCd0tJSIGpLaaGptna2+w7Pft5xMqLpA1sz99DK5TZ1TjGbdQ/VI1eCSZ06dv3lT4JOw== + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.26.3.tgz#7c5e028d623b4683c1f83a0bd4713b9100560caa" + integrity sha512-Nl03d6T9ky516DGK2YMxrTqvnpUW63TnJMOMonj+Zae0JiPC5BC9xPMSL6L8fiSpA5vP88qfygavVQvnLp+6Cw== dependencies: "@babel/helper-plugin-utils" "^7.25.9" "@babel/helper-validator-option" "^7.25.9" @@ -1121,15 +1126,15 @@ globals "^11.1.0" "@babel/traverse@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.9.tgz#a50f8fe49e7f69f53de5bea7e413cd35c5e13c84" - integrity sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw== + version "7.26.4" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.4.tgz#ac3a2a84b908dde6d463c3bfa2c5fdc1653574bd" + integrity sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w== dependencies: - "@babel/code-frame" "^7.25.9" - "@babel/generator" "^7.25.9" - "@babel/parser" "^7.25.9" + "@babel/code-frame" "^7.26.2" + "@babel/generator" "^7.26.3" + "@babel/parser" "^7.26.3" "@babel/template" "^7.25.9" - "@babel/types" "^7.25.9" + "@babel/types" "^7.26.3" debug "^4.3.1" globals "^11.1.0" @@ -1142,7 +1147,7 @@ "@babel/helper-validator-identifier" "^7.25.7" to-fast-properties "^2.0.0" -"@babel/types@^7.25.7", "@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.4.4": +"@babel/types@^7.25.7", "@babel/types@^7.4.4": version "7.26.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.0.tgz#deabd08d6b753bc8e0f198f8709fb575e31774ff" integrity sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA== @@ -1150,6 +1155,14 @@ "@babel/helper-string-parser" "^7.25.9" "@babel/helper-validator-identifier" "^7.25.9" +"@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.26.3": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.3.tgz#37e79830f04c2b5687acc77db97fbc75fb81f3c0" + integrity sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA== + dependencies: + "@babel/helper-string-parser" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -1571,10 +1584,10 @@ dependencies: "@floating-ui/dom" "^1.0.0" -"@floating-ui/react@^0.26.24": - version "0.26.25" - resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.26.25.tgz#cf4c8a2b89fab1a71712d15e6551df3bfbd2ea1d" - integrity sha512-hZOmgN0NTOzOuZxI1oIrDu3Gcl8WViIkvPMpB4xdd4QD6xAMtwgwr3VPoiyH/bLtRcS1cDnhxLSD1NsMJmwh/A== +"@floating-ui/react@^0.27.0": + version "0.27.0" + resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.27.0.tgz#e0931fd09374ab4b8ce1a1af5cb44d1ccd1bb95a" + integrity sha512-WLEksq7fJapXSJbmfiyq9pAW0a7ZFMEJToFE4oTDESxGjoa+nZu3YMjmZE2KvoUtQhqOK2yMMfWQFZyeWD0wGQ== dependencies: "@floating-ui/react-dom" "^2.1.2" "@floating-ui/utils" "^0.2.8" @@ -1585,36 +1598,47 @@ resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.8.tgz#21a907684723bbbaa5f0974cf7730bd797eb8e62" integrity sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig== -"@formatjs/ecma402-abstract@2.2.3": - version "2.2.3" - resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-2.2.3.tgz#dc5a032e1971c709b32b9ab511fa35504a7d3bc9" - integrity sha512-aElGmleuReGnk2wtYOzYFmNWYoiWWmf1pPPCYg0oiIQSJj0mjc4eUfzUXaSOJ4S8WzI/cLqnCTWjqz904FT2OQ== +"@fontsource/inconsolata@^5": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@fontsource/inconsolata/-/inconsolata-5.1.0.tgz#f6a76680173336d02d2ce4009699821a6be239ce" + integrity sha512-vYPdG3R46MhK+99De8e8MMyNad5BAb1oTnHMpojlctZyWJIcin8bKHFPUpQSNRhZ4HQL/+DCW+RTiG2RbnweTw== + +"@fontsource/inter@^5": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@fontsource/inter/-/inter-5.1.0.tgz#ab629b2c662457022d2d6a29854b8dc8ba538c47" + integrity sha512-zKZR3kf1G0noIes1frLfOHP5EXVVm0M7sV/l9f/AaYf+M/DId35FO4LkigWjqWYjTJZGgplhdv4cB+ssvCqr5A== + +"@formatjs/ecma402-abstract@2.3.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.1.tgz#cdeb3ffe1aeea9c4284b85b7e37e8e8615314c39" + integrity sha512-Ip9uV+/MpLXWRk03U/GzeJMuPeOXpJBSB5V1tjA6kJhvqssye5J5LoYLc7Z5IAHb7nR62sRoguzrFiVCP/hnzw== dependencies: - "@formatjs/fast-memoize" "2.2.3" - "@formatjs/intl-localematcher" "0.5.7" + "@formatjs/fast-memoize" "2.2.5" + "@formatjs/intl-localematcher" "0.5.9" + decimal.js "10" tslib "2" -"@formatjs/fast-memoize@2.2.3": - version "2.2.3" - resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-2.2.3.tgz#74e64109279d5244f9fc281f3ae90c407cece823" - integrity sha512-3jeJ+HyOfu8osl3GNSL4vVHUuWFXR03Iz9jjgI7RwjG6ysu/Ymdr0JRCPHfF5yGbTE6JCrd63EpvX1/WybYRbA== +"@formatjs/fast-memoize@2.2.5": + version "2.2.5" + resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-2.2.5.tgz#54a4a1793d773b72c372d3dcab3595149aee7880" + integrity sha512-6PoewUMrrcqxSoBXAOJDiW1m+AmkrAj0RiXnOMD59GRaswjXhm3MDhgepXPBgonc09oSirAJTsAggzAGQf6A6g== dependencies: tslib "2" -"@formatjs/intl-localematcher@0.5.7": - version "0.5.7" - resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.5.7.tgz#f889d076881b785d11ff993b966f527d199436d0" - integrity sha512-GGFtfHGQVFe/niOZp24Kal5b2i36eE2bNL0xi9Sg/yd0TR8aLjcteApZdHmismP5QQax1cMnZM9yWySUUjJteA== +"@formatjs/intl-localematcher@0.5.9": + version "0.5.9" + resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.5.9.tgz#43c6ee22be85b83340bcb09bdfed53657a2720db" + integrity sha512-8zkGu/sv5euxbjfZ/xmklqLyDGQSxsLqg8XOq88JW3cmJtzhCP8EtSJXlaKZnVO4beEaoiT9wj4eIoCQ9smwxA== dependencies: tslib "2" "@formatjs/intl-segmenter@^11.5.7": - version "11.7.3" - resolved "https://registry.yarnpkg.com/@formatjs/intl-segmenter/-/intl-segmenter-11.7.3.tgz#aeb49c33c81fec68419922c64c72188b659eaa5a" - integrity sha512-IvEDQRe0t0ouqaqZK2KobGt/+BhwDHdtbS8GWhdl+fjmWbhXMz2mHihu5fAYkYChum5eNfGhEF5P+bLCeYq67w== + version "11.7.7" + resolved "https://registry.yarnpkg.com/@formatjs/intl-segmenter/-/intl-segmenter-11.7.7.tgz#8a5aaa316e11ca2d31b99222e6fcf1ab539b085e" + integrity sha512-610J5xz5DxtEpa16zNR89CrvA9qWHxQFkUB3FKiGao0Nwn7i8cl+oyBhuH9SvtXF9j2LUOM9VMdVCMzJkVANNw== dependencies: - "@formatjs/ecma402-abstract" "2.2.3" - "@formatjs/intl-localematcher" "0.5.7" + "@formatjs/ecma402-abstract" "2.3.1" + "@formatjs/intl-localematcher" "0.5.9" tslib "2" "@humanwhocodes/config-array@^0.13.0": @@ -1857,9 +1881,9 @@ chalk "^4.0.0" "@jridgewell/gen-mapping@^0.3.5": - version "0.3.5" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" - integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + version "0.3.8" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz#4f0e06362e01362f823d348f1872b08f666d8142" + integrity sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA== dependencies: "@jridgewell/set-array" "^1.2.1" "@jridgewell/sourcemap-codec" "^1.4.10" @@ -2073,11 +2097,11 @@ webcrypto-core "^1.8.0" "@playwright/test@^1.40.1": - version "1.48.2" - resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.48.2.tgz#87dd40633f980872283404c8142a65744d3f13d6" - integrity sha512-54w1xCWfXuax7dz4W2M9uw0gDyh+ti/0K/MxcCUxChFh37kkdxPdfZDw5QBbuPUJHr1CiHJ1hXgSs+GgeQc5Zw== + version "1.49.0" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.49.0.tgz#74227385b58317ee076b86b56d0e1e1b25cff01e" + integrity sha512-DMulbwQURa8rNIQrf94+jPJQ4FmOVdpE5ZppRNvWVjvhC+6sOeo28r8MgIpQRYouXRtt/FCCXU7zn20jnHR4Qw== dependencies: - playwright "1.48.2" + playwright "1.49.0" "@polka/url@^1.0.0-next.24": version "1.0.0-next.28" @@ -2375,115 +2399,105 @@ resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== -"@sentry-internal/browser-utils@8.37.1": - version "8.37.1" - resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.37.1.tgz#374028d8e37047aeda14b226707e6601de65996e" - integrity sha512-OSR/V5GCsSCG7iapWtXCT/y22uo3HlawdEgfM1NIKk1mkP15UyGQtGEzZDdih2H+SNuX1mp9jQLTjr5FFp1A5w== +"@sentry-internal/browser-utils@8.43.0": + version "8.43.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.43.0.tgz#b064908a537d1cc17d8ddaf0f4c5d712557cbf40" + integrity sha512-5WhJZ3SA5sZVDBwOsChDd5JCzYcwBX7sEqBqEcm3pFru6TUihEnFIJmDIbreIyrQMwUhs3dTxnfnidgjr5z1Ag== dependencies: - "@sentry/core" "8.37.1" - "@sentry/types" "8.37.1" - "@sentry/utils" "8.37.1" + "@sentry/core" "8.43.0" -"@sentry-internal/feedback@8.37.1": - version "8.37.1" - resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-8.37.1.tgz#e2d5fc934ca3b4925a5f5d0e63549830a1cf147e" - integrity sha512-Se25NXbSapgS2S+JssR5YZ48b3OY4UGmAuBOafgnMW91LXMxRNWRbehZuNUmjjHwuywABMxjgu+Yp5uJDATX+g== +"@sentry-internal/feedback@8.43.0": + version "8.43.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-8.43.0.tgz#9477b999c9bca62335eb944a6f7246a96beb0111" + integrity sha512-rcGR2kzFu4vLXBQbI9eGJwjyToyjl36O2q/UKbiZBNJ5IFtDvKRLke6jIHq/YqiHPfFGpVtq5M/lYduDfA/eaQ== dependencies: - "@sentry/core" "8.37.1" - "@sentry/types" "8.37.1" - "@sentry/utils" "8.37.1" + "@sentry/core" "8.43.0" -"@sentry-internal/replay-canvas@8.37.1": - version "8.37.1" - resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-8.37.1.tgz#e8a5e350e486b16938b3dd99886be23b7b6eff18" - integrity sha512-1JLAaPtn1VL5vblB0BMELFV0D+KUm/iMGsrl4/JpRm0Ws5ESzQl33DhXVv1IX/ZAbx9i14EjR7MG9+Hj70tieQ== +"@sentry-internal/replay-canvas@8.43.0": + version "8.43.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-8.43.0.tgz#f5672a08c9eb588afa0bf36f07b9f5c29b5c9920" + integrity sha512-rL8G7E1GtozH8VNalRrBQNjYDJ5ChWS/vpQI5hUG11PZfvQFXEVatLvT3uO2l0xIlHm4idTsHOSLTe/usxnogQ== dependencies: - "@sentry-internal/replay" "8.37.1" - "@sentry/core" "8.37.1" - "@sentry/types" "8.37.1" - "@sentry/utils" "8.37.1" + "@sentry-internal/replay" "8.43.0" + "@sentry/core" "8.43.0" -"@sentry-internal/replay@8.37.1": - version "8.37.1" - resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-8.37.1.tgz#6dc2e3955879f6e7ab830db1ddee54e0a9b401f3" - integrity sha512-E/Plhisk/pXJjOdOU12sg8m/APTXTA21iEniidP6jW3/+O0tD/H/UovEqa4odNTqxPMa798xHQSQNt5loYiaLA== +"@sentry-internal/replay@8.43.0": + version "8.43.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-8.43.0.tgz#4e2e3844f52b47b16bf816d21857921bbfe85d62" + integrity sha512-geV5/zejLfGGwWHjylzrb1w8NI3U37GMG9/53nmv13FmTXUDF5XF2lh41KXFVYwvp7Ha4bd1FRQ9IU9YtBWskw== dependencies: - "@sentry-internal/browser-utils" "8.37.1" - "@sentry/core" "8.37.1" - "@sentry/types" "8.37.1" - "@sentry/utils" "8.37.1" + "@sentry-internal/browser-utils" "8.43.0" + "@sentry/core" "8.43.0" -"@sentry/babel-plugin-component-annotate@2.22.5": - version "2.22.5" - resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.22.5.tgz#494978f4dfd741006368f74fefee2d1858a9c3af" - integrity sha512-+93qwB9vTX1nj4hD8AMWowXZsZVkvmP9OwTqSh5d4kOeiJ+dZftUk4+FKeKkAX9lvY2reyHV8Gms5mo67c27RQ== +"@sentry/babel-plugin-component-annotate@2.22.7": + version "2.22.7" + resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.22.7.tgz#604c7e33d48528a13477e7af597c4d5fca51b8bd" + integrity sha512-aa7XKgZMVl6l04NY+3X7BP7yvQ/s8scn8KzQfTLrGRarziTlMGrsCOBQtCNWXOPEbtxAIHpZ9dsrAn5EJSivOQ== "@sentry/browser@^8.0.0": - version "8.37.1" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-8.37.1.tgz#2e6e4accc395ad9e6313e07b09415370c71e5874" - integrity sha512-5ym+iGiIpjIKKpMWi9S3/tXh9xneS+jqxwRTJqed3cb8i4ydfMAAP8sM3U8xMCWWABpWyIUW+fpewC0tkhE1aQ== + version "8.43.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-8.43.0.tgz#4eec67bc6fb278727304045b612ac392674cade6" + integrity sha512-LGvLLnfmR8+AEgFmd7Q7KHiOTiV0P1Lvio2ENDELhEqJOIiICauttibVmig+AW02qg4kMeywvleMsUYaZv2RVA== dependencies: - "@sentry-internal/browser-utils" "8.37.1" - "@sentry-internal/feedback" "8.37.1" - "@sentry-internal/replay" "8.37.1" - "@sentry-internal/replay-canvas" "8.37.1" - "@sentry/core" "8.37.1" - "@sentry/types" "8.37.1" - "@sentry/utils" "8.37.1" + "@sentry-internal/browser-utils" "8.43.0" + "@sentry-internal/feedback" "8.43.0" + "@sentry-internal/replay" "8.43.0" + "@sentry-internal/replay-canvas" "8.43.0" + "@sentry/core" "8.43.0" -"@sentry/bundler-plugin-core@2.22.5": - version "2.22.5" - resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-2.22.5.tgz#ca92e8145921fc5d4498bfd18168cad2c3376659" - integrity sha512-nfvTthV0aNM9/MwgnCi1WjAlCtau1I4kw6+oZIDOwJRDqGNziz517mYRXSsvCUebtGxDZtPcF7hSEBMSHjpncA== +"@sentry/bundler-plugin-core@2.22.7": + version "2.22.7" + resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-2.22.7.tgz#28204a224cd1fef58d157e5beeb2493947a9bc35" + integrity sha512-ouQh5sqcB8vsJ8yTTe0rf+iaUkwmeUlGNFi35IkCFUQlWJ22qS6OfvNjOqFI19e6eGUXks0c/2ieFC4+9wJ+1g== dependencies: "@babel/core" "^7.18.5" - "@sentry/babel-plugin-component-annotate" "2.22.5" - "@sentry/cli" "^2.36.1" + "@sentry/babel-plugin-component-annotate" "2.22.7" + "@sentry/cli" "2.39.1" dotenv "^16.3.1" find-up "^5.0.0" glob "^9.3.2" magic-string "0.30.8" unplugin "1.0.1" -"@sentry/cli-darwin@2.37.0": - version "2.37.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.37.0.tgz#9c890c68abf30ceaad27826212a0963b125b8bbf" - integrity sha512-CsusyMvO0eCPSN7H+sKHXS1pf637PWbS4rZak/7giz/z31/6qiXmeMlcL3f9lLZKtFPJmXVFO9uprn1wbBVF8A== +"@sentry/cli-darwin@2.39.1": + version "2.39.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.39.1.tgz#75c338a53834b4cf72f57599f4c72ffb36cf0781" + integrity sha512-kiNGNSAkg46LNGatfNH5tfsmI/kCAaPA62KQuFZloZiemTNzhy9/6NJP8HZ/GxGs8GDMxic6wNrV9CkVEgFLJQ== -"@sentry/cli-linux-arm64@2.37.0": - version "2.37.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.37.0.tgz#2070155bade6d72d6b706807c6f365c65f9b82ea" - integrity sha512-2vzUWHLZ3Ct5gpcIlfd/2Qsha+y9M8LXvbZE26VxzYrIkRoLAWcnClBv8m4XsHLMURYvz3J9QSZHMZHSO7kAzw== +"@sentry/cli-linux-arm64@2.39.1": + version "2.39.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.39.1.tgz#27db44700c33fcb1e8966257020b43f8494373e6" + integrity sha512-5VbVJDatolDrWOgaffsEM7znjs0cR8bHt9Bq0mStM3tBolgAeSDHE89NgHggfZR+DJ2VWOy4vgCwkObrUD6NQw== -"@sentry/cli-linux-arm@2.37.0": - version "2.37.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.37.0.tgz#a08c2133e8e2566074fd6fe4f68e9ffd0c85664a" - integrity sha512-Dz0qH4Yt+gGUgoVsqVt72oDj4VQynRF1QB1/Sr8g76Vbi+WxWZmUh0iFwivYVwWxdQGu/OQrE0tx946HToCRyA== +"@sentry/cli-linux-arm@2.39.1": + version "2.39.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.39.1.tgz#451683fa9a5a60b1359d104ec71334ed16f4b63c" + integrity sha512-DkENbxyRxUrfLnJLXTA4s5UL/GoctU5Cm4ER1eB7XN7p9WsamFJd/yf2KpltkjEyiTuplv0yAbdjl1KX3vKmEQ== -"@sentry/cli-linux-i686@2.37.0": - version "2.37.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.37.0.tgz#53fff0e7f232b656b0ee3413b66006ee724a4abf" - integrity sha512-MHRLGs4t/CQE1pG+mZBQixyWL6xDZfNalCjO8GMcTTbZFm44S3XRHfYJZNVCgdtnUP7b6OHGcu1v3SWE10LcwQ== +"@sentry/cli-linux-i686@2.39.1": + version "2.39.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.39.1.tgz#9965a81f97a94e8b6d1d15589e43fee158e35201" + integrity sha512-pXWVoKXCRrY7N8vc9H7mETiV9ZCz+zSnX65JQCzZxgYrayQPJTc+NPRnZTdYdk5RlAupXaFicBI2GwOCRqVRkg== -"@sentry/cli-linux-x64@2.37.0": - version "2.37.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.37.0.tgz#2fbaf51ef3884bd6561c987f01ac98f544457150" - integrity sha512-k76ClefKZaDNJZU/H3mGeR8uAzAGPzDRG/A7grzKfBeyhP3JW09L7Nz9IQcSjCK+xr399qLhM2HFCaPWQ6dlMw== +"@sentry/cli-linux-x64@2.39.1": + version "2.39.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.39.1.tgz#31fe008b02f92769543dc9919e2a5cbc4cda7889" + integrity sha512-IwayNZy+it7FWG4M9LayyUmG1a/8kT9+/IEm67sT5+7dkMIMcpmHDqL8rWcPojOXuTKaOBBjkVdNMBTXy0mXlA== -"@sentry/cli-win32-i686@2.37.0": - version "2.37.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.37.0.tgz#fa195664da27ce8c40fdb6db1bf1d125cdf587d9" - integrity sha512-FFyi5RNYQQkEg4GkP2f3BJcgQn0F4fjFDMiWkjCkftNPXQG+HFUEtrGsWr6mnHPdFouwbYg3tEPUWNxAoypvTw== +"@sentry/cli-win32-i686@2.39.1": + version "2.39.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.39.1.tgz#609e8790c49414011445e397130560c777850b35" + integrity sha512-NglnNoqHSmE+Dz/wHeIVRnV2bLMx7tIn3IQ8vXGO5HWA2f8zYJGktbkLq1Lg23PaQmeZLPGlja3gBQfZYSG10Q== -"@sentry/cli-win32-x64@2.37.0": - version "2.37.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.37.0.tgz#84fa4d070b8a4a115c46ab38f42d29580143fd26" - integrity sha512-nSMj4OcfQmyL+Tu/jWCJwhKCXFsCZW1MUk6wjjQlRt9SDLfgeapaMlK1ZvT1eZv5ZH6bj3qJfefwj4U8160uOA== +"@sentry/cli-win32-x64@2.39.1": + version "2.39.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.39.1.tgz#1a874a5570c6d162b35d9d001c96e5389d07d2cb" + integrity sha512-xv0R2CMf/X1Fte3cMWie1NXuHmUyQPDBfCyIt6k6RPFPxAYUgcqgMPznYwVMwWEA1W43PaOkSn3d8ZylsDaETw== -"@sentry/cli@^2.36.1": - version "2.37.0" - resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.37.0.tgz#dd01e933cf1caed7d7b6abab5a96044fe1c9c7a1" - integrity sha512-fM3V4gZRJR/s8lafc3O07hhOYRnvkySdPkvL/0e0XW0r+xRwqIAgQ5ECbsZO16A5weUiXVSf03ztDL1FcmbJCQ== +"@sentry/cli@2.39.1": + version "2.39.1" + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.39.1.tgz#916bb5b7567ccf7fdf94ef6cf8a2b9ab78370d29" + integrity sha512-JIb3e9vh0+OmQ0KxmexMXg9oZsR/G7HMwxt5BUIKAXZ9m17Xll4ETXTRnRUBT3sf7EpNGAmlQk1xEmVN9pYZYQ== dependencies: https-proxy-agent "^5.0.0" node-fetch "^2.6.7" @@ -2491,40 +2505,25 @@ proxy-from-env "^1.1.0" which "^2.0.2" optionalDependencies: - "@sentry/cli-darwin" "2.37.0" - "@sentry/cli-linux-arm" "2.37.0" - "@sentry/cli-linux-arm64" "2.37.0" - "@sentry/cli-linux-i686" "2.37.0" - "@sentry/cli-linux-x64" "2.37.0" - "@sentry/cli-win32-i686" "2.37.0" - "@sentry/cli-win32-x64" "2.37.0" + "@sentry/cli-darwin" "2.39.1" + "@sentry/cli-linux-arm" "2.39.1" + "@sentry/cli-linux-arm64" "2.39.1" + "@sentry/cli-linux-i686" "2.39.1" + "@sentry/cli-linux-x64" "2.39.1" + "@sentry/cli-win32-i686" "2.39.1" + "@sentry/cli-win32-x64" "2.39.1" -"@sentry/core@8.37.1": - version "8.37.1" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.37.1.tgz#4bafb25c762ec8680874056f6160df276c1cc7c6" - integrity sha512-82csXby589iDupM3VgCHJeWZagUyEEaDnbFcoZ/Z91QX2Sjq8FcF5OsforoXjw09i0XTFqlkFAnQVpDBmMXcpQ== - dependencies: - "@sentry/types" "8.37.1" - "@sentry/utils" "8.37.1" - -"@sentry/types@8.37.1": - version "8.37.1" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-8.37.1.tgz#e92a7d346cfa29116568f4ffb58f65caedee0149" - integrity sha512-ryMOTROLSLINKFEbHWvi7GigNrsQhsaScw2NddybJGztJQ5UhxIGESnxGxWCufBmWFDwd7+5u0jDPCVUJybp7w== - -"@sentry/utils@8.37.1": - version "8.37.1" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-8.37.1.tgz#6e020cd222d56d79953ea9d4630d91b3e323ceda" - integrity sha512-Qtn2IfpII12K17txG/ZtTci35XYjYi4CxbQ3j7nXY7toGv/+MqPXwV5q2i9g94XaSXlE5Wy9/hoCZoZpZs/djA== - dependencies: - "@sentry/types" "8.37.1" +"@sentry/core@8.43.0": + version "8.43.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.43.0.tgz#e96a489e87a9999199f5ac27d8860da37c1fa8b4" + integrity sha512-ktyovtjkTMNud+kC/XfqHVCoQKreIKgx/hgeRvzPwuPyd1t1KzYmRL3DBkbcWVnyOPpVTHn+RsEI1eRcVYHtvw== "@sentry/webpack-plugin@^2.7.1": - version "2.22.5" - resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-2.22.5.tgz#aafb3e526954b72759bae38bb34edecf55e68142" - integrity sha512-D8irs8H0IuLZbCS0Te5zsYGu9sABmMJTfCCkRkf7fV8S0BQZQmxnQGf9cVxcTj07RWAgnhhUtsRQzkK7MLuIwg== + version "2.22.7" + resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-2.22.7.tgz#992c6c782c736f22e72eb318745e28cc24aabad7" + integrity sha512-j5h5LZHWDlm/FQCCmEghQ9FzYXwfZdlOf3FE/X6rK6lrtx0JCAkq+uhMSasoyP4XYKL4P4vRS6WFSos4jxf/UA== dependencies: - "@sentry/bundler-plugin-core" "2.22.5" + "@sentry/bundler-plugin-core" "2.22.7" unplugin "1.0.1" uuid "^9.0.0" @@ -2562,11 +2561,11 @@ p-map "^4.0.0" "@stylistic/eslint-plugin@^2.9.0": - version "2.10.1" - resolved "https://registry.yarnpkg.com/@stylistic/eslint-plugin/-/eslint-plugin-2.10.1.tgz#809924752a1a13ebff2b0b6d7884fd61d389a907" - integrity sha512-U+4yzNXElTf9q0kEfnloI9XbOyD4cnEQCxjUI94q0+W++0GAEQvJ/slwEj9lwjDHfGADRSr+Tco/z0XJvmDfCQ== + version "2.11.0" + resolved "https://registry.yarnpkg.com/@stylistic/eslint-plugin/-/eslint-plugin-2.11.0.tgz#50d0289f36f7201055b7fa1729fdc1d8c46e93fa" + integrity sha512-PNRHbydNG5EH8NK4c+izdJlxajIR6GxcUhzsYNRsn6Myep4dsZt0qFCz3rCPnkvgO5FYibDcMqgNHUT+zvjYZw== dependencies: - "@typescript-eslint/utils" "^8.12.2" + "@typescript-eslint/utils" "^8.13.0" eslint-visitor-keys "^4.2.0" espree "^10.3.0" estraverse "^5.3.0" @@ -2706,9 +2705,9 @@ redent "^3.0.0" "@testing-library/react@^16.0.0": - version "16.0.1" - resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-16.0.1.tgz#29c0ee878d672703f5e7579f239005e4e0faa875" - integrity sha512-dSmwJVtJXmku+iocRhWOUFbrERC76TX2Mnf0ATODz8brzAZrMBbzLwQixlBSanZxR6LddK3eiwpSFZgDET1URg== + version "16.1.0" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-16.1.0.tgz#aa0c61398bac82eaf89776967e97de41ac742d71" + integrity sha512-Q2ToPvg0KsVL0ohND9A3zLJWcOXXcO8IDu3fj11KhNt0UlCWyFyvnCIBkd12tidB2lkiVRG8VFqdhcqhqnAQtg== dependencies: "@babel/runtime" "^7.12.5" @@ -2836,9 +2835,9 @@ integrity sha512-aqBg5oAGo/qh/+wxUfuMadDu2WO0MEWOblyzwaM1Ske2xilUxBfgPqapAFVAfrVTDMVwa0UMarzGot8m64IAzA== "@types/css-tree@^2.3.8": - version "2.3.8" - resolved "https://registry.yarnpkg.com/@types/css-tree/-/css-tree-2.3.8.tgz#0eabc115e45051b2f7abe51ee1531074b234ed19" - integrity sha512-zABG3nI2UENsx7AQv63tI5/ptoAG/7kQR1H0OvG+WTWYHOR5pfAT3cGgC8SdyCrgX/TTxJBZNmx82IjCXs1juQ== + version "2.3.9" + resolved "https://registry.yarnpkg.com/@types/css-tree/-/css-tree-2.3.9.tgz#54c404e0a803e7e660fdc08c84fe73ee5266cece" + integrity sha512-g1FE6xkPDP4tsccmTd6jIugjKZdxIDqAf9h2pc+4LsGgYbOyfa9phNjBHYbm6FtwIlNfT1NBx3f2zSeqO7aRAw== "@types/diff-match-patch@^1.0.32": version "1.0.36" @@ -2850,7 +2849,23 @@ resolved "https://registry.yarnpkg.com/@types/escape-html/-/escape-html-1.0.4.tgz#dc7c166b76c7b03b27e32f80edf01d91eb5d9af2" integrity sha512-qZ72SFTgUAZ5a7Tj6kf2SHLetiH5S6f8G5frB2SPQ3EyF02kxdyBFf4Tz4banE3xCgGnKgWLt//a6VuYHKYJTg== -"@types/estree@^1.0.5": +"@types/eslint-scope@^3.7.7": + version "3.7.7" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" + integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*": + version "9.6.1" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-9.6.1.tgz#d5795ad732ce81715f27f75da913004a56751584" + integrity sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*", "@types/estree@^1.0.6": version "1.0.6" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== @@ -2861,9 +2876,9 @@ integrity sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g== "@types/express-serve-static-core@*", "@types/express-serve-static-core@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.0.0.tgz#91f06cda1049e8f17eeab364798ed79c97488a1c" - integrity sha512-AbXMTZGt40T+KON9/Fdxx0B2WK5hsgxcfXJLr5bFpZ7b4JCex2WyQPTEKdXqfHiY5nKKBScZ7yCoO6Pvgxfvnw== + version "5.0.2" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.0.2.tgz#812d2871e5eea17fb0bd5214dda7a7b748c0e12a" + integrity sha512-vluaspfvWEtE4vcSDlKRNer52DvOGrB2xv6diXy6UKyKW0lqZiWHGNApSyxOv+8DE5Z27IzVvE7hNkxg7EXIcg== dependencies: "@types/node" "*" "@types/qs" "*" @@ -3003,7 +3018,7 @@ "@types/tough-cookie" "*" parse5 "^7.0.0" -"@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": +"@types/json-schema@*", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== @@ -3021,9 +3036,9 @@ "@types/node" "*" "@types/jsrsasign@^10.5.4": - version "10.5.14" - resolved "https://registry.yarnpkg.com/@types/jsrsasign/-/jsrsasign-10.5.14.tgz#61d1dbd791ecd11db556c1ca5d82453fc7207338" - integrity sha512-lppSlfK6etu+cuKs40K4rg8As79PH6hzIB+v55zSqImbSH3SE6Fm8MBHCiI91cWlAP3Z4igtJK1VL3fSN09blQ== + version "10.5.15" + resolved "https://registry.yarnpkg.com/@types/jsrsasign/-/jsrsasign-10.5.15.tgz#5cf1ee506b2fa2435b6e1786a873285c7110eb82" + integrity sha512-3stUTaSRtN09PPzVWR6aySD9gNnuymz+WviNHoTb85dKu+BjaV4uBbWWGykBBJkfwPtcNZVfTn2lbX00U+yhpQ== "@types/katex@^0.16.0": version "0.16.7" @@ -3065,9 +3080,9 @@ integrity sha512-yslwR0zZ3zAT1qXcCPxIcD23CZ6W6nKsl6JufSJHAmdwOBuYwCVJkaMsEo9yzxGV7ATfoX8S+RgtnajOEtKxYA== "@types/node-fetch@^2.6.2": - version "2.6.11" - resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.11.tgz#9b39b78665dae0e82a08f02f4967d62c66f95d24" - integrity sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g== + version "2.6.12" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.12.tgz#8ab5c3ef8330f13100a7479e2cd56d3386830a03" + integrity sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA== dependencies: "@types/node" "*" form-data "^4.0.0" @@ -3080,16 +3095,16 @@ "@types/node" "*" "@types/node@*": - version "22.7.6" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.7.6.tgz#3ec3e2b071e136cd11093c19128405e1d1f92f33" - integrity sha512-/d7Rnj0/ExXDMcioS78/kf1lMzYk4BZV8MZGTBKzTGZ6/406ukkbYlIsZmMPhcR5KlkunDHQLrtAVmSq7r+mSw== + version "22.10.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.1.tgz#41ffeee127b8975a05f8c4f83fb89bcb2987d766" + integrity sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ== dependencies: - undici-types "~6.19.2" + undici-types "~6.20.0" "@types/node@18": - version "18.19.64" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.64.tgz#122897fb79f2a9ec9c979bded01c11461b2b1478" - integrity sha512-955mDqvO2vFf/oL7V3WiUtiz+BugyX8uVbaT2H8oj3+8dRyH2FLiNdowe7eNqRM7IOIZvzDH76EoAT+gwm6aIQ== + version "18.19.68" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.68.tgz#f4f10d9927a7eaf3568c46a6d739cc0967ccb701" + integrity sha512-QGtpFH1vB99ZmTa63K4/FU8twThj4fuVSBkGddTp7uIL/cuoLWIUSL2RcOaigBhfR+hg5pgGkBnkoOxrTVBMKw== dependencies: undici-types "~5.26.4" @@ -3284,74 +3299,61 @@ "@types/yargs-parser" "*" "@typescript-eslint/eslint-plugin@^8.0.0": - version "8.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.14.0.tgz#7dc0e419c87beadc8f554bf5a42e5009ed3748dc" - integrity sha512-tqp8H7UWFaZj0yNO6bycd5YjMwxa6wIHOLZvWPkidwbgLCsBMetQoGj7DPuAlWa2yGO3H48xmPwjhsSPPCGU5w== + version "8.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.16.0.tgz#ac56825bcdf3b392fc76a94b1315d4a162f201a6" + integrity sha512-5YTHKV8MYlyMI6BaEG7crQ9BhSc8RxzshOReKwZwRWN0+XvvTOm+L/UYLCYxFpfwYuAAqhxiq4yae0CMFwbL7Q== dependencies: "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "8.14.0" - "@typescript-eslint/type-utils" "8.14.0" - "@typescript-eslint/utils" "8.14.0" - "@typescript-eslint/visitor-keys" "8.14.0" + "@typescript-eslint/scope-manager" "8.16.0" + "@typescript-eslint/type-utils" "8.16.0" + "@typescript-eslint/utils" "8.16.0" + "@typescript-eslint/visitor-keys" "8.16.0" graphemer "^1.4.0" ignore "^5.3.1" natural-compare "^1.4.0" ts-api-utils "^1.3.0" "@typescript-eslint/parser@^8.0.0": - version "8.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.14.0.tgz#0a7e9dbc11bc07716ab2d7b1226217e9f6b51fc8" - integrity sha512-2p82Yn9juUJq0XynBXtFCyrBDb6/dJombnz6vbo6mgQEtWHfvHbQuEa9kAOVIt1c9YFwi7H6WxtPj1kg+80+RA== + version "8.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.16.0.tgz#ee5b2d6241c1ab3e2e53f03fd5a32d8e266d8e06" + integrity sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w== dependencies: - "@typescript-eslint/scope-manager" "8.14.0" - "@typescript-eslint/types" "8.14.0" - "@typescript-eslint/typescript-estree" "8.14.0" - "@typescript-eslint/visitor-keys" "8.14.0" + "@typescript-eslint/scope-manager" "8.16.0" + "@typescript-eslint/types" "8.16.0" + "@typescript-eslint/typescript-estree" "8.16.0" + "@typescript-eslint/visitor-keys" "8.16.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@8.14.0": - version "8.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.14.0.tgz#01f37c147a735cd78f0ff355e033b9457da1f373" - integrity sha512-aBbBrnW9ARIDn92Zbo7rguLnqQ/pOrUguVpbUwzOhkFg2npFDwTgPGqFqE0H5feXcOoJOfX3SxlJaKEVtq54dw== +"@typescript-eslint/scope-manager@8.16.0": + version "8.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.16.0.tgz#ebc9a3b399a69a6052f3d88174456dd399ef5905" + integrity sha512-mwsZWubQvBki2t5565uxF0EYvG+FwdFb8bMtDuGQLdCCnGPrDEDvm1gtfynuKlnpzeBRqdFCkMf9jg1fnAK8sg== dependencies: - "@typescript-eslint/types" "8.14.0" - "@typescript-eslint/visitor-keys" "8.14.0" + "@typescript-eslint/types" "8.16.0" + "@typescript-eslint/visitor-keys" "8.16.0" -"@typescript-eslint/scope-manager@8.9.0": - version "8.9.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.9.0.tgz#c98fef0c4a82a484e6a1eb610a55b154d14d46f3" - integrity sha512-bZu9bUud9ym1cabmOYH9S6TnbWRzpklVmwqICeOulTCZ9ue2/pczWzQvt/cGj2r2o1RdKoZbuEMalJJSYw3pHQ== +"@typescript-eslint/type-utils@8.16.0": + version "8.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.16.0.tgz#585388735f7ac390f07c885845c3d185d1b64740" + integrity sha512-IqZHGG+g1XCWX9NyqnI/0CX5LL8/18awQqmkZSl2ynn8F76j579dByc0jhfVSnSnhf7zv76mKBQv9HQFKvDCgg== dependencies: - "@typescript-eslint/types" "8.9.0" - "@typescript-eslint/visitor-keys" "8.9.0" - -"@typescript-eslint/type-utils@8.14.0": - version "8.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.14.0.tgz#455c6af30c336b24a1af28bc4f81b8dd5d74d94d" - integrity sha512-Xcz9qOtZuGusVOH5Uk07NGs39wrKkf3AxlkK79RBK6aJC1l03CobXjJbwBPSidetAOV+5rEVuiT1VSBUOAsanQ== - dependencies: - "@typescript-eslint/typescript-estree" "8.14.0" - "@typescript-eslint/utils" "8.14.0" + "@typescript-eslint/typescript-estree" "8.16.0" + "@typescript-eslint/utils" "8.16.0" debug "^4.3.4" ts-api-utils "^1.3.0" -"@typescript-eslint/types@8.14.0": - version "8.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.14.0.tgz#0d33d8d0b08479c424e7d654855fddf2c71e4021" - integrity sha512-yjeB9fnO/opvLJFAsPNYlKPnEM8+z4og09Pk504dkqonT02AyL5Z9SSqlE0XqezS93v6CXn49VHvB2G7XSsl0g== +"@typescript-eslint/types@8.16.0": + version "8.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.16.0.tgz#49c92ae1b57942458ab83d9ec7ccab3005e64737" + integrity sha512-NzrHj6thBAOSE4d9bsuRNMvk+BvaQvmY4dDglgkgGC0EW/tB3Kelnp3tAKH87GEwzoxgeQn9fNGRyFJM/xd+GQ== -"@typescript-eslint/types@8.9.0": - version "8.9.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.9.0.tgz#b733af07fb340b32e962c6c63b1062aec2dc0fe6" - integrity sha512-SjgkvdYyt1FAPhU9c6FiYCXrldwYYlIQLkuc+LfAhCna6ggp96ACncdtlbn8FmnG72tUkXclrDExOpEYf1nfJQ== - -"@typescript-eslint/typescript-estree@8.14.0": - version "8.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.14.0.tgz#a7a3a5a53a6c09313e12fb4531d4ff582ee3c312" - integrity sha512-OPXPLYKGZi9XS/49rdaCbR5j/S14HazviBlUQFvSKz3npr3NikF+mrgK7CFVur6XEt95DZp/cmke9d5i3vtVnQ== +"@typescript-eslint/typescript-estree@8.16.0": + version "8.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.16.0.tgz#9d741e56e5b13469b5190e763432ce5551a9300c" + integrity sha512-E2+9IzzXMc1iaBy9zmo+UYvluE3TW7bCGWSF41hVWUE01o8nzr1rvOQYSxelxr6StUvRcTMe633eY8mXASMaNw== dependencies: - "@typescript-eslint/types" "8.14.0" - "@typescript-eslint/visitor-keys" "8.14.0" + "@typescript-eslint/types" "8.16.0" + "@typescript-eslint/visitor-keys" "8.16.0" debug "^4.3.4" fast-glob "^3.3.2" is-glob "^4.0.3" @@ -3359,55 +3361,23 @@ semver "^7.6.0" ts-api-utils "^1.3.0" -"@typescript-eslint/typescript-estree@8.9.0": - version "8.9.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.9.0.tgz#1714f167e9063062dc0df49c1d25afcbc7a96199" - integrity sha512-9iJYTgKLDG6+iqegehc5+EqE6sqaee7kb8vWpmHZ86EqwDjmlqNNHeqDVqb9duh+BY6WCNHfIGvuVU3Tf9Db0g== - dependencies: - "@typescript-eslint/types" "8.9.0" - "@typescript-eslint/visitor-keys" "8.9.0" - debug "^4.3.4" - fast-glob "^3.3.2" - is-glob "^4.0.3" - minimatch "^9.0.4" - semver "^7.6.0" - ts-api-utils "^1.3.0" - -"@typescript-eslint/utils@8.14.0", "@typescript-eslint/utils@^8.12.2": - version "8.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.14.0.tgz#ac2506875e03aba24e602364e43b2dfa45529dbd" - integrity sha512-OGqj6uB8THhrHj0Fk27DcHPojW7zKwKkPmHXHvQ58pLYp4hy8CSUdTKykKeh+5vFqTTVmjz0zCOOPKRovdsgHA== +"@typescript-eslint/utils@8.16.0", "@typescript-eslint/utils@^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/utils@^8.13.0": + version "8.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.16.0.tgz#c71264c437157feaa97842809836254a6fc833c3" + integrity sha512-C1zRy/mOL8Pj157GiX4kaw7iyRLKfJXBR3L82hk5kS/GyHcOFmy4YUq/zfZti72I9wnuQtA/+xzft4wCC8PJdA== dependencies: "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "8.14.0" - "@typescript-eslint/types" "8.14.0" - "@typescript-eslint/typescript-estree" "8.14.0" + "@typescript-eslint/scope-manager" "8.16.0" + "@typescript-eslint/types" "8.16.0" + "@typescript-eslint/typescript-estree" "8.16.0" -"@typescript-eslint/utils@^6.0.0 || ^7.0.0 || ^8.0.0": - version "8.9.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.9.0.tgz#748bbe3ea5bee526d9786d9405cf1b0df081c299" - integrity sha512-PKgMmaSo/Yg/F7kIZvrgrWa1+Vwn036CdNUvYFEkYbPwOH4i8xvkaRlu148W3vtheWK9ckKRIz7PBP5oUlkrvQ== +"@typescript-eslint/visitor-keys@8.16.0": + version "8.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.16.0.tgz#d5086afc060b01ff7a4ecab8d49d13d5a7b07705" + integrity sha512-pq19gbaMOmFE3CbL0ZB8J8BFCo2ckfHBfaIsaOZgBIF4EoISJIdLX5xRhd0FGB0LlHReNRuzoJoMGpTjq8F2CQ== dependencies: - "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "8.9.0" - "@typescript-eslint/types" "8.9.0" - "@typescript-eslint/typescript-estree" "8.9.0" - -"@typescript-eslint/visitor-keys@8.14.0": - version "8.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.14.0.tgz#2418d5a54669af9658986ade4e6cfb7767d815ad" - integrity sha512-vG0XZo8AdTH9OE6VFRwAZldNc7qtJ/6NLGWak+BtENuEUXGZgFpihILPiBvKXvJ2nFu27XNGC6rKiwuaoMbYzQ== - dependencies: - "@typescript-eslint/types" "8.14.0" - eslint-visitor-keys "^3.4.3" - -"@typescript-eslint/visitor-keys@8.9.0": - version "8.9.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.9.0.tgz#5f11f4d9db913f37da42776893ffe0dd1ae78f78" - integrity sha512-Ht4y38ubk4L5/U8xKUBfKNYGmvKvA1CANoxiTRMM+tOLk3lbF3DvzZCxJCRSE+2GdCMSh6zq9VZJc3asc1XuAA== - dependencies: - "@typescript-eslint/types" "8.9.0" - eslint-visitor-keys "^3.4.3" + "@typescript-eslint/types" "8.16.0" + eslint-visitor-keys "^4.2.0" "@ungap/structured-clone@^1.2.0": version "1.2.0" @@ -3415,16 +3385,16 @@ integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== "@vector-im/compound-design-tokens@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-2.0.1.tgz#add14494caab16cdbe98f2bdabe726908739def4" - integrity sha512-4nkPcrPII+sejispn+UkWZYFN7LecN39e4WGBupdceiMq0NJrfXrnVtJ9/6BDLgSqHInb6R/IWQkIbPbzfqRMg== + version "2.1.1" + resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-2.1.1.tgz#d6175a99fe4b97688464126f255386990f3048d6" + integrity sha512-QnUi2K14D9KTXxcLQKUU3V75cforZLMwhaaJDNftT8F5mG86950hAM+qhgDNEpEU+pkTffQj0/g/5859YmqWzQ== -"@vector-im/compound-web@^7.4.0": - version "7.4.0" - resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-7.4.0.tgz#a5af8af6346f8ff6c14c70f5d4eb2eab7357a7cc" - integrity sha512-ZRBUeEGNmj/fTkIRa8zGnyVN7ytowpfOtHChqNm+m/+OTJN3o/lOMuQHDV8jeSEW2YwPJqGvPuG/dRr89IcQkA== +"@vector-im/compound-web@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-7.5.0.tgz#1547af5f0ee27b94f79ab11eee006059f3d09707" + integrity sha512-Xhef8H5WrRmPuanzRBs8rnl+hwbcQnC7nKSCupUczAQ5hjlieBx4vcQYQ/nMkrs4rMGjgfFtR3E18wT5LlML/A== dependencies: - "@floating-ui/react" "^0.26.24" + "@floating-ui/react" "^0.27.0" "@radix-ui/react-context-menu" "^2.2.1" "@radix-ui/react-dropdown-menu" "^2.1.1" "@radix-ui/react-form" "^0.1.0" @@ -3442,125 +3412,125 @@ dependencies: eslint-plugin-unicorn "^54.0.0" -"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1": - version "1.12.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb" - integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg== +"@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.14.1.tgz#a9f6a07f2b03c95c8d38c4536a1fdfb521ff55b6" + integrity sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ== dependencies: - "@webassemblyjs/helper-numbers" "1.11.6" - "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/helper-numbers" "1.13.2" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" -"@webassemblyjs/floating-point-hex-parser@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" - integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== +"@webassemblyjs/floating-point-hex-parser@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz#fcca1eeddb1cc4e7b6eed4fc7956d6813b21b9fb" + integrity sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA== -"@webassemblyjs/helper-api-error@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" - integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== +"@webassemblyjs/helper-api-error@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz#e0a16152248bc38daee76dd7e21f15c5ef3ab1e7" + integrity sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ== -"@webassemblyjs/helper-buffer@1.12.1": - version "1.12.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz#6df20d272ea5439bf20ab3492b7fb70e9bfcb3f6" - integrity sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw== +"@webassemblyjs/helper-buffer@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz#822a9bc603166531f7d5df84e67b5bf99b72b96b" + integrity sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA== -"@webassemblyjs/helper-numbers@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" - integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g== +"@webassemblyjs/helper-numbers@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz#dbd932548e7119f4b8a7877fd5a8d20e63490b2d" + integrity sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA== dependencies: - "@webassemblyjs/floating-point-hex-parser" "1.11.6" - "@webassemblyjs/helper-api-error" "1.11.6" + "@webassemblyjs/floating-point-hex-parser" "1.13.2" + "@webassemblyjs/helper-api-error" "1.13.2" "@xtuc/long" "4.2.2" -"@webassemblyjs/helper-wasm-bytecode@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" - integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== +"@webassemblyjs/helper-wasm-bytecode@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz#e556108758f448aae84c850e593ce18a0eb31e0b" + integrity sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA== -"@webassemblyjs/helper-wasm-section@1.12.1": - version "1.12.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz#3da623233ae1a60409b509a52ade9bc22a37f7bf" - integrity sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g== +"@webassemblyjs/helper-wasm-section@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz#9629dda9c4430eab54b591053d6dc6f3ba050348" + integrity sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw== dependencies: - "@webassemblyjs/ast" "1.12.1" - "@webassemblyjs/helper-buffer" "1.12.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.6" - "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/wasm-gen" "1.14.1" -"@webassemblyjs/ieee754@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" - integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg== +"@webassemblyjs/ieee754@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz#1c5eaace1d606ada2c7fd7045ea9356c59ee0dba" + integrity sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw== dependencies: "@xtuc/ieee754" "^1.2.0" -"@webassemblyjs/leb128@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" - integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ== +"@webassemblyjs/leb128@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.13.2.tgz#57c5c3deb0105d02ce25fa3fd74f4ebc9fd0bbb0" + integrity sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw== dependencies: "@xtuc/long" "4.2.2" -"@webassemblyjs/utf8@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" - integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== +"@webassemblyjs/utf8@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.13.2.tgz#917a20e93f71ad5602966c2d685ae0c6c21f60f1" + integrity sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ== -"@webassemblyjs/wasm-edit@^1.12.1": - version "1.12.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b" - integrity sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g== +"@webassemblyjs/wasm-edit@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz#ac6689f502219b59198ddec42dcd496b1004d597" + integrity sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ== dependencies: - "@webassemblyjs/ast" "1.12.1" - "@webassemblyjs/helper-buffer" "1.12.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.6" - "@webassemblyjs/helper-wasm-section" "1.12.1" - "@webassemblyjs/wasm-gen" "1.12.1" - "@webassemblyjs/wasm-opt" "1.12.1" - "@webassemblyjs/wasm-parser" "1.12.1" - "@webassemblyjs/wast-printer" "1.12.1" + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/helper-wasm-section" "1.14.1" + "@webassemblyjs/wasm-gen" "1.14.1" + "@webassemblyjs/wasm-opt" "1.14.1" + "@webassemblyjs/wasm-parser" "1.14.1" + "@webassemblyjs/wast-printer" "1.14.1" -"@webassemblyjs/wasm-gen@1.12.1": - version "1.12.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz#a6520601da1b5700448273666a71ad0a45d78547" - integrity sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w== +"@webassemblyjs/wasm-gen@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz#991e7f0c090cb0bb62bbac882076e3d219da9570" + integrity sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg== dependencies: - "@webassemblyjs/ast" "1.12.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.6" - "@webassemblyjs/ieee754" "1.11.6" - "@webassemblyjs/leb128" "1.11.6" - "@webassemblyjs/utf8" "1.11.6" + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/ieee754" "1.13.2" + "@webassemblyjs/leb128" "1.13.2" + "@webassemblyjs/utf8" "1.13.2" -"@webassemblyjs/wasm-opt@1.12.1": - version "1.12.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz#9e6e81475dfcfb62dab574ac2dda38226c232bc5" - integrity sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg== +"@webassemblyjs/wasm-opt@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz#e6f71ed7ccae46781c206017d3c14c50efa8106b" + integrity sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw== dependencies: - "@webassemblyjs/ast" "1.12.1" - "@webassemblyjs/helper-buffer" "1.12.1" - "@webassemblyjs/wasm-gen" "1.12.1" - "@webassemblyjs/wasm-parser" "1.12.1" + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/wasm-gen" "1.14.1" + "@webassemblyjs/wasm-parser" "1.14.1" -"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1": - version "1.12.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937" - integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ== +"@webassemblyjs/wasm-parser@1.14.1", "@webassemblyjs/wasm-parser@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz#b3e13f1893605ca78b52c68e54cf6a865f90b9fb" + integrity sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ== dependencies: - "@webassemblyjs/ast" "1.12.1" - "@webassemblyjs/helper-api-error" "1.11.6" - "@webassemblyjs/helper-wasm-bytecode" "1.11.6" - "@webassemblyjs/ieee754" "1.11.6" - "@webassemblyjs/leb128" "1.11.6" - "@webassemblyjs/utf8" "1.11.6" + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-api-error" "1.13.2" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/ieee754" "1.13.2" + "@webassemblyjs/leb128" "1.13.2" + "@webassemblyjs/utf8" "1.13.2" -"@webassemblyjs/wast-printer@1.12.1": - version "1.12.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz#bcecf661d7d1abdaf989d8341a4833e33e2b31ac" - integrity sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA== +"@webassemblyjs/wast-printer@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz#3bb3e9638a8ae5fdaf9610e7a06b4d9f9aa6fe07" + integrity sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw== dependencies: - "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/ast" "1.14.1" "@xtuc/long" "4.2.2" "@webpack-cli/configtest@^2.1.1": @@ -3626,11 +3596,6 @@ acorn-globals@^7.0.0: acorn "^8.1.0" acorn-walk "^8.0.2" -acorn-import-attributes@^1.9.5: - version "1.9.5" - resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" - integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== - acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -3643,12 +3608,12 @@ acorn-walk@^8.0.0, acorn-walk@^8.0.2, acorn-walk@^8.1.1: dependencies: acorn "^8.11.0" -acorn@^8.0.4, acorn@^8.1.0, acorn@^8.11.0, acorn@^8.12.0, acorn@^8.4.1, acorn@^8.7.1, acorn@^8.8.1, acorn@^8.8.2, acorn@^8.9.0: +acorn@^8.0.4, acorn@^8.1.0, acorn@^8.11.0, acorn@^8.12.0, acorn@^8.4.1, acorn@^8.9.0: version "8.13.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.13.0.tgz#2a30d670818ad16ddd6a35d3842dacec9e5d7ca3" integrity sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w== -acorn@^8.14.0: +acorn@^8.14.0, acorn@^8.8.1, acorn@^8.8.2: version "8.14.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0" integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== @@ -3804,19 +3769,12 @@ aria-query@5.3.0: dependencies: dequal "^2.0.3" -aria-query@^5.0.0: +aria-query@^5.0.0, aria-query@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59" integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw== -aria-query@~5.1.3: - version "5.1.3" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e" - integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ== - dependencies: - deep-equal "^2.0.5" - -array-buffer-byte-length@^1.0.0, array-buffer-byte-length@^1.0.1: +array-buffer-byte-length@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== @@ -3968,12 +3926,7 @@ await-lock@^2.1.0: resolved "https://registry.yarnpkg.com/await-lock/-/await-lock-2.2.2.tgz#a95a9b269bfd2f69d22b17a321686f551152bcef" integrity sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw== -axe-core@^4.10.0: - version "4.10.1" - resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.1.tgz#7d2589b0183f05b0f23e55c2f4cdf97b5bdc66d9" - integrity sha512-qPC9o+kD8Tir0lzNGLeghbOrWMr3ZJpaRlCIb6Uobt/7N4FiEDvqUMnxzCHRHmg8vOg14kr5gVNyScRmbMaJ9g== - -axe-core@~4.10.2: +axe-core@^4.10.0, axe-core@~4.10.2: version "4.10.2" resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.2.tgz#85228e3e1d8b8532a27659b332e39b7fa0e022df" integrity sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w== @@ -4103,11 +4056,6 @@ base64-arraybuffer@^1.0.2: resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc" integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ== -base64-js@^1.3.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - batch@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" @@ -4129,16 +4077,14 @@ blob-polyfill@^9.0.0: integrity sha512-DPUO/EjNANCgSVg0geTy1vmUpu5hhp9tV2F7xUSTUd1jwe4XpwupGB+lt5PhVUqpqAk+zK1etqp6Pl/HVf71Ug== bloom-filters@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/bloom-filters/-/bloom-filters-3.0.3.tgz#bef683f04806ccf5f9d32f9693233bec505c1a96" - integrity sha512-whXz8q3iFb0Wc8TsVmkkN03htP1Bzgpue0u6GIv+OZvkvhtkxXf71J3821YxFBb3QzoubltzTRy6cwKxNM2sNA== + version "3.0.4" + resolved "https://registry.yarnpkg.com/bloom-filters/-/bloom-filters-3.0.4.tgz#2712bd8f8092fd0a081fe52a17c63be79dbbfc1e" + integrity sha512-BdnPWo2OpYhlvuP2fRzJBdioMCkm7Zp0HCf8NJgF5Mbyqy7VQ/CnTiVWMMyq4EZCBHwj0Kq6098gW2/3RsZsrA== dependencies: "@types/seedrandom" "^3.0.8" base64-arraybuffer "^1.0.2" is-buffer "^2.0.5" - lodash "^4.17.15" - lodash.eq "^4.0.0" - lodash.indexof "^4.0.5" + lodash "^4.17.21" long "^5.2.0" reflect-metadata "^0.1.13" seedrandom "^3.0.5" @@ -4202,7 +4148,7 @@ braces@^3.0.3, braces@~3.0.2: dependencies: fill-range "^7.1.1" -browserslist@^4.0.0, browserslist@^4.21.10, browserslist@^4.23.1, browserslist@^4.23.2, browserslist@^4.23.3, browserslist@^4.24.0, browserslist@^4.24.2: +browserslist@^4.0.0, browserslist@^4.23.1, browserslist@^4.23.2, browserslist@^4.23.3, browserslist@^4.24.0, browserslist@^4.24.2: version "4.24.2" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.2.tgz#f5845bc91069dbd55ee89faf9822e1d885d16580" integrity sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg== @@ -4231,14 +4177,6 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -buffer@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" - integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.2.1" - builtin-modules@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" @@ -4261,7 +4199,15 @@ bytes@3.1.2: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== -call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: +call-bind-apply-helpers@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz#32e5892e6361b29b0b545ba6f7763378daca2840" + integrity sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + +call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== @@ -4272,6 +4218,16 @@ call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: get-intrinsic "^1.2.4" set-function-length "^1.2.1" +call-bind@^1.0.7: + version "1.0.8" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" + integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== + dependencies: + call-bind-apply-helpers "^1.0.0" + es-define-property "^1.0.0" + get-intrinsic "^1.2.4" + set-function-length "^1.2.2" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -4310,10 +4266,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@1.0.30001679, caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001669: - version "1.0.30001679" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001679.tgz#18c573b72f72ba70822194f6c39e7888597f9e32" - integrity sha512-j2YqID/YwpLnKzCmBOS4tlZdWprXm3ZmQLBH9ZBXFOhoxLA46fwyBvx6toCBWBmnuwUY/qB3kEU6gFx8qgCroA== +caniuse-lite@1.0.30001684, caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001669: + version "1.0.30001684" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001684.tgz#0eca437bab7d5f03452ff0ef9de8299be6b08e16" + integrity sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ== chalk@5.2.0: version "5.2.0" @@ -4388,9 +4344,9 @@ ci-info@^3.2.0: integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== ci-info@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.0.0.tgz#65466f8b280fc019b9f50a5388115d17a63a44f2" - integrity sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg== + version "4.1.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.1.0.tgz#92319d2fa29d2620180ea5afed31f589bc98cf83" + integrity sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A== cjs-module-lexer@^1.0.0: version "1.4.1" @@ -4599,9 +4555,9 @@ concat-map@0.0.1: integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== concurrently@^9.0.0: - version "9.0.1" - resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-9.0.1.tgz#01e171bf6c7af0c022eb85daef95bff04d8185aa" - integrity sha512-wYKvCd/f54sTXJMSfV6Ln/B8UrfLBKOYa+lzc6CHay3Qek+LorVSBdMVfyewFhRbH0Rbabsk4D+3PL/VjQ5gzg== + version "9.1.0" + resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-9.1.0.tgz#8da6d609f4321752912dab9be8710232ac496aa0" + integrity sha512-VxkzwMAn4LP7WyMnJNbHN5mKV9L2IbyDjpzemKr99sXNR3GqRNMMHdm7prV1ws9wg7ETj6WUkNOigZVsptwbgg== dependencies: chalk "^4.1.2" lodash "^4.17.21" @@ -4670,9 +4626,9 @@ core-js-compat@^3.38.0, core-js-compat@^3.38.1: browserslist "^4.24.2" core-js@^3.0.0, core-js@^3.38.1: - version "3.38.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.38.1.tgz#aa375b79a286a670388a1a363363d53677c0383e" - integrity sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw== + version "3.39.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.39.0.tgz#57f7647f4d2d030c32a72ea23a0555b2eaa30f83" + integrity sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g== core-util-is@~1.0.0: version "1.0.3" @@ -4745,9 +4701,9 @@ create-require@^1.1.0: integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== cronstrue@^2.41.0: - version "2.50.0" - resolved "https://registry.yarnpkg.com/cronstrue/-/cronstrue-2.50.0.tgz#eabba0f915f186765258b707b7a3950c663b5573" - integrity sha512-ULYhWIonJzlScCCQrPUG5uMXzXxSixty4djud9SS37DoNxDdkeRocxzHuAo4ImRBUK+mAuU5X9TSwEDccnnuPg== + version "2.52.0" + resolved "https://registry.yarnpkg.com/cronstrue/-/cronstrue-2.52.0.tgz#00af1a8dcf76a1dece149e4416db823105b28cdb" + integrity sha512-NKgHbWkSZXJUcaBHSsyzC8eegD6bBd4O0oCI6XMIJ+y4Bq3v4w7sY3wfWoKPuVlq9pQHRB6od0lmKpIqi8TlKA== cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.5" @@ -4852,20 +4808,12 @@ css-tree@^2.3.1: mdn-data "2.0.30" source-map-js "^1.0.1" -css-tree@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-3.0.0.tgz#079c7b87e465a28cedbc826502f9a227213db0f3" - integrity sha512-o88DVQ6GzsABn1+6+zo2ct801dBO5OASVyxbbvA2W20ue2puSh/VOuqUj90eUeMSX/xqGqBmOKiRQN7tJOuBXw== +css-tree@^3.0.0, css-tree@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-3.1.0.tgz#7aabc035f4e66b5c86f54570d55e05b1346eb0fd" + integrity sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w== dependencies: - mdn-data "2.10.0" - source-map-js "^1.0.1" - -css-tree@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-3.0.1.tgz#bea6deaea60bb5bcf416adfb1ecf607a8d9471f6" - integrity sha512-8Fxxv+tGhORlshCdCwnNJytvlvq46sOLSYEx2ZIGurahWvMucSRnyjPA3AmrMq4VPRYbHVpWj5VkiVasrM2H4Q== - dependencies: - mdn-data "2.12.1" + mdn-data "2.12.2" source-map-js "^1.0.1" css-tree@~2.2.0: @@ -5042,10 +4990,10 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.7, debug@~4.3.6: - version "4.3.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" - integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== +debug@4, debug@^4.1.0, debug@^4.3.1: + version "4.4.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== dependencies: ms "^2.1.3" @@ -5056,12 +5004,19 @@ debug@^3.2.7: dependencies: ms "^2.1.1" +debug@^4.1.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.7, debug@~4.3.6: + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== -decimal.js@^10.4.2: +decimal.js@10, decimal.js@^10.4.2: version "10.4.3" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== @@ -5071,30 +5026,6 @@ dedent@^1.0.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.3.tgz#99aee19eb9bae55a67327717b6e848d0bf777e5a" integrity sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ== -deep-equal@^2.0.5: - version "2.2.3" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.3.tgz#af89dafb23a396c7da3e862abc0be27cf51d56e1" - integrity sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA== - dependencies: - array-buffer-byte-length "^1.0.0" - call-bind "^1.0.5" - es-get-iterator "^1.1.3" - get-intrinsic "^1.2.2" - is-arguments "^1.1.1" - is-array-buffer "^3.0.2" - is-date-object "^1.0.5" - is-regex "^1.1.4" - is-shared-array-buffer "^1.0.2" - isarray "^2.0.5" - object-is "^1.1.5" - object-keys "^1.1.1" - object.assign "^4.1.4" - regexp.prototype.flags "^1.5.1" - side-channel "^1.0.4" - which-boxed-primitive "^1.0.2" - which-collection "^1.0.1" - which-typed-array "^1.1.13" - deep-is@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" @@ -5337,9 +5268,18 @@ dot-case@^3.0.4: tslib "^2.0.3" dotenv@^16.0.2, dotenv@^16.3.1: - version "16.4.5" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" - integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== + version "16.4.7" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.7.tgz#0e20c5b82950140aa99be360a8a5f52335f53c26" + integrity sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ== + +dunder-proto@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.0.tgz#c2fce098b3c8f8899554905f4377b6d85dabaa80" + integrity sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A== + dependencies: + call-bind-apply-helpers "^1.0.0" + es-errors "^1.3.0" + gopd "^1.2.0" duplexer@^0.1.2: version "0.1.2" @@ -5378,9 +5318,9 @@ ejs@^3.1.8: jake "^10.8.5" electron-to-chromium@^1.5.41: - version "1.5.56" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.56.tgz#3213f369efc3a41091c3b2c05bc0f406108ac1df" - integrity sha512-7lXb9dAvimCFdvUMTyucD4mnIndt/xhRKFAlky0CyFogdnNmdPQNoHI23msF/2V4mpTxMzgMdjK4+YRlFlRQZw== + version "1.5.72" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.72.tgz#a732805986d3a5b5fedd438ddf4616c7d78ac2df" + integrity sha512-ZpSAUOZ2Izby7qnZluSrAlGgGQzucmFbN0n64dYzocYxnxV5ufurpj3VgEe4cUp7ir9LmeLxNYo8bVnlM8bQHw== emittery@^0.13.1: version "0.13.1" @@ -5477,10 +5417,10 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2, es-abstract@^1.23.3: - version "1.23.3" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" - integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== +es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.2, es-abstract@^1.23.3, es-abstract@^1.23.5: + version "1.23.5" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.5.tgz#f4599a4946d57ed467515ed10e4f157289cd52fb" + integrity sha512-vlmniQ0WNPwXqA0BnmwV3Ng7HxiGlh6r5U6JcTMNx8OilcAGqVJBHJcPjqOMaczU9fRuRK5Px2BdVyPRnKMMVQ== dependencies: array-buffer-byte-length "^1.0.1" arraybuffer.prototype.slice "^1.0.3" @@ -5497,7 +5437,7 @@ es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23 function.prototype.name "^1.1.6" get-intrinsic "^1.2.4" get-symbol-description "^1.0.2" - globalthis "^1.0.3" + globalthis "^1.0.4" gopd "^1.0.1" has-property-descriptors "^1.0.2" has-proto "^1.0.3" @@ -5513,10 +5453,10 @@ es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23 is-string "^1.0.7" is-typed-array "^1.1.13" is-weakref "^1.0.2" - object-inspect "^1.13.1" + object-inspect "^1.13.3" object-keys "^1.1.1" object.assign "^4.1.5" - regexp.prototype.flags "^1.5.2" + regexp.prototype.flags "^1.5.3" safe-array-concat "^1.1.2" safe-regex-test "^1.0.3" string.prototype.trim "^1.2.9" @@ -5529,37 +5469,20 @@ es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23 unbox-primitive "^1.0.2" which-typed-array "^1.1.15" -es-define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" - integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== - dependencies: - get-intrinsic "^1.2.4" +es-define-property@^1.0.0, es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== es-errors@^1.2.1, es-errors@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== -es-get-iterator@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6" - integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.3" - has-symbols "^1.0.3" - is-arguments "^1.1.1" - is-map "^2.0.2" - is-set "^2.0.2" - is-string "^1.0.7" - isarray "^2.0.5" - stop-iteration-iterator "^1.0.0" - -es-iterator-helpers@^1.0.19: - version "1.1.0" - resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.1.0.tgz#f6d745d342aea214fe09497e7152170dc333a7a6" - integrity sha512-/SurEfycdyssORP/E+bj4sEu1CWw4EmLDsHynHwSXQ7utgbrMRWW195pTrCjFgFCddf/UkYm3oqKPRq5i8bJbw== +es-iterator-helpers@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.2.0.tgz#2f1a3ab998b30cb2d10b195b587c6d9ebdebf152" + integrity sha512-tpxqxncxnpw3c93u8n3VOzACmRFoVmWJqbWXvX/JfKbkhBw1oslgPrUfeSt2psuqyEJFD6N/9lg5i7bsKpoq+Q== dependencies: call-bind "^1.0.7" define-properties "^1.2.1" @@ -5569,6 +5492,7 @@ es-iterator-helpers@^1.0.19: function-bind "^1.1.2" get-intrinsic "^1.2.4" globalthis "^1.0.4" + gopd "^1.0.1" has-property-descriptors "^1.0.2" has-proto "^1.0.3" has-symbols "^1.0.3" @@ -5605,13 +5529,13 @@ es-shim-unscopables@^1.0.0, es-shim-unscopables@^1.0.2: hasown "^2.0.0" es-to-primitive@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.3.0.tgz#96c89c82cc49fd8794a24835ba3e1ff87f214e18" + integrity sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g== dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" + is-callable "^1.2.7" + is-date-object "^1.0.5" + is-symbol "^1.0.4" escalade@^3.1.1, escalade@^3.2.0: version "3.2.0" @@ -5706,18 +5630,18 @@ eslint-plugin-import@^2.25.4: tsconfig-paths "^3.15.0" eslint-plugin-jest@^28.0.0: - version "28.8.3" - resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-28.8.3.tgz#c5699bba0ad06090ad613535e4f1572f4c2567c0" - integrity sha512-HIQ3t9hASLKm2IhIOqnu+ifw7uLZkIlR7RYNv7fMcEi/p0CIiJmfriStQS2LDkgtY4nyLbIZAD+JL347Yc2ETQ== + version "28.9.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-28.9.0.tgz#19168dfaed124339cd2252c4c4d1ac3688aeb243" + integrity sha512-rLu1s1Wf96TgUUxSw6loVIkNtUjq1Re7A9QdCCHSohnvXEBAjuL420h0T/fMmkQlNsQP2GhQzEUpYHPfxBkvYQ== dependencies: "@typescript-eslint/utils" "^6.0.0 || ^7.0.0 || ^8.0.0" eslint-plugin-jsx-a11y@^6.5.1: - version "6.10.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.0.tgz#36fb9dead91cafd085ddbe3829602fb10ef28339" - integrity sha512-ySOHvXX8eSN6zz8Bywacm7CvGNhUtdjvqfQDVe6020TUK34Cywkw7m0KsCCk1Qtm9G1FayfTN1/7mMYnYO2Bhg== + version "6.10.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz#d2812bb23bf1ab4665f1718ea442e8372e638483" + integrity sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q== dependencies: - aria-query "~5.1.3" + aria-query "^5.3.2" array-includes "^3.1.8" array.prototype.flatmap "^1.3.2" ast-types-flow "^0.0.8" @@ -5725,14 +5649,13 @@ eslint-plugin-jsx-a11y@^6.5.1: axobject-query "^4.1.0" damerau-levenshtein "^1.0.8" emoji-regex "^9.2.2" - es-iterator-helpers "^1.0.19" hasown "^2.0.2" jsx-ast-utils "^3.3.5" language-tags "^1.0.9" minimatch "^3.1.2" object.fromentries "^2.0.8" safe-regex-test "^1.0.3" - string.prototype.includes "^2.0.0" + string.prototype.includes "^2.0.1" eslint-plugin-matrix-org@^2.0.2: version "2.0.2" @@ -5745,16 +5668,16 @@ eslint-plugin-react-hooks@^5.0.0: integrity sha512-hIOwI+5hYGpJEc4uPRmz2ulCjAGD/N13Lukkh8cLV0i2IRk/bdZDYjgLVHj+U9Z704kLIdIO6iueGvxNur0sgw== eslint-plugin-react@^7.28.0: - version "7.37.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.1.tgz#56493d7d69174d0d828bc83afeffe96903fdadbd" - integrity sha512-xwTnwDqzbDRA8uJ7BMxPs/EXRB3i8ZfnOIp8BsxEQkT0nHPp+WWceqGgo6rKb9ctNi8GJLDT4Go5HAWELa/WMg== + version "7.37.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.2.tgz#cd0935987876ba2900df2f58339f6d92305acc7a" + integrity sha512-EsTAnj9fLVr/GZleBLFbj/sSuXeWmp1eXIN60ceYnZveqEaUCyW4X+Vh4WTdUhCkW4xutXYqTXCUSyqD4rB75w== dependencies: array-includes "^3.1.8" array.prototype.findlast "^1.2.5" array.prototype.flatmap "^1.3.2" array.prototype.tosorted "^1.1.4" doctrine "^2.1.0" - es-iterator-helpers "^1.0.19" + es-iterator-helpers "^1.1.0" estraverse "^5.3.0" hasown "^2.0.2" jsx-ast-utils "^2.4.1 || ^3.0.0" @@ -5791,9 +5714,9 @@ eslint-plugin-unicorn@^54.0.0: strip-indent "^3.0.0" eslint-plugin-unicorn@^56.0.0: - version "56.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-unicorn/-/eslint-plugin-unicorn-56.0.0.tgz#9fd3ebe6f478571734541fa745026b743175b59e" - integrity sha512-aXpddVz/PQMmd69uxO98PA4iidiVNvA0xOtbpUoz1WhBd4RxOQQYqN618v68drY0hmy5uU2jy1bheKEVWBjlPw== + version "56.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-unicorn/-/eslint-plugin-unicorn-56.0.1.tgz#d10a3df69ba885939075bdc95a65a0c872e940d4" + integrity sha512-FwVV0Uwf8XPfVnKSGpMg7NtlZh0G0gBarCaFcMUOoqPxXryxdYxTRRv4kH6B9TFCVIrjRXG+emcxIk2ayZilog== dependencies: "@babel/helper-validator-identifier" "^7.24.7" "@eslint-community/eslint-utils" "^4.4.0" @@ -6032,9 +5955,9 @@ expect@^29.0.0, expect@^29.7.0: jest-util "^29.7.0" express@^4.18.2, express@^4.19.2: - version "4.21.1" - resolved "https://registry.yarnpkg.com/express/-/express-4.21.1.tgz#9dae5dda832f16b4eec941a4e44aa89ec481b281" - integrity sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ== + version "4.21.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32" + integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA== dependencies: accepts "~1.3.8" array-flatten "1.1.1" @@ -6055,7 +5978,7 @@ express@^4.18.2, express@^4.19.2: methods "~1.1.2" on-finished "2.4.1" parseurl "~1.3.3" - path-to-regexp "0.1.10" + path-to-regexp "0.1.12" proxy-addr "~2.0.7" qs "6.13.0" range-parser "~1.2.1" @@ -6416,7 +6339,7 @@ get-east-asian-width@^1.0.0: resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz#21b4071ee58ed04ee0db653371b55b4299875389" integrity sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ== -get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: +get-intrinsic@^1.2.1, get-intrinsic@^1.2.3: version "1.2.4" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== @@ -6427,6 +6350,20 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@ has-symbols "^1.0.3" hasown "^2.0.0" +get-intrinsic@^1.2.4: + version "1.2.5" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.5.tgz#dfe7dd1b30761b464fe51bf4bb00ac7c37b681e7" + integrity sha512-Y4+pKa7XeRUPWFNvOOYHkRYrfzW07oraURSvjDmRVOJ748OrVmeXtpE4+GCEHncjCjkTxPNRt8kEbxDhsn6VTg== + dependencies: + call-bind-apply-helpers "^1.0.0" + dunder-proto "^1.0.0" + es-define-property "^1.0.1" + es-errors "^1.3.0" + function-bind "^1.1.2" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + get-nonce@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" @@ -6457,9 +6394,9 @@ get-symbol-description@^1.0.2: get-intrinsic "^1.2.4" github-markdown-css@^5.5.1: - version "5.7.0" - resolved "https://registry.yarnpkg.com/github-markdown-css/-/github-markdown-css-5.7.0.tgz#40fa19a8b9826a22874f305c40a5260d4d57e90a" - integrity sha512-GoYhaqELL4YUjz4tZ00PQ4JzFQkMfrBVuEeRB8W74HoikHWNiaGqSgynpwJEc+xom5uf04qoD/tUSS6ziZltaQ== + version "5.8.1" + resolved "https://registry.yarnpkg.com/github-markdown-css/-/github-markdown-css-5.8.1.tgz#2a53cf17f0c9bde5ff9e83710a3310a02f5278a7" + integrity sha512-8G+PFvqigBQSWLQjyzgpa2ThD9bo7+kDsriUIidGcRhXgmcaAWUIpCZf8DavJgc+xifjbCG+GvMyWr0XMXmc7g== gl-matrix@^3.4.3: version "3.4.3" @@ -6562,11 +6499,11 @@ globals@^14.0.0: integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== globals@^15.9.0: - version "15.11.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-15.11.0.tgz#b96ed4c6998540c6fb824b24b5499216d2438d6e" - integrity sha512-yeyNSjdbyVaWurlwCpcA6XNBrHTMIeDdj0/hnvX/OLJ9ekOXYbLsLinH/MucQyGvNnXhidTdNhTtJaffL2sMfw== + version "15.13.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-15.13.0.tgz#bbec719d69aafef188ecd67954aae76a696010fc" + integrity sha512-49TewVEz0UxZjr1WYYsWpPrhyC/B/pA8Bq0fUmet2n+eR7yn0IvNzNaoBwnK6mdkzcN+se7Ez9zUgULTz2QH4g== -globalthis@^1.0.3, globalthis@^1.0.4: +globalthis@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== @@ -6603,12 +6540,17 @@ globjoin@^0.1.4: resolved "https://registry.yarnpkg.com/globjoin/-/globjoin-0.1.4.tgz#2f4494ac8919e3767c5cbb691e9f463324285d43" integrity sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg== -gopd@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" - integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== +gopd@^1.0.1, gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + +gopd@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.1.0.tgz#df8f0839c2d48caefc32a025a49294d39606c912" + integrity sha512-FQoVQnqcdk4hVM4JN1eromaun4iuS34oStkdlLENLdpULsuQcTyXj8w7ayhuUfPwEYZ1ZOooOTT6fdA9Vmx/RA== dependencies: - get-intrinsic "^1.1.3" + get-intrinsic "^1.2.4" graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.11" @@ -6654,16 +6596,30 @@ has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: dependencies: es-define-property "^1.0.0" -has-proto@^1.0.1, has-proto@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" - integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== +has-proto@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.2.0.tgz#5de5a6eabd95fdffd9818b43055e8065e39fe9d5" + integrity sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ== + dependencies: + dunder-proto "^1.0.0" -has-symbols@^1.0.2, has-symbols@^1.0.3: +has-proto@^1.0.3: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.1.0.tgz#deb10494cbbe8809bce168a3b961f42969f5ed43" + integrity sha512-QLdzI9IIO1Jg7f9GT1gXpPpXArAn6cS31R1eEZqz08Gc+uQ8/XiqHWt17Fiw+2p6oTTIq5GXEpQkAlA88YRl/Q== + dependencies: + call-bind "^1.0.7" + +has-symbols@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== +has-symbols@^1.0.3, has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" @@ -6746,9 +6702,9 @@ html-tags@^3.3.1: integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ== html-webpack-plugin@^5.5.3: - version "5.6.0" - resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz#50a8fa6709245608cb00e811eacecb8e0d7b7ea0" - integrity sha512-iwaY4wzbe48AfKLZ/Cc8k0L+FKG6oSNRaZ8x5A/T/IVDGyXcbHncM9TdDa93wn0FsSm82FhTKW7f3vS61thXAw== + version "5.6.3" + resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.6.3.tgz#a31145f0fee4184d53a794f9513147df1e653685" + integrity sha512-QSf1yjtSAsmf7rYBV7XX86uua4W/vkhIt0xNXKbsi2foEeW7vjJQz4bhnpL3xH+l1ryl1680uNv968Z+X6jSYg== dependencies: "@types/html-minifier-terser" "^6.0.0" html-minifier-terser "^6.0.2" @@ -6855,9 +6811,9 @@ human-signals@^5.0.0: integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== husky@^9.0.0: - version "9.1.6" - resolved "https://registry.yarnpkg.com/husky/-/husky-9.1.6.tgz#e23aa996b6203ab33534bdc82306b0cf2cb07d6c" - integrity sha512-sqbjZKK7kf44hfdE94EoX8MZNk0n7HeW37O4YrVGCF4wzgQjp+akPAkfUK5LZ6KuR/6sqeAVuXHji+RzQgOn5A== + version "9.1.7" + resolved "https://registry.yarnpkg.com/husky/-/husky-9.1.7.tgz#d46a38035d101b46a70456a850ff4201344c0b2d" + integrity sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA== hyperdyperid@^1.2.0: version "1.2.0" @@ -6871,7 +6827,7 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@0.6.3, iconv-lite@^0.6: +iconv-lite@0.6.3, iconv-lite@^0.6, iconv-lite@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== @@ -6883,7 +6839,7 @@ icss-utils@^5.0.0, icss-utils@^5.1.0: resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== -ieee754@^1.1.12, ieee754@^1.2.1: +ieee754@^1.1.12: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -6962,7 +6918,7 @@ ini@^4.1.3: resolved "https://registry.yarnpkg.com/ini/-/ini-4.1.3.tgz#4c359675a6071a46985eb39b14e4a2c0ec98a795" integrity sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg== -internal-slot@^1.0.4, internal-slot@^1.0.7: +internal-slot@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== @@ -6998,7 +6954,7 @@ ipaddr.js@^2.1.0: resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8" integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== -is-arguments@^1.0.4, is-arguments@^1.1.1: +is-arguments@^1.0.4: version "1.1.1" resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== @@ -7006,7 +6962,7 @@ is-arguments@^1.0.4, is-arguments@^1.1.1: call-bind "^1.0.2" has-tostringtag "^1.0.0" -is-array-buffer@^3.0.2, is-array-buffer@^3.0.4: +is-array-buffer@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== @@ -7041,12 +6997,12 @@ is-binary-path@~2.1.0: binary-extensions "^2.0.0" is-boolean-object@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" - integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.2.0.tgz#9743641e80a62c094b5941c5bb791d66a88e497a" + integrity sha512-kR5g0+dXf/+kXnqI+lu0URKYPKgICtHGGNCDSB10AaUFj3o/HkB3u7WfpRBJGFopxxY0oH3ux7ZsDjLtK7xqvw== dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" + call-bind "^1.0.7" + has-tostringtag "^1.0.2" is-buffer@^2.0.5: version "2.0.5" @@ -7060,7 +7016,7 @@ is-builtin-module@^3.2.1: dependencies: builtin-modules "^3.3.0" -is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: +is-callable@^1.1.3, is-callable@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== @@ -7079,7 +7035,7 @@ is-data-view@^1.0.1: dependencies: is-typed-array "^1.1.13" -is-date-object@^1.0.1, is-date-object@^1.0.5: +is-date-object@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== @@ -7096,12 +7052,12 @@ is-extglob@^2.1.1: resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== -is-finalizationregistry@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz#c8749b65f17c133313e661b1289b95ad3dbd62e6" - integrity sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw== +is-finalizationregistry@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.1.0.tgz#d74a7d0c5f3578e34a20729e69202e578d495dc2" + integrity sha512-qfMdqbAQEwBw78ZyReKnlA8ezmPdb9BemzIIip/JkjaZUhitfXDkkr+3QTboW0JrSXT1QWyYShpvnNHGZ4c4yA== dependencies: - call-bind "^1.0.2" + call-bind "^1.0.7" is-fullwidth-code-point@^3.0.0: version "3.0.0" @@ -7153,7 +7109,7 @@ is-ip@^3.1.0: dependencies: ip-regex "^4.0.0" -is-map@^2.0.2, is-map@^2.0.3: +is-map@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== @@ -7169,11 +7125,12 @@ is-network-error@^1.0.0: integrity sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g== is-number-object@^1.0.4: - version "1.0.7" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" - integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.1.0.tgz#5a867e9ecc3d294dda740d9f127835857af7eb05" + integrity sha512-KVSZV0Dunv9DTPkhXwcZ3Q+tUc9TsaE1ZwX5J2WMvsSGS6Md8TFPun5uwh0yRdrNerI6vf/tbJxqSx4c1ZI1Lw== dependencies: - has-tostringtag "^1.0.0" + call-bind "^1.0.7" + has-tostringtag "^1.0.2" is-number@^7.0.0: version "7.0.0" @@ -7208,14 +7165,16 @@ is-potential-custom-element-name@^1.0.1: integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== is-regex@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" - integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.0.tgz#41b9d266e7eb7451312c64efc37e8a7d453077cf" + integrity sha512-B6ohK4ZmoftlUe+uvenXSbPJFo6U37BH7oO1B3nQH8f/7h27N56s85MhUtbFJAziz5dcmuR3i8ovUl35zp8pFA== dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" + call-bind "^1.0.7" + gopd "^1.1.0" + has-tostringtag "^1.0.2" + hasown "^2.0.2" -is-set@^2.0.2, is-set@^2.0.3: +is-set@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== @@ -7238,18 +7197,19 @@ is-stream@^3.0.0: integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== is-string@^1.0.5, is-string@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" - integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.1.0.tgz#8cb83c5d57311bf8058bc6c8db294711641da45d" + integrity sha512-PlfzajuF9vSo5wErv3MJAKD/nqf9ngAs1NFQYm16nUYFO2IzxJ2hcm+IOCg+EEopdykNNUhVq5cz35cAUxU8+g== dependencies: - has-tostringtag "^1.0.0" + call-bind "^1.0.7" + has-tostringtag "^1.0.2" is-subset@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6" integrity sha512-6Ybun0IkarhmEqxXCNw/C0bna6Zb/TkfUX9UbwJtK6ObwAVCxmAP308WWTHviM/zAqXk05cdhYsUsZeGQh99iw== -is-symbol@^1.0.2, is-symbol@^1.0.3: +is-symbol@^1.0.3, is-symbol@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== @@ -7796,9 +7756,9 @@ jiti@^1.20.0: integrity sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w== jiti@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.4.0.tgz#393d595fb6031a11d11171b5e4fc0b989ba3e053" - integrity sha512-H5UpaUI+aHOqZXlYOaFP/8AzKsg+guWu+Pr3Y8i7+Y3zr1aXAvCvTAQ1RxSc6oVD8R8c7brgNtTVP91E7upH/g== + version "2.4.1" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.4.1.tgz#4de9766ccbfa941d9b6390d2b159a4b295a52e6b" + integrity sha512-yPBThwecp1wS9DmoA4x4KR2h3QoslacnDR8ypuFM962kI4/456Iy1oHx2RAgh4jfZNdn0bctsdadceiBUgpU1g== "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" @@ -7857,16 +7817,21 @@ jsdom@^20.0.0: ws "^8.11.0" xml-name-validator "^4.0.0" -jsesc@^3.0.2, jsesc@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" - integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== +jsesc@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== jsesc@~0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== +jsesc@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" + integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== + json-buffer@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" @@ -7983,9 +7948,9 @@ kleur@^3.0.3: integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== knip@^5.36.2: - version "5.36.2" - resolved "https://registry.yarnpkg.com/knip/-/knip-5.36.2.tgz#346ce5eb464bdf34329cdcf3a6d41d0a41dce647" - integrity sha512-MudNTKBSqThAFAV29GuRPSKSebByZeQCFeNgXVRVSd+sXcubehTgQHTGqqiwlXGCt4WBP7vuVekp0ZehfZtHuw== + version "5.39.2" + resolved "https://registry.yarnpkg.com/knip/-/knip-5.39.2.tgz#1faacd8d8ef36b509b2f6e396cce85b645abb04e" + integrity sha512-BuvuWRllLWV/r2G4m9ggNH+DZ6gouP/dhtJPXVlMbWNF++w9/EfrF6k2g7YBKCwjzCC+PXmYtpH8S2t8RjnY4Q== dependencies: "@nodelib/fs.walk" "1.2.8" "@snyk/github-codeowners" "1.1.0" @@ -7998,7 +7963,7 @@ knip@^5.36.2: picocolors "^1.1.0" picomatch "^4.0.1" pretty-ms "^9.0.0" - smol-toml "^1.3.0" + smol-toml "^1.3.1" strip-json-comments "5.0.1" summary "2.1.0" zod "^3.22.4" @@ -8064,10 +8029,10 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== -linkify-element@4.1.3: - version "4.1.3" - resolved "https://registry.yarnpkg.com/linkify-element/-/linkify-element-4.1.3.tgz#c0de98f2a36683bf3a4bfa28eaa23c4c917bd546" - integrity sha512-oUoG7BWaR3Q6kAKdlLi8slsu5rkVRxbiDVVlkpoL7vtidY5THggLzRHIBtmcj+tvMpcAUQomJApDxg0ub0qpdA== +linkify-element@4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/linkify-element/-/linkify-element-4.2.0.tgz#fb5c6d47576487a463fd22a0cc889e15833aa943" + integrity sha512-LahyRMhXAgWTP9TOid7pTv8UUZFDz+saLkIVAoGNmOvISt+uSeBzdGhk3dsvkdzAh1QMhkO+fVJjmkMEITre5g== linkify-it@^4.0.1: version "4.0.1" @@ -8076,20 +8041,20 @@ linkify-it@^4.0.1: dependencies: uc.micro "^1.0.1" -linkify-react@4.1.3: - version "4.1.3" - resolved "https://registry.yarnpkg.com/linkify-react/-/linkify-react-4.1.3.tgz#461d348b4bdab3fcd0452ae1b5bbc22536395b97" - integrity sha512-rhI3zM/fxn5BfRPHfi4r9N7zgac4vOIxub1wHIWXLA5ENTMs+BGaIaFO1D1PhmxgwhIKmJz3H7uCP0Dg5JwSlA== +linkify-react@4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/linkify-react/-/linkify-react-4.2.0.tgz#d143b2af8efa5e3b09517b66ed442624c3e06bcf" + integrity sha512-dIcDGo+n4FP2FPIHDcqB7cUE+omkcEgQJpc7sNNP4+XZ9FUhFAkKjGnHMzsZM+B4yF93sK166z9K5cKTe/JpzA== -linkify-string@4.1.3: - version "4.1.3" - resolved "https://registry.yarnpkg.com/linkify-string/-/linkify-string-4.1.3.tgz#a47dbbf64c9fbd2f6ae5e26cd41ec2e5748a54d1" - integrity sha512-6dAgx4MiTcvEX87OS5aNpAioO7cSELUXp61k7azOvMYOLSmREx0w4yM1Uf0+O3JLC08YdkUyZhAX+YkasRt/mw== +linkify-string@4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/linkify-string/-/linkify-string-4.2.0.tgz#e8c5a9d57698e81e7cce7f4915ddbcbde17134c0" + integrity sha512-LqOKk0+RlqibFkxjPAGOL7Mfssqj6/SdG9QWGvzeVDpoWXhaw9OXxseCtFanjIl7C6UyTTZizhyGr4IWLfijiw== -linkifyjs@4.1.3: - version "4.1.3" - resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.1.3.tgz#0edbc346428a7390a23ea2e5939f76112c9ae07f" - integrity sha512-auMesunaJ8yfkHvK4gfg1K0SaKX/6Wn9g2Aac/NwX+l5VdmFZzo/hdPGxEOETj+ryRa4/fiOPjeeKURSAJx1sg== +linkifyjs@4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.2.0.tgz#9dd30222b9cbabec9c950e725ec00031c7fa3f08" + integrity sha512-pCj3PrQyATaoTYKHrgWRF3SJwsm61udVh+vuls/Rl6SptiDhgE7ziUIudAedRY9QEfynmM7/RmLEfPUyw1HPCw== lint-staged@^15.0.2: version "15.2.10" @@ -8159,16 +8124,6 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== -lodash.eq@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/lodash.eq/-/lodash.eq-4.0.0.tgz#a39f06779e72f9c0d1f310c90cd292c1661d5035" - integrity sha512-vbrJpXL6kQNG6TkInxX12DZRfuYVllSxhwYqjYB78g2zF3UI15nFO/0AgmZnZRnaQ38sZtjCiVjGr2rnKt4v0g== - -lodash.indexof@^4.0.5: - version "4.0.5" - resolved "https://registry.yarnpkg.com/lodash.indexof/-/lodash.indexof-4.0.5.tgz#53714adc2cddd6ed87638f893aa9b6c24e31ef3c" - integrity sha512-t9wLWMQsawdVmf6/IcAgVGqAJkNzYVcn4BHYZKTPW//l7N5Oq7Bq138BaVk19agcsPZePcidSgTTw4NqS1nUAw== - lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" @@ -8199,7 +8154,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== -lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21: +lodash@^4.17.20, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -8353,8 +8308,8 @@ matrix-events-sdk@0.0.1: integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== "matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": - version "34.12.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/544ac86d2080da8e55d0b727cae826e42600c490" + version "34.13.0" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/c4ea57d42dcf8bd04c40feaa2c686487dbcab338" dependencies: "@babel/runtime" "^7.12.5" "@matrix-org/matrix-sdk-crypto-wasm" "^9.0.0" @@ -8401,17 +8356,7 @@ mdn-data@2.0.30: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc" integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA== -mdn-data@2.10.0: - version "2.10.0" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.10.0.tgz#701da407f8fbc7a42aa0ba0c149ec897daef8986" - integrity sha512-qq7C3EtK3yJXMwz1zAab65pjl+UhohqMOctTgcqjLOWABqmwj+me02LSsCuEUxnst9X1lCBpoE0WArGKgdGDzw== - -mdn-data@2.12.1: - version "2.12.1" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.12.1.tgz#10cb462215c13d95c92ff60d0fb3becac1bbb924" - integrity sha512-rsfnCbOHjqrhWxwt5/wtSLzpoKTzW7OXdT5lLOIH1OTYhWu9rRJveGq0sKvDZODABH7RX+uoR+DYcpFnq4Tf6Q== - -mdn-data@^2.12.2: +mdn-data@2.12.2, mdn-data@^2.12.2: version "2.12.2" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.12.2.tgz#9ae6c41a9e65adf61318b32bff7b64fbfb13f8cf" integrity sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA== @@ -8695,9 +8640,9 @@ node-int64@^0.4.0: integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== node-releases@^2.0.18: - version "2.0.18" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" - integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== + version "2.0.19" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" + integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== normalize-package-data@^2.5.0: version "2.5.0" @@ -8750,18 +8695,10 @@ object-assign@^4.1.1: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -object-inspect@^1.13.1: - version "1.13.2" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" - integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== - -object-is@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07" - integrity sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" +object-inspect@^1.13.1, object-inspect@^1.13.3: + version "1.13.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.3.tgz#f14c183de51130243d6d18ae149375ff50ea488a" + integrity sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA== object-keys@^1.1.1: version "1.1.1" @@ -9091,10 +9028,10 @@ path-scurry@^2.0.0: lru-cache "^11.0.0" minipass "^7.1.2" -path-to-regexp@0.1.10: - version "0.1.10" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" - integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== +path-to-regexp@0.1.12: + version "0.1.12" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7" + integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== path-to-regexp@^2.2.1: version "2.4.0" @@ -9163,17 +9100,17 @@ pkg-dir@^7.0.0: dependencies: find-up "^6.3.0" -playwright-core@1.48.2, playwright-core@^1.45.1: - version "1.48.2" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.48.2.tgz#cd76ed8af61690edef5c05c64721c26a8db2f3d7" - integrity sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA== +playwright-core@1.49.0, playwright-core@^1.45.1: + version "1.49.0" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.49.0.tgz#8e69ffed3f41855b854982f3632f2922c890afcb" + integrity sha512-R+3KKTQF3npy5GTiKH/T+kdhoJfJojjHESR1YEWhYuEKRVfVaxH3+4+GvXE5xyCngCxhxnykk0Vlah9v8fs3jA== -playwright@1.48.2: - version "1.48.2" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.48.2.tgz#fca45ae8abdc34835c715718072aaff7e305167e" - integrity sha512-NjYvYgp4BPmiwfe31j4gHLa3J7bD2WiBz8Lk2RoSsmX38SVIARZ18VYjxLjAcDsAhA+F4iSEXTSGgjua0rrlgQ== +playwright@1.49.0: + version "1.49.0" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.49.0.tgz#df6b9e05423377a99658202844a294a8afb95d0a" + integrity sha512-eKpmys0UFDnfNb3vfsf8Vx2LEOtflgRebl0Im2eQQnYMA4Aqd+Zw8bEOB+7ZKvN76901mRnqdsiOGKxzVTbi7A== dependencies: - playwright-core "1.48.2" + playwright-core "1.49.0" optionalDependencies: fsevents "2.3.2" @@ -9832,10 +9769,10 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prettier@3.3.3: - version "3.3.3" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105" - integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew== +prettier@3.4.2: + version "3.4.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.4.2.tgz#a5ce1fb522a588bf2b78ca44c6e6fe5aa5a2b13f" + integrity sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ== pretty-error@^4.0.0: version "4.0.0" @@ -9864,9 +9801,9 @@ pretty-format@^29.0.0, pretty-format@^29.7.0: react-is "^18.0.0" pretty-ms@^9.0.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-9.1.0.tgz#0ad44de6086454f48a168e5abb3c26f8db1b3253" - integrity sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw== + version "9.2.0" + resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-9.2.0.tgz#e14c0aad6493b69ed63114442a84133d7e560ef0" + integrity sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg== dependencies: parse-ms "^4.0.0" @@ -10232,18 +10169,18 @@ reflect-metadata@^0.1.13: resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.14.tgz#24cf721fe60677146bb77eeb0e1f9dece3d65859" integrity sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A== -reflect.getprototypeof@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz#3ab04c32a8390b770712b7a8633972702d278859" - integrity sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg== +reflect.getprototypeof@^1.0.4, reflect.getprototypeof@^1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.7.tgz#04311b33a1b713ca5eb7b5aed9950a86481858e5" + integrity sha512-bMvFGIUKlc/eSfXNX+aZ+EL95/EgZzuwA0OBPTbZZDEJw/0AkentjMuM1oiRfwHrshqk4RzdgiTg5CcDalXN5g== dependencies: call-bind "^1.0.7" define-properties "^1.2.1" - es-abstract "^1.23.1" + es-abstract "^1.23.5" es-errors "^1.3.0" get-intrinsic "^1.2.4" - globalthis "^1.0.3" - which-builtin-type "^1.1.3" + gopd "^1.0.1" + which-builtin-type "^1.1.4" regenerate-unicode-properties@^10.2.0: version "10.2.0" @@ -10274,7 +10211,7 @@ regexp-tree@^0.1.27: resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.27.tgz#2198f0ef54518ffa743fe74d983b56ffd631b6cd" integrity sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA== -regexp.prototype.flags@^1.5.1, regexp.prototype.flags@^1.5.2: +regexp.prototype.flags@^1.5.2, regexp.prototype.flags@^1.5.3: version "1.5.3" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz#b3ae40b1d2499b8350ab2c3fe6ef3845d3a96f42" integrity sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ== @@ -10648,7 +10585,7 @@ set-blocking@^2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== -set-function-length@^1.2.1: +set-function-length@^1.2.1, set-function-length@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== @@ -10705,9 +10642,9 @@ shebang-regex@^3.0.0: integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== shell-quote@^1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" - integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== + version "1.8.2" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.2.tgz#d2d83e057959d53ec261311e9e9b8f51dcb2934a" + integrity sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA== side-channel@^1.0.4, side-channel@^1.0.6: version "1.0.6" @@ -10778,10 +10715,10 @@ slice-ansi@^7.1.0: ansi-styles "^6.2.1" is-fullwidth-code-point "^5.0.0" -smol-toml@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/smol-toml/-/smol-toml-1.3.0.tgz#5200e251fffadbb72570c84e9776d2a3eca48143" - integrity sha512-tWpi2TsODPScmi48b/OQZGi2lgUmBCHy6SZrhi/FdnnHiU1GwebbCfuQuxsC3nHaLwtYeJGPrDZDIeodDOc4pA== +smol-toml@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/smol-toml/-/smol-toml-1.3.1.tgz#d9084a9e212142e3cab27ef4e2b8e8ba620bfe15" + integrity sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ== snake-case@^3.0.4: version "3.0.4" @@ -10800,11 +10737,19 @@ sockjs@^0.3.24: uuid "^8.3.2" websocket-driver "^0.7.4" -source-map-js@^1.0.1, source-map-js@^1.2.0, source-map-js@^1.2.1: +source-map-js@^1.0.1, source-map-js@^1.0.2, source-map-js@^1.2.0, source-map-js@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== +source-map-loader@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-5.0.0.tgz#f593a916e1cc54471cfc8851b905c8a845fc7e38" + integrity sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA== + dependencies: + iconv-lite "^0.6.3" + source-map-js "^1.0.2" + source-map-support@0.5.13: version "0.5.13" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" @@ -10902,13 +10847,6 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== -stop-iteration-iterator@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4" - integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ== - dependencies: - internal-slot "^1.0.4" - string-argv@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" @@ -10958,7 +10896,7 @@ string-width@^7.0.0: get-east-asian-width "^1.0.0" strip-ansi "^7.1.0" -string.prototype.includes@^2.0.0: +string.prototype.includes@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz#eceef21283640761a81dbe16d6c7171a4edf7d92" integrity sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg== @@ -11114,9 +11052,9 @@ stylelint-config-standard@^36.0.0: stylelint-config-recommended "^14.0.1" stylelint-scss@^6.0.0: - version "6.9.0" - resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-6.9.0.tgz#a5ab9b2a8ed7e0a9c113558fdbd1b66ad673b259" - integrity sha512-oWOR+g6ccagfrENecImGmorWWjVyWpt2R8bmkhOW8FkNNPGStZPQMqb8QWMW4Lwu9TyPqmyjHkkAsy3weqsnNw== + version "6.10.0" + resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-6.10.0.tgz#ba5b807793e145421e9879dd15ae672af6820a45" + integrity sha512-y03if6Qw9xBMoVaf7tzp5BbnYhYvudIKzURkhSHzcHG0bW0fAYvQpTUVJOe7DyhHaxeThBil4ObEMvGbV7+M+w== dependencies: css-tree "^3.0.1" is-plain-object "^5.0.0" @@ -11124,7 +11062,7 @@ stylelint-scss@^6.0.0: mdn-data "^2.12.2" postcss-media-query-parser "^0.2.3" postcss-resolve-nested-selector "^0.1.6" - postcss-selector-parser "^6.1.2" + postcss-selector-parser "^7.0.0" postcss-value-parser "^4.2.0" stylelint-value-no-unknown-custom-properties@^6.0.1: @@ -11307,7 +11245,7 @@ terser-webpack-plugin@^5.3.10, terser-webpack-plugin@^5.3.9: serialize-javascript "^6.0.1" terser "^5.26.0" -terser@^5.10.0, terser@^5.26.0: +terser@^5.10.0: version "5.36.0" resolved "https://registry.yarnpkg.com/terser/-/terser-5.36.0.tgz#8b0dbed459ac40ff7b4c9fd5a3a2029de105180e" integrity sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w== @@ -11317,6 +11255,16 @@ terser@^5.10.0, terser@^5.26.0: commander "^2.20.0" source-map-support "~0.5.20" +terser@^5.26.0: + version "5.37.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.37.0.tgz#38aa66d1cfc43d0638fab54e43ff8a4f72a21ba3" + integrity sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" + commander "^2.20.0" + source-map-support "~0.5.20" + test-exclude@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" @@ -11438,9 +11386,9 @@ truncate-utf8-bytes@^1.0.0: utf8-byte-length "^1.0.1" ts-api-utils@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.4.0.tgz#709c6f2076e511a81557f3d07a0cbd566ae8195c" - integrity sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ== + version "1.4.3" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.4.3.tgz#bfc2215fe6528fecab2b0fba570a2e8a4263b064" + integrity sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw== ts-morph@^13.0.1: version "13.0.3" @@ -11496,12 +11444,12 @@ tsconfig-paths@^3.15.0: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@2: +tslib@2, tslib@^2.0.3, tslib@^2.1.0: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== -tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.6.1, tslib@^2.6.2, tslib@^2.7.0: +tslib@^2.0.0, tslib@^2.4.0, tslib@^2.6.1, tslib@^2.6.2, tslib@^2.7.0: version "2.8.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.0.tgz#d124c86c3c05a40a91e6fdea4021bd31d377971b" integrity sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA== @@ -11567,9 +11515,9 @@ typed-array-byte-length@^1.0.1: is-typed-array "^1.1.13" typed-array-byte-offset@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz#f9ec1acb9259f395093e4567eb3c28a580d02063" - integrity sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA== + version "1.0.3" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.3.tgz#3fa9f22567700cc86aaf86a1e7176f74b59600f2" + integrity sha512-GsvTyUHTriq6o/bHcTd0vM7OQ9JEdlvluu9YISaA7+KzDzPaIzEeDFNkTfhdE3MYcNhNi0vq/LlegYgIs5yPAw== dependencies: available-typed-arrays "^1.0.7" call-bind "^1.0.7" @@ -11577,18 +11525,19 @@ typed-array-byte-offset@^1.0.2: gopd "^1.0.1" has-proto "^1.0.3" is-typed-array "^1.1.13" + reflect.getprototypeof "^1.0.6" typed-array-length@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3" - integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g== + version "1.0.7" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.7.tgz#ee4deff984b64be1e118b0de8c9c877d5ce73d3d" + integrity sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg== dependencies: call-bind "^1.0.7" for-each "^0.3.3" gopd "^1.0.1" - has-proto "^1.0.3" is-typed-array "^1.1.13" possible-typed-array-names "^1.0.0" + reflect.getprototypeof "^1.0.6" typescript@5.6.3: version "5.6.3" @@ -11625,10 +11574,10 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -undici-types@~6.19.2: - version "6.19.8" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" - integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== +undici-types@~6.20.0: + version "6.20.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" + integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== unhomoglyph@^1.0.6: version "1.0.6" @@ -11763,9 +11712,9 @@ utils-merge@1.0.1: integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== uuid@11, uuid@^11.0.0: - version "11.0.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.0.2.tgz#a8d68ba7347d051e7ea716cc8dcbbab634d66875" - integrity sha512-14FfcOJmqdjbBPdDjFQyk/SdT4NySW4eM0zcG+HqbHP5jzuH56xO3J1DGhgs/cEMCfwYi3HQI1gnTO62iaG+tQ== + version "11.0.3" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.0.3.tgz#248451cac9d1a4a4128033e765d137e2b2c49a3d" + integrity sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg== uuid@8.3.2, uuid@^8.3.2: version "8.3.2" @@ -12011,17 +11960,17 @@ webpack-virtual-modules@^0.5.0: integrity sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw== webpack@^5.89.0: - version "5.95.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.95.0.tgz#8fd8c454fa60dad186fbe36c400a55848307b4c0" - integrity sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q== + version "5.97.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.97.1.tgz#972a8320a438b56ff0f1d94ade9e82eac155fa58" + integrity sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg== dependencies: - "@types/estree" "^1.0.5" - "@webassemblyjs/ast" "^1.12.1" - "@webassemblyjs/wasm-edit" "^1.12.1" - "@webassemblyjs/wasm-parser" "^1.12.1" - acorn "^8.7.1" - acorn-import-attributes "^1.9.5" - browserslist "^4.21.10" + "@types/eslint-scope" "^3.7.7" + "@types/estree" "^1.0.6" + "@webassemblyjs/ast" "^1.14.1" + "@webassemblyjs/wasm-edit" "^1.14.1" + "@webassemblyjs/wasm-parser" "^1.14.1" + acorn "^8.14.0" + browserslist "^4.24.0" chrome-trace-event "^1.0.2" enhanced-resolve "^5.17.1" es-module-lexer "^1.2.1" @@ -12106,16 +12055,17 @@ which-boxed-primitive@^1.0.2: is-string "^1.0.5" is-symbol "^1.0.3" -which-builtin-type@^1.1.3: - version "1.1.4" - resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.1.4.tgz#592796260602fc3514a1b5ee7fa29319b72380c3" - integrity sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w== +which-builtin-type@^1.1.4: + version "1.2.0" + resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.2.0.tgz#58042ac9602d78a6d117c7e811349df1268ba63c" + integrity sha512-I+qLGQ/vucCby4tf5HsLmGueEla4ZhwTBSqaooS+Y0BuxN4Cp+okmGuV+8mXZ84KDI9BA+oklo+RzKg0ONdSUA== dependencies: + call-bind "^1.0.7" function.prototype.name "^1.1.6" has-tostringtag "^1.0.2" is-async-function "^2.0.0" is-date-object "^1.0.5" - is-finalizationregistry "^1.0.2" + is-finalizationregistry "^1.1.0" is-generator-function "^1.0.10" is-regex "^1.1.4" is-weakref "^1.0.2" @@ -12124,7 +12074,7 @@ which-builtin-type@^1.1.3: which-collection "^1.0.2" which-typed-array "^1.1.15" -which-collection@^1.0.1, which-collection@^1.0.2: +which-collection@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== @@ -12139,7 +12089,18 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== -which-typed-array@^1.1.13, which-typed-array@^1.1.14, which-typed-array@^1.1.15, which-typed-array@^1.1.2: +which-typed-array@^1.1.14, which-typed-array@^1.1.15: + version "1.1.16" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.16.tgz#db4db429c4706feca2f01677a144278e4a8c216b" + integrity sha512-g+N+GAWiRj66DngFwHvISJd+ITsyphZvD1vChfVg6cEdnzy53GzB3oy0fUNlvhz7H7+MiqhYr26qxQShCpKTTQ== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.2" + +which-typed-array@^1.1.2: version "1.1.15" resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== @@ -12273,9 +12234,9 @@ yaml@^1.10.0: integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== yaml@^2.3.3: - version "2.6.0" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.6.0.tgz#14059ad9d0b1680d0f04d3a60fe00f3a857303c3" - integrity sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ== + version "2.6.1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.6.1.tgz#42f2b1ba89203f374609572d5349fb8686500773" + integrity sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg== yaml@~2.5.0: version "2.5.1" @@ -12346,6 +12307,6 @@ zod-validation-error@^3.0.3: integrity sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ== zod@^3.22.4: - version "3.23.8" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" - integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== + version "3.24.0" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.0.tgz#babb32313f7c5f4a99812feee806d186b4f76bde" + integrity sha512-Hz+wiY8yD0VLA2k/+nsg2Abez674dDGTai33SwNvMPuf9uIrBC9eFgIMQxBBbHFxVXi8W+5nX9DcAh9YNSQm/w==