Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/react18/context-hocs
90
.github/workflows/element-web.yaml
vendored
@@ -1,90 +0,0 @@
|
||||
# Produce a build of element-web with this version of react-sdk
|
||||
# and any matching branches of element-web and js-sdk, output it
|
||||
# as an artifact and run integration tests.
|
||||
name: Element Web - Build
|
||||
on:
|
||||
pull_request: {}
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
push:
|
||||
branches: [develop, master]
|
||||
repository_dispatch:
|
||||
types: [upstream-sdk-notify]
|
||||
|
||||
# support triggering from other workflows
|
||||
workflow_call:
|
||||
inputs:
|
||||
react-sdk-repository:
|
||||
type: string
|
||||
required: true
|
||||
description: "The name of the github repository to check out and build."
|
||||
|
||||
matrix-js-sdk-sha:
|
||||
type: string
|
||||
required: false
|
||||
description: "The Git SHA of matrix-js-sdk to build against. By default, will use a matching branch name if it exists, or develop."
|
||||
element-web-sha:
|
||||
type: string
|
||||
required: false
|
||||
description: "The Git SHA of element-web to build against. By default, will use a matching branch name if it exists, or develop."
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
# fetchdep.sh needs to know our PR number
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: "Build Element-Web"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ inputs.react-sdk-repository || github.repository }}
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
- name: Fetch layered build
|
||||
id: layered_build
|
||||
env:
|
||||
# tell layered.sh to check out the right sha of the JS-SDK & EW, if they were given one
|
||||
JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }}
|
||||
ELEMENT_WEB_GITHUB_BASE_REF: ${{ inputs.element-web-sha }}
|
||||
run: |
|
||||
scripts/ci/layered.sh
|
||||
JSSDK_SHA=$(git -C matrix-js-sdk rev-parse --short=12 HEAD)
|
||||
REACT_SHA=$(git rev-parse --short=12 HEAD)
|
||||
VECTOR_SHA=$(git -C element-web rev-parse --short=12 HEAD)
|
||||
echo "VERSION=$VECTOR_SHA-react-$REACT_SHA-js-$JSSDK_SHA" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Copy config
|
||||
run: cp element.io/develop/config.json config.json
|
||||
working-directory: ./element-web
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
CI_PACKAGE: true
|
||||
VERSION: "${{ steps.layered_build.outputs.VERSION }}"
|
||||
run: |
|
||||
yarn build
|
||||
echo $VERSION > webapp/version
|
||||
working-directory: ./element-web
|
||||
|
||||
# Record the react-sdk sha so our Playwright tests are from the same sha
|
||||
- name: Record react-sdk SHA
|
||||
run: |
|
||||
git rev-parse HEAD > element-web/webapp/sha
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: previewbuild
|
||||
path: element-web/webapp
|
||||
# We'll only use this in a triggered job, then we're done with it
|
||||
retention-days: 1
|
67
.github/workflows/end-to-end-tests-netlify.yaml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
# Triggers after the playwright tests have finished,
|
||||
# taking the artifact and uploading it to Netlify for easier viewing
|
||||
name: Upload End to End Test report to Netlify
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["End to End Tests"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }}
|
||||
cancel-in-progress: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
report:
|
||||
name: Report results
|
||||
runs-on: ubuntu-latest
|
||||
environment: Netlify
|
||||
permissions:
|
||||
statuses: write
|
||||
deployments: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Download blob reports from GitHub Actions Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
pattern: all-blob-reports-*
|
||||
path: all-blob-reports
|
||||
merge-multiple: true
|
||||
|
||||
- name: Merge into HTML Report
|
||||
run: yarn playwright merge-reports --reporter=html,github,./playwright/flaky-reporter.ts ./all-blob-reports
|
||||
env:
|
||||
# Only pass creds to the flaky-reporter on main branch runs
|
||||
GITHUB_TOKEN: ${{ github.event.workflow_run.head_branch == 'develop' && secrets.ELEMENT_BOT_TOKEN || '' }}
|
||||
|
||||
- name: Upload HTML report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: html-report--attempt-${{ github.run_attempt }}
|
||||
path: playwright-report
|
||||
retention-days: 14
|
||||
|
||||
- name: 📤 Deploy to Netlify
|
||||
uses: matrix-org/netlify-pr-preview@v3
|
||||
with:
|
||||
path: playwright-report
|
||||
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
||||
branch: ${{ github.event.workflow_run.head_branch }}
|
||||
revision: ${{ github.event.workflow_run.head_sha }}
|
||||
token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
site_id: ${{ secrets.NETLIFY_SITE_ID }}
|
||||
desc: Playwright Report
|
||||
deployment_env: EndToEndTests
|
||||
prefix: "e2e-"
|
202
.github/workflows/end-to-end-tests.yaml
vendored
@@ -1,90 +1,121 @@
|
||||
# Triggers after the layered build has finished, taking the artifact and running Playwright on it
|
||||
# Produce a build of element-web with this version of react-sdk
|
||||
# and any matching branches of element-web and js-sdk, output it
|
||||
# as an artifact and run end-to-end tests.
|
||||
name: End to End Tests
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Element Web - Build"]
|
||||
types:
|
||||
- completed
|
||||
pull_request: {}
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
push:
|
||||
branches: [develop, master]
|
||||
repository_dispatch:
|
||||
types: [upstream-sdk-notify]
|
||||
|
||||
# support calls from other workflows for downstream testing
|
||||
# support triggering from other workflows
|
||||
workflow_call:
|
||||
inputs:
|
||||
skip:
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
description: "A boolean to skip the playwright check itself while still creating the passing check. Useful when only running in Merge Queues."
|
||||
|
||||
react-sdk-repository:
|
||||
type: string
|
||||
required: true
|
||||
description: "The name of the github repository to check out and build."
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: true
|
||||
|
||||
matrix-js-sdk-sha:
|
||||
type: string
|
||||
required: false
|
||||
description: "The Git SHA of matrix-js-sdk to build against. By default, will use a matching branch name if it exists, or develop."
|
||||
element-web-sha:
|
||||
type: string
|
||||
required: false
|
||||
description: "The Git SHA of element-web to build against. By default, will use a matching branch name if it exists, or develop."
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }}
|
||||
cancel-in-progress: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
# fetchdep.sh needs to know our PR number
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
name: Prepare
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
build:
|
||||
name: "Build Element-Web"
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
statuses: write
|
||||
steps:
|
||||
# We create the status here and then update it to success/failure in the `report` stage
|
||||
# This provides an easy link to this workflow_run from the PR before the tests are done.
|
||||
- uses: Sibz/github-status-action@071b5370da85afbb16637d6eed8524a06bc2053e # v1
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: pending
|
||||
context: ${{ github.workflow }} / end-to-end-tests
|
||||
sha: ${{ github.event.workflow_run.head_sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
repository: ${{ inputs.react-sdk-repository || github.repository }}
|
||||
|
||||
tests:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
- name: Fetch layered build
|
||||
id: layered_build
|
||||
env:
|
||||
# tell layered.sh to check out the right sha of the JS-SDK & EW, if they were given one
|
||||
JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }}
|
||||
ELEMENT_WEB_GITHUB_BASE_REF: ${{ inputs.element-web-sha }}
|
||||
run: |
|
||||
scripts/ci/layered.sh
|
||||
JSSDK_SHA=$(git -C matrix-js-sdk rev-parse --short=12 HEAD)
|
||||
REACT_SHA=$(git rev-parse --short=12 HEAD)
|
||||
VECTOR_SHA=$(git -C element-web rev-parse --short=12 HEAD)
|
||||
echo "VERSION=$VECTOR_SHA-react-$REACT_SHA-js-$JSSDK_SHA" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Copy config
|
||||
run: cp element.io/develop/config.json config.json
|
||||
working-directory: ./element-web
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
CI_PACKAGE: true
|
||||
VERSION: "${{ steps.layered_build.outputs.VERSION }}"
|
||||
run: |
|
||||
yarn build
|
||||
echo $VERSION > webapp/version
|
||||
working-directory: ./element-web
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: webapp
|
||||
path: element-web/webapp
|
||||
retention-days: 1
|
||||
|
||||
playwright:
|
||||
name: "Run Tests ${{ matrix.runner }}/${{ strategy.job-total }}"
|
||||
needs: prepare
|
||||
needs: build
|
||||
if: inputs.skip != true
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
issues: read
|
||||
pull-requests: read
|
||||
environment: EndToEndTests
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Run multiple instances in parallel to speed up the tests
|
||||
runner: [1, 2, 3, 4, 5, 6, 7, 8]
|
||||
steps:
|
||||
- name: 📥 Download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
name: previewbuild
|
||||
path: webapp
|
||||
|
||||
# The workflow_run.head_sha is the sha of the head commit but the element-web was built using a simulated
|
||||
# merge commit - https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
|
||||
# so use the sha from the tarball for the checkout of the tests
|
||||
# to make sure we get a matching set of code and tests.
|
||||
- name: Grab sha from webapp
|
||||
id: sha
|
||||
run: |
|
||||
echo "sha=$(cat webapp/sha)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# XXX: We're checking out untrusted code in a secure context
|
||||
# We need to be careful to not trust anything this code outputs/may do
|
||||
#
|
||||
# Note that (in the absence of a `react-sdk-repository` input),
|
||||
# we check out from the default repository, which is (for this workflow) the
|
||||
# *target* repository for the pull request.
|
||||
#
|
||||
ref: ${{ steps.sha.outputs.sha }}
|
||||
persist-credentials: false
|
||||
path: matrix-react-sdk
|
||||
repository: ${{ inputs.react-sdk-repository || github.repository }}
|
||||
|
||||
- name: 📥 Download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: webapp
|
||||
path: webapp
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
@@ -126,66 +157,11 @@ jobs:
|
||||
path: matrix-react-sdk/blob-report
|
||||
retention-days: 1
|
||||
|
||||
report:
|
||||
name: Report results
|
||||
needs: tests
|
||||
complete:
|
||||
name: end-to-end-tests
|
||||
needs: playwright
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
environment: Netlify
|
||||
if: always()
|
||||
permissions:
|
||||
statuses: write
|
||||
deployments: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: ${{ inputs.react-sdk-repository || github.repository }}
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Download blob reports from GitHub Actions Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: all-blob-reports-*
|
||||
path: all-blob-reports
|
||||
merge-multiple: true
|
||||
|
||||
- name: Merge into HTML Report
|
||||
run: yarn playwright merge-reports --reporter=html,github,./playwright/flaky-reporter.ts ./all-blob-reports
|
||||
env:
|
||||
# Only pass creds to the flaky-reporter on main branch runs
|
||||
GITHUB_TOKEN: ${{ github.event.workflow_run.head_branch == 'develop' && secrets.ELEMENT_BOT_TOKEN || '' }}
|
||||
|
||||
- name: Upload HTML report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: html-report--attempt-${{ github.run_attempt }}
|
||||
path: playwright-report
|
||||
retention-days: 14
|
||||
|
||||
- uses: Sibz/github-status-action@071b5370da85afbb16637d6eed8524a06bc2053e # v1
|
||||
if: always()
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: ${{ needs.tests.result == 'success' && 'success' || 'failure' }}
|
||||
context: ${{ github.workflow }} / end-to-end-tests
|
||||
sha: ${{ github.event.workflow_run.head_sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
- name: 📤 Deploy to Netlify
|
||||
uses: matrix-org/netlify-pr-preview@v3
|
||||
with:
|
||||
path: playwright-report
|
||||
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
||||
branch: ${{ github.event.workflow_run.head_branch }}
|
||||
revision: ${{ github.event.workflow_run.head_sha }}
|
||||
token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
site_id: ${{ secrets.NETLIFY_SITE_ID }}
|
||||
desc: Playwright Report
|
||||
deployment_env: EndToEndTests
|
||||
prefix: "e2e-"
|
||||
- run: exit 1
|
||||
if: failure() || cancelled()
|
||||
|
4
.github/workflows/netlify.yaml
vendored
@@ -3,7 +3,7 @@
|
||||
name: Upload Preview Build to Netlify
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Element Web - Build"]
|
||||
workflows: ["End to End Tests"]
|
||||
types:
|
||||
- completed
|
||||
jobs:
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
with:
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
name: previewbuild
|
||||
name: webapp
|
||||
path: webapp
|
||||
|
||||
- name: 📤 Deploy to Netlify
|
||||
|
26
CHANGELOG.md
@@ -1,3 +1,29 @@
|
||||
Changes in [3.98.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.98.0) (2024-04-23)
|
||||
=====================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Make empty state copy for TAC depend on the value of the setting ([#12419](https://github.com/matrix-org/matrix-react-sdk/pull/12419)). Contributed by @dbkr.
|
||||
* Linkify User Interactive Authentication errors ([#12271](https://github.com/matrix-org/matrix-react-sdk/pull/12271)). Contributed by @t3chguy.
|
||||
* Add support for device dehydration v2 ([#12316](https://github.com/matrix-org/matrix-react-sdk/pull/12316)). Contributed by @uhoreg.
|
||||
* Replace `SecurityCustomisations` with `CryptoSetupExtension` ([#12342](https://github.com/matrix-org/matrix-react-sdk/pull/12342)). Contributed by @thoraj.
|
||||
* Add activity toggle for TAC ([#12413](https://github.com/matrix-org/matrix-react-sdk/pull/12413)). Contributed by @dbkr.
|
||||
* Humanize spell check language labels ([#12409](https://github.com/matrix-org/matrix-react-sdk/pull/12409)). Contributed by @t3chguy.
|
||||
* Call Guest Access, give user the option to change the acces level so they can generate a call link. ([#12401](https://github.com/matrix-org/matrix-react-sdk/pull/12401)). Contributed by @toger5.
|
||||
* TAC: Release Announcement ([#12380](https://github.com/matrix-org/matrix-react-sdk/pull/12380)). Contributed by @florianduros.
|
||||
* Show the call and share button if the user can create a guest link. ([#12385](https://github.com/matrix-org/matrix-react-sdk/pull/12385)). Contributed by @toger5.
|
||||
* Add analytics for mark all threads unread ([#12384](https://github.com/matrix-org/matrix-react-sdk/pull/12384)). Contributed by @dbkr.
|
||||
* Add `EventType.RoomEncryption` to the auto approve capabilities of Element Call widgets ([#12386](https://github.com/matrix-org/matrix-react-sdk/pull/12386)). Contributed by @toger5.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix link modal not shown after access upgrade ([#12411](https://github.com/matrix-org/matrix-react-sdk/pull/12411)). Contributed by @toger5.
|
||||
* Fix thread navigation in timeline ([#12412](https://github.com/matrix-org/matrix-react-sdk/pull/12412)). Contributed by @florianduros.
|
||||
* Fix inability to join a `knock` room via space hierarchy view ([#12404](https://github.com/matrix-org/matrix-react-sdk/pull/12404)). Contributed by @t3chguy.
|
||||
* Focus the thread panel when clicking on an item in the TAC ([#12410](https://github.com/matrix-org/matrix-react-sdk/pull/12410)). Contributed by @dbkr.
|
||||
* Fix space hierarchy tile busy state being stuck after join error ([#12405](https://github.com/matrix-org/matrix-react-sdk/pull/12405)). Contributed by @t3chguy.
|
||||
* Fix room topic in-app links not being handled correctly on topic dialog ([#12406](https://github.com/matrix-org/matrix-react-sdk/pull/12406)). Contributed by @t3chguy.
|
||||
|
||||
|
||||
Changes in [3.97.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.97.0) (2024-04-09)
|
||||
=====================================================================================================
|
||||
## ✨ Features
|
||||
|
18
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-react-sdk",
|
||||
"version": "3.97.0",
|
||||
"version": "3.98.0",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
@@ -60,14 +60,14 @@
|
||||
"lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react-dom": "17.0.21",
|
||||
"@types/react": "17.0.68",
|
||||
"@types/react-dom": "17.0.25",
|
||||
"@types/react": "17.0.80",
|
||||
"oidc-client-ts": "3.0.1",
|
||||
"jwt-decode": "4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/analytics-events": "^0.19.0",
|
||||
"@matrix-org/analytics-events": "^0.20.0",
|
||||
"@matrix-org/emojibase-bindings": "^1.1.2",
|
||||
"@matrix-org/matrix-wysiwyg": "2.17.0",
|
||||
"@matrix-org/olm": "3.2.15",
|
||||
@@ -76,7 +76,7 @@
|
||||
"@sentry/browser": "^7.0.0",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@vector-im/compound-design-tokens": "^1.2.0",
|
||||
"@vector-im/compound-web": "^4.0.2",
|
||||
"@vector-im/compound-web": "^4.1.0",
|
||||
"@zxcvbn-ts/core": "^3.0.4",
|
||||
"@zxcvbn-ts/language-common": "^3.0.4",
|
||||
"@zxcvbn-ts/language-en": "^3.0.2",
|
||||
@@ -116,7 +116,7 @@
|
||||
"opus-recorder": "^8.0.3",
|
||||
"pako": "^2.0.3",
|
||||
"png-chunks-extract": "^1.0.0",
|
||||
"posthog-js": "1.126.0",
|
||||
"posthog-js": "1.130.1",
|
||||
"proposal-temporal": "^0.9.0",
|
||||
"qrcode": "1.5.3",
|
||||
"re-resizable": "^6.9.0",
|
||||
@@ -176,9 +176,9 @@
|
||||
"@types/node-fetch": "^2.6.2",
|
||||
"@types/pako": "^2.0.0",
|
||||
"@types/qrcode": "^1.3.5",
|
||||
"@types/react": "17.0.68",
|
||||
"@types/react": "17.0.80",
|
||||
"@types/react-beautiful-dnd": "^13.0.0",
|
||||
"@types/react-dom": "17.0.21",
|
||||
"@types/react-dom": "17.0.25",
|
||||
"@types/react-transition-group": "^4.4.0",
|
||||
"@types/sanitize-html": "2.11.0",
|
||||
"@types/sdp-transform": "^2.4.6",
|
||||
@@ -223,7 +223,7 @@
|
||||
"stylelint-config-standard": "^36.0.0",
|
||||
"stylelint-scss": "^6.0.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "5.4.3"
|
||||
"typescript": "5.4.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4.19",
|
||||
|
@@ -15,14 +15,17 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Page } from "@playwright/test";
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { expect, test } from "../../element-web-test";
|
||||
import {
|
||||
copyAndContinue,
|
||||
createRoom,
|
||||
createSharedRoomWithUser,
|
||||
doTwoWaySasVerification,
|
||||
copyAndContinue,
|
||||
enableKeyBackup,
|
||||
logIntoElement,
|
||||
logOutOfElement,
|
||||
sendMessageInCurrentRoom,
|
||||
verifySession,
|
||||
waitForVerificationRequest,
|
||||
} from "./utils";
|
||||
import { Bot } from "../../pages/bot";
|
||||
@@ -453,8 +456,8 @@ test.describe("Cryptography", function () {
|
||||
// no e2e icon
|
||||
await expect(lastTileE2eIcon).not.toBeVisible();
|
||||
|
||||
// It can take up to 10 seconds for the key to be backed up. We don't really have much option other than
|
||||
// to wait :/
|
||||
// Workaround for https://github.com/element-hq/element-web/issues/27267. It can take up to 10 seconds for
|
||||
// the key to be backed up.
|
||||
await page.waitForTimeout(10000);
|
||||
|
||||
/* log out, and back in */
|
||||
@@ -532,4 +535,69 @@ test.describe("Cryptography", function () {
|
||||
).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("decryption failure messages", () => {
|
||||
test("should handle device-relative historical messages", async ({
|
||||
homeserver,
|
||||
page,
|
||||
app,
|
||||
credentials,
|
||||
user,
|
||||
cryptoBackend,
|
||||
}) => {
|
||||
test.skip(cryptoBackend === "legacy", "Not implemented for legacy crypto");
|
||||
test.setTimeout(60000);
|
||||
|
||||
// Start with a logged-in session, without key backup, and send a message.
|
||||
await createRoom(page, "Test room", true);
|
||||
await sendMessageInCurrentRoom(page, "test test");
|
||||
|
||||
// Log out, discarding the key for the sent message.
|
||||
await logOutOfElement(page, true);
|
||||
|
||||
// Log in again, and see how the message looks.
|
||||
await logIntoElement(page, homeserver, credentials);
|
||||
await app.viewRoomByName("Test room");
|
||||
const lastTile = page.locator(".mx_EventTile").last();
|
||||
await expect(lastTile).toContainText("Historical messages are not available on this device");
|
||||
await expect(lastTile.locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible();
|
||||
|
||||
// Now, we set up key backup, and then send another message.
|
||||
const secretStorageKey = await enableKeyBackup(app);
|
||||
await app.viewRoomByName("Test room");
|
||||
await sendMessageInCurrentRoom(page, "test2 test2");
|
||||
|
||||
// Workaround for https://github.com/element-hq/element-web/issues/27267. It can take up to 10 seconds for
|
||||
// the key to be backed up.
|
||||
await page.waitForTimeout(10000);
|
||||
|
||||
// Finally, log out again, and back in, skipping verification for now, and see what we see.
|
||||
await logOutOfElement(page);
|
||||
await logIntoElement(page, homeserver, credentials);
|
||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Skip verification for now" }).click();
|
||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "I'll verify later" }).click();
|
||||
await app.viewRoomByName("Test room");
|
||||
|
||||
// There should be two historical events in the timeline
|
||||
const tiles = await page.locator(".mx_EventTile").all();
|
||||
expect(tiles.length).toBeGreaterThanOrEqual(2);
|
||||
// look at the last two tiles only
|
||||
for (const tile of tiles.slice(-2)) {
|
||||
await expect(tile).toContainText("You need to verify this device for access to historical messages");
|
||||
await expect(tile.locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible();
|
||||
}
|
||||
|
||||
// Now verify our device (setting up key backup), and check what happens
|
||||
await verifySession(app, secretStorageKey);
|
||||
const tilesAfterVerify = (await page.locator(".mx_EventTile").all()).slice(-2);
|
||||
|
||||
// The first message still cannot be decrypted, because it was never backed up. It's now a regular UTD though.
|
||||
await expect(tilesAfterVerify[0]).toContainText("Unable to decrypt message");
|
||||
await expect(tilesAfterVerify[0].locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible();
|
||||
|
||||
// The second message should now be decrypted, with a grey shield
|
||||
await expect(tilesAfterVerify[1]).toContainText("test2 test2");
|
||||
await expect(tilesAfterVerify[1].locator(".mx_EventTile_e2eIcon_normal")).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -14,15 +14,15 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type Page, expect, JSHandle } from "@playwright/test";
|
||||
import { expect, JSHandle, type Page } from "@playwright/test";
|
||||
|
||||
import type { CryptoEvent, ICreateRoomOpts, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import type {
|
||||
EmojiMapping,
|
||||
ShowSasCallbacks,
|
||||
VerificationRequest,
|
||||
Verifier,
|
||||
EmojiMapping,
|
||||
VerifierEvent,
|
||||
ShowSasCallbacks,
|
||||
} from "matrix-js-sdk/src/crypto-api";
|
||||
import { Credentials, HomeserverInstance } from "../../plugins/homeserver";
|
||||
import { Client } from "../../pages/client";
|
||||
@@ -148,7 +148,7 @@ export async function logIntoElement(
|
||||
// select homeserver
|
||||
await page.getByRole("button", { name: "Edit" }).click();
|
||||
await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl);
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
|
||||
// wait for the dialog to go away
|
||||
await expect(page.locator(".mx_ServerPickerDialog")).not.toBeVisible();
|
||||
@@ -167,15 +167,40 @@ export async function logIntoElement(
|
||||
}
|
||||
}
|
||||
|
||||
export async function logOutOfElement(page: Page) {
|
||||
/**
|
||||
* Click the "sign out" option in Element, and wait for the login page to load
|
||||
*
|
||||
* @param page - Playwright `Page` object.
|
||||
* @param discardKeys - if true, expect a "You'll lose access to your encrypted messages" dialog, and dismiss it.
|
||||
*/
|
||||
export async function logOutOfElement(page: Page, discardKeys: boolean = false) {
|
||||
await page.getByRole("button", { name: "User menu" }).click();
|
||||
await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Sign out" }).click();
|
||||
if (discardKeys) {
|
||||
await page.getByRole("button", { name: "I don't want my encrypted messages" }).click();
|
||||
} else {
|
||||
await page.locator(".mx_Dialog .mx_QuestionDialog").getByRole("button", { name: "Sign out" }).click();
|
||||
}
|
||||
|
||||
// Wait for the login page to load
|
||||
await page.getByRole("heading", { name: "Sign in" }).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the security settings, and verify the current session using the security key.
|
||||
*
|
||||
* @param app - `ElementAppPage` wrapper for the playwright `Page`.
|
||||
* @param securityKey - The security key (i.e., 4S key), set up during a previous session.
|
||||
*/
|
||||
export async function verifySession(app: ElementAppPage, securityKey: string) {
|
||||
const settings = await app.settings.openUserSettings("Security & Privacy");
|
||||
await settings.getByRole("button", { name: "Verify this session" }).click();
|
||||
await app.page.getByRole("button", { name: "Verify with Security Key" }).click();
|
||||
await app.page.locator(".mx_Dialog").locator('input[type="password"]').fill(securityKey);
|
||||
await app.page.getByRole("button", { name: "Continue", disabled: false }).click();
|
||||
await app.page.getByRole("button", { name: "Done" }).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a SAS verifier for a bot client:
|
||||
* - wait for the bot to receive the emojis
|
||||
@@ -289,4 +314,9 @@ export async function createRoom(page: Page, roomName: string, isEncrypted: bool
|
||||
}
|
||||
|
||||
await dialog.getByRole("button", { name: "Create room" }).click();
|
||||
|
||||
// Wait for the client to process the encryption event before carrying on (and potentially sending events).
|
||||
if (isEncrypted) {
|
||||
await expect(page.getByText("Encryption enabled")).toBeVisible();
|
||||
}
|
||||
}
|
||||
|
@@ -187,11 +187,11 @@ test.describe("Read receipts", () => {
|
||||
// Given we have read the thread
|
||||
await util.goTo(room1);
|
||||
await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]);
|
||||
await util.assertUnread(room2, 2);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.goTo(room2);
|
||||
await util.openThread("Msg1");
|
||||
await util.assertRead(room2);
|
||||
await util.backToThreadsList();
|
||||
await util.assertReadThread("Resp1");
|
||||
await util.goTo(room1);
|
||||
|
||||
// When a message inside it is edited
|
||||
@@ -202,6 +202,7 @@ test.describe("Read receipts", () => {
|
||||
await util.goTo(room2);
|
||||
await util.assertReadThread("Msg1");
|
||||
});
|
||||
|
||||
test("Reading an edit of a threaded message makes the room read", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -211,11 +212,11 @@ test.describe("Read receipts", () => {
|
||||
// Given an edited thread message appears after we read it
|
||||
await util.goTo(room1);
|
||||
await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]);
|
||||
await util.assertUnread(room2, 2);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.goTo(room2);
|
||||
await util.openThread("Msg1");
|
||||
await util.assertRead(room2);
|
||||
await util.backToThreadsList();
|
||||
await util.assertReadThread("Resp1");
|
||||
await util.goTo(room1);
|
||||
await util.receiveMessages(room2, [msg.editOf("Resp1", "Edit1")]);
|
||||
await util.assertStillRead(room2);
|
||||
@@ -228,6 +229,7 @@ test.describe("Read receipts", () => {
|
||||
await util.assertStillRead(room2);
|
||||
await util.assertReadThread("Msg1");
|
||||
});
|
||||
|
||||
test("Marking a room as read after an edit in a thread makes it read", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -241,14 +243,16 @@ test.describe("Read receipts", () => {
|
||||
msg.threadedOff("Msg1", "Resp1"),
|
||||
msg.editOf("Resp1", "Edit1"),
|
||||
]);
|
||||
await util.assertUnread(room2, 2);
|
||||
await util.assertUnread(room2, 1);
|
||||
|
||||
// When I mark the room as read
|
||||
await util.markAsRead(room2);
|
||||
|
||||
// Then it is read
|
||||
await util.assertRead(room2);
|
||||
await util.assertReadThread("Msg1");
|
||||
});
|
||||
|
||||
test("Editing a thread message after marking as read leaves the room read", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -258,7 +262,7 @@ test.describe("Read receipts", () => {
|
||||
// Given a room is marked as read
|
||||
await util.goTo(room1);
|
||||
await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]);
|
||||
await util.assertUnread(room2, 2);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.markAsRead(room2);
|
||||
await util.assertRead(room2);
|
||||
|
||||
@@ -267,7 +271,9 @@ test.describe("Read receipts", () => {
|
||||
|
||||
// Then the room remains read
|
||||
await util.assertStillRead(room2);
|
||||
await util.assertReadThread("Msg1");
|
||||
});
|
||||
|
||||
test("A room with an edited threaded message is still read after restart", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -287,6 +293,7 @@ test.describe("Read receipts", () => {
|
||||
// Then is it still read
|
||||
await util.assertRead(room2);
|
||||
});
|
||||
|
||||
test("A room where all threaded edits are read is still read after restart", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -295,20 +302,23 @@ test.describe("Read receipts", () => {
|
||||
}) => {
|
||||
await util.goTo(room1);
|
||||
await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]);
|
||||
await util.assertUnread(room2, 2);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.receiveMessages(room2, [msg.editOf("Resp1", "Edit1")]);
|
||||
await util.assertUnread(room2, 2);
|
||||
await util.assertUnread(room2, 1);
|
||||
|
||||
await util.goTo(room2);
|
||||
|
||||
await util.openThread("Msg1");
|
||||
await util.assertRead(room2);
|
||||
await util.assertReadThread("Msg1");
|
||||
await util.goTo(room1); // Make sure we are looking at room1 after reload
|
||||
await util.assertStillRead(room2);
|
||||
|
||||
await util.saveAndReload();
|
||||
await util.assertRead(room2);
|
||||
await util.assertReadThread("Msg1");
|
||||
});
|
||||
|
||||
test("A room where all threaded edits are marked as read is still read after restart", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -321,15 +331,17 @@ test.describe("Read receipts", () => {
|
||||
msg.threadedOff("Msg1", "Resp1"),
|
||||
msg.editOf("Resp1", "Edit1"),
|
||||
]);
|
||||
await util.assertUnread(room2, 2);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.markAsRead(room2);
|
||||
await util.assertRead(room2);
|
||||
await util.assertReadThread("Msg1");
|
||||
|
||||
// When I restart
|
||||
await util.saveAndReload();
|
||||
|
||||
// It is still read
|
||||
await util.assertRead(room2);
|
||||
await util.assertReadThread("Msg1");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -343,7 +355,7 @@ test.describe("Read receipts", () => {
|
||||
// Given I have read a thread
|
||||
await util.goTo(room1);
|
||||
await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]);
|
||||
await util.assertUnread(room2, 2);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.goTo(room2);
|
||||
await util.openThread("Msg1");
|
||||
await util.backToThreadsList();
|
||||
@@ -361,6 +373,7 @@ test.describe("Read receipts", () => {
|
||||
await util.assertStillRead(room2);
|
||||
await util.assertReadThread("Edit1");
|
||||
});
|
||||
|
||||
test("Reading an edit of a thread root leaves the room read", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -386,6 +399,7 @@ test.describe("Read receipts", () => {
|
||||
await util.goTo(room1);
|
||||
await util.assertStillRead(room2);
|
||||
});
|
||||
|
||||
test("Editing a thread root after reading leaves the room read", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -405,6 +419,7 @@ test.describe("Read receipts", () => {
|
||||
// Then the room stays read
|
||||
await util.assertStillRead(room2);
|
||||
});
|
||||
|
||||
test("Marking a room as read after an edit of a thread root keeps it read", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -431,6 +446,7 @@ test.describe("Read receipts", () => {
|
||||
await util.goTo(room1);
|
||||
await util.assertStillRead(room2);
|
||||
});
|
||||
|
||||
test("Editing a thread root that is a reply after marking as read leaves the room read", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -444,7 +460,7 @@ test.describe("Read receipts", () => {
|
||||
msg.replyTo("Msg", "Reply"),
|
||||
msg.threadedOff("Reply", "InThread"),
|
||||
]);
|
||||
await util.assertUnread(room2, 3);
|
||||
await util.assertUnread(room2, 2);
|
||||
await util.markAsRead(room2);
|
||||
await util.assertRead(room2);
|
||||
|
||||
@@ -458,6 +474,7 @@ test.describe("Read receipts", () => {
|
||||
await util.goTo(room2);
|
||||
await util.assertReadThread("Edited Reply");
|
||||
});
|
||||
|
||||
test("Marking a room as read after an edit of a thread root that is a reply leaves it read", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -472,7 +489,7 @@ test.describe("Read receipts", () => {
|
||||
msg.threadedOff("Reply", "InThread"),
|
||||
]);
|
||||
await util.receiveMessages(room2, [msg.editOf("Reply", "Edited Reply")]);
|
||||
await util.assertUnread(room2, 3);
|
||||
await util.assertUnread(room2, 2);
|
||||
|
||||
// When I mark the room as read
|
||||
await util.markAsRead(room2);
|
||||
|
@@ -224,15 +224,15 @@ test.describe("Read receipts", () => {
|
||||
...msg.manyThreadedOff("Root3", many("T", 20)),
|
||||
]);
|
||||
await util.goTo(room2);
|
||||
await util.assertUnread(room2, 60);
|
||||
await util.assertRead(room2);
|
||||
await util.assertUnreadThread("Root1");
|
||||
await util.assertUnreadThread("Root2");
|
||||
await util.assertUnreadThread("Root3");
|
||||
await util.openThread("Root1");
|
||||
await util.assertUnread(room2, 40);
|
||||
await util.assertReadThread("Root1");
|
||||
await util.openThread("Root2");
|
||||
await util.assertUnread(room2, 20);
|
||||
await util.assertReadThread("Root2");
|
||||
await util.openThread("Root3");
|
||||
await util.assertRead(room2);
|
||||
await util.assertReadThread("Root3");
|
||||
|
||||
// When I restart and page up to load old thread roots
|
||||
@@ -247,6 +247,7 @@ test.describe("Read receipts", () => {
|
||||
await util.assertReadThread("Root2");
|
||||
await util.assertReadThread("Root3");
|
||||
});
|
||||
|
||||
test("Paging up to find old threads that were never read keeps the room unread", async ({
|
||||
cryptoBackend,
|
||||
roomAlpha: room1,
|
||||
@@ -268,7 +269,7 @@ test.describe("Read receipts", () => {
|
||||
...many("Msg", 100),
|
||||
]);
|
||||
await util.goTo(room2);
|
||||
await util.assertUnread(room2, 6);
|
||||
await util.assertRead(room2);
|
||||
await util.assertUnreadThread("Root1");
|
||||
await util.assertUnreadThread("Root2");
|
||||
await util.assertUnreadThread("Root3");
|
||||
@@ -278,20 +279,21 @@ test.describe("Read receipts", () => {
|
||||
await util.goTo(room1);
|
||||
await util.saveAndReload();
|
||||
|
||||
// Then the room remembers it's unread
|
||||
// Then the room remembers it's read
|
||||
// TODO: I (andyb) think this will fall in an encrypted room
|
||||
await util.assertUnread(room2, 6);
|
||||
await util.assertRead(room2);
|
||||
|
||||
// And when I page up to load old thread roots
|
||||
await util.goTo(room2);
|
||||
await util.pageUp();
|
||||
|
||||
// Then the room remains unread
|
||||
await util.assertUnread(room2, 6);
|
||||
// Then the room remains read
|
||||
await util.assertRead(room2);
|
||||
await util.assertUnreadThread("Root1");
|
||||
await util.assertUnreadThread("Root2");
|
||||
await util.assertUnreadThread("Root3");
|
||||
});
|
||||
|
||||
test("Looking in thread view to find old threads that were never read makes the room unread", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -310,7 +312,7 @@ test.describe("Read receipts", () => {
|
||||
...many("Msg", 100),
|
||||
]);
|
||||
await util.goTo(room2);
|
||||
await util.assertUnread(room2, 6);
|
||||
await util.assertRead(room2);
|
||||
await util.assertUnreadThread("Root1");
|
||||
await util.assertUnreadThread("Root2");
|
||||
await util.assertUnreadThread("Root3");
|
||||
@@ -320,20 +322,21 @@ test.describe("Read receipts", () => {
|
||||
await util.goTo(room1);
|
||||
await util.saveAndReload();
|
||||
|
||||
// Then the room remembers it's unread
|
||||
// Then the room remembers it's read
|
||||
// TODO: I (andyb) think this will fall in an encrypted room
|
||||
await util.assertUnread(room2, 6);
|
||||
await util.assertRead(room2);
|
||||
|
||||
// And when I open the threads view
|
||||
await util.goTo(room2);
|
||||
await util.openThreadList();
|
||||
|
||||
// Then the room remains unread
|
||||
await util.assertUnread(room2, 6);
|
||||
// Then the room remains read
|
||||
await util.assertRead(room2);
|
||||
await util.assertUnreadThread("Root1");
|
||||
await util.assertUnreadThread("Root2");
|
||||
await util.assertUnreadThread("Root3");
|
||||
});
|
||||
|
||||
test("After marking room as read, paging up to find old threads that were never read leaves the room read", async ({
|
||||
cryptoBackend,
|
||||
roomAlpha: room1,
|
||||
|
@@ -403,7 +403,7 @@ class Helpers {
|
||||
* tests we only open the threads panel.)
|
||||
*/
|
||||
async closeThreadsPanel() {
|
||||
await this.page.locator(".mx_RightPanel").getByTitle("Close").click();
|
||||
await this.page.locator(".mx_RightPanel").getByLabel("Close").click();
|
||||
await expect(this.page.locator(".mx_RightPanel")).not.toBeVisible();
|
||||
}
|
||||
|
||||
@@ -411,7 +411,7 @@ class Helpers {
|
||||
* Return to the list of threads, given we are viewing a single thread.
|
||||
*/
|
||||
async backToThreadsList() {
|
||||
await this.page.locator(".mx_RightPanel").getByTitle("Threads").click();
|
||||
await this.page.locator(".mx_RightPanel").getByLabel("Threads").click();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -539,7 +539,7 @@ class Helpers {
|
||||
const threadPanel = this.page.locator(".mx_ThreadPanel");
|
||||
await expect(threadPanel).toBeVisible();
|
||||
await threadPanel.evaluate(($panel) => {
|
||||
const $button = $panel.querySelector<HTMLElement>('.mx_BaseCard_back[title="Threads"]');
|
||||
const $button = $panel.querySelector<HTMLElement>('.mx_BaseCard_back[aria-label="Threads"]');
|
||||
// If the Threads back button is present then click it - the
|
||||
// threads button can open either threads list or thread panel
|
||||
if ($button) {
|
||||
|
@@ -183,9 +183,13 @@ test.describe("Read receipts", () => {
|
||||
// When I receive a threaded message
|
||||
await util.receiveMessages(room2, [msg.threadedOff("Msg1", "Resp1")]);
|
||||
|
||||
// Then the room becomes unread
|
||||
await util.assertUnread(room2, 1);
|
||||
// Then the room stays read
|
||||
await util.assertRead(room2);
|
||||
// but the thread is unread
|
||||
await util.goTo(room2);
|
||||
await util.assertUnreadThread("Msg1");
|
||||
});
|
||||
|
||||
test("Reading the last threaded message makes the room read", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -195,15 +199,16 @@ test.describe("Read receipts", () => {
|
||||
// Given a thread exists and is not read
|
||||
await util.goTo(room1);
|
||||
await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]);
|
||||
await util.assertUnread(room2, 2);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.goTo(room2);
|
||||
|
||||
// When I read it
|
||||
await util.openThread("Msg1");
|
||||
|
||||
// The room becomes read
|
||||
await util.assertRead(room2);
|
||||
// The thread becomes read
|
||||
await util.assertReadThread("Msg1");
|
||||
});
|
||||
|
||||
test("Reading a thread message makes the thread read", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -217,19 +222,20 @@ test.describe("Read receipts", () => {
|
||||
msg.threadedOff("Msg1", "Resp1"),
|
||||
msg.threadedOff("Msg1", "Resp2"),
|
||||
]);
|
||||
await util.assertUnread(room2, 3); // (Sanity)
|
||||
await util.assertUnread(room2, 1); // (Sanity)
|
||||
|
||||
// When I read the main timeline
|
||||
await util.goTo(room2);
|
||||
|
||||
// Then room does appear unread
|
||||
await util.assertUnread(room2, 2);
|
||||
// Then room is read
|
||||
await util.assertRead(room2);
|
||||
|
||||
// Until we open the thread
|
||||
// Reading the thread causes it to become read too
|
||||
await util.openThread("Msg1");
|
||||
await util.assertReadThread("Msg1");
|
||||
await util.assertRead(room2);
|
||||
});
|
||||
|
||||
test("Reading an older thread message leaves the thread unread", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -242,40 +248,19 @@ test.describe("Read receipts", () => {
|
||||
"ThreadRoot",
|
||||
...msg.manyThreadedOff("ThreadRoot", many("InThread", 20)),
|
||||
]);
|
||||
await util.assertUnread(room2, 21);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.goTo(room2);
|
||||
await util.assertUnreadThread("ThreadRoot");
|
||||
await util.goTo(room1);
|
||||
|
||||
// When I read an older message in the thread
|
||||
await msg.jumpTo(room2.name, "InThread0000", true);
|
||||
await util.assertUnreadLessThan(room2, 21);
|
||||
|
||||
// Then the thread is still marked as unread
|
||||
await util.backToThreadsList();
|
||||
await util.assertUnreadThread("ThreadRoot");
|
||||
});
|
||||
test("Reading only one thread's message does not make the room read", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
util,
|
||||
msg,
|
||||
}) => {
|
||||
// Given two threads are unread
|
||||
await util.goTo(room1);
|
||||
await util.receiveMessages(room2, [
|
||||
"Msg1",
|
||||
msg.threadedOff("Msg1", "Resp1"),
|
||||
"Msg2",
|
||||
msg.threadedOff("Msg2", "Resp2"),
|
||||
]);
|
||||
await util.assertUnread(room2, 4);
|
||||
await util.goTo(room2);
|
||||
await util.assertUnread(room2, 2);
|
||||
|
||||
// When I only read one of them
|
||||
await util.openThread("Msg1");
|
||||
|
||||
// The room is still unread
|
||||
await util.assertUnread(room2, 1);
|
||||
});
|
||||
test("Reading only one thread's message makes that thread read but not others", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -290,9 +275,9 @@ test.describe("Read receipts", () => {
|
||||
msg.threadedOff("Msg1", "Resp1"),
|
||||
msg.threadedOff("Msg2", "Resp2"),
|
||||
]);
|
||||
await util.assertUnread(room2, 4); // (Sanity)
|
||||
await util.assertUnread(room2, 2); // (Sanity)
|
||||
await util.goTo(room2);
|
||||
await util.assertUnread(room2, 2);
|
||||
await util.assertRead(room2);
|
||||
await util.assertUnreadThread("Msg1");
|
||||
await util.assertUnreadThread("Msg2");
|
||||
|
||||
@@ -303,6 +288,7 @@ test.describe("Read receipts", () => {
|
||||
await util.assertReadThread("Msg1");
|
||||
await util.assertUnreadThread("Msg2");
|
||||
});
|
||||
|
||||
test("Reading the main timeline does not mark a thread message as read", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -316,15 +302,16 @@ test.describe("Read receipts", () => {
|
||||
msg.threadedOff("Msg1", "Resp1"),
|
||||
msg.threadedOff("Msg1", "Resp2"),
|
||||
]);
|
||||
await util.assertUnread(room2, 3); // (Sanity)
|
||||
await util.assertUnread(room2, 1); // (Sanity)
|
||||
|
||||
// When I read the main timeline
|
||||
await util.goTo(room2);
|
||||
await util.assertUnread(room2, 2);
|
||||
await util.assertRead(room2);
|
||||
|
||||
// Then thread does appear unread
|
||||
await util.assertUnreadThread("Msg1");
|
||||
});
|
||||
|
||||
test("Marking a room with unread threads as read makes it read", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -338,14 +325,17 @@ test.describe("Read receipts", () => {
|
||||
msg.threadedOff("Msg1", "Resp1"),
|
||||
msg.threadedOff("Msg1", "Resp2"),
|
||||
]);
|
||||
await util.assertUnread(room2, 3); // (Sanity)
|
||||
await util.assertUnread(room2, 1); // (Sanity)
|
||||
|
||||
// When I mark the room as read
|
||||
await util.markAsRead(room2);
|
||||
|
||||
// Then the room is read
|
||||
await util.assertRead(room2);
|
||||
// and so are the threads
|
||||
await util.assertReadThread("Msg1");
|
||||
});
|
||||
|
||||
test("Sending a new thread message after marking as read makes it unread", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -367,9 +357,11 @@ test.describe("Read receipts", () => {
|
||||
// Then another message appears in the thread
|
||||
await util.receiveMessages(room2, [msg.threadedOff("Msg1", "Resp3")]);
|
||||
|
||||
// Then the room becomes unread
|
||||
await util.assertUnread(room2, 1);
|
||||
// Then the thread becomes unread
|
||||
await util.goTo(room2);
|
||||
await util.assertUnreadThread("Msg1");
|
||||
});
|
||||
|
||||
test("Sending a new different-thread message after marking as read makes it unread", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -381,11 +373,8 @@ test.describe("Read receipts", () => {
|
||||
await util.receiveMessages(room2, ["Thread1", "Thread2", msg.threadedOff("Thread1", "t1a")]);
|
||||
// Make sure the message in Thread 1 has definitely arrived, so that we know for sure
|
||||
// that the one in Thread 2 is the latest.
|
||||
await util.assertUnread(room2, 3);
|
||||
|
||||
await util.receiveMessages(room2, [msg.threadedOff("Thread2", "t2a")]);
|
||||
// Make sure the 4th message has arrived before we mark as read.
|
||||
await util.assertUnread(room2, 4);
|
||||
|
||||
// When I mark the room as read (making an unthreaded receipt for t2a)
|
||||
await util.markAsRead(room2);
|
||||
@@ -394,9 +383,11 @@ test.describe("Read receipts", () => {
|
||||
// Then another message appears in the other thread
|
||||
await util.receiveMessages(room2, [msg.threadedOff("Thread1", "t1b")]);
|
||||
|
||||
// Then the room becomes unread
|
||||
await util.assertUnread(room2, 1);
|
||||
// Then the other thread becomes unread
|
||||
await util.goTo(room2);
|
||||
await util.assertUnreadThread("Thread1");
|
||||
});
|
||||
|
||||
test("A room with a new threaded message is still unread after restart", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -410,21 +401,26 @@ test.describe("Read receipts", () => {
|
||||
msg.threadedOff("Msg1", "Resp1"),
|
||||
msg.threadedOff("Msg1", "Resp2"),
|
||||
]);
|
||||
await util.assertUnread(room2, 3); // (Sanity)
|
||||
await util.assertUnread(room2, 1); // (Sanity)
|
||||
|
||||
// When I read the main timeline
|
||||
await util.goTo(room2);
|
||||
|
||||
// Then room does appear unread
|
||||
await util.assertUnread(room2, 2);
|
||||
// Then room appears read
|
||||
await util.assertRead(room2);
|
||||
/// but with an unread thread
|
||||
await util.assertUnreadThread("Msg1");
|
||||
|
||||
await util.saveAndReload();
|
||||
await util.assertUnread(room2, 2);
|
||||
|
||||
// Until we open the thread
|
||||
await util.openThread("Msg1");
|
||||
await util.assertRead(room2);
|
||||
await util.goTo(room2);
|
||||
await util.assertUnreadThread("Msg1");
|
||||
|
||||
// Opening the thread now marks it as read
|
||||
await util.openThread("Msg1");
|
||||
await util.assertReadThread("Msg1");
|
||||
});
|
||||
|
||||
test("A room where all threaded messages are read is still read after restart", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -438,17 +434,20 @@ test.describe("Read receipts", () => {
|
||||
msg.threadedOff("Msg1", "Resp1"),
|
||||
msg.threadedOff("Msg1", "Resp2"),
|
||||
]);
|
||||
await util.assertUnread(room2, 3); // (Sanity)
|
||||
await util.assertUnread(room2, 1); // (Sanity)
|
||||
await util.goTo(room2);
|
||||
await util.assertUnread(room2, 2);
|
||||
await util.openThread("Msg1");
|
||||
await util.assertRead(room2);
|
||||
await util.assertUnreadThread("Msg1");
|
||||
await util.openThread("Msg1");
|
||||
await util.assertReadThread("Msg1");
|
||||
|
||||
// When I restart
|
||||
await util.saveAndReload();
|
||||
|
||||
// Then the room is still read
|
||||
// Then the room & thread still read
|
||||
await util.assertRead(room2);
|
||||
await util.goTo(room2);
|
||||
await util.assertReadThread("Msg1");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -462,15 +461,16 @@ test.describe("Read receipts", () => {
|
||||
// Given a thread exists
|
||||
await util.goTo(room1);
|
||||
await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Resp1")]);
|
||||
await util.assertUnread(room2, 2); // (Sanity)
|
||||
await util.assertUnread(room2, 1); // (Sanity)
|
||||
|
||||
// When I read the main timeline
|
||||
await util.goTo(room2);
|
||||
|
||||
// Then room does appear unread
|
||||
await util.assertUnread(room2, 1);
|
||||
// Then room doesn't appear unread but the thread does
|
||||
await util.assertRead(room2);
|
||||
await util.assertUnreadThread("Msg1");
|
||||
});
|
||||
|
||||
test("Reading a thread root within the thread view marks it as read in the main timeline", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -485,7 +485,7 @@ test.describe("Read receipts", () => {
|
||||
msg.threadedOff("ThreadRoot", "InThread"),
|
||||
...many("afterThread", 30),
|
||||
]);
|
||||
await util.assertUnread(room2, 62); // Sanity
|
||||
await util.assertUnread(room2, 61); // Sanity
|
||||
|
||||
// When I jump to an old message and read the thread
|
||||
await msg.jumpTo(room2.name, "beforeThread0000");
|
||||
@@ -496,6 +496,7 @@ test.describe("Read receipts", () => {
|
||||
// 30 remaining messages are unread - 7 messages are displayed under the thread root
|
||||
await util.assertUnread(room2, 30 - 7);
|
||||
});
|
||||
|
||||
test("Creating a new thread based on a reply makes the room unread", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -513,10 +514,12 @@ test.describe("Read receipts", () => {
|
||||
// When I receive a thread message created on the reply
|
||||
await util.receiveMessages(room2, [msg.threadedOff("Reply1", "Resp1")]);
|
||||
|
||||
// Then the room is unread
|
||||
await util.assertUnread(room2, 1);
|
||||
// Then the thread is unread
|
||||
await util.goTo(room2);
|
||||
await util.assertUnreadThread("Reply1");
|
||||
});
|
||||
test("Reading a thread whose root is a reply makes the room read", async ({
|
||||
|
||||
test("Reading a thread whose root is a reply makes the thread read", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
util,
|
||||
@@ -529,9 +532,9 @@ test.describe("Read receipts", () => {
|
||||
msg.replyTo("Msg1", "Reply1"),
|
||||
msg.threadedOff("Reply1", "Resp1"),
|
||||
]);
|
||||
await util.assertUnread(room2, 3);
|
||||
await util.assertUnread(room2, 2);
|
||||
await util.goTo(room2);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.assertRead(room2);
|
||||
await util.assertUnreadThread("Reply1");
|
||||
|
||||
// When I read the thread
|
||||
|
@@ -107,10 +107,11 @@ test.describe("Read receipts", () => {
|
||||
await util.goTo(room1);
|
||||
await util.assertRead(room2);
|
||||
await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Reply1")]);
|
||||
await util.assertUnread(room2, 2);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.goTo(room2);
|
||||
await util.openThread("Msg1");
|
||||
await util.assertRead(room2);
|
||||
await util.assertReadThread("Msg1");
|
||||
await util.goTo(room1);
|
||||
|
||||
// When someone reacts to a thread message
|
||||
@@ -118,7 +119,9 @@ test.describe("Read receipts", () => {
|
||||
|
||||
// Then the room remains read
|
||||
await util.assertStillRead(room2);
|
||||
await util.assertReadThread("Msg1");
|
||||
});
|
||||
|
||||
test("Marking a room as read after a reaction in a thread makes it read", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -133,7 +136,7 @@ test.describe("Read receipts", () => {
|
||||
msg.threadedOff("Msg1", "Reply1"),
|
||||
msg.reactionTo("Reply1", "🪿"),
|
||||
]);
|
||||
await util.assertUnread(room2, 2);
|
||||
await util.assertUnread(room2, 1);
|
||||
|
||||
// When I mark the room as read
|
||||
await util.markAsRead(room2);
|
||||
@@ -141,6 +144,7 @@ test.describe("Read receipts", () => {
|
||||
// Then it becomes read
|
||||
await util.assertRead(room2);
|
||||
});
|
||||
|
||||
test("Reacting to a thread message after marking as read does not make the room unread", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -155,7 +159,7 @@ test.describe("Read receipts", () => {
|
||||
msg.threadedOff("Msg1", "Reply1"),
|
||||
msg.reactionTo("Reply1", "🪿"),
|
||||
]);
|
||||
await util.assertUnread(room2, 2);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.markAsRead(room2);
|
||||
await util.assertRead(room2);
|
||||
|
||||
@@ -164,7 +168,10 @@ test.describe("Read receipts", () => {
|
||||
|
||||
// Then the room remains read
|
||||
await util.assertStillRead(room2);
|
||||
// as does the thread
|
||||
await util.assertReadThread("Msg1");
|
||||
});
|
||||
|
||||
test("A room with a reaction to a threaded message is still unread after restart", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -175,22 +182,25 @@ test.describe("Read receipts", () => {
|
||||
await util.goTo(room1);
|
||||
await util.assertRead(room2);
|
||||
await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Reply1")]);
|
||||
await util.assertUnread(room2, 2);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.goTo(room2);
|
||||
await util.openThread("Msg1");
|
||||
await util.assertRead(room2);
|
||||
await util.goTo(room1);
|
||||
|
||||
// And someone reacted to it, which doesn't stop it being read
|
||||
// And someone reacted to it, which doesn't make it read
|
||||
await util.receiveMessages(room2, [msg.reactionTo("Reply1", "🪿")]);
|
||||
await util.assertStillRead(room2);
|
||||
await util.assertReadThread("Msg1");
|
||||
|
||||
// When I restart
|
||||
await util.saveAndReload();
|
||||
|
||||
// Then the room is still read
|
||||
await util.assertRead(room2);
|
||||
await util.assertReadThread("Msg1");
|
||||
});
|
||||
|
||||
test("A room where all reactions in threads are read is still read after restart", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -213,7 +223,7 @@ test.describe("Read receipts", () => {
|
||||
msg.reactionTo("Reply2b", "c"),
|
||||
msg.reactionTo("Reply1b", "t"),
|
||||
]);
|
||||
await util.assertUnread(room2, 6);
|
||||
await util.assertUnread(room2, 2);
|
||||
await util.goTo(room2);
|
||||
await util.openThread("Msg1");
|
||||
await util.assertReadThread("Msg1");
|
||||
@@ -231,6 +241,7 @@ test.describe("Read receipts", () => {
|
||||
await util.assertReadThread("Msg1");
|
||||
await util.assertReadThread("Msg2");
|
||||
});
|
||||
|
||||
test("Can remove a reaction in a thread", async ({
|
||||
page,
|
||||
roomAlpha: room1,
|
||||
@@ -247,7 +258,7 @@ test.describe("Read receipts", () => {
|
||||
await util.goTo(room1);
|
||||
await util.assertRead(room2);
|
||||
await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Reply1a")]);
|
||||
await util.assertUnread(room2, 2);
|
||||
await util.assertUnread(room2, 1);
|
||||
|
||||
// When I react to a thread message
|
||||
await util.goTo(room2);
|
||||
@@ -283,10 +294,11 @@ test.describe("Read receipts", () => {
|
||||
await util.goTo(room1);
|
||||
await util.assertRead(room2);
|
||||
await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Reply1")]);
|
||||
await util.assertUnread(room2, 2);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.goTo(room2);
|
||||
await util.openThread("Msg1");
|
||||
await util.assertRead(room2);
|
||||
await util.assertReadThread("Msg1");
|
||||
|
||||
// When someone reacts to it
|
||||
await util.goTo(room1);
|
||||
@@ -295,7 +307,10 @@ test.describe("Read receipts", () => {
|
||||
|
||||
// Then the room is still read
|
||||
await util.assertRead(room2);
|
||||
// as is the thread
|
||||
await util.assertReadThread("Msg1");
|
||||
});
|
||||
|
||||
test("Reading a reaction to a thread root leaves the room read", async ({
|
||||
page,
|
||||
roomAlpha: room1,
|
||||
@@ -307,7 +322,7 @@ test.describe("Read receipts", () => {
|
||||
await util.goTo(room1);
|
||||
await util.assertRead(room2);
|
||||
await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Reply1")]);
|
||||
await util.assertUnread(room2, 2);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.goTo(room2);
|
||||
await util.openThread("Msg1");
|
||||
await util.assertRead(room2);
|
||||
@@ -316,6 +331,7 @@ test.describe("Read receipts", () => {
|
||||
await util.goTo(room1);
|
||||
await util.receiveMessages(room2, [msg.reactionTo("Msg1", "🪿")]);
|
||||
await util.assertRead(room2);
|
||||
await util.assertReadThread("Msg1");
|
||||
|
||||
// When we read the reaction and go away again
|
||||
await util.goTo(room2);
|
||||
@@ -326,7 +342,9 @@ test.describe("Read receipts", () => {
|
||||
|
||||
// Then the room is still read
|
||||
await util.assertRead(room2);
|
||||
await util.assertReadThread("Msg1");
|
||||
});
|
||||
|
||||
test("Reacting to a thread root after marking as read makes the room unread but not the thread", async ({
|
||||
page,
|
||||
roomAlpha: room1,
|
||||
@@ -338,11 +356,12 @@ test.describe("Read receipts", () => {
|
||||
await util.goTo(room1);
|
||||
await util.assertRead(room2);
|
||||
await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Reply1")]);
|
||||
await util.assertUnread(room2, 2);
|
||||
await util.assertUnread(room2, 1);
|
||||
|
||||
// And we have marked the room as read
|
||||
await util.markAsRead(room2);
|
||||
await util.assertRead(room2);
|
||||
await util.assertReadThread("Msg1");
|
||||
|
||||
// When someone reacts to it
|
||||
await util.receiveMessages(room2, [msg.reactionTo("Msg1", "🪿")]);
|
||||
@@ -350,6 +369,8 @@ test.describe("Read receipts", () => {
|
||||
|
||||
// Then the room is still read
|
||||
await util.assertRead(room2);
|
||||
// as is the thread
|
||||
await util.assertReadThread("Msg1");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -16,9 +16,10 @@ limitations under the License.
|
||||
|
||||
import type { JSHandle } from "@playwright/test";
|
||||
import type { MatrixEvent, ISendEventResponse, ReceiptType } from "matrix-js-sdk/src/matrix";
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { expect } from "../../element-web-test";
|
||||
import { ElementAppPage } from "../../pages/ElementAppPage";
|
||||
import { Bot } from "../../pages/bot";
|
||||
import { test } from ".";
|
||||
|
||||
test.describe("Read receipts", () => {
|
||||
test.use({
|
||||
@@ -189,29 +190,31 @@ test.describe("Read receipts", () => {
|
||||
page,
|
||||
app,
|
||||
bot,
|
||||
util,
|
||||
}) => {
|
||||
// Given we sent 3 events on the main thread
|
||||
const main1 = await sendMessage(bot);
|
||||
const thread1a = await botSendThreadMessage(bot, main1.event_id);
|
||||
await botSendThreadMessage(bot, main1.event_id);
|
||||
// 1 unread on the main thread, 2 in the new thread
|
||||
await expect(page.getByLabel(`${otherRoomName} 3 unread messages.`)).toBeVisible();
|
||||
// 1 unread on the main thread, 2 in the new thread that aren't shown
|
||||
await expect(page.getByLabel(`${otherRoomName} 1 unread message.`)).toBeVisible();
|
||||
|
||||
// When we send receipts for main, and the second-last in the thread
|
||||
await sendThreadedReadReceipt(app, main1);
|
||||
await sendThreadedReadReceipt(app, thread1a, main1);
|
||||
|
||||
// Then the room has only one unread - the one in the thread
|
||||
await expect(page.getByLabel(`${otherRoomName} 1 unread message.`)).toBeVisible();
|
||||
await util.goTo(otherRoomName);
|
||||
await util.assertUnreadThread("Message 1");
|
||||
});
|
||||
|
||||
test("Considers room read if there are receipts for main and other thread", async ({ page, app, bot }) => {
|
||||
test("Considers room read if there are receipts for main and other thread", async ({ page, app, bot, util }) => {
|
||||
// Given we sent 3 events on the main thread
|
||||
const main1 = await sendMessage(bot);
|
||||
await botSendThreadMessage(bot, main1.event_id);
|
||||
const thread1b = await botSendThreadMessage(bot, main1.event_id);
|
||||
// 1 unread on the main thread, 2 in the new thread
|
||||
await expect(page.getByLabel(`${otherRoomName} 3 unread messages.`)).toBeVisible();
|
||||
// 1 unread on the main thread, 2 in the new thread which don't show
|
||||
await expect(page.getByLabel(`${otherRoomName} 1 unread message.`)).toBeVisible();
|
||||
|
||||
// When we send receipts for main, and the last in the thread
|
||||
await sendThreadedReadReceipt(app, main1);
|
||||
@@ -219,27 +222,33 @@ test.describe("Read receipts", () => {
|
||||
|
||||
// Then the room has no unreads
|
||||
await expect(page.getByLabel(`${otherRoomName}`)).toBeVisible();
|
||||
await util.goTo(otherRoomName);
|
||||
await util.assertReadThread("Message 1");
|
||||
});
|
||||
|
||||
test("Recognises unread messages on a thread after receiving a unthreaded receipt for earlier ones", async ({
|
||||
page,
|
||||
app,
|
||||
bot,
|
||||
util,
|
||||
}) => {
|
||||
// Given we sent 3 events on the main thread
|
||||
const main1 = await sendMessage(bot);
|
||||
const thread1a = await botSendThreadMessage(bot, main1.event_id);
|
||||
await botSendThreadMessage(bot, main1.event_id);
|
||||
// 1 unread on the main thread, 2 in the new thread
|
||||
await expect(page.getByLabel(`${otherRoomName} 3 unread messages.`)).toBeVisible();
|
||||
// 1 unread on the main thread, 2 in the new thread which don't count
|
||||
await expect(page.getByLabel(`${otherRoomName} 1 unread message.`)).toBeVisible();
|
||||
|
||||
// When we send an unthreaded receipt for the second-last in the thread
|
||||
await sendUnthreadedReadReceipt(app, thread1a);
|
||||
|
||||
// Then the room has only one unread - the one in the
|
||||
// thread. The one in main is read because the unthreaded
|
||||
// receipt is for a later event.
|
||||
await expect(page.getByLabel(`${otherRoomName} 1 unread message.`)).toBeVisible();
|
||||
// receipt is for a later event. The room should therefore be
|
||||
// read, and the thread unread.
|
||||
await expect(page.getByLabel(`${otherRoomName}`)).toBeVisible();
|
||||
await util.goTo(otherRoomName);
|
||||
await util.assertUnreadThread("Message 1");
|
||||
});
|
||||
|
||||
test("Recognises unread messages on main after receiving a unthreaded receipt for a thread message", async ({
|
||||
@@ -252,8 +261,8 @@ test.describe("Read receipts", () => {
|
||||
await botSendThreadMessage(bot, main1.event_id);
|
||||
const thread1b = await botSendThreadMessage(bot, main1.event_id);
|
||||
await sendMessage(bot);
|
||||
// 2 unreads on the main thread, 2 in the new thread
|
||||
await expect(page.getByLabel(`${otherRoomName} 4 unread messages.`)).toBeVisible();
|
||||
// 2 unreads on the main thread, 2 in the new thread which don't count
|
||||
await expect(page.getByLabel(`${otherRoomName} 2 unread messages.`)).toBeVisible();
|
||||
|
||||
// When we send an unthreaded receipt for the last in the thread
|
||||
await sendUnthreadedReadReceipt(app, thread1b);
|
||||
|
@@ -344,18 +344,23 @@ test.describe("Read receipts", () => {
|
||||
"Root2",
|
||||
msg.threadedOff("Root2", "Root2->A"),
|
||||
]);
|
||||
await util.assertUnread(room2, 5);
|
||||
await util.assertUnread(room2, 2);
|
||||
|
||||
// And I have read them
|
||||
await util.goTo(room2);
|
||||
await util.assertUnreadThread("Root1");
|
||||
await util.assertUnreadThread("Root2");
|
||||
|
||||
// And I have read them
|
||||
await util.assertUnreadThread("Root1");
|
||||
await util.openThread("Root1");
|
||||
await util.assertUnreadLessThan(room2, 4);
|
||||
await util.openThread("Root2");
|
||||
await util.assertRead(room2);
|
||||
await util.backToThreadsList();
|
||||
await util.assertReadThread("Root1");
|
||||
|
||||
await util.openThread("Root2");
|
||||
await util.assertReadThread("Root2");
|
||||
await util.closeThreadsPanel();
|
||||
await util.goTo(room1);
|
||||
await util.assertRead(room2);
|
||||
|
||||
// When the latest message in a thread is redacted
|
||||
await util.receiveMessages(room2, [msg.redactionOf("ThreadMsg2")]);
|
||||
@@ -365,6 +370,7 @@ test.describe("Read receipts", () => {
|
||||
await util.goTo(room2);
|
||||
await util.assertReadThread("Root1");
|
||||
});
|
||||
|
||||
test("Reading an unread thread after a redaction of the latest message makes it read", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -378,9 +384,9 @@ test.describe("Read receipts", () => {
|
||||
msg.threadedOff("Root", "ThreadMsg1"),
|
||||
msg.threadedOff("Root", "ThreadMsg2"),
|
||||
]);
|
||||
await util.assertUnread(room2, 3);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.receiveMessages(room2, [msg.redactionOf("ThreadMsg2")]);
|
||||
await util.assertUnread(room2, 2);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.goTo(room2);
|
||||
await util.assertUnreadThread("Root");
|
||||
|
||||
@@ -395,6 +401,7 @@ test.describe("Read receipts", () => {
|
||||
await util.goTo(room2);
|
||||
await util.assertReadThread("Root");
|
||||
});
|
||||
|
||||
test("Reading an unread thread after a redaction of the latest message makes it read after restart", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -408,9 +415,9 @@ test.describe("Read receipts", () => {
|
||||
msg.threadedOff("Root", "ThreadMsg1"),
|
||||
msg.threadedOff("Root", "ThreadMsg2"),
|
||||
]);
|
||||
await util.assertUnread(room2, 3);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.receiveMessages(room2, [msg.redactionOf("ThreadMsg2")]);
|
||||
await util.assertUnread(room2, 2);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.goTo(room2);
|
||||
await util.assertUnreadThread("Root");
|
||||
await util.openThread("Root");
|
||||
@@ -424,9 +431,12 @@ test.describe("Read receipts", () => {
|
||||
// When I restart
|
||||
await util.saveAndReload();
|
||||
|
||||
// Then the room is still read
|
||||
// Then the room and thread are still read
|
||||
await util.assertRead(room2);
|
||||
await util.openThreadList();
|
||||
await util.assertReadThread("Root");
|
||||
});
|
||||
|
||||
test("Reading an unread thread after a redaction of an older message makes it read", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -440,9 +450,9 @@ test.describe("Read receipts", () => {
|
||||
msg.threadedOff("Root", "ThreadMsg1"),
|
||||
msg.threadedOff("Root", "ThreadMsg2"),
|
||||
]);
|
||||
await util.assertUnread(room2, 3);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.receiveMessages(room2, [msg.redactionOf("ThreadMsg1")]);
|
||||
await util.assertUnread(room2, 2);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.goTo(room2);
|
||||
await util.assertUnreadThread("Root");
|
||||
|
||||
@@ -457,6 +467,7 @@ test.describe("Read receipts", () => {
|
||||
await util.goTo(room2);
|
||||
await util.assertReadThread("Root");
|
||||
});
|
||||
|
||||
test("Marking an unread thread as read after a redaction makes it read", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -470,9 +481,9 @@ test.describe("Read receipts", () => {
|
||||
msg.threadedOff("Root", "ThreadMsg1"),
|
||||
msg.threadedOff("Root", "ThreadMsg2"),
|
||||
]);
|
||||
await util.assertUnread(room2, 3);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.receiveMessages(room2, [msg.redactionOf("ThreadMsg1")]);
|
||||
await util.assertUnread(room2, 2);
|
||||
await util.assertUnread(room2, 1);
|
||||
|
||||
// When I mark the room as read
|
||||
await util.markAsRead(room2);
|
||||
@@ -483,6 +494,7 @@ test.describe("Read receipts", () => {
|
||||
await util.goTo(room2);
|
||||
await util.assertReadThread("Root");
|
||||
});
|
||||
|
||||
test("Sending and redacting a message after marking the thread as read leaves it read", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -496,20 +508,22 @@ test.describe("Read receipts", () => {
|
||||
msg.threadedOff("Root", "ThreadMsg1"),
|
||||
msg.threadedOff("Root", "ThreadMsg2"),
|
||||
]);
|
||||
await util.assertUnread(room2, 3);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.markAsRead(room2);
|
||||
await util.assertRead(room2);
|
||||
|
||||
// When I send and redact a message
|
||||
await util.receiveMessages(room2, [msg.threadedOff("Root", "Msg3")]);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.goTo(room2);
|
||||
await util.openThreadList();
|
||||
await util.assertUnreadThread("Root");
|
||||
await util.receiveMessages(room2, [msg.redactionOf("Msg3")]);
|
||||
|
||||
// Then the room and thread are read
|
||||
await util.assertRead(room2);
|
||||
await util.goTo(room2);
|
||||
await util.assertReadThread("Root");
|
||||
});
|
||||
|
||||
test("Redacting a message after marking the thread as read leaves it read", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -523,7 +537,7 @@ test.describe("Read receipts", () => {
|
||||
msg.threadedOff("Root", "ThreadMsg1"),
|
||||
msg.threadedOff("Root", "ThreadMsg2"),
|
||||
]);
|
||||
await util.assertUnread(room2, 3);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.markAsRead(room2);
|
||||
await util.assertRead(room2);
|
||||
|
||||
@@ -535,6 +549,7 @@ test.describe("Read receipts", () => {
|
||||
await util.goTo(room2);
|
||||
await util.assertReadThread("Root");
|
||||
});
|
||||
|
||||
test("Reacting to a redacted message leaves the thread read", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -548,21 +563,27 @@ test.describe("Read receipts", () => {
|
||||
msg.threadedOff("Root", "Msg2"),
|
||||
msg.threadedOff("Root", "Msg3"),
|
||||
]);
|
||||
await util.assertUnread(room2, 3);
|
||||
await util.receiveMessages(room2, [msg.redactionOf("Msg2")]);
|
||||
await util.assertUnread(room2, 2);
|
||||
await util.goTo(room2);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.receiveMessages(room2, [msg.redactionOf("Msg2")]);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.goTo(room2);
|
||||
await util.assertRead(room2);
|
||||
await util.openThread("Root");
|
||||
await util.assertRead(room2);
|
||||
await util.backToThreadsList();
|
||||
await util.assertReadThread("Root");
|
||||
await util.goTo(room1);
|
||||
|
||||
// When we receive a reaction to the redacted event
|
||||
await util.receiveMessages(room2, [msg.reactionTo("Msg2", "z")]);
|
||||
|
||||
// Then the room is unread
|
||||
// Then the room is read
|
||||
await util.assertStillRead(room2);
|
||||
await util.goTo(room2);
|
||||
await util.openThreadList();
|
||||
await util.assertReadThread("Root");
|
||||
});
|
||||
|
||||
test("Editing a redacted message leaves the thread read", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -576,13 +597,15 @@ test.describe("Read receipts", () => {
|
||||
msg.threadedOff("Root", "Msg2"),
|
||||
msg.threadedOff("Root", "Msg3"),
|
||||
]);
|
||||
await util.assertUnread(room2, 3);
|
||||
await util.receiveMessages(room2, [msg.redactionOf("Msg2")]);
|
||||
await util.assertUnread(room2, 2);
|
||||
await util.goTo(room2);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.openThread("Root");
|
||||
await util.receiveMessages(room2, [msg.redactionOf("Msg2")]);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.goTo(room2);
|
||||
await util.assertRead(room2);
|
||||
await util.openThreadList();
|
||||
await util.assertUnreadThread("Root");
|
||||
await util.openThread("Root");
|
||||
await util.assertReadThread("Root");
|
||||
await util.goTo(room1);
|
||||
|
||||
// When we receive an edit of the redacted message
|
||||
@@ -590,7 +613,12 @@ test.describe("Read receipts", () => {
|
||||
|
||||
// Then the room is unread
|
||||
await util.assertStillRead(room2);
|
||||
// and so is the thread
|
||||
await util.goTo(room2);
|
||||
await util.openThreadList();
|
||||
await util.assertReadThread("Root");
|
||||
});
|
||||
|
||||
test("Reading a thread after a reaction to a redacted message marks the thread as read", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -605,9 +633,9 @@ test.describe("Read receipts", () => {
|
||||
msg.threadedOff("Root", "Msg3"),
|
||||
msg.reactionTo("Msg3", "x"),
|
||||
]);
|
||||
await util.assertUnread(room2, 3);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.receiveMessages(room2, [msg.redactionOf("Msg3")]);
|
||||
await util.assertUnread(room2, 2);
|
||||
await util.assertUnread(room2, 1);
|
||||
|
||||
// When we read the thread
|
||||
await util.goTo(room2);
|
||||
@@ -617,6 +645,7 @@ test.describe("Read receipts", () => {
|
||||
await util.assertRead(room2);
|
||||
await util.assertReadThread("Root");
|
||||
});
|
||||
|
||||
test("Reading a thread containing a redacted, edited message marks the thread as read", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -631,7 +660,7 @@ test.describe("Read receipts", () => {
|
||||
msg.threadedOff("Root", "Msg3"),
|
||||
msg.editOf("Msg3", "Msg3 Edited"),
|
||||
]);
|
||||
await util.assertUnread(room2, 3);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.receiveMessages(room2, [msg.redactionOf("Msg3")]);
|
||||
|
||||
// When we read the thread
|
||||
@@ -642,6 +671,7 @@ test.describe("Read receipts", () => {
|
||||
await util.assertRead(room2);
|
||||
await util.assertReadThread("Root");
|
||||
});
|
||||
|
||||
test("Reading a reply to a redacted message marks the thread as read", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -656,7 +686,7 @@ test.describe("Read receipts", () => {
|
||||
msg.threadedOff("Root", "Msg3"),
|
||||
msg.replyTo("Msg3", "Msg3Reply"),
|
||||
]);
|
||||
await util.assertUnread(room2, 4);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.receiveMessages(room2, [msg.redactionOf("Msg3")]);
|
||||
|
||||
// When we read the thread, creating a receipt that points at the edit
|
||||
@@ -667,6 +697,7 @@ test.describe("Read receipts", () => {
|
||||
await util.assertRead(room2);
|
||||
await util.assertReadThread("Root");
|
||||
});
|
||||
|
||||
test("Reading a thread root when its only message has been redacted leaves the room read", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -676,7 +707,7 @@ test.describe("Read receipts", () => {
|
||||
// Given we had a thread
|
||||
await util.goTo(room1);
|
||||
await util.receiveMessages(room2, ["Root", msg.threadedOff("Root", "Msg2")]);
|
||||
await util.assertUnread(room2, 2);
|
||||
await util.assertUnread(room2, 1);
|
||||
|
||||
// And then redacted the message that makes it a thread
|
||||
await util.receiveMessages(room2, [msg.redactionOf("Msg2")]);
|
||||
@@ -687,7 +718,11 @@ test.describe("Read receipts", () => {
|
||||
|
||||
// Then the room is read
|
||||
await util.assertRead(room2);
|
||||
// and that thread is read
|
||||
await util.openThreadList();
|
||||
await util.assertReadThread("Root");
|
||||
});
|
||||
|
||||
test("A thread with a redacted unread is still read after restart", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -701,13 +736,13 @@ test.describe("Read receipts", () => {
|
||||
msg.threadedOff("Root", "ThreadMsg1"),
|
||||
msg.threadedOff("Root", "ThreadMsg2"),
|
||||
]);
|
||||
await util.assertUnread(room2, 3);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.goTo(room2);
|
||||
await util.openThread("Root");
|
||||
await util.assertRead(room2);
|
||||
await util.assertReadThread("Root");
|
||||
await util.receiveMessages(room2, [msg.threadedOff("Root", "Msg3")]);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.assertRead(room2);
|
||||
await util.receiveMessages(room2, [msg.redactionOf("Msg3")]);
|
||||
await util.assertRead(room2);
|
||||
await util.goTo(room2);
|
||||
@@ -722,7 +757,13 @@ test.describe("Read receipts", () => {
|
||||
await util.goTo(room2);
|
||||
await util.assertReadThread("Root");
|
||||
});
|
||||
test("A thread with a read redaction is still read after restart", async ({
|
||||
|
||||
/*
|
||||
* Disabled: this doesn't seem to work as, at some point after syncing from cache, the redaction and redacted
|
||||
* event get removed from the thread timeline such that we have no record of the events that the read receipt
|
||||
* points to. I suspect this may have been passing by fluke before.
|
||||
*/
|
||||
test.skip("A thread with a read redaction is still read after restart", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
util,
|
||||
@@ -737,11 +778,11 @@ test.describe("Read receipts", () => {
|
||||
"Root2",
|
||||
msg.threadedOff("Root2", "Root2->A"),
|
||||
]);
|
||||
await util.assertUnread(room2, 5);
|
||||
await util.assertUnread(room2, 2);
|
||||
await util.goTo(room2);
|
||||
await util.assertUnreadThread("Root1");
|
||||
await util.openThread("Root1");
|
||||
await util.assertUnreadLessThan(room2, 4);
|
||||
await util.assertRead(room2);
|
||||
await util.openThread("Root2");
|
||||
await util.assertRead(room2);
|
||||
await util.closeThreadsPanel();
|
||||
@@ -757,7 +798,12 @@ test.describe("Read receipts", () => {
|
||||
|
||||
// Then the room is still read
|
||||
await util.assertRead(room2);
|
||||
// and so is the thread
|
||||
await util.openThreadList();
|
||||
await util.assertReadThread("Root1");
|
||||
await util.assertReadThread("Root2");
|
||||
});
|
||||
|
||||
test("A thread with an unread reply to a redacted message is still unread after restart", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -772,7 +818,7 @@ test.describe("Read receipts", () => {
|
||||
msg.threadedOff("Root", "Msg3"),
|
||||
msg.replyTo("Msg3", "Msg3Reply"),
|
||||
]);
|
||||
await util.assertUnread(room2, 4);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.receiveMessages(room2, [msg.redactionOf("Msg3")]);
|
||||
|
||||
// And we have read all this
|
||||
@@ -788,6 +834,7 @@ test.describe("Read receipts", () => {
|
||||
await util.assertRead(room2);
|
||||
await util.assertReadThread("Root");
|
||||
});
|
||||
|
||||
test("A thread with a read reply to a redacted message is still read after restart", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -802,7 +849,7 @@ test.describe("Read receipts", () => {
|
||||
msg.threadedOff("Root", "Msg3"),
|
||||
msg.replyTo("Msg3", "Msg3Reply"),
|
||||
]);
|
||||
await util.assertUnread(room2, 4);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.receiveMessages(room2, [msg.redactionOf("Msg3")]);
|
||||
|
||||
// And I read it, so the room is read
|
||||
@@ -836,7 +883,7 @@ test.describe("Read receipts", () => {
|
||||
msg.threadedOff("Root", "Msg2"),
|
||||
msg.threadedOff("Root", "Msg3"),
|
||||
]);
|
||||
await util.assertUnread(room2, 3);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.goTo(room2);
|
||||
await util.openThread("Root");
|
||||
await util.assertRead(room2);
|
||||
@@ -848,7 +895,12 @@ test.describe("Read receipts", () => {
|
||||
// Then the room is still read
|
||||
await util.assertStillRead(room2);
|
||||
});
|
||||
test("Redacting a thread root still allows us to read the thread", async ({
|
||||
|
||||
/*
|
||||
* Disabled for the same reason as "A thread with a read redaction is still read after restart"
|
||||
* above
|
||||
*/
|
||||
test.skip("Redacting a thread root still allows us to read the thread", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
util,
|
||||
@@ -861,23 +913,24 @@ test.describe("Read receipts", () => {
|
||||
msg.threadedOff("Root", "Msg2"),
|
||||
msg.threadedOff("Root", "Msg3"),
|
||||
]);
|
||||
await util.assertUnread(room2, 3);
|
||||
await util.assertUnread(room2, 1);
|
||||
|
||||
// When someone redacts the thread root
|
||||
await util.receiveMessages(room2, [msg.redactionOf("Root")]);
|
||||
|
||||
// Then the room is still unread
|
||||
await util.assertUnread(room2, 2);
|
||||
await util.assertUnread(room2, 1);
|
||||
|
||||
// And I can open the thread and read it
|
||||
await util.goTo(room2);
|
||||
await util.assertUnread(room2, 2);
|
||||
await util.assertRead(room2);
|
||||
// The redacted message gets collapsed into, "foo was invited, joined and removed a message"
|
||||
await util.openCollapsedMessage(1);
|
||||
await util.openThread("Message deleted");
|
||||
await util.assertRead(room2);
|
||||
await util.assertReadThread("Root");
|
||||
});
|
||||
|
||||
test("Sending a threaded message onto a redacted thread root leaves the room unread", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -891,7 +944,7 @@ test.describe("Read receipts", () => {
|
||||
msg.threadedOff("Root", "Msg2"),
|
||||
msg.threadedOff("Root", "Msg3"),
|
||||
]);
|
||||
await util.assertUnread(room2, 3);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.goTo(room2);
|
||||
await util.openThread("Root");
|
||||
await util.assertRead(room2);
|
||||
@@ -901,11 +954,12 @@ test.describe("Read receipts", () => {
|
||||
// When we receive a new message on it
|
||||
await util.receiveMessages(room2, [msg.threadedOff("Root", "Msg4")]);
|
||||
|
||||
// Then the room and thread are unread
|
||||
await util.assertUnread(room2, 1);
|
||||
// Then the room is read but the thread is unread
|
||||
await util.assertRead(room2);
|
||||
await util.goTo(room2);
|
||||
await util.assertUnreadThread("Message deleted");
|
||||
});
|
||||
|
||||
test("Reacting to a redacted thread root leaves the room read", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -919,7 +973,7 @@ test.describe("Read receipts", () => {
|
||||
msg.threadedOff("Root", "Msg2"),
|
||||
msg.threadedOff("Root", "Msg3"),
|
||||
]);
|
||||
await util.assertUnread(room2, 3);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.goTo(room2);
|
||||
await util.openThread("Root");
|
||||
await util.assertRead(room2);
|
||||
@@ -931,7 +985,9 @@ test.describe("Read receipts", () => {
|
||||
|
||||
// Then the room is still read
|
||||
await util.assertRead(room2);
|
||||
await util.assertReadThread("Root");
|
||||
});
|
||||
|
||||
test("Editing a redacted thread root leaves the room read", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -945,7 +1001,7 @@ test.describe("Read receipts", () => {
|
||||
msg.threadedOff("Root", "Msg2"),
|
||||
msg.threadedOff("Root", "Msg3"),
|
||||
]);
|
||||
await util.assertUnread(room2, 3);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.goTo(room2);
|
||||
await util.openThread("Root");
|
||||
await util.assertRead(room2);
|
||||
@@ -957,7 +1013,10 @@ test.describe("Read receipts", () => {
|
||||
|
||||
// Then the room is still read
|
||||
await util.assertRead(room2);
|
||||
// as is the thread
|
||||
await util.assertReadThread("Root");
|
||||
});
|
||||
|
||||
test("Replying to a redacted thread root makes the room unread", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -971,7 +1030,7 @@ test.describe("Read receipts", () => {
|
||||
msg.threadedOff("Root", "Msg2"),
|
||||
msg.threadedOff("Root", "Msg3"),
|
||||
]);
|
||||
await util.assertUnread(room2, 3);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.goTo(room2);
|
||||
await util.openThread("Root");
|
||||
await util.assertRead(room2);
|
||||
@@ -984,6 +1043,7 @@ test.describe("Read receipts", () => {
|
||||
// Then the room is unread
|
||||
await util.assertUnread(room2, 1);
|
||||
});
|
||||
|
||||
test("Reading a reply to a redacted thread root makes the room read", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
@@ -998,7 +1058,7 @@ test.describe("Read receipts", () => {
|
||||
msg.threadedOff("Root", "Msg2"),
|
||||
msg.threadedOff("Root", "Msg3"),
|
||||
]);
|
||||
await util.assertUnread(room2, 3);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.goTo(room2);
|
||||
await util.openThread("Root");
|
||||
await util.assertRead(room2);
|
||||
|
@@ -341,7 +341,7 @@ export class Helpers {
|
||||
*/
|
||||
assertThreadPanelFocused() {
|
||||
return expect(
|
||||
this.page.locator(".mx_ThreadPanel").locator(".mx_BaseCard_header").getByTitle("Close"),
|
||||
this.page.locator(".mx_ThreadPanel").locator(".mx_BaseCard_header").getByLabel("Close"),
|
||||
).toBeFocused();
|
||||
}
|
||||
|
||||
|
@@ -495,14 +495,14 @@ test.describe("Threads", () => {
|
||||
await createThread("Hello again Mr. Bot", "Hello again Mr. User in a thread");
|
||||
|
||||
// Open thread panel
|
||||
await page.getByRole("button", { name: "Threads" }).click();
|
||||
await page.getByTestId("threadsButton").click();
|
||||
const threadPanel = page.locator(".mx_ThreadPanel");
|
||||
await expect(
|
||||
threadPanel.locator(".mx_EventTile_last").getByText("Hello again Mr. User in a thread"),
|
||||
).toBeVisible();
|
||||
|
||||
// Open threads list
|
||||
await threadPanel.getByRole("button", { name: "Threads" }).click();
|
||||
await page.locator(".mx_BaseCard_back").click();
|
||||
const rightPanel = page.locator(".mx_RightPanel");
|
||||
// Check that the threads are listed
|
||||
await expect(rightPanel.locator(".mx_EventTile").getByText("Hello Mr. User in a thread")).toBeVisible();
|
||||
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.2 KiB |
@@ -32,36 +32,10 @@ limitations under the License.
|
||||
margin-top: $spacing-8;
|
||||
}
|
||||
|
||||
.mx_LoginWithQR_separator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: "";
|
||||
flex: 1;
|
||||
border-bottom: 1px solid $quinary-content;
|
||||
}
|
||||
|
||||
&:not(:empty) {
|
||||
&::before {
|
||||
margin-right: 1em;
|
||||
}
|
||||
&::after {
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
font-size: $font-15px;
|
||||
}
|
||||
|
||||
.mx_UserSettingsDialog .mx_LoginWithQR {
|
||||
.mx_AccessibleButton + .mx_AccessibleButton {
|
||||
margin-left: $spacing-12;
|
||||
}
|
||||
|
||||
font: var(--cpd-font-body-md-regular);
|
||||
|
||||
h1 {
|
||||
@@ -69,18 +43,14 @@ limitations under the License.
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
line-height: 1.8;
|
||||
h2 {
|
||||
margin-top: $spacing-24;
|
||||
}
|
||||
|
||||
.mx_QRCode {
|
||||
margin: $spacing-28 0;
|
||||
}
|
||||
|
||||
.mx_LoginWithQR_buttons {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mx_LoginWithQR_qrWrapper {
|
||||
display: flex;
|
||||
}
|
||||
@@ -91,12 +61,6 @@ limitations under the License.
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.mx_LoginWithQR_centreTitle {
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
h1 > svg {
|
||||
&.normal {
|
||||
color: $secondary-content;
|
||||
@@ -137,11 +101,69 @@ limitations under the License.
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style-position: inside;
|
||||
padding-inline-start: 0;
|
||||
list-style: none; /* list markers do not support the outlined number styling we need */
|
||||
|
||||
li::marker {
|
||||
color: $accent;
|
||||
li {
|
||||
position: relative;
|
||||
padding-left: var(--cpd-space-7x);
|
||||
color: 1px solid $input-placeholder;
|
||||
margin-bottom: var(--cpd-space-4x);
|
||||
line-height: 20px;
|
||||
text-align: initial;
|
||||
}
|
||||
|
||||
/* Circled number list item marker */
|
||||
li::before {
|
||||
content: counter(list-item);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid $input-placeholder;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
label[for="mx_LoginWithQR_checkCode"] {
|
||||
margin-top: var(--cpd-space-6x);
|
||||
color: var(--cpd-color-text-primary);
|
||||
margin-bottom: var(--cpd-space-1x);
|
||||
}
|
||||
|
||||
.mx_LoginWithQR_icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
padding: var(--cpd-space-3x);
|
||||
gap: 10px;
|
||||
|
||||
background-color: var(--cpd-color-bg-success-subtle);
|
||||
svg {
|
||||
color: var(--cpd-color-icon-success-primary);
|
||||
}
|
||||
|
||||
&.mx_LoginWithQR_icon--critical {
|
||||
background-color: var(--cpd-color-bg-critical-subtle);
|
||||
svg {
|
||||
color: var(--cpd-color-icon-critical-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_LoginWithQR_checkCode_input {
|
||||
margin-bottom: var(--cpd-space-1x);
|
||||
text-align: initial;
|
||||
|
||||
input {
|
||||
/* Workaround for one of the input rules in _common.pcss being not specific enough */
|
||||
padding: 0;
|
||||
padding-inline-start: calc(40px / 2 - (1ch / 2));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,13 +186,39 @@ limitations under the License.
|
||||
|
||||
.mx_LoginWithQR_breadcrumbs {
|
||||
font-size: $font-13px;
|
||||
color: var(--cpd-color-text-secondary);
|
||||
color: $secondary-content;
|
||||
}
|
||||
|
||||
.mx_LoginWithQR_main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
color: $primary-content;
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
color: $secondary-content;
|
||||
}
|
||||
}
|
||||
|
||||
&.mx_LoginWithQR_error .mx_LoginWithQR_main {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.mx_LoginWithQR_buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $spacing-16;
|
||||
margin-top: var(--cpd-space-6x);
|
||||
|
||||
.mx_AccessibleButton {
|
||||
width: 300px;
|
||||
height: 48px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_QRCode {
|
||||
|
@@ -58,6 +58,8 @@ export class DecryptionFailureTracker {
|
||||
case DecryptionFailureCode.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED:
|
||||
case DecryptionFailureCode.HISTORICAL_MESSAGE_WORKING_BACKUP:
|
||||
return "HistoricalMessage";
|
||||
case DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED:
|
||||
return "ExpectedDueToMembership";
|
||||
default:
|
||||
return "UnknownError";
|
||||
}
|
||||
|
@@ -73,22 +73,44 @@ export interface IMatrixClientCreds {
|
||||
* you'll find a `MatrixClient` hanging on the `MatrixClientPeg`.
|
||||
*/
|
||||
export interface IMatrixClientPeg {
|
||||
/**
|
||||
* The opts used to start the client
|
||||
*/
|
||||
opts: IStartClientOpts;
|
||||
|
||||
/**
|
||||
* Return the server name of the user's homeserver
|
||||
* Throws an error if unable to deduce the homeserver name
|
||||
* (eg. if the user is not logged in)
|
||||
* (e.g. if the user is not logged in)
|
||||
*
|
||||
* @returns {string} The homeserver name, if present.
|
||||
*/
|
||||
getHomeserverName(): string;
|
||||
|
||||
/**
|
||||
* Get the current MatrixClient, if any
|
||||
*/
|
||||
get(): MatrixClient | null;
|
||||
|
||||
/**
|
||||
* Get the current MatrixClient, throwing an error if there isn't one
|
||||
*/
|
||||
safeGet(): MatrixClient;
|
||||
|
||||
/**
|
||||
* Unset the current MatrixClient
|
||||
*/
|
||||
unset(): void;
|
||||
assign(): Promise<any>;
|
||||
start(): Promise<any>;
|
||||
|
||||
/**
|
||||
* Prepare the MatrixClient for use, including initialising the store and crypto, but do not start it
|
||||
*/
|
||||
assign(): Promise<IStartClientOpts>;
|
||||
|
||||
/**
|
||||
* Prepare the MatrixClient for use, including initialising the store and crypto, and start it
|
||||
*/
|
||||
start(): Promise<void>;
|
||||
|
||||
/**
|
||||
* If we've registered a user ID we set this to the ID of the
|
||||
@@ -235,7 +257,7 @@ class MatrixClientPegClass implements IMatrixClientPeg {
|
||||
PlatformPeg.get()?.reload();
|
||||
};
|
||||
|
||||
public async assign(): Promise<any> {
|
||||
public async assign(): Promise<IStartClientOpts> {
|
||||
if (!this.matrixClient) {
|
||||
throw new Error("createClient must be called first");
|
||||
}
|
||||
@@ -273,17 +295,9 @@ class MatrixClientPegClass implements IMatrixClientPeg {
|
||||
opts.threadSupport = true;
|
||||
|
||||
if (SettingsStore.getValue("feature_sliding_sync")) {
|
||||
const proxyUrl = SettingsStore.getValue("feature_sliding_sync_proxy_url");
|
||||
if (proxyUrl) {
|
||||
logger.log("Activating sliding sync using proxy at ", proxyUrl);
|
||||
opts.slidingSync = await SlidingSyncManager.instance.setup(this.matrixClient);
|
||||
} else {
|
||||
logger.log("Activating sliding sync");
|
||||
}
|
||||
opts.slidingSync = SlidingSyncManager.instance.configure(
|
||||
this.matrixClient,
|
||||
proxyUrl || this.matrixClient.baseUrl,
|
||||
);
|
||||
SlidingSyncManager.instance.startSpidering(100, 50); // 100 rooms at a time, 50ms apart
|
||||
SlidingSyncManager.instance.checkSupport(this.matrixClient);
|
||||
}
|
||||
|
||||
// Connect the matrix client to the dispatcher and setting handlers
|
||||
@@ -362,7 +376,7 @@ class MatrixClientPegClass implements IMatrixClientPeg {
|
||||
}
|
||||
}
|
||||
|
||||
public async start(): Promise<any> {
|
||||
public async start(): Promise<void> {
|
||||
const opts = await this.assign();
|
||||
|
||||
logger.log(`MatrixClientPeg: really starting MatrixClient`);
|
||||
|
@@ -313,7 +313,7 @@ export class PosthogAnalytics {
|
||||
// No point identifying again
|
||||
return;
|
||||
}
|
||||
if (this.posthog.persistence?.get_user_state() === "identified") {
|
||||
if (this.posthog.persistence?.get_property("$user_state") === "identified") {
|
||||
// Analytics ID has changed, reset as Posthog will refuse to merge in this case
|
||||
this.posthog.reset();
|
||||
}
|
||||
|
@@ -44,7 +44,7 @@ limitations under the License.
|
||||
* list ops)
|
||||
*/
|
||||
|
||||
import { MatrixClient, EventType } from "matrix-js-sdk/src/matrix";
|
||||
import { MatrixClient, EventType, AutoDiscovery, Method, timeoutSignal } from "matrix-js-sdk/src/matrix";
|
||||
import {
|
||||
MSC3575Filter,
|
||||
MSC3575List,
|
||||
@@ -56,6 +56,9 @@ import {
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { defer, sleep } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import SlidingSyncController from "./settings/controllers/SlidingSyncController";
|
||||
|
||||
// how long to long poll for
|
||||
const SLIDING_SYNC_TIMEOUT_MS = 20 * 1000;
|
||||
|
||||
@@ -323,4 +326,93 @@ export class SlidingSyncManager {
|
||||
firstTime = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the Sliding Sync instance; configures the end point and starts spidering.
|
||||
* The sliding sync endpoint is derived the following way:
|
||||
* 1. The user-defined sliding sync proxy URL (legacy, for backwards compatibility)
|
||||
* 2. The client `well-known` sliding sync proxy URL [declared at the unstable prefix](https://github.com/matrix-org/matrix-spec-proposals/blob/kegan/sync-v3/proposals/3575-sync.md#unstable-prefix)
|
||||
* 3. The homeserver base url (for native server support)
|
||||
* @param client The MatrixClient to use
|
||||
* @returns A working Sliding Sync or undefined
|
||||
*/
|
||||
public async setup(client: MatrixClient): Promise<SlidingSync | undefined> {
|
||||
const baseUrl = client.baseUrl;
|
||||
const proxyUrl = SettingsStore.getValue("feature_sliding_sync_proxy_url");
|
||||
const wellKnownProxyUrl = await this.getProxyFromWellKnown(client);
|
||||
|
||||
const slidingSyncEndpoint = proxyUrl || wellKnownProxyUrl || baseUrl;
|
||||
|
||||
this.configure(client, slidingSyncEndpoint);
|
||||
logger.info("Sliding sync activated at", slidingSyncEndpoint);
|
||||
this.startSpidering(100, 50); // 100 rooms at a time, 50ms apart
|
||||
|
||||
return this.slidingSync;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the sliding sync proxy URL from the client well known
|
||||
* @param client The MatrixClient to use
|
||||
* @return The proxy url
|
||||
*/
|
||||
public async getProxyFromWellKnown(client: MatrixClient): Promise<string | undefined> {
|
||||
let proxyUrl: string | undefined;
|
||||
|
||||
try {
|
||||
const clientWellKnown = await AutoDiscovery.findClientConfig(client.baseUrl);
|
||||
proxyUrl = clientWellKnown?.["org.matrix.msc3575.proxy"]?.url;
|
||||
} catch (e) {
|
||||
// client.baseUrl is invalid, `AutoDiscovery.findClientConfig` has thrown
|
||||
}
|
||||
|
||||
if (proxyUrl != undefined) {
|
||||
logger.log("getProxyFromWellKnown: client well-known declares sliding sync proxy at", proxyUrl);
|
||||
}
|
||||
return proxyUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the server "natively" supports sliding sync (at the unstable endpoint).
|
||||
* @param client The MatrixClient to use
|
||||
* @return Whether the "native" (unstable) endpoint is up
|
||||
*/
|
||||
public async nativeSlidingSyncSupport(client: MatrixClient): Promise<boolean> {
|
||||
try {
|
||||
await client.http.authedRequest<void>(Method.Post, "/sync", undefined, undefined, {
|
||||
localTimeoutMs: 10 * 1000, // 10s
|
||||
prefix: "/_matrix/client/unstable/org.matrix.msc3575",
|
||||
});
|
||||
} catch (e) {
|
||||
return false; // 404, M_UNRECOGNIZED
|
||||
}
|
||||
|
||||
logger.log("nativeSlidingSyncSupport: sliding sync endpoint is up");
|
||||
return true; // 200, OK
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether our homeserver has sliding sync support, that the endpoint is up, and
|
||||
* is a sliding sync endpoint.
|
||||
*
|
||||
* Sets static member `SlidingSyncController.serverSupportsSlidingSync`
|
||||
* @param client The MatrixClient to use
|
||||
*/
|
||||
public async checkSupport(client: MatrixClient): Promise<void> {
|
||||
if (await this.nativeSlidingSyncSupport(client)) {
|
||||
SlidingSyncController.serverSupportsSlidingSync = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const proxyUrl = await this.getProxyFromWellKnown(client);
|
||||
if (proxyUrl != undefined) {
|
||||
const response = await fetch(proxyUrl + "/client/server.json", {
|
||||
method: Method.Get,
|
||||
signal: timeoutSignal(10 * 1000), // 10s
|
||||
});
|
||||
if (response.status === 200) {
|
||||
logger.log("checkSupport: well-known sliding sync proxy is up at", proxyUrl);
|
||||
SlidingSyncController.serverSupportsSlidingSync = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -28,15 +28,15 @@ type Props<T extends keyof JSX.IntrinsicElements> = ComponentProps<typeof Access
|
||||
|
||||
// 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, ...props }: Props<T>,
|
||||
{ label, isExpanded, children, onClick, onContextMenu, element, ...props }: Props<T>,
|
||||
ref: Ref<HTMLElement>,
|
||||
) {
|
||||
return (
|
||||
<AccessibleButton
|
||||
{...props}
|
||||
element={element as keyof JSX.IntrinsicElements}
|
||||
onClick={onClick}
|
||||
onContextMenu={onContextMenu ?? onClick ?? undefined}
|
||||
title={label}
|
||||
aria-label={label}
|
||||
aria-haspopup={true}
|
||||
aria-expanded={isExpanded}
|
||||
|
@@ -27,12 +27,13 @@ type Props<T extends keyof JSX.IntrinsicElements> = ComponentProps<typeof Access
|
||||
|
||||
// Semantic component for representing the AccessibleButton which launches a <ContextMenu />
|
||||
export const ContextMenuTooltipButton = forwardRef(function <T extends keyof JSX.IntrinsicElements>(
|
||||
{ isExpanded, children, onClick, onContextMenu, ...props }: Props<T>,
|
||||
{ isExpanded, children, onClick, onContextMenu, element, ...props }: Props<T>,
|
||||
ref: Ref<HTMLElement>,
|
||||
) {
|
||||
return (
|
||||
<AccessibleTooltipButton
|
||||
{...props}
|
||||
element={element as keyof JSX.IntrinsicElements}
|
||||
onClick={onClick}
|
||||
onContextMenu={onContextMenu ?? onClick ?? undefined}
|
||||
aria-haspopup={true}
|
||||
|
@@ -18,25 +18,16 @@ limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
|
||||
import { RovingAccessibleButton, RovingAccessibleTooltipButton } from "../RovingTabIndex";
|
||||
import { RovingAccessibleButton } from "../RovingTabIndex";
|
||||
|
||||
interface IProps extends React.ComponentProps<typeof RovingAccessibleButton> {
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
// Semantic component for representing a role=menuitem
|
||||
export const MenuItem: React.FC<IProps> = ({ children, label, tooltip, ...props }) => {
|
||||
export const MenuItem: React.FC<IProps> = ({ children, label, ...props }) => {
|
||||
const ariaLabel = props["aria-label"] || label;
|
||||
|
||||
if (tooltip) {
|
||||
return (
|
||||
<RovingAccessibleTooltipButton {...props} role="menuitem" aria-label={ariaLabel} title={tooltip}>
|
||||
{children}
|
||||
</RovingAccessibleTooltipButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<RovingAccessibleButton {...props} role="menuitem" aria-label={ariaLabel}>
|
||||
{children}
|
||||
|
@@ -34,12 +34,14 @@ export const RovingAccessibleButton = <T extends keyof JSX.IntrinsicElements>({
|
||||
onFocus,
|
||||
onMouseOver,
|
||||
focusOnMouseOver,
|
||||
element,
|
||||
...props
|
||||
}: Props<T>): JSX.Element => {
|
||||
const [onFocusInternal, isActive, ref] = useRovingTabIndex(inputRef);
|
||||
return (
|
||||
<AccessibleButton
|
||||
{...props}
|
||||
element={element as keyof JSX.IntrinsicElements}
|
||||
onFocus={(event: React.FocusEvent) => {
|
||||
onFocusInternal();
|
||||
onFocus?.(event);
|
||||
|
@@ -16,27 +16,26 @@ limitations under the License.
|
||||
|
||||
import React, { ComponentProps } from "react";
|
||||
|
||||
import AccessibleTooltipButton from "../../components/views/elements/AccessibleTooltipButton";
|
||||
import { useRovingTabIndex } from "../RovingTabIndex";
|
||||
import { Ref } from "./types";
|
||||
import AccessibleButton from "../../components/views/elements/AccessibleButton";
|
||||
|
||||
type Props<T extends keyof JSX.IntrinsicElements> = Omit<
|
||||
ComponentProps<typeof AccessibleTooltipButton<T>>,
|
||||
"tabIndex"
|
||||
> & {
|
||||
type Props<T extends keyof JSX.IntrinsicElements> = Omit<ComponentProps<typeof AccessibleButton<T>>, "tabIndex"> & {
|
||||
inputRef?: Ref;
|
||||
};
|
||||
|
||||
// Wrapper to allow use of useRovingTabIndex for simple AccessibleTooltipButtons outside of React Functional Components.
|
||||
// Wrapper to allow use of useRovingTabIndex for simple AccessibleButtons outside of React Functional Components.
|
||||
export const RovingAccessibleTooltipButton = <T extends keyof JSX.IntrinsicElements>({
|
||||
inputRef,
|
||||
onFocus,
|
||||
element,
|
||||
...props
|
||||
}: Props<T>): JSX.Element => {
|
||||
const [onFocusInternal, isActive, ref] = useRovingTabIndex(inputRef);
|
||||
return (
|
||||
<AccessibleTooltipButton
|
||||
<AccessibleButton
|
||||
{...props}
|
||||
element={element as keyof JSX.IntrinsicElements}
|
||||
onFocus={(event: React.FocusEvent) => {
|
||||
onFocusInternal();
|
||||
onFocus?.(event);
|
||||
|
@@ -54,7 +54,6 @@ import dis from "../../dispatcher/dispatcher";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import Timer from "../../utils/Timer";
|
||||
import shouldHideEvent from "../../shouldHideEvent";
|
||||
import { arrayFastClone } from "../../utils/arrays";
|
||||
import MessagePanel from "./MessagePanel";
|
||||
import { IScrollState } from "./ScrollPanel";
|
||||
import { ActionPayload } from "../../dispatcher/payloads";
|
||||
@@ -1751,15 +1750,11 @@ export class TimelinePanel extends React.Component<IProps, IState> {
|
||||
[...mainEvents],
|
||||
);
|
||||
|
||||
// `arrayFastClone` performs a shallow copy of the array
|
||||
// we want the last event to be decrypted first but displayed last
|
||||
// `reverse` is destructive and unfortunately mutates the "events" array
|
||||
arrayFastClone(events)
|
||||
.reverse()
|
||||
.forEach((event) => {
|
||||
// We want the last event to be decrypted first
|
||||
const client = MatrixClientPeg.safeGet();
|
||||
client.decryptEventIfNeeded(event);
|
||||
});
|
||||
for (let i = events.length - 1; i >= 0; --i) {
|
||||
client.decryptEventIfNeeded(events[i]);
|
||||
}
|
||||
|
||||
const firstVisibleEventIndex = this.checkForPreJoinUISI(events);
|
||||
|
||||
|
43
src/components/views/auth/LoginWithQR-types.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The intention of this enum is to have a mode that scans a QR code instead of generating one.
|
||||
*/
|
||||
export enum Mode {
|
||||
/**
|
||||
* A QR code with be generated and shown
|
||||
*/
|
||||
Show = "show",
|
||||
}
|
||||
|
||||
export enum Phase {
|
||||
Loading,
|
||||
ShowingQR,
|
||||
Connecting,
|
||||
Connected,
|
||||
WaitingForDevice,
|
||||
Verifying,
|
||||
Error,
|
||||
}
|
||||
|
||||
export enum Click {
|
||||
Cancel,
|
||||
Decline,
|
||||
Approve,
|
||||
TryAgain,
|
||||
Back,
|
||||
}
|
@@ -24,34 +24,7 @@ import { HTTPError, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { wrapRequestWithDialog } from "../../../utils/UserInteractiveAuth";
|
||||
import LoginWithQRFlow from "./LoginWithQRFlow";
|
||||
|
||||
/**
|
||||
* The intention of this enum is to have a mode that scans a QR code instead of generating one.
|
||||
*/
|
||||
export enum Mode {
|
||||
/**
|
||||
* A QR code with be generated and shown
|
||||
*/
|
||||
Show = "show",
|
||||
}
|
||||
|
||||
export enum Phase {
|
||||
Loading,
|
||||
ShowingQR,
|
||||
Connecting,
|
||||
Connected,
|
||||
WaitingForDevice,
|
||||
Verifying,
|
||||
Error,
|
||||
}
|
||||
|
||||
export enum Click {
|
||||
Cancel,
|
||||
Decline,
|
||||
Approve,
|
||||
TryAgain,
|
||||
Back,
|
||||
}
|
||||
import { Click, Mode, Phase } from "./LoginWithQR-types";
|
||||
|
||||
interface IProps {
|
||||
client: MatrixClient;
|
||||
|
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2022 - 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -14,19 +14,24 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { RendezvousFailureReason } from "matrix-js-sdk/src/rendezvous";
|
||||
import React, { ReactNode } from "react";
|
||||
import { RendezvousFailureReason as LegacyRendezvousFailureReason } from "matrix-js-sdk/src/rendezvous";
|
||||
import { Icon as ChevronLeftIcon } from "@vector-im/compound-design-tokens/icons/chevron-left.svg";
|
||||
import { Icon as CheckCircleSolidIcon } from "@vector-im/compound-design-tokens/icons/check-circle-solid.svg";
|
||||
import { Icon as ErrorIcon } from "@vector-im/compound-design-tokens/icons/error.svg";
|
||||
import { Heading, Text } from "@vector-im/compound-web";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import QRCode from "../elements/QRCode";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import { Icon as InfoIcon } from "../../../../res/img/element-icons/i.svg";
|
||||
import { Click, FailureReason, LoginWithQRFailureReason, Phase } from "./LoginWithQR";
|
||||
import { Click, Phase } from "./LoginWithQR-types";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import { FailureReason, LoginWithQRFailureReason } from "./LoginWithQR";
|
||||
|
||||
interface IProps {
|
||||
interface Props {
|
||||
phase: Phase;
|
||||
code?: string;
|
||||
onClick(type: Click): Promise<void>;
|
||||
@@ -39,8 +44,8 @@ interface IProps {
|
||||
*
|
||||
* This uses the unstable feature of MSC3906: https://github.com/matrix-org/matrix-spec-proposals/pull/3906
|
||||
*/
|
||||
export default class LoginWithQRFlow extends React.Component<IProps> {
|
||||
public constructor(props: IProps) {
|
||||
export default class LoginWithQRFlow extends React.Component<Props> {
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
@@ -72,49 +77,75 @@ export default class LoginWithQRFlow extends React.Component<IProps> {
|
||||
let main: JSX.Element | undefined;
|
||||
let buttons: JSX.Element | undefined;
|
||||
let backButton = true;
|
||||
let cancellationMessage: string | undefined;
|
||||
let centreTitle = false;
|
||||
let className = "";
|
||||
|
||||
switch (this.props.phase) {
|
||||
case Phase.Error:
|
||||
case Phase.Error: {
|
||||
let success = false;
|
||||
let title: string | undefined;
|
||||
let message: ReactNode | undefined;
|
||||
|
||||
switch (this.props.failureReason) {
|
||||
case RendezvousFailureReason.Expired:
|
||||
cancellationMessage = _t("auth|qr_code_login|error_linking_incomplete");
|
||||
case LegacyRendezvousFailureReason.UnsupportedAlgorithm:
|
||||
case LegacyRendezvousFailureReason.UnsupportedTransport:
|
||||
case LegacyRendezvousFailureReason.HomeserverLacksSupport:
|
||||
title = _t("auth|qr_code_login|error_unsupported_protocol_title");
|
||||
message = _t("auth|qr_code_login|error_unsupported_protocol");
|
||||
break;
|
||||
case RendezvousFailureReason.InvalidCode:
|
||||
cancellationMessage = _t("auth|qr_code_login|error_invalid_scanned_code");
|
||||
|
||||
case LegacyRendezvousFailureReason.UserCancelled:
|
||||
title = _t("auth|qr_code_login|error_user_cancelled_title");
|
||||
message = _t("auth|qr_code_login|error_user_cancelled");
|
||||
break;
|
||||
case RendezvousFailureReason.UnsupportedAlgorithm:
|
||||
cancellationMessage = _t("auth|qr_code_login|error_device_unsupported");
|
||||
|
||||
case LegacyRendezvousFailureReason.Expired:
|
||||
title = _t("auth|qr_code_login|error_expired_title");
|
||||
message = _t("auth|qr_code_login|error_expired");
|
||||
break;
|
||||
case RendezvousFailureReason.UserDeclined:
|
||||
cancellationMessage = _t("auth|qr_code_login|error_request_declined");
|
||||
|
||||
case LegacyRendezvousFailureReason.InvalidCode:
|
||||
title = _t("auth|qr_code_login|error_insecure_channel_detected_title");
|
||||
message = (
|
||||
<>
|
||||
{_t("auth|qr_code_login|error_insecure_channel_detected")}
|
||||
|
||||
<Text as="h2" size="lg" weight="semibold" data-testid="cancellation-message">
|
||||
{_t("auth|qr_code_login|error_insecure_channel_detected_instructions")}
|
||||
</Text>
|
||||
<ol>
|
||||
<li>{_t("auth|qr_code_login|error_insecure_channel_detected_instructions_1")}</li>
|
||||
<li>{_t("auth|qr_code_login|error_insecure_channel_detected_instructions_2")}</li>
|
||||
<li>{_t("auth|qr_code_login|error_insecure_channel_detected_instructions_3")}</li>
|
||||
</ol>
|
||||
</>
|
||||
);
|
||||
break;
|
||||
case RendezvousFailureReason.OtherDeviceAlreadySignedIn:
|
||||
cancellationMessage = _t("auth|qr_code_login|error_device_already_signed_in");
|
||||
|
||||
case LegacyRendezvousFailureReason.OtherDeviceAlreadySignedIn:
|
||||
success = true;
|
||||
title = _t("auth|qr_code_login|error_other_device_already_signed_in_title");
|
||||
message = _t("auth|qr_code_login|error_other_device_already_signed_in");
|
||||
break;
|
||||
case RendezvousFailureReason.OtherDeviceNotSignedIn:
|
||||
cancellationMessage = _t("auth|qr_code_login|error_device_not_signed_in");
|
||||
break;
|
||||
case RendezvousFailureReason.UserCancelled:
|
||||
cancellationMessage = _t("auth|qr_code_login|error_request_cancelled");
|
||||
|
||||
case LegacyRendezvousFailureReason.UserDeclined:
|
||||
title = _t("auth|qr_code_login|error_user_declined_title");
|
||||
message = _t("auth|qr_code_login|error_user_declined");
|
||||
break;
|
||||
|
||||
case LoginWithQRFailureReason.RateLimited:
|
||||
cancellationMessage = _t("auth|qr_code_login|error_rate_limited");
|
||||
break;
|
||||
case RendezvousFailureReason.Unknown:
|
||||
cancellationMessage = _t("auth|qr_code_login|error_unexpected");
|
||||
break;
|
||||
case RendezvousFailureReason.HomeserverLacksSupport:
|
||||
cancellationMessage = _t("auth|qr_code_login|error_homeserver_lacks_support");
|
||||
title = _t("error|something_went_wrong");
|
||||
message = _t("auth|qr_code_login|error_rate_limited");
|
||||
break;
|
||||
|
||||
case LegacyRendezvousFailureReason.OtherDeviceNotSignedIn:
|
||||
case LegacyRendezvousFailureReason.Unknown:
|
||||
default:
|
||||
cancellationMessage = _t("auth|qr_code_login|error_request_cancelled");
|
||||
title = _t("error|something_went_wrong");
|
||||
message = _t("auth|qr_code_login|error_unexpected");
|
||||
break;
|
||||
}
|
||||
centreTitle = true;
|
||||
className = "mx_LoginWithQR_error";
|
||||
backButton = false;
|
||||
main = <p data-testid="cancellation-message">{cancellationMessage}</p>;
|
||||
buttons = (
|
||||
<>
|
||||
<AccessibleButton
|
||||
@@ -127,7 +158,23 @@ export default class LoginWithQRFlow extends React.Component<IProps> {
|
||||
{this.cancelButton()}
|
||||
</>
|
||||
);
|
||||
main = (
|
||||
<>
|
||||
<div
|
||||
className={classNames("mx_LoginWithQR_icon", {
|
||||
"mx_LoginWithQR_icon--critical": !success,
|
||||
})}
|
||||
>
|
||||
{success ? <CheckCircleSolidIcon width="32px" /> : <ErrorIcon width="32px" />}
|
||||
</div>
|
||||
<Heading as="h1" size="sm" weight="semibold">
|
||||
{title}
|
||||
</Heading>
|
||||
{typeof message === "object" ? message : <p data-testid="cancellation-message">{message}</p>}
|
||||
</>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case Phase.Connected:
|
||||
backButton = false;
|
||||
main = (
|
||||
@@ -145,13 +192,6 @@ export default class LoginWithQRFlow extends React.Component<IProps> {
|
||||
|
||||
buttons = (
|
||||
<>
|
||||
<AccessibleButton
|
||||
data-testid="decline-login-button"
|
||||
kind="primary_outline"
|
||||
onClick={this.handleClick(Click.Decline)}
|
||||
>
|
||||
{_t("action|cancel")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
data-testid="approve-login-button"
|
||||
kind="primary"
|
||||
@@ -159,23 +199,28 @@ export default class LoginWithQRFlow extends React.Component<IProps> {
|
||||
>
|
||||
{_t("action|approve")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
data-testid="decline-login-button"
|
||||
kind="primary_outline"
|
||||
onClick={this.handleClick(Click.Decline)}
|
||||
>
|
||||
{_t("action|cancel")}
|
||||
</AccessibleButton>
|
||||
</>
|
||||
);
|
||||
break;
|
||||
case Phase.ShowingQR:
|
||||
if (this.props.code) {
|
||||
const code = (
|
||||
<div className="mx_LoginWithQR_qrWrapper">
|
||||
<QRCode
|
||||
data={[{ data: Buffer.from(this.props.code ?? ""), mode: "byte" }]}
|
||||
className="mx_QRCode"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
const data = Buffer.from(this.props.code ?? "");
|
||||
|
||||
main = (
|
||||
<>
|
||||
<h1>{_t("auth|qr_code_login|scan_code_instruction")}</h1>
|
||||
{code}
|
||||
<Heading as="h1" size="sm" weight="semibold">
|
||||
{_t("auth|qr_code_login|scan_code_instruction")}
|
||||
</Heading>
|
||||
<div className="mx_LoginWithQR_qrWrapper">
|
||||
<QRCode data={[{ data, mode: "byte" }]} className="mx_QRCode" />
|
||||
</div>
|
||||
<ol>
|
||||
<li>
|
||||
{_t("auth|qr_code_login|open_element_other_device", {
|
||||
@@ -209,14 +254,12 @@ export default class LoginWithQRFlow extends React.Component<IProps> {
|
||||
buttons = this.cancelButton();
|
||||
break;
|
||||
case Phase.Verifying:
|
||||
centreTitle = true;
|
||||
main = this.simpleSpinner(_t("auth|qr_code_login|completing_setup"));
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-testid="login-with-qr" className="mx_LoginWithQR">
|
||||
<div className={centreTitle ? "mx_LoginWithQR_centreTitle" : ""}>
|
||||
<div data-testid="login-with-qr" className={classNames("mx_LoginWithQR", className)}>
|
||||
{backButton ? (
|
||||
<div className="mx_LoginWithQR_heading">
|
||||
<AccessibleButton
|
||||
@@ -232,7 +275,6 @@ export default class LoginWithQRFlow extends React.Component<IProps> {
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mx_LoginWithQR_main">{main}</div>
|
||||
<div className="mx_LoginWithQR_buttons">{buttons}</div>
|
||||
</div>
|
||||
|
@@ -41,8 +41,6 @@ import { avatarUrlForUser } from "../../../Avatar";
|
||||
import EventTile from "../rooms/EventTile";
|
||||
import SearchBox from "../../structures/SearchBox";
|
||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||
import { Alignment } from "../elements/Tooltip";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
|
||||
import NotificationBadge from "../rooms/NotificationBadge";
|
||||
@@ -54,7 +52,7 @@ import EntityTile from "../rooms/EntityTile";
|
||||
import BaseAvatar from "../avatars/BaseAvatar";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import { isLocationEvent } from "../../../utils/EventUtils";
|
||||
import { isSelfLocation, locationEventGeoUri } from "../../../utils/location";
|
||||
import { RoomContextDetails } from "../rooms/RoomContextDetails";
|
||||
@@ -159,11 +157,11 @@ const Entry: React.FC<IEntryProps<any>> = ({ room, type, content, matrixClient:
|
||||
onFocus={onFocus}
|
||||
id={id}
|
||||
>
|
||||
<AccessibleTooltipButton
|
||||
<AccessibleButton
|
||||
className="mx_ForwardList_roomButton"
|
||||
onClick={jumpToRoom}
|
||||
title={_t("forward|open_room")}
|
||||
alignment={Alignment.Top}
|
||||
placement="top"
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
>
|
||||
<DecoratedRoomAvatar room={room} size="32px" tooltipProps={{ tabIndex: isActive ? 0 : -1 }} />
|
||||
@@ -171,20 +169,20 @@ const Entry: React.FC<IEntryProps<any>> = ({ room, type, content, matrixClient:
|
||||
{room.name}
|
||||
</span>
|
||||
<RoomContextDetails component="span" className="mx_ForwardList_entry_detail" room={room} />
|
||||
</AccessibleTooltipButton>
|
||||
<AccessibleTooltipButton
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
kind={sendState === SendState.Failed ? "danger_outline" : "primary_outline"}
|
||||
className={`mx_ForwardList_sendButton ${className}`}
|
||||
onClick={send}
|
||||
disabled={disabled}
|
||||
title={title}
|
||||
alignment={Alignment.Top}
|
||||
placement="top"
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
id={`${id}_send`}
|
||||
>
|
||||
<div className="mx_ForwardList_sendLabel">{_t("forward|send_label")}</div>
|
||||
{icon}
|
||||
</AccessibleTooltipButton>
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@@ -1,142 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { MatrixClient, Method } from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import TextInputDialog from "./TextInputDialog";
|
||||
import withValidation from "../elements/Validation";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
|
||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||
|
||||
/**
|
||||
* Check that the server natively supports sliding sync.
|
||||
* @param cli The MatrixClient of the logged in user.
|
||||
* @throws if the proxy server is unreachable or not configured to the given homeserver
|
||||
*/
|
||||
async function syncHealthCheck(cli: MatrixClient): Promise<void> {
|
||||
await cli.http.authedRequest(Method.Post, "/sync", undefined, undefined, {
|
||||
localTimeoutMs: 10 * 1000, // 10s
|
||||
prefix: "/_matrix/client/unstable/org.matrix.msc3575",
|
||||
});
|
||||
logger.info("server natively support sliding sync OK");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the proxy url is in fact a sliding sync proxy endpoint and it is up.
|
||||
* @param endpoint The proxy endpoint url
|
||||
* @param hsUrl The homeserver url of the logged in user.
|
||||
* @throws if the proxy server is unreachable or not configured to the given homeserver
|
||||
*/
|
||||
async function proxyHealthCheck(endpoint: string, hsUrl?: string): Promise<void> {
|
||||
const controller = new AbortController();
|
||||
const id = window.setTimeout(() => controller.abort(), 10 * 1000); // 10s
|
||||
const res = await fetch(endpoint + "/client/server.json", {
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(id);
|
||||
if (res.status != 200) {
|
||||
throw new Error(`proxyHealthCheck: proxy server returned HTTP ${res.status}`);
|
||||
}
|
||||
const body = await res.json();
|
||||
if (body.server !== hsUrl) {
|
||||
throw new Error(`proxyHealthCheck: client using ${hsUrl} but server is as ${body.server}`);
|
||||
}
|
||||
logger.info("sliding sync proxy is OK");
|
||||
}
|
||||
|
||||
export const SlidingSyncOptionsDialog: React.FC<{ onFinished(enabled: boolean): void }> = ({ onFinished }) => {
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
const currentProxy = SettingsStore.getValue("feature_sliding_sync_proxy_url");
|
||||
const hasNativeSupport = useAsyncMemo(
|
||||
() =>
|
||||
syncHealthCheck(cli).then(
|
||||
() => true,
|
||||
() => false,
|
||||
),
|
||||
[],
|
||||
null,
|
||||
);
|
||||
|
||||
let nativeSupport: string;
|
||||
if (hasNativeSupport === null) {
|
||||
nativeSupport = _t("labs|sliding_sync_checking");
|
||||
} else {
|
||||
nativeSupport = hasNativeSupport
|
||||
? _t("labs|sliding_sync_server_support")
|
||||
: _t("labs|sliding_sync_server_no_support");
|
||||
}
|
||||
|
||||
const validProxy = withValidation<undefined, { error?: unknown }>({
|
||||
async deriveData({ value }): Promise<{ error?: unknown }> {
|
||||
if (!value) return {};
|
||||
try {
|
||||
await proxyHealthCheck(value, MatrixClientPeg.safeGet().baseUrl);
|
||||
return {};
|
||||
} catch (error) {
|
||||
return { error };
|
||||
}
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test: async ({ value }) => !!value || !!hasNativeSupport,
|
||||
invalid: () => _t("labs|sliding_sync_server_specify_proxy"),
|
||||
},
|
||||
{
|
||||
key: "working",
|
||||
final: true,
|
||||
test: async (_, { error }) => !error,
|
||||
valid: () => _t("spotlight|public_rooms|network_dropdown_available_valid"),
|
||||
invalid: ({ error }) => (error instanceof Error ? error.message : null),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return (
|
||||
<TextInputDialog
|
||||
title={_t("labs|sliding_sync_configuration")}
|
||||
description={
|
||||
<div>
|
||||
<div>
|
||||
<b>{_t("labs|sliding_sync_disable_warning")}</b>
|
||||
</div>
|
||||
{nativeSupport}
|
||||
</div>
|
||||
}
|
||||
placeholder={
|
||||
hasNativeSupport
|
||||
? _t("labs|sliding_sync_proxy_url_optional_label")
|
||||
: _t("labs|sliding_sync_proxy_url_label")
|
||||
}
|
||||
value={currentProxy}
|
||||
button={_t("action|enable")}
|
||||
validator={validProxy}
|
||||
onFinished={(enable, proxyUrl) => {
|
||||
if (enable) {
|
||||
SettingsStore.setValue("feature_sliding_sync_proxy_url", null, SettingLevel.DEVICE, proxyUrl);
|
||||
onFinished(true);
|
||||
} else {
|
||||
onFinished(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -20,7 +20,7 @@ import React from "react";
|
||||
import TabbedView, { Tab } from "../../structures/TabbedView";
|
||||
import { _t, _td } from "../../../languageHandler";
|
||||
import GeneralUserSettingsTab from "../settings/tabs/user/GeneralUserSettingsTab";
|
||||
import SettingsStore, { CallbackFn } from "../../../settings/SettingsStore";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import LabsUserSettingsTab, { showLabsFlags } from "../settings/tabs/user/LabsUserSettingsTab";
|
||||
import AppearanceUserSettingsTab from "../settings/tabs/user/AppearanceUserSettingsTab";
|
||||
import SecurityUserSettingsTab from "../settings/tabs/user/SecurityUserSettingsTab";
|
||||
@@ -37,6 +37,7 @@ import SessionManagerTab from "../settings/tabs/user/SessionManagerTab";
|
||||
import { UserTab } from "./UserTab";
|
||||
import { NonEmptyArray } from "../../../@types/common";
|
||||
import { SDKContext, SdkContextClass } from "../../../contexts/SDKContext";
|
||||
import { useSettingValue } from "../../../hooks/useSettings";
|
||||
|
||||
interface IProps {
|
||||
initialTabId?: UserTab;
|
||||
@@ -44,35 +45,11 @@ interface IProps {
|
||||
onFinished(): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
mjolnirEnabled: boolean;
|
||||
}
|
||||
export default function UserSettingsDialog(props: IProps): JSX.Element {
|
||||
const voipEnabled = useSettingValue<boolean>(UIFeature.Voip);
|
||||
const mjolnirEnabled = useSettingValue<boolean>("feature_mjolnir");
|
||||
|
||||
export default class UserSettingsDialog extends React.Component<IProps, IState> {
|
||||
private settingsWatchers: string[] = [];
|
||||
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
mjolnirEnabled: SettingsStore.getValue("feature_mjolnir"),
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.settingsWatchers = [SettingsStore.watchSetting("feature_mjolnir", null, this.mjolnirChanged)];
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
this.settingsWatchers.forEach((watcherRef) => SettingsStore.unwatchSetting(watcherRef));
|
||||
}
|
||||
|
||||
private mjolnirChanged: CallbackFn = (settingName, roomId, atLevel, newValue) => {
|
||||
// We can cheat because we know what levels a feature is tracked at, and how it is tracked
|
||||
this.setState({ mjolnirEnabled: newValue });
|
||||
};
|
||||
|
||||
private getTabs(): NonEmptyArray<Tab<UserTab>> {
|
||||
const getTabs = (): NonEmptyArray<Tab<UserTab>> => {
|
||||
const tabs: Tab<UserTab>[] = [];
|
||||
|
||||
tabs.push(
|
||||
@@ -80,7 +57,7 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
|
||||
UserTab.General,
|
||||
_td("common|general"),
|
||||
"mx_UserSettingsDialog_settingsIcon",
|
||||
<GeneralUserSettingsTab closeSettingsFn={this.props.onFinished} />,
|
||||
<GeneralUserSettingsTab closeSettingsFn={props.onFinished} />,
|
||||
"UserSettingsGeneral",
|
||||
),
|
||||
);
|
||||
@@ -90,7 +67,6 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
|
||||
_td("settings|sessions|title"),
|
||||
"mx_UserSettingsDialog_sessionsIcon",
|
||||
<SessionManagerTab />,
|
||||
// don't track with posthog while under construction
|
||||
undefined,
|
||||
),
|
||||
);
|
||||
@@ -117,7 +93,7 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
|
||||
UserTab.Preferences,
|
||||
_td("common|preferences"),
|
||||
"mx_UserSettingsDialog_preferencesIcon",
|
||||
<PreferencesUserSettingsTab closeSettingsFn={this.props.onFinished} />,
|
||||
<PreferencesUserSettingsTab closeSettingsFn={props.onFinished} />,
|
||||
"UserSettingsPreferences",
|
||||
),
|
||||
);
|
||||
@@ -140,7 +116,7 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
|
||||
),
|
||||
);
|
||||
|
||||
if (SettingsStore.getValue(UIFeature.Voip)) {
|
||||
if (voipEnabled) {
|
||||
tabs.push(
|
||||
new Tab(
|
||||
UserTab.Voice,
|
||||
@@ -157,11 +133,11 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
|
||||
UserTab.Security,
|
||||
_td("room_settings|security|title"),
|
||||
"mx_UserSettingsDialog_securityIcon",
|
||||
<SecurityUserSettingsTab closeSettingsFn={this.props.onFinished} />,
|
||||
<SecurityUserSettingsTab closeSettingsFn={props.onFinished} />,
|
||||
"UserSettingsSecurityPrivacy",
|
||||
),
|
||||
);
|
||||
// Show the Labs tab if enabled or if there are any active betas
|
||||
|
||||
if (showLabsFlags() || SettingsStore.getFeatureSettingNames().some((k) => SettingsStore.getBetaInfo(k))) {
|
||||
tabs.push(
|
||||
new Tab(
|
||||
@@ -173,7 +149,7 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
|
||||
),
|
||||
);
|
||||
}
|
||||
if (this.state.mjolnirEnabled) {
|
||||
if (mjolnirEnabled) {
|
||||
tabs.push(
|
||||
new Tab(
|
||||
UserTab.Mjolnir,
|
||||
@@ -195,29 +171,23 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
|
||||
);
|
||||
|
||||
return tabs as NonEmptyArray<Tab<UserTab>>;
|
||||
}
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
// XXX: SDKContext is provided within the LoggedInView subtree.
|
||||
// Modals function outside the MatrixChat React tree, so sdkContext is reprovided here to simulate that.
|
||||
// The longer term solution is to move our ModalManager into the React tree to inherit contexts properly.
|
||||
<SDKContext.Provider value={this.props.sdkContext}>
|
||||
<SDKContext.Provider value={props.sdkContext}>
|
||||
<BaseDialog
|
||||
className="mx_UserSettingsDialog"
|
||||
hasCancel={true}
|
||||
onFinished={this.props.onFinished}
|
||||
onFinished={props.onFinished}
|
||||
title={_t("common|settings")}
|
||||
>
|
||||
<div className="mx_SettingsDialog_content">
|
||||
<TabbedView
|
||||
tabs={this.getTabs()}
|
||||
initialTabId={this.props.initialTabId}
|
||||
screenName="UserSettings"
|
||||
/>
|
||||
<TabbedView tabs={getTabs()} initialTabId={props.initialTabId} screenName="UserSettings" />
|
||||
</div>
|
||||
</BaseDialog>
|
||||
</SDKContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { NotificationCountType, Room, Thread, ReceiptType } from "matrix-js-sdk/src/matrix";
|
||||
import React, { useContext, useMemo } from "react";
|
||||
import React, { useContext } from "react";
|
||||
import { ReadReceipt } from "matrix-js-sdk/src/models/read-receipt";
|
||||
|
||||
import MatrixClientContext from "../../../../contexts/MatrixClientContext";
|
||||
@@ -25,7 +25,6 @@ import { determineUnreadState } from "../../../../RoomNotifs";
|
||||
import { humanReadableNotificationLevel } from "../../../../stores/notifications/NotificationLevel";
|
||||
import { doesRoomOrThreadHaveUnreadMessages } from "../../../../Unread";
|
||||
import BaseTool, { DevtoolsContext, IDevtoolsProps } from "./BaseTool";
|
||||
import SettingsStore from "../../../../settings/SettingsStore";
|
||||
|
||||
function UserReadUpTo({ target }: { target: ReadReceipt<any, any> }): JSX.Element {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
@@ -66,12 +65,10 @@ function UserReadUpTo({ target }: { target: ReadReceipt<any, any> }): JSX.Elemen
|
||||
}
|
||||
|
||||
export default function RoomNotifications({ onBack }: IDevtoolsProps): JSX.Element {
|
||||
const tacEnabled = useMemo(() => SettingsStore.getValue("threadsActivityCentre"), []);
|
||||
|
||||
const { room } = useContext(DevtoolsContext);
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
const { level, count } = determineUnreadState(room, undefined, !tacEnabled);
|
||||
const { level, count } = determineUnreadState(room, undefined, false);
|
||||
const [notificationState] = useNotificationState(room);
|
||||
|
||||
return (
|
||||
|
@@ -939,7 +939,9 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
||||
setInviteLinkCopied(true);
|
||||
copyPlaintext(ownInviteLink);
|
||||
}}
|
||||
onHideTooltip={() => setInviteLinkCopied(false)}
|
||||
onTooltipOpenChange={(open) => {
|
||||
if (!open) setInviteLinkCopied(false);
|
||||
}}
|
||||
title={inviteLinkCopied ? _t("common|copied") : _t("action|copy")}
|
||||
>
|
||||
<span className="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline">
|
||||
|
@@ -17,18 +17,19 @@ limitations under the License.
|
||||
import classNames from "classnames";
|
||||
import React, { ComponentProps, ReactNode } from "react";
|
||||
|
||||
import { RovingAccessibleTooltipButton } from "../../../../accessibility/roving/RovingAccessibleTooltipButton";
|
||||
import { useRovingTabIndex } from "../../../../accessibility/RovingTabIndex";
|
||||
import AccessibleTooltipButton from "../../elements/AccessibleTooltipButton";
|
||||
import AccessibleButton from "../../elements/AccessibleButton";
|
||||
import { Ref } from "../../../../accessibility/roving/types";
|
||||
|
||||
interface TooltipOptionProps extends ComponentProps<typeof RovingAccessibleTooltipButton> {
|
||||
interface TooltipOptionProps extends ComponentProps<typeof AccessibleButton> {
|
||||
endAdornment?: ReactNode;
|
||||
inputRef?: Ref;
|
||||
}
|
||||
|
||||
export const TooltipOption: React.FC<TooltipOptionProps> = ({ inputRef, className, ...props }) => {
|
||||
const [onFocus, isActive, ref] = useRovingTabIndex(inputRef);
|
||||
return (
|
||||
<AccessibleTooltipButton
|
||||
<AccessibleButton
|
||||
{...props}
|
||||
className={classNames(className, "mx_SpotlightDialog_option")}
|
||||
onFocus={onFocus}
|
||||
|
@@ -14,8 +14,9 @@
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { forwardRef, FunctionComponent, HTMLAttributes, InputHTMLAttributes, Ref } from "react";
|
||||
import React, { ComponentProps, forwardRef, FunctionComponent, HTMLAttributes, InputHTMLAttributes, Ref } from "react";
|
||||
import classnames from "classnames";
|
||||
import { Tooltip } from "@vector-im/compound-web";
|
||||
|
||||
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
||||
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||
@@ -60,6 +61,8 @@ type DynamicElementProps<T extends keyof JSX.IntrinsicElements> = Partial<
|
||||
> &
|
||||
Omit<InputHTMLAttributes<Element>, "onClick">;
|
||||
|
||||
type TooltipProps = ComponentProps<typeof Tooltip>;
|
||||
|
||||
/**
|
||||
* Type of props accepted by {@link AccessibleButton}.
|
||||
*
|
||||
@@ -86,6 +89,23 @@ type Props<T extends keyof JSX.IntrinsicElements> = DynamicHtmlElementProps<T> &
|
||||
* Event handler for button activation. Should be implemented exactly like a normal `onClick` handler.
|
||||
*/
|
||||
onClick: ((e: ButtonEvent) => void | Promise<void>) | null;
|
||||
/**
|
||||
* The tooltip to show on hover or focus.
|
||||
*/
|
||||
title?: string;
|
||||
/**
|
||||
* The caption is a secondary text displayed under the `title` of the tooltip.
|
||||
* Only valid when used in conjunction with `title`.
|
||||
*/
|
||||
caption?: string;
|
||||
/**
|
||||
* The placement of the tooltip.
|
||||
*/
|
||||
placement?: TooltipProps["placement"];
|
||||
/**
|
||||
* Callback for when the tooltip is opened or closed.
|
||||
*/
|
||||
onTooltipOpenChange?: TooltipProps["onOpenChange"];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -116,11 +136,16 @@ const AccessibleButton = forwardRef(function <T extends keyof JSX.IntrinsicEleme
|
||||
onKeyDown,
|
||||
onKeyUp,
|
||||
triggerOnMouseDown,
|
||||
title,
|
||||
caption,
|
||||
placement = "right",
|
||||
onTooltipOpenChange,
|
||||
...restProps
|
||||
}: Props<T>,
|
||||
ref: Ref<HTMLElement>,
|
||||
): JSX.Element {
|
||||
const newProps: RenderedElementProps = restProps;
|
||||
newProps["aria-label"] = newProps["aria-label"] ?? title;
|
||||
if (disabled) {
|
||||
newProps["aria-disabled"] = true;
|
||||
newProps["disabled"] = true;
|
||||
@@ -182,7 +207,22 @@ const AccessibleButton = forwardRef(function <T extends keyof JSX.IntrinsicEleme
|
||||
});
|
||||
|
||||
// React.createElement expects InputHTMLAttributes
|
||||
return React.createElement(element, newProps, children);
|
||||
const button = React.createElement(element, newProps, children);
|
||||
|
||||
if (title) {
|
||||
return (
|
||||
<Tooltip
|
||||
label={title}
|
||||
caption={caption}
|
||||
isTriggerInteractive={true}
|
||||
placement={placement}
|
||||
onOpenChange={onTooltipOpenChange}
|
||||
>
|
||||
{button}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return button;
|
||||
});
|
||||
|
||||
// Type assertion required due to forwardRef type workaround in react.d.ts
|
||||
|
@@ -60,8 +60,11 @@ type Props<T extends keyof JSX.IntrinsicElements> = ComponentProps<typeof Access
|
||||
onHideTooltip?(ev: SyntheticEvent): void;
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated use AccessibleButton with `title` and `caption` instead.
|
||||
*/
|
||||
const AccessibleTooltipButton = forwardRef(function <T extends keyof JSX.IntrinsicElements>(
|
||||
{ title, tooltip, children, forceHide, alignment, onHideTooltip, tooltipClassName, ...props }: Props<T>,
|
||||
{ title, tooltip, children, forceHide, alignment, onHideTooltip, tooltipClassName, element, ...props }: Props<T>,
|
||||
ref: Ref<HTMLElement>,
|
||||
) {
|
||||
const [hover, setHover] = useState(false);
|
||||
@@ -97,6 +100,7 @@ const AccessibleTooltipButton = forwardRef(function <T extends keyof JSX.Intrins
|
||||
return (
|
||||
<AccessibleButton
|
||||
{...props}
|
||||
element={element as keyof JSX.IntrinsicElements}
|
||||
onMouseOver={showTooltip}
|
||||
onMouseLeave={hideTooltip}
|
||||
onFocus={onFocus}
|
||||
|
@@ -20,8 +20,7 @@ import classNames from "classnames";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { copyPlaintext } from "../../../utils/strings";
|
||||
import { ButtonEvent } from "./AccessibleButton";
|
||||
import AccessibleTooltipButton from "./AccessibleTooltipButton";
|
||||
import AccessibleButton, { ButtonEvent } from "./AccessibleButton";
|
||||
|
||||
interface IProps {
|
||||
children?: React.ReactNode;
|
||||
@@ -53,11 +52,13 @@ const CopyableText: React.FC<IProps> = ({ children, getTextToCopy, border = true
|
||||
return (
|
||||
<div className={combinedClassName}>
|
||||
{children}
|
||||
<AccessibleTooltipButton
|
||||
<AccessibleButton
|
||||
title={tooltip ?? _t("action|copy")}
|
||||
onClick={onCopyClickInternal}
|
||||
className="mx_CopyableText_copyButton"
|
||||
onHideTooltip={onHideTooltip}
|
||||
onTooltipOpenChange={(open) => {
|
||||
if (!open) onHideTooltip();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@@ -21,7 +21,6 @@ import FocusLock from "react-focus-lock";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import AccessibleTooltipButton from "./AccessibleTooltipButton";
|
||||
import MemberAvatar from "../avatars/MemberAvatar";
|
||||
import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton";
|
||||
import MessageContextMenu from "../context_menus/MessageContextMenu";
|
||||
@@ -38,6 +37,7 @@ import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
||||
import { presentableTextForFile } from "../../../utils/FileUtils";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
|
||||
// Max scale to keep gaps around the image
|
||||
const MAX_SCALE = 0.95;
|
||||
@@ -513,14 +513,14 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
const zoomOutButton = (
|
||||
<AccessibleTooltipButton
|
||||
<AccessibleButton
|
||||
className="mx_ImageView_button mx_ImageView_button_zoomOut"
|
||||
title={_t("action|zoom_out")}
|
||||
onClick={this.onZoomOutClick}
|
||||
/>
|
||||
);
|
||||
const zoomInButton = (
|
||||
<AccessibleTooltipButton
|
||||
<AccessibleButton
|
||||
className="mx_ImageView_button mx_ImageView_button_zoomIn"
|
||||
title={_t("action|zoom_in")}
|
||||
onClick={this.onZoomInClick}
|
||||
@@ -553,23 +553,23 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||
<div className="mx_ImageView_toolbar">
|
||||
{zoomOutButton}
|
||||
{zoomInButton}
|
||||
<AccessibleTooltipButton
|
||||
<AccessibleButton
|
||||
className="mx_ImageView_button mx_ImageView_button_rotateCCW"
|
||||
title={_t("lightbox|rotate_left")}
|
||||
onClick={this.onRotateCounterClockwiseClick}
|
||||
/>
|
||||
<AccessibleTooltipButton
|
||||
<AccessibleButton
|
||||
className="mx_ImageView_button mx_ImageView_button_rotateCW"
|
||||
title={_t("lightbox|rotate_right")}
|
||||
onClick={this.onRotateClockwiseClick}
|
||||
/>
|
||||
<AccessibleTooltipButton
|
||||
<AccessibleButton
|
||||
className="mx_ImageView_button mx_ImageView_button_download"
|
||||
title={_t("action|download")}
|
||||
onClick={this.onDownloadClick}
|
||||
/>
|
||||
{contextMenuButton}
|
||||
<AccessibleTooltipButton
|
||||
<AccessibleButton
|
||||
className="mx_ImageView_button mx_ImageView_button_close"
|
||||
title={_t("action|close")}
|
||||
onClick={this.props.onFinished}
|
||||
|
@@ -30,7 +30,6 @@ import { Signup } from "@matrix-org/analytics-events/types/typescript/Signup";
|
||||
import PlatformPeg from "../../../PlatformPeg";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import AccessibleTooltipButton from "./AccessibleTooltipButton";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import { PosthogAnalytics } from "../../../PosthogAnalytics";
|
||||
|
||||
@@ -131,9 +130,9 @@ const SSOButton: React.FC<ISSOButtonProps> = ({
|
||||
if (mini) {
|
||||
// TODO fallback icon
|
||||
return (
|
||||
<AccessibleTooltipButton {...props} title={label} className={classes} onClick={onClick}>
|
||||
<AccessibleButton {...props} title={label} className={classes} onClick={onClick}>
|
||||
{icon}
|
||||
</AccessibleTooltipButton>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -18,7 +18,7 @@ limitations under the License.
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import AccessibleTooltipButton from "./AccessibleTooltipButton";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
|
||||
interface IProps {
|
||||
// Whether or not this toggle is in the 'on' position.
|
||||
@@ -41,7 +41,7 @@ interface IProps {
|
||||
}
|
||||
|
||||
// Controlled Toggle Switch element, written with Accessibility in mind
|
||||
export default ({ checked, disabled = false, onChange, ...props }: IProps): JSX.Element => {
|
||||
export default ({ checked, disabled = false, onChange, title, tooltip, ...props }: IProps): JSX.Element => {
|
||||
const _onClick = (): void => {
|
||||
if (disabled) return;
|
||||
onChange(!checked);
|
||||
@@ -54,15 +54,17 @@ export default ({ checked, disabled = false, onChange, ...props }: IProps): JSX.
|
||||
});
|
||||
|
||||
return (
|
||||
<AccessibleTooltipButton
|
||||
<AccessibleButton
|
||||
{...props}
|
||||
className={classes}
|
||||
onClick={_onClick}
|
||||
role="switch"
|
||||
aria-label={title}
|
||||
aria-checked={checked}
|
||||
aria-disabled={disabled}
|
||||
title={tooltip}
|
||||
>
|
||||
<div className="mx_ToggleSwitch_ball" />
|
||||
</AccessibleTooltipButton>
|
||||
</AccessibleButton>
|
||||
);
|
||||
};
|
||||
|
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2022-2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -14,23 +14,41 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { forwardRef, ForwardRefExoticComponent } from "react";
|
||||
import React, { forwardRef, ForwardRefExoticComponent, useContext } from "react";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { DecryptionFailureCode } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { IBodyProps } from "./IBodyProps";
|
||||
import { LocalDeviceVerificationStateContext } from "../../../contexts/LocalDeviceVerificationStateContext";
|
||||
|
||||
function getErrorMessage(mxEvent?: MatrixEvent): string {
|
||||
return mxEvent?.isEncryptedDisabledForUnverifiedDevices
|
||||
? _t("timeline|decryption_failure|blocked")
|
||||
: _t("timeline|decryption_failure|unable_to_decrypt");
|
||||
function getErrorMessage(mxEvent: MatrixEvent, isVerified: boolean | undefined): string {
|
||||
if (mxEvent.isEncryptedDisabledForUnverifiedDevices) return _t("timeline|decryption_failure|blocked");
|
||||
switch (mxEvent.decryptionFailureReason) {
|
||||
case DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP:
|
||||
return _t("timeline|decryption_failure|historical_event_no_key_backup");
|
||||
|
||||
case DecryptionFailureCode.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED:
|
||||
if (isVerified === false) {
|
||||
// The user seems to have a key backup, so prompt them to verify in the hope that doing so will
|
||||
// mean we can restore from backup and we'll get the key for this message.
|
||||
return _t("timeline|decryption_failure|historical_event_unverified_device");
|
||||
}
|
||||
// otherwise, use the default.
|
||||
break;
|
||||
|
||||
case DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED:
|
||||
return _t("timeline|decryption_failure|historical_event_user_not_joined");
|
||||
}
|
||||
return _t("timeline|decryption_failure|unable_to_decrypt");
|
||||
}
|
||||
|
||||
// A placeholder element for messages that could not be decrypted
|
||||
export const DecryptionFailureBody = forwardRef<HTMLDivElement, IBodyProps>(({ mxEvent }, ref): JSX.Element => {
|
||||
export const DecryptionFailureBody = forwardRef<HTMLDivElement, IBodyProps>(({ mxEvent }, ref): React.JSX.Element => {
|
||||
const verificationState = useContext(LocalDeviceVerificationStateContext);
|
||||
return (
|
||||
<div className="mx_DecryptionFailureBody mx_EventTile_content" ref={ref}>
|
||||
{getErrorMessage(mxEvent)}
|
||||
{getErrorMessage(mxEvent, verificationState)}
|
||||
</div>
|
||||
);
|
||||
}) as ForwardRefExoticComponent<IBodyProps>;
|
||||
|
@@ -229,22 +229,16 @@ const ReplyInThreadButton: React.FC<IReplyInThreadButton> = ({ mxEvent }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const title = !hasARelation ? _t("action|reply_in_thread") : _t("threads|error_start_thread_existing_relation");
|
||||
|
||||
return (
|
||||
<RovingAccessibleTooltipButton
|
||||
className="mx_MessageActionBar_iconButton mx_MessageActionBar_threadButton"
|
||||
disabled={hasARelation}
|
||||
tooltip={
|
||||
<>
|
||||
<div className="mx_Tooltip_title">
|
||||
{!hasARelation
|
||||
? _t("action|reply_in_thread")
|
||||
: _t("threads|error_start_thread_existing_relation")}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
title={!hasARelation ? _t("action|reply_in_thread") : _t("threads|error_start_thread_existing_relation")}
|
||||
title={title}
|
||||
onClick={onClick}
|
||||
onContextMenu={onClick}
|
||||
placement="left"
|
||||
>
|
||||
<ThreadIcon />
|
||||
</RovingAccessibleTooltipButton>
|
||||
@@ -508,18 +502,7 @@ class MessageActionBar extends React.PureComponent<IMessageActionBarProps> {
|
||||
mx_MessageActionBar_iconButton: true,
|
||||
mx_MessageActionBar_expandCollapseMessageButton: true,
|
||||
});
|
||||
const tooltip = (
|
||||
<>
|
||||
<div className="mx_Tooltip_title">
|
||||
{this.props.isQuoteExpanded
|
||||
? _t("timeline|mab|collapse_reply_chain")
|
||||
: _t("timeline|mab|expand_reply_chain")}
|
||||
</div>
|
||||
<div className="mx_Tooltip_sub">
|
||||
{_t(ALTERNATE_KEY_NAME[Key.SHIFT]) + " + " + _t("action|click")}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
toolbarOpts.push(
|
||||
<RovingAccessibleTooltipButton
|
||||
className={expandClassName}
|
||||
@@ -528,9 +511,10 @@ class MessageActionBar extends React.PureComponent<IMessageActionBarProps> {
|
||||
? _t("timeline|mab|collapse_reply_chain")
|
||||
: _t("timeline|mab|expand_reply_chain")
|
||||
}
|
||||
tooltip={tooltip}
|
||||
caption={_t(ALTERNATE_KEY_NAME[Key.SHIFT]) + " + " + _t("action|click")}
|
||||
onClick={this.props.toggleThreadExpanded}
|
||||
key="expand"
|
||||
placement="left"
|
||||
>
|
||||
{this.props.isQuoteExpanded ? <CollapseMessageIcon /> : <ExpandMessageIcon />}
|
||||
</RovingAccessibleTooltipButton>,
|
||||
|
@@ -34,7 +34,6 @@ import { WidgetType } from "../../../widgets/WidgetType";
|
||||
import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
|
||||
import WidgetUtils from "../../../utils/WidgetUtils";
|
||||
import { ElementWidgetActions } from "../../../stores/widgets/ElementWidgetActions";
|
||||
import { Alignment } from "../elements/Tooltip";
|
||||
|
||||
interface Props {
|
||||
widgetId: string;
|
||||
@@ -128,9 +127,9 @@ export const WidgetPip: FC<Props> = ({ widgetId, room, viewingRoom, onStartMovin
|
||||
<Toolbar className="mx_WidgetPip_footer">
|
||||
<RovingAccessibleTooltipButton
|
||||
onClick={onLeaveClick}
|
||||
tooltip={_t("action|leave")}
|
||||
title={_t("action|leave")}
|
||||
aria-label={_t("action|leave")}
|
||||
alignment={Alignment.Top}
|
||||
placement="top"
|
||||
>
|
||||
<HangupIcon className="mx_Icon mx_Icon_24" />
|
||||
</RovingAccessibleTooltipButton>
|
||||
|
@@ -237,7 +237,12 @@ export function DeviceItem({
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<AccessibleButton className={classes} title={device.deviceId} onClick={onDeviceClick}>
|
||||
<AccessibleButton
|
||||
className={classes}
|
||||
title={device.deviceId}
|
||||
aria-label={deviceName}
|
||||
onClick={onDeviceClick}
|
||||
>
|
||||
<div className={iconClasses} />
|
||||
<div className="mx_UserInfo_device_name">{deviceName}</div>
|
||||
<div className="mx_UserInfo_device_trusted">{trustedLabel}</div>
|
||||
|
@@ -81,6 +81,7 @@ const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick, onH
|
||||
src={require("../../../../res/img/cancel.svg").default}
|
||||
width="18"
|
||||
height="18"
|
||||
draggable="false"
|
||||
/>
|
||||
</AccessibleButton>
|
||||
) : undefined}
|
||||
|
@@ -127,16 +127,6 @@ interface IFormatButtonProps {
|
||||
class FormatButton extends React.PureComponent<IFormatButtonProps> {
|
||||
public render(): React.ReactNode {
|
||||
const className = `mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIcon${this.props.icon}`;
|
||||
let shortcut;
|
||||
if (this.props.shortcut) {
|
||||
shortcut = <div className="mx_MessageComposerFormatBar_tooltipShortcut">{this.props.shortcut}</div>;
|
||||
}
|
||||
const tooltip = (
|
||||
<div>
|
||||
<div className="mx_Tooltip_title">{this.props.label}</div>
|
||||
<div className="mx_Tooltip_sub">{shortcut}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// element="button" and type="button" are necessary for the buttons to work on WebKit,
|
||||
// otherwise the text is deselected before onClick can ever be called
|
||||
@@ -145,8 +135,9 @@ class FormatButton extends React.PureComponent<IFormatButtonProps> {
|
||||
element="button"
|
||||
type="button"
|
||||
onClick={this.props.onClick}
|
||||
aria-label={this.props.label}
|
||||
title={this.props.label}
|
||||
tooltip={tooltip}
|
||||
caption={this.props.shortcut}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
|
@@ -159,7 +159,7 @@ const DmAuxButton: React.FC<IAuxButtonProps> = ({ tabIndex, dispatcher = default
|
||||
showSpaceInvite(activeSpace);
|
||||
}}
|
||||
disabled={!canInvite}
|
||||
tooltip={canInvite ? undefined : _t("spaces|error_no_permission_invite")}
|
||||
title={canInvite ? undefined : _t("spaces|error_no_permission_invite")}
|
||||
/>
|
||||
)}
|
||||
</IconizedContextMenuOptionList>
|
||||
@@ -249,7 +249,7 @@ const UntaggedAuxButton: React.FC<IAuxButtonProps> = ({ tabIndex }) => {
|
||||
PosthogTrackers.trackInteraction("WebRoomListRoomsSublistPlusMenuCreateRoomItem", e);
|
||||
}}
|
||||
disabled={!canAddRooms}
|
||||
tooltip={canAddRooms ? undefined : _t("spaces|error_no_permission_create_room")}
|
||||
title={canAddRooms ? undefined : _t("spaces|error_no_permission_create_room")}
|
||||
/>
|
||||
{videoRoomsEnabled && (
|
||||
<IconizedContextMenuOption
|
||||
@@ -265,7 +265,7 @@ const UntaggedAuxButton: React.FC<IAuxButtonProps> = ({ tabIndex }) => {
|
||||
);
|
||||
}}
|
||||
disabled={!canAddRooms}
|
||||
tooltip={canAddRooms ? undefined : _t("spaces|error_no_permission_create_room")}
|
||||
title={canAddRooms ? undefined : _t("spaces|error_no_permission_create_room")}
|
||||
>
|
||||
<BetaPill />
|
||||
</IconizedContextMenuOption>
|
||||
@@ -280,7 +280,7 @@ const UntaggedAuxButton: React.FC<IAuxButtonProps> = ({ tabIndex }) => {
|
||||
showAddExistingRooms(activeSpace);
|
||||
}}
|
||||
disabled={!canAddRooms}
|
||||
tooltip={canAddRooms ? undefined : _t("spaces|error_no_permission_add_room")}
|
||||
title={canAddRooms ? undefined : _t("spaces|error_no_permission_add_room")}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
@@ -267,7 +267,7 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
|
||||
closePlusMenu();
|
||||
}}
|
||||
disabled={!canAddSubRooms}
|
||||
tooltip={!canAddSubRooms ? _t("spaces|error_no_permission_add_room") : undefined}
|
||||
title={!canAddSubRooms ? _t("spaces|error_no_permission_add_room") : undefined}
|
||||
/>
|
||||
{canCreateSpaces && (
|
||||
<IconizedContextMenuOption
|
||||
@@ -280,7 +280,7 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
|
||||
closePlusMenu();
|
||||
}}
|
||||
disabled={!canAddSubSpaces}
|
||||
tooltip={!canAddSubSpaces ? _t("spaces|error_no_permission_add_space") : undefined}
|
||||
title={!canAddSubSpaces ? _t("spaces|error_no_permission_add_space") : undefined}
|
||||
>
|
||||
<BetaPill />
|
||||
</IconizedContextMenuOption>
|
||||
|
@@ -23,7 +23,7 @@ import AccessibleTooltipButton from "../../elements/AccessibleTooltipButton";
|
||||
|
||||
type Props<T extends keyof JSX.IntrinsicElements> = Omit<
|
||||
ComponentProps<typeof AccessibleTooltipButton<T>>,
|
||||
"aria-label" | "title" | "kind" | "className" | "onClick"
|
||||
"aria-label" | "title" | "kind" | "className" | "onClick" | "element"
|
||||
> & {
|
||||
isExpanded: boolean;
|
||||
onClick: () => void;
|
||||
|
@@ -35,21 +35,23 @@ interface IProps {
|
||||
wellKnown?: IClientWellKnown;
|
||||
}
|
||||
|
||||
export default class LoginWithQRSection extends React.Component<IProps> {
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
function shouldShowQrLegacy(
|
||||
versions?: IServerVersions,
|
||||
wellKnown?: IClientWellKnown,
|
||||
capabilities?: Capabilities,
|
||||
): boolean {
|
||||
// Needs server support for (get_login_token or OIDC Device Authorization Grant) and MSC3886:
|
||||
// in r0 of MSC3882 it is exposed as a feature flag, but in stable and unstable r1 it is a capability
|
||||
const loginTokenCapability = GET_LOGIN_TOKEN_CAPABILITY.findIn<IGetLoginTokenCapability>(capabilities);
|
||||
const getLoginTokenSupported =
|
||||
!!versions?.unstable_features?.["org.matrix.msc3882"] || !!loginTokenCapability?.enabled;
|
||||
const msc3886Supported =
|
||||
!!versions?.unstable_features?.["org.matrix.msc3886"] || !!wellKnown?.["io.element.rendezvous"]?.server;
|
||||
return getLoginTokenSupported && msc3886Supported;
|
||||
}
|
||||
|
||||
public render(): JSX.Element | null {
|
||||
// Needs server support for get_login_token and MSC3886:
|
||||
// in r0 of MSC3882 it is exposed as a feature flag, but in stable and unstable r1 it is a capability
|
||||
const capability = GET_LOGIN_TOKEN_CAPABILITY.findIn<IGetLoginTokenCapability>(this.props.capabilities);
|
||||
const getLoginTokenSupported =
|
||||
!!this.props.versions?.unstable_features?.["org.matrix.msc3882"] || !!capability?.enabled;
|
||||
const msc3886Supported =
|
||||
!!this.props.versions?.unstable_features?.["org.matrix.msc3886"] ||
|
||||
this.props.wellKnown?.["io.element.rendezvous"]?.server;
|
||||
const offerShowQr = getLoginTokenSupported && msc3886Supported;
|
||||
const LoginWithQRSection: React.FC<IProps> = ({ onShowQr, versions, capabilities, wellKnown }) => {
|
||||
const offerShowQr = shouldShowQrLegacy(versions, wellKnown, capabilities);
|
||||
|
||||
// don't show anything if no method is available
|
||||
if (!offerShowQr) {
|
||||
@@ -59,15 +61,14 @@ export default class LoginWithQRSection extends React.Component<IProps> {
|
||||
return (
|
||||
<SettingsSubsection heading={_t("settings|sessions|sign_in_with_qr")}>
|
||||
<div className="mx_LoginWithQRSection">
|
||||
<p className="mx_SettingsTab_subsectionText">
|
||||
{_t("settings|sessions|sign_in_with_qr_description")}
|
||||
</p>
|
||||
<AccessibleButton onClick={this.props.onShowQr} kind="primary">
|
||||
<p className="mx_SettingsTab_subsectionText">{_t("settings|sessions|sign_in_with_qr_description")}</p>
|
||||
<AccessibleButton onClick={onShowQr} kind="primary">
|
||||
<QrCodeIcon height={20} width={20} />
|
||||
{_t("settings|sessions|sign_in_with_qr_button")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</SettingsSubsection>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default LoginWithQRSection;
|
||||
|
@@ -32,7 +32,8 @@ import { ExtendedDevice } from "../../devices/types";
|
||||
import { deleteDevicesWithInteractiveAuth } from "../../devices/deleteDevices";
|
||||
import SettingsTab from "../SettingsTab";
|
||||
import LoginWithQRSection from "../../devices/LoginWithQRSection";
|
||||
import LoginWithQR, { Mode } from "../../../auth/LoginWithQR";
|
||||
import LoginWithQR from "../../../auth/LoginWithQR";
|
||||
import { Mode } from "../../../auth/LoginWithQR-types";
|
||||
import { useAsyncMemo } from "../../../../../hooks/useAsyncMemo";
|
||||
import QuestionDialog from "../../../dialogs/QuestionDialog";
|
||||
import { FilterVariation } from "../../devices/filter";
|
||||
@@ -284,6 +285,12 @@ const SessionManagerTab: React.FC = () => {
|
||||
return (
|
||||
<SettingsTab>
|
||||
<SettingsSection heading={_t("settings|sessions|title")}>
|
||||
<LoginWithQRSection
|
||||
onShowQr={onShowQrClicked}
|
||||
versions={clientVersions}
|
||||
capabilities={capabilities}
|
||||
wellKnown={wellKnown}
|
||||
/>
|
||||
<SecurityRecommendations
|
||||
devices={devices}
|
||||
goToFilteredList={onGoToFilteredList}
|
||||
@@ -337,12 +344,6 @@ const SessionManagerTab: React.FC = () => {
|
||||
/>
|
||||
</SettingsSubsection>
|
||||
)}
|
||||
<LoginWithQRSection
|
||||
onShowQr={onShowQrClicked}
|
||||
versions={clientVersions}
|
||||
capabilities={capabilities}
|
||||
wellKnown={wellKnown}
|
||||
/>
|
||||
</SettingsSection>
|
||||
</SettingsTab>
|
||||
);
|
||||
|
@@ -368,8 +368,6 @@ const SpacePanel: React.FC = () => {
|
||||
}
|
||||
});
|
||||
|
||||
const isThreadsActivityCentreEnabled = useSettingValue<boolean>("threadsActivityCentre");
|
||||
|
||||
return (
|
||||
<RovingTabIndexProvider handleHomeEnd handleUpDown={!dragging}>
|
||||
{({ onKeyDownHandler, onDragEndHandler }) => (
|
||||
@@ -426,9 +424,8 @@ const SpacePanel: React.FC = () => {
|
||||
)}
|
||||
</Droppable>
|
||||
|
||||
{isThreadsActivityCentreEnabled && (
|
||||
<ThreadsActivityCentre displayButtonLabel={!isPanelCollapsed} />
|
||||
)}
|
||||
|
||||
<QuickSettingsButton isPanelCollapsed={isPanelCollapsed} />
|
||||
</nav>
|
||||
</DragDropContext>
|
||||
|
@@ -51,7 +51,7 @@ import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||
|
||||
type ButtonProps<T extends keyof JSX.IntrinsicElements> = Omit<
|
||||
ComponentProps<typeof AccessibleTooltipButton<T>>,
|
||||
"title" | "onClick" | "size"
|
||||
"title" | "onClick" | "size" | "element"
|
||||
> & {
|
||||
space?: Room;
|
||||
spaceKey?: SpaceKey;
|
||||
|
@@ -15,13 +15,12 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { RoomEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import type { NotificationCount, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { determineUnreadState } from "../RoomNotifs";
|
||||
import { NotificationLevel } from "../stores/notifications/NotificationLevel";
|
||||
import { useEventEmitter } from "./useEventEmitter";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
|
||||
export const useUnreadNotifications = (
|
||||
room?: Room,
|
||||
@@ -31,8 +30,6 @@ export const useUnreadNotifications = (
|
||||
count: number;
|
||||
level: NotificationLevel;
|
||||
} => {
|
||||
const tacEnabled = useMemo(() => SettingsStore.getValue("threadsActivityCentre"), []);
|
||||
|
||||
const [symbol, setSymbol] = useState<string | null>(null);
|
||||
const [count, setCount] = useState<number>(0);
|
||||
const [level, setLevel] = useState<NotificationLevel>(NotificationLevel.None);
|
||||
@@ -53,11 +50,11 @@ export const useUnreadNotifications = (
|
||||
useEventEmitter(room, RoomEvent.MyMembership, () => updateNotificationState());
|
||||
|
||||
const updateNotificationState = useCallback(() => {
|
||||
const { symbol, count, level } = determineUnreadState(room, threadId, !tacEnabled);
|
||||
const { symbol, count, level } = determineUnreadState(room, threadId, false);
|
||||
setSymbol(symbol);
|
||||
setCount(count);
|
||||
setLevel(level);
|
||||
}, [room, threadId, tacEnabled]);
|
||||
}, [room, threadId]);
|
||||
|
||||
useEffect(() => {
|
||||
updateNotificationState();
|
||||
|
@@ -249,15 +249,7 @@
|
||||
"completing_setup": "Dokončování nastavení nového zařízení",
|
||||
"confirm_code_match": "Zkontrolujte, zda se níže uvedený kód shoduje s vaším dalším zařízením:",
|
||||
"connecting": "Připojování…",
|
||||
"error_device_already_signed_in": "Druhé zařízení je již přihlášeno.",
|
||||
"error_device_not_signed_in": "Druhé zařízení není přihlášeno.",
|
||||
"error_device_unsupported": "Propojení s tímto zařízením není podporováno.",
|
||||
"error_homeserver_lacks_support": "Domovský server nepodporuje přihlášení pomocí jiného zařízení.",
|
||||
"error_invalid_scanned_code": "Naskenovaný kód je neplatný.",
|
||||
"error_linking_incomplete": "Propojení nebylo dokončeno v požadovaném čase.",
|
||||
"error_rate_limited": "Příliš mnoho pokusů v krátkém čase. Počkejte chvíli, než to zkusíte znovu.",
|
||||
"error_request_cancelled": "Požadavek byl zrušen.",
|
||||
"error_request_declined": "Požadavek byl na druhém zařízení odmítnut.",
|
||||
"error_unexpected": "Došlo k neočekávané chybě.",
|
||||
"scan_code_instruction": "Níže uvedený QR kód naskenujte pomocí přihlašovaného zařízení.",
|
||||
"scan_qr_code": "Skenovat QR kód",
|
||||
@@ -1446,18 +1438,9 @@
|
||||
"rust_crypto_optin_warning": "Přechod na Rust kryptografii vyžaduje proces migrace, který může trvat několik minut. Pro deaktivaci se budete muset odhlásit a znovu přihlásit; používejte s opatrností!",
|
||||
"rust_crypto_requires_logout": "Jakmile je Rust kryptografie povolena, lze ji vypnout pouze odhlášením a opětovným přihlášením.",
|
||||
"sliding_sync": "Režim klouzavé synchronizace",
|
||||
"sliding_sync_checking": "Kontrola…",
|
||||
"sliding_sync_configuration": "Nastavení klouzavé synchronizace",
|
||||
"sliding_sync_description": "V aktivním vývoji, nelze zakázat.",
|
||||
"sliding_sync_disable_warning": "Pro deaktivaci se musíte odhlásit a znovu přihlásit, používejte s opatrností!",
|
||||
"sliding_sync_disabled_notice": "Pro vypnutí se odhlaste a znovu přihlaste",
|
||||
"sliding_sync_proxy_url_label": "URL proxy serveru",
|
||||
"sliding_sync_proxy_url_optional_label": "URL proxy serveru (volitelné)",
|
||||
"sliding_sync_server_no_support": "Váš server nemá nativní podporu",
|
||||
"sliding_sync_server_specify_proxy": "Váš server nemá nativní podporu, musíte zadat proxy server",
|
||||
"sliding_sync_server_support": "Váš server má nativní podporu",
|
||||
"threads_activity_centre": "Centrum aktivit vláken (ve vývoji).",
|
||||
"threads_activity_centre_description": "Upozornění: V aktivním vývoji; znovu načte %(brand)s.",
|
||||
"under_active_development": "V aktivním vývoji.",
|
||||
"unrealiable_e2e": "Nespolehlivé v šifrovaných místnostech",
|
||||
"video_rooms": "Video místnosti",
|
||||
|
@@ -245,15 +245,7 @@
|
||||
"completing_setup": "Schließe Anmeldung deines neuen Gerätes ab",
|
||||
"confirm_code_match": "Überprüfe, dass der unten angezeigte Code mit deinem anderen Gerät übereinstimmt:",
|
||||
"connecting": "Verbinde …",
|
||||
"error_device_already_signed_in": "Das andere Gerät ist bereits angemeldet.",
|
||||
"error_device_not_signed_in": "Das andere Gerät ist nicht angemeldet.",
|
||||
"error_device_unsupported": "Die Verbindung mit diesem Gerät wird nicht unterstützt.",
|
||||
"error_homeserver_lacks_support": "Der Heim-Server unterstützt die Anmeldung eines anderen Gerätes nicht.",
|
||||
"error_invalid_scanned_code": "Der gescannte Code ist ungültig.",
|
||||
"error_linking_incomplete": "Die Verbindung konnte nicht in der erforderlichen Zeit hergestellt werden.",
|
||||
"error_rate_limited": "Zu viele Versuche in zu kurzer Zeit. Warte ein wenig, bevor du es erneut versuchst.",
|
||||
"error_request_cancelled": "Die Anfrage wurde abgebrochen.",
|
||||
"error_request_declined": "Die Anfrage wurde auf dem anderen Gerät abgelehnt.",
|
||||
"error_unexpected": "Ein unerwarteter Fehler ist aufgetreten.",
|
||||
"scan_code_instruction": "Lese den folgenden QR-Code mit deinem nicht angemeldeten Gerät ein.",
|
||||
"scan_qr_code": "QR-Code einlesen",
|
||||
@@ -1431,16 +1423,9 @@
|
||||
"report_to_moderators_description": "In Räumen, die Moderation unterstützen, lässt dich die Schaltfläche „Melden“ missbräuchliche Verwendung an die Raummoderation melden.",
|
||||
"rust_crypto": "Rust-Verschlüsselungsumsetzung",
|
||||
"sliding_sync": "Sliding-Sync-Modus",
|
||||
"sliding_sync_checking": "Überprüfe …",
|
||||
"sliding_sync_configuration": "Sliding-Sync-Konfiguration",
|
||||
"sliding_sync_description": "In aktiver Entwicklung, kann nicht deaktiviert werden.",
|
||||
"sliding_sync_disable_warning": "Zum Deaktivieren musst du dich neu anmelden. Mit Vorsicht verwenden!",
|
||||
"sliding_sync_disabled_notice": "Zum Deaktivieren, melde dich ab und erneut an",
|
||||
"sliding_sync_proxy_url_label": "Proxy-URL",
|
||||
"sliding_sync_proxy_url_optional_label": "Proxy-URL (optional)",
|
||||
"sliding_sync_server_no_support": "Dein Server unterstützt dies nicht nativ",
|
||||
"sliding_sync_server_specify_proxy": "Dein Server unterstützt dies nicht nativ, du musst einen Proxy angeben",
|
||||
"sliding_sync_server_support": "Dein Server unterstützt dies nativ",
|
||||
"under_active_development": "In aktiver Entwicklung.",
|
||||
"unrealiable_e2e": "Nicht zuverlässig in verschlüsselten Räumen",
|
||||
"video_rooms": "Videoräume",
|
||||
|
@@ -249,21 +249,29 @@
|
||||
"completing_setup": "Completing set up of your new device",
|
||||
"confirm_code_match": "Check that the code below matches with your other device:",
|
||||
"connecting": "Connecting…",
|
||||
"error_device_already_signed_in": "The other device is already signed in.",
|
||||
"error_device_not_signed_in": "The other device isn't signed in.",
|
||||
"error_device_unsupported": "Linking with this device is not supported.",
|
||||
"error_homeserver_lacks_support": "The homeserver doesn't support signing in another device.",
|
||||
"error_invalid_scanned_code": "The scanned code is invalid.",
|
||||
"error_linking_incomplete": "The linking wasn't completed in the required time.",
|
||||
"error_expired": "Sign in expired. Please try again.",
|
||||
"error_expired_title": "The sign in was not completed in time",
|
||||
"error_insecure_channel_detected": "A secure connection could not be made to the new device. Your existing devices are still safe and you don't need to worry about them.",
|
||||
"error_insecure_channel_detected_instructions": "Now what?",
|
||||
"error_insecure_channel_detected_instructions_1": "Try signing in to the other device again with a QR code in case this was a network problem",
|
||||
"error_insecure_channel_detected_instructions_2": "If you encounter the same problem, try a different wifi network or use your mobile data instead of wifi",
|
||||
"error_insecure_channel_detected_instructions_3": "If that doesn't work, sign in manually",
|
||||
"error_insecure_channel_detected_title": "Connection not secure",
|
||||
"error_other_device_already_signed_in": "You don’t need to do anything else.",
|
||||
"error_other_device_already_signed_in_title": "Your other device is already signed in",
|
||||
"error_rate_limited": "Too many attempts in a short time. Wait some time before trying again.",
|
||||
"error_request_cancelled": "The request was cancelled.",
|
||||
"error_request_declined": "The request was declined on the other device.",
|
||||
"error_unexpected": "An unexpected error occurred.",
|
||||
"follow_remaining_instructions": "Follow the remaining instructions to verify your other device",
|
||||
"error_unexpected": "An unexpected error occurred. The request to connect your other device has been cancelled.",
|
||||
"error_unsupported_protocol": "This device does not support signing in to the other device with a QR code.",
|
||||
"error_unsupported_protocol_title": "Other device not compatible",
|
||||
"error_user_cancelled": "The sign in was cancelled on the other device.",
|
||||
"error_user_cancelled_title": "Sign in request cancelled",
|
||||
"error_user_declined": "You declined the request from your other device to sign in.",
|
||||
"error_user_declined_title": "Sign in declined",
|
||||
"follow_remaining_instructions": "Follow the instructions to link your other device",
|
||||
"open_element_other_device": "Open %(brand)s on your other device",
|
||||
"point_the_camera": "Point the camera at the QR code shown here",
|
||||
"scan_code_instruction": "Scan the QR code with another device",
|
||||
"scan_qr_code": "Scan QR code",
|
||||
"scan_qr_code": "Sign in with QR code",
|
||||
"select_qr_code": "Select \"%(scanQRCode)s\"",
|
||||
"sign_in_new_device": "Sign in new device",
|
||||
"waiting_for_device": "Waiting for device to sign in"
|
||||
@@ -1452,18 +1460,9 @@
|
||||
"rust_crypto_optin_warning": "Switching to the Rust cryptography requires a migration process that may take several minutes. To disable you will need to log out and back in; use with caution!",
|
||||
"rust_crypto_requires_logout": "Once enabled, Rust cryptography can only be disabled by logging out and in again",
|
||||
"sliding_sync": "Sliding Sync mode",
|
||||
"sliding_sync_checking": "Checking…",
|
||||
"sliding_sync_configuration": "Sliding Sync configuration",
|
||||
"sliding_sync_description": "Under active development, cannot be disabled.",
|
||||
"sliding_sync_disable_warning": "To disable you will need to log out and back in, use with caution!",
|
||||
"sliding_sync_disabled_notice": "Log out and back in to disable",
|
||||
"sliding_sync_proxy_url_label": "Proxy URL",
|
||||
"sliding_sync_proxy_url_optional_label": "Proxy URL (optional)",
|
||||
"sliding_sync_server_no_support": "Your server lacks native support",
|
||||
"sliding_sync_server_specify_proxy": "Your server lacks native support, you must specify a proxy",
|
||||
"sliding_sync_server_support": "Your server has native support",
|
||||
"threads_activity_centre": "Threads Activity Centre (in development)",
|
||||
"threads_activity_centre_description": "Warning: Under active development; reloads %(brand)s.",
|
||||
"sliding_sync_server_no_support": "Your server lacks support",
|
||||
"under_active_development": "Under active development.",
|
||||
"unrealiable_e2e": "Unreliable in encrypted rooms",
|
||||
"video_rooms": "Video rooms",
|
||||
@@ -3214,6 +3213,9 @@
|
||||
"creation_summary_room": "%(creator)s created and configured the room.",
|
||||
"decryption_failure": {
|
||||
"blocked": "The sender has blocked you from receiving this message",
|
||||
"historical_event_no_key_backup": "Historical messages are not available on this device",
|
||||
"historical_event_unverified_device": "You need to verify this device for access to historical messages",
|
||||
"historical_event_user_not_joined": "You don't have access to this message",
|
||||
"unable_to_decrypt": "Unable to decrypt message"
|
||||
},
|
||||
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
||||
|
@@ -233,14 +233,7 @@
|
||||
"completing_setup": "Terminando de configurar tu nuevo dispositivo",
|
||||
"confirm_code_match": "Comprueba que el siguiente código también aparece en el otro dispositivo:",
|
||||
"connecting": "Conectando…",
|
||||
"error_device_already_signed_in": "El otro dispositivo ya tiene una sesión iniciada.",
|
||||
"error_device_not_signed_in": "El otro dispositivo no tiene una sesión iniciada.",
|
||||
"error_homeserver_lacks_support": "Tu servidor base no es compatible con el inicio de sesión en otro dispositivo.",
|
||||
"error_invalid_scanned_code": "El código escaneado no es válido.",
|
||||
"error_linking_incomplete": "El proceso de enlace ha tardado demasiado tiempo, por lo que no se ha completado.",
|
||||
"error_rate_limited": "Demasiados intentos en poco tiempo. Espera un poco antes de volverlo a intentar.",
|
||||
"error_request_cancelled": "La solicitud ha sido cancelada.",
|
||||
"error_request_declined": "El otro dispositivo ha rechazado la solicitud.",
|
||||
"error_unexpected": "Ha ocurrido un error inesperado.",
|
||||
"scan_code_instruction": "Escanea el siguiente código QR con tu dispositivo.",
|
||||
"scan_qr_code": "Escanear código QR",
|
||||
@@ -1323,16 +1316,9 @@
|
||||
"report_to_moderators_description": "En las salas que sean compatible con la moderación, el botón de «Denunciar» avisará a los moderadores de la sala.",
|
||||
"rust_crypto": "Implementación de la criptografía en Rust",
|
||||
"sliding_sync": "Modo de sincronización progresiva",
|
||||
"sliding_sync_checking": "Comprobando…",
|
||||
"sliding_sync_configuration": "Configuración de la sincronización progresiva",
|
||||
"sliding_sync_description": "En desarrollo, no se puede desactivar.",
|
||||
"sliding_sync_disable_warning": "Para desactivarlo, tendrás que cerrar sesión y volverla a iniciar. ¡Ten cuidado!",
|
||||
"sliding_sync_disabled_notice": "Cierra sesión y vuélvela a abrir para desactivar",
|
||||
"sliding_sync_proxy_url_label": "URL de servidor proxy",
|
||||
"sliding_sync_proxy_url_optional_label": "URL de servidor proxy (opcional)",
|
||||
"sliding_sync_server_no_support": "Tu servidor no es compatible",
|
||||
"sliding_sync_server_specify_proxy": "Tu servidor no es compatible, debes configurar un intermediario (proxy)",
|
||||
"sliding_sync_server_support": "Tu servidor es compatible",
|
||||
"under_active_development": "Funcionalidad en desarrollo.",
|
||||
"video_rooms": "Salas de vídeo",
|
||||
"video_rooms_a_new_way_to_chat": "Una nueva forma de hablar por voz y vídeo en %(brand)s.",
|
||||
|
@@ -249,15 +249,7 @@
|
||||
"completing_setup": "Lõpetame uue seadme seadistamise",
|
||||
"confirm_code_match": "Kontrolli, et järgnev kood klapib teises seadmes kuvatava koodiga:",
|
||||
"connecting": "Kõne on ühendamisel…",
|
||||
"error_device_already_signed_in": "Teine seade on juba võrku loginud.",
|
||||
"error_device_not_signed_in": "Teine seade ei ole võrku loginud.",
|
||||
"error_device_unsupported": "Sidumine selle seadmega ei ole toetatud.",
|
||||
"error_homeserver_lacks_support": "Koduserver ei toeta muude seadmete võrku logimise võimalust.",
|
||||
"error_invalid_scanned_code": "Skaneeritud QR-kood on vigane.",
|
||||
"error_linking_incomplete": "Sidumine ei lõppenud etteantud aja jooksul.",
|
||||
"error_rate_limited": "Liiga palju päringuid napis ajavahemikus. Enne uuesti proovimist palun oota veidi.",
|
||||
"error_request_cancelled": "Päring katkestati.",
|
||||
"error_request_declined": "Teine seade lükkas päringu tagasi.",
|
||||
"error_unexpected": "Tekkis teadmata viga.",
|
||||
"scan_code_instruction": "Loe QR-koodi seadmega, kus sa oled Matrix'i võrgust välja loginud.",
|
||||
"scan_qr_code": "Loe QR-koodi",
|
||||
@@ -1439,16 +1431,9 @@
|
||||
"rust_crypto_optin_warning": "Rust'i teekidel põhineva krüptograafia kasutusele võtmine eeldab andmete ümbertõstmist ja selleks võib kuluda õige mitu minutit. Selle funktsionaalsuse väljalülitamiseks pead võrgust välja logima ning seejärel tagasi logima. Palun ole kindel, et tead, mida teed!",
|
||||
"rust_crypto_requires_logout": "Kui Rust'i põhised teegid on kasutusel, siis selle funktsionaalsuse väljalülitamiseks pead võrgust välja logima ning seejärel tagasi logima",
|
||||
"sliding_sync": "Järkjärgulise sünkroniseerimise režiim",
|
||||
"sliding_sync_checking": "Kontrollin…",
|
||||
"sliding_sync_configuration": "Sliding Sync konfiguratsioon",
|
||||
"sliding_sync_description": "Aktiivselt arendamisel ega ole võimalik välja lülitada.",
|
||||
"sliding_sync_disable_warning": "Väljalülitamiseks palun logi välja ning seejärel tagasi, kuid ole sellega ettevaatlik!",
|
||||
"sliding_sync_disabled_notice": "Väljalülitamiseks logi Matrix'i võrgust välja ja seejärel tagasi",
|
||||
"sliding_sync_proxy_url_label": "Puhverserveri aadress",
|
||||
"sliding_sync_proxy_url_optional_label": "Puhverserveri aadress (kui vaja)",
|
||||
"sliding_sync_server_no_support": "Selle funktsionaalsuse tugi on sinu koduserveris puudu",
|
||||
"sliding_sync_server_specify_proxy": "Selle funktsionaalsuse tugi on sinu koduserveris puudu, palun kasuta puhverserverit",
|
||||
"sliding_sync_server_support": "Selle funktsionaalsuse tugi on sinu koduserveris olemas",
|
||||
"under_active_development": "Aktiivselt arendamisel.",
|
||||
"video_rooms": "Videotoad",
|
||||
"video_rooms_a_new_way_to_chat": "Uus võimalus videovestlusteks rakenduses %(brand)s.",
|
||||
|
@@ -230,14 +230,7 @@
|
||||
"phone_optional_label": "Puhelin (valinnainen)",
|
||||
"qr_code_login": {
|
||||
"connecting": "Yhdistetään…",
|
||||
"error_device_already_signed_in": "Toinen laite on jo sisäänkirjautunut.",
|
||||
"error_device_not_signed_in": "Toinen laite ei ole sisäänkirjautunut.",
|
||||
"error_device_unsupported": "Tämän laitteen kanssa linkittäminen ei ole tuettu.",
|
||||
"error_invalid_scanned_code": "Skannattu koodi on virheellinen.",
|
||||
"error_linking_incomplete": "Linkitystä ei suoritettu vaaditussa ajassa.",
|
||||
"error_rate_limited": "Liikaa yrityksiä lyhyessä ajassa. Odota hetki, ennen kuin yrität uudelleen.",
|
||||
"error_request_cancelled": "Pyyntö peruttiin.",
|
||||
"error_request_declined": "Pyyntö hylättiin toiselta laitteelta.",
|
||||
"error_unexpected": "Tapahtui odottamaton virhe.",
|
||||
"sign_in_new_device": "Kirjaa sisään uusi laite",
|
||||
"waiting_for_device": "Odotetaan laitteen sisäänkirjautumista"
|
||||
@@ -1253,14 +1246,9 @@
|
||||
"report_to_moderators_description": "Moderointia tukevissa huoneissa väärinkäytökset voi ilmoittaa Ilmoita-painikkeella huoneen moderaattoreille.",
|
||||
"rust_crypto": "Rust-kryptografiatoteutus",
|
||||
"sliding_sync": "Liukuvan synkronoinnin tila",
|
||||
"sliding_sync_checking": "Tarkistetaan…",
|
||||
"sliding_sync_configuration": "Liukuvan synkronoinnin asetukset",
|
||||
"sliding_sync_description": "Työn alla, käytöstä poistaminen ei ole mahdollista.",
|
||||
"sliding_sync_disabled_notice": "Poista käytöstä kirjautumalla ulos ja takaisin sisään",
|
||||
"sliding_sync_proxy_url_label": "Välityspalvelimen URL-osoite",
|
||||
"sliding_sync_proxy_url_optional_label": "Välityspalvelimen URL-osoite (valinnainen)",
|
||||
"sliding_sync_server_no_support": "Palvelimellasi ei ole natiivitukea",
|
||||
"sliding_sync_server_support": "Palvelimellasi on natiivituki",
|
||||
"under_active_development": "Aktiivisen kehityksen kohteena.",
|
||||
"video_rooms": "Videohuoneet",
|
||||
"video_rooms_a_new_way_to_chat": "Uusi tapa keskustella äänen ja videon välityksellä %(brand)sissä.",
|
||||
|
@@ -249,15 +249,7 @@
|
||||
"completing_setup": "Fin de la configuration de votre nouvel appareil",
|
||||
"confirm_code_match": "Vérifiez que le code ci-dessous correspond à celui sur votre autre appareil :",
|
||||
"connecting": "Connexion…",
|
||||
"error_device_already_signed_in": "L’autre appareil est déjà connecté.",
|
||||
"error_device_not_signed_in": "L’autre appareil n’est pas connecté.",
|
||||
"error_device_unsupported": "L’appairage avec cet appareil n’est pas pris en charge.",
|
||||
"error_homeserver_lacks_support": "Le serveur d’accueil ne prend pas en charge la connexion d’un autre appareil.",
|
||||
"error_invalid_scanned_code": "Le code scanné est invalide.",
|
||||
"error_linking_incomplete": "L’appairage n’a pas été effectué dans le temps imparti.",
|
||||
"error_rate_limited": "Trop de tentatives consécutives. Attendez un peu avant de réessayer.",
|
||||
"error_request_cancelled": "La demande a été annulée.",
|
||||
"error_request_declined": "La requête a été refusée sur l’autre appareil.",
|
||||
"error_unexpected": "Une erreur inattendue s’est produite.",
|
||||
"scan_code_instruction": "Scannez le QR code ci-dessous avec l’appareil qui n’est pas connecté.",
|
||||
"scan_qr_code": "Scanner le QR code",
|
||||
@@ -1445,18 +1437,9 @@
|
||||
"rust_crypto_optin_warning": "Si vous passez à la cryptographie Rust, cela démarrera un processus de migration qui peut durer plusieurs minutes. Pour la désactiver, vous devrez vous déconnecter et vous reconnecter; à utiliser prudemment !",
|
||||
"rust_crypto_requires_logout": "Une fois activée, la cryptographie Rust ne peut être désactivée qu'en se déconnectant et se reconnectant",
|
||||
"sliding_sync": "Mode synchronisation progressive",
|
||||
"sliding_sync_checking": "Vérification…",
|
||||
"sliding_sync_configuration": "Configuration de la synchronisation progressive",
|
||||
"sliding_sync_description": "En cours de développement, ne peut être désactivé.",
|
||||
"sliding_sync_disable_warning": "Pour la désactiver, vous devrez vous déconnecter et vous reconnecter, faites attention !",
|
||||
"sliding_sync_disabled_notice": "Déconnectez et revenez pour désactiver",
|
||||
"sliding_sync_proxy_url_label": "URL du serveur mandataire (proxy)",
|
||||
"sliding_sync_proxy_url_optional_label": "URL du serveur mandataire (proxy – facultatif)",
|
||||
"sliding_sync_server_no_support": "Votre serveur manque d’un support natif",
|
||||
"sliding_sync_server_specify_proxy": "Votre serveur manque d’un support natif, vous devez spécifier un serveur mandataire (proxy)",
|
||||
"sliding_sync_server_support": "Votre serveur a un support natif",
|
||||
"threads_activity_centre": "Centre d'activité des fils de discussion (en développement)",
|
||||
"threads_activity_centre_description": "Attention: en cours de développement actif. Recharge %(brand)s",
|
||||
"under_active_development": "En cours de développement.",
|
||||
"unrealiable_e2e": "Non fiable dans les salons chiffrés",
|
||||
"video_rooms": "Salons vidéo",
|
||||
|
@@ -1211,13 +1211,7 @@
|
||||
"leave_beta_reload": "Ao saír da beta volveremos a cargar %(brand)s.",
|
||||
"msc3531_hide_messages_pending_moderation": "Permitir que a moderación agoche mensaxes pendentes de moderar.",
|
||||
"pinning": "Fixando mensaxe",
|
||||
"sliding_sync_configuration": "Configuración Sliding Sync",
|
||||
"sliding_sync_disable_warning": "Para desactivalo tes que saír e volver a acceder, usa con precaución!",
|
||||
"sliding_sync_proxy_url_label": "URL do Proxy",
|
||||
"sliding_sync_proxy_url_optional_label": "URL do proxy (optativo)",
|
||||
"sliding_sync_server_no_support": "O teu servidor non ten soporte nativo",
|
||||
"sliding_sync_server_specify_proxy": "O teu servidor non ten servidor nativo, tes que indicar un proxy",
|
||||
"sliding_sync_server_support": "O teu servidor ten soporte nativo",
|
||||
"video_rooms": "Salas de vídeo",
|
||||
"video_rooms_a_new_way_to_chat": "Un novo xeito de conversar con voz e vídeo en %(brand)s.",
|
||||
"video_rooms_always_on_voip_channels": "As salas de vídeo son canles VoIP sempre activas dentro dunha sala en %(brand)s.",
|
||||
|
@@ -243,14 +243,6 @@
|
||||
"completing_setup": "Új eszköz beállításának elvégzése",
|
||||
"confirm_code_match": "Ellenőrizze, hogy az alábbi kód megegyezik a másik eszközödön lévővel:",
|
||||
"connecting": "Kapcsolás…",
|
||||
"error_device_already_signed_in": "A másik eszköz már bejelentkezett.",
|
||||
"error_device_not_signed_in": "A másik eszköz még nincs bejelentkezve.",
|
||||
"error_device_unsupported": "Összekötés ezzel az eszközzel nem támogatott.",
|
||||
"error_homeserver_lacks_support": "A Matrix-kiszolgáló nem támogatja más eszköz bejelentkeztetését.",
|
||||
"error_invalid_scanned_code": "A beolvasott kód érvénytelen.",
|
||||
"error_linking_incomplete": "Az összekötés az elvárt időn belül nem fejeződött be.",
|
||||
"error_request_cancelled": "A kérés megszakítva.",
|
||||
"error_request_declined": "A kérést elutasították a másik eszközön.",
|
||||
"error_unexpected": "Nemvárt hiba történt.",
|
||||
"scan_code_instruction": "A kijelentkezett eszközzel olvasd be a QR kódot alább.",
|
||||
"scan_qr_code": "QR kód beolvasása",
|
||||
@@ -1412,16 +1404,9 @@
|
||||
"report_to_moderators_description": "A moderálást támogató szobákban a problémás tartalmat a „Jelentés” gombbal lehet a moderátorok felé jelezni.",
|
||||
"rust_crypto": "Rust titkosítási implementáció",
|
||||
"sliding_sync": "Csúszó szinkronizációs mód",
|
||||
"sliding_sync_checking": "Ellenőrzés…",
|
||||
"sliding_sync_configuration": "Csúszó szinkronizáció beállítása",
|
||||
"sliding_sync_description": "Aktív fejlesztés alatt, nem kapcsolható ki.",
|
||||
"sliding_sync_disable_warning": "A kikapcsoláshoz ki, majd újra be kell jelentkezni, használja óvatosan.",
|
||||
"sliding_sync_disabled_notice": "A kikapcsoláshoz ki-, és bejelentkezés szükséges",
|
||||
"sliding_sync_proxy_url_label": "Proxy webcíme",
|
||||
"sliding_sync_proxy_url_optional_label": "Proxy webcíme (nem kötelező)",
|
||||
"sliding_sync_server_no_support": "A kiszolgálója nem támogatja natívan",
|
||||
"sliding_sync_server_specify_proxy": "A kiszolgálója nem támogatja natívan, proxy kiszolgálót kell beállítani",
|
||||
"sliding_sync_server_support": "A kiszolgálója natívan támogatja",
|
||||
"under_active_development": "Aktív fejlesztés alatt.",
|
||||
"video_rooms": "Videószobák",
|
||||
"video_rooms_a_new_way_to_chat": "Új csevegési lehetőség a(z) %(brand)s alkalmazásban, hanggal és videóval.",
|
||||
|
@@ -243,14 +243,6 @@
|
||||
"completing_setup": "Menyelesaikan penyiapan perangkat baru Anda",
|
||||
"confirm_code_match": "Periksa bahwa kode di bawah cocok dengan perangkat Anda yang lain:",
|
||||
"connecting": "Menghubungkan…",
|
||||
"error_device_already_signed_in": "Perangkat yang lain sudah masuk.",
|
||||
"error_device_not_signed_in": "Perangkat yang lain belum masuk.",
|
||||
"error_device_unsupported": "Penautan dengan perangkat ini tidak didukung.",
|
||||
"error_homeserver_lacks_support": "Homeserver tidak mendukung masuk ke perangkat lain.",
|
||||
"error_invalid_scanned_code": "Kode yang dipindai tidak absah.",
|
||||
"error_linking_incomplete": "Penautan tidak selesai dalam waktu yang dibutuhkan.",
|
||||
"error_request_cancelled": "Permintaan dibatalkan.",
|
||||
"error_request_declined": "Permintaan ditolak di perangkat yang lain.",
|
||||
"error_unexpected": "Sebuah kesalahan terjadi secara tidak terduga.",
|
||||
"scan_code_instruction": "Pindai kode QR di bawah dengan perangkat Anda yang sudah keluar dari akun.",
|
||||
"scan_qr_code": "Pindai kode QR",
|
||||
@@ -1418,16 +1410,9 @@
|
||||
"report_to_moderators_description": "Dalam ruangan yang mendukung moderasi, tombol “Laporkan” memungkinkan Anda untuk melaporkan penyalahgunaan ke moderator ruangan.",
|
||||
"rust_crypto": "Implementasi kriptografi Rust",
|
||||
"sliding_sync": "Mode Sinkronisasi Geser",
|
||||
"sliding_sync_checking": "Memeriksa…",
|
||||
"sliding_sync_configuration": "Konfigurasi Penyinkronan Bergeser",
|
||||
"sliding_sync_description": "Dalam pengembangan aktif, tidak dapat dinonaktifkan.",
|
||||
"sliding_sync_disable_warning": "Untuk menonaktifkan Anda harus keluar dan masuk kembali, gunakan dengan hati-hati!",
|
||||
"sliding_sync_disabled_notice": "Keluar dan masuk kembali ke akun untuk menonaktifkan",
|
||||
"sliding_sync_proxy_url_label": "URL Proksi",
|
||||
"sliding_sync_proxy_url_optional_label": "URL Proksi (opsional)",
|
||||
"sliding_sync_server_no_support": "Server Anda belum mendukungnya",
|
||||
"sliding_sync_server_specify_proxy": "Server Anda belum mendukungnya, Anda harus menetapkan sebuah proksi",
|
||||
"sliding_sync_server_support": "Server Anda mendukungnya",
|
||||
"under_active_development": "Dalam pengembangan aktif.",
|
||||
"unrealiable_e2e": "Tidak dapat diandalkan di ruangan terenkripsi",
|
||||
"video_rooms": "Ruangan video",
|
||||
|
@@ -218,7 +218,6 @@
|
||||
"phone_label": "Sími",
|
||||
"phone_optional_label": "Sími (valfrjálst)",
|
||||
"qr_code_login": {
|
||||
"error_invalid_scanned_code": "Skannaði kóðinn er ógildur.",
|
||||
"sign_in_new_device": "Skrá inn nýtt tæki",
|
||||
"waiting_for_device": "Bíð eftir að tækið skráist inn"
|
||||
},
|
||||
@@ -1167,8 +1166,6 @@
|
||||
"pinning": "Festing skilaboða",
|
||||
"report_to_moderators": "Tilkynna til umsjónarmanna",
|
||||
"report_to_moderators_description": "Í spjallrásum sem styðja eftirlit umsjónarmanna, mun 'Kæra'-hnappurinn gefa þér færi á að tilkynna misnotkun til umsjónarmanna spjallrása.",
|
||||
"sliding_sync_proxy_url_label": "Slóð milliþjóns",
|
||||
"sliding_sync_proxy_url_optional_label": "Slóð milliþjóns (valfrjálst)",
|
||||
"under_active_development": "Í virkri þróun.",
|
||||
"video_rooms": "Myndspjallrásir",
|
||||
"video_rooms_a_new_way_to_chat": "Ný leið til að spjalla með tali og myndmerki í %(brand)s.",
|
||||
|
@@ -249,15 +249,7 @@
|
||||
"completing_setup": "Completamento configurazione nuovo dispositivo",
|
||||
"confirm_code_match": "Controlla che il codice sottostante corrisponda nell'altro dispositivo:",
|
||||
"connecting": "In connessione…",
|
||||
"error_device_already_signed_in": "L'altro dispositivo ha già fatto l'accesso.",
|
||||
"error_device_not_signed_in": "L'altro dispositivo non ha fatto l'accesso.",
|
||||
"error_device_unsupported": "Il collegamento con questo dispositivo non è supportato.",
|
||||
"error_homeserver_lacks_support": "L'homeserver non supporta l'accesso in un altro dispositivo.",
|
||||
"error_invalid_scanned_code": "Il codice scansionato non è valido.",
|
||||
"error_linking_incomplete": "Il collegamento non è stato completato nel tempo previsto.",
|
||||
"error_rate_limited": "Troppi tentativi in poco tempo. Attendi un po' prima di riprovare.",
|
||||
"error_request_cancelled": "La richiesta è stata annullata.",
|
||||
"error_request_declined": "La richiesta è stata negata sull'altro dispositivo.",
|
||||
"error_unexpected": "Si è verificato un errore imprevisto.",
|
||||
"scan_code_instruction": "Scansiona il codice QR sottostante con il dispositivo che è disconnesso.",
|
||||
"scan_qr_code": "Scansiona codice QR",
|
||||
@@ -1445,18 +1437,9 @@
|
||||
"rust_crypto_optin_warning": "Il passaggio alla crittografia Rust richiede un processo di migrazione che può impiegare diversi minuti. Per disattivarla dovrai disconnetterti e poi riaccedere, usala con cautela!",
|
||||
"rust_crypto_requires_logout": "Una volta attivata, la crittografia Rust può essere disattivata solo disconnettendoti e riaccedendo.",
|
||||
"sliding_sync": "Modalità di sincr. con slide",
|
||||
"sliding_sync_checking": "Controllo…",
|
||||
"sliding_sync_configuration": "Configurazione sincr. Sliding",
|
||||
"sliding_sync_description": "In sviluppo attivo, non può essere disattivato.",
|
||||
"sliding_sync_disable_warning": "Per disattivarlo dovrai disconnetterti e riaccedere, usare con cautela!",
|
||||
"sliding_sync_disabled_notice": "Disconnettiti e riconnettiti per disattivare",
|
||||
"sliding_sync_proxy_url_label": "URL proxy",
|
||||
"sliding_sync_proxy_url_optional_label": "URL proxy (facoltativo)",
|
||||
"sliding_sync_server_no_support": "Il tuo server non ha il supporto nativo",
|
||||
"sliding_sync_server_specify_proxy": "Il tuo server non ha il supporto nativo, devi specificare un proxy",
|
||||
"sliding_sync_server_support": "Il tuo server ha il supporto nativo",
|
||||
"threads_activity_centre": "Centro attività in discussioni (in sviluppo).",
|
||||
"threads_activity_centre_description": "Attenzione: in fase di sviluppo attivo; ricarica %(brand)s.",
|
||||
"under_active_development": "In sviluppo attivo.",
|
||||
"unrealiable_e2e": "Inaffidabile nelle stanze cifrate",
|
||||
"video_rooms": "Stanze video",
|
||||
|
@@ -233,15 +233,7 @@
|
||||
"completing_setup": "新しい端末の設定を完了しています",
|
||||
"confirm_code_match": "以下のコードが他の端末と一致していることを確認してください:",
|
||||
"connecting": "接続しています…",
|
||||
"error_device_already_signed_in": "もう一方のデバイスは既にサインインしています。",
|
||||
"error_device_not_signed_in": "もう一方の端末はサインインしていません。",
|
||||
"error_device_unsupported": "この端末とのリンクはサポートしていません。",
|
||||
"error_homeserver_lacks_support": "ホームサーバーは他の端末でのサインインをサポートしていません。",
|
||||
"error_invalid_scanned_code": "スキャンされたコードは無効です。",
|
||||
"error_linking_incomplete": "時間内にリンクが完了しませんでした。",
|
||||
"error_rate_limited": "再試行の数が多すぎます。少し待ってから再度試してください。",
|
||||
"error_request_cancelled": "リクエストはキャンセルされました。",
|
||||
"error_request_declined": "リクエストはもう一方の端末で拒否されました。",
|
||||
"error_unexpected": "予期しないエラーが発生しました。",
|
||||
"scan_code_instruction": "サインアウトした端末で以下のQRコードをスキャンしてください。",
|
||||
"scan_qr_code": "QRコードをスキャン",
|
||||
@@ -1338,15 +1330,8 @@
|
||||
"report_to_moderators_description": "モデレートをサポートするルームで「報告」ボタンを使用すると、ルームのモデレーターに問題を報告できます。",
|
||||
"rust_crypto": "Rustによる暗号の実装",
|
||||
"sliding_sync": "スライド式同期モード",
|
||||
"sliding_sync_checking": "確認しています…",
|
||||
"sliding_sync_configuration": "スライド式同期の設定",
|
||||
"sliding_sync_description": "開発中です。無効にできません。",
|
||||
"sliding_sync_disable_warning": "無効にするにはログアウトして、再度ログインする必要があります。注意して使用してください!",
|
||||
"sliding_sync_proxy_url_label": "プロクシーのURL",
|
||||
"sliding_sync_proxy_url_optional_label": "プロクシーのURL(任意)",
|
||||
"sliding_sync_server_no_support": "あなたのサーバーはネイティブでサポートしていません",
|
||||
"sliding_sync_server_specify_proxy": "あなたのサーバーはネイティブでサポートしていません。プロクシーを指定してください",
|
||||
"sliding_sync_server_support": "あなたのサーバーはネイティブでサポートしています",
|
||||
"under_active_development": "開発中。",
|
||||
"video_rooms": "ビデオ通話ルーム",
|
||||
"video_rooms_a_new_way_to_chat": "%(brand)sで音声と動画により会話する新しい方法です。",
|
||||
|
@@ -221,14 +221,6 @@
|
||||
"approve_access_warning": "Door de toegang voor dit apparaat goed te keuren, heeft het volledige toegang tot jouw account.",
|
||||
"completing_setup": "De configuratie van je nieuwe apparaat voltooien",
|
||||
"confirm_code_match": "Controleer of de onderstaande code overeenkomt met je andere apparaat:",
|
||||
"error_device_already_signed_in": "Het andere apparaat is al aangemeld.",
|
||||
"error_device_not_signed_in": "Het andere apparaat is niet ingelogd.",
|
||||
"error_device_unsupported": "Koppelen met dit apparaat wordt niet ondersteund.",
|
||||
"error_homeserver_lacks_support": "De server ondersteunt het inloggen op een ander apparaat niet.",
|
||||
"error_invalid_scanned_code": "De gescande code is ongeldig.",
|
||||
"error_linking_incomplete": "De koppeling is niet binnen de vereiste tijd voltooid.",
|
||||
"error_request_cancelled": "Het verzoek is geannuleerd.",
|
||||
"error_request_declined": "Het verzoek is afgewezen op het andere apparaat.",
|
||||
"error_unexpected": "Er is een onverwachte fout opgetreden.",
|
||||
"scan_code_instruction": "Scan de onderstaande QR-code met je apparaat dat is uitgelogd.",
|
||||
"sign_in_new_device": "Aanmelden nieuw apparaat",
|
||||
@@ -1203,12 +1195,7 @@
|
||||
"leave_beta_reload": "Als je de bèta verlaat, wordt %(brand)s opnieuw geladen.",
|
||||
"msc3531_hide_messages_pending_moderation": "Laat moderators berichten verbergen in afwachting van moderatie.",
|
||||
"pinning": "Berichten vastprikken",
|
||||
"sliding_sync_configuration": "Scrollende Synchronisatie-configuratie",
|
||||
"sliding_sync_disable_warning": "Om uit te schakelen moet je uitloggen en weer inloggen, wees voorzichtig!",
|
||||
"sliding_sync_proxy_url_optional_label": "Proxy-URL (optioneel)",
|
||||
"sliding_sync_server_no_support": "Jouw server heeft geen native ondersteuning",
|
||||
"sliding_sync_server_specify_proxy": "Jouw server heeft geen native ondersteuning, je moet een proxy opgeven",
|
||||
"sliding_sync_server_support": "Jouw server heeft native ondersteuning",
|
||||
"video_rooms": "Video kamers",
|
||||
"video_rooms_a_new_way_to_chat": "Een nieuwe manier om te chatten via spraak en video in %(brand)s.",
|
||||
"video_rooms_always_on_voip_channels": "Videoruimten zijn altijd-aan VoIP-kanalen die zijn geïntegreerd in een kamer in %(brand)s.",
|
||||
|
@@ -249,15 +249,7 @@
|
||||
"completing_setup": "Kończenie konfiguracji nowego urządzenia",
|
||||
"confirm_code_match": "Potwierdź, że kod poniżej pasuje z Twoim drugim urządzeniem:",
|
||||
"connecting": "Łączenie…",
|
||||
"error_device_already_signed_in": "Drugie urządzenie jest już zalogowane.",
|
||||
"error_device_not_signed_in": "Drugie urządzenie nie jest zalogowane.",
|
||||
"error_device_unsupported": "Wiązanie z tym urządzeniem nie jest wspierane.",
|
||||
"error_homeserver_lacks_support": "Serwer domowy nie wspiera logowania innych urządzeń.",
|
||||
"error_invalid_scanned_code": "Zeskanowany kod jest nieprawidłowy.",
|
||||
"error_linking_incomplete": "Wiązanie nie zostało zakończone w ustalonym czasie.",
|
||||
"error_rate_limited": "Za dużo prób w krótkim odstępie czasu. Odczekaj trochę, zanim spróbujesz ponownie.",
|
||||
"error_request_cancelled": "Żądanie zostało anulowane.",
|
||||
"error_request_declined": "Żądanie zostało odrzucone przez drugie urządzenie.",
|
||||
"error_unexpected": "Wystąpił niespodziewany błąd.",
|
||||
"follow_remaining_instructions": "Podążaj zgodnie z pozostałymi instrukcjami, aby zweryfikować drugie urządzenie",
|
||||
"open_element_other_device": "Otwórz %(brand)s na swoim drugim urządzeniu",
|
||||
@@ -1454,18 +1446,9 @@
|
||||
"rust_crypto_optin_warning": "Przejście na kryptografię Rust wymaga procesu migracji, która może potrwać kilka minut. Aby ją wyłączyć, będziesz musiał zalogować się ponownie; zachowaj ostrożność!",
|
||||
"rust_crypto_requires_logout": "Po włączeniu, kryptografia Rust może zostać wyłączona tylko po ponownym zalogowaniu.",
|
||||
"sliding_sync": "Tryb synchronizacji przesuwanej",
|
||||
"sliding_sync_checking": "Sprawdzanie…",
|
||||
"sliding_sync_configuration": "Konfiguracja synchronizacji przesuwanej",
|
||||
"sliding_sync_description": "W trakcie aktywnego rozwoju, nie można wyłączyć.",
|
||||
"sliding_sync_disable_warning": "By wyłączyć, będziesz musiał się zalogować ponownie. Korzystaj z rozwagą!",
|
||||
"sliding_sync_disabled_notice": "Zaloguj się ponownie, aby wyłączyć",
|
||||
"sliding_sync_proxy_url_label": "URL proxy",
|
||||
"sliding_sync_proxy_url_optional_label": "URL proxy (opcjonalne)",
|
||||
"sliding_sync_server_no_support": "Twój serwer nie posiada wsparcia natywnego",
|
||||
"sliding_sync_server_specify_proxy": "Twój serwer nie posiada wsparcia natywnego, musisz podać serwer proxy",
|
||||
"sliding_sync_server_support": "Twój serwer posiada wsparcie natywne",
|
||||
"threads_activity_centre": "Centrum aktywności wątków (w trakcie rozwoju)",
|
||||
"threads_activity_centre_description": "Ostrzeżenie: W trakcie aktywnego rozwoju; przeładowuje %(brand)s.",
|
||||
"under_active_development": "W trakcie aktywnego rozwoju.",
|
||||
"unrealiable_e2e": "Problematyczny w pokojach szyfrowanych",
|
||||
"video_rooms": "Pokoje wideo",
|
||||
|
@@ -244,14 +244,6 @@
|
||||
"completing_setup": "Завершение настройки нового устройства",
|
||||
"confirm_code_match": "Проверьте, чтобы код ниже совпадал с тем, что показан на другом устройстве:",
|
||||
"connecting": "Подключение…",
|
||||
"error_device_already_signed_in": "Уже выполнен вход на другом устройстве.",
|
||||
"error_device_not_signed_in": "На другом устройстве вход не выполнен.",
|
||||
"error_device_unsupported": "Соединение с этим устройством не поддерживается.",
|
||||
"error_homeserver_lacks_support": "Домашний сервер не поддерживает вход с другого устройства.",
|
||||
"error_invalid_scanned_code": "Отсканированный код недействителен.",
|
||||
"error_linking_incomplete": "Соединение не было завершено в нужное время.",
|
||||
"error_request_cancelled": "Запрос был отменён.",
|
||||
"error_request_declined": "Запрос был отклонен на другом устройстве.",
|
||||
"error_unexpected": "Произошла неожиданная ошибка.",
|
||||
"scan_code_instruction": "Отсканируйте приведенный ниже QR-код на устройстве, которое вышло из системы.",
|
||||
"scan_qr_code": "Сканировать QR-код",
|
||||
@@ -1430,16 +1422,9 @@
|
||||
"report_to_moderators_description": "В поддерживающих модерирование комнатах, кнопка \"Пожаловаться\" позволит вам сообщить о нарушении модераторам комнаты.",
|
||||
"rust_crypto": "Реализация криптографии Rust",
|
||||
"sliding_sync": "Режим Sliding Sync",
|
||||
"sliding_sync_checking": "Проверка…",
|
||||
"sliding_sync_configuration": "Настройка Sliding sync",
|
||||
"sliding_sync_description": "В активной разработке, нельзя отключить.",
|
||||
"sliding_sync_disable_warning": "Чтобы отключить, вам нужно выйти из системы и снова войти в систему, используйте с осторожностью!",
|
||||
"sliding_sync_disabled_notice": "Выйдите из системы и снова войдите, чтобы отключить",
|
||||
"sliding_sync_proxy_url_label": "URL-адрес прокси-сервера",
|
||||
"sliding_sync_proxy_url_optional_label": "URL-адрес прокси-сервера (необязательно)",
|
||||
"sliding_sync_server_no_support": "На вашем сервере отсутствует встроенная поддержка",
|
||||
"sliding_sync_server_specify_proxy": "На вашем сервере отсутствует встроенная поддержка, необходимо указать прокси-сервер",
|
||||
"sliding_sync_server_support": "Ваш сервер имеет встроенную поддержку",
|
||||
"under_active_development": "В активной разработке.",
|
||||
"unrealiable_e2e": "Ненадежно в зашифрованных комнатах",
|
||||
"video_rooms": "Видеокомнаты",
|
||||
|
@@ -244,14 +244,6 @@
|
||||
"completing_setup": "Dokončenie nastavenia nového zariadenia",
|
||||
"confirm_code_match": "Skontrolujte, či sa nižšie uvedený kód zhoduje s vaším druhým zariadením:",
|
||||
"connecting": "Pripájanie…",
|
||||
"error_device_already_signed_in": "Druhé zariadenie je už prihlásené.",
|
||||
"error_device_not_signed_in": "Druhé zariadenie nie je prihlásené.",
|
||||
"error_device_unsupported": "Prepojenie s týmto zariadením nie je podporované.",
|
||||
"error_homeserver_lacks_support": "Domovský server nepodporuje prihlasovanie do iného zariadenia.",
|
||||
"error_invalid_scanned_code": "Naskenovaný kód je neplatný.",
|
||||
"error_linking_incomplete": "Prepojenie nebolo dokončené v požadovanom čase.",
|
||||
"error_request_cancelled": "Žiadosť bola zrušená.",
|
||||
"error_request_declined": "Žiadosť bola na druhom zariadení zamietnutá.",
|
||||
"error_unexpected": "Vyskytla sa neočakávaná chyba.",
|
||||
"scan_code_instruction": "Naskenujte nižšie uvedený QR kód pomocou zariadenia, ktoré je odhlásené.",
|
||||
"scan_qr_code": "Skenovať QR kód",
|
||||
@@ -1435,16 +1427,9 @@
|
||||
"report_to_moderators_description": "V miestnostiach, ktoré podporujú moderovanie, môžete pomocou tlačidla \"Nahlásiť\" nahlásiť porušovanie pravidiel moderátorom miestnosti.",
|
||||
"rust_crypto": "Implementácia kryptografie Rust",
|
||||
"sliding_sync": "Režim kĺzavej synchronizácie",
|
||||
"sliding_sync_checking": "Kontrolovanie…",
|
||||
"sliding_sync_configuration": "Konfigurácia kĺzavej synchronizácie",
|
||||
"sliding_sync_description": "V štádiu aktívneho vývoja, nie je možné to vypnúť.",
|
||||
"sliding_sync_disable_warning": "Pre vypnutie sa musíte odhlásiť a znova prihlásiť, používajte opatrne!",
|
||||
"sliding_sync_disabled_notice": "Odhláste sa a znova sa prihláste, aby sa to vyplo",
|
||||
"sliding_sync_proxy_url_label": "URL adresa proxy servera",
|
||||
"sliding_sync_proxy_url_optional_label": "URL adresa proxy servera (voliteľná)",
|
||||
"sliding_sync_server_no_support": "Váš server nemá natívnu podporu",
|
||||
"sliding_sync_server_specify_proxy": "Váš server nemá natívnu podporu, musíte zadať proxy server",
|
||||
"sliding_sync_server_support": "Váš server má natívnu podporu",
|
||||
"under_active_development": "V štádiu aktívneho vývoja.",
|
||||
"unrealiable_e2e": "Nespoľahlivé v šifrovaných miestnostiach",
|
||||
"video_rooms": "Video miestnosti",
|
||||
|
@@ -235,15 +235,7 @@
|
||||
"completing_setup": "Po plotësohet ujdisja e pajisjes tuaj të re",
|
||||
"confirm_code_match": "Kontrolloni se kodi më poshtë përkon me atë në pajisjen tuaj tjetër:",
|
||||
"connecting": "Po lidhet…",
|
||||
"error_device_already_signed_in": "Nga pajisja tjetër është bërë tashmë hyrja.",
|
||||
"error_device_not_signed_in": "Te pajisja tjetër s’është bërë hyrja.",
|
||||
"error_device_unsupported": "Lidhja me këtë pajisje nuk mbulohet.",
|
||||
"error_homeserver_lacks_support": "Shërbyesi Home nuk mbulon bërje hyrjeje në një pajisje tjetër.",
|
||||
"error_invalid_scanned_code": "Kodi i skanuar është i pavlefshëm.",
|
||||
"error_linking_incomplete": "Lidhja s’u plotësua brenda kohës së domosdoshme.",
|
||||
"error_rate_limited": "Shumë përpjekje në një kohë të shkurtër. Prisni ca, para se të riprovoni.",
|
||||
"error_request_cancelled": "Kërkesa u anulua.",
|
||||
"error_request_declined": "Kërkesa u hodh poshtë në pajisjen tjetër.",
|
||||
"error_unexpected": "Ndodhi një gabim të papritur.",
|
||||
"scan_code_instruction": "Skanoni kodin QR më poshtë me pajisjen ku është bërë dalja.",
|
||||
"scan_qr_code": "Skanoni kodin QR",
|
||||
@@ -1348,16 +1340,9 @@
|
||||
"report_to_moderators": "Raportojeni te moderatorët",
|
||||
"report_to_moderators_description": "Në dhoma që mbulojnë moderimin, butoni “Raportojeni” do t’ju lejojë t’u raportoni abuzim moderatorëve të dhomës.",
|
||||
"rust_crypto": "Sendërtim kriptografie Rust",
|
||||
"sliding_sync_checking": "Po kontrollohet…",
|
||||
"sliding_sync_configuration": "Formësim Sliding Sync-u",
|
||||
"sliding_sync_description": "Nën zhvillim aktiv, s’mund të çaktivizohet.",
|
||||
"sliding_sync_disable_warning": "Për ta çaktivizuar do t’ju duhet të bëni daljen dhe ribëni hyrjen, përdoreni me kujdes!",
|
||||
"sliding_sync_disabled_notice": "Që të çaktivizohet, dilni dhe rihyni në llogari",
|
||||
"sliding_sync_proxy_url_label": "URL Ndërmjetësi",
|
||||
"sliding_sync_proxy_url_optional_label": "URL ndërmjetësi (opsionale)",
|
||||
"sliding_sync_server_no_support": "Shërbyesit tuaj i mungon mbulim i brendshëm për këtë",
|
||||
"sliding_sync_server_specify_proxy": "Shërbyesit tuaj i mungon mbulimi së brendshmi, duhet të specifikoni një ndërmjetës",
|
||||
"sliding_sync_server_support": "Shërbyesi juaj ka mbulim të brendshëm për këtë",
|
||||
"under_active_development": "Nën zhvillim aktiv.",
|
||||
"video_rooms": "Dhoma me video",
|
||||
"video_rooms_a_new_way_to_chat": "Një rrugë e re për të biseduar me zë dhe video në %(brand)s.",
|
||||
|
@@ -249,15 +249,7 @@
|
||||
"completing_setup": "Slutför inställning av din nya enhet",
|
||||
"confirm_code_match": "Kolla att koden nedan matchar din andra enhet:",
|
||||
"connecting": "Kopplar upp …",
|
||||
"error_device_already_signed_in": "Den andra enheten är redan inloggad.",
|
||||
"error_device_not_signed_in": "Den andra enheten är inte inloggad.",
|
||||
"error_device_unsupported": "Länkning med den här enheten stöds inte.",
|
||||
"error_homeserver_lacks_support": "Hemservern stöder inte inloggning av en annan enhet.",
|
||||
"error_invalid_scanned_code": "Den skannade koden är ogiltig.",
|
||||
"error_linking_incomplete": "Länkningen slutfördes inte inom den krävda tiden.",
|
||||
"error_rate_limited": "För många försök under för kort tid. Vänta ett tag innan du försöker igen.",
|
||||
"error_request_cancelled": "Förfrågan avbröts.",
|
||||
"error_request_declined": "Förfrågan nekades på den andra enheten.",
|
||||
"error_unexpected": "Ett oväntade fel inträffade.",
|
||||
"scan_code_instruction": "Skanna QR-koden nedan med din andra enhet som är utloggad.",
|
||||
"scan_qr_code": "Skanna QR-kod",
|
||||
@@ -1445,18 +1437,9 @@
|
||||
"rust_crypto_optin_warning": "Byte till Rust-kryptografi kräver en migreringsprocess som kan ta flera minuter. För att inaktivera måste du logga ut och in igen; använd med försiktighet!",
|
||||
"rust_crypto_requires_logout": "När Rust-kryptografi har aktiverats kan den endast avaktiveras genom att logga ut och logga in igen",
|
||||
"sliding_sync": "Sliding sync-läge",
|
||||
"sliding_sync_checking": "Kontrollerar …",
|
||||
"sliding_sync_configuration": "Sliding sync-konfiguration",
|
||||
"sliding_sync_description": "Under aktiv utveckling, kan inte inaktiveras.",
|
||||
"sliding_sync_disable_warning": "För att inaktivera det här så behöver du logga ut och logga in igen, använd varsamt!",
|
||||
"sliding_sync_disabled_notice": "Logga ut och in igen för att inaktivera",
|
||||
"sliding_sync_proxy_url_label": "Proxy-URL",
|
||||
"sliding_sync_proxy_url_optional_label": "Proxy-URL (valfritt)",
|
||||
"sliding_sync_server_no_support": "Din server saknar nativt stöd",
|
||||
"sliding_sync_server_specify_proxy": "Din server saknar nativt stöd, du måste ange en proxy",
|
||||
"sliding_sync_server_support": "Din server har nativt stöd",
|
||||
"threads_activity_centre": "Aktivitetscenter för trådar (under utveckling). För närvarande tar detta bara bort antalet trådaviseringar från det totala antalet i rumslistan",
|
||||
"threads_activity_centre_description": "Varning: Under aktiv utveckling; laddar om Element.",
|
||||
"under_active_development": "Under aktiv utveckling.",
|
||||
"unrealiable_e2e": "Otillförlitlig i krypterade rum",
|
||||
"video_rooms": "Videorum",
|
||||
|
@@ -241,15 +241,7 @@
|
||||
"completing_setup": "Завершення налаштування нового пристрою",
|
||||
"confirm_code_match": "Перевірте, чи збігається наведений внизу код з кодом на вашому іншому пристрої:",
|
||||
"connecting": "З'єднання…",
|
||||
"error_device_already_signed_in": "На іншому пристрої вхід було виконано.",
|
||||
"error_device_not_signed_in": "На іншому пристрої вхід не виконано.",
|
||||
"error_device_unsupported": "Зв'язок з цим пристроєм не підтримується.",
|
||||
"error_homeserver_lacks_support": "Домашній сервер не підтримує вхід на іншому пристрої.",
|
||||
"error_invalid_scanned_code": "Сканований код недійсний.",
|
||||
"error_linking_incomplete": "У встановлені терміни з'єднання не було виконано.",
|
||||
"error_rate_limited": "Забагато спроб за короткий час. Зачекайте трохи, перш ніж повторити спробу.",
|
||||
"error_request_cancelled": "Запит було скасовано.",
|
||||
"error_request_declined": "На іншому пристрої запит відхилено.",
|
||||
"error_unexpected": "Виникла непередбачувана помилка.",
|
||||
"scan_code_instruction": "Скануйте QR-код знизу своїм пристроєм, на якому ви вийшли.",
|
||||
"scan_qr_code": "Скануйте QR-код",
|
||||
@@ -1396,16 +1388,9 @@
|
||||
"report_to_moderators_description": "У кімнатах, які підтримують модерацію, кнопка «Поскаржитися» дає змогу повідомити про зловживання модераторам кімнати.",
|
||||
"rust_crypto": "Реалізація криптографії Rust",
|
||||
"sliding_sync": "Режим ковзної синхронізації",
|
||||
"sliding_sync_checking": "Перевірка…",
|
||||
"sliding_sync_configuration": "Конфігурація ковзної синхронізації",
|
||||
"sliding_sync_description": "На стадії активної розробки, вимкнути не можна.",
|
||||
"sliding_sync_disable_warning": "Для вимкнення потрібно буде вийти з системи та зайти знову, користуйтеся з обережністю!",
|
||||
"sliding_sync_disabled_notice": "Вийдіть і знову увійдіть, щоб вимкнути",
|
||||
"sliding_sync_proxy_url_label": "URL-адреса проксі-сервера",
|
||||
"sliding_sync_proxy_url_optional_label": "URL-адреса проксі-сервера (необов'язково)",
|
||||
"sliding_sync_server_no_support": "На вашому сервері немає вбудованої підтримки",
|
||||
"sliding_sync_server_specify_proxy": "На вашому сервері немає вбудованої підтримки, ви повинні вказати проксі",
|
||||
"sliding_sync_server_support": "Ваш сервер має вбудовану підтримку",
|
||||
"under_active_development": "У стадії активної розробки.",
|
||||
"video_rooms": "Відеокімнати",
|
||||
"video_rooms_a_new_way_to_chat": "Новий спосіб спілкування за допомогою голосового та відеозв’язку в %(brand)s.",
|
||||
|
@@ -229,9 +229,6 @@
|
||||
"password_field_weak_label": "Mật khẩu được phép, nhưng không an toàn",
|
||||
"phone_label": "Điện thoại",
|
||||
"phone_optional_label": "Điện thoại (tùy chọn)",
|
||||
"qr_code_login": {
|
||||
"error_invalid_scanned_code": "Mã vừa quét là không hợp lệ."
|
||||
},
|
||||
"register_action": "Tạo tài khoản",
|
||||
"registration": {
|
||||
"continue_without_email_description": "Lưu ý là nếu bạn không thêm địa chỉ thư điện tử và quên mật khẩu, bạn có thể <b>vĩnh viễn mất quyền truy cập vào tài khoản của mình</b>.",
|
||||
@@ -1291,10 +1288,7 @@
|
||||
"sliding_sync": "Chế độ đồng bộ tối ưu (Sync v3)",
|
||||
"sliding_sync_description": "Đang được phát triển tích cực, không thể vô hiệu.",
|
||||
"sliding_sync_disabled_notice": "Đăng xuất và đăng nhập lại để vô hiệu hóa",
|
||||
"sliding_sync_proxy_url_label": "Đường dẫn máy chủ ủy nhiệm (proxy)",
|
||||
"sliding_sync_server_no_support": "Máy chủ của bạn không hoàn toàn hỗ trợ",
|
||||
"sliding_sync_server_specify_proxy": "Máy chủ của bạn không hỗ trợ, bạn cần chỉ định máy chủ ủy nhiệm (proxy)",
|
||||
"sliding_sync_server_support": "Máy chủ của bạn hoàn toàn hỗ trợ",
|
||||
"under_active_development": "Đang được phát triển tích cực.",
|
||||
"video_rooms": "Phòng video",
|
||||
"video_rooms_a_new_way_to_chat": "Một cách mới để trò chuyện bằng thoại và video trong %(brand)s.",
|
||||
|
@@ -243,8 +243,7 @@
|
||||
"approve_access_warning": "为此设备批准访问权限后,它对你的帐户有完全的访问权限。",
|
||||
"completing_setup": "完成新设备的设置",
|
||||
"confirm_code_match": "检查以下代码是否与你的其他设备匹配:",
|
||||
"connecting": "正在连接……",
|
||||
"error_homeserver_lacks_support": "此服务器不支持多设备登录"
|
||||
"connecting": "正在连接……"
|
||||
},
|
||||
"register_action": "创建账户",
|
||||
"registration": {
|
||||
@@ -1309,12 +1308,7 @@
|
||||
"rust_crypto": "Rust加密实现",
|
||||
"sliding_sync": "滑动同步模式",
|
||||
"sliding_sync_description": "正在积极开发中,不能禁用。",
|
||||
"sliding_sync_disable_warning": "要停用,你必须登出并重新登录,请小心!",
|
||||
"sliding_sync_proxy_url_label": "代理URL",
|
||||
"sliding_sync_proxy_url_optional_label": "代理URL(可选)",
|
||||
"sliding_sync_server_no_support": "你的服务器缺少原生支持",
|
||||
"sliding_sync_server_specify_proxy": "你的服务器缺少原生支持,你必须指定代理",
|
||||
"sliding_sync_server_support": "你的服务器有原生支持",
|
||||
"under_active_development": "积极开发中。",
|
||||
"video_rooms": "视频房间",
|
||||
"video_rooms_a_new_way_to_chat": "在 %(brand)s 中使用语音和视频的新方式。",
|
||||
|
@@ -241,15 +241,7 @@
|
||||
"completing_setup": "完成您新裝置的設定",
|
||||
"confirm_code_match": "請確認下列代碼與您另一台裝置上的代碼相符:",
|
||||
"connecting": "連線中…",
|
||||
"error_device_already_signed_in": "其他裝置已登入。",
|
||||
"error_device_not_signed_in": "其他裝置未登入。",
|
||||
"error_device_unsupported": "不支援與此裝置連結。",
|
||||
"error_homeserver_lacks_support": "家伺服器不支援在其他裝置上登入。",
|
||||
"error_invalid_scanned_code": "掃描的代碼無效。",
|
||||
"error_linking_incomplete": "未在要求的時間內完成連結。",
|
||||
"error_rate_limited": "短時間內嘗試太多次,請稍待一段時間後再嘗試。",
|
||||
"error_request_cancelled": "請求已取消。",
|
||||
"error_request_declined": "請求在另一台裝置上被拒絕。",
|
||||
"error_unexpected": "發生預料之外的錯誤。",
|
||||
"scan_code_instruction": "請用您已登出的裝置掃描下列 QR Code。",
|
||||
"scan_qr_code": "掃描 QR Code",
|
||||
@@ -1399,16 +1391,9 @@
|
||||
"report_to_moderators_description": "在支援審核的聊天室中,「回報」按鈕讓您可以回報濫用行為給聊天室管理員。",
|
||||
"rust_crypto": "Rust 密碼學實作",
|
||||
"sliding_sync": "滑動同步模式",
|
||||
"sliding_sync_checking": "正在檢查…",
|
||||
"sliding_sync_configuration": "滑動同步設定",
|
||||
"sliding_sync_description": "正在積極開發中,無法停用。",
|
||||
"sliding_sync_disable_warning": "要停用,您必須登出並重新登入,請小心使用!",
|
||||
"sliding_sync_disabled_notice": "登出並重新登入以停用",
|
||||
"sliding_sync_proxy_url_label": "代理伺服器網址",
|
||||
"sliding_sync_proxy_url_optional_label": "代理伺服器網址(選填)",
|
||||
"sliding_sync_server_no_support": "您的伺服器缺乏原生支援",
|
||||
"sliding_sync_server_specify_proxy": "您的伺服器缺乏原生支援,您必須指定代理",
|
||||
"sliding_sync_server_support": "您的伺服器有原生支援",
|
||||
"under_active_development": "正在積極開發中。",
|
||||
"video_rooms": "視訊聊天室",
|
||||
"video_rooms_a_new_way_to_chat": "在 %(brand)s 中透過語音及視訊聊天的新方式。",
|
||||
|
@@ -47,6 +47,7 @@ import ServerSupportUnstableFeatureController from "./controllers/ServerSupportU
|
||||
import { WatchManager } from "./WatchManager";
|
||||
import { CustomTheme } from "../theme";
|
||||
import SettingsStore from "./SettingsStore";
|
||||
import AnalyticsController from "./controllers/AnalyticsController";
|
||||
|
||||
export const defaultWatchManager = new WatchManager();
|
||||
|
||||
@@ -406,7 +407,7 @@ export const SETTINGS: { [setting: string]: ISetting } = {
|
||||
controller: new SlidingSyncController(),
|
||||
},
|
||||
"feature_sliding_sync_proxy_url": {
|
||||
// This is not a distinct feature, it is a setting for feature_sliding_sync above
|
||||
// This is not a distinct feature, it is a legacy setting for feature_sliding_sync above
|
||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
|
||||
default: "",
|
||||
},
|
||||
@@ -597,11 +598,13 @@ export const SETTINGS: { [setting: string]: ISetting } = {
|
||||
displayName: _td("settings|showbold"),
|
||||
default: false,
|
||||
invertedSettingName: "feature_hidebold",
|
||||
controller: new AnalyticsController("WebSettingsNotificationsShowBoldToggle"),
|
||||
},
|
||||
"Notifications.tac_only_notifications": {
|
||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
|
||||
displayName: _td("settings|tac_only_notifications"),
|
||||
default: true,
|
||||
controller: new AnalyticsController("WebSettingsNotificationsTACOnlyNotificationsToggle"),
|
||||
},
|
||||
"feature_ask_to_join": {
|
||||
isFeature: true,
|
||||
@@ -1148,15 +1151,6 @@ export const SETTINGS: { [setting: string]: ISetting } = {
|
||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
||||
default: [],
|
||||
},
|
||||
"threadsActivityCentre": {
|
||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
labsGroup: LabGroup.Threads,
|
||||
controller: new ReloadOnChangeController(),
|
||||
displayName: _td("labs|threads_activity_centre"),
|
||||
description: () => _t("labs|threads_activity_centre_description", { brand: SdkConfig.get().brand }),
|
||||
default: false,
|
||||
isFeature: true,
|
||||
},
|
||||
/**
|
||||
* Enable or disable the release announcement feature
|
||||
*/
|
||||
|
42
src/settings/controllers/AnalyticsController.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import SettingController from "./SettingController";
|
||||
import { SettingLevel } from "../SettingLevel";
|
||||
import PosthogTrackers, { InteractionName } from "../../PosthogTrackers";
|
||||
|
||||
/**
|
||||
* Controller that sends events to analytics when a setting is changed.
|
||||
* Since it will only trigger events when the setting is changed,
|
||||
* (and the value isn't reported: only the fact that it's been toggled)
|
||||
* it won't be useful for tracking what percentage of a userbase has a given setting
|
||||
* enabled, but many of our settings can be set per device and Posthog only supports
|
||||
* per-user properties, so this isn't straightforward. This is only for seeing how
|
||||
* often people interact with the settings.
|
||||
*/
|
||||
export default class AnalyticsController extends SettingController {
|
||||
/**
|
||||
*
|
||||
* @param interactionName The name of the event to send to analytics
|
||||
*/
|
||||
public constructor(private interactionName: InteractionName) {
|
||||
super();
|
||||
}
|
||||
|
||||
public onChange(_level: SettingLevel, _roomId: string | null, _newValue: any): void {
|
||||
PosthogTrackers.trackInteraction(this.interactionName);
|
||||
}
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2024 Ed Geraghty <ed@geraghty.family>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -16,18 +17,11 @@ limitations under the License.
|
||||
|
||||
import SettingController from "./SettingController";
|
||||
import PlatformPeg from "../../PlatformPeg";
|
||||
import { SettingLevel } from "../SettingLevel";
|
||||
import { SlidingSyncOptionsDialog } from "../../components/views/dialogs/SlidingSyncOptionsDialog";
|
||||
import Modal from "../../Modal";
|
||||
import SettingsStore from "../SettingsStore";
|
||||
import { _t } from "../../languageHandler";
|
||||
|
||||
export default class SlidingSyncController extends SettingController {
|
||||
public async beforeChange(level: SettingLevel, roomId: string, newValue: any): Promise<boolean> {
|
||||
const { finished } = Modal.createDialog(SlidingSyncOptionsDialog);
|
||||
const [value] = await finished;
|
||||
return newValue === value; // abort the operation if we're already in the state the user chose via modal
|
||||
}
|
||||
public static serverSupportsSlidingSync: boolean;
|
||||
|
||||
public async onChange(): Promise<void> {
|
||||
PlatformPeg.get()?.reload();
|
||||
@@ -38,6 +32,9 @@ export default class SlidingSyncController extends SettingController {
|
||||
if (SettingsStore.getValue("feature_sliding_sync")) {
|
||||
return _t("labs|sliding_sync_disabled_notice");
|
||||
}
|
||||
if (!SlidingSyncController.serverSupportsSlidingSync) {
|
||||
return _t("labs|sliding_sync_server_no_support");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
@@ -42,8 +42,6 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
|
||||
private listMap = new Map<TagID, ListNotificationState>();
|
||||
private _globalState = new SummarizedNotificationState();
|
||||
|
||||
private tacEnabled = SettingsStore.getValue("threadsActivityCentre");
|
||||
|
||||
private constructor(dispatcher = defaultDispatcher) {
|
||||
super(dispatcher, {});
|
||||
SettingsStore.watchSetting("feature_dynamic_room_predecessors", null, () => {
|
||||
@@ -99,7 +97,7 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
|
||||
*/
|
||||
public getRoomState(room: Room): RoomNotificationState {
|
||||
if (!this.roomMap.has(room)) {
|
||||
this.roomMap.set(room, new RoomNotificationState(room, !this.tacEnabled));
|
||||
this.roomMap.set(room, new RoomNotificationState(room, false));
|
||||
}
|
||||
return this.roomMap.get(room)!;
|
||||
}
|
||||
|
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Room, RelationType, MatrixEvent, Thread, M_POLL_START } from "matrix-js-sdk/src/matrix";
|
||||
import { Room, RelationType, MatrixEvent, Thread, M_POLL_START, RoomEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import { ActionPayload } from "../../dispatcher/payloads";
|
||||
@@ -186,7 +186,7 @@ export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
|
||||
}
|
||||
|
||||
private async generatePreview(room: Room, tagId?: TagID): Promise<void> {
|
||||
const events = [...room.getLiveTimeline().getEvents()];
|
||||
const events = [...room.getLiveTimeline().getEvents(), ...room.getPendingEvents()];
|
||||
|
||||
// add last reply from each thread
|
||||
room.getThreads().forEach((thread: Thread): void => {
|
||||
@@ -279,4 +279,19 @@ export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
|
||||
await this.generatePreview(room, TAG_ANY);
|
||||
}
|
||||
}
|
||||
|
||||
protected async onReady(): Promise<void> {
|
||||
if (!this.matrixClient) return;
|
||||
this.matrixClient.on(RoomEvent.LocalEchoUpdated, this.onLocalEchoUpdated);
|
||||
}
|
||||
|
||||
protected async onNotReady(): Promise<void> {
|
||||
if (!this.matrixClient) return;
|
||||
this.matrixClient.off(RoomEvent.LocalEchoUpdated, this.onLocalEchoUpdated);
|
||||
}
|
||||
|
||||
protected onLocalEchoUpdated = async (ev: MatrixEvent, room: Room): Promise<void> => {
|
||||
if (!this.previews.has(room.roomId)) return;
|
||||
await this.generatePreview(room, TAG_ANY);
|
||||
};
|
||||
}
|
||||
|
@@ -388,4 +388,60 @@ describe("DecryptionFailureTracker", function () {
|
||||
// should track remapped error code
|
||||
expect(counts["XEDNI_EGASSEM_NWONKNU_MLO"]).toBe(1);
|
||||
});
|
||||
|
||||
it("default error code mapper maps error codes correctly", async () => {
|
||||
const errorCodes: string[] = [];
|
||||
|
||||
// @ts-ignore access to private constructor
|
||||
const tracker = new DecryptionFailureTracker(
|
||||
(total: number, errorCode: string) => {
|
||||
errorCodes.push(errorCode);
|
||||
},
|
||||
// @ts-ignore access to private member
|
||||
DecryptionFailureTracker.instance.errorCodeMapFn,
|
||||
);
|
||||
|
||||
const event1 = await createFailedDecryptionEvent(DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID);
|
||||
tracker.addVisibleEvent(event1);
|
||||
tracker.eventDecrypted(event1);
|
||||
|
||||
const event2 = await createFailedDecryptionEvent(DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX);
|
||||
tracker.addVisibleEvent(event2);
|
||||
tracker.eventDecrypted(event2);
|
||||
|
||||
const event3 = await createFailedDecryptionEvent(DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP);
|
||||
tracker.addVisibleEvent(event3);
|
||||
tracker.eventDecrypted(event3);
|
||||
|
||||
const event4 = await createFailedDecryptionEvent(DecryptionFailureCode.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED);
|
||||
tracker.addVisibleEvent(event4);
|
||||
tracker.eventDecrypted(event4);
|
||||
|
||||
const event5 = await createFailedDecryptionEvent(DecryptionFailureCode.HISTORICAL_MESSAGE_WORKING_BACKUP);
|
||||
tracker.addVisibleEvent(event5);
|
||||
tracker.eventDecrypted(event5);
|
||||
|
||||
const event6 = await createFailedDecryptionEvent(DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED);
|
||||
tracker.addVisibleEvent(event6);
|
||||
tracker.eventDecrypted(event6);
|
||||
|
||||
const event7 = await createFailedDecryptionEvent(DecryptionFailureCode.UNKNOWN_ERROR);
|
||||
tracker.addVisibleEvent(event7);
|
||||
tracker.eventDecrypted(event7);
|
||||
|
||||
// Pretend "now" is Infinity
|
||||
tracker.checkFailures(Infinity);
|
||||
|
||||
tracker.trackFailures();
|
||||
|
||||
expect(errorCodes).toEqual([
|
||||
"OlmKeysNotSentError",
|
||||
"OlmIndexError",
|
||||
"HistoricalMessage",
|
||||
"HistoricalMessage",
|
||||
"HistoricalMessage",
|
||||
"ExpectedDueToMembership",
|
||||
"UnknownError",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
@@ -36,7 +36,7 @@ const getFakePosthog = (): PostHog =>
|
||||
register: jest.fn(),
|
||||
get_distinct_id: jest.fn(),
|
||||
persistence: {
|
||||
get_user_state: jest.fn(),
|
||||
get_property: jest.fn(),
|
||||
},
|
||||
identifyUser: jest.fn(),
|
||||
}) as unknown as PostHog;
|
||||
|
@@ -20,6 +20,8 @@ import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { SlidingSyncManager } from "../src/SlidingSyncManager";
|
||||
import { stubClient } from "./test-utils";
|
||||
import SlidingSyncController from "../src/settings/controllers/SlidingSyncController";
|
||||
import SettingsStore from "../src/settings/SettingsStore";
|
||||
|
||||
jest.mock("matrix-js-sdk/src/sliding-sync");
|
||||
const MockSlidingSync = <jest.Mock<SlidingSync>>(<unknown>SlidingSync);
|
||||
@@ -231,4 +233,53 @@ describe("SlidingSyncManager", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
describe("checkSupport", () => {
|
||||
beforeEach(() => {
|
||||
SlidingSyncController.serverSupportsSlidingSync = false;
|
||||
jest.spyOn(manager, "getProxyFromWellKnown").mockResolvedValue("proxy");
|
||||
});
|
||||
it("shorts out if the server has 'native' sliding sync support", async () => {
|
||||
jest.spyOn(manager, "nativeSlidingSyncSupport").mockResolvedValue(true);
|
||||
expect(SlidingSyncController.serverSupportsSlidingSync).toBeFalsy();
|
||||
await manager.checkSupport(client);
|
||||
expect(manager.getProxyFromWellKnown).not.toHaveBeenCalled(); // We return earlier
|
||||
expect(SlidingSyncController.serverSupportsSlidingSync).toBeTruthy();
|
||||
});
|
||||
it("tries to find a sliding sync proxy url from the client well-known if there's no 'native' support", async () => {
|
||||
jest.spyOn(manager, "nativeSlidingSyncSupport").mockResolvedValue(false);
|
||||
expect(SlidingSyncController.serverSupportsSlidingSync).toBeFalsy();
|
||||
await manager.checkSupport(client);
|
||||
expect(manager.getProxyFromWellKnown).toHaveBeenCalled();
|
||||
expect(SlidingSyncController.serverSupportsSlidingSync).toBeTruthy();
|
||||
});
|
||||
});
|
||||
describe("setup", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(manager, "configure");
|
||||
jest.spyOn(manager, "startSpidering");
|
||||
});
|
||||
it("uses the baseUrl as a proxy if no proxy is set in the client well-known and the server has no native support", async () => {
|
||||
await manager.setup(client);
|
||||
expect(manager.configure).toHaveBeenCalled();
|
||||
expect(manager.configure).toHaveBeenCalledWith(client, client.baseUrl);
|
||||
expect(manager.startSpidering).toHaveBeenCalled();
|
||||
});
|
||||
it("uses the proxy declared in the client well-known", async () => {
|
||||
jest.spyOn(manager, "getProxyFromWellKnown").mockResolvedValue("proxy");
|
||||
await manager.setup(client);
|
||||
expect(manager.configure).toHaveBeenCalled();
|
||||
expect(manager.configure).toHaveBeenCalledWith(client, "proxy");
|
||||
expect(manager.startSpidering).toHaveBeenCalled();
|
||||
});
|
||||
it("uses the legacy `feature_sliding_sync_proxy_url` if it was set", async () => {
|
||||
jest.spyOn(manager, "getProxyFromWellKnown").mockResolvedValue("proxy");
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => {
|
||||
if (name === "feature_sliding_sync_proxy_url") return "legacy-proxy";
|
||||
});
|
||||
await manager.setup(client);
|
||||
expect(manager.configure).toHaveBeenCalled();
|
||||
expect(manager.configure).toHaveBeenCalledWith(client, "legacy-proxy");
|
||||
expect(manager.startSpidering).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -61,6 +61,7 @@ import SettingsStore from "../../../src/settings/SettingsStore";
|
||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||
import { MatrixClientPeg as peg } from "../../../src/MatrixClientPeg";
|
||||
import DMRoomMap from "../../../src/utils/DMRoomMap";
|
||||
import { ReleaseAnnouncementStore } from "../../../src/stores/ReleaseAnnouncementStore";
|
||||
|
||||
jest.mock("matrix-js-sdk/src/oidc/authorize", () => ({
|
||||
completeAuthorizationCodeGrant: jest.fn(),
|
||||
@@ -627,6 +628,12 @@ describe("<MatrixChat />", () => {
|
||||
(id) => [room, spaceRoom].find((room) => room.roomId === id) || null,
|
||||
);
|
||||
jest.spyOn(spaceRoom, "isSpaceRoom").mockReturnValue(true);
|
||||
|
||||
jest.spyOn(ReleaseAnnouncementStore.instance, "getReleaseAnnouncement").mockReturnValue(null);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("leave_room", () => {
|
||||
|
@@ -397,6 +397,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
|
||||
<button
|
||||
aria-label="Bold"
|
||||
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconBold"
|
||||
data-state="closed"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
@@ -404,6 +405,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
|
||||
<button
|
||||
aria-label="Italics"
|
||||
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconItalic"
|
||||
data-state="closed"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
@@ -411,6 +413,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
|
||||
<button
|
||||
aria-label="Strikethrough"
|
||||
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconStrikethrough"
|
||||
data-state="closed"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
@@ -418,6 +421,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
|
||||
<button
|
||||
aria-label="Code block"
|
||||
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconCode"
|
||||
data-state="closed"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
@@ -425,6 +429,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
|
||||
<button
|
||||
aria-label="Quote"
|
||||
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconQuote"
|
||||
data-state="closed"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
@@ -432,6 +437,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
|
||||
<button
|
||||
aria-label="Insert link"
|
||||
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconInsertLink"
|
||||
data-state="closed"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
@@ -644,6 +650,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
|
||||
<button
|
||||
aria-label="Bold"
|
||||
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconBold"
|
||||
data-state="closed"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
@@ -651,6 +658,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
|
||||
<button
|
||||
aria-label="Italics"
|
||||
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconItalic"
|
||||
data-state="closed"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
@@ -658,6 +666,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
|
||||
<button
|
||||
aria-label="Strikethrough"
|
||||
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconStrikethrough"
|
||||
data-state="closed"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
@@ -665,6 +674,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
|
||||
<button
|
||||
aria-label="Code block"
|
||||
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconCode"
|
||||
data-state="closed"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
@@ -672,6 +682,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
|
||||
<button
|
||||
aria-label="Quote"
|
||||
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconQuote"
|
||||
data-state="closed"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
@@ -679,6 +690,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
|
||||
<button
|
||||
aria-label="Insert link"
|
||||
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconInsertLink"
|
||||
data-state="closed"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
|
@@ -12,7 +12,6 @@ exports[`<UserMenu> <UserMenu> when video broadcast when rendered should render
|
||||
class="mx_AccessibleButton mx_UserMenu_contextMenuButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
title="User menu"
|
||||
>
|
||||
<div
|
||||
class="mx_UserMenu_userAvatar"
|
||||
|
@@ -48,6 +48,7 @@ exports[`<BeaconListItem /> when a beacon is live and has locations renders beac
|
||||
<div
|
||||
aria-label="Copy"
|
||||
class="mx_AccessibleButton mx_CopyableText_copyButton"
|
||||
data-state="closed"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
|
@@ -14,11 +14,12 @@ exports[`<DialogSidebar /> renders sidebar correctly with beacons 1`] = `
|
||||
View list
|
||||
</h4>
|
||||
<div
|
||||
aria-label="Close sidebar"
|
||||
class="mx_AccessibleButton mx_DialogSidebar_closeButton"
|
||||
data-state="closed"
|
||||
data-testid="dialog-sidebar-close"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
title="Close sidebar"
|
||||
>
|
||||
<div
|
||||
class="mx_DialogSidebar_closeButtonIcon"
|
||||
@@ -81,6 +82,7 @@ exports[`<DialogSidebar /> renders sidebar correctly with beacons 1`] = `
|
||||
<div
|
||||
aria-label="Copy"
|
||||
class="mx_AccessibleButton mx_CopyableText_copyButton"
|
||||
data-state="closed"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
@@ -113,11 +115,12 @@ exports[`<DialogSidebar /> renders sidebar correctly without beacons 1`] = `
|
||||
View list
|
||||
</h4>
|
||||
<div
|
||||
aria-label="Close sidebar"
|
||||
class="mx_AccessibleButton mx_DialogSidebar_closeButton"
|
||||
data-state="closed"
|
||||
data-testid="dialog-sidebar-close"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
title="Close sidebar"
|
||||
>
|
||||
<div
|
||||
class="mx_DialogSidebar_closeButtonIcon"
|
||||
|
@@ -3,10 +3,11 @@
|
||||
exports[`<LeftPanelLiveShareWarning /> when user has live location monitor renders correctly when minimized 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
aria-label="You are sharing your live location"
|
||||
class="mx_AccessibleButton mx_LeftPanelLiveShareWarning mx_LeftPanelLiveShareWarning__minimized"
|
||||
data-state="closed"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
title="You are sharing your live location"
|
||||
>
|
||||
<div
|
||||
height="10"
|
||||
|
@@ -109,11 +109,12 @@ exports[`<RoomLiveShareWarning /> when user has live beacons and geolocation is
|
||||
Retry
|
||||
</button>
|
||||
<button
|
||||
aria-label="Stop and close"
|
||||
class="mx_AccessibleButton mx_RoomLiveShareWarning_closeButton"
|
||||
data-state="closed"
|
||||
data-testid="room-live-share-wire-error-close-button"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
title="Stop and close"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomLiveShareWarning_closeButtonIcon"
|
||||
|
@@ -19,6 +19,7 @@ exports[`<ShareLatestLocation /> renders share buttons when there is a location
|
||||
<div
|
||||
aria-label="Copy"
|
||||
class="mx_AccessibleButton mx_CopyableText_copyButton"
|
||||
data-state="closed"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
|