diff --git a/.editorconfig b/.editorconfig index b50253df1f..5f5b128623 100644 --- a/.editorconfig +++ b/.editorconfig @@ -18,7 +18,7 @@ trim_trailing_whitespace = true indent_size = 4 [package.json] -indent_size = 2 +indent_size = 4 [*.tsx.snap] trim_trailing_whitespace = false diff --git a/.eslintrc.js b/.eslintrc.js index f4d92f1cf2..dd406134fe 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -266,6 +266,9 @@ module.exports = { parserOptions: { project: ["./playwright/tsconfig.json"], }, + rules: { + "react-hooks/rules-of-hooks": ["off"], + }, }, ], settings: { diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0e8c21e786..3a9c29e197 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 ce088a9327..5fb969a1c6 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 536f78e18d..c2f3028176 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 diff --git a/.node-version b/.node-version index 209e3ef4b6..2bd5a0a98a 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -20 +22 diff --git a/.prettierignore b/.prettierignore index 0cca8fb6aa..418329cf28 100644 --- a/.prettierignore +++ b/.prettierignore @@ -37,5 +37,10 @@ package-lock.json # Downloaded and already minified res/jitsi_external_api.min.js + # This file is also machine-generated /playwright/e2e/crypto/test_indexeddb_cryptostore_dump/dump.json +/playwright/test-results/ +/playwright/html-report/ +/playwright/logs/ +/playwright/snapshots/ diff --git a/CHANGELOG.md b/CHANGELOG.md index ef18822d38..1274048c8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,68 @@ +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 3f3b9a1d71..908c05520c 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 cc5046ed69..0967ef424b 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 b5075b6fce..a6f72ab1ab 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 af8f0e7eac..1c182cdd34 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 9d3d67e68d..100baeca71 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 4dcc75aeeb..771df35091 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 9cc320d6ea..4f75eb04db 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -44,6 +44,8 @@ const config: Config = { // getSessionLock is piped into a different JS context via stringification, and the coverage functionality is // not available in that contest. So, turn off coverage instrumentation for it. "!/src/utils/SessionLock.ts", + // Coverage chokes on type definition files + "!/src/**/*.d.ts", ], coverageReporters: ["text-summary", "lcov"], testResultsProcessor: "@casualbot/jest-sonar-reporter", diff --git a/package.json b/package.json index 44e1a73f00..cbb8a33480 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "element-web", - "version": "1.11.81", + "version": "1.11.84", "description": "A feature-rich client for Matrix.org", "author": "New Vector Ltd.", "repository": { @@ -74,13 +74,9 @@ "update:jitsi": "curl -s https://meet.element.io/libs/external_api.min.js > ./res/jitsi_external_api.min.js" }, "resolutions": { - "@types/react-dom": "17.0.25", - "@types/react": "17.0.83", "@types/seedrandom": "3.0.8", "oidc-client-ts": "3.1.0", "jwt-decode": "4.0.0", - "@floating-ui/react": "0.26.11", - "@radix-ui/react-id": "1.1.0", "caniuse-lite": "1.0.30001668", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0", "wrap-ansi": "npm:wrap-ansi@^7.0.0" @@ -88,15 +84,14 @@ "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", - "@testing-library/react-hooks": "^8.0.1", "@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", @@ -119,8 +114,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", @@ -141,10 +136,10 @@ "posthog-js": "1.157.2", "qrcode": "1.5.4", "re-resizable": "6.9.17", - "react": "17.0.2", + "react": "^18.3.1", "react-beautiful-dnd": "^13.1.0", "react-blurhash": "^0.3.0", - "react-dom": "17.0.2", + "react-dom": "^18.3.1", "react-focus-lock": "^2.5.1", "react-transition-group": "^4.4.1", "rfc4648": "^1.4.0", @@ -153,7 +148,7 @@ "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": { @@ -186,10 +181,10 @@ "@sentry/webpack-plugin": "^2.7.1", "@stylistic/eslint-plugin": "^2.9.0", "@svgr/webpack": "^8.0.0", - "@testing-library/dom": "^9.0.0", - "@testing-library/jest-dom": "^6.0.0", - "@testing-library/react": "^12.1.5", - "@testing-library/user-event": "^14.4.3", + "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.4.8", + "@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", @@ -211,9 +206,9 @@ "@types/node-fetch": "^2.6.2", "@types/pako": "^2.0.0", "@types/qrcode": "^1.3.5", - "@types/react": "17.0.83", + "@types/react": "18.3.3", "@types/react-beautiful-dnd": "^13.0.0", - "@types/react-dom": "17.0.25", + "@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", @@ -224,7 +219,7 @@ "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", - "axe-core": "4.10.0", + "axe-core": "4.10.2", "babel-jest": "^29.0.0", "babel-loader": "^9.0.0", "babel-plugin-jsx-remove-data-test-id": "^3.0.0", @@ -247,7 +242,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", @@ -260,7 +255,7 @@ "husky": "^9.0.0", "jest": "^29.6.2", "jest-canvas-mock": "^2.5.2", - "jest-environment-jsdom": "^29.6.2", + "jest-environment-jsdom": "^29.7.0", "jest-mock": "^29.6.2", "jest-raw-loader": "^1.0.1", "jsqr": "^1.4.0", diff --git a/playwright/.gitignore b/playwright/.gitignore index 0d50077f5a..78bb2e0277 100644 --- a/playwright/.gitignore +++ b/playwright/.gitignore @@ -4,3 +4,5 @@ # Only commit snapshots from Linux /snapshots/**/*.png !/snapshots/**/*-linux.png +# This file is machine-generated +/e2e/crypto/test_indexeddb_cryptostore_dump/dump.json diff --git a/playwright/Dockerfile b/playwright/Dockerfile index cbce8f0d00..9d478ff231 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/audio-player/audio-player.spec.ts b/playwright/e2e/audio-player/audio-player.spec.ts index a8f483a375..c2081dfcd8 100644 --- a/playwright/e2e/audio-player/audio-player.spec.ts +++ b/playwright/e2e/audio-player/audio-player.spec.ts @@ -242,6 +242,7 @@ test.describe("Audio player", () => { // Find and click "Reply" button const clickButtonReply = async () => { + await tile.scrollIntoViewIfNeeded(); await tile.hover(); await tile.getByRole("button", { name: "Reply", exact: true }).click(); }; diff --git a/playwright/e2e/crypto/crypto.spec.ts b/playwright/e2e/crypto/crypto.spec.ts index e3285b898d..2ab49e72ec 100644 --- a/playwright/e2e/crypto/crypto.spec.ts +++ b/playwright/e2e/crypto/crypto.spec.ts @@ -114,13 +114,10 @@ test.describe("Cryptography", function () { await dialog.getByRole("button", { name: "Continue" }).click(); await copyAndContinue(page); - // When the device is verified, the `Setting up keys` step is skipped - if (!isDeviceVerified) { - const uiaDialogTitle = page.locator(".mx_InteractiveAuthDialog .mx_Dialog_title"); - await expect(uiaDialogTitle.getByText("Setting up keys")).toBeVisible(); - await expect(uiaDialogTitle.getByText("Setting up keys")).not.toBeVisible(); - } + // If the device is unverified, there should be a "Setting up keys" step; however, it + // can be quite quick, and playwright can miss it, so we can't test for it. + // Either way, we end up at a success dialog: await expect(dialog.getByText("Secure Backup successful")).toBeVisible(); await dialog.getByRole("button", { name: "Done" }).click(); await expect(dialog.getByText("Secure Backup successful")).not.toBeVisible(); diff --git a/playwright/e2e/crypto/invisible-crypto.spec.ts b/playwright/e2e/crypto/invisible-crypto.spec.ts index c53bacd32c..f207d2c6bb 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 f1def98469..4c8d641e6f 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/pinned-messages.spec.ts b/playwright/e2e/pinned-messages/pinned-messages.spec.ts index 9f6f38f177..ef2c1b27d4 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 81146be70e..59db80c3c6 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/spotlight/spotlight.spec.ts b/playwright/e2e/spotlight/spotlight.spec.ts index 4a6b5611d0..22513ca47a 100644 --- a/playwright/e2e/spotlight/spotlight.spec.ts +++ b/playwright/e2e/spotlight/spotlight.spec.ts @@ -216,7 +216,7 @@ test.describe("Spotlight", () => { * * https://github.com/matrix-org/synapse/issues/16472 */ - test.skip("should find unknown people", async ({ page, app }) => { + test("should find unknown people", async ({ page, app }) => { const spotlight = await app.openSpotlight(); await page.waitForTimeout(500); // wait for the dialog to settle await spotlight.filter(Filter.People); diff --git a/playwright/e2e/timeline/timeline.spec.ts b/playwright/e2e/timeline/timeline.spec.ts index 1abf229e2e..e8ef0e577c 100644 --- a/playwright/e2e/timeline/timeline.spec.ts +++ b/playwright/e2e/timeline/timeline.spec.ts @@ -653,7 +653,7 @@ test.describe("Timeline", () => { const toggleEventButton = viewSourceEventExpanded.getByRole("button", { name: "toggle event" }); // Check size and position of toggle on expanded view source event // See: _ViewSourceEvent.pcss - await expect(toggleEventButton).toHaveCSS("height", "12px"); // --ViewSourceEvent_toggle-size + await expect(toggleEventButton).toHaveCSS("height", "16px"); // --ViewSourceEvent_toggle-size await expect(toggleEventButton).toHaveCSS("align-self", "flex-end"); // Click again to collapse the source await toggleEventButton.click({ position: { x: 0, y: 0 } }); @@ -679,7 +679,7 @@ test.describe("Timeline", () => { ); // Click view source event toggle - await viewSourceEventIrc.getByRole("button", { name: "toggle event" }).click({ position: { x: 0, y: 0 } }); + await viewSourceEventIrc.getByRole("button", { name: "toggle event" }).click({ position: { x: 8, y: 8 } }); // Make sure the expand toggle worked await expect(page.locator(".mx_EventTile[data-layout=irc] .mx_ViewSourceEvent_expanded")).toBeVisible(); diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts index 66dd666389..8d5229a510 100644 --- a/playwright/element-web-test.ts +++ b/playwright/element-web-test.ts @@ -224,7 +224,7 @@ export const test = base.extend<{ }, axe: async ({ page }, use) => { - await use(new AxeBuilder({ page }).exclude("[id^='floating-ui-']")); + await use(new AxeBuilder({ page }).exclude("[data-floating-ui-portal]")); }, checkA11y: async ({ axe }, use, testInfo) => use(async () => { @@ -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/pages/ElementAppPage.ts b/playwright/pages/ElementAppPage.ts index 81c57cc6f7..4cff3c72ea 100644 --- a/playwright/pages/ElementAppPage.ts +++ b/playwright/pages/ElementAppPage.ts @@ -188,6 +188,6 @@ export class ElementAppPage { "Element has no aria-labelledby or aria-describedy attributes! The tooltip should have added either one of these.", ); } - return this.page.locator(`#${labelledById ?? describedById}`); + return this.page.locator(`id=${labelledById ?? describedById}`); } } diff --git a/playwright/plugins/homeserver/synapse/index.ts b/playwright/plugins/homeserver/synapse/index.ts index cb4882f1b9..6434e70f48 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:6c33604ee62f009f3b34454a3c3e85f7e3ff5de63e45011fcd79e0ddc54a4e51"; async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise> { const templateDir = path.join(__dirname, "templates", opts.template); 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 6a490c2157..8519e162f2 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/invite/invite-dialog.spec.ts/invite-dialog-dm-with-user-pill-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-with-user-pill-linux.png index 94d6b79f1e..554400123f 100644 Binary files a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-with-user-pill-linux.png and b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-with-user-pill-linux.png differ diff --git a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png index 2130dd5d1a..1342f5bf27 100644 Binary files a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png and b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png differ diff --git a/playwright/snapshots/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 c195186db8..97b751ec6a 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/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png b/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png index 9a38579e21..bbc29d4be3 100644 Binary files a/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png and b/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png differ diff --git a/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png b/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png index f5bc02ef1e..120b80320b 100644 Binary files a/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png and b/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png differ diff --git a/playwright/snapshots/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 81c08756df..31a7ed42f1 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/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 d8e7d62819..f5eb3935ba 100644 Binary files a/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png and b/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png differ diff --git a/playwright/snapshots/widgets/layout.spec.ts/apps-drawer-linux.png b/playwright/snapshots/widgets/layout.spec.ts/apps-drawer-linux.png index 20618f5d66..203991bc32 100644 Binary files a/playwright/snapshots/widgets/layout.spec.ts/apps-drawer-linux.png and b/playwright/snapshots/widgets/layout.spec.ts/apps-drawer-linux.png differ diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 4ebfcec8de..0fcdf6dee6 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/location/_ZoomButtons.pcss b/res/css/components/views/location/_ZoomButtons.pcss index e44c929432..4e9d3b124b 100644 --- a/res/css/components/views/location/_ZoomButtons.pcss +++ b/res/css/components/views/location/_ZoomButtons.pcss @@ -27,7 +27,7 @@ Please see LICENSE files in the repository root for full details. box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.25); .mx_ZoomButtons_icon { - $ZoomButtons_icon-size: 10px; + $ZoomButtons_icon-size: 12px; height: $ZoomButtons_icon-size; width: $ZoomButtons_icon-size; diff --git a/res/css/structures/_RoomView.pcss b/res/css/structures/_RoomView.pcss index 52fa523c4e..359f67c803 100644 --- a/res/css/structures/_RoomView.pcss +++ b/res/css/structures/_RoomView.pcss @@ -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/_SpacePanel.pcss b/res/css/structures/_SpacePanel.pcss index f3bd82cc96..7875e62973 100644 --- a/res/css/structures/_SpacePanel.pcss +++ b/res/css/structures/_SpacePanel.pcss @@ -229,7 +229,7 @@ Please see LICENSE files in the repository root for full details. &.mx_SpaceButton_new .mx_SpaceButton_icon { &::before { background-color: $primary-content; - mask-image: url("$(res)/img/element-icons/plus.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/plus.svg"); transition: all 0.2s ease-in-out; /* TODO transition */ } } @@ -434,7 +434,7 @@ Please see LICENSE files in the repository root for full details. } .mx_SpacePanel_iconPlus::before { - mask-image: url("$(res)/img/element-icons/plus.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/plus.svg"); } .mx_SpacePanel_iconExplore::before { diff --git a/res/css/views/context_menus/_MessageContextMenu.pcss b/res/css/views/context_menus/_MessageContextMenu.pcss index 85b415ae2b..20d7ed1d13 100644 --- a/res/css/views/context_menus/_MessageContextMenu.pcss +++ b/res/css/views/context_menus/_MessageContextMenu.pcss @@ -25,7 +25,7 @@ Please see LICENSE files in the repository root for full details. } .mx_MessageContextMenu_iconCollapse::before { - mask-image: url("$(res)/img/element-icons/message/chevron-up.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/chevron-up.svg"); } .mx_MessageContextMenu_iconReport::before { diff --git a/res/css/views/dialogs/_InviteDialog.pcss b/res/css/views/dialogs/_InviteDialog.pcss index 753e4155cf..e5c00cef18 100644 --- a/res/css/views/dialogs/_InviteDialog.pcss +++ b/res/css/views/dialogs/_InviteDialog.pcss @@ -143,14 +143,15 @@ Please see LICENSE files in the repository root for full details. margin-inline-end: $spacing-8; .mx_InviteDialog_userTile_pill { - background-color: var(--cpd-color-bg-success-subtle); - border-radius: 12px; + background-color: var(--cpd-color-bg-canvas-default); + border: 1px solid var(--cpd-color-gray-400); + border-radius: 99px; display: inline-block; height: 24px; line-height: $font-24px; padding-inline: $spacing-8; vertical-align: middle; - color: $primary-content; + color: var(--cpd-color-gray-1100); .mx_SearchResultAvatar { border-radius: 20px; diff --git a/res/css/views/elements/_EditableItemList.pcss b/res/css/views/elements/_EditableItemList.pcss index 34ec3199b4..8a85f615d8 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/_InviteReason.pcss b/res/css/views/elements/_InviteReason.pcss index 3060e8a16e..c57c59ae1f 100644 --- a/res/css/views/elements/_InviteReason.pcss +++ b/res/css/views/elements/_InviteReason.pcss @@ -27,7 +27,7 @@ Please see LICENSE files in the repository root for full details. content: ""; margin-right: 8px; background-color: $secondary-content; - mask-image: url("$(res)/img/feather-customised/eye.svg"); + mask-image: url("$(res)/img/element-icons/eye.svg"); display: inline-block; width: 18px; height: 14px; diff --git a/res/css/views/messages/_DecryptionFailureBody.pcss b/res/css/views/messages/_DecryptionFailureBody.pcss index 64a09be7ef..516e7bcc89 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/_MImageBody.pcss b/res/css/views/messages/_MImageBody.pcss index bb6ca1b782..6a6f31948a 100644 --- a/res/css/views/messages/_MImageBody.pcss +++ b/res/css/views/messages/_MImageBody.pcss @@ -100,7 +100,7 @@ Please see LICENSE files in the repository root for full details. margin-right: 8px; background-color: $accent; - mask-image: url("$(res)/img/feather-customised/eye.svg"); + mask-image: url("$(res)/img/element-icons/eye.svg"); display: inline-block; width: 18px; height: 14px; diff --git a/res/css/views/messages/_ViewSourceEvent.pcss b/res/css/views/messages/_ViewSourceEvent.pcss index b8b240816f..6b497719d9 100644 --- a/res/css/views/messages/_ViewSourceEvent.pcss +++ b/res/css/views/messages/_ViewSourceEvent.pcss @@ -25,19 +25,18 @@ Please see LICENSE files in the repository root for full details. } .mx_ViewSourceEvent_toggle { - --ViewSourceEvent_toggle-size: 12px; + --ViewSourceEvent_toggle-size: 16px; visibility: hidden; - /* override styles from AccessibleButton */ - border-radius: 0; /* icon */ - mask-repeat: no-repeat; - mask-position: 0 center; - mask-size: auto var(--ViewSourceEvent_toggle-size); width: var(--ViewSourceEvent_toggle-size); min-width: var(--ViewSourceEvent_toggle-size); - background-color: $accent; - mask-image: url("$(res)/img/element-icons/maximise-expand.svg"); + + svg { + color: $accent; + width: var(--ViewSourceEvent_toggle-size); + height: var(--ViewSourceEvent_toggle-size); + } .mx_EventTile:hover & { visibility: visible; @@ -47,8 +46,5 @@ Please see LICENSE files in the repository root for full details. &.mx_ViewSourceEvent_expanded .mx_ViewSourceEvent_toggle { align-self: flex-end; height: var(--ViewSourceEvent_toggle-size); - mask-position: 0 bottom; - margin-bottom: 5px; - mask-image: url("$(res)/img/element-icons/minimise-collapse.svg"); } } diff --git a/res/css/views/right_panel/_BaseCard.pcss b/res/css/views/right_panel/_BaseCard.pcss index 2adf71e25b..d09a280dfa 100644 --- a/res/css/views/right_panel/_BaseCard.pcss +++ b/res/css/views/right_panel/_BaseCard.pcss @@ -73,7 +73,7 @@ Please see LICENSE files in the repository root for full details. width: 100%; mask-repeat: no-repeat; mask-position: center; - mask-image: url("$(res)/img/element-icons/message/overflow-large.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/overflow-horizontal.svg"); background-color: $secondary-content; } diff --git a/res/css/views/rooms/_EventPreview.pcss b/res/css/views/rooms/_EventPreview.pcss new file mode 100644 index 0000000000..0639c76d98 --- /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/_EventTile.pcss b/res/css/views/rooms/_EventTile.pcss index 92e4cf78ea..311e059166 100644 --- a/res/css/views/rooms/_EventTile.pcss +++ b/res/css/views/rooms/_EventTile.pcss @@ -982,11 +982,11 @@ $left-gutter: 64px; } .mx_EventTile_collapseButton { - mask-image: url("$(res)/img/element-icons/minimise-collapse.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/collapse.svg"); } .mx_EventTile_expandButton { - mask-image: url("$(res)/img/element-icons/maximise-expand.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/expand.svg"); } .mx_EventTile_tileError { diff --git a/res/css/views/rooms/_JumpToBottomButton.pcss b/res/css/views/rooms/_JumpToBottomButton.pcss index 7ca4efe5d0..89aac3c58b 100644 --- a/res/css/views/rooms/_JumpToBottomButton.pcss +++ b/res/css/views/rooms/_JumpToBottomButton.pcss @@ -53,9 +53,9 @@ Please see LICENSE files in the repository root for full details. content: ""; position: absolute; inset: 0; - mask-image: url("$(res)/img/element-icons/message/chevron-up.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/chevron-up.svg"); mask-repeat: no-repeat; - mask-size: 20px; + mask-size: 24px; mask-position: center 6px; transform: rotate(180deg); background: var(--cpd-color-icon-tertiary); diff --git a/res/css/views/rooms/_LegacyRoomHeader.pcss b/res/css/views/rooms/_LegacyRoomHeader.pcss deleted file mode 100644 index dc41108041..0000000000 --- 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/_PinnedMessageBanner.pcss b/res/css/views/rooms/_PinnedMessageBanner.pcss index dd753b7c9e..27c7971833 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 16bf45435a..a53d06fd1c 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 e474cacfcd..07aa1cbf5b 100644 --- a/res/css/views/rooms/_RoomListHeader.pcss +++ b/res/css/views/rooms/_RoomListHeader.pcss @@ -75,7 +75,7 @@ Please see LICENSE files in the repository root for full details. mask-size: contain; mask-repeat: no-repeat; background-color: $secondary-content; - mask-image: url("$(res)/img/element-icons/roomlist/plus.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/plus.svg"); } &:hover { @@ -104,5 +104,5 @@ Please see LICENSE files in the repository root for full details. mask-image: url("$(res)/img/element-icons/roomlist/hash-search.svg"); } .mx_RoomListHeader_iconPlus::before { - mask-image: url("$(res)/img/element-icons/plus.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/plus.svg"); } diff --git a/res/css/views/rooms/_RoomSublist.pcss b/res/css/views/rooms/_RoomSublist.pcss index 2e90c68f87..d4d6f05719 100644 --- a/res/css/views/rooms/_RoomSublist.pcss +++ b/res/css/views/rooms/_RoomSublist.pcss @@ -127,7 +127,7 @@ Please see LICENSE files in the repository root for full details. } .mx_RoomSublist_auxButton::before { - mask-image: url("$(res)/img/element-icons/roomlist/plus.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/plus.svg"); } .mx_RoomSublist_menuButton::before { diff --git a/res/css/views/rooms/_TopUnreadMessagesBar.pcss b/res/css/views/rooms/_TopUnreadMessagesBar.pcss index 95164105d3..dafe192e76 100644 --- a/res/css/views/rooms/_TopUnreadMessagesBar.pcss +++ b/res/css/views/rooms/_TopUnreadMessagesBar.pcss @@ -41,9 +41,9 @@ Please see LICENSE files in the repository root for full details. position: absolute; width: 36px; height: 36px; - mask-image: url("$(res)/img/element-icons/message/chevron-up.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/chevron-up.svg"); mask-repeat: no-repeat; - mask-size: 20px; + mask-size: 24px; mask-position: center; background: var(--cpd-color-icon-tertiary); } diff --git a/res/css/views/voip/LegacyCallView/_LegacyCallViewButtons.pcss b/res/css/views/voip/LegacyCallView/_LegacyCallViewButtons.pcss index 369527b79f..a96fe20ec2 100644 --- a/res/css/views/voip/LegacyCallView/_LegacyCallViewButtons.pcss +++ b/res/css/views/voip/LegacyCallView/_LegacyCallViewButtons.pcss @@ -67,9 +67,9 @@ Please see LICENSE files in the repository root for full details. bottom: 0; &::before { - width: 14px; - height: 14px; - mask-image: url("$(res)/img/element-icons/message/chevron-up.svg"); + width: 16px; + height: 16px; + mask-image: url("@vector-im/compound-design-tokens/icons/chevron-up.svg"); } &.mx_LegacyCallViewButtons_dropdownButton_collapsed::before { diff --git a/res/img/camera.svg b/res/img/camera.svg deleted file mode 100644 index 6519496f78..0000000000 --- a/res/img/camera.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - icon_camera - Created with Sketch. - - - - - - - diff --git a/res/img/element-icons/add-photo.svg b/res/img/element-icons/add-photo.svg deleted file mode 100644 index bde5253bea..0000000000 --- a/res/img/element-icons/add-photo.svg +++ /dev/null @@ -1,5 +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 0a883b7833..0000000000 --- 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 f9d96a1e85..0000000000 --- a/res/img/element-icons/call/spotlight.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/element-icons/community-rooms.svg b/res/img/element-icons/community-rooms.svg deleted file mode 100644 index 570b45a488..0000000000 --- a/res/img/element-icons/community-rooms.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/element-icons/maximise-expand.svg b/res/img/element-icons/maximise-expand.svg deleted file mode 100644 index a63f7e0022..0000000000 --- a/res/img/element-icons/maximise-expand.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/element-icons/message/chevron-up.svg b/res/img/element-icons/message/chevron-up.svg deleted file mode 100644 index 4eb5ecc33e..0000000000 --- a/res/img/element-icons/message/chevron-up.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/res/img/element-icons/message/overflow-large.svg b/res/img/element-icons/message/overflow-large.svg deleted file mode 100644 index 65a52e4aa2..0000000000 --- a/res/img/element-icons/message/overflow-large.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/res/img/element-icons/minimise-collapse.svg b/res/img/element-icons/minimise-collapse.svg deleted file mode 100644 index 535c56a36b..0000000000 --- a/res/img/element-icons/minimise-collapse.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/element-icons/minus-button.svg b/res/img/element-icons/minus-button.svg deleted file mode 100644 index 6e7ea87c0b..0000000000 --- a/res/img/element-icons/minus-button.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/element-icons/plus-button.svg b/res/img/element-icons/plus-button.svg deleted file mode 100644 index 9a14c85ee5..0000000000 --- a/res/img/element-icons/plus-button.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/element-icons/plus.svg b/res/img/element-icons/plus.svg deleted file mode 100644 index ea1972237d..0000000000 --- a/res/img/element-icons/plus.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 3179e33a23..0000000000 --- a/res/img/element-icons/reduce.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/res/img/element-icons/roomlist/archived.svg b/res/img/element-icons/roomlist/archived.svg deleted file mode 100644 index 4d30195082..0000000000 --- a/res/img/element-icons/roomlist/archived.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/element-icons/roomlist/plus.svg b/res/img/element-icons/roomlist/plus.svg deleted file mode 100644 index f6d80ac7ef..0000000000 --- a/res/img/element-icons/roomlist/plus.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/feather-customised/alert-triangle.svg b/res/img/feather-customised/alert-triangle.svg deleted file mode 100644 index ceb664790f..0000000000 --- a/res/img/feather-customised/alert-triangle.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/res/img/feather-customised/edit.svg b/res/img/feather-customised/edit.svg deleted file mode 100644 index f511aa1477..0000000000 --- a/res/img/feather-customised/edit.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/res/img/feather-customised/eye.svg b/res/img/feather-customised/eye.svg deleted file mode 100644 index fd06bf7b21..0000000000 --- a/res/img/feather-customised/eye.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/res/img/feather-customised/plus.svg b/res/img/feather-customised/plus.svg deleted file mode 100644 index c747253139..0000000000 --- a/res/img/feather-customised/plus.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/res/img/feather-customised/widget/external-link.svg b/res/img/feather-customised/widget/external-link.svg deleted file mode 100644 index e2df9883fa..0000000000 --- a/res/img/feather-customised/widget/external-link.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/res/img/icon-address-delete.svg b/res/img/icon-address-delete.svg deleted file mode 100644 index 1289d5aafc..0000000000 --- a/res/img/icon-address-delete.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - 943783E9-DBD7-4D4E-BAC9-35437C17C2C4 - Created with sketchtool. - - - - - - - - - - diff --git a/res/img/icon-email-user.svg b/res/img/icon-email-user.svg deleted file mode 100644 index 2d41e06f98..0000000000 --- a/res/img/icon-email-user.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - 6F488856-F8EF-479C-9747-AB6E0945C7DE - Created with sketchtool. - - - - - - - - - - - - diff --git a/res/img/icon-invite-people.svg b/res/img/icon-invite-people.svg deleted file mode 100644 index 73500ebe06..0000000000 --- a/res/img/icon-invite-people.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/res/img/icon-return.svg b/res/img/icon-return.svg deleted file mode 100644 index 80da0f82aa..0000000000 --- a/res/img/icon-return.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - B542A09B-DBBF-41D4-A5FD-D05EE1E6BBC4 - Created with sketchtool. - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/icon_context_delete.svg b/res/img/icon_context_delete.svg deleted file mode 100644 index 896b94ad13..0000000000 --- a/res/img/icon_context_delete.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/res/img/icons-create-room.svg b/res/img/icons-create-room.svg deleted file mode 100644 index 78c45563d1..0000000000 --- a/res/img/icons-create-room.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/res/img/icons-room-add.svg b/res/img/icons-room-add.svg deleted file mode 100644 index f0b7584df9..0000000000 --- a/res/img/icons-room-add.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/res/img/location/pointer.svg b/res/img/location/pointer.svg deleted file mode 100644 index 8a7c5edf71..0000000000 --- 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 9aadd3cb7f..0000000000 --- a/res/img/markdown.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/subtract.svg b/res/img/subtract.svg deleted file mode 100644 index 55e25831ef..0000000000 --- a/res/img/subtract.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/voip/signal-bars.svg b/res/img/voip/signal-bars.svg deleted file mode 100644 index 6802ba2d34..0000000000 --- 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 880aec5b21..2bbee8305f 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/scripts/make-react-component.js b/scripts/make-react-component.js index 40eb331785..69d2957d42 100755 --- a/scripts/make-react-component.js +++ b/scripts/make-react-component.js @@ -32,7 +32,7 @@ export default %%ComponentName%%; `, TEST: ` import React from "react"; -import { render } from "@testing-library/react"; +import { render } from "jest-matrix-react"; import %%ComponentName%% from '%%RelativeComponentPath%%'; diff --git a/sonar-project.properties b/sonar-project.properties index 925cffcacd..2d87d32efc 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -11,5 +11,13 @@ sonar.exclusions=__mocks__,docs,element.io,nginx sonar.cpd.exclusions=src/i18n/strings/*.json sonar.typescript.tsconfigPath=./tsconfig.json sonar.javascript.lcov.reportPaths=coverage/lcov.info -sonar.coverage.exclusions=test/**/*,playwright/**/*,res/**/*,src/vector/modernizr.js,src/components/views/dialogs/devtools/**/*,src/utils/SessionLock.ts +sonar.coverage.exclusions=\ + test/**/*,\ + playwright/**/*,\ + res/**/*,\ + scripts/**/*,\ + src/vector/modernizr.js,\ + src/components/views/dialogs/devtools/**/*,\ + src/utils/SessionLock.ts,\ + src/**/*.d.ts sonar.testExecutionReportPaths=coverage/jest-sonar-report.xml diff --git a/src/@types/react.d.ts b/src/@types/react.d.ts index 3579e0cec0..ba97b2bba6 100644 --- a/src/@types/react.d.ts +++ b/src/@types/react.d.ts @@ -13,4 +13,7 @@ declare module "react" { function forwardRef( render: (props: PropsWithChildren

, ref: React.ForwardedRef) => React.ReactElement | null, ): (props: P & React.RefAttributes) => React.ReactElement | null; + + // Fix lazy types - https://stackoverflow.com/a/71017028 + function lazy>(factory: () => Promise<{ default: T }>): T; } diff --git a/src/AsyncWrapper.tsx b/src/AsyncWrapper.tsx index 179d42668e..cec814df17 100644 --- a/src/AsyncWrapper.tsx +++ b/src/AsyncWrapper.tsx @@ -37,6 +37,7 @@ export default class AsyncWrapper extends React.Component { public state: IState = {}; public componentDidMount(): void { + this.unmounted = false; this.props.prom .then((result) => { if (this.unmounted) return; diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts index 8bbad339c7..0017d00dac 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/CreateCrossSigning.ts b/src/CreateCrossSigning.ts new file mode 100644 index 0000000000..e67e030f60 --- /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 d3f2ad2671..1e07ba252b 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 ea812d7379..02e26729d2 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 5e4265bc26..9a346bf2f6 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 53a1935294..a2919bdc5f 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -7,10 +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 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"; @@ -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() { @@ -389,19 +393,14 @@ 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({ action: "aria_unhide_main_app", }); - ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateContainer()); - ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateStaticContainer()); + ModalManager.getOrCreateRoot().render(<>); + ModalManager.getOrCreateStaticRoot().render(<>); return; } @@ -416,24 +415,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 +444,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 bb6885e427..3ca098311f 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, ReactFragment, ReactInstance, ReactPortal } 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 { @@ -24,7 +23,7 @@ interface IProps { innerRef?: MutableRefObject; } -function isReactElement(c: ReactElement | ReactFragment | ReactPortal): c is ReactElement { +function isReactElement(c: ReturnType<(typeof React.Children)["toArray"]>[number]): c is ReactElement { return typeof c === "object" && "type" in c; } @@ -36,7 +35,7 @@ function isReactElement(c: ReactElement | ReactFragment | ReactPortal): c is Rea * 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,25 +97,29 @@ export default class NodeAnimator extends React.Component { }); } - private collectNode(k: Key, node: React.ReactInstance, restingStyle: React.CSSProperties): void { - if (node && this.nodes[k] === undefined && this.props.startStyles.length > 0) { + private collectNode(k: Key, domNode: HTMLElement | null, restingStyle: React.CSSProperties): void { + const key = typeof k === "bigint" ? Number(k) : k; + 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[k] = 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/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 2ffe8b8166..6217d9b7dd 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 11a333ce04..af06d4a1d6 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 2d103cf25e..4717404222 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -76,7 +76,7 @@ async function getSecretStorageKey({ keys: Record; }): Promise<[string, Uint8Array]> { const cli = MatrixClientPeg.safeGet(); - let keyId = await cli.getDefaultSecretStorageKeyId(); + let keyId = await cli.secretStorage.getDefaultKeyId(); let keyInfo!: SecretStorage.SecretStorageKeyDescription; if (keyId) { // use the default SSSS key if set diff --git a/src/async-components/structures/ErrorView.tsx b/src/async-components/structures/ErrorView.tsx index f2713e00af..5e60b8e67c 100644 --- a/src/async-components/structures/ErrorView.tsx +++ b/src/async-components/structures/ErrorView.tsx @@ -25,6 +25,7 @@ interface IProps { title: string; messages?: string[]; footer?: ReactNode; + children?: ReactNode; } export const ErrorView: React.FC = ({ title, messages, footer, children }) => { diff --git a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx index 3ec62c3df5..9608abc4bc 100644 --- a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx @@ -69,7 +69,7 @@ export default class CreateKeyBackupDialog 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 keyCallback = (k: Uint8Array): void => {}; - - const { finished } = Modal.createDialog( - RestoreKeyBackupDialog, - { - showSummary: false, - keyCallback, - }, - 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 => { @@ -498,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 (
@@ -832,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 fa41d53a45..d08259f2cb 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/components/structures/EmbeddedPage.tsx b/src/components/structures/EmbeddedPage.tsx index 5de1261ecb..5c7e81caf5 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 91e52a1905..4b0f060952 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 f8cd0184d4..49d0f570a5 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 84c43fc19d..75156cdf60 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -228,9 +228,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 b6c9d890cb..80a648b5d5 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -231,10 +231,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 +256,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 +280,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 +452,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 +496,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 +1010,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 +1087,7 @@ export default class MatrixChat extends React.PureComponent { }, () => { ThemeController.isLogin = false; - this.themeWatcher.recheck(); + this.themeWatcher?.recheck(); this.notifyNewScreen("room/" + presentedId, replaceLast); }, ); @@ -1113,7 +1112,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 +1122,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 +1135,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 +1356,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(); @@ -1709,9 +1708,10 @@ export default class MatrixChat extends React.PureComponent { } } - if (cli.getCrypto()) { + const crypto = cli.getCrypto(); + if (crypto) { const blacklistEnabled = SettingsStore.getValueAt(SettingLevel.DEVICE, "blacklistUnverifiedDevices"); - cli.setGlobalBlacklistUnverifiedDevices(blacklistEnabled); + crypto.globalBlacklistUnverifiedDevices = blacklistEnabled; // With cross-signing enabled, we send to unknown devices // without prompting. Any bad-device status the user should @@ -2087,6 +2087,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 2eca6db934..d01bf78959 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 bd236f2286..76f3b0c229 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 486a7fb652..520760713c 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -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 => { @@ -904,6 +848,66 @@ export class RoomView extends React.Component { } public componentDidMount(): void { + this.unmounted = false; + + this.dispatcherRef = dis.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(); diff --git a/src/components/structures/ScrollPanel.tsx b/src/components/structures/ScrollPanel.tsx index 6c0da3018f..b354f6b005 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 4f0c895233..3ea2a03c1a 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 e5c1ccb266..be538a6669 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 b55709e8c2..68b65965f5 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 8c572442a0..3e5b4a4474 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 93ce6d6bf2..01ecae96dc 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 971e07193b..b2c7990746 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 a74e07692d..ec65a62cef 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 9e103f2ac5..80a135fe19 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 666313321a..32528fc7e3 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/views/audio_messages/AudioPlayerBase.tsx b/src/components/views/audio_messages/AudioPlayerBase.tsx index 70e30dccca..601611e422 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 56661c7a0c..ca72d29a05 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 e495098144..3794ab9a4f 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 1053a89eea..1cd2d168b4 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 8de3cb71e6..b3d736758b 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 5f59289879..0f95f7084b 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 c81617b9db..8d27a04c83 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 3ff11ba3f2..07cc2f978a 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 e9beb6d2a0..2782d0a641 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 44ccd3a30e..b1360f5560 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 234c6b127b..0000000000 --- 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 3cdf30cafc..0000000000 --- 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 55cc76fa45..0000000000 --- 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/context_menus/GenericElementContextMenu.tsx b/src/components/views/context_menus/GenericElementContextMenu.tsx index 42ed8ce5be..afb39d6ebe 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 817b4632e8..e6bb191df8 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 5cf947092b..d5749658c9 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 = ( { onClick={this.onCancelClick} className="mx_Dialog_cancelButton" aria-label={_t("dialog_close_label")} - title={_t("action|close")} placement="bottom" /> ); diff --git a/src/components/views/dialogs/BugReportDialog.tsx b/src/components/views/dialogs/BugReportDialog.tsx index 22a7efacb9..373f30d3ae 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 c5a8080e3f..990efdda71 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 fbcd26d38f..d68c931cc1 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 8e1d49c138..35e04fb12e 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 */ diff --git a/src/components/views/dialogs/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx index 2c4656745a..cb804b8e00 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/VerificationRequestDialog.tsx b/src/components/views/dialogs/VerificationRequestDialog.tsx index 50644ccf30..d2ea83f2af 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/security/AccessSecretStorageDialog.tsx b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx index 50b728f4da..7361e3982d 100644 --- a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx +++ b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx @@ -229,7 +229,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent => { // Now reset cross-signing so everything Just Works™ again. const cli = MatrixClientPeg.safeGet(); - await cli.bootstrapCrossSigning({ + await cli.getCrypto()?.bootstrapCrossSigning({ authUploadDeviceSigningKeys: async (makeRequest): Promise => { const { finished } = Modal.createDialog(InteractiveAuthDialog, { title: _t("encryption|bootstrap_title"), diff --git a/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx index bc5bc6b21e..73da6b178c 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(); - } - } - - 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, - }); + setError(false); try { - const cli = MatrixClientPeg.safeGet(); - await cli.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); } - }; + }, [matrixClient, tokenLogin, accountPassword, onFinished]); - private onCancel = (): void => { - this.props.onFinished(false); - }; + const onCancel = useCallback(() => { + onFinished(false); + }, [onFinished]); - public render(): React.ReactNode { - let content; - if (this.state.error) { - content = ( -
-

{_t("encryption|unable_to_setup_keys_error")}

-
- -
+ 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/dialogs/security/RestoreKeyBackupDialog.tsx b/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx index 3b54f4f197..4d29c1cfa3 100644 --- a/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx +++ b/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx @@ -37,9 +37,6 @@ interface IProps { // if false, will close the dialog as soon as the restore completes successfully // default: true showSummary?: boolean; - // If specified, gather the key from the user but then call the function with the backup - // key rather than actually (necessarily) restoring the backup. - keyCallback?: (key: Uint8Array) => void; onFinished(done?: boolean): void; } @@ -156,13 +153,6 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { 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); @@ -301,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(); @@ -338,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); } @@ -372,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; } } @@ -579,18 +586,21 @@ export default class AppTile extends React.Component { : Container.Center; WidgetLayoutStore.instance.moveToContainer(this.props.room, this.props.app, targetContainer); - // If the right panel has a timeline, but we're about to show the timeline in the main view, pop the right panel - if ( - targetContainer === Container.Top && - RightPanelStore.instance.currentCardForRoom(this.props.room.roomId).phase === RightPanelPhases.Timeline - ) { - RightPanelStore.instance.popCard(this.props.room.roomId); - } + if (targetContainer === Container.Top) this.closeChatCardIfNeeded(); }; private onMinimiseClicked = (): void => { if (!this.props.room) return; // ignore action - it shouldn't even be visible WidgetLayoutStore.instance.moveToContainer(this.props.room, this.props.app, Container.Right); + this.closeChatCardIfNeeded(); + }; + + private closeChatCardIfNeeded = (): void => { + if (!this.props.room) return; // ignore action - it shouldn't even be visible + // If the right panel has a timeline, but we're about to show the timeline in the main view, pop the right panel + if (RightPanelStore.instance.currentCardForRoom(this.props.room.roomId).phase === RightPanelPhases.Timeline) { + RightPanelStore.instance.popCard(this.props.room.roomId); + } }; private onContextMenuClick = (): void => { @@ -602,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 @@ -645,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 = ( @@ -672,7 +682,7 @@ export default class AppTile extends React.Component {
); - } else { + } else if (this.sgWidget) { appTileBody = ( <>
@@ -759,7 +769,7 @@ export default class AppTile extends React.Component { {isMaximised ? ( ) : ( - + )} , ); @@ -771,7 +781,7 @@ export default class AppTile extends React.Component { title={_t("action|minimise")} onClick={this.onMinimiseClicked} > - + , ); } @@ -806,7 +816,7 @@ export default class AppTile extends React.Component { ref={this.contextMenuButton} onClick={this.onContextMenuClick} > - + )} diff --git a/src/components/views/elements/DesktopCapturerSourcePicker.tsx b/src/components/views/elements/DesktopCapturerSourcePicker.tsx index 26b759bcb2..e1f1def836 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 c8802cd880..a7ce84163c 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/LinkWithTooltip.tsx b/src/components/views/elements/LinkWithTooltip.tsx index a9ca2606ae..016297d9f1 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 1b7b6543e9..3feb856145 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 b600b2ba96..385d932b87 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 4eb3707031..71846d6065 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 34346cbe25..b589ce3635 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 2c2eb442a0..b62df99e25 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/ZoomButtons.tsx b/src/components/views/location/ZoomButtons.tsx index 2ba3d6a598..1b370744de 100644 --- a/src/components/views/location/ZoomButtons.tsx +++ b/src/components/views/location/ZoomButtons.tsx @@ -8,11 +8,10 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import * as maplibregl from "maplibre-gl"; +import { PlusIcon, MinusIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { _t } from "../../../languageHandler"; import AccessibleButton from "../elements/AccessibleButton"; -import { Icon as PlusIcon } from "../../../../res/img/element-icons/plus-button.svg"; -import { Icon as MinusIcon } from "../../../../res/img/element-icons/minus-button.svg"; interface Props { map: maplibregl.Map; diff --git a/src/components/views/messages/DateSeparator.tsx b/src/components/views/messages/DateSeparator.tsx index 10ec7ddad9..6aed04d8f9 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 => { diff --git a/src/components/views/messages/DecryptionFailureBody.tsx b/src/components/views/messages/DecryptionFailureBody.tsx index 81894fa51f..108ec45b03 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 dcb8b82774..8316d0835b 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 a547a78f94..bec4f56164 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 742587e0a7..b226476fa8 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 4900432b8c..4036b9ddec 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/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 08b7991807..0c05236176 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/messages/ViewSourceEvent.tsx b/src/components/views/messages/ViewSourceEvent.tsx index 6f8dd7f270..8352f0f7e2 100644 --- a/src/components/views/messages/ViewSourceEvent.tsx +++ b/src/components/views/messages/ViewSourceEvent.tsx @@ -9,6 +9,7 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/matrix"; import classNames from "classnames"; +import { CollapseIcon, ExpandIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { _t } from "../../../languageHandler"; @@ -73,7 +74,9 @@ export default class ViewSourceEvent extends React.PureComponent title={_t("devtools|toggle_event")} className="mx_ViewSourceEvent_toggle" onClick={this.onToggle} - /> + > + {expanded ? : } + ); } diff --git a/src/components/views/right_panel/TimelineCard.tsx b/src/components/views/right_panel/TimelineCard.tsx index 4f9d1dd917..e0988eeaa5 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/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index d6f4b0300a..d07b3566e2 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -52,7 +52,7 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases"; import EncryptionPanel from "./EncryptionPanel"; import { useAsyncMemo } from "../../../hooks/useAsyncMemo"; -import { legacyVerifyUser, verifyDevice, verifyUser } from "../../../verification"; +import { verifyDevice, verifyUser } from "../../../verification"; import { Action } from "../../../dispatcher/actions"; import { useIsEncrypted } from "../../../hooks/useIsEncrypted"; import BaseCard from "./BaseCard"; @@ -1551,13 +1551,7 @@ const BasicUserInfo: React.FC<{ { - if (hasCrossSigningKeys) { - verifyUser(cli, member as User); - } else { - legacyVerifyUser(cli, member as User); - } - }} + onClick={() => verifyUser(cli, member as User)} > {_t("action|verify")} diff --git a/src/components/views/rooms/AppsDrawer.tsx b/src/components/views/rooms/AppsDrawer.tsx index cba6e6c691..c02bfe8cf2 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 db4ac37415..af33fb2d9e 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 0add0c1027..5f033de238 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/EditMessageComposer.tsx b/src/components/views/rooms/EditMessageComposer.tsx index 06f189df59..d62a451b8b 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 eca61f7d22..22da73bef7 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 993a2ba1f1..e503ce2363 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 73a048d3dd..7273e45e0e 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -135,9 +135,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 = ""; @@ -146,13 +143,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, - }); - } } } @@ -172,11 +162,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 { @@ -249,6 +234,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!); @@ -332,7 +336,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 c4cc418db4..6825ea8e43 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 19405e96b0..bb787d509a 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 e47a785779..c2642ea733 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 12f6e70d31..34961c0853 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 93fb42f447..8351c176ff 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 776963eb33..a12a09dcb7 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 ea76dd0d36..4a3032d641 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 6d00e182e8..a91238848e 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 853a75e6b1..ae0436a9e5 100644 --- a/src/components/views/settings/CryptographyPanel.tsx +++ b/src/components/views/settings/CryptographyPanel.tsx @@ -11,7 +11,6 @@ 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"; @@ -20,6 +19,7 @@ import SettingsStore from "../../../settings/SettingsStore"; import SettingsFlag from "../elements/SettingsFlag"; import { SettingLevel } from "../../../settings/SettingLevel"; import SettingsSubsection, { SettingsSubsectionText } from "./shared/SettingsSubsection"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; interface IProps {} @@ -33,17 +33,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 +62,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) { @@ -126,7 +133,7 @@ export default class CryptographyPanel extends React.Component { import("../../../async-components/views/dialogs/security/ExportE2eKeysDialog") as unknown as Promise< typeof ExportE2eKeysDialog >, - { matrixClient: MatrixClientPeg.safeGet() }, + { matrixClient: this.context }, ); }; @@ -135,11 +142,12 @@ export default class CryptographyPanel extends React.Component { import("../../../async-components/views/dialogs/security/ImportE2eKeysDialog") as unknown as Promise< typeof ImportE2eKeysDialog >, - { matrixClient: MatrixClientPeg.safeGet() }, + { matrixClient: this.context }, ); }; private updateBlacklistDevicesFlag = (checked: boolean): void => { - MatrixClientPeg.safeGet().setGlobalBlacklistUnverifiedDevices(checked); + const crypto = this.context.getCrypto(); + if (crypto) crypto.globalBlacklistUnverifiedDevices = checked; }; } diff --git a/src/components/views/settings/FontScalingPanel.tsx b/src/components/views/settings/FontScalingPanel.tsx index 5cdd9d16bb..b7f7c64a3b 100644 --- a/src/components/views/settings/FontScalingPanel.tsx +++ b/src/components/views/settings/FontScalingPanel.tsx @@ -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/IntegrationManager.tsx b/src/components/views/settings/IntegrationManager.tsx index 91b3b4633f..3a31a9e9c8 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/Notifications.tsx b/src/components/views/settings/Notifications.tsx index 78660d4f9d..6890c7b5d3 100644 --- a/src/components/views/settings/Notifications.tsx +++ b/src/components/views/settings/Notifications.tsx @@ -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 dac7425e3c..6a855c8ea8 100644 --- a/src/components/views/settings/SecureBackupPanel.tsx +++ b/src/components/views/settings/SecureBackupPanel.tsx @@ -83,6 +83,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { } public componentDidMount(): void { + this.unmounted = false; this.loadBackupStatus(); MatrixClientPeg.safeGet().on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus); diff --git a/src/components/views/settings/SetIdServer.tsx b/src/components/views/settings/SetIdServer.tsx index e7a55f1133..8ed6461d0a 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/devices/DeviceDetails.tsx b/src/components/views/settings/devices/DeviceDetails.tsx index dda0089d8b..1a418f5dd5 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/LoginWithQRSection.tsx b/src/components/views/settings/devices/LoginWithQRSection.tsx index c5efb35efc..e9d8029987 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, @@ -28,27 +25,11 @@ 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/discovery/DiscoverySettings.tsx b/src/components/views/settings/discovery/DiscoverySettings.tsx index d240d53d7c..6fbc91874c 100644 --- a/src/components/views/settings/discovery/DiscoverySettings.tsx +++ b/src/components/views/settings/discovery/DiscoverySettings.tsx @@ -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} /> diff --git a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx index c32ac5150b..337cead3a3 100644 --- a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx @@ -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/user/SecurityUserSettingsTab.tsx b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx index 9e15df6e92..ba4b5eb54b 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx @@ -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<{ { 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 06cd331b11..aba3d60743 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 c1b8b580a9..0000000000 --- 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/dispatcher.ts b/src/dispatcher/dispatcher.ts index 6d8d3a15a8..0c28de0e2b 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); } 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 adf16cc5cc..e1db9f9248 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 ff55a12633..7647377196 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 e8bff7cd44..abe4566f8c 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 59309710cf..2c042c0dc3 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 560397c9fb..616043aa4e 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" @@ -935,7 +930,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": { @@ -1117,7 +1111,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", @@ -2044,14 +2046,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", @@ -2593,18 +2587,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." @@ -3278,8 +3267,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)", @@ -3718,7 +3707,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 51d17e0141..33d8e57f48 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 7b5185d360..cb6a8557b3 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 9777913c0b..b05d9024c0 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 cddf37c691..5541bbbfbd 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 acb77b4f8d..091761af4b 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 bb79008a83..905aa11fa7 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", @@ -1809,10 +1804,23 @@ }, "right_panel": { "add_integrations": "Ajouter des widgets, passerelles et robots", + "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é", @@ -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", @@ -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" }, diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index c1b2fe83e9..00277a5f3e 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 85982203ce..c98d59c659 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 97e87f663a..cf410fb82f 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 4881ee45e9..77cdd8a78f 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 6d5a2194e9..c746caef1d 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 61889463b0..9f0ab6d430 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 db4cb20ca0..963de355ac 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 fffbd25e9b..6310faf200 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 9ccd0962ca..8f503bacdd 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 18500da4bf..1231a9a330 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 7aa90f7f85..e148c873f1 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 6428774388..6789fb4ee6 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 fef57bbef2..50a3ee50e9 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 d336a89bba..34ba4789bf 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 a8b484da54..b7258c26cb 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 4be1a8df93..bb4e489952 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 f5652b6b44..7d438ec2a4 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 7f92c66820..5ce36aed05 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 a9e1df778a..99d5586a5c 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 f5e8b0926c..68b3694ee8 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/languageHandler.tsx b/src/languageHandler.tsx index dedcb60dcd..c30ca94922 100644 --- a/src/languageHandler.tsx +++ b/src/languageHandler.tsx @@ -436,7 +436,7 @@ export function replaceByRegexes(text: string, mapping: IVariables | Tags): stri } if (shouldWrapInSpan) { - return React.createElement("span", null, ...output); + return React.createElement("span", null, ...(output as Array)); } else { return output.join(""); } diff --git a/src/mjolnir/Mjolnir.ts b/src/mjolnir/Mjolnir.ts index 8c9d939908..9dacc0d41f 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 0238d15914..4beb5fccc1 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/settings/SettingsStore.ts b/src/settings/SettingsStore.ts index 45ba4e3dbb..98ae347a0a 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 0c7e596530..64a6a27f58 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 74a3158c62..d0f00c52d9 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 5697969a1d..4baf807247 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 418143a16c..a131614c7c 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 f60dae07fe..ccd4bf33a3 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 a46a09899a..922e8b8393 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/SetupEncryptionStore.ts b/src/stores/SetupEncryptionStore.ts index 6fafd6efaa..6b67bcfc49 100644 --- a/src/stores/SetupEncryptionStore.ts +++ b/src/stores/SetupEncryptionStore.ts @@ -101,18 +101,14 @@ export class SetupEncryptionStore extends EventEmitter { this.keyInfo = keys[this.keyId]; } - // do we have any other verified devices which are E2EE which we can verify against? - const dehydratedDevice = await cli.getDehydratedDevice(); const ownUserId = cli.getUserId()!; const crypto = cli.getCrypto()!; + // do we have any other verified devices which are E2EE which we can verify against? const userDevices: Iterable = (await crypto.getUserDeviceInfo([ownUserId])).get(ownUserId)?.values() ?? []; this.hasDevicesToVerifyAgainst = await asyncSome(userDevices, async (device) => { - // Ignore dehydrated devices. `dehydratedDevice` is set by the - // implementation of MSC2697, whereas MSC3814 proposes that devices - // should set a `dehydrated` flag in the device key. We ignore - // both types of dehydrated devices. - if (dehydratedDevice && device.deviceId == dehydratedDevice?.device_id) return false; + // Ignore dehydrated devices. MSC3814 proposes that devices + // should set a `dehydrated` flag in the device key. if (device.dehydrated) return false; // ignore devices without an identity key diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index bf4ee16b5d..09553b40ce 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -127,12 +127,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 +169,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 +414,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"); diff --git a/src/stores/widgets/WidgetLayoutStore.ts b/src/stores/widgets/WidgetLayoutStore.ts index cefbee0f6b..f0d3cafc83 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 39408ba7f7..0dd54bb18f 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 96efc82f4b..0000000000 --- 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 08e488e5ff..9a6bb93bba 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} +
+ ${roomAvatar} +
+
+ + ${safeRoomName} + +
-
-
-
- ${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 3d034d7df5..b3fa8e81d3 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 2c19f11491..1859e90fd6 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 0000000000..164d704d91 --- /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 d3d8a7ab8e..13b0de23c4 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 65ce431a97..fc319b2024 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 0c2230bbb8..426163db0b 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/init.tsx b/src/vector/init.tsx index da9827cb55..2028f9af36 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 c71865c3c1..d7ebd94bb2 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 040f3d713d..0000000000 --- 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 53ff14d82f..bb573c89c0 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/src/verification.ts b/src/verification.ts index 5b6d011ba1..e446186f80 100644 --- a/src/verification.ts +++ b/src/verification.ts @@ -7,7 +7,6 @@ Please see LICENSE files in the repository root for full details. */ import { User, MatrixClient, RoomMember } from "matrix-js-sdk/src/matrix"; -import { VerificationMethod } from "matrix-js-sdk/src/types"; import { CrossSigningKey, VerificationRequest } from "matrix-js-sdk/src/crypto-api"; import dis from "./dispatcher/dispatcher"; @@ -39,7 +38,7 @@ export async function verifyDevice(matrixClient: MatrixClient, user: User, devic return; } // if cross-signing is not explicitly disabled, check if it should be enabled first. - if (matrixClient.getCryptoTrustCrossSignedDevices()) { + if (matrixClient.getCrypto()?.getTrustCrossSignedDevices()) { if (!(await enable4SIfNeeded(matrixClient))) { return; } @@ -50,11 +49,9 @@ export async function verifyDevice(matrixClient: MatrixClient, user: User, devic device, onFinished: async (action): Promise => { if (action === "sas") { - const verificationRequestPromise = matrixClient.legacyDeviceVerification( - user.userId, - device.deviceId, - VerificationMethod.Sas, - ); + const verificationRequestPromise = matrixClient + .getCrypto() + ?.requestDeviceVerification(user.userId, device.deviceId); setRightPanel({ member: user, verificationRequestPromise }); } else if (action === "legacy") { Modal.createDialog(ManualDeviceKeyVerificationDialog, { @@ -66,21 +63,6 @@ export async function verifyDevice(matrixClient: MatrixClient, user: User, devic }); } -export async function legacyVerifyUser(matrixClient: MatrixClient, user: User): Promise { - if (matrixClient.isGuest()) { - dis.dispatch({ action: "require_registration" }); - return; - } - // if cross-signing is not explicitly disabled, check if it should be enabled first. - if (matrixClient.getCryptoTrustCrossSignedDevices()) { - if (!(await enable4SIfNeeded(matrixClient))) { - return; - } - } - const verificationRequestPromise = matrixClient.requestVerification(user.userId); - setRightPanel({ member: user, verificationRequestPromise }); -} - export async function verifyUser(matrixClient: MatrixClient, user: User): Promise { if (matrixClient.isGuest()) { dis.dispatch({ action: "require_registration" }); diff --git a/test/CreateCrossSigning-test.ts b/test/CreateCrossSigning-test.ts new file mode 100644 index 0000000000..e1762bb504 --- /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 0000000000..3e5dc4eb94 --- /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/setupTests.ts b/test/setupTests.ts index 64b8fa2909..f0067a5d23 100644 --- a/test/setupTests.ts +++ b/test/setupTests.ts @@ -13,6 +13,13 @@ import { mocked } from "jest-mock"; import { PredictableRandom } from "./test-utils/predictableRandom"; // https://github.com/jsdom/jsdom/issues/2555 +declare global { + // eslint-disable-next-line no-var + var IS_REACT_ACT_ENVIRONMENT: boolean; +} + +globalThis.IS_REACT_ACT_ENVIRONMENT = true; + // Fake random strings to give a predictable snapshot for IDs jest.mock("matrix-js-sdk/src/randomstring"); beforeEach(() => { diff --git a/test/test-utils/client.ts b/test/test-utils/client.ts index 7b0e22e70e..0a5798d8a1 100644 --- a/test/test-utils/client.ts +++ b/test/test-utils/client.ts @@ -141,10 +141,8 @@ export const mockClientMethodsDevice = ( export const mockClientMethodsCrypto = (): Partial< Record & PropertyLikeKeys, unknown> > => ({ - isCrossSigningReady: jest.fn(), isKeyBackupKeyStored: jest.fn(), getCrossSigningCacheCallbacks: jest.fn().mockReturnValue({ getCrossSigningKeyCache: jest.fn() }), - getStoredCrossSigningForUser: jest.fn(), getKeyBackupVersion: jest.fn().mockResolvedValue(null), secretStorage: { hasKey: jest.fn() }, getCrypto: jest.fn().mockReturnValue({ diff --git a/test/test-utils/jest-matrix-react.tsx b/test/test-utils/jest-matrix-react.tsx index 2aad5d45ff..4fbb0dc77d 100644 --- a/test/test-utils/jest-matrix-react.tsx +++ b/test/test-utils/jest-matrix-react.tsx @@ -27,6 +27,7 @@ const wrapWithTooltipProvider = (Wrapper: RenderOptions["wrapper"]) => { const customRender = (ui: ReactElement, options: RenderOptions = {}) => { return render(ui, { + legacyRoot: true, ...options, wrapper: wrapWithTooltipProvider(options?.wrapper) as RenderOptions["wrapper"], }) as ReturnType; diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index 33219eca35..0e608b1426 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -95,21 +95,17 @@ export function createTestClient(): MatrixClient { getUser: jest.fn().mockReturnValue({ on: jest.fn(), off: jest.fn() }), getDevice: jest.fn(), getDeviceId: jest.fn().mockReturnValue("ABCDEFGHI"), - getStoredCrossSigningForUser: jest.fn(), - getStoredDevice: jest.fn(), - requestVerification: jest.fn(), deviceId: "ABCDEFGHI", getDevices: jest.fn().mockResolvedValue({ devices: [{ device_id: "ABCDEFGHI" }] }), getSessionId: jest.fn().mockReturnValue("iaszphgvfku"), credentials: { userId: "@userId:matrix.org" }, - bootstrapCrossSigning: jest.fn(), - hasSecretStorageKey: jest.fn(), getKeyBackupVersion: jest.fn(), secretStorage: { get: jest.fn(), isStored: jest.fn().mockReturnValue(false), checkKey: jest.fn().mockResolvedValue(false), + hasKey: jest.fn().mockReturnValue(false), }, store: { @@ -129,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(), @@ -209,12 +210,10 @@ export function createTestClient(): MatrixClient { }), hasLazyLoadMembersEnabled: jest.fn().mockReturnValue(false), isInitialSyncComplete: jest.fn().mockReturnValue(true), - downloadKeys: jest.fn(), fetchRoomEvent: jest.fn().mockRejectedValue({}), makeTxnId: jest.fn().mockImplementation(() => `t${txnId++}`), sendToDevice: jest.fn().mockResolvedValue(undefined), queueToDevice: jest.fn().mockResolvedValue(undefined), - encryptAndSendToDevices: jest.fn().mockResolvedValue(undefined), cancelPendingEvent: jest.fn(), getMediaHandler: jest.fn().mockReturnValue({ @@ -274,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 1b9e7f0a18..d2459653e5 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 4278b73f74..29b25fda21 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 b7884076ae..898816923f 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 64761d7da1..0862c6b385 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/MatrixClientPeg-test.ts b/test/unit-tests/MatrixClientPeg-test.ts index efa624349a..5a19b568c0 100644 --- a/test/unit-tests/MatrixClientPeg-test.ts +++ b/test/unit-tests/MatrixClientPeg-test.ts @@ -83,12 +83,10 @@ describe("MatrixClientPeg", () => { it("should initialise the rust crypto library by default", async () => { const mockSetValue = jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined); - const mockInitCrypto = jest.spyOn(testPeg.safeGet(), "initCrypto").mockResolvedValue(undefined); const mockInitRustCrypto = jest.spyOn(testPeg.safeGet(), "initRustCrypto").mockResolvedValue(undefined); const cryptoStoreKey = new Uint8Array([1, 2, 3, 4]); await testPeg.start({ rustCryptoStoreKey: cryptoStoreKey }); - expect(mockInitCrypto).not.toHaveBeenCalled(); expect(mockInitRustCrypto).toHaveBeenCalledWith({ storageKey: cryptoStoreKey }); // we should have stashed the setting in the settings store diff --git a/test/unit-tests/async-components/dialogs/security/NewRecoveryMethodDialog-test.tsx b/test/unit-tests/async-components/dialogs/security/NewRecoveryMethodDialog-test.tsx index fc964e57bf..f6d9a59b51 100644 --- a/test/unit-tests/async-components/dialogs/security/NewRecoveryMethodDialog-test.tsx +++ b/test/unit-tests/async-components/dialogs/security/NewRecoveryMethodDialog-test.tsx @@ -7,10 +7,9 @@ import React from "react"; import { MatrixClient } from "matrix-js-sdk/src/matrix"; -import { render, screen } from "jest-matrix-react"; +import { render, screen, act } from "jest-matrix-react"; import { waitFor } from "@testing-library/dom"; import userEvent from "@testing-library/user-event"; -import { act } from "@testing-library/react-hooks/dom"; import NewRecoveryMethodDialog from "../../../../../src/async-components/views/dialogs/security/NewRecoveryMethodDialog"; import { createTestClient } from "../../../../test-utils"; @@ -55,15 +54,13 @@ describe("", () => { const onFinished = jest.fn(); - await act(async () => { - const { asFragment } = renderComponent(onFinished); - await waitFor(() => - expect( - screen.getByText("This session is encrypting history using the new recovery method."), - ).toBeInTheDocument(), - ); - expect(asFragment()).toMatchSnapshot(); - }); + const { asFragment } = renderComponent(onFinished); + await waitFor(() => + expect( + screen.getByText("This session is encrypting history using the new recovery method."), + ).toBeInTheDocument(), + ); + expect(asFragment()).toMatchSnapshot(); await userEvent.click(screen.getByRole("button", { name: "Set up Secure Messages" })); expect(onFinished).toHaveBeenCalled(); diff --git a/test/unit-tests/components/structures/MatrixChat-test.tsx b/test/unit-tests/components/structures/MatrixChat-test.tsx index da6e005a23..4b396b66a9 100644 --- a/test/unit-tests/components/structures/MatrixChat-test.tsx +++ b/test/unit-tests/components/structures/MatrixChat-test.tsx @@ -55,13 +55,14 @@ import * as Lifecycle from "../../../../src/Lifecycle"; import { SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY } from "../../../../src/BasePlatform"; import SettingsStore from "../../../../src/settings/SettingsStore"; import { SettingLevel } from "../../../../src/settings/SettingLevel"; -import { MatrixClientPeg, MatrixClientPeg as peg } from "../../../../src/MatrixClientPeg"; +import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import DMRoomMap from "../../../../src/utils/DMRoomMap"; import { ReleaseAnnouncementStore } from "../../../../src/stores/ReleaseAnnouncementStore"; 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(), @@ -125,7 +126,6 @@ describe("", () => { }), getVisibleRooms: jest.fn().mockReturnValue([]), getRooms: jest.fn().mockReturnValue([]), - setGlobalBlacklistUnverifiedDevices: jest.fn(), setGlobalErrorOnUnknownDevices: jest.fn(), getCrypto: jest.fn().mockReturnValue({ getVerificationRequestsToDeviceInProgress: jest.fn().mockReturnValue([]), @@ -136,9 +136,10 @@ describe("", () => { setDeviceIsolationMode: jest.fn(), userHasCrossSigningKeys: jest.fn(), getActiveSessionBackupVersion: jest.fn().mockResolvedValue(null), + globalBlacklistUnverifiedDevices: false, + // This needs to not finish immediately because we need to test the screen appears + bootstrapCrossSigning: jest.fn().mockImplementation(() => bootstrapDeferred.promise), }), - // This needs to not finish immediately because we need to test the screen appears - bootstrapCrossSigning: jest.fn().mockImplementation(() => bootstrapDeferred.promise), secretStorage: { isStored: jest.fn().mockReturnValue(null), }, @@ -933,17 +934,13 @@ describe("", () => { // but as the exception was swallowed, the test was passing (see in `initClientCrypto`). // There are several uses of the peg in the app, so during all these tests you might end-up // with a real client instead of the mocked one. Not sure how reliable all these tests are. - const originalReplace = peg.replaceUsingCreds; - peg.replaceUsingCreds = jest.fn().mockResolvedValue(mockClient); - // @ts-ignore - need to mock this for the test - peg.matrixClient = mockClient; + jest.spyOn(MatrixClientPeg, "replaceUsingCreds"); + jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient); const result = getComponent(); await result.findByText("You're signed out"); expect(result.container).toMatchSnapshot(); - - peg.replaceUsingCreds = originalReplace; }); }); @@ -956,7 +953,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({ @@ -1011,6 +1008,8 @@ describe("", () => { .mockResolvedValue(new UserVerificationStatus(false, false, false)), setDeviceIsolationMode: jest.fn(), userHasCrossSigningKeys: jest.fn().mockResolvedValue(false), + // This needs to not finish immediately because we need to test the screen appears + bootstrapCrossSigning: jest.fn().mockImplementation(() => bootstrapDeferred.promise), }; loginClient.getCrypto.mockReturnValue(mockCrypto as any); }); @@ -1114,8 +1113,6 @@ describe("", () => { expect(loginClient.getCrypto()!.userHasCrossSigningKeys).toHaveBeenCalled(); - await flushPromises(); - // set up keys screen is rendered expect(screen.getByText("Setting up keys")).toBeInTheDocument(); }); @@ -1483,15 +1480,13 @@ 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({ action: "start_mobile_registration", }); - await flushPromises(); - return renderResult; }; @@ -1505,13 +1500,14 @@ 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 () => { enabledMobileRegistration(); await getComponentAndWaitForReady(); + await flushPromises(); expect(screen.getByTestId("mobile-register")).toBeInTheDocument(); }); @@ -1519,7 +1515,9 @@ describe("", () => { describe("when key backup failed", () => { it("should show the new recovery method dialog", async () => { + const spy = jest.spyOn(Modal, "createDialogAsync"); jest.mock("../../../../src/async-components/views/dialogs/security/NewRecoveryMethodDialog", () => ({ + __test: true, __esModule: true, default: () => mocked dialog, })); @@ -1531,7 +1529,8 @@ describe("", () => { }); await flushPromises(); mockClient.emit(CryptoEvent.KeyBackupFailed, "error code"); - await waitFor(() => expect(screen.getByText("mocked dialog")).toBeInTheDocument()); + await waitFor(() => expect(spy).toHaveBeenCalledTimes(1)); + expect(await spy.mock.lastCall![0]).toEqual(expect.objectContaining({ __test: true })); }); }); }); diff --git a/test/unit-tests/components/structures/TimelinePanel-test.tsx b/test/unit-tests/components/structures/TimelinePanel-test.tsx index 7de688bfe6..4a66351779 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/__snapshots__/FilePanel-test.tsx.snap b/test/unit-tests/components/structures/__snapshots__/FilePanel-test.tsx.snap index ace762cd23..6a58ac9811 100644 --- a/test/unit-tests/components/structures/__snapshots__/FilePanel-test.tsx.snap +++ b/test/unit-tests/components/structures/__snapshots__/FilePanel-test.tsx.snap @@ -19,7 +19,7 @@ exports[`FilePanel renders empty state 1`] = `

+
@@ -168,331 +217,6 @@ exports[`CreateSecretStorageDialog shows a loading spinner initially 1`] = `
`; -exports[`CreateSecretStorageDialog when backup is present but not trusted shows migrate text, then 'RestoreKeyBackupDialog' if 'Restore' is clicked 1`] = ` -
-
- -
-
-`; - -exports[`CreateSecretStorageDialog when canUploadKeysWithPasswordOnly calls bootstrapSecretStorage once keys are restored if the backup is now trusted 1`] = ` -
-
- -
-
-`; - -exports[`CreateSecretStorageDialog when canUploadKeysWithPasswordOnly prompts for a password and then shows RestoreKeyBackupDialog 1`] = ` -
-
- -
-
-`; - -exports[`CreateSecretStorageDialog when canUploadKeysWithPasswordOnly when there is an error fetching the backup version after RestoreKeyBackupDialog handles the error sensibly 1`] = ` -
-
- -
-
-`; - exports[`CreateSecretStorageDialog when there is an error fetching the backup version shows an error 1`] = `
", () => { const { asFragment, getByRole } = render( , ); - expect(getByRole("img")).toHaveAttribute("src", "https://logo"); + expect(getByRole("presentation")).toHaveAttribute("src", "https://logo"); expect(asFragment()).toMatchSnapshot(); }); diff --git a/test/unit-tests/components/views/elements/__snapshots__/AppTile-test.tsx.snap b/test/unit-tests/components/views/elements/__snapshots__/AppTile-test.tsx.snap index 520c0ee191..96b06a4eda 100644 --- a/test/unit-tests/components/views/elements/__snapshots__/AppTile-test.tsx.snap +++ b/test/unit-tests/components/views/elements/__snapshots__/AppTile-test.tsx.snap @@ -30,7 +30,7 @@ exports[`AppTile destroys non-persisted right panel widget on room change 1`] = />
Fourth pinned message @@ -542,8 +545,9 @@ exports[` should render a single pinned event 1`] = ` /> First pinned message diff --git a/test/unit-tests/components/views/rooms/__snapshots__/ReadReceiptGroup-test.tsx.snap b/test/unit-tests/components/views/rooms/__snapshots__/ReadReceiptGroup-test.tsx.snap index 8003299f78..b0ba944a66 100644 --- a/test/unit-tests/components/views/rooms/__snapshots__/ReadReceiptGroup-test.tsx.snap +++ b/test/unit-tests/components/views/rooms/__snapshots__/ReadReceiptGroup-test.tsx.snap @@ -3,7 +3,8 @@ exports[`ReadReceiptGroup should display a tooltip 1`] = `
Enter the URL of a custom theme you want to apply. @@ -517,7 +517,7 @@ exports[` custom theme should render the custom theme sectio
+
+