diff --git a/.eslintrc-module_system.js b/.eslintrc-module_system.js deleted file mode 100644 index d56087e5221..00000000000 --- a/.eslintrc-module_system.js +++ /dev/null @@ -1,60 +0,0 @@ -module.exports = { - plugins: ["matrix-org"], - extends: ["./.eslintrc.js"], - parserOptions: { - project: ["./tsconfig.module_system.json"], - }, - overrides: [ - { - files: ["module_system/**/*.{ts,tsx}"], - extends: ["plugin:matrix-org/typescript", "plugin:matrix-org/react"], - // NOTE: These rules are frozen and new rules should not be added here. - // New changes belong in https://github.com/matrix-org/eslint-plugin-matrix-org/ - rules: { - // Things we do that break the ideal style - "prefer-promise-reject-errors": "off", - "quotes": "off", - - // We disable this while we're transitioning - "@typescript-eslint/no-explicit-any": "off", - // We're okay with assertion errors when we ask for them - "@typescript-eslint/no-non-null-assertion": "off", - - // Ban matrix-js-sdk/src imports in favour of matrix-js-sdk/src/matrix imports to prevent unleashing hell. - "no-restricted-imports": [ - "error", - { - paths: [ - { - name: "matrix-js-sdk", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/index", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - ], - patterns: [ - { - group: ["matrix-js-sdk/lib", "matrix-js-sdk/lib/", "matrix-js-sdk/lib/**"], - message: "Please use matrix-js-sdk/src/* instead", - }, - ], - }, - ], - }, - }, - ], -}; diff --git a/.eslintrc.js b/.eslintrc.js index 8dbfb67c311..f168a87a066 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -262,6 +262,63 @@ module.exports = { parserOptions: { project: ["./playwright/tsconfig.json"], }, + rules: { + "react-hooks/rules-of-hooks": ["off"], + }, + }, + { + files: ["module_system/**/*.{ts,tsx}"], + parserOptions: { + project: ["./tsconfig.module_system.json"], + }, + extends: ["plugin:matrix-org/typescript", "plugin:matrix-org/react"], + // NOTE: These rules are frozen and new rules should not be added here. + // New changes belong in https://github.com/matrix-org/eslint-plugin-matrix-org/ + rules: { + // Things we do that break the ideal style + "prefer-promise-reject-errors": "off", + "quotes": "off", + + // We disable this while we're transitioning + "@typescript-eslint/no-explicit-any": "off", + // We're okay with assertion errors when we ask for them + "@typescript-eslint/no-non-null-assertion": "off", + + // Ban matrix-js-sdk/src imports in favour of matrix-js-sdk/src/matrix imports to prevent unleashing hell. + "no-restricted-imports": [ + "error", + { + paths: [ + { + name: "matrix-js-sdk", + message: "Please use matrix-js-sdk/src/matrix instead", + }, + { + name: "matrix-js-sdk/", + message: "Please use matrix-js-sdk/src/matrix instead", + }, + { + name: "matrix-js-sdk/src", + message: "Please use matrix-js-sdk/src/matrix instead", + }, + { + name: "matrix-js-sdk/src/", + message: "Please use matrix-js-sdk/src/matrix instead", + }, + { + name: "matrix-js-sdk/src/index", + message: "Please use matrix-js-sdk/src/matrix instead", + }, + ], + patterns: [ + { + group: ["matrix-js-sdk/lib", "matrix-js-sdk/lib/", "matrix-js-sdk/lib/**"], + message: "Please use matrix-js-sdk/src/* instead", + }, + ], + }, + ], + }, }, ], settings: { diff --git a/.github/workflows/dockerhub.yaml b/.github/workflows/dockerhub.yaml index cdd50e0bcc4..65457ab8f94 100644 --- a/.github/workflows/dockerhub.yaml +++ b/.github/workflows/dockerhub.yaml @@ -21,13 +21,13 @@ jobs: fetch-depth: 0 # needed for docker-package to be able to calculate the version - name: Install Cosign - uses: sigstore/cosign-installer@4959ce089c160fddf62f7b42464195ba1a56d382 # v3 + uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3 - name: Set up QEMU uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3 + uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3 with: install: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0e8c21e7869..3a9c29e1976 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,7 +49,7 @@ jobs: ref: master repo-token: ${{ secrets.GITHUB_TOKEN }} wait-interval: 10 - check-name: "Docker Buildx (vanilla)" + check-name: "Docker Buildx" allowed-conclusions: success - name: Wait for debian package diff --git a/.github/workflows/release_prepare.yml b/.github/workflows/release_prepare.yml index ce088a93276..5fb969a1c67 100644 --- a/.github/workflows/release_prepare.yml +++ b/.github/workflows/release_prepare.yml @@ -20,6 +20,9 @@ on: jobs: prepare: runs-on: ubuntu-24.04 + env: + # The order is specified bottom-up to avoid any races for allchange + REPOS: matrix-js-sdk element-web element-desktop steps: - name: Checkout Element Desktop uses: actions/checkout@v4 diff --git a/.github/workflows/static_analysis.yaml b/.github/workflows/static_analysis.yaml index 536f78e18dd..3eecea9d4de 100644 --- a/.github/workflows/static_analysis.yaml +++ b/.github/workflows/static_analysis.yaml @@ -34,27 +34,6 @@ jobs: - name: Typecheck run: "yarn run lint:types" - - name: Switch js-sdk to release mode - working-directory: node_modules/matrix-js-sdk - run: | - scripts/switch_package_to_release.cjs - yarn install - yarn run build:compile - yarn run build:types - - - name: Typecheck (release mode) - run: "yarn run lint:types" - - # Temporary while we directly import matrix-js-sdk/src/* which means we need - # certain @types/* packages to make sense of matrix-js-sdk types. - #- name: Typecheck (release mode; no yarn link) - # if: github.event_name != 'pull_request' && github.ref_name != 'master' - # run: | - # yarn unlink matrix-js-sdk - # yarn add github:matrix-org/matrix-js-sdk#develop - # yarn install --force - # yarn run lint:types - i18n_lint: name: "i18n Check" uses: matrix-org/matrix-web-i18n/.github/workflows/i18n_check.yml@main @@ -144,6 +123,12 @@ jobs: cache: "yarn" node-version: "lts/*" + - name: Install Deps + run: "yarn install --frozen-lockfile" + + - name: Run linter + run: "yarn run lint:knip" + - name: Install Deps run: "scripts/layered.sh" diff --git a/.lintstagedrc b/.lintstagedrc index c07ed8df5b7..6b93e89d5a7 100644 --- a/.lintstagedrc +++ b/.lintstagedrc @@ -2,6 +2,6 @@ "*": "prettier --write", "src/**/*.(ts|tsx)": ["eslint --fix"], "scripts/**/*.(ts|tsx)": ["eslint --fix"], - "module_system/**/*.(ts|tsx)": ["eslint --fix --config .eslintrc-module_system.js module_system"], + "module_system/**/*.(ts|tsx)": ["eslint --fix"], "*.pcss": ["stylelint --fix"] } diff --git a/.node-version b/.node-version index 209e3ef4b62..2bd5a0a98a3 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -20 +22 diff --git a/.stylelintrc.js b/.stylelintrc.js index 259c626deef..dc8ae6376bd 100644 --- a/.stylelintrc.js +++ b/.stylelintrc.js @@ -1,6 +1,6 @@ module.exports = { extends: ["stylelint-config-standard"], - customSyntax: require("postcss-scss"), + customSyntax: "postcss-scss", plugins: ["stylelint-scss"], rules: { "comment-empty-line-before": null, diff --git a/CHANGELOG.md b/CHANGELOG.md index ef18822d38b..6260a72f99d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,76 @@ +Changes in [1.11.85](https://github.com/element-hq/element-web/releases/tag/v1.11.85) (2024-11-12) +================================================================================================== +# Security +- Fixes for [CVE-2024-51750](https://www.cve.org/CVERecord?id=CVE-2024-51750) / [GHSA-w36j-v56h-q9pc](https://github.com/element-hq/element-web/security/advisories/GHSA-w36j-v56h-q9pc) +- Fixes for [CVE-2024-51749](https://www.cve.org/CVERecord?id=CVE-2024-51749) / [GHSA-5486-384g-mcx2](https://github.com/element-hq/element-web/security/advisories/GHSA-5486-384g-mcx2) +- Update JS SDK with the fixes for [CVE-2024-50336](https://www.cve.org/CVERecord?id=CVE-2024-50336) / [GHSA-xvg8-m4x3-w6xr](https://github.com/matrix-org/matrix-js-sdk/security/advisories/GHSA-xvg8-m4x3-w6xr) + + +Changes in [1.11.84](https://github.com/element-hq/element-web/releases/tag/v1.11.84) (2024-11-05) +================================================================================================== +## ✨ Features + +* Remove abandoned MSC3886, MSC3903, MSC3906 implementations ([#28274](https://github.com/element-hq/element-web/pull/28274)). Contributed by @t3chguy. +* Update to React 18 ([#24763](https://github.com/element-hq/element-web/pull/24763)). Contributed by @t3chguy. +* Deduplicate icons using Compound ([#28239](https://github.com/element-hq/element-web/pull/28239)). Contributed by @t3chguy. +* Replace legacy Tooltips with Compound tooltips ([#28231](https://github.com/element-hq/element-web/pull/28231)). Contributed by @t3chguy. +* Deduplicate icons using Compound Design Tokens ([#28219](https://github.com/element-hq/element-web/pull/28219)). Contributed by @t3chguy. +* Add reactions to html export ([#28210](https://github.com/element-hq/element-web/pull/28210)). Contributed by @langleyd. +* Remove feature\_dehydration ([#28173](https://github.com/element-hq/element-web/pull/28173)). Contributed by @florianduros. + +## 🐛 Bug Fixes + +* Remove upgrade encryption in `DeviceListener` and `SetupEncryptionToast` ([#28299](https://github.com/element-hq/element-web/pull/28299)). Contributed by @florianduros. +* Fix 'remove alias' button in room settings ([#28269](https://github.com/element-hq/element-web/pull/28269)). Contributed by @Dev-Gurjar. +* Add back unencrypted path in `StopGapWidgetDriver.sendToDevice` ([#28295](https://github.com/element-hq/element-web/pull/28295)). Contributed by @florianduros. +* Fix other devices not being decorated as such ([#28279](https://github.com/element-hq/element-web/pull/28279)). Contributed by @t3chguy. +* Fix pill contrast in invitation dialog ([#28250](https://github.com/element-hq/element-web/pull/28250)). Contributed by @florianduros. +* Close right panel chat when minimising maximised voip widget ([#28241](https://github.com/element-hq/element-web/pull/28241)). Contributed by @t3chguy. +* Fix develop changelog parsing ([#28232](https://github.com/element-hq/element-web/pull/28232)). Contributed by @t3chguy. +* Fix Ctrl+F shortcut not working with minimised room summary card ([#28223](https://github.com/element-hq/element-web/pull/28223)). Contributed by @t3chguy. +* Fix network dropdown missing checkbox \& aria-checked ([#28220](https://github.com/element-hq/element-web/pull/28220)). Contributed by @t3chguy. + + +Changes in [1.11.83](https://github.com/element-hq/element-web/releases/tag/v1.11.83) (2024-10-29) +================================================================================================== +## ✨ Features + +* Enable Element Call by default on release instances ([#28314](https://github.com/element-hq/element-web/pull/28314)). Contributed by @t3chguy. + + + +Changes in [1.11.82](https://github.com/element-hq/element-web/releases/tag/v1.11.82) (2024-10-22) +================================================================================================== +## ✨ Features + +* Deduplicate more icons using Compound Design Tokens ([#132](https://github.com/element-hq/matrix-react-sdk/pull/132)). Contributed by @t3chguy. +* Always show link new device flow even if unsupported ([#147](https://github.com/element-hq/matrix-react-sdk/pull/147)). Contributed by @t3chguy. +* Update design of files list in right panel ([#144](https://github.com/element-hq/matrix-react-sdk/pull/144)). Contributed by @t3chguy. +* Remove feature\_dehydration ([#138](https://github.com/element-hq/matrix-react-sdk/pull/138)). Contributed by @florianduros. +* Upgrade emojibase-bindings and remove local handling of emoticon variations ([#127](https://github.com/element-hq/matrix-react-sdk/pull/127)). Contributed by @langleyd. +* Add support for rendering media captions ([#43](https://github.com/element-hq/matrix-react-sdk/pull/43)). Contributed by @tulir. +* Replace composer icons with Compound variants ([#123](https://github.com/element-hq/matrix-react-sdk/pull/123)). Contributed by @t3chguy. +* Tweak default right panel size to be 320px except for maximised widgets at 420px ([#110](https://github.com/element-hq/matrix-react-sdk/pull/110)). Contributed by @t3chguy. +* Add a pinned message badge under a pinned message ([#118](https://github.com/element-hq/matrix-react-sdk/pull/118)). Contributed by @florianduros. +* Ditch right panel tabs and re-add close button ([#99](https://github.com/element-hq/matrix-react-sdk/pull/99)). Contributed by @t3chguy. +* Force verification even for refreshed clients ([#44](https://github.com/element-hq/matrix-react-sdk/pull/44)). Contributed by @dbkr. +* Update emoji text, border and background colour in timeline ([#119](https://github.com/element-hq/matrix-react-sdk/pull/119)). Contributed by @florianduros. +* Disable ICE fallback based on well-known configuration ([#111](https://github.com/element-hq/matrix-react-sdk/pull/111)). Contributed by @t3chguy. +* Remove legacy room header and promote beta room header ([#105](https://github.com/element-hq/matrix-react-sdk/pull/105)). Contributed by @t3chguy. +* Respect `io.element.jitsi` `useFor1To1Calls` in well-known ([#112](https://github.com/element-hq/matrix-react-sdk/pull/112)). Contributed by @t3chguy. +* Use Compound close icon in favour of mishmash of x/close icons ([#108](https://github.com/element-hq/matrix-react-sdk/pull/108)). Contributed by @t3chguy. + +## 🐛 Bug Fixes + +* Correct typo in option documentation ([#28148](https://github.com/element-hq/element-web/pull/28148)). Contributed by @AndrewKvalheim. +* Revert #124 and #135 ([#139](https://github.com/element-hq/matrix-react-sdk/pull/139)). Contributed by @dbkr. +* Add aria-label to e2e icon ([#136](https://github.com/element-hq/matrix-react-sdk/pull/136)). Contributed by @florianduros. +* Fix bell icons on room list hover being black squares ([#135](https://github.com/element-hq/matrix-react-sdk/pull/135)). Contributed by @dbkr. +* Fix vertical overflow on the mobile register screen ([#137](https://github.com/element-hq/matrix-react-sdk/pull/137)). Contributed by @langleyd. +* Allow to unpin redacted event ([#98](https://github.com/element-hq/matrix-react-sdk/pull/98)). Contributed by @florianduros. + + + Changes in [1.11.81](https://github.com/element-hq/element-web/releases/tag/v1.11.81) (2024-10-15) ================================================================================================== This release fixes High severity vulnerability CVE-2024-47771 / GHSA-963w-49j9-gxj6 diff --git a/Dockerfile b/Dockerfile index 3f3b9a1d71c..908c05520cc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Builder -FROM --platform=$BUILDPLATFORM node:20-bullseye as builder +FROM --platform=$BUILDPLATFORM node:22-bullseye as builder # Support custom branch of the js-sdk. This also helps us build images of element-web develop. ARG USE_CUSTOM_SDKS=false diff --git a/components.json b/components.json index cc5046ed695..0967ef424bc 100644 --- a/components.json +++ b/components.json @@ -1,5 +1 @@ -{ - "src/components/views/auth/AuthFooter.tsx": "src/components/views/auth/VectorAuthFooter.tsx", - "src/components/views/auth/AuthHeaderLogo.tsx": "src/components/views/auth/VectorAuthHeaderLogo.tsx", - "src/components/views/auth/AuthPage.tsx": "src/components/views/auth/VectorAuthPage.tsx" -} +{} diff --git a/docs/customisations.md b/docs/customisations.md index b5075b6fcec..a6f72ab1abc 100644 --- a/docs/customisations.md +++ b/docs/customisations.md @@ -11,8 +11,8 @@ Customisations will be removed from the codebase in a future release. Element Web and the React SDK support "customisation points" that can be used to easily add custom logic specific to a particular deployment of Element Web. -An example of this is the [security customisations -module](https://github.com/element-hq/element-web/blob/develop/src/customisations/Security.ts). +An example of this is the [media customisations +module](https://github.com/element-hq/element-web/blob/develop/src/customisations/Media.ts). This module in the React SDK only defines some empty functions and their types: it does not do anything by default. @@ -21,14 +21,14 @@ Web so that you can add your own code. Even though the default module is part of the React SDK, you can still override it from the Element Web layer: 1. Copy the default customisation module to - `element-web/src/customisations/YourNameSecurity.ts` + `element-web/src/customisations/YourNameMedia.ts` 2. Edit customisations points and make sure export the ones you actually want to activate 3. Create/add an entry to `customisations.json` next to the webpack config: ```json { - "src/customisations/Security.ts": "src/customisations/YourNameSecurity.ts" + "src/customisations/Media.ts": "src/customisations/YourNameMedia.ts" } ``` diff --git a/docs/install.md b/docs/install.md index af8f0e7eac1..1c182cdd34c 100644 --- a/docs/install.md +++ b/docs/install.md @@ -41,7 +41,15 @@ The Docker image can be used to serve element-web as a web server. The easiest w it is to use the prebuilt image: ```bash -docker run -p 80:80 vectorim/element-web +docker run --rm -p 127.0.0.1:80:80 vectorim/element-web +``` + +A server can also be made available to clients outside the local host by omitting the +explicit local address as described in +[docker run documentation](https://docs.docker.com/engine/reference/commandline/run/#publish-or-expose-port--p---expose): + +```bash +docker run --rm -p 80:80 vectorim/element-web ``` To supply your own custom `config.json`, map a volume to `/app/config.json`. For example, @@ -49,7 +57,7 @@ if your custom config was located at `/etc/element-web/config.json` then your Do would be: ```bash -docker run -p 80:80 -v /etc/element-web/config.json:/app/config.json vectorim/element-web +docker run --rm -p 127.0.0.1:80:80 -v /etc/element-web/config.json:/app/config.json vectorim/element-web ``` To build the image yourself: diff --git a/docs/theming.md b/docs/theming.md index 9d3d67e68da..100baeca71f 100644 --- a/docs/theming.md +++ b/docs/theming.md @@ -29,7 +29,7 @@ default theme, you would use `default_theme: "custom-Electric Blue"`. e.g. in config.json: -``` +```json5 "setting_defaults": { "custom_themes": [ { @@ -59,6 +59,10 @@ e.g. in config.json: "timeline-text-color": "#2e2f32", "timeline-text-secondary-color": "#61708b", "timeline-highlights-color": "#f3f8fd", + + // These should both be 8 values long + "username-colors": ["#ff0000", /*...*/], + "avatar-background-colors": ["#cc0000", /*...*/] }, "compound": { "--cpd-color-icon-accent-tertiary": "var(--cpd-color-blue-800)", diff --git a/element.io/app/config.json b/element.io/app/config.json index 4dcc75aeeb1..771df350919 100644 --- a/element.io/app/config.json +++ b/element.io/app/config.json @@ -46,5 +46,13 @@ "map_style_url": "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx", "setting_defaults": { "RustCrypto.staged_rollout_percent": 60 + }, + "features": { + "feature_video_rooms": true, + "feature_group_calls": true, + "feature_element_call_video_rooms": true + }, + "element_call": { + "url": "https://call.element.io" } } diff --git a/jest.config.ts b/jest.config.ts index 4f75eb04db4..04f1a91e77c 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -38,7 +38,7 @@ const config: Config = { "recorderWorkletFactory": "/__mocks__/empty.js", "^fetch-mock$": "/node_modules/fetch-mock", }, - transformIgnorePatterns: ["/node_modules/(?!matrix-js-sdk).+$"], + transformIgnorePatterns: ["/node_modules/(?!(mime|matrix-js-sdk)).+$"], collectCoverageFrom: [ "/src/**/*.{js,ts,tsx}", // getSessionLock is piped into a different JS context via stringification, and the coverage functionality is diff --git a/knip.ts b/knip.ts new file mode 100644 index 00000000000..247f9d97894 --- /dev/null +++ b/knip.ts @@ -0,0 +1,53 @@ +import { KnipConfig } from "knip"; + +export default { + entry: [ + "src/vector/index.ts", + "src/serviceworker/index.ts", + "src/workers/*.worker.ts", + "src/utils/exportUtils/exportJS.js", + "scripts/**", + "playwright/**", + "test/**", + "res/decoder-ring/**", + ], + project: ["**/*.{js,ts,jsx,tsx}"], + ignore: [ + "docs/**", + "res/jitsi_external_api.min.js", + // Used by jest + "__mocks__/maplibre-gl.js", + // Keep for now + "src/hooks/useLocalStorageState.ts", + "src/components/views/elements/InfoTooltip.tsx", + "src/components/views/elements/StyledCheckbox.tsx", + ], + ignoreDependencies: [ + // Required for `action-validator` + "@action-validator/*", + // Used for git pre-commit hooks + "husky", + // Used by jest + "babel-jest", + // Used by babel + "@babel/runtime", + "@babel/plugin-transform-class-properties", + // Referenced in PCSS + "github-markdown-css", + // False positive + "sw.js", + // Used by webpack + "buffer", + "process", + "util", + // Used by workflows + "ts-prune", + // Required due to bug in bloom-filters https://github.com/Callidon/bloom-filters/issues/75 + "@types/seedrandom", + ], + ignoreBinaries: [ + // Used in scripts & workflows + "jq", + ], + ignoreExportsUsedInFile: true, +} satisfies KnipConfig; diff --git a/package.json b/package.json index 4ab26bb1dfd..21781c354ce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "element-web", - "version": "1.11.81", + "version": "1.11.85", "description": "A feature-rich client for Matrix.org", "author": "New Vector Ltd.", "repository": { @@ -35,7 +35,7 @@ "i18n:lint": "matrix-i18n-lint && prettier --log-level=silent --write src/i18n/strings/ --ignore-path /dev/null", "i18n:diff": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && yarn i18n && matrix-compare-i18n-files src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json", "make-component": "node scripts/make-react-component.js", - "rethemendex": "res/css/rethemendex.sh", + "rethemendex": "./res/css/rethemendex.sh", "clean": "rimraf lib webapp", "build": "yarn clean && yarn build:genfiles && yarn build:bundle", "build-stats": "yarn clean && yarn build:genfiles && yarn build:bundle-stats", @@ -45,23 +45,20 @@ "build:bundle": "webpack --progress --mode production", "build:bundle-stats": "webpack --progress --mode production --json > webpack-stats.json", "build:module_system": "ts-node --project ./tsconfig.module_system.json module_system/scripts/install.ts", - "dist": "scripts/package.sh", + "dist": "./scripts/package.sh", "start": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n modules,res \"yarn build:module_system\" \"yarn build:res\" && concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n res,element-js \"yarn start:res\" \"yarn start:js\"", "start:https": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n res,element-js \"yarn start:res\" \"yarn start:js --server-type https\"", "start:res": "ts-node scripts/copy-res.ts -w", "start:js": "webpack serve --output-path webapp --output-filename=bundles/_dev_/[name].js --output-chunk-filename=bundles/_dev_/[name].js --mode development", "lint": "yarn lint:types && yarn lint:js && yarn lint:style && yarn lint:workflows", - "lint:js": "yarn lint:js:src && yarn lint:js:module_system", - "lint:js:src": "eslint --max-warnings 0 src test playwright && prettier --check .", - "lint:js:module_system": "eslint --max-warnings 0 --config .eslintrc-module_system.js module_system", - "lint:js-fix": "yarn lint:js-fix:src && yarn lint:js-fix:module_system", - "lint:js-fix:src": "prettier --log-level=warn --write . && eslint --fix src test playwright", - "lint:js-fix:module_system": "eslint --fix --config .eslintrc-module_system.js module_system", + "lint:js": "eslint --max-warnings 0 src test playwright module_system && prettier --check .", + "lint:js-fix": "prettier --log-level=warn --write . && eslint --fix src test playwright module_system", "lint:types": "yarn lint:types:src && yarn lint:types:module_system", "lint:types:src": "tsc --noEmit --jsx react && tsc --noEmit --jsx react -p playwright", "lint:types:module_system": "tsc --noEmit --project ./tsconfig.module_system.json", "lint:style": "stylelint \"res/css/**/*.pcss\"", "lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'", + "lint:knip": "knip", "test": "jest", "test:playwright": "playwright test", "test:playwright:open": "yarn test:playwright --ui", @@ -74,29 +71,28 @@ "update:jitsi": "curl -s https://meet.element.io/libs/external_api.min.js > ./res/jitsi_external_api.min.js" }, "resolutions": { - "@types/seedrandom": "3.0.8", "oidc-client-ts": "3.1.0", "jwt-decode": "4.0.0", - "caniuse-lite": "1.0.30001668", + "caniuse-lite": "1.0.30001679", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0", "wrap-ansi": "npm:wrap-ansi@^7.0.0" }, "dependencies": { "@babel/runtime": "^7.12.5", "@formatjs/intl-segmenter": "^11.5.7", - "@matrix-org/analytics-events": "^0.26.0", + "@matrix-org/analytics-events": "^0.29.0", "@matrix-org/emojibase-bindings": "^1.3.3", - "@vector-im/matrix-wysiwyg": "2.37.13", "@matrix-org/react-sdk-module-api": "^2.4.0", "@matrix-org/spec": "^1.7.0", "@sentry/browser": "^8.0.0", "@vector-im/compound-design-tokens": "^1.8.0", "@vector-im/compound-web": "^7.1.0", + "@vector-im/matrix-wysiwyg": "2.37.13", "@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4", "@zxcvbn-ts/language-en": "^3.0.2", "await-lock": "^2.1.0", - "bloom-filters": "^3.0.1", + "bloom-filters": "^3.0.2", "blurhash": "^2.0.3", "browserslist": "^4.23.2", "classnames": "^2.2.6", @@ -114,8 +110,8 @@ "highlight.js": "^11.3.1", "html-entities": "^2.0.0", "is-ip": "^3.1.0", - "jsrsasign": "^11.0.0", "js-xxhash": "^4.0.0", + "jsrsasign": "^11.0.0", "jszip": "^3.7.0", "katex": "^0.16.0", "linkify-element": "4.1.3", @@ -127,15 +123,16 @@ "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "0.0.1", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", - "matrix-widget-api": "^1.9.0", + "matrix-widget-api": "^1.10.0", "memoize-one": "^6.0.0", + "mime": "^4.0.4", "oidc-client-ts": "^3.0.1", "opus-recorder": "^8.0.3", "pako": "^2.0.3", "png-chunks-extract": "^1.0.0", "posthog-js": "1.157.2", "qrcode": "1.5.4", - "re-resizable": "6.9.17", + "re-resizable": "6.10.1", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.0", "react-blurhash": "^0.3.0", @@ -148,18 +145,16 @@ "tar-js": "^0.3.0", "temporal-polyfill": "^0.2.5", "ua-parser-js": "^1.0.2", - "uuid": "^10.0.0", + "uuid": "^11.0.0", "what-input": "^5.2.10" }, "devDependencies": { "@action-validator/cli": "^0.6.0", "@action-validator/core": "^0.6.0", "@axe-core/playwright": "^4.8.1", - "@babel/cli": "^7.12.10", "@babel/core": "^7.12.10", "@babel/eslint-parser": "^7.12.10", "@babel/eslint-plugin": "^7.12.10", - "@babel/parser": "^7.12.11", "@babel/plugin-proposal-export-default-from": "^7.12.1", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-transform-class-properties": "^7.12.1", @@ -172,7 +167,6 @@ "@babel/preset-env": "^7.12.11", "@babel/preset-react": "^7.12.10", "@babel/preset-typescript": "^7.12.7", - "@babel/register": "^7.12.10", "@babel/runtime": "^7.12.5", "@casualbot/jest-sonar-reporter": "2.2.7", "@peculiar/webcrypto": "^1.4.3", @@ -186,7 +180,6 @@ "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.5.2", "@types/commonmark": "^0.27.4", - "@types/content-type": "^1.1.5", "@types/counterpart": "^0.18.1", "@types/css-tree": "^2.3.8", "@types/diff-match-patch": "^1.0.32", @@ -208,10 +201,9 @@ "@types/qrcode": "^1.3.5", "@types/react": "18.3.3", "@types/react-beautiful-dnd": "^13.0.0", - "@types/react-dom": "18.3.0", + "@types/react-dom": "18.3.1", "@types/react-transition-group": "^4.4.0", "@types/sanitize-html": "2.13.0", - "@types/sdp-transform": "^2.4.6", "@types/seedrandom": "3.0.8", "@types/semver": "^7.5.8", "@types/tar-js": "^0.3.5", @@ -219,7 +211,6 @@ "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", - "axe-core": "4.10.0", "babel-jest": "^29.0.0", "babel-loader": "^9.0.0", "babel-plugin-jsx-remove-data-test-id": "^3.0.0", @@ -242,7 +233,7 @@ "eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-matrix-org": "^2.0.2", "eslint-plugin-react": "^7.28.0", - "eslint-plugin-react-hooks": "^4.3.0", + "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-unicorn": "^56.0.0", "express": "^4.18.2", "fake-indexeddb": "^6.0.0", @@ -259,14 +250,12 @@ "jest-mock": "^29.6.2", "jest-raw-loader": "^1.0.1", "jsqr": "^1.4.0", + "knip": "^5.36.2", "lint-staged": "^15.0.2", "mailhog": "^4.16.0", - "matrix-mock-request": "^2.5.0", "matrix-web-i18n": "^3.2.1", "mini-css-extract-plugin": "2.9.0", "minimist": "^1.2.6", - "mkdirp": "^3.0.0", - "mocha-junit-reporter": "^2.2.0", "modernizr": "^3.12.0", "node-fetch": "^2.6.7", "playwright-core": "^1.45.1", @@ -276,7 +265,7 @@ "postcss-import": "16.1.0", "postcss-loader": "8.1.1", "postcss-mixins": "^11.0.0", - "postcss-nested": "^6.0.0", + "postcss-nested": "^7.0.0", "postcss-preset-env": "^10.0.0", "postcss-scss": "^4.0.4", "postcss-simple-vars": "^7.0.1", diff --git a/playwright/Dockerfile b/playwright/Dockerfile index cbce8f0d008..9d478ff231a 100644 --- a/playwright/Dockerfile +++ b/playwright/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/playwright:v1.48.0-jammy +FROM mcr.microsoft.com/playwright:v1.48.2-jammy WORKDIR /work diff --git a/playwright/e2e/crypto/invisible-crypto.spec.ts b/playwright/e2e/crypto/invisible-crypto.spec.ts index c53bacd32c3..f207d2c6bb3 100644 --- a/playwright/e2e/crypto/invisible-crypto.spec.ts +++ b/playwright/e2e/crypto/invisible-crypto.spec.ts @@ -51,6 +51,6 @@ test.describe("Invisible cryptography", () => { /* should show an error for a message from a previously verified device */ await bobSecondDevice.sendMessage(testRoomId, "test encrypted from user that was previously verified"); const lastTile = page.locator(".mx_EventTile_last"); - await expect(lastTile).toContainText("Verified identity has changed"); + await expect(lastTile).toContainText("Sender's verified identity has changed"); }); }); diff --git a/playwright/e2e/crypto/user-verification.spec.ts b/playwright/e2e/crypto/user-verification.spec.ts index f1def98469c..4c8d641e6f7 100644 --- a/playwright/e2e/crypto/user-verification.spec.ts +++ b/playwright/e2e/crypto/user-verification.spec.ts @@ -60,6 +60,11 @@ test.describe("User verification", () => { // Accept await toast.getByRole("button", { name: "Verify User" }).click(); + // Wait for the QR code to be rendered. If we don't do this, then the QR code can be rendered just as + // Playwright tries to click the "Verify by emoji" button, which seems to make it miss the button. + // (richvdh: I thought Playwright was supposed to be resilient to such things, but empirically not.) + await expect(page.getByAltText("QR Code")).toBeVisible(); + // request verification by emoji await page.locator("#mx_RightPanel").getByRole("button", { name: "Verify by emoji" }).click(); @@ -101,13 +106,20 @@ test.describe("User verification", () => { const toast = await toasts.getToast("Verification requested"); await toast.getByRole("button", { name: "Verify User" }).click(); + // Wait for the QR code to be rendered. If we don't do this, then the QR code can be rendered just as + // Playwright tries to click the "Verify by emoji" button, which seems to make it miss the button. + // (richvdh: I thought Playwright was supposed to be resilient to such things, but empirically not.) + await expect(page.getByAltText("QR Code")).toBeVisible(); + // request verification by emoji await page.locator("#mx_RightPanel").getByRole("button", { name: "Verify by emoji" }).click(); /* on the bot side, wait for the verifier to exist ... */ const botVerifier = await awaitVerifier(bobVerificationRequest); - // ... confirm ... - botVerifier.evaluate((verifier) => verifier.verify()).catch(() => {}); + // ... and confirm. We expect the verification to fail; we catch the error on the DOM side + // to stop playwright marking the evaluate as failing in the UI. + const botVerification = botVerifier.evaluate((verifier) => verifier.verify().catch(() => {})); + // ... and abort the verification await page.getByRole("button", { name: "They don't match" }).click(); @@ -115,6 +127,8 @@ test.describe("User verification", () => { await expect(dialog.getByText("Your messages are not secure")).toBeVisible(); await dialog.getByRole("button", { name: "OK" }).click(); await expect(dialog).not.toBeVisible(); + + await botVerification; }); }); diff --git a/playwright/e2e/pinned-messages/index.ts b/playwright/e2e/pinned-messages/index.ts index 75928a438bb..ac50b62294b 100644 --- a/playwright/e2e/pinned-messages/index.ts +++ b/playwright/e2e/pinned-messages/index.ts @@ -196,14 +196,7 @@ export class Helpers { */ async assertEmptyPinnedMessagesList() { const rightPanel = this.getRightPanel(); - await expect(rightPanel).toMatchScreenshot(`pinned-messages-list-empty.png`, { - css: ` - // hide the tooltip "Room information" to avoid flakiness - [data-floating-ui-portal] { - display: none !important; - } - `, - }); + await expect(rightPanel).toMatchScreenshot(`pinned-messages-list-empty.png`); } /** diff --git a/playwright/e2e/pinned-messages/pinned-messages.spec.ts b/playwright/e2e/pinned-messages/pinned-messages.spec.ts index 9f6f38f1774..ef2c1b27d4c 100644 --- a/playwright/e2e/pinned-messages/pinned-messages.spec.ts +++ b/playwright/e2e/pinned-messages/pinned-messages.spec.ts @@ -31,6 +31,12 @@ test.describe("Pinned messages", () => { const tile = util.getEventTile("Msg1"); await expect(tile).toMatchScreenshot("pinned-message-Msg1.png", { mask: [tile.locator(".mx_MessageTimestamp")], + // Hide the jump to bottom button in the timeline to avoid flakiness + css: ` + .mx_JumpToBottomButton { + display: none !important; + } + `, }); }); diff --git a/playwright/e2e/release-announcement/index.ts b/playwright/e2e/release-announcement/index.ts index 81146be70e7..59db80c3c6d 100644 --- a/playwright/e2e/release-announcement/index.ts +++ b/playwright/e2e/release-announcement/index.ts @@ -42,7 +42,7 @@ export class Helpers { */ async assertReleaseAnnouncementIsVisible(name: string) { await expect(this.getReleaseAnnouncement(name)).toBeVisible(); - await expect(this.page).toMatchScreenshot(`release-announcement-${name}.png`); + await expect(this.page).toMatchScreenshot(`release-announcement-${name}.png`, { showTooltips: true }); } /** diff --git a/playwright/e2e/widgets/stickers.spec.ts b/playwright/e2e/widgets/stickers.spec.ts index ff5526a6e7b..318f7129616 100644 --- a/playwright/e2e/widgets/stickers.spec.ts +++ b/playwright/e2e/widgets/stickers.spec.ts @@ -6,32 +6,48 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ +import * as fs from "node:fs"; + import type { Page } from "@playwright/test"; import { test, expect } from "../../element-web-test"; import { ElementAppPage } from "../../pages/ElementAppPage"; +import { Credentials } from "../../plugins/homeserver"; const STICKER_PICKER_WIDGET_ID = "fake-sticker-picker"; const STICKER_PICKER_WIDGET_NAME = "Fake Stickers"; const STICKER_NAME = "Test Sticker"; const ROOM_NAME_1 = "Sticker Test"; const ROOM_NAME_2 = "Sticker Test Two"; -const STICKER_MESSAGE = JSON.stringify({ - action: "m.sticker", - api: "fromWidget", - data: { - name: "teststicker", - description: STICKER_NAME, - file: "test.png", - content: { - body: STICKER_NAME, - msgtype: "m.sticker", - url: "mxc://localhost/somewhere", +const STICKER_IMAGE = fs.readFileSync("playwright/sample-files/riot.png"); + +function getStickerMessage(contentUri: string, mimetype: string): string { + return JSON.stringify({ + action: "m.sticker", + api: "fromWidget", + data: { + name: "teststicker", + description: STICKER_NAME, + file: "test.png", + content: { + body: STICKER_NAME, + info: { + h: 480, + mimetype: mimetype, + size: 13818, + w: 480, + }, + msgtype: "m.sticker", + url: contentUri, + }, }, - }, - requestId: "1", - widgetId: STICKER_PICKER_WIDGET_ID, -}); -const WIDGET_HTML = ` + requestId: "1", + widgetId: STICKER_PICKER_WIDGET_ID, + }); +} + +function getWidgetHtml(contentUri: string, mimetype: string) { + const stickerMessage = getStickerMessage(contentUri, mimetype); + return ` Fake Sticker Picker @@ -51,13 +67,13 @@ const WIDGET_HTML = ` `; - +} async function openStickerPicker(app: ElementAppPage) { const options = await app.openMessageComposerOptions(); await options.getByRole("menuitem", { name: "Sticker" }).click(); @@ -71,7 +87,8 @@ async function sendStickerFromPicker(page: Page) { await expect(page.locator(".mx_AppTileFullWidth#stickers")).not.toBeVisible(); } -async function expectTimelineSticker(page: Page, roomId: string) { +async function expectTimelineSticker(page: Page, roomId: string, contentUri: string) { + const contentId = contentUri.split("/").slice(-1)[0]; // Make sure it's in the right room await expect(page.locator(".mx_EventTile_sticker > a")).toHaveAttribute("href", new RegExp(`/${roomId}/`)); @@ -80,13 +97,43 @@ async function expectTimelineSticker(page: Page, roomId: string) { // download URL. await expect(page.locator(`img[alt="${STICKER_NAME}"]`)).toHaveAttribute( "src", - new RegExp("/download/localhost/somewhere"), + new RegExp(`/localhost/${contentId}`), ); } +async function expectFileTile(page: Page, roomId: string, contentUri: string) { + await expect(page.locator(".mx_MFileBody_info_filename")).toContainText(STICKER_NAME); +} + +async function setWidgetAccountData( + app: ElementAppPage, + user: Credentials, + stickerPickerUrl: string, + provideCreatorUserId: boolean = true, +) { + await app.client.setAccountData("m.widgets", { + [STICKER_PICKER_WIDGET_ID]: { + content: { + type: "m.stickerpicker", + name: STICKER_PICKER_WIDGET_NAME, + url: stickerPickerUrl, + creatorUserId: provideCreatorUserId ? user.userId : undefined, + }, + sender: user.userId, + state_key: STICKER_PICKER_WIDGET_ID, + type: "m.widget", + id: STICKER_PICKER_WIDGET_ID, + }, + }); +} + test.describe("Stickers", () => { test.use({ displayName: "Sally", + room: async ({ app }, use) => { + const roomId = await app.client.createRoom({ name: ROOM_NAME_1 }); + await use({ roomId }); + }, }); // We spin up a web server for the sticker picker so that we're not testing to see if @@ -96,34 +143,19 @@ test.describe("Stickers", () => { // // See sendStickerFromPicker() for more detail on iframe comms. let stickerPickerUrl: string; - test.beforeEach(async ({ webserver }) => { - stickerPickerUrl = webserver.start(WIDGET_HTML); - }); - test("should send a sticker to multiple rooms", async ({ page, app, user }) => { - const roomId1 = await app.client.createRoom({ name: ROOM_NAME_1 }); + test("should send a sticker to multiple rooms", async ({ webserver, page, app, user, room }) => { const roomId2 = await app.client.createRoom({ name: ROOM_NAME_2 }); - - await app.client.setAccountData("m.widgets", { - [STICKER_PICKER_WIDGET_ID]: { - content: { - type: "m.stickerpicker", - name: STICKER_PICKER_WIDGET_NAME, - url: stickerPickerUrl, - creatorUserId: user.userId, - }, - sender: user.userId, - state_key: STICKER_PICKER_WIDGET_ID, - type: "m.widget", - id: STICKER_PICKER_WIDGET_ID, - }, - }); + const { content_uri: contentUri } = await app.client.uploadContent(STICKER_IMAGE, { type: "image/png" }); + const widgetHtml = getWidgetHtml(contentUri, "image/png"); + stickerPickerUrl = webserver.start(widgetHtml); + setWidgetAccountData(app, user, stickerPickerUrl); await app.viewRoomByName(ROOM_NAME_1); - await expect(page).toHaveURL(`/#/room/${roomId1}`); + await expect(page).toHaveURL(`/#/room/${room.roomId}`); await openStickerPicker(app); await sendStickerFromPicker(page); - await expectTimelineSticker(page, roomId1); + await expectTimelineSticker(page, room.roomId, contentUri); // Ensure that when we switch to a different room that the sticker // goes to the right place @@ -131,31 +163,40 @@ test.describe("Stickers", () => { await expect(page).toHaveURL(`/#/room/${roomId2}`); await openStickerPicker(app); await sendStickerFromPicker(page); - await expectTimelineSticker(page, roomId2); + await expectTimelineSticker(page, roomId2, contentUri); }); - test("should handle a sticker picker widget missing creatorUserId", async ({ page, app, user }) => { - const roomId1 = await app.client.createRoom({ name: ROOM_NAME_1 }); + test("should handle a sticker picker widget missing creatorUserId", async ({ + webserver, + page, + app, + user, + room, + }) => { + const { content_uri: contentUri } = await app.client.uploadContent(STICKER_IMAGE, { type: "image/png" }); + const widgetHtml = getWidgetHtml(contentUri, "image/png"); + stickerPickerUrl = webserver.start(widgetHtml); + setWidgetAccountData(app, user, stickerPickerUrl, false); - await app.client.setAccountData("m.widgets", { - [STICKER_PICKER_WIDGET_ID]: { - content: { - type: "m.stickerpicker", - name: STICKER_PICKER_WIDGET_NAME, - url: stickerPickerUrl, - // No creatorUserId - }, - sender: user.userId, - state_key: STICKER_PICKER_WIDGET_ID, - type: "m.widget", - id: STICKER_PICKER_WIDGET_ID, - }, + await app.viewRoomByName(ROOM_NAME_1); + await expect(page).toHaveURL(`/#/room/${room.roomId}`); + await openStickerPicker(app); + await sendStickerFromPicker(page); + await expectTimelineSticker(page, room.roomId, contentUri); + }); + + test("should render invalid mimetype as a file", async ({ webserver, page, app, user, room }) => { + const { content_uri: contentUri } = await app.client.uploadContent(STICKER_IMAGE, { + type: "application/octet-stream", }); + const widgetHtml = getWidgetHtml(contentUri, "application/octet-stream"); + stickerPickerUrl = webserver.start(widgetHtml); + setWidgetAccountData(app, user, stickerPickerUrl); await app.viewRoomByName(ROOM_NAME_1); - await expect(page).toHaveURL(`/#/room/${roomId1}`); + await expect(page).toHaveURL(`/#/room/${room.roomId}`); await openStickerPicker(app); await sendStickerFromPicker(page); - await expectTimelineSticker(page, roomId1); + await expectFileTile(page, room.roomId, contentUri); }); }); diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts index 93b119ee7a6..8d5229a5100 100644 --- a/playwright/element-web-test.ts +++ b/playwright/element-web-test.ts @@ -345,6 +345,7 @@ export const expect = baseExpect.extend({ if (!options?.showTooltips) { css += ` + [data-floating-ui-portal], [role="tooltip"] { visibility: hidden !important; } diff --git a/playwright/plugins/homeserver/synapse/index.ts b/playwright/plugins/homeserver/synapse/index.ts index cb4882f1b90..c0ae4e466fc 100644 --- a/playwright/plugins/homeserver/synapse/index.ts +++ b/playwright/plugins/homeserver/synapse/index.ts @@ -20,7 +20,7 @@ import { randB64Bytes } from "../../utils/rand"; // Docker tag to use for synapse docker image. // We target a specific digest as every now and then a Synapse update will break our CI. // This digest is updated by the playwright-image-updates.yaml workflow periodically. -const DOCKER_TAG = "develop@sha256:47c62aa9507a24820190eef547861c0d278cc83fe90329c46b9f4329eed88ef4"; +const DOCKER_TAG = "develop@sha256:b7d8089c4593d4aa12834d04849971717b17254a76257e7c5cd433a16d5e966e"; async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise> { const templateDir = path.join(__dirname, "templates", opts.template); diff --git a/playwright/plugins/homeserver/synapse/templates/default/homeserver.yaml b/playwright/plugins/homeserver/synapse/templates/default/homeserver.yaml index bc3ecd7c9b5..539a917b200 100644 --- a/playwright/plugins/homeserver/synapse/templates/default/homeserver.yaml +++ b/playwright/plugins/homeserver/synapse/templates/default/homeserver.yaml @@ -102,3 +102,5 @@ experimental_features: # messages > non-joined historical messages. # Can be removed after Synapse enables it by default msc4115_membership_on_events: true + +enable_authenticated_media: true diff --git a/playwright/snapshots/chat-export/html-export.spec.ts/html-export-linux.png b/playwright/snapshots/chat-export/html-export.spec.ts/html-export-linux.png index 6a490c2157b..8519e162f2f 100644 Binary files a/playwright/snapshots/chat-export/html-export.spec.ts/html-export-linux.png and b/playwright/snapshots/chat-export/html-export.spec.ts/html-export-linux.png differ diff --git a/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-verify-email-linux.png b/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-verify-email-linux.png index b43d78bec85..5fa7969c574 100644 Binary files a/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-verify-email-linux.png and b/playwright/snapshots/forgot-password/forgot-password.spec.ts/forgot-password-verify-email-linux.png differ diff --git a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-Msg1-linux.png b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-Msg1-linux.png index 2f9c3841ac3..97b751ec6a1 100644 Binary files a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-Msg1-linux.png and b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-Msg1-linux.png differ diff --git a/playwright/snapshots/register/email.spec.ts/registration-check-your-email-linux.png b/playwright/snapshots/register/email.spec.ts/registration-check-your-email-linux.png index 25380a74b2f..1387ef062d2 100644 Binary files a/playwright/snapshots/register/email.spec.ts/registration-check-your-email-linux.png and b/playwright/snapshots/register/email.spec.ts/registration-check-your-email-linux.png differ diff --git a/playwright/snapshots/register/register.spec.ts/registration-linux.png b/playwright/snapshots/register/register.spec.ts/registration-linux.png index ab9fdb2bf62..bac041646ad 100644 Binary files a/playwright/snapshots/register/register.spec.ts/registration-linux.png and b/playwright/snapshots/register/register.spec.ts/registration-linux.png differ diff --git a/playwright/snapshots/register/register.spec.ts/terms-prompt-linux.png b/playwright/snapshots/register/register.spec.ts/terms-prompt-linux.png index 30436d0abc6..913ccf98393 100644 Binary files a/playwright/snapshots/register/register.spec.ts/terms-prompt-linux.png and b/playwright/snapshots/register/register.spec.ts/terms-prompt-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png index bceaa4a283d..a30e9969b62 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png index bf47c913881..ef855a2da2a 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png differ diff --git a/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png b/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png index 01a6c6089b2..9f34ba19e32 100644 Binary files a/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png and b/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png differ diff --git a/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png b/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png index 81c08756df1..082650056f9 100644 Binary files a/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png and b/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png differ diff --git a/playwright/snapshots/settings/security-user-settings-tab.spec.ts/Security-user-settings-tab-with-posthog-enable-b5d89-csLearnMoreDialog-should-be-rendered-properly-1-linux.png b/playwright/snapshots/settings/security-user-settings-tab.spec.ts/Security-user-settings-tab-with-posthog-enable-b5d89-csLearnMoreDialog-should-be-rendered-properly-1-linux.png index d2852b7c0fb..e858838ab98 100644 Binary files a/playwright/snapshots/settings/security-user-settings-tab.spec.ts/Security-user-settings-tab-with-posthog-enable-b5d89-csLearnMoreDialog-should-be-rendered-properly-1-linux.png and b/playwright/snapshots/settings/security-user-settings-tab.spec.ts/Security-user-settings-tab-with-posthog-enable-b5d89-csLearnMoreDialog-should-be-rendered-properly-1-linux.png differ diff --git a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-linux.png b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-linux.png index 26f5bfdfa98..7d8fccd672e 100644 Binary files a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-linux.png and b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-linux.png differ diff --git a/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png b/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png index e7f09c67fb4..f5eb3935ba5 100644 Binary files a/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png and b/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png differ diff --git a/res/css/_common.pcss b/res/css/_common.pcss index 05a3dac0675..15ba02b6b88 100644 --- a/res/css/_common.pcss +++ b/res/css/_common.pcss @@ -186,7 +186,7 @@ input[type="search"].mx_textinput_icon { /* FIXME THEME - Tint by CSS rather than referencing a duplicate asset */ input[type="text"].mx_textinput_icon.mx_textinput_search, input[type="search"].mx_textinput_icon.mx_textinput_search { - background-image: url("$(res)/img/feather-customised/search-input.svg"); + background-image: url("@vector-im/compound-design-tokens/icons/search.svg"); } /* dont search UI as not all browsers support it, */ diff --git a/res/css/_components.pcss b/res/css/_components.pcss index c0dd2ee0b02..12239fac2df 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -282,11 +282,11 @@ @import "./views/rooms/_EmojiButton.pcss"; @import "./views/rooms/_EntityTile.pcss"; @import "./views/rooms/_EventBubbleTile.pcss"; +@import "./views/rooms/_EventPreview.pcss"; @import "./views/rooms/_EventTile.pcss"; @import "./views/rooms/_HistoryTile.pcss"; @import "./views/rooms/_IRCLayout.pcss"; @import "./views/rooms/_JumpToBottomButton.pcss"; -@import "./views/rooms/_LegacyRoomHeader.pcss"; @import "./views/rooms/_LinkPreviewGroup.pcss"; @import "./views/rooms/_LinkPreviewWidget.pcss"; @import "./views/rooms/_LiveContentSummary.pcss"; diff --git a/res/css/components/views/settings/devices/_DeviceExpandDetailsButton.pcss b/res/css/components/views/settings/devices/_DeviceExpandDetailsButton.pcss index de5d0bc5e61..8ad786c4baf 100644 --- a/res/css/components/views/settings/devices/_DeviceExpandDetailsButton.pcss +++ b/res/css/components/views/settings/devices/_DeviceExpandDetailsButton.pcss @@ -32,8 +32,8 @@ Please see LICENSE files in the repository root for full details. } .mx_DeviceExpandDetailsButton_icon { - height: 16px; - width: 16px; + height: 24px; + width: 24px; transition: all 0.3s; transform: var(--icon-transform); diff --git a/res/css/structures/_GenericDropdownMenu.pcss b/res/css/structures/_GenericDropdownMenu.pcss index d58c29f81a8..bf0098b4ed2 100644 --- a/res/css/structures/_GenericDropdownMenu.pcss +++ b/res/css/structures/_GenericDropdownMenu.pcss @@ -25,7 +25,7 @@ Please see LICENSE files in the repository root for full details. width: 18px; height: 18px; background: currentColor; - mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg"); mask-size: 100%; mask-repeat: no-repeat; float: right; diff --git a/res/css/structures/_RoomStatusBar.pcss b/res/css/structures/_RoomStatusBar.pcss index b131009868b..0f30401a6b1 100644 --- a/res/css/structures/_RoomStatusBar.pcss +++ b/res/css/structures/_RoomStatusBar.pcss @@ -125,7 +125,7 @@ Please see LICENSE files in the repository root for full details. padding-left: 34px; /* 28px from above, but +6px to account for the wider icon */ &::before { - mask-image: url("$(res)/img/element-icons/retry.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/restart.svg"); } } } diff --git a/res/css/structures/_RoomView.pcss b/res/css/structures/_RoomView.pcss index 52fa523c4ee..eaa02cd2d22 100644 --- a/res/css/structures/_RoomView.pcss +++ b/res/css/structures/_RoomView.pcss @@ -62,7 +62,7 @@ Please see LICENSE files in the repository root for full details. &::before { background-color: $info-plinth-fg-color; - mask: url("$(res)/img/feather-customised/search-input.svg"); + mask: url("@vector-im/compound-design-tokens/icons/search.svg"); mask-repeat: no-repeat; mask-position: center; mask-size: 50px; @@ -181,11 +181,6 @@ Please see LICENSE files in the repository root for full details. } } -/* Rooms with immersive content */ -.mx_RoomView_immersive .mx_LegacyRoomHeader_wrapper { - border: unset; -} - .mx_RoomView_inCall { .mx_RoomView_statusAreaBox_line { margin-top: 2px; diff --git a/res/css/structures/_SpaceHierarchy.pcss b/res/css/structures/_SpaceHierarchy.pcss index d91d5b8d9b0..ccbeef07347 100644 --- a/res/css/structures/_SpaceHierarchy.pcss +++ b/res/css/structures/_SpaceHierarchy.pcss @@ -77,7 +77,7 @@ Please see LICENSE files in the repository root for full details. height: 16px; width: 16px; left: 0; - background-image: url("$(res)/img/element-icons/warning-badge.svg"); + background-image: url("@vector-im/compound-design-tokens/icons/error.svg"); background-size: cover; background-repeat: no-repeat; } @@ -121,7 +121,7 @@ Please see LICENSE files in the repository root for full details. background-color: $tertiary-content; mask-size: 16px; transform: rotate(270deg); - mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg"); } &.mx_SpaceHierarchy_subspace_toggle_shown::before { diff --git a/res/css/structures/_SpacePanel.pcss b/res/css/structures/_SpacePanel.pcss index 7875e629733..668dde945a5 100644 --- a/res/css/structures/_SpacePanel.pcss +++ b/res/css/structures/_SpacePanel.pcss @@ -48,7 +48,7 @@ Please see LICENSE files in the repository root for full details. mask-size: contain; mask-repeat: no-repeat; background-color: $background; - mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg"); transform: rotate(270deg); } @@ -169,7 +169,7 @@ Please see LICENSE files in the repository root for full details. mask-size: 20px; mask-repeat: no-repeat; background-color: $tertiary-content; - mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg"); } .mx_SpaceButton_icon { diff --git a/res/css/views/context_menus/_MessageContextMenu.pcss b/res/css/views/context_menus/_MessageContextMenu.pcss index 20d7ed1d13b..e06782ebe96 100644 --- a/res/css/views/context_menus/_MessageContextMenu.pcss +++ b/res/css/views/context_menus/_MessageContextMenu.pcss @@ -29,7 +29,7 @@ Please see LICENSE files in the repository root for full details. } .mx_MessageContextMenu_iconReport::before { - mask-image: url("$(res)/img/element-icons/warning-badge.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/error.svg"); } .mx_MessageContextMenu_iconLink::before { @@ -61,7 +61,7 @@ Please see LICENSE files in the repository root for full details. } .mx_MessageContextMenu_iconResend::before { - mask-image: url("$(res)/img/element-icons/retry.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/restart.svg"); } .mx_MessageContextMenu_iconSource::before { diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.pcss b/res/css/views/dialogs/_AddExistingToSpaceDialog.pcss index 6ac9bc39750..1656ca7e676 100644 --- a/res/css/views/dialogs/_AddExistingToSpaceDialog.pcss +++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.pcss @@ -125,7 +125,7 @@ Please see LICENSE files in the repository root for full details. mask-repeat: no-repeat; mask-position: center; mask-size: contain; - mask-image: url("$(res)/img/element-icons/retry.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/restart.svg"); width: 18px; height: 18px; left: 0; diff --git a/res/css/views/dialogs/_AnalyticsLearnMoreDialog.pcss b/res/css/views/dialogs/_AnalyticsLearnMoreDialog.pcss index 01d69b03857..456b28d88a5 100644 --- a/res/css/views/dialogs/_AnalyticsLearnMoreDialog.pcss +++ b/res/css/views/dialogs/_AnalyticsLearnMoreDialog.pcss @@ -36,9 +36,24 @@ Please see LICENSE files in the repository root for full details. } .mx_AnalyticsLearnMore_bullets li { - background: url("$(res)/img/tick-circle.svg") no-repeat; list-style-type: none; - padding: 2px 0px 20px 32px; + padding: 2px 0 0 32px; + margin-bottom: 20px; vertical-align: middle; + position: relative; + + &::before { + content: ""; + position: absolute; + width: 26px; + height: 26px; + left: 0; + top: 0; + background-color: #0dbd8b; + mask-image: url("@vector-im/compound-design-tokens/icons/check-circle.svg"); + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + } } } diff --git a/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss b/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss index 0b42281e3ef..e5abc1e48bc 100644 --- a/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss +++ b/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss @@ -21,7 +21,7 @@ Please see LICENSE files in the repository root for full details. &.mx_AccessSecretStorageDialog_resetBadge::before { /* The image isn't capable of masking, so we use a background instead. */ - background-image: url("$(res)/img/element-icons/warning-badge.svg"); + background-image: url("@vector-im/compound-design-tokens/icons/error.svg"); background-size: 24px; background-color: transparent; } @@ -120,7 +120,7 @@ Please see LICENSE files in the repository root for full details. width: 16px; left: 0; top: 2px; /* alignment */ - background-image: url("$(res)/img/element-icons/warning-badge.svg"); + background-image: url("@vector-im/compound-design-tokens/icons/error.svg"); background-size: contain; } diff --git a/res/css/views/elements/_Dropdown.pcss b/res/css/views/elements/_Dropdown.pcss index 7a3ebb9c29b..b91af285fd6 100644 --- a/res/css/views/elements/_Dropdown.pcss +++ b/res/css/views/elements/_Dropdown.pcss @@ -39,11 +39,13 @@ Please see LICENSE files in the repository root for full details. } .mx_Dropdown_arrow { - width: 10px; - height: 6px; - padding-right: 9px; - mask: url("$(res)/img/feather-customised/dropdown-arrow.svg"); + width: 16px; + height: 16px; + margin-right: 4px; + mask: url("@vector-im/compound-design-tokens/icons/chevron-down.svg"); mask-repeat: no-repeat; + mask-position: center; + mask-size: 18px; background: $primary-content; } diff --git a/res/css/views/elements/_EditableItemList.pcss b/res/css/views/elements/_EditableItemList.pcss index 34ec3199b49..8a85f615d86 100644 --- a/res/css/views/elements/_EditableItemList.pcss +++ b/res/css/views/elements/_EditableItemList.pcss @@ -18,10 +18,9 @@ Please see LICENSE files in the repository root for full details. .mx_EditableItem_delete { @mixin customisedCancelButton; order: 3; - margin-right: 5px; vertical-align: middle; - width: 14px; - height: 14px; + width: 28px; + height: 28px; background-color: $alert; mask-size: 100%; } @@ -42,7 +41,7 @@ Please see LICENSE files in the repository root for full details. .mx_EditableItem_item { flex: auto 1 0; order: 1; - width: calc(100% - 14px); /* leave space for the remove button */ + width: calc(100% - 28px); /* leave space for the remove button */ overflow-x: hidden; text-overflow: ellipsis; } diff --git a/res/css/views/elements/_Field.pcss b/res/css/views/elements/_Field.pcss index 2659c4d3899..21a0a0208aa 100644 --- a/res/css/views/elements/_Field.pcss +++ b/res/css/views/elements/_Field.pcss @@ -51,12 +51,15 @@ Please see LICENSE files in the repository root for full details. .mx_Field_select::before { content: ""; position: absolute; - top: 15px; - right: 10px; - width: 10px; - height: 6px; - mask: url("$(res)/img/feather-customised/dropdown-arrow.svg"); + top: 50%; + transform: translateY(-50%); + right: 4px; + width: 18px; + height: 18px; + mask: url("@vector-im/compound-design-tokens/icons/chevron-down.svg"); mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; background-color: $primary-content; z-index: 1; pointer-events: none; diff --git a/res/css/views/elements/_InfoTooltip.pcss b/res/css/views/elements/_InfoTooltip.pcss index a9a4dd42e65..0329f6a63bd 100644 --- a/res/css/views/elements/_InfoTooltip.pcss +++ b/res/css/views/elements/_InfoTooltip.pcss @@ -29,5 +29,5 @@ Please see LICENSE files in the repository root for full details. } .mx_InfoTooltip_icon_warning::before { - mask-image: url("$(res)/img/element-icons/warning.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/error.svg"); } diff --git a/res/css/views/messages/_DateSeparator.pcss b/res/css/views/messages/_DateSeparator.pcss index 7bbf465f555..aa6f88eaaa5 100644 --- a/res/css/views/messages/_DateSeparator.pcss +++ b/res/css/views/messages/_DateSeparator.pcss @@ -30,6 +30,6 @@ Please see LICENSE files in the repository root for full details. mask-position: center; mask-size: contain; mask-repeat: no-repeat; - mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg"); background-color: var(--cpd-color-icon-secondary); } diff --git a/res/css/views/messages/_DecryptionFailureBody.pcss b/res/css/views/messages/_DecryptionFailureBody.pcss index 64a09be7efa..516e7bcc89f 100644 --- a/res/css/views/messages/_DecryptionFailureBody.pcss +++ b/res/css/views/messages/_DecryptionFailureBody.pcss @@ -11,22 +11,11 @@ Please see LICENSE files in the repository root for full details. font-style: italic; } -/* Formatting for the "Verified identity has changed" error */ -.mx_DecryptionFailureVerifiedIdentityChanged > span { - /* Show it in red */ - color: var(--cpd-color-text-critical-primary); - background-color: var(--cpd-color-bg-critical-subtle); - - /* With a red border */ - border: 1px solid var(--cpd-color-border-critical-subtle); - border-radius: $font-16px; - - /* Some space inside the border */ - padding: var(--cpd-space-1x) var(--cpd-space-3x) var(--cpd-space-1x) var(--cpd-space-2x); - - /* some space between the (!) icon and text */ +/* Formatting for errors due to sender trust requirement failures */ +.mx_DecryptionFailureSenderTrustRequirement > span { + /* some space between the (/) icon and text */ display: inline-flex; - gap: var(--cpd-space-2x); + gap: var(--cpd-space-1x); /* Center vertically */ align-items: center; diff --git a/res/css/views/messages/_MessageActionBar.pcss b/res/css/views/messages/_MessageActionBar.pcss index 4fe68f08d07..3768bfb0213 100644 --- a/res/css/views/messages/_MessageActionBar.pcss +++ b/res/css/views/messages/_MessageActionBar.pcss @@ -108,6 +108,10 @@ Please see LICENSE files in the repository root for full details. color: var(--cpd-color-icon-primary); } + &.mx_MessageActionBar_retryButton { + --MessageActionBar-icon-size: 16px; + } + &.mx_MessageActionBar_downloadButton { --MessageActionBar-icon-size: 14px; diff --git a/res/css/views/right_panel/_ThreadPanel.pcss b/res/css/views/right_panel/_ThreadPanel.pcss index a9743d945b6..93efded3044 100644 --- a/res/css/views/right_panel/_ThreadPanel.pcss +++ b/res/css/views/right_panel/_ThreadPanel.pcss @@ -45,7 +45,7 @@ Please see LICENSE files in the repository root for full details. width: 18px; height: 18px; background: currentColor; - mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg"); mask-size: 100%; mask-repeat: no-repeat; float: right; diff --git a/res/css/views/right_panel/_UserInfo.pcss b/res/css/views/right_panel/_UserInfo.pcss index 0deb3d37088..d381d03867d 100644 --- a/res/css/views/right_panel/_UserInfo.pcss +++ b/res/css/views/right_panel/_UserInfo.pcss @@ -26,9 +26,9 @@ Please see LICENSE files in the repository root for full details. height: 16px; width: 16px; padding: 4px; - mask-image: url("$(res)/img/minimise.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/chevron-left.svg"); mask-repeat: no-repeat; - mask-position: 7px center; + mask-position: center; background-color: $header-panel-text-primary-color; } } diff --git a/res/css/views/rooms/_EntityTile.pcss b/res/css/views/rooms/_EntityTile.pcss index 7b23cde43cb..979d5bb5d43 100644 --- a/res/css/views/rooms/_EntityTile.pcss +++ b/res/css/views/rooms/_EntityTile.pcss @@ -31,8 +31,9 @@ Please see LICENSE files in the repository root for full details. position: absolute; top: calc(50% - 8px); /* center */ right: -8px; - mask: url("$(res)/img/member_chevron.png"); + mask: url("@vector-im/compound-design-tokens/icons/chevron-right.svg"); mask-repeat: no-repeat; + mask-position: center; width: 16px; height: 16px; background-color: $header-panel-text-primary-color; diff --git a/res/css/views/rooms/_EventPreview.pcss b/res/css/views/rooms/_EventPreview.pcss new file mode 100644 index 00000000000..0639c76d98d --- /dev/null +++ b/res/css/views/rooms/_EventPreview.pcss @@ -0,0 +1,18 @@ +/* +* Copyright 2024 New Vector Ltd. +* Copyright 2024 The Matrix.org Foundation C.I.C. +* +* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +* Please see LICENSE files in the repository root for full details. + */ + +.mx_EventPreview { + font: var(--cpd-font-body-sm-regular); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + .mx_EventPreview_prefix { + font: var(--cpd-font-body-sm-semibold); + } +} diff --git a/res/css/views/rooms/_LegacyRoomHeader.pcss b/res/css/views/rooms/_LegacyRoomHeader.pcss deleted file mode 100644 index dc41108041d..00000000000 --- a/res/css/views/rooms/_LegacyRoomHeader.pcss +++ /dev/null @@ -1,281 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. -Copyright 2015, 2016 OpenMarket Ltd - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -:root { - --RoomHeader-indicator-dot-size: 8px; - --RoomHeader-indicator-dot-offset: -3px; - --RoomHeader-indicator-pulseColor: $alert; -} - -.mx_LegacyRoomHeader { - flex: 0 0 50px; - border-bottom: 1px solid $primary-hairline-color; - background-color: $background; - - .mx_LegacyRoomHeader_icon { - height: 12px; - width: 12px; - - &.mx_LegacyRoomHeader_icon_video { - height: 14px; - width: 14px; - background-color: $secondary-content; - mask-image: url("$(res)/img/element-icons/call/video-call.svg"); - mask-size: 100%; - } - - &.mx_E2EIcon { - margin: 0; - height: 100%; /* To give the tooltip room to breathe */ - } - } - - .mx_CallDuration { - margin-top: calc(($font-15px - $font-13px) / 2); /* To align with the name */ - font-size: $font-13px; - } -} - -.mx_LegacyRoomHeader_wrapper { - height: 44px; - display: flex; - align-items: center; - min-width: 0; - padding: 10px 20px 9px 16px; - border-bottom: 1px solid $separator; - - .mx_InviteOnlyIcon_large { - margin: 0; - } - - .mx_BetaCard_betaPill { - margin-right: $spacing-8; - } - - /* The container of E2EIcon in the legacy header needs to have its height set */ - & > span { - height: 100%; - } -} - -.mx_LegacyRoomHeader_name { - flex: 0 1 auto; - overflow: hidden; - color: $primary-content; - font: var(--cpd-font-heading-sm-semibold); - font-weight: var(--cpd-font-weight-semibold); - min-height: 24px; - align-items: center; - border-radius: 6px; - margin: 0 3px; - padding: 1px 4px; - display: flex; - user-select: none; - cursor: pointer; - - &:hover { - background-color: $quinary-content; - } - - .mx_LegacyRoomHeader_nametext { - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - } - - .mx_LegacyRoomHeader_chevron { - align-self: center; - width: 20px; - height: 20px; - mask-position: center; - mask-size: 20px; - mask-repeat: no-repeat; - mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); - background-color: $tertiary-content; - } - - &.mx_LegacyRoomHeader_name--textonly { - cursor: unset; - - &:hover { - background-color: unset; - } - } - - &[aria-expanded="true"] { - background-color: $separator; - - .mx_LegacyRoomHeader_chevron { - transform: rotate(180deg); - } - } -} - -.mx_LegacyRoomHeader_settingsHint { - color: $settings-grey-fg-color !important; -} - -.mx_LegacyRoomHeader_searchStatus { - font-weight: normal; - opacity: 0.6; -} - -.mx_RoomTopic { - position: relative; - cursor: pointer; -} - -.mx_LegacyRoomHeader_topic { - $lines: 2; - - flex: 1; - color: $secondary-content; - font: var(--cpd-font-body-sm-regular); - line-height: 1rem; - max-height: calc(1rem * $lines); - - overflow: hidden; - -webkit-line-clamp: $lines; /* See: https://drafts.csswg.org/css-overflow-3/#webkit-line-clamp */ - -webkit-box-orient: vertical; - display: -webkit-box; -} - -.mx_LegacyRoomHeader_topic .mx_Emoji { - /* Undo font size increase to prevent vertical cropping and ensure the same size */ - /* as in plain text emojis */ - font-size: inherit; -} - -.mx_LegacyRoomHeader_avatar { - flex: 0; - margin: 0 7px; - position: relative; - cursor: pointer; -} - -.mx_LegacyRoomHeader_button_unreadIndicator_bg { - position: absolute; - right: var(--RoomHeader-indicator-dot-offset); - top: var(--RoomHeader-indicator-dot-offset); - margin: 4px; - width: var(--RoomHeader-indicator-dot-size); - height: var(--RoomHeader-indicator-dot-size); - border-radius: 50%; - transform: scale(1.6); - transform-origin: center center; - background: $background; -} - -.mx_LegacyRoomHeader_button_unreadIndicator { - position: absolute; - right: var(--RoomHeader-indicator-dot-offset); - top: var(--RoomHeader-indicator-dot-offset); - margin: 4px; - - &.mx_Indicator_highlight { - background: var(--cpd-color-icon-critical-primary); - box-shadow: var(--cpd-color-icon-critical-primary); - } - - &.mx_Indicator_notification { - background: var(--cpd-color-icon-success-primary); - box-shadow: var(--cpd-color-icon-success-primary); - } - - &.mx_Indicator_activity { - background: var(--cpd-color-icon-primary); - box-shadow: var(--cpd-color-icon-primary); - } -} - -.mx_LegacyRoomHeader_forgetButton::before { - mask-image: url("$(res)/img/element-icons/leave.svg"); - width: 26px; -} - -.mx_LegacyRoomHeader_appsButton::before { - mask-image: url("$(res)/img/element-icons/room/apps.svg"); -} - -.mx_LegacyRoomHeader_appsButton_highlight::before { - background-color: $accent; -} - -.mx_LegacyRoomHeader_searchButton::before { - mask-image: url("$(res)/img/element-icons/room/search-inset.svg"); -} - -.mx_LegacyRoomHeader_inviteButton::before { - mask-image: url("$(res)/img/element-icons/room/invite.svg"); -} - -.mx_LegacyRoomHeader_voiceCallButton::before { - mask-image: url("$(res)/img/element-icons/call/voice-call.svg"); - - /* The call button SVG is padded slightly differently, so match it up to the size */ - /* of the other icons */ - mask-size: 20px; - mask-position: center; -} - -.mx_LegacyRoomHeader_videoCallButton::before { - mask-image: url("$(res)/img/element-icons/call/video-call.svg"); -} - -.mx_LegacyRoomHeader_layoutButton--freedom::before, -.mx_LegacyRoomHeader_freedomIcon::before { - mask-image: url("$(res)/img/element-icons/call/freedom.svg"); -} - -.mx_LegacyRoomHeader_layoutButton--spotlight::before, -.mx_LegacyRoomHeader_spotlightIcon::before { - mask-image: url("$(res)/img/element-icons/call/spotlight.svg"); -} - -.mx_LegacyRoomHeader_closeButton { - &::before { - mask-image: url("@vector-im/compound-design-tokens/icons/close.svg"); - mask-size: 20px; - mask-position: center; - } - - &:hover { - background: unset; /* remove background color on hover */ - - &::before { - background-color: $icon-button-color; /* set the default background color */ - } - } -} - -.mx_LegacyRoomHeader_minimiseButton::before { - mask-image: url("$(res)/img/element-icons/reduce.svg"); -} - -.mx_LegacyRoomHeader_layoutMenu .mx_IconizedContextMenu_icon::before { - content: ""; - width: 16px; - height: 16px; - display: block; - mask-position: center; - mask-size: 20px; - mask-repeat: no-repeat; - background: $primary-content; -} - -@media only screen and (max-width: 480px) { - .mx_LegacyRoomHeader_wrapper { - padding: 0; - margin: 0; - } - - .mx_LegacyRoomHeader { - overflow: hidden; - } -} diff --git a/res/css/views/rooms/_LinkPreviewGroup.pcss b/res/css/views/rooms/_LinkPreviewGroup.pcss index e540c149b62..751a394c442 100644 --- a/res/css/views/rooms/_LinkPreviewGroup.pcss +++ b/res/css/views/rooms/_LinkPreviewGroup.pcss @@ -18,7 +18,7 @@ Please see LICENSE files in the repository root for full details. } } - &:hover .mx_LinkPreviewGroup_hide img, + &:hover .mx_LinkPreviewGroup_hide svg, .mx_LinkPreviewGroup_hide:focus-visible:focus svg { visibility: visible; } diff --git a/res/css/views/rooms/_PinnedMessageBanner.pcss b/res/css/views/rooms/_PinnedMessageBanner.pcss index dd753b7c9e7..27c79718338 100644 --- a/res/css/views/rooms/_PinnedMessageBanner.pcss +++ b/res/css/views/rooms/_PinnedMessageBanner.pcss @@ -81,15 +81,7 @@ .mx_PinnedMessageBanner_message { grid-area: message; - font: var(--cpd-font-body-sm-regular); line-height: 20px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - - .mx_PinnedMessageBanner_prefix { - font: var(--cpd-font-body-sm-semibold); - } } .mx_PinnedMessageBanner_redactedMessage { diff --git a/res/css/views/rooms/_RoomHeader.pcss b/res/css/views/rooms/_RoomHeader.pcss index 16bf45435a4..a53d06fd1c0 100644 --- a/res/css/views/rooms/_RoomHeader.pcss +++ b/res/css/views/rooms/_RoomHeader.pcss @@ -88,3 +88,8 @@ Please see LICENSE files in the repository root for full details. .mx_RoomHeader .mx_BaseAvatar { flex-shrink: 0; } + +.mx_RoomHeader_videoCallOption { + /* Workaround for https://github.com/element-hq/compound/issues/331 */ + min-width: 240px; +} diff --git a/res/css/views/rooms/_RoomListHeader.pcss b/res/css/views/rooms/_RoomListHeader.pcss index 07aa1cbf5be..6fbd2a38dbd 100644 --- a/res/css/views/rooms/_RoomListHeader.pcss +++ b/res/css/views/rooms/_RoomListHeader.pcss @@ -42,7 +42,7 @@ Please see LICENSE files in the repository root for full details. mask-size: contain; mask-repeat: no-repeat; background-color: $tertiary-content; - mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg"); } &[aria-expanded="true"] { diff --git a/res/css/views/rooms/_RoomSublist.pcss b/res/css/views/rooms/_RoomSublist.pcss index d4d6f05719a..a8041344301 100644 --- a/res/css/views/rooms/_RoomSublist.pcss +++ b/res/css/views/rooms/_RoomSublist.pcss @@ -160,7 +160,7 @@ Please see LICENSE files in the repository root for full details. mask-size: contain; mask-repeat: no-repeat; background-color: var(--cpd-color-icon-secondary); - mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg"); } &.mx_RoomSublist_collapseBtn_collapsed::before { @@ -276,7 +276,7 @@ Please see LICENSE files in the repository root for full details. .mx_RoomSublist_showMoreButtonChevron, .mx_RoomSublist_showLessButtonChevron { - mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg"); } .mx_RoomSublist_showLessButtonChevron { diff --git a/res/css/views/rooms/_ThreadSummary.pcss b/res/css/views/rooms/_ThreadSummary.pcss index b07c747d29c..118ee512831 100644 --- a/res/css/views/rooms/_ThreadSummary.pcss +++ b/res/css/views/rooms/_ThreadSummary.pcss @@ -53,11 +53,11 @@ Please see LICENSE files in the repository root for full details. content: ""; position: absolute; top: 50%; - right: $spacing-12; + right: var(--cpd-space-1x); transform: translateY(-50%); - width: 12px; - height: 12px; - mask-image: url("$(res)/img/compound/chevron-right-12px.svg"); + width: 24px; + height: 24px; + mask-image: url("@vector-im/compound-design-tokens/icons/chevron-right.svg"); mask-position: center; mask-size: contain; mask-repeat: no-repeat; diff --git a/res/css/views/spaces/_SpaceCreateMenu.pcss b/res/css/views/spaces/_SpaceCreateMenu.pcss index e501852e291..763807d48dc 100644 --- a/res/css/views/spaces/_SpaceCreateMenu.pcss +++ b/res/css/views/spaces/_SpaceCreateMenu.pcss @@ -67,7 +67,7 @@ Please see LICENSE files in the repository root for full details. mask-repeat: no-repeat; mask-position: 2px 3px; mask-size: 24px; - mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg"); } } diff --git a/res/css/views/voip/_CallView.pcss b/res/css/views/voip/_CallView.pcss index 7cb7925cd88..6a6f9757106 100644 --- a/res/css/views/voip/_CallView.pcss +++ b/res/css/views/voip/_CallView.pcss @@ -147,7 +147,7 @@ Please see LICENSE files in the repository root for full details. &::before { content: ""; display: inline-block; - mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg"); mask-size: 20px; mask-position: center; background-color: $call-primary-content; diff --git a/res/img/camera.svg b/res/img/camera.svg deleted file mode 100644 index 6519496f789..00000000000 --- a/res/img/camera.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - icon_camera - Created with Sketch. - - - - - - - diff --git a/res/img/compound/chevron-right-12px.svg b/res/img/compound/chevron-right-12px.svg deleted file mode 100644 index 02f61f36ff3..00000000000 --- a/res/img/compound/chevron-right-12px.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/res/img/compound/retry-16px.svg b/res/img/compound/retry-16px.svg deleted file mode 100644 index 443a0d7b85a..00000000000 --- a/res/img/compound/retry-16px.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/element-icons/call/freedom.svg b/res/img/element-icons/call/freedom.svg deleted file mode 100644 index 0a883b78339..00000000000 --- a/res/img/element-icons/call/freedom.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/element-icons/call/spotlight.svg b/res/img/element-icons/call/spotlight.svg deleted file mode 100644 index f9d96a1e85a..00000000000 --- a/res/img/element-icons/call/spotlight.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/element-icons/reduce.svg b/res/img/element-icons/reduce.svg deleted file mode 100644 index 3179e33a232..00000000000 --- a/res/img/element-icons/reduce.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/res/img/element-icons/retry.svg b/res/img/element-icons/retry.svg deleted file mode 100644 index 6e5b8651fce..00000000000 --- a/res/img/element-icons/retry.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/res/img/element-icons/warning-badge.svg b/res/img/element-icons/warning-badge.svg deleted file mode 100644 index 09e0944bdb3..00000000000 --- a/res/img/element-icons/warning-badge.svg +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - diff --git a/res/img/element-icons/warning.svg b/res/img/element-icons/warning.svg deleted file mode 100644 index eef51931408..00000000000 --- a/res/img/element-icons/warning.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/feather-customised/chevron-down.svg b/res/img/feather-customised/chevron-down.svg deleted file mode 100644 index a091913b42e..00000000000 --- a/res/img/feather-customised/chevron-down.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/feather-customised/edit.svg b/res/img/feather-customised/edit.svg deleted file mode 100644 index f511aa14772..00000000000 --- a/res/img/feather-customised/edit.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/res/img/feather-customised/search-input.svg b/res/img/feather-customised/search-input.svg deleted file mode 100644 index 028b84d5598..00000000000 --- a/res/img/feather-customised/search-input.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/res/img/location/pointer.svg b/res/img/location/pointer.svg deleted file mode 100644 index 8a7c5edf712..00000000000 --- a/res/img/location/pointer.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/markdown.svg b/res/img/markdown.svg deleted file mode 100644 index 9aadd3cb7f8..00000000000 --- a/res/img/markdown.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/member_chevron.png b/res/img/member_chevron.png deleted file mode 100644 index cbbd289dcf4..00000000000 Binary files a/res/img/member_chevron.png and /dev/null differ diff --git a/res/img/minimise.svg b/res/img/minimise.svg deleted file mode 100644 index eecf181f617..00000000000 --- a/res/img/minimise.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - minimise - Created with sketchtool. - - - - - - - - - - - - - diff --git a/res/img/tick-circle.svg b/res/img/tick-circle.svg deleted file mode 100644 index 7cedb629853..00000000000 --- a/res/img/tick-circle.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/res/img/voip/signal-bars.svg b/res/img/voip/signal-bars.svg deleted file mode 100644 index 6802ba2d34b..00000000000 --- a/res/img/voip/signal-bars.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/res/jitsi_external_api.min.js b/res/jitsi_external_api.min.js index 880aec5b21b..2bbee8305f2 100644 --- a/res/jitsi_external_api.min.js +++ b/res/jitsi_external_api.min.js @@ -1,2 +1,2 @@ -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.JitsiMeetExternalAPI=t():e.JitsiMeetExternalAPI=t()}(self,(()=>(()=>{var e={372:(e,t,n)=>{"use strict";n.d(t,{default:()=>N});var r=n(620),i=n.n(r);class s extends r{constructor(){var e,t,n;super(...arguments),e=this,n={},(t=function(e){var t=function(e,t){if("object"!=typeof e||null===e)return e;var n=e[Symbol.toPrimitive];if(void 0!==n){var r=n.call(e,t);if("object"!=typeof r)return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(e)}(e,"string");return"symbol"==typeof t?t:String(t)}(t="_storage"))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n}clear(){this._storage={}}get length(){return Object.keys(this._storage).length}getItem(e){return this._storage[e]}setItem(e,t){this._storage[e]=t}removeItem(e){delete this._storage[e]}key(e){const t=Object.keys(this._storage);if(!(t.length<=e))return t[e]}serialize(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];if(0===e.length)return JSON.stringify(this._storage);const t={...this._storage};return e.forEach((e=>{delete t[e]})),JSON.stringify(t)}}const o=new class extends r{constructor(){super();try{this._storage=window.localStorage,this._localStorageDisabled=!1}catch(e){}this._storage||(console.warn("Local storage is disabled."),this._storage=new s,this._localStorageDisabled=!0)}isLocalStorageDisabled(){return this._localStorageDisabled}setLocalStorageDisabled(e){this._localStorageDisabled=e;try{this._storage=e?new s:window.localStorage}catch(e){}this._storage||(this._storage=new s)}clear(){this._storage.clear(),this.emit("changed")}get length(){return this._storage.length}getItem(e){return this._storage.getItem(e)}setItem(e,t){let n=arguments.length>2&&void 0!==arguments[2]&&arguments[2];this._storage.setItem(e,t),n||this.emit("changed")}removeItem(e){this._storage.removeItem(e),this.emit("changed")}key(e){return this._storage.key(e)}serialize(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];if(this.isLocalStorageDisabled())return this._storage.serialize(e);const t=this._storage.length,n={};for(let r=0;r0&&void 0!==arguments[0]?arguments[0]:{};this.postis=function(e){var t,n=e.scope,r=e.window,i=e.windowForEventListening||window,s=e.allowedOrigin,o={},a=[],d={},l=!1,u="__ready__",p=function(e){var t;try{t=c(e.data)}catch(e){return}if((!s||e.origin===s)&&t&&t.postis&&t.scope===n){var r=o[t.method];if(r)for(var i=0;i{},this.postis.listen(v,(e=>this._receiveCallback(e)))}dispose(){this.postis.destroy()}send(e){this.postis.send({method:v,params:e})}setReceiveCallback(e){this._receiveCallback=e}}const _="request",b="response";class w{constructor(){let{backend:e}=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};this._listeners=new Map,this._requestID=0,this._responseHandlers=new Map,this._unprocessedMessages=new Set,this.addListener=this.on,e&&this.setBackend(e)}_disposeBackend(){this._backend&&(this._backend.dispose(),this._backend=null)}_onMessageReceived(e){if(e.type===b){const t=this._responseHandlers.get(e.id);t&&(t(e),this._responseHandlers.delete(e.id))}else e.type===_?this.emit("request",e.data,((t,n)=>{this._backend.send({type:b,error:n,id:e.id,result:t})})):this.emit("event",e.data)}dispose(){this._responseHandlers.clear(),this._unprocessedMessages.clear(),this.removeAllListeners(),this._disposeBackend()}emit(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r{s=e(...n)||s})),s||this._unprocessedMessages.add(n),s}on(e,t){let n=this._listeners.get(e);return n||(n=new Set,this._listeners.set(e,n)),n.add(t),this._unprocessedMessages.forEach((e=>{t(...e)&&this._unprocessedMessages.delete(e)})),this}removeAllListeners(e){return e?this._listeners.delete(e):this._listeners.clear(),this}removeListener(e,t){const n=this._listeners.get(e);return n&&n.delete(t),this}sendEvent(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};this._backend&&this._backend.send({type:"event",data:e})}sendRequest(e){if(!this._backend)return Promise.reject(new Error("No transport backend defined!"));this._requestID++;const t=this._requestID;return new Promise(((n,r)=>{this._responseHandlers.set(t,(e=>{let{error:t,result:i}=e;void 0!==i?n(i):r(void 0!==t?t:new Error("Unexpected response format!"))}));try{this._backend.send({type:_,data:e,id:t})}catch(e){this._responseHandlers.delete(t),r(e)}}))}setBackend(e){this._disposeBackend(),this._backend=e,this._backend.setReceiveCallback(this._onMessageReceived.bind(this))}}let L;try{L=function(e,t=!1,n="hash"){if(!e)return{};"string"==typeof e&&(e=new URL(e));const r="search"===n?e.search:e.hash,i={},s=r?.substr(1).split("&")||[];if("hash"===n&&1===s.length){const e=s[0];if(e.startsWith("/")&&1===e.split("&").length)return i}return s.forEach((e=>{const n=e.split("="),r=n[0];if(!r||r.split(".").some((e=>d.includes(e))))return;let s;try{if(s=n[1],!t){const e=decodeURIComponent(s).replace(/\\&/,"&");s="undefined"===e?void 0:c(e)}}catch(e){return void function(e,t=""){console.error(t,e),window.onerror?.(t,void 0,void 0,void 0,e)}(e,`Failed to parse URL parameter value: ${String(s)}`)}i[r]=s})),i}(window.location).jitsi_meet_external_api_id}catch(e){}(window.JitsiMeetJS||(window.JitsiMeetJS={}),window.JitsiMeetJS.app||(window.JitsiMeetJS.app={}),window.JitsiMeetJS.app).setExternalTransportBackend=e=>undefined.setBackend(e);var k=n(860);const C=n.n(k)().getLogger("modules/API/external/functions.js");function E(e,t){return e.sendRequest({type:"devices",name:"setDevice",device:t})}const S=["css/all.css","libs/alwaysontop.min.js"],x={addBreakoutRoom:"add-breakout-room",answerKnockingParticipant:"answer-knocking-participant",approveVideo:"approve-video",askToUnmute:"ask-to-unmute",autoAssignToBreakoutRooms:"auto-assign-to-breakout-rooms",avatarUrl:"avatar-url",cancelPrivateChat:"cancel-private-chat",closeBreakoutRoom:"close-breakout-room",displayName:"display-name",endConference:"end-conference",email:"email",grantModerator:"grant-moderator",hangup:"video-hangup",hideNotification:"hide-notification",initiatePrivateChat:"initiate-private-chat",joinBreakoutRoom:"join-breakout-room",localSubject:"local-subject",kickParticipant:"kick-participant",muteEveryone:"mute-everyone",overwriteConfig:"overwrite-config",overwriteNames:"overwrite-names",password:"password",pinParticipant:"pin-participant",rejectParticipant:"reject-participant",removeBreakoutRoom:"remove-breakout-room",resizeFilmStrip:"resize-film-strip",resizeLargeVideo:"resize-large-video",sendCameraFacingMode:"send-camera-facing-mode-message",sendChatMessage:"send-chat-message",sendEndpointTextMessage:"send-endpoint-text-message",sendParticipantToRoom:"send-participant-to-room",sendTones:"send-tones",setAssumedBandwidthBps:"set-assumed-bandwidth-bps",setFollowMe:"set-follow-me",setLargeVideoParticipant:"set-large-video-participant",setMediaEncryptionKey:"set-media-encryption-key",setNoiseSuppressionEnabled:"set-noise-suppression-enabled",setParticipantVolume:"set-participant-volume",setSubtitles:"set-subtitles",setTileView:"set-tile-view",setVideoQuality:"set-video-quality",showNotification:"show-notification",startRecording:"start-recording",startShareVideo:"start-share-video",stopRecording:"stop-recording",stopShareVideo:"stop-share-video",subject:"subject",submitFeedback:"submit-feedback",toggleAudio:"toggle-audio",toggleCamera:"toggle-camera",toggleCameraMirror:"toggle-camera-mirror",toggleChat:"toggle-chat",toggleE2EE:"toggle-e2ee",toggleFilmStrip:"toggle-film-strip",toggleLobby:"toggle-lobby",toggleModeration:"toggle-moderation",toggleNoiseSuppression:"toggle-noise-suppression",toggleParticipantsPane:"toggle-participants-pane",toggleRaiseHand:"toggle-raise-hand",toggleShareScreen:"toggle-share-screen",toggleSubtitles:"toggle-subtitles",toggleTileView:"toggle-tile-view",toggleVirtualBackgroundDialog:"toggle-virtual-background",toggleVideo:"toggle-video",toggleWhiteboard:"toggle-whiteboard"},O={"avatar-changed":"avatarChanged","audio-availability-changed":"audioAvailabilityChanged","audio-mute-status-changed":"audioMuteStatusChanged","audio-or-video-sharing-toggled":"audioOrVideoSharingToggled","breakout-rooms-updated":"breakoutRoomsUpdated","browser-support":"browserSupport","camera-error":"cameraError","chat-updated":"chatUpdated","compute-pressure-changed":"computePressureChanged","content-sharing-participants-changed":"contentSharingParticipantsChanged","data-channel-closed":"dataChannelClosed","data-channel-opened":"dataChannelOpened","device-list-changed":"deviceListChanged","display-name-change":"displayNameChange","dominant-speaker-changed":"dominantSpeakerChanged","email-change":"emailChange","error-occurred":"errorOccurred","endpoint-text-message-received":"endpointTextMessageReceived","face-landmark-detected":"faceLandmarkDetected","feedback-submitted":"feedbackSubmitted","feedback-prompt-displayed":"feedbackPromptDisplayed","filmstrip-display-changed":"filmstripDisplayChanged","incoming-message":"incomingMessage","knocking-participant":"knockingParticipant",log:"log","mic-error":"micError","moderation-participant-approved":"moderationParticipantApproved","moderation-participant-rejected":"moderationParticipantRejected","moderation-status-changed":"moderationStatusChanged","mouse-enter":"mouseEnter","mouse-leave":"mouseLeave","mouse-move":"mouseMove","non-participant-message-received":"nonParticipantMessageReceived","notification-triggered":"notificationTriggered","outgoing-message":"outgoingMessage","p2p-status-changed":"p2pStatusChanged","participant-joined":"participantJoined","participant-kicked-out":"participantKickedOut","participant-left":"participantLeft","participant-role-changed":"participantRoleChanged","participants-pane-toggled":"participantsPaneToggled","password-required":"passwordRequired","peer-connection-failure":"peerConnectionFailure","prejoin-screen-loaded":"prejoinScreenLoaded","proxy-connection-event":"proxyConnectionEvent","raise-hand-updated":"raiseHandUpdated",ready:"ready","recording-link-available":"recordingLinkAvailable","recording-status-changed":"recordingStatusChanged","participant-menu-button-clicked":"participantMenuButtonClick","video-ready-to-close":"readyToClose","video-conference-joined":"videoConferenceJoined","video-conference-left":"videoConferenceLeft","video-availability-changed":"videoAvailabilityChanged","video-mute-status-changed":"videoMuteStatusChanged","video-quality-changed":"videoQualityChanged","screen-sharing-status-changed":"screenSharingStatusChanged","subject-change":"subjectChange","suspend-detected":"suspendDetected","tile-view-changed":"tileViewChanged","toolbar-button-clicked":"toolbarButtonClicked","transcription-chunk-received":"transcriptionChunkReceived","whiteboard-status-changed":"whiteboardStatusChanged"},R={"_request-desktop-sources":"_requestDesktopSources"};let j=0;function I(e,t){e._numberOfParticipants+=t}function P(e){let t;return"string"==typeof e&&null!==String(e).match(/([0-9]*\.?[0-9]+)(em|pt|px|((d|l|s)?v)(h|w)|%)$/)?t=e:"number"==typeof e&&(t=`${e}px`),t}class N extends(i()){constructor(e){super();for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r1&&void 0!==arguments[1]?arguments[1]:{},url:`https://${e}/#jitsi_meet_external_api_id=${j}`})}(e,{configOverwrite:d,iceServers:f,interfaceConfigOverwrite:l,jwt:u,lang:p,roomName:i,devices:v,userInfo:_,appData:{localStorageContent:C},release:L}),this._createIFrame(a,s,k),this._transport=new w({backend:new y({postisOptions:{allowedOrigin:new URL(this._url).origin,scope:`jitsi_meet_external_api_${j}`,window:this._frame.contentWindow}})}),Array.isArray(g)&&g.length>0&&this.invite(g),this._onload=h,this._tmpE2EEKey=b,this._isLargeVideoVisible=!1,this._isPrejoinVideoVisible=!1,this._numberOfParticipants=0,this._participants={},this._myUserID=void 0,this._onStageParticipant=void 0,this._setupListeners(),j++}_createIFrame(e,t,n){const r=`jitsiConferenceFrame${j}`;this._frame=document.createElement("iframe"),this._frame.allow=["autoplay","camera","clipboard-write","compute-pressure","display-capture","hid","microphone","screen-wake-lock"].join("; "),this._frame.name=r,this._frame.id=r,this._setSize(e,t),this._frame.setAttribute("allowFullScreen","true"),this._frame.style.border=0,n&&(this._frame.sandbox=n),this._frame.src=this._url,this._frame=this._parentNode.appendChild(this._frame)}_getAlwaysOnTopResources(){const e=this._frame.contentWindow,t=e.document;let n="";const r=t.querySelector("base");if(r&&r.href)n=r.href;else{const{protocol:t,host:r}=e.location;n=`${t}//${r}`}return S.map((e=>new URL(e,n).href))}_getFormattedDisplayName(e){const{formattedDisplayName:t}=this._participants[e]||{};return t}_getOnStageParticipant(){return this._onStageParticipant}_getLargeVideo(){const e=this.getIFrame();if(this._isLargeVideoVisible&&e&&e.contentWindow&&e.contentWindow.document)return e.contentWindow.document.getElementById("largeVideo")}_getPrejoinVideo(){const e=this.getIFrame();if(this._isPrejoinVideoVisible&&e&&e.contentWindow&&e.contentWindow.document)return e.contentWindow.document.getElementById("prejoinVideo")}_getParticipantVideo(e){const t=this.getIFrame();if(t&&t.contentWindow&&t.contentWindow.document)return void 0===e||e===this._myUserID?t.contentWindow.document.getElementById("localVideo_container"):t.contentWindow.document.querySelector(`#participant_${e} video`)}_setSize(e,t){const n=P(e),r=P(t);void 0!==n&&(this._height=e,this._frame.style.height=n),void 0!==r&&(this._width=t,this._frame.style.width=r)}_setupListeners(){this._transport.on("event",(e=>{let{name:t,...n}=e;const r=n.id;switch(t){case"ready":var i;null===(i=this._onload)||void 0===i||i.call(this);break;case"video-conference-joined":if(void 0!==this._tmpE2EEKey){const e=e=>{const t=[];for(let n=0;n{const n=R[e.name],r={...e,name:n};n&&this.emit(n,r,t)}))}updateNumberOfParticipants(e){if(!e||!Object.keys(e).length)return;const t=Object.keys(e).reduce(((t,n)=>{var r;return null!==(r=e[n])&&void 0!==r&&r.participants?Object.keys(e[n].participants).length+t:t}),0);this._numberOfParticipants=t}async getRoomsInfo(){return this._transport.sendRequest({name:"rooms-info"})}isP2pActive(){return this._transport.sendRequest({name:"get-p2p-status"})}addEventListener(e,t){this.on(e,t)}addEventListeners(e){for(const t in e)this.addEventListener(t,e[t])}captureLargeVideoScreenshot(){return this._transport.sendRequest({name:"capture-largevideo-screenshot"})}dispose(){this.emit("_willDispose"),this._transport.dispose(),this.removeAllListeners(),this._frame&&this._frame.parentNode&&this._frame.parentNode.removeChild(this._frame)}executeCommand(e){if(e in x){for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r(C.error(e),{})))}(this._transport)}getContentSharingParticipants(){return this._transport.sendRequest({name:"get-content-sharing-participants"})}getCurrentDevices(){return function(e){return e.sendRequest({type:"devices",name:"getCurrentDevices"}).catch((e=>(C.error(e),{})))}(this._transport)}getCustomAvatarBackgrounds(){return this._transport.sendRequest({name:"get-custom-avatar-backgrounds"})}getLivestreamUrl(){return this._transport.sendRequest({name:"get-livestream-url"})}getParticipantsInfo(){const e=Object.keys(this._participants),t=Object.values(this._participants);return t.forEach(((t,n)=>{t.participantId=e[n]})),t}getVideoQuality(){return this._videoQuality}isAudioAvailable(){return this._transport.sendRequest({name:"is-audio-available"})}isDeviceChangeAvailable(e){return function(e,t){return e.sendRequest({deviceType:t,type:"devices",name:"isDeviceChangeAvailable"})}(this._transport,e)}isDeviceListAvailable(){return function(e){return e.sendRequest({type:"devices",name:"isDeviceListAvailable"})}(this._transport)}isMultipleAudioInputSupported(){return function(e){return e.sendRequest({type:"devices",name:"isMultipleAudioInputSupported"})}(this._transport)}invite(e){return Array.isArray(e)&&0!==e.length?this._transport.sendRequest({name:"invite",invitees:e}):Promise.reject(new TypeError("Invalid Argument"))}isAudioMuted(){return this._transport.sendRequest({name:"is-audio-muted"})}isAudioDisabled(){return this._transport.sendRequest({name:"is-audio-disabled"})}isModerationOn(e){return this._transport.sendRequest({name:"is-moderation-on",mediaType:e})}isParticipantForceMuted(e,t){return this._transport.sendRequest({name:"is-participant-force-muted",participantId:e,mediaType:t})}isParticipantsPaneOpen(){return this._transport.sendRequest({name:"is-participants-pane-open"})}isSharingScreen(){return this._transport.sendRequest({name:"is-sharing-screen"})}isStartSilent(){return this._transport.sendRequest({name:"is-start-silent"})}getAvatarURL(e){const{avatarURL:t}=this._participants[e]||{};return t}getDeploymentInfo(){return this._transport.sendRequest({name:"deployment-info"})}getDisplayName(e){const{displayName:t}=this._participants[e]||{};return t}getEmail(e){const{email:t}=this._participants[e]||{};return t}getIFrame(){return this._frame}getNumberOfParticipants(){return this._numberOfParticipants}getSupportedCommands(){return Object.keys(x)}getSupportedEvents(){return Object.values(O)}isVideoAvailable(){return this._transport.sendRequest({name:"is-video-available"})}isVideoMuted(){return this._transport.sendRequest({name:"is-video-muted"})}listBreakoutRooms(){return this._transport.sendRequest({name:"list-breakout-rooms"})}_isNewElectronScreensharingSupported(){return this._transport.sendRequest({name:"_new_electron_screensharing_supported"})}pinParticipant(e,t){this.executeCommand("pinParticipant",e,t)}removeEventListener(e){this.removeAllListeners(e)}removeEventListeners(e){e.forEach((e=>this.removeEventListener(e)))}resizeLargeVideo(e,t){e<=this._width&&t<=this._height&&this.executeCommand("resizeLargeVideo",e,t)}sendProxyConnectionEvent(e){this._transport.sendEvent({data:[e],name:"proxy-connection-event"})}setAudioInputDevice(e,t){return function(e,t,n){return E(e,{id:n,kind:"audioinput",label:t})}(this._transport,e,t)}setAudioOutputDevice(e,t){return function(e,t,n){return E(e,{id:n,kind:"audiooutput",label:t})}(this._transport,e,t)}setLargeVideoParticipant(e,t){this.executeCommand("setLargeVideoParticipant",e,t)}setVideoInputDevice(e,t){return function(e,t,n){return E(e,{id:n,kind:"videoinput",label:t})}(this._transport,e,t)}startRecording(e){this.executeCommand("startRecording",e)}stopRecording(e){this.executeCommand("stopRecording",e)}toggleE2EE(e){this.executeCommand("toggleE2EE",e)}async setMediaEncryptionKey(e){const{key:t,index:n}=e;if(t){const e=await crypto.subtle.exportKey("raw",t);this.executeCommand("setMediaEncryptionKey",JSON.stringify({exportedKey:Array.from(new Uint8Array(e)),index:n}))}else this.executeCommand("setMediaEncryptionKey",JSON.stringify({exportedKey:!1,index:n}))}}},872:(e,t,n)=>{e.exports=n(372).default},571:(e,t)=>{"use strict";const n=/"(?:_|\\u005[Ff])(?:_|\\u005[Ff])(?:p|\\u0070)(?:r|\\u0072)(?:o|\\u006[Ff])(?:t|\\u0074)(?:o|\\u006[Ff])(?:_|\\u005[Ff])(?:_|\\u005[Ff])"\s*\:/;t.parse=function(e){const r="object"==typeof(arguments.length<=1?void 0:arguments[1])&&(arguments.length<=1?void 0:arguments[1]),i=(arguments.length<=1?0:arguments.length-1)>1||!r?arguments.length<=1?void 0:arguments[1]:void 0,s=(arguments.length<=1?0:arguments.length-1)>1&&(arguments.length<=2?void 0:arguments[2])||r||{},o=JSON.parse(e,i);return"ignore"===s.protoAction?o:o&&"object"==typeof o&&e.match(n)?(t.scan(o,s),o):o},t.scan=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=[e];for(;n.length;){const e=n;n=[];for(const r of e){if(Object.prototype.hasOwnProperty.call(r,"__proto__")){if("remove"!==t.protoAction)throw new SyntaxError("Object contains forbidden prototype property");delete r.__proto__}for(const e in r){const t=r[e];t&&"object"==typeof t&&n.push(r[e])}}}},t.safeParse=function(e,n){try{return t.parse(e,n)}catch(e){return null}}},369:(e,t,n)=>{var r=n(7);function i(e,t){this.logStorage=e,this.stringifyObjects=!(!t||!t.stringifyObjects)&&t.stringifyObjects,this.storeInterval=t&&t.storeInterval?t.storeInterval:3e4,this.maxEntryLength=t&&t.maxEntryLength?t.maxEntryLength:1e4,Object.values(r.levels).forEach(function(e){this[e]=function(){this._log.apply(this,arguments)}.bind(this,e)}.bind(this)),this.storeLogsIntervalID=null,this.queue=[],this.totalLen=0,this.outputCache=[]}i.prototype.stringify=function(e){try{return JSON.stringify(e)}catch(e){return"[object with circular refs?]"}},i.prototype.formatLogMessage=function(e){for(var t="",n=1,r=arguments.length;n=this.maxEntryLength&&this._flush(!0,!0)},i.prototype.start=function(){this._reschedulePublishInterval()},i.prototype._reschedulePublishInterval=function(){this.storeLogsIntervalID&&(window.clearTimeout(this.storeLogsIntervalID),this.storeLogsIntervalID=null),this.storeLogsIntervalID=window.setTimeout(this._flush.bind(this,!1,!0),this.storeInterval)},i.prototype.flush=function(){this._flush(!1,!0)},i.prototype._storeLogs=function(e){try{this.logStorage.storeLogs(e)}catch(e){console.error("LogCollector error when calling logStorage.storeLogs(): ",e)}},i.prototype._flush=function(e,t){var n=!1;try{n=this.logStorage.isReady()}catch(e){console.error("LogCollector error when calling logStorage.isReady(): ",e)}this.totalLen>0&&(n||e)&&(n?(this.outputCache.length&&(this.outputCache.forEach(function(e){this._storeLogs(e)}.bind(this)),this.outputCache=[]),this._storeLogs(this.queue)):this.outputCache.push(this.queue),this.queue=[],this.totalLen=0),t&&this._reschedulePublishInterval()},i.prototype.stop=function(){this._flush(!1,!1)},e.exports=i},7:e=>{var t={trace:0,debug:1,info:2,log:3,warn:4,error:5};o.consoleTransport=console;var n=[o.consoleTransport];o.addGlobalTransport=function(e){-1===n.indexOf(e)&&n.push(e)},o.removeGlobalTransport=function(e){var t=n.indexOf(e);-1!==t&&n.splice(t,1)};var r={};function i(){var e={methodName:"",fileLocation:"",line:null,column:null},t=new Error,n=t.stack?t.stack.split("\n"):[];if(!n||n.length<3)return e;var r=null;return n[3]&&(r=n[3].match(/\s*at\s*(.+?)\s*\((\S*)\s*:(\d*)\s*:(\d*)\)/)),!r||r.length<=4?(0===n[2].indexOf("log@")?e.methodName=n[3].substr(0,n[3].indexOf("@")):e.methodName=n[2].substr(0,n[2].indexOf("@")),e):(e.methodName=r[1],e.fileLocation=r[2],e.line=r[3],e.column=r[4],e)}function s(){var e=arguments[0],s=arguments[1],o=Array.prototype.slice.call(arguments,2);if(!(t[s]1&&p.push("<"+a.methodName+">: ");var h=p.concat(o);try{u.bind(l).apply(l,h)}catch(e){console.error("An error occured when trying to log with one of the available transports",e)}}}}function o(e,n,r,i){this.id=n,this.options=i||{},this.transports=r,this.transports||(this.transports=[]),this.level=t[e];for(var o=Object.keys(t),a=0;a{var r=n(7),i=n(369),s={},o=[],a=r.levels.TRACE;e.exports={addGlobalTransport:function(e){r.addGlobalTransport(e)},removeGlobalTransport:function(e){r.removeGlobalTransport(e)},setGlobalOptions:function(e){r.setGlobalOptions(e)},getLogger:function(e,t,n){var i=new r(a,e,t,n);return e?(s[e]=s[e]||[],s[e].push(i)):o.push(i),i},getUntrackedLogger:function(e,t,n){return new r(a,e,t,n)},setLogLevelById:function(e,t){for(var n=t?s[t]||[]:o,r=0;r{"use strict";var t,n="object"==typeof Reflect?Reflect:null,r=n&&"function"==typeof n.apply?n.apply:function(e,t,n){return Function.prototype.apply.call(e,t,n)};t=n&&"function"==typeof n.ownKeys?n.ownKeys:Object.getOwnPropertySymbols?function(e){return Object.getOwnPropertyNames(e).concat(Object.getOwnPropertySymbols(e))}:function(e){return Object.getOwnPropertyNames(e)};var i=Number.isNaN||function(e){return e!=e};function s(){s.init.call(this)}e.exports=s,e.exports.once=function(e,t){return new Promise((function(n,r){function i(n){e.removeListener(t,s),r(n)}function s(){"function"==typeof e.removeListener&&e.removeListener("error",i),n([].slice.call(arguments))}m(e,t,s,{once:!0}),"error"!==t&&function(e,t,n){"function"==typeof e.on&&m(e,"error",t,{once:!0})}(e,i)}))},s.EventEmitter=s,s.prototype._events=void 0,s.prototype._eventsCount=0,s.prototype._maxListeners=void 0;var o=10;function a(e){if("function"!=typeof e)throw new TypeError('The "listener" argument must be of type Function. Received type '+typeof e)}function c(e){return void 0===e._maxListeners?s.defaultMaxListeners:e._maxListeners}function d(e,t,n,r){var i,s,o,d;if(a(n),void 0===(s=e._events)?(s=e._events=Object.create(null),e._eventsCount=0):(void 0!==s.newListener&&(e.emit("newListener",t,n.listener?n.listener:n),s=e._events),o=s[t]),void 0===o)o=s[t]=n,++e._eventsCount;else if("function"==typeof o?o=s[t]=r?[n,o]:[o,n]:r?o.unshift(n):o.push(n),(i=c(e))>0&&o.length>i&&!o.warned){o.warned=!0;var l=new Error("Possible EventEmitter memory leak detected. "+o.length+" "+String(t)+" listeners added. Use emitter.setMaxListeners() to increase limit");l.name="MaxListenersExceededWarning",l.emitter=e,l.type=t,l.count=o.length,d=l,console&&console.warn&&console.warn(d)}return e}function l(){if(!this.fired)return this.target.removeListener(this.type,this.wrapFn),this.fired=!0,0===arguments.length?this.listener.call(this.target):this.listener.apply(this.target,arguments)}function u(e,t,n){var r={fired:!1,wrapFn:void 0,target:e,type:t,listener:n},i=l.bind(r);return i.listener=n,r.wrapFn=i,i}function p(e,t,n){var r=e._events;if(void 0===r)return[];var i=r[t];return void 0===i?[]:"function"==typeof i?n?[i.listener||i]:[i]:n?function(e){for(var t=new Array(e.length),n=0;n0&&(o=t[0]),o instanceof Error)throw o;var a=new Error("Unhandled error."+(o?" ("+o.message+")":""));throw a.context=o,a}var c=s[e];if(void 0===c)return!1;if("function"==typeof c)r(c,this,t);else{var d=c.length,l=g(c,d);for(n=0;n=0;s--)if(n[s]===t||n[s].listener===t){o=n[s].listener,i=s;break}if(i<0)return this;0===i?n.shift():function(e,t){for(;t+1=0;r--)this.removeListener(e,t[r]);return this},s.prototype.listeners=function(e){return p(this,e,!0)},s.prototype.rawListeners=function(e){return p(this,e,!1)},s.listenerCount=function(e,t){return"function"==typeof e.listenerCount?e.listenerCount(t):h.call(e,t)},s.prototype.listenerCount=h,s.prototype.eventNames=function(){return this._eventsCount>0?t(this._events):[]}}},t={};function n(r){var i=t[r];if(void 0!==i)return i.exports;var s=t[r]={exports:{}};return e[r](s,s.exports,n),s.exports}return n.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return n.d(t,{a:t}),t},n.d=(e,t)=>{for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),n(872)})())); +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.JitsiMeetExternalAPI=t():e.JitsiMeetExternalAPI=t()}(self,(()=>(()=>{var e={372:(e,t,n)=>{"use strict";n.d(t,{default:()=>D});var r=n(620),i=n.n(r);class s extends r{constructor(){var e,t,n;super(...arguments),e=this,n={},(t=function(e){var t=function(e,t){if("object"!=typeof e||!e)return e;var n=e[Symbol.toPrimitive];if(void 0!==n){var r=n.call(e,"string");if("object"!=typeof r)return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(e)}(e);return"symbol"==typeof t?t:t+""}(t="_storage"))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n}clear(){this._storage={}}get length(){return Object.keys(this._storage).length}getItem(e){return this._storage[e]}setItem(e,t){this._storage[e]=t}removeItem(e){delete this._storage[e]}key(e){const t=Object.keys(this._storage);if(!(t.length<=e))return t[e]}serialize(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];if(0===e.length)return JSON.stringify(this._storage);const t={...this._storage};return e.forEach((e=>{delete t[e]})),JSON.stringify(t)}}const o=new class extends r{constructor(){super();try{this._storage=window.localStorage,this._localStorageDisabled=!1}catch(e){}this._storage||(console.warn("Local storage is disabled."),this._storage=new s,this._localStorageDisabled=!0)}isLocalStorageDisabled(){return this._localStorageDisabled}setLocalStorageDisabled(e){this._localStorageDisabled=e;try{this._storage=e?new s:window.localStorage}catch(e){}this._storage||(this._storage=new s)}clear(){this._storage.clear(),this.emit("changed")}get length(){return this._storage.length}getItem(e){return this._storage.getItem(e)}setItem(e,t){let n=arguments.length>2&&void 0!==arguments[2]&&arguments[2];this._storage.setItem(e,t),n||this.emit("changed")}removeItem(e){this._storage.removeItem(e),this.emit("changed")}key(e){return this._storage.key(e)}serialize(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];if(this.isLocalStorageDisabled())return this._storage.serialize(e);const t=this._storage.length,n={};for(let r=0;r0&&void 0!==arguments[0]?arguments[0]:{};this.postis=function(e){var t,n=e.scope,r=e.window,i=e.windowForEventListening||window,s=e.allowedOrigin,o={},a=[],l={},d=!1,u="__ready__",h=function(e){var t;try{t=c(e.data)}catch(e){return}if((!s||e.origin===s)&&t&&t.postis&&t.scope===n){var r=o[t.method];if(r)for(var i=0;i{},this.postis.listen(_,(e=>this._receiveCallback(e)))}dispose(){this.postis.destroy()}send(e){this.postis.send({method:_,params:e})}setReceiveCallback(e){this._receiveCallback=e}}const w="request",L="response";class k{constructor(){let{backend:e}=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};this._listeners=new Map,this._requestID=0,this._responseHandlers=new Map,this._unprocessedMessages=new Set,this.addListener=this.on,e&&this.setBackend(e)}_disposeBackend(){this._backend&&(this._backend.dispose(),this._backend=null)}_onMessageReceived(e){if(e.type===L){const t=this._responseHandlers.get(e.id);t&&(t(e),this._responseHandlers.delete(e.id))}else e.type===w?this.emit("request",e.data,((t,n)=>{this._backend.send({type:L,error:n,id:e.id,result:t})})):this.emit("event",e.data)}dispose(){this._responseHandlers.clear(),this._unprocessedMessages.clear(),this.removeAllListeners(),this._disposeBackend()}emit(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r{s=e(...n)||s})),s||this._unprocessedMessages.add(n),s}on(e,t){let n=this._listeners.get(e);return n||(n=new Set,this._listeners.set(e,n)),n.add(t),this._unprocessedMessages.forEach((e=>{t(...e)&&this._unprocessedMessages.delete(e)})),this}removeAllListeners(e){return e?this._listeners.delete(e):this._listeners.clear(),this}removeListener(e,t){const n=this._listeners.get(e);return n&&n.delete(t),this}sendEvent(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};this._backend&&this._backend.send({type:"event",data:e})}sendRequest(e){if(!this._backend)return Promise.reject(new Error("No transport backend defined!"));this._requestID++;const t=this._requestID;return new Promise(((n,r)=>{this._responseHandlers.set(t,(e=>{let{error:t,result:i}=e;void 0!==i?n(i):r(void 0!==t?t:new Error("Unexpected response format!"))}));try{this._backend.send({type:w,data:e,id:t})}catch(e){this._responseHandlers.delete(t),r(e)}}))}setBackend(e){this._disposeBackend(),this._backend=e,this._backend.setReceiveCallback(this._onMessageReceived.bind(this))}}let C;try{C=function(e,t=!1,n="hash"){if(!e)return{};"string"==typeof e&&(e=new URL(e));const r="search"===n?e.search:e.hash,i={},s=r?.substr(1).split("&")||[];if("hash"===n&&1===s.length){const e=s[0];if(e.startsWith("/")&&1===e.split("&").length)return i}return s.forEach((e=>{const n=e.split("="),r=n[0];if(!r||r.split(".").some((e=>l.includes(e))))return;let s;try{if(s=n[1],!t){const e=decodeURIComponent(s).replace(/\\&/,"&").replace(/[\u2018\u2019]/g,"'").replace(/[\u201C\u201D]/g,'"');s="undefined"===e?void 0:c(e)}}catch(e){return void function(e,t=""){console.error(t,e),window.onerror?.(t,void 0,void 0,void 0,e)}(e,`Failed to parse URL parameter value: ${String(s)}`)}i[r]=s})),i}(window.location).jitsi_meet_external_api_id}catch(e){}(window.JitsiMeetJS||(window.JitsiMeetJS={}),window.JitsiMeetJS.app||(window.JitsiMeetJS.app={}),window.JitsiMeetJS.app).setExternalTransportBackend=e=>undefined.setBackend(e);var x=n(860);const E=n.n(x)().getLogger("modules/API/external/functions.js");function S(e,t){return e.sendRequest({type:"devices",name:"setDevice",device:t})}const R=["css/all.css","libs/alwaysontop.min.js"],O={addBreakoutRoom:"add-breakout-room",answerKnockingParticipant:"answer-knocking-participant",approveVideo:"approve-video",askToUnmute:"ask-to-unmute",autoAssignToBreakoutRooms:"auto-assign-to-breakout-rooms",avatarUrl:"avatar-url",cancelPrivateChat:"cancel-private-chat",closeBreakoutRoom:"close-breakout-room",displayName:"display-name",endConference:"end-conference",email:"email",grantModerator:"grant-moderator",hangup:"video-hangup",hideNotification:"hide-notification",initiatePrivateChat:"initiate-private-chat",joinBreakoutRoom:"join-breakout-room",localSubject:"local-subject",kickParticipant:"kick-participant",muteEveryone:"mute-everyone",overwriteConfig:"overwrite-config",overwriteNames:"overwrite-names",password:"password",pinParticipant:"pin-participant",rejectParticipant:"reject-participant",removeBreakoutRoom:"remove-breakout-room",resizeFilmStrip:"resize-film-strip",resizeLargeVideo:"resize-large-video",sendCameraFacingMode:"send-camera-facing-mode-message",sendChatMessage:"send-chat-message",sendEndpointTextMessage:"send-endpoint-text-message",sendParticipantToRoom:"send-participant-to-room",sendTones:"send-tones",setAssumedBandwidthBps:"set-assumed-bandwidth-bps",setFollowMe:"set-follow-me",setLargeVideoParticipant:"set-large-video-participant",setMediaEncryptionKey:"set-media-encryption-key",setNoiseSuppressionEnabled:"set-noise-suppression-enabled",setParticipantVolume:"set-participant-volume",setSubtitles:"set-subtitles",setTileView:"set-tile-view",setVideoQuality:"set-video-quality",showNotification:"show-notification",startRecording:"start-recording",startShareVideo:"start-share-video",stopRecording:"stop-recording",stopShareVideo:"stop-share-video",subject:"subject",submitFeedback:"submit-feedback",toggleAudio:"toggle-audio",toggleCamera:"toggle-camera",toggleCameraMirror:"toggle-camera-mirror",toggleChat:"toggle-chat",toggleE2EE:"toggle-e2ee",toggleFilmStrip:"toggle-film-strip",toggleLobby:"toggle-lobby",toggleModeration:"toggle-moderation",toggleNoiseSuppression:"toggle-noise-suppression",toggleParticipantsPane:"toggle-participants-pane",toggleRaiseHand:"toggle-raise-hand",toggleShareScreen:"toggle-share-screen",toggleSubtitles:"toggle-subtitles",toggleTileView:"toggle-tile-view",toggleVirtualBackgroundDialog:"toggle-virtual-background",toggleVideo:"toggle-video",toggleWhiteboard:"toggle-whiteboard"},j={"avatar-changed":"avatarChanged","audio-availability-changed":"audioAvailabilityChanged","audio-mute-status-changed":"audioMuteStatusChanged","audio-or-video-sharing-toggled":"audioOrVideoSharingToggled","breakout-rooms-updated":"breakoutRoomsUpdated","browser-support":"browserSupport","camera-error":"cameraError","chat-updated":"chatUpdated","compute-pressure-changed":"computePressureChanged","conference-created-timestamp":"conferenceCreatedTimestamp","content-sharing-participants-changed":"contentSharingParticipantsChanged","data-channel-closed":"dataChannelClosed","data-channel-opened":"dataChannelOpened","device-list-changed":"deviceListChanged","display-name-change":"displayNameChange","dominant-speaker-changed":"dominantSpeakerChanged","email-change":"emailChange","error-occurred":"errorOccurred","endpoint-text-message-received":"endpointTextMessageReceived","face-landmark-detected":"faceLandmarkDetected","feedback-submitted":"feedbackSubmitted","feedback-prompt-displayed":"feedbackPromptDisplayed","filmstrip-display-changed":"filmstripDisplayChanged","incoming-message":"incomingMessage","knocking-participant":"knockingParticipant",log:"log","mic-error":"micError","moderation-participant-approved":"moderationParticipantApproved","moderation-participant-rejected":"moderationParticipantRejected","moderation-status-changed":"moderationStatusChanged","mouse-enter":"mouseEnter","mouse-leave":"mouseLeave","mouse-move":"mouseMove","non-participant-message-received":"nonParticipantMessageReceived","notification-triggered":"notificationTriggered","outgoing-message":"outgoingMessage","p2p-status-changed":"p2pStatusChanged","participant-joined":"participantJoined","participant-kicked-out":"participantKickedOut","participant-left":"participantLeft","participant-role-changed":"participantRoleChanged","participants-pane-toggled":"participantsPaneToggled","password-required":"passwordRequired","peer-connection-failure":"peerConnectionFailure","prejoin-screen-loaded":"prejoinScreenLoaded","proxy-connection-event":"proxyConnectionEvent","raise-hand-updated":"raiseHandUpdated",ready:"ready","recording-link-available":"recordingLinkAvailable","recording-status-changed":"recordingStatusChanged","participant-menu-button-clicked":"participantMenuButtonClick","video-ready-to-close":"readyToClose","video-conference-joined":"videoConferenceJoined","video-conference-left":"videoConferenceLeft","video-availability-changed":"videoAvailabilityChanged","video-mute-status-changed":"videoMuteStatusChanged","video-quality-changed":"videoQualityChanged","screen-sharing-status-changed":"screenSharingStatusChanged","subject-change":"subjectChange","suspend-detected":"suspendDetected","tile-view-changed":"tileViewChanged","toolbar-button-clicked":"toolbarButtonClicked","transcribing-status-changed":"transcribingStatusChanged","transcription-chunk-received":"transcriptionChunkReceived","whiteboard-status-changed":"whiteboardStatusChanged"},I={"_request-desktop-sources":"_requestDesktopSources"};let P=0;function N(e,t){e._numberOfParticipants+=t}function A(e){let t;return"string"==typeof e&&null!==String(e).match(/([0-9]*\.?[0-9]+)(em|pt|px|((d|l|s)?v)(h|w)|%)$/)?t=e:"number"==typeof e&&(t=`${e}px`),t}class D extends(i()){constructor(e){super();for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r1&&void 0!==arguments[1]?arguments[1]:{},url:`https://${e}/#jitsi_meet_external_api_id=${P}`})}(e,{configOverwrite:l,iceServers:y,interfaceConfigOverwrite:d,jwt:u,lang:h,roomName:i,devices:_,userInfo:w,appData:{localStorageContent:E},release:C}),this._createIFrame(a,s,x),this._transport=new k({backend:new b({postisOptions:{allowedOrigin:new URL(this._url).origin,scope:`jitsi_meet_external_api_${P}`,window:this._frame.contentWindow}})}),Array.isArray(v)&&v.length>0&&this.invite(v),this._onload=p,this._tmpE2EEKey=L,this._isLargeVideoVisible=!1,this._isPrejoinVideoVisible=!1,this._numberOfParticipants=0,this._participants={},this._myUserID=void 0,this._onStageParticipant=void 0,this._setupListeners(),P++}_createIFrame(e,t,n){const r=`jitsiConferenceFrame${P}`;this._frame=document.createElement("iframe"),this._frame.allow=["autoplay","camera","clipboard-write","compute-pressure","display-capture","hid","microphone","screen-wake-lock","speaker-selection"].join("; "),this._frame.name=r,this._frame.id=r,this._setSize(e,t),this._frame.setAttribute("allowFullScreen","true"),this._frame.style.border=0,n&&(this._frame.sandbox=n),this._frame.src=this._url,this._frame=this._parentNode.appendChild(this._frame)}_getAlwaysOnTopResources(){const e=this._frame.contentWindow,t=e.document;let n="";const r=t.querySelector("base");if(r&&r.href)n=r.href;else{const{protocol:t,host:r}=e.location;n=`${t}//${r}`}return R.map((e=>new URL(e,n).href))}_getFormattedDisplayName(e){const{formattedDisplayName:t}=this._participants[e]||{};return t}_getOnStageParticipant(){return this._onStageParticipant}_getLargeVideo(){const e=this.getIFrame();if(this._isLargeVideoVisible&&e&&e.contentWindow&&e.contentWindow.document)return e.contentWindow.document.getElementById("largeVideo")}_getPrejoinVideo(){const e=this.getIFrame();if(this._isPrejoinVideoVisible&&e&&e.contentWindow&&e.contentWindow.document)return e.contentWindow.document.getElementById("prejoinVideo")}_getParticipantVideo(e){const t=this.getIFrame();if(t&&t.contentWindow&&t.contentWindow.document)return void 0===e||e===this._myUserID?t.contentWindow.document.getElementById("localVideo_container"):t.contentWindow.document.querySelector(`#participant_${e} video`)}_setSize(e,t){const n=A(e),r=A(t);void 0!==n&&(this._height=e,this._frame.style.height=n),void 0!==r&&(this._width=t,this._frame.style.width=r)}_setupListeners(){this._transport.on("event",(e=>{let{name:t,...n}=e;const r=n.id;switch(t){case"ready":var i;null===(i=this._onload)||void 0===i||i.call(this);break;case"video-conference-joined":if(void 0!==this._tmpE2EEKey){const e=e=>{const t=[];for(let n=0;n{const n=I[e.name],r={...e,name:n};n&&this.emit(n,r,t)}))}updateNumberOfParticipants(e){if(!e||!Object.keys(e).length)return;const t=Object.keys(e).reduce(((t,n)=>{var r;return null!==(r=e[n])&&void 0!==r&&r.participants?Object.keys(e[n].participants).length+t:t}),0);this._numberOfParticipants=t}async getRoomsInfo(){return this._transport.sendRequest({name:"rooms-info"})}isP2pActive(){return this._transport.sendRequest({name:"get-p2p-status"})}addEventListener(e,t){this.on(e,t)}addEventListeners(e){for(const t in e)this.addEventListener(t,e[t])}captureLargeVideoScreenshot(){return this._transport.sendRequest({name:"capture-largevideo-screenshot"})}dispose(){this.emit("_willDispose"),this._transport.dispose(),this.removeAllListeners(),this._frame&&this._frame.parentNode&&this._frame.parentNode.removeChild(this._frame)}executeCommand(e){if(e in O){for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r(E.error(e),{})))}(this._transport)}getContentSharingParticipants(){return this._transport.sendRequest({name:"get-content-sharing-participants"})}getCurrentDevices(){return function(e){return e.sendRequest({type:"devices",name:"getCurrentDevices"}).catch((e=>(E.error(e),{})))}(this._transport)}getCustomAvatarBackgrounds(){return this._transport.sendRequest({name:"get-custom-avatar-backgrounds"})}getLivestreamUrl(){return this._transport.sendRequest({name:"get-livestream-url"})}getParticipantsInfo(){const e=Object.keys(this._participants),t=Object.values(this._participants);return t.forEach(((t,n)=>{t.participantId=e[n]})),t}getVideoQuality(){return this._videoQuality}isAudioAvailable(){return this._transport.sendRequest({name:"is-audio-available"})}isDeviceChangeAvailable(e){return function(e,t){return e.sendRequest({deviceType:t,type:"devices",name:"isDeviceChangeAvailable"})}(this._transport,e)}isDeviceListAvailable(){return function(e){return e.sendRequest({type:"devices",name:"isDeviceListAvailable"})}(this._transport)}isMultipleAudioInputSupported(){return function(e){return e.sendRequest({type:"devices",name:"isMultipleAudioInputSupported"})}(this._transport)}invite(e){return Array.isArray(e)&&0!==e.length?this._transport.sendRequest({name:"invite",invitees:e}):Promise.reject(new TypeError("Invalid Argument"))}isAudioMuted(){return this._transport.sendRequest({name:"is-audio-muted"})}isAudioDisabled(){return this._transport.sendRequest({name:"is-audio-disabled"})}isModerationOn(e){return this._transport.sendRequest({name:"is-moderation-on",mediaType:e})}isParticipantForceMuted(e,t){return this._transport.sendRequest({name:"is-participant-force-muted",participantId:e,mediaType:t})}isParticipantsPaneOpen(){return this._transport.sendRequest({name:"is-participants-pane-open"})}isSharingScreen(){return this._transport.sendRequest({name:"is-sharing-screen"})}isStartSilent(){return this._transport.sendRequest({name:"is-start-silent"})}getAvatarURL(e){const{avatarURL:t}=this._participants[e]||{};return t}getDeploymentInfo(){return this._transport.sendRequest({name:"deployment-info"})}getDisplayName(e){const{displayName:t}=this._participants[e]||{};return t}getEmail(e){const{email:t}=this._participants[e]||{};return t}getIFrame(){return this._frame}getNumberOfParticipants(){return this._numberOfParticipants}getSessionId(){return this._transport.sendRequest({name:"session-id"})}getSupportedCommands(){return Object.keys(O)}getSupportedEvents(){return Object.values(j)}isVideoAvailable(){return this._transport.sendRequest({name:"is-video-available"})}isVideoMuted(){return this._transport.sendRequest({name:"is-video-muted"})}listBreakoutRooms(){return this._transport.sendRequest({name:"list-breakout-rooms"})}_isNewElectronScreensharingSupported(){return this._transport.sendRequest({name:"_new_electron_screensharing_supported"})}pinParticipant(e,t){this.executeCommand("pinParticipant",e,t)}removeEventListener(e){this.removeAllListeners(e)}removeEventListeners(e){e.forEach((e=>this.removeEventListener(e)))}resizeLargeVideo(e,t){e<=this._width&&t<=this._height&&this.executeCommand("resizeLargeVideo",e,t)}sendProxyConnectionEvent(e){this._transport.sendEvent({data:[e],name:"proxy-connection-event"})}setAudioInputDevice(e,t){return function(e,t,n){return S(e,{id:n,kind:"audioinput",label:t})}(this._transport,e,t)}setAudioOutputDevice(e,t){return function(e,t,n){return S(e,{id:n,kind:"audiooutput",label:t})}(this._transport,e,t)}setLargeVideoParticipant(e,t){this.executeCommand("setLargeVideoParticipant",e,t)}setVideoInputDevice(e,t){return function(e,t,n){return S(e,{id:n,kind:"videoinput",label:t})}(this._transport,e,t)}startRecording(e){this.executeCommand("startRecording",e)}stopRecording(e,t){this.executeCommand("stopRecording",e,t)}toggleE2EE(e){this.executeCommand("toggleE2EE",e)}async setMediaEncryptionKey(e){const{key:t,index:n}=e;if(t){const e=await crypto.subtle.exportKey("raw",t);this.executeCommand("setMediaEncryptionKey",JSON.stringify({exportedKey:Array.from(new Uint8Array(e)),index:n}))}else this.executeCommand("setMediaEncryptionKey",JSON.stringify({exportedKey:!1,index:n}))}}},872:(e,t,n)=>{e.exports=n(372).default},135:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.BLANK_URL=t.relativeFirstCharacters=t.urlSchemeRegex=t.ctrlCharactersRegex=t.htmlCtrlEntityRegex=t.htmlEntitiesRegex=t.invalidProtocolRegex=void 0,t.invalidProtocolRegex=/^([^\w]*)(javascript|data|vbscript)/im,t.htmlEntitiesRegex=/&#(\w+)(^\w|;)?/g,t.htmlCtrlEntityRegex=/&(newline|tab);/gi,t.ctrlCharactersRegex=/[\u0000-\u001F\u007F-\u009F\u2000-\u200D\uFEFF]/gim,t.urlSchemeRegex=/^.+(:|:)/gim,t.relativeFirstCharacters=[".","/"],t.BLANK_URL="about:blank"},449:(e,t,n)=>{"use strict";n(135)},571:(e,t)=>{"use strict";const n=/"(?:_|\\u005[Ff])(?:_|\\u005[Ff])(?:p|\\u0070)(?:r|\\u0072)(?:o|\\u006[Ff])(?:t|\\u0074)(?:o|\\u006[Ff])(?:_|\\u005[Ff])(?:_|\\u005[Ff])"\s*\:/;t.parse=function(e){const r="object"==typeof(arguments.length<=1?void 0:arguments[1])&&(arguments.length<=1?void 0:arguments[1]),i=(arguments.length<=1?0:arguments.length-1)>1||!r?arguments.length<=1?void 0:arguments[1]:void 0,s=(arguments.length<=1?0:arguments.length-1)>1&&(arguments.length<=2?void 0:arguments[2])||r||{},o=JSON.parse(e,i);return"ignore"===s.protoAction?o:o&&"object"==typeof o&&e.match(n)?(t.scan(o,s),o):o},t.scan=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=[e];for(;n.length;){const e=n;n=[];for(const r of e){if(Object.prototype.hasOwnProperty.call(r,"__proto__")){if("remove"!==t.protoAction)throw new SyntaxError("Object contains forbidden prototype property");delete r.__proto__}for(const e in r){const t=r[e];t&&"object"==typeof t&&n.push(r[e])}}}},t.safeParse=function(e,n){try{return t.parse(e,n)}catch(e){return null}}},369:(e,t,n)=>{var r=n(7);function i(e,t){this.logStorage=e,this.stringifyObjects=!(!t||!t.stringifyObjects)&&t.stringifyObjects,this.storeInterval=t&&t.storeInterval?t.storeInterval:3e4,this.maxEntryLength=t&&t.maxEntryLength?t.maxEntryLength:1e4,Object.values(r.levels).forEach(function(e){this[e]=function(){this._log.apply(this,arguments)}.bind(this,e)}.bind(this)),this.storeLogsIntervalID=null,this.queue=[],this.totalLen=0,this.outputCache=[]}i.prototype.stringify=function(e){try{return JSON.stringify(e)}catch(e){return"[object with circular refs?]"}},i.prototype.formatLogMessage=function(e){for(var t="",n=1,r=arguments.length;n=this.maxEntryLength&&this._flush(!0,!0)},i.prototype.start=function(){this._reschedulePublishInterval()},i.prototype._reschedulePublishInterval=function(){this.storeLogsIntervalID&&(window.clearTimeout(this.storeLogsIntervalID),this.storeLogsIntervalID=null),this.storeLogsIntervalID=window.setTimeout(this._flush.bind(this,!1,!0),this.storeInterval)},i.prototype.flush=function(){this._flush(!1,!0)},i.prototype._storeLogs=function(e){try{this.logStorage.storeLogs(e)}catch(e){console.error("LogCollector error when calling logStorage.storeLogs(): ",e)}},i.prototype._flush=function(e,t){var n=!1;try{n=this.logStorage.isReady()}catch(e){console.error("LogCollector error when calling logStorage.isReady(): ",e)}this.totalLen>0&&(n||e)&&(n?(this.outputCache.length&&(this.outputCache.forEach(function(e){this._storeLogs(e)}.bind(this)),this.outputCache=[]),this._storeLogs(this.queue)):this.outputCache.push(this.queue),this.queue=[],this.totalLen=0),t&&this._reschedulePublishInterval()},i.prototype.stop=function(){this._flush(!1,!1)},e.exports=i},7:e=>{var t={trace:0,debug:1,info:2,log:3,warn:4,error:5};s.consoleTransport=console;var n=[s.consoleTransport];s.addGlobalTransport=function(e){-1===n.indexOf(e)&&n.push(e)},s.removeGlobalTransport=function(e){var t=n.indexOf(e);-1!==t&&n.splice(t,1)};var r={};function i(){var e=arguments[0],i=arguments[1],s=Array.prototype.slice.call(arguments,2);if(!(t[i]1&&u.push("<"+o.methodName+">: ");var h=u.concat(s);try{d.bind(l).apply(l,h)}catch(e){console.error("An error occured when trying to log with one of the available transports",e)}}}}function s(e,n,r,s){this.id=n,this.options=s||{},this.transports=r,this.transports||(this.transports=[]),this.level=t[e];for(var o=Object.keys(t),a=0;a{var r=n(7),i=n(369),s={},o=[],a=r.levels.TRACE;e.exports={addGlobalTransport:function(e){r.addGlobalTransport(e)},removeGlobalTransport:function(e){r.removeGlobalTransport(e)},setGlobalOptions:function(e){r.setGlobalOptions(e)},getLogger:function(e,t,n){var i=new r(a,e,t,n);return e?(s[e]=s[e]||[],s[e].push(i)):o.push(i),i},getUntrackedLogger:function(e,t,n){return new r(a,e,t,n)},setLogLevelById:function(e,t){for(var n=t?s[t]||[]:o,r=0;r{"use strict";var t,n="object"==typeof Reflect?Reflect:null,r=n&&"function"==typeof n.apply?n.apply:function(e,t,n){return Function.prototype.apply.call(e,t,n)};t=n&&"function"==typeof n.ownKeys?n.ownKeys:Object.getOwnPropertySymbols?function(e){return Object.getOwnPropertyNames(e).concat(Object.getOwnPropertySymbols(e))}:function(e){return Object.getOwnPropertyNames(e)};var i=Number.isNaN||function(e){return e!=e};function s(){s.init.call(this)}e.exports=s,e.exports.once=function(e,t){return new Promise((function(n,r){function i(n){e.removeListener(t,s),r(n)}function s(){"function"==typeof e.removeListener&&e.removeListener("error",i),n([].slice.call(arguments))}m(e,t,s,{once:!0}),"error"!==t&&function(e,t,n){"function"==typeof e.on&&m(e,"error",t,{once:!0})}(e,i)}))},s.EventEmitter=s,s.prototype._events=void 0,s.prototype._eventsCount=0,s.prototype._maxListeners=void 0;var o=10;function a(e){if("function"!=typeof e)throw new TypeError('The "listener" argument must be of type Function. Received type '+typeof e)}function c(e){return void 0===e._maxListeners?s.defaultMaxListeners:e._maxListeners}function l(e,t,n,r){var i,s,o,l;if(a(n),void 0===(s=e._events)?(s=e._events=Object.create(null),e._eventsCount=0):(void 0!==s.newListener&&(e.emit("newListener",t,n.listener?n.listener:n),s=e._events),o=s[t]),void 0===o)o=s[t]=n,++e._eventsCount;else if("function"==typeof o?o=s[t]=r?[n,o]:[o,n]:r?o.unshift(n):o.push(n),(i=c(e))>0&&o.length>i&&!o.warned){o.warned=!0;var d=new Error("Possible EventEmitter memory leak detected. "+o.length+" "+String(t)+" listeners added. Use emitter.setMaxListeners() to increase limit");d.name="MaxListenersExceededWarning",d.emitter=e,d.type=t,d.count=o.length,l=d,console&&console.warn&&console.warn(l)}return e}function d(){if(!this.fired)return this.target.removeListener(this.type,this.wrapFn),this.fired=!0,0===arguments.length?this.listener.call(this.target):this.listener.apply(this.target,arguments)}function u(e,t,n){var r={fired:!1,wrapFn:void 0,target:e,type:t,listener:n},i=d.bind(r);return i.listener=n,r.wrapFn=i,i}function h(e,t,n){var r=e._events;if(void 0===r)return[];var i=r[t];return void 0===i?[]:"function"==typeof i?n?[i.listener||i]:[i]:n?function(e){for(var t=new Array(e.length),n=0;n0&&(o=t[0]),o instanceof Error)throw o;var a=new Error("Unhandled error."+(o?" ("+o.message+")":""));throw a.context=o,a}var c=s[e];if(void 0===c)return!1;if("function"==typeof c)r(c,this,t);else{var l=c.length,d=g(c,l);for(n=0;n=0;s--)if(n[s]===t||n[s].listener===t){o=n[s].listener,i=s;break}if(i<0)return this;0===i?n.shift():function(e,t){for(;t+1=0;r--)this.removeListener(e,t[r]);return this},s.prototype.listeners=function(e){return h(this,e,!0)},s.prototype.rawListeners=function(e){return h(this,e,!1)},s.listenerCount=function(e,t){return"function"==typeof e.listenerCount?e.listenerCount(t):p.call(e,t)},s.prototype.listenerCount=p,s.prototype.eventNames=function(){return this._eventsCount>0?t(this._events):[]}}},t={};function n(r){var i=t[r];if(void 0!==i)return i.exports;var s=t[r]={exports:{}};return e[r](s,s.exports,n),s.exports}return n.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return n.d(t,{a:t}),t},n.d=(e,t)=>{for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),n(872)})())); //# sourceMappingURL=external_api.min.js.map \ No newline at end of file diff --git a/src/@types/common.ts b/src/@types/common.ts index 1331ba92b5e..f80b66a6322 100644 --- a/src/@types/common.ts +++ b/src/@types/common.ts @@ -14,14 +14,6 @@ export type ComponentClass = keyof JSX.IntrinsicElements | JSXElementConstructor export type { Leaves } from "matrix-web-i18n"; -export type RecursivePartial = { - [P in keyof T]?: T[P] extends (infer U)[] - ? RecursivePartial[] - : T[P] extends object - ? RecursivePartial - : T[P]; -}; - export type KeysStartingWith = { // eslint-disable-next-line @typescript-eslint/no-unused-vars [P in keyof Input]: P extends `${Str}${infer _X}` ? P : never; // we don't use _X diff --git a/src/AddThreepid.ts b/src/AddThreepid.ts index 34ba9d51ed7..5232132535c 100644 --- a/src/AddThreepid.ts +++ b/src/AddThreepid.ts @@ -10,13 +10,13 @@ Please see LICENSE files in the repository root for full details. import { IAddThreePidOnlyBody, - IAuthData, IRequestMsisdnTokenResponse, IRequestTokenResponse, MatrixClient, MatrixError, HTTPError, IThreepid, + UIAResponse, } from "matrix-js-sdk/src/matrix"; import Modal from "./Modal"; @@ -179,7 +179,9 @@ export default class AddThreepid { * with a "message" property which contains a human-readable message detailing why * the request failed. */ - public async checkEmailLinkClicked(): Promise<[success?: boolean, result?: IAuthData | Error | null]> { + public async checkEmailLinkClicked(): Promise< + [success?: boolean, result?: UIAResponse | Error | null] + > { try { if (this.bind) { const authClient = new IdentityAuthClient(); @@ -220,7 +222,7 @@ export default class AddThreepid { continueKind: "primary", }, }; - const { finished } = Modal.createDialog(InteractiveAuthDialog<{}>, { + const { finished } = Modal.createDialog(InteractiveAuthDialog, { title: _t("settings|general|add_email_dialog_title"), matrixClient: this.matrixClient, authData: err.data, @@ -263,7 +265,9 @@ export default class AddThreepid { * with a "message" property which contains a human-readable message detailing why * the request failed. */ - public async haveMsisdnToken(msisdnToken: string): Promise<[success?: boolean, result?: IAuthData | Error | null]> { + public async haveMsisdnToken( + msisdnToken: string, + ): Promise<[success?: boolean, result?: UIAResponse | Error | null]> { const authClient = new IdentityAuthClient(); if (this.submitUrl) { @@ -319,7 +323,7 @@ export default class AddThreepid { continueKind: "primary", }, }; - const { finished } = Modal.createDialog(InteractiveAuthDialog<{}>, { + const { finished } = Modal.createDialog(InteractiveAuthDialog, { title: _t("settings|general|add_msisdn_dialog_title"), matrixClient: this.matrixClient, authData: err.data, diff --git a/src/AsyncWrapper.tsx b/src/AsyncWrapper.tsx index 179d42668ee..2a04d804b77 100644 --- a/src/AsyncWrapper.tsx +++ b/src/AsyncWrapper.tsx @@ -6,24 +6,19 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { ComponentType, PropsWithChildren } from "react"; -import { logger } from "matrix-js-sdk/src/logger"; +import React, { ReactNode, Suspense } from "react"; import { _t } from "./languageHandler"; import BaseDialog from "./components/views/dialogs/BaseDialog"; import DialogButtons from "./components/views/elements/DialogButtons"; import Spinner from "./components/views/elements/Spinner"; -type AsyncImport = { default: T }; - interface IProps { - // A promise which resolves with the real component - prom: Promise | AsyncImport>>; onFinished(): void; + children: ReactNode; } interface IState { - component?: ComponentType>; error?: Error; } @@ -32,55 +27,26 @@ interface IState { * spinner until the real component loads. */ export default class AsyncWrapper extends React.Component { - private unmounted = false; - - public state: IState = {}; - - public componentDidMount(): void { - this.props.prom - .then((result) => { - if (this.unmounted) return; - - // Take the 'default' member if it's there, then we support - // passing in just an import()ed module, since ES6 async import - // always returns a module *namespace*. - const component = (result as AsyncImport).default - ? (result as AsyncImport).default - : (result as ComponentType); - this.setState({ component }); - }) - .catch((e) => { - logger.warn("AsyncWrapper promise failed", e); - this.setState({ error: e }); - }); + public static getDerivedStateFromError(error: Error): IState { + return { error }; } - public componentWillUnmount(): void { - this.unmounted = true; - } - - private onWrapperCancelClick = (): void => { - this.props.onFinished(); - }; + public state: IState = {}; public render(): React.ReactNode { - if (this.state.component) { - const Component = this.state.component; - return ; - } else if (this.state.error) { + if (this.state.error) { return ( {_t("failed_load_async_component")} ); - } else { - // show a spinner until the component is loaded. - return ; } + + return }>{this.props.children}; } } diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts index 8bbad339c79..0017d00dacb 100644 --- a/src/BasePlatform.ts +++ b/src/BasePlatform.ts @@ -31,6 +31,8 @@ import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload"; import { IConfigOptions } from "./IConfigOptions"; import SdkConfig from "./SdkConfig"; import { buildAndEncodePickleKey, encryptPickleKey } from "./utils/tokens/pickling"; +import Favicon from "./favicon.ts"; +import { getVectorConfig } from "./vector/getconfig.ts"; export const SSO_HOMESERVER_URL_KEY = "mx_sso_hs_url"; export const SSO_ID_SERVER_URL_KEY = "mx_sso_is_url"; @@ -66,14 +68,20 @@ const UPDATE_DEFER_KEY = "mx_defer_update"; export default abstract class BasePlatform { protected notificationCount = 0; protected errorDidOccur = false; + protected _favicon?: Favicon; protected constructor() { dis.register(this.onAction); this.startUpdateCheck = this.startUpdateCheck.bind(this); } - public abstract getConfig(): Promise; + public async getConfig(): Promise { + return getVectorConfig(); + } + /** + * Get a sensible default display name for the device Element is running on + */ public abstract getDefaultDeviceDisplayName(): string; protected onAction = (payload: ActionPayload): void => { @@ -89,11 +97,15 @@ export default abstract class BasePlatform { public abstract getHumanReadableName(): string; public setNotificationCount(count: number): void { + if (this.notificationCount === count) return; this.notificationCount = count; + this.updateFavicon(); } public setErrorStatus(errorDidOccur: boolean): void { + if (this.errorDidOccur === errorDidOccur) return; this.errorDidOccur = errorDidOccur; + this.updateFavicon(); } /** @@ -456,4 +468,34 @@ export default abstract class BasePlatform { url.hash = ""; return url; } + + /** + * Delay creating the `Favicon` instance until first use (on the first notification) as + * it uses canvas, which can trigger a permission prompt in Firefox's resist fingerprinting mode. + * See https://github.com/element-hq/element-web/issues/9605. + */ + public get favicon(): Favicon { + if (this._favicon) { + return this._favicon; + } + this._favicon = new Favicon(); + return this._favicon; + } + + private updateFavicon(): void { + let bgColor = "#d00"; + let notif: string | number = this.notificationCount; + + if (this.errorDidOccur) { + notif = notif || "×"; + bgColor = "#f00"; + } + + this.favicon.badge(notif, { bgColor }); + } + + /** + * Begin update polling, if applicable + */ + public startUpdater(): void {} } diff --git a/src/BlurhashEncoder.ts b/src/BlurhashEncoder.ts index 63521d5d0eb..9cd386894f6 100644 --- a/src/BlurhashEncoder.ts +++ b/src/BlurhashEncoder.ts @@ -6,7 +6,6 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -// @ts-ignore - `.ts` is needed here to make TS happy import { Request, Response } from "./workers/blurhash.worker.ts"; import { WorkerManager } from "./WorkerManager"; import blurhashWorkerFactory from "./workers/blurhashWorkerFactory"; diff --git a/src/CreateCrossSigning.ts b/src/CreateCrossSigning.ts new file mode 100644 index 00000000000..e67e030f60e --- /dev/null +++ b/src/CreateCrossSigning.ts @@ -0,0 +1,118 @@ +/* +Copyright 2024 New Vector Ltd. +Copyright 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2018, 2019 New Vector Ltd + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import { logger } from "matrix-js-sdk/src/logger"; +import { AuthDict, CrossSigningKeys, MatrixClient, MatrixError, UIAFlow, UIAResponse } from "matrix-js-sdk/src/matrix"; + +import { SSOAuthEntry } from "./components/views/auth/InteractiveAuthEntryComponents"; +import Modal from "./Modal"; +import { _t } from "./languageHandler"; +import InteractiveAuthDialog from "./components/views/dialogs/InteractiveAuthDialog"; + +/** + * Determine if the homeserver allows uploading device keys with only password auth. + * @param cli The Matrix Client to use + * @returns True if the homeserver allows uploading device keys with only password auth, otherwise false + */ +async function canUploadKeysWithPasswordOnly(cli: MatrixClient): Promise { + try { + await cli.uploadDeviceSigningKeys(undefined, {} as CrossSigningKeys); + // We should never get here: the server should always require + // UI auth to upload device signing keys. If we do, we upload + // no keys which would be a no-op. + logger.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!"); + return false; + } catch (error) { + if (!(error instanceof MatrixError) || !error.data || !error.data.flows) { + logger.log("uploadDeviceSigningKeys advertised no flows!"); + return false; + } + const canUploadKeysWithPasswordOnly = error.data.flows.some((f: UIAFlow) => { + return f.stages.length === 1 && f.stages[0] === "m.login.password"; + }); + return canUploadKeysWithPasswordOnly; + } +} + +/** + * Ensures that cross signing keys are created and uploaded for the user. + * The homeserver may require user-interactive auth to upload the keys, in + * which case the user will be prompted to authenticate. If the homeserver + * allows uploading keys with just an account password and one is provided, + * the keys will be uploaded without user interaction. + * + * This function does not set up backups of the created cross-signing keys + * (or message keys): the cross-signing keys are stored locally and will be + * lost requiring a crypto reset, if the user logs out or loses their session. + * + * @param cli The Matrix Client to use + * @param isTokenLogin True if the user logged in via a token login, otherwise false + * @param accountPassword The password that the user logged in with + */ +export async function createCrossSigning( + cli: MatrixClient, + isTokenLogin: boolean, + accountPassword?: string, +): Promise { + const cryptoApi = cli.getCrypto(); + if (!cryptoApi) { + throw new Error("No crypto API found!"); + } + + const doBootstrapUIAuth = async ( + makeRequest: (authData: AuthDict) => Promise>, + ): Promise => { + if (accountPassword && (await canUploadKeysWithPasswordOnly(cli))) { + await makeRequest({ + type: "m.login.password", + identifier: { + type: "m.id.user", + user: cli.getUserId(), + }, + password: accountPassword, + }); + } else if (isTokenLogin) { + // We are hoping the grace period is active + await makeRequest({}); + } else { + const dialogAesthetics = { + [SSOAuthEntry.PHASE_PREAUTH]: { + title: _t("auth|uia|sso_title"), + body: _t("auth|uia|sso_preauth_body"), + continueText: _t("auth|sso"), + continueKind: "primary", + }, + [SSOAuthEntry.PHASE_POSTAUTH]: { + title: _t("encryption|confirm_encryption_setup_title"), + body: _t("encryption|confirm_encryption_setup_body"), + continueText: _t("action|confirm"), + continueKind: "primary", + }, + }; + + const { finished } = Modal.createDialog(InteractiveAuthDialog, { + title: _t("encryption|bootstrap_title"), + matrixClient: cli, + makeRequest, + aestheticsForStagePhases: { + [SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics, + [SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics, + }, + }); + const [confirmed] = await finished; + if (!confirmed) { + throw new Error("Cross-signing key upload auth canceled"); + } + } + }; + + await cryptoApi.bootstrapCrossSigning({ + authUploadDeviceSigningKeys: doBootstrapUIAuth, + }); +} diff --git a/src/DecryptionFailureTracker.ts b/src/DecryptionFailureTracker.ts index d3f2ad2671e..1e07ba252b5 100644 --- a/src/DecryptionFailureTracker.ts +++ b/src/DecryptionFailureTracker.ts @@ -82,6 +82,10 @@ export class DecryptionFailureTracker { return "HistoricalMessage"; case DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED: return "ExpectedDueToMembership"; + case DecryptionFailureCode.SENDER_IDENTITY_PREVIOUSLY_VERIFIED: + return "ExpectedVerificationViolation"; + case DecryptionFailureCode.UNSIGNED_SENDER_DEVICE: + return "ExpectedSentByInsecureDevice"; default: return "UnknownError"; } diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index ea812d7379f..02e26729d26 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -113,13 +113,9 @@ export default class DeviceListener { this.client.removeListener(ClientEvent.Sync, this.onSync); this.client.removeListener(RoomStateEvent.Events, this.onRoomStateEvents); } - if (this.deviceClientInformationSettingWatcherRef) { - SettingsStore.unwatchSetting(this.deviceClientInformationSettingWatcherRef); - } - if (this.dispatcherRef) { - dis.unregister(this.dispatcherRef); - this.dispatcherRef = undefined; - } + SettingsStore.unwatchSetting(this.deviceClientInformationSettingWatcherRef); + dis.unregister(this.dispatcherRef); + this.dispatcherRef = undefined; this.dismissed.clear(); this.dismissedThisDeviceToast = false; this.keyBackupInfo = null; @@ -292,27 +288,21 @@ export default class DeviceListener { await crypto.getUserDeviceInfo([cli.getSafeUserId()]); // cross signing isn't enabled - nag to enable it - // There are 3 different toasts for: + // There are 2 different toasts for: if (!(await crypto.getCrossSigningKeyId()) && (await crypto.userHasCrossSigningKeys())) { // Cross-signing on account but this device doesn't trust the master key (verify this session) showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION); this.checkKeyBackupStatus(); } else { - const backupInfo = await this.getKeyBackupInfo(); - if (backupInfo) { - // No cross-signing on account but key backup available (upgrade encryption) - showSetupEncryptionToast(SetupKind.UPGRADE_ENCRYPTION); + // No cross-signing or key backup on account (set up encryption) + await cli.waitForClientWellKnown(); + if (isSecureBackupRequired(cli) && isLoggedIn()) { + // If we're meant to set up, and Secure Backup is required, + // trigger the flow directly without a toast once logged in. + hideSetupEncryptionToast(); + accessSecretStorage(); } else { - // No cross-signing or key backup on account (set up encryption) - await cli.waitForClientWellKnown(); - if (isSecureBackupRequired(cli) && isLoggedIn()) { - // If we're meant to set up, and Secure Backup is required, - // trigger the flow directly without a toast once logged in. - hideSetupEncryptionToast(); - accessSecretStorage(); - } else { - showSetupEncryptionToast(SetupKind.SET_UP_ENCRYPTION); - } + showSetupEncryptionToast(SetupKind.SET_UP_ENCRYPTION); } } } diff --git a/src/Markdown.ts b/src/Markdown.ts index 5e4265bc268..9a346bf2f6d 100644 --- a/src/Markdown.ts +++ b/src/Markdown.ts @@ -383,6 +383,9 @@ export default class Markdown { if (isMultiLine(node) && node.next) this.lit("\n\n"); }; - return renderer.render(this.parsed); + // We inhibit the default escape function as we escape the entire output string to correctly handle backslashes + renderer.esc = (input: string) => input; + + return escape(renderer.render(this.parsed)); } } diff --git a/src/Modal.tsx b/src/Modal.tsx index 53a1935294f..2aefdccb462 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -7,14 +7,14 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React from "react"; -import ReactDOM from "react-dom"; +import React, { StrictMode } from "react"; +import { createRoot, Root } from "react-dom/client"; import classNames from "classnames"; -import { IDeferred, defer, sleep } from "matrix-js-sdk/src/utils"; +import { IDeferred, defer } from "matrix-js-sdk/src/utils"; import { TypedEventEmitter } from "matrix-js-sdk/src/matrix"; import { Glass, TooltipProvider } from "@vector-im/compound-web"; -import dis, { defaultDispatcher } from "./dispatcher/dispatcher"; +import defaultDispatcher from "./dispatcher/dispatcher"; import AsyncWrapper from "./AsyncWrapper"; import { Defaultize } from "./@types/common"; import { ActionPayload } from "./dispatcher/payloads"; @@ -69,6 +69,16 @@ type HandlerMap = { type ModalCloseReason = "backgroundClick"; +function getOrCreateContainer(id: string): HTMLDivElement { + let container = document.getElementById(id) as HTMLDivElement | null; + if (!container) { + container = document.createElement("div"); + container.id = id; + document.body.appendChild(container); + } + return container; +} + export class ModalManager extends TypedEventEmitter { private counter = 0; // The modal to prioritise over all others. If this is set, only show @@ -83,28 +93,22 @@ export class ModalManager extends TypedEventEmitter[] = []; - private static getOrCreateContainer(): HTMLElement { - let container = document.getElementById(DIALOG_CONTAINER_ID); - - if (!container) { - container = document.createElement("div"); - container.id = DIALOG_CONTAINER_ID; - document.body.appendChild(container); + private static root?: Root; + private static getOrCreateRoot(): Root { + if (!ModalManager.root) { + const container = getOrCreateContainer(DIALOG_CONTAINER_ID); + ModalManager.root = createRoot(container); } - - return container; + return ModalManager.root; } - private static getOrCreateStaticContainer(): HTMLElement { - let container = document.getElementById(STATIC_DIALOG_CONTAINER_ID); - - if (!container) { - container = document.createElement("div"); - container.id = STATIC_DIALOG_CONTAINER_ID; - document.body.appendChild(container); + private static staticRoot?: Root; + private static getOrCreateStaticRoot(): Root { + if (!ModalManager.staticRoot) { + const container = getOrCreateContainer(STATIC_DIALOG_CONTAINER_ID); + ModalManager.staticRoot = createRoot(container); } - - return container; + return ModalManager.staticRoot; } public constructor() { @@ -132,32 +136,6 @@ export class ModalManager extends TypedEventEmitter 0; } - public createDialog( - Element: C, - props?: ComponentProps, - className?: string, - isPriorityModal = false, - isStaticModal = false, - options: IOptions = {}, - ): IHandle { - return this.createDialogAsync( - Promise.resolve(Element), - props, - className, - isPriorityModal, - isStaticModal, - options, - ); - } - - public appendDialog( - Element: C, - props?: ComponentProps, - className?: string, - ): IHandle { - return this.appendDialogAsync(Promise.resolve(Element), props, className); - } - /** * DEPRECATED. * This is used only for tests. They should be using forceCloseAllModals but that @@ -192,8 +170,11 @@ export class ModalManager extends TypedEventEmitter( - prom: Promise, + Component: C, props?: ComponentProps, className?: string, options?: IOptions, @@ -218,9 +199,12 @@ export class ModalManager extends TypedEventEmitter; + // Typescript doesn't like us passing props as any here, but we know that they are well typed due to the rigorous generics. + modal.elem = ( + + + + ); modal.close = closeDialog; return { modal, closeDialog, onFinishedProm }; @@ -287,29 +271,30 @@ export class ModalManager extends TypedEventEmitter'], cb); * } * - * @param {Promise} prom a promise which resolves with a React component - * which will be displayed as the modal view. + * @param component The component to render as a dialog. This component must accept an `onFinished` prop function as + * per the type {@link ComponentType}. If loading a component with esoteric dependencies consider + * using React.lazy to async load the component. + * e.g. `lazy(() => import('./MyComponent'))` * - * @param {Object} props properties to pass to the displayed - * component. (We will also pass an 'onFinished' property.) + * @param props properties to pass to the displayed component. (We will also pass an 'onFinished' property.) * - * @param {String} className CSS class to apply to the modal wrapper + * @param className CSS class to apply to the modal wrapper * - * @param {boolean} isPriorityModal if true, this modal will be displayed regardless + * @param isPriorityModal if true, this modal will be displayed regardless * of other modals that are currently in the stack. * Also, when closed, all modals will be removed * from the stack. - * @param {boolean} isStaticModal if true, this modal will be displayed under other + * @param isStaticModal if true, this modal will be displayed under other * modals in the stack. When closed, all modals will * also be removed from the stack. This is not compatible * with being a priority modal. Only one modal can be * static at a time. - * @param {Object} options? extra options for the dialog - * @param {onBeforeClose} options.onBeforeClose a callback to decide whether to close the dialog - * @returns {object} Object with 'close' parameter being a function that will close the dialog + * @param options? extra options for the dialog + * @param options.onBeforeClose a callback to decide whether to close the dialog + * @returns Object with 'close' parameter being a function that will close the dialog */ - public createDialogAsync( - prom: Promise, + public createDialog( + component: C, props?: ComponentProps, className?: string, isPriorityModal = false, @@ -317,7 +302,7 @@ export class ModalManager extends TypedEventEmitter = {}, ): IHandle { const beforeModal = this.getCurrentModal(); - const { modal, closeDialog, onFinishedProm } = this.buildModal(prom, props, className, options); + const { modal, closeDialog, onFinishedProm } = this.buildModal(component, props, className, options); if (isPriorityModal) { // XXX: This is destructive this.priorityModal = modal; @@ -337,13 +322,13 @@ export class ModalManager extends TypedEventEmitter( - prom: Promise, + public appendDialog( + component: C, props?: ComponentProps, className?: string, ): IHandle { const beforeModal = this.getCurrentModal(); - const { modal, closeDialog, onFinishedProm } = this.buildModal(prom, props, className, {}); + const { modal, closeDialog, onFinishedProm } = this.buildModal(component, props, className, {}); this.modals.push(modal); @@ -389,26 +374,21 @@ export class ModalManager extends TypedEventEmitter { - // TODO: We should figure out how to remove this weird sleep. It also makes testing harder - // - // await next tick because sometimes ReactDOM can race with itself and cause the modal to wrongly stick around - await sleep(0); - if (this.modals.length === 0 && !this.priorityModal && !this.staticModal) { // If there is no modal to render, make all of Element available // to screen reader users again - dis.dispatch({ + defaultDispatcher.dispatch({ action: "aria_unhide_main_app", }); - ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateContainer()); - ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateStaticContainer()); + ModalManager.getOrCreateRoot().render(<>); + ModalManager.getOrCreateStaticRoot().render(<>); return; } // Hide the content outside the modal to screen reader users // so they won't be able to navigate into it and act on it using // screen reader specific features - dis.dispatch({ + defaultDispatcher.dispatch({ action: "aria_hide_main_app", }); @@ -416,24 +396,26 @@ export class ModalManager extends TypedEventEmitter -
- -
{this.staticModal.elem}
-
-
-
- + + +
+ +
{this.staticModal.elem}
+
+
+
+ + ); - ReactDOM.render(staticDialog, ModalManager.getOrCreateStaticContainer()); + ModalManager.getOrCreateStaticRoot().render(staticDialog); } else { // This is safe to call repeatedly if we happen to do that - ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateStaticContainer()); + ModalManager.getOrCreateStaticRoot().render(<>); } const modal = this.getCurrentModal(); @@ -443,24 +425,26 @@ export class ModalManager extends TypedEventEmitter -
- -
{modal.elem}
-
-
-
- + + +
+ +
{modal.elem}
+
+
+
+ + ); - setTimeout(() => ReactDOM.render(dialog, ModalManager.getOrCreateContainer()), 0); + ModalManager.getOrCreateRoot().render(dialog); } else { // This is safe to call repeatedly if we happen to do that - ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateContainer()); + ModalManager.getOrCreateRoot().render(<>); } } } diff --git a/src/NodeAnimator.tsx b/src/NodeAnimator.tsx index b5abcd24402..3ca098311fc 100644 --- a/src/NodeAnimator.tsx +++ b/src/NodeAnimator.tsx @@ -6,12 +6,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { Key, MutableRefObject, ReactElement, ReactInstance } from "react"; -import ReactDom from "react-dom"; +import React, { Key, MutableRefObject, ReactElement, RefCallback } from "react"; interface IChildProps { style: React.CSSProperties; - ref: (node: React.ReactInstance) => void; + ref: RefCallback; } interface IProps { @@ -36,7 +35,7 @@ function isReactElement(c: ReturnType<(typeof React.Children)["toArray"]>[number * automatic positional animation, look at react-shuffle or similar libraries. */ export default class NodeAnimator extends React.Component { - private nodes: Record = {}; + private nodes: Record = {}; private children: { [key: string]: ReactElement } = {}; public static defaultProps: Partial = { startStyles: [], @@ -71,10 +70,10 @@ export default class NodeAnimator extends React.Component { if (!isReactElement(c)) return; if (oldChildren[c.key!]) { const old = oldChildren[c.key!]; - const oldNode = ReactDom.findDOMNode(this.nodes[old.key!]); + const oldNode = this.nodes[old.key!]; - if (oldNode && (oldNode as HTMLElement).style.left !== c.props.style.left) { - this.applyStyles(oldNode as HTMLElement, { left: c.props.style.left }); + if (oldNode && oldNode.style.left !== c.props.style.left) { + this.applyStyles(oldNode, { left: c.props.style.left }); } // clone the old element with the props (and children) of the new element // so prop updates are still received by the children. @@ -98,26 +97,29 @@ export default class NodeAnimator extends React.Component { }); } - private collectNode(k: Key, node: React.ReactInstance, restingStyle: React.CSSProperties): void { + private collectNode(k: Key, domNode: HTMLElement | null, restingStyle: React.CSSProperties): void { const key = typeof k === "bigint" ? Number(k) : k; - if (node && this.nodes[key] === undefined && this.props.startStyles.length > 0) { + if (domNode && this.nodes[key] === undefined && this.props.startStyles.length > 0) { const startStyles = this.props.startStyles; - const domNode = ReactDom.findDOMNode(node); // start from startStyle 1: 0 is the one we gave it // to start with, so now we animate 1 etc. for (let i = 1; i < startStyles.length; ++i) { - this.applyStyles(domNode as HTMLElement, startStyles[i]); + this.applyStyles(domNode, startStyles[i]); } // and then we animate to the resting state window.setTimeout(() => { - this.applyStyles(domNode as HTMLElement, restingStyle); + this.applyStyles(domNode, restingStyle); }, 0); } - this.nodes[key] = node; + if (domNode) { + this.nodes[key] = domNode; + } else { + delete this.nodes[key]; + } if (this.props.innerRef) { - this.props.innerRef.current = node; + this.props.innerRef.current = domNode; } } diff --git a/src/PlaybackEncoder.ts b/src/PlaybackEncoder.ts index aa3fb9bf6a5..10a539799de 100644 --- a/src/PlaybackEncoder.ts +++ b/src/PlaybackEncoder.ts @@ -6,7 +6,6 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -// @ts-ignore - `.ts` is needed here to make TS happy import { Request, Response } from "./workers/playback.worker"; import { WorkerManager } from "./WorkerManager"; import playbackWorkerFactory from "./workers/playbackWorkerFactory"; diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 2ffe8b81665..6217d9b7dd6 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -326,7 +326,7 @@ export class PosthogAnalytics { if (this.enabled) { this.posthog.reset(); } - if (this.watchSettingRef) SettingsStore.unwatchSetting(this.watchSettingRef); + SettingsStore.unwatchSetting(this.watchSettingRef); this.setAnonymity(Anonymity.Disabled); } diff --git a/src/Presence.ts b/src/Presence.ts index 11a333ce047..af06d4a1d6c 100644 --- a/src/Presence.ts +++ b/src/Presence.ts @@ -20,9 +20,9 @@ import { ActionPayload } from "./dispatcher/payloads"; const UNAVAILABLE_TIME_MS = 3 * 60 * 1000; // 3 mins class Presence { - private unavailableTimer: Timer | null = null; - private dispatcherRef: string | null = null; - private state: SetPresence | null = null; + private unavailableTimer?: Timer; + private dispatcherRef?: string; + private state?: SetPresence; /** * Start listening the user activity to evaluate his presence state. @@ -46,14 +46,10 @@ class Presence { * Stop tracking user activity */ public stop(): void { - if (this.dispatcherRef) { - dis.unregister(this.dispatcherRef); - this.dispatcherRef = null; - } - if (this.unavailableTimer) { - this.unavailableTimer.abort(); - this.unavailableTimer = null; - } + dis.unregister(this.dispatcherRef); + this.dispatcherRef = undefined; + this.unavailableTimer?.abort(); + this.unavailableTimer = undefined; } /** @@ -61,7 +57,7 @@ class Presence { * @returns {string} the presence state (see PRESENCE enum) */ public getState(): SetPresence | null { - return this.state; + return this.state ?? null; } private onAction = (payload: ActionPayload): void => { diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index 4717404222f..cf8d40acc8d 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -6,11 +6,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ +import { lazy } from "react"; import { ICryptoCallbacks, SecretStorage } from "matrix-js-sdk/src/matrix"; import { deriveRecoveryKeyFromPassphrase, decodeRecoveryKey } from "matrix-js-sdk/src/crypto-api"; import { logger } from "matrix-js-sdk/src/logger"; -import type CreateSecretStorageDialog from "./async-components/views/dialogs/security/CreateSecretStorageDialog"; import Modal from "./Modal"; import { MatrixClientPeg } from "./MatrixClientPeg"; import { _t } from "./languageHandler"; @@ -232,10 +232,8 @@ async function doAccessSecretStorage(func: () => Promise, forceReset: bool if (createNew) { // This dialog calls bootstrap itself after guiding the user through // passphrase creation. - const { finished } = Modal.createDialogAsync( - import("./async-components/views/dialogs/security/CreateSecretStorageDialog") as unknown as Promise< - typeof CreateSecretStorageDialog - >, + const { finished } = Modal.createDialog( + lazy(() => import("./async-components/views/dialogs/security/CreateSecretStorageDialog")), { forceReset, }, diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx index 2278fb38060..1e87b5b8260 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx @@ -11,7 +11,7 @@ import React, { createRef } from "react"; import FileSaver from "file-saver"; import { logger } from "matrix-js-sdk/src/logger"; import { AuthDict, CrossSigningKeys, MatrixError, UIAFlow, UIAResponse } from "matrix-js-sdk/src/matrix"; -import { CryptoEvent, BackupTrustInfo, GeneratedSecretStorageKey, KeyBackupInfo } from "matrix-js-sdk/src/crypto-api"; +import { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api"; import classNames from "classnames"; import CheckmarkIcon from "@vector-im/compound-design-tokens/assets/web/icons/check"; @@ -25,7 +25,6 @@ import StyledRadioButton from "../../../../components/views/elements/StyledRadio import AccessibleButton from "../../../../components/views/elements/AccessibleButton"; import DialogButtons from "../../../../components/views/elements/DialogButtons"; import InlineSpinner from "../../../../components/views/elements/InlineSpinner"; -import RestoreKeyBackupDialog from "../../../../components/views/dialogs/security/RestoreKeyBackupDialog"; import { getSecureBackupSetupMethods, isSecureBackupRequired, @@ -45,7 +44,6 @@ enum Phase { Loading = "loading", LoadError = "load_error", ChooseKeyPassphrase = "choose_key_passphrase", - Migrate = "migrate", Passphrase = "passphrase", PassphraseConfirm = "passphrase_confirm", ShowKey = "show_key", @@ -72,24 +70,6 @@ interface IState { downloaded: boolean; setPassphrase: boolean; - /** Information on the current key backup version, as returned by the server. - * - * `null` could mean any of: - * * we haven't yet requested the data from the server. - * * we were unable to reach the server. - * * the server returned key backup version data we didn't understand or was malformed. - * * there is actually no backup on the server. - */ - backupInfo: KeyBackupInfo | null; - - /** - * Information on whether the backup in `backupInfo` is correctly signed, and whether we have the right key to - * decrypt it. - * - * `undefined` if `backupInfo` is null, or if crypto is not enabled in the client. - */ - backupTrustInfo: BackupTrustInfo | undefined; - // does the server offer a UI auth flow with just m.login.password // for /keys/device_signing/upload? canUploadKeysWithPasswordOnly: boolean | null; @@ -137,20 +117,19 @@ export default class CreateSecretStorageDialog extends React.PureComponent { - try { - const cli = MatrixClientPeg.safeGet(); - const backupInfo = await cli.getKeyBackupVersion(); - const backupTrustInfo = - // we may not have started crypto yet, in which case we definitely don't trust the backup - backupInfo ? await cli.getCrypto()?.isKeyBackupTrusted(backupInfo) : undefined; - - const { forceReset } = this.props; - const phase = backupInfo && !forceReset ? Phase.Migrate : Phase.ChooseKeyPassphrase; - - this.setState({ - phase, - backupInfo, - backupTrustInfo, - }); - - return backupTrustInfo; - } catch (e) { - console.error("Error fetching backup data from server", e); - this.setState({ phase: Phase.LoadError }); - return undefined; - } + private initExtension(keyFromCustomisations: Uint8Array): void { + logger.log("CryptoSetupExtension: Created key via extension, jumping to bootstrap step"); + this.recoveryKey = { + privateKey: keyFromCustomisations, + }; + this.bootstrapSecretStorage(); } private async queryKeyUploadAuth(): Promise { @@ -237,10 +178,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { - if (this.state.phase === Phase.Migrate) this.fetchBackupInfo(); - }; - private onKeyPassphraseChange = (e: React.ChangeEvent): void => { this.setState({ passPhraseKeySelected: e.target.value, @@ -265,15 +202,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { - e.preventDefault(); - if (this.state.backupTrustInfo?.trusted) { - this.bootstrapSecretStorage(); - } else { - this.restoreBackup(); - } - }; - private onCopyClick = (): void => { const successful = copyNode(this.recoveryKeyNode.current); if (successful) { @@ -340,16 +268,28 @@ export default class CreateSecretStorageDialog extends React.PureComponent => { + const cli = MatrixClientPeg.safeGet(); + const crypto = cli.getCrypto()!; + const { forceReset } = this.props; + + let backupInfo; + // First, unless we know we want to do a reset, we see if there is an existing key backup + if (!forceReset) { + try { + this.setState({ phase: Phase.Loading }); + backupInfo = await cli.getKeyBackupVersion(); + } catch (e) { + logger.error("Error fetching backup data from server", e); + this.setState({ phase: Phase.LoadError }); + return; + } + } + this.setState({ phase: Phase.Storing, error: undefined, }); - const cli = MatrixClientPeg.safeGet(); - const crypto = cli.getCrypto()!; - - const { forceReset } = this.props; - try { if (forceReset) { logger.log("Forcing secret storage reset"); @@ -371,8 +311,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent this.recoveryKey!, - keyBackupInfo: this.state.backupInfo!, - setupNewKeyBackup: !this.state.backupInfo, + setupNewKeyBackup: !backupInfo, }); } await initialiseDehydration(true); @@ -381,20 +320,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent => { - const { finished } = Modal.createDialog( - RestoreKeyBackupDialog, - { - showSummary: false, - }, - undefined, - /* priority = */ false, - /* static = */ false, - ); - - await finished; - const backupTrustInfo = await this.fetchBackupInfo(); - if (backupTrustInfo?.trusted && this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) { - this.bootstrapSecretStorage(); - } - }; - private onLoadRetryClick = (): void => { - this.setState({ phase: Phase.Loading }); - this.fetchBackupInfo(); + this.bootstrapSecretStorage(); }; private onShowKeyContinueClick = (): void => { @@ -495,12 +402,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent): void => { - this.setState({ - accountPassword: e.target.value, - }); - }; - private renderOptionKey(): JSX.Element { return ( -
{_t("settings|key_backup|setup_secure_backup|requires_password_confirmation")}
-
- -
-
- ); - } else if (!this.state.backupTrustInfo?.trusted) { - authPrompt = ( -
-
{_t("settings|key_backup|setup_secure_backup|requires_key_restore")}
-
- ); - nextCaption = _t("action|restore"); - } else { - authPrompt =

{_t("settings|key_backup|setup_secure_backup|requires_server_authentication")}

; - } - - return ( -
-

{_t("settings|key_backup|setup_secure_backup|session_upgrade_description")}

-
{authPrompt}
- - - -
- ); - } - private renderPhasePassPhrase(): JSX.Element { return (
@@ -829,8 +681,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent }; } + public componentDidMount(): void { + this.unmounted = false; + } + public componentWillUnmount(): void { this.unmounted = true; } diff --git a/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx index fa41d53a45a..d08259f2cb5 100644 --- a/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx +++ b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx @@ -64,6 +64,10 @@ export default class ImportE2eKeysDialog extends React.Component }; } + public componentDidMount(): void { + this.unmounted = false; + } + public componentWillUnmount(): void { this.unmounted = true; } diff --git a/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx b/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx index ac180397499..69fc4b4814b 100644 --- a/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx +++ b/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx @@ -28,7 +28,7 @@ interface NewRecoveryMethodDialogProps { onFinished(): void; } -// Export as default instead of a named export so that it can be dynamically imported with `Modal.createDialogAsync` +// Export as default instead of a named export so that it can be dynamically imported with React lazy /** * Dialog to inform the user that a new recovery method has been detected. diff --git a/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.tsx b/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.tsx index aec447735e0..b1a6ebafc79 100644 --- a/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.tsx +++ b/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.tsx @@ -7,11 +7,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React from "react"; +import React, { lazy } from "react"; import dis from "../../../../dispatcher/dispatcher"; import { _t } from "../../../../languageHandler"; -import Modal, { ComponentType } from "../../../../Modal"; +import Modal from "../../../../Modal"; import { Action } from "../../../../dispatcher/actions"; import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; import DialogButtons from "../../../../components/views/elements/DialogButtons"; @@ -28,8 +28,8 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent { this.props.onFinished(); - Modal.createDialogAsync( - import("./CreateKeyBackupDialog") as unknown as Promise, + Modal.createDialog( + lazy(() => import("./CreateKeyBackupDialog")), undefined, undefined, /* priority = */ false, diff --git a/src/components/structures/BackdropPanel.tsx b/src/components/structures/BackdropPanel.tsx index 80c21235cce..32c75a936e3 100644 --- a/src/components/structures/BackdropPanel.tsx +++ b/src/components/structures/BackdropPanel.tsx @@ -31,4 +31,3 @@ export const BackdropPanel: React.FC = ({ backgroundImage, blurMultiplie
); }; -export default BackdropPanel; diff --git a/src/components/structures/EmbeddedPage.tsx b/src/components/structures/EmbeddedPage.tsx index 5de1261ecb6..5c7e81caf54 100644 --- a/src/components/structures/EmbeddedPage.tsx +++ b/src/components/structures/EmbeddedPage.tsx @@ -38,7 +38,7 @@ export default class EmbeddedPage extends React.PureComponent { public static contextType = MatrixClientContext; public declare context: React.ContextType; private unmounted = false; - private dispatcherRef: string | null = null; + private dispatcherRef?: string; public constructor(props: IProps, context: React.ContextType) { super(props, context); @@ -100,7 +100,7 @@ export default class EmbeddedPage extends React.PureComponent { public componentWillUnmount(): void { this.unmounted = true; - if (this.dispatcherRef !== null) dis.unregister(this.dispatcherRef); + dis.unregister(this.dispatcherRef); } private onAction = (payload: ActionPayload): void => { diff --git a/src/components/structures/InteractiveAuth.tsx b/src/components/structures/InteractiveAuth.tsx index 91e52a19057..4b0f0609524 100644 --- a/src/components/structures/InteractiveAuth.tsx +++ b/src/components/structures/InteractiveAuth.tsx @@ -90,8 +90,8 @@ interface IState { export default class InteractiveAuthComponent extends React.Component, IState> { private readonly authLogic: InteractiveAuth; - private readonly intervalId: number | null = null; private readonly stageComponent = createRef(); + private intervalId: number | null = null; private unmounted = false; @@ -126,15 +126,17 @@ export default class InteractiveAuthComponent extends React.Component { this.authLogic.poll(); }, 2000); } - } - public componentDidMount(): void { this.authLogic .attemptAuth() .then(async (result) => { diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index f8cd0184d42..49d0f570a5c 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -67,10 +67,6 @@ export default class LeftPanel extends React.Component { activeSpace: SpaceStore.instance.activeSpace, showBreadcrumbs: LeftPanel.breadcrumbsMode, }; - - BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate); - RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate); - SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.updateActiveSpace); } private static get breadcrumbsMode(): BreadcrumbsMode { @@ -78,6 +74,10 @@ export default class LeftPanel extends React.Component { } public componentDidMount(): void { + BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate); + RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate); + SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.updateActiveSpace); + if (this.listContainerRef.current) { UIStore.instance.trackElementDimensions("ListContainer", this.listContainerRef.current); // Using the passive option to not block the main thread diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 84c43fc19da..0042169f45c 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -49,11 +49,10 @@ import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../LegacyCallHandl import AudioFeedArrayForLegacyCall from "../views/voip/AudioFeedArrayForLegacyCall"; import { OwnProfileStore } from "../../stores/OwnProfileStore"; import { UPDATE_EVENT } from "../../stores/AsyncStore"; -import RoomView from "./RoomView"; -import type { RoomView as RoomViewType } from "./RoomView"; +import { RoomView } from "./RoomView"; import ToastContainer from "./ToastContainer"; import UserView from "./UserView"; -import BackdropPanel from "./BackdropPanel"; +import { BackdropPanel } from "./BackdropPanel"; import { mediaFromMxc } from "../../customisations/Media"; import { UserTab } from "../views/dialogs/UserTab"; import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload"; @@ -125,7 +124,7 @@ class LoggedInView extends React.Component { public static displayName = "LoggedInView"; protected readonly _matrixClient: MatrixClient; - protected readonly _roomView: React.RefObject; + protected readonly _roomView: React.RefObject; protected readonly _resizeContainer: React.RefObject; protected readonly resizeHandler: React.RefObject; protected layoutWatcherRef?: string; @@ -228,9 +227,9 @@ class LoggedInView extends React.Component { this._matrixClient.removeListener(ClientEvent.Sync, this.onSync); this._matrixClient.removeListener(RoomStateEvent.Events, this.onRoomStateEvents); OwnProfileStore.instance.off(UPDATE_EVENT, this.refreshBackgroundImage); - if (this.layoutWatcherRef) SettingsStore.unwatchSetting(this.layoutWatcherRef); - if (this.compactLayoutWatcherRef) SettingsStore.unwatchSetting(this.compactLayoutWatcherRef); - if (this.backgroundImageWatcherRef) SettingsStore.unwatchSetting(this.backgroundImageWatcherRef); + SettingsStore.unwatchSetting(this.layoutWatcherRef); + SettingsStore.unwatchSetting(this.compactLayoutWatcherRef); + SettingsStore.unwatchSetting(this.backgroundImageWatcherRef); this.timezoneProfileUpdateRef?.forEach((s) => SettingsStore.unwatchSetting(s)); this.resizer?.detach(); } diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index c5ef3975a55..afd444c9524 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { createRef } from "react"; +import React, { createRef, lazy } from "react"; import { ClientEvent, createClient, @@ -28,8 +28,6 @@ import { TooltipProvider } from "@vector-im/compound-web"; // what-input helps improve keyboard accessibility import "what-input"; -import type NewRecoveryMethodDialog from "../../async-components/views/dialogs/security/NewRecoveryMethodDialog"; -import type RecoveryMethodRemovedDialog from "../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog"; import PosthogTrackers from "../../PosthogTrackers"; import { DecryptionFailureTracker } from "../../DecryptionFailureTracker"; import { IMatrixClientCreds, MatrixClientPeg } from "../../MatrixClientPeg"; @@ -231,10 +229,10 @@ export default class MatrixChat extends React.PureComponent { private prevWindowWidth: number; private voiceBroadcastResumer?: VoiceBroadcastResumer; - private readonly loggedInView: React.RefObject; - private readonly dispatcherRef: string; - private readonly themeWatcher: ThemeWatcher; - private readonly fontWatcher: FontWatcher; + private readonly loggedInView = createRef(); + private dispatcherRef?: string; + private themeWatcher?: ThemeWatcher; + private fontWatcher?: FontWatcher; private readonly stores: SdkContextClass; public constructor(props: IProps) { @@ -256,8 +254,6 @@ export default class MatrixChat extends React.PureComponent { ready: false, }; - this.loggedInView = createRef(); - SdkConfig.put(this.props.config); // Used by _viewRoom before getting state from sync @@ -282,32 +278,10 @@ export default class MatrixChat extends React.PureComponent { } this.prevWindowWidth = UIStore.instance.windowWidth || 1000; - UIStore.instance.on(UI_EVENTS.Resize, this.handleResize); - - // For PersistentElement - this.state.resizeNotifier.on("middlePanelResized", this.dispatchTimelineResize); - - RoomNotificationStateStore.instance.on(UPDATE_STATUS_INDICATOR, this.onUpdateStatusIndicator); - - this.dispatcherRef = dis.register(this.onAction); - - this.themeWatcher = new ThemeWatcher(); - this.fontWatcher = new FontWatcher(); - this.themeWatcher.start(); - this.fontWatcher.start(); // object field used for tracking the status info appended to the title tag. // we don't do it as react state as i'm scared about triggering needless react refreshes. this.subTitleStatus = ""; - - initSentry(SdkConfig.get("sentry")); - - if (!checkSessionLockFree()) { - // another instance holds the lock; confirm its theft before proceeding - setTimeout(() => this.setState({ view: Views.CONFIRM_LOCK_THEFT }), 0); - } else { - this.startInitSession(); - } } /** @@ -476,6 +450,29 @@ export default class MatrixChat extends React.PureComponent { } public componentDidMount(): void { + UIStore.instance.on(UI_EVENTS.Resize, this.handleResize); + + // For PersistentElement + this.state.resizeNotifier.on("middlePanelResized", this.dispatchTimelineResize); + + RoomNotificationStateStore.instance.on(UPDATE_STATUS_INDICATOR, this.onUpdateStatusIndicator); + + this.dispatcherRef = dis.register(this.onAction); + + this.themeWatcher = new ThemeWatcher(); + this.fontWatcher = new FontWatcher(); + this.themeWatcher.start(); + this.fontWatcher.start(); + + initSentry(SdkConfig.get("sentry")); + + if (!checkSessionLockFree()) { + // another instance holds the lock; confirm its theft before proceeding + setTimeout(() => this.setState({ view: Views.CONFIRM_LOCK_THEFT }), 0); + } else { + this.startInitSession(); + } + window.addEventListener("resize", this.onWindowResized); } @@ -497,8 +494,8 @@ export default class MatrixChat extends React.PureComponent { public componentWillUnmount(): void { Lifecycle.stopMatrixClient(); dis.unregister(this.dispatcherRef); - this.themeWatcher.stop(); - this.fontWatcher.stop(); + this.themeWatcher?.stop(); + this.fontWatcher?.stop(); UIStore.destroy(); this.state.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize); window.removeEventListener("resize", this.onWindowResized); @@ -1011,7 +1008,7 @@ export default class MatrixChat extends React.PureComponent { this.setStateForNewView(newState); ThemeController.isLogin = true; - this.themeWatcher.recheck(); + this.themeWatcher?.recheck(); this.notifyNewScreen(isMobileRegistration ? "mobile_register" : "register"); } @@ -1088,7 +1085,7 @@ export default class MatrixChat extends React.PureComponent { }, () => { ThemeController.isLogin = false; - this.themeWatcher.recheck(); + this.themeWatcher?.recheck(); this.notifyNewScreen("room/" + presentedId, replaceLast); }, ); @@ -1113,7 +1110,7 @@ export default class MatrixChat extends React.PureComponent { }); this.notifyNewScreen("welcome"); ThemeController.isLogin = true; - this.themeWatcher.recheck(); + this.themeWatcher?.recheck(); } private viewLogin(otherState?: any): void { @@ -1123,7 +1120,7 @@ export default class MatrixChat extends React.PureComponent { }); this.notifyNewScreen("login"); ThemeController.isLogin = true; - this.themeWatcher.recheck(); + this.themeWatcher?.recheck(); } private viewHome(justRegistered = false): void { @@ -1136,7 +1133,7 @@ export default class MatrixChat extends React.PureComponent { this.setPage(PageType.HomePage); this.notifyNewScreen("home"); ThemeController.isLogin = false; - this.themeWatcher.recheck(); + this.themeWatcher?.recheck(); } private viewUser(userId: string, subAction: string): void { @@ -1357,7 +1354,7 @@ export default class MatrixChat extends React.PureComponent { */ private async onLoggedIn(): Promise { ThemeController.isLogin = false; - this.themeWatcher.recheck(); + this.themeWatcher?.recheck(); StorageManager.tryPersistStorage(); await this.onShowPostLoginScreen(); @@ -1650,16 +1647,12 @@ export default class MatrixChat extends React.PureComponent { } if (haveNewVersion) { - Modal.createDialogAsync( - import( - "../../async-components/views/dialogs/security/NewRecoveryMethodDialog" - ) as unknown as Promise, + Modal.createDialog( + lazy(() => import("../../async-components/views/dialogs/security/NewRecoveryMethodDialog")), ); } else { - Modal.createDialogAsync( - import( - "../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog" - ) as unknown as Promise, + Modal.createDialog( + lazy(() => import("../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog")), ); } }); @@ -2088,6 +2081,7 @@ export default class MatrixChat extends React.PureComponent { } else if (this.state.view === Views.E2E_SETUP) { view = ( { private readReceiptsByUserId: Map = new Map(); private readonly _showHiddenEvents: boolean; - private isMounted = false; + private unmounted = false; private readMarkerNode = createRef(); private whoIsTyping = createRef(); - private scrollPanel = createRef(); + public scrollPanel = createRef(); - private readonly showTypingNotificationsWatcherRef: string; + private showTypingNotificationsWatcherRef?: string; private eventTiles: Record = {}; // A map to allow groupers to maintain consistent keys even if their first event is uprooted due to back-pagination. @@ -268,22 +267,21 @@ export default class MessagePanel extends React.Component { // and we check this in a hot code path. This is also cached in our // RoomContext, however we still need a fallback for roomless MessagePanels. this._showHiddenEvents = SettingsStore.getValue("showHiddenEventsInTimeline"); + } + public componentDidMount(): void { + this.unmounted = false; this.showTypingNotificationsWatcherRef = SettingsStore.watchSetting( "showTypingNotifications", null, this.onShowTypingNotificationsChange, ); - } - - public componentDidMount(): void { this.calculateRoomMembersCount(); this.props.room?.currentState.on(RoomStateEvent.Update, this.calculateRoomMembersCount); - this.isMounted = true; } public componentWillUnmount(): void { - this.isMounted = false; + this.unmounted = true; this.props.room?.currentState.off(RoomStateEvent.Update, this.calculateRoomMembersCount); SettingsStore.unwatchSetting(this.showTypingNotificationsWatcherRef); this.readReceiptMap = {}; @@ -376,13 +374,13 @@ export default class MessagePanel extends React.Component { // +1: read marker is below the window public getReadMarkerPosition(): number | null { const readMarker = this.readMarkerNode.current; - const messageWrapper = this.scrollPanel.current; + const messageWrapper = this.scrollPanel.current?.divScroll; if (!readMarker || !messageWrapper) { return null; } - const wrapperRect = (ReactDOM.findDOMNode(messageWrapper) as HTMLElement).getBoundingClientRect(); + const wrapperRect = messageWrapper.getBoundingClientRect(); const readMarkerRect = readMarker.getBoundingClientRect(); // the read-marker pretends to have zero height when it is actually @@ -442,7 +440,7 @@ export default class MessagePanel extends React.Component { } private isUnmounting = (): boolean => { - return !this.isMounted; + return this.unmounted; }; public get showHiddenEvents(): boolean { diff --git a/src/components/structures/NonUrgentToastContainer.tsx b/src/components/structures/NonUrgentToastContainer.tsx index 2eca6db9349..d01bf789590 100644 --- a/src/components/structures/NonUrgentToastContainer.tsx +++ b/src/components/structures/NonUrgentToastContainer.tsx @@ -25,7 +25,9 @@ export default class NonUrgentToastContainer extends React.PureComponent { - private readonly dispatcherRef: string; - - public constructor(props: IProps) { - super(props); + private dispatcherRef?: string; + public componentDidMount(): void { this.dispatcherRef = defaultDispatcher.register(this.onAction); } diff --git a/src/components/structures/RoomStatusBar.tsx b/src/components/structures/RoomStatusBar.tsx index bd236f2286a..76f3b0c2298 100644 --- a/src/components/structures/RoomStatusBar.tsx +++ b/src/components/structures/RoomStatusBar.tsx @@ -103,6 +103,8 @@ export default class RoomStatusBar extends React.PureComponent { } public componentDidMount(): void { + this.unmounted = false; + const client = this.context; client.on(ClientEvent.Sync, this.onSyncStateChange); client.on(RoomEvent.LocalEchoUpdated, this.onRoomLocalEchoUpdated); diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 486a7fb6521..470b73de7c6 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -45,7 +45,7 @@ import ResizeNotifier from "../../utils/ResizeNotifier"; import ContentMessages from "../../ContentMessages"; import Modal from "../../Modal"; import { LegacyCallHandlerEvent } from "../../LegacyCallHandler"; -import dis, { defaultDispatcher } from "../../dispatcher/dispatcher"; +import defaultDispatcher from "../../dispatcher/dispatcher"; import * as Rooms from "../../Rooms"; import MainSplit from "./MainSplit"; import RightPanel from "./RightPanel"; @@ -351,8 +351,8 @@ export class RoomView extends React.Component { private static e2eStatusCache = new Map(); private readonly askToJoinEnabled: boolean; - private readonly dispatcherRef: string; - private settingWatchers: string[]; + private dispatcherRef?: string; + private settingWatchers: string[] = []; private unmounted = false; private permalinkCreators: Record = {}; @@ -418,62 +418,6 @@ export class RoomView extends React.Component { promptAskToJoin: false, viewRoomOpts: { buttons: [] }, }; - - this.dispatcherRef = dis.register(this.onAction); - context.client.on(ClientEvent.Room, this.onRoom); - context.client.on(RoomEvent.Timeline, this.onRoomTimeline); - context.client.on(RoomEvent.TimelineReset, this.onRoomTimelineReset); - context.client.on(RoomEvent.Name, this.onRoomName); - context.client.on(RoomStateEvent.Events, this.onRoomStateEvents); - context.client.on(RoomStateEvent.Update, this.onRoomStateUpdate); - context.client.on(RoomEvent.MyMembership, this.onMyMembership); - context.client.on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus); - context.client.on(CryptoEvent.UserTrustStatusChanged, this.onUserVerificationChanged); - context.client.on(CryptoEvent.KeysChanged, this.onCrossSigningKeysChanged); - context.client.on(MatrixEventEvent.Decrypted, this.onEventDecrypted); - // Start listening for RoomViewStore updates - context.roomViewStore.on(UPDATE_EVENT, this.onRoomViewStoreUpdate); - - context.rightPanelStore.on(UPDATE_EVENT, this.onRightPanelStoreUpdate); - - WidgetEchoStore.on(UPDATE_EVENT, this.onWidgetEchoStoreUpdate); - context.widgetStore.on(UPDATE_EVENT, this.onWidgetStoreUpdate); - - CallStore.instance.on(CallStoreEvent.ConnectedCalls, this.onConnectedCalls); - - this.props.resizeNotifier.on("isResizing", this.onIsResizing); - - this.settingWatchers = [ - SettingsStore.watchSetting("layout", null, (...[, , , value]) => - this.setState({ layout: value as Layout }), - ), - SettingsStore.watchSetting("lowBandwidth", null, (...[, , , value]) => - this.setState({ lowBandwidth: value as boolean }), - ), - SettingsStore.watchSetting("alwaysShowTimestamps", null, (...[, , , value]) => - this.setState({ alwaysShowTimestamps: value as boolean }), - ), - SettingsStore.watchSetting("showTwelveHourTimestamps", null, (...[, , , value]) => - this.setState({ showTwelveHourTimestamps: value as boolean }), - ), - SettingsStore.watchSetting(TimezoneHandler.USER_TIMEZONE_KEY, null, (...[, , , value]) => - this.setState({ userTimezone: value as string }), - ), - SettingsStore.watchSetting("readMarkerInViewThresholdMs", null, (...[, , , value]) => - this.setState({ readMarkerInViewThresholdMs: value as number }), - ), - SettingsStore.watchSetting("readMarkerOutOfViewThresholdMs", null, (...[, , , value]) => - this.setState({ readMarkerOutOfViewThresholdMs: value as number }), - ), - SettingsStore.watchSetting("showHiddenEventsInTimeline", null, (...[, , , value]) => - this.setState({ showHiddenEvents: value as boolean }), - ), - SettingsStore.watchSetting("urlPreviewsEnabled", null, this.onUrlPreviewsEnabledChange), - SettingsStore.watchSetting("urlPreviewsEnabled_e2ee", null, this.onUrlPreviewsEnabledChange), - SettingsStore.watchSetting("feature_dynamic_room_predecessors", null, (...[, , , value]) => - this.setState({ msc3946ProcessDynamicPredecessor: value as boolean }), - ), - ]; } private onIsResizing = (resizing: boolean): void => { @@ -493,7 +437,7 @@ export class RoomView extends React.Component { private onWidgetLayoutChange = (): void => { if (!this.state.room) return; - dis.dispatch({ + defaultDispatcher.dispatch({ action: "appsDrawer", show: true, }); @@ -654,7 +598,7 @@ export class RoomView extends React.Component { // Handle the use case of a link to a thread message // ie: #/room/roomId/eventId (eventId of a thread message) if (thread?.rootEvent && !initialEvent?.isThreadRoot) { - dis.dispatch({ + defaultDispatcher.dispatch({ action: Action.ShowThread, rootEvent: thread.rootEvent, initialEvent, @@ -760,7 +704,7 @@ export class RoomView extends React.Component { const activeCall = CallStore.instance.getActiveCall(this.state.roomId); if (activeCall === null) { // We disconnected from the call, so stop viewing it - dis.dispatch( + defaultDispatcher.dispatch( { action: Action.ViewRoom, room_id: this.state.roomId, @@ -904,6 +848,66 @@ export class RoomView extends React.Component { } public componentDidMount(): void { + this.unmounted = false; + + this.dispatcherRef = defaultDispatcher.register(this.onAction); + if (this.context.client) { + this.context.client.on(ClientEvent.Room, this.onRoom); + this.context.client.on(RoomEvent.Timeline, this.onRoomTimeline); + this.context.client.on(RoomEvent.TimelineReset, this.onRoomTimelineReset); + this.context.client.on(RoomEvent.Name, this.onRoomName); + this.context.client.on(RoomStateEvent.Events, this.onRoomStateEvents); + this.context.client.on(RoomStateEvent.Update, this.onRoomStateUpdate); + this.context.client.on(RoomEvent.MyMembership, this.onMyMembership); + this.context.client.on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus); + this.context.client.on(CryptoEvent.UserTrustStatusChanged, this.onUserVerificationChanged); + this.context.client.on(CryptoEvent.KeysChanged, this.onCrossSigningKeysChanged); + this.context.client.on(MatrixEventEvent.Decrypted, this.onEventDecrypted); + } + // Start listening for RoomViewStore updates + this.context.roomViewStore.on(UPDATE_EVENT, this.onRoomViewStoreUpdate); + + this.context.rightPanelStore.on(UPDATE_EVENT, this.onRightPanelStoreUpdate); + + WidgetEchoStore.on(UPDATE_EVENT, this.onWidgetEchoStoreUpdate); + this.context.widgetStore.on(UPDATE_EVENT, this.onWidgetStoreUpdate); + + CallStore.instance.on(CallStoreEvent.ConnectedCalls, this.onConnectedCalls); + + this.props.resizeNotifier.on("isResizing", this.onIsResizing); + + this.settingWatchers = [ + SettingsStore.watchSetting("layout", null, (...[, , , value]) => + this.setState({ layout: value as Layout }), + ), + SettingsStore.watchSetting("lowBandwidth", null, (...[, , , value]) => + this.setState({ lowBandwidth: value as boolean }), + ), + SettingsStore.watchSetting("alwaysShowTimestamps", null, (...[, , , value]) => + this.setState({ alwaysShowTimestamps: value as boolean }), + ), + SettingsStore.watchSetting("showTwelveHourTimestamps", null, (...[, , , value]) => + this.setState({ showTwelveHourTimestamps: value as boolean }), + ), + SettingsStore.watchSetting(TimezoneHandler.USER_TIMEZONE_KEY, null, (...[, , , value]) => + this.setState({ userTimezone: value as string }), + ), + SettingsStore.watchSetting("readMarkerInViewThresholdMs", null, (...[, , , value]) => + this.setState({ readMarkerInViewThresholdMs: value as number }), + ), + SettingsStore.watchSetting("readMarkerOutOfViewThresholdMs", null, (...[, , , value]) => + this.setState({ readMarkerOutOfViewThresholdMs: value as number }), + ), + SettingsStore.watchSetting("showHiddenEventsInTimeline", null, (...[, , , value]) => + this.setState({ showHiddenEvents: value as boolean }), + ), + SettingsStore.watchSetting("urlPreviewsEnabled", null, this.onUrlPreviewsEnabledChange), + SettingsStore.watchSetting("urlPreviewsEnabled_e2ee", null, this.onUrlPreviewsEnabledChange), + SettingsStore.watchSetting("feature_dynamic_room_predecessors", null, (...[, , , value]) => + this.setState({ msc3946ProcessDynamicPredecessor: value as boolean }), + ), + ]; + this.onRoomViewStoreUpdate(true); const call = this.getCallForRoom(); @@ -963,7 +967,7 @@ export class RoomView extends React.Component { // stop tracking room changes to format permalinks this.stopAllPermalinkCreators(); - dis.unregister(this.dispatcherRef); + defaultDispatcher.unregister(this.dispatcherRef); if (this.context.client) { this.context.client.removeListener(ClientEvent.Room, this.onRoom); this.context.client.removeListener(RoomEvent.Timeline, this.onRoomTimeline); @@ -1041,7 +1045,7 @@ export class RoomView extends React.Component { handled = true; break; case KeyBindingAction.UploadFile: { - dis.dispatch( + defaultDispatcher.dispatch( { action: "upload_file", context: TimelineRenderingType.Room, @@ -1141,7 +1145,7 @@ export class RoomView extends React.Component { if (payload.event && payload.event.getRoomId() !== this.state.roomId) { // If the event is in a different room (e.g. because the event to be edited is being displayed // in the results of an all-rooms search), we need to view that room first. - dis.dispatch({ + defaultDispatcher.dispatch({ action: Action.ViewRoom, room_id: payload.event.getRoomId(), metricsTrigger: undefined, @@ -1184,7 +1188,7 @@ export class RoomView extends React.Component { } // re-dispatch to the correct composer - dis.dispatch({ + defaultDispatcher.dispatch({ ...(payload as ComposerInsertPayload), timelineRenderingType, composerType: this.state.editState ? ComposerType.Edit : ComposerType.Send, @@ -1193,7 +1197,7 @@ export class RoomView extends React.Component { } case Action.FocusAComposer: { - dis.dispatch({ + defaultDispatcher.dispatch({ ...(payload as FocusComposerPayload), // re-dispatch to the correct composer (the send message will still be on screen even when editing a message) action: this.state.editState ? Action.FocusEditMessageComposer : Action.FocusSendMessageComposer, @@ -1299,7 +1303,7 @@ export class RoomView extends React.Component { if (containsEmoji(ev.getContent(), effect.emojis) || ev.getContent().msgtype === effect.msgType) { // For initial threads launch, chat effects are disabled see #19731 if (!ev.isRelation(THREAD_RELATION_TYPE.name)) { - dis.dispatch({ action: `effects.${effect.command}`, event: ev }); + defaultDispatcher.dispatch({ action: `effects.${effect.command}`, event: ev }); } } }); @@ -1359,7 +1363,7 @@ export class RoomView extends React.Component { liveTimeline: room.getLiveTimeline(), }); - dis.dispatch({ action: Action.RoomLoaded }); + defaultDispatcher.dispatch({ action: Action.RoomLoaded }); }; private onRoomTimelineReset = (room?: Room): void => { @@ -1557,7 +1561,7 @@ export class RoomView extends React.Component { private onInviteClick = (): void => { // open the room inviter - dis.dispatch({ + defaultDispatcher.dispatch({ action: "view_invite", roomId: this.getRoomId(), }); @@ -1568,7 +1572,7 @@ export class RoomView extends React.Component { if (this.context.client?.isGuest()) { // Join this room once the user has registered and logged in // (If we failed to peek, we may not have a valid room object.) - dis.dispatch>({ + defaultDispatcher.dispatch>({ action: Action.DoAfterSyncPrepared, deferred_action: { action: Action.ViewRoom, @@ -1576,13 +1580,13 @@ export class RoomView extends React.Component { metricsTrigger: undefined, }, }); - dis.dispatch({ action: "require_registration" }); + defaultDispatcher.dispatch({ action: "require_registration" }); } else { Promise.resolve().then(() => { const signUrl = this.props.threepidInvite?.signUrl; const roomId = this.getRoomId(); if (isNotUndefined(roomId)) { - dis.dispatch({ + defaultDispatcher.dispatch({ action: Action.JoinRoom, roomId, opts: { inviteSignUrl: signUrl }, @@ -1618,7 +1622,7 @@ export class RoomView extends React.Component { this.state.initialEventId === eventId ) { debuglog("Removing scroll_into_view flag from initial event"); - dis.dispatch({ + defaultDispatcher.dispatch({ action: Action.ViewRoom, room_id: this.getRoomId(), event_id: this.state.initialEventId, @@ -1634,7 +1638,7 @@ export class RoomView extends React.Component { const roomId = this.getRoomId(); if (!this.context.client || !roomId) return; if (this.context.client.isGuest()) { - dis.dispatch({ action: "require_registration" }); + defaultDispatcher.dispatch({ action: "require_registration" }); return; } @@ -1684,7 +1688,7 @@ export class RoomView extends React.Component { }; private onForgetClick = (): void => { - dis.dispatch({ + defaultDispatcher.dispatch({ action: "forget_room", room_id: this.getRoomId(), }); @@ -1698,7 +1702,7 @@ export class RoomView extends React.Component { }); this.context.client?.leave(roomId).then( () => { - dis.dispatch({ action: Action.ViewHomePage }); + defaultDispatcher.dispatch({ action: Action.ViewHomePage }); this.setState({ rejecting: false, }); @@ -1732,7 +1736,7 @@ export class RoomView extends React.Component { await this.context.client!.setIgnoredUsers(ignoredUsers); await this.context.client!.leave(this.state.roomId!); - dis.dispatch({ action: Action.ViewHomePage }); + defaultDispatcher.dispatch({ action: Action.ViewHomePage }); this.setState({ rejecting: false, }); @@ -1756,7 +1760,7 @@ export class RoomView extends React.Component { // using /leave rather than /join. In the short term though, we // just ignore them. // https://github.com/vector-im/vector-web/issues/1134 - dis.fire(Action.ViewRoomDirectory); + defaultDispatcher.fire(Action.ViewRoomDirectory); }; private onSearchChange = debounce((e: ChangeEvent): void => { @@ -1782,7 +1786,7 @@ export class RoomView extends React.Component { // If we were viewing a highlighted event, firing view_room without // an event will take care of both clearing the URL fragment and // jumping to the bottom - dis.dispatch({ + defaultDispatcher.dispatch({ action: Action.ViewRoom, room_id: this.getRoomId(), metricsTrigger: undefined, // room doesn't change @@ -1790,7 +1794,7 @@ export class RoomView extends React.Component { } else { // Otherwise we have to jump manually this.messagePanel?.jumpToLiveTimeline(); - dis.fire(Action.FocusSendMessageComposer); + defaultDispatcher.fire(Action.FocusSendMessageComposer); } }; @@ -1914,7 +1918,7 @@ export class RoomView extends React.Component { public onHiddenHighlightsClick = (): void => { const oldRoom = this.getOldRoom(); if (!oldRoom) return; - dis.dispatch({ + defaultDispatcher.dispatch({ action: Action.ViewRoom, room_id: oldRoom.roomId, metricsTrigger: "Predecessor", @@ -1997,7 +2001,7 @@ export class RoomView extends React.Component { const roomId = this.getRoomId(); if (isNotUndefined(roomId)) { - dis.dispatch({ + defaultDispatcher.dispatch({ action: Action.SubmitAskToJoin, roomId, opts: { reason }, @@ -2014,7 +2018,7 @@ export class RoomView extends React.Component { const roomId = this.getRoomId(); if (isNotUndefined(roomId)) { - dis.dispatch({ + defaultDispatcher.dispatch({ action: Action.CancelAskToJoin, roomId, }); @@ -2543,5 +2547,3 @@ export class RoomView extends React.Component { ); } } - -export default RoomView; diff --git a/src/components/structures/ScrollPanel.tsx b/src/components/structures/ScrollPanel.tsx index 6c0da3018ff..b354f6b0055 100644 --- a/src/components/structures/ScrollPanel.tsx +++ b/src/components/structures/ScrollPanel.tsx @@ -186,17 +186,17 @@ export default class ScrollPanel extends React.Component { private bottomGrowth!: number; private minListHeight!: number; private heightUpdateInProgress = false; - private divScroll: HTMLDivElement | null = null; + public divScroll: HTMLDivElement | null = null; public constructor(props: IProps) { super(props); - this.props.resizeNotifier?.on("middlePanelResizedNoisy", this.onResize); - this.resetScrollState(); } public componentDidMount(): void { + this.unmounted = false; + this.props.resizeNotifier?.on("middlePanelResizedNoisy", this.onResize); this.checkScroll(); } diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 4f0c895233e..3ea2a03c1a4 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -599,7 +599,7 @@ export default class SpaceRoomView extends React.PureComponent { public static contextType = MatrixClientContext; public declare context: React.ContextType; - private readonly dispatcherRef: string; + private dispatcherRef?: string; public constructor(props: IProps, context: React.ContextType) { super(props, context); @@ -621,12 +621,11 @@ export default class SpaceRoomView extends React.PureComponent { showRightPanel: RightPanelStore.instance.isOpenForRoom(this.props.space.roomId), myMembership: this.props.space.getMyMembership(), }; - - this.dispatcherRef = defaultDispatcher.register(this.onAction); - RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate); } public componentDidMount(): void { + this.dispatcherRef = defaultDispatcher.register(this.onAction); + RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate); this.context.on(RoomEvent.MyMembership, this.onMyMembership); } diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index e5c1ccb266c..be538a66693 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -77,8 +77,8 @@ export default class ThreadView extends React.Component { public static contextType = RoomContext; public declare context: React.ContextType; - private dispatcherRef: string | null = null; - private readonly layoutWatcherRef: string; + private dispatcherRef?: string; + private layoutWatcherRef?: string; private timelinePanel = createRef(); private card = createRef(); @@ -91,7 +91,6 @@ export default class ThreadView extends React.Component { this.setEventId(this.props.mxEvent); const thread = this.props.room.getThread(this.eventId) ?? undefined; - this.setupThreadListeners(thread); this.state = { layout: SettingsStore.getValue("layout"), narrow: false, @@ -100,13 +99,15 @@ export default class ThreadView extends React.Component { return ev.isRelation(THREAD_RELATION_TYPE.name) && !ev.status; }), }; + } + + public componentDidMount(): void { + this.setupThreadListeners(this.state.thread); this.layoutWatcherRef = SettingsStore.watchSetting("layout", null, (...[, , , value]) => this.setState({ layout: value as Layout }), ); - } - public componentDidMount(): void { if (this.state.thread) { this.postThreadUpdate(this.state.thread); } @@ -118,7 +119,7 @@ export default class ThreadView extends React.Component { } public componentWillUnmount(): void { - if (this.dispatcherRef) dis.unregister(this.dispatcherRef); + dis.unregister(this.dispatcherRef); const roomId = this.props.mxEvent.getRoomId(); SettingsStore.unwatchSetting(this.layoutWatcherRef); diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index b55709e8c28..68b65965f59 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -7,7 +7,6 @@ Please see LICENSE files in the repository root for full details. */ import React, { createRef, ReactNode } from "react"; -import ReactDOM from "react-dom"; import { Room, RoomEvent, @@ -67,9 +66,6 @@ const READ_RECEIPT_INTERVAL_MS = 500; const READ_MARKER_DEBOUNCE_MS = 100; -// How far off-screen a decryption failure can be for it to still count as "visible" -const VISIBLE_DECRYPTION_FAILURE_MARGIN = 100; - const debuglog = (...args: any[]): void => { if (SettingsStore.getValue("debug_timeline_panel")) { logger.log.call(console, "TimelinePanel debuglog:", ...args); @@ -252,7 +248,7 @@ class TimelinePanel extends React.Component { private lastRMSentEventId: string | null | undefined = undefined; private readonly messagePanel = createRef(); - private readonly dispatcherRef: string; + private dispatcherRef?: string; private timelineWindow?: TimelineWindow; private overlayTimelineWindow?: TimelineWindow; private unmounted = false; @@ -295,6 +291,10 @@ class TimelinePanel extends React.Component { readMarkerInViewThresholdMs: SettingsStore.getValue("readMarkerInViewThresholdMs"), readMarkerOutOfViewThresholdMs: SettingsStore.getValue("readMarkerOutOfViewThresholdMs"), }; + } + + public componentDidMount(): void { + this.unmounted = false; this.dispatcherRef = dis.register(this.onAction); const cli = MatrixClientPeg.safeGet(); @@ -316,9 +316,7 @@ class TimelinePanel extends React.Component { cli.on(ClientEvent.Sync, this.onSync); this.props.timelineSet.room?.on(ThreadEvent.Update, this.onThreadUpdate); - } - public componentDidMount(): void { if (this.props.manageReadReceipts) { this.updateReadReceiptOnUserActivity(); } @@ -398,6 +396,10 @@ class TimelinePanel extends React.Component { } } + private get messagePanelDiv(): HTMLDivElement | null { + return this.messagePanel.current?.scrollPanel.current?.divScroll ?? null; + } + /** * Logs out debug info to describe the state of the TimelinePanel and the * events in the room according to the matrix-js-sdk. This is useful when @@ -418,15 +420,12 @@ class TimelinePanel extends React.Component { // And we can suss out any corrupted React `key` problems. let renderedEventIds: string[] | undefined; try { - const messagePanel = this.messagePanel.current; - if (messagePanel) { - const messagePanelNode = ReactDOM.findDOMNode(messagePanel) as Element; - if (messagePanelNode) { - const actuallyRenderedEvents = messagePanelNode.querySelectorAll("[data-event-id]"); - renderedEventIds = [...actuallyRenderedEvents].map((renderedEvent) => { - return renderedEvent.getAttribute("data-event-id")!; - }); - } + const messagePanelNode = this.messagePanelDiv; + if (messagePanelNode) { + const actuallyRenderedEvents = messagePanelNode.querySelectorAll("[data-event-id]"); + renderedEventIds = [...actuallyRenderedEvents].map((renderedEvent) => { + return renderedEvent.getAttribute("data-event-id")!; + }); } } catch (err) { logger.error(`onDumpDebugLogs: Failed to get the actual event ID's in the DOM`, err); @@ -1766,45 +1765,6 @@ class TimelinePanel extends React.Component { return index > -1 ? index : null; } - /** - * Get a list of undecryptable events currently visible on-screen. - * - * @param {boolean} addMargin Whether to add an extra margin beyond the viewport - * where events are still considered "visible" - * - * @returns {MatrixEvent[] | null} A list of undecryptable events, or null if - * the list of events could not be determined. - */ - public getVisibleDecryptionFailures(addMargin?: boolean): MatrixEvent[] | null { - const messagePanel = this.messagePanel.current; - if (!messagePanel) return null; - - const messagePanelNode = ReactDOM.findDOMNode(messagePanel) as Element; - if (!messagePanelNode) return null; // sometimes this happens for fresh rooms/post-sync - const wrapperRect = messagePanelNode.getBoundingClientRect(); - const margin = addMargin ? VISIBLE_DECRYPTION_FAILURE_MARGIN : 0; - const screenTop = wrapperRect.top - margin; - const screenBottom = wrapperRect.bottom + margin; - - const result: MatrixEvent[] = []; - for (const ev of this.state.liveEvents) { - const eventId = ev.getId(); - if (!eventId) continue; - const node = messagePanel.getNodeForEventId(eventId); - if (!node) continue; - - const boundingRect = node.getBoundingClientRect(); - if (boundingRect.top > screenBottom) { - // we have gone past the visible section of timeline - break; - } else if (boundingRect.bottom >= screenTop) { - // the tile for this event is in the visible part of the screen (or just above/below it). - if (ev.isDecryptionFailure()) result.push(ev); - } - } - return result; - } - private getLastDisplayedEventIndex(opts: IEventIndexOpts = {}): number | null { const ignoreOwn = opts.ignoreOwn || false; const allowPartial = opts.allowPartial || false; @@ -1812,7 +1772,7 @@ class TimelinePanel extends React.Component { const messagePanel = this.messagePanel.current; if (!messagePanel) return null; - const messagePanelNode = ReactDOM.findDOMNode(messagePanel) as Element; + const messagePanelNode = this.messagePanelDiv; if (!messagePanelNode) return null; // sometimes this happens for fresh rooms/post-sync const wrapperRect = messagePanelNode.getBoundingClientRect(); const myUserId = MatrixClientPeg.safeGet().credentials.userId; diff --git a/src/components/structures/ToastContainer.tsx b/src/components/structures/ToastContainer.tsx index 8c572442a02..3e5b4a4474b 100644 --- a/src/components/structures/ToastContainer.tsx +++ b/src/components/structures/ToastContainer.tsx @@ -24,12 +24,11 @@ export default class ToastContainer extends React.Component<{}, IState> { toasts: ToastStore.sharedInstance().getToasts(), countSeen: ToastStore.sharedInstance().getCountSeen(), }; + } - // Start listening here rather than in componentDidMount because - // toasts may dismiss themselves in their didMount if they find - // they're already irrelevant by the time they're mounted, and - // our own componentDidMount is too late. + public componentDidMount(): void { ToastStore.sharedInstance().on("update", this.onToastStoreUpdate); + this.onToastStoreUpdate(); } public componentWillUnmount(): void { diff --git a/src/components/structures/UploadBar.tsx b/src/components/structures/UploadBar.tsx index 93ce6d6bf22..01ecae96dce 100644 --- a/src/components/structures/UploadBar.tsx +++ b/src/components/structures/UploadBar.tsx @@ -46,7 +46,7 @@ function isUploadPayload(payload: ActionPayload): payload is UploadPayload { export default class UploadBar extends React.PureComponent { private dispatcherRef: Optional; - private mounted = false; + private unmounted = false; public constructor(props: IProps) { super(props); @@ -57,12 +57,12 @@ export default class UploadBar extends React.PureComponent { } public componentDidMount(): void { + this.unmounted = false; this.dispatcherRef = dis.register(this.onAction); - this.mounted = true; } public componentWillUnmount(): void { - this.mounted = false; + this.unmounted = true; dis.unregister(this.dispatcherRef!); } @@ -83,7 +83,7 @@ export default class UploadBar extends React.PureComponent { } private onAction = (payload: ActionPayload): void => { - if (!this.mounted) return; + if (this.unmounted) return; if (isUploadPayload(payload)) { this.setState(this.calculateState()); } diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 971e07193bd..b2c79907468 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -96,9 +96,6 @@ export default class UserMenu extends React.Component { selectedSpace: SpaceStore.instance.activeSpaceRoom, showLiveAvatarAddon: this.context.voiceBroadcastRecordingsStore.hasCurrent(), }; - - OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate); - SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate); } private get hasHomePage(): boolean { @@ -112,6 +109,8 @@ export default class UserMenu extends React.Component { }; public componentDidMount(): void { + OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate); + SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate); this.context.voiceBroadcastRecordingsStore.on( VoiceBroadcastRecordingsStoreEvent.CurrentChanged, this.onCurrentVoiceBroadcastRecordingChanged, @@ -121,9 +120,9 @@ export default class UserMenu extends React.Component { } public componentWillUnmount(): void { - if (this.themeWatcherRef) SettingsStore.unwatchSetting(this.themeWatcherRef); - if (this.dndWatcherRef) SettingsStore.unwatchSetting(this.dndWatcherRef); - if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef); + SettingsStore.unwatchSetting(this.themeWatcherRef); + SettingsStore.unwatchSetting(this.dndWatcherRef); + defaultDispatcher.unregister(this.dispatcherRef); OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate); SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate); this.context.voiceBroadcastRecordingsStore.off( diff --git a/src/components/structures/auth/CompleteSecurity.tsx b/src/components/structures/auth/CompleteSecurity.tsx index a74e07692d5..ec65a62cefa 100644 --- a/src/components/structures/auth/CompleteSecurity.tsx +++ b/src/components/structures/auth/CompleteSecurity.tsx @@ -29,11 +29,15 @@ export default class CompleteSecurity extends React.Component { public constructor(props: IProps) { super(props); const store = SetupEncryptionStore.sharedInstance(); - store.on("update", this.onStoreUpdate); store.start(); this.state = { phase: store.phase, lostKeys: store.lostKeys() }; } + public componentDidMount(): void { + const store = SetupEncryptionStore.sharedInstance(); + store.on("update", this.onStoreUpdate); + } + private onStoreUpdate = (): void => { const store = SetupEncryptionStore.sharedInstance(); this.setState({ phase: store.phase, lostKeys: store.lostKeys() }); diff --git a/src/components/structures/auth/E2eSetup.tsx b/src/components/structures/auth/E2eSetup.tsx index 9e103f2ac5e..80a135fe19f 100644 --- a/src/components/structures/auth/E2eSetup.tsx +++ b/src/components/structures/auth/E2eSetup.tsx @@ -7,15 +7,17 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; +import { MatrixClient } from "matrix-js-sdk/src/matrix"; import AuthPage from "../../views/auth/AuthPage"; import CompleteSecurityBody from "../../views/auth/CompleteSecurityBody"; import CreateCrossSigningDialog from "../../views/dialogs/security/CreateCrossSigningDialog"; interface IProps { + matrixClient: MatrixClient; onFinished: () => void; accountPassword?: string; - tokenLogin?: boolean; + tokenLogin: boolean; } export default class E2eSetup extends React.Component { @@ -24,6 +26,7 @@ export default class E2eSetup extends React.Component { } public componentDidMount(): void { + this.unmounted = false; this.initLoginLogic(this.props.serverConfig); } diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index 666313321a7..32528fc7e39 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -39,7 +39,6 @@ export default class SetupEncryptionBody extends React.Component public constructor(props: IProps) { super(props); const store = SetupEncryptionStore.sharedInstance(); - store.on("update", this.onStoreUpdate); store.start(); this.state = { phase: store.phase, @@ -52,6 +51,11 @@ export default class SetupEncryptionBody extends React.Component }; } + public componentDidMount(): void { + const store = SetupEncryptionStore.sharedInstance(); + store.on("update", this.onStoreUpdate); + } + private onStoreUpdate = (): void => { const store = SetupEncryptionStore.sharedInstance(); if (store.phase === Phase.Finished) { diff --git a/src/components/structures/auth/forgot-password/CheckEmail.tsx b/src/components/structures/auth/forgot-password/CheckEmail.tsx index feca3318941..dbc667c07ee 100644 --- a/src/components/structures/auth/forgot-password/CheckEmail.tsx +++ b/src/components/structures/auth/forgot-password/CheckEmail.tsx @@ -8,10 +8,10 @@ Please see LICENSE files in the repository root for full details. import React, { ReactNode } from "react"; import { Tooltip } from "@vector-im/compound-web"; +import { RestartIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import AccessibleButton from "../../../views/elements/AccessibleButton"; import { Icon as EMailPromptIcon } from "../../../../../res/img/element-icons/email-prompt.svg"; -import { Icon as RetryIcon } from "../../../../../res/img/compound/retry-16px.svg"; import { _t } from "../../../../languageHandler"; import { useTimeoutToggle } from "../../../../hooks/useTimeoutToggle"; import { ErrorMessage } from "../../ErrorMessage"; @@ -60,7 +60,7 @@ export const CheckEmail: React.FC = ({ {_t("auth|check_email_resend_prompt")} - + {_t("action|resend")} diff --git a/src/components/structures/auth/forgot-password/VerifyEmailModal.tsx b/src/components/structures/auth/forgot-password/VerifyEmailModal.tsx index d883177d0c7..24caa2b13dc 100644 --- a/src/components/structures/auth/forgot-password/VerifyEmailModal.tsx +++ b/src/components/structures/auth/forgot-password/VerifyEmailModal.tsx @@ -8,10 +8,10 @@ Please see LICENSE files in the repository root for full details. import React, { ReactNode } from "react"; import { Tooltip } from "@vector-im/compound-web"; +import { RestartIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { _t } from "../../../../languageHandler"; import AccessibleButton from "../../../views/elements/AccessibleButton"; -import { Icon as RetryIcon } from "../../../../../res/img/compound/retry-16px.svg"; import { Icon as EmailPromptIcon } from "../../../../../res/img/element-icons/email-prompt.svg"; import { useTimeoutToggle } from "../../../../hooks/useTimeoutToggle"; import { ErrorMessage } from "../../ErrorMessage"; @@ -59,7 +59,7 @@ export const VerifyEmailModal: React.FC = ({ {_t("auth|check_email_resend_prompt")} - + {_t("action|resend")} diff --git a/src/components/views/audio_messages/AudioPlayerBase.tsx b/src/components/views/audio_messages/AudioPlayerBase.tsx index 70e30dcccac..601611e4223 100644 --- a/src/components/views/audio_messages/AudioPlayerBase.tsx +++ b/src/components/views/audio_messages/AudioPlayerBase.tsx @@ -41,7 +41,9 @@ export default abstract class AudioPlayerBase extends this.state = { playbackPhase: this.props.playback.currentState, }; + } + public componentDidMount(): void { // We don't need to de-register: the class handles this for us internally this.props.playback.on(UPDATE_EVENT, this.onPlaybackUpdate); diff --git a/src/components/views/audio_messages/Clock.tsx b/src/components/views/audio_messages/Clock.tsx index 56661c7a0c5..ca72d29a057 100644 --- a/src/components/views/audio_messages/Clock.tsx +++ b/src/components/views/audio_messages/Clock.tsx @@ -27,10 +27,6 @@ export default class Clock extends React.Component { formatFn: formatSeconds, }; - public constructor(props: Props) { - super(props); - } - public shouldComponentUpdate(nextProps: Readonly): boolean { const currentFloor = Math.floor(this.props.seconds); const nextFloor = Math.floor(nextProps.seconds); diff --git a/src/components/views/audio_messages/DurationClock.tsx b/src/components/views/audio_messages/DurationClock.tsx index e495098144e..3794ab9a4f0 100644 --- a/src/components/views/audio_messages/DurationClock.tsx +++ b/src/components/views/audio_messages/DurationClock.tsx @@ -33,6 +33,9 @@ export default class DurationClock extends React.PureComponent { // member property to track "did we get a duration". durationSeconds: this.props.playback.clockInfo.durationSeconds, }; + } + + public componentDidMount(): void { this.props.playback.clockInfo.liveData.onUpdate(this.onTimeUpdate); } diff --git a/src/components/views/audio_messages/PlayPauseButton.tsx b/src/components/views/audio_messages/PlayPauseButton.tsx index 1053a89eea0..1cd2d168b4a 100644 --- a/src/components/views/audio_messages/PlayPauseButton.tsx +++ b/src/components/views/audio_messages/PlayPauseButton.tsx @@ -26,10 +26,6 @@ type Props = Omit, "title" | "onClick" | "disabled" | "elemen * to be displayed in reference to a recording. */ export default class PlayPauseButton extends React.PureComponent { - public constructor(props: Props) { - super(props); - } - private onClick = (): void => { // noinspection JSIgnoredPromiseFromCall this.toggleState(); diff --git a/src/components/views/audio_messages/PlaybackClock.tsx b/src/components/views/audio_messages/PlaybackClock.tsx index 8de3cb71e68..b3d736758be 100644 --- a/src/components/views/audio_messages/PlaybackClock.tsx +++ b/src/components/views/audio_messages/PlaybackClock.tsx @@ -43,6 +43,9 @@ export default class PlaybackClock extends React.PureComponent { durationSeconds: this.props.playback.clockInfo.durationSeconds, playbackPhase: PlaybackState.Stopped, // assume not started, so full clock }; + } + + public componentDidMount(): void { this.props.playback.on(UPDATE_EVENT, this.onPlaybackUpdate); this.props.playback.clockInfo.liveData.onUpdate(this.onTimeUpdate); } diff --git a/src/components/views/audio_messages/PlaybackWaveform.tsx b/src/components/views/audio_messages/PlaybackWaveform.tsx index 5f592898795..0f95f7084bf 100644 --- a/src/components/views/audio_messages/PlaybackWaveform.tsx +++ b/src/components/views/audio_messages/PlaybackWaveform.tsx @@ -34,7 +34,9 @@ export default class PlaybackWaveform extends React.PureComponent { this.state = { percentage: percentageOf(this.props.playback.timeSeconds, 0, this.props.playback.durationSeconds), }; + } + public componentDidMount(): void { // We don't need to de-register: the class handles this for us internally this.props.playback.liveData.onUpdate(() => this.animationFrameFn.mark()); } diff --git a/src/components/views/auth/AuthFooter.tsx b/src/components/views/auth/AuthFooter.tsx index c81617b9db0..8d27a04c83d 100644 --- a/src/components/views/auth/AuthFooter.tsx +++ b/src/components/views/auth/AuthFooter.tsx @@ -7,18 +7,36 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React from "react"; +import React, { ReactElement } from "react"; +import SdkConfig from "../../../SdkConfig"; import { _t } from "../../../languageHandler"; -export default class AuthFooter extends React.Component { - public render(): React.ReactNode { - return ( - +const AuthFooter = (): ReactElement => { + const brandingConfig = SdkConfig.getObject("branding"); + const links = brandingConfig?.get("auth_footer_links") ?? [ + { text: "Blog", url: "https://element.io/blog" }, + { text: "Twitter", url: "https://twitter.com/element_hq" }, + { text: "GitHub", url: "https://github.com/element-hq/element-web" }, + ]; + + const authFooterLinks: JSX.Element[] = []; + for (const linkEntry of links) { + authFooterLinks.push( + + {linkEntry.text} + , ); } -} + + return ( + + ); +}; + +export default AuthFooter; diff --git a/src/components/views/auth/AuthHeaderLogo.tsx b/src/components/views/auth/AuthHeaderLogo.tsx index 3ff11ba3f29..07cc2f978a1 100644 --- a/src/components/views/auth/AuthHeaderLogo.tsx +++ b/src/components/views/auth/AuthHeaderLogo.tsx @@ -1,5 +1,6 @@ /* Copyright 2019-2024 New Vector Ltd. +Copyright 2015, 2016 OpenMarket Ltd SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. @@ -7,8 +8,17 @@ Please see LICENSE files in the repository root for full details. import React from "react"; +import SdkConfig from "../../../SdkConfig"; + export default class AuthHeaderLogo extends React.PureComponent { - public render(): React.ReactNode { - return ; + public render(): React.ReactElement { + const brandingConfig = SdkConfig.getObject("branding"); + const logoUrl = brandingConfig?.get("auth_header_logo_url") ?? "themes/element/img/logos/element-logo.svg"; + + return ( + + ); } } diff --git a/src/components/views/auth/AuthPage.tsx b/src/components/views/auth/AuthPage.tsx index e9beb6d2a09..2782d0a641b 100644 --- a/src/components/views/auth/AuthPage.tsx +++ b/src/components/views/auth/AuthPage.tsx @@ -7,15 +7,69 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { ReactNode } from "react"; +import React from "react"; +import SdkConfig from "../../../SdkConfig"; import AuthFooter from "./AuthFooter"; -export default class AuthPage extends React.PureComponent<{ children: ReactNode }> { - public render(): React.ReactNode { +export default class AuthPage extends React.PureComponent { + private static welcomeBackgroundUrl?: string; + + // cache the url as a static to prevent it changing without refreshing + private static getWelcomeBackgroundUrl(): string { + if (AuthPage.welcomeBackgroundUrl) return AuthPage.welcomeBackgroundUrl; + + const brandingConfig = SdkConfig.getObject("branding"); + AuthPage.welcomeBackgroundUrl = "themes/element/img/backgrounds/lake.jpg"; + + const configuredUrl = brandingConfig?.get("welcome_background_url"); + if (configuredUrl) { + if (Array.isArray(configuredUrl)) { + const index = Math.floor(Math.random() * configuredUrl.length); + AuthPage.welcomeBackgroundUrl = configuredUrl[index]; + } else { + AuthPage.welcomeBackgroundUrl = configuredUrl; + } + } + + return AuthPage.welcomeBackgroundUrl; + } + + public render(): React.ReactElement { + const pageStyle = { + background: `center/cover fixed url(${AuthPage.getWelcomeBackgroundUrl()})`, + }; + + const modalStyle: React.CSSProperties = { + position: "relative", + background: "initial", + }; + + const blurStyle: React.CSSProperties = { + position: "absolute", + top: 0, + right: 0, + bottom: 0, + left: 0, + filter: "blur(40px)", + background: pageStyle.background, + }; + + const modalContentStyle: React.CSSProperties = { + display: "flex", + zIndex: 1, + background: "rgba(255, 255, 255, 0.59)", + borderRadius: "8px", + }; + return ( -
-
{this.props.children}
+
+
+
+
+ {this.props.children} +
+
); diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index 44ccd3a30ec..b1360f55604 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -801,7 +801,6 @@ export class SSOAuthEntry extends React.Component extends React.Component { private finished = false; @@ -104,9 +84,6 @@ export default class LoginWithQR extends React.Component { if (this.state.rendezvous) { const rendezvous = this.state.rendezvous; rendezvous.onFailure = undefined; - if (rendezvous instanceof MSC3906Rendezvous) { - await rendezvous.cancel(LegacyRendezvousFailureReason.UserCancelled); - } this.setState({ rendezvous: undefined }); } if (mode === Mode.Show) { @@ -119,60 +96,7 @@ export default class LoginWithQR extends React.Component { // eslint-disable-next-line react/no-direct-mutation-state this.state.rendezvous.onFailure = undefined; // calling cancel will call close() as well to clean up the resources - if (this.state.rendezvous instanceof MSC3906Rendezvous) { - this.state.rendezvous.cancel(LegacyRendezvousFailureReason.UserCancelled); - } else { - this.state.rendezvous.cancel(MSC4108FailureReason.UserCancelled); - } - } - } - - private async legacyApproveLogin(): Promise { - if (!(this.state.rendezvous instanceof MSC3906Rendezvous)) { - throw new Error("Rendezvous not found"); - } - if (!this.props.client) { - throw new Error("No client to approve login with"); - } - this.setState({ phase: Phase.Loading }); - - try { - logger.info("Requesting login token"); - - const { login_token: loginToken } = await wrapRequestWithDialog(this.props.client.requestLoginToken, { - matrixClient: this.props.client, - title: _t("auth|qr_code_login|sign_in_new_device"), - })(); - - this.setState({ phase: Phase.WaitingForDevice }); - - const newDeviceId = await this.state.rendezvous.approveLoginOnExistingDevice(loginToken); - if (!newDeviceId) { - // user denied - return; - } - if (!this.props.client.getCrypto()) { - // no E2EE to set up - this.onFinished(true); - return; - } - this.setState({ phase: Phase.Verifying }); - await this.state.rendezvous.verifyNewDeviceOnExistingDevice(); - // clean up our state: - try { - await this.state.rendezvous.close(); - } finally { - this.setState({ rendezvous: undefined }); - } - this.onFinished(true); - } catch (e) { - logger.error("Error whilst approving sign in", e); - if (e instanceof HTTPError && e.httpStatus === 429) { - // 429: rate limit - this.setState({ phase: Phase.Error, failureReason: LoginWithQRFailureReason.RateLimited }); - return; - } - this.setState({ phase: Phase.Error, failureReason: ClientRendezvousFailureReason.Unknown }); + this.state.rendezvous.cancel(MSC4108FailureReason.UserCancelled); } } @@ -182,28 +106,18 @@ export default class LoginWithQR extends React.Component { } private generateAndShowCode = async (): Promise => { - let rendezvous: MSC4108SignInWithQR | MSC3906Rendezvous; + let rendezvous: MSC4108SignInWithQR; try { const fallbackRzServer = this.props.client?.getClientWellKnown()?.["io.element.rendezvous"]?.server; - if (this.props.legacy) { - const transport = new MSC3886SimpleHttpRendezvousTransport({ - onFailure: this.onFailure, - client: this.props.client, - fallbackRzServer, - }); - const channel = new MSC3903ECDHv2RendezvousChannel(transport, undefined, this.onFailure); - rendezvous = new MSC3906Rendezvous(channel, this.props.client, this.onFailure); - } else { - const transport = new MSC4108RendezvousSession({ - onFailure: this.onFailure, - client: this.props.client, - fallbackRzServer, - }); - await transport.send(""); - const channel = new MSC4108SecureChannel(transport, undefined, this.onFailure); - rendezvous = new MSC4108SignInWithQR(channel, false, this.props.client, this.onFailure); - } + const transport = new MSC4108RendezvousSession({ + onFailure: this.onFailure, + client: this.props.client, + fallbackRzServer, + }); + await transport.send(""); + const channel = new MSC4108SecureChannel(transport, undefined, this.onFailure); + rendezvous = new MSC4108SignInWithQR(channel, false, this.props.client, this.onFailure); await rendezvous.generateCode(); this.setState({ @@ -218,10 +132,7 @@ export default class LoginWithQR extends React.Component { } try { - if (rendezvous instanceof MSC3906Rendezvous) { - const confirmationDigits = await rendezvous.startAfterShowingCode(); - this.setState({ phase: Phase.LegacyConnected, confirmationDigits }); - } else if (this.ourIntent === RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE) { + if (this.ourIntent === RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE) { // MSC4108-Flow: NewScanned await rendezvous.negotiateProtocols(); const { verificationUri } = await rendezvous.deviceAuthorizationGrant(); @@ -234,18 +145,9 @@ export default class LoginWithQR extends React.Component { // we ask the user to confirm that the channel is secure } catch (e: RendezvousError | unknown) { logger.error("Error whilst approving login", e); - if (rendezvous instanceof MSC3906Rendezvous) { - // only set to error phase if it hasn't already been set by onFailure or similar - if (this.state.phase !== Phase.Error) { - this.setState({ phase: Phase.Error, failureReason: LegacyRendezvousFailureReason.Unknown }); - } - } else { - await rendezvous?.cancel( - e instanceof RendezvousError - ? (e.code as MSC4108FailureReason) - : ClientRendezvousFailureReason.Unknown, - ); - } + await rendezvous?.cancel( + e instanceof RendezvousError ? (e.code as MSC4108FailureReason) : ClientRendezvousFailureReason.Unknown, + ); } }; @@ -298,7 +200,6 @@ export default class LoginWithQR extends React.Component { public reset(): void { this.setState({ rendezvous: undefined, - confirmationDigits: undefined, verificationUri: undefined, failureReason: undefined, userCode: undefined, @@ -311,16 +212,12 @@ export default class LoginWithQR extends React.Component { private onClick = async (type: Click, checkCode?: string): Promise => { switch (type) { case Click.Cancel: - if (this.state.rendezvous instanceof MSC3906Rendezvous) { - await this.state.rendezvous?.cancel(LegacyRendezvousFailureReason.UserCancelled); - } else { - await this.state.rendezvous?.cancel(MSC4108FailureReason.UserCancelled); - } + await this.state.rendezvous?.cancel(MSC4108FailureReason.UserCancelled); this.reset(); this.onFinished(false); break; case Click.Approve: - await (this.props.legacy ? this.legacyApproveLogin() : this.approveLogin(checkCode)); + await this.approveLogin(checkCode); break; case Click.Decline: await this.state.rendezvous?.declineLoginOnExistingDevice(); @@ -328,11 +225,7 @@ export default class LoginWithQR extends React.Component { this.onFinished(false); break; case Click.Back: - if (this.state.rendezvous instanceof MSC3906Rendezvous) { - await this.state.rendezvous?.cancel(LegacyRendezvousFailureReason.UserCancelled); - } else { - await this.state.rendezvous?.cancel(MSC4108FailureReason.UserCancelled); - } + await this.state.rendezvous?.cancel(MSC4108FailureReason.UserCancelled); this.onFinished(false); break; case Click.ShowQr: @@ -342,20 +235,6 @@ export default class LoginWithQR extends React.Component { }; public render(): React.ReactNode { - if (this.state.rendezvous instanceof MSC3906Rendezvous) { - return ( - - ); - } - return ( { - code?: string; - confirmationDigits?: string; -} - interface Props { phase: Phase; code?: Uint8Array; @@ -47,22 +33,14 @@ interface Props { checkCode?: string; } -// n.b MSC3886/MSC3903/MSC3906 that this is based on are now closed. -// However, we want to keep this implementation around for some time. -// TODO: define an end-of-life date for this implementation. - /** * A component that implements the UI for sign in and E2EE set up with a QR code. * - * This supports the unstable features of MSC3906 and MSC4108 + * This supports the unstable features of MSC4108 */ -export default class LoginWithQRFlow extends React.Component> { +export default class LoginWithQRFlow extends React.Component { private checkCodeInput = createRef(); - public constructor(props: XOR) { - super(props); - } - private handleClick = (type: Click): ((e: React.FormEvent) => Promise) => { return async (e: React.FormEvent): Promise => { e.preventDefault(); @@ -104,20 +82,17 @@ export default class LoginWithQRFlow extends React.Component -

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

-
{this.props.confirmationDigits}
-
-
- -
-
{_t("auth|qr_code_login|approve_access_warning")}
-
- - ); - - buttons = ( - <> - - {_t("action|approve")} - - - {_t("action|cancel")} - - - ); - break; case Phase.OutOfBandConfirmation: backButton = false; main = ( @@ -288,8 +228,7 @@ export default class LoginWithQRFlow extends React.Component diff --git a/src/components/views/auth/VectorAuthFooter.tsx b/src/components/views/auth/VectorAuthFooter.tsx deleted file mode 100644 index 234c6b127b3..00000000000 --- a/src/components/views/auth/VectorAuthFooter.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/* -Copyright 2019-2024 New Vector Ltd. -Copyright 2015, 2016 OpenMarket Ltd - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React, { ReactElement } from "react"; - -import SdkConfig from "../../../SdkConfig"; -import { _t } from "../../../languageHandler"; - -const VectorAuthFooter = (): ReactElement => { - const brandingConfig = SdkConfig.getObject("branding"); - const links = brandingConfig?.get("auth_footer_links") ?? [ - { text: "Blog", url: "https://element.io/blog" }, - { text: "Twitter", url: "https://twitter.com/element_hq" }, - { text: "GitHub", url: "https://github.com/element-hq/element-web" }, - ]; - - const authFooterLinks: JSX.Element[] = []; - for (const linkEntry of links) { - authFooterLinks.push( - - {linkEntry.text} - , - ); - } - - return ( - - ); -}; - -export default VectorAuthFooter; diff --git a/src/components/views/auth/VectorAuthHeaderLogo.tsx b/src/components/views/auth/VectorAuthHeaderLogo.tsx deleted file mode 100644 index 3cdf30cafc7..00000000000 --- a/src/components/views/auth/VectorAuthHeaderLogo.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/* -Copyright 2019-2024 New Vector Ltd. -Copyright 2015, 2016 OpenMarket Ltd - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import * as React from "react"; - -import SdkConfig from "../../../SdkConfig"; - -export default class VectorAuthHeaderLogo extends React.PureComponent { - public render(): React.ReactElement { - const brandingConfig = SdkConfig.getObject("branding"); - const logoUrl = brandingConfig?.get("auth_header_logo_url") ?? "themes/element/img/logos/element-logo.svg"; - - return ( - - ); - } -} diff --git a/src/components/views/auth/VectorAuthPage.tsx b/src/components/views/auth/VectorAuthPage.tsx deleted file mode 100644 index 969cc560a33..00000000000 --- a/src/components/views/auth/VectorAuthPage.tsx +++ /dev/null @@ -1,75 +0,0 @@ -/* -Copyright 2019-2024 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import * as React from "react"; - -import SdkConfig from "../../../SdkConfig"; -import VectorAuthFooter from "./VectorAuthFooter"; - -export default class VectorAuthPage extends React.PureComponent { - private static welcomeBackgroundUrl?: string; - - // cache the url as a static to prevent it changing without refreshing - private static getWelcomeBackgroundUrl(): string { - if (VectorAuthPage.welcomeBackgroundUrl) return VectorAuthPage.welcomeBackgroundUrl; - - const brandingConfig = SdkConfig.getObject("branding"); - VectorAuthPage.welcomeBackgroundUrl = "themes/element/img/backgrounds/lake.jpg"; - - const configuredUrl = brandingConfig?.get("welcome_background_url"); - if (configuredUrl) { - if (Array.isArray(configuredUrl)) { - const index = Math.floor(Math.random() * configuredUrl.length); - VectorAuthPage.welcomeBackgroundUrl = configuredUrl[index]; - } else { - VectorAuthPage.welcomeBackgroundUrl = configuredUrl; - } - } - - return VectorAuthPage.welcomeBackgroundUrl; - } - - public render(): React.ReactElement { - const pageStyle = { - background: `center/cover fixed url(${VectorAuthPage.getWelcomeBackgroundUrl()})`, - }; - - const modalStyle: React.CSSProperties = { - position: "relative", - background: "initial", - }; - - const blurStyle: React.CSSProperties = { - position: "absolute", - top: 0, - right: 0, - bottom: 0, - left: 0, - filter: "blur(40px)", - background: pageStyle.background, - }; - - const modalContentStyle: React.CSSProperties = { - display: "flex", - zIndex: 1, - background: "rgba(255, 255, 255, 0.59)", - borderRadius: "8px", - }; - - return ( -
-
-
-
- {this.props.children} -
-
- -
- ); - } -} diff --git a/src/components/views/avatars/MemberStatusMessageAvatar.tsx b/src/components/views/avatars/MemberStatusMessageAvatar.tsx deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/components/views/beacon/RoomCallBanner.tsx b/src/components/views/beacon/RoomCallBanner.tsx index 1615f6b010c..b5626da95be 100644 --- a/src/components/views/beacon/RoomCallBanner.tsx +++ b/src/components/views/beacon/RoomCallBanner.tsx @@ -12,7 +12,7 @@ import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "../../../languageHandler"; import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; -import dispatcher, { defaultDispatcher } from "../../../dispatcher/dispatcher"; +import defaultDispatcher from "../../../dispatcher/dispatcher"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { Action } from "../../../dispatcher/actions"; import { ConnectionState, ElementCall } from "../../../models/Call"; @@ -53,7 +53,7 @@ const RoomCallBannerInner: React.FC = ({ roomId, call }) => return; } - dispatcher.dispatch({ + defaultDispatcher.dispatch({ action: Action.ViewRoom, room_id: roomId, metricsTrigger: undefined, diff --git a/src/components/views/context_menus/GenericElementContextMenu.tsx b/src/components/views/context_menus/GenericElementContextMenu.tsx index 42ed8ce5be8..afb39d6ebe2 100644 --- a/src/components/views/context_menus/GenericElementContextMenu.tsx +++ b/src/components/views/context_menus/GenericElementContextMenu.tsx @@ -20,10 +20,6 @@ interface IProps { * menu. */ export default class GenericElementContextMenu extends React.Component { - public constructor(props: IProps) { - super(props); - } - public componentDidMount(): void { window.addEventListener("resize", this.resize); } diff --git a/src/components/views/context_menus/LegacyCallContextMenu.tsx b/src/components/views/context_menus/LegacyCallContextMenu.tsx index 817b4632e8a..e6bb191df8c 100644 --- a/src/components/views/context_menus/LegacyCallContextMenu.tsx +++ b/src/components/views/context_menus/LegacyCallContextMenu.tsx @@ -17,10 +17,6 @@ interface IProps extends IContextMenuProps { } export default class LegacyCallContextMenu extends React.Component { - public constructor(props: IProps) { - super(props); - } - public onHoldClick = (): void => { this.props.call.setRemoteOnHold(true); this.props.onFinished(); diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index 5cf947092b8..d5749658c98 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -507,7 +507,7 @@ export default class MessageContextMenu extends React.Component } let jumpToRelatedEventButton: JSX.Element | undefined; - const relatedEventId = mxEvent.relationEventId; + const relatedEventId = mxEvent.getAssociatedId(); if (relatedEventId && SettingsStore.getValue("developerMode")) { jumpToRelatedEventButton = ( = ({ if (error) { footer = ( <> - +
diff --git a/src/components/views/dialogs/BugReportDialog.tsx b/src/components/views/dialogs/BugReportDialog.tsx index 22a7efacb9c..373f30d3aeb 100644 --- a/src/components/views/dialogs/BugReportDialog.tsx +++ b/src/components/views/dialogs/BugReportDialog.tsx @@ -64,6 +64,11 @@ export default class BugReportDialog extends React.Component { this.unmounted = false; this.issueRef = React.createRef(); + } + + public componentDidMount(): void { + this.unmounted = false; + this.issueRef.current?.focus(); // Get all of the extra info dumped to the console when someone is about // to send debug logs. Since this is a fire and forget action, we do @@ -76,10 +81,6 @@ export default class BugReportDialog extends React.Component { }); } - public componentDidMount(): void { - this.issueRef.current?.focus(); - } - public componentWillUnmount(): void { this.unmounted = true; } diff --git a/src/components/views/dialogs/CreateRoomDialog.tsx b/src/components/views/dialogs/CreateRoomDialog.tsx index c5a8080e3f1..990efdda71d 100644 --- a/src/components/views/dialogs/CreateRoomDialog.tsx +++ b/src/components/views/dialogs/CreateRoomDialog.tsx @@ -113,14 +113,6 @@ export default class CreateRoomDialog extends React.Component { nameIsValid: false, canChangeEncryption: false, }; - - checkUserIsAllowedToChangeEncryption(cli, Preset.PrivateChat).then(({ allowChange, forcedValue }) => - this.setState((state) => ({ - canChangeEncryption: allowChange, - // override with forcedValue if it is set - isEncrypted: forcedValue ?? state.isEncrypted, - })), - ); } private roomCreateOptions(): IOpts { @@ -160,6 +152,15 @@ export default class CreateRoomDialog extends React.Component { } public componentDidMount(): void { + const cli = MatrixClientPeg.safeGet(); + checkUserIsAllowedToChangeEncryption(cli, Preset.PrivateChat).then(({ allowChange, forcedValue }) => + this.setState((state) => ({ + canChangeEncryption: allowChange, + // override with forcedValue if it is set + isEncrypted: forcedValue ?? state.isEncrypted, + })), + ); + // move focus to first field when showing dialog this.nameField.current?.focus(); } diff --git a/src/components/views/dialogs/DeactivateAccountDialog.tsx b/src/components/views/dialogs/DeactivateAccountDialog.tsx index fbcd26d38f4..d68c931cc16 100644 --- a/src/components/views/dialogs/DeactivateAccountDialog.tsx +++ b/src/components/views/dialogs/DeactivateAccountDialog.tsx @@ -58,7 +58,9 @@ export default class DeactivateAccountDialog extends React.Component { opponentProfileError: null, sas: null, }; + } + + public componentDidMount(): void { this.props.verifier.on(VerifierEvent.ShowSas, this.onVerifierShowSas); this.props.verifier.on(VerifierEvent.Cancel, this.onVerifierCancel); this.fetchOpponentProfile(); diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index 8e1d49c1389..35e04fb12e2 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -397,6 +397,7 @@ export default class InviteDialog extends React.PureComponent { this.state = { backupStatus: BackupStatus.LOADING, }; + } - // we can't call setState() immediately, so wait a beat - window.setTimeout(() => this.startLoadBackupStatus(), 0); + public componentDidMount(): void { + this.startLoadBackupStatus(); } /** kick off the asynchronous calls to populate `state.backupStatus` in the background */ @@ -115,10 +114,8 @@ export default class LogoutDialog extends React.Component { } private onExportE2eKeysClicked = (): void => { - Modal.createDialogAsync( - import("../../../async-components/views/dialogs/security/ExportE2eKeysDialog") as unknown as Promise< - typeof ExportE2eKeysDialog - >, + Modal.createDialog( + lazy(() => import("../../../async-components/views/dialogs/security/ExportE2eKeysDialog")), { matrixClient: MatrixClientPeg.safeGet(), }, @@ -146,10 +143,8 @@ export default class LogoutDialog extends React.Component { /* static = */ true, ); } else { - Modal.createDialogAsync( - import("../../../async-components/views/dialogs/security/CreateKeyBackupDialog") as unknown as Promise< - typeof CreateKeyBackupDialog - >, + Modal.createDialog( + lazy(() => import("../../../async-components/views/dialogs/security/CreateKeyBackupDialog")), undefined, undefined, /* priority = */ false, diff --git a/src/components/views/dialogs/ModalWidgetDialog.tsx b/src/components/views/dialogs/ModalWidgetDialog.tsx index 90f330c6256..7df9130a7aa 100644 --- a/src/components/views/dialogs/ModalWidgetDialog.tsx +++ b/src/components/views/dialogs/ModalWidgetDialog.tsx @@ -22,6 +22,7 @@ import { WidgetApiFromWidgetAction, WidgetKind, } from "matrix-widget-api"; +import { ErrorIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import BaseDialog from "./BaseDialog"; import { _t, getUserLanguage } from "../../../languageHandler"; @@ -33,7 +34,6 @@ import { arrayFastClone } from "../../../utils/arrays"; import { ElementWidget } from "../../../stores/widgets/StopGapWidget"; import { ELEMENT_CLIENT_ID } from "../../../identifiers"; import SettingsStore from "../../../settings/SettingsStore"; -import WarningBadgeSvg from "../../../../res/img/element-icons/warning-badge.svg"; interface IProps { widgetDefinition: IModalWidgetOpenRequestData; @@ -186,7 +186,7 @@ export default class ModalWidgetDialog extends React.PureComponent
- + {_t("widget|modal_data_warning", { widgetDomain: parsed.hostname, })} diff --git a/src/components/views/dialogs/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx index 2c4656745a2..cb804b8e004 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.tsx +++ b/src/components/views/dialogs/RoomSettingsDialog.tsx @@ -80,9 +80,7 @@ class RoomSettingsDialog extends React.Component { } public componentWillUnmount(): void { - if (this.dispatcherRef) { - dis.unregister(this.dispatcherRef); - } + dis.unregister(this.dispatcherRef); MatrixClientPeg.get()?.removeListener(RoomEvent.Name, this.onRoomName); MatrixClientPeg.get()?.removeListener(RoomStateEvent.Events, this.onStateEvent); diff --git a/src/components/views/dialogs/SpacePreferencesDialog.tsx b/src/components/views/dialogs/SpacePreferencesDialog.tsx index dd5898d5ff6..1361b2728fe 100644 --- a/src/components/views/dialogs/SpacePreferencesDialog.tsx +++ b/src/components/views/dialogs/SpacePreferencesDialog.tsx @@ -21,7 +21,7 @@ import { SpacePreferenceTab } from "../../../dispatcher/payloads/OpenSpacePrefer import { NonEmptyArray } from "../../../@types/common"; import SettingsTab from "../settings/tabs/SettingsTab"; import { SettingsSection } from "../settings/shared/SettingsSection"; -import SettingsSubsection, { SettingsSubsectionText } from "../settings/shared/SettingsSubsection"; +import { SettingsSubsection, SettingsSubsectionText } from "../settings/shared/SettingsSubsection"; interface IProps { space: Room; diff --git a/src/components/views/dialogs/VerificationRequestDialog.tsx b/src/components/views/dialogs/VerificationRequestDialog.tsx index 50644ccf30e..d2ea83f2af8 100644 --- a/src/components/views/dialogs/VerificationRequestDialog.tsx +++ b/src/components/views/dialogs/VerificationRequestDialog.tsx @@ -32,6 +32,9 @@ export default class VerificationRequestDialog extends React.Component { this.setState({ verificationRequest: r }); }); diff --git a/src/components/views/dialogs/devtools/VerificationExplorer.tsx b/src/components/views/dialogs/devtools/VerificationExplorer.tsx deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx index 69b13a93c96..73da6b178c9 100644 --- a/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx +++ b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx @@ -7,189 +7,93 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React from "react"; -import { CrossSigningKeys, AuthDict, MatrixError, UIAFlow, UIAResponse } from "matrix-js-sdk/src/matrix"; +import React, { useCallback, useEffect, useState } from "react"; import { logger } from "matrix-js-sdk/src/logger"; +import { MatrixClient } from "matrix-js-sdk/src/matrix"; -import { MatrixClientPeg } from "../../../../MatrixClientPeg"; import { _t } from "../../../../languageHandler"; -import Modal from "../../../../Modal"; -import { SSOAuthEntry } from "../../auth/InteractiveAuthEntryComponents"; import DialogButtons from "../../elements/DialogButtons"; import BaseDialog from "../BaseDialog"; import Spinner from "../../elements/Spinner"; -import InteractiveAuthDialog from "../InteractiveAuthDialog"; +import { createCrossSigning } from "../../../../CreateCrossSigning"; -interface IProps { +interface Props { + matrixClient: MatrixClient; accountPassword?: string; - tokenLogin?: boolean; + tokenLogin: boolean; onFinished: (success?: boolean) => void; } -interface IState { - error: boolean; - canUploadKeysWithPasswordOnly: boolean | null; - accountPassword: string; -} - /* * Walks the user through the process of creating a cross-signing keys. In most * cases, only a spinner is shown, but for more complex auth like SSO, the user * may need to complete some steps to proceed. */ -export default class CreateCrossSigningDialog extends React.PureComponent { - public constructor(props: IProps) { - super(props); +const CreateCrossSigningDialog: React.FC = ({ matrixClient, accountPassword, tokenLogin, onFinished }) => { + const [error, setError] = useState(false); - this.state = { - error: false, - // Does the server offer a UI auth flow with just m.login.password - // for /keys/device_signing/upload? - // If we have an account password in memory, let's simplify and - // assume it means password auth is also supported for device - // signing key upload as well. This avoids hitting the server to - // test auth flows, which may be slow under high load. - canUploadKeysWithPasswordOnly: props.accountPassword ? true : null, - accountPassword: props.accountPassword || "", - }; + const bootstrapCrossSigning = useCallback(async () => { + const cryptoApi = matrixClient.getCrypto(); + if (!cryptoApi) return; - if (!this.state.accountPassword) { - this.queryKeyUploadAuth(); - } - } + setError(false); - public componentDidMount(): void { - this.bootstrapCrossSigning(); - } - - private async queryKeyUploadAuth(): Promise { try { - await MatrixClientPeg.safeGet().uploadDeviceSigningKeys(undefined, {} as CrossSigningKeys); - // We should never get here: the server should always require - // UI auth to upload device signing keys. If we do, we upload - // no keys which would be a no-op. - logger.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!"); - } catch (error) { - if (!(error instanceof MatrixError) || !error.data || !error.data.flows) { - logger.log("uploadDeviceSigningKeys advertised no flows!"); - return; - } - const canUploadKeysWithPasswordOnly = error.data.flows.some((f: UIAFlow) => { - return f.stages.length === 1 && f.stages[0] === "m.login.password"; - }); - this.setState({ - canUploadKeysWithPasswordOnly, - }); - } - } - - private doBootstrapUIAuth = async ( - makeRequest: (authData: AuthDict) => Promise>, - ): Promise => { - if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) { - await makeRequest({ - type: "m.login.password", - identifier: { - type: "m.id.user", - user: MatrixClientPeg.safeGet().getUserId(), - }, - password: this.state.accountPassword, - }); - } else if (this.props.tokenLogin) { - // We are hoping the grace period is active - await makeRequest({}); - } else { - const dialogAesthetics = { - [SSOAuthEntry.PHASE_PREAUTH]: { - title: _t("auth|uia|sso_title"), - body: _t("auth|uia|sso_preauth_body"), - continueText: _t("auth|sso"), - continueKind: "primary", - }, - [SSOAuthEntry.PHASE_POSTAUTH]: { - title: _t("encryption|confirm_encryption_setup_title"), - body: _t("encryption|confirm_encryption_setup_body"), - continueText: _t("action|confirm"), - continueKind: "primary", - }, - }; - - const { finished } = Modal.createDialog(InteractiveAuthDialog, { - title: _t("encryption|bootstrap_title"), - matrixClient: MatrixClientPeg.safeGet(), - makeRequest, - aestheticsForStagePhases: { - [SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics, - [SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics, - }, - }); - const [confirmed] = await finished; - if (!confirmed) { - throw new Error("Cross-signing key upload auth canceled"); - } - } - }; - - private bootstrapCrossSigning = async (): Promise => { - this.setState({ - error: false, - }); - - try { - const cli = MatrixClientPeg.safeGet(); - await cli.getCrypto()?.bootstrapCrossSigning({ - authUploadDeviceSigningKeys: this.doBootstrapUIAuth, - }); - this.props.onFinished(true); + await createCrossSigning(matrixClient, tokenLogin, accountPassword); + onFinished(true); } catch (e) { - if (this.props.tokenLogin) { + if (tokenLogin) { // ignore any failures, we are relying on grace period here - this.props.onFinished(false); + onFinished(false); return; } - this.setState({ error: true }); + setError(true); logger.error("Error bootstrapping cross-signing", e); } - }; - - private onCancel = (): void => { - this.props.onFinished(false); - }; - - public render(): React.ReactNode { - let content; - if (this.state.error) { - content = ( -
-

{_t("encryption|unable_to_setup_keys_error")}

-
- -
+ }, [matrixClient, tokenLogin, accountPassword, onFinished]); + + const onCancel = useCallback(() => { + onFinished(false); + }, [onFinished]); + + useEffect(() => { + bootstrapCrossSigning(); + }, [bootstrapCrossSigning]); + + let content; + if (error) { + content = ( +
+

{_t("encryption|unable_to_setup_keys_error")}

+
+
- ); - } else { - content = ( -
- -
- ); - } - - return ( - -
{content}
-
+
+ ); + } else { + content = ( +
+ +
); } -} + + return ( + +
{content}
+
+ ); +}; + +export default CreateCrossSigningDialog; diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx index 4803ac52728..dae452fd5d0 100644 --- a/src/components/views/elements/AppTile.tsx +++ b/src/components/views/elements/AppTile.tsx @@ -134,29 +134,20 @@ export default class AppTile extends React.Component { private iframe?: HTMLIFrameElement; // ref to the iframe (callback style) private allowedWidgetsWatchRef?: string; private persistKey: string; - private sgWidget: StopGapWidget | null; + private sgWidget?: StopGapWidget; private dispatcherRef?: string; private unmounted = false; public constructor(props: IProps, context: ContextType) { super(props, context); - // Tiles in miniMode are floating, and therefore not docked - if (!this.props.miniMode) { - ActiveWidgetStore.instance.dockWidget( - this.props.app.id, - isAppWidget(this.props.app) ? this.props.app.roomId : null, - ); - } - // The key used for PersistedElement this.persistKey = getPersistKey(WidgetUtils.getWidgetUid(this.props.app)); try { this.sgWidget = new StopGapWidget(this.props); - this.setupSgListeners(); } catch (e) { logger.log("Failed to construct widget", e); - this.sgWidget = null; + this.sgWidget = undefined; } this.state = this.getNewState(props); @@ -303,6 +294,20 @@ export default class AppTile extends React.Component { } public componentDidMount(): void { + this.unmounted = false; + + // Tiles in miniMode are floating, and therefore not docked + if (!this.props.miniMode) { + ActiveWidgetStore.instance.dockWidget( + this.props.app.id, + isAppWidget(this.props.app) ? this.props.app.roomId : null, + ); + } + + if (this.sgWidget) { + this.setupSgListeners(); + } + // Only fetch IM token on mount if we're showing and have permission to load if (this.sgWidget && this.state.hasPermissionToLoad) { this.startWidget(); @@ -340,13 +345,13 @@ export default class AppTile extends React.Component { } // Widget action listeners - if (this.dispatcherRef) dis.unregister(this.dispatcherRef); + dis.unregister(this.dispatcherRef); if (this.props.room) { this.context.off(RoomEvent.MyMembership, this.onMyMembership); } - if (this.allowedWidgetsWatchRef) SettingsStore.unwatchSetting(this.allowedWidgetsWatchRef); + SettingsStore.unwatchSetting(this.allowedWidgetsWatchRef); OwnProfileStore.instance.removeListener(UPDATE_EVENT, this.onUserReady); } @@ -374,7 +379,7 @@ export default class AppTile extends React.Component { this.startWidget(); } catch (e) { logger.error("Failed to construct widget", e); - this.sgWidget = null; + this.sgWidget = undefined; } } @@ -607,7 +612,7 @@ export default class AppTile extends React.Component { }; public render(): React.ReactNode { - let appTileBody: JSX.Element; + let appTileBody: JSX.Element | undefined; // Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin // because that would allow the iframe to programmatically remove the sandbox attribute, but @@ -650,7 +655,7 @@ export default class AppTile extends React.Component {
); - } else if (!this.state.hasPermissionToLoad && this.props.room) { + } else if (!this.state.hasPermissionToLoad && this.props.room && this.sgWidget) { // only possible for room widgets, can assert this.props.room here const isEncrypted = this.context.isRoomEncrypted(this.props.room.roomId); appTileBody = ( @@ -677,7 +682,7 @@ export default class AppTile extends React.Component {
); - } else { + } else if (this.sgWidget) { appTileBody = ( <>
diff --git a/src/components/views/elements/DesktopCapturerSourcePicker.tsx b/src/components/views/elements/DesktopCapturerSourcePicker.tsx index 26b759bcb2a..e1f1def8360 100644 --- a/src/components/views/elements/DesktopCapturerSourcePicker.tsx +++ b/src/components/views/elements/DesktopCapturerSourcePicker.tsx @@ -41,10 +41,6 @@ export interface ExistingSourceIProps { } export class ExistingSource extends React.Component { - public constructor(props: ExistingSourceIProps) { - super(props); - } - private onClick = (): void => { this.props.onSelect(this.props.source); }; diff --git a/src/components/views/elements/Dropdown.tsx b/src/components/views/elements/Dropdown.tsx index c8802cd8803..a7ce84163c7 100644 --- a/src/components/views/elements/Dropdown.tsx +++ b/src/components/views/elements/Dropdown.tsx @@ -127,7 +127,9 @@ export default class Dropdown extends React.Component { // the current search query searchQuery: "", }; + } + public componentDidMount(): void { // Listen for all clicks on the document so we can close the // menu when the user clicks somewhere else document.addEventListener("click", this.onDocumentClick, false); diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 8032f07b6ee..711a221994f 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -8,9 +8,9 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { createRef, CSSProperties } from "react"; +import React, { createRef, CSSProperties, useRef, useState } from "react"; import FocusLock from "react-focus-lock"; -import { MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { MatrixEvent, parseErrorResponse } from "matrix-js-sdk/src/matrix"; import { _t } from "../../../languageHandler"; import MemberAvatar from "../avatars/MemberAvatar"; @@ -30,6 +30,9 @@ import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; import { getKeyBindingsManager } from "../../../KeyBindingsManager"; import { presentableTextForFile } from "../../../utils/FileUtils"; import AccessibleButton from "./AccessibleButton"; +import Modal from "../../../Modal"; +import ErrorDialog from "../dialogs/ErrorDialog"; +import { FileDownloader } from "../../../utils/FileDownloader"; // Max scale to keep gaps around the image const MAX_SCALE = 0.95; @@ -309,15 +312,6 @@ export default class ImageView extends React.Component { this.setZoomAndRotation(cur + 90); }; - private onDownloadClick = (): void => { - const a = document.createElement("a"); - a.href = this.props.src; - if (this.props.name) a.download = this.props.name; - a.target = "_blank"; - a.rel = "noreferrer noopener"; - a.click(); - }; - private onOpenContextMenu = (): void => { this.setState({ contextMenuDisplayed: true, @@ -555,11 +549,7 @@ export default class ImageView extends React.Component { title={_t("lightbox|rotate_right")} onClick={this.onRotateClockwiseClick} /> - + {contextMenuButton} { ); } } + +function DownloadButton({ url, fileName }: { url: string; fileName?: string }): JSX.Element { + const downloader = useRef(new FileDownloader()).current; + const [loading, setLoading] = useState(false); + const blobRef = useRef(); + + function showError(e: unknown): void { + Modal.createDialog(ErrorDialog, { + title: _t("timeline|download_failed"), + description: ( + <> +
{_t("timeline|download_failed_description")}
+
{e instanceof Error ? e.toString() : ""}
+ + ), + }); + setLoading(false); + } + + const onDownloadClick = async (): Promise => { + try { + if (loading) return; + setLoading(true); + + if (blobRef.current) { + // Cheat and trigger a download, again. + return downloadBlob(blobRef.current); + } + + const res = await fetch(url); + if (!res.ok) { + throw parseErrorResponse(res, await res.text()); + } + const blob = await res.blob(); + blobRef.current = blob; + await downloadBlob(blob); + } catch (e) { + showError(e); + } + }; + + async function downloadBlob(blob: Blob): Promise { + await downloader.download({ + blob, + name: fileName ?? _t("common|image"), + }); + setLoading(false); + } + + return ( + + ); +} diff --git a/src/components/views/elements/LinkWithTooltip.tsx b/src/components/views/elements/LinkWithTooltip.tsx index a9ca2606ae6..016297d9f1c 100644 --- a/src/components/views/elements/LinkWithTooltip.tsx +++ b/src/components/views/elements/LinkWithTooltip.tsx @@ -15,10 +15,6 @@ interface IProps extends Omit, "tab } export default class LinkWithTooltip extends React.Component { - public constructor(props: IProps) { - super(props); - } - public render(): React.ReactNode { const { children, tooltip, ...props } = this.props; diff --git a/src/components/views/elements/PersistedElement.tsx b/src/components/views/elements/PersistedElement.tsx index 1b7b6543e95..3feb8561453 100644 --- a/src/components/views/elements/PersistedElement.tsx +++ b/src/components/views/elements/PersistedElement.tsx @@ -5,8 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { MutableRefObject, ReactNode } from "react"; -import ReactDOM from "react-dom"; +import React, { MutableRefObject, ReactNode, StrictMode } from "react"; +import { createRoot, Root } from "react-dom/client"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import { TooltipProvider } from "@vector-im/compound-web"; @@ -24,7 +24,7 @@ export const getPersistKey = (appId: string): string => "widget_" + appId; // We contain all persisted elements within a master container to allow them all to be within the same // CSS stacking context, and thus be able to control their z-indexes relative to each other. function getOrCreateMasterContainer(): HTMLDivElement { - let container = getContainer("mx_PersistedElement_container"); + let container = document.getElementById("mx_PersistedElement_container") as HTMLDivElement; if (!container) { container = document.createElement("div"); container.id = "mx_PersistedElement_container"; @@ -34,18 +34,10 @@ function getOrCreateMasterContainer(): HTMLDivElement { return container; } -function getContainer(containerId: string): HTMLDivElement { - return document.getElementById(containerId) as HTMLDivElement; -} - function getOrCreateContainer(containerId: string): HTMLDivElement { - let container = getContainer(containerId); - - if (!container) { - container = document.createElement("div"); - container.id = containerId; - getOrCreateMasterContainer().appendChild(container); - } + const container = document.createElement("div"); + container.id = containerId; + getOrCreateMasterContainer().appendChild(container); return container; } @@ -79,21 +71,16 @@ interface IProps { */ export default class PersistedElement extends React.Component { private resizeObserver: ResizeObserver; - private dispatcherRef: string; + private dispatcherRef?: string; private childContainer?: HTMLDivElement; private child?: HTMLDivElement; + private static rootMap: Record = {}; + public constructor(props: IProps) { super(props); this.resizeObserver = new ResizeObserver(this.repositionChild); - // Annoyingly, a resize observer is insufficient, since we also care - // about when the element moves on the screen without changing its - // dimensions. Doesn't look like there's a ResizeObserver equivalent - // for this, so we bodge it by listening for document resize and - // the timeline_resize action. - window.addEventListener("resize", this.repositionChild); - this.dispatcherRef = dis.register(this.onAction); if (this.props.moveRef) this.props.moveRef.current = this.repositionChild; } @@ -106,14 +93,16 @@ export default class PersistedElement extends React.Component { * @param {string} persistKey Key used to uniquely identify this PersistedElement */ public static destroyElement(persistKey: string): void { - const container = getContainer("mx_persistedElement_" + persistKey); - if (container) { - container.remove(); + const pair = PersistedElement.rootMap[persistKey]; + if (pair) { + pair[0].unmount(); + pair[1].remove(); } + delete PersistedElement.rootMap[persistKey]; } public static isMounted(persistKey: string): boolean { - return Boolean(getContainer("mx_persistedElement_" + persistKey)); + return Boolean(PersistedElement.rootMap[persistKey]); } private collectChildContainer = (ref: HTMLDivElement): void => { @@ -132,6 +121,14 @@ export default class PersistedElement extends React.Component { }; public componentDidMount(): void { + // Annoyingly, a resize observer is insufficient, since we also care + // about when the element moves on the screen without changing its + // dimensions. Doesn't look like there's a ResizeObserver equivalent + // for this, so we bodge it by listening for document resize and + // the timeline_resize action. + window.addEventListener("resize", this.repositionChild); + this.dispatcherRef = dis.register(this.onAction); + this.updateChild(); this.renderApp(); } @@ -167,16 +164,25 @@ export default class PersistedElement extends React.Component { private renderApp(): void { const content = ( - - -
- {this.props.children} -
-
-
+ + + +
+ {this.props.children} +
+
+
+
); - ReactDOM.render(content, getOrCreateContainer("mx_persistedElement_" + this.props.persistKey)); + let rootPair = PersistedElement.rootMap[this.props.persistKey]; + if (!rootPair) { + const container = getOrCreateContainer("mx_persistedElement_" + this.props.persistKey); + const root = createRoot(container); + rootPair = [root, container]; + PersistedElement.rootMap[this.props.persistKey] = rootPair; + } + rootPair[0].render(content); } private updateChildVisibility(child?: HTMLDivElement, visible = false): void { diff --git a/src/components/views/elements/PowerSelector.tsx b/src/components/views/elements/PowerSelector.tsx index b600b2ba968..385d932b870 100644 --- a/src/components/views/elements/PowerSelector.tsx +++ b/src/components/views/elements/PowerSelector.tsx @@ -68,6 +68,7 @@ export default class PowerSelector extends React.C } public componentDidMount(): void { + this.unmounted = false; this.initStateFromProps(); } diff --git a/src/components/views/elements/ReplyChain.tsx b/src/components/views/elements/ReplyChain.tsx index 4eb3707031c..71846d60658 100644 --- a/src/components/views/elements/ReplyChain.tsx +++ b/src/components/views/elements/ReplyChain.tsx @@ -89,6 +89,7 @@ export default class ReplyChain extends React.Component { } public componentDidMount(): void { + this.unmounted = false; this.initialize(); this.trySetExpandableQuotes(); } diff --git a/src/components/views/elements/TextWithTooltip.tsx b/src/components/views/elements/TextWithTooltip.tsx index 34346cbe253..b589ce3635c 100644 --- a/src/components/views/elements/TextWithTooltip.tsx +++ b/src/components/views/elements/TextWithTooltip.tsx @@ -16,10 +16,6 @@ interface IProps extends HTMLAttributes { } export default class TextWithTooltip extends React.Component { - public constructor(props: IProps) { - super(props); - } - public render(): React.ReactNode { const { className, children, tooltip, tooltipProps } = this.props; diff --git a/src/components/views/emojipicker/ReactionPicker.tsx b/src/components/views/emojipicker/ReactionPicker.tsx index 2c2eb442a0a..b62df99e254 100644 --- a/src/components/views/emojipicker/ReactionPicker.tsx +++ b/src/components/views/emojipicker/ReactionPicker.tsx @@ -37,6 +37,9 @@ class ReactionPicker extends React.Component { this.state = { selectedEmojis: new Set(Object.keys(this.getReactions())), }; + } + + public componentDidMount(): void { this.addListeners(); } diff --git a/src/components/views/location/LocationButton.tsx b/src/components/views/location/LocationButton.tsx index 5ec67c42148..654a3b69f5f 100644 --- a/src/components/views/location/LocationButton.tsx +++ b/src/components/views/location/LocationButton.tsx @@ -23,7 +23,7 @@ export interface IProps { relation?: IEventRelation; } -export const LocationButton: React.FC = ({ roomId, sender, menuPosition, relation }) => { +const LocationButton: React.FC = ({ roomId, sender, menuPosition, relation }) => { const overflowMenuCloser = useContext(OverflowMenuContext); const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); diff --git a/src/components/views/location/MapError.tsx b/src/components/views/location/MapError.tsx index 5b19d10522c..319223d3f93 100644 --- a/src/components/views/location/MapError.tsx +++ b/src/components/views/location/MapError.tsx @@ -8,8 +8,8 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import classNames from "classnames"; +import { ErrorIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; -import { Icon as WarningBadge } from "../../../../res/img/element-icons/warning-badge.svg"; import { _t } from "../../../languageHandler"; import { getLocationShareErrorMessage, LocationShareError } from "../../../utils/location"; import AccessibleButton from "../elements/AccessibleButton"; @@ -29,7 +29,7 @@ export const MapError: React.FC = ({ error, isMinimised, classNam className={classNames("mx_MapError", className, { mx_MapError_isMinimised: isMinimised })} onClick={onClick} > - + {_t("location_sharing|failed_load_map")} diff --git a/src/components/views/location/index.tsx b/src/components/views/location/index.tsx index 452b7ac54f2..d51d6c58f36 100644 --- a/src/components/views/location/index.tsx +++ b/src/components/views/location/index.tsx @@ -22,16 +22,6 @@ export function Map(props: ComponentProps): JSX.Element { ); } -const LocationPickerComponent = lazy(() => import("./LocationPicker")); - -export function LocationPicker(props: ComponentProps): JSX.Element { - return ( - }> - - - ); -} - const SmartMarkerComponent = lazy(() => import("./SmartMarker")); export function SmartMarker(props: ComponentProps): JSX.Element { diff --git a/src/components/views/messages/DateSeparator.tsx b/src/components/views/messages/DateSeparator.tsx index 10ec7ddad90..98f397eb459 100644 --- a/src/components/views/messages/DateSeparator.tsx +++ b/src/components/views/messages/DateSeparator.tsx @@ -58,7 +58,9 @@ export default class DateSeparator extends React.Component { this.state = { jumpToDateEnabled: SettingsStore.getValue("feature_jump_to_date"), }; + } + public componentDidMount(): void { // We're using a watcher so the date headers in the timeline are updated // when the lab setting is toggled. this.settingWatcherRef = SettingsStore.watchSetting( @@ -71,7 +73,7 @@ export default class DateSeparator extends React.Component { } public componentWillUnmount(): void { - if (this.settingWatcherRef) SettingsStore.unwatchSetting(this.settingWatcherRef); + SettingsStore.unwatchSetting(this.settingWatcherRef); } private onContextMenuOpenClick = (e: ButtonEvent): void => { @@ -96,25 +98,29 @@ export default class DateSeparator extends React.Component { } private getLabel(): string { - const date = new Date(this.props.ts); - const disableRelativeTimestamps = !SettingsStore.getValue(UIFeature.TimelineEnableRelativeDates); - - // During the time the archive is being viewed, a specific day might not make sense, so we return the full date - if (this.props.forExport || disableRelativeTimestamps) return formatFullDateNoTime(date); - - const today = new Date(); - const yesterday = new Date(); - const days = getDaysArray("long"); - yesterday.setDate(today.getDate() - 1); - - if (date.toDateString() === today.toDateString()) { - return this.relativeTimeFormat.format(0, "day"); // Today - } else if (date.toDateString() === yesterday.toDateString()) { - return this.relativeTimeFormat.format(-1, "day"); // Yesterday - } else if (today.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) { - return days[date.getDay()]; // Sunday-Saturday - } else { - return formatFullDateNoTime(date); + try { + const date = new Date(this.props.ts); + const disableRelativeTimestamps = !SettingsStore.getValue(UIFeature.TimelineEnableRelativeDates); + + // During the time the archive is being viewed, a specific day might not make sense, so we return the full date + if (this.props.forExport || disableRelativeTimestamps) return formatFullDateNoTime(date); + + const today = new Date(); + const yesterday = new Date(); + const days = getDaysArray("long"); + yesterday.setDate(today.getDate() - 1); + + if (date.toDateString() === today.toDateString()) { + return this.relativeTimeFormat.format(0, "day"); // Today + } else if (date.toDateString() === yesterday.toDateString()) { + return this.relativeTimeFormat.format(-1, "day"); // Yesterday + } else if (today.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) { + return days[date.getDay()]; // Sunday-Saturday + } else { + return formatFullDateNoTime(date); + } + } catch { + return _t("common|message_timestamp_invalid"); } } diff --git a/src/components/views/messages/DecryptionFailureBody.tsx b/src/components/views/messages/DecryptionFailureBody.tsx index 81894fa51f1..108ec45b03e 100644 --- a/src/components/views/messages/DecryptionFailureBody.tsx +++ b/src/components/views/messages/DecryptionFailureBody.tsx @@ -10,7 +10,7 @@ import classNames from "classnames"; 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 { WarningIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { BlockIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { _t } from "../../../languageHandler"; import { IBodyProps } from "./IBodyProps"; @@ -41,7 +41,7 @@ function getErrorMessage(mxEvent: MatrixEvent, isVerified: boolean | undefined): case DecryptionFailureCode.SENDER_IDENTITY_PREVIOUSLY_VERIFIED: return ( - + {_t("timeline|decryption_failure|sender_identity_previously_verified")} ); @@ -49,7 +49,12 @@ function getErrorMessage(mxEvent: MatrixEvent, isVerified: boolean | undefined): case DecryptionFailureCode.UNSIGNED_SENDER_DEVICE: // TODO: event should be hidden instead of showing this error. // To be revisited as part of https://github.com/element-hq/element-meta/issues/2449 - return _t("timeline|decryption_failure|sender_unsigned_device"); + return ( + + + {_t("timeline|decryption_failure|sender_unsigned_device")} + + ); } return _t("timeline|decryption_failure|unable_to_decrypt"); } @@ -58,7 +63,8 @@ function getErrorMessage(mxEvent: MatrixEvent, isVerified: boolean | undefined): function errorClassName(mxEvent: MatrixEvent): string | null { switch (mxEvent.decryptionFailureReason) { case DecryptionFailureCode.SENDER_IDENTITY_PREVIOUSLY_VERIFIED: - return "mx_DecryptionFailureVerifiedIdentityChanged"; + case DecryptionFailureCode.UNSIGNED_SENDER_DEVICE: + return "mx_DecryptionFailureSenderTrustRequirement"; default: return null; diff --git a/src/components/views/messages/EditHistoryMessage.tsx b/src/components/views/messages/EditHistoryMessage.tsx index dcb8b82774c..8316d0835b3 100644 --- a/src/components/views/messages/EditHistoryMessage.tsx +++ b/src/components/views/messages/EditHistoryMessage.tsx @@ -13,8 +13,8 @@ import classNames from "classnames"; import * as HtmlUtils from "../../../HtmlUtils"; import { editBodyDiffToHtml } from "../../../utils/MessageDiffUtils"; import { formatTime } from "../../../DateUtils"; -import { pillifyLinks, unmountPills } from "../../../utils/pillify"; -import { tooltipifyLinks, unmountTooltips } from "../../../utils/tooltipify"; +import { pillifyLinks } from "../../../utils/pillify"; +import { tooltipifyLinks } from "../../../utils/tooltipify"; import { _t } from "../../../languageHandler"; import Modal from "../../../Modal"; import RedactedBody from "./RedactedBody"; @@ -23,6 +23,7 @@ import ConfirmAndWaitRedactDialog from "../dialogs/ConfirmAndWaitRedactDialog"; import ViewSource from "../../structures/ViewSource"; import SettingsStore from "../../../settings/SettingsStore"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import { ReactRootManager } from "../../../utils/react"; function getReplacedContent(event: MatrixEvent): IContent { const originalContent = event.getOriginalContent(); @@ -47,8 +48,8 @@ export default class EditHistoryMessage extends React.PureComponent; private content = createRef(); - private pills: Element[] = []; - private tooltips: Element[] = []; + private pills = new ReactRootManager(); + private tooltips = new ReactRootManager(); public constructor(props: IProps, context: React.ContextType) { super(props, context); @@ -103,7 +104,7 @@ export default class EditHistoryMessage extends React.PureComponent { public static contextType = RoomContext; public declare context: React.ContextType; - private unmounted = true; + private unmounted = false; private image = createRef(); + private placeholder = createRef(); private timeout?: number; private sizeWatcher?: string; @@ -367,7 +368,7 @@ export default class MImageBody extends React.Component { this.unmounted = true; MatrixClientPeg.get()?.off(ClientEvent.Sync, this.reconnectedListener); this.clearBlurhashTimeout(); - if (this.sizeWatcher) SettingsStore.unwatchSetting(this.sizeWatcher); + SettingsStore.unwatchSetting(this.sizeWatcher); if (this.state.isAnimated && this.state.thumbUrl) { URL.revokeObjectURL(this.state.thumbUrl); } @@ -453,7 +454,11 @@ export default class MImageBody extends React.Component { "mx_MImageBody_placeholder--blurhash": this.props.mxEvent.getContent().info?.[BLURHASH_FIELD], }); - placeholder =
{this.getPlaceholder(maxWidth, maxHeight)}
; + placeholder = ( +
+ {this.getPlaceholder(maxWidth, maxHeight)} +
+ ); } let showPlaceholder = Boolean(placeholder); @@ -499,8 +504,19 @@ export default class MImageBody extends React.Component { if (!this.props.forExport) { placeholder = ( - - {showPlaceholder ? placeholder : <> /* Transition always expects a child */} + + { + showPlaceholder ? ( + placeholder + ) : ( +
+ ) /* Transition always expects a child */ + } ); diff --git a/src/components/views/messages/MJitsiWidgetEvent.tsx b/src/components/views/messages/MJitsiWidgetEvent.tsx index a547a78f941..bec4f56164b 100644 --- a/src/components/views/messages/MJitsiWidgetEvent.tsx +++ b/src/components/views/messages/MJitsiWidgetEvent.tsx @@ -21,10 +21,6 @@ interface IProps { } export default class MJitsiWidgetEvent extends React.PureComponent { - public constructor(props: IProps) { - super(props); - } - public render(): React.ReactNode { const url = this.props.mxEvent.getContent()["url"]; const prevUrl = this.props.mxEvent.getPrevContent()["url"]; diff --git a/src/components/views/messages/MLocationBody.tsx b/src/components/views/messages/MLocationBody.tsx index 742587e0a7d..b226476fa85 100644 --- a/src/components/views/messages/MLocationBody.tsx +++ b/src/components/views/messages/MLocationBody.tsx @@ -75,6 +75,10 @@ export default class MLocationBody extends React.Component { this.context.on(ClientEvent.Sync, this.reconnectedListener); }; + public componentDidMount(): void { + this.unmounted = false; + } + public componentWillUnmount(): void { this.unmounted = true; this.context.off(ClientEvent.Sync, this.reconnectedListener); diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx index 4900432b8c6..4036b9ddecf 100644 --- a/src/components/views/messages/MVideoBody.tsx +++ b/src/components/views/messages/MVideoBody.tsx @@ -175,7 +175,7 @@ export default class MVideoBody extends React.PureComponent } public componentWillUnmount(): void { - if (this.sizeWatcher) SettingsStore.unwatchSetting(this.sizeWatcher); + SettingsStore.unwatchSetting(this.sizeWatcher); } private videoOnPlay = async (): Promise => { diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx index ddf637dee26..0776c514377 100644 --- a/src/components/views/messages/MessageActionBar.tsx +++ b/src/components/views/messages/MessageActionBar.tsx @@ -27,17 +27,17 @@ import { OverflowHorizontalIcon, ReplyIcon, DeleteIcon, + RestartIcon, } from "@vector-im/compound-design-tokens/assets/web/icons"; import { Icon as EditIcon } from "../../../../res/img/element-icons/room/message-bar/edit.svg"; import { Icon as EmojiIcon } from "../../../../res/img/element-icons/room/message-bar/emoji.svg"; -import { Icon as ResendIcon } from "../../../../res/img/element-icons/retry.svg"; import { Icon as ThreadIcon } from "../../../../res/img/element-icons/message/thread.svg"; import { Icon as ExpandMessageIcon } from "../../../../res/img/element-icons/expand-message.svg"; import { Icon as CollapseMessageIcon } from "../../../../res/img/element-icons/collapse-message.svg"; import type { Relations } from "matrix-js-sdk/src/matrix"; import { _t } from "../../../languageHandler"; -import dis, { defaultDispatcher } from "../../../dispatcher/dispatcher"; +import defaultDispatcher from "../../../dispatcher/dispatcher"; import ContextMenu, { aboveLeftOf, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu"; import { isContentActionable, canEditContent, editEvent, canCancel } from "../../../utils/EventUtils"; import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext"; @@ -323,7 +323,7 @@ export default class MessageActionBar extends React.PureComponent - + , ); diff --git a/src/components/views/messages/MessageEvent.tsx b/src/components/views/messages/MessageEvent.tsx index e4f3cb83ec2..60fcce6493e 100644 --- a/src/components/views/messages/MessageEvent.tsx +++ b/src/components/views/messages/MessageEvent.tsx @@ -6,7 +6,9 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ +import mime from "mime"; import React, { createRef } from "react"; +import { logger } from "matrix-js-sdk/src/logger"; import { EventType, MsgType, @@ -15,6 +17,7 @@ import { M_LOCATION, M_POLL_END, M_POLL_START, + IContent, } from "matrix-js-sdk/src/matrix"; import SettingsStore from "../../../settings/SettingsStore"; @@ -144,6 +147,103 @@ export default class MessageEvent extends React.Component implements IMe this.forceUpdate(); }; + /** + * Validates that the filename extension and advertised mimetype + * of the supplied image/file message content match and are actuallly video/image content. + * For image/video messages with a thumbnail it also validates the mimetype is an image. + * @param content The mxEvent content of the message + * @returns A boolean indicating whether the validation passed + */ + private validateImageOrVideoMimetype = (content: IContent): boolean => { + // As per the spec if filename is not present the body represents the filename + const filename = content.filename ?? content.body; + if (!filename) { + logger.log("Failed to validate image/video content, filename null"); + return false; + } + // Check mimetype of the thumbnail + if (!this.validateThumbnailMimetype(content)) { + logger.log("Failed to validate file/image thumbnail"); + return false; + } + + // if there is no mimetype from the extesion or the mimetype is not image/video validation fails + const typeFromExtension = mime.getType(filename) ?? undefined; + const extensionMajorMimetype = this.parseMajorMimetype(typeFromExtension); + if (!typeFromExtension || !this.validateAllowedMimetype(typeFromExtension, ["image", "video"])) { + logger.log("Failed to validate image/video content, invalid or missing extension"); + return false; + } + + // if the content mimetype is set check it is an image/video and that it matches the extesion mimetype otherwise validation fails + const contentMimetype = content.info?.mimetype; + if (contentMimetype) { + const contentMajorMimetype = this.parseMajorMimetype(contentMimetype); + if ( + !this.validateAllowedMimetype(contentMimetype, ["image", "video"]) || + extensionMajorMimetype !== contentMajorMimetype + ) { + logger.log("Failed to validate image/video content, invalid or missing mimetype"); + return false; + } + } + return true; + }; + + /** + * Validates that the advertised mimetype of the sticker content + * is an image. + * For stickers with a thumbnail it also validates the mimetype is an image. + * @param content The mxEvent content of the message + * @returns A boolean indicating whether the validation passed + */ + private validateStickerMimetype = (content: IContent): boolean => { + // Validate mimetype of the thumbnail + const thumbnailResult = this.validateThumbnailMimetype(content); + if (!thumbnailResult) { + logger.log("Failed to validate sticker thumbnail"); + return false; + } + // Validate mimetype of the content info is valid if it is set + const contentMimetype = content.info?.mimetype; + if (contentMimetype && !this.validateAllowedMimetype(contentMimetype, ["image"])) { + logger.log("Failed to validate image/video content, invalid or missing mimetype/extensions"); + return false; + } + return true; + }; + + /** + * For image/video messages or stickers that have a thumnail mimetype specified, + * validates that the major mimetime is image. + * @param content The mxEvent content of the message + * @returns A boolean indicating whether the validation passed + */ + private validateThumbnailMimetype = (content: IContent): boolean => { + const thumbnailMimetype = content.info?.thumbnail_info?.mimetype; + return !thumbnailMimetype || this.validateAllowedMimetype(thumbnailMimetype, ["image"]); + }; + + /** + * Validates that the major part of a mimetime from an allowed list. + * @param mimetype The mimetype to validate + * @param allowedMajorMimeTypes The list of allowed major mimetimes + * @returns A boolean indicating whether the validation passed + */ + private validateAllowedMimetype = (mimetype: string, allowedMajorMimeTypes: string[]): boolean => { + const majorMimetype = this.parseMajorMimetype(mimetype); + return !!majorMimetype && allowedMajorMimeTypes.includes(majorMimetype); + }; + + /** + * Parses and returns the the major part of a mimetype(before the "/"). + * @param mimetype As optional mimetype string to parse + * @returns The major part of the mimetype string or undefined + */ + private parseMajorMimetype(mimetype?: string): string | undefined { + return mimetype?.split("/")[0]; + } + public render(): React.ReactNode { const content = this.props.mxEvent.getContent(); const type = this.props.mxEvent.getType(); @@ -165,6 +265,13 @@ export default class MessageEvent extends React.Component implements IMe BodyType = UnknownBody; } + if ( + ((BodyType === MImageBody || BodyType == MVideoBody) && !this.validateImageOrVideoMimetype(content)) || + (BodyType === MStickerBody && !this.validateStickerMimetype(content)) + ) { + BodyType = this.bodyTypes.get(MsgType.File)!; + } + // TODO: move to eventTypes when location sharing spec stabilises if (M_LOCATION.matches(type) || (type === EventType.RoomMessage && msgtype === MsgType.Location)) { BodyType = MLocationBody; diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 08b79918075..0c05236176f 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -6,8 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { createRef, SyntheticEvent, MouseEvent } from "react"; -import ReactDOM from "react-dom"; +import React, { createRef, SyntheticEvent, MouseEvent, StrictMode } from "react"; import { MsgType } from "matrix-js-sdk/src/matrix"; import { TooltipProvider } from "@vector-im/compound-web"; @@ -17,8 +16,8 @@ import Modal from "../../../Modal"; import dis from "../../../dispatcher/dispatcher"; import { _t } from "../../../languageHandler"; import SettingsStore from "../../../settings/SettingsStore"; -import { pillifyLinks, unmountPills } from "../../../utils/pillify"; -import { tooltipifyLinks, unmountTooltips } from "../../../utils/tooltipify"; +import { pillifyLinks } from "../../../utils/pillify"; +import { tooltipifyLinks } from "../../../utils/tooltipify"; import { IntegrationManagers } from "../../../integrations/IntegrationManagers"; import { isPermalinkHost, tryTransformPermalinkToLocalHref } from "../../../utils/permalinks/Permalinks"; import { Action } from "../../../dispatcher/actions"; @@ -36,6 +35,7 @@ import { EditWysiwygComposer } from "../rooms/wysiwyg_composer"; import { IEventTileOps } from "../rooms/EventTile"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import CodeBlock from "./CodeBlock"; +import { ReactRootManager } from "../../../utils/react"; interface IState { // the URLs (if any) to be previewed with a LinkPreviewWidget inside this TextualBody. @@ -48,9 +48,11 @@ interface IState { export default class TextualBody extends React.Component { private readonly contentRef = createRef(); - private pills: Element[] = []; - private tooltips: Element[] = []; - private reactRoots: Element[] = []; + private pills = new ReactRootManager(); + private tooltips = new ReactRootManager(); + private reactRoots = new ReactRootManager(); + + private ref = createRef(); public static contextType = RoomContext; public declare context: React.ContextType; @@ -80,12 +82,12 @@ export default class TextualBody extends React.Component { // tooltipifyLinks AFTER calculateUrlPreview because the DOM inside the tooltip // container is empty before the internal component has mounted so calculateUrlPreview // won't find any anchors - tooltipifyLinks([content], this.pills, this.tooltips); + tooltipifyLinks([content], [...this.pills.elements, ...this.reactRoots.elements], this.tooltips); if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") { // Handle expansion and add buttons - const pres = (ReactDOM.findDOMNode(this) as Element).getElementsByTagName("pre"); - if (pres.length > 0) { + const pres = this.ref.current?.getElementsByTagName("pre"); + if (pres && pres.length > 0) { for (let i = 0; i < pres.length; i++) { // If there already is a div wrapping the codeblock we want to skip this. // This happens after the codeblock was edited. @@ -111,12 +113,16 @@ export default class TextualBody extends React.Component { private wrapPreInReact(pre: HTMLPreElement): void { const root = document.createElement("div"); root.className = "mx_EventTile_pre_container"; - this.reactRoots.push(root); // Insert containing div in place of
 block
         pre.parentNode?.replaceChild(root, pre);
 
-        ReactDOM.render({pre}, root);
+        this.reactRoots.render(
+            
+                {pre}
+            ,
+            root,
+        );
     }
 
     public componentDidUpdate(prevProps: Readonly): void {
@@ -130,16 +136,9 @@ export default class TextualBody extends React.Component {
     }
 
     public componentWillUnmount(): void {
-        unmountPills(this.pills);
-        unmountTooltips(this.tooltips);
-
-        for (const root of this.reactRoots) {
-            ReactDOM.unmountComponentAtNode(root);
-        }
-
-        this.pills = [];
-        this.tooltips = [];
-        this.reactRoots = [];
+        this.pills.unmount();
+        this.tooltips.unmount();
+        this.reactRoots.unmount();
     }
 
     public shouldComponentUpdate(nextProps: Readonly, nextState: Readonly): boolean {
@@ -190,12 +189,15 @@ export default class TextualBody extends React.Component {
                 const reason = node.getAttribute("data-mx-spoiler") ?? undefined;
                 node.removeAttribute("data-mx-spoiler"); // we don't want to recurse
                 const spoiler = (
-                    
-                        
-                    
+                    
+                        
+                            
+                        
+                    
                 );
 
-                ReactDOM.render(spoiler, spoilerContainer);
+                this.reactRoots.render(spoiler, spoilerContainer);
+
                 node.parentNode?.replaceChild(spoilerContainer, node);
 
                 node = spoilerContainer;
@@ -477,7 +479,12 @@ export default class TextualBody extends React.Component {
 
         if (isEmote) {
             return (
-                
+
{mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()} @@ -490,7 +497,7 @@ export default class TextualBody extends React.Component { } if (isNotice) { return ( -
+
{body} {widgets}
@@ -498,14 +505,14 @@ export default class TextualBody extends React.Component { } if (isCaption) { return ( -
+
{body} {widgets}
); } return ( -
+
{body} {widgets}
diff --git a/src/components/views/right_panel/TimelineCard.tsx b/src/components/views/right_panel/TimelineCard.tsx index 4f9d1dd9179..e0988eeaa5d 100644 --- a/src/components/views/right_panel/TimelineCard.tsx +++ b/src/components/views/right_panel/TimelineCard.tsx @@ -100,14 +100,10 @@ export default class TimelineCard extends React.Component { public componentWillUnmount(): void { SdkContextClass.instance.roomViewStore.removeListener(UPDATE_EVENT, this.onRoomViewStoreUpdate); - if (this.readReceiptsSettingWatcher) { - SettingsStore.unwatchSetting(this.readReceiptsSettingWatcher); - } - if (this.layoutWatcherRef) { - SettingsStore.unwatchSetting(this.layoutWatcherRef); - } + SettingsStore.unwatchSetting(this.readReceiptsSettingWatcher); + SettingsStore.unwatchSetting(this.layoutWatcherRef); - if (this.dispatcherRef) dis.unregister(this.dispatcherRef); + dis.unregister(this.dispatcherRef); } private onRoomViewStoreUpdate = async (_initial?: boolean): Promise => { diff --git a/src/components/views/rooms/AppsDrawer.tsx b/src/components/views/rooms/AppsDrawer.tsx index cba6e6c691b..c02bfe8cf28 100644 --- a/src/components/views/rooms/AppsDrawer.tsx +++ b/src/components/views/rooms/AppsDrawer.tsx @@ -68,11 +68,13 @@ export default class AppsDrawer extends React.Component { }; this.resizer = this.createResizer(); - - this.props.resizeNotifier.on("isResizing", this.onIsResizing); } public componentDidMount(): void { + this.unmounted = false; + + this.props.resizeNotifier.on("isResizing", this.onIsResizing); + ScalarMessaging.startListening(); WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(this.props.room), this.updateApps); this.dispatcherRef = dis.register(this.onAction); @@ -82,7 +84,7 @@ export default class AppsDrawer extends React.Component { this.unmounted = true; ScalarMessaging.stopListening(); WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(this.props.room), this.updateApps); - if (this.dispatcherRef) dis.unregister(this.dispatcherRef); + dis.unregister(this.dispatcherRef); if (this.resizeContainer) { this.resizer.detach(); } diff --git a/src/components/views/rooms/Autocomplete.tsx b/src/components/views/rooms/Autocomplete.tsx index db4ac374152..af33fb2d9e0 100644 --- a/src/components/views/rooms/Autocomplete.tsx +++ b/src/components/views/rooms/Autocomplete.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { createRef, KeyboardEvent } from "react"; +import React, { createRef, KeyboardEvent, RefObject } from "react"; import classNames from "classnames"; import { flatMap } from "lodash"; import { Room } from "matrix-js-sdk/src/matrix"; @@ -45,6 +45,7 @@ export default class Autocomplete extends React.PureComponent { public queryRequested?: string; public debounceCompletionsRequest?: number; private containerRef = createRef(); + private completionRefs: Record> = {}; public static contextType = RoomContext; public declare context: React.ContextType; @@ -260,7 +261,7 @@ export default class Autocomplete extends React.PureComponent { public componentDidUpdate(prevProps: IProps): void { this.applyNewProps(prevProps.query, prevProps.room); // this is the selected completion, so scroll it into view if needed - const selectedCompletion = this.refs[`completion${this.state.selectionOffset}`] as HTMLElement; + const selectedCompletion = this.completionRefs[`completion${this.state.selectionOffset}`]?.current; if (selectedCompletion) { selectedCompletion.scrollIntoView({ @@ -286,9 +287,13 @@ export default class Autocomplete extends React.PureComponent { this.onCompletionClicked(componentPosition); }; + const refId = `completion${componentPosition}`; + if (!this.completionRefs[refId]) { + this.completionRefs[refId] = createRef(); + } return React.cloneElement(completion.component, { "key": j, - "ref": `completion${componentPosition}`, + "ref": this.completionRefs[refId], "id": generateCompletionDomId(componentPosition - 1), // 0 index the completion IDs className, onClick, diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index 0add0c10271..5f033de2380 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -128,10 +128,10 @@ export default class BasicMessageEditor extends React.Component private lastCaret!: DocumentOffset; private lastSelection: ReturnType | null = null; - private readonly useMarkdownHandle: string; - private readonly emoticonSettingHandle: string; - private readonly shouldShowPillAvatarSettingHandle: string; - private readonly surroundWithHandle: string; + private useMarkdownHandle?: string; + private emoticonSettingHandle?: string; + private shouldShowPillAvatarSettingHandle?: string; + private surroundWithHandle?: string; private readonly historyManager = new HistoryManager(); public constructor(props: IProps) { @@ -145,28 +145,7 @@ export default class BasicMessageEditor extends React.Component const ua = navigator.userAgent.toLowerCase(); this.isSafari = ua.includes("safari/") && !ua.includes("chrome/"); - - this.useMarkdownHandle = SettingsStore.watchSetting( - "MessageComposerInput.useMarkdown", - null, - this.configureUseMarkdown, - ); - this.emoticonSettingHandle = SettingsStore.watchSetting( - "MessageComposerInput.autoReplaceEmoji", - null, - this.configureEmoticonAutoReplace, - ); this.configureEmoticonAutoReplace(); - this.shouldShowPillAvatarSettingHandle = SettingsStore.watchSetting( - "Pill.shouldShowPillAvatar", - null, - this.configureShouldShowPillAvatar, - ); - this.surroundWithHandle = SettingsStore.watchSetting( - "MessageComposerInput.surroundWith", - null, - this.surroundWithSettingChanged, - ); } public componentDidUpdate(prevProps: IProps): void { @@ -737,6 +716,27 @@ export default class BasicMessageEditor extends React.Component } public componentDidMount(): void { + this.useMarkdownHandle = SettingsStore.watchSetting( + "MessageComposerInput.useMarkdown", + null, + this.configureUseMarkdown, + ); + this.emoticonSettingHandle = SettingsStore.watchSetting( + "MessageComposerInput.autoReplaceEmoji", + null, + this.configureEmoticonAutoReplace, + ); + this.shouldShowPillAvatarSettingHandle = SettingsStore.watchSetting( + "Pill.shouldShowPillAvatar", + null, + this.configureShouldShowPillAvatar, + ); + this.surroundWithHandle = SettingsStore.watchSetting( + "MessageComposerInput.surroundWith", + null, + this.surroundWithSettingChanged, + ); + const model = this.props.model; model.setUpdateCallback(this.updateEditorState); const partCreator = model.partCreator; diff --git a/src/components/views/rooms/E2EIcon.tsx b/src/components/views/rooms/E2EIcon.tsx index 1e403613468..29899e85ba9 100644 --- a/src/components/views/rooms/E2EIcon.tsx +++ b/src/components/views/rooms/E2EIcon.tsx @@ -19,9 +19,7 @@ import { XOR } from "../../../@types/common"; export enum E2EState { Verified = "verified", Warning = "warning", - Unknown = "unknown", Normal = "normal", - Unauthenticated = "unauthenticated", } const crossSigningUserTitles: { [key in E2EState]?: TranslationKey } = { diff --git a/src/components/views/rooms/EditMessageComposer.tsx b/src/components/views/rooms/EditMessageComposer.tsx index 06f189df590..d62a451b8b0 100644 --- a/src/components/views/rooms/EditMessageComposer.tsx +++ b/src/components/views/rooms/EditMessageComposer.tsx @@ -124,7 +124,7 @@ class EditMessageComposer extends React.Component; private readonly editorRef = createRef(); - private readonly dispatcherRef: string; + private dispatcherRef?: string; private readonly replyToEvent?: MatrixEvent; private model!: EditorModel; @@ -140,7 +140,9 @@ class EditMessageComposer extends React.Component { + /** + * The event to display the preview for + */ + mxEvent: MatrixEvent; +} + +/** + * A component that displays a preview for the given event. + * Wraps both `useEventPreview` & `EventPreviewTile`. + */ +export function EventPreview({ mxEvent, className, ...props }: Props): JSX.Element | null { + const preview = useEventPreview(mxEvent); + if (!preview) return null; + + return ; +} + +/** + * The props for the {@link EventPreviewTile} component. + */ +interface EventPreviewTileProps extends HTMLProps { + /** + * The preview to display + */ + preview: Preview; +} + +/** + * A component that displays a preview given the output from `useEventPreview`. + */ +export function EventPreviewTile({ + preview: [preview, prefix], + className, + ...props +}: EventPreviewTileProps): JSX.Element | null { + const classes = classNames("mx_EventPreview", className); + if (!prefix) + return ( + + {preview} + + ); + + return ( + + {_t( + "event_preview|preview", + { + prefix, + preview, + }, + { + bold: (sub) => {sub}, + }, + )} + + ); +} + +type Preview = [preview: string, prefix: string | null]; + +/** + * Hooks to generate a preview for the event. + * @param mxEvent + */ +export function useEventPreview(mxEvent: MatrixEvent | undefined): Preview | null { + const cli = useContext(MatrixClientContext); + // track the content as a means to regenerate the preview upon edits & decryption + const [content, setContent] = useState(mxEvent?.getContent()); + useTypedEventEmitter(mxEvent ?? undefined, MatrixEventEvent.Replaced, () => { + setContent(mxEvent!.getContent()); + }); + const awaitDecryption = mxEvent?.shouldAttemptDecryption() || mxEvent?.isBeingDecrypted(); + useTypedEventEmitter(awaitDecryption ? (mxEvent ?? undefined) : undefined, MatrixEventEvent.Decrypted, () => { + setContent(mxEvent!.getContent()); + }); + + return useAsyncMemo( + async () => { + if (!mxEvent || mxEvent.isRedacted() || mxEvent.isDecryptionFailure()) return null; + await cli.decryptEventIfNeeded(mxEvent); + return [ + MessagePreviewStore.instance.generatePreviewForEvent(mxEvent), + getPreviewPrefix(mxEvent.getType(), content?.msgtype as MsgType), + ]; + }, + [mxEvent, content], + null, + ); +} + +/** + * Get the prefix for the preview based on the type and the message type. + * @param type + * @param msgType + */ +function getPreviewPrefix(type: string, msgType: MsgType): string | null { + switch (type) { + case M_POLL_START.name: + return _t("event_preview|prefix|poll"); + default: + } + + switch (msgType) { + case MsgType.Audio: + return _t("event_preview|prefix|audio"); + case MsgType.Image: + return _t("event_preview|prefix|image"); + case MsgType.Video: + return _t("event_preview|prefix|video"); + case MsgType.File: + return _t("event_preview|prefix|file"); + default: + return null; + } +} diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index eca61f7d227..22da73bef7f 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -28,6 +28,7 @@ import { logger } from "matrix-js-sdk/src/logger"; import { CallErrorCode } from "matrix-js-sdk/src/webrtc/call"; import { CryptoEvent, + DecryptionFailureCode, EventShieldColour, EventShieldReason, UserVerificationStatus, @@ -60,7 +61,6 @@ import { IReadReceiptPosition } from "./ReadReceiptMarker"; import MessageActionBar from "../messages/MessageActionBar"; import ReactionsRow from "../messages/ReactionsRow"; import { getEventDisplayInfo } from "../../../utils/EventRenderingUtils"; -import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore"; import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext"; import { MediaEventHelper } from "../../../utils/MediaEventHelper"; import { ButtonEvent } from "../elements/AccessibleButton"; @@ -82,6 +82,7 @@ import { EventTileThreadToolbar } from "./EventTile/EventTileThreadToolbar"; import { getLateEventInfo } from "../../structures/grouper/LateEventGrouper"; import PinningUtils from "../../../utils/PinningUtils"; import { PinnedMessageBadge } from "../messages/PinnedMessageBadge"; +import { EventPreview } from "./EventPreview"; export type GetRelationsForEvent = ( eventId: string, @@ -386,6 +387,7 @@ export class UnwrappedEventTile extends React.Component } public componentDidMount(): void { + this.unmounted = false; this.suppressReadReceiptAnimation = false; const client = MatrixClientPeg.safeGet(); if (!this.props.forExport) { @@ -718,7 +720,14 @@ export class UnwrappedEventTile extends React.Component // event could not be decrypted if (ev.isDecryptionFailure()) { - return ; + switch (ev.decryptionFailureReason) { + // These two errors get icons from DecryptionFailureBody, so we hide the padlock icon + case DecryptionFailureCode.SENDER_IDENTITY_PREVIOUSLY_VERIFIED: + case DecryptionFailureCode.UNSIGNED_SENDER_DEVICE: + return null; + default: + return ; + } } if (this.state.shieldColour !== EventShieldColour.NONE) { @@ -1332,7 +1341,7 @@ export class UnwrappedEventTile extends React.Component ) : this.props.mxEvent.isDecryptionFailure() ? ( ) : ( - MessagePreviewStore.instance.generatePreviewForEvent(this.props.mxEvent) + )}
{this.renderThreadPanelSummary()} diff --git a/src/components/views/rooms/MemberList.tsx b/src/components/views/rooms/MemberList.tsx index 993a2ba1f13..e503ce23633 100644 --- a/src/components/views/rooms/MemberList.tsx +++ b/src/components/views/rooms/MemberList.tsx @@ -72,7 +72,7 @@ interface IState { export default class MemberList extends React.Component { private readonly showPresence: boolean; - private mounted = false; + private unmounted = false; public static contextType = SDKContext; public declare context: React.ContextType; @@ -82,8 +82,6 @@ export default class MemberList extends React.Component { super(props, context); this.state = this.getMembersState([], []); this.showPresence = context?.memberListStore.isPresenceEnabled() ?? true; - this.mounted = true; - this.listenForMembersChanges(); } private listenForMembersChanges(): void { @@ -102,11 +100,13 @@ export default class MemberList extends React.Component { } public componentDidMount(): void { + this.unmounted = false; + this.listenForMembersChanges(); this.updateListNow(true); } public componentWillUnmount(): void { - this.mounted = false; + this.unmounted = true; const cli = MatrixClientPeg.get(); if (cli) { cli.removeListener(RoomStateEvent.Update, this.onRoomStateUpdate); @@ -205,7 +205,7 @@ export default class MemberList extends React.Component { // XXX: exported for tests public async updateListNow(showLoadingSpinner?: boolean): Promise { - if (!this.mounted) { + if (this.unmounted) { return; } if (showLoadingSpinner) { @@ -215,7 +215,7 @@ export default class MemberList extends React.Component { this.props.roomId, this.props.searchQuery, ); - if (!this.mounted) { + if (this.unmounted) { return; } this.setState({ diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index e44265b9479..69139fae5bc 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -134,9 +134,6 @@ export class MessageComposer extends React.Component { super(props, context); this.context = context; // otherwise React will only set it prior to render due to type def above - VoiceRecordingStore.instance.on(UPDATE_EVENT, this.onVoiceStoreUpdate); - - window.addEventListener("beforeunload", this.saveWysiwygEditorState); const isWysiwygLabEnabled = SettingsStore.getValue("feature_wysiwyg_composer"); let isRichTextEnabled = true; let initialComposerContent = ""; @@ -145,13 +142,6 @@ export class MessageComposer extends React.Component { if (wysiwygState) { isRichTextEnabled = wysiwygState.isRichText; initialComposerContent = wysiwygState.content; - if (wysiwygState.replyEventId) { - dis.dispatch({ - action: "reply_to_event", - event: this.props.room.findEventById(wysiwygState.replyEventId), - context: this.context.timelineRenderingType, - }); - } } } @@ -171,11 +161,6 @@ export class MessageComposer extends React.Component { }; this.instanceId = instanceCount++; - - SettingsStore.monitorSetting("MessageComposerInput.showStickersButton", null); - SettingsStore.monitorSetting("MessageComposerInput.showPollsButton", null); - SettingsStore.monitorSetting(Features.VoiceBroadcast, null); - SettingsStore.monitorSetting("feature_wysiwyg_composer", null); } private get editorStateKey(): string { @@ -248,6 +233,25 @@ export class MessageComposer extends React.Component { } public componentDidMount(): void { + VoiceRecordingStore.instance.on(UPDATE_EVENT, this.onVoiceStoreUpdate); + + window.addEventListener("beforeunload", this.saveWysiwygEditorState); + if (this.state.isWysiwygLabEnabled) { + const wysiwygState = this.restoreWysiwygEditorState(); + if (wysiwygState?.replyEventId) { + dis.dispatch({ + action: "reply_to_event", + event: this.props.room.findEventById(wysiwygState.replyEventId), + context: this.context.timelineRenderingType, + }); + } + } + + SettingsStore.monitorSetting("MessageComposerInput.showStickersButton", null); + SettingsStore.monitorSetting("MessageComposerInput.showPollsButton", null); + SettingsStore.monitorSetting(Features.VoiceBroadcast, null); + SettingsStore.monitorSetting("feature_wysiwyg_composer", null); + this.dispatcherRef = dis.register(this.onAction); this.waitForOwnMember(); UIStore.instance.trackElementDimensions(`MessageComposer${this.instanceId}`, this.ref.current!); @@ -331,7 +335,7 @@ export class MessageComposer extends React.Component { public componentWillUnmount(): void { VoiceRecordingStore.instance.off(UPDATE_EVENT, this.onVoiceStoreUpdate); - if (this.dispatcherRef) dis.unregister(this.dispatcherRef); + dis.unregister(this.dispatcherRef); UIStore.instance.stopTrackingElementDimensions(`MessageComposer${this.instanceId}`); UIStore.instance.removeListener(`MessageComposer${this.instanceId}`, this.onResize); diff --git a/src/components/views/rooms/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge.tsx index c4cc418db4a..6825ea8e43f 100644 --- a/src/components/views/rooms/NotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge.tsx @@ -44,15 +44,23 @@ interface IState { } export default class NotificationBadge extends React.PureComponent, IState> { - private countWatcherRef: string; + private countWatcherRef?: string; public constructor(props: IProps) { super(props); - this.props.notification.on(NotificationStateEvents.Update, this.onNotificationUpdate); this.state = { showCounts: SettingsStore.getValue("Notifications.alwaysShowBadgeCounts", this.roomId), }; + } + + private get roomId(): string | null { + // We should convert this to null for safety with the SettingsStore + return this.props.roomId || null; + } + + public componentDidMount(): void { + this.props.notification.on(NotificationStateEvents.Update, this.onNotificationUpdate); this.countWatcherRef = SettingsStore.watchSetting( "Notifications.alwaysShowBadgeCounts", @@ -61,11 +69,6 @@ export default class NotificationBadge extends React.PureComponent )} - + {/* In case of redacted event, we want to display the nice sentence of the message event like in the timeline or in the pinned message list */} {shouldUseMessageEvent && (
@@ -124,84 +128,6 @@ export function PinnedMessageBanner({ room, permalinkCreator }: PinnedMessageBan ); } -/** - * The props for the {@link EventPreview} component. - */ -interface EventPreviewProps { - /** - * The pinned event to display the preview for - */ - pinnedEvent: MatrixEvent; -} - -/** - * A component that displays a preview for the pinned event. - */ -function EventPreview({ pinnedEvent }: EventPreviewProps): JSX.Element | null { - const preview = useEventPreview(pinnedEvent); - if (!preview) return null; - - const prefix = getPreviewPrefix(pinnedEvent.getType(), pinnedEvent.getContent().msgtype as MsgType); - if (!prefix) - return ( - - {preview} - - ); - - return ( - - {_t( - "room|pinned_message_banner|preview", - { - prefix, - preview, - }, - { - bold: (sub) => {sub}, - }, - )} - - ); -} - -/** - * Hooks to generate a preview for the pinned event. - * @param pinnedEvent - */ -function useEventPreview(pinnedEvent: MatrixEvent | null): string | null { - return useMemo(() => { - if (!pinnedEvent || pinnedEvent.isRedacted() || pinnedEvent.isDecryptionFailure()) return null; - return MessagePreviewStore.instance.generatePreviewForEvent(pinnedEvent); - }, [pinnedEvent]); -} - -/** - * Get the prefix for the preview based on the type and the message type. - * @param type - * @param msgType - */ -function getPreviewPrefix(type: string, msgType: MsgType): string | null { - switch (type) { - case M_POLL_START.name: - return _t("room|pinned_message_banner|prefix|poll"); - default: - } - - switch (msgType) { - case MsgType.Audio: - return _t("room|pinned_message_banner|prefix|audio"); - case MsgType.Image: - return _t("room|pinned_message_banner|prefix|image"); - case MsgType.Video: - return _t("room|pinned_message_banner|prefix|video"); - case MsgType.File: - return _t("room|pinned_message_banner|prefix|file"); - default: - return null; - } -} - const MAX_INDICATORS = 3; /** diff --git a/src/components/views/rooms/RoomBreadcrumbs.tsx b/src/components/views/rooms/RoomBreadcrumbs.tsx index 19405e96b0b..bb787d509a9 100644 --- a/src/components/views/rooms/RoomBreadcrumbs.tsx +++ b/src/components/views/rooms/RoomBreadcrumbs.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React from "react"; +import React, { createRef } from "react"; import { Room } from "matrix-js-sdk/src/matrix"; import { CSSTransition } from "react-transition-group"; @@ -60,7 +60,8 @@ const RoomBreadcrumbTile: React.FC<{ room: Room; onClick: (ev: ButtonEvent) => v }; export default class RoomBreadcrumbs extends React.PureComponent { - private isMounted = true; + private unmounted = false; + private toolbar = createRef(); public constructor(props: IProps) { super(props); @@ -69,17 +70,20 @@ export default class RoomBreadcrumbs extends React.PureComponent doAnimation: true, // technically we want animation on mount, but it won't be perfect skipFirst: false, // render the thing, as boring as it is }; + } + public componentDidMount(): void { + this.unmounted = false; BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate); } public componentWillUnmount(): void { - this.isMounted = false; + this.unmounted = true; BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate); } private onBreadcrumbsUpdate = (): void => { - if (!this.isMounted) return; + if (this.unmounted) return; // We need to trick the CSSTransition component into updating, which means we need to // tell it to not animate, then to animate a moment later. This causes two updates @@ -113,8 +117,18 @@ export default class RoomBreadcrumbs extends React.PureComponent if (tiles.length > 0) { // NOTE: The CSSTransition timeout MUST match the timeout in our CSS! return ( - - + + {tiles.slice(this.state.skipFirst ? 1 : 0)} diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index e47a785779d..c2642ea733a 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -27,7 +27,7 @@ import { useRoomMemberCount, useRoomMembers } from "../../../hooks/useRoomMember import { _t } from "../../../languageHandler"; import { Flex } from "../../utils/Flex"; import { Box } from "../../utils/Box"; -import { getPlatformCallTypeLabel, useRoomCall } from "../../../hooks/room/useRoomCall"; +import { getPlatformCallTypeProps, useRoomCall } from "../../../hooks/room/useRoomCall"; import { useRoomThreadNotifications } from "../../../hooks/room/useRoomThreadNotifications"; import { useGlobalNotificationState } from "../../../hooks/useGlobalNotificationState"; import SdkConfig from "../../../SdkConfig"; @@ -167,16 +167,21 @@ export default function RoomHeader({ side="left" align="start" > - {callOptions.map((option) => ( - videoCallClick(ev, option)} - Icon={VideoCallIcon} - onSelect={() => {} /* Dummy handler since we want the click event.*/} - /> - ))} + {callOptions.map((option) => { + const { label, children } = getPlatformCallTypeProps(option); + return ( + videoCallClick(ev, option)} + Icon={VideoCallIcon} + onSelect={() => {} /* Dummy handler since we want the click event.*/} + /> + ); + })} ) : ( { public componentWillUnmount(): void { SpaceStore.instance.off(UPDATE_SUGGESTED_ROOMS, this.updateSuggestedRooms); RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists); - if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef); + defaultDispatcher.unregister(this.dispatcherRef); SdkContextClass.instance.roomViewStore.off(UPDATE_EVENT, this.onRoomViewStoreUpdate); } diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx index 12f6e70d31c..34961c08530 100644 --- a/src/components/views/rooms/RoomSublist.tsx +++ b/src/components/views/rooms/RoomSublist.tsx @@ -248,7 +248,7 @@ export default class RoomSublist extends React.Component { } public componentWillUnmount(): void { - if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef); + defaultDispatcher.unregister(this.dispatcherRef); RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onListsUpdated); RoomListStore.instance.off(LISTS_LOADING_EVENT, this.onListsLoading); this.tilesRef.current?.removeEventListener("scroll", this.onScrollPrevent); diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index 93fb42f447a..8351c176ff0 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -94,7 +94,6 @@ export class RoomTile extends React.PureComponent { // generatePreview() will return nothing if the user has previews disabled messagePreview: null, }; - this.generatePreview(); this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room); this.roomProps = EchoChamber.forRoom(this.props.room); @@ -147,6 +146,8 @@ export class RoomTile extends React.PureComponent { } public componentDidMount(): void { + this.generatePreview(); + // when we're first rendered (or our sublist is expanded) make sure we are visible if we're active if (this.state.selected) { this.scrollIntoView(); @@ -175,7 +176,7 @@ export class RoomTile extends React.PureComponent { this.onRoomPreviewChanged, ); this.props.room.off(RoomEvent.Name, this.onRoomNameUpdate); - if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef); + defaultDispatcher.unregister(this.dispatcherRef); this.notificationState.off(NotificationStateEvents.Update, this.onNotificationUpdate); this.roomProps.off(PROPERTY_UPDATED, this.onRoomPropertyUpdate); CallStore.instance.off(CallStoreEvent.Call, this.onCallChanged); diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 776963eb338..a12a09dcb7f 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -255,7 +255,7 @@ export class SendMessageComposer extends React.Component(); private model: EditorModel; private currentlyComposedEditorState: SerializedPart[] | null = null; - private dispatcherRef: string; + private dispatcherRef?: string; private sendHistoryManager: SendHistoryManager; public static defaultProps = { @@ -275,15 +275,17 @@ export class SendMessageComposer extends React.Component { if (client) client.removeListener(ClientEvent.AccountData, this.updateWidget); RightPanelStore.instance.off(UPDATE_EVENT, this.onRightPanelStoreUpdate); window.removeEventListener("resize", this.onResize); - if (this.dispatcherRef) { - dis.unregister(this.dispatcherRef); - } + dis.unregister(this.dispatcherRef); } public componentDidUpdate(): void { diff --git a/src/components/views/rooms/ThreadSummary.tsx b/src/components/views/rooms/ThreadSummary.tsx index ea76dd0d369..4a3032d6411 100644 --- a/src/components/views/rooms/ThreadSummary.tsx +++ b/src/components/views/rooms/ThreadSummary.tsx @@ -6,8 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { useContext, useState } from "react"; -import { Thread, ThreadEvent, IContent, MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/matrix"; +import React, { useContext } from "react"; +import { Thread, ThreadEvent, MatrixEvent } from "matrix-js-sdk/src/matrix"; import { IndicatorIcon } from "@vector-im/compound-web"; import ThreadIconSolid from "@vector-im/compound-design-tokens/assets/web/icons/threads-solid"; @@ -15,17 +15,15 @@ import { _t } from "../../../languageHandler"; import { CardContext } from "../right_panel/context"; import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; import PosthogTrackers from "../../../PosthogTrackers"; -import { useTypedEventEmitter, useTypedEventEmitterState } from "../../../hooks/useEventEmitter"; +import { useTypedEventEmitterState } from "../../../hooks/useEventEmitter"; import RoomContext from "../../../contexts/RoomContext"; -import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore"; import MemberAvatar from "../avatars/MemberAvatar"; -import { useAsyncMemo } from "../../../hooks/useAsyncMemo"; -import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { Action } from "../../../dispatcher/actions"; import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import { useUnreadNotifications } from "../../../hooks/useUnreadNotifications"; import { notificationLevelToIndicator } from "../../../utils/notifications"; +import { EventPreviewTile, useEventPreview } from "./EventPreview.tsx"; interface IProps { mxEvent: MatrixEvent; @@ -75,24 +73,9 @@ interface IPreviewProps { } export const ThreadMessagePreview: React.FC = ({ thread, showDisplayname = false }) => { - const cli = useContext(MatrixClientContext); - const lastReply = useTypedEventEmitterState(thread, ThreadEvent.Update, () => thread.replyToEvent) ?? undefined; - // track the content as a means to regenerate the thread message preview upon edits & decryption - const [content, setContent] = useState(lastReply?.getContent()); - useTypedEventEmitter(lastReply, MatrixEventEvent.Replaced, () => { - setContent(lastReply!.getContent()); - }); - const awaitDecryption = lastReply?.shouldAttemptDecryption() || lastReply?.isBeingDecrypted(); - useTypedEventEmitter(awaitDecryption ? lastReply : undefined, MatrixEventEvent.Decrypted, () => { - setContent(lastReply!.getContent()); - }); + const preview = useEventPreview(lastReply); - const preview = useAsyncMemo(async (): Promise => { - if (!lastReply) return; - await cli.decryptEventIfNeeded(lastReply); - return MessagePreviewStore.instance.generatePreviewForEvent(lastReply); - }, [lastReply, content]); if (!preview || !lastReply) { return null; } @@ -114,14 +97,10 @@ export const ThreadMessagePreview: React.FC = ({ thread, showDisp className="mx_ThreadSummary_content mx_DecryptionFailureBody" title={_t("timeline|decryption_failure|unable_to_decrypt")} > - - {_t("timeline|decryption_failure|unable_to_decrypt")} - + {_t("timeline|decryption_failure|unable_to_decrypt")}
) : ( -
- {preview} -
+ )} ); diff --git a/src/components/views/settings/CrossSigningPanel.tsx b/src/components/views/settings/CrossSigningPanel.tsx index 6d00e182e80..a91238848e9 100644 --- a/src/components/views/settings/CrossSigningPanel.tsx +++ b/src/components/views/settings/CrossSigningPanel.tsx @@ -45,6 +45,7 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> { } public componentDidMount(): void { + this.unmounted = false; const cli = MatrixClientPeg.safeGet(); cli.on(ClientEvent.AccountData, this.onAccountData); cli.on(CryptoEvent.UserTrustStatusChanged, this.onStatusChanged); diff --git a/src/components/views/settings/CryptographyPanel.tsx b/src/components/views/settings/CryptographyPanel.tsx index 08917d215cd..b418c0b05df 100644 --- a/src/components/views/settings/CryptographyPanel.tsx +++ b/src/components/views/settings/CryptographyPanel.tsx @@ -6,12 +6,9 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React from "react"; +import React, { lazy } from "react"; import { logger } from "matrix-js-sdk/src/logger"; -import type ExportE2eKeysDialog from "../../../async-components/views/dialogs/security/ExportE2eKeysDialog"; -import type ImportE2eKeysDialog from "../../../async-components/views/dialogs/security/ImportE2eKeysDialog"; -import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { _t } from "../../../languageHandler"; import Modal from "../../../Modal"; import AccessibleButton from "../elements/AccessibleButton"; @@ -19,7 +16,8 @@ import * as FormattingUtils from "../../../utils/FormattingUtils"; import SettingsStore from "../../../settings/SettingsStore"; import SettingsFlag from "../elements/SettingsFlag"; import { SettingLevel } from "../../../settings/SettingLevel"; -import SettingsSubsection, { SettingsSubsectionText } from "./shared/SettingsSubsection"; +import { SettingsSubsection, SettingsSubsectionText } from "./shared/SettingsSubsection"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; interface IProps {} @@ -33,17 +31,24 @@ interface IState { } export default class CryptographyPanel extends React.Component { - public constructor(props: IProps) { + public static contextType = MatrixClientContext; + public declare context: React.ContextType; + + public constructor(props: IProps, context: React.ContextType) { super(props); - const client = MatrixClientPeg.safeGet(); - const crypto = client.getCrypto(); - if (!crypto) { + if (!context.getCrypto()) { this.state = { deviceIdentityKey: null }; } else { this.state = { deviceIdentityKey: undefined }; - crypto - .getOwnDeviceKeys() + } + } + + public componentDidMount(): void { + if (this.state.deviceIdentityKey === undefined) { + this.context + .getCrypto() + ?.getOwnDeviceKeys() .then((keys) => { this.setState({ deviceIdentityKey: keys.ed25519 }); }) @@ -55,7 +60,7 @@ export default class CryptographyPanel extends React.Component { } public render(): React.ReactNode { - const client = MatrixClientPeg.safeGet(); + const client = this.context; const deviceId = client.deviceId; let identityKey = this.state.deviceIdentityKey; if (identityKey === undefined) { @@ -122,25 +127,21 @@ export default class CryptographyPanel extends React.Component { } private onExportE2eKeysClicked = (): void => { - Modal.createDialogAsync( - import("../../../async-components/views/dialogs/security/ExportE2eKeysDialog") as unknown as Promise< - typeof ExportE2eKeysDialog - >, - { matrixClient: MatrixClientPeg.safeGet() }, + Modal.createDialog( + lazy(() => import("../../../async-components/views/dialogs/security/ExportE2eKeysDialog")), + { matrixClient: this.context }, ); }; private onImportE2eKeysClicked = (): void => { - Modal.createDialogAsync( - import("../../../async-components/views/dialogs/security/ImportE2eKeysDialog") as unknown as Promise< - typeof ImportE2eKeysDialog - >, - { matrixClient: MatrixClientPeg.safeGet() }, + Modal.createDialog( + lazy(() => import("../../../async-components/views/dialogs/security/ImportE2eKeysDialog")), + { matrixClient: this.context }, ); }; private updateBlacklistDevicesFlag = (checked: boolean): void => { - const crypto = MatrixClientPeg.safeGet().getCrypto(); + const crypto = this.context.getCrypto(); if (crypto) crypto.globalBlacklistUnverifiedDevices = checked; }; } diff --git a/src/components/views/settings/EventIndexPanel.tsx b/src/components/views/settings/EventIndexPanel.tsx index d2ade3571c3..0051c4dc3a0 100644 --- a/src/components/views/settings/EventIndexPanel.tsx +++ b/src/components/views/settings/EventIndexPanel.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React from "react"; +import React, { lazy } from "react"; import { _t } from "../../../languageHandler"; import SdkConfig from "../../../SdkConfig"; @@ -94,14 +94,12 @@ export default class EventIndexPanel extends React.Component<{}, IState> { } private onManage = async (): Promise => { - Modal.createDialogAsync( - // @ts-ignore: TS doesn't seem to like the type of this now that it - // has also been converted to TS as well, but I can't figure out why... - import("../../../async-components/views/dialogs/eventindex/ManageEventIndexDialog"), + Modal.createDialog( + lazy(() => import("../../../async-components/views/dialogs/eventindex/ManageEventIndexDialog")), { onFinished: () => {}, }, - null, + undefined, /* priority = */ false, /* static = */ true, ); diff --git a/src/components/views/settings/FontScalingPanel.tsx b/src/components/views/settings/FontScalingPanel.tsx index 5cdd9d16bb2..edc6c66645e 100644 --- a/src/components/views/settings/FontScalingPanel.tsx +++ b/src/components/views/settings/FontScalingPanel.tsx @@ -14,7 +14,7 @@ import { Layout } from "../../../settings/enums/Layout"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { SettingLevel } from "../../../settings/SettingLevel"; import { _t } from "../../../languageHandler"; -import SettingsSubsection from "./shared/SettingsSubsection"; +import { SettingsSubsection } from "./shared/SettingsSubsection"; import Field from "../elements/Field"; import { FontWatcher } from "../../../settings/watchers/FontWatcher"; @@ -55,6 +55,7 @@ export default class FontScalingPanel extends React.Component { } public async componentDidMount(): Promise { + this.unmounted = false; // Fetch the current user profile for the message preview const client = MatrixClientPeg.safeGet(); const userId = client.getSafeUserId(); @@ -79,9 +80,7 @@ export default class FontScalingPanel extends React.Component { public componentWillUnmount(): void { this.unmounted = true; - if (this.layoutWatcherRef) { - SettingsStore.unwatchSetting(this.layoutWatcherRef); - } + SettingsStore.unwatchSetting(this.layoutWatcherRef); } /** diff --git a/src/components/views/settings/ImageSizePanel.tsx b/src/components/views/settings/ImageSizePanel.tsx index e2157926282..dca21d89e2f 100644 --- a/src/components/views/settings/ImageSizePanel.tsx +++ b/src/components/views/settings/ImageSizePanel.tsx @@ -13,7 +13,7 @@ import StyledRadioButton from "../elements/StyledRadioButton"; import { _t } from "../../../languageHandler"; import { SettingLevel } from "../../../settings/SettingLevel"; import { ImageSize } from "../../../settings/enums/ImageSize"; -import SettingsSubsection from "./shared/SettingsSubsection"; +import { SettingsSubsection } from "./shared/SettingsSubsection"; interface IProps { // none diff --git a/src/components/views/settings/IntegrationManager.tsx b/src/components/views/settings/IntegrationManager.tsx index 91b3b4633f7..3a31a9e9c85 100644 --- a/src/components/views/settings/IntegrationManager.tsx +++ b/src/components/views/settings/IntegrationManager.tsx @@ -52,7 +52,7 @@ export default class IntegrationManager extends React.Component } public componentWillUnmount(): void { - if (this.dispatcherRef) dis.unregister(this.dispatcherRef); + dis.unregister(this.dispatcherRef); document.removeEventListener("keydown", this.onKeyDown); } diff --git a/src/components/views/settings/LayoutSwitcher.tsx b/src/components/views/settings/LayoutSwitcher.tsx index 5ca2610a388..bbf090aa383 100644 --- a/src/components/views/settings/LayoutSwitcher.tsx +++ b/src/components/views/settings/LayoutSwitcher.tsx @@ -9,7 +9,7 @@ import React, { JSX, useEffect, useState } from "react"; import { Field, HelpMessage, InlineField, Label, RadioControl, Root, ToggleControl } from "@vector-im/compound-web"; -import SettingsSubsection from "./shared/SettingsSubsection"; +import { SettingsSubsection } from "./shared/SettingsSubsection"; import { _t } from "../../../languageHandler"; import SettingsStore from "../../../settings/SettingsStore"; import { SettingLevel } from "../../../settings/SettingLevel"; diff --git a/src/components/views/settings/Notifications.tsx b/src/components/views/settings/Notifications.tsx index 78660d4f9dc..4ac5e2069b0 100644 --- a/src/components/views/settings/Notifications.tsx +++ b/src/components/views/settings/Notifications.tsx @@ -48,7 +48,7 @@ import { } from "../../../utils/pushRules/updatePushRuleActions"; import { Caption } from "../typography/Caption"; import { SettingsSubsectionHeading } from "./shared/SettingsSubsectionHeading"; -import SettingsSubsection from "./shared/SettingsSubsection"; +import { SettingsSubsection } from "./shared/SettingsSubsection"; import { doesRoomHaveUnreadMessages } from "../../../Unread"; import SettingsFlag from "../elements/SettingsFlag"; @@ -206,7 +206,7 @@ const NotificationActivitySettings = (): JSX.Element => { * The old, deprecated notifications tab view, only displayed if the user has the labs flag disabled. */ export default class Notifications extends React.PureComponent { - private settingWatchers: string[]; + private settingWatchers: string[] = []; public constructor(props: IProps) { super(props); @@ -220,7 +220,17 @@ export default class Notifications extends React.PureComponent { clearingNotifications: false, ruleIdsWithError: {}, }; + } + + private get isInhibited(): boolean { + // Caution: The master rule's enabled state is inverted from expectation. When + // the master rule is *enabled* it means all other rules are *disabled* (or + // inhibited). Conversely, when the master rule is *disabled* then all other rules + // are *enabled* (or operate fine). + return !!this.state.masterPushRule?.enabled; + } + public componentDidMount(): void { this.settingWatchers = [ SettingsStore.watchSetting("notificationsEnabled", null, (...[, , , , value]) => this.setState({ desktopNotifications: value as boolean }), @@ -235,17 +245,7 @@ export default class Notifications extends React.PureComponent { this.setState({ audioNotifications: value as boolean }), ), ]; - } - - private get isInhibited(): boolean { - // Caution: The master rule's enabled state is inverted from expectation. When - // the master rule is *enabled* it means all other rules are *disabled* (or - // inhibited). Conversely, when the master rule is *disabled* then all other rules - // are *enabled* (or operate fine). - return !!this.state.masterPushRule?.enabled; - } - public componentDidMount(): void { // noinspection JSIgnoredPromiseFromCall this.refreshFromServer(); this.refreshFromAccountData(); diff --git a/src/components/views/settings/SecureBackupPanel.tsx b/src/components/views/settings/SecureBackupPanel.tsx index dac7425e3c3..db165eb115a 100644 --- a/src/components/views/settings/SecureBackupPanel.tsx +++ b/src/components/views/settings/SecureBackupPanel.tsx @@ -7,11 +7,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { ReactNode } from "react"; +import React, { lazy, ReactNode } from "react"; import { CryptoEvent, BackupTrustInfo, KeyBackupInfo } from "matrix-js-sdk/src/crypto-api"; import { logger } from "matrix-js-sdk/src/logger"; -import type CreateKeyBackupDialog from "../../../async-components/views/dialogs/security/CreateKeyBackupDialog"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { _t } from "../../../languageHandler"; import Modal from "../../../Modal"; @@ -83,6 +82,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { } public componentDidMount(): void { + this.unmounted = false; this.loadBackupStatus(); MatrixClientPeg.safeGet().on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus); @@ -169,10 +169,8 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { } private startNewBackup = (): void => { - Modal.createDialogAsync( - import("../../../async-components/views/dialogs/security/CreateKeyBackupDialog") as unknown as Promise< - typeof CreateKeyBackupDialog - >, + Modal.createDialog( + lazy(() => import("../../../async-components/views/dialogs/security/CreateKeyBackupDialog")), { onFinished: () => { this.loadBackupStatus(); diff --git a/src/components/views/settings/SetIdServer.tsx b/src/components/views/settings/SetIdServer.tsx index e7a55f11333..8ed6461d0a3 100644 --- a/src/components/views/settings/SetIdServer.tsx +++ b/src/components/views/settings/SetIdServer.tsx @@ -101,7 +101,7 @@ export default class SetIdServer extends React.Component { } public componentWillUnmount(): void { - if (this.dispatcherRef) dis.unregister(this.dispatcherRef); + dis.unregister(this.dispatcherRef); } private onAction = (payload: ActionPayload): void => { diff --git a/src/components/views/settings/ThemeChoicePanel.tsx b/src/components/views/settings/ThemeChoicePanel.tsx index 4ba08612a03..83f17a2f7be 100644 --- a/src/components/views/settings/ThemeChoicePanel.tsx +++ b/src/components/views/settings/ThemeChoicePanel.tsx @@ -23,7 +23,7 @@ import classNames from "classnames"; import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "../../../languageHandler"; -import SettingsSubsection from "./shared/SettingsSubsection"; +import { SettingsSubsection } from "./shared/SettingsSubsection"; import ThemeWatcher from "../../../settings/watchers/ThemeWatcher"; import SettingsStore from "../../../settings/SettingsStore"; import { SettingLevel } from "../../../settings/SettingLevel"; diff --git a/src/components/views/settings/UserPersonalInfoSettings.tsx b/src/components/views/settings/UserPersonalInfoSettings.tsx index c8c8729817f..e33aa87781d 100644 --- a/src/components/views/settings/UserPersonalInfoSettings.tsx +++ b/src/components/views/settings/UserPersonalInfoSettings.tsx @@ -12,7 +12,7 @@ import { Alert } from "@vector-im/compound-web"; import { _t } from "../../../languageHandler"; import InlineSpinner from "../elements/InlineSpinner"; -import SettingsSubsection from "./shared/SettingsSubsection"; +import { SettingsSubsection } from "./shared/SettingsSubsection"; import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; import { ThirdPartyIdentifier } from "../../../AddThreepid"; import SettingsStore from "../../../settings/SettingsStore"; @@ -125,5 +125,3 @@ export const UserPersonalInfoSettings: React.FC =
); }; - -export default UserPersonalInfoSettings; diff --git a/src/components/views/settings/devices/CurrentDeviceSection.tsx b/src/components/views/settings/devices/CurrentDeviceSection.tsx index cf338392c89..153a9c5d4bd 100644 --- a/src/components/views/settings/devices/CurrentDeviceSection.tsx +++ b/src/components/views/settings/devices/CurrentDeviceSection.tsx @@ -11,7 +11,7 @@ import { LocalNotificationSettings } from "matrix-js-sdk/src/matrix"; import { _t } from "../../../../languageHandler"; import Spinner from "../../elements/Spinner"; -import SettingsSubsection from "../shared/SettingsSubsection"; +import { SettingsSubsection } from "../shared/SettingsSubsection"; import { SettingsSubsectionHeading } from "../shared/SettingsSubsectionHeading"; import DeviceDetails from "./DeviceDetails"; import { DeviceExpandDetailsButton } from "./DeviceExpandDetailsButton"; diff --git a/src/components/views/settings/devices/DeviceDetails.tsx b/src/components/views/settings/devices/DeviceDetails.tsx index dda0089d8b0..1a418f5dd52 100644 --- a/src/components/views/settings/devices/DeviceDetails.tsx +++ b/src/components/views/settings/devices/DeviceDetails.tsx @@ -111,7 +111,11 @@ const DeviceDetails: React.FC = ({
- +

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

diff --git a/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx b/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx index d99a2e5d312..e7839b71da2 100644 --- a/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx +++ b/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx @@ -8,8 +8,8 @@ Please see LICENSE files in the repository root for full details. import classNames from "classnames"; import React, { ComponentProps } from "react"; +import { ChevronDownIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; -import { Icon as CaretIcon } from "../../../../../res/img/feather-customised/dropdown-arrow.svg"; import { _t } from "../../../../languageHandler"; import AccessibleButton from "../../elements/AccessibleButton"; @@ -38,7 +38,7 @@ export const DeviceExpandDetailsButton = })} onClick={onClick} > - + ); }; diff --git a/src/components/views/settings/devices/LoginWithQRSection.tsx b/src/components/views/settings/devices/LoginWithQRSection.tsx index c5efb35efcf..a164ff894b4 100644 --- a/src/components/views/settings/devices/LoginWithQRSection.tsx +++ b/src/components/views/settings/devices/LoginWithQRSection.tsx @@ -8,10 +8,7 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import { - IGetLoginTokenCapability, IServerVersions, - GET_LOGIN_TOKEN_CAPABILITY, - Capabilities, IClientWellKnown, OidcClientConfig, MatrixClient, @@ -22,33 +19,17 @@ import { Text } from "@vector-im/compound-web"; import { _t } from "../../../../languageHandler"; import AccessibleButton from "../../elements/AccessibleButton"; -import SettingsSubsection from "../shared/SettingsSubsection"; +import { SettingsSubsection } from "../shared/SettingsSubsection"; import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext"; interface IProps { onShowQr: () => void; versions?: IServerVersions; - capabilities?: Capabilities; wellKnown?: IClientWellKnown; oidcClientConfig?: OidcClientConfig; isCrossSigningReady?: boolean; } -function shouldShowQrLegacy( - versions?: IServerVersions, - wellKnown?: IClientWellKnown, - capabilities?: Capabilities, -): boolean { - // Needs server support for (get_login_token or OIDC Device Authorization Grant) and MSC3886: - // in r0 of MSC3882 it is exposed as a feature flag, but in stable and unstable r1 it is a capability - const loginTokenCapability = GET_LOGIN_TOKEN_CAPABILITY.findIn(capabilities); - const getLoginTokenSupported = - !!versions?.unstable_features?.["org.matrix.msc3882"] || !!loginTokenCapability?.enabled; - const msc3886Supported = - !!versions?.unstable_features?.["org.matrix.msc3886"] || !!wellKnown?.["io.element.rendezvous"]?.server; - return getLoginTokenSupported && msc3886Supported; -} - export function shouldShowQr( cli: MatrixClient, isCrossSigningReady: boolean, @@ -73,15 +54,12 @@ export function shouldShowQr( const LoginWithQRSection: React.FC = ({ onShowQr, versions, - capabilities, wellKnown, oidcClientConfig, isCrossSigningReady, }) => { const cli = useMatrixClientContext(); - const offerShowQr = oidcClientConfig - ? shouldShowQr(cli, !!isCrossSigningReady, oidcClientConfig, versions, wellKnown) - : shouldShowQrLegacy(versions, wellKnown, capabilities); + const offerShowQr = shouldShowQr(cli, !!isCrossSigningReady, oidcClientConfig, versions, wellKnown); return ( diff --git a/src/components/views/settings/devices/SecurityRecommendations.tsx b/src/components/views/settings/devices/SecurityRecommendations.tsx index be4b7495008..c0fc8e26b80 100644 --- a/src/components/views/settings/devices/SecurityRecommendations.tsx +++ b/src/components/views/settings/devices/SecurityRecommendations.tsx @@ -10,7 +10,7 @@ import React from "react"; import { _t } from "../../../../languageHandler"; import AccessibleButton from "../../elements/AccessibleButton"; -import SettingsSubsection from "../shared/SettingsSubsection"; +import { SettingsSubsection } from "../shared/SettingsSubsection"; import DeviceSecurityCard from "./DeviceSecurityCard"; import { DeviceSecurityLearnMore } from "./DeviceSecurityLearnMore"; import { filterDevicesBySecurityRecommendation, FilterVariation, INACTIVE_DEVICE_AGE_DAYS } from "./filter"; diff --git a/src/components/views/settings/discovery/DiscoverySettings.tsx b/src/components/views/settings/discovery/DiscoverySettings.tsx index d240d53d7c7..f96b79fe8dc 100644 --- a/src/components/views/settings/discovery/DiscoverySettings.tsx +++ b/src/components/views/settings/discovery/DiscoverySettings.tsx @@ -18,7 +18,7 @@ import SettingsStore from "../../../../settings/SettingsStore"; import { UIFeature } from "../../../../settings/UIFeature"; import { _t } from "../../../../languageHandler"; import SetIdServer from "../SetIdServer"; -import SettingsSubsection from "../shared/SettingsSubsection"; +import { SettingsSubsection } from "../shared/SettingsSubsection"; import InlineTermsAgreement from "../../terms/InlineTermsAgreement"; import { Service, ServicePolicyPair, startTermsFlow } from "../../../../Terms"; import IdentityAuthClient from "../../../../IdentityAuthClient"; @@ -51,7 +51,6 @@ export const DiscoverySettings: React.FC = () => { const [emails, setEmails] = useState([]); const [phoneNumbers, setPhoneNumbers] = useState([]); const [idServerName, setIdServerName] = useState(abbreviateUrl(client.getIdentityServerUrl())); - const [canMake3pidChanges, setCanMake3pidChanges] = useState(false); const [requiredPolicyInfo, setRequiredPolicyInfo] = useState({ // This object is passed along to a component for handling @@ -88,11 +87,6 @@ export const DiscoverySettings: React.FC = () => { try { await getThreepidState(); - const capabilities = await client.getCapabilities(); - setCanMake3pidChanges( - !capabilities["m.3pid_changes"] || capabilities["m.3pid_changes"].enabled === true, - ); - // By starting the terms flow we get the logic for checking which terms the user has signed // for free. So we might as well use that for our own purposes. const idServerUrl = client.getIdentityServerUrl(); @@ -166,7 +160,7 @@ export const DiscoverySettings: React.FC = () => { medium={ThreepidMedium.Email} threepids={emails} onChange={getThreepidState} - disabled={!canMake3pidChanges} + disabled={!hasTerms} isLoading={isLoadingThreepids} /> @@ -180,7 +174,7 @@ export const DiscoverySettings: React.FC = () => { medium={ThreepidMedium.Phone} threepids={phoneNumbers} onChange={getThreepidState} - disabled={!canMake3pidChanges} + disabled={!hasTerms} isLoading={isLoadingThreepids} /> @@ -196,5 +190,3 @@ export const DiscoverySettings: React.FC = () => { ); }; - -export default DiscoverySettings; diff --git a/src/components/views/settings/notifications/NotificationPusherSettings.tsx b/src/components/views/settings/notifications/NotificationPusherSettings.tsx index 193436f5d11..9e17e7b829d 100644 --- a/src/components/views/settings/notifications/NotificationPusherSettings.tsx +++ b/src/components/views/settings/notifications/NotificationPusherSettings.tsx @@ -20,7 +20,7 @@ import { UserTab } from "../../dialogs/UserTab"; import AccessibleButton from "../../elements/AccessibleButton"; import LabelledCheckbox from "../../elements/LabelledCheckbox"; import { SettingsIndent } from "../shared/SettingsIndent"; -import SettingsSubsection, { SettingsSubsectionText } from "../shared/SettingsSubsection"; +import { SettingsSubsection, SettingsSubsectionText } from "../shared/SettingsSubsection"; function generalTabButton(content: string): JSX.Element { return ( diff --git a/src/components/views/settings/notifications/NotificationSettings2.tsx b/src/components/views/settings/notifications/NotificationSettings2.tsx index e7b92de7ce9..5f91c3874c7 100644 --- a/src/components/views/settings/notifications/NotificationSettings2.tsx +++ b/src/components/views/settings/notifications/NotificationSettings2.tsx @@ -31,7 +31,7 @@ import TagComposer from "../../elements/TagComposer"; import { StatelessNotificationBadge } from "../../rooms/NotificationBadge/StatelessNotificationBadge"; import { SettingsBanner } from "../shared/SettingsBanner"; import { SettingsSection } from "../shared/SettingsSection"; -import SettingsSubsection from "../shared/SettingsSubsection"; +import { SettingsSubsection } from "../shared/SettingsSubsection"; import { NotificationPusherSettings } from "./NotificationPusherSettings"; import SettingsFlag from "../../elements/SettingsFlag"; diff --git a/src/components/views/settings/shared/SettingsSubsection.tsx b/src/components/views/settings/shared/SettingsSubsection.tsx index a3b9c3c96cd..3248a5eb906 100644 --- a/src/components/views/settings/shared/SettingsSubsection.tsx +++ b/src/components/views/settings/shared/SettingsSubsection.tsx @@ -65,5 +65,3 @@ export const SettingsSubsection: React.FC = ({ {!legacy && }
); - -export default SettingsSubsection; diff --git a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx index c32ac5150b2..5798771e678 100644 --- a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx @@ -20,7 +20,7 @@ import { ViewRoomPayload } from "../../../../../dispatcher/payloads/ViewRoomPayl import SettingsStore from "../../../../../settings/SettingsStore"; import SettingsTab from "../SettingsTab"; import { SettingsSection } from "../../shared/SettingsSection"; -import SettingsSubsection from "../../shared/SettingsSubsection"; +import { SettingsSubsection } from "../../shared/SettingsSubsection"; interface IProps { room: Room; @@ -53,9 +53,11 @@ export default class AdvancedRoomSettingsTab extends React.Component { this.context.on(RoomStateEvent.Events, this.onStateEvent); - this.hasAliases().then((hasAliases) => this.setState({ hasAliases })); + + this.setState({ + hasAliases: await this.hasAliases(), + encrypted: Boolean(await this.context.getCrypto()?.isEncryptionEnabledInRoom(this.props.room.roomId)), + }); } private pullContentPropertyFromEvent(event: MatrixEvent | null | undefined, key: string, defaultValue: T): T { diff --git a/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx index 1521ff1bb49..783ea1bce3b 100644 --- a/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx @@ -11,7 +11,7 @@ import { JoinRule, EventType, RoomState, Room } from "matrix-js-sdk/src/matrix"; import { _t } from "../../../../../languageHandler"; import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch"; -import SettingsSubsection from "../../shared/SettingsSubsection"; +import { SettingsSubsection } from "../../shared/SettingsSubsection"; import SettingsTab from "../SettingsTab"; import { ElementCall } from "../../../../../models/Call"; import { useRoomState } from "../../../../../hooks/useRoomState"; diff --git a/src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx index 97f0e2e59e5..cd52b2a76b2 100644 --- a/src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx @@ -22,9 +22,9 @@ import ErrorDialog, { extractErrorMessageFromError } from "../../../dialogs/Erro import ChangePassword from "../../ChangePassword"; import SettingsTab from "../SettingsTab"; import { SettingsSection } from "../../shared/SettingsSection"; -import SettingsSubsection, { SettingsSubsectionText } from "../../shared/SettingsSubsection"; +import { SettingsSubsection, SettingsSubsectionText } from "../../shared/SettingsSubsection"; import { SDKContext } from "../../../../../contexts/SDKContext"; -import UserPersonalInfoSettings from "../../UserPersonalInfoSettings"; +import { UserPersonalInfoSettings } from "../../UserPersonalInfoSettings"; import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext"; interface IProps { diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index 90d54f1049e..f220803f723 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -23,7 +23,7 @@ import { ThemeChoicePanel } from "../../ThemeChoicePanel"; import ImageSizePanel from "../../ImageSizePanel"; import SettingsTab from "../SettingsTab"; import { SettingsSection } from "../../shared/SettingsSection"; -import SettingsSubsection from "../../shared/SettingsSubsection"; +import { SettingsSubsection } from "../../shared/SettingsSubsection"; interface IProps {} diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx index ec0e20fb321..7866131a01e 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx @@ -19,7 +19,7 @@ import BugReportDialog from "../../../dialogs/BugReportDialog"; import CopyableText from "../../../elements/CopyableText"; import SettingsTab from "../SettingsTab"; import { SettingsSection } from "../../shared/SettingsSection"; -import SettingsSubsection, { SettingsSubsectionText } from "../../shared/SettingsSubsection"; +import { SettingsSubsection, SettingsSubsectionText } from "../../shared/SettingsSubsection"; import ExternalLink from "../../../elements/ExternalLink"; import MatrixClientContext from "../../../../../contexts/MatrixClientContext"; diff --git a/src/components/views/settings/tabs/user/KeyboardUserSettingsTab.tsx b/src/components/views/settings/tabs/user/KeyboardUserSettingsTab.tsx index c973fa4b908..f4dd3de0ff5 100644 --- a/src/components/views/settings/tabs/user/KeyboardUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/KeyboardUserSettingsTab.tsx @@ -18,7 +18,7 @@ import { import { KeyboardShortcut } from "../../KeyboardShortcut"; import SettingsTab from "../SettingsTab"; import { SettingsSection } from "../../shared/SettingsSection"; -import SettingsSubsection from "../../shared/SettingsSubsection"; +import { SettingsSubsection } from "../../shared/SettingsSubsection"; import { showLabsFlags } from "./LabsUserSettingsTab"; interface IKeyboardShortcutRowProps { diff --git a/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx b/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx index 3be630fd2cc..54995415e2e 100644 --- a/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx @@ -17,7 +17,7 @@ import SettingsFlag from "../../../elements/SettingsFlag"; import { LabGroup, labGroupNames } from "../../../../../settings/Settings"; import { EnhancedMap } from "../../../../../utils/maps"; import { SettingsSection } from "../../shared/SettingsSection"; -import SettingsSubsection, { SettingsSubsectionText } from "../../shared/SettingsSubsection"; +import { SettingsSubsection, SettingsSubsectionText } from "../../shared/SettingsSubsection"; import SettingsTab from "../SettingsTab"; export const showLabsFlags = (): boolean => { diff --git a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx index 338f5ee9106..9ad7df31e98 100644 --- a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx @@ -22,7 +22,7 @@ import AccessibleButton from "../../../elements/AccessibleButton"; import Field from "../../../elements/Field"; import SettingsTab from "../SettingsTab"; import { SettingsSection } from "../../shared/SettingsSection"; -import SettingsSubsection, { SettingsSubsectionText } from "../../shared/SettingsSubsection"; +import { SettingsSubsection, SettingsSubsectionText } from "../../shared/SettingsSubsection"; interface IState { busy: boolean; diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx index d95b0894d9b..8cb662a9f02 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx @@ -24,7 +24,7 @@ import { OpenToTabPayload } from "../../../../../dispatcher/payloads/OpenToTabPa import { Action } from "../../../../../dispatcher/actions"; import SdkConfig from "../../../../../SdkConfig"; import { showUserOnboardingPage } from "../../../user-onboarding/UserOnboardingPage"; -import SettingsSubsection from "../../shared/SettingsSubsection"; +import { SettingsSubsection } from "../../shared/SettingsSubsection"; import SettingsTab from "../SettingsTab"; import { SettingsSection } from "../../shared/SettingsSection"; import LanguageDropdown from "../../../elements/LanguageDropdown"; diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx index 9e15df6e92e..7d5e27580cc 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx @@ -32,9 +32,9 @@ import { privateShouldBeEncrypted } from "../../../../../utils/rooms"; import type { IServerVersions } from "matrix-js-sdk/src/matrix"; import SettingsTab from "../SettingsTab"; import { SettingsSection } from "../../shared/SettingsSection"; -import SettingsSubsection, { SettingsSubsectionText } from "../../shared/SettingsSubsection"; +import { SettingsSubsection, SettingsSubsectionText } from "../../shared/SettingsSubsection"; import { useOwnDevices } from "../../devices/useOwnDevices"; -import DiscoverySettings from "../../discovery/DiscoverySettings"; +import { DiscoverySettings } from "../../discovery/DiscoverySettings"; import SetIntegrationManager from "../../SetIntegrationManager"; interface IIgnoredUserProps { @@ -129,7 +129,7 @@ export default class SecurityUserSettingsTab extends React.Component matrixClient.getVersions(), [matrixClient]); - const capabilities = useAsyncMemo(async () => matrixClient?.getCapabilities(), [matrixClient]); const wellKnown = useMemo(() => matrixClient?.getClientWellKnown(), [matrixClient]); const oidcClientConfig = useAsyncMemo(async () => { try { @@ -292,12 +291,7 @@ const SessionManagerTab: React.FC<{ if (signInWithQrMode) { return ( }> - + ); } @@ -308,7 +302,6 @@ const SessionManagerTab: React.FC<{ { - defaultDispatcher.dispatch({ + defaultDispatcher.dispatch({ action: Action.OpenSpotlight, initialFilter: Filter.PublicSpaces, }); diff --git a/src/components/views/spaces/SpaceSettingsGeneralTab.tsx b/src/components/views/spaces/SpaceSettingsGeneralTab.tsx index d2103782903..8d80f85c581 100644 --- a/src/components/views/spaces/SpaceSettingsGeneralTab.tsx +++ b/src/components/views/spaces/SpaceSettingsGeneralTab.tsx @@ -19,7 +19,7 @@ import { leaveSpace } from "../../../utils/leave-behaviour"; import { getTopic } from "../../../hooks/room/useTopic"; import SettingsTab from "../settings/tabs/SettingsTab"; import { SettingsSection } from "../settings/shared/SettingsSection"; -import SettingsSubsection from "../settings/shared/SettingsSubsection"; +import { SettingsSubsection } from "../settings/shared/SettingsSubsection"; interface IProps { matrixClient: MatrixClient; diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index 1cb60062286..c9498b8dd9c 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -205,7 +205,9 @@ export class SpaceItem extends React.PureComponent { collapsed, childSpaces: this.childSpaces, }; + } + public componentDidMount(): void { SpaceStore.instance.on(this.props.space.roomId, this.onSpaceUpdate); this.props.space.on(RoomEvent.Name, this.onRoomNameChange); } diff --git a/src/components/views/voip/LegacyCallView.tsx b/src/components/views/voip/LegacyCallView.tsx index 06cd331b11c..aba3d60743d 100644 --- a/src/components/views/voip/LegacyCallView.tsx +++ b/src/components/views/voip/LegacyCallView.tsx @@ -110,11 +110,10 @@ export default class LegacyCallView extends React.Component { sidebarFeeds: sidebar, sidebarShown: true, }; - - this.updateCallListeners(null, this.props.call); } public componentDidMount(): void { + this.updateCallListeners(null, this.props.call); this.dispatcherRef = dis.register(this.onAction); document.addEventListener("keydown", this.onNativeKeyDown); } @@ -126,7 +125,7 @@ export default class LegacyCallView extends React.Component { document.removeEventListener("keydown", this.onNativeKeyDown); this.updateCallListeners(this.props.call, null); - if (this.dispatcherRef) dis.unregister(this.dispatcherRef); + dis.unregister(this.dispatcherRef); } public static getDerivedStateFromProps(props: IProps): Partial { diff --git a/src/customisations/Security.ts b/src/customisations/Security.ts deleted file mode 100644 index c1b8b580a91..00000000000 --- a/src/customisations/Security.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2020 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ -import { CryptoCallbacks } from "matrix-js-sdk/src/crypto-api"; - -import { IMatrixClientCreds } from "../MatrixClientPeg"; -import { Kind as SetupEncryptionKind } from "../toasts/SetupEncryptionToast"; - -/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ -function examineLoginResponse(response: any, credentials: IMatrixClientCreds): void { - // E.g. add additional data to the persisted credentials -} - -/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ -function persistCredentials(credentials: IMatrixClientCreds): void { - // E.g. store any additional credential fields -} - -/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ -function createSecretStorageKey(): Uint8Array | null { - // E.g. generate or retrieve secret storage key somehow - return null; -} - -/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ -function getSecretStorageKey(): Uint8Array | null { - // E.g. retrieve secret storage key from some other place - return null; -} - -/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ -function catchAccessSecretStorageError(e: unknown): void { - // E.g. notify the user in some way -} - -/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ -function setupEncryptionNeeded(kind: SetupEncryptionKind): boolean { - // E.g. trigger some kind of setup - return false; -} - -// This interface summarises all available customisation points and also marks -// them all as optional. This allows customisers to only define and export the -// customisations they need while still maintaining type safety. -export interface ISecurityCustomisations { - examineLoginResponse?: typeof examineLoginResponse; - persistCredentials?: typeof persistCredentials; - createSecretStorageKey?: typeof createSecretStorageKey; - getSecretStorageKey?: typeof getSecretStorageKey; - catchAccessSecretStorageError?: typeof catchAccessSecretStorageError; - setupEncryptionNeeded?: typeof setupEncryptionNeeded; - getDehydrationKey?: CryptoCallbacks["getDehydrationKey"]; - - /** - * When false, disables the post-login UI from showing. If there's - * an error during setup, that will be shown to the user. - * - * Note: when this is set to false then the app will assume the user's - * encryption is set up some other way which would circumvent the default - * UI, such as by presenting alternative UI. - */ - SHOW_ENCRYPTION_SETUP_UI?: boolean; // default true -} - -// A real customisation module will define and export one or more of the -// customisation points that make up `ISecurityCustomisations`. -export default { - SHOW_ENCRYPTION_SETUP_UI: true, -} as ISecurityCustomisations; diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index 50c8160721d..718f592e6a2 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -135,12 +135,6 @@ export enum Action { */ OpenDialPad = "open_dial_pad", - /** - * Dial the phone number in the payload - * payload: DialNumberPayload - */ - DialNumber = "dial_number", - /** * Fired when CallHandler has checked for PSTN protocol support * payload: none diff --git a/src/dispatcher/dispatcher.ts b/src/dispatcher/dispatcher.ts index 6d8d3a15a8e..f50e2bfe007 100644 --- a/src/dispatcher/dispatcher.ts +++ b/src/dispatcher/dispatcher.ts @@ -45,8 +45,11 @@ export class MatrixDispatcher { /** * Removes a callback based on its token. + * @param id The token that was returned by `register`. + * Can be undefined to avoid needing an if around every caller. */ - public unregister(id: DispatchToken): void { + public unregister(id: DispatchToken | undefined): void { + if (!id) return; invariant(this.callbacks.has(id), `Dispatcher.unregister(...): '${id}' does not map to a registered callback.`); this.callbacks.delete(id); } @@ -175,7 +178,7 @@ export class MatrixDispatcher { } } -export const defaultDispatcher = new MatrixDispatcher(); +const defaultDispatcher = new MatrixDispatcher(); if (!window.mxDispatcher) { window.mxDispatcher = defaultDispatcher; diff --git a/src/hooks/room/useRoomCall.ts b/src/hooks/room/useRoomCall.tsx similarity index 93% rename from src/hooks/room/useRoomCall.ts rename to src/hooks/room/useRoomCall.tsx index adf16cc5cc9..e1db9f92489 100644 --- a/src/hooks/room/useRoomCall.ts +++ b/src/hooks/room/useRoomCall.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import { Room } from "matrix-js-sdk/src/matrix"; -import React, { useCallback, useEffect, useMemo, useState } from "react"; +import React, { ReactNode, useCallback, useEffect, useMemo, useState } from "react"; import { CallType } from "matrix-js-sdk/src/webrtc/call"; import { useFeatureEnabled } from "../useSettings"; @@ -35,22 +35,42 @@ import { isVideoRoom } from "../../utils/video-rooms"; import { useGuestAccessInformation } from "./useGuestAccessInformation"; import SettingsStore from "../../settings/SettingsStore"; import { UIFeature } from "../../settings/UIFeature"; +import { BetaPill } from "../../components/views/beta/BetaCard"; +import { InteractionName } from "../../PosthogTrackers"; export enum PlatformCallType { ElementCall, JitsiCall, LegacyCall, } -export const getPlatformCallTypeLabel = (platformCallType: PlatformCallType): string => { + +export const getPlatformCallTypeProps = ( + platformCallType: PlatformCallType, +): { + label: string; + children?: ReactNode; + analyticsName: InteractionName; +} => { switch (platformCallType) { case PlatformCallType.ElementCall: - return _t("voip|element_call"); + return { + label: _t("voip|element_call"), + analyticsName: "WebVoipOptionElementCall", + children: , + }; case PlatformCallType.JitsiCall: - return _t("voip|jitsi_call"); + return { + label: _t("voip|jitsi_call"), + analyticsName: "WebVoipOptionJitsi", + }; case PlatformCallType.LegacyCall: - return _t("voip|legacy_call"); + return { + label: _t("voip|legacy_call"), + analyticsName: "WebVoipOptionLegacy", + }; } }; + const enum State { NoCall, NoOneHere, diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index ff55a126332..7647377196b 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -103,7 +103,6 @@ "report_content": "Nahlásit obsah", "resend": "Poslat znovu", "reset": "Resetovat", - "restore": "Obnovit", "resume": "Pokračovat", "retry": "Zkusit znovu", "review": "Prohlédnout", @@ -208,7 +207,6 @@ "failed_query_registration_methods": "Nepovedlo se načíst podporované způsoby přihlášení.", "failed_soft_logout_auth": "Nepovedlo se autentifikovat", "failed_soft_logout_homeserver": "Kvůli problémům s domovským server se nepovedlo autentifikovat znovu", - "footer_powered_by_matrix": "používá protokol Matrix", "forgot_password_email_invalid": "E-mailová adresa se nezdá být platná.", "forgot_password_email_required": "Musíte zadat e-mailovou adresu spojenou s vaším účtem.", "forgot_password_prompt": "Zapomněli jste heslo?", @@ -247,15 +245,12 @@ "phone_label": "Telefon", "phone_optional_label": "Telefonní číslo (nepovinné)", "qr_code_login": { - "approve_access_warning": "Schválením přístupu tohoto zařízení získá zařízení plný přístup k vašemu účtu.", "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:", "error_rate_limited": "Příliš mnoho pokusů v krátkém čase. Počkejte chvíli, než to zkusíte znovu.", "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", "select_qr_code": "Vybrat '%(scanQRCode)s'", - "sign_in_new_device": "Přihlásit nové zařízení", "waiting_for_device": "Čekání na přihlášení zařízení" }, "register_action": "Vytvořit účet", @@ -896,7 +891,6 @@ }, "unable_to_setup_keys_error": "Nepovedlo se nastavit klíče", "unsupported": "Tento klient nepodporuje koncové šifrování.", - "upgrade_toast_title": "Je dostupná aktualizace šifrování", "verification": { "accepting": "Přijímání…", "after_new_login": { @@ -2454,18 +2448,13 @@ "pass_phrase_match_failed": "To nesedí.", "pass_phrase_match_success": "To odpovídá!", "phrase_strong_enough": "Skvělé! Tato bezpečnostní fráze vypadá dostatečně silně.", - "requires_key_restore": "Pro aktualizaci šifrování obnovte klíče ze zálohy", - "requires_password_confirmation": "Potvrďte, že chcete aktualizaci provést zadáním svého uživatelského hesla:", - "requires_server_authentication": "Server si vás potřebuje ověřit, abychom mohli provést aktualizaci.", "secret_storage_query_failure": "Nelze zjistit stav úložiště klíčů", "security_key_safety_reminder": "Bezpečnostní klíč uložte na bezpečné místo, například do správce hesel nebo do trezoru, protože slouží k ochraně zašifrovaných dat.", - "session_upgrade_description": "Aktualizujte tuto přihlášenou relaci abyste mohli ověřovat ostatní relace. Tím jim dáte přístup k šifrovaným konverzacím a ostatní uživatelé je jim budou automaticky věřit.", "set_phrase_again": "Nastavit heslo znovu.", "settings_reminder": "Zabezpečené zálohování a správu klíčů můžete také nastavit v Nastavení.", "title_confirm_phrase": "Potvrďte bezpečnostní frázi", "title_save_key": "Uložte svůj bezpečnostní klíč", "title_set_phrase": "Nastavit bezpečnostní frázi", - "title_upgrade_encryption": "Aktualizovat šifrování", "unable_to_setup": "Nepovedlo se nastavit bezpečné úložiště", "use_different_passphrase": "Použít jinou přístupovou frázi?", "use_phrase_only_you_know": "Použijte tajnou frázi, kterou znáte pouze vy, a volitelně uložte bezpečnostní klíč, který použijete pro zálohování." @@ -3535,7 +3524,6 @@ "truncated_list_n_more": { "other": "A %(count)s dalších..." }, - "unknown_device": "Neznámé zařízení", "unsupported_server_description": "Tento server používá starší verzi Matrix. Chcete-li používat %(brand)s bez možných problémů, aktualizujte Matrixu na %(version)s .", "unsupported_server_title": "Váš server není podporován", "update": { diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index e8bff7cd442..abe4566f8c9 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -99,7 +99,6 @@ "report_content": "Inhalt melden", "resend": "Erneut senden", "reset": "Zurücksetzen", - "restore": "Wiederherstellen", "resume": "Fortsetzen", "retry": "Wiederholen", "review": "Überprüfen", @@ -204,7 +203,6 @@ "failed_query_registration_methods": "Konnte unterstützte Registrierungsmethoden nicht abrufen.", "failed_soft_logout_auth": "Erneute Authentifizierung fehlgeschlagen", "failed_soft_logout_homeserver": "Erneute Authentifizierung aufgrund eines Problems des Heim-Servers fehlgeschlagen", - "footer_powered_by_matrix": "Betrieben mit Matrix", "forgot_password_email_invalid": "E-Mail-Adresse scheint ungültig zu sein.", "forgot_password_email_required": "Es muss die mit dem Benutzerkonto verbundene E-Mail-Adresse eingegeben werden.", "forgot_password_prompt": "Passwort vergessen?", @@ -243,15 +241,12 @@ "phone_label": "Telefon", "phone_optional_label": "Telefon (optional)", "qr_code_login": { - "approve_access_warning": "Indem du den Zugriff dieses Gerätes bestätigst, erhält es vollen Zugang zu deinem Konto.", "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:", "error_rate_limited": "Zu viele Versuche in zu kurzer Zeit. Warte ein wenig, bevor du es erneut versuchst.", "error_unexpected": "Ein unerwarteter Fehler ist aufgetreten.", "scan_code_instruction": "Lese den folgenden QR-Code mit deinem nicht angemeldeten Gerät ein.", "scan_qr_code": "QR-Code einlesen", "select_qr_code": "Wähle „%(scanQRCode)s“", - "sign_in_new_device": "Neues Gerät anmelden", "waiting_for_device": "Warte auf Anmeldung des Gerätes" }, "register_action": "Konto erstellen", @@ -889,7 +884,6 @@ }, "unable_to_setup_keys_error": "Schlüssel können nicht eingerichtet werden", "unsupported": "Diese Anwendung unterstützt keine Ende-zu-Ende-Verschlüsselung.", - "upgrade_toast_title": "Verschlüsselungsaktualisierung verfügbar", "verification": { "accepting": "Annehmen…", "after_new_login": { @@ -2434,18 +2428,13 @@ "pass_phrase_match_failed": "Das passt nicht.", "pass_phrase_match_success": "Das passt!", "phrase_strong_enough": "Großartig! Diese Sicherheitsphrase sieht stark genug aus.", - "requires_key_restore": "Schlüsselsicherung wiederherstellen, um deine Verschlüsselung zu aktualisieren", - "requires_password_confirmation": "Gib dein Kontopasswort ein, um die Aktualisierung zu bestätigen:", - "requires_server_authentication": "Du musst dich authentifizieren, um die Aktualisierung zu bestätigen.", "secret_storage_query_failure": "Status des sicheren Speichers kann nicht gelesen werden", "security_key_safety_reminder": "Bewahre deinen Sicherheitsschlüssel sicher auf, etwa in einem Passwortmanager oder einem Safe, da er verwendet wird, um deine Daten zu sichern.", - "session_upgrade_description": "Aktualisiere diese Sitzung, um mit ihr andere Sitzungen verifizieren zu können, damit sie Zugang zu verschlüsselten Nachrichten erhalten und für andere als vertrauenswürdig markiert werden.", "set_phrase_again": "Gehe zurück und setze es erneut.", "settings_reminder": "Du kannst auch in den Einstellungen Sicherungen einrichten und deine Schlüssel verwalten.", "title_confirm_phrase": "Sicherheitsphrase bestätigen", "title_save_key": "Sicherungsschlüssel sichern", "title_set_phrase": "Sicherheitsphrase setzen", - "title_upgrade_encryption": "Aktualisiere deine Verschlüsselung", "unable_to_setup": "Sicherer Speicher kann nicht eingerichtet werden", "use_different_passphrase": "Eine andere Passphrase verwenden?", "use_phrase_only_you_know": "Verwende für deine Sicherung eine geheime Phrase, die nur du kennst, und speichere optional einen Sicherheitsschlüssel." @@ -3510,7 +3499,6 @@ "truncated_list_n_more": { "other": "Und %(count)s weitere …" }, - "unknown_device": "Unbekanntes Gerät", "unsupported_server_description": "Dieser Server nutzt eine ältere Matrix-Version. Aktualisiere auf Matrix %(version)s, um %(brand)s fehlerfrei nutzen zu können.", "unsupported_server_title": "Dein Server wird nicht unterstützt", "update": { diff --git a/src/i18n/strings/el.json b/src/i18n/strings/el.json index 59309710cf3..2c042c0dc36 100644 --- a/src/i18n/strings/el.json +++ b/src/i18n/strings/el.json @@ -91,7 +91,6 @@ "report_content": "Αναφορά Περιεχομένου", "resend": "Αποστολή ξανά", "reset": "Επαναφορά", - "restore": "Επαναφορά", "resume": "Συνέχιση", "retry": "Προσπάθεια ξανά", "review": "Ανασκόπηση", @@ -183,7 +182,6 @@ "failed_query_registration_methods": "Αδυναμία λήψης των υποστηριζόμενων μεθόδων εγγραφής.", "failed_soft_logout_auth": "Απέτυχε ο εκ νέου έλεγχος ταυτότητας", "failed_soft_logout_homeserver": "Απέτυχε ο εκ νέου έλεγχος ταυτότητας λόγω προβλήματος με τον κεντρικό διακομιστή", - "footer_powered_by_matrix": "λειτουργεί με το Matrix", "forgot_password_email_invalid": "Η διεύθυνση email δε φαίνεται να είναι έγκυρη.", "forgot_password_email_required": "Πρέπει να εισηχθεί η διεύθυνση ηλ. αλληλογραφίας που είναι συνδεδεμένη με τον λογαριασμό σας.", "forgot_password_prompt": "Ξεχάσετε τον κωδικό σας;", @@ -742,7 +740,6 @@ }, "unable_to_setup_keys_error": "Δεν είναι δυνατή η ρύθμιση των κλειδιών", "unsupported": "Αυτό το πρόγραμμα-πελάτης δεν υποστηρίζει κρυπτογράφηση από άκρο σε άκρο.", - "upgrade_toast_title": "Διατίθεται αναβάθμιση κρυπτογράφησης", "verification": { "accepting": "Αποδοχή …", "after_new_login": { @@ -1965,18 +1962,13 @@ "pass_phrase_match_failed": "Αυτό δεν ταιριάζει.", "pass_phrase_match_success": "Ταιριάζει!", "phrase_strong_enough": "Τέλεια! Αυτή η Φράση Ασφαλείας φαίνεται αρκετά ισχυρή.", - "requires_key_restore": "Επαναφέρετε το αντίγραφο ασφαλείας του κλειδιού σας για να αναβαθμίσετε την κρυπτογράφηση", - "requires_password_confirmation": "Εισαγάγετε τον κωδικό πρόσβασης του λογαριασμού σας για να επιβεβαιώσετε την αναβάθμιση:", - "requires_server_authentication": "Θα χρειαστεί να πραγματοποιήσετε έλεγχο ταυτότητας με τον διακομιστή για να επιβεβαιώσετε την αναβάθμιση.", "secret_storage_query_failure": "Δεν είναι δυνατή η υποβολή ερωτήματος για την κατάσταση του μυστικού χώρου αποθήκευσης", "security_key_safety_reminder": "Αποθηκεύστε το Κλειδί ασφαλείας σας σε ασφαλές μέρος, όπως έναν διαχείριστη κωδικών πρόσβασης ή ένα χρηματοκιβώτιο, καθώς χρησιμοποιείται για την προστασία των κρυπτογραφημένων δεδομένων σας.", - "session_upgrade_description": "Αναβαθμίστε αυτήν την συνεδρία για να της επιτρέψετε να επαληθεύει άλλες συνεδρίες, παραχωρώντας τους πρόσβαση σε κρυπτογραφημένα μηνύματα και επισημαίνοντάς τα ως αξιόπιστα για άλλους χρήστες.", "set_phrase_again": "Επιστρέψτε για να το ρυθμίσετε ξανά.", "settings_reminder": "Μπορείτε επίσης να ρυθμίσετε το Ασφαλές αντίγραφο ασφαλείας και να διαχειριστείτε τα κλειδιά σας στις Ρυθμίσεις.", "title_confirm_phrase": "Επιβεβαίωση Φράσης Ασφαλείας", "title_save_key": "Αποθηκεύστε το κλειδί ασφαλείας σας", "title_set_phrase": "Ορίστε μια Φράση Ασφαλείας", - "title_upgrade_encryption": "Αναβαθμίστε την κρυπτογράφηση σας", "unable_to_setup": "Δεν είναι δυνατή η ρύθμιση του μυστικού χώρου αποθήκευσης", "use_different_passphrase": "Να χρησιμοποιηθεί διαφορετική φράση;", "use_phrase_only_you_know": "Χρησιμοποιήστε μια μυστική φράση που γνωρίζετε μόνο εσείς και προαιρετικά αποθηκεύστε ένα κλειδί ασφαλείας για να το χρησιμοποιήσετε για τη δημιουργία αντιγράφων ασφαλείας." @@ -2836,7 +2828,6 @@ "truncated_list_n_more": { "other": "Και %(count)s ακόμα..." }, - "unknown_device": "Άγνωστη συσκευή", "update": { "changelog": "Αλλαγές", "check_action": "Έλεγχος για ενημέρωση", diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7e96be0589a..4a524db97cd 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -103,7 +103,6 @@ "report_content": "Report Content", "resend": "Resend", "reset": "Reset", - "restore": "Restore", "resume": "Resume", "retry": "Retry", "review": "Review", @@ -210,7 +209,6 @@ "failed_query_registration_methods": "Unable to query for supported registration methods.", "failed_soft_logout_auth": "Failed to re-authenticate", "failed_soft_logout_homeserver": "Failed to re-authenticate due to a homeserver problem", - "footer_powered_by_matrix": "powered by Matrix", "forgot_password_email_invalid": "The email address doesn't appear to be valid.", "forgot_password_email_required": "The email address linked to your account must be entered.", "forgot_password_prompt": "Forgotten your password?", @@ -250,13 +248,11 @@ "phone_label": "Phone", "phone_optional_label": "Phone (optional)", "qr_code_login": { - "approve_access_warning": "By approving access for this device, it will have full access to your account.", "check_code_explainer": "This will verify that the connection to your other device is secure.", "check_code_heading": "Enter the number shown on your other device", "check_code_input_label": "2-digit code", "check_code_mismatch": "The numbers don't match", "completing_setup": "Completing set up of your new device", - "confirm_code_match": "Check that the code below matches with your other device:", "error_etag_missing": "An unexpected error occurred. This may be due to a browser extension, proxy server, or server misconfiguration.", "error_expired": "Sign in expired. Please try again.", "error_expired_title": "The sign in was not completed in time", @@ -284,7 +280,6 @@ "security_code": "Security code", "security_code_prompt": "If asked, enter the code below on your other device.", "select_qr_code": "Select \"%(scanQRCode)s\"", - "sign_in_new_device": "Sign in new device", "unsupported_explainer": "Your account provider doesn't support signing into a new device with a QR code.", "unsupported_heading": "QR code not supported", "waiting_for_device": "Waiting for device to sign in" @@ -509,6 +504,7 @@ "matrix": "Matrix", "message": "Message", "message_layout": "Message layout", + "message_timestamp_invalid": "Invalid timestamp", "microphone": "Microphone", "model": "Model", "modern": "Modern", @@ -933,7 +929,6 @@ }, "unable_to_setup_keys_error": "Unable to set up keys", "unsupported": "This client does not support end-to-end encryption.", - "upgrade_toast_title": "Encryption upgrade available", "verification": { "accepting": "Accepting…", "after_new_login": { @@ -1115,7 +1110,15 @@ "you": "You reacted %(reaction)s to %(message)s" }, "m.sticker": "%(senderName)s: %(stickerName)s", - "m.text": "%(senderName)s: %(message)s" + "m.text": "%(senderName)s: %(message)s", + "prefix": { + "audio": "Audio", + "file": "File", + "image": "Image", + "poll": "Poll", + "video": "Video" + }, + "preview": "%(prefix)s: %(preview)s" }, "export_chat": { "cancelled": "Export Cancelled", @@ -2042,14 +2045,6 @@ "button_view_all": "View all", "description": "This room has pinned messages. Click to view them.", "go_to_message": "View the pinned message in the timeline.", - "prefix": { - "audio": "Audio", - "file": "File", - "image": "Image", - "poll": "Poll", - "video": "Video" - }, - "preview": "%(prefix)s: %(preview)s", "title": "%(index)s of %(length)s Pinned messages" }, "read_topic": "Click to read topic", @@ -2591,18 +2586,13 @@ "pass_phrase_match_failed": "That doesn't match.", "pass_phrase_match_success": "That matches!", "phrase_strong_enough": "Great! This Security Phrase looks strong enough.", - "requires_key_restore": "Restore your key backup to upgrade your encryption", - "requires_password_confirmation": "Enter your account password to confirm the upgrade:", - "requires_server_authentication": "You'll need to authenticate with the server to confirm the upgrade.", "secret_storage_query_failure": "Unable to query secret storage status", "security_key_safety_reminder": "Store your Security Key somewhere safe, like a password manager or a safe, as it's used to safeguard your encrypted data.", - "session_upgrade_description": "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.", "set_phrase_again": "Go back to set it again.", "settings_reminder": "You can also set up Secure Backup & manage your keys in Settings.", "title_confirm_phrase": "Confirm Security Phrase", "title_save_key": "Save your Security Key", "title_set_phrase": "Set a Security Phrase", - "title_upgrade_encryption": "Upgrade your encryption", "unable_to_setup": "Unable to set up secret storage", "use_different_passphrase": "Use a different passphrase?", "use_phrase_only_you_know": "Use a secret phrase only you know, and optionally save a Security Key to use for backup." @@ -3276,8 +3266,8 @@ "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", - "sender_identity_previously_verified": "Verified identity has changed", - "sender_unsigned_device": "Encrypted by a device not verified by its owner.", + "sender_identity_previously_verified": "Sender's verified identity has changed", + "sender_unsigned_device": "Sent from an insecure device.", "unable_to_decrypt": "Unable to decrypt message" }, "disambiguated_profile": "%(displayName)s (%(matrixId)s)", @@ -3716,7 +3706,6 @@ "truncated_list_n_more": { "other": "And %(count)s more..." }, - "unknown_device": "Unknown device", "unsupported_browser": { "description": "If you continue, some features may stop working and there is a risk that you may lose data in the future. Update your browser to continue using %(brand)s.", "title": "%(brand)s does not support this browser" diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json index 51d17e01411..33d8e57f488 100644 --- a/src/i18n/strings/eo.json +++ b/src/i18n/strings/eo.json @@ -81,7 +81,6 @@ "report_content": "Raporti enhavon", "resend": "Resendi", "reset": "Restarigi", - "restore": "Rehavi", "resume": "Daŭrigi", "retry": "Reprovi", "review": "Rekontroli", @@ -172,7 +171,6 @@ "failed_query_registration_methods": "Ne povas peti subtenatajn registrajn metodojn.", "failed_soft_logout_auth": "Malsukcesis reaŭtentikigi", "failed_soft_logout_homeserver": "Malsukcesis reaŭtentikigi pro hejmservila problemo", - "footer_powered_by_matrix": "funkciigata de Matrix", "forgot_password_email_invalid": "La retpoŝtadreso ŝajnas ne valida.", "forgot_password_email_required": "Vi devas enigi retpoŝtadreson ligitan al via konto.", "forgot_password_prompt": "Ĉu vi forgesis vian pasvorton?", @@ -680,7 +678,6 @@ }, "unable_to_setup_keys_error": "Ne povas agordi ŝlosilojn", "unsupported": "Ĉi tiu kliento ne subtenas tutvojan ĉifradon.", - "upgrade_toast_title": "Ĝisdatigo de ĉifrado haveblas", "verification": { "accepting": "Akceptante…", "cancelled": "Vi nuligis kontrolon.", @@ -1770,18 +1767,13 @@ "pass_phrase_match_failed": "Tio ne akordas.", "pass_phrase_match_success": "Tio akordas!", "phrase_strong_enough": "Bonege! La Sekureca frazo ŝajnas sufiĉe forta.", - "requires_key_restore": "Rehavu vian savkopion de ŝlosiloj por gradaltigi vian ĉifradon", - "requires_password_confirmation": "Enigu pasvorton de via konto por konfirmi la gradaltigon:", - "requires_server_authentication": "Vi devos aŭtentikigi kun la servilo por konfirmi la gradaltigon.", "secret_storage_query_failure": "Ne povis peti staton de sekreta deponejo", "security_key_safety_reminder": "Konservu vian Sekurecan ŝlosilon ie sekure, kiel pasvortadministranto aŭ monŝranko, ĉar ĝi estas uzata por protekti viajn ĉifritajn datumojn.", - "session_upgrade_description": "Gradaltigu ĉi tiun salutaĵon por ebligi al ĝi kontroladon de aliaj salutaĵoj, donante al ili aliron al ĉifritaj mesaĵoj, kaj markante ilin fidataj por aliaj uzantoj.", "set_phrase_again": "Reiru por reagordi ĝin.", "settings_reminder": "Vi ankaŭ povas agordi Sekuran savkopiadon kaj administri viajn ŝlosilojn per Agordoj.", "title_confirm_phrase": "Konfirmi Sekurecan frazon", "title_save_key": "Konservi vian Sekurecan ŝlosilon", "title_set_phrase": "Agordi Sekurecan frazon", - "title_upgrade_encryption": "Gradaltigi vian ĉifradon", "unable_to_setup": "Ne povas starigi sekretan deponejon", "use_different_passphrase": "Ĉu uzi alian pasfrazon?", "use_phrase_only_you_know": "Uzu sekretan frazon kiun konas nur vi, kaj laŭplaĉe konservu sekurecan ŝlosilon, uzotan por savkopiado." @@ -2551,7 +2543,6 @@ "truncated_list_n_more": { "other": "Kaj %(count)s pliaj…" }, - "unknown_device": "Nekonata aparato", "update": { "changelog": "Protokolo de ŝanĝoj", "check_action": "Kontroli ĝisdatigojn", diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json index 7b5185d360c..cb6a8557b39 100644 --- a/src/i18n/strings/es.json +++ b/src/i18n/strings/es.json @@ -93,7 +93,6 @@ "report_content": "Denunciar contenido", "resend": "Reenviar", "reset": "Restablecer", - "restore": "Restaurar", "resume": "Recuperar", "retry": "Reintentar", "review": "Revisar", @@ -194,7 +193,6 @@ "failed_query_registration_methods": "No se pueden consultar los métodos de registro admitidos.", "failed_soft_logout_auth": "No se pudo volver a autenticar", "failed_soft_logout_homeserver": "No ha sido posible volver a autenticarse debido a un problema con el servidor base", - "footer_powered_by_matrix": "con el poder de Matrix", "forgot_password_email_invalid": "La dirección de correo no parece ser válida.", "forgot_password_email_required": "Debes ingresar la dirección de correo electrónico vinculada a tu cuenta.", "forgot_password_prompt": "¿Olvidaste tu contraseña?", @@ -231,15 +229,12 @@ "phone_label": "Teléfono", "phone_optional_label": "Teléfono (opcional)", "qr_code_login": { - "approve_access_warning": "Si apruebas acceso a este dispositivo, tendrá acceso completo a tu cuenta.", "completing_setup": "Terminando de configurar tu nuevo dispositivo", - "confirm_code_match": "Comprueba que el siguiente código también aparece en el otro dispositivo:", "error_rate_limited": "Demasiados intentos en poco tiempo. Espera un poco antes de volverlo a intentar.", "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", "select_qr_code": "Selecciona «%(scanQRCode)s»", - "sign_in_new_device": "Conectar nuevo dispositivo", "waiting_for_device": "Esperando a que el dispositivo inicie sesión" }, "register_action": "Crear cuenta", @@ -827,7 +822,6 @@ }, "unable_to_setup_keys_error": "No se han podido configurar las claves", "unsupported": "Este cliente no es compatible con el cifrado de extremo a extremo.", - "upgrade_toast_title": "Mejora de cifrado disponible", "verification": { "accepting": "Aceptando…", "after_new_login": { @@ -2247,18 +2241,13 @@ "pass_phrase_match_failed": "No coincide.", "pass_phrase_match_success": "¡Eso combina!", "phrase_strong_enough": "¡Genial! Esta frase de seguridad parece lo suficientemente segura.", - "requires_key_restore": "Restaure la copia de seguridad de su clave para actualizar su cifrado", - "requires_password_confirmation": "Ingrese la contraseña de su cuenta para confirmar la actualización:", - "requires_server_authentication": "Deberá autenticarse con el servidor para confirmar la actualización.", "secret_storage_query_failure": "No se puede consultar el estado del almacenamiento secreto", "security_key_safety_reminder": "Guarda tu clave de seguridad en un lugar seguro (por ejemplo, un gestor de contraseñas o una caja fuerte) porque sirve para proteger tus datos cifrados.", - "session_upgrade_description": "Actualice esta sesión para permitirle verificar otras sesiones, otorgándoles acceso a mensajes cifrados y marcándolos como confiables para otros usuarios.", "set_phrase_again": "Volver y ponerlo de nuevo.", "settings_reminder": "También puedes configurar la copia de seguridad segura y gestionar sus claves en configuración.", "title_confirm_phrase": "Confirmar la frase de seguridad", "title_save_key": "Guarde su llave de seguridad", "title_set_phrase": "Establecer una frase de seguridad", - "title_upgrade_encryption": "Actualice su cifrado", "unable_to_setup": "No se puede configurar el almacenamiento secreto", "use_different_passphrase": "¿Utiliza una frase de contraseña diferente?", "use_phrase_only_you_know": "Usa una frase secreta que solo tú conozcas y, opcionalmente, guarda una clave de seguridad para usarla como respaldo." @@ -3234,7 +3223,6 @@ "truncated_list_n_more": { "other": "Y %(count)s más…" }, - "unknown_device": "Dispositivo desconocido", "update": { "changelog": "Registro de cambios", "check_action": "Comprobar si hay actualizaciones", diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index 9777913c0b1..b05d9024c05 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -103,7 +103,6 @@ "report_content": "Teata sisust haldurile", "resend": "Saada uuesti", "reset": "Taasta algolek", - "restore": "Taasta", "resume": "Jätka", "retry": "Proovi uuesti", "review": "Vaata üle", @@ -208,7 +207,6 @@ "failed_query_registration_methods": "Ei õnnestunud pärida toetatud registreerimismeetodite loendit.", "failed_soft_logout_auth": "Uuesti autentimine ei õnnestunud", "failed_soft_logout_homeserver": "Uuesti autentimine ei õnnestunud koduserveri vea tõttu", - "footer_powered_by_matrix": "põhineb Matrix'il", "forgot_password_email_invalid": "See e-posti aadress ei tundu olema korrektne.", "forgot_password_email_required": "Sa pead sisestama oma kontoga seotud e-posti aadressi.", "forgot_password_prompt": "Kas sa unustasid oma salasõna?", @@ -247,15 +245,12 @@ "phone_label": "Telefon", "phone_optional_label": "Telefoninumber (kui soovid)", "qr_code_login": { - "approve_access_warning": "Lubades ligipääsu sellele seadmele, annad talle ka täismahulise ligipääsu oma kasutajakontole.", "completing_setup": "Lõpetame uue seadme seadistamise", - "confirm_code_match": "Kontrolli, et järgnev kood klapib teises seadmes kuvatava koodiga:", "error_rate_limited": "Liiga palju päringuid napis ajavahemikus. Enne uuesti proovimist palun oota veidi.", "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", "select_qr_code": "Vali „%(scanQRCode)s“", - "sign_in_new_device": "Logi sisse uus seade", "waiting_for_device": "Ootame, et teine seade logiks võrku" }, "register_action": "Loo konto", @@ -894,7 +889,6 @@ }, "unable_to_setup_keys_error": "Krüptovõtmete kasutuselevõtmine ei õnnestu", "unsupported": "See klient ei toeta läbivat krüptimist.", - "upgrade_toast_title": "Krüptimise uuendus on saadaval", "verification": { "accepting": "Nõustun …", "after_new_login": { @@ -2418,18 +2412,13 @@ "pass_phrase_match_failed": "Ei klapi mitte.", "pass_phrase_match_success": "Klapib!", "phrase_strong_enough": "Suurepärane! Turvafraas on piisavalt kange.", - "requires_key_restore": "Krüptimine uuendamiseks taasta oma varundatud võtmed", - "requires_password_confirmation": "Kinnitamaks seda muudatust, sisesta oma konto salasõna:", - "requires_server_authentication": "Uuenduse kinnitamiseks pead end autentima serveris.", "secret_storage_query_failure": "Ei õnnestu tuvastada turvahoidla olekut", "security_key_safety_reminder": "Kuna seda kasutatakse sinu krüptitud andmete kaitsmiseks, siis hoia oma turvavõtit kaitstud ja turvalises kohas, nagu näiteks arvutis salasõnade halduris või vana kooli seifis.", - "session_upgrade_description": "Teiste sessioonide verifitseerimiseks pead uuendama seda sessiooni. Muud verifitseeritud sessioonid saavad sellega ligipääsu krüptitud sõnumitele ning nad märgitakse usaldusväärseteks ka teiste kasutajate jaoks.", "set_phrase_again": "Mine tagasi ja sisesta nad uuesti.", "settings_reminder": "Samuti võid sa seadetes võtta kasutusse turvalise varunduse ning hallata oma krüptovõtmeid.", "title_confirm_phrase": "Kinnita turvafraas", "title_save_key": "Salvesta turvavõti", "title_set_phrase": "Määra turvafraas", - "title_upgrade_encryption": "Uuenda oma krüptimist", "unable_to_setup": "Turvahoidla kasutuselevõtmine ei õnnestu", "use_different_passphrase": "Kas kasutame muud paroolifraasi?", "use_phrase_only_you_know": "Sisesta turvafraas, mida vaid sina tead ning lisaks võid salvestada varunduse turvavõtme." @@ -3475,7 +3464,6 @@ "truncated_list_n_more": { "other": "Ja %(count)s muud..." }, - "unknown_device": "Tundmatu seade", "unsupported_server_description": "See server kasutab Matrixi vanemat versiooni. Selleks, et %(brand)s'i kasutamisel vigu ei tekiks palun uuenda serverit nii, et kasutusel oleks Matrixi %(version)s.", "unsupported_server_title": "Sinu server ei ole toetatud", "update": { diff --git a/src/i18n/strings/fa.json b/src/i18n/strings/fa.json index cddf37c691e..5541bbbfbd0 100644 --- a/src/i18n/strings/fa.json +++ b/src/i18n/strings/fa.json @@ -81,7 +81,6 @@ "report_content": "گزارش محتوا", "resend": "بازفرست", "reset": "بازراه‌اندازی", - "restore": "بازیابی", "resume": "ادامه", "retry": "تلاش مجدد", "review": "بازبینی", @@ -166,7 +165,6 @@ "failed_query_registration_methods": "درخواست از روش‌های پشتیبانی‌شده‌ی ثبت‌نام میسر نیست.", "failed_soft_logout_auth": "احراز هویت مجدد موفیت‌آمیز نبود", "failed_soft_logout_homeserver": "به دلیل مشکلی که در سرور وجود دارد ، احراز هویت مجدد انجام نشد", - "footer_powered_by_matrix": "قدرت‌یافته از ماتریکس", "forgot_password_email_required": "آدرس ایمیلی که به حساب کاربری شما متصل است، باید وارد شود.", "forgot_password_prompt": "گذرواژه‌ی خود را فراموش کردید؟", "identifier_label": "نحوه ورود", @@ -641,7 +639,6 @@ }, "unable_to_setup_keys_error": "تنظیم کلیدها امکان پذیر نیست", "unsupported": "این کلاینت از رمزگذاری سرتاسر پشتیبانی نمی کند.", - "upgrade_toast_title": "ارتقای رمزنگاری ممکن است", "verification": { "accepting": "پذیرش…", "cancelled": "شما تأیید هویت را لغو کردید.", @@ -1563,17 +1560,12 @@ "pass_phrase_match_failed": "مطابقت ندارد.", "pass_phrase_match_success": "مطابقت دارد!", "phrase_strong_enough": "عالی! این عبارت امنیتی به اندازه کافی قوی به نظر می رسد.", - "requires_key_restore": "برای ارتقاء رمزنگاری، ابتدا نسخه‌ی پشتیبان خود را بازیابی کنید", - "requires_password_confirmation": "گذرواژه‌ی خود را جهت تائيد عملیات ارتقاء وارد کنید:", - "requires_server_authentication": "برای تائید ارتقاء، نیاز به احراز هویت نزد سرور خواهید داشت.", "secret_storage_query_failure": "امکان جستجو و کنکاش وضعیت حافظه‌ی مخفی میسر نیست", - "session_upgrade_description": "برای اینکه بتوانید بقیه‌ی نشست‌ها را تائید کرده و به آن‌ها امکان مشاهده‌ی پیام‌های رمزشده را بدهید، ابتدا باید این نشست را ارتقاء دهید. بعد از تائیدشدن، به عنوان نشست‌ّای تائید‌شده به سایر کاربران نمایش داده خواهند شد.", "set_phrase_again": "برای تنظیم مجدد آن به عقب برگردید.", "settings_reminder": "همچنین می‌توانید پشتیبان‌گیری امن را برپا کرده و کلید‌های خود را در تنظیمات مدیریت کنید.", "title_confirm_phrase": "عبارت امنیتی را تأیید کنید", "title_save_key": "کلید امنیتی خود را ذخیره کنید", "title_set_phrase": "یک عبارت امنیتی تنظیم کنید", - "title_upgrade_encryption": "رمزنگاری خود را ارتقا دهید", "unable_to_setup": "تنظیم حافظه‌ی پنهان امکان پذیر نیست", "use_different_passphrase": "از عبارت امنیتی دیگری استفاده شود؟", "use_phrase_only_you_know": "از یک عبارت محرمانه که فقط خودتان می‌دانید استفاده کنید، و محض احتیاط کلید امینی خود را برای استفاده هنگام پشتیبان‌گیری ذخیره نمائید." @@ -2237,7 +2229,6 @@ "truncated_list_n_more": { "other": "و %(count)s مورد بیشتر ..." }, - "unknown_device": "دستگاه ناشناخته", "update": { "changelog": "تغییراتِ به‌وجودآمده", "check_action": "بررسی برای به‌روزرسانی جدید", diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index acb77b4f8dd..091761af4b8 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -93,7 +93,6 @@ "report_content": "Ilmoita sisällöstä", "resend": "Lähetä uudelleen", "reset": "Palauta alkutilaan", - "restore": "Palauta", "resume": "Jatka", "retry": "Yritä uudelleen", "review": "Katselmoi", @@ -195,7 +194,6 @@ "failed_query_registration_methods": "Tuettuja rekisteröitymistapoja ei voitu kysellä.", "failed_soft_logout_auth": "Uudelleenautentikointi epäonnistui", "failed_soft_logout_homeserver": "Uudelleenautentikointi epäonnistui kotipalvelinongelmasta johtuen", - "footer_powered_by_matrix": "moottorina Matrix", "forgot_password_email_invalid": "Sähköpostiosoite ei vaikuta kelvolliselta.", "forgot_password_email_required": "Sinun pitää syöttää tiliisi liitetty sähköpostiosoite.", "forgot_password_prompt": "Unohditko salasanasi?", @@ -233,7 +231,6 @@ "qr_code_login": { "error_rate_limited": "Liikaa yrityksiä lyhyessä ajassa. Odota hetki, ennen kuin yrität uudelleen.", "error_unexpected": "Tapahtui odottamaton virhe.", - "sign_in_new_device": "Kirjaa sisään uusi laite", "waiting_for_device": "Odotetaan laitteen sisäänkirjautumista" }, "register_action": "Luo tili", @@ -789,7 +786,6 @@ "title": "Ei luotettu" }, "unsupported": "Tämä asiakasohjelma ei tue päästä päähän -salausta.", - "upgrade_toast_title": "Salauksen päivitys saatavilla", "verification": { "accepting": "Hyväksytään…", "after_new_login": { @@ -2138,7 +2134,6 @@ "pass_phrase_match_failed": "Ei täsmää.", "pass_phrase_match_success": "Täsmää!", "phrase_strong_enough": "Hienoa! Tämä turvalause vaikuttaa riittävän vahvalta.", - "requires_password_confirmation": "Syötä tilisi salasana vahvistaaksesi päivityksen:", "secret_storage_query_failure": "Salaisen tallennustilan tilaa ei voi kysellä", "security_key_safety_reminder": "Talleta turva-avaimesi turvalliseen paikkaan, kuten salasanojen hallintasovellukseen tai kassakaappiin, sillä sitä käytetään salaamasi datan suojaamiseen.", "set_phrase_again": "Palaa asettamaan se uudelleen.", @@ -2146,7 +2141,6 @@ "title_confirm_phrase": "Vahvista turvalause", "title_save_key": "Tallenna turva-avain", "title_set_phrase": "Aseta turvalause", - "title_upgrade_encryption": "Päivitä salauksesi", "unable_to_setup": "Salavaraston käyttöönotto epäonnistui", "use_different_passphrase": "Käytä eri salalausetta?" } @@ -3105,7 +3099,6 @@ "truncated_list_n_more": { "other": "Ja %(count)s muuta..." }, - "unknown_device": "Tuntematon laite", "update": { "changelog": "Muutosloki", "check_action": "Tarkista päivitykset", diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index bb79008a834..7d209a8a45b 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -103,7 +103,6 @@ "report_content": "Signaler le contenu", "resend": "Renvoyer", "reset": "Réinitialiser", - "restore": "Restaurer", "resume": "Reprendre", "retry": "Réessayer", "review": "Examiner", @@ -210,7 +209,6 @@ "failed_query_registration_methods": "Impossible de demander les méthodes d’inscription prises en charge.", "failed_soft_logout_auth": "Échec de la ré-authentification", "failed_soft_logout_homeserver": "Échec de la ré-authentification à cause d’un problème du serveur d’accueil", - "footer_powered_by_matrix": "propulsé par Matrix", "forgot_password_email_invalid": "L’adresse e-mail semble être invalide.", "forgot_password_email_required": "L’adresse e-mail liée à votre compte doit être renseignée.", "forgot_password_prompt": "Mot de passe oublié ?", @@ -249,13 +247,11 @@ "phone_label": "Numéro de téléphone", "phone_optional_label": "Téléphone (facultatif)", "qr_code_login": { - "approve_access_warning": "En autorisant l’accès pour cet appareil, il aura un accès complet à votre compte.", "check_code_explainer": "Cela vérifiera que la connexion à votre autre appareil est sécurisée.", "check_code_heading": "Entrez le numéro affiché sur votre autre appareil", "check_code_input_label": "code à 2 chiffres", "check_code_mismatch": "Les chiffres ne correspondent pas", "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 :", "error_etag_missing": "Une erreur inattendue s'est produite. Cela peut être dû à une extension de navigateur, à un serveur proxy ou à une mauvaise configuration du serveur.", "error_expired": "Connexion expirée. Veuillez réessayer.", "error_expired_title": "La connexion a pris trop de temps.", @@ -283,7 +279,6 @@ "security_code": "Code de sécurité", "security_code_prompt": "Si vous y êtes invité, saisissez le code ci-dessous sur votre autre appareil.", "select_qr_code": "Sélectionnez « %(scanQRCode)s »", - "sign_in_new_device": "Connecter le nouvel appareil", "waiting_for_device": "En attente de connexion de l’appareil" }, "register_action": "Créer un compte", @@ -928,7 +923,6 @@ }, "unable_to_setup_keys_error": "Impossible de configurer les clés", "unsupported": "Ce client ne prend pas en charge le chiffrement de bout en bout.", - "upgrade_toast_title": "Mise à niveau du chiffrement disponible", "verification": { "accepting": "Acceptation…", "after_new_login": { @@ -1598,6 +1592,7 @@ "keyword": "Mot-clé", "keyword_new": "Nouveau mot-clé", "level_activity": "Activité", + "level_highlight": "Surligner", "level_muted": "Muet", "level_none": "Aucun", "level_notification": "Notification", @@ -1629,7 +1624,7 @@ "download_f_droid": "Récupérez-le sur F-Droid", "download_google_play": "Récupérez-le sur Google Play", "enable_notifications": "Activer les notifications", - "enable_notifications_action": "Activer les notifications", + "enable_notifications_action": "Ouvrir les paramètres", "enable_notifications_description": "Ne ratez pas une réponse ou un message important", "explore_rooms": "Explorez les salons publics", "find_community_members": "Trouvez et invitez les membres de votre communauté", @@ -1808,14 +1803,27 @@ "restore_failed_error": "Impossible de restaurer la sauvegarde" }, "right_panel": { - "add_integrations": "Ajouter des widgets, passerelles et robots", + "add_integrations": "Ajouter des extensions", + "add_topic": "Ajouter un sujet", "files_button": "Fichiers", "pinned_messages": { + "empty_title": "Épingler des messages importants afin qu'ils puissent être facilement découverts", + "header": { + "one": "1 Message épinglé", + "other": "%(count)s Messages épinglés" + }, "limits": { "other": "Vous ne pouvez épingler que jusqu’à %(count)s widgets" + }, + "menu": "Ouvrir le menu", + "release_announcement": { + "title": "Tous les nouveaux messages épinglés" + }, + "unpin_all": { + "button": "Désépingler tous les messages" } }, - "pinned_messages_button": "Épinglé", + "pinned_messages_button": "Messages épinglés", "poll": { "active_heading": "Sondages en cours", "empty_active": "Il n’y a aucun sondage en cours dans ce salon", @@ -1840,7 +1848,7 @@ "view_in_timeline": "Consulter la chronologie des sondages", "view_poll": "Voir le sondage" }, - "polls_button": "Historique des sondages", + "polls_button": "Sondages", "room_summary_card": { "title": "Information du salon" }, @@ -1912,8 +1920,13 @@ "forget_room": "Oublier ce salon", "forget_space": "Oublier cet espace", "header": { + "n_people_asking_to_join": { + "one": "Demander à rejoindre", + "other": "%(count)s personnes demandant à se joindre" + }, "room_is_public": "Ce salon est public" }, + "header_face_pile_tooltip": "Personnes", "header_untrusted_label": "Non fiable", "inaccessible": "Ce salon ou cet espace n’est pas accessible en ce moment.", "inaccessible_name": "%(roomName)s n’est pas joignable pour le moment.", @@ -1983,11 +1996,15 @@ "not_found_title": "Ce salon ou cet espace n’existe pas.", "not_found_title_name": "%(roomName)s n’existe pas.", "peek_join_prompt": "Ceci est un aperçu de %(roomName)s. Voulez-vous rejoindre le salon ?", + "pinned_message_banner": { + "description": "Ce salon contient des messages épinglés. Cliquez pour les consulter." + }, "read_topic": "Cliquer pour lire le sujet", "rejecting": "Rejet de l’invitation…", "rejoin_button": "Revenir", "search": { "all_rooms_button": "Rechercher dans tous les salons", + "placeholder": "Rechercher des messages…", "this_room_button": "Rechercher dans ce salon" }, "status_bar": { @@ -2350,6 +2367,7 @@ "brand_version": "Version de %(brand)s :", "clear_cache_reload": "Vider le cache et recharger", "crypto_version": "Version crypto :", + "dialog_title": "Paramètres : Aide et À propos", "help_link": "Pour obtenir de l’aide sur l’utilisation de %(brand)s, cliquez ici.", "homeserver": "Le serveur d’accueil est %(homeserverUrl)s", "identity_server": "Le serveur d’identité est %(identityServerUrl)s", @@ -2369,7 +2387,9 @@ "custom_font_size": "Utiliser une taille personnalisée", "custom_theme_error_downloading": "Erreur lors du téléchargement du thème", "custom_theme_invalid": "Schéma du thème invalide.", + "dialog_title": "Paramètres : Apparence", "font_size": "Taille de la police", + "font_size_default": "%(fontSize)s(par défaut)", "high_contrast": "Contraste élevé", "image_size_default": "Par défaut", "image_size_large": "Grande", @@ -2400,6 +2420,10 @@ "add_msisdn_dialog_title": "Ajouter un numéro de téléphone", "add_msisdn_instructions": "Un SMS a été envoyé à +%(msisdn)s. Saisissez le code de vérification qu’il contient.", "add_msisdn_misconfigured": "L’ajout / liaison avec le flux MSISDN est mal configuré", + "application_language_reload_hint": "L’application se rechargera après avoir sélectionné une autre langue", + "avatar_remove_progress": "Suppression de l'image...", + "avatar_save_progress": "Chargement de l'image...", + "avatar_upload_error_text_generic": "Le format de fichier n'est peut-être pas pris en charge.", "confirm_adding_email_body": "Cliquez sur le bouton ci-dessous pour confirmer l’ajout de l’adresse e-mail.", "confirm_adding_email_title": "Confirmer l’ajout de l’adresse e-mail", "deactivate_confirm_body": "Voulez-vous vraiment désactiver votre compte ? Ceci est irréversible.", @@ -2419,6 +2443,8 @@ "discovery_email_verification_instructions": "Vérifiez le lien dans votre boîte de réception", "discovery_msisdn_empty": "Les options de découverte apparaîtront quand vous aurez ajouté un numéro de téléphone ci-dessus.", "discovery_needs_terms": "Acceptez les conditions de service du serveur d’identité (%(serverName)s) pour vous permettre d’être découvrable par votre adresse e-mail ou votre numéro de téléphone.", + "display_name": "Nom d'affichage", + "display_name_error": "Impossible de définir le nom d'affichage", "email_address_in_use": "Cette adresse e-mail est déjà utilisée", "email_address_label": "Adresse e-mail", "email_not_verified": "Votre adresse e-mail n’a pas encore été vérifiée", @@ -2443,7 +2469,7 @@ "error_share_msisdn_discovery": "Impossible de partager le numéro de téléphone", "identity_server_no_token": "Aucun jeton d’accès d’identité trouvé", "identity_server_not_set": "Serveur d'identité non défini", - "language_section": "Langue et région", + "language_section": "Langue", "msisdn_in_use": "Ce numéro de téléphone est déjà utilisé", "msisdn_label": "Numéro de téléphone", "msisdn_verification_field_label": "Code de vérification", @@ -2452,9 +2478,12 @@ "oidc_manage_button": "Gérer le compte", "password_change_section": "Définir un nouveau mot de passe de compte…", "password_change_success": "Votre mot de passe a été mis à jour.", + "personal_info": "Informations personnelles", + "profile_subtitle_oidc": "Votre compte est géré séparément par un fournisseur d'identité et certaines de vos informations personnelles ne peuvent donc pas être modifiées ici.", "remove_email_prompt": "Supprimer %(email)s ?", "remove_msisdn_prompt": "Supprimer %(phone)s ?", - "spell_check_locale_placeholder": "Choisir une langue" + "spell_check_locale_placeholder": "Choisir une langue", + "username": "Nom d’utilisateur" }, "image_thumbnails": "Afficher les aperçus/vignettes pour les images", "inline_url_previews_default": "Activer l’aperçu des URL par défaut", @@ -2483,18 +2512,13 @@ "pass_phrase_match_failed": "Ça ne correspond pas.", "pass_phrase_match_success": "Ça correspond !", "phrase_strong_enough": "Super ! Cette phrase secrète a l’air assez solide.", - "requires_key_restore": "Restaurez votre sauvegarde de clés pour mettre à niveau votre chiffrement", - "requires_password_confirmation": "Saisissez le mot de passe de votre compte pour confirmer la mise à niveau :", - "requires_server_authentication": "Vous devrez vous identifier avec le serveur pour confirmer la mise à niveau.", "secret_storage_query_failure": "Impossible de demander le statut du coffre secret", "security_key_safety_reminder": "Stockez votre clé de sécurité dans un endroit sûr, comme un gestionnaire de mots de passe ou un coffre, car elle est utilisée pour protéger vos données chiffrées.", - "session_upgrade_description": "Mettez à niveau cette session pour l’autoriser à vérifier d’autres sessions, ce qui leur permettra d’accéder aux messages chiffrés et de les marquer comme fiables pour les autres utilisateurs.", "set_phrase_again": "Retournez en arrière pour la redéfinir.", "settings_reminder": "Vous pouvez aussi configurer la sauvegarde sécurisée et gérer vos clés depuis les paramètres.", "title_confirm_phrase": "Confirmer la phrase de sécurité", "title_save_key": "Sauvegarder votre clé de sécurité", "title_set_phrase": "Définir une phrase de sécurité", - "title_upgrade_encryption": "Mettre à niveau votre chiffrement", "unable_to_setup": "Impossible de configurer le coffre secret", "use_different_passphrase": "Utiliser une phrase secrète différente ?", "use_phrase_only_you_know": "Utilisez une phrase secrète que vous êtes seul à connaître et enregistrez éventuellement une clé de sécurité à utiliser pour la sauvegarde." @@ -2515,12 +2539,20 @@ "phrase_strong_enough": "Super ! Cette phrase secrète a l’air assez robuste" }, "keyboard": { + "dialog_title": "Paramètres : Clavier", "title": "Clavier" }, + "labs": { + "dialog_title": "Paramètres : Expérimental" + }, + "labs_mjolnir": { + "dialog_title": "Paramètres : Utilisateurs ignorés" + }, "notifications": { "default_setting_description": "Ce réglage sera appliqué par défaut à tous vos salons.", "default_setting_section": "Je veux être notifié pour (réglage par défaut)", "desktop_notification_message_preview": "Afficher l’aperçu du message dans la notification de bureau", + "dialog_title": "Paramètres : Notifications", "email_description": "Recevoir un résumé par courriel des notifications manquées", "email_section": "Résumé en courriel", "email_select": "Sélectionner les adresses auxquelles envoyer les résumés. Gérer vos courriels dans .", @@ -2579,6 +2611,7 @@ "code_blocks_heading": "Blocs de code", "compact_modern": "Utiliser une mise en page « moderne » plus compacte", "composer_heading": "Compositeur", + "dialog_title": "Paramètres : Préférences", "enable_hardware_acceleration": "Activer l’accélération matérielle", "enable_tray_icon": "Afficher l’icône dans la barre d’état et minimiser la fenêtre lors de la fermeture", "keyboard_heading": "Raccourcis clavier", @@ -2624,8 +2657,11 @@ "cross_signing_self_signing_private_key": "Clé privée d’auto-signature :", "cross_signing_user_signing_private_key": "Clé privée de signature de l’utilisateur :", "cryptography_section": "Chiffrement", + "dehydrated_device_description": "La fonctionnalité d’appareil hors ligne vous permet de recevoir des messages chiffrés même lorsque vous n’êtes connecté à aucun appareil", + "dehydrated_device_enabled": "Appareil hors ligne activé", "delete_backup": "Supprimer la sauvegarde", "delete_backup_confirm_description": "En êtes-vous sûr ? Vous perdrez vos messages chiffrés si vos clés ne sont pas sauvegardées correctement.", + "dialog_title": "Paramètres : Sécurité et confidentialité", "e2ee_default_disabled_warning": "L’administrateur de votre serveur a désactivé le chiffrement de bout en bout par défaut dans les salons privés et les conversations privées.", "enable_message_search": "Activer la recherche de messages dans les salons chiffrés", "encryption_section": "Chiffrement", @@ -2703,6 +2739,7 @@ "device_unverified_description_current": "Vérifiez cette session pour renforcer la sécurité de votre messagerie.", "device_verified_description": "Cette session est prête pour l’envoi de messages sécurisés.", "device_verified_description_current": "Votre session actuelle est prête pour une messagerie sécurisée.", + "dialog_title": "Paramètres : Sessions", "error_pusher_state": "Échec lors de la définition de l’état push", "error_set_name": "Impossible d'enregistrer le nom de la session", "filter_all": "Tout", @@ -2744,7 +2781,7 @@ "show_details": "Afficher les détails", "sign_in_with_qr": "Associer un nouvel appareil", "sign_in_with_qr_button": "Afficher le QR code", - "sign_in_with_qr_description": "Vous pouvez utiliser cet appareil pour vous connecter sur un autre appareil avec un QR code. Vous devrez scanner le QR code affiché sur cet appareil avec votre autre appareil qui n’est pas connecté.", + "sign_in_with_qr_description": "Utilisez un code QR pour vous connecter à un autre appareil et configurer votre messagerie sécurisée.", "sign_out": "Se déconnecter de cette session", "sign_out_all_other_sessions": "Déconnecter toutes les autres sessions (%(otherSessionsCount)s)", "sign_out_confirm_description": { @@ -2786,6 +2823,7 @@ "show_typing_notifications": "Afficher les notifications de saisie", "showbold": "Afficher toute l'activité dans la liste des salons (points ou nombre de messages non lus)", "sidebar": { + "dialog_title": "Paramètres : Barre latérale", "metaspaces_favourites_description": "Regroupez tous vos salons et personnes préférés au même endroit.", "metaspaces_home_all_rooms": "Afficher tous les salons", "metaspaces_home_all_rooms_description": "Affiche tous vos salons dans l’accueil, même s’ils font partis d’un espace.", @@ -2794,6 +2832,9 @@ "metaspaces_orphans_description": "Regroupe tous les salons n’appartenant pas à un espace au même endroit.", "metaspaces_people_description": "Regrouper toutes vos connaissances au même endroit.", "metaspaces_subsection": "Espaces à afficher", + "metaspaces_video_rooms": "Salons vidéo et conférences", + "metaspaces_video_rooms_description": "Regroupez tous les salons vidéo et conférences.", + "metaspaces_video_rooms_description_invite_extension": "Lors des conférences, vous pouvez inviter des personnes extérieures à Matrix.", "spaces_explainer": "Les espaces sont un nouveau moyen de regrouper les salons et les gens. En plus des espaces auxquels vous participez, vous pouvez également utiliser ceux qui sont prédéfinis.", "title": "Barre latérale" }, @@ -2812,6 +2853,7 @@ "audio_output_empty": "Aucune sortie audio détectée", "auto_gain_control": "Contrôle automatique du gain", "connection_section": "Connexion", + "dialog_title": "Paramètres : Audio et vidéo", "echo_cancellation": "Annulation d’écho", "enable_fallback_ice_server": "Autoriser le serveur de secours d’assistance d’appel (%(server)s)", "enable_fallback_ice_server_description": "Concerne seulement les serveurs d’accueil qui n’en proposent pas. Votre adresse IP pourrait être diffusée pendant un appel.", @@ -2833,6 +2875,9 @@ "link_title": "Lien vers le salon", "permalink_message": "Lien vers le message sélectionné", "permalink_most_recent": "Lien vers le message le plus récent", + "share_call": "Lien d'invitation à la conférence", + "share_call_subtitle": "Lien permettant aux utilisateurs externes de rejoindre l'appel sans compte Matrix :", + "title_link": "Lien de partage", "title_message": "Partager le message du salon", "title_room": "Partager le salon", "title_user": "Partager l’utilisateur" @@ -2858,6 +2903,7 @@ "devtools": "Ouvre la fenêtre des outils de développeur", "discardsession": "Force la session de groupe sortante actuelle dans un salon chiffré à être rejetée", "error_invalid_rendering_type": "Erreur de commande : Impossible de trouver le type de rendu (%(renderingType)s)", + "error_invalid_room": "Échec de la commande : Impossible de trouver le salon (%(roomId)s)", "error_invalid_runfn": "Erreur de commande : Impossible de gérer la commande de barre oblique.", "error_invalid_user_in_room": "Impossible de trouver l’utilisateur dans le salon", "help": "Affiche la liste des commandes avec leurs utilisations et descriptions", @@ -3035,7 +3081,7 @@ "keyboard_scroll_hint": "Utilisez pour faire défiler", "message_search_section_title": "Autres recherches", "other_rooms_in_space": "Autres salons dans %(spaceName)s", - "public_rooms_label": "Salons public", + "public_rooms_label": "Salons publics", "public_spaces_label": "Espaces publics", "recent_searches_section_title": "Recherches récentes", "recently_viewed_section_title": "Affiché récemment", @@ -3080,6 +3126,8 @@ "one": "%(count)s réponse", "other": "%(count)s réponses" }, + "empty_description": "Utiliser \"%(replyInThread)s\" lorsque vous survolez un message.", + "empty_title": "Les fils de discussion aident à garder vos conversations sur le sujet et à les suivre facilement.", "error_start_thread_existing_relation": "Impossible de créer un fil de discussion à partir d’un événement avec une relation existante", "mark_all_read": "Tout marquer comme lu", "my_threads": "Mes fils de discussion", @@ -3090,6 +3138,8 @@ "threads_activity_centre": { "header": "Activité des fils de discussions", "no_rooms_with_threads_notifs": "Vous n’avez pas encore de salons avec des notifications de fil de discussion.", + "no_rooms_with_unread_threads": "Vous n'avez pas encore de salons contenant des fils de discussion non lus.", + "release_announcement_description": "Les notifications des fils de discussion ont été déplacées. À partir de maintenant, retrouvez-les ici.", "release_announcement_header": "Centre d'activité des fils de discussions" }, "time": { @@ -3128,18 +3178,23 @@ "report": "Signaler", "resent_unsent_reactions": "Renvoyer %(unsentCount)s réaction(s)", "show_url_preview": "Afficher l’aperçu", - "view_related_event": "Afficher les événements liés", + "view_related_event": "Voir l’événement associé", "view_source": "Afficher la source" }, "creation_summary_dm": "%(creator)s a créé cette conversation privée.", "creation_summary_room": "%(creator)s a créé et configuré le salon.", "decryption_failure": { + "blocked": "L'expéditeur vous a empêché de recevoir ce message car votre appareil n'est pas vérifié", "historical_event_no_key_backup": "L'historique des messages n'est pas disponible sur cet appareil", - "historical_event_user_not_joined": "Vous n'avez pas accès à ce message" + "historical_event_unverified_device": "Vous devez vérifier cet appareil pour accéder à l'historique des messages", + "historical_event_user_not_joined": "Vous n'avez pas accès à ce message", + "unable_to_decrypt": "Impossible de déchiffrer le message" }, "disambiguated_profile": "%(displayName)s (%(matrixId)s)", "download_action_decrypting": "Déchiffrement", "download_action_downloading": "Téléchargement en cours", + "download_failed": "Échec du téléchargement", + "download_failed_description": "Une erreur s'est produite lors du téléchargement de ce fichier", "edits": { "tooltip_label": "Modifié le %(date)s. Cliquer pour voir les modifications.", "tooltip_sub": "Cliquez pour voir les modifications", @@ -3154,6 +3209,7 @@ "you": "Vous avez terminé une diffusion audio" }, "io.element.widgets.layout": "%(senderName)s a mis à jour la mise en page du salon", + "late_event_separator": "Initialement envoyé%(dateTime)s", "load_error": { "no_permission": "Un instant donné du fil de discussion n’a pu être chargé car vous n’avez pas la permission de le visualiser.", "title": "Échec du chargement de la position dans le fil de discussion", @@ -3196,7 +3252,7 @@ }, "m.file": { "error_decrypting": "Erreur lors du déchiffrement de la pièce jointe", - "error_invalid": "Fichier %(extra)s non valide" + "error_invalid": "Fichier invalide" }, "m.image": { "error": "Impossible d’afficher l’image à cause d’une erreur", @@ -3568,7 +3624,6 @@ "truncated_list_n_more": { "other": "Et %(count)s autres…" }, - "unknown_device": "Appareil inconnu", "unsupported_server_description": "Ce serveur utilise une ancienne version de Matrix. Mettez-le à jour vers Matrix %(version)s pour utiliser %(brand)s sans erreurs.", "unsupported_server_title": "Votre serveur n’est pas pris en charge", "update": { @@ -3586,6 +3641,12 @@ "toast_title": "Mettre à jour %(brand)s", "unavailable": "Indisponible" }, + "update_room_access_modal": { + "description": "Pour créer un lien de partage, vous devez autoriser les invités à rejoindre ce salon. Cela peut rendre le salon moins sûr. Lorsque vous aurez terminé l'appel, vous pourrez redéfinir la confidentialité du salon.", + "dont_change_description": "Vous pouvez également prendre l'appel dans un salon séparé.", + "no_change": "Je ne souhaite pas modifier le niveau d'accès.", + "title": "Modifier le niveau d'accès du salon" + }, "upload_failed_generic": "Le fichier « %(fileName)s » n’a pas pu être envoyé.", "upload_failed_size": "Le fichier « %(fileName)s » dépasse la taille limite autorisée par ce serveur pour les envois", "upload_failed_title": "Échec de l’envoi", @@ -3664,13 +3725,13 @@ "no_recent_messages_description": "Essayez de faire défiler le fil de discussion vers le haut pour voir s’il y en a de plus anciens.", "no_recent_messages_title": "Aucun message récent de %(user)s n’a été trouvé" }, - "redact_button": "Supprimer les messages récents", + "redact_button": "Supprimer des messages", "revoke_invite": "Révoquer l’invitation", "room_encrypted": "Les messages dans ce salon sont chiffrés de bout en bout.", "room_encrypted_detail": "Vos messages sont sécurisés et seuls vous et le destinataire avez les clés uniques pour les déchiffrer.", "room_unencrypted": "Les messages dans ce salon ne sont pas chiffrés de bout en bout.", "room_unencrypted_detail": "Dans les salons chiffrés, vos messages sont sécurisés et seuls vous et le destinataire avez les clés uniques pour les déchiffrer.", - "share_button": "Partager le lien vers l’utilisateur", + "share_button": "Partager le profil", "unban_button_room": "Révoquer le bannissement du salon", "unban_button_space": "Révoquer le bannissement de l’espace", "unban_room_confirm_title": "Annuler le bannissement de %(roomName)s", @@ -3681,6 +3742,7 @@ "verify_explainer": "Pour une sécurité supplémentaire, vérifiez cet utilisateur en comparant un code à usage unique sur vos deux appareils." }, "user_menu": { + "link_new_device": "Associer un nouvel appareil", "settings": "Tous les paramètres", "switch_theme_dark": "Passer au mode sombre", "switch_theme_light": "Passer au mode clair" @@ -3737,6 +3799,7 @@ "camera_enabled": "Votre caméra est toujours allumée", "cannot_call_yourself_description": "Vous ne pouvez pas passer d’appel avec vous-même.", "change_input_device": "Change de périphérique d’entrée", + "close_lobby": "Fermer la salle d'attente", "connecting": "Connexion", "connection_lost": "La connexion au serveur a été perdue", "connection_lost_description": "Vous ne pouvez pas passer d’appels sans connexion au serveur.", @@ -3750,18 +3813,24 @@ "disabled_no_perms_start_video_call": "Vous n’avez pas la permission de démarrer un appel vidéo", "disabled_no_perms_start_voice_call": "Vous n’avez pas la permission de démarrer un appel audio", "disabled_ongoing_call": "Appel en cours", + "element_call": "Element Call", "enable_camera": "Activer la caméra", "enable_microphone": "Activer le microphone", "expand": "Revenir à l’appel", "failed_call_live_broadcast_description": "Vous ne pouvez pas démarrer un appel car vous êtes en train d’enregistrer une diffusion en direct. Veuillez terminer cette diffusion pour démarrer un appel.", "failed_call_live_broadcast_title": "Impossible de démarrer un appel", + "get_call_link": "Partager le lien de l'appel", "hangup": "Raccrocher", "hide_sidebar_button": "Masquer la barre latérale", "input_devices": "Périphériques d’entrée", + "jitsi_call": "Conférence Jitsi", "join_button_tooltip_call_full": "Désolé — Cet appel est actuellement complet", "join_button_tooltip_connecting": "Connexion", "maximise": "Remplir l’écran", "maximise_call": "Plein écran", + "metaspace_video_rooms": { + "conference_room_section": "Conférences" + }, "minimise_call": "Quitter le mode plein écran", "misconfigured_server": "L’appel a échoué à cause d’un serveur mal configuré", "misconfigured_server_description": "Demandez à l’administrateur de votre serveur d’accueil (%(homeserverDomain)s) de configurer un serveur TURN afin que les appels fonctionnent de manière fiable.", @@ -3810,6 +3879,7 @@ "user_is_presenting": "%(sharerName)s est à l’écran", "video_call": "Appel vidéo", "video_call_started": "Appel vidéo commencé", + "video_call_using": "Appel vidéo utilisant :", "voice_call": "Appel audio", "you_are_presenting": "Vous êtes à l’écran" }, @@ -3918,7 +3988,7 @@ "title": "Autoriser ce widget à vérifier votre identité" }, "popout": "Détacher le widget", - "set_room_layout": "Définir ma disposition de salon pour tout le monde", + "set_room_layout": "Définir la mise en page pour tout le monde", "shared_data_avatar": "Votre URL d’image de profil", "shared_data_device_id": "Votre ID d’appareil", "shared_data_lang": "Votre langue", diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index c1b2fe83e92..00277a5f3e4 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -91,7 +91,6 @@ "report_content": "Denunciar contido", "resend": "Volver a enviar", "reset": "Restablecer", - "restore": "Restablecer", "resume": "Retomar", "retry": "Reintentar", "review": "Revisar", @@ -185,7 +184,6 @@ "failed_query_registration_methods": "Non se puido consultar os métodos de rexistro soportados.", "failed_soft_logout_auth": "Fallo na reautenticación", "failed_soft_logout_homeserver": "Fallo ó reautenticar debido a un problema no servidor", - "footer_powered_by_matrix": "funciona grazas a Matrix", "forgot_password_email_invalid": "O enderezo de email non semella ser válido.", "forgot_password_email_required": "Debe introducir o correo electrónico ligado a súa conta.", "forgot_password_prompt": "¿Esqueceches o contrasinal?", @@ -760,7 +758,6 @@ }, "unable_to_setup_keys_error": "Non se puideron configurar as chaves", "unsupported": "Este cliente non soporta o cifrado extremo-a-extremo.", - "upgrade_toast_title": "Mellora do cifrado dispoñible", "verification": { "accepting": "Aceptando…", "after_new_login": { @@ -2083,18 +2080,13 @@ "pass_phrase_match_failed": "Non concorda.", "pass_phrase_match_success": "Concorda!", "phrase_strong_enough": "Ben! Esta Frase de Seguridade semella ser forte abondo.", - "requires_key_restore": "Restablece a copia das chaves para actualizar o cifrado", - "requires_password_confirmation": "Escribe o contrasinal para confirmar a actualización:", - "requires_server_authentication": "Debes autenticarte no servidor para confirmar a actualización.", "secret_storage_query_failure": "Non se obtivo o estado do almacenaxe segredo", "security_key_safety_reminder": "Garda a túa Chave de Seguridade nun lugar seguro, como un xestor de contrasinais ou caixa forte, xa que vai protexer os teus datos cifrados.", - "session_upgrade_description": "Actualiza esta sesión para permitirlle que verifique as outras sesións, outorgándolles acceso ás mensaxes cifradas e marcándoas como confiables para outras usuarias.", "set_phrase_again": "Vai atrás e volve a escribila.", "settings_reminder": "Podes configurar a Copia de apoio Segura e xestionar as chaves en Axustes.", "title_confirm_phrase": "Confirma a Frase de Seguridade", "title_save_key": "Garda a Chave de Seguridade", "title_set_phrase": "Establece a Frase de Seguridade", - "title_upgrade_encryption": "Mellora o teu cifrado", "unable_to_setup": "Non se configurou un almacenaxe segredo", "use_different_passphrase": "¿Usar unha frase de paso diferente?", "use_phrase_only_you_know": "Usa unha frase segreda que só ti coñezas, e de xeito optativo unha Chave de Seguridade para usar como apoio." @@ -3000,7 +2992,6 @@ "truncated_list_n_more": { "other": "E %(count)s máis..." }, - "unknown_device": "Dispositivo descoñecido", "update": { "changelog": "Rexistro de cambios", "check_action": "Comprobar actualización", diff --git a/src/i18n/strings/he.json b/src/i18n/strings/he.json index 85982203ce1..c98d59c6597 100644 --- a/src/i18n/strings/he.json +++ b/src/i18n/strings/he.json @@ -82,7 +82,6 @@ "report_content": "דווח על תוכן", "resend": "שלח מחדש", "reset": "אתחל", - "restore": "לשחזר", "resume": "תקציר", "retry": "נסה שוב", "review": "סקירה", @@ -168,7 +167,6 @@ "failed_query_registration_methods": "לא ניתן לשאול לשיטות רישום נתמכות.", "failed_soft_logout_auth": "האימות מחדש נכשל", "failed_soft_logout_homeserver": "האימות מחדש נכשל עקב בעיית שרת בית", - "footer_powered_by_matrix": "מופעל ע\"י Matrix", "forgot_password_email_required": "יש להזין את כתובת הדוא\"ל המקושרת לחשבונך.", "forgot_password_prompt": "שכחת את הסיסמה שלך?", "forgot_password_send_email": "שלח אימייל", @@ -644,7 +642,6 @@ }, "unable_to_setup_keys_error": "לא ניתן להגדיר מקשים", "unsupported": "לקוח זה אינו תומך בהצפנה מקצה לקצה.", - "upgrade_toast_title": "שדרוג הצפנה קיים", "verification": { "accepting": "מקבל…", "after_new_login": { @@ -1683,17 +1680,12 @@ "pass_phrase_match_failed": "זה לא תואם.", "pass_phrase_match_success": "זה מתאים!", "phrase_strong_enough": "מצוין! ביטוי אבטחה זה נראה מספיק חזק.", - "requires_key_restore": "שחזר את גיבוי המפתח שלך כדי לשדרג את ההצפנה שלך", - "requires_password_confirmation": "הזן את סיסמת החשבון שלך כדי לאשר את השדרוג:", - "requires_server_authentication": "יהיה עליך לבצע אימות מול השרת כדי לאשר את השדרוג.", "secret_storage_query_failure": "לא ניתן לשאול על סטטוס האחסון הסודי", - "session_upgrade_description": "שדרג את ההפעלה הזו כדי לאפשר לה לאמת פעילויות אחרות, הענק להם גישה להודעות מוצפנות וסמן אותן כאמינות עבור משתמשים אחרים.", "set_phrase_again": "חזור להגדיר אותו שוב.", "settings_reminder": "אתה יכול גם להגדיר גיבוי מאובטח ולנהל את המפתחות שלך בהגדרות.", "title_confirm_phrase": "אשר את ביטוי האבטחה", "title_save_key": "שמור את מפתח האבטחה שלך", "title_set_phrase": "הגדר ביטוי אבטחה", - "title_upgrade_encryption": "שדרג את ההצפנה שלך", "unable_to_setup": "לא ניתן להגדיר אחסון סודי", "use_different_passphrase": "להשתמש בביטוי סיסמה אחר?", "use_phrase_only_you_know": "השתמש בביטוי סודי רק אתה מכיר, ושמור שמור מפתח אבטחה לשימוש לגיבוי." @@ -2403,7 +2395,6 @@ "truncated_list_n_more": { "other": "ו%(count)s עוד..." }, - "unknown_device": "מכשיר לא ידוע", "update": { "changelog": "דו\"ח שינויים", "check_action": "בדוק עדכונים", diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 97e87f663a4..cf410fb82f3 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -98,7 +98,6 @@ "report_content": "Tartalom jelentése", "resend": "Újraküldés", "reset": "Visszaállítás", - "restore": "Visszaállítás", "resume": "Folytatás", "retry": "Újra", "review": "Átnézés", @@ -203,7 +202,6 @@ "failed_query_registration_methods": "A támogatott regisztrációs módokat nem lehet lekérdezni.", "failed_soft_logout_auth": "Újra bejelentkezés sikertelen", "failed_soft_logout_homeserver": "Az újbóli hitelesítés a Matrix-kiszolgáló hibájából sikertelen", - "footer_powered_by_matrix": "a gépházban: Matrix", "forgot_password_email_invalid": "Az e-mail cím nem tűnik érvényesnek.", "forgot_password_email_required": "A fiókodhoz kötött e-mail címet add meg.", "forgot_password_prompt": "Elfelejtetted a jelszavad?", @@ -241,14 +239,11 @@ "phone_label": "Telefon", "phone_optional_label": "Telefonszám (nem kötelező)", "qr_code_login": { - "approve_access_warning": "Ennek az eszköznek a hozzáférés engedélyezése után az eszköznek teljes hozzáférése lesz a fiókjához.", "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:", "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", "select_qr_code": "Kiválasztás „%(scanQRCode)s”", - "sign_in_new_device": "Új eszköz bejelentkeztetése", "waiting_for_device": "Várakozás a másik eszköz bejelentkezésére" }, "register_action": "Fiók létrehozása", @@ -886,7 +881,6 @@ }, "unable_to_setup_keys_error": "Nem sikerült a kulcsok beállítása", "unsupported": "A kliens nem támogatja a végponttól végpontig való titkosítást.", - "upgrade_toast_title": "A titkosítási fejlesztés elérhető", "verification": { "accepting": "Elfogadás…", "after_new_login": { @@ -2377,18 +2371,13 @@ "pass_phrase_match_failed": "Nem egyeznek.", "pass_phrase_match_success": "Egyeznek!", "phrase_strong_enough": "Nagyszerű! Ez a biztonsági jelmondat elég erősnek tűnik.", - "requires_key_restore": "A titkosítás fejlesztéséhez allítsd vissza a kulcs mentést", - "requires_password_confirmation": "A fejlesztés megerősítéséhez add meg a fiók jelszavadat:", - "requires_server_authentication": "A fejlesztés megerősítéséhez újból hitelesítenie kell a kiszolgálóval.", "secret_storage_query_failure": "A biztonsági tároló állapotát nem lehet lekérdezni", "security_key_safety_reminder": "A biztonsági kulcsot tárolja biztonságos helyen, például egy jelszókezelőben vagy egy széfben, mivel ez tartja biztonságban a titkosított adatait.", - "session_upgrade_description": "Fejleszd ezt a munkamenetet, hogy más munkameneteket is tudj vele hitelesíteni, azért, hogy azok hozzáférhessenek a titkosított üzenetekhez és megbízhatónak legyenek jelölve más felhasználók számára.", "set_phrase_again": "Lépj vissza és állítsd be újra.", "settings_reminder": "A biztonsági mentés beállítását és a kulcsok kezelését a Beállításokban is megadhatja.", "title_confirm_phrase": "Biztonsági jelmondat megerősítése", "title_save_key": "Mentse el a biztonsági kulcsát", "title_set_phrase": "Biztonsági Jelmondat beállítása", - "title_upgrade_encryption": "Titkosításod fejlesztése", "unable_to_setup": "A biztonsági tárolót nem sikerült beállítani", "use_different_passphrase": "Másik jelmondat használata?", "use_phrase_only_you_know": "Olyan biztonsági jelmondatot használjon, amelyet csak Ön ismer, és esetleg mentsen el egy biztonsági kulcsot vésztartaléknak." @@ -3446,7 +3435,6 @@ "truncated_list_n_more": { "other": "És még %(count)s..." }, - "unknown_device": "Ismeretlen eszköz", "unsupported_server_description": "Ez a kiszolgáló a Matrix régebbi verzióját használja. Frissítsen a Matrix %(version)s verzióra az %(brand)s hibamentes használatához.", "unsupported_server_title": "A kiszolgálója nem támogatott", "update": { diff --git a/src/i18n/strings/id.json b/src/i18n/strings/id.json index 4881ee45e90..77cdd8a78ff 100644 --- a/src/i18n/strings/id.json +++ b/src/i18n/strings/id.json @@ -98,7 +98,6 @@ "report_content": "Laporkan Konten", "resend": "Kirim Ulang", "reset": "Atur Ulang", - "restore": "Pulihkan", "resume": "Lanjutkan", "retry": "Coba Ulang", "review": "Lihat", @@ -203,7 +202,6 @@ "failed_query_registration_methods": "Tidak dapat menanyakan metode pendaftaran yang didukung.", "failed_soft_logout_auth": "Gagal untuk mengautentikasi ulang", "failed_soft_logout_homeserver": "Gagal untuk mengautentikasi ulang karena masalah homeserver", - "footer_powered_by_matrix": "diberdayakan oleh Matrix", "forgot_password_email_invalid": "Alamat email ini tidak terlihat absah.", "forgot_password_email_required": "Alamat email yang tertaut ke akun Anda harus dimasukkan.", "forgot_password_prompt": "Lupa kata sandi Anda?", @@ -241,14 +239,11 @@ "phone_label": "Ponsel", "phone_optional_label": "Nomor telepon (opsional)", "qr_code_login": { - "approve_access_warning": "Dengan menerima akses untuk perangkat ini, itu akan memiliki akses penuh ke akun Anda.", "completing_setup": "Menyelesaikan penyiapan perangkat baru Anda", - "confirm_code_match": "Periksa bahwa kode di bawah cocok dengan perangkat Anda 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", "select_qr_code": "Pilih '%(scanQRCode)s'", - "sign_in_new_device": "Masuk perangkat baru", "waiting_for_device": "Menunggu perangkat untuk masuk" }, "register_action": "Buat Akun", @@ -884,7 +879,6 @@ }, "unable_to_setup_keys_error": "Tidak dapat mengatur kunci-kunci", "unsupported": "Klien ini tidak mendukung enkripsi ujung ke ujung.", - "upgrade_toast_title": "Tersedia peningkatan enkripsi", "verification": { "accepting": "Menerima…", "after_new_login": { @@ -2409,18 +2403,13 @@ "pass_phrase_match_failed": "Itu tidak cocok.", "pass_phrase_match_success": "Mereka cocok!", "phrase_strong_enough": "Hebat! Frasa Keamanan ini kelihatannya kuat.", - "requires_key_restore": "Pulihkan cadangan kunci Anda untuk meningkatkan enkripsi Anda", - "requires_password_confirmation": "Masukkan kata sandi akun Anda untuk mengkonfirmasi peningkatannya:", - "requires_server_authentication": "Anda harus mengautentikasi dengan servernya untuk mengkonfirmasi peningkatannya.", "secret_storage_query_failure": "Tidak dapat menanyakan status penyimpanan rahasia", "security_key_safety_reminder": "Simpan Kunci Keamanan Anda di tempat yang aman, seperti manajer sandi atau sebuah brankas, yang digunakan untuk mengamankan data terenkripsi Anda.", - "session_upgrade_description": "Tingkatkan sesi ini untuk mengizinkan memverifikasi sesi lainnya, memberikan akses ke pesan terenkripsi dan menandainya sebagai terpercaya untuk pengguna lain.", "set_phrase_again": "Pergi kembali untuk menyiapkannya lagi.", "settings_reminder": "Anda juga dapat menyiapkan Cadangan Aman & kelola kunci Anda di Pengaturan.", "title_confirm_phrase": "Konfirmasi Frasa Keamanan", "title_save_key": "Simpan Kunci Keamanan Anda", "title_set_phrase": "Atur sebuah Frasa Keamanan", - "title_upgrade_encryption": "Tingkatkan enkripsi Anda", "unable_to_setup": "Tidak dapat menyiapkan penyimpanan rahasia", "use_different_passphrase": "Gunakan frasa sandi yang berbeda?", "use_phrase_only_you_know": "Gunakan frasa rahasia yang hanya Anda tahu, dan simpan sebuah Kunci Keamanan untuk menggunakannya untuk cadangan secara opsional." @@ -3479,7 +3468,6 @@ "truncated_list_n_more": { "other": "Dan %(count)s lagi..." }, - "unknown_device": "Perangkat tidak diketahui", "unsupported_server_description": "Server ini menjalankan sebuah versi Matrix yang lama. Tingkatkan ke Matrix %(version)s untuk menggunakan %(brand)s tanpa eror.", "unsupported_server_title": "Server Anda tidak didukung", "update": { diff --git a/src/i18n/strings/is.json b/src/i18n/strings/is.json index 6d5a2194e93..c746caef1d2 100644 --- a/src/i18n/strings/is.json +++ b/src/i18n/strings/is.json @@ -94,7 +94,6 @@ "report_content": "Kæra efni", "resend": "Endursenda", "reset": "Endursetja", - "restore": "Endurheimta", "resume": "Halda áfram", "retry": "Reyna aftur", "review": "Yfirfara", @@ -185,7 +184,6 @@ "failed_connect_identity_server": "Næ ekki sambandi við auðkennisþjón", "failed_soft_logout_auth": "Tókst ekki að endurauðkenna", "failed_soft_logout_homeserver": "Tókst ekki að endurauðkenna vegna vandamála með heimaþjón", - "footer_powered_by_matrix": "keyrt með Matrix", "forgot_password_email_invalid": "Tölvupóstfangið lítur ekki út fyrir að vera í lagi.", "forgot_password_email_required": "Það þarf að setja inn tölvupóstfangið sem tengt er notandaaðgangnum þínum.", "forgot_password_prompt": "Gleymdirðu lykilorðinu þínu?", @@ -220,7 +218,6 @@ "phone_label": "Sími", "phone_optional_label": "Sími (valfrjálst)", "qr_code_login": { - "sign_in_new_device": "Skrá inn nýtt tæki", "waiting_for_device": "Bíð eftir að tækið skráist inn" }, "register_action": "Búa til notandaaðgang", @@ -748,7 +745,6 @@ }, "unable_to_setup_keys_error": "Tókst ekki að setja upp lykla", "unsupported": "Þetta forrit styður ekki enda-í-enda dulritun.", - "upgrade_toast_title": "Uppfærsla dulritunar tiltæk", "verification": { "accepting": "Samþykki…", "after_new_login": { @@ -1976,7 +1972,6 @@ "pass_phrase_match_failed": "Þetta stemmir ekki.", "pass_phrase_match_success": "Þetta passar!", "phrase_strong_enough": "Frábært! Þessi öryggisfrasi virðist vera nógu sterkur.", - "requires_password_confirmation": "Sláðu inn lykilorðið þitt til að staðfesta uppfærsluna:", "secret_storage_query_failure": "Tókst ekki að finna stöðu á leynigeymslu", "security_key_safety_reminder": "Geymdu öryggislykilinn þinn á öruggum stað, eins og í lykilorðastýringu eða jafnvel í peningaskáp, þar sem hann er notaður til að verja gögnin þín.", "set_phrase_again": "Farðu til baka til að setja hann aftur.", @@ -1984,7 +1979,6 @@ "title_confirm_phrase": "Staðfestu öryggisfrasa", "title_save_key": "Vista öryggislykilinn þinn", "title_set_phrase": "Setja öryggisfrasa", - "title_upgrade_encryption": "Uppfærðu dulritunina þína", "unable_to_setup": "Tókst ekki að setja upp leynigeymslu", "use_different_passphrase": "Nota annan lykilfrasa?", "use_phrase_only_you_know": "Notaðu leynilegan frasa eða setningu sem aðeins þú þekkir, og útbúðu öryggislykil fyrir öryggisafrit." @@ -2907,7 +2901,6 @@ "truncated_list_n_more": { "other": "Og %(count)s til viðbótar..." }, - "unknown_device": "Óþekkt tæki", "update": { "changelog": "Breytingaskrá", "check_action": "Athuga með uppfærslu", diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 61889463b0f..9f0ab6d4304 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -103,7 +103,6 @@ "report_content": "Segnala contenuto", "resend": "Reinvia", "reset": "Ripristina", - "restore": "Ripristina", "resume": "Riprendi", "retry": "Riprova", "review": "Controlla", @@ -208,7 +207,6 @@ "failed_query_registration_methods": "Impossibile richiedere i metodi di registrazione supportati.", "failed_soft_logout_auth": "Riautenticazione fallita", "failed_soft_logout_homeserver": "Riautenticazione fallita per un problema dell'homeserver", - "footer_powered_by_matrix": "offerto da Matrix", "forgot_password_email_invalid": "L'indirizzo email non sembra essere valido.", "forgot_password_email_required": "Deve essere inserito l'indirizzo email collegato al tuo account.", "forgot_password_prompt": "Hai dimenticato la password?", @@ -247,15 +245,12 @@ "phone_label": "Telefono", "phone_optional_label": "Telefono (facoltativo)", "qr_code_login": { - "approve_access_warning": "Approvando l'accesso per questo dispositivo, avrà accesso completo al tuo account.", "completing_setup": "Completamento configurazione nuovo dispositivo", - "confirm_code_match": "Controlla che il codice sottostante corrisponda nell'altro dispositivo:", "error_rate_limited": "Troppi tentativi in poco tempo. Attendi un po' prima di riprovare.", "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", "select_qr_code": "Seleziona '%(scanQRCode)s'", - "sign_in_new_device": "Accedi nel nuovo dispositivo", "waiting_for_device": "In attesa che il dispositivo acceda" }, "register_action": "Crea account", @@ -895,7 +890,6 @@ }, "unable_to_setup_keys_error": "Impossibile impostare le chiavi", "unsupported": "Questo client non supporta la crittografia end-to-end.", - "upgrade_toast_title": "Aggiornamento crittografia disponibile", "verification": { "accepting": "Accettazione…", "after_new_login": { @@ -2450,18 +2444,13 @@ "pass_phrase_match_failed": "Non corrisponde.", "pass_phrase_match_success": "Corrisponde!", "phrase_strong_enough": "Ottimo! Questa password di sicurezza sembra abbastanza robusta.", - "requires_key_restore": "Ripristina il tuo backup chiavi per aggiornare la crittografia", - "requires_password_confirmation": "Inserisci la password del tuo account per confermare l'aggiornamento:", - "requires_server_authentication": "Dovrai autenticarti con il server per confermare l'aggiornamento.", "secret_storage_query_failure": "Impossibile rilevare lo stato dell'archivio segreto", "security_key_safety_reminder": "Conserva la chiave di sicurezza in un posto sicuro, come in un gestore di password o in una cassaforte, dato che è usata per proteggere i tuoi dati cifrati.", - "session_upgrade_description": "Aggiorna questa sessione per consentirle di verificare altre sessioni, garantendo loro l'accesso ai messaggi cifrati e contrassegnandole come fidate per gli altri utenti.", "set_phrase_again": "Torna per reimpostare.", "settings_reminder": "Puoi anche impostare il Backup Sicuro e gestire le tue chiavi nelle impostazioni.", "title_confirm_phrase": "Conferma frase di sicurezza", "title_save_key": "Salva la tua chiave di sicurezza", "title_set_phrase": "Imposta una frase di sicurezza", - "title_upgrade_encryption": "Aggiorna la tua crittografia", "unable_to_setup": "Impossibile impostare un archivio segreto", "use_different_passphrase": "Usare una password diversa?", "use_phrase_only_you_know": "Usa una frase segreta che conosci solo tu e salva facoltativamente una chiave di sicurezza da usare come backup." @@ -3528,7 +3517,6 @@ "truncated_list_n_more": { "other": "E altri %(count)s ..." }, - "unknown_device": "Dispositivo sconosciuto", "unsupported_server_description": "Questo server usa una versione più vecchia di Matrix. Aggiorna a Matrix %(version)s per usare %(brand)s senza errori.", "unsupported_server_title": "Il tuo server non è supportato", "update": { diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json index db4cb20ca02..963de355ac6 100644 --- a/src/i18n/strings/ja.json +++ b/src/i18n/strings/ja.json @@ -93,7 +93,6 @@ "report_content": "コンテンツを報告", "resend": "再送信", "reset": "リセット", - "restore": "復元", "resume": "再開", "retry": "再試行", "review": "確認", @@ -231,15 +230,12 @@ "phone_label": "電話", "phone_optional_label": "電話番号(任意)", "qr_code_login": { - "approve_access_warning": "この端末へのアクセスを許可すると、あなたのアカウントに完全にアクセスできるようになります。", "completing_setup": "新しい端末の設定を完了しています", - "confirm_code_match": "以下のコードが他の端末と一致していることを確認してください:", "error_rate_limited": "再試行の数が多すぎます。少し待ってから再度試してください。", "error_unexpected": "予期しないエラーが発生しました。", "scan_code_instruction": "サインアウトした端末で以下のQRコードをスキャンしてください。", "scan_qr_code": "QRコードをスキャン", "select_qr_code": "「%(scanQRCode)s」を選択", - "sign_in_new_device": "新しい端末でサインイン", "waiting_for_device": "端末のサインインを待機しています" }, "register_action": "アカウントを作成", @@ -846,7 +842,6 @@ }, "unable_to_setup_keys_error": "鍵を設定できません", "unsupported": "このクライアントはエンドツーエンド暗号化に対応していません。", - "upgrade_toast_title": "暗号化のアップグレードが利用できます", "verification": { "accepting": "承認しています…", "after_new_login": { @@ -2238,18 +2233,13 @@ "pass_phrase_match_failed": "合致しません。", "pass_phrase_match_success": "合致します!", "phrase_strong_enough": "すばらしい! このセキュリティーフレーズは十分に強力なようです。", - "requires_key_restore": "鍵のバックアップを復元し、暗号化をアップグレードしてください", - "requires_password_confirmation": "アップグレードを承認するには、アカウントのパスワードを入力してください:", - "requires_server_authentication": "サーバーをアップグレードするには認証が必要です。", "secret_storage_query_failure": "機密ストレージの状態を読み込めません", "security_key_safety_reminder": "セキュリティーキーは、暗号化されたデータを保護するために使用されます。パスワードマネージャーもしくは金庫のような安全な場所で保管してください。", - "session_upgrade_description": "このセッションをアップグレードすると、他のセッションを認証できるようになります。また、暗号化されたメッセージへのアクセスが可能となり、メッセージを信頼済として相手に表示できるようになります。", "set_phrase_again": "戻って、改めて設定してください。", "settings_reminder": "セキュアバックアップを設定し、設定画面から鍵を管理することもできます。", "title_confirm_phrase": "セキュリティーフレーズを確認", "title_save_key": "セキュリティーキーを保存", "title_set_phrase": "セキュリティーフレーズを設定", - "title_upgrade_encryption": "暗号化をアップグレード", "unable_to_setup": "機密ストレージを設定できません", "use_different_passphrase": "異なるパスフレーズを使用しますか?", "use_phrase_only_you_know": "あなただけが知っている秘密のパスワードを使用してください。また、バックアップ用にセキュリティーキーを保存することができます(任意)。" @@ -3241,7 +3231,6 @@ "truncated_list_n_more": { "other": "他%(count)s人以上…" }, - "unknown_device": "不明な端末", "update": { "changelog": "更新履歴", "check_action": "更新を確認", diff --git a/src/i18n/strings/lo.json b/src/i18n/strings/lo.json index fffbd25e9ba..6310faf2005 100644 --- a/src/i18n/strings/lo.json +++ b/src/i18n/strings/lo.json @@ -90,7 +90,6 @@ "report_content": "ລາຍງານເນື້ອຫາ", "resend": "ສົ່ງຄືນ", "reset": "ຕັ້ງຄ່າຄືນ", - "restore": "ກູ້ຄືນ", "resume": "ປະຫວັດຫຍໍ້", "retry": "ລອງໃໝ່", "review": "ທົບທວນຄືນ", @@ -182,7 +181,6 @@ "failed_query_registration_methods": "ບໍ່ສາມາດສອບຖາມວິທີການລົງທະບຽນໄດ້.", "failed_soft_logout_auth": "ການພິສູດຢືນຢັນຄືນໃໝ່ບໍ່ສຳເລັດ", "failed_soft_logout_homeserver": "ການພິສູດຢືນຢັນຄືນໃໝ່ເນື່ອງຈາກບັນຫາ homeserver ບໍ່ສຳເລັດ", - "footer_powered_by_matrix": "ຂັບເຄື່ອນໂດຍ Matrix", "forgot_password_email_invalid": "ທີ່ຢູ່ອີເມວບໍ່ຖືກຕ້ອງ.", "forgot_password_email_required": "ຕ້ອງໃສ່ທີ່ຢູ່ອີເມວທີ່ເຊື່ອມຕໍ່ກັບບັນຊີຂອງທ່ານ.", "forgot_password_prompt": "ລືມລະຫັດຜ່ານຂອງທ່ານບໍ?", @@ -748,7 +746,6 @@ }, "unable_to_setup_keys_error": "ບໍ່ສາມາດຕັ້ງຄ່າກະແຈໄດ້", "unsupported": "ລູກຄ້ານີ້ບໍ່ຮອງຮັບການເຂົ້າລະຫັດແບບຕົ້ນທາງເຖິງປາຍທາງ.", - "upgrade_toast_title": "ມີການຍົກລະດັບການເຂົ້າລະຫັດ", "verification": { "accepting": "ກຳລັງຍອມຮັບ…", "after_new_login": { @@ -1993,18 +1990,13 @@ "pass_phrase_match_failed": "ບໍ່ກົງກັນ.", "pass_phrase_match_success": "ກົງກັນ!", "phrase_strong_enough": "ດີເລີດ! ປະໂຫຍກຄວາມປອດໄພນີ້ເບິ່ງຄືວ່າເຂັ້ມແຂງພຽງພໍ.", - "requires_key_restore": "ກູ້ຄືນການສຳຮອງຂໍ້ມູນກະແຈຂອງທ່ານເພື່ອຍົກລະດັບການເຂົ້າລະຫັດຂອງທ່ານ", - "requires_password_confirmation": "ໃສ່ລະຫັດຜ່ານບັນຊີຂອງທ່ານເພື່ອຢືນຢັນການຍົກລະດັບ:", - "requires_server_authentication": "ທ່ານຈະຕ້ອງພິສູດຢືນຢັນກັບເຊີບເວີເພື່ອຢືນຢັນການປັບປຸງ.", "secret_storage_query_failure": "ບໍ່ສາມາດຄົ້ນຫາສະຖານະການເກັບຮັກສາຄວາມລັບໄດ້", "security_key_safety_reminder": "ການເກັບຮັກສາກະແຈຄວາມປອດໄພຂອງທ່ານໄວ້ບ່ອນໃດບ່ອນໜຶ່ງທີ່ປອດໄພ ເຊັ່ນ: ຕົວຈັດການລະຫັດຜ່ານ ຫຼືບ່ອນປອດໄພ ເພາະຈະຖືກໃຊ້ເພື່ອປົກປ້ອງຂໍ້ມູນທີ່ເຂົ້າລະຫັດໄວ້ຂອງທ່ານ.", - "session_upgrade_description": "ປັບປຸງລະບົບນີ້ເພື່ອໃຫ້ມັນກວດສອບລະບົບອື່ນ, ອະນຸຍາດໃຫ້ພວກເຂົາເຂົ້າເຖິງຂໍ້ຄວາມທີ່ຖືກເຂົ້າລະຫັດ ແລະເປັນເຄື່ອງໝາຍໃຫ້ເປັນທີ່ເຊື່ອຖືໄດ້ສໍາລັບຜູ້ໃຊ້ອື່ນ.", "set_phrase_again": "ກັບຄືນໄປຕັ້ງໃໝ່ອີກຄັ້ງ.", "settings_reminder": "ນອກນັ້ນທ່ານຍັງສາມາດຕັ້ງຄ່າການສໍາຮອງຂໍ້ມູນທີ່ປອດໄພ & ຈັດການກະແຈຂອງທ່ານໃນການຕັ້ງຄ່າ.", "title_confirm_phrase": "ຢືນຢັນປະໂຫຍກຄວາມປອດໄພ", "title_save_key": "ບັນທຶກກະແຈຄວາມປອດໄພຂອງທ່ານ", "title_set_phrase": "ຕັ້ງຄ່າປະໂຫຍກຄວາມປອດໄພ", - "title_upgrade_encryption": "ປັບປຸງການເຂົ້າລະຫັດຂອງທ່ານ", "unable_to_setup": "ບໍ່ສາມາດກຳນົດບ່ອນເກັບຂໍ້ມູນລັບໄດ້", "use_different_passphrase": "ໃຊ້ຂໍ້ຄວາມລະຫັດຜ່ານອື່ນບໍ?", "use_phrase_only_you_know": "ໃຊ້ປະໂຫຍກລັບທີ່ທ່ານຮູ້ເທົ່ານັ້ນ, ແລະ ເປັນທາງເລືອກທີ່ຈະບັນທຶກກະແຈຄວາມປອດໄພເພື່ອໃຊ້ສຳລັບການສຳຮອງຂໍ້ມູນ." @@ -2852,7 +2844,6 @@ "truncated_list_n_more": { "other": "ແລະ %(count)sອີກ..." }, - "unknown_device": "ທີ່ບໍ່ຮູ້ຈັກອຸປະກອນນີ້", "update": { "changelog": "ບັນທຶກການປ່ຽນແປງ", "check_action": "ກວດເບິ່ງເພຶ່ອອັບເດດ", diff --git a/src/i18n/strings/lt.json b/src/i18n/strings/lt.json index 9ccd0962caa..8f503bacdd8 100644 --- a/src/i18n/strings/lt.json +++ b/src/i18n/strings/lt.json @@ -83,7 +83,6 @@ "report_content": "Pranešti", "resend": "Siųsti iš naujo", "reset": "Iš naujo nustatyti", - "restore": "Atkurti", "resume": "Tęsti", "retry": "Bandyti dar kartą", "review": "Peržiūrėti", @@ -153,7 +152,6 @@ "failed_connect_identity_server_register": "Jūs galite registruotis, tačiau kai kurios funkcijos bus nepasiekiamos, kol tapatybės serveris prisijungs. Jei ir toliau matote šį įspėjimą, patikrinkite savo konfigūraciją arba susisiekite su serverio administratoriumi.", "failed_connect_identity_server_reset_password": "Jūs galite iš naujo nustatyti savo slaptažodį, tačiau kai kurios funkcijos bus nepasiekiamos, kol tapatybės serveris prisijungs. Jei ir toliau matote šį įspėjimą, patikrinkite savo konfigūraciją arba susisiekite su serverio administratoriumi.", "failed_homeserver_discovery": "Nepavyko atlikti serverio radimo", - "footer_powered_by_matrix": "veikia su Matrix", "forgot_password_email_required": "Privalo būti įvestas su jūsų paskyra susietas el. pašto adresas.", "forgot_password_prompt": "Pamiršote savo slaptažodį?", "identifier_label": "Prisijungti naudojant", @@ -583,7 +581,6 @@ }, "unable_to_setup_keys_error": "Nepavyksta nustatyti raktų", "unsupported": "Šis klientas nepalaiko visapusio šifravimo.", - "upgrade_toast_title": "Galimas šifravimo atnaujinimas", "verification": { "accepting": "Priimama…", "cancelled": "Jūs atšaukėte patvirtinimą.", @@ -1576,15 +1573,12 @@ "pass_phrase_match_failed": "Tai nesutampa.", "pass_phrase_match_success": "Tai sutampa!", "phrase_strong_enough": "Puiku! Ši Saugumo Frazė atrodo pakankamai stipri.", - "requires_key_restore": "Atkurkite savo atsarginę raktų kopiją, kad atnaujintumėte šifravimą", "secret_storage_query_failure": "Slaptos saugyklos būsenos užklausa neįmanoma", - "session_upgrade_description": "Atnaujinkite šį seansą, kad jam būtų leista patvirtinti kitus seansus, suteikiant jiems prieigą prie šifruotų žinučių ir juos pažymint kaip patikimus kitiems vartotojams.", "set_phrase_again": "Grįžti atgal, kad nustatyti iš naujo.", "settings_reminder": "Jūs taip pat galite nustatyti Saugią Atsarginę Kopiją ir tvarkyti savo raktus Nustatymuose.", "title_confirm_phrase": "Patvirtinkite Slaptafrazę", "title_save_key": "Išsaugoti savo Saugumo Raktą", "title_set_phrase": "Nustatyti Slaptafrazę", - "title_upgrade_encryption": "Atnaujinkite savo šifravimą", "unable_to_setup": "Neįmanoma nustatyti slaptos saugyklos", "use_different_passphrase": "Naudoti kitą slaptafrazę?", "use_phrase_only_you_know": "Naudokite slaptafrazę, kurią žinote tik jūs ir pasirinktinai išsaugokite Apsaugos Raktą, naudoti kaip atsarginę kopiją." @@ -2290,7 +2284,6 @@ "truncated_list_n_more": { "other": "Ir dar %(count)s..." }, - "unknown_device": "Nežinomas įrenginys", "update": { "changelog": "Keitinių žurnalas", "check_action": "Tikrinti, ar yra atnaujinimų", diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index 18500da4bf7..1231a9a3303 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -1,6 +1,8 @@ { "a11y": { + "emoji_picker": "Emoji kiezer", "jump_first_invite": "Ga naar de eerste uitnodiging.", + "message_composer": "Bericht opsteller", "n_unread_messages": { "other": "%(count)s ongelezen berichten.", "one": "1 ongelezen bericht." @@ -9,21 +11,26 @@ "other": "%(count)s ongelezen berichten, inclusief vermeldingen.", "one": "1 ongelezen vermelding." }, + "recent_rooms": "Recente kamers", "room_name": "Kamer %(name)s", + "room_status_bar": "Kamerstatus balk", + "seek_bar_label": "Audio zoekbalk", "unread_messages": "Ongelezen berichten.", - "user_menu": "Persoonsmenu" + "user_menu": "Gebruikersmenu" }, - "a11y_jump_first_unread_room": "Ga naar het eerste ongelezen kamer.", + "a11y_jump_first_unread_room": "Ga naar de eerste ongelezen kamer", "action": { - "accept": "Aannemen", + "accept": "Accepteren", "add": "Toevoegen", - "add_existing_room": "Bestaande kamers toevoegen", + "add_existing_room": "Voeg bestaande kamer toeroom", "add_people": "Personen toevoegen", - "approve": "Goedkeuren", + "apply": "Toepassen", + "approve": "Keur goed", + "ask_to_join": "Vraag om toe te treden", "back": "Terug", "call": "Bellen", - "cancel": "Annuleren", - "change": "Wijzigen", + "cancel": "Annuleer", + "change": "Wijzig", "clear": "Wis", "click": "Klik", "click_to_copy": "Klik om te kopiëren", @@ -39,6 +46,7 @@ "create_account": "Registreren", "decline": "Weigeren", "delete": "Verwijderen", + "deny": "Weigeren", "disable": "Uitschakelen", "disconnect": "Verbinding verbreken", "dismiss": "Sluiten", @@ -81,11 +89,13 @@ "pause": "Pauze", "pin": "Vastmaken", "play": "Afspelen", + "proceed": "Doorgaan", "quote": "Citeren", "react": "Reageren", "refresh": "Herladen", "register": "Registreren", "reject": "Weigeren", + "reload": "Herladen", "remove": "Verwijderen", "rename": "Hernoemen", "reply": "Beantwoorden", @@ -93,7 +103,6 @@ "report_content": "Inhoud melden", "resend": "Opnieuw versturen", "reset": "Opnieuw instellen", - "restore": "Herstellen", "resume": "Hervatten", "retry": "Opnieuw proberen", "review": "Controleer", @@ -108,8 +117,10 @@ "sign_in": "Inloggen", "sign_out": "Uitloggen", "skip": "Overslaan", + "start": "Starten", "start_chat": "Gesprek beginnen", "start_new_chat": "Nieuwe chat beginnen", + "stop": "Stop", "submit": "Bevestigen", "subscribe": "Abonneren", "transfer": "Doorschakelen", @@ -185,7 +196,6 @@ "failed_query_registration_methods": "Kan ondersteunde registratiemethoden niet opvragen.", "failed_soft_logout_auth": "Opnieuw inloggen is mislukt", "failed_soft_logout_homeserver": "Opnieuw inloggen is mislukt wegens een probleem met de homeserver", - "footer_powered_by_matrix": "draait op Matrix", "forgot_password_email_invalid": "Dit e-mailadres lijkt niet geldig te zijn.", "forgot_password_email_required": "Het aan jouw account gekoppelde e-mailadres dient ingevoerd worden.", "forgot_password_prompt": "Wachtwoord vergeten?", @@ -220,12 +230,9 @@ "phone_label": "Telefoonnummer", "phone_optional_label": "Telefoonnummer (optioneel)", "qr_code_login": { - "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_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", "waiting_for_device": "Wachten op apparaat om in te loggen" }, "register_action": "Registreren", @@ -374,12 +381,15 @@ "other": "en %(count)s anderen…", "one": "en één andere…" }, + "android": "Android", "appearance": "Weergave", "application": "Toepassing", "are_you_sure": "Weet je het zeker?", "attachment": "Bijlage", "authentication": "Login bevestigen", "avatar": "Afbeelding", + "beta": "BÈTA", + "camera": "Camera", "cameras": "Camera's", "capabilities": "Mogelijkheden", "copied": "Gekopieerd!", @@ -391,27 +401,38 @@ "device": "Apparaat", "edited": "bewerkt", "email_address": "E-mailadres", + "emoji": "Emoji", "encrypted": "Versleuteld", "encryption_enabled": "Versleuteling ingeschakeld", "error": "Fout", + "faq": "FAQ", "favourites": "Favorieten", "filter_results": "Resultaten filteren", "forward_message": "Bericht doorsturen", "general": "Algemeen", "go_to_settings": "Ga naar instellingen", "guest": "Gast", + "help": "Hulp", "historical": "Historisch", + "home": "Thuis", + "homeserver": "Homeserver", "identity_server": "Identiteitsserver", "image": "Afbeelding", "integration_manager": "Integratiebeheerder", + "ios": "iOS", "joined": "Toegetreden", + "labs": "Labs", "legal": "Juridisch", "light": "Helder", + "loading": "Laden...", "location": "Locatie", "low_priority": "Lage prioriteit", + "matrix": "Matrix", "message": "Bericht", "message_layout": "Berichtlayout", "microphone": "Microfoon", + "model": "Model", + "modern": "Modern", "mute": "Dempen", "n_members": { "other": "%(count)s personen", @@ -426,6 +447,7 @@ "no_results_found": "Geen resultaten gevonden", "not_trusted": "Niet vertrouwd", "off": "Uit", + "offline": "Offline", "on": "Aan", "options": "Opties", "orphan_rooms": "Andere kamers", @@ -434,6 +456,7 @@ "preferences": "Voorkeuren", "presence": "Aanwezigheid", "preview_message": "Hey. Jij bent de beste!", + "privacy": "Privacy", "private": "Privé", "private_room": "Privé kamer", "private_space": "Privé Space", @@ -451,10 +474,13 @@ "secure_backup": "Beveiligde back-up", "security": "Beveiliging", "select_all": "Allemaal selecteren", + "server": "Server", "settings": "Instellingen", "setup_secure_messages": "Beveiligde berichten instellen", "show_more": "Meer tonen", "someone": "Iemand", + "space": "Space", + "sticker": "Sticker", "stickerpack": "Stickerpakket", "success": "Klaar", "suggestions": "Suggesties", @@ -462,6 +488,7 @@ "system_alerts": "Systeemmeldingen", "theme": "Thema", "thread": "Draad", + "threads": "Onderwerpen", "timeline": "Tijdslijn", "trusted": "Vertrouwd", "unencrypted": "Onversleuteld", @@ -469,11 +496,13 @@ "unnamed_room": "Naamloze Kamer", "unnamed_space": "Naamloze Space", "unverified": "Niet geverifieerd", + "user": "Gebruiker", "user_avatar": "Profielfoto", "username": "Inlognaam", "verification_cancelled": "Verificatie geannuleerd", "verified": "Geverifieerd", "version": "Versie", + "video": "Video", "video_room": "Video kamer", "view_message": "Bericht bekijken", "warning": "Let op", @@ -754,7 +783,6 @@ }, "unable_to_setup_keys_error": "Kan geen sleutels instellen", "unsupported": "Deze cliënt biedt geen ondersteuning voor eind-tot-eind-versleuteling.", - "upgrade_toast_title": "Versleutelingsupgrade beschikbaar", "verification": { "accepting": "Toestaan…", "after_new_login": { @@ -2071,18 +2099,13 @@ "pass_phrase_match_failed": "Dat komt niet overeen.", "pass_phrase_match_success": "Dat komt overeen!", "phrase_strong_enough": "Geweldig. Dit veiligheidswachtwoord ziet er sterk genoeg uit.", - "requires_key_restore": "Herstel je sleutelback-up om je versleuteling te upgraden", - "requires_password_confirmation": "Voer je wachtwoord in om het upgraden te bevestigen:", - "requires_server_authentication": "Je zal moeten inloggen bij de server om het upgraden te bevestigen.", "secret_storage_query_failure": "Kan status sleutelopslag niet opvragen", "security_key_safety_reminder": "Bewaar je veiligheidssleutel op een veilige plaats, zoals in een wachtwoordmanager of een kluis, aangezien hiermee je versleutelde gegevens zijn beveiligd.", - "session_upgrade_description": "Upgrade deze sessie om er andere sessies mee te verifiëren. Hiermee krijgen de andere sessies toegang tot je versleutelde berichten en is het voor andere personen als vertrouwd gemarkeerd .", "set_phrase_again": "Ga terug om het opnieuw in te stellen.", "settings_reminder": "Je kan ook een beveiligde back-up instellen en je sleutels beheren via instellingen.", "title_confirm_phrase": "Veiligheidswachtwoord bevestigen", "title_save_key": "Jouw veiligheidssleutel opslaan", "title_set_phrase": "Een veiligheidswachtwoord instellen", - "title_upgrade_encryption": "Upgrade je versleuteling", "unable_to_setup": "Kan sleutelopslag niet instellen", "use_different_passphrase": "Gebruik een ander wachtwoord?", "use_phrase_only_you_know": "Gebruik een veiligheidswachtwoord die alleen jij kent, en sla optioneel een veiligheidssleutel op om te gebruiken als back-up." @@ -2589,6 +2612,7 @@ "about_minute_ago": "ongeveer een minuut geleden", "date_at_time": "%(date)s om %(time)s", "few_seconds_ago": "enige tellen geleden", + "hours_minutes_seconds_left": "%(hours)su, %(minutes)sm %(seconds)ss over", "in_about_day": "over een dag of zo", "in_about_hour": "over ongeveer een uur", "in_about_minute": "over ongeveer een minuut", @@ -2597,10 +2621,16 @@ "in_n_hours": "over %(num)s uur", "in_n_minutes": "over %(num)s minuten", "left": "%(timeRemaining)s over", + "minutes_seconds_left": "%(minutes)sm %(seconds)ss over", "n_days_ago": "%(num)s dagen geleden", "n_hours_ago": "%(num)s uur geleden", "n_minutes_ago": "%(num)s minuten geleden", - "seconds_left": "%(seconds)s's over" + "seconds_left": "%(seconds)s's over", + "short_days": "%(value)sd", + "short_days_hours_minutes_seconds": "%(days)sd %(hours)su %(minutes)sm %(seconds)ss", + "short_hours": "%(value)sh", + "short_minutes": "%(value)sm", + "short_seconds": "%(value)ss" }, "timeline": { "context_menu": { @@ -3015,7 +3045,6 @@ "truncated_list_n_more": { "other": "En %(count)s meer…" }, - "unknown_device": "Onbekend apparaat", "update": { "changelog": "Wijzigingslogboek", "check_action": "Controleren op updates", diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index 7aa90f7f85b..e148c873f12 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -103,7 +103,6 @@ "report_content": "Zgłoś treść", "resend": "Wyślij jeszcze raz", "reset": "Resetuj", - "restore": "Przywróć", "resume": "Wznów", "retry": "Ponów", "review": "Przejrzyj", @@ -210,7 +209,6 @@ "failed_query_registration_methods": "Nie można uzyskać wspieranych metod rejestracji.", "failed_soft_logout_auth": "Nie udało się uwierzytelnić ponownie", "failed_soft_logout_homeserver": "Nie udało się uwierzytelnić ponownie z powodu błędu serwera domowego", - "footer_powered_by_matrix": "napędzany przez Matrix", "forgot_password_email_invalid": "Adres e-mail nie wygląda na prawidłowy.", "forgot_password_email_required": "Musisz wpisać adres e-mail połączony z twoim kontem.", "forgot_password_prompt": "Nie pamiętasz hasła?", @@ -250,13 +248,11 @@ "phone_label": "Telefon", "phone_optional_label": "Telefon (opcjonalny)", "qr_code_login": { - "approve_access_warning": "Akceptując dostęp temu urządzeniu, będzie miał on pełny dostęp do Twojego konta.", "check_code_explainer": "Bezpieczeństwo połączenia z urządzeniem zostanie sprawdzone.", "check_code_heading": "Wprowadź numer wyświetlany na drugim urządzeniu", "check_code_input_label": "2-cyfrowy kod", "check_code_mismatch": "Liczby się nie zgadzają", "completing_setup": "Kończenie konfiguracji nowego urządzenia", - "confirm_code_match": "Potwierdź, że kod poniżej pasuje z Twoim drugim urządzeniem:", "error_etag_missing": "Wystąpił nieoczekiwany błąd. Może to być spowodowane rozszerzeniem przeglądarki, serwerem proxy lub błędną konfiguracją serwera.", "error_expired": "Logowanie wygasło. Spróbuj ponownie.", "error_expired_title": "Logowanie nie zostało zakończone na czas", @@ -284,7 +280,8 @@ "security_code": "Kod bezpieczeństwa", "security_code_prompt": "Jeśli zostaniesz poproszony, wprowadź poniższy kod na drugim urządzeniu.", "select_qr_code": "Wybierz \"%(scanQRCode)s\"", - "sign_in_new_device": "Zaloguj nowe urządzenie", + "unsupported_explainer": "Twój dostawca konta nie obsługuje logowania nowego urządzenia za pomocą kodu QR.", + "unsupported_heading": "Kod QR nie jest wspierany", "waiting_for_device": "Oczekiwanie na logowanie urządzenia" }, "register_action": "Utwórz konto", @@ -452,8 +449,9 @@ "all_rooms": "Wszystkie pokoje", "analytics": "Analityka", "and_n_others": { - "other": "i %(count)s innych...", - "one": "i jeden inny..." + "one": "i jeden inny...", + "few": "i %(count)s innych...", + "many": "i %(count)s innych..." }, "android": "Android", "appearance": "Wygląd", @@ -744,7 +742,7 @@ "developer_tools": "Narzędzia programistyczne", "edit_setting": "Edytuj ustawienie", "edit_values": "Edytuj wartości", - "empty_string": "", + "empty_string": "", "event_content": "Zawartość wydarzenia", "event_id": "ID wydarzenia: %(eventId)s", "event_sent": "Wydarzenie wysłane!", @@ -800,7 +798,8 @@ "show_hidden_events": "Pokaż ukryte wydarzenia na linii czasowej", "spaces": { "one": "", - "other": "<%(count)s spacji>" + "few": "<%(count)s spacje>", + "many": "<%(count)s spacji>" }, "state_key": "Klucz stanu", "thread_root_id": "ID Root Wątku:%(threadRootId)s", @@ -933,7 +932,6 @@ }, "unable_to_setup_keys_error": "Nie można ustawić kluczy", "unsupported": "Ten klient nie obsługuje szyfrowania end-to-end.", - "upgrade_toast_title": "Dostępne ulepszenie szyfrowania", "verification": { "accepting": "Akceptowanie…", "after_new_login": { @@ -1131,20 +1129,24 @@ "export_successful": "Eksport zakończony pomyślnie!", "exported_n_events_in_time": { "one": "Wyeksportowano %(count)s wydarzenie w %(seconds)s sekund", - "other": "Wyeksportowano %(count)s wydarzeń w %(seconds)s sekund" + "few": "Wyeksportowano %(count)s wydarzenia w %(seconds)s sekund", + "many": "Wyeksportowano %(count)s wydarzeń w %(seconds)s sekund" }, "exporting_your_data": "Eksportowanie Twoich danych", "fetched_n_events": { - "one": "Pobrano %(count)s wydarzenie", - "other": "Pobrano %(count)s wydarzeń" + "one": "Pobrano %(count)s wydarzenie do tej pory", + "few": "Pobrano %(count)s wydarzenia do tej pory", + "many": "Pobrano %(count)s wydarzeń do tej pory" }, "fetched_n_events_in_time": { "one": "Pobrano %(count)s wydarzenie w %(seconds)ss", - "other": "Pobrano %(count)s wydarzeń w %(seconds)ss" + "few": "Pobrano %(count)s wydarzenia w %(seconds)ss", + "many": "Pobrano %(count)s wydarzeń w %(seconds)ss" }, "fetched_n_events_with_total": { "one": "Pobrano %(count)s wydarzenie z %(total)s", - "other": "Pobrano %(count)s wydarzeń z %(total)s" + "few": "Pobrano %(count)s wydarzenia z %(total)s", + "many": "Pobrano %(count)s wydarzeń z %(total)s" }, "fetching_events": "Pobieranie wydarzeń…", "file_attached": "Plik załączony", @@ -1234,8 +1236,9 @@ "in_space": "W przestrzeni %(spaceName)s.", "in_space1_and_space2": "W przestrzeniach %(space1Name)s i %(space2Name)s.", "in_space_and_n_other_spaces": { - "one": "W %(spaceName)s i %(count)s innej przestrzeni.", - "other": "W %(spaceName)s i %(count)s innych przestrzeniach." + "one": "W %(spaceName)s i jednej innej przestrzeni.", + "few": "W %(spaceName)s i %(count)s innych przestrzeniach.", + "many": "W %(spaceName)s i %(count)s innych przestrzeniach." }, "incompatible_browser": { "continue": "Kontynuuj mimo to", @@ -1243,11 +1246,14 @@ "detail_can_continue": "Jeśli kontynuujesz, niektóre funkcje mogą przestać działać, jak i istnieje ryzyko utraty danych w przyszłości.", "detail_no_continue": "Zaktualizuj przeglądarkę, jeśli jeszcze tego nie zrobiłeś i spróbuj ponownie.", "learn_more": "Dowiedz się więcej", + "linux": "Linux", + "macos": "Mac", "supported_browsers": "Dla najlepszego doświadczenia korzystaj z Chrome, Firefox, Edge lub Safari.", "title": "%(brand)s nie wspiera tej przeglądarki", "use_desktop_heading": "Zamiast tego użyj %(brand)s Desktop", "use_mobile_heading": "Zamiast tego użyj %(brand)s Mobile", - "use_mobile_heading_after_desktop": "lub skorzystaj z naszej aplikacji mobilnej" + "use_mobile_heading_after_desktop": "lub skorzystaj z naszej aplikacji mobilnej", + "windows": "Windows (%(bits)s-bity)" }, "info_tooltip_title": "Informacje", "integration_manager": { @@ -1322,8 +1328,9 @@ }, "inviting_user1_and_user2": "Zapraszanie %(user1)s i %(user2)s", "inviting_user_and_n_others": { - "one": "Zapraszanie %(user)s i 1 więcej", - "other": "Zapraszanie %(user)s i %(count)s innych" + "one": "Zapraszanie %(user)s i jedną inną osobę", + "few": "Zapraszanie %(user)s i %(count)s inne", + "many": "Zapraszanie %(user)s i %(count)s innych" }, "items_and_n_others": { "other": " i %(count)s innych", @@ -1517,7 +1524,7 @@ "rules_title": "Zbanuj listę zasad - %(roomName)s", "rules_user": "Zasady użytkownika", "something_went_wrong": "Coś poszło nie tak. Spróbuj ponownie lub sprawdź konsolę przeglądarki dla wskazówek.", - "title": "Zignorowani użytkownicy", + "title": "Ignorowani użytkownicy", "view_rules": "Zobacz zasady" }, "language_dropdown_label": "Rozwiń języki", @@ -1667,7 +1674,8 @@ "no_avatar_label": "Dodaj zdjęcie, aby inni mogli Cię rozpoznać.", "only_n_steps_to_go": { "one": "Jeszcze tylko %(count)s krok", - "other": "Jeszcze tylko %(count)s kroki" + "few": "Jeszcze tylko %(count)s kroki", + "many": "Jeszcze tylko %(count)s kroków" }, "personal_messaging_action": "Zacznij swoją pierwszą rozmowę", "personal_messaging_title": "Bezpieczna komunikacja dla znajomych i rodziny", @@ -2040,14 +2048,6 @@ "button_view_all": "Pokaż wszystkie", "description": "Ten pokój ma przypięte wiadomości. Kliknij, aby je wyświetlić.", "go_to_message": "Wyświetl przypiętą wiadomość na osi czasu.", - "prefix": { - "audio": "Audio", - "file": "Plik", - "image": "Obraz", - "poll": "Ankieta", - "video": "Wideo" - }, - "preview": "%(prefix)s: %(preview)s", "title": "%(index)s z %(length)s przypiętych wiadomości" }, "read_topic": "Kliknij, aby przeczytać temat", @@ -2590,18 +2590,13 @@ "pass_phrase_match_failed": "To się nie zgadza.", "pass_phrase_match_success": "Zgadza się!", "phrase_strong_enough": "Wspaniale! Hasło bezpieczeństwa wygląda na silne.", - "requires_key_restore": "Przywróć kopię zapasową klucza, aby ulepszyć szyfrowanie", - "requires_password_confirmation": "Wprowadź hasło do konta, aby potwierdzić aktualizację:", - "requires_server_authentication": "Wymagane jest uwierzytelnienie z serwerem, aby potwierdzić ulepszenie.", "secret_storage_query_failure": "Nie udało się uzyskać statusu sekretnego magazynu", "security_key_safety_reminder": "Przechowuj swój klucz bezpieczeństwa w bezpiecznym miejscu, takim jak menedżer haseł lub sejf, ponieważ jest on używany do ochrony zaszyfrowanych danych.", - "session_upgrade_description": "Ulepsz tę sesję, aby zezwolić jej na weryfikację innych sesji, dając im dostęp do wiadomości szyfrowanych i oznaczenie ich jako zaufane.", "set_phrase_again": "Wróć, aby skonfigurować to ponownie.", "settings_reminder": "W ustawieniach możesz również skonfigurować bezpieczną kopię zapasową i zarządzać swoimi kluczami.", "title_confirm_phrase": "Potwierdź hasło bezpieczeństwa", "title_save_key": "Zapisz swój klucz bezpieczeństwa", "title_set_phrase": "Ustaw hasło bezpieczeństwa", - "title_upgrade_encryption": "Ulepsz swoje szyfrowanie", "unable_to_setup": "Nie można ustawić sekretnego magazynu", "use_different_passphrase": "Użyć innego hasła?", "use_phrase_only_you_know": "Użyj sekretnej frazy, którą znasz tylko Ty, i opcjonalnie zapisz klucz bezpieczeństwa, który będzie używany do tworzenia kopii zapasowych." @@ -2754,7 +2749,7 @@ "error_loading_key_backup_status": "Nie można załadować stanu kopii zapasowej klucza", "export_megolm_keys": "Eksportuj klucze E2E pokojów", "ignore_users_empty": "Nie posiadasz ignorowanych użytkowników.", - "ignore_users_section": "Zignorowani użytkownicy", + "ignore_users_section": "Ignorowani użytkownicy", "import_megolm_keys": "Importuj klucze pokoju E2E", "key_backup_active": "Ta sesja tworzy kopię zapasową kluczy.", "key_backup_active_version": "Aktywna wersja kopii zapasowej:", @@ -2829,7 +2824,7 @@ "error_pusher_state": "Nie udało się ustawić stanu pushera", "error_set_name": "Nie udało się ustawić nazwy sesji", "filter_all": "Wszystkie", - "filter_inactive": "Nieaktywny", + "filter_inactive": "Nieaktywne", "filter_inactive_description": "Nieaktywne przez %(inactiveAgeDays)s dni lub dłużej", "filter_label": "Filtruj urządzenia", "filter_unverified_description": "Nieprzygotowane do bezpiecznej komunikacji", @@ -2838,7 +2833,7 @@ "inactive_days": "Nieaktywne przez %(inactiveAgeDays)s+ dni", "inactive_sessions": "Sesje nieaktywne", "inactive_sessions_explainer_1": "Sesje nieaktywne to sesje, które nie były używane przez dłuższy czas, ale wciąż otrzymują klucze szyfrujące.", - "inactive_sessions_explainer_2": "Regularne usuwanie sesji nieaktywnych poprawia bezpieczeństwo, wydajność i upraszcza Tobie detekcje podejrzanych sesji.", + "inactive_sessions_explainer_2": "Regularne usuwanie sesji nieaktywnych poprawia bezpieczeństwo, wydajność i upraszcza Tobie wykrywanie podejrzanych sesji.", "inactive_sessions_list_description": "Rozważ wylogowanie się ze starych sesji (%(inactiveAgeDays)s dni lub starsze), jeśli już z nich nie korzystasz.", "ip": "Adres IP", "last_activity": "Ostatnia aktywność", @@ -2868,6 +2863,7 @@ "sign_in_with_qr": "Połącz nowe urządzenie", "sign_in_with_qr_button": "Pokaż kod QR", "sign_in_with_qr_description": "Użyj kodu QR, aby zalogować się na innym urządzeniu i skonfigurować bezpieczne przesyłanie wiadomości.", + "sign_in_with_qr_unsupported": "Nieobsługiwane przez dostawcę konta", "sign_out": "Wyloguj się z tej sesji", "sign_out_all_other_sessions": "Wyloguj się z wszystkich pozostałych sesji (%(otherSessionsCount)s)", "sign_out_confirm_description": { @@ -2913,7 +2909,7 @@ "metaspaces_favourites_description": "Pogrupuj wszystkie swoje ulubione pokoje i osoby w jednym miejscu.", "metaspaces_home_all_rooms": "Pokaż wszystkie pokoje", "metaspaces_home_all_rooms_description": "Pokaż wszystkie swoje pokoje na głównej, nawet jeśli znajdują się w przestrzeni.", - "metaspaces_home_description": "Główna to przydatne miejsce, gdzie znajdziesz przegląd wszystkiego.", + "metaspaces_home_description": "Główna to przydatne miejsce, gdzie znajdziesz przegląd wszystkich pokoi.", "metaspaces_orphans": "Pokoje poza przestrzenią", "metaspaces_orphans_description": "Pogrupuj wszystkie pokoje, które nie są częścią przestrzeni, w jednym miejscu.", "metaspaces_people_description": "Pogrupuj wszystkie osoby w jednym miejscu.", @@ -3341,7 +3337,7 @@ }, "m.file": { "error_decrypting": "Błąd odszyfrowywania załącznika", - "error_invalid": "Nieprawidłowy plik %(extra)s" + "error_invalid": "Nieprawidłowy plik" }, "m.image": { "error": "Nie można pokazać zdjęcia z powodu błędu", @@ -3616,8 +3612,9 @@ "other": "%(severalUsers)s dołączyło i wyszło %(count)s razy" }, "joined_multiple": { - "one": "%(severalUsers)sdołączyło", - "other": "%(severalUsers)s dołączyło %(count)s razy" + "one": "%(severalUsers)s dołączył", + "few": "%(severalUsers)s dołączyli %(count)s razy", + "many": "%(severalUsers)s dołączyło %(count)s razy" }, "kicked": { "one": "zostało usunięte", @@ -3695,8 +3692,9 @@ "thread_info_basic": "Z wątku", "typing_indicator": { "more_users": { - "other": "%(names)s i %(count)s innych piszą…", - "one": "%(names)s i jedna osoba pisze…" + "one": "%(names)s i jedna inna pisze ...", + "few": "%(names)s i %(count)s inne piszą ...", + "many": "%(names)s i %(count)s innych pisze ..." }, "one_user": "%(displayName)s pisze…", "two_users": "%(names)s i %(lastPerson)s piszą…" @@ -3713,7 +3711,6 @@ "truncated_list_n_more": { "other": "I %(count)s więcej…" }, - "unknown_device": "Nieznane urządzenie", "unsupported_browser": { "description": "Jeśli kontynuujesz, niektóre funkcje mogą przestać działać, jak i istnieje ryzyko utraty danych w przyszłości. Zaktualizuj przeglądarkę, aby nadal używać %(brand)s.", "title": "%(brand)s nie wspiera tej przeglądarki" diff --git a/src/i18n/strings/pt_BR.json b/src/i18n/strings/pt_BR.json index 6428774388c..6789fb4ee66 100644 --- a/src/i18n/strings/pt_BR.json +++ b/src/i18n/strings/pt_BR.json @@ -84,7 +84,6 @@ "report_content": "Denunciar conteúdo", "resend": "Reenviar", "reset": "Redefinir", - "restore": "Restaurar", "resume": "Retomar", "retry": "Tentar novamente", "review": "Revisar", @@ -170,7 +169,6 @@ "failed_query_registration_methods": "Não foi possível consultar as opções de registro suportadas.", "failed_soft_logout_auth": "Falha em autenticar novamente", "failed_soft_logout_homeserver": "Falha em autenticar novamente devido à um problema no servidor local", - "footer_powered_by_matrix": "oferecido por Matrix", "forgot_password_email_required": "O e-mail vinculado à sua conta precisa ser informado.", "forgot_password_prompt": "Esqueceu sua senha?", "identifier_label": "Entrar com", @@ -661,7 +659,6 @@ }, "unable_to_setup_keys_error": "Não foi possível configurar as chaves", "unsupported": "A sua versão do aplicativo não suporta a criptografia de ponta a ponta.", - "upgrade_toast_title": "Atualização de criptografia disponível", "verification": { "accepting": "Aceitando…", "cancelled": "Você cancelou a confirmação.", @@ -1676,17 +1673,12 @@ "pass_phrase_match_failed": "Isto não corresponde.", "pass_phrase_match_success": "Isto corresponde!", "phrase_strong_enough": "Ótimo! Essa frase de segurança parece ser segura o suficiente.", - "requires_key_restore": "Restaurar o backup das suas chaves para atualizar a sua criptografia", - "requires_password_confirmation": "Digite a senha da sua conta para confirmar a atualização:", - "requires_server_authentication": "Você precisará se autenticar no servidor para confirmar a atualização.", "secret_storage_query_failure": "Não foi possível obter o status do armazenamento secreto", - "session_upgrade_description": "Atualize esta sessão para permitir que ela confirme outras sessões, dando a elas acesso às mensagens criptografadas e marcando-as como confiáveis para os seus contatos.", "set_phrase_again": "Voltar para configurar novamente.", "settings_reminder": "Você também pode configurar o Backup online & configurar as suas senhas nas Configurações.", "title_confirm_phrase": "Confirme a frase de segurança", "title_save_key": "Salve sua Chave de Segurança", "title_set_phrase": "Defina uma frase de segurança", - "title_upgrade_encryption": "Atualizar sua criptografia", "unable_to_setup": "Não foi possível definir o armazenamento secreto", "use_different_passphrase": "Usar uma frase secreta diferente?", "use_phrase_only_you_know": "Use uma frase secreta que apenas você conhece, e opcionalmente salve uma Chave de Segurança para acessar o backup." @@ -2453,7 +2445,6 @@ "truncated_list_n_more": { "other": "E %(count)s mais..." }, - "unknown_device": "Dispositivo desconhecido", "update": { "changelog": "Registro de alterações", "check_action": "Verificar atualizações", diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index fef57bbef2b..50a3ee50e97 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -98,7 +98,6 @@ "report_content": "Пожаловаться на сообщение", "resend": "Переотправить", "reset": "Сброс", - "restore": "Восстановление", "resume": "Возобновить", "retry": "Попробуйте снова", "review": "Обзор", @@ -203,7 +202,6 @@ "failed_query_registration_methods": "Невозможно запросить поддерживаемые методы регистрации.", "failed_soft_logout_auth": "Ошибка повторной аутентификации", "failed_soft_logout_homeserver": "Ошибка повторной аутентификации из-за проблем на сервере", - "footer_powered_by_matrix": "основано на Matrix", "forgot_password_email_invalid": "Адрес электронной почты не является действительным.", "forgot_password_email_required": "Введите адрес электронной почты, связанный с вашей учётной записью.", "forgot_password_prompt": "Забыли Ваш пароль?", @@ -242,14 +240,11 @@ "phone_label": "Телефон", "phone_optional_label": "Телефон (не обязательно)", "qr_code_login": { - "approve_access_warning": "Разрешив доступ к этому устройству, оно получит полный доступ к вашей учетной записи.", "completing_setup": "Завершение настройки нового устройства", - "confirm_code_match": "Проверьте, чтобы код ниже совпадал с тем, что показан на другом устройстве:", "error_unexpected": "Произошла неожиданная ошибка.", "scan_code_instruction": "Отсканируйте приведенный ниже QR-код на устройстве, которое вышло из системы.", "scan_qr_code": "Сканировать QR-код", "select_qr_code": "Выберите '%(scanQRCode)s'", - "sign_in_new_device": "Войдите в систему c нового устройства", "waiting_for_device": "Ожидание входа устройства в систему" }, "register_action": "Создать учётную запись", @@ -891,7 +886,6 @@ }, "unable_to_setup_keys_error": "Невозможно настроить ключи", "unsupported": "Этот клиент не поддерживает сквозное шифрование.", - "upgrade_toast_title": "Доступно обновление шифрования", "verification": { "accepting": "Принятие…", "after_new_login": { @@ -2435,18 +2429,13 @@ "pass_phrase_match_failed": "Они не совпадают.", "pass_phrase_match_success": "Они совпадают!", "phrase_strong_enough": "Отлично! Эта контрольная фраза выглядит достаточно сильной.", - "requires_key_restore": "Восстановите резервную копию ключа для обновления шифрования", - "requires_password_confirmation": "Введите пароль своей учетной записи для подтверждения обновления:", - "requires_server_authentication": "Вам нужно будет пройти аутентификацию на сервере,чтобы подтвердить обновление.", "secret_storage_query_failure": "Невозможно запросить состояние секретного хранилища", "security_key_safety_reminder": "Храните ключ безопасности в надежном месте, например в менеджере паролей или сейфе, так как он используется для защиты ваших зашифрованных данных.", - "session_upgrade_description": "Модернизируйте этот сеанс, чтобы через него можно было подтвердить другие сеансы, предоставляя им доступ к зашифрованным сообщениям и помечая их как доверенные для других пользователей.", "set_phrase_again": "Задать другой пароль.", "settings_reminder": "Вы также можете настроить безопасное резервное копирование и управлять своими ключами в настройках.", "title_confirm_phrase": "Подтвердите секретную фразу", "title_save_key": "Сохраните свой ключ безопасности", "title_set_phrase": "Задайте секретную фразу", - "title_upgrade_encryption": "Обновите свое шифрование", "unable_to_setup": "Невозможно настроить секретное хранилище", "use_different_passphrase": "Использовать другую кодовую фразу?", "use_phrase_only_you_know": "Используйте секретную фразу, известную только вам, и при необходимости сохраните ключ безопасности для резервного копирования." @@ -3513,7 +3502,6 @@ "truncated_list_n_more": { "other": "Еще %(count)s…" }, - "unknown_device": "Неизвестное устройство", "unsupported_server_description": "На этом сервере используется старая версия Matrix. Перейдите на Matrix%(version)s, чтобы использовать %(brand)s ее без ошибок.", "unsupported_server_title": "Ваш сервер не поддерживается", "update": { diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json index d336a89bba7..34ba4789bf5 100644 --- a/src/i18n/strings/sk.json +++ b/src/i18n/strings/sk.json @@ -98,7 +98,6 @@ "report_content": "Nahlásiť obsah", "resend": "Poslať znovu", "reset": "Obnoviť predvolené", - "restore": "Obnoviť", "resume": "Pokračovať", "retry": "Skúsiť znovu", "review": "Skontrolovať", @@ -203,7 +202,6 @@ "failed_query_registration_methods": "Nie je možné požiadať o podporované metódy registrácie.", "failed_soft_logout_auth": "Nepodarilo sa opätovne overiť", "failed_soft_logout_homeserver": "Opätovná autentifikácia zlyhala kvôli problému domovského servera", - "footer_powered_by_matrix": "používa protokol Matrix", "forgot_password_email_invalid": "Zdá sa, že e-mailová adresa nie je platná.", "forgot_password_email_required": "Musíte zadať emailovú adresu prepojenú s vašim účtom.", "forgot_password_prompt": "Zabudli ste heslo?", @@ -242,14 +240,11 @@ "phone_label": "Telefón", "phone_optional_label": "Telefón (nepovinné)", "qr_code_login": { - "approve_access_warning": "Schválením prístupu pre toto zariadenie bude mať plný prístup k vášmu účtu.", "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:", "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", "select_qr_code": "Vyberte '%(scanQRCode)s'", - "sign_in_new_device": "Prihlásiť nové zariadenie", "waiting_for_device": "Čaká sa na prihlásenie zariadenia" }, "register_action": "Vytvoriť účet", @@ -891,7 +886,6 @@ }, "unable_to_setup_keys_error": "Nie je možné nastaviť kľúče", "unsupported": "Tento klient nepodporuje end-to-end šifrovanie.", - "upgrade_toast_title": "Je dostupná aktualizácia šifrovania", "verification": { "accepting": "Akceptovanie…", "after_new_login": { @@ -2438,18 +2432,13 @@ "pass_phrase_match_failed": "To sa nezhoduje.", "pass_phrase_match_success": "Zhoda!", "phrase_strong_enough": "Skvelé! Táto bezpečnostná fráza vyzerá dostatočne silná.", - "requires_key_restore": "Obnovte zálohu kľúča a aktualizujte šifrovanie", - "requires_password_confirmation": "Na potvrdenie aktualizácie zadajte heslo svojho účtu:", - "requires_server_authentication": "Na potvrdenie aktualizácie sa budete musieť overiť na serveri.", "secret_storage_query_failure": "Nie je možné vykonať dopyt na stav tajného úložiska", "security_key_safety_reminder": "Bezpečnostný kľúč uložte na bezpečné miesto, napríklad do správcu hesiel alebo trezora, pretože slúži na ochranu zašifrovaných údajov.", - "session_upgrade_description": "Aktualizujte túto reláciu, aby mohla overovať ostatné relácie, udeľovať im prístup k zašifrovaným správam a označovať ich ako dôveryhodné pre ostatných používateľov.", "set_phrase_again": "Vráťte sa späť a nastavte to znovu.", "settings_reminder": "Bezpečné zálohovanie a správu kľúčov môžete nastaviť aj v Nastaveniach.", "title_confirm_phrase": "Potvrdiť bezpečnostnú frázu", "title_save_key": "Uložte svoj bezpečnostný kľúč", "title_set_phrase": "Nastaviť bezpečnostnú frázu", - "title_upgrade_encryption": "Aktualizujte svoje šifrovanie", "unable_to_setup": "Nie je možné nastaviť tajné úložisko", "use_different_passphrase": "Použiť inú prístupovú frázu?", "use_phrase_only_you_know": "Použite tajnú frázu, ktorú poznáte len vy, a prípadne uložte si bezpečnostný kľúč, ktorý môžete použiť na zálohovanie." @@ -3542,7 +3531,6 @@ "truncated_list_n_more": { "other": "A %(count)s ďalších…" }, - "unknown_device": "Neznáme zariadenie", "unsupported_server_description": "Tento server používa staršiu verziu systému Matrix. Ak chcete používať %(brand)s bez chýb, aktualizujte na Matrix %(version)s.", "unsupported_server_title": "Váš server nie je podporovaný", "update": { diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index a8b484da54d..b7258c26cbf 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -94,7 +94,6 @@ "report_content": "Raportoni Lëndë", "resend": "Ridërgoje", "reset": "Rikthe te parazgjedhjet", - "restore": "Riktheje", "resume": "Rimerre", "retry": "Riprovo", "review": "Shqyrtojeni", @@ -196,7 +195,6 @@ "failed_query_registration_methods": "S’arrihet të kërkohet për metoda regjistrimi që mbulohen.", "failed_soft_logout_auth": "S’u arrit të ribëhej mirëfilltësimi", "failed_soft_logout_homeserver": "S’u arrit të ribëhej mirëfilltësimi, për shkak të një problemi me shërbyesin Home", - "footer_powered_by_matrix": "bazuar në Matrix", "forgot_password_email_invalid": "Adresa email s’duket të jetë e vlefshme.", "forgot_password_email_required": "Duhet dhënë adresa email e lidhur me llogarinë tuaj.", "forgot_password_prompt": "Harruat fjalëkalimin tuaj?", @@ -233,15 +231,12 @@ "phone_label": "Telefon", "phone_optional_label": "Telefoni (në daçi)", "qr_code_login": { - "approve_access_warning": "Duke miratuar hyrje për këtë pajisje, ajo do të ketë hyrje të plotë në llogarinë tuaj.", "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:", "error_rate_limited": "Shumë përpjekje në një kohë të shkurtër. Prisni ca, para se të riprovoni.", "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", "select_qr_code": "Përzgjidhni “%(scanQRCode)s”", - "sign_in_new_device": "Hyni në pajisje të re", "waiting_for_device": "Po pritet që të bëhet hyrja te pajisja" }, "register_action": "Krijoni Llogari", @@ -849,7 +844,6 @@ }, "unable_to_setup_keys_error": "S’arrihet të ujdisen kyçe", "unsupported": "Ky klient nuk mbulon fshehtëzim skaj-më-skaj.", - "upgrade_toast_title": "Ka të gatshëm përmirësim fshehtëzimi", "verification": { "accepting": "Po pranohet…", "after_new_login": { @@ -2303,18 +2297,13 @@ "pass_phrase_match_failed": "S’përputhen.", "pass_phrase_match_success": "U përputhën!", "phrase_strong_enough": "Bukur! Kjo Frazë Sigurie duket goxha e fuqishme.", - "requires_key_restore": "Që të përmirësoni fshehtëzimin tuaj, riktheni kopjeruajtjen e kyçeve tuaj", - "requires_password_confirmation": "Që të ripohohet përmirësimi, jepni fjalëkalimin e llogarisë tuaj:", - "requires_server_authentication": "Do t’ju duhet të bëni mirëfilltësimin me shërbyesin që të ripohohet përmirësimi.", "secret_storage_query_failure": "S’u arrit të merret gjendje depozite të fshehtë", "security_key_safety_reminder": "Depozitojeni Kyçin tuaj të Sigurisë diku të parrezik, bie fjala në një përgjegjës fjalëkalimesh, ose në një kasafortë, ngaqë përdoret për të mbrojtur të dhënat tuaja të fshehtëzuara.", - "session_upgrade_description": "Përmirësojeni këtë sesion për ta lejuar të verifikojë sesione të tjerë, duke u akorduar hyrje te mesazhe të fshehtëzuar dhe duke u vënë shenjë si të besuar për përdorues të tjerë.", "set_phrase_again": "Shkoni mbrapsht që ta ricaktoni.", "settings_reminder": "Mundeni edhe të ujdisni Kopjeruajtje të Sigurt & administroni kyçet tuaj që nga Rregullimet.", "title_confirm_phrase": "Ripohoni Frazë Sigurie", "title_save_key": "Ruani Kyçin tuaj të Sigurisë", "title_set_phrase": "Caktoni një Frazë Sigurie", - "title_upgrade_encryption": "Përmirësoni fshehtëzimin tuaj", "unable_to_setup": "S’u arrit të ujdiset depozitë e fshehtë", "use_different_passphrase": "Të përdoret një frazëkalim tjetër?", "use_phrase_only_you_know": "Jepni një frazë të fshehtë që e dini vetëm ju, dhe, në daçi, ruani një Kyç Sigurie për ta përdorur për kopjeruajtje." @@ -3308,7 +3297,6 @@ "truncated_list_n_more": { "other": "Dhe %(count)s të tjerë…" }, - "unknown_device": "Pajisje e panjohur", "update": { "changelog": "Regjistër ndryshimesh", "check_action": "Kontrollo për përditësime", diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json index 4be1a8df938..bb4e489952a 100644 --- a/src/i18n/strings/sv.json +++ b/src/i18n/strings/sv.json @@ -103,7 +103,6 @@ "report_content": "Rapportera innehåll", "resend": "Skicka igen", "reset": "Återställ", - "restore": "Återställ", "resume": "Återuppta", "retry": "Försök igen", "review": "Granska", @@ -208,7 +207,6 @@ "failed_query_registration_methods": "Kunde inte fråga efter stödda registreringsmetoder.", "failed_soft_logout_auth": "Misslyckades att återautentisera", "failed_soft_logout_homeserver": "Misslyckades att återautentisera p.g.a. ett hemserverproblem", - "footer_powered_by_matrix": "drivs av Matrix", "forgot_password_email_invalid": "Den här e-postadressen ser inte giltig ut.", "forgot_password_email_required": "E-postadressen som är kopplad till ditt konto måste anges.", "forgot_password_prompt": "Glömt ditt lösenord?", @@ -247,15 +245,12 @@ "phone_label": "Telefon", "phone_optional_label": "Telefon (valfritt)", "qr_code_login": { - "approve_access_warning": "Genom att godkänna åtkomst för den här enheten så får den full åtkomst till ditt konto.", "completing_setup": "Slutför inställning av din nya enhet", - "confirm_code_match": "Kolla att koden nedan matchar din andra enhet:", "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_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", "select_qr_code": "Välj '%(scanQRCode)s'", - "sign_in_new_device": "Logga in ny enhet", "waiting_for_device": "Väntar på att enheter loggar in" }, "register_action": "Skapa konto", @@ -896,7 +891,6 @@ }, "unable_to_setup_keys_error": "Kunde inte ställa in nycklar", "unsupported": "Den här klienten stöder inte totalsträckskryptering.", - "upgrade_toast_title": "Krypteringsuppgradering tillgänglig", "verification": { "accepting": "Accepterar…", "after_new_login": { @@ -2450,18 +2444,13 @@ "pass_phrase_match_failed": "Det matchar inte.", "pass_phrase_match_success": "Det matchar!", "phrase_strong_enough": "Fantastiskt! Den här säkerhetsfrasen ser tillräckligt stark ut.", - "requires_key_restore": "Återställ din nyckelsäkerhetskopia för att uppgradera din kryptering", - "requires_password_confirmation": "Ange ditt kontolösenord för att bekräfta uppgraderingen:", - "requires_server_authentication": "Du kommer behöva autentisera mot servern för att bekräfta uppgraderingen.", "secret_storage_query_failure": "Kunde inte fråga efter status på hemlig lagring", "security_key_safety_reminder": "Lagra din säkerhetsnyckel någonstans säkert, som en lösenordshanterare eller ett kassaskåp, eftersom den används för att säkra din krypterade data.", - "session_upgrade_description": "Uppgradera den här sessionen för att låta den verifiera andra sessioner, för att ge dem åtkomst till krypterade meddelanden och markera dem som betrodda för andra användare.", "set_phrase_again": "Gå tillbaka och sätt den igen.", "settings_reminder": "Du kan även ställa in säker säkerhetskopiering och hantera dina nycklar i inställningarna.", "title_confirm_phrase": "Bekräfta säkerhetsfras", "title_save_key": "Spara din säkerhetsnyckel", "title_set_phrase": "Sätt en säkerhetsfras", - "title_upgrade_encryption": "Uppgradera din kryptering", "unable_to_setup": "Kunde inte sätta upp hemlig lagring", "use_different_passphrase": "Använd en annan lösenfras?", "use_phrase_only_you_know": "Använd en hemlig fras endast du känner till, och spara valfritt en säkerhetsnyckel att använda för säkerhetskopiering." @@ -3528,7 +3517,6 @@ "truncated_list_n_more": { "other": "Och %(count)s till…" }, - "unknown_device": "Okänd enhet", "unsupported_server_description": "Servern använder en äldre version av Matrix. Uppgradera till Matrix %(version)s för att använda %(brand)s utan fel.", "unsupported_server_title": "Din server stöds inte", "update": { diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json index f5652b6b444..7d438ec2a41 100644 --- a/src/i18n/strings/uk.json +++ b/src/i18n/strings/uk.json @@ -98,7 +98,6 @@ "report_content": "Поскаржитись на вміст", "resend": "Перенадіслати", "reset": "Скинути", - "restore": "Відновити", "resume": "Продовжити", "retry": "Повторити спробу", "review": "Переглянути", @@ -202,7 +201,6 @@ "failed_query_registration_methods": "Не вдалося запитати підтримувані способи реєстрації.", "failed_soft_logout_auth": "Не вдалося перезайти", "failed_soft_logout_homeserver": "Не вдалося перезайти через проблему з домашнім сервером", - "footer_powered_by_matrix": "працює на Matrix", "forgot_password_email_invalid": "Хибна адреса е-пошти.", "forgot_password_email_required": "Введіть е-пошту, прив'язану до вашого облікового запису.", "forgot_password_prompt": "Забули свій пароль?", @@ -239,15 +237,12 @@ "phone_label": "Телефон", "phone_optional_label": "Телефон (не обов'язково)", "qr_code_login": { - "approve_access_warning": "Затвердивши доступ для цього пристрою, ви надасте йому повний доступ до вашого облікового запису.", "completing_setup": "Завершення налаштування нового пристрою", - "confirm_code_match": "Перевірте, чи збігається наведений внизу код з кодом на вашому іншому пристрої:", "error_rate_limited": "Забагато спроб за короткий час. Зачекайте трохи, перш ніж повторити спробу.", "error_unexpected": "Виникла непередбачувана помилка.", "scan_code_instruction": "Скануйте QR-код знизу своїм пристроєм, на якому ви вийшли.", "scan_qr_code": "Скануйте QR-код", "select_qr_code": "Виберіть «%(scanQRCode)s»", - "sign_in_new_device": "Увійти на новому пристрої", "waiting_for_device": "Очікування входу з пристрою" }, "register_action": "Створити обліковий запис", @@ -878,7 +873,6 @@ }, "unable_to_setup_keys_error": "Не вдалося налаштувати ключі", "unsupported": "Цей клієнт не підтримує наскрізного шифрування.", - "upgrade_toast_title": "Доступне поліпшене шифрування", "verification": { "accepting": "Прийняття…", "after_new_login": { @@ -2376,18 +2370,13 @@ "pass_phrase_match_failed": "Не збігається.", "pass_phrase_match_success": "Збіг!", "phrase_strong_enough": "Чудово! Фраза безпеки досить надійна.", - "requires_key_restore": "Відновіть резервну копію вашого ключа, щоб поліпшити шифрування", - "requires_password_confirmation": "Введіть пароль вашого облікового запису щоб підтвердити поліпшення:", - "requires_server_authentication": "Ви матимете пройти розпізнання на сервері, щоб підтвердити поліпшення.", "secret_storage_query_failure": "Не вдалося дізнатися стан таємного сховища", "security_key_safety_reminder": "Зберігайте ключ безпеки в надійному місці, скажімо в менеджері паролів чи сейфі, бо ключ оберігає ваші зашифровані дані.", - "session_upgrade_description": "Поліпшіть цей сеанс, щоб уможливити звірення інших сеансів, надаючи їм доступ до зашифрованих повідомлень та позначаючи їх довіреними для інших користувачів.", "set_phrase_again": "Поверніться, щоб налаштувати заново.", "settings_reminder": "Ввімкнути захищене резервне копіювання й керувати своїми ключами можна в налаштуваннях.", "title_confirm_phrase": "Підвердьте фразу безпеки", "title_save_key": "Збережіть свій ключ безпеки", "title_set_phrase": "Вкажіть фразу безпеки", - "title_upgrade_encryption": "Поліпшити ваше шифрування", "unable_to_setup": "Не вдалося налаштувати таємне сховище", "use_different_passphrase": "Використати іншу парольну фразу?", "use_phrase_only_you_know": "Захистіть резервну копію відомою лише вам таємною фразою. Можете також зберегти ключ безпеки." @@ -3440,7 +3429,6 @@ "truncated_list_n_more": { "other": "І ще %(count)s..." }, - "unknown_device": "Невідомий пристрій", "unsupported_server_description": "Цей сервер використовує стару версію Matrix. Оновіть Matrix до %(version)s, щоб використовувати %(brand)s без помилок.", "unsupported_server_title": "Ваш сервер не підтримується", "update": { diff --git a/src/i18n/strings/vi.json b/src/i18n/strings/vi.json index 7f92c668203..5ce36aed05f 100644 --- a/src/i18n/strings/vi.json +++ b/src/i18n/strings/vi.json @@ -97,7 +97,6 @@ "report_content": "Báo cáo nội dung", "resend": "Gửi lại", "reset": "Cài lại", - "restore": "Khôi phục", "resume": "Tiếp tục", "retry": "Thử lại", "review": "Xem xét", @@ -196,7 +195,6 @@ "failed_query_registration_methods": "Không thể truy vấn các phương pháp đăng ký được hỗ trợ.", "failed_soft_logout_auth": "Không xác thực lại được", "failed_soft_logout_homeserver": "Không xác thực lại được do sự cố máy chủ", - "footer_powered_by_matrix": "cung cấp bởi Matrix", "forgot_password_email_invalid": "Địa chỉ thư điện tử dường như không hợp lệ.", "forgot_password_email_required": "Địa chỉ thư điện tử được liên kết đến tài khoản của bạn phải được nhập.", "forgot_password_prompt": "Quên mật khẩu của bạn?", @@ -803,7 +801,6 @@ }, "unable_to_setup_keys_error": "Không thể thiết lập khóa", "unsupported": "Ứng dụng khách này không hỗ trợ mã hóa đầu cuối.", - "upgrade_toast_title": "Nâng cấp mã hóa có sẵn", "verification": { "accepting": "Đang chấp nhận…", "after_new_login": { @@ -2189,18 +2186,13 @@ "pass_phrase_match_failed": "Điều đó không phù hợp.", "pass_phrase_match_success": "Điều đó phù hợp!", "phrase_strong_enough": "Tuyệt vời! Cụm từ bảo mật này trông đủ mạnh.", - "requires_key_restore": "Khôi phục bản sao lưu khóa của bạn để nâng cấp mã hóa của bạn", - "requires_password_confirmation": "Nhập mật khẩu tài khoản của bạn để xác nhận nâng cấp:", - "requires_server_authentication": "Bạn sẽ cần xác thực với máy chủ để xác nhận nâng cấp.", "secret_storage_query_failure": "Không thể truy vấn trạng thái lưu trữ bí mật", "security_key_safety_reminder": "Lưu trữ Khóa bảo mật của bạn ở nơi an toàn, như trình quản lý mật khẩu hoặc két sắt, vì nó được sử dụng để bảo vệ dữ liệu được mã hóa của bạn.", - "session_upgrade_description": "Nâng cấp phiên này để cho phép nó xác thực các phiên khác, cấp cho họ quyền truy cập vào các thư được mã hóa và đánh dấu chúng là đáng tin cậy đối với những người dùng khác.", "set_phrase_again": "Quay lại để thiết lập lại.", "settings_reminder": "Bạn cũng có thể thiết lập Sao lưu bảo mật và quản lý khóa của mình trong Cài đặt.", "title_confirm_phrase": "Xác nhận cụm từ bảo mật", "title_save_key": "Lưu Khóa Bảo mật của bạn", "title_set_phrase": "Đặt Cụm từ Bảo mật", - "title_upgrade_encryption": "Nâng cấp mã hóa của bạn", "unable_to_setup": "Không thể thiết lập bộ nhớ bí mật", "use_different_passphrase": "Sử dụng một cụm mật khẩu khác?", "use_phrase_only_you_know": "Sử dụng một cụm từ bí mật mà chỉ bạn biết và tùy chọn lưu Khóa bảo mật để sử dụng để sao lưu." @@ -3180,7 +3172,6 @@ "truncated_list_n_more": { "other": "Và %(count)s thêm…" }, - "unknown_device": "Thiết bị không xác định", "update": { "changelog": "Lịch sử thay đổi", "check_action": "Kiểm tra cập nhật", diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json index a9e1df778a9..99d5586a5cc 100644 --- a/src/i18n/strings/zh_Hans.json +++ b/src/i18n/strings/zh_Hans.json @@ -98,7 +98,6 @@ "report_content": "举报内容", "resend": "重新发送", "reset": "重置", - "restore": "恢复", "resume": "恢复", "retry": "重试", "review": "开始验证", @@ -203,7 +202,6 @@ "failed_query_registration_methods": "无法查询支持的注册方法。", "failed_soft_logout_auth": "重新认证失败", "failed_soft_logout_homeserver": "由于家服务器的问题,重新认证失败", - "footer_powered_by_matrix": "由 Matrix 驱动", "forgot_password_email_invalid": "电子邮件地址似乎无效。", "forgot_password_email_required": "必须输入和你账户关联的邮箱地址。", "forgot_password_prompt": "忘记你的密码了吗?", @@ -242,9 +240,7 @@ "phone_label": "电话", "phone_optional_label": "电话号码(可选)", "qr_code_login": { - "approve_access_warning": "为此设备批准访问权限后,它对你的帐户有完全的访问权限。", - "completing_setup": "完成新设备的设置", - "confirm_code_match": "检查以下代码是否与你的其他设备匹配:" + "completing_setup": "完成新设备的设置" }, "register_action": "创建账户", "registration": { @@ -822,7 +818,6 @@ }, "unable_to_setup_keys_error": "无法设置密钥", "unsupported": "此客户端不支持端到端加密。", - "upgrade_toast_title": "提供加密升级", "verification": { "accepting": "正在接受……", "after_new_login": { @@ -2203,18 +2198,13 @@ "pass_phrase_match_failed": "不匹配。", "pass_phrase_match_success": "匹配成功!", "phrase_strong_enough": "棒!这个安全短语看着够强。", - "requires_key_restore": "恢复你的密钥备份以更新你的加密方式", - "requires_password_confirmation": "输入你的账户密码以确认升级:", - "requires_server_authentication": "你需要和服务器进行认证以确认更新。", "secret_storage_query_failure": "无法查询秘密存储状态", "security_key_safety_reminder": "将您的安全密钥存放在安全的地方,例如密码管理器或保险箱,因为它用于保护您的加密数据。", - "session_upgrade_description": "更新此会话以允许其验证其他会话、允许其他会话访问加密消息,并将它们对别的用户标记为已信任。", "set_phrase_again": "返回重新设置。", "settings_reminder": "你也可以在设置中设置安全备份并管理你的密钥。", "title_confirm_phrase": "确认安全密码", "title_save_key": "保存你的安全密钥", "title_set_phrase": "设置一个安全密码", - "title_upgrade_encryption": "更新你的加密方法", "unable_to_setup": "无法设置秘密存储", "use_different_passphrase": "使用不同的口令词组?", "use_phrase_only_you_know": "使用一个只有你知道的密码,你也可以保存安全密钥以供备份使用。" @@ -3162,7 +3152,6 @@ "truncated_list_n_more": { "other": "和 %(count)s 个其他…" }, - "unknown_device": "未知设备", "update": { "changelog": "更改日志", "check_action": "检查更新", diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index f5e8b0926c5..68b3694ee8c 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -98,7 +98,6 @@ "report_content": "回報內容", "resend": "重新傳送", "reset": "重設", - "restore": "還原", "resume": "繼續", "retry": "重試", "review": "評論", @@ -202,7 +201,6 @@ "failed_query_registration_methods": "無法查詢支援的註冊方法。", "failed_soft_logout_auth": "無法重新驗證", "failed_soft_logout_homeserver": "因為家伺服器的問題,所以無法重新驗證", - "footer_powered_by_matrix": "由 Matrix 提供", "forgot_password_email_invalid": "電子郵件地址似乎無效。", "forgot_password_email_required": "必須輸入和您帳號綁定的電子郵件地址。", "forgot_password_prompt": "忘記您的密碼了?", @@ -239,15 +237,12 @@ "phone_label": "電話", "phone_optional_label": "電話(選擇性)", "qr_code_login": { - "approve_access_warning": "透過批准此裝置的存取權限,其將對您的帳號有完全的存取權限。", "completing_setup": "完成您新裝置的設定", - "confirm_code_match": "請確認下列代碼與您另一台裝置上的代碼相符:", "error_rate_limited": "短時間內嘗試太多次,請稍待一段時間後再嘗試。", "error_unexpected": "發生預料之外的錯誤。", "scan_code_instruction": "請用您已登出的裝置掃描下列 QR Code。", "scan_qr_code": "掃描 QR Code", "select_qr_code": "選取「%(scanQRCode)s」", - "sign_in_new_device": "登入新裝置", "waiting_for_device": "正在等待裝置登入" }, "register_action": "建立帳號", @@ -879,7 +874,6 @@ }, "unable_to_setup_keys_error": "無法設定金鑰", "unsupported": "此客戶端不支援端對端加密。", - "upgrade_toast_title": "已提供加密升級", "verification": { "accepting": "正在接受…", "after_new_login": { @@ -2378,18 +2372,13 @@ "pass_phrase_match_failed": "不相符。", "pass_phrase_match_success": "相符!", "phrase_strong_enough": "很好!此安全密語看起來夠強。", - "requires_key_restore": "復原您的金鑰備份以升級您的加密", - "requires_password_confirmation": "輸入您的帳號密碼以確認升級:", - "requires_server_authentication": "您必須透過伺服器驗證以確認升級。", "secret_storage_query_failure": "無法查詢秘密儲存空間狀態", "security_key_safety_reminder": "由於安全金鑰是用來保護您的加密資料,請將其儲存在安全的地方,例如密碼管理員或保險箱等。", - "session_upgrade_description": "升級此工作階段以驗證其他工作階段,給予其他工作階段存取加密訊息的權限,並為其他使用者標記它們為受信任。", "set_phrase_again": "返回重新設定。", "settings_reminder": "您也可以在設定中設定安全備份並管理您的金鑰。", "title_confirm_phrase": "確認安全密語", "title_save_key": "儲存您的安全金鑰", "title_set_phrase": "設定安全密語", - "title_upgrade_encryption": "升級您的加密", "unable_to_setup": "無法設定秘密資訊儲存空間", "use_different_passphrase": "使用不同的安全密語?", "use_phrase_only_you_know": "使用僅有您知道的安全密語,也可再儲存安全金鑰作為備份。" @@ -3431,7 +3420,6 @@ "truncated_list_n_more": { "other": "與更多 %(count)s 個…" }, - "unknown_device": "未知裝置", "unsupported_server_description": "此伺服器正在使用較舊版本的 Matrix。升級至 Matrix %(version)s 以在沒有錯誤的情況下使用 %(brand)s。", "unsupported_server_title": "您的伺服器不支援", "update": { diff --git a/src/mjolnir/Mjolnir.ts b/src/mjolnir/Mjolnir.ts index 8c9d9399084..9dacc0d41ff 100644 --- a/src/mjolnir/Mjolnir.ts +++ b/src/mjolnir/Mjolnir.ts @@ -22,12 +22,12 @@ import { Action } from "../dispatcher/actions"; // TODO: Move this and related files to the js-sdk or something once finalized. export class Mjolnir { - private static instance: Mjolnir | null = null; + private static instance?: Mjolnir; private _lists: BanList[] = []; // eslint-disable-line @typescript-eslint/naming-convention private _roomIds: string[] = []; // eslint-disable-line @typescript-eslint/naming-convention - private mjolnirWatchRef: string | null = null; - private dispatcherRef: string | null = null; + private mjolnirWatchRef?: string; + private dispatcherRef?: string; public get roomIds(): string[] { return this._roomIds; @@ -61,15 +61,11 @@ export class Mjolnir { } public stop(): void { - if (this.mjolnirWatchRef) { - SettingsStore.unwatchSetting(this.mjolnirWatchRef); - this.mjolnirWatchRef = null; - } + SettingsStore.unwatchSetting(this.mjolnirWatchRef); + this.mjolnirWatchRef = undefined; - if (this.dispatcherRef) { - dis.unregister(this.dispatcherRef); - this.dispatcherRef = null; - } + dis.unregister(this.dispatcherRef); + this.dispatcherRef = undefined; MatrixClientPeg.get()?.removeListener(RoomStateEvent.Events, this.onEvent); } diff --git a/src/models/Call.ts b/src/models/Call.ts index 0238d159145..4beb5fccc1e 100644 --- a/src/models/Call.ts +++ b/src/models/Call.ts @@ -643,8 +643,8 @@ export class ElementCall extends Call { public static readonly MEMBER_EVENT_TYPE = new NamespacedValue(null, EventType.GroupCallMemberPrefix); public readonly STUCK_DEVICE_TIMEOUT_MS = 1000 * 60 * 60; // 1 hour - private settingsStoreCallEncryptionWatcher: string | null = null; - private terminationTimer: number | null = null; + private settingsStoreCallEncryptionWatcher?: string; + private terminationTimer?: number; private _layout = Layout.Tile; public get layout(): Layout { return this._layout; @@ -938,13 +938,9 @@ export class ElementCall extends Call { this.session.off(MatrixRTCSessionEvent.MembershipsChanged, this.onMembershipChanged); this.client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionEnded, this.onRTCSessionEnded); - if (this.settingsStoreCallEncryptionWatcher) { - SettingsStore.unwatchSetting(this.settingsStoreCallEncryptionWatcher); - } - if (this.terminationTimer !== null) { - clearTimeout(this.terminationTimer); - this.terminationTimer = null; - } + SettingsStore.unwatchSetting(this.settingsStoreCallEncryptionWatcher); + clearTimeout(this.terminationTimer); + this.terminationTimer = undefined; super.destroy(); } diff --git a/src/performance/entry-names.ts b/src/performance/entry-names.ts index 331930cb1e7..13953ebf324 100644 --- a/src/performance/entry-names.ts +++ b/src/performance/entry-names.ts @@ -11,38 +11,12 @@ export enum PerformanceEntryNames { * Application wide */ - APP_STARTUP = "mx_AppStartup", PAGE_CHANGE = "mx_PageChange", - /** - * Events - */ - - RESEND_EVENT = "mx_ResendEvent", - SEND_E2EE_EVENT = "mx_SendE2EEEvent", - SEND_ATTACHMENT = "mx_SendAttachment", - - /** - * Rooms - */ - - SWITCH_ROOM = "mx_SwithRoom", - JUMP_TO_ROOM = "mx_JumpToRoom", - JOIN_ROOM = "mx_JoinRoom", // ✅ - CREATE_DM = "mx_CreateDM", // ✅ - PEEK_ROOM = "mx_PeekRoom", - /** * User */ - VERIFY_E2EE_USER = "mx_VerifyE2EEUser", // ✅ LOGIN = "mx_Login", // ✅ REGISTER = "mx_Register", // ✅ - - /** - * VoIP - */ - - SETUP_VOIP_CALL = "mx_SetupVoIPCall", } diff --git a/src/sentry.ts b/src/sentry.ts index c454fed7edd..c70201679c0 100644 --- a/src/sentry.ts +++ b/src/sentry.ts @@ -8,7 +8,6 @@ Please see LICENSE files in the repository root for full details. import * as Sentry from "@sentry/browser"; import { MatrixClient } from "matrix-js-sdk/src/matrix"; -import { type Integration } from "@sentry/types/build/types/integration"; import SdkConfig from "./SdkConfig"; import { MatrixClientPeg } from "./MatrixClientPeg"; @@ -196,7 +195,7 @@ export function setSentryUser(mxid: string): void { export async function initSentry(sentryConfig: IConfigOptions["sentry"]): Promise { if (!sentryConfig) return; // Only enable Integrations.GlobalHandlers, which hooks uncaught exceptions, if automaticErrorReporting is true - const integrations: Integration[] = [ + const integrations = [ Sentry.inboundFiltersIntegration(), Sentry.functionToStringIntegration(), Sentry.breadcrumbsIntegration(), diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts index 45ba4e3dbb9..98ae347a0ab 100644 --- a/src/settings/SettingsStore.ts +++ b/src/settings/SettingsStore.ts @@ -192,10 +192,11 @@ export default class SettingsStore { /** * Stops the SettingsStore from watching a setting. This is a no-op if the watcher * provided is not found. - * @param {string} watcherReference The watcher reference (received from #watchSetting) - * to cancel. + * @param watcherReference The watcher reference (received from #watchSetting) to cancel. + * Can be undefined to avoid needing an if around every caller. */ - public static unwatchSetting(watcherReference: string): void { + public static unwatchSetting(watcherReference: string | undefined): void { + if (!watcherReference) return; if (!SettingsStore.watchers.has(watcherReference)) { logger.warn(`Ending non-existent watcher ID ${watcherReference}`); return; diff --git a/src/settings/watchers/FontWatcher.ts b/src/settings/watchers/FontWatcher.ts index 0c7e5965307..64a6a27f581 100644 --- a/src/settings/watchers/FontWatcher.ts +++ b/src/settings/watchers/FontWatcher.ts @@ -28,11 +28,7 @@ export class FontWatcher implements IWatcher { */ public static readonly DEFAULT_DELTA = 0; - private dispatcherRef: string | null; - - public constructor() { - this.dispatcherRef = null; - } + private dispatcherRef?: string; public async start(): Promise { this.updateFont(); @@ -148,7 +144,6 @@ export class FontWatcher implements IWatcher { } public stop(): void { - if (!this.dispatcherRef) return; dis.unregister(this.dispatcherRef); } diff --git a/src/settings/watchers/ThemeWatcher.ts b/src/settings/watchers/ThemeWatcher.ts index 74a3158c62b..d0f00c52d9a 100644 --- a/src/settings/watchers/ThemeWatcher.ts +++ b/src/settings/watchers/ThemeWatcher.ts @@ -18,9 +18,9 @@ import { ActionPayload } from "../../dispatcher/payloads"; import { SettingLevel } from "../SettingLevel"; export default class ThemeWatcher { - private themeWatchRef: string | null; - private systemThemeWatchRef: string | null; - private dispatcherRef: string | null; + private themeWatchRef?: string; + private systemThemeWatchRef?: string; + private dispatcherRef?: string; private preferDark: MediaQueryList; private preferLight: MediaQueryList; @@ -29,10 +29,6 @@ export default class ThemeWatcher { private currentTheme: string; public constructor() { - this.themeWatchRef = null; - this.systemThemeWatchRef = null; - this.dispatcherRef = null; - // we have both here as each may either match or not match, so by having both // we can get the tristate of dark/light/unsupported this.preferDark = (global).matchMedia("(prefers-color-scheme: dark)"); @@ -55,9 +51,9 @@ export default class ThemeWatcher { this.preferDark.removeEventListener("change", this.onChange); this.preferLight.removeEventListener("change", this.onChange); this.preferHighContrast.removeEventListener("change", this.onChange); - if (this.systemThemeWatchRef) SettingsStore.unwatchSetting(this.systemThemeWatchRef); - if (this.themeWatchRef) SettingsStore.unwatchSetting(this.themeWatchRef); - if (this.dispatcherRef) dis.unregister(this.dispatcherRef); + SettingsStore.unwatchSetting(this.systemThemeWatchRef); + SettingsStore.unwatchSetting(this.themeWatchRef); + dis.unregister(this.dispatcherRef); } private onChange = (): void => { diff --git a/src/stores/AsyncStore.ts b/src/stores/AsyncStore.ts index 5697969a1d9..4baf8072473 100644 --- a/src/stores/AsyncStore.ts +++ b/src/stores/AsyncStore.ts @@ -65,7 +65,7 @@ export abstract class AsyncStore extends EventEmitter { * Stops the store's listening functions, such as the listener to the dispatcher. */ protected stop(): void { - if (this.dispatcherRef) this.dispatcher.unregister(this.dispatcherRef); + this.dispatcher.unregister(this.dispatcherRef); } /** diff --git a/src/stores/AsyncStoreWithClient.ts b/src/stores/AsyncStoreWithClient.ts index 418143a16c7..a131614c7c1 100644 --- a/src/stores/AsyncStoreWithClient.ts +++ b/src/stores/AsyncStoreWithClient.ts @@ -23,7 +23,7 @@ export abstract class AsyncStoreWithClient extends AsyncStore< const asyncStore = this; // eslint-disable-line @typescript-eslint/no-this-alias this.readyStore = new (class extends ReadyWatchingStore { public get mxClient(): MatrixClient | null { - return this.matrixClient; + return this.matrixClient ?? null; } protected async onReady(): Promise { diff --git a/src/stores/OwnBeaconStore.ts b/src/stores/OwnBeaconStore.ts index f60dae07fe5..ccd4bf33a3d 100644 --- a/src/stores/OwnBeaconStore.ts +++ b/src/stores/OwnBeaconStore.ts @@ -142,7 +142,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient { this.matrixClient.removeListener(BeaconEvent.Destroy, this.onDestroyBeacon); this.matrixClient.removeListener(RoomStateEvent.Members, this.onRoomStateMembers); } - SettingsStore.unwatchSetting(this.dynamicWatcherRef ?? ""); + SettingsStore.unwatchSetting(this.dynamicWatcherRef); this.clearBeacons(); } diff --git a/src/stores/ReadyWatchingStore.ts b/src/stores/ReadyWatchingStore.ts index a46a09899af..922e8b83936 100644 --- a/src/stores/ReadyWatchingStore.ts +++ b/src/stores/ReadyWatchingStore.ts @@ -16,8 +16,8 @@ import { Action } from "../dispatcher/actions"; import { MatrixDispatcher } from "../dispatcher/dispatcher"; export abstract class ReadyWatchingStore extends EventEmitter implements IDestroyable { - protected matrixClient: MatrixClient | null = null; - private dispatcherRef: string | null = null; + protected matrixClient?: MatrixClient; + private dispatcherRef?: string; public constructor(protected readonly dispatcher: MatrixDispatcher) { super(); @@ -35,7 +35,7 @@ export abstract class ReadyWatchingStore extends EventEmitter implements IDestro } public get mxClient(): MatrixClient | null { - return this.matrixClient; // for external readonly access + return this.matrixClient ?? null; // for external readonly access } public useUnitTestClient(cli: MatrixClient): void { @@ -43,7 +43,7 @@ export abstract class ReadyWatchingStore extends EventEmitter implements IDestro } public destroy(): void { - if (this.dispatcherRef !== null) this.dispatcher.unregister(this.dispatcherRef); + this.dispatcher.unregister(this.dispatcherRef); } protected async onReady(): Promise { @@ -80,7 +80,7 @@ export abstract class ReadyWatchingStore extends EventEmitter implements IDestro } else if (payload.action === "on_client_not_viable" || payload.action === Action.OnLoggedOut) { if (this.matrixClient) { await this.onNotReady(); - this.matrixClient = null; + this.matrixClient = undefined; } } }; diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index e5c46e6a6af..8362f1048a0 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -154,7 +154,10 @@ export class StopGapWidget extends EventEmitter { private kind: WidgetKind; private readonly virtual: boolean; private readUpToMap: { [roomId: string]: string } = {}; // room ID to event ID - private stickyPromise?: () => Promise; // This promise will be called and needs to resolve before the widget will actually become sticky. + // This promise will be called and needs to resolve before the widget will actually become sticky. + private stickyPromise?: () => Promise; + // Holds events that should be fed to the widget once they finish decrypting + private readonly eventsToFeed = new WeakSet(); public constructor(private appTileProps: IAppTileProps) { super(); @@ -465,12 +468,10 @@ export class StopGapWidget extends EventEmitter { private onEvent = (ev: MatrixEvent): void => { this.client.decryptEventIfNeeded(ev); - if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return; this.feedEvent(ev); }; private onEventDecrypted = (ev: MatrixEvent): void => { - if (ev.isDecryptionFailure()) return; this.feedEvent(ev); }; @@ -480,72 +481,103 @@ export class StopGapWidget extends EventEmitter { await this.messaging?.feedToDevice(ev.getEffectiveEvent() as IRoomEvent, ev.isEncrypted()); }; - private feedEvent(ev: MatrixEvent): void { - if (!this.messaging) return; - - // Check to see if this event would be before or after our "read up to" marker. If it's - // before, or we can't decide, then we assume the widget will have already seen the event. - // If the event is after, or we don't have a marker for the room, then we'll send it through. - // - // This approach of "read up to" prevents widgets receiving decryption spam from startup or - // receiving out-of-order events from backfill and such. - // - // Skip marker timeline check for events with relations to unknown parent because these - // events are not added to the timeline here and will be ignored otherwise: - // https://github.com/matrix-org/matrix-js-sdk/blob/d3dfcd924201d71b434af3d77343b5229b6ed75e/src/models/room.ts#L2207-L2213 - let isRelationToUnknown: boolean | undefined = undefined; - const upToEventId = this.readUpToMap[ev.getRoomId()!]; - if (upToEventId) { - // Small optimization for exact match (prevent search) - if (upToEventId === ev.getId()) { - return; - } + /** + * Determines whether the event has a relation to an unknown parent. + */ + private relatesToUnknown(ev: MatrixEvent): boolean { + // Replies to unknown events don't count + if (!ev.relationEventId || ev.replyEventId) return false; + const room = this.client.getRoom(ev.getRoomId()); + return room === null || !room.findEventById(ev.relationEventId); + } - // should be true to forward the event to the widget - let shouldForward = false; - - const room = this.client.getRoom(ev.getRoomId()!); - if (!room) return; - // Timelines are most recent last, so reverse the order and limit ourselves to 100 events - // to avoid overusing the CPU. - const timeline = room.getLiveTimeline(); - const events = arrayFastClone(timeline.getEvents()).reverse().slice(0, 100); - - for (const timelineEvent of events) { - if (timelineEvent.getId() === upToEventId) { - break; - } else if (timelineEvent.getId() === ev.getId()) { - shouldForward = true; - break; - } - } + /** + * Determines whether the event comes from a room that we've been invited to + * (in which case we likely don't have the full timeline). + */ + private isFromInvite(ev: MatrixEvent): boolean { + const room = this.client.getRoom(ev.getRoomId()); + return room?.getMyMembership() === KnownMembership.Invite; + } - if (!shouldForward) { - // checks that the event has a relation to unknown event - isRelationToUnknown = - !ev.replyEventId && !!ev.relationEventId && !room.findEventById(ev.relationEventId); - if (!isRelationToUnknown) { - // Ignore the event: it is before our interest. - return; - } - } + /** + * Advances the "read up to" marker for a room to a certain event. No-ops if + * the event is before the marker. + * @returns Whether the "read up to" marker was advanced. + */ + private advanceReadUpToMarker(ev: MatrixEvent): boolean { + const evId = ev.getId(); + if (evId === undefined) return false; + const roomId = ev.getRoomId(); + if (roomId === undefined) return false; + const room = this.client.getRoom(roomId); + if (room === null) return false; + + const upToEventId = this.readUpToMap[ev.getRoomId()!]; + if (!upToEventId) { + // There's no marker yet; start it at this event + this.readUpToMap[roomId] = evId; + return true; } - // Skip marker assignment if membership is 'invite', otherwise 'm.room.member' from - // invitation room will assign it and new state events will be not forwarded to the widget - // because of empty timeline for invitation room and assigned marker. - const evRoomId = ev.getRoomId(); - const evId = ev.getId(); - if (evRoomId && evId) { - const room = this.client.getRoom(evRoomId); - if (room && room.getMyMembership() === KnownMembership.Join && !isRelationToUnknown) { - this.readUpToMap[evRoomId] = evId; + // Small optimization for exact match (skip the search) + if (upToEventId === evId) return false; + + // Timelines are most recent last, so reverse the order and limit ourselves to 100 events + // to avoid overusing the CPU. + const timeline = room.getLiveTimeline(); + const events = arrayFastClone(timeline.getEvents()).reverse().slice(0, 100); + + for (const timelineEvent of events) { + if (timelineEvent.getId() === upToEventId) { + // The event must be somewhere before the "read up to" marker + return false; + } else if (timelineEvent.getId() === ev.getId()) { + // The event is after the marker; advance it + this.readUpToMap[roomId] = evId; + return true; } } - const raw = ev.getEffectiveEvent(); - this.messaging.feedEvent(raw as IRoomEvent, this.eventListenerRoomId!).catch((e) => { - logger.error("Error sending event to widget: ", e); - }); + // We can't say for sure whether the widget has seen the event; let's + // just assume that it has + return false; + } + + private feedEvent(ev: MatrixEvent): void { + if (this.messaging === null) return; + if ( + // If we had decided earlier to feed this event to the widget, but + // it just wasn't ready, give it another try + this.eventsToFeed.delete(ev) || + // Skip marker timeline check for events with relations to unknown parent because these + // events are not added to the timeline here and will be ignored otherwise: + // https://github.com/matrix-org/matrix-js-sdk/blob/d3dfcd924201d71b434af3d77343b5229b6ed75e/src/models/room.ts#L2207-L2213 + this.relatesToUnknown(ev) || + // Skip marker timeline check for rooms where membership is + // 'invite', otherwise the membership event from the invitation room + // will advance the marker and new state events will not be + // forwarded to the widget. + this.isFromInvite(ev) || + // Check whether this event would be before or after our "read up to" marker. If it's + // before, or we can't decide, then we assume the widget will have already seen the event. + // If the event is after, or we don't have a marker for the room, then the marker will advance and we'll + // send it through. + // This approach of "read up to" prevents widgets receiving decryption spam from startup or + // receiving ancient events from backfill and such. + this.advanceReadUpToMarker(ev) + ) { + // If the event is still being decrypted, remember that we want to + // feed it to the widget (even if not strictly in the order given by + // the timeline) and get back to it later + if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) { + this.eventsToFeed.add(ev); + } else { + const raw = ev.getEffectiveEvent(); + this.messaging.feedEvent(raw as IRoomEvent, this.eventListenerRoomId!).catch((e) => { + logger.error("Error sending event to widget: ", e); + }); + } + } } } diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index bf4ee16b5d8..5bc2ac7fc01 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -24,6 +24,7 @@ import { WidgetDriver, WidgetEventCapability, WidgetKind, + IWidgetApiErrorResponseDataDetails, ISearchUserDirectoryResult, IGetMediaConfigResult, UpdateDelayedEventAction, @@ -33,6 +34,7 @@ import { ITurnServer as IClientTurnServer, EventType, IContent, + MatrixError, MatrixEvent, Room, Direction, @@ -127,12 +129,6 @@ export class StopGapWidgetDriver extends WidgetDriver { this.allowedCapabilities.add(MatrixCapabilities.MSC4157SendDelayedEvent); this.allowedCapabilities.add(MatrixCapabilities.MSC4157UpdateDelayedEvent); - this.allowedCapabilities.add( - WidgetEventCapability.forRoomEvent(EventDirection.Send, "org.matrix.rageshake_request").raw, - ); - this.allowedCapabilities.add( - WidgetEventCapability.forRoomEvent(EventDirection.Receive, "org.matrix.rageshake_request").raw, - ); this.allowedCapabilities.add( WidgetEventCapability.forStateEvent(EventDirection.Receive, EventType.RoomMember).raw, ); @@ -175,7 +171,13 @@ export class StopGapWidgetDriver extends WidgetDriver { WidgetEventCapability.forStateEvent(EventDirection.Receive, EventType.RoomCreate).raw, ); - const sendRecvRoomEvents = ["io.element.call.encryption_keys", EventType.Reaction, EventType.RoomRedaction]; + const sendRecvRoomEvents = [ + "io.element.call.encryption_keys", + "org.matrix.rageshake_request", + EventType.Reaction, + EventType.RoomRedaction, + "io.element.call.reaction", + ]; for (const eventType of sendRecvRoomEvents) { this.allowedCapabilities.add(WidgetEventCapability.forRoomEvent(EventDirection.Send, eventType).raw); this.allowedCapabilities.add(WidgetEventCapability.forRoomEvent(EventDirection.Receive, eventType).raw); @@ -414,6 +416,58 @@ export class StopGapWidgetDriver extends WidgetDriver { await client._unstable_updateDelayedEvent(delayId, action); } + /** + * Implements {@link WidgetDriver#sendToDevice} + */ + public async sendToDevice( + eventType: string, + encrypted: boolean, + contentMap: { [userId: string]: { [deviceId: string]: object } }, + ): Promise { + const client = MatrixClientPeg.safeGet(); + + if (encrypted) { + const crypto = client.getCrypto(); + if (!crypto) throw new Error("E2EE not enabled"); + + // attempt to re-batch these up into a single request + const invertedContentMap: { [content: string]: { userId: string; deviceId: string }[] } = {}; + + for (const userId of Object.keys(contentMap)) { + const userContentMap = contentMap[userId]; + for (const deviceId of Object.keys(userContentMap)) { + const content = userContentMap[deviceId]; + const stringifiedContent = JSON.stringify(content); + invertedContentMap[stringifiedContent] = invertedContentMap[stringifiedContent] || []; + invertedContentMap[stringifiedContent].push({ userId, deviceId }); + } + } + + await Promise.all( + Object.entries(invertedContentMap).map(async ([stringifiedContent, recipients]) => { + const batch = await crypto.encryptToDeviceMessages( + eventType, + recipients, + JSON.parse(stringifiedContent), + ); + + await client.queueToDevice(batch); + }), + ); + } else { + await client.queueToDevice({ + eventType, + batch: Object.entries(contentMap).flatMap(([userId, userContentMap]) => + Object.entries(userContentMap).map(([deviceId, content]) => ({ + userId, + deviceId, + payload: content, + })), + ), + }); + } + } + private pickRooms(roomIds?: (string | Symbols.AnyRoom)[]): Room[] { const client = MatrixClientPeg.get(); if (!client) throw new Error("Not attached to a client"); @@ -637,4 +691,15 @@ export class StopGapWidgetDriver extends WidgetDriver { const blob = await response.blob(); return { file: blob }; } + + /** + * Expresses a {@link MatrixError} as a JSON payload + * for use by Widget API error responses. + * @param error The error to handle. + * @returns The error expressed as a JSON payload, + * or undefined if it is not a {@link MatrixError}. + */ + public processError(error: unknown): IWidgetApiErrorResponseDataDetails | undefined { + return error instanceof MatrixError ? { matrix_api_error: error.asWidgetApiErrorData() } : undefined; + } } diff --git a/src/stores/widgets/WidgetLayoutStore.ts b/src/stores/widgets/WidgetLayoutStore.ts index cefbee0f6b7..f0d3cafc83c 100644 --- a/src/stores/widgets/WidgetLayoutStore.ts +++ b/src/stores/widgets/WidgetLayoutStore.ts @@ -91,9 +91,9 @@ export class WidgetLayoutStore extends ReadyWatchingStore { this.byRoom = new MapWithDefault(() => new Map()); this.matrixClient?.off(RoomStateEvent.Events, this.updateRoomFromState); - if (this.pinnedRef) SettingsStore.unwatchSetting(this.pinnedRef); - if (this.layoutRef) SettingsStore.unwatchSetting(this.layoutRef); - if (this.dynamicRef) SettingsStore.unwatchSetting(this.dynamicRef); + SettingsStore.unwatchSetting(this.pinnedRef); + SettingsStore.unwatchSetting(this.layoutRef); + SettingsStore.unwatchSetting(this.dynamicRef); WidgetStore.instance.off(UPDATE_EVENT, this.updateFromWidgetStore); } diff --git a/src/toasts/SetupEncryptionToast.ts b/src/toasts/SetupEncryptionToast.ts index 39408ba7f74..0dd54bb18fd 100644 --- a/src/toasts/SetupEncryptionToast.ts +++ b/src/toasts/SetupEncryptionToast.ts @@ -23,8 +23,6 @@ const getTitle = (kind: Kind): string => { switch (kind) { case Kind.SET_UP_ENCRYPTION: return _t("encryption|set_up_toast_title"); - case Kind.UPGRADE_ENCRYPTION: - return _t("encryption|upgrade_toast_title"); case Kind.VERIFY_THIS_SESSION: return _t("encryption|verify_toast_title"); } @@ -33,7 +31,6 @@ const getTitle = (kind: Kind): string => { const getIcon = (kind: Kind): string => { switch (kind) { case Kind.SET_UP_ENCRYPTION: - case Kind.UPGRADE_ENCRYPTION: return "secure_backup"; case Kind.VERIFY_THIS_SESSION: return "verification_warning"; @@ -44,8 +41,6 @@ const getSetupCaption = (kind: Kind): string => { switch (kind) { case Kind.SET_UP_ENCRYPTION: return _t("action|continue"); - case Kind.UPGRADE_ENCRYPTION: - return _t("action|upgrade"); case Kind.VERIFY_THIS_SESSION: return _t("action|verify"); } @@ -54,7 +49,6 @@ const getSetupCaption = (kind: Kind): string => { const getDescription = (kind: Kind): string => { switch (kind) { case Kind.SET_UP_ENCRYPTION: - case Kind.UPGRADE_ENCRYPTION: return _t("encryption|set_up_toast_description"); case Kind.VERIFY_THIS_SESSION: return _t("encryption|verify_toast_description"); @@ -63,7 +57,6 @@ const getDescription = (kind: Kind): string => { export enum Kind { SET_UP_ENCRYPTION = "set_up_encryption", - UPGRADE_ENCRYPTION = "upgrade_encryption", VERIFY_THIS_SESSION = "verify_this_session", } diff --git a/src/utils/UserInteractiveAuth.ts b/src/utils/UserInteractiveAuth.ts deleted file mode 100644 index 96efc82f4ba..00000000000 --- a/src/utils/UserInteractiveAuth.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { AuthDict } from "matrix-js-sdk/src/interactive-auth"; -import { UIAResponse } from "matrix-js-sdk/src/matrix"; - -import Modal from "../Modal"; -import InteractiveAuthDialog, { InteractiveAuthDialogProps } from "../components/views/dialogs/InteractiveAuthDialog"; - -type FunctionWithUIA = (auth?: AuthDict, ...args: A[]) => Promise>; - -export function wrapRequestWithDialog( - requestFunction: FunctionWithUIA, - opts: Omit, "makeRequest" | "onFinished">, -): (...args: A[]) => Promise { - return async function (...args): Promise { - return new Promise((resolve, reject) => { - const boundFunction = requestFunction.bind(opts.matrixClient) as FunctionWithUIA; - boundFunction(undefined, ...args) - .then((res) => resolve(res as R)) - .catch((error) => { - if (error.httpStatus !== 401 || !error.data?.flows) { - // doesn't look like an interactive-auth failure - return reject(error); - } - - Modal.createDialog(InteractiveAuthDialog, { - ...opts, - authData: error.data, - makeRequest: (authData: AuthDict) => boundFunction(authData, ...args), - onFinished: (success, result) => { - if (success) { - resolve(result as R); - } else { - reject(result); - } - }, - }); - }); - }); - }; -} diff --git a/src/utils/exportUtils/HtmlExport.tsx b/src/utils/exportUtils/HtmlExport.tsx index 08e488e5ffe..9a6bb93bba8 100644 --- a/src/utils/exportUtils/HtmlExport.tsx +++ b/src/utils/exportUtils/HtmlExport.tsx @@ -7,12 +7,13 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; -import ReactDOM from "react-dom"; +import { createRoot } from "react-dom/client"; import { Room, MatrixEvent, EventType, MsgType } from "matrix-js-sdk/src/matrix"; import { renderToStaticMarkup } from "react-dom/server"; import { logger } from "matrix-js-sdk/src/logger"; import escapeHtml from "escape-html"; import { TooltipProvider } from "@vector-im/compound-web"; +import { defer } from "matrix-js-sdk/src/utils"; import Exporter from "./Exporter"; import { mediaFromMxc } from "../../customisations/Media"; @@ -161,24 +162,19 @@ export default class HTMLExporter extends Exporter {
-
-
-
-
- ${roomAvatar} -
-
-
-
- ${safeRoomName} -
+
+ ${roomAvatar} +
+
+ + ${safeRoomName} + +
-
${safeTopic}
-
${previousMessagesLink}
@@ -268,7 +264,7 @@ export default class HTMLExporter extends Exporter { return wantsDateSeparator(prevEvent.getDate() || undefined, event.getDate() || undefined); } - public getEventTile(mxEv: MatrixEvent, continuation: boolean): JSX.Element { + public getEventTile(mxEv: MatrixEvent, continuation: boolean, ref?: () => void): JSX.Element { return (
@@ -292,6 +288,7 @@ export default class HTMLExporter extends Exporter { layout={Layout.Group} showReadReceipts={false} getRelationsForEvent={this.getRelationsForEvent} + ref={ref} /> @@ -303,7 +300,10 @@ export default class HTMLExporter extends Exporter { const avatarUrl = this.getAvatarURL(mxEv); const hasAvatar = !!avatarUrl; if (hasAvatar) await this.saveAvatarIfNeeded(mxEv); - const EventTile = this.getEventTile(mxEv, continuation); + // We have to wait for the component to be rendered before we can get the markup + // so pass a deferred as a ref to the component. + const deferred = defer(); + const EventTile = this.getEventTile(mxEv, continuation, deferred.resolve); let eventTileMarkup: string; if ( @@ -313,9 +313,12 @@ export default class HTMLExporter extends Exporter { ) { // to linkify textual events, we'll need lifecycle methods which won't be invoked in renderToString // So, we'll have to render the component into a temporary root element - const tempRoot = document.createElement("div"); - ReactDOM.render(EventTile, tempRoot); - eventTileMarkup = tempRoot.innerHTML; + const tempElement = document.createElement("div"); + const tempRoot = createRoot(tempElement); + tempRoot.render(EventTile); + await deferred.promise; + eventTileMarkup = tempElement.innerHTML; + tempRoot.unmount(); } else { eventTileMarkup = renderToStaticMarkup(EventTile); } diff --git a/src/utils/exportUtils/exportCustomCSS.css b/src/utils/exportUtils/exportCustomCSS.css index 3d034d7df5d..b3fa8e81d38 100644 --- a/src/utils/exportUtils/exportCustomCSS.css +++ b/src/utils/exportUtils/exportCustomCSS.css @@ -130,6 +130,14 @@ a.mx_reply_anchor:hover { } } +.mx_RoomHeader { + --mx-flex-display: flex; + --mx-flex-direction: row; + --mx-flex-align: center; + --mx-flex-justify: start; + --mx-flex-gap: var(--cpd-space-3x); +} + .mx_ReplyChain_Export { margin-top: 0; margin-bottom: 5px; diff --git a/src/utils/pillify.tsx b/src/utils/pillify.tsx index 2c19f114917..1859e90fd6b 100644 --- a/src/utils/pillify.tsx +++ b/src/utils/pillify.tsx @@ -6,8 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React from "react"; -import ReactDOM from "react-dom"; +import React, { StrictMode } from "react"; import { PushProcessor } from "matrix-js-sdk/src/pushprocessor"; import { MatrixClient, MatrixEvent, RuleId } from "matrix-js-sdk/src/matrix"; import { TooltipProvider } from "@vector-im/compound-web"; @@ -16,6 +15,7 @@ import SettingsStore from "../settings/SettingsStore"; import { Pill, pillRoomNotifLen, pillRoomNotifPos, PillType } from "../components/views/elements/Pill"; import { parsePermalink } from "./permalinks/Permalinks"; import { PermalinkParts } from "./permalinks/PermalinkConstructor"; +import { ReactRootManager } from "./react"; /** * A node here is an A element with a href attribute tag. @@ -48,7 +48,7 @@ const shouldBePillified = (node: Element, href: string, parts: PermalinkParts | * to turn into pills. * @param {MatrixEvent} mxEvent - the matrix event which the DOM nodes are * part of representing. - * @param {Element[]} pills: an accumulator of the DOM nodes which contain + * @param {ReactRootManager} pills - an accumulator of the DOM nodes which contain * React components which have been mounted as part of this. * The initial caller should pass in an empty array to seed the accumulator. */ @@ -56,7 +56,7 @@ export function pillifyLinks( matrixClient: MatrixClient, nodes: ArrayLike, mxEvent: MatrixEvent, - pills: Element[], + pills: ReactRootManager, ): void { const room = matrixClient.getRoom(mxEvent.getRoomId()) ?? undefined; const shouldShowPillAvatar = SettingsStore.getValue("Pill.shouldShowPillAvatar"); @@ -64,7 +64,7 @@ export function pillifyLinks( while (node) { let pillified = false; - if (node.tagName === "PRE" || node.tagName === "CODE" || pills.includes(node)) { + if (node.tagName === "PRE" || node.tagName === "CODE" || pills.elements.includes(node)) { // Skip code blocks and existing pills node = node.nextSibling as Element; continue; @@ -76,14 +76,16 @@ export function pillifyLinks( const pillContainer = document.createElement("span"); const pill = ( - - - + + + + + ); - ReactDOM.render(pill, pillContainer); + pills.render(pill, pillContainer); + node.parentNode?.replaceChild(pillContainer, node); - pills.push(pillContainer); // Pills within pills aren't going to go well, so move on pillified = true; @@ -133,19 +135,20 @@ export function pillifyLinks( const pillContainer = document.createElement("span"); const pill = ( - - - + + + + + ); - ReactDOM.render(pill, pillContainer); + pills.render(pill, pillContainer); roomNotifTextNode.parentNode?.replaceChild(pillContainer, roomNotifTextNode); - pills.push(pillContainer); } // Nothing else to do for a text node (and we don't need to advance // the loop pointer because we did it above) @@ -161,20 +164,3 @@ export function pillifyLinks( node = node.nextSibling as Element; } } - -/** - * Unmount all the pill containers from React created by pillifyLinks. - * - * It's critical to call this after pillifyLinks, otherwise - * Pills will leak, leaking entire DOM trees via the event - * emitter on BaseAvatar as per - * https://github.com/vector-im/element-web/issues/12417 - * - * @param {Element[]} pills - array of pill containers whose React - * components should be unmounted. - */ -export function unmountPills(pills: Element[]): void { - for (const pillContainer of pills) { - ReactDOM.unmountComponentAtNode(pillContainer); - } -} diff --git a/src/utils/react.tsx b/src/utils/react.tsx new file mode 100644 index 00000000000..164d704d913 --- /dev/null +++ b/src/utils/react.tsx @@ -0,0 +1,37 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import { ReactNode } from "react"; +import { createRoot, Root } from "react-dom/client"; + +/** + * Utility class to render & unmount additional React roots, + * e.g. for pills, tooltips and other components rendered atop user-generated events. + */ +export class ReactRootManager { + private roots: Root[] = []; + private rootElements: Element[] = []; + + public get elements(): Element[] { + return this.rootElements; + } + + public render(children: ReactNode, element: Element): void { + const root = createRoot(element); + this.roots.push(root); + this.rootElements.push(element); + root.render(children); + } + + public unmount(): void { + while (this.roots.length) { + const root = this.roots.pop()!; + this.rootElements.pop(); + root.unmount(); + } + } +} diff --git a/src/utils/room/placeCall.ts b/src/utils/room/placeCall.ts index d3d8a7ab8e0..13b0de23c4c 100644 --- a/src/utils/room/placeCall.ts +++ b/src/utils/room/placeCall.ts @@ -10,10 +10,11 @@ import { CallType } from "matrix-js-sdk/src/webrtc/call"; import { Room } from "matrix-js-sdk/src/matrix"; import LegacyCallHandler from "../../LegacyCallHandler"; -import { PlatformCallType } from "../../hooks/room/useRoomCall"; +import { getPlatformCallTypeProps, PlatformCallType } from "../../hooks/room/useRoomCall"; import defaultDispatcher from "../../dispatcher/dispatcher"; import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; import { Action } from "../../dispatcher/actions"; +import PosthogTrackers from "../../PosthogTrackers"; /** * Helper to place a call in a room that works with all the legacy modes @@ -27,6 +28,9 @@ export const placeCall = async ( platformCallType: PlatformCallType, skipLobby: boolean, ): Promise => { + const { analyticsName } = getPlatformCallTypeProps(platformCallType); + PosthogTrackers.trackInteraction(analyticsName); + if (platformCallType == PlatformCallType.LegacyCall || platformCallType == PlatformCallType.JitsiCall) { await LegacyCallHandler.instance.placeCall(room.roomId, callType); } else if (platformCallType == PlatformCallType.ElementCall) { diff --git a/src/utils/tooltipify.tsx b/src/utils/tooltipify.tsx index 65ce431a976..fc319b2024c 100644 --- a/src/utils/tooltipify.tsx +++ b/src/utils/tooltipify.tsx @@ -6,12 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React from "react"; -import ReactDOM from "react-dom"; +import React, { StrictMode } from "react"; import { TooltipProvider } from "@vector-im/compound-web"; import PlatformPeg from "../PlatformPeg"; import LinkWithTooltip from "../components/views/elements/LinkWithTooltip"; +import { ReactRootManager } from "./react"; /** * If the platform enabled needsUrlTooltips, recurses depth-first through a DOM tree, adding tooltip previews @@ -19,12 +19,16 @@ import LinkWithTooltip from "../components/views/elements/LinkWithTooltip"; * * @param {Element[]} rootNodes - a list of sibling DOM nodes to traverse to try * to add tooltips. - * @param {Element[]} ignoredNodes: a list of nodes to not recurse into. - * @param {Element[]} containers: an accumulator of the DOM nodes which contain + * @param {Element[]} ignoredNodes - a list of nodes to not recurse into. + * @param {ReactRootManager} tooltips - an accumulator of the DOM nodes which contain * React components that have been mounted by this function. The initial caller * should pass in an empty array to seed the accumulator. */ -export function tooltipifyLinks(rootNodes: ArrayLike, ignoredNodes: Element[], containers: Element[]): void { +export function tooltipifyLinks( + rootNodes: ArrayLike, + ignoredNodes: Element[], + tooltips: ReactRootManager, +): void { if (!PlatformPeg.get()?.needsUrlTooltips()) { return; } @@ -32,7 +36,7 @@ export function tooltipifyLinks(rootNodes: ArrayLike, ignoredNodes: Ele let node = rootNodes[0]; while (node) { - if (ignoredNodes.includes(node) || containers.includes(node)) { + if (ignoredNodes.includes(node) || tooltips.elements.includes(node)) { node = node.nextSibling as Element; continue; } @@ -53,33 +57,20 @@ export function tooltipifyLinks(rootNodes: ArrayLike, ignoredNodes: Ele // wrapping the link with the LinkWithTooltip component, keeping the same children. Ideally we'd do this // without the superfluous span but this is not something React trivially supports at this time. const tooltip = ( - - - - - + + + + + + + ); - ReactDOM.render(tooltip, node); - containers.push(node); + tooltips.render(tooltip, node); } else if (node.childNodes?.length) { - tooltipifyLinks(node.childNodes as NodeListOf, ignoredNodes, containers); + tooltipifyLinks(node.childNodes as NodeListOf, ignoredNodes, tooltips); } node = node.nextSibling as Element; } } - -/** - * Unmount tooltip containers created by tooltipifyLinks. - * - * It's critical to call this after tooltipifyLinks, otherwise - * tooltips will leak. - * - * @param {Element[]} containers - array of tooltip containers to unmount - */ -export function unmountTooltips(containers: Element[]): void { - for (const container of containers) { - ReactDOM.unmountComponentAtNode(container); - } -} diff --git a/src/vector/app.tsx b/src/vector/app.tsx index 0c2230bbb8a..426163db0bb 100644 --- a/src/vector/app.tsx +++ b/src/vector/app.tsx @@ -12,7 +12,7 @@ Please see LICENSE files in the repository root for full details. // To ensure we load the browser-matrix version first import "matrix-js-sdk/src/browser-index"; -import React, { ReactElement } from "react"; +import React, { ReactElement, StrictMode } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import { createClient, AutoDiscovery, ClientConfig } from "matrix-js-sdk/src/matrix"; import { WrapperLifecycle, WrapperOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/WrapperLifecycle"; @@ -27,7 +27,6 @@ import MatrixChat from "../components/structures/MatrixChat"; import { ValidatedServerConfig } from "../utils/ValidatedServerConfig"; import { ModuleRunner } from "../modules/ModuleRunner"; import { parseQs } from "./url_utils"; -import VectorBasePlatform from "./platform/VectorBasePlatform"; import { getInitialScreenAfterLogin, getScreenFromLocation, init as initRouting, onNewScreen } from "./routing"; import { UserFriendlyError } from "../languageHandler"; @@ -64,7 +63,7 @@ export async function loadApp(fragParams: {}, matrixChatRef: React.Ref - + + + ); } diff --git a/src/vector/indexeddb-worker.ts b/src/vector/indexeddb-worker.ts deleted file mode 100644 index 12f3f8094de..00000000000 --- a/src/vector/indexeddb-worker.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2017 Vector Creations Ltd - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { IndexedDBStoreWorker } from "matrix-js-sdk/src/indexeddb-worker"; - -const remoteWorker = new IndexedDBStoreWorker(postMessage as InstanceType["postMessage"]); - -global.onmessage = remoteWorker.onMessage; diff --git a/src/vector/init.tsx b/src/vector/init.tsx index da9827cb55b..2028f9af365 100644 --- a/src/vector/init.tsx +++ b/src/vector/init.tsx @@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details. */ import * as ReactDOM from "react-dom"; -import * as React from "react"; +import React, { StrictMode } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import * as languageHandler from "../languageHandler"; @@ -105,7 +105,9 @@ export async function showError(title: string, messages?: string[]): Promise, + + + , document.getElementById("matrixchat"), ); } @@ -116,7 +118,9 @@ export async function showIncompatibleBrowser(onAccept: () => void): Promise, + + + , document.getElementById("matrixchat"), ); } diff --git a/src/vector/platform/ElectronPlatform.tsx b/src/vector/platform/ElectronPlatform.tsx index c71865c3c12..d7ebd94bb21 100644 --- a/src/vector/platform/ElectronPlatform.tsx +++ b/src/vector/platform/ElectronPlatform.tsx @@ -15,7 +15,7 @@ import React from "react"; import { randomString } from "matrix-js-sdk/src/randomstring"; import { logger } from "matrix-js-sdk/src/logger"; -import { UpdateCheckStatus, UpdateStatus } from "../../BasePlatform"; +import BasePlatform, { UpdateCheckStatus, UpdateStatus } from "../../BasePlatform"; import BaseEventIndexManager from "../../indexing/BaseEventIndexManager"; import dis from "../../dispatcher/dispatcher"; import SdkConfig from "../../SdkConfig"; @@ -35,7 +35,6 @@ import { UPDATE_EVENT } from "../../stores/AsyncStore"; import { avatarUrlForRoom, getInitialLetter } from "../../Avatar"; import DesktopCapturerSourcePicker from "../../components/views/elements/DesktopCapturerSourcePicker"; import { MatrixClientPeg } from "../../MatrixClientPeg"; -import VectorBasePlatform from "./VectorBasePlatform"; import { SeshatIndexManager } from "./SeshatIndexManager"; import { IPCManager } from "./IPCManager"; import { _t } from "../../languageHandler"; @@ -90,7 +89,7 @@ function getUpdateCheckStatus(status: boolean | string): UpdateStatus { } } -export default class ElectronPlatform extends VectorBasePlatform { +export default class ElectronPlatform extends BasePlatform { private readonly ipc = new IPCManager("ipcCall", "ipcReply"); private readonly eventIndexManager: BaseEventIndexManager = new SeshatIndexManager(); // this is the opaque token we pass to the HS which when we get it in our callback we can resolve to a profile diff --git a/src/vector/platform/VectorBasePlatform.ts b/src/vector/platform/VectorBasePlatform.ts deleted file mode 100644 index 040f3d713dc..00000000000 --- a/src/vector/platform/VectorBasePlatform.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* -Copyright 2018-2024 New Vector Ltd. -Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2016 Aviral Dasgupta -Copyright 2016 OpenMarket Ltd - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import type { IConfigOptions } from "../../IConfigOptions"; -import BasePlatform from "../../BasePlatform"; -import { getVectorConfig } from "../getconfig"; -import Favicon from "../../favicon"; -import { _t } from "../../languageHandler"; - -/** - * Vector-specific extensions to the BasePlatform template - */ -export default abstract class VectorBasePlatform extends BasePlatform { - protected _favicon?: Favicon; - - public async getConfig(): Promise { - return getVectorConfig(); - } - - public getHumanReadableName(): string { - return "Vector Base Platform"; // no translation required: only used for analytics - } - - /** - * Delay creating the `Favicon` instance until first use (on the first notification) as - * it uses canvas, which can trigger a permission prompt in Firefox's resist fingerprinting mode. - * See https://github.com/element-hq/element-web/issues/9605. - */ - public get favicon(): Favicon { - if (this._favicon) { - return this._favicon; - } - this._favicon = new Favicon(); - return this._favicon; - } - - private updateFavicon(): void { - let bgColor = "#d00"; - let notif: string | number = this.notificationCount; - - if (this.errorDidOccur) { - notif = notif || "×"; - bgColor = "#f00"; - } - - this.favicon.badge(notif, { bgColor }); - } - - public setNotificationCount(count: number): void { - if (this.notificationCount === count) return; - super.setNotificationCount(count); - this.updateFavicon(); - } - - public setErrorStatus(errorDidOccur: boolean): void { - if (this.errorDidOccur === errorDidOccur) return; - super.setErrorStatus(errorDidOccur); - this.updateFavicon(); - } - - /** - * Begin update polling, if applicable - */ - public startUpdater(): void {} - - /** - * Get a sensible default display name for the - * device Vector is running on - */ - public getDefaultDeviceDisplayName(): string { - return _t("unknown_device"); - } -} diff --git a/src/vector/platform/WebPlatform.ts b/src/vector/platform/WebPlatform.ts index 53ff14d82f4..bb573c89c0f 100644 --- a/src/vector/platform/WebPlatform.ts +++ b/src/vector/platform/WebPlatform.ts @@ -11,12 +11,11 @@ import UAParser from "ua-parser-js"; import { logger } from "matrix-js-sdk/src/logger"; import { MatrixClientPeg } from "../../MatrixClientPeg"; -import { UpdateCheckStatus, UpdateStatus } from "../../BasePlatform"; +import BasePlatform, { UpdateCheckStatus, UpdateStatus } from "../../BasePlatform"; import dis from "../../dispatcher/dispatcher"; import { hideToast as hideUpdateToast, showToast as showUpdateToast } from "../../toasts/UpdateToast"; import { Action } from "../../dispatcher/actions"; import { CheckUpdatesPayload } from "../../dispatcher/payloads/CheckUpdatesPayload"; -import VectorBasePlatform from "./VectorBasePlatform"; import { parseQs } from "../url_utils"; import { _t } from "../../languageHandler"; @@ -31,7 +30,7 @@ function getNormalizedAppVersion(version: string): string { return version; } -export default class WebPlatform extends VectorBasePlatform { +export default class WebPlatform extends BasePlatform { private static readonly VERSION = process.env.VERSION!; // baked in by Webpack public constructor() { @@ -54,8 +53,8 @@ export default class WebPlatform extends VectorBasePlatform { return; } - await registration.update(); navigator.serviceWorker.addEventListener("message", this.onServiceWorkerPostMessage.bind(this)); + await registration.update(); } private onServiceWorkerPostMessage(event: MessageEvent): void { diff --git a/test/CreateCrossSigning-test.ts b/test/CreateCrossSigning-test.ts new file mode 100644 index 00000000000..e1762bb5040 --- /dev/null +++ b/test/CreateCrossSigning-test.ts @@ -0,0 +1,93 @@ +/* +Copyright 2024 New Vector Ltd. +Copyright 2018-2022 The Matrix.org Foundation C.I.C. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import { MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix"; +import { mocked } from "jest-mock"; + +import { createCrossSigning } from "../src/CreateCrossSigning"; +import { createTestClient } from "./test-utils"; +import Modal from "../src/Modal"; + +describe("CreateCrossSigning", () => { + let client: MatrixClient; + + beforeEach(() => { + client = createTestClient(); + }); + + it("should call bootstrapCrossSigning with an authUploadDeviceSigningKeys function", async () => { + await createCrossSigning(client, false, "password"); + + expect(client.getCrypto()?.bootstrapCrossSigning).toHaveBeenCalledWith({ + authUploadDeviceSigningKeys: expect.any(Function), + }); + }); + + it("should upload with password auth if possible", async () => { + client.uploadDeviceSigningKeys = jest.fn().mockRejectedValueOnce( + new MatrixError({ + flows: [ + { + stages: ["m.login.password"], + }, + ], + }), + ); + + await createCrossSigning(client, false, "password"); + + const { authUploadDeviceSigningKeys } = mocked(client.getCrypto()!).bootstrapCrossSigning.mock.calls[0][0]; + + const makeRequest = jest.fn(); + await authUploadDeviceSigningKeys!(makeRequest); + expect(makeRequest).toHaveBeenCalledWith({ + type: "m.login.password", + identifier: { + type: "m.id.user", + user: client.getUserId(), + }, + password: "password", + }); + }); + + it("should attempt to upload keys without auth if using token login", async () => { + await createCrossSigning(client, true, undefined); + + const { authUploadDeviceSigningKeys } = mocked(client.getCrypto()!).bootstrapCrossSigning.mock.calls[0][0]; + + const makeRequest = jest.fn(); + await authUploadDeviceSigningKeys!(makeRequest); + expect(makeRequest).toHaveBeenCalledWith({}); + }); + + it("should prompt user if password upload not possible", async () => { + const createDialog = jest.spyOn(Modal, "createDialog").mockReturnValue({ + finished: Promise.resolve([true]), + close: jest.fn(), + }); + + client.uploadDeviceSigningKeys = jest.fn().mockRejectedValueOnce( + new MatrixError({ + flows: [ + { + stages: ["dummy.mystery_flow_nobody_knows"], + }, + ], + }), + ); + + await createCrossSigning(client, false, "password"); + + const { authUploadDeviceSigningKeys } = mocked(client.getCrypto()!).bootstrapCrossSigning.mock.calls[0][0]; + + const makeRequest = jest.fn(); + await authUploadDeviceSigningKeys!(makeRequest); + expect(makeRequest).not.toHaveBeenCalledWith(); + expect(createDialog).toHaveBeenCalled(); + }); +}); diff --git a/test/components/views/dialogs/security/CreateCrossSigningDialog-test.tsx b/test/components/views/dialogs/security/CreateCrossSigningDialog-test.tsx new file mode 100644 index 00000000000..3e5dc4eb94e --- /dev/null +++ b/test/components/views/dialogs/security/CreateCrossSigningDialog-test.tsx @@ -0,0 +1,131 @@ +/* +Copyright 2024 New Vector Ltd. +Copyright 2018-2022 The Matrix.org Foundation C.I.C. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import React from "react"; +import { render, screen, waitFor } from "jest-matrix-react"; +import { mocked } from "jest-mock"; +import { MatrixClient } from "matrix-js-sdk/src/matrix"; + +import { createCrossSigning } from "../../../../../src/CreateCrossSigning"; +import CreateCrossSigningDialog from "../../../../../src/components/views/dialogs/security/CreateCrossSigningDialog"; +import { createTestClient } from "../../../../test-utils"; + +jest.mock("../../../../../src/CreateCrossSigning", () => ({ + createCrossSigning: jest.fn(), +})); + +describe("CreateCrossSigningDialog", () => { + let client: MatrixClient; + let createCrossSigningResolve: () => void; + let createCrossSigningReject: (e: Error) => void; + + beforeEach(() => { + client = createTestClient(); + mocked(createCrossSigning).mockImplementation(() => { + return new Promise((resolve, reject) => { + createCrossSigningResolve = resolve; + createCrossSigningReject = reject; + }); + }); + }); + + afterEach(() => { + jest.resetAllMocks(); + jest.restoreAllMocks(); + }); + + it("should call createCrossSigning and show a spinner while it runs", async () => { + const onFinished = jest.fn(); + + render( + , + ); + + expect(createCrossSigning).toHaveBeenCalledWith(client, false, "hunter2"); + expect(screen.getByTestId("spinner")).toBeInTheDocument(); + + createCrossSigningResolve!(); + + await waitFor(() => expect(onFinished).toHaveBeenCalledWith(true)); + }); + + it("should display an error if createCrossSigning fails", async () => { + render( + , + ); + + createCrossSigningReject!(new Error("generic error message")); + + await expect(await screen.findByRole("button", { name: "Retry" })).toBeInTheDocument(); + }); + + it("ignores failures when tokenLogin is true", async () => { + const onFinished = jest.fn(); + + render( + , + ); + + createCrossSigningReject!(new Error("generic error message")); + + await waitFor(() => expect(onFinished).toHaveBeenCalledWith(false)); + }); + + it("cancels the dialog when the cancel button is clicked", async () => { + const onFinished = jest.fn(); + + render( + , + ); + + createCrossSigningReject!(new Error("generic error message")); + + const cancelButton = await screen.findByRole("button", { name: "Cancel" }); + cancelButton.click(); + + expect(onFinished).toHaveBeenCalledWith(false); + }); + + it("should retry when the retry button is clicked", async () => { + render( + , + ); + + createCrossSigningReject!(new Error("generic error message")); + + const retryButton = await screen.findByRole("button", { name: "Retry" }); + retryButton.click(); + + expect(createCrossSigning).toHaveBeenCalledTimes(2); + }); +}); diff --git a/test/test-utils/poll.ts b/test/test-utils/poll.ts index 4f20403fb29..276730c2ff3 100644 --- a/test/test-utils/poll.ts +++ b/test/test-utils/poll.ts @@ -18,7 +18,7 @@ import { M_POLL_RESPONSE, M_TEXT, } from "matrix-js-sdk/src/matrix"; -import { uuid4 } from "@sentry/utils"; +import { randomString } from "matrix-js-sdk/src/randomstring"; import { flushPromises } from "./utilities"; @@ -67,7 +67,7 @@ export const makePollEndEvent = ( id?: string, ): MatrixEvent => { return new MatrixEvent({ - event_id: id || uuid4(), + event_id: id || randomString(16), room_id: roomId, origin_server_ts: ts, type: M_POLL_END.name, @@ -91,7 +91,7 @@ export const makePollResponseEvent = ( ts = 0, ): MatrixEvent => new MatrixEvent({ - event_id: uuid4(), + event_id: randomString(16), room_id: roomId, origin_server_ts: ts, type: M_POLL_RESPONSE.name, diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index 6dc9533ac94..0e608b14266 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -125,7 +125,12 @@ export function createTestClient(): MatrixClient { getVerificationRequestsToDeviceInProgress: jest.fn().mockReturnValue([]), setDeviceIsolationMode: jest.fn(), prepareToEncrypt: jest.fn(), + bootstrapCrossSigning: jest.fn(), getActiveSessionBackupVersion: jest.fn().mockResolvedValue(null), + isKeyBackupTrusted: jest.fn().mockResolvedValue({}), + createRecoveryKeyFromPassphrase: jest.fn().mockResolvedValue({}), + bootstrapSecretStorage: jest.fn(), + isDehydrationSupported: jest.fn().mockResolvedValue(false), }), getPushActionsForEvent: jest.fn(), @@ -268,6 +273,8 @@ export function createTestClient(): MatrixClient { getAuthIssuer: jest.fn(), getOrCreateFilter: jest.fn(), sendStickerMessage: jest.fn(), + getLocalAliases: jest.fn().mockReturnValue([]), + uploadDeviceSigningKeys: jest.fn(), } as unknown as MatrixClient; client.reEmitter = new ReEmitter(client); diff --git a/test/test-utils/threads.ts b/test/test-utils/threads.ts index 1b9e7f0a182..d2459653e53 100644 --- a/test/test-utils/threads.ts +++ b/test/test-utils/threads.ts @@ -84,7 +84,7 @@ export const makeThreadEvents = ({ rootEvent.setUnsigned({ "m.relations": { [RelationType.Thread]: { - latest_event: events[events.length - 1], + latest_event: events[events.length - 1].event, count: length, current_user_participated: [...participantUserIds, authorId].includes(currentUserId!), }, diff --git a/test/test-utils/utilities.ts b/test/test-utils/utilities.ts index 4278b73f74d..29b25fda218 100644 --- a/test/test-utils/utilities.ts +++ b/test/test-utils/utilities.ts @@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import EventEmitter from "events"; +import { act } from "jest-matrix-react"; import { ActionPayload } from "../../src/dispatcher/payloads"; import defaultDispatcher from "../../src/dispatcher/dispatcher"; @@ -119,7 +120,7 @@ export function untilEmission( }); } -export const flushPromises = async () => await new Promise((resolve) => window.setTimeout(resolve)); +export const flushPromises = () => act(async () => await new Promise((resolve) => window.setTimeout(resolve))); // with jest's modern fake timers process.nextTick is also mocked, // flushing promises in the normal way then waits for some advancement diff --git a/test/unit-tests/DecryptionFailureTracker-test.ts b/test/unit-tests/DecryptionFailureTracker-test.ts index b7884076ae1..898816923fa 100644 --- a/test/unit-tests/DecryptionFailureTracker-test.ts +++ b/test/unit-tests/DecryptionFailureTracker-test.ts @@ -496,6 +496,8 @@ describe("DecryptionFailureTracker", function () { await createAndTrackEventWithError(DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED); await createAndTrackEventWithError(DecryptionFailureCode.MEGOLM_KEY_WITHHELD); await createAndTrackEventWithError(DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE); + await createAndTrackEventWithError(DecryptionFailureCode.SENDER_IDENTITY_PREVIOUSLY_VERIFIED); + await createAndTrackEventWithError(DecryptionFailureCode.UNSIGNED_SENDER_DEVICE); await createAndTrackEventWithError(DecryptionFailureCode.UNKNOWN_ERROR); // Pretend "now" is Infinity @@ -510,6 +512,8 @@ describe("DecryptionFailureTracker", function () { "ExpectedDueToMembership", "OlmKeysNotSentError", "RoomKeysWithheldForUnverifiedDevice", + "ExpectedVerificationViolation", + "ExpectedSentByInsecureDevice", "UnknownError", ]); }); diff --git a/test/unit-tests/DeviceListener-test.ts b/test/unit-tests/DeviceListener-test.ts index 64761d7da10..0862c6b385c 100644 --- a/test/unit-tests/DeviceListener-test.ts +++ b/test/unit-tests/DeviceListener-test.ts @@ -39,6 +39,7 @@ jest.mock("matrix-js-sdk/src/logger"); jest.mock("../../src/dispatcher/dispatcher", () => ({ dispatch: jest.fn(), register: jest.fn(), + unregister: jest.fn(), })); jest.mock("../../src/SecurityManager", () => ({ @@ -351,13 +352,13 @@ describe("DeviceListener", () => { mockCrypto!.getCrossSigningKeyId.mockResolvedValue("abc"); }); - it("shows upgrade encryption toast when user has a key backup available", async () => { + it("shows set up encryption toast when user has a key backup available", async () => { // non falsy response mockClient!.getKeyBackupVersion.mockResolvedValue({} as unknown as KeyBackupInfo); await createAndStart(); expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith( - SetupEncryptionToast.Kind.UPGRADE_ENCRYPTION, + SetupEncryptionToast.Kind.SET_UP_ENCRYPTION, ); }); }); diff --git a/test/unit-tests/SecurityManager-test.ts b/test/unit-tests/SecurityManager-test.ts index 5ba60ac638b..63143d4644a 100644 --- a/test/unit-tests/SecurityManager-test.ts +++ b/test/unit-tests/SecurityManager-test.ts @@ -11,6 +11,13 @@ import { CryptoApi } from "matrix-js-sdk/src/crypto-api"; import { accessSecretStorage } from "../../src/SecurityManager"; import { filterConsole, stubClient } from "../test-utils"; +import Modal from "../../src/Modal.tsx"; + +jest.mock("react", () => { + const React = jest.requireActual("react"); + React.lazy = (children: any) => children(); // stub out lazy for dialog test + return React; +}); describe("SecurityManager", () => { describe("accessSecretStorage", () => { @@ -50,5 +57,21 @@ describe("SecurityManager", () => { }).rejects.toThrow("End-to-end encryption is disabled - unable to access secret storage"); }); }); + + it("should show CreateSecretStorageDialog if forceReset=true", async () => { + jest.mock("../../src/async-components/views/dialogs/security/CreateSecretStorageDialog", () => ({ + __test: true, + __esModule: true, + default: () => jest.fn(), + })); + const spy = jest.spyOn(Modal, "createDialog"); + stubClient(); + + const func = jest.fn(); + accessSecretStorage(func, true); + + expect(spy).toHaveBeenCalledTimes(1); + await expect(spy.mock.lastCall![0]).resolves.toEqual(expect.objectContaining({ __test: true })); + }); }); }); diff --git a/test/unit-tests/SupportedBrowser-test.ts b/test/unit-tests/SupportedBrowser-test.ts index f713a4a0b36..ccf75e0dab7 100644 --- a/test/unit-tests/SupportedBrowser-test.ts +++ b/test/unit-tests/SupportedBrowser-test.ts @@ -62,18 +62,18 @@ describe("SupportedBrowser", () => { ); it.each([ - // Safari 17.5 on macOS Sonoma - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15", - // Firefox 129 on macOS Sonoma - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:129.0) Gecko/20100101 Firefox/129.0", + // Safari 18.0 on macOS Sonoma + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15", + // Firefox 131 on macOS Sonoma + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:131.0) Gecko/20100101 Firefox/131.0", // Edge 129 on Windows "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/129.0.2792.79", // Edge 129 on macOS "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/129.0.2792.79", - // Firefox 129 on Windows - "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:129.0) Gecko/20100101 Firefox/129.0", - // Firefox 129 on Linux - "Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:129.0) Gecko/20100101 Firefox/129.0", + // Firefox 131 on Windows + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0", + // Firefox 131 on Linux + "Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:131.0) Gecko/20100101 Firefox/131.0", // Chrome 130 on Windows "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36", ])("should not warn for supported browsers", testUserAgentFactory()); diff --git a/test/unit-tests/async-components/dialogs/security/RecoveryMethodRemovedDialog-test.tsx b/test/unit-tests/async-components/dialogs/security/RecoveryMethodRemovedDialog-test.tsx new file mode 100644 index 00000000000..e3515244270 --- /dev/null +++ b/test/unit-tests/async-components/dialogs/security/RecoveryMethodRemovedDialog-test.tsx @@ -0,0 +1,33 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only + * Please see LICENSE files in the repository root for full details. + */ + +import React from "react"; +import { render, screen, fireEvent, waitFor } from "jest-matrix-react"; + +import RecoveryMethodRemovedDialog from "../../../../../src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog"; +import Modal from "../../../../../src/Modal.tsx"; + +describe("", () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + it("should open CreateKeyBackupDialog on primary action click", async () => { + const onFinished = jest.fn(); + const spy = jest.spyOn(Modal, "createDialog"); + jest.mock("../../../../../src/async-components/views/dialogs/security/CreateKeyBackupDialog", () => ({ + __test: true, + __esModule: true, + default: () => mocked dialog, + })); + + render(); + fireEvent.click(screen.getByRole("button", { name: "Set up Secure Messages" })); + await waitFor(() => expect(spy).toHaveBeenCalledTimes(1)); + expect((spy.mock.lastCall![0] as any)._payload._result).toEqual(expect.objectContaining({ __test: true })); + }); +}); diff --git a/test/unit-tests/components/structures/MatrixChat-test.tsx b/test/unit-tests/components/structures/MatrixChat-test.tsx index af0453af2af..16106ee0d22 100644 --- a/test/unit-tests/components/structures/MatrixChat-test.tsx +++ b/test/unit-tests/components/structures/MatrixChat-test.tsx @@ -62,6 +62,7 @@ import { DRAFT_LAST_CLEANUP_KEY } from "../../../../src/DraftCleaner"; import { UIFeature } from "../../../../src/settings/UIFeature"; import AutoDiscoveryUtils from "../../../../src/utils/AutoDiscoveryUtils"; import { ValidatedServerConfig } from "../../../../src/utils/ValidatedServerConfig"; +import Modal from "../../../../src/Modal.tsx"; jest.mock("matrix-js-sdk/src/oidc/authorize", () => ({ completeAuthorizationCodeGrant: jest.fn(), @@ -148,6 +149,7 @@ describe("", () => { isRoomEncrypted: jest.fn(), logout: jest.fn(), getDeviceId: jest.fn(), + getKeyBackupVersion: jest.fn().mockResolvedValue(null), }); let mockClient: Mocked; const serverConfig = { @@ -952,7 +954,7 @@ describe("", () => { const getComponentAndWaitForReady = async (): Promise => { const renderResult = getComponent(); // wait for welcome page chrome render - await screen.findByText("powered by Matrix"); + await screen.findByText("Powered by Matrix"); // go to login page defaultDispatcher.dispatch({ @@ -1112,8 +1114,6 @@ describe("", () => { expect(loginClient.getCrypto()!.userHasCrossSigningKeys).toHaveBeenCalled(); - await flushPromises(); - // set up keys screen is rendered expect(screen.getByText("Setting up keys")).toBeInTheDocument(); }); @@ -1481,7 +1481,7 @@ describe("", () => { const getComponentAndWaitForReady = async (): Promise => { const renderResult = getComponent(); // wait for welcome page chrome render - await screen.findByText("powered by Matrix"); + await screen.findByText("Powered by Matrix"); // go to mobile_register page defaultDispatcher.dispatch({ @@ -1501,7 +1501,7 @@ describe("", () => { it("should render welcome screen if mobile registration is not enabled in settings", async () => { await getComponentAndWaitForReady(); - await screen.findByText("powered by Matrix"); + await screen.findByText("Powered by Matrix"); }); it("should render mobile registration", async () => { @@ -1516,7 +1516,9 @@ describe("", () => { describe("when key backup failed", () => { it("should show the new recovery method dialog", async () => { + const spy = jest.spyOn(Modal, "createDialog"); jest.mock("../../../../src/async-components/views/dialogs/security/NewRecoveryMethodDialog", () => ({ + __test: true, __esModule: true, default: () => mocked dialog, })); @@ -1528,7 +1530,26 @@ describe("", () => { }); await flushPromises(); mockClient.emit(CryptoEvent.KeyBackupFailed, "error code"); - await waitFor(() => expect(screen.getByText("mocked dialog")).toBeInTheDocument()); + await waitFor(() => expect(spy).toHaveBeenCalledTimes(1)); + expect((spy.mock.lastCall![0] as any)._payload._result).toEqual(expect.objectContaining({ __test: true })); + }); + + it("should show the recovery method removed dialog", async () => { + const spy = jest.spyOn(Modal, "createDialog"); + jest.mock("../../../../src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog", () => ({ + __test: true, + __esModule: true, + default: () => mocked dialog, + })); + + getComponent({}); + defaultDispatcher.dispatch({ + action: "will_start_client", + }); + await flushPromises(); + mockClient.emit(CryptoEvent.KeyBackupFailed, "error code"); + await waitFor(() => expect(spy).toHaveBeenCalledTimes(1)); + expect((spy.mock.lastCall![0] as any)._payload._result).toEqual(expect.objectContaining({ __test: true })); }); }); }); diff --git a/test/unit-tests/components/structures/RightPanel-test.tsx b/test/unit-tests/components/structures/RightPanel-test.tsx index 45af4764379..e569369db54 100644 --- a/test/unit-tests/components/structures/RightPanel-test.tsx +++ b/test/unit-tests/components/structures/RightPanel-test.tsx @@ -8,7 +8,6 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import { render, screen, waitFor } from "jest-matrix-react"; -import { jest } from "@jest/globals"; import { mocked, MockedObject } from "jest-mock"; import { MatrixClient } from "matrix-js-sdk/src/matrix"; diff --git a/test/unit-tests/components/structures/RoomView-test.tsx b/test/unit-tests/components/structures/RoomView-test.tsx index b6a0f286376..02bed8cf4fc 100644 --- a/test/unit-tests/components/structures/RoomView-test.tsx +++ b/test/unit-tests/components/structures/RoomView-test.tsx @@ -42,7 +42,7 @@ import { } from "../../../test-utils"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import { Action } from "../../../../src/dispatcher/actions"; -import dis, { defaultDispatcher } from "../../../../src/dispatcher/dispatcher"; +import defaultDispatcher from "../../../../src/dispatcher/dispatcher"; import { ViewRoomPayload } from "../../../../src/dispatcher/payloads/ViewRoomPayload"; import { RoomView as _RoomView } from "../../../../src/components/structures/RoomView"; import ResizeNotifier from "../../../../src/utils/ResizeNotifier"; @@ -527,7 +527,7 @@ describe("RoomView", () => { beforeEach(() => { jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => setting === "feature_ask_to_join"); jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Knock); - jest.spyOn(dis, "dispatch"); + jest.spyOn(defaultDispatcher, "dispatch"); }); it("allows to request to join", async () => { @@ -536,9 +536,9 @@ describe("RoomView", () => { await mountRoomView(); fireEvent.click(screen.getByRole("button", { name: "Request access" })); - await untilDispatch(Action.SubmitAskToJoin, dis); + await untilDispatch(Action.SubmitAskToJoin, defaultDispatcher); - expect(dis.dispatch).toHaveBeenCalledWith({ + expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: "submit_ask_to_join", roomId: room.roomId, opts: { reason: undefined }, @@ -552,9 +552,12 @@ describe("RoomView", () => { await mountRoomView(); fireEvent.click(screen.getByRole("button", { name: "Cancel request" })); - await untilDispatch(Action.CancelAskToJoin, dis); + await untilDispatch(Action.CancelAskToJoin, defaultDispatcher); - expect(dis.dispatch).toHaveBeenCalledWith({ action: "cancel_ask_to_join", roomId: room.roomId }); + expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ + action: "cancel_ask_to_join", + roomId: room.roomId, + }); }); }); @@ -669,7 +672,7 @@ describe("RoomView", () => { await waitFor(() => { expect(container.querySelector(".mx_RoomView_searchResultsPanel")).toBeVisible(); }); - const prom = untilDispatch(Action.ViewRoom, dis); + const prom = untilDispatch(Action.ViewRoom, defaultDispatcher); await userEvent.hover(getByText("search term")); await userEvent.click(await findByLabelText("Edit")); @@ -678,8 +681,8 @@ describe("RoomView", () => { }); it("fires Action.RoomLoaded", async () => { - jest.spyOn(dis, "dispatch"); + jest.spyOn(defaultDispatcher, "dispatch"); await mountRoomView(); - expect(dis.dispatch).toHaveBeenCalledWith({ action: Action.RoomLoaded }); + expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: Action.RoomLoaded }); }); }); diff --git a/test/unit-tests/components/structures/TimelinePanel-test.tsx b/test/unit-tests/components/structures/TimelinePanel-test.tsx index 7de688bfe64..4a663517795 100644 --- a/test/unit-tests/components/structures/TimelinePanel-test.tsx +++ b/test/unit-tests/components/structures/TimelinePanel-test.tsx @@ -41,6 +41,8 @@ import { mkThread } from "../../../test-utils/threads"; import { createMessageEventContent } from "../../../test-utils/events"; import SettingsStore from "../../../../src/settings/SettingsStore"; import ScrollPanel from "../../../../src/components/structures/ScrollPanel"; +import defaultDispatcher from "../../../../src/dispatcher/dispatcher"; +import { Action } from "../../../../src/dispatcher/actions"; // ScrollPanel calls this, but jsdom doesn't mock it for us HTMLDivElement.prototype.scrollBy = () => {}; @@ -1002,4 +1004,27 @@ describe("TimelinePanel", () => { await waitFor(() => expect(screen.queryByRole("progressbar")).toBeNull()); await waitFor(() => expect(container.querySelector(".mx_RoomView_MessageList")).not.toBeEmptyDOMElement()); }); + + it("should dump debug logs on Action.DumpDebugLogs", async () => { + const spy = jest.spyOn(console, "debug"); + + const [, room, events] = setupTestData(); + const eventsPage2 = events.slice(1, 2); + + // Start with only page 2 of the main events in the window + const [, timelineSet] = mkTimeline(room, eventsPage2); + room.getTimelineSets = jest.fn().mockReturnValue([timelineSet]); + + await withScrollPanelMountSpy(async () => { + const { container } = render(); + + await waitFor(() => expectEvents(container, [events[1]])); + }); + + defaultDispatcher.fire(Action.DumpDebugLogs); + + await waitFor(() => + expect(spy).toHaveBeenCalledWith(expect.stringContaining("TimelinePanel(Room): Debugging info for roomId")), + ); + }); }); diff --git a/test/unit-tests/components/structures/UploadBar-test.tsx b/test/unit-tests/components/structures/UploadBar-test.tsx index 41dcd5fe7cf..6f6c038414c 100644 --- a/test/unit-tests/components/structures/UploadBar-test.tsx +++ b/test/unit-tests/components/structures/UploadBar-test.tsx @@ -8,7 +8,6 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import { render } from "jest-matrix-react"; -import { jest } from "@jest/globals"; import { Room } from "matrix-js-sdk/src/matrix"; import { stubClient } from "../../../test-utils"; diff --git a/test/unit-tests/components/structures/__snapshots__/MatrixChat-test.tsx.snap b/test/unit-tests/components/structures/__snapshots__/MatrixChat-test.tsx.snap index 71bde418ff7..e0749581448 100644 --- a/test/unit-tests/components/structures/__snapshots__/MatrixChat-test.tsx.snap +++ b/test/unit-tests/components/structures/__snapshots__/MatrixChat-test.tsx.snap @@ -114,46 +114,56 @@ exports[` Multi-tab lockout waits for other tab to stop during sta >
+
-

- Hello -

+
+

+ Hello +

+
-
-
@@ -162,12 +172,33 @@ exports[` Multi-tab lockout waits for other tab to stop during sta class="mx_AuthFooter" role="contentinfo" > + + Blog + + + Twitter + + + GitHub + - powered by Matrix + Powered by Matrix
@@ -201,116 +232,150 @@ exports[` with a soft-logged-out session should show the soft-logo >
+
-
+ -
-
-

- You're signed out -

-

- Sign in -

-
- -

- Enter your password to sign in and regain access to your account. -

-
- -
+

+ Clear personal data +

+

+ Warning: your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account. +

+
- -
-

- Clear personal data -

-

- Warning: your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account. -

-
-
- Clear all data
-
-
+
+
diff --git a/test/unit-tests/components/structures/auth/ForgotPassword-test.tsx b/test/unit-tests/components/structures/auth/ForgotPassword-test.tsx index f439605319a..db6ce005c05 100644 --- a/test/unit-tests/components/structures/auth/ForgotPassword-test.tsx +++ b/test/unit-tests/components/structures/auth/ForgotPassword-test.tsx @@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import { mocked } from "jest-mock"; -import { act, render, RenderResult, screen } from "jest-matrix-react"; +import { act, render, RenderResult, screen, waitFor } from "jest-matrix-react"; import userEvent from "@testing-library/user-event"; import { MatrixClient, createClient } from "matrix-js-sdk/src/matrix"; @@ -47,14 +47,12 @@ describe("", () => { }; const click = async (element: Element): Promise => { - await act(async () => { - await userEvent.click(element, { delay: null }); - }); + await userEvent.click(element, { delay: null }); }; const itShouldCloseTheDialogAndShowThePasswordInput = (): void => { - it("should close the dialog and show the password input", () => { - expect(screen.queryByText("Verify your email to continue")).not.toBeInTheDocument(); + it("should close the dialog and show the password input", async () => { + await waitFor(() => expect(screen.queryByText("Verify your email to continue")).not.toBeInTheDocument()); expect(screen.getByText("Reset your password")).toBeInTheDocument(); }); }; @@ -314,7 +312,7 @@ describe("", () => { }); }); - it("should send the new password and show the click validation link dialog", () => { + it("should send the new password and show the click validation link dialog", async () => { expect(client.setPassword).toHaveBeenCalledWith( { type: "m.login.email.identity", @@ -326,15 +324,15 @@ describe("", () => { testPassword, false, ); - expect(screen.getByText("Verify your email to continue")).toBeInTheDocument(); + await expect( + screen.findByText("Verify your email to continue"), + ).resolves.toBeInTheDocument(); expect(screen.getByText(testEmail)).toBeInTheDocument(); }); describe("and dismissing the dialog by clicking the background", () => { beforeEach(async () => { - await act(async () => { - await userEvent.click(screen.getByTestId("dialog-background"), { delay: null }); - }); + await userEvent.click(await screen.findByTestId("dialog-background"), { delay: null }); await waitEnoughCyclesForModal({ useFakeTimers: true, }); @@ -345,7 +343,7 @@ describe("", () => { describe("and dismissing the dialog", () => { beforeEach(async () => { - await click(screen.getByLabelText("Close dialog")); + await click(await screen.findByLabelText("Close dialog")); await waitEnoughCyclesForModal({ useFakeTimers: true, }); @@ -356,14 +354,16 @@ describe("", () => { describe("and clicking »Re-enter email address«", () => { beforeEach(async () => { - await click(screen.getByText("Re-enter email address")); + await click(await screen.findByText("Re-enter email address")); await waitEnoughCyclesForModal({ useFakeTimers: true, }); }); - it("should close the dialog and go back to the email input", () => { - expect(screen.queryByText("Verify your email to continue")).not.toBeInTheDocument(); + it("should close the dialog and go back to the email input", async () => { + await waitFor(() => + expect(screen.queryByText("Verify your email to continue")).not.toBeInTheDocument(), + ); expect(screen.queryByText("Enter your email to reset password")).toBeInTheDocument(); }); }); @@ -397,11 +397,11 @@ describe("", () => { }); it("should show the sign out warning dialog", async () => { - expect( - screen.getByText( + await expect( + screen.findByText( "Signing out your devices will delete the message encryption keys stored on them, making encrypted chat history unreadable.", ), - ).toBeInTheDocument(); + ).resolves.toBeInTheDocument(); // confirm dialog await click(screen.getByText("Continue")); diff --git a/test/unit-tests/components/views/auth/VectorAuthPage-test.tsx b/test/unit-tests/components/views/auth/AuthFooter-test.tsx similarity index 73% rename from test/unit-tests/components/views/auth/VectorAuthPage-test.tsx rename to test/unit-tests/components/views/auth/AuthFooter-test.tsx index 2c5cb461b1a..f8d0d8fd5ea 100644 --- a/test/unit-tests/components/views/auth/VectorAuthPage-test.tsx +++ b/test/unit-tests/components/views/auth/AuthFooter-test.tsx @@ -9,16 +9,16 @@ Please see LICENSE files in the repository root for full details. import * as React from "react"; import { render } from "jest-matrix-react"; -import VectorAuthPage from "../../../../../src/components/views/auth/VectorAuthPage"; +import AuthFooter from "../../../../../src/components/views/auth/AuthFooter"; import { setupLanguageMock } from "../../../../setup/setupLanguage"; -describe("", () => { +describe("", () => { beforeEach(() => { setupLanguageMock(); }); it("should match snapshot", () => { - const { asFragment } = render(); + const { asFragment } = render(); expect(asFragment()).toMatchSnapshot(); }); }); diff --git a/test/unit-tests/components/views/auth/VectorAuthHeaderLogo-test.tsx b/test/unit-tests/components/views/auth/AuthHeaderLogo-test.tsx similarity index 64% rename from test/unit-tests/components/views/auth/VectorAuthHeaderLogo-test.tsx rename to test/unit-tests/components/views/auth/AuthHeaderLogo-test.tsx index 6b3839a5b18..ce187805e41 100644 --- a/test/unit-tests/components/views/auth/VectorAuthHeaderLogo-test.tsx +++ b/test/unit-tests/components/views/auth/AuthHeaderLogo-test.tsx @@ -9,11 +9,11 @@ Please see LICENSE files in the repository root for full details. import * as React from "react"; import { render } from "jest-matrix-react"; -import VectorAuthHeaderLogo from "../../../../../src/components/views/auth/VectorAuthHeaderLogo"; +import AuthHeaderLogo from "../../../../../src/components/views/auth/AuthHeaderLogo"; -describe("", () => { +describe("", () => { it("should match snapshot", () => { - const { asFragment } = render(); + const { asFragment } = render(); expect(asFragment()).toMatchSnapshot(); }); }); diff --git a/test/unit-tests/components/views/auth/AuthPage-test.tsx b/test/unit-tests/components/views/auth/AuthPage-test.tsx new file mode 100644 index 00000000000..836b08f20b8 --- /dev/null +++ b/test/unit-tests/components/views/auth/AuthPage-test.tsx @@ -0,0 +1,36 @@ +/* +Copyright 2024 New Vector Ltd. +Copyright 2022 The Matrix.org Foundation C.I.C. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import * as React from "react"; +import { render } from "jest-matrix-react"; + +import AuthPage from "../../../../../src/components/views/auth/AuthPage"; +import { setupLanguageMock } from "../../../../setup/setupLanguage"; +import SdkConfig from "../../../../../src/SdkConfig.ts"; + +describe("", () => { + beforeEach(() => { + setupLanguageMock(); + SdkConfig.reset(); + // @ts-ignore private access + AuthPage.welcomeBackgroundUrl = undefined; + }); + + it("should match snapshot", () => { + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); + + it("should use configured background url", () => { + SdkConfig.add({ branding: { welcome_background_url: ["https://example.com/image.png"] } }); + const { container } = render(); + expect(container.querySelector(".mx_AuthPage")).toHaveStyle({ + background: "center/cover fixed url(https://example.com/image.png)", + }); + }); +}); diff --git a/test/unit-tests/components/views/auth/VectorAuthFooter-test.tsx b/test/unit-tests/components/views/auth/VectorAuthFooter-test.tsx deleted file mode 100644 index ebd2a6ffe48..00000000000 --- a/test/unit-tests/components/views/auth/VectorAuthFooter-test.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import * as React from "react"; -import { render } from "jest-matrix-react"; - -import VectorAuthFooter from "../../../../../src/components/views/auth/VectorAuthFooter"; -import { setupLanguageMock } from "../../../../setup/setupLanguage"; - -describe("", () => { - beforeEach(() => { - setupLanguageMock(); - }); - - it("should match snapshot", () => { - const { asFragment } = render(); - expect(asFragment()).toMatchSnapshot(); - }); -}); diff --git a/test/unit-tests/components/views/auth/__snapshots__/VectorAuthFooter-test.tsx.snap b/test/unit-tests/components/views/auth/__snapshots__/AuthFooter-test.tsx.snap similarity index 92% rename from test/unit-tests/components/views/auth/__snapshots__/VectorAuthFooter-test.tsx.snap rename to test/unit-tests/components/views/auth/__snapshots__/AuthFooter-test.tsx.snap index d1a16c081b8..f1321ece2ae 100644 --- a/test/unit-tests/components/views/auth/__snapshots__/VectorAuthFooter-test.tsx.snap +++ b/test/unit-tests/components/views/auth/__snapshots__/AuthFooter-test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` should match snapshot 1`] = ` +exports[` should match snapshot 1`] = `