diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 75a00af7aa..9eca8c8636 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -25,14 +25,14 @@ jobs: fetch-depth: 0 # needed for docker-package to be able to calculate the version - name: Install Cosign - uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3 + uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3 if: github.event_name != 'pull_request' - name: Set up QEMU uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3 + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3 with: install: true @@ -53,7 +53,7 @@ jobs: - name: Build and load id: test-build - uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6 + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6 with: context: . load: true @@ -110,7 +110,7 @@ jobs: - name: Build and push id: build-and-push - uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6 + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6 if: github.event_name != 'pull_request' with: context: . diff --git a/.github/workflows/end-to-end-tests.yaml b/.github/workflows/end-to-end-tests.yaml index b2d49d7703..5901d7a700 100644 --- a/.github/workflows/end-to-end-tests.yaml +++ b/.github/workflows/end-to-end-tests.yaml @@ -227,7 +227,7 @@ jobs: - name: Merge into HTML Report if: inputs.skip != true - run: yarn playwright merge-reports --reporter=html,./playwright/flaky-reporter.ts,./playwright/stale-screenshot-reporter.ts ./all-blob-reports + run: yarn playwright merge-reports --reporter=html,./playwright/flaky-reporter.ts,@element-hq/element-web-playwright-common/lib/stale-screenshot-reporter.js ./all-blob-reports env: # Only pass creds to the flaky-reporter on main branch runs GITHUB_TOKEN: ${{ github.ref_name == 'develop' && secrets.ELEMENT_BOT_TOKEN || '' }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 276c53c098..a17e457252 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -104,7 +104,7 @@ jobs: - name: Skip SonarCloud in merge queue if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true' - uses: guibranco/github-status-action-v2@5f2b01ce1394109f70954ae6b69ef41cf7928e63 + uses: guibranco/github-status-action-v2@741ea90ba6c3ca76fe0d43ba11a90cda97d5e685 with: authToken: ${{ secrets.GITHUB_TOKEN }} state: success diff --git a/.github/workflows/update-topics.yaml b/.github/workflows/update-topics.yaml index 5ee9f2b608..7bf751384d 100644 --- a/.github/workflows/update-topics.yaml +++ b/.github/workflows/update-topics.yaml @@ -26,7 +26,7 @@ jobs: env: HS_URL: ${{ secrets.BETABOT_HS_URL }} LOBBY_ROOM_ID: ${{ secrets.ROOM_ID }} - PUBLIC_ROOM_ID: "!YTvKGNlinIzlkMTVRl:matrix.org" + PUBLIC_ROOM_ID: "!IemiTbwVankHTFiEoh:matrix.org" ANNOUNCEMENT_ROOM_ID: "!bijaLdadorKgNGtHdA:matrix.org" TOKEN: ${{ secrets.BETABOT_ACCESS_TOKEN }} RELEASE_STATUS: "Release status: ${{ inputs.expected_status }} expected ${{ inputs.expected_date }}" @@ -81,6 +81,11 @@ jobs: d.body = d.body.replace(regex, releaseTopic); }); } + if (data["m.topic"]) { + data["m.topic"].forEach(d => { + d.body = d.body.replace(regex, releaseTopic); + }); + } res = await fetch(apiUrl, { method: "PUT", diff --git a/CHANGELOG.md b/CHANGELOG.md index b3a18c5396..5eaefe2965 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,53 @@ +Changes in [1.11.105](https://github.com/element-hq/element-web/releases/tag/v1.11.105) (2025-07-01) +==================================================================================================== +## ✨ Features + +* New room list: add context menu to room list item ([#29952](https://github.com/element-hq/element-web/pull/29952)). Contributed by @florianduros. +* Support for custom message components via Module API ([#30074](https://github.com/element-hq/element-web/pull/30074)). Contributed by @Half-Shot. +* Prompt users to set up recovery ([#30075](https://github.com/element-hq/element-web/pull/30075)). Contributed by @uhoreg. +* Update `IconButton` colors ([#30124](https://github.com/element-hq/element-web/pull/30124)). Contributed by @florianduros. +* New room list: filter list can be collapsed ([#29992](https://github.com/element-hq/element-web/pull/29992)). Contributed by @florianduros. +* Show `EmptyRoomListView` when low priority filter matches zero rooms ([#30122](https://github.com/element-hq/element-web/pull/30122)). Contributed by @MidhunSureshR. + +## 🐛 Bug Fixes + +* Fix untranslatable string "People" in notifications beta ([#30165](https://github.com/element-hq/element-web/pull/30165)). Contributed by @t3chguy. +* Force verification even after logging in via delegate ([#30141](https://github.com/element-hq/element-web/pull/30141)). Contributed by @andybalaam. +* Hide add integrations button based on UIComponent.AddIntegrations ([#30140](https://github.com/element-hq/element-web/pull/30140)). Contributed by @t3chguy. +* Use nav for new room list and label sections ([#30134](https://github.com/element-hq/element-web/pull/30134)). Contributed by @dbkr. +* Spacestore should emit event after rebuilding home space ([#30132](https://github.com/element-hq/element-web/pull/30132)). Contributed by @MidhunSureshR. +* Handle m.room.pinned\_events being invalid ([#30129](https://github.com/element-hq/element-web/pull/30129)). Contributed by @t3chguy. + + +Changes in [1.11.104](https://github.com/element-hq/element-web/releases/tag/v1.11.104) (2025-06-17) +==================================================================================================== +## ✨ Features + +* Update the mobile\_guide page to the new design. ([#30006](https://github.com/element-hq/element-web/pull/30006)). Contributed by @pixlwave. +* Provide a devtool for manually verifying other devices ([#30094](https://github.com/element-hq/element-web/pull/30094)). Contributed by @andybalaam. +* Implement MSC4155: Invite filtering ([#29603](https://github.com/element-hq/element-web/pull/29603)). Contributed by @Half-Shot. +* Add low priority avatar decoration to room tile ([#30065](https://github.com/element-hq/element-web/pull/30065)). Contributed by @MidhunSureshR. +* Add ability to prevent window content being captured by other apps (Desktop) ([#30098](https://github.com/element-hq/element-web/pull/30098)). Contributed by @t3chguy. +* New room list: move message preview in user settings ([#30023](https://github.com/element-hq/element-web/pull/30023)). Contributed by @florianduros. +* New room list: change room options icon ([#30029](https://github.com/element-hq/element-web/pull/30029)). Contributed by @florianduros. +* RoomListStore: Sort low priority rooms to the bottom of the list ([#30070](https://github.com/element-hq/element-web/pull/30070)). Contributed by @MidhunSureshR. +* Add low priority filter pill to the room list UI ([#30060](https://github.com/element-hq/element-web/pull/30060)). Contributed by @MidhunSureshR. +* New room list: remove color gradient in space panel ([#29721](https://github.com/element-hq/element-web/pull/29721)). Contributed by @florianduros. +* /share?msg=foo endpoint using forward message dialog ([#29874](https://github.com/element-hq/element-web/pull/29874)). Contributed by @ara4n. + +## 🐛 Bug Fixes + +* Do not send empty auth when setting up cross-signing keys ([#29914](https://github.com/element-hq/element-web/pull/29914)). Contributed by @gnieto. +* Settings: flip local video feed by default ([#29501](https://github.com/element-hq/element-web/pull/29501)). Contributed by @jbtrystram. +* AccessSecretStorageDialog: various fixes ([#30093](https://github.com/element-hq/element-web/pull/30093)). Contributed by @richvdh. +* AccessSecretStorageDialog: fix inability to enter recovery key ([#30090](https://github.com/element-hq/element-web/pull/30090)). Contributed by @richvdh. +* Fix failure to upload thumbnail causing image to send as file ([#30086](https://github.com/element-hq/element-web/pull/30086)). Contributed by @t3chguy. +* Low priority menu item should be a toggle ([#30071](https://github.com/element-hq/element-web/pull/30071)). Contributed by @MidhunSureshR. +* Add sanity checks to prevent users from ignoring themselves ([#30079](https://github.com/element-hq/element-web/pull/30079)). Contributed by @MidhunSureshR. +* Fix issue with duplicate images ([#30073](https://github.com/element-hq/element-web/pull/30073)). Contributed by @fatlewis. +* Handle errors returned from Seshat ([#30083](https://github.com/element-hq/element-web/pull/30083)). Contributed by @richvdh. + + Changes in [1.11.103](https://github.com/element-hq/element-web/releases/tag/v1.11.103) (2025-06-10) ==================================================================================================== ## 🐛 Bug Fixes diff --git a/Dockerfile b/Dockerfile index ceb5fd34b9..5c122ffbd7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ -# syntax=docker.io/docker/dockerfile:1.16-labs@sha256:bb5e2b225985193779991f3256d1901a0b3e6a0b284c7bffa0972064f4a6d458 +# syntax=docker.io/docker/dockerfile:1.17-labs@sha256:9187104f31e3a002a8a6a3209ea1f937fb7486c093cbbde1e14b0fa0d7e4f1b5 # Builder -FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:f16d8e8af67bb6361231e932b8b3e7afa040cbfed181719a450b02c3821b26c1 AS builder +FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:9ba013a850f62c6c2a4768232cb971c4c386a6ec130ca241b165bb3b405c368d AS builder # Support custom branch of the js-sdk. This also helps us build images of element-web develop. ARG USE_CUSTOM_SDKS=false @@ -19,7 +19,7 @@ RUN /src/scripts/docker-package.sh RUN cp /src/config.sample.json /src/webapp/config.json # App -FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:66e34aa81c2faf290ea4e4c28a490f2b35a07478265a2d5994c8637506045eee +FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:ec6b8b1f1bfa594db13e99fc692b9fb816cb7e365927196610f622659de95913 # Need root user to install packages & manipulate the usr directory USER root diff --git a/config.sample.json b/config.sample.json index 5ddc34b3fc..3bc875cb7e 100644 --- a/config.sample.json +++ b/config.sample.json @@ -20,8 +20,7 @@ "https://scalar.vector.im/_matrix/integrations/v1", "https://scalar.vector.im/api", "https://scalar-staging.vector.im/_matrix/integrations/v1", - "https://scalar-staging.vector.im/api", - "https://scalar-staging.riot.im/scalar/api" + "https://scalar-staging.vector.im/api" ], "default_widget_container_height": 280, "default_country_code": "GB", diff --git a/developer_guide.md b/developer_guide.md index fa4bb9a239..2185a43a63 100644 --- a/developer_guide.md +++ b/developer_guide.md @@ -109,7 +109,7 @@ yarn test ### End-to-End tests -See [matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/#end-to-end-tests) for how to run the end-to-end tests. +See [`docs/playwright.md`](./docs/playwright.md) for how to run the end-to-end tests. ## General github guidelines diff --git a/docs/config.md b/docs/config.md index f1ced14fd2..2bc36e206f 100644 --- a/docs/config.md +++ b/docs/config.md @@ -130,32 +130,37 @@ complete re-branding/private labeling, a more personalised experience can be ach 6. `mobile_builds`: Optional. Like `desktop_builds`, except for the mobile apps. Also described in more detail down below. 7. `mobile_guide_toast`: When `true` (default), users accessing the Element Web instance from a mobile device will be prompted to download the app instead. -8. `update_base_url`: For the desktop app only, the URL where to acquire update packages. If specified, must be a path to a directory +8. `mobile_guide_app_variant`: Optional. The mobile app that the user is prompted to download from the `/mobile_guide` page. When omitted + the mobile guide will be configured for the new Element X apps. Allowed values are as follows: + 1. `element`: Element X Android/iOS. + 2. `element-classic`: Element Classic Android/iOS. + 3. `element-pro`: Element Pro Android/iOS. +9. `update_base_url`: For the desktop app only, the URL where to acquire update packages. If specified, must be a path to a directory containing `macos` and `win32` directories, with the update packages within. Defaults to `https://packages.element.io/desktop/update/` in production. -9. `map_style_url`: Map tile server style URL for location sharing. e.g. `https://api.maptiler.com/maps/streets/style.json?key=YOUR_KEY_GOES_HERE` - This setting is ignored if your homeserver provides `/.well-known/matrix/client` in its well-known location, and the JSON file - at that location has a key `m.tile_server` (or the unstable version `org.matrix.msc3488.tile_server`). In this case, the - configuration found in the well-known location is used instead. -10. `welcome_user_id`: **DEPRECATED** An optional user ID to start a DM with after creating an account. Defaults to nothing (no DM created). -11. `custom_translations_url`: An optional URL to allow overriding of translatable strings. The JSON file must be in a format of +10. `map_style_url`: Map tile server style URL for location sharing. e.g. `https://api.maptiler.com/maps/streets/style.json?key=YOUR_KEY_GOES_HERE` + This setting is ignored if your homeserver provides `/.well-known/matrix/client` in its well-known location, and the JSON file + at that location has a key `m.tile_server` (or the unstable version `org.matrix.msc3488.tile_server`). In this case, the + configuration found in the well-known location is used instead. +11. `welcome_user_id`: **DEPRECATED** An optional user ID to start a DM with after creating an account. Defaults to nothing (no DM created). +12. `custom_translations_url`: An optional URL to allow overriding of translatable strings. The JSON file must be in a format of `{"affected|translation|key": {"languageCode": "new string"}}`. See https://github.com/matrix-org/matrix-react-sdk/pull/7886 for details. -12. `branding`: Options for configuring various assets used within the app. Described in more detail down below. -13. `embedded_pages`: Further optional URLs for various assets used within the app. Described in more detail down below. -14. `disable_3pid_login`: When `false` (default), **enables** the options to log in with email address or phone number. Set to +13. `branding`: Options for configuring various assets used within the app. Described in more detail down below. +14. `embedded_pages`: Further optional URLs for various assets used within the app. Described in more detail down below. +15. `disable_3pid_login`: When `false` (default), **enables** the options to log in with email address or phone number. Set to `true` to hide these options. -15. `disable_login_language_selector`: When `false` (default), **enables** the language selector on the login pages. Set to `true` +16. `disable_login_language_selector`: When `false` (default), **enables** the language selector on the login pages. Set to `true` to hide this dropdown. -16. `disable_guests`: When `false` (default), **enable** guest-related functionality (peeking/previewing rooms, etc) for unregistered +17. `disable_guests`: When `false` (default), **enable** guest-related functionality (peeking/previewing rooms, etc) for unregistered users. Set to `true` to disable this functionality. -17. `user_notice`: Optional notice to show to the user, e.g. for sunsetting a deployment and pushing users to move in their own time. +18. `user_notice`: Optional notice to show to the user, e.g. for sunsetting a deployment and pushing users to move in their own time. Takes a configuration object as below: 1. `title`: Required. Title to show at the top of the notice. 2. `description`: Required. The description to use for the notice. 3. `show_once`: Optional. If true then the notice will only be shown once per device. -18. `help_url`: The URL to point users to for help with the app, defaults to `https://element.io/help`. -19. `help_encryption_url`: The URL to point users to for help with encryption, defaults to `https://element.io/help#encryption`. -20. `force_verification`: If true, users must verify new logins (eg. with another device / their recovery key) +19. `help_url`: The URL to point users to for help with the app, defaults to `https://element.io/help`. +20. `help_encryption_url`: The URL to point users to for help with encryption, defaults to `https://element.io/help#encryption`. +21. `force_verification`: If true, users must verify new logins (eg. with another device / their recovery key) ### `desktop_builds` and `mobile_builds` @@ -445,8 +450,7 @@ If you would like to use Scalar, the integration manager maintained by Element, "https://scalar.vector.im/_matrix/integrations/v1", "https://scalar.vector.im/api", "https://scalar-staging.vector.im/_matrix/integrations/v1", - "https://scalar-staging.vector.im/api", - "https://scalar-staging.riot.im/scalar/api" + "https://scalar-staging.vector.im/api" ] } ``` diff --git a/docs/kubernetes.md b/docs/kubernetes.md index cae8526e9c..23c3fde611 100644 --- a/docs/kubernetes.md +++ b/docs/kubernetes.md @@ -55,8 +55,7 @@ Then you can deploy it to your cluster with something like `kubectl apply -f my- "https://scalar.vector.im/_matrix/integrations/v1", "https://scalar.vector.im/api", "https://scalar-staging.vector.im/_matrix/integrations/v1", - "https://scalar-staging.vector.im/api", - "https://scalar-staging.riot.im/scalar/api" + "https://scalar-staging.vector.im/api" ], "bug_report_endpoint_url": "https://element.io/bugreports/submit", "defaultCountryCode": "GB", diff --git a/element.io/app/config.json b/element.io/app/config.json index 771df35091..4324ffc7fb 100644 --- a/element.io/app/config.json +++ b/element.io/app/config.json @@ -15,8 +15,7 @@ "https://scalar.vector.im/_matrix/integrations/v1", "https://scalar.vector.im/api", "https://scalar-staging.vector.im/_matrix/integrations/v1", - "https://scalar-staging.vector.im/api", - "https://scalar-staging.riot.im/scalar/api" + "https://scalar-staging.vector.im/api" ], "bug_report_endpoint_url": "https://element.io/bugreports/submit", "uisi_autorageshake_app": "element-auto-uisi", diff --git a/element.io/develop/config.json b/element.io/develop/config.json index aaee51afd0..ce4a8a0407 100644 --- a/element.io/develop/config.json +++ b/element.io/develop/config.json @@ -15,8 +15,7 @@ "https://scalar.vector.im/_matrix/integrations/v1", "https://scalar.vector.im/api", "https://scalar-staging.vector.im/_matrix/integrations/v1", - "https://scalar-staging.vector.im/api", - "https://scalar-staging.riot.im/scalar/api" + "https://scalar-staging.vector.im/api" ], "bug_report_endpoint_url": "https://element.io/bugreports/submit", "uisi_autorageshake_app": "element-auto-uisi", diff --git a/package.json b/package.json index d673a211d4..ff3338dd39 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "element-web", - "version": "1.11.103", + "version": "1.11.105", "description": "Element: the future of secure communication", "author": "New Vector Ltd.", "repository": { @@ -69,19 +69,19 @@ }, "resolutions": { "**/pretty-format/react-is": "19.1.0", - "@playwright/test": "1.52.0", - "@types/react": "19.1.6", + "@playwright/test": "1.53.1", + "@types/react": "19.1.8", "@types/react-dom": "19.1.6", - "oidc-client-ts": "3.2.1", + "oidc-client-ts": "3.3.0", "jwt-decode": "4.0.0", - "caniuse-lite": "1.0.30001721", + "caniuse-lite": "1.0.30001724", "testcontainers": "^11.0.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0", "wrap-ansi": "npm:wrap-ansi@^7.0.0" }, "dependencies": { "@babel/runtime": "^7.12.5", - "@element-hq/element-web-module-api": "1.0.0", + "@element-hq/element-web-module-api": "1.3.0", "@fontsource/inconsolata": "^5", "@fontsource/inter": "^5", "@formatjs/intl-segmenter": "^11.5.7", @@ -93,8 +93,8 @@ "@types/png-chunks-extract": "^1.0.2", "@types/react-virtualized": "^9.21.30", "@vector-im/compound-design-tokens": "^4.0.4", - "@vector-im/compound-web": "^8.1.0", - "@vector-im/matrix-wysiwyg": "2.38.3", + "@vector-im/compound-web": "^8.1.2", + "@vector-im/matrix-wysiwyg": "2.38.4", "@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4", "@zxcvbn-ts/language-en": "^3.0.2", @@ -138,7 +138,7 @@ "opus-recorder": "^8.0.3", "pako": "^2.0.3", "png-chunks-extract": "^1.0.0", - "posthog-js": "1.249.4", + "posthog-js": "1.255.1", "qrcode": "1.5.4", "re-resizable": "6.11.2", "react": "^19.0.0", @@ -181,7 +181,7 @@ "@babel/runtime": "^7.12.5", "@casualbot/jest-sonar-reporter": "2.2.7", "@element-hq/element-call-embedded": "0.12.2", - "@element-hq/element-web-playwright-common": "^1.1.5", + "@element-hq/element-web-playwright-common": "^1.4.2", "@peculiar/webcrypto": "^1.4.3", "@playwright/test": "^1.50.1", "@principalstudio/html-webpack-inject-preload": "^1.2.7", @@ -212,7 +212,7 @@ "@types/node-fetch": "^2.6.2", "@types/pako": "^2.0.0", "@types/qrcode": "^1.3.5", - "@types/react": "19.1.6", + "@types/react": "19.1.8", "@types/react-beautiful-dnd": "^13.0.0", "@types/react-dom": "19.1.6", "@types/react-transition-group": "^4.4.0", @@ -252,7 +252,6 @@ "fetch-mock": "9.11.0", "fetch-mock-jest": "^1.5.1", "file-loader": "^6.0.0", - "glob": "^11.0.0", "html-webpack-plugin": "^5.5.3", "husky": "^9.0.0", "jest": "^29.6.2", diff --git a/patches/@types+react+19.1.4.patch b/patches/@types+react+19.1.4.patch deleted file mode 100644 index ceba85b000..0000000000 --- a/patches/@types+react+19.1.4.patch +++ /dev/null @@ -1,31 +0,0 @@ -diff --git a/node_modules/@types/react/index.d.ts b/node_modules/@types/react/index.d.ts -index d3318dc..c2b2c77 100644 ---- a/node_modules/@types/react/index.d.ts -+++ b/node_modules/@types/react/index.d.ts -@@ -134,7 +134,7 @@ declare namespace React { - props: P, - ) => ReactNode | Promise) - // constructor signature must match React.Component -- | (new(props: P) => Component); -+ | (new(props: P, context?: any) => Component); - - /** - * Created by {@link createRef}, or {@link useRef} when passed `null`. -@@ -945,7 +945,7 @@ declare namespace React { - context: unknown; - - // Keep in sync with constructor signature of JSXElementConstructor and ComponentClass. -- constructor(props: P); -+ constructor(props: P, context?: unknown); - - // We MUST keep setState() as a unified signature because it allows proper checking of the method return type. - // See: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment-351013257 -@@ -1117,7 +1117,7 @@ declare namespace React { - */ - interface ComponentClass

extends StaticLifecycle { - // constructor signature must match React.Component -- new(props: P): Component; -+ new(props: P, context?: any): Component; - /** - * Ignored by React. - * @deprecated Only kept in types for backwards compatibility. Will be removed in a future major release. diff --git a/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts b/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts index 26d27cc01c..0e92e734c5 100644 --- a/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts +++ b/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts @@ -229,13 +229,13 @@ test.describe("Room list filters and sort", () => { // only one room should be visible await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible(); await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible(); - expect(await roomList.locator("role=gridcell").count()).toBe(4); + await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(4); await expect(primaryFilters).toMatchScreenshot("unread-primary-filters.png"); await primaryFilters.getByRole("option", { name: "People" }).click(); await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible(); await expect(roomList.getByRole("gridcell", { name: "invited room" })).toBeVisible(); - expect(await roomList.locator("role=gridcell").count()).toBe(2); + await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(2); await primaryFilters.getByRole("option", { name: "Rooms" }).click(); await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible(); @@ -243,21 +243,21 @@ test.describe("Room list filters and sort", () => { await expect(roomList.getByRole("gridcell", { name: "empty room" })).toBeVisible(); await expect(roomList.getByRole("gridcell", { name: "room with mention" })).toBeVisible(); await expect(roomList.getByRole("gridcell", { name: "Low prio room" })).toBeVisible(); - expect(await roomList.locator("role=gridcell").count()).toBe(5); + await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(5); await getFilterExpandButton(page).click(); await primaryFilters.getByRole("option", { name: "Favourite" }).click(); await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible(); - expect(await roomList.locator("role=gridcell").count()).toBe(1); + await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(1); await primaryFilters.getByRole("option", { name: "Mentions" }).click(); await expect(roomList.getByRole("gridcell", { name: "room with mention" })).toBeVisible(); - expect(await roomList.locator("role=gridcell").count()).toBe(1); + await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(1); await primaryFilters.getByRole("option", { name: "Invites" }).click(); await expect(roomList.getByRole("gridcell", { name: "invited room" })).toBeVisible(); - expect(await roomList.locator("role=gridcell").count()).toBe(1); + await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(1); await getFilterCollapseButton(page).click(); await expect(primaryFilters.locator("role=option").first()).toHaveText("Invites"); diff --git a/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts b/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts index 0567b8a162..1f965ffb8f 100644 --- a/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts +++ b/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts @@ -60,6 +60,12 @@ test.describe("Room list", () => { await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible(); }); + test("should open the context menu", { tag: "@screenshot" }, async ({ page, app, user }) => { + const roomListView = getRoomList(page); + await roomListView.getByRole("gridcell", { name: "Open room room29" }).click({ button: "right" }); + await expect(page.getByRole("menu", { name: "More Options" })).toBeVisible(); + }); + test("should open the more options menu", { tag: "@screenshot" }, async ({ page, app, user }) => { const roomListView = getRoomList(page); const roomItem = roomListView.getByRole("gridcell", { name: "Open room room29" }); @@ -109,10 +115,15 @@ test.describe("Room list", () => { // It should make the room muted await page.getByRole("menuitem", { name: "Mute room" }).click(); + await expect(roomItem.getByTestId("notification-decoration")).not.toBeVisible(); + // Put focus on the room list await roomListView.getByRole("gridcell", { name: "Open room room28" }).click(); - // Scroll to the end of the room list - await page.mouse.wheel(0, 1000); + + while (!(await roomItem.isVisible())) { + // Scroll to the end of the room list + await page.mouse.wheel(0, 1000); + } // The room decoration should have the muted icon await expect(roomItem.getByTestId("notification-decoration")).toBeVisible(); @@ -135,6 +146,7 @@ test.describe("Room list", () => { // Scroll to the end of the room list await page.mouse.wheel(0, 1000); + await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible(); await roomListView.getByRole("gridcell", { name: "Open room room0" }).click(); const filters = page.getByRole("listbox", { name: "Room list filters" }); @@ -223,17 +235,17 @@ test.describe("Room list", () => { await expect(notificationButton).toBeFocused(); // Open the menu - await notificationButton.click(); + await page.keyboard.press("Enter"); // Wait for the menu to be open await expect(page.getByRole("menuitem", { name: "Match default settings" })).toHaveAttribute( "aria-selected", "true", ); - // Close the menu + await page.keyboard.press("ArrowDown"); await page.keyboard.press("Escape"); - // Focus should be back on the room list item - await expect(room29).toBeFocused(); + // Focus should be back on the notification button + await expect(notificationButton).toBeFocused(); }); }); }); diff --git a/playwright/e2e/mobile-guide/mobile-guide.spec.ts b/playwright/e2e/mobile-guide/mobile-guide.spec.ts new file mode 100644 index 0000000000..ff2ad69bf8 --- /dev/null +++ b/playwright/e2e/mobile-guide/mobile-guide.spec.ts @@ -0,0 +1,36 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { test, expect } from "../../element-web-test"; +import { MobileAppVariant } from "../../../src/vector/mobile_guide/mobile-apps"; + +const variants = [MobileAppVariant.Classic, MobileAppVariant.X, MobileAppVariant.Pro]; + +test.describe("Mobile Guide Screenshots", { tag: "@screenshot" }, () => { + for (const variant of variants) { + test.describe(`for variant ${variant}`, () => { + test.use({ + config: { + default_server_config: { + "m.homeserver": { + base_url: "https://matrix.server.invalid", + server_name: "server.invalid", + }, + }, + mobile_guide_app_variant: variant, + }, + viewport: { width: 390, height: 844 }, // iPhone 16e + }); + + test("should match the mobile_guide screenshot", async ({ page, axe }) => { + await page.goto("/mobile_guide/"); + await expect(page).toMatchScreenshot(`mobile-guide-${variant}.png`); + await expect(axe).toHaveNoViolations(); + }); + }); + } +}); diff --git a/playwright/e2e/modules/custom-component.spec.ts b/playwright/e2e/modules/custom-component.spec.ts new file mode 100644 index 0000000000..f263ac8b9c --- /dev/null +++ b/playwright/e2e/modules/custom-component.spec.ts @@ -0,0 +1,160 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { type Page } from "@playwright/test"; +import fs from "node:fs"; + +import { test, expect } from "../../element-web-test"; + +const screenshotOptions = (page: Page) => ({ + mask: [page.locator(".mx_MessageTimestamp")], + // Hide the jump to bottom button in the timeline to avoid flakiness + // Exclude timestamp and read marker from snapshot + css: ` + .mx_JumpToBottomButton { + display: none !important; + } + .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker { + display: none !important; + } + `, +}); + +const IMAGE_FILE = fs.readFileSync("playwright/sample-files/element.png"); + +test.describe("Custom Component API", () => { + test.use({ + displayName: "Manny", + config: { + modules: ["/modules/custom-component-module.js"], + }, + page: async ({ page }, use) => { + await page.route("/modules/custom-component-module.js", async (route) => { + await route.fulfill({ path: "playwright/sample-files/custom-component-module.js" }); + }); + await use(page); + }, + room: async ({ page, app, user, bot }, use) => { + const roomId = await app.client.createRoom({ name: "TestRoom" }); + await use({ roomId }); + }, + }); + test.describe("basic functionality", () => { + test( + "should replace the render method of a textual event", + { tag: "@screenshot" }, + async ({ page, room, app }) => { + await app.viewRoomById(room.roomId); + await app.client.sendMessage(room.roomId, "Simple message"); + await expect(await page.locator(".mx_EventTile_last")).toMatchScreenshot( + "custom-component-tile.png", + screenshotOptions(page), + ); + }, + ); + test( + "should fall through if one module does not render a component", + { tag: "@screenshot" }, + async ({ page, room, app }) => { + await app.viewRoomById(room.roomId); + await app.client.sendMessage(room.roomId, "Fall through here"); + await expect(await page.locator(".mx_EventTile_last")).toMatchScreenshot( + "custom-component-tile-fall-through.png", + screenshotOptions(page), + ); + }, + ); + test( + "should render the original content of a textual event conditionally", + { tag: "@screenshot" }, + async ({ page, room, app }) => { + await app.viewRoomById(room.roomId); + await app.client.sendMessage(room.roomId, "Do not replace me"); + await expect(await page.locator(".mx_EventTile_last")).toMatchScreenshot( + "custom-component-tile-original.png", + screenshotOptions(page), + ); + }, + ); + test("should disallow editing when the allowEditingEvent hint is set to false", async ({ page, room, app }) => { + await app.viewRoomById(room.roomId); + await app.client.sendMessage(room.roomId, "Do not show edits"); + await page.getByText("Do not show edits").hover(); + await expect( + await page.getByRole("toolbar", { name: "Message Actions" }).getByRole("button", { name: "Edit" }), + ).not.toBeVisible(); + }); + test("should disallow downloading media when the allowDownloading hint is set to false", async ({ + page, + room, + app, + }) => { + await app.viewRoomById(room.roomId); + await app.viewRoomById(room.roomId); + const upload = await app.client.uploadContent(IMAGE_FILE, { name: "bad.png", type: "image/png" }); + await app.client.sendEvent(room.roomId, null, "m.room.message", { + msgtype: "m.image", + body: "bad.png", + url: upload.content_uri, + }); + + await app.timeline.scrollToBottom(); + const imgTile = page.locator(".mx_MImageBody").first(); + await expect(imgTile).toBeVisible(); + await imgTile.hover(); + await expect(page.getByRole("button", { name: "Download" })).not.toBeVisible(); + await imgTile.click(); + await expect(page.getByLabel("Image view").getByLabel("Download")).not.toBeVisible(); + }); + test("should allow downloading media when the allowDownloading hint is set to true", async ({ + page, + room, + app, + }) => { + await app.viewRoomById(room.roomId); + await app.viewRoomById(room.roomId); + const upload = await app.client.uploadContent(IMAGE_FILE, { name: "good.png", type: "image/png" }); + await app.client.sendEvent(room.roomId, null, "m.room.message", { + msgtype: "m.image", + body: "good.png", + url: upload.content_uri, + }); + + await app.timeline.scrollToBottom(); + const imgTile = page.locator(".mx_MImageBody").first(); + await expect(imgTile).toBeVisible(); + await imgTile.hover(); + await expect(page.getByRole("button", { name: "Download" })).toBeVisible(); + await imgTile.click(); + await expect(page.getByLabel("Image view").getByLabel("Download")).toBeVisible(); + }); + test( + "should render the next registered component if the filter function throws", + { tag: "@screenshot" }, + async ({ page, room, app }) => { + await app.viewRoomById(room.roomId); + await app.client.sendMessage(room.roomId, "Crash the filter!"); + await expect(await page.locator(".mx_EventTile_last")).toMatchScreenshot( + "custom-component-crash-handle-filter.png", + screenshotOptions(page), + ); + }, + ); + test( + "should render original component if the render function throws", + { tag: "@screenshot" }, + async ({ page, room, app }) => { + await app.viewRoomById(room.roomId); + await app.client.sendMessage(room.roomId, "Crash the renderer!"); + await expect(await page.locator(".mx_EventTile_last")).toMatchScreenshot( + "custom-component-crash-handle-renderer.png", + screenshotOptions(page), + ); + }, + ); + }); +}); diff --git a/playwright/e2e/oidc/oidc-native.spec.ts b/playwright/e2e/oidc/oidc-native.spec.ts index 3268c2b65a..8b49942dd3 100644 --- a/playwright/e2e/oidc/oidc-native.spec.ts +++ b/playwright/e2e/oidc/oidc-native.spec.ts @@ -81,7 +81,7 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => { test( "it should log out the user & wipe data when logging out via MAS", { tag: "@screenshot" }, - async ({ mas, page, mailpitClient }, testInfo) => { + async ({ mas, page, mailpitClient, homeserver }, testInfo) => { // We use this over the `user` fixture to ensure we get an OIDC session rather than a compatibility one await page.goto("/#/login"); await page.getByRole("button", { name: "Continue" }).click(); @@ -95,11 +95,15 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => { const result = await mas.manage("kill-sessions", userId); expect(result.output).toContain("Ended 1 active OAuth 2.0 session"); + // Workaround for Synapse's 2 minute cache on MAS token validity + // (https://github.com/element-hq/synapse/pull/18231) + await homeserver.restart(); + await page.goto("http://localhost:8080"); await expect( page.getByText("For security, this session has been signed out. Please sign in again."), ).toBeVisible(); - await expect(page).toMatchScreenshot("token-expired.png", { includeDialogBackground: true }); + //await expect(page).toMatchScreenshot("token-expired.png", { includeDialogBackground: true }); const localStorageKeys = await page.evaluate(() => Object.keys(localStorage)); expect(localStorageKeys).toHaveLength(0); diff --git a/playwright/e2e/release-announcement/index.ts b/playwright/e2e/release-announcement/index.ts index 3b6c2dd38a..2a2e704d74 100644 --- a/playwright/e2e/release-announcement/index.ts +++ b/playwright/e2e/release-announcement/index.ts @@ -42,7 +42,10 @@ export class Helpers { */ async assertReleaseAnnouncementIsVisible(name: string) { await expect(this.getReleaseAnnouncement(name)).toBeVisible(); - await expect(this.page).toMatchScreenshot(`release-announcement-${name}.png`, { showTooltips: true }); + await expect(this.page).toMatchScreenshot(`release-announcement-${name}.png`, { + showTooltips: true, + hideJumpToBottomButton: true, + }); } /** diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts index 8520533461..fee9f070d8 100644 --- a/playwright/element-web-test.ts +++ b/playwright/element-web-test.ts @@ -107,6 +107,7 @@ interface ExtendedToMatchScreenshotOptions extends ToMatchScreenshotOptions { includeDialogBackground?: boolean; showTooltips?: boolean; timeout?: number; + hideJumpToBottomButton?: boolean; } type Expectations = { @@ -165,6 +166,14 @@ export const expect = baseExpect.extend({ `; } + if (options?.hideJumpToBottomButton) { + css += ` + .mx_JumpToBottomButton { + display: none !important; + } + `; + } + if (options?.css) { css += options.css; } diff --git a/playwright/sample-files/custom-component-module.js b/playwright/sample-files/custom-component-module.js new file mode 100644 index 0000000000..be2ab5928d --- /dev/null +++ b/playwright/sample-files/custom-component-module.js @@ -0,0 +1,74 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +// Note: eslint-plugin-jsdoc doesn't like import types as parameters, so we +// get around it with @typedef +/** + * @typedef {import("@element-hq/element-web-module-api").Api} Api + */ + +export default class CustomComponentModule { + static moduleApiVersion = "^1.2.0"; + /** + * Basic module for testing. + * @param {Api} api API object + */ + constructor(api) { + this.api = api; + this.api.customComponents.registerMessageRenderer( + (evt) => evt.content.body === "Do not show edits", + (_props, originalComponent) => { + return originalComponent(); + }, + { allowEditingEvent: false }, + ); + this.api.customComponents.registerMessageRenderer( + (evt) => evt.content.body === "Fall through here", + (props) => { + const body = props.mxEvent.content.body; + return `Fallthrough text for ${body}`; + }, + ); + this.api.customComponents.registerMessageRenderer( + (evt) => { + if (evt.content.body === "Crash the filter!") { + throw new Error("Fail test!"); + } + return false; + }, + () => { + return `Should not render!`; + }, + ); + this.api.customComponents.registerMessageRenderer( + (evt) => evt.content.body === "Crash the renderer!", + () => { + throw new Error("Fail test!"); + }, + ); + + this.api.customComponents.registerMessageRenderer( + (mxEvent) => mxEvent.type === "m.room.message" && mxEvent.content.msgtype === "m.image", + (_props, originalComponent) => { + return originalComponent(); + }, + { allowDownloadingMedia: async (mxEvent) => mxEvent.content.body !== "bad.png" }, + ); + + // Order is specific here to avoid this overriding the other renderers + this.api.customComponents.registerMessageRenderer("m.room.message", (props, originalComponent) => { + const body = props.mxEvent.content.body; + if (body === "Do not replace me") { + return originalComponent(); + } else if (body === "Fall through here") { + return null; + } + return `Custom text for ${body}`; + }); + } + async load() {} +} diff --git a/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-rtldisplayname-linux.png index 35523b7db8..c1f4477d66 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-ltrdisplayname-linux.png index 9f31f518fc..59b9fc41e8 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-ltrdisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-ltrdisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-rtldisplayname-linux.png index 40a096409e..07f007545c 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-rtldisplayname-linux.png index 557613e7e6..e694dde7c6 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-ltrdisplayname-linux.png index 85a3c69c0e..8c2e2d153b 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-ltrdisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-ltrdisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-rtldisplayname-linux.png index bd26e84628..4bb04ba279 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-ltr-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-ltr-rtldisplayname-linux.png index 055ad23a81..c711b21a58 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/emote-ltr-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/emote-ltr-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-rtldisplayname-linux.png index cfb905b689..b92c4691d3 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-ltrdisplayname-linux.png index 537f1dd2c4..c727b98745 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-ltrdisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-ltrdisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-rtldisplayname-linux.png index ea29e98a75..bd1baed9aa 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-rtl-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-rtl-ltrdisplayname-linux.png index 27357dc503..b55925218d 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/emote-rtl-ltrdisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/emote-rtl-ltrdisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-rtl-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-rtl-rtldisplayname-linux.png index d84780f530..cb18ce68c7 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/emote-rtl-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/emote-rtl-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/reply-message-ltr-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/reply-message-ltr-rtldisplayname-linux.png index b59d960f4f..e3d3e68c64 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/reply-message-ltr-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/reply-message-ltr-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-ltrdisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-ltrdisplayname-linux.png index a3acc741f3..d6964445b1 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-ltrdisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-ltrdisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-rtldisplayname-linux.png index f6eaea241a..8830c22d92 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/mobile-guide/mobile-guide.spec.ts/mobile-guide-element-classic-linux.png b/playwright/snapshots/mobile-guide/mobile-guide.spec.ts/mobile-guide-element-classic-linux.png new file mode 100644 index 0000000000..f091eeed74 Binary files /dev/null and b/playwright/snapshots/mobile-guide/mobile-guide.spec.ts/mobile-guide-element-classic-linux.png differ diff --git a/playwright/snapshots/mobile-guide/mobile-guide.spec.ts/mobile-guide-element-linux.png b/playwright/snapshots/mobile-guide/mobile-guide.spec.ts/mobile-guide-element-linux.png new file mode 100644 index 0000000000..9c32731ab7 Binary files /dev/null and b/playwright/snapshots/mobile-guide/mobile-guide.spec.ts/mobile-guide-element-linux.png differ diff --git a/playwright/snapshots/mobile-guide/mobile-guide.spec.ts/mobile-guide-element-pro-linux.png b/playwright/snapshots/mobile-guide/mobile-guide.spec.ts/mobile-guide-element-pro-linux.png new file mode 100644 index 0000000000..ff1bb69a4f Binary files /dev/null and b/playwright/snapshots/mobile-guide/mobile-guide.spec.ts/mobile-guide-element-pro-linux.png differ diff --git a/playwright/snapshots/modules/custom-component.spec.ts/custom-component-crash-handle-filter-linux.png b/playwright/snapshots/modules/custom-component.spec.ts/custom-component-crash-handle-filter-linux.png new file mode 100644 index 0000000000..b144ca6a5e Binary files /dev/null and b/playwright/snapshots/modules/custom-component.spec.ts/custom-component-crash-handle-filter-linux.png differ diff --git a/playwright/snapshots/modules/custom-component.spec.ts/custom-component-crash-handle-renderer-linux.png b/playwright/snapshots/modules/custom-component.spec.ts/custom-component-crash-handle-renderer-linux.png new file mode 100644 index 0000000000..b3fa5e0f57 Binary files /dev/null and b/playwright/snapshots/modules/custom-component.spec.ts/custom-component-crash-handle-renderer-linux.png differ diff --git a/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-fall-through-linux.png b/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-fall-through-linux.png new file mode 100644 index 0000000000..0fe98072a0 Binary files /dev/null and b/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-fall-through-linux.png differ diff --git a/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-linux.png b/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-linux.png new file mode 100644 index 0000000000..7c5d6b66e6 Binary files /dev/null and b/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-linux.png differ diff --git a/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-original-linux.png b/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-original-linux.png new file mode 100644 index 0000000000..9a00a3b04b Binary files /dev/null and b/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-original-linux.png differ diff --git a/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-linux.png b/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-linux.png index 3b4031063c..677473dded 100644 Binary files a/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-linux.png and b/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-linux.png differ diff --git a/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-smallscreen-linux.png b/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-smallscreen-linux.png index e0e46682a3..da4d594e23 100644 Binary files a/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-smallscreen-linux.png and b/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-smallscreen-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png index 9ce20649df..b7f7c75c5b 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png index a847075a4d..65258303c9 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png differ diff --git a/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-recovery-linux.png b/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-recovery-linux.png index e26d001a90..39e74833b6 100644 Binary files a/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-recovery-linux.png and b/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-recovery-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 9e6f0f75d1..2bf50796ae 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/stale-screenshot-reporter.ts b/playwright/stale-screenshot-reporter.ts deleted file mode 100644 index 36aba56a07..0000000000 --- a/playwright/stale-screenshot-reporter.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2024 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -/** - * Test reporter which compares the reported screenshots vs those on disk to find stale screenshots - * Only intended to run from within GitHub Actions - */ - -import path from "node:path"; -import { glob } from "glob"; - -import type { Reporter, TestCase } from "@playwright/test/reporter"; - -const snapshotRoot = path.join(__dirname, "snapshots"); - -class StaleScreenshotReporter implements Reporter { - private screenshots = new Set(); - private failing = false; - private success = true; - - public onTestEnd(test: TestCase): void { - if (!test.ok()) { - this.failing = true; - } - for (const annotation of test.annotations) { - if (annotation.type === "_screenshot") { - this.screenshots.add(annotation.description); - } - } - } - - private error(msg: string, file: string) { - if (process.env.GITHUB_ACTIONS) { - console.log(`::error file=${file}::${msg}`); - } - console.error(msg, file); - this.success = false; - } - - public async onExit(): Promise { - if (this.failing) return; - const screenshotFiles = new Set(await glob(`**/*.png`, { cwd: snapshotRoot })); - for (const screenshot of screenshotFiles) { - if (screenshot.split("-").at(-1) !== "linux.png") { - this.error( - "Found screenshot belonging to different platform, this should not be checked in", - screenshot, - ); - } - } - for (const screenshot of this.screenshots) { - screenshotFiles.delete(screenshot); - } - if (screenshotFiles.size > 0) { - for (const screenshot of screenshotFiles) { - this.error("Stale screenshot file", screenshot); - } - } - - if (!this.success) { - process.exit(1); - } - } -} - -export default StaleScreenshotReporter; diff --git a/playwright/testcontainers/synapse.ts b/playwright/testcontainers/synapse.ts index 19d1544196..8ea64d2a9b 100644 --- a/playwright/testcontainers/synapse.ts +++ b/playwright/testcontainers/synapse.ts @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. import { SynapseContainer as BaseSynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers"; -const TAG = "develop@sha256:66955f34a593cfc3b6e77b8d5510c60c6094f5bade8a17d2feaefbb8662ccf09"; +const TAG = "develop@sha256:7eeeb41a161411aab63acc2901e9dfa030dd4a300c00f18a5a23c26968d59773"; /** * SynapseContainer which freezes the docker digest to stabilise tests, diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 247dd7edab..b68da87167 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -177,7 +177,6 @@ @import "./views/dialogs/_WidgetCapabilitiesPromptDialog.pcss"; @import "./views/dialogs/security/_AccessSecretStorageDialog.pcss"; @import "./views/dialogs/security/_CreateCrossSigningDialog.pcss"; -@import "./views/dialogs/security/_CreateKeyBackupDialog.pcss"; @import "./views/dialogs/security/_CreateSecretStorageDialog.pcss"; @import "./views/dialogs/security/_KeyBackupFailedDialog.pcss"; @import "./views/dialogs/security/_RestoreKeyBackupDialog.pcss"; diff --git a/res/css/views/dialogs/_SettingsDialog.pcss b/res/css/views/dialogs/_SettingsDialog.pcss index 186a82c0f5..2b65bff63b 100644 --- a/res/css/views/dialogs/_SettingsDialog.pcss +++ b/res/css/views/dialogs/_SettingsDialog.pcss @@ -30,4 +30,28 @@ Please see LICENSE files in the repository root for full details. /* colliding harshly with the dialog when scrolled down. */ padding-bottom: 100px; } + + .mx_SettingsDialog_tabLabelsAlert::after { + display: inline-block; + content: ""; + width: 8px; + height: 8px; + background-color: var(--cpd-color-icon-critical-primary); + clip-path: circle(4px); + position: absolute; + right: var(--cpd-space-4x); + } +} + +/* On narrow viewports, the tab labels are hidden, so we need to shift the indicator so it isn't over the tab icon. */ +@media (max-width: 1024px) { + .mx_UserSettingsDialog, + .mx_RoomSettingsDialog, + .mx_SpaceSettingsDialog, + .mx_SpacePreferencesDialog { + .mx_SettingsDialog_tabLabelsAlert::after { + right: var(--cpd-space-1x); + top: var(--cpd-space-1x); + } + } } diff --git a/res/css/views/dialogs/security/_CreateKeyBackupDialog.pcss b/res/css/views/dialogs/security/_CreateKeyBackupDialog.pcss deleted file mode 100644 index 9bd8539881..0000000000 --- a/res/css/views/dialogs/security/_CreateKeyBackupDialog.pcss +++ /dev/null @@ -1,73 +0,0 @@ -/* -Copyright 2018-2024 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -.mx_CreateKeyBackupDialog .mx_Dialog_title { - /* TODO: Consider setting this for all dialog titles. */ - margin-bottom: 1em; -} - -.mx_CreateKeyBackupDialog_primaryContainer { - /* FIXME: plinth colour in new theme(s). background-color: $accent; */ - padding: 20px; -} - -.mx_CreateKeyBackupDialog_primaryContainer::after { - content: ""; - clear: both; - display: block; -} - -.mx_CreateKeyBackupDialog_passPhraseContainer { - display: flex; - align-items: flex-start; -} - -.mx_CreateKeyBackupDialog_passPhraseInput { - flex: none; - width: 250px; - border: 1px solid $accent; - border-radius: 5px; - padding: 10px; - margin-bottom: 1em; -} - -.mx_CreateKeyBackupDialog_passPhraseMatch { - margin-left: 20px; -} - -.mx_CreateKeyBackupDialog_recoveryKeyHeader { - margin-bottom: 1em; -} - -.mx_CreateKeyBackupDialog_recoveryKeyContainer { - display: flex; -} - -.mx_CreateKeyBackupDialog_recoveryKey { - width: 262px; - padding: 20px; - color: $info-plinth-fg-color; - background-color: $info-plinth-bg-color; - margin-right: 12px; -} - -.mx_CreateKeyBackupDialog_recoveryKeyButtons { - flex: 1; - display: flex; - align-items: center; -} - -.mx_CreateKeyBackupDialog_recoveryKeyButtons button { - flex: 1; - white-space: nowrap; -} - -.mx_CreateKeyBackupDialog { - details .mx_AccessibleButton { - margin: 1em 0; /* emulate paragraph spacing because we can't put this button in a paragraph due to HTML rules */ - } -} diff --git a/res/css/views/rooms/_E2EIcon.pcss b/res/css/views/rooms/_E2EIcon.pcss index f3aaf8a883..a8e3a34095 100644 --- a/res/css/views/rooms/_E2EIcon.pcss +++ b/res/css/views/rooms/_E2EIcon.pcss @@ -59,3 +59,10 @@ Please see LICENSE files in the repository root for full details. mask-image: url("$(res)/img/e2e/verified.svg"); background-color: $e2e-verified-color; } + +// When using the "normal" icon as a background for verified or warning icon, +// it should be slightly smaller than the foreground icon +.mx_E2EIcon_verified, .mx_E2EIcon_warning .mx_E2EIcon_normal::after { + mask-size: 90%; + background-color: white; +} diff --git a/res/css/views/settings/_SettingsHeader.pcss b/res/css/views/settings/_SettingsHeader.pcss index a705deda6c..e1c470214f 100644 --- a/res/css/views/settings/_SettingsHeader.pcss +++ b/res/css/views/settings/_SettingsHeader.pcss @@ -16,4 +16,13 @@ font: var(--cpd-font-body-sm-medium); color: var(--cpd-color-text-action-accent); } + + &.mx_SettingsHeader_recommended::after { + display: inline-block; + content: ""; + width: 8px; + height: 8px; + background-color: var(--cpd-color-icon-critical-primary); + clip-path: circle(4px); + } } diff --git a/sonar-project.properties b/sonar-project.properties index 2d87d32efc..d95f46460b 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -19,5 +19,6 @@ sonar.coverage.exclusions=\ src/vector/modernizr.js,\ src/components/views/dialogs/devtools/**/*,\ src/utils/SessionLock.ts,\ - src/**/*.d.ts + src/**/*.d.ts,\ + src/vector/mobile_guide/**/* sonar.testExecutionReportPaths=coverage/jest-sonar-report.xml diff --git a/src/@types/matrix-js-sdk.d.ts b/src/@types/matrix-js-sdk.d.ts index e1f8c4562e..ad75ca95f0 100644 --- a/src/@types/matrix-js-sdk.d.ts +++ b/src/@types/matrix-js-sdk.d.ts @@ -92,6 +92,9 @@ declare module "matrix-js-sdk/src/types" { // MSC4155: Invite filtering [INVITE_RULES_ACCOUNT_DATA_TYPE]: InviteConfigAccountData; "io.element.msc4278.media_preview_config": MediaPreviewConfig; + + // Indicate whether recovery is enabled or disabled + "io.element.recovery": { enabled: boolean }; } export interface AudioContent { diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index e13d296bc1..85adb0fe55 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -57,6 +57,11 @@ const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000; */ export const BACKUP_DISABLED_ACCOUNT_DATA_KEY = "m.org.matrix.custom.backup_disabled"; +/** + * Account data key to indicate whether the user has chosen to enable or disable recovery. + */ +export const RECOVERY_ACCOUNT_DATA_KEY = "io.element.recovery"; + const logger = baseLogger.getChild("DeviceListener:"); export default class DeviceListener { @@ -165,6 +170,13 @@ export default class DeviceListener { await this.client?.setAccountData(BACKUP_DISABLED_ACCOUNT_DATA_KEY, { disabled: true }); } + /** + * Set the account data to indicate that recovery is disabled + */ + public async recordRecoveryDisabled(): Promise { + await this.client?.setAccountData(RECOVERY_ACCOUNT_DATA_KEY, { enabled: false }); + } + private async ensureDeviceIdsAtStartPopulated(): Promise { if (this.ourDeviceIdsAtStart === null) { this.ourDeviceIdsAtStart = await this.getDeviceIds(); @@ -220,7 +232,8 @@ export default class DeviceListener { ev.getType().startsWith("m.secret_storage.") || ev.getType().startsWith("m.cross_signing.") || ev.getType() === "m.megolm_backup.v1" || - ev.getType() === BACKUP_DISABLED_ACCOUNT_DATA_KEY + ev.getType() === BACKUP_DISABLED_ACCOUNT_DATA_KEY || + ev.getType() === RECOVERY_ACCOUNT_DATA_KEY ) { this.recheck(); } @@ -332,6 +345,9 @@ export default class DeviceListener { crossSigningStatus.privateKeysCachedLocally.userSigningKey; const defaultKeyId = await cli.secretStorage.getDefaultKeyId(); + const recoveryDisabled = await this.recheckRecoveryDisabled(cli); + + const recoveryIsOk = secretStorageReady || recoveryDisabled; const isCurrentDeviceTrusted = crossSigningReady && @@ -346,8 +362,7 @@ export default class DeviceListener { // said we are OK with that. const keyBackupIsOk = keyBackupUploadActive || backupDisabled; - const allSystemsReady = - crossSigningReady && keyBackupIsOk && secretStorageReady && allCrossSigningSecretsCached; + const allSystemsReady = isCurrentDeviceTrusted && allCrossSigningSecretsCached && keyBackupIsOk && recoveryIsOk; await this.reportCryptoSessionStateToAnalytics(cli); @@ -360,13 +375,8 @@ export default class DeviceListener { // make sure our keys are finished downloading await crypto.getUserDeviceInfo([cli.getSafeUserId()]); - if (!crossSigningReady) { - // This account is legacy and doesn't have cross-signing set up at all. - // Prompt the user to set it up. - logSpan.info("Cross-signing not ready: showing SET_UP_ENCRYPTION toast"); - showSetupEncryptionToast(SetupKind.SET_UP_ENCRYPTION); - } else if (!isCurrentDeviceTrusted) { - // cross signing is ready but the current device is not trusted: prompt the user to verify + if (!isCurrentDeviceTrusted) { + // the current device is not trusted: prompt the user to verify logSpan.info("Current device not verified: showing VERIFY_THIS_SESSION toast"); showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION); } else if (!allCrossSigningSecretsCached) { @@ -384,7 +394,10 @@ export default class DeviceListener { // The user just hasn't set up 4S yet: if they have key // backup, prompt them to turn on recovery too. (If not, they // have explicitly opted out, so don't hassle them.) - if (keyBackupUploadActive) { + if (recoveryDisabled) { + logSpan.info("Recovery disabled: no toast needed"); + hideSetupEncryptionToast(); + } else if (keyBackupUploadActive) { logSpan.info("No default 4S key: showing SET_UP_RECOVERY toast"); showSetupEncryptionToast(SetupKind.SET_UP_RECOVERY); } else { @@ -392,16 +405,17 @@ export default class DeviceListener { hideSetupEncryptionToast(); } } else { - // some other condition... yikes! Show the 'set up encryption' toast: this is what we previously did - // in 'other' situations. Possibly we should consider prompting for a full reset in this case? - logSpan.warn("Couldn't match encryption state to a known case: showing 'setup encryption' prompt", { + // If we get here, then we are verified, have key backup, and + // 4S, but crypto.isSecretStorageReady returned false, which + // means that 4S doesn't have all the secrets. + logSpan.warn("4S is missing secrets", { crossSigningReady, secretStorageReady, allCrossSigningSecretsCached, isCurrentDeviceTrusted, defaultKeyId, }); - showSetupEncryptionToast(SetupKind.SET_UP_ENCRYPTION); + showSetupEncryptionToast(SetupKind.KEY_STORAGE_OUT_OF_SYNC_STORE); } } else { logSpan.info("Not yet ready, but shouldShowSetupEncryptionToast==false"); @@ -482,6 +496,20 @@ export default class DeviceListener { return !!backupDisabled?.disabled; } + /** + * Check whether the user has disabled recovery. If this is the first time, + * fetch it from the server (in case the initial sync has not finished). + * Otherwise, fetch it from the store as normal. + */ + private async recheckRecoveryDisabled(cli: MatrixClient): Promise { + const recoveryStatus = await cli.getAccountDataFromServer(RECOVERY_ACCOUNT_DATA_KEY); + // Recovery is disabled only if the `enabled` flag is set to `false`. + // If it is missing, or set to any other value, we consider it as + // not-disabled, and will prompt the user to create recovery (if + // missing). + return recoveryStatus?.enabled === false; + } + /** * Reports current recovery state to analytics. * Checks if the session is verified and if the recovery is correctly set up (i.e all secrets known locally and in 4S). diff --git a/src/IConfigOptions.ts b/src/IConfigOptions.ts index 8b88a18075..952d35e88d 100644 --- a/src/IConfigOptions.ts +++ b/src/IConfigOptions.ts @@ -81,6 +81,7 @@ export interface IConfigOptions { }; mobile_guide_toast?: boolean; + mobile_guide_app_variant?: "element" | "element-classic" | "element-pro"; default_theme?: "light" | "dark" | string; // custom themes are strings default_country_code?: string; // ISO 3166 alpha2 country code diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index bfd0eb237d..ecf895fd79 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -176,10 +176,11 @@ export async function withSecretStorageKeyCache(func: () => Promise): Prom } export interface AccessSecretStorageOpts { - /** Reset secret storage even if it's already set up. */ + /** + * Reset secret storage even if it's already set up. + * @deprecated send the user to the Encryption settings tab to reset secret storage + */ forceReset?: boolean; - /** Create new cross-signing keys. Only applicable if `forceReset` is `true`. */ - resetCrossSigning?: boolean; } /** @@ -189,8 +190,8 @@ export interface AccessSecretStorageOpts { * provided function. * * Bootstrapping secret storage may take one of these paths: - * 1. Create secret storage from a passphrase and store cross-signing keys - * in secret storage. + * 1. (Only if `opts.forceReset` is set) create secret storage from a passphrase + * and store cross-signing keys in secret storage. * 2. Access existing secret storage by requesting passphrase and accessing * cross-signing keys as needed. * 3. All keys are loaded and there's nothing to do. @@ -199,6 +200,8 @@ export interface AccessSecretStorageOpts { * to ensure the user is prompted only once for their secret storage * passphrase. The cache is then cleared once the provided function completes. * + * Throws an error if secret storage is not set up (and `opts.forceReset` is not set) + * * @param {Function} [func] An operation to perform once secret storage has been * bootstrapped. Optional. * @param [opts] The options to use when accessing secret storage. @@ -219,16 +222,8 @@ async function doAccessSecretStorage(func: () => Promise, opts: AccessSecr throw new Error("End-to-end encryption is disabled - unable to access secret storage."); } - let createNew = false; if (opts.forceReset) { logger.debug("accessSecretStorage: resetting 4S"); - createNew = true; - } else if (!(await cli.secretStorage.hasKey())) { - logger.debug("accessSecretStorage: no 4S key configured, creating a new one"); - createNew = true; - } - - if (createNew) { // This dialog calls bootstrap itself after guiding the user through // passphrase creation. const { finished } = Modal.createDialog( @@ -251,6 +246,9 @@ async function doAccessSecretStorage(func: () => Promise, opts: AccessSecr if (!confirmed) { throw new Error("Secret storage creation canceled"); } + } else if (!(await cli.secretStorage.hasKey())) { + logger.debug("accessSecretStorage: no 4S key configured"); + throw new Error("Secret storage has not been created yet."); } else { logger.debug("accessSecretStorage: bootstrapCrossSigning"); await crypto.bootstrapCrossSigning({ diff --git a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx deleted file mode 100644 index 0bc6fea219..0000000000 --- a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx +++ /dev/null @@ -1,186 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. -Copyright 2018, 2019 New Vector Ltd - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import React, { type JSX } from "react"; -import { logger } from "matrix-js-sdk/src/logger"; - -import { MatrixClientPeg } from "../../../../MatrixClientPeg"; -import { _t } from "../../../../languageHandler"; -import { accessSecretStorage, withSecretStorageKeyCache } from "../../../../SecurityManager"; -import Spinner from "../../../../components/views/elements/Spinner"; -import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; -import DialogButtons from "../../../../components/views/elements/DialogButtons"; - -enum Phase { - BackingUp = "backing_up", - Done = "done", -} - -interface IProps { - onFinished(done?: boolean): void; -} - -interface IState { - phase: Phase; - passPhrase: string; - passPhraseValid: boolean; - passPhraseConfirm: string; - copied: boolean; - downloaded: boolean; - error?: boolean; -} - -/** - * Walks the user through the process of setting up e2e key backups to a new backup, and storing the decryption key in - * SSSS. - * - * Uses {@link accessSecretStorage}, which means that if 4S is not already configured, it will be bootstrapped (which - * involves displaying an {@link CreateSecretStorageDialog} so the user can enter a passphrase and/or download the 4S - * key). - */ -export default class CreateKeyBackupDialog extends React.PureComponent { - public constructor(props: IProps) { - super(props); - - this.state = { - phase: Phase.BackingUp, - passPhrase: "", - passPhraseValid: false, - passPhraseConfirm: "", - copied: false, - downloaded: false, - }; - } - - public componentDidMount(): void { - this.createBackup(); - } - - private createBackup = async (): Promise => { - this.setState({ - error: undefined, - }); - const cli = MatrixClientPeg.safeGet(); - try { - // Check if 4S already set up - const secretStorageAlreadySetup = await cli.secretStorage.hasKey(); - - if (!secretStorageAlreadySetup) { - // bootstrap secret storage; that will also create a backup version - await accessSecretStorage(async (): Promise => { - // do nothing, all is now set up correctly - }); - } else { - await withSecretStorageKeyCache(async () => { - const crypto = cli.getCrypto(); - if (!crypto) { - throw new Error("End-to-end encryption is disabled - unable to create backup."); - } - - // Before we reset the backup, let's make sure we can access secret storage, to - // reduce the chance of us getting into a broken state where we have an outdated - // secret in secret storage. - // `SecretStorage.get` will ask the user to enter their passphrase/key if necessary; - // it will then be cached for the actual backup reset operation. - await cli.secretStorage.get("m.megolm_backup.v1"); - - // We now know we can store the new backup key in secret storage, so it is safe to - // go ahead with the reset. - await crypto.resetKeyBackup(); - }); - } - - this.setState({ - phase: Phase.Done, - }); - } catch (e) { - logger.error("Error creating key backup", e); - // TODO: If creating a version succeeds, but backup fails, should we - // delete the version, disable backup, or do nothing? If we just - // disable without deleting, we'll enable on next app reload since - // it is trusted. - this.setState({ - error: true, - }); - } - }; - - private onCancel = (): void => { - this.props.onFinished(false); - }; - - private onDone = (): void => { - this.props.onFinished(true); - }; - - private renderBusyPhase(): JSX.Element { - return ( -

- -
- ); - } - - private renderPhaseDone(): JSX.Element { - return ( -
-

{_t("settings|key_backup|backup_in_progress")}

- -
- ); - } - - private titleForPhase(phase: Phase): string { - switch (phase) { - case Phase.BackingUp: - return _t("settings|key_backup|backup_starting"); - case Phase.Done: - return _t("settings|key_backup|backup_success"); - default: - return _t("settings|key_backup|create_title"); - } - } - - public render(): React.ReactNode { - let content; - if (this.state.error) { - content = ( -
-

{_t("settings|key_backup|cannot_create_backup")}

- -
- ); - } else { - switch (this.state.phase) { - case Phase.BackingUp: - content = this.renderBusyPhase(); - break; - case Phase.Done: - content = this.renderPhaseDone(); - break; - } - } - - return ( - -
{content}
-
- ); - } -} diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx index 0d17ddaa39..4ed369fb13 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx @@ -56,7 +56,6 @@ const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, interface IProps { forceReset?: boolean; - resetCrossSigning?: boolean; onFinished(ok?: boolean): void; } @@ -80,11 +79,12 @@ interface IState { * If the user already has a key backup, follows a "migration" flow (aka "Upgrade your encryption") which * prompts the user to enter their backup decryption password (a Curve25519 private key, possibly derived * from a passphrase), and uses that as the (AES) 4S encryption key. + * + * @deprecated send the user to EncryptionUserSettingsTab instead */ export default class CreateSecretStorageDialog extends React.PureComponent { public static defaultProps: Partial = { forceReset: false, - resetCrossSigning: false, }; private recoveryKey?: GeneratedSecretStorageKey; private recoveryKeyNode = createRef(); @@ -211,7 +211,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent => { const cli = MatrixClientPeg.safeGet(); const crypto = cli.getCrypto()!; - const { forceReset, resetCrossSigning } = this.props; + 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 @@ -246,13 +246,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent this.recoveryKey!, setupNewSecretStorage: true, }); - if (resetCrossSigning) { - logger.log("Resetting cross signing"); - await crypto.bootstrapCrossSigning({ - authUploadDeviceSigningKeys: this.doBootstrapUIAuth, - setupNewCrossSigning: true, - }); - } logger.log("Resetting key backup"); await crypto.resetKeyBackup(); } else { diff --git a/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.tsx b/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.tsx index 7e82ff722b..7c3abf37b1 100644 --- a/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.tsx +++ b/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.tsx @@ -7,14 +7,15 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { lazy } from "react"; +import React from "react"; import dis from "../../../../dispatcher/dispatcher"; import { _t } from "../../../../languageHandler"; -import Modal from "../../../../Modal"; import { Action } from "../../../../dispatcher/actions"; +import { UserTab } from "../../../../components/views/dialogs/UserTab"; import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; import DialogButtons from "../../../../components/views/elements/DialogButtons"; +import { type OpenToTabPayload } from "../../../../dispatcher/payloads/OpenToTabPayload"; interface IProps { onFinished(): void; @@ -28,13 +29,12 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent { this.props.onFinished(); - Modal.createDialog( - lazy(() => import("./CreateKeyBackupDialog")), - undefined, - undefined, - /* priority = */ false, - /* static = */ true, - ); + // Open the user settings dialog to the encryption tab and start the flow to reset encryption + const payload: OpenToTabPayload = { + action: Action.ViewUserSettings, + initialTabId: UserTab.Encryption, + }; + dis.dispatch(payload); }; public render(): React.ReactNode { diff --git a/src/components/structures/FileDropTarget.tsx b/src/components/structures/FileDropTarget.tsx index dcf4a2fdb1..5ad16303bf 100644 --- a/src/components/structures/FileDropTarget.tsx +++ b/src/components/structures/FileDropTarget.tsx @@ -7,11 +7,14 @@ Please see LICENSE files in the repository root for full details. */ import React, { useEffect, useState } from "react"; +import { type Room } from "matrix-js-sdk/src/matrix"; import { _t } from "../../languageHandler"; import UploadBigSvg from "../../../res/img/upload-big.svg"; +import { useRoomState } from "../../hooks/useRoomState.ts"; interface IProps { + room: Room; parent: HTMLElement | null; onFileDrop(dataTransfer: DataTransfer): void; } @@ -21,14 +24,15 @@ interface IState { counter: number; } -const FileDropTarget: React.FC = ({ parent, onFileDrop }) => { +const FileDropTarget: React.FC = ({ parent, onFileDrop, room }) => { const [state, setState] = useState({ dragging: false, counter: 0, }); + const hasPermission = useRoomState(room, (state) => state.maySendMessage(room.client.getUserId()!)); useEffect(() => { - if (!parent || parent.ondrop) return; + if (!hasPermission || !parent || parent.ondrop) return; const onDragEnter = (ev: DragEvent): void => { ev.stopPropagation(); @@ -102,9 +106,9 @@ const FileDropTarget: React.FC = ({ parent, onFileDrop }) => { parent?.removeEventListener("dragenter", onDragEnter); parent?.removeEventListener("dragleave", onDragLeave); }; - }, [parent, onFileDrop]); + }, [parent, onFileDrop, hasPermission]); - if (state.dragging) { + if (hasPermission && state.dragging) { return (
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index b6f8bc06b3..20951448af 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -316,7 +316,7 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement {
- +
{encryptionTile} @@ -2564,7 +2564,11 @@ export class RoomView extends React.Component { {auxPanel} {pinnedMessageBanner}
- + {topUnreadMessagesBar} {jumpToBottom} {messagePanel} diff --git a/src/components/structures/TabbedView.tsx b/src/components/structures/TabbedView.tsx index b01f160551..029226fc87 100644 --- a/src/components/structures/TabbedView.tsx +++ b/src/components/structures/TabbedView.tsx @@ -29,6 +29,7 @@ export class Tab { * @param {string|JSX.Element} icon An SVG element to use for the tab icon. Can also be a string for legacy icons, in which case it is the class for the tab icon. This should be a simple mask. * @param {JSX.Element} body The JSX for the tab container. * @param {string} screenName The screen name to report to Posthog. + * @param {string} labelClassName Additional class to add to the tab label. */ public constructor( public readonly id: T, @@ -36,6 +37,7 @@ export class Tab { public readonly icon: string | JSX.Element | null, public readonly body: JSX.Element, public readonly screenName?: ScreenName, + public readonly labelClassName?: string, ) {} } @@ -85,7 +87,7 @@ interface ITabLabelProps { } function TabLabel({ tab, isActive, showToolip, onClick }: ITabLabelProps): JSX.Element { - const classes = classNames("mx_TabbedView_tabLabel", { + const classes = classNames("mx_TabbedView_tabLabel", tab.labelClassName, { mx_TabbedView_tabLabel_active: isActive, }); diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index c1c4b3ff57..514fd9875c 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -388,7 +388,7 @@ export default class ThreadView extends React.Component { timeline = ( <> - + void; +} + +export const useUserInfoPowerlevelViewModel = (user: RoomMember, room: Room): UserInfoPowerLevelState => { + const [selectedPowerLevel, setSelectedPowerLevel] = useState(user.powerLevel); + + useEffect(() => { + setSelectedPowerLevel(user.powerLevel); + }, [user]); + + const cli = useContext(MatrixClientContext); + const onPowerChange = useCallback( + async (powerLevel: number) => { + setSelectedPowerLevel(powerLevel); + + const applyPowerChange = (roomId: string, target: string, powerLevel: number): Promise => { + return cli.setPowerLevel(roomId, target, powerLevel).then( + function () { + logger.info("Power change success"); + }, + function (err) { + logger.error("Failed to change power level " + err); + Modal.createDialog(ErrorDialog, { + title: _t("common|error"), + description: _t("error|update_power_level"), + }); + }, + ); + }; + + const roomId = user.roomId; + const target = user.userId; + + const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); + if (!powerLevelEvent) return; + + const myUserId = cli.getUserId(); + const myPower = powerLevelEvent.getContent().users[myUserId || ""]; + if (myPower && parseInt(myPower) <= powerLevel && myUserId !== target) { + const { finished } = Modal.createDialog(QuestionDialog, { + title: _t("common|warning"), + description: ( +
+ {_t("user_info|promote_warning")} +
+ {_t("common|are_you_sure")} +
+ ), + button: _t("action|continue"), + }); + + const [confirmed] = await finished; + if (!confirmed) return; + } else if (myUserId === target && myPower && parseInt(myPower) > powerLevel) { + // If we are changing our own PL it can only ever be decreasing, which we cannot reverse. + try { + if (!(await warnSelfDemote(room?.isSpaceRoom()))) return; + } catch (e) { + logger.error("Failed to warn about self demotion: " + e); + } + } + + await applyPowerChange(roomId, target, powerLevel); + }, + [user.roomId, user.userId, cli, room], + ); + + const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); + const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0; + + return { + powerLevelUsersDefault, + onPowerChange, + selectedPowerLevel, + }; +}; diff --git a/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx b/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx index e009033875..107d1a4c60 100644 --- a/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx +++ b/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx @@ -30,6 +30,10 @@ export interface RoomListItemViewState { * The name of the room. */ name: string; + /** + * Whether the context menu should be shown. + */ + showContextMenu: boolean; /** * Whether the hover menu should be shown. */ @@ -105,12 +109,12 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState { setNotificationValues(getNotificationValues(notificationState)); }, [notificationState]); - // We don't want to show the hover menu if + // We don't want to show the menus if // - there is an invitation for this room - // - the user doesn't have access to both notification and more options menus + // - the user doesn't have access to notification and more options menus + const showContextMenu = !invited && hasAccessToOptionsMenu(room); const showHoverMenu = - !invited && - (hasAccessToOptionsMenu(room) || hasAccessToNotificationMenu(room, matrixClient.isGuest(), isArchived)); + !invited && (showContextMenu || hasAccessToNotificationMenu(room, matrixClient.isGuest(), isArchived)); const messagePreview = useRoomMessagePreview(room); @@ -137,6 +141,7 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState { return { name, notificationState, + showContextMenu, showHoverMenu, openRoom, a11yLabel, diff --git a/src/components/views/dialogs/LogoutDialog.tsx b/src/components/views/dialogs/LogoutDialog.tsx index 6e000ef631..830e5fb589 100644 --- a/src/components/views/dialogs/LogoutDialog.tsx +++ b/src/components/views/dialogs/LogoutDialog.tsx @@ -13,9 +13,11 @@ import { type MatrixClient } from "matrix-js-sdk/src/matrix"; import Modal from "../../../Modal"; import dis from "../../../dispatcher/dispatcher"; +import { type OpenToTabPayload } from "../../../dispatcher/payloads/OpenToTabPayload"; +import { Action } from "../../../dispatcher/actions"; +import { UserTab } from "../../../components/views/dialogs/UserTab"; import { _t } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import RestoreKeyBackupDialog from "./security/RestoreKeyBackupDialog"; import QuestionDialog from "./QuestionDialog"; import BaseDialog from "./BaseDialog"; import Spinner from "../elements/Spinner"; @@ -138,26 +140,12 @@ export default class LogoutDialog extends React.Component { }; private onSetRecoveryMethodClick = (): void => { - if (this.state.backupStatus === BackupStatus.SERVER_BACKUP_BUT_DISABLED) { - // A key backup exists for this account, but the creating device is not - // verified, so restore the backup which will give us the keys from it and - // allow us to trust it (ie. upload keys to it) - Modal.createDialog( - RestoreKeyBackupDialog, - undefined, - undefined, - /* priority = */ false, - /* static = */ true, - ); - } else { - Modal.createDialog( - lazy(() => import("../../../async-components/views/dialogs/security/CreateKeyBackupDialog")), - undefined, - undefined, - /* priority = */ false, - /* static = */ true, - ); - } + // Open the user settings dialog to the encryption tab and start the flow to reset encryption + const payload: OpenToTabPayload = { + action: Action.ViewUserSettings, + initialTabId: UserTab.Encryption, + }; + dis.dispatch(payload); // close dialog this.props.onFinished(true); @@ -190,22 +178,13 @@ export default class LogoutDialog extends React.Component {
); - let setupButtonCaption; - if (this.state.backupStatus === BackupStatus.SERVER_BACKUP_BUT_DISABLED) { - setupButtonCaption = _t("settings|security|key_backup_connect"); - } else { - // if there's an error fetching the backup info, we'll just assume there's - // no backup for the purpose of the button caption - setupButtonCaption = _t("auth|logout_dialog|use_key_backup"); - } - const dialogContent = (
{description}
=> { + if (event === undefined || event.getType() === "m.secret_storage.default_key") { + const client = props.sdkContext.client; + if (!client) { + return false; + } + + return !(await client.secretStorage.getDefaultKeyId()); + } + return new NoChange(); + }, + [], + false, + ); + const getTabs = (): NonEmptyArray> => { const tabs: Tab[] = []; @@ -196,6 +218,7 @@ export default function UserSettingsDialog(props: IProps): JSX.Element { , , "UserSettingsEncryption", + showSetupRecoveryIndicator ? "mx_SettingsDialog_tabLabelsAlert" : undefined, ), ); diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index e3ee6f20d5..f236e5193e 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -8,9 +8,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type JSX, createRef, type CSSProperties, useRef, useState, useMemo } from "react"; +import React, { type JSX, createRef, type CSSProperties, useRef, useState, useMemo, useEffect } from "react"; import FocusLock from "react-focus-lock"; import { type MatrixEvent, parseErrorResponse } from "matrix-js-sdk/src/matrix"; +import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "../../../languageHandler"; import MemberAvatar from "../avatars/MemberAvatar"; @@ -34,6 +35,7 @@ import Modal from "../../../Modal"; import ErrorDialog from "../dialogs/ErrorDialog"; import { FileDownloader } from "../../../utils/FileDownloader"; import { MediaEventHelper } from "../../../utils/MediaEventHelper.ts"; +import ModuleApi from "../../../modules/Api"; // Max scale to keep gaps around the image const MAX_SCALE = 0.95; @@ -591,12 +593,36 @@ function DownloadButton({ url: string; fileName?: string; mxEvent?: MatrixEvent; -}): JSX.Element { +}): JSX.Element | null { const downloader = useRef(new FileDownloader()).current; const [loading, setLoading] = useState(false); + const [canDownload, setCanDownload] = useState(false); const blobRef = useRef(undefined); const mediaEventHelper = useMemo(() => (mxEvent ? new MediaEventHelper(mxEvent) : undefined), [mxEvent]); + useEffect(() => { + if (!mxEvent) { + // If we have no event, we assume this is safe to download. + setCanDownload(true); + return; + } + const hints = ModuleApi.customComponents.getHintsForMessage(mxEvent); + if (hints?.allowDownloadingMedia) { + // Disable downloading as soon as we know there is a hint. + setCanDownload(false); + hints + .allowDownloadingMedia() + .then((downloadable) => { + setCanDownload(downloadable); + }) + .catch((ex) => { + logger.error(`Failed to check if media from ${mxEvent.getId()} could be downloaded`, ex); + // Err on the side of safety. + setCanDownload(false); + }); + } + }, [mxEvent]); + function showError(e: unknown): void { Modal.createDialog(ErrorDialog, { title: _t("timeline|download_failed"), @@ -640,6 +666,10 @@ function DownloadButton({ setLoading(false); } + if (!canDownload) { + return null; + } + return ( = { canDownload: true }; + if (moduleHints?.allowDownloadingMedia) { + downloadState.canDownload = null; + moduleHints + .allowDownloadingMedia() + .then((canDownload) => { + this.setState({ + canDownload: canDownload, + }); + }) + .catch((ex) => { + logger.error(`Failed to check if media from ${props.mxEvent.getId()} could be downloaded`, ex); + this.setState({ + canDownload: false, + }); + }); + } + this.state = { loading: false, tooltip: _td("timeline|download_action_downloading"), + ...downloadState, }; } @@ -97,6 +120,14 @@ export default class DownloadActionButton extends React.PureComponent; } + if (this.state.canDownload === null) { + spinner = ; + } + + if (this.state.canDownload === false) { + return null; + } + const classes = classNames({ mx_MessageActionBar_iconButton: true, mx_MessageActionBar_downloadButton: true, diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index 542e6421de..970f8a2278 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -43,7 +43,6 @@ import { type ButtonEvent } from "../elements/AccessibleButton"; import SdkConfig from "../../../SdkConfig"; import MultiInviter from "../../../utils/MultiInviter"; import { useTypedEventEmitter } from "../../../hooks/useEventEmitter"; -import { textualPowerLevel } from "../../../Roles"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases"; import EncryptionPanel from "./EncryptionPanel"; @@ -54,7 +53,6 @@ import { useIsEncrypted } from "../../../hooks/useIsEncrypted"; import BaseCard from "./BaseCard"; import ImageView from "../elements/ImageView"; import Spinner from "../elements/Spinner"; -import PowerSelector from "../elements/PowerSelector"; import MemberAvatar from "../avatars/MemberAvatar"; import PresenceLabel from "../rooms/PresenceLabel"; import { ShareDialog } from "../dialogs/ShareDialog"; @@ -76,6 +74,7 @@ import { Flex } from "../../utils/Flex"; import CopyableText from "../elements/CopyableText"; import { useUserTimezone } from "../../../hooks/useUserTimezone"; import { UserInfoAdminToolsContainer } from "./user_info/UserInfoAdminToolsContainer"; +import { PowerLevelSection } from "./user_info/UserInfoPowerLevels"; export interface IDevice extends Device { ambiguous?: boolean; @@ -437,7 +436,7 @@ const useHomeserverSupportsCrossSigning = (cli: MatrixClient): boolean => { ); }; -interface IRoomPermissions { +export interface IRoomPermissions { modifyLevelMax: number; canEdit: boolean; canInvite: boolean; @@ -492,112 +491,6 @@ function useRoomPermissions(cli: MatrixClient, room: Room, user: RoomMember): IR return roomPermissions; } -const PowerLevelSection: React.FC<{ - user: RoomMember; - room: Room; - roomPermissions: IRoomPermissions; - powerLevels: IPowerLevelsContent; -}> = ({ user, room, roomPermissions, powerLevels }) => { - if (roomPermissions.canEdit) { - return ; - } else { - const powerLevelUsersDefault = powerLevels.users_default || 0; - const powerLevel = user.powerLevel; - const role = textualPowerLevel(powerLevel, powerLevelUsersDefault); - return ( -
-
{role}
-
- ); - } -}; - -export const PowerLevelEditor: React.FC<{ - user: RoomMember; - room: Room; - roomPermissions: IRoomPermissions; -}> = ({ user, room, roomPermissions }) => { - const cli = useContext(MatrixClientContext); - - const [selectedPowerLevel, setSelectedPowerLevel] = useState(user.powerLevel); - useEffect(() => { - setSelectedPowerLevel(user.powerLevel); - }, [user]); - - const onPowerChange = useCallback( - async (powerLevel: number) => { - setSelectedPowerLevel(powerLevel); - - const applyPowerChange = (roomId: string, target: string, powerLevel: number): Promise => { - return cli.setPowerLevel(roomId, target, powerLevel).then( - function () { - // NO-OP; rely on the m.room.member event coming down else we could - // get out of sync if we force setState here! - logger.log("Power change success"); - }, - function (err) { - logger.error("Failed to change power level " + err); - Modal.createDialog(ErrorDialog, { - title: _t("common|error"), - description: _t("error|update_power_level"), - }); - }, - ); - }; - - const roomId = user.roomId; - const target = user.userId; - - const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); - if (!powerLevelEvent) return; - - const myUserId = cli.getUserId(); - const myPower = powerLevelEvent.getContent().users[myUserId || ""]; - if (myPower && parseInt(myPower) <= powerLevel && myUserId !== target) { - const { finished } = Modal.createDialog(QuestionDialog, { - title: _t("common|warning"), - description: ( -
- {_t("user_info|promote_warning")} -
- {_t("common|are_you_sure")} -
- ), - button: _t("action|continue"), - }); - - const [confirmed] = await finished; - if (!confirmed) return; - } else if (myUserId === target && myPower && parseInt(myPower) > powerLevel) { - // If we are changing our own PL it can only ever be decreasing, which we cannot reverse. - try { - if (!(await warnSelfDemote(room?.isSpaceRoom()))) return; - } catch (e) { - logger.error("Failed to warn about self demotion: ", e); - } - } - - await applyPowerChange(roomId, target, powerLevel); - }, - [user.roomId, user.userId, cli, room], - ); - - const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); - const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0; - - return ( -
- -
- ); -}; - async function getUserDeviceInfo( userId: string, cli: MatrixClient, @@ -820,12 +713,7 @@ const BasicUserInfo: React.FC<{ // hide the Roles section for DMs as it doesn't make sense there if (!DMRoomMap.shared().getUserIdForRoomId((member as RoomMember).roomId)) { memberDetails = ( - + ); } diff --git a/src/components/views/right_panel/user_info/UserInfoPowerLevels.tsx b/src/components/views/right_panel/user_info/UserInfoPowerLevels.tsx new file mode 100644 index 0000000000..c7c42da559 --- /dev/null +++ b/src/components/views/right_panel/user_info/UserInfoPowerLevels.tsx @@ -0,0 +1,53 @@ +/* +Copyright 2025 New Vector Ltd. +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import React from "react"; +import { type RoomMember, type Room } from "matrix-js-sdk/src/matrix"; + +import { textualPowerLevel } from "../../../../Roles"; +import PowerSelector from "../../elements/PowerSelector"; +import { type IRoomPermissions } from "../UserInfo"; +import { + type UserInfoPowerLevelState, + useUserInfoPowerlevelViewModel, +} from "../../../viewmodels/right_panel/UserInfoPowerlevelViewModel"; + +export const PowerLevelSection: React.FC<{ + user: RoomMember; + room: Room; + roomPermissions: IRoomPermissions; +}> = ({ user, room, roomPermissions }) => { + const vm = useUserInfoPowerlevelViewModel(user, room); + + if (roomPermissions.canEdit) { + return ; + } + + const powerLevel = user.powerLevel; + const role = textualPowerLevel(powerLevel, vm.powerLevelUsersDefault); + return ( +
+
{role}
+
+ ); +}; + +export const PowerLevelEditor: React.FC<{ + vm: UserInfoPowerLevelState; + roomPermissions: IRoomPermissions; +}> = ({ vm, roomPermissions }) => { + return ( +
+ +
+ ); +}; diff --git a/src/components/views/rooms/E2EIcon.tsx b/src/components/views/rooms/E2EIcon.tsx index e4f19762ff..7cc76066ee 100644 --- a/src/components/views/rooms/E2EIcon.tsx +++ b/src/components/views/rooms/E2EIcon.tsx @@ -76,7 +76,17 @@ const E2EIcon: React.FC = ({ if (onClick) { content = ; } else { - content =
; + // Verified and warning icon have a transparent cutout, so add a white background. + // The normal icon already has the correct shape and size, so reuse that. + if (status === E2EStatus.Verified || status === E2EStatus.Warning) { + content = ( +
+
+
+ ); + } else { + content =
; + } } if (!e2eTitle || hideTooltip) { diff --git a/src/components/views/rooms/RoomListPanel/RoomListItemContextMenuView.tsx b/src/components/views/rooms/RoomListPanel/RoomListItemContextMenuView.tsx new file mode 100644 index 0000000000..0769d9e40a --- /dev/null +++ b/src/components/views/rooms/RoomListPanel/RoomListItemContextMenuView.tsx @@ -0,0 +1,50 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { type Room } from "matrix-js-sdk/src/matrix"; +import { type JSX, type PropsWithChildren } from "react"; +import { ContextMenu } from "@vector-im/compound-web"; +import React from "react"; + +import { _t } from "../../../../languageHandler"; +import { MoreOptionContent } from "./RoomListItemMenuView"; +import { useRoomListItemMenuViewModel } from "../../../viewmodels/roomlist/RoomListItemMenuViewModel"; + +interface RoomListItemContextMenuViewProps { + /** + * The room to display the menu for. + */ + room: Room; + /** + * Set the menu open state. + */ + setMenuOpen: (isOpen: boolean) => void; +} + +/** + * A view for the room list item context menu. + */ +export function RoomListItemContextMenuView({ + room, + setMenuOpen, + children, +}: PropsWithChildren): JSX.Element { + const vm = useRoomListItemMenuViewModel(room); + + return ( + + + + ); +} diff --git a/src/components/views/rooms/RoomListPanel/RoomListItemMenuView.tsx b/src/components/views/rooms/RoomListPanel/RoomListItemMenuView.tsx index fa7a85b54f..a901003342 100644 --- a/src/components/views/rooms/RoomListPanel/RoomListItemMenuView.tsx +++ b/src/components/views/rooms/RoomListPanel/RoomListItemMenuView.tsx @@ -35,7 +35,6 @@ interface RoomListItemMenuViewProps { room: Room; /** * Set the menu open state. - * @param isOpen */ setMenuOpen: (isOpen: boolean) => void; } @@ -84,6 +83,21 @@ function MoreOptionsMenu({ vm, setMenuOpen }: MoreOptionsMenuProps): JSX.Element align="start" trigger={} > + + + ); +} + +interface MoreOptionContentProps { + /** + * The view model state for the menu. + */ + vm: RoomListItemMenuViewState; +} + +export function MoreOptionContent({ vm }: MoreOptionContentProps): JSX.Element { + return ( + <> {vm.canMarkAsRead && ( evt.stopPropagation()} hideChevron={true} /> - + ); } @@ -154,7 +168,7 @@ interface MoreOptionsButtonProps extends ComponentProps { /** * A button to trigger the more options menu. */ -export const MoreOptionsButton = function MoreOptionsButton(props: MoreOptionsButtonProps): JSX.Element { +const MoreOptionsButton = function MoreOptionsButton(props: MoreOptionsButtonProps): JSX.Element { return ( @@ -244,7 +258,7 @@ interface NotificationButtonProps extends ComponentProps { /** * A button to trigger the notification menu. */ -export const NotificationButton = function MoreOptionsButton({ +const NotificationButton = function MoreOptionsButton({ isRoomMuted, ref, ...props diff --git a/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx b/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx index ac4d72ec57..cabb034975 100644 --- a/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx +++ b/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx @@ -15,6 +15,7 @@ import { RoomListItemMenuView } from "./RoomListItemMenuView"; import { NotificationDecoration } from "../NotificationDecoration"; import { RoomAvatarView } from "../../avatars/RoomAvatarView"; import { useRovingTabIndex } from "../../../../accessibility/RovingTabIndex"; +import { RoomListItemContextMenuView } from "./RoomListItemContextMenuView"; interface RoomListItemViewProps extends React.HTMLAttributes { /** @@ -47,7 +48,13 @@ export const RoomListItemView = memo(function RoomListItemView({ const showHoverDecoration = isMenuOpen || isHover; const showHoverMenu = showHoverDecoration && vm.showHoverMenu; - return ( + const closeMenu = useCallback(() => { + // To avoid icon blinking when closing the menu, we delay the state update + // Also, let the focus move to the menu trigger before closing the menu + setTimeout(() => setIsMenuOpen(false), 10); + }, []); + + const content = ( ); + + if (!vm.showContextMenu) return content; + + return ( + { + if (isOpen) { + // To avoid icon blinking when the context menu is re-opened + setTimeout(() => setIsMenuOpen(true), 0); + } else { + closeMenu(); + } + }} + > + {content} + + ); }); /** diff --git a/src/components/views/settings/SettingsHeader.tsx b/src/components/views/settings/SettingsHeader.tsx index 10534958f4..1db7fc9027 100644 --- a/src/components/views/settings/SettingsHeader.tsx +++ b/src/components/views/settings/SettingsHeader.tsx @@ -6,10 +6,9 @@ */ import React, { type JSX } from "react"; +import classNames from "classnames"; import { Heading } from "@vector-im/compound-web"; -import { _t } from "../../../languageHandler"; - /** * The heading for a settings section. */ @@ -25,9 +24,12 @@ interface SettingsHeaderProps { } export function SettingsHeader({ hasRecommendedTag = false, label }: SettingsHeaderProps): JSX.Element { + const classes = classNames("mx_SettingsHeader", { + mx_SettingsHeader_recommended: hasRecommendedTag, + }); return ( - - {label} {hasRecommendedTag && {_t("common|recommended")}} + + {label} ); } diff --git a/src/components/views/settings/encryption/ChangeRecoveryKey.tsx b/src/components/views/settings/encryption/ChangeRecoveryKey.tsx index 58efc80afb..b096708947 100644 --- a/src/components/views/settings/encryption/ChangeRecoveryKey.tsx +++ b/src/components/views/settings/encryption/ChangeRecoveryKey.tsx @@ -29,6 +29,7 @@ import { initialiseDehydrationIfEnabled } from "../../../../utils/device/dehydra import { withSecretStorageKeyCache } from "../../../../SecurityManager"; import { EncryptionCardButtons } from "./EncryptionCardButtons"; import { logErrorAndShowErrorDialog } from "../../../../utils/ErrorUtils.tsx"; +import { RECOVERY_ACCOUNT_DATA_KEY } from "../../../../DeviceListener"; /** * The possible states of the component. @@ -131,6 +132,10 @@ export function ChangeRecoveryKey({ }); await initialiseDehydrationIfEnabled(matrixClient, { createNewKey: true }); }); + + // Record the fact that the user explicitly enabled recovery. + await matrixClient.setAccountData(RECOVERY_ACCOUNT_DATA_KEY, { enabled: true }); + onFinish(); } catch (e) { logErrorAndShowErrorDialog("Failed to set up secret storage", e); diff --git a/src/components/views/settings/notifications/NotificationSettings2.tsx b/src/components/views/settings/notifications/NotificationSettings2.tsx index fe3f9f87c6..ab4da46fca 100644 --- a/src/components/views/settings/notifications/NotificationSettings2.tsx +++ b/src/components/views/settings/notifications/NotificationSettings2.tsx @@ -161,7 +161,7 @@ export default function NotificationSettings2(): JSX.Element { description={_t("settings|notifications|play_sound_for_description")} > { diff --git a/src/events/EventTileFactory.tsx b/src/events/EventTileFactory.tsx index f1fc224471..0d91ded160 100644 --- a/src/events/EventTileFactory.tsx +++ b/src/events/EventTileFactory.tsx @@ -43,6 +43,7 @@ import ViewSourceEvent from "../components/views/messages/ViewSourceEvent"; import { shouldDisplayAsBeaconTile } from "../utils/beacon/timeline"; import { ElementCall } from "../models/Call"; import { type IBodyProps } from "../components/views/messages/IBodyProps"; +import ModuleApi from "../modules/Api"; // Subset of EventTile's IProps plus some mixins export interface EventTileTypeProps @@ -257,7 +258,14 @@ export function renderTile( cli = cli ?? MatrixClientPeg.safeGet(); // because param defaults don't do the correct thing const factory = pickFactory(props.mxEvent, cli, showHiddenEvents); - if (!factory) return undefined; + if (!factory) { + // If we don't have a factory for this event, attempt + // to find a custom component that can render it. + // Will return null if no custom component can render it. + return ModuleApi.customComponents.renderMessage({ + mxEvent: props.mxEvent, + }); + } // Note that we split off the ones we actually care about here just to be sure that we're // not going to accidentally send things we shouldn't from lazy callers. Eg: EventTile's @@ -284,36 +292,48 @@ export function renderTile( case TimelineRenderingType.File: case TimelineRenderingType.Notification: case TimelineRenderingType.Thread: - // We only want a subset of props, so we don't end up causing issues for downstream components. - return factory(props.ref, { - mxEvent, - highlights, - highlightLink, - showUrlPreview, - editState, - replacingEventId, - getRelationsForEvent, - isSeeingThroughMessageHiddenForModeration, - permalinkCreator, - inhibitInteraction, - }); + return ModuleApi.customComponents.renderMessage( + { + mxEvent: props.mxEvent, + }, + (origProps) => + factory(props.ref, { + // We only want a subset of props, so we don't end up causing issues for downstream components. + mxEvent, + highlights, + highlightLink, + showUrlPreview: origProps?.showUrlPreview ?? showUrlPreview, + editState, + replacingEventId, + getRelationsForEvent, + isSeeingThroughMessageHiddenForModeration, + permalinkCreator, + inhibitInteraction, + }), + ); default: - // NEARLY ALL THE OPTIONS! - return factory(ref, { - mxEvent, - forExport, - replacingEventId, - editState, - highlights, - highlightLink, - showUrlPreview, - permalinkCreator, - callEventGrouper, - getRelationsForEvent, - isSeeingThroughMessageHiddenForModeration, - timestamp, - inhibitInteraction, - }); + return ModuleApi.customComponents.renderMessage( + { + mxEvent: props.mxEvent, + }, + (origProps) => + factory(ref, { + // NEARLY ALL THE OPTIONS! + mxEvent, + forExport, + replacingEventId, + editState, + highlights, + highlightLink, + showUrlPreview: origProps?.showUrlPreview ?? showUrlPreview, + permalinkCreator, + callEventGrouper, + getRelationsForEvent, + isSeeingThroughMessageHiddenForModeration, + timestamp, + inhibitInteraction, + }), + ); } } @@ -332,7 +352,14 @@ export function renderReplyTile( cli = cli ?? MatrixClientPeg.safeGet(); // because param defaults don't do the correct thing const factory = pickFactory(props.mxEvent, cli, showHiddenEvents); - if (!factory) return undefined; + if (!factory) { + // If we don't have a factory for this event, attempt + // to find a custom component that can render it. + // Will return null if no custom component can render it. + return ModuleApi.customComponents.renderMessage({ + mxEvent: props.mxEvent, + }); + } // See renderTile() for why we split off so much const { @@ -350,19 +377,25 @@ export function renderReplyTile( permalinkCreator, } = props; - return factory(ref, { - mxEvent, - highlights, - highlightLink, - showUrlPreview, - overrideBodyTypes, - overrideEventTypes, - replacingEventId, - maxImageHeight, - getRelationsForEvent, - isSeeingThroughMessageHiddenForModeration, - permalinkCreator, - }); + return ModuleApi.customComponents.renderMessage( + { + mxEvent: props.mxEvent, + }, + (origProps) => + factory(ref, { + mxEvent, + highlights, + highlightLink, + showUrlPreview: origProps?.showUrlPreview ?? showUrlPreview, + overrideBodyTypes, + overrideEventTypes, + replacingEventId, + maxImageHeight, + getRelationsForEvent, + isSeeingThroughMessageHiddenForModeration, + permalinkCreator, + }), + ); } // XXX: this'll eventually be dynamic based on the fields once we have extensible event types @@ -386,6 +419,12 @@ export function haveRendererForEvent( return false; } + // Check to see if we have any hints for this message, which indicates + // there is a custom renderer for the event. + if (ModuleApi.customComponents.getHintsForMessage(mxEvent)) { + return true; + } + // No tile for replacement events since they update the original tile if (mxEvent.isRelation(RelationType.Replace)) return false; diff --git a/src/hooks/useEventEmitter.ts b/src/hooks/useEventEmitter.ts index 67049195a0..12bb57a1ee 100644 --- a/src/hooks/useEventEmitter.ts +++ b/src/hooks/useEventEmitter.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { useRef, useEffect, useState, useCallback } from "react"; +import { useRef, useEffect, useState, useCallback, type DependencyList } from "react"; import { type ListenerMap, type TypedEventEmitter } from "matrix-js-sdk/src/matrix"; import type { EventEmitter } from "events"; @@ -93,3 +93,100 @@ export function useEventEmitterState( useEventEmitter(emitter, eventName, handler); return value; } + +/** + * The return value of the callback function for `useEventEmitterAsyncState`. + */ +export type AsyncStateCallbackResult = Promise; + +/** + * Creates a state, which is computed asynchronously, and can be updated by events. + * + * Similar to `useEventEmitterState`, but the callback is `async`. + * + * If the event is emitted while the callback is running, it will wait until + * after the callback completes before calling the callback again. If the event + * is emitted multiple times while the callback is running, the callback will be + * called once for each time the event was emitted, in the order that the events + * were emitted. + * + * @param emitter The emitter sending the event + * @param eventName Event name to listen for + * @param fn The callback function, that should return the state value. + * It should have the signature of the event callback, except that all + * parameters are optional. If the params are not set, a default value + * for the state should be returned. If the state value should not + * change from its previous value, the function can return a `NoChange` + * object. + * @param deps The dependencies of the callback function. + * @param initialValue The initial value of the state, before the callback finishes its initial run. + * @returns State + */ +export function useEventEmitterAsyncState>( + emitter: TypedEventEmitter | undefined, + eventName: string | symbol, + fn: Mapper>, + deps: DependencyList, + initialValue: T, +): T; +export function useEventEmitterAsyncState>( + emitter: TypedEventEmitter | undefined, + eventName: string | symbol, + fn: Mapper>, + deps: DependencyList, + initialValue?: T, +): T | undefined; +export function useEventEmitterAsyncState>( + emitter: TypedEventEmitter | undefined, + eventName: string | symbol, + fn: Mapper>, + deps: DependencyList, + initialValue?: T, +): T | undefined { + const [value, setValue] = useState(initialValue); + + let running = false; + // If the handler is called while it's already running, we remember the + // arguments that it was called with, and call the handler again when the + // first call is done. + const rerunArgs: any[] = []; + + const handler = useCallback( + (...args: any[]) => { + if (running) { + // We're already running, so remember the arguments we were + // called with, so that we can call the handler again when we're + // done. + rerunArgs.push(args); + return; + } + running = true; // eslint-disable-line react-hooks/exhaustive-deps + // Note: We need to use .then notation instead of async/await, + // because async/await would cause this function to return a + // promise, which `useEffect` doesn't like. + fn(...args) + .then((v) => { + if (!(v instanceof NoChange)) { + setValue(v); + } + }) + .finally(() => { + running = false; + if (rerunArgs.length != 0) { + handler(...rerunArgs.shift()); + } + }); + }, + [fn, ...deps], // eslint-disable-line react-compiler/react-compiler + ); + + // re-run when the emitter changes + useEffect(handler, [emitter, handler, ...deps]); + useEventEmitter(emitter, eventName, handler); + return value; +} + +/** + * Indicates that the callback for `useEventEmitterAsyncState` is not changing the value of the state. + */ +export class NoChange {} diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index 7c9773881e..e36b034b1e 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -801,6 +801,8 @@ "secret_storage_ready": "připraveno", "secret_storage_status": "Bezpečné úložiště:", "self_signing_private_key_cached_status": "Soukromý klíč s vlastním podpisem:", + "session": "Relace", + "session_fingerprint": "Otisk prstu (klíč relace)", "title": "Koncové šifrování", "user_signing_private_key_cached_status": "Podpisový soukromý klíč uživatele:" }, @@ -826,6 +828,7 @@ "low_bandwidth_mode": "Režim malé šířky pásma", "low_bandwidth_mode_description": "Vyžaduje kompatibilní domovský server.", "main_timeline": "Hlavní časová osa", + "manual_device_verification": "Ruční ověření zařízení", "no_receipt_found": "Žádné potvrzení o přečtení", "notification_state": "Stav oznámení je %(notificationState)s", "notifications_debug": "Ladění oznámení", @@ -967,7 +970,6 @@ }, "reset_all_button": "Zapomněli nebo ztratili jste všechny metody obnovy? Resetovat vše", "set_up_recovery": "Nastavení obnovení", - "set_up_recovery_later": "Teď ne", "set_up_recovery_toast_description": "Vygenerujte klíč pro obnovení, který lze použít k obnovení historie šifrovaných zpráv v případě, že ztratíte přístup k zařízením.", "set_up_toast_description": "Zabezpečení proti ztrátě přístupu k šifrovaným zprávám a datům", "set_up_toast_title": "Nastavení zabezpečené zálohy", @@ -1010,6 +1012,21 @@ "incoming_sas_dialog_waiting": "Čekání na potvrzení partnerem…", "incoming_sas_user_dialog_text_1": "Po ověření bude uživatel označen jako důvěryhodný. Ověřování uživatelů vám dává větší jistotu, že je komunikace důvěrná.", "incoming_sas_user_dialog_text_2": "Ověření uživatele označí jeho relace za důvěryhodné a vaše relace budou důvěryhodné pro něj.", + "manual": { + "already_verified": "Toto zařízení je již ověřeno", + "already_verified_and_wrong_fingerprint": "Zadaný otisk prstu se neshoduje, ale zařízení je již ověřené!", + "device_id": "ID zařízení", + "failure_description": "Nepodařilo se ověřit '%(deviceId)s': %(error)s", + "failure_title": "Ověření se nezdařilo", + "fingerprint": "Otisk prstu (klíč relace)", + "no_crypto": "Nelze ověřit zařízení - šifrování není povoleno", + "no_device": "Nelze ověřit zařízení - zařízení '%(deviceId)s' nebylo nalezeno", + "no_userid": "Nelze ověřit zařízení - nelze najít naše ID uživatele", + "success_description": "Zařízení (%(deviceId)s) je nyní křížově podepsáno", + "success_title": "Ověření proběhlo úspěšně", + "text": "Zadejte ID a otisk prstu jednoho ze svých vlastních zařízení a ověřte jej. POZNÁMKA to umožňuje druhému zařízení odesílat a přijímat zprávy jako vy. POKUD VÁM NĚKDO ŘEKL, ABYSTE SEM NĚCO VLOŽILI, JE PRAVDĚPODOBNÉ, ŽE JSTE PODVEDENI!", + "wrong_fingerprint": "Nelze ověřit zařízení '%(deviceId)s' - zadaný otisk prstu '%(fingerprint)s' neodpovídá otisku prstu zařízení, '%(fprint)s'" + }, "no_key_or_device": "Vypadá to, že nemáte klíč pro obnovení ani žádné jiné zařízení, které byste mohli ověřit. Toto zařízení nebude mít přístup ke starým zašifrovaným zprávám. Abyste mohli na tomto zařízení ověřit svou identitu, budete muset obnovit ověřovací klíče.", "no_support_qr_emoji": "Zařízení, které se snažíte ověřit, neumožňuje ověření QR kódem ani pomocí emotikonů, které %(brand)s podporuje. Zkuste použít jiného klienta.", "other_party_cancelled": "Druhá strana ověření zrušila.", @@ -2052,6 +2069,7 @@ "read_topic": "Klikněte pro přečtení tématu", "rejecting": "Odmítání pozvánky…", "rejoin_button": "Znovu vstoupit", + "room_content": "Obsah místnosti", "room_is_low_priority": "Toto je místnost s nízkou prioritou", "search": { "all_rooms_button": "Vyhledávat ve všech místnostech", @@ -2103,6 +2121,7 @@ "add_space_label": "Přidat prostor", "breadcrumbs_empty": "Žádné nedávno navštívené místnosti", "breadcrumbs_label": "Nedávno navštívené místnosti", + "collapse_filters": "Sbalit seznam filtrů", "empty": { "no_chats": "Zatím žádné chaty", "no_chats_description": "Začněte tím, že někomu pošlete zprávu nebo vytvoříte místnost", @@ -2110,6 +2129,7 @@ "no_favourites": "Zatím nemáte oblíbený chat", "no_favourites_description": "Chat si můžete přidat do oblíbených v nastavení chatu", "no_invites": "Nemáte žádné nepřečtené pozvánky", + "no_lowpriority": "Nemáte žádné místnosti s nízkou prioritou", "no_mentions": "Nemáte žádné nepřečtené zmínky", "no_people": "Zatím s nikým nemáte přímé chaty", "no_people_description": "Můžete zrušit výběr filtrů, abyste viděli ostatní chaty", @@ -2119,6 +2139,7 @@ "show_activity": "Zobrazit veškerou aktivitu", "show_chats": "Zobrazit všechny chaty" }, + "expand_filters": "Rozbalit seznam filtrů", "failed_add_tag": "Nepodařilo se přidat štítek %(tagName)s k místnosti", "failed_remove_tag": "Nepodařilo se odstranit štítek %(tagName)s z místnosti", "failed_set_dm_tag": "Nepodařilo se nastavit značku přímé zprávy", @@ -3101,6 +3122,8 @@ "jumptodate": "Přejít na zadané datum na časové ose", "jumptodate_invalid_input": "Nebyli jsme schopni porozumět zadanému datu (%(inputDate)s). Zkuste použít formát RRRR-MM-DD.", "lenny": "Vloží ( ͡° ͜ʖ ͡°) na začátek zprávy", + "manual_device_verification_confirm_description": "To umožní jinému zařízení odesílat a přijímat zprávy jako vy. POKUD VÁM NĚKDO ŘEKL, ABYSTE SEM NĚCO VLOŽILI, JE PRAVDĚPODOBNÉ, ŽE JSTE PODVEDENI! Opravdu chcete ověřit toto další zařízení?", + "manual_device_verification_confirm_title": "Upozornění: ruční ověření zařízení", "me": "Zobrazí akci", "msg": "Pošle zprávu danému uživateli", "myavatar": "Změní váš profilový obrázek ve všech místnostech", diff --git a/src/i18n/strings/cy.json b/src/i18n/strings/cy.json index 2252975325..59aa9f1619 100644 --- a/src/i18n/strings/cy.json +++ b/src/i18n/strings/cy.json @@ -962,7 +962,6 @@ }, "reset_all_button": "Wedi anghofio neu golli pob dull adfer? Ailosod y cyfan", "set_up_recovery": "Gosod adfer", - "set_up_recovery_later": "Nid nawr", "set_up_recovery_toast_description": "Cynhyrchwch allwedd adfer y mae modd ei defnyddio i adfer hanes eich neges wedi'i hamgryptio rhag ofn i chi golli mynediad i'ch dyfeisiau.", "set_up_toast_description": "Diogelu rhag colli mynediad i negeseuon a data wedi'u hamgryptio", "set_up_toast_title": "Gosod Copi Wrth Gefn Diogel", diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 602b3f22f5..ff8070250b 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -964,7 +964,6 @@ }, "reset_all_button": "Hast du alle Wiederherstellungsmethoden vergessen? Setze sie hier zurück", "set_up_recovery": "Wiederherstellung einrichten", - "set_up_recovery_later": "Nicht jetzt", "set_up_recovery_toast_description": "Generieren Sie einen Wiederherstellungsschlüssel, damit Sie Ihren verschlüsselten Nachrichtenverlauf wiederherstellen können, falls Sie den Zugriff auf Ihre Geräte verlieren.", "set_up_toast_description": "Schütze dich vor dem Verlust verschlüsselter Nachrichten und Daten", "set_up_toast_title": "Schlüsselsicherung einrichten", diff --git a/src/i18n/strings/el.json b/src/i18n/strings/el.json index 70efbb20e9..218eff3188 100644 --- a/src/i18n/strings/el.json +++ b/src/i18n/strings/el.json @@ -50,6 +50,8 @@ "download": "Λήψη", "edit": "Επεξεργασία", "enable": "Ενεργοποίηση", + "enter_fullscreen": "Είσοδος σε πλήρη οθόνη", + "exit_fullscreeen": "Έξοδος από την πλήρη οθόνη", "expand": "Επέκταση", "explore_public_rooms": "Εξερευνήστε δημόσιες αίθουσες", "explore_rooms": "Εξερευνήστε αίθουσες", @@ -149,10 +151,12 @@ "shared_data_heading": "Οποιοδήποτε από τα ακόλουθα δεδομένα μπορεί να κοινοποιηθεί:" }, "auth": { + "3pid_in_use": "Αυτή η διεύθυνση ηλεκτρονικού ταχυδρομείου ή ο αριθμός τηλεφώνου χρησιμοποιείται ήδη.", "account_clash": "Ο νέος λογαριασμός σας (%(newAccountId)s) έχει εγγραφεί, αλλά έχετε ήδη συνδεθεί με διαφορετικό λογαριασμό (%(loggedInUserId)s).", "account_clash_previous_account": "Συνέχεια με τον προηγούμενο λογαριασμό", "account_deactivated": "Αυτός ο λογαριασμός έχει απενεργοποιηθεί.", "autodiscovery_generic_failure": "Απέτυχε η λήψη της διαμόρφωσης αυτόματης ανακάλυψης από τον διακομιστή", + "autodiscovery_hs_incompatible": "Ο αρχικός σας διακομιστής είναι πολύ παλιός και δεν υποστηρίζει την ελάχιστη απαιτούμενη έκδοση API. Παρακαλούμε επικοινωνήστε με τον ιδιοκτήτη του διακομιστή σας ή αναβαθμίστε τον διακομιστή σας.", "autodiscovery_invalid": "Μη έγκυρη απόκριση εντοπισμού κεντρικού διακομιστή", "autodiscovery_invalid_hs": "Η διεύθυνση URL του κεντρικού διακομιστή δε φαίνεται να αντιστοιχεί σε έγκυρο διακομιστή Matrix", "autodiscovery_invalid_hs_base_url": "Μη έγκυρο base_url για m.homeserver", @@ -160,6 +164,7 @@ "autodiscovery_invalid_is_base_url": "Μη έγκυρο base_url για m.identity_server", "autodiscovery_invalid_is_response": "Μη έγκυρη απόκριση εντοπισμού διακομιστή ταυτότητας", "autodiscovery_invalid_json": "Μη έγκυρο JSON", + "autodiscovery_no_well_known": "Δεν βρέθηκε αρχείο .well-known JSON", "autodiscovery_unexpected_error_hs": "Μη αναμενόμενο σφάλμα κατά την επίλυση της διαμόρφωσης του κεντρικού διακομιστή", "autodiscovery_unexpected_error_is": "Μη αναμενόμενο σφάλμα κατά την επίλυση της διαμόρφωσης διακομιστή ταυτότητας", "captcha_description": "Αυτός ο κεντρικός διακομιστής θα ήθελε να βεβαιωθεί ότι δεν είστε ρομπότ.", @@ -168,8 +173,14 @@ "change_password_confirm_label": "Επιβεβαίωση κωδικού πρόσβασης", "change_password_current_label": "Τωρινός κωδικός πρόσβασης", "change_password_empty": "Οι κωδικοί πρόσβασης δεν γίνετε να είναι κενοί", + "change_password_error": "Σφάλμα κατά την αλλαγή κωδικού πρόσβασης: %(error)s", "change_password_mismatch": "Οι νέοι κωδικοί πρόσβασης είναι διαφορετικοί", "change_password_new_label": "Νέος κωδικός πρόσβασης", + "check_email_explainer": "Ακολουθήστε τις οδηγίες που στάλθηκαν στο %(email)s", + "check_email_resend_prompt": "Δεν το λάβατε;", + "check_email_resend_tooltip": "Επανεστάλη email με τον συνδέσμο επαλήθευσης!", + "check_email_wrong_email_button": "Εισαγάγετε ξανά τη διεύθυνση ηλεκτρονικού ταχυδρομείου", + "check_email_wrong_email_prompt": "Λάθος διεύθυνση ηλεκτρονικού ταχυδρομείου;", "continue_with_idp": "Συνεχίστε με %(provider)s", "continue_with_sso": "Συνέχεια με %(ssoButtons)s", "country_dropdown": "Αναπτυσσόμενο μενού Χώρας", @@ -181,6 +192,8 @@ "email_field_label_required": "Εισάγετε διεύθυνση email", "email_help_text": "Προσθέστε ένα email για να μπορείτε να κάνετε επαναφορά του κωδικού πρόσβασης σας.", "email_phone_discovery_text": "Χρησιμοποιήστε email ή τηλέφωνο για να είστε προαιρετικά ανιχνεύσιμος από υπάρχουσες επαφές.", + "enter_email_explainer": "Το %(homeserver)s θα σας στείλει έναν σύνδεσμο επαλήθευσης για να επαναφέρετε τον κωδικό πρόσβασής σας.", + "enter_email_heading": "Εισάγετε το email σας για να επαναφέρετε τον κωδικό πρόσβασης", "failed_connect_identity_server": "Δεν είναι δυνατή η πρόσβαση στον διακομιστή ταυτότητας", "failed_connect_identity_server_other": "Μπορείτε να συνδεθείτε, αλλά ορισμένες λειτουργίες δε θα είναι διαθέσιμες μέχρι να συνδεθεί ξανά ο διακομιστής ταυτότητας. Εάν εξακολουθείτε να βλέπετε αυτήν την προειδοποίηση, ελέγξτε τις ρυθμίσεις σας ή επικοινωνήστε με έναν διαχειριστή του διακομιστή σας.", "failed_connect_identity_server_register": "Μπορείτε να εγγραφείτε, αλλά ορισμένες λειτουργίες δεν θα είναι διαθέσιμες μέχρι να συνδεθεί ξανά ο διακομιστής ταυτότητας. Εάν εξακολουθείτε να βλέπετε αυτήν την ειδοποίηση, ελέγξτε τις ρυθμίσεις σας ή επικοινωνήστε με έναν διαχειριστή διακομιστή.", @@ -192,6 +205,7 @@ "forgot_password_email_invalid": "Η διεύθυνση email δε φαίνεται να είναι έγκυρη.", "forgot_password_email_required": "Πρέπει να εισηχθεί η διεύθυνση ηλ. αλληλογραφίας που είναι συνδεδεμένη με τον λογαριασμό σας.", "forgot_password_prompt": "Ξεχάσετε τον κωδικό σας;", + "forgot_password_send_email": "Αποστολή email", "identifier_label": "Συνδεθείτε με", "incorrect_credentials": "Λανθασμένο όνομα χρήστη και/ή κωδικός.", "incorrect_credentials_detail": "Σημειώστε ότι συνδέεστε στον διακομιστή %(hs)s, όχι στο matrix.org.", @@ -202,6 +216,7 @@ "megolm_export": "Μη αυτόματη εξαγωγή κλειδιών", "setup_key_backup_title": "Θα χάσετε την πρόσβαση στα κρυπτογραφημένα μηνύματά σας", "setup_secure_backup_description_1": "Τα κρυπτογραφημένα μηνύματα προστατεύονται με κρυπτογράφηση από άκρο σε άκρο. Μόνο εσείς και οι παραλήπτες έχετε τα κλειδιά για να διαβάσετε αυτά τα μηνύματα.", + "setup_secure_backup_description_2": "Όταν αποσυνδεθείτε, αυτά τα κλειδιά θα διαγραφούν από αυτήν τη συσκευή, πράγμα που σημαίνει ότι δεν θα μπορείτε να διαβάσετε κρυπτογραφημένα μηνύματα, εκτός εάν έχετε τα κλειδιά για αυτά στις άλλες συσκευές σας ή έχετε δημιουργήσει αντίγραφα ασφαλείας τους στον διακομιστή.", "skip_key_backup": "Δε θέλω τα κρυπτογραφημένα μηνύματά μου", "use_key_backup": "Ξεκινήστε να χρησιμοποιείτε το αντίγραφο ασφαλείας κλειδιού" }, @@ -216,11 +231,20 @@ "error_title": "Δεν μπορέσαμε να σας συνδέσουμε", "missing_or_invalid_stored_state": "Ζητήσαμε από το πρόγραμμα περιήγησης να θυμάται τον διακομιστή που χρησιμοποιείτε για να συνδέεστε, αλλά το πρόγραμμα περιήγησης δεν το έχει αποθηκεύσει. Πηγαίνετε στην σελίδα σύνδεσεις για να προσπαθήσετε ξανά." }, + "password_field_keep_going_prompt": "Συνεχίστε...", "password_field_label": "Εισάγετε τον κωδικό πρόσβασης", "password_field_strong_label": "Πολύ καλά, ισχυρός κωδικός πρόσβασης!", "password_field_weak_label": "Έγκυρος κωδικός πρόσβασης, αλλά δεν είναι ασφαλής", "phone_label": "Τηλέφωνο", "phone_optional_label": "Τηλέφωνο (προαιρετικό)", + "qr_code_login": { + "completing_setup": "Ολοκλήρωση της ρύθμισης της νέας συσκευής σας", + "error_unexpected": "Παρουσιάστηκε μη αναμενόμενο σφάλμα. Το αίτημα σύνδεσης της άλλης συσκευής σας ακυρώθηκε.", + "scan_code_instruction": "Σαρώστε τον κωδικό QR με άλλη συσκευή", + "scan_qr_code": "Συνδεθείτε με κωδικό QR", + "select_qr_code": "Επιλέξτε \"%(scanQRCode)s\"", + "waiting_for_device": "Αναμονή για σύνδεση της συσκευής" + }, "register_action": "Δημιουργία Λογαριασμού", "registration": { "continue_without_email_description": "Μια προειδοποίηση, αν δεν προσθέσετε ένα email και ξεχάσετε τον κωδικό πρόσβασης, ενδέχεται να χάσετε οριστικά την πρόσβαση στον λογαριασμό σας.", @@ -234,14 +258,25 @@ "registration_username_unable_check": "Δεν είναι δυνατός ο έλεγχος εάν το όνομα χρήστη είναι διαθέσιμο. Δοκιμάστε ξανά αργότερα.", "registration_username_validation": "Χρησιμοποιήστε μόνο πεζά γράμματα, αριθμούς, παύλες και κάτω παύλες", "reset_password": { + "confirm_new_password": "Επιβεβαίωση νέου κωδικού πρόσβασης", + "devices_logout_success": "Έχετε αποσυνδεθεί από όλες τις συσκευές και δεν θα λαμβάνετε πλέον ειδοποιήσεις push. Για να ενεργοποιήσετε ξανά τις ειδοποιήσεις, συνδεθείτε ξανά σε κάθε συσκευή.", + "other_devices_logout_warning_1": "Η αποσύνδεση των συσκευών σας θα διαγράψει τα κλειδιά κρυπτογράφησης μηνυμάτων που είναι αποθηκευμένα σε αυτές, καθιστώντας το κρυπτογραφημένο ιστορικό συνομιλιών μη αναγνώσιμο.", + "other_devices_logout_warning_2": "Αν θέλετε να διατηρήσετε πρόσβαση στο ιστορικό των συνομιλιών σας σε κρυπτογραφημένες αίθουσες, ρυθμίστε τη δημιουργία αντιγράφων ασφαλείας κλειδιών ή εξάγετε τα κλειδιά των μηνυμάτων σας από μία από τις άλλες συσκευές σας πριν προχωρήσετε.", "password_not_entered": "Ο νέος κωδικός πρόσβασης πρέπει να εισαχθεί.", "passwords_mismatch": "Οι νέοι κωδικοί πρόσβασης πρέπει να ταιριάζουν.", + "rate_limit_error": "Πάρα πολλές προσπάθειες σε σύντομο χρονικό διάστημα. Περιμένετε λίγο πριν προσπαθήσετε ξανά.", + "rate_limit_error_with_time": "Πάρα πολλές προσπάθειες σε σύντομο χρονικό διάστημα. Δοκιμάστε ξανά μετά από %(timeout)s.", "reset_successful": "Ο κωδικός πρόσβασής σας επαναφέρθηκε.", - "return_to_login": "Επιστροφή στην οθόνη σύνδεσης" + "return_to_login": "Επιστροφή στην οθόνη σύνδεσης", + "sign_out_other_devices": "Αποσύνδεση από όλες τις συσκευές" }, + "reset_password_action": "Επαναφορά κωδικού πρόσβασης", + "reset_password_button": "Ξέχασες τον κωδικό πρόσβασης;", "reset_password_email_field_description": "Χρησιμοποιήστε μια διεύθυνση email για να ανακτήσετε τον λογαριασμό σας", "reset_password_email_field_required_invalid": "Εισαγάγετε τη διεύθυνση email (απαιτείται σε αυτόν τον κεντρικό διακομιστή)", + "reset_password_email_not_associated": "Η διεύθυνση ηλεκτρονικού ταχυδρομείου σας δεν φαίνεται να σχετίζεται με κάποιο αναγνωριστικό Matrix σε αυτόν τον αρχικό διακομιστή.", "reset_password_email_not_found_title": "Δεν βρέθηκε η διεύθυνση ηλ. αλληλογραφίας", + "reset_password_title": "Επαναφέρετε τον κωδικό πρόσβασής σας", "server_picker_custom": "Άλλος κεντρικός διακομιστής", "server_picker_description": "Μπορείτε να χρησιμοποιήσετε τις προσαρμοσμένες επιλογές διακομιστή για να συνδεθείτε σε άλλους διακομιστές Matrix, καθορίζοντας μια διαφορετική διεύθυνση URL του κεντρικού διακομιστή. Αυτό σας επιτρέπει να χρησιμοποιείτε το %(brand)s με έναν υπάρχοντα λογαριασμό Matrix σε διαφορετικό τοπικό διακομιστή.", "server_picker_description_matrix.org": "Συμμετέχετε δωρεάν στον μεγαλύτερο δημόσιο διακομιστή", @@ -264,11 +299,14 @@ "verification_pending_title": "Εκκρεμεί επιβεβαίωση" }, "set_email_prompt": "Θέλετε να ορίσετε μια διεύθυνση ηλεκτρονικής αλληλογραφίας;", + "sign_in_description": "Χρησιμοποιήστε τον λογαριασμό σας για να συνεχίσετε.", + "sign_in_instead": "Συνδεθείτε αντ' αυτού", "sign_in_instead_prompt": "Έχετε ήδη λογαριασμό; Συνδεθείτε εδώ", "sign_in_or_register": "Συνδεθείτε ή Δημιουργήστε Λογαριασμό", "sign_in_or_register_description": "Χρησιμοποιήστε τον λογαριασμό σας ή δημιουργήστε νέο για να συνεχίσετε.", "sign_in_prompt": "Έχετε λογαριασμό; Συνδεθείτε", "sign_in_with_sso": "Συνδεθείτε με απλή σύνδεση", + "signing_in": "Σύνδεση...", "soft_logout": { "clear_data_button": "Εκκαθάριση όλων των δεδομένων", "clear_data_description": "Η εκκαθάριση όλων των δεδομένων από αυτήν τη συνεδρία είναι μόνιμη. Τα κρυπτογραφημένα μηνύματα θα χαθούν εκτός εάν έχουν δημιουργηθεί αντίγραφα ασφαλείας των κλειδιών τους.", @@ -279,19 +317,27 @@ "soft_logout_intro_sso": "Συνδεθείτε και αποκτήστε ξανά πρόσβαση στον λογαριασμό σας.", "soft_logout_intro_unsupported_auth": "Δεν μπορείτε να συνδεθείτε στον λογαριασμό σας. Επικοινωνήστε με τον διαχειριστή του κεντρικού διακομιστή σας για περισσότερες πληροφορίες.", "soft_logout_subheading": "Εκκαθάριση προσωπικών δεδομένων", + "soft_logout_warning": "Προειδοποίηση: τα προσωπικά σας δεδομένα (συμπεριλαμβανομένων των κλειδιών κρυπτογράφησης) εξακολουθούν να είναι αποθηκευμένα σε αυτήν την συνεδρία. Διαγράψτε τα εάν ολοκληρώσατε τη χρήση αυτής της συνεδρίας ή θέλετε να συνδεθείτε σε άλλον λογαριασμό.", + "sso": "Ενιαία Σύνδεση", "sso_complete_in_browser_dialog_title": "Μεταβείτε στο πρόγραμμα περιήγησής σας για να ολοκληρώσετε τη σύνδεση", "sso_failed_missing_storage": "Ζητήσαμε από το πρόγραμμα περιήγησης να θυμάται τον διακομιστή που χρησιμοποιείτε για να συνδέεστε, αλλά το πρόγραμμα περιήγησης δεν το έχει αποθηκεύσει. Πηγαίνετε στην σελίδα σύνδεσεις για να προσπαθήσετε ξανά.", "sso_or_username_password": "%(ssoButtons)s Ή %(usernamePassword)s", "sync_footer_subtitle": "Εάν έχετε συμμετάσχει σε πολλές αίθουσες, αυτό μπορεί να διαρκέσει λίγο", + "syncing": "Συγχρονισμός...", "uia": { "code": "Κωδικός", + "email": "Για να δημιουργήσετε τον λογαριασμό σας, ανοίξτε τον σύνδεσμο στο email που μόλις στείλαμε στο %(emailAddress)s.", "email_auth_header": "Ελέγξτε το email σας για να συνεχίσετε", + "email_resend_prompt": "Δεν το λάβατε; Στείλτε το ξανά", + "email_resent": "Επαναστάλθηκε!", "fallback_button": "Έναρξη πιστοποίησης", "msisdn": "Ένα μήνυμα κειμένου έχει σταλεί στη διεύθυνση %(msisdn)s", "msisdn_token_incorrect": "Εσφαλμένο διακριτικό", "msisdn_token_prompt": "Παρακαλούμε εισάγετε τον κωδικό που περιέχει:", "password_prompt": "Ταυτοποιηθείτε εισάγοντας παρακάτω τον κωδικό πρόσβασης του λογαριασμού σας.", "recaptcha_missing_params": "Λείπει το δημόσιο κλειδί captcha από τη διαμόρφωση του κεντρικού διακομιστή. Αναφέρετε αυτό στον διαχειριστή του.", + "registration_token_label": "Διακριτικό εγγραφής", + "registration_token_prompt": "Εισάγετε ένα διακριτικό εγγραφής που παρέχεται από τον διαχειριστή του αρχικού διακομιστή.", "sso_body": "Επιβεβαιώστε την προσθήκη αυτής της διεύθυνσης ηλ. ταχυδρομείου με την χρήση Single Sign On για να επικυρώσετε την ταυτότητα σας.", "sso_failed": "Κάτι πήγε στραβά στην επιβεβαίωση της ταυτότητάς σας. Ακυρώστε και δοκιμάστε ξανά.", "sso_postauth_body": "Κλικ στο κουμπί παρακάτω για να επιβεβαιώσετε την ταυτότητά σας.", @@ -301,10 +347,13 @@ "terms": "Παρακαλώ διαβάστε και αποδεχτείτε όλες τις πολιτικές αυτού του κεντρικού διακομιστή:", "terms_invalid": "Παρακαλώ διαβάστε και αποδεχτείτε όλες τις πολιτικές του κεντρικού διακομιστή" }, + "unsupported_auth": "Αυτός ο αρχικός διακομιστής δεν προσφέρει καμία ροή σύνδεσης που υποστηρίζεται από αυτήν την εφαρμογή.", "unsupported_auth_email": "Αυτός ο κεντρικός διακομιστής δεν υποστηρίζει σύνδεση με χρήση διεύθυνσης email.", "unsupported_auth_msisdn": "Αυτός ο διακομιστής δεν υποστηρίζει πιστοποίηση με αριθμό τηλεφώνου.", "username_field_required_invalid": "Εισάγετε όνομα χρήστη", - "username_in_use": "Κάποιος έχει ήδη αυτό το όνομα χρήστη, δοκιμάστε ένα άλλο." + "username_in_use": "Κάποιος έχει ήδη αυτό το όνομα χρήστη, δοκιμάστε ένα άλλο.", + "verify_email_explainer": "Πρέπει να γνωρίζουμε ότι είστε εσείς πριν επαναφέρουμε τον κωδικό πρόσβασής σας. Κάντε κλικ στον σύνδεσμο στο email που μόλις στείλαμε στη διεύθυνση %(email)s", + "verify_email_heading": "Επαληθεύστε το email σας για να συνεχίσετε" }, "bug_reporting": { "additional_context": "Εάν υπάρχουν πρόσθετες πληροφορίες που θα βοηθούσαν στην ανάλυση του ζητήματος, όπως τι κάνατε εκείνη τη στιγμή, αναγνωριστικά αιθουσών, αναγνωριστικά χρηστών κ.λπ., συμπεριλάβετέ τα εδώ.", @@ -332,6 +381,7 @@ "uploading_logs": "Μεταφόρτωση αρχείων καταγραφής", "waiting_for_server": "Αναμονή απάντησης από τον διακομιστή" }, + "cannot_invite_without_identity_server": "Δεν είναι δυνατή η πρόσκληση χρήστη μέσω email χωρίς διακομιστή ταυτότητας. Μπορείτε να συνδεθείτε σε έναν από τις \"Ρυθμίσεις\".", "cannot_reach_homeserver": "Δεν είναι δυνατή η πρόσβαση στον κεντρικό διακομιστή", "cannot_reach_homeserver_detail": "Βεβαιωθείτε ότι έχετε σταθερή σύνδεση στο διαδίκτυο ή επικοινωνήστε με τον διαχειριστή του διακομιστή", "cant_load_page": "Δεν ήταν δυνατή η φόρτωση της σελίδας", @@ -362,6 +412,7 @@ "are_you_sure": "Είστε σίγουροι;", "attachment": "Επισύναψη", "authentication": "Πιστοποίηση", + "avatar": "Εικόνα Προφίλ", "beta": "Beta", "camera": "Κάμερα", "cameras": "Κάμερες", @@ -444,6 +495,7 @@ "room": "Αίθουσα", "room_name": "Όνομα αίθουσας", "rooms": "Αίθουσες", + "saving": "Γίνεται αποθήκευση...", "secure_backup": "Ασφαλές αντίγραφο ασφαλείας", "select_all": "Επιλογή όλων", "server": "Διακομιστής", @@ -463,6 +515,7 @@ "thread": "Νήμα", "threads": "Νήμα εκτέλεσης", "timeline": "Χρονολόγιο", + "unavailable": "μη διαθέσιμο", "unencrypted": "Μη κρυπτογραφημένο", "unmute": "Άρση σίγασης", "unnamed_room": "Ανώνυμη αίθουσα", @@ -532,6 +585,7 @@ "create_room": { "action_create_room": "Δημιουργία δωματίου", "action_create_video_room": "Δημιουργία δωματίου βίντεο", + "encrypted_video_room_warning": "Δεν μπορείτε να το απενεργοποιήσετε αργότερα. Η αίθουσα θα κρυπτογραφηθεί, αλλά η ενσωματωμένη κλήση όχι.", "encrypted_warning": "Δεν μπορείτε να το απενεργοποιήσετε αργότερα. Οι γέφυρες και τα περισσότερα ρομπότ δεν μπορούν να λειτουργήσουν ακόμα.", "encryption_forced": "Ο διακομιστής σας απαιτεί την ενεργοποίηση της κρυπτογράφησης σε ιδιωτικά δωμάτια.", "encryption_label": "Ενεργοποίηση κρυπτογράφησης από άκρο-σε-άκρο", @@ -540,6 +594,7 @@ "join_rule_change_notice": "Μπορείτε να το αλλάξετε ανά πάσα στιγμή από τις ρυθμίσεις δωματίου.", "join_rule_invite": "Ιδιωτικό δωμάτιο (μόνο με πρόσκληση)", "join_rule_invite_label": "Μόνο τα άτομα που έχουν προσκληθεί θα μπορούν να βρουν και να εγγραφούν σε αυτό τον δωμάτιο.", + "join_rule_knock_label": "Οποιοσδήποτε μπορεί να ζητήσει να συμμετάσχει, αλλά οι διαχειριστές ή οι συντονιστές πρέπει να χορηγήσουν πρόσβαση. Μπορείτε να το αλλάξετε αργότερα.", "join_rule_public_label": "Οποιοσδήποτε θα μπορεί να βρει και να εγγραφεί σε αυτό το δωμάτιο.", "join_rule_public_parent_space_label": "Οποιοσδήποτε θα μπορεί να βρει και να εγγραφεί σε αυτόν τον χώρο, όχι μόνο μέλη του .", "join_rule_restricted": "Ορατό στα μέλη του χώρου", @@ -711,13 +766,14 @@ "cancel_search_label": "Ακύρωση αναζήτησης" }, "empty_room": "Άδειο δωμάτιο", + "empty_room_was_name": "Άδεια αίθουσα (ήταν %(oldName)s)", "encryption": { "access_secret_storage_dialog": { "key_validation_text": { - "wrong_security_key": "Λάθος Κλειδί Ασφαλείας" + "wrong_security_key": "Το κλειδί ανάκτησης που εισαγάγατε δεν είναι σωστό." }, "restoring": "Επαναφορά κλειδιών από αντίγραφο ασφαλείας", - "security_key_title": "Κλειδί Ασφαλείας" + "security_key_title": "Κλειδί Ανάκτησης" }, "bootstrap_title": "Ρύθμιση κλειδιών", "confirm_encryption_setup_body": "Κάντε κλικ στο κουμπί παρακάτω για να επιβεβαιώσετε τη ρύθμιση της κρυπτογράφησης.", @@ -730,6 +786,9 @@ "cross_signing_user_warning": "Αυτός ο χρήστης δεν έχει επαληθεύσει όλες τις συνεδρίες του.", "event_shield_reason_authenticity_not_guaranteed": "Η αυθεντικότητα αυτού του κρυπτογραφημένου μηνύματος δεν είναι εγγυημένη σε αυτήν τη συσκευή.", "event_shield_reason_mismatched_sender_key": "Κρυπτογραφήθηκε από μια μη επαληθευμένη συνεδρία", + "event_shield_reason_unknown_device": "Κρυπτογραφημένο από άγνωστη ή διαγραμμένη συσκευή.", + "event_shield_reason_unsigned_device": "Κρυπτογραφημένο από συσκευή που δεν έχει επαληθευτεί από τον κάτοχό της.", + "event_shield_reason_unverified_identity": "Κρυπτογραφημένο από μη επαληθευμένο χρήστη.", "export_unsupported": "Ο περιηγητής σας δεν υποστηρίζει τα απαιτούμενα πρόσθετα κρυπτογράφησης", "import_invalid_keyfile": "Μη έγκυρο αρχείο κλειδιού %(brand)s", "import_invalid_passphrase": "Αποτυχία ελέγχου πιστοποίησης: λανθασμένος κωδικός πρόσβασης;", @@ -760,6 +819,7 @@ "explainer": "Δημιουργήστε αντίγραφα ασφαλείας των κλειδιών σας πριν αποσυνδεθείτε για να μην τα χάσετε." }, "udd": { + "interactive_verification_button": "Διαδραστική επαλήθευση με emoji", "other_ask_verify_text": "Ζητήστε από αυτόν τον χρήστη να επιβεβαιώσει την συνεδρία του, ή επιβεβαιώστε την χειροκίνητα παρακάτω.", "other_new_session_text": "Ο %(name)s (%(userId)s) συνδέθηκε σε μία νέα συνεδρία χωρίς να την επιβεβαιώσει:", "own_ask_verify_text": "Επιβεβαιώστε την άλλη σας συνεδρία χρησιμοποιώντας μία από τις παρακάτω επιλογές.", @@ -782,14 +842,17 @@ "complete_action": "Κατανοώ", "complete_description": "Επαληθεύσατε με επιτυχία αυτόν τον χρήστη.", "complete_title": "Επαληθεύτηκε!", + "error_starting_description": "Δεν ήταν δυνατή η έναρξη συνομιλίας με τον άλλο χρήστη.", + "error_starting_title": "Σφάλμα κατά την έναρξη της επαλήθευσης", "explainer": "Τα ασφαλή μηνύματα με αυτόν τον χρήστη είναι κρυπτογραφημένα από άκρο σε άκρο και δεν μπορούν να διαβαστούν από τρίτους.", "in_person": "Για να είστε ασφαλείς, κάντε το αυτοπροσώπως ή χρησιμοποιήστε έναν αξιόπιστο τρόπο επικοινωνίας.", "incoming_sas_device_dialog_text_1": "Επαληθεύστε αυτήν τη συσκευή για να την επισημάνετε ως αξιόπιστη. Η εμπιστοσύνη αυτής της συσκευής προσφέρει σε εσάς και σε άλλους χρήστες επιπλέον ηρεμία όταν χρησιμοποιείτε μηνύματα με κρυπτογράφηση από άκρο σε άκρο.", "incoming_sas_device_dialog_text_2": "Η επαλήθευση αυτής της συσκευής θα την επισημάνει ως αξιόπιστη και οι χρήστες που έχουν επαληθευτεί μαζί σας θα εμπιστεύονται αυτήν τη συσκευή.", "incoming_sas_dialog_title": "Εισερχόμενο Αίτημα Επαλήθευσης", + "incoming_sas_dialog_waiting": "Αναμονή επιβεβαίωσης από τον συνεργάτη…", "incoming_sas_user_dialog_text_1": "Επαληθεύστε αυτόν τον χρήστη για να τον επισημάνετε ως αξιόπιστο. Η εμπιστοσύνη των χρηστών σάς προσφέρει επιπλέον ηρεμία όταν χρησιμοποιείτε μηνύματα με κρυπτογράφηση από άκρο σε άκρο.", "incoming_sas_user_dialog_text_2": "Η επαλήθευση αυτού του χρήστη θα επισημάνει τη συνεδρία του ως αξιόπιστη και θα επισημάνει επίσης τη συνεδρία σας ως αξιόπιστη σε αυτόν.", - "no_key_or_device": "Φαίνεται ότι δε διαθέτετε Κλειδί Ασφαλείας ή άλλες συσκευές με τις οποίες μπορείτε να επαληθεύσετε. Αυτή η συσκευή δε θα έχει πρόσβαση σε παλιά κρυπτογραφημένα μηνύματα. Για να επαληθεύσετε την ταυτότητά σας σε αυτήν τη συσκευή, θα πρέπει να επαναφέρετε τα κλειδιά επαλήθευσης.", + "no_key_or_device": "Φαίνεται ότι δεν έχετε Κλειδί Ανάκτησης ή άλλες συσκευές με τις οποίες μπορείτε να κάνετε επαλήθευση. Αυτή η συσκευή δεν θα έχει πρόσβαση σε παλιά κρυπτογραφημένα μηνύματα. Για να επαληθεύσετε την ταυτότητά σας σε αυτήν τη συσκευή, θα πρέπει να επαναφέρετε τα κλειδιά επαλήθευσης.", "no_support_qr_emoji": "Η συσκευή που προσπαθείτε να επαληθεύσετε δεν υποστηρίζει τη σάρωση κωδικού QR ή επαλήθευσης emoji, κάτι που υποστηρίζει το %(brand)s. Δοκιμάστε με διαφορετικό πρόγραμμα-πελάτη.", "other_party_cancelled": "Το άλλο μέρος ακύρωσε την επαλήθευση.", "prompt_encrypted": "Επαληθεύστε όλους τους χρήστες σε ένα δωμάτιο για να βεβαιωθείτε ότι είναι ασφαλές.", @@ -801,6 +864,8 @@ "qr_prompt": "Σαρώστε αυτόν τον μοναδικό κωδικό", "qr_reciprocate_same_shield_device": "Σχεδόν έτοιμοι! Εμφανίζεται η ίδια ασπίδα και στην άλλη συσκευή σας;", "qr_reciprocate_same_shield_user": "Σχεδόν έτοιμοι! Εμφανίζεται η ίδια ασπίδα και στον χρήστη %(displayName)s;", + "request_toast_accept": "Επαλήθευση Συνεδρίας", + "request_toast_decline_counter": "Παράβλεψη (%(counter)s)", "request_toast_detail": "%(deviceId)s από %(ip)s", "reset_proceed_prompt": "Προχωρήστε με την επαναφορά", "sas_caption_self": "Επαληθεύστε αυτήν τη συσκευή επιβεβαιώνοντας ότι ο ακόλουθος αριθμός εμφανίζεται στην οθόνη της.", @@ -820,10 +885,12 @@ "successful_user": "Επαληθεύσατε με επιτυχία τον χρήστη %(displayName)s!", "timed_out": "Η επαλήθευση έληξε.", "unsupported_method": "Δεν είναι δυνατή η εύρεση μιας υποστηριζόμενης μεθόδου επαλήθευσης.", + "unverified_session_toast_accept": "Ναι, ήμουν εγώ", "unverified_session_toast_title": "Νέα σύνδεση. Ήσουν εσύ;", "unverified_sessions_toast_description": "Ελέγξτε για να βεβαιωθείτε ότι ο λογαριασμός σας είναι ασφαλής", "unverified_sessions_toast_reject": "Αργότερα", - "verification_description": "Επαληθεύστε την ταυτότητά σας για να αποκτήσετε πρόσβαση σε κρυπτογραφημένα μηνύματα και να αποδείξετε την ταυτότητά σας σε άλλους.", + "unverified_sessions_toast_title": "Έχετε μη επαληθευμένες συνεδρίες", + "verification_description": "Επαληθεύστε την ταυτότητά σας για να αποκτήσετε πρόσβαση σε κρυπτογραφημένα μηνύματα και να αποδείξετε την ταυτότητά σας σε άλλους. Εάν χρησιμοποιείτε επίσης κινητή συσκευή, ανοίξτε την εφαρμογή εκεί πριν προχωρήσετε.", "verification_dialog_title_device": "Επαλήθευση άλλης συσκευής", "verification_dialog_title_user": "Αίτημα επαλήθευσης", "verification_skip_warning": "Χωρίς επαλήθευση, δε θα έχετε πρόσβαση σε όλα τα μηνύματά σας και ενδέχεται να φαίνεστε ως αναξιόπιστος στους άλλους.", @@ -834,8 +901,8 @@ "verify_emoji_prompt_qr": "Εάν δεν μπορείτε να σαρώσετε τον παραπάνω κώδικα, επαληθεύστε το συγκρίνοντας μοναδικά emoji.", "verify_later": "Θα επαληθεύσω αργότερα", "verify_using_device": "Επαλήθευση με άλλη συσκευή", - "verify_using_key": "Επαλήθευση με Κλειδί ασφαλείας", - "verify_using_key_or_phrase": "Επαλήθευση με Κλειδί Ασφαλείας ή Φράση Ασφαλείας", + "verify_using_key": "Επαλήθευση με Κλειδί Ανάκτησης", + "verify_using_key_or_phrase": "Επαλήθευση με Κλειδί ή Φράση Ανάκτησης", "waiting_for_user_accept": "Αναμονή αποδοχής από %(displayName)s…", "waiting_other_device": "Αναμονή για επαλήθευση στην άλλη συσκευή σας…", "waiting_other_device_details": "Αναμονή για επαλήθευση στην άλλη συσκευή σας, %(deviceName)s (%(deviceId)s)…", @@ -881,6 +948,7 @@ "unknown_error_code": "άγνωστος κωδικός σφάλματος", "update_power_level": "Δεν ήταν δυνατή η αλλαγή του επιπέδου δύναμης" }, + "error_database_closed_title": "Το %(brand)s σταμάτησε να λειτουργεί", "error_dialog": { "copy_room_link_failed": { "description": "Αδυναμία αντιγραφής στο πρόχειρο του συνδέσμου δωματίου.", @@ -889,6 +957,7 @@ "error_loading_user_profile": "Αδυναμία φόρτωσης του προφίλ χρήστη", "forget_room_failed": "Δεν ήταν δυνατή η διαγραφή του δωματίου (%(errCode)s)" }, + "error_user_not_logged_in": "Ο χρήστης δεν είναι συνδεδεμένος", "event_preview": { "m.call.answer": { "dm": "Κλήση σε εξέλιξη", @@ -1042,7 +1111,11 @@ "impossible_dialog_title": "Δεν επιτρέπονται πρόσθετα" }, "invite": { + "ask_anyway_description": "Δεν είναι δυνατή η εύρεση προφίλ για τα αναγνωριστικά Matrix που αναφέρονται παρακάτω - θα θέλατε να ξεκινήσετε μία συνομιλία ούτως ή άλλως;", + "ask_anyway_label": "Έναρξη συνομιλίας ούτως ή άλλως", + "ask_anyway_never_warn_label": "Έναρξη συνομιλίας ούτως ή άλλως και μην με προειδοποιήσετε ξανά", "email_caption": "Πρόσκληση μέσω email", + "email_limit_one": "Οι προσκλήσεις μέσω email μπορούν να αποστέλλονται μόνο μία κάθε φορά", "email_use_default_is": "Χρησιμοποιήστε έναν διακομιστή ταυτότητας για πρόσκληση μέσω email. Χρησιμοποιήστε τον προεπιλεγμένο (%(defaultIdentityServerName)s) ή διαμορφώστε στις Ρυθμίσεις.", "email_use_is": "Χρησιμοποιήστε έναν διακομιστή ταυτότητας για πρόσκληση μέσω email. Διαχείριση στις Ρυθμίσεις.", "error_already_invited_room": "Ο χρήστης έχει ήδη προσκληθεί στο δωμάτιο", @@ -1087,7 +1160,13 @@ "unable_find_profiles_description_default": "Δεν είναι δυνατή η εύρεση προφίλ για τα αναγνωριστικά Matrix που αναφέρονται παρακάτω - θα θέλατε να τα προσκαλέσετε ούτως ή άλλως;", "unable_find_profiles_invite_label_default": "Πρόσκληση ούτως ή άλλως", "unable_find_profiles_invite_never_warn_label_default": "Προσκαλέστε ούτως ή άλλως και μην με προειδοποιήσετε ποτέ ξανά", - "unable_find_profiles_title": "Οι παρακάτω χρήστες ενδέχεται να μην υπάρχουν" + "unable_find_profiles_title": "Οι παρακάτω χρήστες ενδέχεται να μην υπάρχουν", + "unban_first_title": "Δεν είναι δυνατή η πρόσκληση χρήστη μέχρι να αρθεί ο αποκλεισμός του" + }, + "inviting_user1_and_user2": "Πρόσκληση %(user1)s και %(user2)s", + "inviting_user_and_n_others": { + "one": "Πρόσκληση %(user)s και ενός άλλου", + "other": "Πρόσκληση %(user)s και %(count)s άλλων" }, "items_and_n_others": { "one": " και ένα ακόμα", @@ -1850,7 +1929,7 @@ }, "join_rule_upgrade_upgrading_room": "Αναβάθμιση δωματίου", "public_without_alias_warning": "Για να δημιουργήσετε σύνδεσμο σε αυτό το δωμάτιο, παρακαλώ προσθέστε μια διεύθυνση.", - "strict_encryption": "Μη στέλνετε ποτέ κρυπτογραφημένα μηνύματα σε μη επαληθευμένες συνεδρίες σε αυτό το δωμάτιο από αυτή τη συνεδρία", + "strict_encryption": "Αποστολή μηνυμάτων μόνο σε επαληθευμένους χρήστες.", "title": "Ασφάλεια & Απόρρητο" }, "title": "Ρυθμίσεις Δωματίου - %(roomName)s", @@ -1880,7 +1959,9 @@ "error_power_level_invalid": "Το επίπεδο δύναμης πρέπει να είναι ένας θετικός ακέραιος.", "error_room_not_visible": "Η αίθουσα %(roomId)s δεν είναι ορατή", "error_room_unknown": "Αυτό το δωμάτιο δεν αναγνωρίζεται.", - "error_send_request": "Δεν ήταν δυνατή η αποστολή αιτήματος." + "error_send_request": "Δεν ήταν δυνατή η αποστολή αιτήματος.", + "failed_read_event": "Αποτυχία ανάγνωσης συμβάντων", + "failed_send_event": "Αποτυχία αποστολής συμβάντος" }, "server_offline": { "description": "Ο διακομιστής σας δεν ανταποκρίνεται σε ορισμένα από τα αιτήματά σας. Παρακάτω είναι μερικοί από τους πιο πιθανούς λόγους.", @@ -2026,9 +2107,13 @@ "cannot_create_backup": "Δεν είναι δυνατή η δημιουργία αντιγράφου ασφαλείας κλειδιού", "create_title": "Δημιουργία αντιγράφου ασφαλείας κλειδιού", "setup_secure_backup": { + "backup_setup_success_description": "Τώρα δημιουργούνται αντίγραφα ασφαλείας των κλειδιών σας από αυτήν τη συσκευή.", + "backup_setup_success_title": "Επιτυχής δημιουργία ασφαλούς αντιγράφου ασφαλείας", "cancel_warning": "Εάν ακυρώσετε τώρα, ενδέχεται να χάσετε κρυπτογραφημένα μηνύματα και δεδομένα εάν χάσετε την πρόσβαση στα στοιχεία σύνδεσής σας.", "confirm_security_phrase": "Επιβεβαιώστε τη Φράση Ασφαλείας σας", "description": "Προστατευτείτε από την απώλεια πρόσβασης σε κρυπτογραφημένα μηνύματα και δεδομένα, δημιουργώντας αντίγραφα ασφαλείας των κλειδιών κρυπτογράφησης στον διακομιστή σας.", + "download_or_copy": "%(downloadButton)s ή %(copyButton)s", + "enter_phrase_description": "Εισαγάγετε μια φράση ασφαλείας που γνωρίζετε μόνο εσείς, καθώς χρησιμοποιείται για την προστασία των δεδομένων σας. Για να είστε ασφαλείς, δεν πρέπει να χρησιμοποιήσετε ξανά τον κωδικό πρόσβασης του λογαριασμού σας.", "enter_phrase_title": "Εισαγάγετε τη Φράση Ασφαλείας", "enter_phrase_to_confirm": "Εισαγάγετε τη Φράση Ασφαλείας σας για δεύτερη φορά για να την επιβεβαιώσετε.", "generate_security_key_description": "Θα δημιουργήσουμε ένα κλειδί ανάκτησης για να το αποθηκεύσετε σε ασφαλές μέρος, όπως έναν διαχειριστή κωδικών πρόσβασης ή ένα χρηματοκιβώτιο.", @@ -2037,11 +2122,11 @@ "pass_phrase_match_success": "Ταιριάζει!", "phrase_strong_enough": "Τέλεια! Αυτή η Φράση Ασφαλείας φαίνεται αρκετά ισχυρή.", "secret_storage_query_failure": "Δεν είναι δυνατή η υποβολή ερωτήματος για την κατάσταση του μυστικού χώρου αποθήκευσης", - "security_key_safety_reminder": "Αποθηκεύστε το Κλειδί ασφαλείας σας σε ασφαλές μέρος, όπως έναν διαχείριστη κωδικών πρόσβασης ή ένα χρηματοκιβώτιο, καθώς χρησιμοποιείται για την προστασία των κρυπτογραφημένων δεδομένων σας.", + "security_key_safety_reminder": "Αποθηκεύστε το Κλειδί Ανάκτησης σε ασφαλές μέρος, όπως έναν διαχειριστή κωδικών πρόσβασης ή ένα χρηματοκιβώτιο, καθώς χρησιμοποιείται για την προστασία των κρυπτογραφημένων δεδομένων σας.", "set_phrase_again": "Επιστρέψτε για να το ρυθμίσετε ξανά.", "settings_reminder": "Μπορείτε επίσης να ρυθμίσετε το Ασφαλές αντίγραφο ασφαλείας και να διαχειριστείτε τα κλειδιά σας στις Ρυθμίσεις.", "title_confirm_phrase": "Επιβεβαίωση Φράσης Ασφαλείας", - "title_save_key": "Αποθηκεύστε το κλειδί ασφαλείας σας", + "title_save_key": "Αποθηκεύστε το Κλειδί Ανάκτησης", "title_set_phrase": "Ορίστε μια Φράση Ασφαλείας", "unable_to_setup": "Δεν είναι δυνατή η ρύθμιση του μυστικού χώρου αποθήκευσης", "use_different_passphrase": "Να χρησιμοποιηθεί διαφορετική φράση;", @@ -2052,13 +2137,15 @@ "confirm_passphrase": "Επιβεβαίωση συνθηματικού", "enter_passphrase": "Εισαγωγή συνθηματικού", "export_description_1": "Αυτή η διαδικασία σας επιτρέπει να εξαγάγετε τα κλειδιά για τα μηνύματα που έχετε λάβει σε κρυπτογραφημένα δωμάτια σε ένα τοπικό αρχείο. Στη συνέχεια, θα μπορέσετε να εισάγετε το αρχείο σε άλλο πρόγραμμα του Matrix, έτσι ώστε το πρόγραμμα να είναι σε θέση να αποκρυπτογραφήσει αυτά τα μηνύματα.", + "export_description_2": "Το εξαγόμενο αρχείο θα επιτρέψει σε οποιονδήποτε μπορεί να το διαβάσει να αποκρυπτογραφήσει τυχόν κρυπτογραφημένα μηνύματα που μπορείτε να δείτε, οπότε θα πρέπει να είστε προσεκτικοί για να το διατηρήσετε ασφαλές. Για να βοηθήσετε σε αυτό, θα πρέπει να εισαγάγετε μια μοναδική φράση πρόσβασης παρακάτω, η οποία θα χρησιμοποιηθεί μόνο για την κρυπτογράφηση των εξαγόμενων δεδομένων. Η εισαγωγή των δεδομένων θα είναι δυνατή μόνο χρησιμοποιώντας την ίδια φράση πρόσβασης.", "export_title": "Εξαγωγή κλειδιών δωματίου", "file_to_import": "Αρχείο για εισαγωγή", "import_description_1": "Αυτή η διαδικασία σας επιτρέπει να εισαγάγετε κλειδιά κρυπτογράφησης που έχετε προηγουμένως εξάγει από άλλο πρόγραμμα του Matrix. Στη συνέχεια, θα μπορέσετε να αποκρυπτογραφήσετε τυχόν μηνύματα που το άλλο πρόγραμμα θα μπορούσε να αποκρυπτογραφήσει.", "import_description_2": "Το αρχείο εξαγωγής θα είναι προστατευμένο με συνθηματικό. Θα χρειαστεί να πληκτρολογήσετε το συνθηματικό εδώ για να αποκρυπτογραφήσετε το αρχείο.", "import_title": "Εισαγωγή κλειδιών δωματίου", "phrase_cannot_be_empty": "Το συνθηματικό δεν πρέπει να είναι κενό", - "phrase_must_match": "Δεν ταιριάζουν τα συνθηματικά" + "phrase_must_match": "Δεν ταιριάζουν τα συνθηματικά", + "phrase_strong_enough": "Υπέροχα! Αυτή η φράση πρόσβασης φαίνεται αρκετά ισχυρή" }, "keyboard": { "title": "Πληκτρολόγιο" @@ -2341,6 +2428,7 @@ }, "slash_command": { "addwidget": "Προσθέτει ένα προσαρμοσμένο widget μέσω URL στο δωμάτιο", + "addwidget_iframe_missing_src": "Το iframe δεν έχει χαρακτηριστικό src", "addwidget_invalid_protocol": "Παρακαλώ εισάγετε ένα widget URL με https:// ή http://", "addwidget_missing_url": "Παρακαλώ εισάγετε ένα widget URL ή ενσωματώστε κώδικα", "addwidget_no_permissions": "Δεν μπορείτε να τροποποιήσετε μικροεφαρμογές σε αυτό το δωμάτιο.", @@ -2354,10 +2442,12 @@ "command_error": "Σφάλμα εντολής", "converttodm": "Μετατρέπει το δωμάτιο σε προσωπική συνομιλία", "converttoroom": "Μετατρέπει την προσωπική συνομιλία σε δωμάτιο", + "could_not_find_room": "Δεν ήταν δυνατή η εύρεση αίθουσας", "deop": "Deop χρήστη με το συγκεκριμένο αναγνωριστικό", "devtools": "Ανοίγει το παράθυρο Εργαλείων για Προγραμματιστές", "discardsession": "Επιβάλλει την τρέχουσα εξερχόμενη ομαδική συνεδρία σε κρυπτογραφημένο δωμάτιο για απόρριψη", "error_invalid_rendering_type": "Σφάλμα εντολής: Δεν είναι δυνατή η εύρεση του τύπου απόδοσης (%(renderingType)s)", + "error_invalid_room": "Η εντολή απέτυχε: Δεν ήταν δυνατή η εύρεση αίθουσας (%(roomId)s)", "error_invalid_runfn": "Σφάλμα εντολής: Δεν είναι δυνατή η χρήση της εντολής slash.", "help": "Εμφανίζει τη λίστα εντολών με τρόπους χρήσης και περιγραφές", "help_dialog_title": "Βοήθεια Εντολών", @@ -2370,6 +2460,7 @@ "invite_3pid_needs_is_error": "Χρησιμοποιήστε έναν διακομιστή ταυτοτήτων για να προσκαλέσετε μέσω email. Μπορείτε να κάνετε διαχείριση στις Ρυθμίσεις.", "invite_3pid_use_default_is_title": "Χρησιμοποιήστε ένα διακομιστή ταυτοτήτων", "invite_3pid_use_default_is_title_description": "Χρησιμοποιήστε έναν διακομιστή ταυτοτήτων για να προσκαλέσετε μέσω email. Πατήστε συνέχεια για να χρησιμοποιήσετε τον προεπιλεγμένο διακομιστή ταυτοτήτων (%(defaultIdentityServerName)s) ή μπείτε στην διαχείριση στις Ρυθμίσεις.", + "invite_failed": "Ο χρήστης (%(user)s) δεν προσκλήθηκε στο %(roomId)s, αλλά δεν δόθηκε σφάλμα από το βοηθητικό πρόγραμμα πρόσκλησης.", "join": "Σύνδεση στην αίθουσα με την δοθείσα διεύθυνση", "jumptodate": "Μεταβείτε στη δεδομένη ημερομηνία στο χρονολόγιο", "jumptodate_invalid_input": "Αδυναμία κατανόησης της δοθείσας ημερομηνίας (%(inputDate)s). Προσπαθήστε να χρησιμοποιήσετε την μορφή YYYY-MM-DD.", @@ -2414,7 +2505,8 @@ "upgraderoom": "Αναβαθμίζει το δωμάτιο σε μια καινούργια έκδοση", "upgraderoom_permission_error": "Δεν διαθέτετε τις απαιτούμενες άδειες για να χρησιμοποιήσετε αυτήν την εντολή.", "usage": "Χρήση", - "verify": "Επιβεβαιώνει έναν χρήστη, συνεδρία, και pubkey tuple", + "verify": "Επαληθεύστε χειροκίνητα μια από τις δικές σας συσκευές", + "view": "Προβάλει την αίθουσα με την δεδομένη διεύθυνση", "whois": "Εμφανίζει πληροφορίες για έναν χρήστη" }, "space": { @@ -3010,6 +3102,8 @@ "truncated_list_n_more": { "other": "Και %(count)s ακόμα..." }, + "unsupported_server_description": "Αυτός ο διακομιστής χρησιμοποιεί μια παλαιότερη έκδοση του Matrix. Αναβαθμίστε σε Matrix %(version)s στο Matrix για να χρησιμοποιήσετε το %(brand)s χωρίς σφάλματα.", + "unsupported_server_title": "Ο διακομιστής σας δεν υποστηρίζεται", "update": { "changelog": "Αλλαγές", "check_action": "Έλεγχος για ενημέρωση", @@ -3122,6 +3216,7 @@ "call_held": "%(peerName)s έβαλε την κλήση σε αναμονή", "call_held_resume": "Έχετε βάλει την κλήση σε αναμονή Επαναφορά", "call_held_switch": "Έχετε βάλει την κλήση σε αναμονή Switch", + "call_toast_unknown_room": "Άγνωστη αίθουσα", "camera_disabled": "Η κάμερά σας είναι απενεργοποιημένη", "camera_enabled": "Η κάμερά σας είναι ακόμα ενεργοποιημένη", "cannot_call_yourself_description": "Δεν μπορείτε να καλέσετε τον εαυτό σας.", @@ -3134,19 +3229,30 @@ "dialpad": "Πληκτρολόγιο κλήσης", "disable_camera": "Απενεργοποίηση κάμερας", "disable_microphone": "Σίγαση μικροφώνου", + "disabled_no_one_here": "Δεν υπάρχει κανείς εδώ για να καλέσετε", + "disabled_no_perms_start_video_call": "Δεν έχετε δικαίωμα έναρξης βιντεοκλήσεων", + "disabled_no_perms_start_voice_call": "Δεν έχετε δικαίωμα έναρξης φωνητικών κλήσεων", + "disabled_ongoing_call": "Κλήση σε εξέλιξη", "enable_camera": "Ενεργοποίηση κάμερας", "enable_microphone": "Κατάργηση σίγασης μικροφώνου", "expand": "Επιστροφή στην κλήση", "hangup": "Κλείσιμο", "hide_sidebar_button": "Απόκρυψη πλαϊνής μπάρας", "input_devices": "Συσκευές εισόδου", + "join_button_tooltip_call_full": "Λυπούμαστε — αυτή η κλήση είναι πλήρης αυτήν τη στιγμή", "maximise": "Γέμισμα οθόνης", "misconfigured_server": "Η κλήση απέτυχε λόγω της λανθασμένης διάρθρωσης του διακομιστή", "misconfigured_server_description": "Παρακαλείστε να ρωτήσετε τον διαχειριστή του κεντρικού διακομιστή σας (%(homeserverDomain)s) να ρυθμίσουν έναν διακομιστή πρωτοκόλλου TURN ώστε οι κλήσεις να λειτουργούν απρόσκοπτα.", + "misconfigured_server_fallback": "Εναλλακτικά, μπορείτε να δοκιμάσετε να χρησιμοποιήσετε τον δημόσιο διακομιστή στη διεύθυνση , αλλά αυτό δεν θα είναι τόσο αξιόπιστο και θα κοινοποιήσει τη διεύθυνση IP σας σε αυτόν τον διακομιστή. Μπορείτε επίσης να το διαχειριστείτε αυτό στις Ρυθμίσεις.", + "misconfigured_server_fallback_accept": "Δοκιμάστε να χρησιμοποιήσετε το %(server)s", "more_button": "Περισσότερα", "msisdn_lookup_failed": "Αδυναμία αναζήτησης αριθμού τηλεφώνου", "msisdn_lookup_failed_description": "Υπήρξε ένα σφάλμα κατά την αναζήτηση αριθμού τηλεφώνου", "msisdn_transfer_failed": "Αδυναμία μεταφοράς κλήσης", + "n_people_joined": { + "one": "%(count)s άτομο εντάχθηκε", + "other": "%(count)s άτομα εντάχθηκαν" + }, "no_audio_input_description": "Δε βρέθηκε μικρόφωνο στη συσκευή σας. Παρακαλώ ελέγξτε τις ρυθμίσεις σας και δοκιμάστε ξανά.", "no_audio_input_title": "Δε βρέθηκε μικρόφωνο", "no_media_perms_description": "Μπορεί να χρειαστεί να ορίσετε χειροκίνητα την πρόσβαση του %(brand)s στο μικρόφωνο/κάμερα", @@ -3276,10 +3382,12 @@ "error_loading": "Σφάλμα φόρτωσης Μικροεφαρμογής", "error_mixed_content": "Σφάλμα - Μικτό περιεχόμενο", "error_need_invite_permission": "Για να το κάνετε αυτό πρέπει να έχετε τη δυνατότητα να προσκαλέσετε χρήστες.", + "error_need_kick_permission": "Πρέπει να έχετε τη δυνατότητα να διώξετε χρήστες για να το κάνετε αυτό.", "error_need_to_be_logged_in": "Πρέπει να είστε συνδεδεμένος.", "error_unable_start_audio_stream_description": "Δεν είναι δυνατή η έναρξη ροής ήχου.", "error_unable_start_audio_stream_title": "Η έναρξη της ζωντανής ροής απέτυχε", - "modal_data_warning": "Τα δεδομένα σε αυτήν την οθόνη μοιράζονται με το %(widgetDomain)s", + "modal_data_warning": "Τα παρακάτω δεδομένα μοιράζονται με %(widgetDomain)s", + "modal_title_default": "Modal Widget", "no_name": "Άγνωστη εφαρμογή", "open_id_permissions_dialog": { "remember_selection": "Να το θυμάσαι αυτό", @@ -3287,15 +3395,20 @@ "title": "Επιτρέψτε σε αυτήν τη μικροεφαρμογή να επαληθεύσει την ταυτότητά σας" }, "popout": "Αναδυόμενη μικροεφαρμογή", - "set_room_layout": "Ορίστε τη διάταξη του δωματίου μου για όλους", + "set_room_layout": "Ορισμός διάταξης για όλους", + "shared_data_avatar": "URL της εικόνας προφίλ σας", + "shared_data_device_id": "Το αναγνωριστικό της συσκευής σας", + "shared_data_lang": "Η γλώσσα σας", "shared_data_mxid": "Το αναγνωριστικό (ID) χρήστη σας", "shared_data_name": "Το εμφανιζόμενο όνομά σας", "shared_data_room_id": "ID Δωματίου", "shared_data_theme": "Το θέμα εμφάνισης", + "shared_data_url": "%(brand)s URL", "shared_data_warning": "Η χρήση αυτής της μικροεφαρμογής ενδέχεται να μοιράζεται δεδομένα με %(widgetDomain)s.", "shared_data_warning_im": "Η χρήση αυτής της μικροεφαρμογής μπορεί να μοιραστεί δεδομένα με το %(widgetDomain)s και τον διαχειριστή πρόσθετων.", "shared_data_widget_id": "Ταυτότητα μικροεφαρμογής", "unencrypted_warning": "Οι μικροεοεφαρμογές δε χρησιμοποιούν κρυπτογράφηση μηνυμάτων.", + "unmaximise": "Απο-μεγιστοποίηση", "unpin_to_view_right_panel": "Ξεκαρφιτσώστε αυτήν τη μικροεφαρμογή για να την προβάλετε σε αυτόν τον πίνακα" }, "zxcvbn": { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 45a70b886f..4e59a7cd6c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -240,8 +240,7 @@ "setup_key_backup_title": "You'll lose access to your encrypted messages", "setup_secure_backup_description_1": "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.", "setup_secure_backup_description_2": "When you sign out, these keys will be deleted from this device, which means you won't be able to read encrypted messages unless you have the keys for them on your other devices, or backed them up to the server.", - "skip_key_backup": "I don't want my encrypted messages", - "use_key_backup": "Start using Key Backup" + "skip_key_backup": "I don't want my encrypted messages" }, "misconfigured_body": "Ask your %(brand)s admin to check your config for incorrect or duplicate entries.", "misconfigured_title": "Your %(brand)s is misconfigured", @@ -968,9 +967,7 @@ }, "reset_all_button": "Forgotten or lost all recovery methods? Reset all", "set_up_recovery": "Set up recovery", - "set_up_recovery_later": "Not now", "set_up_recovery_toast_description": "Generate a recovery key that can be used to restore your encrypted message history in case you lose access to your devices.", - "set_up_toast_description": "Safeguard against losing access to encrypted messages & data", "set_up_toast_title": "Set up Secure Backup", "setup_secure_backup": { "explainer": "Back up your keys before signing out to avoid losing them." @@ -2717,11 +2714,6 @@ }, "jump_to_bottom_on_send": "Jump to the bottom of the timeline when you send a message", "key_backup": { - "backup_in_progress": "Your keys are being backed up (the first backup could take a few minutes).", - "backup_starting": "Starting backup…", - "backup_success": "Success!", - "cannot_create_backup": "Unable to create key backup", - "create_title": "Create key backup", "setup_secure_backup": { "backup_setup_success_description": "Your keys are now being backed up from this device.", "backup_setup_success_title": "Secure Backup successful", @@ -2848,6 +2840,7 @@ "composer_heading": "Composer", "default_timezone": "Browser default (%(timezone)s)", "dialog_title": "Settings: Preferences", + "enable_content_protection": "Enable content protection", "enable_hardware_acceleration": "Enable hardware acceleration", "enable_tray_icon": "Show tray icon and minimise window to it on close", "keyboard_heading": "Keyboard shortcuts", @@ -2881,7 +2874,6 @@ "ignore_users_empty": "You have no ignored users.", "ignore_users_section": "Ignored users", "key_backup_algorithm": "Algorithm:", - "key_backup_connect": "Connect this session to Key Backup", "message_search_disable_warning": "If disabled, messages from encrypted rooms won't appear in search results.", "message_search_disabled": "Securely cache encrypted messages locally for them to appear in search results.", "message_search_enabled": { diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index 24f5661733..776050780b 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -968,7 +968,6 @@ }, "reset_all_button": "Unustasid või oled kaotanud kõik võimalused ligipääsu taastamiseks? Lähtesta kõik ühe korraga", "set_up_recovery": "Seadista krüptovõtmete taastamine", - "set_up_recovery_later": "Mitte praegu", "set_up_recovery_toast_description": "Kui peaksid kaotama ligipääsu oma seadmetele, siis siinloodava taastevõtmega saad taastada ligipääsu oma krüptitud sõnumitele.", "set_up_toast_description": "Hoia ära, et kaotad ligipääsu krüptitud sõnumitele ja andmetele", "set_up_toast_title": "Võta kasutusele turvaline varundus", @@ -2066,6 +2065,7 @@ "read_topic": "Teema lugemiseks klõpsi", "rejecting": "Hülgan kutset…", "rejoin_button": "Liitu uuesti", + "room_content": "Jututoa sisu", "room_is_low_priority": "See on vähetähtis jututuba", "search": { "all_rooms_button": "Otsi kõikidest jututubadest", diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index 7e993adc31..0e40872dfc 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -829,7 +829,6 @@ }, "reset_all_button": "Unohtanut tai kadottanut kaikki palautustavat? Nollaa kaikki", "set_up_recovery": "Määritä palautus", - "set_up_recovery_later": "Ei nyt", "set_up_recovery_toast_description": "Luo palautusavain, jota voit käyttää salatun viestihistorian palauttamiseen, jos menetät pääsyn laitteisiisi.", "set_up_toast_description": "Suojaudu salattuihin viesteihin ja tietoihin pääsyn menettämiseltä", "set_up_toast_title": "Määritä turvallinen varmuuskopio", diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index e7cab1d82a..d1f55d7f50 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -786,6 +786,7 @@ "cross_signing_status": "État de la signature croisée :", "cross_signing_untrusted": "Votre compte a une identité de signature croisée dans le coffre secret, mais cette session ne lui fait pas encore confiance.", "crypto_not_available": "Le module cryptographique n'est pas disponible", + "device_id": "Identifiant de l'appareil", "key_backup_active_version": "Version de sauvegarde active :", "key_backup_active_version_none": "Aucun", "key_backup_inactive_warning": "Vos clés ne sont pas sauvegardées sur cette session.", @@ -798,6 +799,8 @@ "secret_storage_ready": "prêt", "secret_storage_status": "Coffre secret :", "self_signing_private_key_cached_status": "Clé privée d’auto-signature :", + "session": "Session", + "session_fingerprint": "Empreinte numérique (clé de la session)", "title": "Chiffrement de bout en bout", "user_signing_private_key_cached_status": "Clé privée de signature de l’utilisateur :" }, @@ -823,6 +826,7 @@ "low_bandwidth_mode": "Mode faible bande passante", "low_bandwidth_mode_description": "Nécessite un serveur d’accueil compatible.", "main_timeline": "Historique principal", + "manual_device_verification": "Vérification manuelle de l'appareil", "no_receipt_found": "Aucun accusé disponible", "notification_state": "L’état des notifications est %(notificationState)s", "notifications_debug": "Débogage des notifications", @@ -915,7 +919,7 @@ "key_validation_text": { "wrong_security_key": "La clé de récupération que vous avez saisie est incorrecte." }, - "privacy_warning": "Assurez vous que personne d'autre ne regarde votre écran !", + "privacy_warning": "Assurez-vous que personne d'autre ne regarde votre écran !", "restoring": "Restauration des clés depuis la sauvegarde", "security_key_title": "Clé de récupération" }, @@ -964,7 +968,6 @@ }, "reset_all_button": "Vous avez perdu ou oublié tous vos moyens de récupération ? Tout réinitialiser", "set_up_recovery": "Configurer la récupération", - "set_up_recovery_later": "Pas maintenant", "set_up_recovery_toast_description": "Générez une clé de récupération qui peut être utilisée pour restaurer l'historique de vos messages chiffrés au cas où vous perdriez l'accès à vos appareils.", "set_up_toast_description": "Sécurité contre la perte d’accès aux messages et données chiffrées", "set_up_toast_title": "Configurer la sauvegarde sécurisée", @@ -1007,6 +1010,21 @@ "incoming_sas_dialog_waiting": "Attente de la confirmation du partenaire…", "incoming_sas_user_dialog_text_1": "Vérifier cet utilisateur pour le marquer comme fiable. Faire confiance aux utilisateurs vous permet d’être tranquille lorsque vous utilisez des messages chiffrés de bout en bout.", "incoming_sas_user_dialog_text_2": "Vérifier cet utilisateur marquera sa session comme fiable, et marquera aussi votre session comme fiable pour lui.", + "manual": { + "already_verified": "Cet appareil est déjà vérifié", + "already_verified_and_wrong_fingerprint": "L'empreinte numérique fournie ne correspond pas, mais l'appareil est déjà vérifié !", + "device_id": "Identifiant de l'appareil", + "failure_description": "Échec de la vérification de '%(deviceId)s' : %(error)s", + "failure_title": "Échec de la vérification", + "fingerprint": "Empreinte numérique (clé de la session)", + "no_crypto": "Impossible de vérifier l'appareil - le chiffrement n'est pas activé", + "no_device": "Impossible de vérifier l'appareil - l'appareil %(deviceId)s n'a pas été trouvé", + "no_userid": "Impossible de vérifier l'appareil - identifiant utilisateur introuvable", + "success_description": "L’appareil (%(deviceId)s) est maintenant signé de manière croisée", + "success_title": "Vérification réussie", + "text": "Fournissez l'identifiant et l'empreinte numétrique de l'un de vos appareils pour le vérifier. REMARQUE : cela permet à l'autre appareil d'envoyer et de recevoir des messages comme vous. SI QUELQU'UN VOUS A DIT DE COLLER QUELQUE CHOSE ICI, IL EST PROBABLE QUE VOUS SOYEZ VICTIME D'UNE ARNAQUE !", + "wrong_fingerprint": "Impossible de vérifier l'appareil %(deviceId)s - l'empreinte numérique %(fingerprint)s fournie ne correspond pas à celle de l'appareil %(fprint)s" + }, "no_key_or_device": "Il semblerait que vous ne disposiez pas de clé de récupération ou d’autres appareils pour réalisation la vérification. Cet appareil ne pourra pas accéder aux anciens messages chiffrés. Afin de vérifier votre identité sur cet appareil, vous devrez réinitialiser vos clés de vérifications.", "no_support_qr_emoji": "L’appareil que vous essayez de vérifier ne prend pas en charge les QR codes ou la vérification d’émojis, qui sont les méthodes prises en charge par %(brand)s. Essayez avec un autre client.", "other_party_cancelled": "L’autre personne a annulé la vérification.", @@ -1733,9 +1751,9 @@ }, "total_no_votes": "Aucun vote exprimé", "total_not_ended": "Les résultats seront visibles lorsque le sondage sera terminé", - "type_closed": "Sondage terminé", + "type_closed": "Sondage fermé", "type_heading": "Type de sondage", - "type_open": "Ouvrir le sondage", + "type_open": "Sondage ouvert", "unable_edit_description": "Désolé, vous ne pouvez pas modifier un sondage après que les votes aient été exprimés.", "unable_edit_title": "Impossible de modifier le sondage" }, @@ -2046,6 +2064,8 @@ "read_topic": "Cliquer pour lire le sujet", "rejecting": "Rejet de l’invitation…", "rejoin_button": "Revenir", + "room_content": "Contenu du salon", + "room_is_low_priority": "Ce salon est de priorité basse", "search": { "all_rooms_button": "Rechercher dans tous les salons", "placeholder": "Rechercher des messages…", @@ -2094,6 +2114,7 @@ "add_space_label": "Ajouter un espace", "breadcrumbs_empty": "Aucun salon visité récemment", "breadcrumbs_label": "Salons visités récemment", + "collapse_filters": "Réduire la liste des filtres", "empty": { "no_chats": "Pas encore de discussions", "no_chats_description": "Commencez par envoyer un message à quelqu'un ou en créant un salon", @@ -2101,6 +2122,7 @@ "no_favourites": "Vous n'avez pas encore de discussion favorite", "no_favourites_description": "Vous pouvez ajouter une discussion à vos favoris dans les paramètres de discussion", "no_invites": "Vous n'avez aucune invitation non lue", + "no_lowpriority": "Vous n'avez aucun salon avec une priorité basse", "no_mentions": "Vous n'avez aucune mention non lue", "no_people": "Vous n'avez encore de discussions", "no_people_description": "Veuillez désélectionner des filtres pour voir vos discussions", @@ -2110,13 +2132,14 @@ "show_activity": "Voir toutes les activités", "show_chats": "Afficher toutes les discussions" }, + "expand_filters": "Développer la liste des filtres", "failed_add_tag": "Échec de l’ajout de l’étiquette %(tagName)s au salon", "failed_remove_tag": "Échec de la suppression de l’étiquette %(tagName)s du salon", "failed_set_dm_tag": "Échec de l’ajout de l’étiquette de conversation privée", "filters": { "favourite": "Favoris", "invites": "Invitations", - "low_priority": "Faible priorité", + "low_priority": "Priorité basse", "mentions": "Mentions", "people": "Personnes", "rooms": "Salons", @@ -2684,6 +2707,9 @@ "inline_url_previews_room": "Activer l’aperçu des URL par défaut pour les participants de ce salon", "inline_url_previews_room_account": "Activer l’aperçu des URL pour ce salon (n’affecte que vous)", "insert_trailing_colon_mentions": "Insérer deux-points après les mentions de l'utilisateur au début d'un message", + "invite_controls": { + "default_label": "Autoriser les utilisateurs à vous inviter dans les salons" + }, "jump_to_bottom_on_send": "Sauter en bas du fil de discussion lorsque vous envoyez un message", "key_backup": { "backup_in_progress": "Vous clés sont en cours de sauvegarde (la première sauvegarde peut prendre quelques minutes).", @@ -2750,6 +2776,7 @@ "show_in_private": "Dans les salons privés", "show_media": "Toujours afficher" }, + "not_supported": "Votre serveur ne propose pas cette fonctionnalité", "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)", @@ -3087,6 +3114,8 @@ "jumptodate": "Aller à la date correspondante dans la discussion", "jumptodate_invalid_input": "Nous n’avons pas pu comprendre la date saisie (%(inputDate)s). Veuillez essayer en utilisant le format AAAA-MM-JJ.", "lenny": "Ajoute ( ͡° ͜ʖ ͡°) en préfixe du message", + "manual_device_verification_confirm_description": "Cela permettra à un autre appareil d'envoyer et de recevoir des messages comme vous. SI QUELQU'UN VOUS A DIT DE COLLER QUELQUE CHOSE ICI, IL EST PROBABLE QUE VOUS SOYEZ VICTIME D'UNE ARNAQUE ! Êtes-vous sûr de vouloir vérifier cet autre appareil ?", + "manual_device_verification_confirm_title": "Attention : vérification manuelle de l'appareil", "me": "Affiche l’action", "msg": "Envoie un message à l’utilisateur fourni", "myavatar": "Modifier votre image de profil dans tous les salons", @@ -3127,7 +3156,7 @@ "upgraderoom": "Met à niveau un salon vers une nouvelle version", "upgraderoom_permission_error": "Vous n’avez pas les autorisations nécessaires pour utiliser cette commande.", "usage": "Utilisation", - "verify": "Vérifie un utilisateur, une session et une collection de clés publiques", + "verify": "Vérifiez manuellement l'un de vos appareils", "view": "Affiche le salon avec cette adresse", "whois": "Affiche des informations à propos de l’utilisateur" }, diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index c5d7cd6704..a3636061fc 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -784,6 +784,7 @@ "cross_signing_status": "Eszközök közti hitelesítés állapota:", "cross_signing_untrusted": "A fiókjához tartozik egy eszközök közti hitelesítési személyazonosság, de ez a munkamenet még nem jelölte megbízhatónak.", "crypto_not_available": "A kriptográfiai modul nem érhető el", + "device_id": "Eszközazonosító", "key_backup_active_version": "Aktív biztonsági mentés verziója:", "key_backup_active_version_none": "Nincs", "key_backup_inactive_warning": "A kulcsokról nem készül biztonsági mentés ebből a munkamenetből.", @@ -796,6 +797,8 @@ "secret_storage_ready": "kész", "secret_storage_status": "Titkos tároló:", "self_signing_private_key_cached_status": "Önaláíró titkos kulcs:", + "session": "Munkamenet", + "session_fingerprint": "Ujjlenyomat (munkamenetkulcs)", "title": "Végpontok közti titkosítás", "user_signing_private_key_cached_status": "Felhasználó aláírási titkos kulcsa:" }, @@ -821,6 +824,7 @@ "low_bandwidth_mode": "Alacsony sávszélességű mód", "low_bandwidth_mode_description": "Kompatibilis Matrix-kiszolgálóra van szükség.", "main_timeline": "Fő idővonal", + "manual_device_verification": "Kézi eszközellenőrzés", "no_receipt_found": "Nincs visszajelzés", "notification_state": "Értesítés állapota: %(notificationState)s", "notifications_debug": "Értesítések hibakeresése", @@ -908,6 +912,7 @@ "empty_room_was_name": "Üres szoba (%(oldName)s volt)", "encryption": { "access_secret_storage_dialog": { + "alternatives": "Ha van biztonsági kulcsa vagy biztonsági jelmondata, akkor ez is fog működni.", "key_validation_text": { "wrong_security_key": "A megadott helyreállítási kulcs helytelen." }, @@ -960,7 +965,6 @@ }, "reset_all_button": "Elfelejtette vagy elveszett minden helyreállítási lehetőség? Minden alaphelyzetbe állítása", "set_up_recovery": "Helyreállítás beállítása", - "set_up_recovery_later": "Most nem", "set_up_recovery_toast_description": "Létrehozhat egy helyreállítási kulcsot, amellyel helyreállíthatja a titkosított üzenetelőzményeit, ha elveszíti a hozzáférést az eszközeihez.", "set_up_toast_description": "Biztosíték a titkosított üzenetekhez és adatokhoz való hozzáférés elvesztése ellen", "set_up_toast_title": "Biztonsági mentés beállítása", @@ -1003,6 +1007,21 @@ "incoming_sas_dialog_waiting": "Várakozás a partner megerősítésére…", "incoming_sas_user_dialog_text_1": "Ellenőrizze ezt a felhasználót, hogy megbízhatónak jelölje. A felhasználók megbízhatóságának megerősítése további biztonságot nyújt a végpontok közti titkosítással rendelkező üzenetek használatakor.", "incoming_sas_user_dialog_text_2": "A felhasználó ellenőrzése által az ő munkamenete megbízhatónak lesz jelölve, és a te munkameneted is megbízhatónak lesz jelölve nála.", + "manual": { + "already_verified": "Ez az eszköz már ellenőrzött", + "already_verified_and_wrong_fingerprint": "A megadott ujjlenyomat nem egyezik, de az eszköz már ellenőrizve van!", + "device_id": "Eszközazonosító", + "failure_description": "Nem sikerült ellenőrizni ezt: %(deviceId)s: %(error)s", + "failure_title": "Az ellenőrzés sikertelen", + "fingerprint": "Ujjlenyomat (munkamenetkulcs)", + "no_crypto": "Nem lehet ellenőrizni az eszközt – a titkosítás nincs engedélyezve", + "no_device": "Nem sikerült ellenőrizni az eszközt – a(z) „%(deviceId)s” eszköz nem található", + "no_userid": "Nem sikerült ellenőrizni az eszközt – nem található a felhasználói azonosító", + "success_description": "Az eszköz (%(deviceId)s) mostantól keresztaláírással rendelkezik.", + "success_title": "Az ellenőrzés sikeres", + "text": "Adja meg valamelyik saját eszköze azonosítóját és ujjlenyomatát annak ellenőrzéséhez. MEGJEGYZÉS: ez lehetővé teszi a másik eszköz számára, hogy az Ön nevében küldjön és fogadjon üzeneteket. HA VALAKI AZT MONDTA, HOGY ILLESSZEN BE IDE VALAMIT, VALÓSZÍNŰLEG ÁTVERIK!", + "wrong_fingerprint": "Nem sikerült ellenőrizni a(z) „%(deviceId)s” eszközt – a mellékelt „%(fingerprint)s” ujjlenyomat nem egyezik az eszköz ujjlenyomatával: „%(fprint)s”" + }, "no_key_or_device": "Úgy tűnik, hogy nem rendelkezik helyreállítási kulccsal, vagy másik eszközzel, amellyel ellenőrizhetné. Ezzel az eszközzel nem fér majd hozzá a régi titkosított üzenetekhez. Ahhoz, hogy a személyazonosságát ezen az eszközön ellenőrizni lehessen, az ellenőrzési kulcsokat alaphelyzetbe kell állítania.", "no_support_qr_emoji": "Az ellenőrizni kívánt eszköz nem támogatja sem a QR-kód leolvasását, sem az emodzsis ellenőrzést, amelyeket az %(brand)s támogat. Próbálja meg egy másik klienssel.", "other_party_cancelled": "A másik fél megszakította az ellenőrzést.", @@ -1940,6 +1959,7 @@ }, "face_pile_tooltip_shortcut": "Beleértve: %(commaSeparatedMembers)s", "face_pile_tooltip_shortcut_joined": "Önt is beleértve, %(commaSeparatedMembers)s", + "failed_determine_user": "Nem lehet meghatározni, hogy melyik felhasználót kell figyelmen kívül hagyni, mivel az esemény megváltozott.", "failed_reject_invite": "A meghívót nem sikerült elutasítani", "forget_room": "Szoba elfelejtése", "forget_space": "Ennek a térnek az elfelejtése", @@ -2031,6 +2051,8 @@ "read_topic": "Kattintson a téma elolvasásához", "rejecting": "Meghívó elutasítása…", "rejoin_button": "Újra-csatlakozás", + "room_content": "Szoba tartalma", + "room_is_low_priority": "Ez egy alacsony prioritású szoba", "search": { "all_rooms_button": "Keresés az összes szobában", "placeholder": "Üzenetek keresése...", @@ -2077,6 +2099,7 @@ "add_space_label": "Tér hozzáadása", "breadcrumbs_empty": "Nincsenek nemrégiben meglátogatott szobák", "breadcrumbs_label": "Nemrég meglátogatott szobák", + "collapse_filters": "Szűrőlista összecsukása", "empty": { "no_chats": "Még nincsenek csevegések", "no_chats_description": "Kezdje azzal, hogy üzenetet küld valakinek, vagy létrehoz egy szobát", @@ -2084,6 +2107,7 @@ "no_favourites": "Még nincs kedvenc csevegése", "no_favourites_description": "A csevegési beállításokban adhat hozzá csevegést a kedvencekhez", "no_invites": "Nincs olvasatlan meghívója", + "no_lowpriority": "Nincs alacsony prioritású szobája", "no_mentions": "Nincs olvasatlan említése", "no_people": "Még nincs közvetlen csevegése senkivel", "no_people_description": "Kikapcsolhatja a szűrőket a többi csevegés megtekintéséhez", @@ -2093,12 +2117,14 @@ "show_activity": "Összes tevékenység megtekintése", "show_chats": "Összes csevegés megjelenítése" }, + "expand_filters": "Szűrőlista kibontása", "failed_add_tag": "Nem sikerült hozzáadni a szobához ezt: %(tagName)s", "failed_remove_tag": "Nem sikerült a szobáról eltávolítani ezt: %(tagName)s", "failed_set_dm_tag": "Nem sikerült a közvetlen beszélgetés címkét beállítani", "filters": { "favourite": "Kedvencek", "invites": "Meghívók", + "low_priority": "Alacsony prioritás", "mentions": "Említések", "people": "Emberek", "rooms": "Szobák", @@ -2662,6 +2688,9 @@ "inline_url_previews_room": "Webcím-előnézetek alapértelmezett engedélyezése a szobatagok számára", "inline_url_previews_room_account": "Webcím-előnézetek engedélyezése ebben a szobában (csak Önt érinti)", "insert_trailing_colon_mentions": "Záró kettőspont beszúrása egy felhasználó üzenet elején való megemlítésekor", + "invite_controls": { + "default_label": "Felhasználók meghívhatják szobákba" + }, "jump_to_bottom_on_send": "Üzenetküldés után az idővonal aljára ugrás", "key_backup": { "backup_in_progress": "A kulcsaid mentése folyamatban van (az első mentés több percig is eltarthat).", @@ -2728,6 +2757,7 @@ "show_in_private": "Privát szobákban", "show_media": "Megjelenítés mindig" }, + "not_supported": "A kiszolgálója nem valósítja meg ezt a funkciót.", "notifications": { "default_setting_description": "Ez a beállítás alapértelmezés szerint az összes szobájára érvényes lesz.", "default_setting_section": "Szeretnék értesítést kapni az alábbiakról (Alapértelmezett beállítás)", @@ -2785,6 +2815,7 @@ "voip": "Hang- és videóhívások" }, "preferences": { + "Electron.enableContentProtection": "Ablak tartalmának más alkalmazások általi rögzítésének megakadályozása", "Electron.enableHardwareAcceleration": "Hardveres gyorsítás engedélyezése (a(z) %(appName)s újraindítása szükséges az érvénybe lépéshez)", "always_show_menu_bar": "Ablak menüsávjának megjelenítése mindig", "autocomplete_delay": "Automatikus kiegészítés késleltetése (ms)", @@ -2953,6 +2984,7 @@ "show_chat_effects": "Csevegési effektek (például a konfetti animáció) megjelenítése", "show_displayname_changes": "Megjelenítendő nevek változásának megjelenítése", "show_join_leave": "Be- és kilépési üzenetek megjelenítése (a meghívók/kirúgások/kitiltások üzeneteit nem érinti)", + "show_message_previews": "Üzenetelőnézetek megjelenítése", "show_nsfw_content": "Felnőtt tartalmak megjelenítése", "show_read_receipts": "Mások által küldött olvasási visszajelzések megjelenítése", "show_redaction_placeholder": "Helykitöltő megjelenítése a törölt szövegek helyett", @@ -3059,6 +3091,8 @@ "jumptodate": "Az idővonalon megadott dátumra ugrás", "jumptodate_invalid_input": "A megadott dátum (%(inputDate)s) nem értelmezhető. Próbálja meg az ÉÉÉÉ-HH-NN formátum használatát.", "lenny": "Az egyszerű szöveges üzenet elé teszi ezt: ( ͡° ͜ʖ ͡°)", + "manual_device_verification_confirm_description": "Ez lehetővé teszi egy másik eszköz számára, hogy az Ön nevében üzeneteket küldjön és fogadjon. HA VALAKI AZT MONDTA, HOGY ILLESSZEN BE IDE VALAMIT, VALÓSZÍNŰLEG ÁTVERIK! Biztosan ellenőrizni szeretné ezt a másik eszközt?", + "manual_device_verification_confirm_title": "Vigyázat: kézi eszközellenőrzés", "me": "Megjeleníti a tevékenységet", "msg": "Üzenet küldése a megadott felhasználónak", "myavatar": "Az összes szobában módosítja a profilképét", @@ -3099,7 +3133,7 @@ "upgraderoom": "Új verzióra fejleszti a szobát", "upgraderoom_permission_error": "A parancs használatához nincs meg a megfelelő jogosultsága.", "usage": "Használat", - "verify": "Felhasználó, munkamenet és nyilvános kulcs hármas ellenőrzése", + "verify": "Az egyik saját eszköz kézi ellenőrzése", "view": "Megadott címmel rendelkező szobák megjelenítése", "whois": "Információt jelenít meg a felhasználóról" }, diff --git a/src/i18n/strings/id.json b/src/i18n/strings/id.json index 331a1a597e..52b21adea3 100644 --- a/src/i18n/strings/id.json +++ b/src/i18n/strings/id.json @@ -965,7 +965,6 @@ }, "reset_all_button": "Lupa atau kehilangan semua metode pemulihan? Atur ulang semuanya", "set_up_recovery": "Siapkan pemulihan", - "set_up_recovery_later": "Tidak sekarang", "set_up_recovery_toast_description": "Buat kunci pemulihan yang dapat digunakan untuk memulihkan riwayat pesan terenkripsi jika Anda kehilangan akses ke perangkat Anda.", "set_up_toast_description": "Lindungi dari kehilangan akses ke pesan & data terenkripsi", "set_up_toast_title": "Siapkan Cadangan Aman", @@ -2059,6 +2058,7 @@ "read_topic": "Klik untuk membaca topik", "rejecting": "Menolak undangan…", "rejoin_button": "Bergabung Ulang", + "room_content": "Isi ruangan", "room_is_low_priority": "Ini adalah ruangan dengan prioritas rendah", "search": { "all_rooms_button": "Cari semua ruangan", diff --git a/src/i18n/strings/nb_NO.json b/src/i18n/strings/nb_NO.json index dd8b57a1b8..548c14e1aa 100644 --- a/src/i18n/strings/nb_NO.json +++ b/src/i18n/strings/nb_NO.json @@ -968,7 +968,6 @@ }, "reset_all_button": "Glemt eller mistet alle gjenopprettingsmetoder? Tilbakestill alt", "set_up_recovery": "Sett opp gjenoppretting", - "set_up_recovery_later": "Ikke nå", "set_up_recovery_toast_description": "Generer en gjenopprettingsnøkkel som kan brukes til å gjenopprette den krypterte meldingshistorikken i tilfelle du mister tilgangen til enhetene dine.", "set_up_toast_description": "Sikre deg mot å miste tilgang til krypterte meldinger og data", "set_up_toast_title": "Sett opp sikker sikkerhetskopiering", diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index ce09d10a57..d2a3f7a70d 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -356,7 +356,7 @@ "set_email_prompt": "Czy chcesz ustawić adres e-mail?", "sign_in_description": "Użyj swojego konta, aby kontynuować.", "sign_in_instead": "Zamiast tego zaloguj się", - "sign_in_instead_prompt": "Masz już konto? Zaloguj się tutaj", + "sign_in_instead_prompt": "Masz już konto? Zaloguj się tutaj ", "sign_in_or_register": "Zaloguj się lub utwórz konto", "sign_in_or_register_description": "Użyj konta lub utwórz nowe, aby kontynuować.", "sign_in_prompt": "Posiadasz już konto? Zaloguj się", @@ -917,7 +917,7 @@ "encryption": { "access_secret_storage_dialog": { "key_validation_text": { - "wrong_security_key": "Błędny klucz przywracania" + "wrong_security_key": "Wprowadzony klucz przywracania nie jest poprawny." }, "restoring": "Przywracanie kluczy z kopii zapasowej", "security_key_title": "Klucz przywracania" @@ -967,7 +967,6 @@ }, "reset_all_button": "Zapomniałeś lub straciłeś wszystkie opcje odzyskiwania? Resetuj wszystko", "set_up_recovery": "Skonfiguruj przywracanie", - "set_up_recovery_later": "Nie teraz", "set_up_recovery_toast_description": "Wygeneruj klucz przywracania, którego można użyć do przywrócenia zaszyfrowanej historii wiadomości w przypadku utraty dostępu do swoich urządzeń.", "set_up_toast_description": "Zabezpiecz się przed utratą dostępu do szyfrowanych wiadomości i danych", "set_up_toast_title": "Skonfiguruj bezpieczną kopię zapasową", @@ -1650,7 +1649,7 @@ }, "member_list_back_action_label": "Członkowie pokoju", "message_edit_dialog_title": "Edycje wiadomości", - "migrating_crypto": "Trzymaj się mocno. Aktualizujemy %(brand)s, aby szyfrowanie było szybsze i bardziej niezawodne.", + "migrating_crypto": "Spokojnie. Aktualizujemy %(brand)s, aby szyfrowanie było szybsze i bardziej niezawodne.", "mobile_guide": { "toast_accept": "Użyj aplikacji", "toast_description": "%(brand)s jest eksperymentalne na przeglądarce mobilnej. Dla lepszego doświadczenia i najnowszych funkcji użyj naszej darmowej natywnej aplikacji.", @@ -4112,7 +4111,7 @@ "error_need_to_be_logged_in": "Musisz być zalogowany.", "error_unable_start_audio_stream_description": "Nie można rozpocząć przesyłania strumienia audio.", "error_unable_start_audio_stream_title": "Nie udało się rozpocząć transmisji na żywo", - "modal_data_warning": "Poniższe dane są współdzielone z %(widgetDomain)s", + "modal_data_warning": "Poniższe dane są udostępniane z %(widgetDomain)s", "modal_title_default": "Widżet modalny", "no_name": "Nieznana aplikacja", "open_id_permissions_dialog": { diff --git a/src/i18n/strings/pt.json b/src/i18n/strings/pt.json index eefb0a8254..e5dc8acbdc 100644 --- a/src/i18n/strings/pt.json +++ b/src/i18n/strings/pt.json @@ -936,7 +936,6 @@ }, "reset_all_button": "Esqueceste-te ou perdeste todos os métodos de recuperação? Repor tudo", "set_up_recovery": "Configurar a recuperação", - "set_up_recovery_later": "Agora não", "set_up_recovery_toast_description": "Gera uma chave de recuperação que pode ser utilizada para restaurar o teu histórico de mensagens encriptadas, caso percas o acesso aos teus dispositivos.", "set_up_toast_description": "Protege-te contra a perda de acesso a mensagens e dados encriptados", "set_up_toast_title": "Configura uma cópia de segurança segura", diff --git a/src/i18n/strings/pt_BR.json b/src/i18n/strings/pt_BR.json index ebe5b8c12f..facffcc21c 100644 --- a/src/i18n/strings/pt_BR.json +++ b/src/i18n/strings/pt_BR.json @@ -968,7 +968,6 @@ }, "reset_all_button": "Esqueceu ou perdeu todos os métodos de recuperação? Redefinir tudo", "set_up_recovery": "Configurar a recuperação", - "set_up_recovery_later": "Agora não", "set_up_recovery_toast_description": "Gere uma chave de recuperação que possa ser usada para restaurar seu histórico de mensagens criptografadas caso você perca o acesso aos seus dispositivos.", "set_up_toast_description": "Proteja-se contra a perda de acesso a mensagens e dados criptografados", "set_up_toast_title": "Configurar o backup online", @@ -2065,6 +2064,7 @@ "read_topic": "Clique para ler o tópico", "rejecting": "Rejeitando o convite...", "rejoin_button": "Entrar novamente", + "room_content": "Conteúdo da sala", "room_is_low_priority": "Esta é uma sala de baixa prioridade", "search": { "all_rooms_button": "Pesquisar todos as salas", @@ -2114,6 +2114,7 @@ "add_space_label": "Adicionar espaço", "breadcrumbs_empty": "Nenhuma sala foi visitada recentemente", "breadcrumbs_label": "Salas visitadas recentemente", + "collapse_filters": "Recolher lista de filtros", "empty": { "no_chats": "Ainda não há conversas.", "no_chats_description": "Comece enviando uma mensagem para alguém ou criando uma sala", @@ -2121,6 +2122,7 @@ "no_favourites": "Você ainda não tem o bate-papo favorito", "no_favourites_description": "Você pode adicionar um bate-papo aos seus favoritos nas configurações de bate-papo", "no_invites": "Você não tem nenhum convite não lido", + "no_lowpriority": "Você não tem nenhuma sala de baixa prioridade", "no_mentions": "Você não tem nenhuma menção não lida", "no_people": "Você ainda não tem conversas diretas com ninguém", "no_people_description": "Você pode desmarcar os filtros para ver suas outras conversas", @@ -2130,6 +2132,7 @@ "show_activity": "Ver todas as atividades", "show_chats": "Mostrar todas as conversas" }, + "expand_filters": "Expandir lista de filtros", "failed_add_tag": "Falha ao adicionar a tag %(tagName)s para a sala", "failed_remove_tag": "Falha ao remover a tag %(tagName)s da sala", "failed_set_dm_tag": "Falha ao definir a marca de mensagem direta", @@ -3111,6 +3114,8 @@ "jumptodate": "Ir para a data especificada na linha do tempo", "jumptodate_invalid_input": "Não foi possível entender a data fornecida (%(inputDate)s). Tente usando o formato AAAA-MM-DD.", "lenny": "Adiciona ( ͡° ͜ʖ ͡°) a uma mensagem de texto", + "manual_device_verification_confirm_description": "Isso permitirá que outro dispositivo envie e receba mensagens como você. SE ALGUÉM LHE DISSE PARA COLAR ALGO AQUI, É PROVÁVEL QUE VOCÊ ESTEJA SENDO ENGANADO! Tem certeza de que deseja verificar esse outro dispositivo?", + "manual_device_verification_confirm_title": "Cuidado: verificação manual do dispositivo", "me": "Visualizar atividades", "msg": "Envia uma mensagem para determinada pessoa", "myavatar": "Altera a foto do seu perfil em todas as salas", diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index 126a915ca1..b884089970 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -12,6 +12,18 @@ "one": "1 непрочитанное упоминание." }, "recent_rooms": "Недавние комнаты", + "room_messsage_not_sent": "Открыть комнату %(roomName)s с неотправленным сообщением.", + "room_n_unread_invite": "Открыть приглашение в комнату %(roomName)s.", + "room_n_unread_messages": { + "one": "Открыть комнату %(roomName)s с 1 непрочитанным сообщением.", + "few": "Открыть комнату %(roomName)s с %(count)s непрочитанными сообщениями.", + "many": "Открыть комнату %(roomName)s с %(count)s непрочитанными сообщениями." + }, + "room_n_unread_messages_mentions": { + "one": "Открыть комнату %(roomName)s с 1 непрочитанным упоминанием.", + "few": "Открыть комнату %(roomName)s с %(count)s непрочитанными упоминаниями.", + "many": "Открыть комнату %(roomName)s с %(count)s непрочитанными упоминаниями." + }, "room_name": "Комната %(name)s", "room_status_bar": "Строка состояния комнаты", "seek_bar_label": "Панель поиска аудио", @@ -742,6 +754,7 @@ }, "decline_invitation_dialog": { "confirm": "Вы действительно хотите отклонить приглашение присоединиться \"%(roomName)s\"?", + "ignore_user_help": "Вы не увидите сообщений или приглашений в комнату от этого пользователя.", "reason_description": "Опишите причину сообщения о проблеме.", "report_room_description": "Сообщите об этой комнате своему поставщику учетной записи.", "title": "Отклонить приглашение" @@ -960,13 +973,14 @@ }, "reset_all_button": "Забыли или потеряли все варианты восстановления? Сбросить всё", "set_up_recovery": "Настроить восстановление", - "set_up_recovery_later": "Не сейчас", "set_up_recovery_toast_description": "Создайте ключ восстановления, который можно использовать для восстановления зашифрованной истории сообщений в случае потери доступа к своим устройствам.", "set_up_toast_description": "Защита от потери доступа к зашифрованным сообщениям и данным", "set_up_toast_title": "Настроить безопасное резервное копирование", "setup_secure_backup": { "explainer": "Перед выходом сохраните резервную копию ключей шифрования, чтобы не потерять их." }, + "turn_on_key_storage": "Включить хранилище ключей", + "turn_on_key_storage_description": "Безопасно храните свои криптографические идентификационные данные и ключи сообщений на сервере. Это позволит вам просматривать историю сообщений на любых новых устройствах.", "udd": { "interactive_verification_button": "Интерактивная сверка по смайлам", "other_ask_verify_text": "Попросите этого пользователя подтвердить сеанс или подтвердите его вручную ниже.", @@ -1008,7 +1022,12 @@ "failure_description": "Не удалось проверить '%(deviceId)s': %(error)s", "failure_title": "Сбой проверки", "fingerprint": "Отпечаток пальца (ключ сессии)", - "success_title": "Проверка прошла успешно" + "no_crypto": "Невозможно проверить устройство — шифрование не включено", + "no_device": "Не удалось проверить устройство — устройство %(deviceId)s '' не найдено", + "no_userid": "Невозможно проверить устройство — не удается найти идентификатор пользователя", + "success_description": "Теперь устройство (%(deviceId)s) имеет перекрестную подпись", + "success_title": "Проверка прошла успешно", + "text": "Предоставьте идентификатор и отпечаток пальца одного из ваших устройств, чтобы подтвердить это. ПРИМЕЧАНИЕ. Это позволяет другому устройству отправлять и получать сообщения так же, как и вы. ЕСЛИ КТО-ТО СКАЗАЛ ВАМ ЧТО-ТО ВСТАВИТЬ СЮДА, СКОРЕЕ ВСЕГО, ВАС ОБМАНУЛИ!" }, "no_key_or_device": "Похоже, у вас нет Ключа Восстановления, или других сеансов, с которыми вы могли бы свериться. В этом сеансе вы не сможете получить доступ к старым зашифрованным сообщениям. Чтобы подтвердить свою личность в этом сеансе, вам нужно будет сбросить свои ключи шифрования.", "no_support_qr_emoji": "Устройство, которое вы пытаетесь проверить, не поддерживает сканирование QR-кода или проверку смайликов, которые поддерживает %(brand)s. Попробуйте использовать другой клиент.", @@ -1663,6 +1682,7 @@ "class_global": "Глобально", "class_other": "Другие", "default": "По умолчанию", + "default_settings": "Соответствует настройкам по умолчанию", "email_pusher_app_display_name": "Уведомления по электронной почте", "enable_prompt_toast_description": "Включить уведомления на рабочем столе", "enable_prompt_toast_title": "Уведомления", @@ -1681,7 +1701,8 @@ "mentions_and_keywords_description": "Получать уведомления только по упоминаниям и ключевым словам, установленным в ваших настройках", "mentions_keywords": "Упоминания и ключевые слова", "message_didnt_send": "Сообщение не отправлено. Нажмите для получения информации.", - "mute_description": "Вы не будете получать никаких уведомлений" + "mute_description": "Вы не будете получать никаких уведомлений", + "mute_room": "Заглушить комнату" }, "notifier": { "m.key.verification.request": "%(name)s запрашивает проверку" @@ -1805,6 +1826,10 @@ "spam_or_propaganda": "Спам или пропаганда", "toxic_behaviour": "Токсичное поведение" }, + "report_room": { + "description": "Сообщите об этой комнате вашему провайдеру аккаунта. Если сообщения зашифрованы, ваш администратор не сможет их прочитать.", + "reason_label": "Опишите причину" + }, "restore_key_backup_dialog": { "count_of_decryption_failures": "Не удалось расшифровать сеансы (%(failedCount)s)!", "count_of_successfully_restored_keys": "Успешно восстановлены ключи (%(sessionCount)s)", @@ -1956,6 +1981,7 @@ }, "face_pile_tooltip_shortcut": "Включая %(commaSeparatedMembers)s", "face_pile_tooltip_shortcut_joined": "Включая вас, %(commaSeparatedMembers)s", + "failed_determine_user": "Не удается определить, какого пользователя следует игнорировать, так как событие участника изменилось.", "failed_reject_invite": "Не удалось отклонить приглашение", "forget_room": "Забыть эту комнату", "forget_space": "Забыть это пространство", @@ -2048,6 +2074,8 @@ "read_topic": "Нажмите, чтобы увидеть тему", "rejecting": "Отклонение приглашения…", "rejoin_button": "Пере-присоединение", + "room_content": "Содержимое комнаты", + "room_is_low_priority": "Это комната с низким приоритетом.", "search": { "all_rooms_button": "Поиск по всем комнатам", "placeholder": "Поиск сообщений...", @@ -2088,6 +2116,7 @@ }, "uploading_single_file": "Отправка %(filename)s" }, + "video_room": "Эта комната представляет собой видеозал", "waiting_for_join_subtitle": "Как только приглашенные пользователи присоединятся %(brand)s, вы сможете общаться в чате, а комната будет полностью зашифрована", "waiting_for_join_title": "Ожидание присоединения пользователей %(brand)s" }, @@ -2096,27 +2125,36 @@ "add_space_label": "Добавить пространство", "breadcrumbs_empty": "Нет недавно посещенных комнат", "breadcrumbs_label": "Недавно посещённые комнаты", + "collapse_filters": "Свернуть список фильтров", "empty": { "no_chats": "Пока нет доступных чатов", "no_chats_description": "Начните с отправки сообщений или создания комнаты", "no_chats_description_no_room_rights": "Начните переписку с отправки сообщения", "no_favourites": "У вас пока нет чатов в Избранное", "no_favourites_description": "Вы можете добавить в Избранное в настройках чата", + "no_invites": "У вас нет непрочитанных приглашений", + "no_lowpriority": "У вас нет комнат с низким приоритетом", + "no_mentions": "У вас нет непрочитанных упоминаний", "no_people": "У вас пока нет личных чатов", "no_people_description": "Вы можете убрать фильтры, чтобы увидеть другие ваши чаты", "no_rooms": "Вы еще не находитесь ни в одной комнате", "no_rooms_description": "Вы можете убрать фильтры, чтобы увидеть другие ваши чаты", "no_unread": "Поздравляю! У вас нет непрочитанных сообщений", + "show_activity": "Посмотреть всю активность", "show_chats": "Показать все чаты" }, + "expand_filters": "Развернуть список фильтров", "failed_add_tag": "Не удалось добавить тег %(tagName)s в комнату", "failed_remove_tag": "Не удалось удалить тег %(tagName)s из комнаты", "failed_set_dm_tag": "Не удалось установить метку личного сообщения", "filters": { "favourite": "Избранное", + "invites": "Приглашения", + "low_priority": "Низкий приоритет", + "mentions": "Упоминания", "people": "Люди", "rooms": "Комнаты", - "unread": "Непрочитанное" + "unread": "Непрочитанные" }, "home_menu_label": "Параметры раздела \"Главная\"", "join_public_room_label": "Присоединиться к публичной комнате", @@ -2149,9 +2187,14 @@ "one": "Показать ещё %(count)s" }, "show_previews": "Показывать последнее сообщение", + "sort": "Сортировать", "sort_by": "Сортировать", "sort_by_activity": "По активности", "sort_by_alphabet": "А-Я", + "sort_type": { + "activity": "Активность", + "atoz": "А-Я" + }, "sort_unread_first": "Комнаты с непрочитанными сообщениями в начале", "space_menu_label": "Меню %(spaceName)s", "sublist_options": "Настройки списка", @@ -2385,7 +2428,7 @@ "public_without_alias_warning": "Для связи с этой комнатой, пожалуйста, добавьте адрес.", "publish_room": "Сделать эту комнату видимой в каталоге общественных комнат.", "publish_space": "Сделайте это пространство видимым в каталоге общественных комнат.", - "strict_encryption": "Никогда не отправлять зашифрованные сообщения непроверенным сеансам в этой комнате и через этот сеанс", + "strict_encryption": "Отправляйте сообщения только проверенным пользователям.", "title": "Безопасность" }, "title": "Настройки комнаты — %(roomName)s", @@ -2721,6 +2764,13 @@ "labs_mjolnir": { "dialog_title": "Настройки: Игнорируемые пользователи" }, + "media_preview": { + "hide_avatars": "Скрыть аватары комнаты и приглашающего", + "hide_media": "Всегда скрывать", + "show_in_private": "В личных комнатах", + "show_media": "Всегда показывать" + }, + "not_supported": "На вашем сервере эта функция не реализована.", "notifications": { "default_setting_description": "Эта настройка будет применена по умолчанию ко всем вашим комнатам.", "default_setting_section": "Я хочу получать уведомления о (настройка по умолчанию)", @@ -2957,6 +3007,7 @@ "show_chat_effects": "Эффекты (анимация при получении, например, конфетти)", "show_displayname_changes": "Изменения отображаемого имени", "show_join_leave": "Сообщения о присоединении/покидании (приглашения/удаления/блокировки не затрагиваются)", + "show_message_previews": "Показать предварительный просмотр сообщений", "show_nsfw_content": "Показать NSFW-контент", "show_read_receipts": "Уведомления о прочтении другими пользователями", "show_redaction_placeholder": "Плашки вместо удалённых сообщений", diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json index 08fc2c7d5f..5e7f955d1c 100644 --- a/src/i18n/strings/sk.json +++ b/src/i18n/strings/sk.json @@ -793,6 +793,7 @@ "cross_signing_status": "Stav krížového podpisovania:", "cross_signing_untrusted": "Váš účet má identitu s krížovým podpisom v tajnom úložisku, ale táto relácia mu zatiaľ nedôveruje.", "crypto_not_available": "Kryptografický modul nie je k dispozícii", + "device_id": "ID zariadenia", "key_backup_active_version": "Aktívna verzia zálohy:", "key_backup_active_version_none": "Žiadna", "key_backup_inactive_warning": "Vaše kľúče nie sú zálohované z tejto relácie.", @@ -805,6 +806,8 @@ "secret_storage_ready": "pripravený", "secret_storage_status": "Tajné úložisko:", "self_signing_private_key_cached_status": "Súkromný kľúč s vlastným podpisom:", + "session": "Relácia", + "session_fingerprint": "Odtlačok prsta (kľúč relácie)", "title": "Šifrovanie typu end-to-end", "user_signing_private_key_cached_status": "Súkromný kľúč podpisovania používateľa:" }, @@ -830,6 +833,7 @@ "low_bandwidth_mode": "Režim nízkej šírky pásma", "low_bandwidth_mode_description": "Vyžaduje kompatibilný domovský server.", "main_timeline": "Hlavná časová os", + "manual_device_verification": "Manuálne overenie zariadenia", "no_receipt_found": "Nenašlo sa žiadne potvrdenie", "notification_state": "Stav oznámenia je %(notificationState)s", "notifications_debug": "Ladenie oznámení", @@ -973,7 +977,6 @@ }, "reset_all_button": "Zabudli ste alebo ste stratili všetky metódy obnovy? Resetovať všetko", "set_up_recovery": "Nastaviť obnovenie", - "set_up_recovery_later": "Teraz nie", "set_up_recovery_toast_description": "Vytvorte kľúč na obnovenie, ktorý môžete použiť na obnovenie histórie šifrovaných správ v prípade straty prístupu k zariadeniam.", "set_up_toast_description": "Zabezpečte sa proti strate šifrovaných správ a údajov", "set_up_toast_title": "Nastaviť bezpečné zálohovanie", @@ -1016,6 +1019,21 @@ "incoming_sas_dialog_waiting": "Čakanie na potvrdenie od partnera…", "incoming_sas_user_dialog_text_1": "Overte tohto používateľa a označte ho ako dôveryhodného. Dôveryhodní používatelia vám poskytujú dodatočný pokoj na duši pri používaní end-to-end šifrovaných správ.", "incoming_sas_user_dialog_text_2": "Overenie tohto používateľa označí jeho reláciu ako dôveryhodnú a zároveň označí vašu reláciu ako dôveryhodnú pre neho.", + "manual": { + "already_verified": "Toto zariadenie je už overené", + "already_verified_and_wrong_fingerprint": "Zadaný odtlačok prsta sa nezhoduje, ale zariadenie je už overené!", + "device_id": "ID zariadenia", + "failure_description": "Nepodarilo sa overiť '%(deviceId)s': %(error)s", + "failure_title": "Overenie zlyhalo", + "fingerprint": "Odtlačok prsta (kľúč relácie)", + "no_crypto": "Nie je možné overiť zariadenie - kryptografia nie je povolená", + "no_device": "Zariadenie sa nepodarilo overiť - zariadenie '%(deviceId)s' nebolo nájdené", + "no_userid": "Nie je možné overiť zariadenie - nepodarilo sa nájsť naše ID používateľa", + "success_description": "Zariadenie (%(deviceId)s ) je teraz krížovo podpísané", + "success_title": "Overenie úspešné", + "text": "Na overenie zadajte ID a odtlačok jedného z vašich vlastných zariadení. POZNÁMKA: toto umožňuje druhému zariadeniu odosielať a prijímať správy ako vy. AK VÁM NIEKTO POVEDAL, ABY STE SEM NIEČO VLOŽILI, JE PRAVDEPODOBNÉ, ŽE IDE O PODVOD!", + "wrong_fingerprint": "Nie je možné overiť zariadenie '%(deviceId)s' - dodaný odtlačok prsta '%(fingerprint)s' sa nezhoduje s odtlačkom prsta zariadenia, '%(fprint)s'" + }, "no_key_or_device": "Vyzerá to, že nemáte kľúč na obnovenie ani žiadne iné zariadenie, pomocou ktorého by ste to mohli overiť. Toto zariadenie nebude mať prístup k starým zašifrovaným správam. Ak chcete overiť svoju totožnosť na tomto zariadení, budete musieť obnoviť svoje overovacie kľúče.", "no_support_qr_emoji": "Zariadenie, ktoré sa snažíte overiť, nepodporuje overenie skenovaním QR kódu ani overenie pomocou emotikonov, ktoré podporuje aplikácia %(brand)s. Skúste použiť iného klienta.", "other_party_cancelled": "Proti strana zrušila overovanie.", @@ -1981,6 +1999,7 @@ }, "face_pile_tooltip_shortcut": "Vrátane %(commaSeparatedMembers)s", "face_pile_tooltip_shortcut_joined": "Vrátane vás, %(commaSeparatedMembers)s", + "failed_determine_user": "Nie je možné určiť, ktorého používateľa ignorovať, pretože sa zmenila udalosť člena.", "failed_reject_invite": "Nepodarilo sa odmietnuť pozvanie", "forget_room": "Zabudnúť túto miestnosť", "forget_space": "Zabudnúť tento priestor", @@ -2073,6 +2092,8 @@ "read_topic": "Kliknutím si prečítate tému", "rejecting": "Odmietnutie pozvania …", "rejoin_button": "Znovu sa pripojiť", + "room_content": "Obsah miestnosti", + "room_is_low_priority": "Toto je miestnosť s nízkou prioritou", "search": { "all_rooms_button": "Vyhľadávať vo všetkých miestnostiach", "placeholder": "Hľadať správy…", @@ -2124,6 +2145,7 @@ "add_space_label": "Pridať priestor", "breadcrumbs_empty": "Žiadne nedávno navštívené miestnosti", "breadcrumbs_label": "Nedávno navštívené miestnosti", + "collapse_filters": "Zbaliť zoznam filtrov", "empty": { "no_chats": "Zatiaľ žiadne konverzácie", "no_chats_description": "Začnite tým, že niekomu napíšete správu alebo vytvoríte miestnosť", @@ -2131,6 +2153,7 @@ "no_favourites": "Zatiaľ nemáte obľúbenú konverzáciu", "no_favourites_description": "V nastaveniach konverzácií môžete pridať konverzáciu medzi obľúbené", "no_invites": "Nemáte žiadne neprečítané pozvánky", + "no_lowpriority": "Nemáte žiadne miestnosti s nízkou prioritou", "no_mentions": "Nemáte žiadne neprečítané zmienky", "no_people": "Zatiaľ s nikým nemáte priame konverzácie", "no_people_description": "Môžete zrušiť výber filtrov, aby ste videli svoje ostatné konverzácie", @@ -2140,6 +2163,7 @@ "show_activity": "Zobraziť všetku aktivitu", "show_chats": "Zobraziť všetky konverzácie" }, + "expand_filters": "Rozbaliť zoznam filtrov", "failed_add_tag": "Miestnosti sa nepodarilo pridať značku %(tagName)s", "failed_remove_tag": "Z miestnosti sa nepodarilo odstrániť značku %(tagName)s", "failed_set_dm_tag": "Nepodarilo sa nastaviť značku priamej správy", @@ -2720,6 +2744,9 @@ "inline_url_previews_room": "Predvolene povoliť náhľady URL adries pre členov tejto miestnosti", "inline_url_previews_room_account": "Povoliť náhľady URL adries pre túto miestnosť (ovplyvňuje len vás)", "insert_trailing_colon_mentions": "Vložiť na koniec dvojbodku za zmienkou používateľa na začiatku správy", + "invite_controls": { + "default_label": "Povoliť používateľom pozývať vás do miestností" + }, "jump_to_bottom_on_send": "Skok na koniec časovej osi pri odosielaní správy", "key_backup": { "backup_in_progress": "Zálohovanie kľúčov máte aktívne (prvé zálohovanie môže trvať niekoľko minút).", @@ -2786,6 +2813,7 @@ "show_in_private": "V súkromných miestnostiach", "show_media": "Vždy zobraziť" }, + "not_supported": "Váš server túto funkciu nepodporuje.", "notifications": { "default_setting_description": "Toto nastavenie sa predvolene použije pre všetky vaše miestnosti.", "default_setting_section": "Chcem byť upozornený na (predvolené nastavenie)", @@ -2843,6 +2871,7 @@ "voip": "Zvukové a video hovory" }, "preferences": { + "Electron.enableContentProtection": "Zabrániť zachytávaniu obsahu okna inými aplikáciami", "Electron.enableHardwareAcceleration": "Povoliť hardvérovú akceleráciu (reštartujte aplikáciu %(appName)s, aby sa prejavila)", "always_show_menu_bar": "Vždy zobraziť hornú lištu okna", "autocomplete_delay": "Oneskorenie automatického dokončovania (ms)", @@ -3018,6 +3047,7 @@ "show_chat_effects": "Zobraziť efekty konverzácie (animácie pri prijímaní napr. konfety)", "show_displayname_changes": "Zobrazovať zmeny zobrazovaného mena", "show_join_leave": "Zobraziť správy o pripojení/odchode (pozvania/odstránenia/zákazy nie sú ovplyvnené)", + "show_message_previews": "Zobraziť náhľady správ", "show_nsfw_content": "Zobraziť obsah NSFW", "show_read_receipts": "Zobrazovať potvrdenia o prečítaní od ostatných používateľov", "show_redaction_placeholder": "Zobrazovať náhrady za odstránené správy", @@ -3124,6 +3154,8 @@ "jumptodate": "Prejsť na zadaný dátum na časovej osi", "jumptodate_invalid_input": "Nepodarilo sa nám rozpoznať zadaný dátum (%(inputDate)s). Skúste použiť formát RRRR-MM-DD.", "lenny": "Pridá znaky ( ͡° ͜ʖ ͡°) pred správy vo formáte obyčajného textu", + "manual_device_verification_confirm_description": "Toto umožní inému zariadeniu odosielať a prijímať správy ako keby ste to boli vy. AK VÁM NIEKTO POVEDAL, ABY STE SEM NIEČO VLOŽILI, JE PRAVDEPODOBNÉ, ŽE IDE PO PODVOD! Ste si istí, že chcete overiť toto iné zariadenie?", + "manual_device_verification_confirm_title": "Upozornenie: ručné overenie zariadenia", "me": "Zobrazí akciu", "msg": "Pošle správu danému používateľovi", "myavatar": "Zmení váš profilový obrázok vo všetkých miestnostiach", @@ -3164,7 +3196,7 @@ "upgraderoom": "Aktualizuje miestnosť na novšiu verziu", "upgraderoom_permission_error": "Na použitie tohoto príkazu nemáte dostatočné povolenia.", "usage": "Použitie", - "verify": "Overí používateľa, reláciu a verejné kľúče", + "verify": "Manuálne overte jedno z vašich vlastných zariadení", "view": "Zobrazí miestnosti s danou adresou", "whois": "Zobrazuje informácie o používateľovi" }, diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json index 84deb17d98..b0a673a650 100644 --- a/src/i18n/strings/sv.json +++ b/src/i18n/strings/sv.json @@ -386,6 +386,7 @@ "fallback_button": "Starta autentisering", "mas_cross_signing_reset_cta": "Gå till ditt konto", "mas_cross_signing_reset_description": "Återställ din identitet via din kontoleverantör och kom sedan tillbaka och klicka på ”Försök igen”.", + "mas_cross_signing_reset_title": "Gå till ditt konto för att återställa din identitet", "msisdn": "Ett SMS har skickats till %(msisdn)s", "msisdn_token_incorrect": "Felaktig token", "msisdn_token_prompt": "Vänligen ange koden det innehåller:", @@ -960,7 +961,6 @@ }, "reset_all_button": "Glömt eller förlorat alla återställningsalternativ? Återställ allt", "set_up_recovery": "Ställ in återställning", - "set_up_recovery_later": "Inte nu", "set_up_recovery_toast_description": "Generera en återställningsnyckel som kan användas för att återställa din krypterade meddelandehistorik om du förlorar åtkomst till dina enheter.", "set_up_toast_description": "Skydda mot att förlora åtkomst till krypterade meddelanden och data", "set_up_toast_title": "Ställ in säker säkerhetskopiering", @@ -2610,6 +2610,7 @@ "discovery_needs_terms_title": "Låt andra hitta dig", "display_name": "Visningsnamn", "display_name_error": "Det gick inte att ställa in visningsnamn", + "email_adding_unsupported_by_hs": "Den här hemservern stöder inte att lägga till e-postadresser till ditt konto.", "email_address_in_use": "Den här e-postadressen används redan", "email_address_label": "E-postadress", "email_not_verified": "Din e-postadress har inte verifierats än", @@ -2634,7 +2635,9 @@ "error_share_msisdn_discovery": "Kunde inte dela telefonnummer", "identity_server_no_token": "Ingen identitetsåtkomsttoken hittades", "identity_server_not_set": "Identitetsserver inte inställd", + "invalid_phone_number": "Det angivna telefonnumret verkar inte vara giltigt.", "language_section": "Språk", + "msisdn_adding_unsupported_by_hs": "Den här hemservern stöder inte att lägga till telefonnummer till ditt konto.", "msisdn_in_use": "Detta telefonnummer används redan", "msisdn_label": "Telefonnummer", "msisdn_verification_field_label": "Verifieringskod", @@ -3779,6 +3782,7 @@ "description": "För att skapa en delningslänk, gör det här rummet offentligt eller aktivera alternativet för användare att be om att få gå med. Detta gör att gäster kan gå med utan att bli inbjudna.", "dont_change_description": "Om du inte vill ändra åtkomst till det här rummet kan du skapa ett nytt rum för samtalslänken.", "no_change": "Jag vill inte ändra åtkomstnivån.", + "revert_access_description": "(Detta kan återställas till föregående värde i Rumsinställningarna: Säkerhet och sekretess / Åtkomst)", "title": "Tillåt gästanvändare att gå med i det här rummet" }, "upload_failed_generic": "Filen '%(fileName)s' kunde inte laddas upp.", diff --git a/src/i18n/strings/tr.json b/src/i18n/strings/tr.json index 264880a2d4..c46b0d0c9c 100644 --- a/src/i18n/strings/tr.json +++ b/src/i18n/strings/tr.json @@ -934,7 +934,6 @@ }, "reset_all_button": "Tüm kurtarma yöntemlerini unuttunuz veya kaybettiniz mi? Tümünü sıfırla", "set_up_recovery": "Kurtarmayı ayarlayın", - "set_up_recovery_later": "Şimdi değil", "set_up_recovery_toast_description": "Cihazlarınıza erişiminizi kaybetmeniz durumunda şifrelenmiş mesaj geçmişinizi geri yüklemek için kullanılabilecek bir kurtarma anahtarı oluşturun.", "set_up_toast_description": "Şifrelenmiş mesajlara ve verilere erişimi kaybetmemek için koruma sağlayın", "set_up_toast_title": "Güvenli Yedekleme kur", diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json index 6be282da55..dc8c337225 100644 --- a/src/i18n/strings/uk.json +++ b/src/i18n/strings/uk.json @@ -788,6 +788,7 @@ "cross_signing_status": "Стан перехресного підписування:", "cross_signing_untrusted": "Ваш обліковий запис має ідентичність із перехресним підписом у таємному сховищі, але цей сеанс ще не довіряє їй.", "crypto_not_available": "Криптографічний модуль недоступний", + "device_id": "ID пристрою", "key_backup_active_version": "Активна версія резервної копії:", "key_backup_active_version_none": "Немає", "key_backup_inactive_warning": "Резервне копіювання ваших ключів із цього сеансу не виконується.", @@ -800,6 +801,8 @@ "secret_storage_ready": "готовий", "secret_storage_status": "Таємне сховище:", "self_signing_private_key_cached_status": "Самопідписаний приватний ключ:", + "session": "Сеанс", + "session_fingerprint": "Цифровий відбиток (ключ сеансу)", "title": "Наскрізне шифрування", "user_signing_private_key_cached_status": "Приватний ключ підпису користувача:" }, @@ -825,6 +828,7 @@ "low_bandwidth_mode": "Режим низької пропускної спроможності", "low_bandwidth_mode_description": "Потрібен сумісний домашній сервер.", "main_timeline": "Основна стрічка", + "manual_device_verification": "Ручна верифікація пристрою", "no_receipt_found": "Підтвердження не знайдено", "notification_state": "Стан сповіщень %(notificationState)s", "notifications_debug": "Сповіщення зневадження", @@ -967,7 +971,6 @@ }, "reset_all_button": "Забули чи втратили всі способи відновлення? Скинути все", "set_up_recovery": "Налаштування відновлення", - "set_up_recovery_later": "Не зараз", "set_up_recovery_toast_description": "Згенеруйте ключ відновлення, який можна використовувати для відновлення історії зашифрованих повідомлень у разі втрати доступу до своїх пристроїв.", "set_up_toast_description": "Захистіться від втрати доступу до зашифрованих повідомлень і даних", "set_up_toast_title": "Налаштувати захищене резервне копіювання", @@ -1010,6 +1013,21 @@ "incoming_sas_dialog_waiting": "Очікування підтвердження партнером…", "incoming_sas_user_dialog_text_1": "Звірте цього користувача щоб позначити його довіреним. Довіряння користувачам додає спокою якщо ви користуєтесь наскрізно зашифрованими повідомленнями.", "incoming_sas_user_dialog_text_2": "Звірка цього користувача позначить його сеанс довіреним вам, а ваш йому.", + "manual": { + "already_verified": "Цей пристрій вже верифіковано", + "already_verified_and_wrong_fingerprint": "Наданий цифровий відбиток не збігається, але пристрій уже верифіковано!", + "device_id": "ID пристрою", + "failure_description": "Не вдалося верифікувати '%(deviceId)s': %(error)s", + "failure_title": "Не вдалося верифікувати", + "fingerprint": "Цифровий відбиток (ключ сеансу)", + "no_crypto": "Не вдалося верифікувати пристрій – криптографію не ввімкнено", + "no_device": "Не вдалося верифікувати пристрій - пристрій '%(deviceId)s' не знайдено", + "no_userid": "Не вдалося верифікувати пристрій – не вдалося знайти наш ID користувача", + "success_description": "Пристрій (%(deviceId)s) тепер має перехресний підпис", + "success_title": "Успішно верифіковано", + "text": "Надайте ID і цифровий відбиток одного з ваших пристроїв, щоб верифікувати його. ЗАУВАЖТЕ, це дозволяє іншому пристрою надсилати та отримувати повідомлення від вашого імені. ЯКЩО ХТОСЬ СКАЗАВ ВАМ ВСТАВИТИ ЩОСЬ СЮДИ, ШВИДШЕ ЗА ВСЕ, ВАС ОБМАНЮЮТЬ!", + "wrong_fingerprint": "Не вдалося верифікувати пристрій '%(deviceId)s' - наданий цифровий відбиток '%(fingerprint)s' не збігається з відбитком пристрою, '%(fprint)s'" + }, "no_key_or_device": "Схоже, у вас немає ключа відновлення або будь-яких інших пристроїв, за допомогою яких ви можете виконати верифікацію. Цей пристрій не зможе отримати доступ до старих зашифрованих повідомлень. Щоб підтвердити свою ідентичність на цьому пристрої, вам потрібно буде скинути ключі верифікації.", "no_support_qr_emoji": "Пристрій, який ви намагаєтесь звірити, не підтримує сканування QR-коду або звірення за допомогою емоджі, що є підтримувані %(brand)s. Спробуйте використати інший клієнт.", "other_party_cancelled": "Друга сторона скасувала звірення.", @@ -2052,6 +2070,8 @@ "read_topic": "Натисніть, щоб побачити тему", "rejecting": "Відхилення запрошення…", "rejoin_button": "Перепід'єднатись", + "room_content": "Вміст кімнати", + "room_is_low_priority": "Це кімната з низьким пріоритетом", "search": { "all_rooms_button": "Вибрати всі кімнати", "placeholder": "Пошук повідомлень…", @@ -2101,6 +2121,7 @@ "add_space_label": "Додати простір", "breadcrumbs_empty": "Немає недавно відвіданих кімнат", "breadcrumbs_label": "Недавно відвідані кімнати", + "collapse_filters": "Згорнути список фільтрів", "empty": { "no_chats": "Ще немає бесід", "no_chats_description": "Почніть користування, надіславши комусь повідомлення або створивши кімнату", @@ -2108,6 +2129,7 @@ "no_favourites": "У вас ще немає обраних бесід", "no_favourites_description": "Ви можете додати бесіду до обраних у її налаштуваннях", "no_invites": "У вас немає непрочитаних запрошень", + "no_lowpriority": "У вас немає неважливих кімнат", "no_mentions": "У вас немає непрочитаних згадок", "no_people": "У вас ще немає особистих бесід", "no_people_description": "Ви можете очистити фільтри, щоб побачити інші ваші бесіди", @@ -2117,6 +2139,7 @@ "show_activity": "Переглянути всю діяльність", "show_chats": "Показати всі бесіди" }, + "expand_filters": "Розгорнути список фільтрів", "failed_add_tag": "Не вдалось додати до кімнати мітку %(tagName)s", "failed_remove_tag": "Не вдалося прибрати з кімнати мітку %(tagName)s", "failed_set_dm_tag": "Не вдалося встановити мітку особистого повідомлення", @@ -2691,6 +2714,9 @@ "inline_url_previews_room": "Увімкнути попередній перегляд гіперпосилань за умовчанням для учасників цієї кімнати", "inline_url_previews_room_account": "Увімкнути попередній перегляд гіперпосилань в цій кімнаті (стосується тільки вас)", "insert_trailing_colon_mentions": "Додавати двокрапку після згадки користувача на початку повідомлення", + "invite_controls": { + "default_label": "Дозволити користувачам запрошувати вас до кімнат" + }, "jump_to_bottom_on_send": "Переходити вниз стрічки під час надсилання повідомлення", "key_backup": { "backup_in_progress": "Створюється резервна копія ваших ключів (перше копіювання може тривати кілька хвилин).", @@ -2757,6 +2783,7 @@ "show_in_private": "У приватних кімнатах", "show_media": "Завжди показувати" }, + "not_supported": "Ваш сервер не впровадив цю функцію.", "notifications": { "default_setting_description": "Цей параметр буде застосовано усталеним до всіх ваших кімнат.", "default_setting_section": "Я хочу отримувати сповіщення про (типове налаштування)", @@ -3094,6 +3121,8 @@ "jumptodate": "Перейти до вказаної дати в стрічці", "jumptodate_invalid_input": "Не вдалося розпізнати вказану дату (%(inputDate)s). Спробуйте формат рррр-мм-дд.", "lenny": "Додає ( ͡° ͜ʖ ͡°) на початку текстового повідомлення", + "manual_device_verification_confirm_description": "Це дозволить іншому пристрою надсилати та отримувати повідомлення від вашого імені. ЯКЩО ХТОСЬ СКАЗАВ ВАМ ВСТАВИТИ ЩОСЬ СЮДИ, ЦІЛКОМ ІМОВІРНО, ВАС ОБМАНЮЮТЬ! Ви впевнені, що хочете верифікувати цей інший пристрій?", + "manual_device_verification_confirm_title": "Увага: ручна верифікація пристрою", "me": "Показ дій", "msg": "Надсилає повідомлення вказаному користувачеві", "myavatar": "Змінює зображення профілю в усіх кімнатах", @@ -3134,7 +3163,7 @@ "upgraderoom": "Поліпшує кімнату до нової версії", "upgraderoom_permission_error": "Вам бракує дозволу на використання цієї команди.", "usage": "Використання", - "verify": "Звіряє користувача, сеанс та супровід відкритого ключа", + "verify": "Вручну верифікуйте один із власних пристроїв", "view": "Перегляд кімнати з вказаною адресою", "whois": "Показує відомості про користувача" }, diff --git a/src/modules/Api.ts b/src/modules/Api.ts index 23abadf529..db7dd80334 100644 --- a/src/modules/Api.ts +++ b/src/modules/Api.ts @@ -6,8 +6,8 @@ Please see LICENSE files in the repository root for full details. */ import { createRoot, type Root } from "react-dom/client"; +import { type Api, type RuntimeModuleConstructor } from "@element-hq/element-web-module-api"; -import type { Api, RuntimeModuleConstructor } from "@element-hq/element-web-module-api"; import { ModuleRunner } from "./ModuleRunner.ts"; import AliasCustomisations from "../customisations/Alias.ts"; import { RoomListCustomisations } from "../customisations/RoomList.ts"; @@ -21,6 +21,7 @@ import { WidgetPermissionCustomisations } from "../customisations/WidgetPermissi import { WidgetVariableCustomisations } from "../customisations/WidgetVariables.ts"; import { ConfigApi } from "./ConfigApi.ts"; import { I18nApi } from "./I18nApi.ts"; +import { CustomComponentsApi } from "./customComponentApi.ts"; const legacyCustomisationsFactory = (baseCustomisations: T) => { let used = false; @@ -58,6 +59,7 @@ class ModuleApi implements Api { public readonly config = new ConfigApi(); public readonly i18n = new I18nApi(); + public readonly customComponents = new CustomComponentsApi(); public readonly rootNode = document.getElementById("matrixchat")!; public createRoot(element: Element): Root { diff --git a/src/modules/customComponentApi.ts b/src/modules/customComponentApi.ts new file mode 100644 index 0000000000..db2f9ab58a --- /dev/null +++ b/src/modules/customComponentApi.ts @@ -0,0 +1,137 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { logger } from "matrix-js-sdk/src/logger"; + +import type { + CustomComponentsApi as ICustomComponentsApi, + CustomMessageRenderFunction, + CustomMessageComponentProps as ModuleCustomMessageComponentProps, + OriginalComponentProps, + CustomMessageRenderHints as ModuleCustomCustomMessageRenderHints, + MatrixEvent as ModuleMatrixEvent, +} from "@element-hq/element-web-module-api"; +import type React from "react"; + +type EventTypeOrFilter = Parameters[0]; + +type EventRenderer = { + eventTypeOrFilter: EventTypeOrFilter; + renderer: CustomMessageRenderFunction; + hints: ModuleCustomCustomMessageRenderHints; +}; + +interface CustomMessageComponentProps extends Omit { + mxEvent: MatrixEvent; +} + +interface CustomMessageRenderHints extends Omit { + // Note. This just makes it easier to use this API on Element Web as we already have the moduleized event stored. + allowDownloadingMedia?: () => Promise; +} + +export class CustomComponentsApi implements ICustomComponentsApi { + /** + * Convert a matrix-js-sdk event into a ModuleMatrixEvent. + * @param mxEvent + * @returns An event object, or `null` if the event was not a message event. + */ + private static getModuleMatrixEvent(mxEvent: MatrixEvent): ModuleMatrixEvent | null { + const eventId = mxEvent.getId(); + const roomId = mxEvent.getRoomId(); + const sender = mxEvent.sender; + // Typically we wouldn't expect messages without these keys to be rendered + // by the timeline, but for the sake of type safety. + if (!eventId || !roomId || !sender) { + // Not a message event. + return null; + } + return { + content: mxEvent.getContent(), + eventId, + originServerTs: mxEvent.getTs(), + roomId, + sender: sender.userId, + stateKey: mxEvent.getStateKey(), + type: mxEvent.getType(), + unsigned: mxEvent.getUnsigned(), + }; + } + + private readonly registeredMessageRenderers: EventRenderer[] = []; + + public registerMessageRenderer( + eventTypeOrFilter: EventTypeOrFilter, + renderer: CustomMessageRenderFunction, + hints: ModuleCustomCustomMessageRenderHints = {}, + ): void { + this.registeredMessageRenderers.push({ eventTypeOrFilter: eventTypeOrFilter, renderer, hints }); + } + /** + * Select the correct renderer based on the event information. + * @param mxEvent The message event being rendered. + * @returns The registered renderer. + */ + private selectRenderer(mxEvent: ModuleMatrixEvent): EventRenderer | undefined { + return this.registeredMessageRenderers.find((renderer) => { + if (typeof renderer.eventTypeOrFilter === "string") { + return renderer.eventTypeOrFilter === mxEvent.type; + } else { + try { + return renderer.eventTypeOrFilter(mxEvent); + } catch (ex) { + logger.warn("Message renderer failed to process filter", ex); + return false; // Skip erroring renderers. + } + } + }); + } + + /** + * Render the component for a message event. + * @param props Props to be passed to the custom renderer. + * @param originalComponent Function that will be rendered if no custom renderers are present, or as a child of a custom component. + * @returns A component if a custom renderer exists, or originalComponent returns a value. Otherwise null. + */ + public renderMessage( + props: CustomMessageComponentProps, + originalComponent?: (props?: OriginalComponentProps) => React.JSX.Element, + ): React.JSX.Element | null { + const moduleEv = CustomComponentsApi.getModuleMatrixEvent(props.mxEvent); + const renderer = moduleEv && this.selectRenderer(moduleEv); + if (renderer) { + try { + return renderer.renderer({ ...props, mxEvent: moduleEv }, originalComponent); + } catch (ex) { + logger.warn("Message renderer failed to render", ex); + // Fall through to original component. If the module encounters an error we still want to display messages to the user! + } + } + return originalComponent?.() ?? null; + } + + /** + * Get hints about an message before rendering it. + * @param mxEvent The message event being rendered. + * @returns A component if a custom renderer exists, or originalComponent returns a value. Otherwise null. + */ + public getHintsForMessage(mxEvent: MatrixEvent): CustomMessageRenderHints | null { + const moduleEv = CustomComponentsApi.getModuleMatrixEvent(mxEvent); + const renderer = moduleEv && this.selectRenderer(moduleEv); + if (renderer) { + return { + ...renderer.hints, + // Convert from js-sdk style events to module events automatically. + allowDownloadingMedia: renderer.hints.allowDownloadingMedia + ? () => renderer.hints.allowDownloadingMedia!(moduleEv) + : undefined, + }; + } + return null; + } +} diff --git a/src/rageshake/submit-rageshake.ts b/src/rageshake/submit-rageshake.ts index 2e04d7d9dc..0717ccd56a 100644 --- a/src/rageshake/submit-rageshake.ts +++ b/src/rageshake/submit-rageshake.ts @@ -171,7 +171,7 @@ async function collectSynapseSpecific(client: MatrixClient, body: FormData): Pro } catch { try { // If that fails we'll hit any endpoint and look at the server response header - const res = await window.fetch(client.http.getUrl("/login"), { + const res = await fetch(client.http.getUrl("/login"), { method: "GET", mode: "cors", }); @@ -257,7 +257,7 @@ export function collectSettings(body: FormData): void { body.append("lowBandwidth", "enabled"); } - body.append("mx_local_settings", localStorage.getItem("mx_local_settings")!); + body.append("mx_local_settings", SettingsStore.exportForRageshake()); } /** diff --git a/src/sentry.ts b/src/sentry.ts index 92e8403963..214f4f09e3 100644 --- a/src/sentry.ts +++ b/src/sentry.ts @@ -141,7 +141,7 @@ async function getCryptoContext(client: MatrixClient): Promise { function getDeviceContext(client: MatrixClient): DeviceContext { const result: DeviceContext = { device_id: client?.deviceId ?? undefined, - mx_local_settings: localStorage.getItem("mx_local_settings"), + mx_local_settings: SettingsStore.exportForRageshake(), }; if (window.Modernizr) { diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 4c6ae8c198..3ab4682fd6 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -173,6 +173,11 @@ export interface IBaseSetting { // Whether the setting should have a warning sign in the microcopy shouldWarn?: boolean; + + /** + * Whether the setting should be exported in a rageshake report. + */ + shouldExportToRageshake?: boolean; } export interface IFeature extends Omit, "isFeature"> { @@ -441,6 +446,8 @@ export const SETTINGS: Settings = { controller: new InviteRulesConfigController(), supportedLevels: [SettingLevel.ACCOUNT], default: InviteRulesConfigController.default, + // Contains server names + shouldExportToRageshake: false, }, "feature_report_to_moderators": { isFeature: true, @@ -503,10 +510,14 @@ export const SETTINGS: Settings = { "mjolnirRooms": { supportedLevels: [SettingLevel.ACCOUNT], default: [], + // Contains room IDs + shouldExportToRageshake: false, }, "mjolnirPersonalRoom": { supportedLevels: [SettingLevel.ACCOUNT], default: null, + // Contains room ID + shouldExportToRageshake: false, }, "feature_html_topic": { isFeature: true, @@ -797,6 +808,8 @@ export const SETTINGS: Settings = { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, displayName: _td("settings|preferences|user_timezone"), default: "", + // Location leak + shouldExportToRageshake: false, }, "userTimezonePublish": { // This is per-device so you can avoid having devices overwrite each other. @@ -913,6 +926,8 @@ export const SETTINGS: Settings = { "custom_themes": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, default: [], + // Potential privacy leak via theme origin + shouldExportToRageshake: false, }, "use_system_theme": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, @@ -978,26 +993,36 @@ export const SETTINGS: Settings = { "language": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, default: "en", + // For privacy + shouldExportToRageshake: false, }, "breadcrumb_rooms": { // not really a setting supportedLevels: [SettingLevel.ACCOUNT], default: [], + // Contains joined rooms + shouldExportToRageshake: false, }, "recent_emoji": { // not really a setting supportedLevels: [SettingLevel.ACCOUNT], default: [], + // For privacy + shouldExportToRageshake: false, }, "SpotlightSearch.recentSearches": { // not really a setting supportedLevels: [SettingLevel.ACCOUNT], default: [], // list of room IDs, most recent first + // For privacy + shouldExportToRageshake: false, }, "showMediaEventIds": { // not really a setting supportedLevels: [SettingLevel.DEVICE], default: {}, // List of events => is visible + // Exports event IDs + shouldExportToRageshake: false, }, "SpotlightSearch.showNsfwPublicRooms": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, @@ -1007,6 +1032,8 @@ export const SETTINGS: Settings = { "room_directory_servers": { supportedLevels: [SettingLevel.ACCOUNT], default: [], + // Contains connected servers for user + shouldExportToRageshake: false, }, "integrationProvisioning": { supportedLevels: [SettingLevel.ACCOUNT], @@ -1016,6 +1043,7 @@ export const SETTINGS: Settings = { supportedLevels: [SettingLevel.ROOM_ACCOUNT, SettingLevel.ROOM_DEVICE], supportedLevelsAreOrdered: true, default: {}, // none allowed + shouldExportToRageshake: false, }, // Legacy, kept around for transitionary purposes "analyticsOptIn": { @@ -1092,6 +1120,8 @@ export const SETTINGS: Settings = { "notificationSound": { supportedLevels: LEVELS_ROOM_OR_ACCOUNT, default: false, + // Contains personal information in file name + shouldExportToRageshake: false, }, "notificationBodyEnabled": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, @@ -1120,6 +1150,8 @@ export const SETTINGS: Settings = { allow: [], deny: [], }, + // Expses widget information + shouldExportToRageshake: false, }, "breadcrumbs": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, @@ -1209,6 +1241,8 @@ export const SETTINGS: Settings = { // deprecated supportedLevels: LEVELS_ROOM_OR_ACCOUNT, default: {}, + // Sensitive information in widget ID + shouldExportToRageshake: false, }, "Widgets.layout": { supportedLevels: LEVELS_ROOM_OR_ACCOUNT, @@ -1283,6 +1317,8 @@ export const SETTINGS: Settings = { "activeCallRoomIds": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, default: [], + // Contains room IDs + shouldExportToRageshake: false, }, /** * Enable or disable the release announcement feature @@ -1402,7 +1438,7 @@ export const SETTINGS: Settings = { }, "Electron.enableContentProtection": { supportedLevels: [SettingLevel.PLATFORM], - displayName: _td("settings|preferences|enable_hardware_acceleration"), + displayName: _td("settings|preferences|enable_content_protection"), default: false, }, "Developer.elementCallUrl": { diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts index aaa836b6fc..bce943ab7a 100644 --- a/src/settings/SettingsStore.ts +++ b/src/settings/SettingsStore.ts @@ -883,6 +883,21 @@ export default class SettingsStore { logger.log(`--- END DEBUG`); } + /** + * Export all settings as a JSON object, except for settings + * blocked from being exported by `shouldExportToRageshake`. + * @returns Settings as a JSON object string. + */ + public static exportForRageshake(): string { + const settingMap: Record = {}; + for (const settingKey of (Object.keys(SETTINGS) as SettingKey[]).filter( + (s) => SETTINGS[s].shouldExportToRageshake !== false, + )) { + settingMap[settingKey] = SettingsStore.getValue(settingKey); + } + return JSON.stringify(settingMap); + } + private static getHandler(settingName: SettingKey, level: SettingLevel): SettingsHandler | null { const handlers = SettingsStore.getHandlers(settingName); if (!handlers[level]) return null; diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index d4c0f8930c..282d6f5d92 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -123,6 +123,9 @@ export class StopGapWidgetDriver extends WidgetDriver { this.allowedCapabilities.add(MatrixCapabilities.MSC4157SendDelayedEvent); this.allowedCapabilities.add(MatrixCapabilities.MSC4157UpdateDelayedEvent); + this.allowedCapabilities.add( + WidgetEventCapability.forStateEvent(EventDirection.Receive, EventType.RoomName).raw, + ); this.allowedCapabilities.add( WidgetEventCapability.forStateEvent(EventDirection.Receive, EventType.RoomMember).raw, ); diff --git a/src/toasts/SetupEncryptionToast.ts b/src/toasts/SetupEncryptionToast.ts index 802da5b895..f4aef8881f 100644 --- a/src/toasts/SetupEncryptionToast.ts +++ b/src/toasts/SetupEncryptionToast.ts @@ -30,13 +30,12 @@ const TOAST_KEY = "setupencryption"; const getTitle = (kind: Kind): string => { switch (kind) { - case Kind.SET_UP_ENCRYPTION: - return _t("encryption|set_up_toast_title"); case Kind.SET_UP_RECOVERY: return _t("encryption|set_up_recovery"); case Kind.VERIFY_THIS_SESSION: return _t("encryption|verify_toast_title"); case Kind.KEY_STORAGE_OUT_OF_SYNC: + case Kind.KEY_STORAGE_OUT_OF_SYNC_STORE: return _t("encryption|key_storage_out_of_sync"); case Kind.TURN_ON_KEY_STORAGE: return _t("encryption|turn_on_key_storage"); @@ -45,12 +44,11 @@ const getTitle = (kind: Kind): string => { const getIcon = (kind: Kind): string | undefined => { switch (kind) { - case Kind.SET_UP_ENCRYPTION: - return "secure_backup"; case Kind.SET_UP_RECOVERY: return undefined; case Kind.VERIFY_THIS_SESSION: case Kind.KEY_STORAGE_OUT_OF_SYNC: + case Kind.KEY_STORAGE_OUT_OF_SYNC_STORE: return "verification_warning"; case Kind.TURN_ON_KEY_STORAGE: return "key_storage"; @@ -59,13 +57,12 @@ const getIcon = (kind: Kind): string | undefined => { const getSetupCaption = (kind: Kind): string => { switch (kind) { - case Kind.SET_UP_ENCRYPTION: - return _t("action|continue"); case Kind.SET_UP_RECOVERY: return _t("action|continue"); case Kind.VERIFY_THIS_SESSION: return _t("action|verify"); case Kind.KEY_STORAGE_OUT_OF_SYNC: + case Kind.KEY_STORAGE_OUT_OF_SYNC_STORE: return _t("encryption|enter_recovery_key"); case Kind.TURN_ON_KEY_STORAGE: return _t("action|continue"); @@ -79,6 +76,7 @@ const getSetupCaption = (kind: Kind): string => { const getPrimaryButtonIcon = (kind: Kind): ComponentType> | undefined => { switch (kind) { case Kind.KEY_STORAGE_OUT_OF_SYNC: + case Kind.KEY_STORAGE_OUT_OF_SYNC_STORE: return KeyIcon; default: return; @@ -88,11 +86,11 @@ const getPrimaryButtonIcon = (kind: Kind): ComponentType { switch (kind) { case Kind.SET_UP_RECOVERY: - return _t("encryption|set_up_recovery_later"); - case Kind.SET_UP_ENCRYPTION: + return _t("action|dismiss"); case Kind.VERIFY_THIS_SESSION: return _t("encryption|verification|unverified_sessions_toast_reject"); case Kind.KEY_STORAGE_OUT_OF_SYNC: + case Kind.KEY_STORAGE_OUT_OF_SYNC_STORE: return _t("encryption|forgot_recovery_key"); case Kind.TURN_ON_KEY_STORAGE: return _t("action|dismiss"); @@ -101,13 +99,12 @@ const getSecondaryButtonLabel = (kind: Kind): string => { const getDescription = (kind: Kind): string => { switch (kind) { - case Kind.SET_UP_ENCRYPTION: - return _t("encryption|set_up_toast_description"); case Kind.SET_UP_RECOVERY: return _t("encryption|set_up_recovery_toast_description"); case Kind.VERIFY_THIS_SESSION: return _t("encryption|verify_toast_description"); case Kind.KEY_STORAGE_OUT_OF_SYNC: + case Kind.KEY_STORAGE_OUT_OF_SYNC_STORE: return _t("encryption|key_storage_out_of_sync_description"); case Kind.TURN_ON_KEY_STORAGE: return _t("encryption|turn_on_key_storage_description"); @@ -118,10 +115,6 @@ const getDescription = (kind: Kind): string => { * The kind of toast to show. */ export enum Kind { - /** - * Prompt the user to set up encryption - */ - SET_UP_ENCRYPTION = "set_up_encryption", /** * Prompt the user to set up a recovery key */ @@ -131,9 +124,13 @@ export enum Kind { */ VERIFY_THIS_SESSION = "verify_this_session", /** - * Prompt the user to enter their recovery key + * Prompt the user to enter their recovery key, to retrieve secrets */ KEY_STORAGE_OUT_OF_SYNC = "key_storage_out_of_sync", + /** + * Prompt the user to enter their recovery key, to store secrets + */ + KEY_STORAGE_OUT_OF_SYNC_STORE = "key_storage_out_of_sync_store", /** * Prompt the user to turn on key storage */ @@ -156,53 +153,87 @@ export const showToast = (kind: Kind): void => { } const onPrimaryClick = async (): Promise => { - if (kind === Kind.VERIFY_THIS_SESSION) { - Modal.createDialog(SetupEncryptionDialog, {}, undefined, /* priority = */ false, /* static = */ true); - } else if (kind == Kind.TURN_ON_KEY_STORAGE) { - // Open the user settings dialog to the encryption tab - const payload: OpenToTabPayload = { - action: Action.ViewUserSettings, - initialTabId: UserTab.Encryption, - }; - defaultDispatcher.dispatch(payload); - } else { - const modal = Modal.createDialog( - Spinner, - undefined, - "mx_Dialog_spinner", - /* priority */ false, - /* static */ true, - ); - try { - await accessSecretStorage(); - } catch (error) { - onAccessSecretStorageFailed(error as Error); - } finally { - modal.close(); + switch (kind) { + case Kind.SET_UP_RECOVERY: + case Kind.TURN_ON_KEY_STORAGE: { + // Open the user settings dialog to the encryption tab + const payload: OpenToTabPayload = { + action: Action.ViewUserSettings, + initialTabId: UserTab.Encryption, + }; + defaultDispatcher.dispatch(payload); + break; + } + case Kind.VERIFY_THIS_SESSION: + Modal.createDialog(SetupEncryptionDialog, {}, undefined, /* priority = */ false, /* static = */ true); + break; + case Kind.KEY_STORAGE_OUT_OF_SYNC: + case Kind.KEY_STORAGE_OUT_OF_SYNC_STORE: { + const modal = Modal.createDialog( + Spinner, + undefined, + "mx_Dialog_spinner", + /* priority */ false, + /* static */ true, + ); + try { + await accessSecretStorage(); + } catch (error) { + onAccessSecretStorageFailed(kind, error as Error); + } finally { + modal.close(); + } + break; } } }; const onSecondaryClick = async (): Promise => { - if (kind === Kind.KEY_STORAGE_OUT_OF_SYNC) { - // Open the user settings dialog to the encryption tab and start the flow to reset encryption - const payload: OpenToTabPayload = { - action: Action.ViewUserSettings, - initialTabId: UserTab.Encryption, - props: { initialEncryptionState: "reset_identity_forgot" }, - }; - defaultDispatcher.dispatch(payload); - } else if (kind === Kind.TURN_ON_KEY_STORAGE) { - // The user clicked "Dismiss": offer them "Are you sure?" - const modal = Modal.createDialog(ConfirmKeyStorageOffDialog, undefined, "mx_ConfirmKeyStorageOffDialog"); - const [dismissed] = await modal.finished; - if (dismissed) { + switch (kind) { + case Kind.SET_UP_RECOVERY: { + // Record that the user doesn't want to set up recovery const deviceListener = DeviceListener.sharedInstance(); - await deviceListener.recordKeyBackupDisabled(); + await deviceListener.recordRecoveryDisabled(); deviceListener.dismissEncryptionSetup(); + break; } - } else { - DeviceListener.sharedInstance().dismissEncryptionSetup(); + case Kind.KEY_STORAGE_OUT_OF_SYNC: { + // Open the user settings dialog to the encryption tab and start the flow to reset encryption + const payload: OpenToTabPayload = { + action: Action.ViewUserSettings, + initialTabId: UserTab.Encryption, + props: { initialEncryptionState: "reset_identity_forgot" }, + }; + defaultDispatcher.dispatch(payload); + break; + } + case Kind.KEY_STORAGE_OUT_OF_SYNC_STORE: { + // Open the user settings dialog to the encryption tab and start the flow to reset 4S + const payload: OpenToTabPayload = { + action: Action.ViewUserSettings, + initialTabId: UserTab.Encryption, + props: { initialEncryptionState: "change_recovery_key" }, + }; + defaultDispatcher.dispatch(payload); + break; + } + case Kind.TURN_ON_KEY_STORAGE: { + // The user clicked "Dismiss": offer them "Are you sure?" + const modal = Modal.createDialog( + ConfirmKeyStorageOffDialog, + undefined, + "mx_ConfirmKeyStorageOffDialog", + ); + const [dismissed] = await modal.finished; + if (dismissed) { + const deviceListener = DeviceListener.sharedInstance(); + await deviceListener.recordKeyBackupDisabled(); + deviceListener.dismissEncryptionSetup(); + } + break; + } + default: + DeviceListener.sharedInstance().dismissEncryptionSetup(); } }; @@ -210,10 +241,16 @@ export const showToast = (kind: Kind): void => { * We tried to accessSecretStorage, which triggered us to ask for the * recovery key, but this failed. If the user just gave up, that is fine, * but if not, that means downloading encryption info from 4S did not fix - * the problem we identified. Presumably, something is wrong with what - * they have in 4S: we tell them to reset their identity. + * the problem we identified. Presumably, something is wrong with what they + * have in 4S. If we were trying to fetch secrets from 4S, we tell them to + * reset their identity, to reset everything. If we were trying to store + * secrets in 4S, or set up recovery, we tell them to change their recovery + * key, to create a new 4S that we can store the secrets in. */ - const onAccessSecretStorageFailed = (error: Error): void => { + const onAccessSecretStorageFailed = ( + kind: Kind.KEY_STORAGE_OUT_OF_SYNC | Kind.KEY_STORAGE_OUT_OF_SYNC_STORE, + error: Error, + ): void => { if (error instanceof AccessCancelledError) { // The user cancelled the dialog - just allow it to close } else { @@ -221,7 +258,10 @@ export const showToast = (kind: Kind): void => { const payload: OpenToTabPayload = { action: Action.ViewUserSettings, initialTabId: UserTab.Encryption, - props: { initialEncryptionState: "reset_identity_sync_failed" }, + props: { + initialEncryptionState: + kind === Kind.KEY_STORAGE_OUT_OF_SYNC ? "reset_identity_sync_failed" : "change_recovery_key", + }, }; defaultDispatcher.dispatch(payload); } diff --git a/src/utils/EventUtils.ts b/src/utils/EventUtils.ts index 9c387b16b0..ecaa7e06ec 100644 --- a/src/utils/EventUtils.ts +++ b/src/utils/EventUtils.ts @@ -30,6 +30,7 @@ import { type TimelineRenderingType } from "../contexts/RoomContext"; import { launchPollEditor } from "../components/views/messages/MPollBody"; import { Action } from "../dispatcher/actions"; import { type ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload"; +import ModuleApi from "../modules/Api"; /** * Returns whether an event should allow actions like reply, reactions, edit, etc. @@ -77,6 +78,10 @@ export function canEditContent(matrixClient: MatrixClient, mxEvent: MatrixEvent) return false; } + if (ModuleApi.customComponents.getHintsForMessage(mxEvent)?.allowEditingEvent === false) { + return false; + } + const { msgtype, body } = mxEvent.getOriginalContent(); return ( M_POLL_START.matches(mxEvent.getType()) || diff --git a/src/vector/mobile_guide/assets/app-store-badge.svg b/src/vector/mobile_guide/assets/app-store-badge.svg new file mode 100755 index 0000000000..072b425a1a --- /dev/null +++ b/src/vector/mobile_guide/assets/app-store-badge.svg @@ -0,0 +1,46 @@ + + Download_on_the_App_Store_Badge_US-UK_RGB_blk_4SVG_092917 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/vector/mobile_guide/assets/bottom-gradient.svg b/src/vector/mobile_guide/assets/bottom-gradient.svg new file mode 100644 index 0000000000..0740440946 --- /dev/null +++ b/src/vector/mobile_guide/assets/bottom-gradient.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/vector/mobile_guide/assets/element-logo.svg b/src/vector/mobile_guide/assets/element-logo.svg new file mode 100644 index 0000000000..d2cb52e498 --- /dev/null +++ b/src/vector/mobile_guide/assets/element-logo.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/vector/mobile_guide/assets/google-play-badge.svg b/src/vector/mobile_guide/assets/google-play-badge.svg new file mode 100644 index 0000000000..fac62d70ed --- /dev/null +++ b/src/vector/mobile_guide/assets/google-play-badge.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/vector/mobile_guide/index.css b/src/vector/mobile_guide/index.css new file mode 100644 index 0000000000..cb51c3fb24 --- /dev/null +++ b/src/vector/mobile_guide/index.css @@ -0,0 +1,183 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +@import url("@vector-im/compound-design-tokens/assets/web/css/compound-design-tokens.css"); + +html { + min-height: 100%; + position: relative; +} + +body { + background: var(--cpd-color-bg-canvas-default); + max-width: 680px; + margin: var(--cpd-space-0x) auto; + padding-bottom: 178px; /* Match the height of mx_BottomGradient */ + font-family: var(--cpd-font-family-sans); + font-size: var(--cpd-font-size-body-lg); /* Design says 16px, this is 17px */ + color: var(--cpd-color-text-primary); +} + +hr { + border: none; + height: var(--cpd-border-width-1); + background-color: var( + --cpd-color-bg-subtle-primary /* Design uses Border token from "Compound Marketing" set, but this matches. */ + ); + color: var( + --cpd-color-bg-subtle-primary /* Design uses Border token from "Compound Marketing" set, but this matches. */ + ); + margin: 0; +} + +p { + margin: var(--cpd-space-1x) var(--cpd-space-0x); + padding: var(--cpd-space-0x); +} + +.mx_Button { + border: 0; + border-radius: 100px; + min-width: 80px; + background-color: var(--cpd-color-bg-action-primary-rest); + color: var(--cpd-color-text-on-solid-primary); + cursor: pointer; + padding: 12px 22px; + word-break: break-word; + text-decoration: none; +} + +#deep_link_button { + margin-top: 12px; + display: inline-block; + width: auto; + box-sizing: border-box; +} + +.mx_StoreLinks { + margin: 15px 0 12px 0; +} + +.mx_StoreBadge { + text-decoration: none !important; + margin: 16px 16px 16px 0px; +} + +#f_droid_link { + color: var(--cpd-color-text-action-accent); + font-weight: bold; + text-decoration: none; +} + +#f_droid_link:visited { + color: var(--cpd-color-text-action-accent); +} + +.mx_HomePage_header { + color: var(--cpd-color-text-secondary); + align-items: center; + justify-content: center; + text-align: center; + padding-top: 48px; + padding-bottom: 48px; +} + +.mx_HomePage_header #header_title { + margin-top: 8px; + margin-bottom: 0px; +} + +.mx_HomePage h3 { + margin-top: 30px; +} + +.mx_HomePage_col { + display: flex; + flex-direction: row; +} + +.mx_HomePage_row { + flex: 1 1 0; + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: flex-start; +} + +.mx_HomePage_container { + margin: 10px 20px; +} + +.mx_HomePage_errorContainer { + display: none; /* shown in JS if needed */ + margin: 20px; + border: var(--cpd-border-width-1) solid var(--cpd-color-border-critical-primary); + background-color: var(--cpd-color-bg-critical-subtle); + padding: 5px; +} + +.mx_HomePage_container h1, +.mx_HomePage_container h2, +.mx_HomePage_container h3, +.mx_HomePage_container h4 { + font-weight: var(--cpd-font-weight-semibold); + font-size: var(--cpd-font-size-body-lg); /* Design says 16px, this is 17px */ + margin-bottom: 8px; + margin-top: 4px; +} + +.mx_Spacer { + margin-top: 48px; +} + +.mx_DesktopLink { + color: var(--cpd-color-text-action-accent); + font-weight: var(--cpd-font-weight-semibold); + text-decoration: none; +} + +/* + * The bottom gradient is a full-width background image that stretches horizontally across the page. + * It is positioned pinned to the bottom of the viewport unless the content is taller than the viewport, + * in which case it will be pinned to the bottom of the content. + */ +.mx_BottomGradient { + position: absolute; + bottom: 0; + left: 0; + right: 0; + width: 100vw; + height: 178px; /* Match the height of assets/bottom-gradient.svg so the gradient only stretches horizontally */ + background-image: url("./assets/bottom-gradient.svg"); + background-size: 100% 100%; + background-repeat: no-repeat; + z-index: -1; + margin-left: calc(50% - 50vw); /* Center the gradient regardless of body width */ +} + +.mx_HomePage_step_number { + display: flex; + align-items: flex-start; + margin-right: 8px; +} + +.mx_HomePage_step_number span { + display: flex; + align-items: center; + justify-content: center; + width: var(--cpd-space-6x); + height: var(--cpd-space-6x); + border-radius: 50%; + border: var(--cpd-border-width-1) solid var(--cpd-color-bg-subtle-primary); /* Not a border token, but matches the Design (Border token from the "Compound Marketing" set). */ + background-color: transparent; + color: var(--cpd-color-text-secondary); + font-size: var(--cpd-font-size-body-md); /* Design says 14px, this is 15px */ +} + +#step2_description { + color: var(--cpd-color-text-secondary); +} diff --git a/src/vector/mobile_guide/index.html b/src/vector/mobile_guide/index.html index d58842d6a6..3bd8a8e20d 100644 --- a/src/vector/mobile_guide/index.html +++ b/src/vector/mobile_guide/index.html @@ -1,141 +1,13 @@ + Element Mobile Guide - - - + @@ -144,648 +16,90 @@
-
- -

Set up Element on iOS or Android

-
-
- -
+ +
+
+
diff --git a/src/vector/mobile_guide/index.ts b/src/vector/mobile_guide/index.ts index ae769039a8..e9732b4035 100644 --- a/src/vector/mobile_guide/index.ts +++ b/src/vector/mobile_guide/index.ts @@ -5,9 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ +import "./index.css"; +import "@fontsource/inter/400.css"; +import "@fontsource/inter/600.css"; + import { logger } from "matrix-js-sdk/src/logger"; import { getVectorConfig } from "../getconfig"; +import { MobileAppVariant, mobileApps, updateMobilePage } from "./mobile-apps.ts"; function onBackToElementClick(): void { // Cookie should expire in 4 hours @@ -38,18 +43,19 @@ function renderConfigError(message: string): void { } async function initPage(): Promise { - document.getElementById("back_to_element_button")!.onclick = onBackToElementClick; - const config = await getVectorConfig(".."); // We manually parse the config similar to how validateServerConfig works because // calling that function pulls in roughly 4mb of JS we don't use. const wkConfig = config?.["default_server_config"]; // overwritten later under some conditions - const serverName = config?.["default_server_name"]; + let serverName = config?.["default_server_name"]; const defaultHsUrl = config?.["default_hs_url"]; const defaultIsUrl = config?.["default_is_url"]; + const appVariant = (config?.["mobile_guide_app_variant"] as MobileAppVariant) ?? MobileAppVariant.X; + const metadata = mobileApps[appVariant] ?? mobileApps[MobileAppVariant.X]; // Additional fallback in case mobile_guide_app_variant has an unexpected value. + const incompatibleOptions = [wkConfig, serverName, defaultHsUrl].filter((i) => !!i); if (defaultHsUrl && (wkConfig || serverName)) { return renderConfigError( @@ -66,6 +72,7 @@ async function initPage(): Promise { if (!serverName && typeof wkConfig?.["m.homeserver"]?.["base_url"] === "string") { hsUrl = wkConfig["m.homeserver"]["base_url"]; + serverName = wkConfig["m.homeserver"]["server_name"]; if (typeof wkConfig["m.identity_server"]?.["base_url"] === "string") { isUrl = wkConfig["m.identity_server"]["base_url"]; @@ -110,21 +117,21 @@ async function initPage(): Promise { if (hsUrl && !hsUrl.endsWith("/")) hsUrl += "/"; if (isUrl && !isUrl.endsWith("/")) isUrl += "/"; - if (hsUrl !== "https://matrix.org/") { - let url = "https://mobile.element.io?hs_url=" + encodeURIComponent(hsUrl); + let deepLinkUrl = `https://mobile.element.io${metadata.deepLinkPath}`; + if (metadata.usesLegacyDeepLink) { + deepLinkUrl += `?hs_url=${encodeURIComponent(hsUrl)}`; if (isUrl) { - document.getElementById("custom_is")!.style.display = "block"; - document.getElementById("is_url")!.style.display = "block"; - document.getElementById("is_url")!.innerText = isUrl; - url += "&is_url=" + encodeURIComponent(isUrl ?? ""); + deepLinkUrl += `&is_url=${encodeURIComponent(isUrl)}`; } - - (document.getElementById("configure_element_button") as HTMLAnchorElement).href = url; - document.getElementById("step1_heading")!.innerHTML = "1: Install the app"; - document.getElementById("step2_container")!.style.display = "block"; - document.getElementById("hs_url")!.innerText = hsUrl; + } else if (serverName) { + deepLinkUrl += `?account_provider=${serverName}`; } + + // Not part of updateMobilePage as the link is only shown on mobile_guide and not on mobile.element.io + document.getElementById("back_to_element_button")!.onclick = onBackToElementClick; + + updateMobilePage(metadata, deepLinkUrl, serverName ?? hsUrl); } void initPage(); diff --git a/src/vector/mobile_guide/mobile-apps.ts b/src/vector/mobile_guide/mobile-apps.ts new file mode 100644 index 0000000000..ff3430d6b7 --- /dev/null +++ b/src/vector/mobile_guide/mobile-apps.ts @@ -0,0 +1,88 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +/* + * Shared code that is used by the mobile guide and the mobile.element.io site. + */ + +export enum MobileAppVariant { + Classic = "element-classic", + X = "element", + Pro = "element-pro", +} + +export interface MobileAppMetadata { + name: string; + appleAppId: string; + appStoreUrl: string; + playStoreUrl: string; + fDroidUrl?: string; + deepLinkPath: string; + usesLegacyDeepLink: boolean; + isProApp: boolean; +} + +export const mobileApps: Record = { + [MobileAppVariant.Classic]: { + name: "Element", + appleAppId: "id1083446067", + appStoreUrl: "https://apps.apple.com/app/element-messenger/id1083446067", + playStoreUrl: "https://play.google.com/store/apps/details?id=im.vector.app", + fDroidUrl: "https://f-droid.org/packages/im.vector.app", + deepLinkPath: "", + usesLegacyDeepLink: true, + isProApp: false, + }, + [MobileAppVariant.X]: { + name: "Element X", + appleAppId: "id1631335820", + appStoreUrl: "https://apps.apple.com/app/element-x-secure-chat-call/id1631335820", + playStoreUrl: "https://play.google.com/store/apps/details?id=io.element.android.x", + fDroidUrl: "https://f-droid.org/packages/io.element.android.x", + deepLinkPath: "/element", + usesLegacyDeepLink: false, + isProApp: false, + }, + [MobileAppVariant.Pro]: { + name: "Element Pro", + appleAppId: "id6502951615", + appStoreUrl: "https://apps.apple.com/app/element-pro-for-work/id6502951615", + playStoreUrl: "https://play.google.com/store/apps/details?id=io.element.enterprise", + deepLinkPath: "/element-pro", + usesLegacyDeepLink: false, + isProApp: true, + }, +}; + +export function updateMobilePage(metadata: MobileAppMetadata, deepLinkUrl: string, server: string | undefined): void { + const appleMeta = document.querySelector('meta[name="apple-itunes-app"]') as Element; + appleMeta.setAttribute("content", `app-id=${metadata.appleAppId}`); + + if (server) { + (document.getElementById("header_title") as HTMLHeadingElement).innerText = `Join ${server} on Element`; + } + (document.getElementById("app_store_link") as HTMLAnchorElement).href = metadata.appStoreUrl; + (document.getElementById("play_store_link") as HTMLAnchorElement).href = metadata.playStoreUrl; + + if (metadata.fDroidUrl) { + (document.getElementById("f_droid_link") as HTMLAnchorElement).href = metadata.fDroidUrl; + } else { + document.getElementById("f_droid_section")!.style.display = "none"; + } + + const step1Heading = document.getElementById("step1_heading")!; + step1Heading.innerHTML = step1Heading!.innerHTML.replace("Element", metadata.name); + + // Step 2 is only shown on the mobile guide, not on mobile.element.io + if (document.getElementById("step2_container")) { + document.getElementById("step2_container")!.style.display = "block"; + if (metadata.isProApp) { + document.getElementById("step2_description")!.innerHTML = "Use your work email to join"; + } + (document.getElementById("deep_link_button") as HTMLAnchorElement).href = deepLinkUrl; + } +} diff --git a/src/verification.ts b/src/verification.ts index 5dc3ea2979..e3e6fb2293 100644 --- a/src/verification.ts +++ b/src/verification.ts @@ -7,35 +7,24 @@ Please see LICENSE files in the repository root for full details. */ import { type User, type MatrixClient, type RoomMember } from "matrix-js-sdk/src/matrix"; -import { CrossSigningKey, type VerificationRequest } from "matrix-js-sdk/src/crypto-api"; +import { type VerificationRequest } from "matrix-js-sdk/src/crypto-api"; import dis from "./dispatcher/dispatcher"; import { RightPanelPhases } from "./stores/right-panel/RightPanelStorePhases"; -import { accessSecretStorage } from "./SecurityManager"; import RightPanelStore from "./stores/right-panel/RightPanelStore"; import { type IRightPanelCardState } from "./stores/right-panel/RightPanelStoreIPanelState"; import { findDMForUser } from "./utils/dm/findDMForUser"; -async function enable4SIfNeeded(matrixClient: MatrixClient): Promise { - const crypto = matrixClient.getCrypto(); - if (!crypto) return false; - const usk = await crypto.getCrossSigningKeyId(CrossSigningKey.UserSigning); - if (!usk) { - await accessSecretStorage(); - return false; - } - - return true; -} - +/** + * Verify another user. + * + * Note: cross-signing must be set up before calling this function. + */ export async function verifyUser(matrixClient: MatrixClient, user: User): Promise { if (matrixClient.isGuest()) { dis.dispatch({ action: "require_registration" }); return; } - if (!(await enable4SIfNeeded(matrixClient))) { - return; - } const existingRequest = pendingVerificationRequestForUser(matrixClient, user); setRightPanel({ member: user, verificationRequest: existingRequest }); } diff --git a/test/test-utils/client.ts b/test/test-utils/client.ts index cdda7b7dea..658536825b 100644 --- a/test/test-utils/client.ts +++ b/test/test-utils/client.ts @@ -153,7 +153,11 @@ export const mockClientMethodsCrypto = (): Partial< > => ({ isKeyBackupKeyStored: jest.fn(), getCrossSigningCacheCallbacks: jest.fn().mockReturnValue({ getCrossSigningKeyCache: jest.fn() }), - secretStorage: { hasKey: jest.fn(), isStored: jest.fn().mockResolvedValue(null) }, + secretStorage: { + hasKey: jest.fn(), + isStored: jest.fn().mockResolvedValue(null), + getDefaultKeyId: jest.fn().mockResolvedValue(null), + }, getCrypto: jest.fn().mockReturnValue({ getUserDeviceInfo: jest.fn(), getCrossSigningStatus: jest.fn().mockResolvedValue({ diff --git a/test/test-utils/utilities.ts b/test/test-utils/utilities.ts index dd3e5e6a62..fef20bddad 100644 --- a/test/test-utils/utilities.ts +++ b/test/test-utils/utilities.ts @@ -188,35 +188,11 @@ export const waitEnoughCyclesForModal = async ({ }; /** - * A horrible hack necessary to make sure modals don't leak and pollute tests. - * `jest-matrix-react` automatic cleanup function does not pick up the async modal - * rendering and the modals don't unmount when the component unmounts. We should strive - * to fix this. + * Clears all modals that are currently open. */ export const clearAllModals = async (): Promise => { // Prevent modals from leaking and polluting other tests - let keepClosingModals = true; - while (keepClosingModals) { - keepClosingModals = await act(() => Modal.closeCurrentModal()); - - // Then wait for the screen to update (probably React rerender and async/await). - // Important for tests using Jest fake timers to not get into an infinite loop - // of removing the same modal because the promises don't flush otherwise. - // - // XXX: Maybe in the future with Jest 29.5.0+, we could use `runAllTimersAsync` instead. - - // this is called in some places where timers are not faked - // which causes a lot of noise in the console - // to make a hack even hackier check if timers are faked using a weird trick from github - // then call the appropriate promise flusher - // https://github.com/facebook/jest/issues/10555#issuecomment-1136466942 - const jestTimersFaked = setTimeout.name === "setTimeout"; - if (jestTimersFaked) { - await flushPromisesWithFakeTimers(); - } else { - await flushPromises(); - } - } + act(() => Modal.forceCloseAllModals()); }; /** Install a stub object at `navigator.mediaDevices` */ diff --git a/test/unit-tests/DeviceListener-test.ts b/test/unit-tests/DeviceListener-test.ts index 62221d0665..2713423051 100644 --- a/test/unit-tests/DeviceListener-test.ts +++ b/test/unit-tests/DeviceListener-test.ts @@ -307,15 +307,6 @@ describe("DeviceListener", () => { jest.spyOn(mockClient.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true); }); - it("hides setup encryption toast when cross signing and secret storage are ready", async () => { - mockCrypto!.isCrossSigningReady.mockResolvedValue(true); - mockCrypto!.isSecretStorageReady.mockResolvedValue(true); - mockCrypto!.getActiveSessionBackupVersion.mockResolvedValue("1"); - - await createAndStart(); - expect(SetupEncryptionToast.hideToast).toHaveBeenCalled(); - }); - it("hides setup encryption toast when it is dismissed", async () => { const instance = await createAndStart(); instance.dismissEncryptionSetup(); @@ -360,7 +351,15 @@ describe("DeviceListener", () => { ); }); - it("shows an out-of-sync toast when one of the secrets is missing", async () => { + it("hides setup encryption toast when cross signing and secret storage are ready", async () => { + mockCrypto!.isSecretStorageReady.mockResolvedValue(true); + mockCrypto!.getActiveSessionBackupVersion.mockResolvedValue("1"); + + await createAndStart(); + expect(SetupEncryptionToast.hideToast).toHaveBeenCalled(); + }); + + it("shows an out-of-sync toast when one of the secrets is missing locally", async () => { mockCrypto!.getCrossSigningStatus.mockResolvedValue({ publicKeysOnDevice: true, privateKeysInSecretStorage: true, @@ -378,7 +377,7 @@ describe("DeviceListener", () => { ); }); - it("hides the out-of-sync toast when one of the secrets is missing", async () => { + it("hides the out-of-sync toast after we receive the missing secrets", async () => { mockCrypto!.isSecretStorageReady.mockResolvedValue(true); mockCrypto!.getActiveSessionBackupVersion.mockResolvedValue("1"); @@ -427,6 +426,18 @@ describe("DeviceListener", () => { SetupEncryptionToast.Kind.SET_UP_RECOVERY, ); }); + + it("shows an out-of-sync toast when one of the secrets is missing from 4S", async () => { + mockCrypto.getKeyBackupInfo.mockResolvedValue({} as unknown as KeyBackupInfo); + mockCrypto.getActiveSessionBackupVersion.mockResolvedValue("1"); + mockClient.secretStorage.getDefaultKeyId.mockResolvedValue("foo"); + + await createAndStart(); + + expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith( + SetupEncryptionToast.Kind.KEY_STORAGE_OUT_OF_SYNC_STORE, + ); + }); }); }); @@ -448,6 +459,16 @@ describe("DeviceListener", () => { }); it("dispatches keybackup event when key backup is not enabled", async () => { + mockCrypto!.isCrossSigningReady.mockResolvedValue(true); + + // current device is verified + mockCrypto!.getDeviceVerificationStatus.mockResolvedValue( + new DeviceVerificationStatus({ + trustCrossSignedDevices: true, + crossSigningVerified: true, + }), + ); + mockCrypto.getActiveSessionBackupVersion.mockResolvedValue(null); mockClient.getAccountDataFromServer.mockImplementation((eventType) => eventType === BACKUP_DISABLED_ACCOUNT_DATA_KEY ? ({ disabled: true } as any) : null, @@ -480,6 +501,15 @@ describe("DeviceListener", () => { }); }); + it("sets the recovery account data when we call recordRecoveryDisabled", async () => { + const instance = await createAndStart(); + await instance.recordRecoveryDisabled(); + + expect(mockClient.setAccountData).toHaveBeenCalledWith("io.element.recovery", { + enabled: false, + }); + }); + describe("when crypto is in use and set up", () => { beforeEach(() => { // Encryption is in use diff --git a/test/unit-tests/SecurityManager-test.ts b/test/unit-tests/SecurityManager-test.ts index 8c1671b52e..20d93b2c24 100644 --- a/test/unit-tests/SecurityManager-test.ts +++ b/test/unit-tests/SecurityManager-test.ts @@ -67,6 +67,17 @@ describe("SecurityManager", () => { await accessSecretStorage(jest.fn()); }).rejects.toThrow("End-to-end encryption is disabled - unable to access secret storage"); }); + + it("throws if there is no 4S", async () => { + // Given a client with no default 4S key ID + stubClient(); + + // When I run accessSecretStorage + // Then we throw an error + await expect(async () => { + await accessSecretStorage(jest.fn()); + }).rejects.toThrow("Secret storage has not been created yet"); + }); }); it("should show CreateSecretStorageDialog if forceReset=true", async () => { diff --git a/test/unit-tests/async-components/dialogs/security/RecoveryMethodRemovedDialog-test.tsx b/test/unit-tests/async-components/dialogs/security/RecoveryMethodRemovedDialog-test.tsx index ec5e714286..8779748b8e 100644 --- a/test/unit-tests/async-components/dialogs/security/RecoveryMethodRemovedDialog-test.tsx +++ b/test/unit-tests/async-components/dialogs/security/RecoveryMethodRemovedDialog-test.tsx @@ -9,7 +9,9 @@ import React from "react"; import { render, screen, fireEvent, waitFor } from "jest-matrix-react"; import RecoveryMethodRemovedDialog from "../../../../../src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog"; -import Modal from "../../../../../src/Modal.tsx"; +import dispatch from "../../../../../src/dispatcher/dispatcher"; +import { Action } from "../../../../../src/dispatcher/actions"; +import { UserTab } from "../../../../../src/components/views/dialogs/UserTab"; describe("", () => { afterEach(() => { @@ -18,16 +20,15 @@ describe("", () => { it("should open CreateKeyBackupDialog on primary action click", async () => { const onFinished = jest.fn(); - const spy = jest.spyOn(Modal, "createDialog"); - jest.mock("../../../../../src/async-components/views/dialogs/security/CreateKeyBackupDialog", () => ({ - __test: true, - __esModule: true, - default: () => mocked dialog, - })); + jest.spyOn(dispatch, "dispatch"); render(); fireEvent.click(screen.getByRole("button", { name: "Set up Secure Messages" })); - await waitFor(() => expect(spy).toHaveBeenCalledTimes(1)); - expect((spy.mock.lastCall![0] as any)._payload._result).toEqual(expect.objectContaining({ __test: true })); + await waitFor(() => + expect(dispatch.dispatch).toHaveBeenCalledWith({ + action: Action.ViewUserSettings, + initialTabId: UserTab.Encryption, + }), + ); }); }); diff --git a/test/unit-tests/components/structures/FileDropTarget-test.tsx b/test/unit-tests/components/structures/FileDropTarget-test.tsx new file mode 100644 index 0000000000..20b02f9fa5 --- /dev/null +++ b/test/unit-tests/components/structures/FileDropTarget-test.tsx @@ -0,0 +1,59 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import React from "react"; +import { mocked } from "jest-mock"; +import { render, fireEvent } from "jest-matrix-react"; +import { Room } from "matrix-js-sdk/src/matrix"; + +import FileDropTarget from "../../../../src/components/structures/FileDropTarget.tsx"; +import { stubClient } from "../../../test-utils"; + +describe("FileDropTarget", () => { + let room: Room; + beforeEach(() => { + const client = stubClient(); + room = new Room("!roomId:example.com", client, client.getUserId()!); + room.currentState.maySendMessage = jest.fn().mockReturnValue(true); + }); + + it("should render nothing when idle", () => { + const element = document.createElement("div"); + const onFileDrop = jest.fn(); + + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); + + it("should render drop file prompt on mouse over with file if permissions allow", () => { + const element = document.createElement("div"); + const onFileDrop = jest.fn(); + mocked(room.currentState.maySendMessage).mockReturnValue(true); + + const { asFragment } = render(); + fireEvent.dragEnter(element, { + dataTransfer: { + types: ["Files"], + }, + }); + expect(asFragment()).toMatchSnapshot(); + }); + + it("should not render drop file prompt on mouse over with file if permissions do not allow", () => { + const element = document.createElement("div"); + const onFileDrop = jest.fn(); + mocked(room.currentState.maySendMessage).mockReturnValue(false); + + const { asFragment } = render(); + fireEvent.dragEnter(element, { + dataTransfer: { + types: ["Files"], + }, + }); + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/test/unit-tests/components/structures/__snapshots__/FileDropTarget-test.tsx.snap b/test/unit-tests/components/structures/__snapshots__/FileDropTarget-test.tsx.snap new file mode 100644 index 0000000000..367a456a31 --- /dev/null +++ b/test/unit-tests/components/structures/__snapshots__/FileDropTarget-test.tsx.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FileDropTarget should not render drop file prompt on mouse over with file if permissions do not allow 1`] = ``; + +exports[`FileDropTarget should render drop file prompt on mouse over with file if permissions allow 1`] = ` + +
+ + Drop file here to upload +
+
+`; + +exports[`FileDropTarget should render nothing when idle 1`] = ``; diff --git a/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap b/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap index d33ff73307..1196ce2202 100644 --- a/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap +++ b/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap @@ -1809,7 +1809,11 @@ exports[`RoomView should not display the timeline when the room encryption is lo
+ > +
+
({ + createDialog: jest.fn(), +})); + +jest.mock("../../../../../../src/components/views/right_panel/UserInfo", () => ({ + warnSelfDemote: jest.fn(), +})); + +describe("UserInfoAdminPowerlevelViewModel", () => { + const defaultRoomId = "!fkfk"; + const defaultUserId = "@user:example.com"; + const defaultMeId = "@me:example.com"; + const selfUser = new RoomMember(defaultRoomId, defaultMeId); + const defaultMember = new RoomMember(defaultRoomId, defaultUserId); + const startPowerLevel = 50; + const changedPowerLevel = 100; + + let mockClient: Mocked; + let mockRoom: Mocked; + let defaultProps: { + user: RoomMember; + room: Room; + roomPermissions: IRoomPermissions; + }; + + beforeEach(() => { + defaultProps = { + user: defaultMember, + room: mockRoom, + roomPermissions: { + modifyLevelMax: 100, + canEdit: false, + canInvite: false, + }, + }; + + mockRoom = mocked({ + roomId: defaultRoomId, + getType: jest.fn().mockReturnValue(undefined), + isSpaceRoom: jest.fn().mockReturnValue(false), + getMember: jest.fn().mockReturnValue(undefined), + getMxcAvatarUrl: jest.fn().mockReturnValue("mock-avatar-url"), + name: "test room", + on: jest.fn(), + off: jest.fn(), + currentState: { + getStateEvents: jest.fn(), + on: jest.fn(), + off: jest.fn(), + }, + getEventReadUpTo: jest.fn(), + } as unknown as Room); + + mockClient = mocked({ + getUser: jest.fn(), + isGuest: jest.fn().mockReturnValue(false), + isUserIgnored: jest.fn(), + getIgnoredUsers: jest.fn(), + setIgnoredUsers: jest.fn(), + getUserId: jest.fn(), + getSafeUserId: jest.fn(), + getDomain: jest.fn(), + on: jest.fn(), + off: jest.fn(), + isSynapseAdministrator: jest.fn().mockResolvedValue(false), + doesServerSupportUnstableFeature: jest.fn().mockReturnValue(false), + doesServerSupportExtendedProfiles: jest.fn().mockResolvedValue(false), + getExtendedProfileProperty: jest.fn().mockRejectedValue(new Error("Not supported")), + mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"), + removeListener: jest.fn(), + currentState: { + on: jest.fn(), + }, + getRoom: jest.fn(), + credentials: {}, + setPowerLevel: jest.fn().mockResolvedValueOnce({ event_id: "123" }), + } as unknown as MatrixClient); + + jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient); + jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(mockClient); + + (Modal.createDialog as jest.Mock).mockImplementation(() => ({ + finished: Promise.resolve([true]), + })); + (warnSelfDemote as jest.Mock).mockResolvedValue(true); + }); + + const renderComponentHook = (props = defaultProps, client = mockClient) => { + return renderHook( + () => useUserInfoPowerlevelViewModel(props.user, props.room), + withClientContextRenderOptions(client), + ); + }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should give default power level", () => { + const defaultPowerLevel = 1; + const powerLevelEvent = new MatrixEvent({ + type: EventType.RoomPowerLevels, + content: { users: { [defaultUserId]: defaultPowerLevel }, users_default: defaultPowerLevel }, + }); + mockRoom.currentState.getStateEvents.mockReturnValue(powerLevelEvent); + + const { result } = renderComponentHook({ ...defaultProps, room: mockRoom }); + + expect(result.current.powerLevelUsersDefault).toBe(defaultPowerLevel); + }); + + it("handles successful power level change", async () => { + const powerLevelEvent = new MatrixEvent({ + type: EventType.RoomPowerLevels, + content: { users: { [defaultUserId]: startPowerLevel }, users_default: 1 }, + }); + mockRoom.currentState.getStateEvents.mockReturnValue(powerLevelEvent); + mockClient.getSafeUserId.mockReturnValueOnce(defaultUserId); + mockClient.getUserId.mockReturnValueOnce(defaultUserId); + + const { result } = renderComponentHook({ ...defaultProps, room: mockRoom }, mockClient); + + await result.current.onPowerChange(changedPowerLevel); + + expect(mockClient.setPowerLevel).toHaveBeenCalledTimes(1); + expect(mockClient.setPowerLevel).toHaveBeenCalledWith(mockRoom.roomId, defaultMember.userId, changedPowerLevel); + }); + + it("shows warning when promoting user to higher power level", async () => { + const powerLevelEvent = new MatrixEvent({ + type: EventType.RoomPowerLevels, + content: { + users: { + [defaultUserId]: startPowerLevel, + [defaultMeId]: startPowerLevel, + }, + users_default: 1, + }, + }); + mockRoom.currentState.getStateEvents.mockReturnValue(powerLevelEvent); + mockClient.getUserId.mockReturnValue(defaultMeId); + + const { result } = renderComponentHook({ ...defaultProps, room: mockRoom }, mockClient); + + await result.current.onPowerChange(changedPowerLevel); + + expect(Modal.createDialog).toHaveBeenCalled(); + expect(mockClient.setPowerLevel).toHaveBeenCalled(); + }); + + it("shows warning when self-demoting", async () => { + const powerLevelEvent = new MatrixEvent({ + type: EventType.RoomPowerLevels, + content: { + users: { [defaultMeId]: changedPowerLevel }, + users_default: 1, + }, + }); + mockRoom.currentState.getStateEvents.mockReturnValue(powerLevelEvent); + mockClient.getUserId.mockReturnValue(defaultMeId); + + const { result } = renderComponentHook({ ...defaultProps, room: mockRoom, user: selfUser }, mockClient); + + await result.current.onPowerChange(startPowerLevel); + + expect(warnSelfDemote).toHaveBeenCalled(); + expect(mockClient.setPowerLevel).toHaveBeenCalled(); + }); + + it("cancels power level change when user declines warning", async () => { + (Modal.createDialog as jest.Mock).mockImplementation(() => ({ + finished: Promise.resolve([false]), + })); + + const powerLevelEvent = new MatrixEvent({ + type: EventType.RoomPowerLevels, + content: { + users: { + [defaultUserId]: startPowerLevel, + "@me:example.com": startPowerLevel, + }, + users_default: 1, + }, + }); + mockRoom.currentState.getStateEvents.mockReturnValue(powerLevelEvent); + mockClient.getUserId.mockReturnValue(defaultMeId); + + const { result } = renderComponentHook({ ...defaultProps, room: mockRoom }, mockClient); + + await result.current.onPowerChange(changedPowerLevel); + + expect(Modal.createDialog).toHaveBeenCalled(); + expect(mockClient.setPowerLevel).not.toHaveBeenCalled(); + }); + + it("handles missing power level event", async () => { + mockRoom.currentState.getStateEvents.mockReturnValue(null); + + const { result } = renderComponentHook({ ...defaultProps, room: mockRoom }, mockClient); + + await result.current.onPowerChange(changedPowerLevel); + + expect(mockClient.setPowerLevel).not.toHaveBeenCalled(); + }); +}); diff --git a/test/unit-tests/components/viewmodels/roomlist/RoomListItemViewModel-test.tsx b/test/unit-tests/components/viewmodels/roomlist/RoomListItemViewModel-test.tsx index c58ae4168c..96bc53016e 100644 --- a/test/unit-tests/components/viewmodels/roomlist/RoomListItemViewModel-test.tsx +++ b/test/unit-tests/components/viewmodels/roomlist/RoomListItemViewModel-test.tsx @@ -73,6 +73,15 @@ describe("RoomListItemViewModel", () => { ); }); + it("should show context menu if user has access to options menu", async () => { + mocked(hasAccessToOptionsMenu).mockReturnValue(true); + const { result: vm } = renderHook( + () => useRoomListItemViewModel(room), + withClientContextRenderOptions(room.client), + ); + expect(vm.current.showContextMenu).toBe(true); + }); + it("should show hover menu if user has access to options menu", async () => { mocked(hasAccessToOptionsMenu).mockReturnValue(true); const { result: vm } = renderHook( diff --git a/test/unit-tests/components/views/dialogs/BugReportDialog-test.tsx b/test/unit-tests/components/views/dialogs/BugReportDialog-test.tsx index 955ac64b2d..2cd6d609ea 100644 --- a/test/unit-tests/components/views/dialogs/BugReportDialog-test.tsx +++ b/test/unit-tests/components/views/dialogs/BugReportDialog-test.tsx @@ -16,6 +16,7 @@ import BugReportDialog, { } from "../../../../../src/components/views/dialogs/BugReportDialog"; import SdkConfig from "../../../../../src/SdkConfig"; import { type ConsoleLogger } from "../../../../../src/rageshake/rageshake"; +import SettingsStore from "../../../../../src/settings/SettingsStore"; const BUG_REPORT_URL = "https://example.org/submit"; @@ -32,6 +33,16 @@ describe("BugReportDialog", () => { bug_report_endpoint_url: BUG_REPORT_URL, }); + const originalGetValue = SettingsStore.getValue; + jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName, ...args) => { + // These settings rely on a controller that creates an AudioContext in + // order to test whether the setting can be enabled. For the sake of this test, disable that. + if (settingName === "notificationsEnabled" || settingName === "notificationBodyEnabled") { + return true; + } + return originalGetValue(settingName, ...args); + }); + const mockConsoleLogger = { flush: jest.fn(), consume: jest.fn(), @@ -55,6 +66,7 @@ describe("BugReportDialog", () => { }); afterEach(() => { + jest.restoreAllMocks(); SdkConfig.reset(); fetchMock.restore(); }); diff --git a/test/unit-tests/components/views/dialogs/LogoutDialog-test.tsx b/test/unit-tests/components/views/dialogs/LogoutDialog-test.tsx index cee615380f..4e8a017ed8 100644 --- a/test/unit-tests/components/views/dialogs/LogoutDialog-test.tsx +++ b/test/unit-tests/components/views/dialogs/LogoutDialog-test.tsx @@ -10,10 +10,13 @@ import React from "react"; import { mocked, type MockedObject } from "jest-mock"; import { type MatrixClient } from "matrix-js-sdk/src/matrix"; import { type CryptoApi, type KeyBackupInfo } from "matrix-js-sdk/src/crypto-api"; -import { fireEvent, render, type RenderResult, screen } from "jest-matrix-react"; +import { fireEvent, render, type RenderResult, screen, waitFor } from "jest-matrix-react"; import { filterConsole, getMockClientWithEventEmitter, mockClientMethodsCrypto } from "../../../../test-utils"; import LogoutDialog from "../../../../../src/components/views/dialogs/LogoutDialog"; +import dispatch from "../../../../../src/dispatcher/dispatcher"; +import { Action } from "../../../../../src/dispatcher/actions"; +import { UserTab } from "../../../../../src/components/views/dialogs/UserTab"; describe("LogoutDialog", () => { let mockClient: MockedObject; @@ -56,17 +59,26 @@ describe("LogoutDialog", () => { await rendered.findByText("You'll lose access to your encrypted messages"); }); - it("Prompts user to connect backup if there is a backup on the server", async () => { + it("Prompts user to go to settings if there is a backup on the server", async () => { mockCrypto.getKeyBackupInfo.mockResolvedValue({} as KeyBackupInfo); const rendered = renderComponent(); - await rendered.findByText("Connect this session to Key Backup"); + await rendered.findByText("Go to Settings"); expect(rendered.container).toMatchSnapshot(); + + jest.spyOn(dispatch, "dispatch"); + fireEvent.click(await screen.findByRole("button", { name: "Go to Settings" })); + await waitFor(() => + expect(dispatch.dispatch).toHaveBeenCalledWith({ + action: Action.ViewUserSettings, + initialTabId: UserTab.Encryption, + }), + ); }); - it("Prompts user to set up backup if there is no backup on the server", async () => { + it("Prompts user to go to settings if there is no backup on the server", async () => { mockCrypto.getKeyBackupInfo.mockResolvedValue(null); const rendered = renderComponent(); - await rendered.findByText("Start using Key Backup"); + await rendered.findByText("Go to Settings"); expect(rendered.container).toMatchSnapshot(); fireEvent.click(await screen.findByRole("button", { name: "Manually export keys" })); @@ -75,12 +87,12 @@ describe("LogoutDialog", () => { describe("when there is an error fetching backups", () => { filterConsole("Unable to fetch key backup status"); - it("prompts user to set up backup", async () => { + it("prompts user to go to settings", async () => { mockCrypto.getKeyBackupInfo.mockImplementation(async () => { throw new Error("beep"); }); const rendered = renderComponent(); - await rendered.findByText("Start using Key Backup"); + await rendered.findByText("Go to Settings"); }); }); }); diff --git a/test/unit-tests/components/views/dialogs/UserSettingsDialog-test.tsx b/test/unit-tests/components/views/dialogs/UserSettingsDialog-test.tsx index 8ffe9e3de6..6582bd4d3a 100644 --- a/test/unit-tests/components/views/dialogs/UserSettingsDialog-test.tsx +++ b/test/unit-tests/components/views/dialogs/UserSettingsDialog-test.tsx @@ -7,9 +7,9 @@ Please see LICENSE files in the repository root for full details. */ import React, { type ReactElement } from "react"; -import { render, screen } from "jest-matrix-react"; +import { render, screen, waitFor } from "jest-matrix-react"; import { mocked, type MockedObject } from "jest-mock"; -import { type MatrixClient } from "matrix-js-sdk/src/matrix"; +import { ClientEvent, MatrixEvent, type MatrixClient } from "matrix-js-sdk/src/matrix"; import SettingsStore, { type CallbackFn } from "../../../../../src/settings/SettingsStore"; import SdkConfig from "../../../../../src/SdkConfig"; @@ -250,4 +250,28 @@ describe("", () => { // unwatches settings on unmount expect(mockSettingsStore.unwatchSetting).toHaveBeenCalledWith("mock-watcher-id-feature_mjolnir"); }); + + it("displays an indicator when user needs to set up recovery", async () => { + // Initially, the user doesn't have secret storage, so it should display + // an indicator. + mockClient.secretStorage.getDefaultKeyId.mockResolvedValue(null); + + const { container } = render(getComponent()); + + await waitFor(() => { + expect(container.querySelector(".mx_SettingsDialog_tabLabelsAlert")).toBeInTheDocument(); + }); + + // Test that the handler ignores unknown account data + mockClient.emit(ClientEvent.AccountData, new MatrixEvent({ type: "bar" })); + + // The user now has secret storage. Trigger an update and check that + // the indicator disappears. + mockClient.secretStorage.getDefaultKeyId.mockResolvedValue("foo"); + mockClient.emit(ClientEvent.AccountData, new MatrixEvent({ type: "m.secret_storage.default_key" })); + + await waitFor(() => { + expect(container.querySelector(".mx_SettingsDialog_tabLabelsAlert")).not.toBeInTheDocument(); + }); + }); }); diff --git a/test/unit-tests/components/views/dialogs/__snapshots__/LogoutDialog-test.tsx.snap b/test/unit-tests/components/views/dialogs/__snapshots__/LogoutDialog-test.tsx.snap index c5db254cb2..31d5a09a4f 100644 --- a/test/unit-tests/components/views/dialogs/__snapshots__/LogoutDialog-test.tsx.snap +++ b/test/unit-tests/components/views/dialogs/__snapshots__/LogoutDialog-test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`LogoutDialog Prompts user to connect backup if there is a backup on the server 1`] = ` +exports[`LogoutDialog Prompts user to go to settings if there is a backup on the server 1`] = `
- Connect this session to Key Backup + Go to Settings
@@ -87,7 +87,7 @@ exports[`LogoutDialog Prompts user to connect backup if there is a backup on the
`; -exports[`LogoutDialog Prompts user to set up backup if there is no backup on the server 1`] = ` +exports[`LogoutDialog Prompts user to go to settings if there is no backup on the server 1`] = `
- Start using Key Backup + Go to Settings
diff --git a/test/unit-tests/components/views/dialogs/__snapshots__/UntrustedDeviceDialog-test.tsx.snap b/test/unit-tests/components/views/dialogs/__snapshots__/UntrustedDeviceDialog-test.tsx.snap index 2fd5fdd249..b77858899d 100644 --- a/test/unit-tests/components/views/dialogs/__snapshots__/UntrustedDeviceDialog-test.tsx.snap +++ b/test/unit-tests/components/views/dialogs/__snapshots__/UntrustedDeviceDialog-test.tsx.snap @@ -23,7 +23,11 @@ exports[` should display the dialog for the device of a
+ > +
+
Not Trusted
@@ -97,7 +101,11 @@ exports[` should display the dialog for the device of t
+ > +
+
Not Trusted
diff --git a/test/unit-tests/components/views/dialogs/security/CreateKeyBackupDialog-test.tsx b/test/unit-tests/components/views/dialogs/security/CreateKeyBackupDialog-test.tsx deleted file mode 100644 index b5f965cfa2..0000000000 --- a/test/unit-tests/components/views/dialogs/security/CreateKeyBackupDialog-test.tsx +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2024 New Vector Ltd. - * Copyright 2023 The Matrix.org Foundation C.I.C. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -import { render, screen, waitFor } from "jest-matrix-react"; -import React from "react"; -import { mocked } from "jest-mock"; - -import CreateKeyBackupDialog from "../../../../../../src/async-components/views/dialogs/security/CreateKeyBackupDialog"; -import { createTestClient } from "../../../../../test-utils"; -import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg"; - -jest.mock("../../../../../../src/SecurityManager", () => ({ - accessSecretStorage: jest.fn().mockResolvedValue(undefined), - withSecretStorageKeyCache: jest.fn().mockImplementation((fn) => fn()), -})); - -describe("CreateKeyBackupDialog", () => { - beforeEach(() => { - MatrixClientPeg.safeGet = MatrixClientPeg.get = () => createTestClient(); - }); - - it("should display the spinner when creating backup", () => { - const { asFragment } = render(); - - // Check if the spinner is displayed - expect(screen.getByTestId("spinner")).toBeDefined(); - expect(asFragment()).toMatchSnapshot(); - }); - - it("should display an error message when backup creation failed", async () => { - const matrixClient = createTestClient(); - jest.spyOn(matrixClient.secretStorage, "hasKey").mockResolvedValue(true); - mocked(matrixClient.getCrypto()!.resetKeyBackup).mockImplementation(() => { - throw new Error("failed"); - }); - MatrixClientPeg.safeGet = MatrixClientPeg.get = () => matrixClient; - - const { asFragment } = render(); - - // Check if the error message is displayed - await waitFor(() => expect(screen.getByText("Unable to create key backup")).toBeDefined()); - expect(asFragment()).toMatchSnapshot(); - }); - - it("should display an error message when there is no Crypto available", async () => { - const matrixClient = createTestClient(); - jest.spyOn(matrixClient.secretStorage, "hasKey").mockResolvedValue(true); - mocked(matrixClient.getCrypto).mockReturnValue(undefined); - MatrixClientPeg.safeGet = MatrixClientPeg.get = () => matrixClient; - - render(); - - // Check if the error message is displayed - await waitFor(() => expect(screen.getByText("Unable to create key backup")).toBeDefined()); - }); - - it("should display the success dialog when the key backup is finished", async () => { - const onFinished = jest.fn(); - const { asFragment } = render(); - - await waitFor(() => - expect( - screen.getByText("Your keys are being backed up (the first backup could take a few minutes)."), - ).toBeDefined(), - ); - expect(asFragment()).toMatchSnapshot(); - - // Click on the OK button - screen.getByRole("button", { name: "OK" }).click(); - expect(onFinished).toHaveBeenCalledWith(true); - }); -}); diff --git a/test/unit-tests/components/views/dialogs/security/CreateSecretStorageDialog-test.tsx b/test/unit-tests/components/views/dialogs/security/CreateSecretStorageDialog-test.tsx index 000f6efdb4..da68906c63 100644 --- a/test/unit-tests/components/views/dialogs/security/CreateSecretStorageDialog-test.tsx +++ b/test/unit-tests/components/views/dialogs/security/CreateSecretStorageDialog-test.tsx @@ -13,7 +13,7 @@ import { mocked, type MockedObject } from "jest-mock"; import { type MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix"; import { sleep } from "matrix-js-sdk/src/utils"; -import { filterConsole, flushPromises, stubClient } from "../../../../../test-utils"; +import { filterConsole, stubClient } from "../../../../../test-utils"; import CreateSecretStorageDialog from "../../../../../../src/async-components/views/dialogs/security/CreateSecretStorageDialog"; describe("CreateSecretStorageDialog", () => { @@ -97,39 +97,4 @@ describe("CreateSecretStorageDialog", () => { await screen.findByText("Your keys are now being backed up from this device."); }); }); - - it("resets keys in the right order when resetting secret storage and cross-signing", async () => { - const result = renderComponent({ forceReset: true, resetCrossSigning: true }); - - await result.findByText(/Set up Secure Backup/); - jest.spyOn(mockClient.getCrypto()!, "createRecoveryKeyFromPassphrase").mockResolvedValue({ - privateKey: new Uint8Array(), - encodedPrivateKey: "abcd efgh ijkl", - }); - result.getByRole("button", { name: "Continue" }).click(); - - await result.findByText(/Save your Recovery Key/); - result.getByRole("button", { name: "Copy" }).click(); - - // Resetting should reset secret storage, cross signing, and key - // backup. We make sure that all three are reset, and done in the - // right order. - const resetFunctionCallLog: string[] = []; - jest.spyOn(mockClient.getCrypto()!, "bootstrapSecretStorage").mockImplementation(async () => { - resetFunctionCallLog.push("bootstrapSecretStorage"); - }); - jest.spyOn(mockClient.getCrypto()!, "bootstrapCrossSigning").mockImplementation(async () => { - resetFunctionCallLog.push("bootstrapCrossSigning"); - }); - jest.spyOn(mockClient.getCrypto()!, "resetKeyBackup").mockImplementation(async () => { - resetFunctionCallLog.push("resetKeyBackup"); - }); - - await flushPromises(); - result.getByRole("button", { name: "Continue" }).click(); - - await result.findByText("Your keys are now being backed up from this device."); - - expect(resetFunctionCallLog).toEqual(["bootstrapSecretStorage", "bootstrapCrossSigning", "resetKeyBackup"]); - }); }); diff --git a/test/unit-tests/components/views/dialogs/security/__snapshots__/CreateKeyBackupDialog-test.tsx.snap b/test/unit-tests/components/views/dialogs/security/__snapshots__/CreateKeyBackupDialog-test.tsx.snap deleted file mode 100644 index 60a051eec9..0000000000 --- a/test/unit-tests/components/views/dialogs/security/__snapshots__/CreateKeyBackupDialog-test.tsx.snap +++ /dev/null @@ -1,168 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`CreateKeyBackupDialog should display an error message when backup creation failed 1`] = ` - -
- -
- -`; - -exports[`CreateKeyBackupDialog should display the spinner when creating backup 1`] = ` - -
-
@@ -51,12 +51,9 @@ exports[` should ask to set up a recovery key when there is no class="mx_SettingsSection_header" >

- Recovery - - Recommended - + Recovery

Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices. @@ -97,7 +94,7 @@ exports[` should be in loading state when checking the recovery

- Recovery + Recovery

Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices. diff --git a/test/unit-tests/components/views/settings/encryption/__snapshots__/RecoveryPanelOutOfSync-test.tsx.snap b/test/unit-tests/components/views/settings/encryption/__snapshots__/RecoveryPanelOutOfSync-test.tsx.snap index edf7de5af9..ca4914e198 100644 --- a/test/unit-tests/components/views/settings/encryption/__snapshots__/RecoveryPanelOutOfSync-test.tsx.snap +++ b/test/unit-tests/components/views/settings/encryption/__snapshots__/RecoveryPanelOutOfSync-test.tsx.snap @@ -12,7 +12,7 @@ exports[` should render 1`] = `

- Recovery + Recovery

should display a verify button when the e

- Device not verified + Device not verified

should display the recovery out of sync p

- Recovery + Recovery

{ beforeEach(() => { SdkConfig.reset(); + SettingsStore.reset(); }); describe("getValueAt", () => { @@ -82,6 +83,16 @@ describe("SettingsStore", () => { }); }); + describe("exportForRageshake", () => { + it("should not export settings marked as non-exportable", async () => { + await SettingsStore.setValue("userTimezone", null, SettingLevel.DEVICE, "Europe/London"); + const values = JSON.parse(SettingsStore.exportForRageshake()) as Record; + for (const exportedKey of Object.keys(values) as SettingKey[]) { + expect(SETTINGS[exportedKey].shouldExportToRageshake).not.toEqual(false); + } + }); + }); + describe("runMigrations", () => { let client: MatrixClient; let room: Room; diff --git a/test/unit-tests/stores/widgets/StopGapWidgetDriver-test.ts b/test/unit-tests/stores/widgets/StopGapWidgetDriver-test.ts index e08a83dd02..1b646498bb 100644 --- a/test/unit-tests/stores/widgets/StopGapWidgetDriver-test.ts +++ b/test/unit-tests/stores/widgets/StopGapWidgetDriver-test.ts @@ -99,6 +99,7 @@ describe("StopGapWidgetDriver", () => { "org.matrix.msc2762.send.event:m.room.redaction", "org.matrix.msc2762.receive.event:m.room.redaction", "org.matrix.msc2762.receive.state_event:m.room.create", + "org.matrix.msc2762.receive.state_event:m.room.name", "org.matrix.msc2762.receive.state_event:m.room.member", "org.matrix.msc2762.receive.state_event:org.matrix.msc3401.call", "org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#@alice:example.org", diff --git a/test/unit-tests/submit-rageshake-test.ts b/test/unit-tests/submit-rageshake-test.ts index 4eda347a98..832cee3fca 100644 --- a/test/unit-tests/submit-rageshake-test.ts +++ b/test/unit-tests/submit-rageshake-test.ts @@ -22,6 +22,7 @@ import { collectBugReport } from "../../src/rageshake/submit-rageshake"; import SettingsStore from "../../src/settings/SettingsStore"; import { type ConsoleLogger } from "../../src/rageshake/rageshake"; import { type FeatureSettingKey, type SettingKey } from "../../src/settings/Settings.tsx"; +import { SettingLevel } from "../../src/settings/SettingLevel.ts"; describe("Rageshakes", () => { const RUST_CRYPTO_VERSION = "Rust SDK 0.7.0 (691ec63), Vodozemac 0.5.0"; @@ -35,6 +36,8 @@ describe("Rageshakes", () => { onlyData: true, }, ); + let windowSpy: jest.SpyInstance; + let mockWindow: Mocked; beforeEach(() => { mockClient = getMockClientWithEventEmitter({ @@ -50,30 +53,24 @@ describe("Rageshakes", () => { ed25519: "", curve25519: "", }); + mockWindow = { + matchMedia: jest.fn().mockReturnValue({ matches: false }), + navigator: { + userAgent: "", + }, + } as unknown as Mocked; + // @ts-ignore - We just need partial mock + windowSpy = jest.spyOn(global, "window", "get").mockReturnValue(mockWindow); fetchMock.restore(); fetchMock.catch(404); }); + afterEach(() => { + windowSpy.mockRestore(); + }); + describe("Basic Information", () => { - let mockWindow: Mocked; - let windowSpy: jest.SpyInstance; - - beforeEach(() => { - mockWindow = { - matchMedia: jest.fn().mockReturnValue({ matches: false }), - navigator: { - userAgent: "", - }, - } as unknown as Mocked; - // @ts-ignore - We just need partial mock - windowSpy = jest.spyOn(global, "window", "get").mockReturnValue(mockWindow); - }); - - afterEach(() => { - windowSpy.mockRestore(); - }); - it("should include app version", async () => { mockPlatformPeg({ getAppVersion: jest.fn().mockReturnValue("1.11.58") }); @@ -376,6 +373,10 @@ describe("Rageshakes", () => { describe("Settings Store", () => { const mockSettingsStore = mocked(SettingsStore); + afterEach(() => { + jest.restoreAllMocks(); + }); + it("should collect labs from settings store", async () => { const someFeatures = [ "feature_video_rooms", @@ -430,6 +431,7 @@ describe("Rageshakes", () => { afterEach(() => { navigatorSpy.mockRestore(); + SettingsStore.reset(); }); it("should collect navigator storage persisted", async () => { @@ -488,6 +490,7 @@ describe("Rageshakes", () => { }; const disabledFeatures = ["cssanimations", "d0", "d1"]; const mockWindow = { + matchMedia: jest.fn().mockReturnValue({ matches: false }), Modernizr: { ...allFeatures, }, @@ -503,20 +506,16 @@ describe("Rageshakes", () => { }); it("should collect localstorage settings", async () => { - const localSettings = { - language: "fr", - showHiddenEventsInTimeline: true, - activeCallRoomIds: [], - }; - - const spy = jest.spyOn(window.localStorage.__proto__, "getItem").mockImplementation((key) => { - return JSON.stringify(localSettings); - }); + await SettingsStore.setValue("language", null, SettingLevel.DEVICE, "fr"); + await SettingsStore.setValue("showHiddenEventsInTimeline", null, SettingLevel.DEVICE, true); + await SettingsStore.setValue("userTimezone", null, SettingLevel.DEVICE, "Europe/London"); + await SettingsStore.setValue("activeCallRoomIds", null, SettingLevel.DEVICE, []); const formData = await collectBugReport(); - expect(formData.get("mx_local_settings")).toBe(JSON.stringify(localSettings)); - - spy.mockRestore(); + const settingDataJSON = formData.get("mx_local_settings"); + expect(settingDataJSON).not.toBeNull(); + const settingsData = JSON.parse(settingDataJSON as string); + expect(settingsData.showHiddenEventsInTimeline).toEqual(true); }); it("should collect logs", async () => { diff --git a/test/unit-tests/toasts/SetupEncryptionToast-test.tsx b/test/unit-tests/toasts/SetupEncryptionToast-test.tsx index eb584aafd9..7ebd9baeae 100644 --- a/test/unit-tests/toasts/SetupEncryptionToast-test.tsx +++ b/test/unit-tests/toasts/SetupEncryptionToast-test.tsx @@ -36,19 +36,21 @@ describe("SetupEncryptionToast", () => { expect(await screen.findByRole("heading", { name: "Set up recovery" })).toBeInTheDocument(); }); - it("should dismiss the toast when 'not now' button clicked", async () => { + it("should dismiss the toast when 'Dismiss' button clicked, and remember it", async () => { + jest.spyOn(DeviceListener.sharedInstance(), "recordRecoveryDisabled"); jest.spyOn(DeviceListener.sharedInstance(), "dismissEncryptionSetup"); showToast(Kind.SET_UP_RECOVERY); const user = userEvent.setup(); - await user.click(await screen.findByRole("button", { name: "Not now" })); + await user.click(await screen.findByRole("button", { name: "Dismiss" })); + expect(DeviceListener.sharedInstance().recordRecoveryDisabled).toHaveBeenCalled(); expect(DeviceListener.sharedInstance().dismissEncryptionSetup).toHaveBeenCalled(); }); }); - describe("Key storage out of sync", () => { + describe("Key storage out of sync (retrieve secrets)", () => { it("should render the toast", async () => { showToast(Kind.KEY_STORAGE_OUT_OF_SYNC); @@ -86,6 +88,44 @@ describe("SetupEncryptionToast", () => { }); }); + describe("Key storage out of sync (store secrets)", () => { + it("should render the toast", async () => { + showToast(Kind.KEY_STORAGE_OUT_OF_SYNC_STORE); + + await expect(screen.findByText("Your key storage is out of sync.")).resolves.toBeInTheDocument(); + }); + + it("should open settings to the reset flow when 'forgot recovery key' clicked", async () => { + showToast(Kind.KEY_STORAGE_OUT_OF_SYNC_STORE); + + const user = userEvent.setup(); + await user.click(await screen.findByText("Forgot recovery key?")); + + expect(dis.dispatch).toHaveBeenCalledWith({ + action: "view_user_settings", + initialTabId: "USER_ENCRYPTION_TAB", + props: { initialEncryptionState: "change_recovery_key" }, + }); + }); + + it("should open settings to the reset flow when recovering fails", async () => { + jest.spyOn(SecurityManager, "accessSecretStorage").mockImplementation(async () => { + throw new Error("Something went wrong while recovering!"); + }); + + showToast(Kind.KEY_STORAGE_OUT_OF_SYNC_STORE); + + const user = userEvent.setup(); + await user.click(await screen.findByText("Enter recovery key")); + + expect(dis.dispatch).toHaveBeenCalledWith({ + action: "view_user_settings", + initialTabId: "USER_ENCRYPTION_TAB", + props: { initialEncryptionState: "change_recovery_key" }, + }); + }); + }); + describe("Turn on key storage", () => { it("should render the toast", async () => { showToast(Kind.TURN_ON_KEY_STORAGE); diff --git a/webpack.config.js b/webpack.config.js index e0d7d4fe93..bc5b405232 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -679,6 +679,12 @@ module.exports = (env, argv) => { context: path.resolve(__dirname, "node_modules/@element-hq/element-call-embedded/dist"), to: path.join(__dirname, "webapp", "widgets", "element-call"), }, + // Mobile guide assets + { + from: "assets/**", + context: path.resolve(__dirname, "src/vector/mobile_guide"), + to: "mobile_guide", + }, ], }), @@ -771,6 +777,8 @@ module.exports = (env, argv) => { function getAssetOutputPath(url, resourcePath) { const isKaTeX = resourcePath.includes("KaTeX"); const isFontSource = resourcePath.includes("@fontsource"); + const mobileGuideAssetsPath = path.join("mobile_guide", "assets"); + const isMobileGuide = resourcePath.includes(mobileGuideAssetsPath); // `res` is the parent dir for our own assets in various layers // `dist` is the parent dir for KaTeX assets // `files` is the parent dir for @fontsource assets @@ -804,6 +812,11 @@ function getAssetOutputPath(url, resourcePath) { outputDir = "fonts"; } + if (isMobileGuide) { + // Specific handling for the mobile guide assets, as they live alongside the page sources. + outputDir = mobileGuideAssetsPath; + } + if (isKaTeX) { // Add a clearly named directory segment, rather than leaving the KaTeX // assets loose in each asset type directory. diff --git a/yarn.lock b/yarn.lock index 973f0bf4ed..6e8a389d20 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1353,17 +1353,17 @@ "@csstools/color-helpers" "^5.0.1" "@csstools/css-calc" "^2.1.1" -"@csstools/css-parser-algorithms@^3.0.4": +"@csstools/css-parser-algorithms@^3.0.4", "@csstools/css-parser-algorithms@^3.0.5": version "3.0.5" resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz#5755370a9a29abaec5515b43c8b3f2cf9c2e3076" integrity sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ== -"@csstools/css-tokenizer@^3.0.3": +"@csstools/css-tokenizer@^3.0.3", "@csstools/css-tokenizer@^3.0.4": version "3.0.4" resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz#333fedabc3fd1a8e5d0100013731cf19e6a8c5d3" integrity sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw== -"@csstools/media-query-list-parser@^4.0.2": +"@csstools/media-query-list-parser@^4.0.2", "@csstools/media-query-list-parser@^4.0.3": version "4.0.3" resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.3.tgz#7aec77bcb89c2da80ef207e73f474ef9e1b3cdf1" integrity sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ== @@ -1672,18 +1672,19 @@ resolved "https://registry.yarnpkg.com/@element-hq/element-call-embedded/-/element-call-embedded-0.12.2.tgz#b6b6b7df69369b3088960b79591ce1bfd2b84a1a" integrity sha512-2u5/bOARcjc5TFq4929x1R0tvsNbeVA58FBtiW05GlIJCapxzPSOeeGhbqEcJ1TW3/hLGpiKMcw0QwRBQVNzQA== -"@element-hq/element-web-module-api@1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@element-hq/element-web-module-api/-/element-web-module-api-1.0.0.tgz#df09108b0346a44ad2898c603d1a6cda5f50d80b" - integrity sha512-FYId5tYgaKvpqAXRXqs0pY4+7/A09bEl1mCxFqlS9jlZOCjlMZVvZuv8spbY8ZN9HaMvuVmx9J00Fn2gCJd0TQ== - -"@element-hq/element-web-playwright-common@^1.1.5": +"@element-hq/element-web-module-api@1.3.0": version "1.3.0" - resolved "https://registry.yarnpkg.com/@element-hq/element-web-playwright-common/-/element-web-playwright-common-1.3.0.tgz#5f57eb2ee25e9733c92185ef57431db808deb603" - integrity sha512-tq3lN77f0KnTYtPVkRrwvsWpq/JAC6JqDkC2f6dxuvX3a+OmyjGnuoZh83kB2WNObfrrx7gxmsZpS7ov2mrw4Q== + resolved "https://registry.yarnpkg.com/@element-hq/element-web-module-api/-/element-web-module-api-1.3.0.tgz#6067fa654174d1dd0953447bb036e38f9dfa51a5" + integrity sha512-rEV0xnT/tNYPIdqHWWiz2KZo96UeZR0YChfoVLiPT46ZlEYyxqkjxT5bOm1eL2/CiYRe8t1yka3UDkIjq481/g== + +"@element-hq/element-web-playwright-common@^1.4.2": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@element-hq/element-web-playwright-common/-/element-web-playwright-common-1.4.2.tgz#3c05673ddbdc042cb34fa1063e0b8651a74c3f0e" + integrity sha512-p/P0J1TWlfZgJjUCXYWOL6HqpEg63Rm6sKQjBm/9EJD1xWqJY+xMrZu97cuXSKL60NOGF6vpBhW/gdNQT/Nz1w== dependencies: "@axe-core/playwright" "^4.10.1" "@testcontainers/postgresql" "^11.0.0" + glob "^11.0.3" lodash-es "^4.17.21" mailpit-api "^1.2.0" strip-ansi "^7.1.0" @@ -1879,6 +1880,18 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== +"@isaacs/balanced-match@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz#3081dadbc3460661b751e7591d7faea5df39dd29" + integrity sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ== + +"@isaacs/brace-expansion@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz#4b3dabab7d8e75a429414a96bd67bf4c1d13e0f3" + integrity sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA== + dependencies: + "@isaacs/balanced-match" "^4.0.1" + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" @@ -2250,15 +2263,10 @@ emojibase "^15.3.1" emojibase-data "^15.3.1" -"@matrix-org/matrix-sdk-crypto-wasm@^14.2.0": - version "14.2.0" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-14.2.0.tgz#1d6bddb2f777ac1674546467aab6f8584a7f2e71" - integrity sha512-xYbH1Yg8YwfXxGsCVDypiRvSVYPnCybsoRqlBDuAvIOs9tOfmdeeJqN+3VxvLWH28g3CtJs+9Afw8dYSHViTFg== - -"@matrix-org/olm@3.2.15": - version "3.2.15" - resolved "https://registry.yarnpkg.com/@matrix-org/olm/-/olm-3.2.15.tgz#55f3c1b70a21bbee3f9195cecd6846b1083451ec" - integrity sha512-S7lOrndAK9/8qOtaTq/WhttJC/o4GAzdfK0MUPpo8ApzsJEC0QjtwrkC3KBXdFP1cD1MXi/mlKR7aaoVMKgs6Q== +"@matrix-org/matrix-sdk-crypto-wasm@^15.0.0": + version "15.0.0" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-15.0.0.tgz#5b29ca1c62f3aface9db06d7441d0a9ba2cd3439" + integrity sha512-tzBGf/jugrOw190Na77LljZIQMTSL6SAnZaATKMlb2j1XOfc5Q+bSJTb9ZWBR7TFs0d8K9spcwRHPc4S/7CMYw== "@matrix-org/react-sdk-module-api@^2.4.0": version "2.5.0" @@ -2408,12 +2416,12 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== -"@playwright/test@1.52.0", "@playwright/test@^1.50.1": - version "1.52.0" - resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.52.0.tgz#267ec595b43a8f4fa5e444ea503689629e91a5b8" - integrity sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g== +"@playwright/test@1.53.1", "@playwright/test@^1.50.1": + version "1.53.1" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.53.1.tgz#3ad5a2ce334b4a78390fd91e0a9d8423c09bc808" + integrity sha512-Z4c23LHV0muZ8hfv4jw6HngPJkbbtZxTkxPNIg7cJcTc9C28N/p2q7g3JZS2SiKBBHJ3uM1dgDye66bB7LEk5w== dependencies: - playwright "1.52.0" + playwright "1.53.1" "@polka/url@^1.0.0-next.24": version "1.0.0-next.28" @@ -2769,35 +2777,35 @@ resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== -"@sentry-internal/browser-utils@9.27.0": - version "9.27.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-9.27.0.tgz#1860a9925aadb700fc273d59d4d6e7083408f765" - integrity sha512-SJa7f6Ct1BzP8rWEomnshSGN1CmT+axNKvT+StrbFPD6AyHnYfFLJpKgc2iToIJHB/pmeuOI9dUwqtzVx+5nSw== +"@sentry-internal/browser-utils@9.31.0": + version "9.31.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-9.31.0.tgz#91a373226ead0ecefa1ec962698013748477f644" + integrity sha512-rviu/jUmeQbY4rSO8l4pubOtRIhFtH5Gu/ryRNMTlpJRdomp4uxddqthHUDH5g6xCXZsMTyJEIdx0aTqbgr/GQ== dependencies: - "@sentry/core" "9.27.0" + "@sentry/core" "9.31.0" -"@sentry-internal/feedback@9.27.0": - version "9.27.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-9.27.0.tgz#ec9311033b401f4f89304c47ffde2de942a281ef" - integrity sha512-e7L8eG0y63RulN352lmafoCCfQGg4jLVT8YLx6096eWu/YKLkgmVpgi8livsT5WREnH+HB+iFSrejOwK7cRkhw== +"@sentry-internal/feedback@9.31.0": + version "9.31.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-9.31.0.tgz#52122336d42a72de4cc7d361ca721b360f9eecf0" + integrity sha512-Ygi/8UZ7p2B4DhXQjZDtOc45vNUHkfk2XETBTBGkByEQkE8vygzSiKhgRcnVpzwq+8xKFMRy+PxvpcCo+PNQew== dependencies: - "@sentry/core" "9.27.0" + "@sentry/core" "9.31.0" -"@sentry-internal/replay-canvas@9.27.0": - version "9.27.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-9.27.0.tgz#d18e95ace5c6bd593935cacea1980b9e640b3afe" - integrity sha512-44rVSt3LCH6qePYRQrl4WUBwnkOk9dzinmnKmuwRksEdDOkVq5KBRhi/IDr7omwSpX8C+KrX5alfKhOx1cP0gQ== +"@sentry-internal/replay-canvas@9.31.0": + version "9.31.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-9.31.0.tgz#6f9e1505a167e7264c541c1247c74403e75855df" + integrity sha512-VGqfvQCIuXQZeecrBf8bd4sj8lYGzUA/2CffTAkad1nB1Onyz0Kzo54qLWemivCxA3ufHf6DCpNA3Loa/0ywFQ== dependencies: - "@sentry-internal/replay" "9.27.0" - "@sentry/core" "9.27.0" + "@sentry-internal/replay" "9.31.0" + "@sentry/core" "9.31.0" -"@sentry-internal/replay@9.27.0": - version "9.27.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-9.27.0.tgz#e357d45a6fc2b80ac312798cc5b1b758c5ca9c66" - integrity sha512-n2kO1wOfCG7GxkMAqbYYkpgTqJM5tuVLdp0JuNCqTOLTXWvw6svWGaYKlYpKUgsK9X/GDzJYSXZmfe+Dbg+FJQ== +"@sentry-internal/replay@9.31.0": + version "9.31.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-9.31.0.tgz#8f759fc7add224129f176fc8a1d46625ddc58526" + integrity sha512-V5rvcO/xSj8JMw4ZnZT2cBYC+UOuIiZ2Flj4EoIurxMrTgowE1uMXUBA32EBfuB5/vQSJXB6W5uAudhk7LjBPQ== dependencies: - "@sentry-internal/browser-utils" "9.27.0" - "@sentry/core" "9.27.0" + "@sentry-internal/browser-utils" "9.31.0" + "@sentry/core" "9.31.0" "@sentry/babel-plugin-component-annotate@3.5.0": version "3.5.0" @@ -2805,15 +2813,15 @@ integrity sha512-s2go8w03CDHbF9luFGtBHKJp4cSpsQzNVqgIa9Pfa4wnjipvrK6CxVT4icpLA3YO6kg5u622Yoa5GF3cJdippw== "@sentry/browser@^9.0.0": - version "9.27.0" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-9.27.0.tgz#5aa87a34600aa7ee19e29d2295266c18a32d7bc7" - integrity sha512-geR3lhRJOmUQqi1WgovLSYcD/f66zYnctdnDEa7j1BW2XIB1nlTJn0mpYyAHghXKkUN/pBpp1Z+Jk0XlVwFYVg== + version "9.31.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-9.31.0.tgz#c7472a58a88ef5556ed7eb7e0d6aca7db981d207" + integrity sha512-DzG72JJTqHzE0Qo2fHeHm3xgFs97InaSQStmTMxOA59yPqvAXbweNPcsgCNu1q76+jZyaJcoy1qOwahnLuEVDg== dependencies: - "@sentry-internal/browser-utils" "9.27.0" - "@sentry-internal/feedback" "9.27.0" - "@sentry-internal/replay" "9.27.0" - "@sentry-internal/replay-canvas" "9.27.0" - "@sentry/core" "9.27.0" + "@sentry-internal/browser-utils" "9.31.0" + "@sentry-internal/feedback" "9.31.0" + "@sentry-internal/replay" "9.31.0" + "@sentry-internal/replay-canvas" "9.31.0" + "@sentry/core" "9.31.0" "@sentry/bundler-plugin-core@3.5.0": version "3.5.0" @@ -2883,10 +2891,10 @@ "@sentry/cli-win32-i686" "2.42.2" "@sentry/cli-win32-x64" "2.42.2" -"@sentry/core@9.27.0": - version "9.27.0" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-9.27.0.tgz#e97d6957c243d95f9f05ccb553b937838bb500ea" - integrity sha512-Zb2SSAdWXQjTem+sVWrrAq9L6YYfxyoTwtapaE6C6qZBR5C8Uak0wcYww8StaCFH7dDA/PSW+VxOwjNXocrQHQ== +"@sentry/core@9.31.0": + version "9.31.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-9.31.0.tgz#dd306dc4f7a7a3e95fe2e83ee10cd494bbf86ac7" + integrity sha512-6JeoPGvBgT9m2YFIf2CrW+KrrOYzUqb9+Xwr/Dw25kPjVKy+WJjWqK8DKCNLgkBA22OCmSOmHuRwFR0YxGVdZQ== "@sentry/webpack-plugin@^3.0.0": version "3.5.0" @@ -3407,9 +3415,9 @@ integrity sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ== "@types/lodash@^4.14.168": - version "4.17.17" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.17.tgz#fb85a04f47e9e4da888384feead0de05f7070355" - integrity sha512-RRVJ+J3J+WmyOTqnz3PiBLA501eKwXl2noseKOrNo/6+XEHjTAxO4xHvxQB6QuNm+s4WRbn6rSiap8+EA+ykFQ== + version "4.17.18" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.18.tgz#4710e7db5b3857103764bf7b7b666414e6141baf" + integrity sha512-KJ65INaxqxmU6EoCiJmRPZC9H9RVWCRd349tXM2M3O5NA7cY6YL7c0bHAHQ93NOfTObEQ004kd2QVHs/r0+m4g== "@types/mapbox__point-geometry@*", "@types/mapbox__point-geometry@^0.1.4": version "0.1.4" @@ -3463,9 +3471,9 @@ undici-types "~7.8.0" "@types/node@18", "@types/node@^18.11.18": - version "18.19.111" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.111.tgz#e95b89efc24cc625834b43bcd70bd5591a5dfba5" - integrity sha512-90sGdgA+QLJr1F9X79tQuEut0gEYIfkX9pydI4XGRgvFo9g2JWswefI+WUSUHPYVBHYSEfTEqBxA5hQvAZB3Mw== + version "18.19.112" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.112.tgz#cd2aee9c075402e0e1942a44101428881dbeb110" + integrity sha512-i+Vukt9POdS/MBI7YrrkkI5fMfwFtOjphSmt4WXYLfwqsfr6z/HdCx7LqT9M7JktGob8WNgj8nFB4TbGNE4Cog== dependencies: undici-types "~5.26.4" @@ -3546,10 +3554,10 @@ "@types/prop-types" "*" "@types/react" "*" -"@types/react@*", "@types/react@19.1.6": - version "19.1.6" - resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.6.tgz#dee39f3e1e9a7d693f156a5840570b6d57f325ea" - integrity sha512-JeG0rEWak0N6Itr6QUx+X60uQmN+5t3j9r/OVDtWzFXKaj6kD1BwJzOksD0FF6iWxZlbE1kB0q9vtnU2ekqa1Q== +"@types/react@*", "@types/react@19.1.8": + version "19.1.8" + resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.8.tgz#ff8395f2afb764597265ced15f8dddb0720ae1c3" + integrity sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g== dependencies: csstype "^3.0.2" @@ -3685,29 +3693,29 @@ "@types/yargs-parser" "*" "@typescript-eslint/eslint-plugin@^8.19.0": - version "8.34.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.0.tgz#96c9f818782fe24cd5883a5d517ca1826d3fa9c2" - integrity sha512-QXwAlHlbcAwNlEEMKQS2RCgJsgXrTJdjXT08xEgbPFa2yYQgVjBymxP5DrfrE7X7iodSzd9qBUHUycdyVJTW1w== + version "8.35.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.0.tgz#515170100ff867445fe0a17ce05c14fc5fd9ca63" + integrity sha512-ijItUYaiWuce0N1SoSMrEd0b6b6lYkYt99pqCPfybd+HKVXtEvYhICfLdwp42MhiI5mp0oq7PKEL+g1cNiz/Eg== dependencies: "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "8.34.0" - "@typescript-eslint/type-utils" "8.34.0" - "@typescript-eslint/utils" "8.34.0" - "@typescript-eslint/visitor-keys" "8.34.0" + "@typescript-eslint/scope-manager" "8.35.0" + "@typescript-eslint/type-utils" "8.35.0" + "@typescript-eslint/utils" "8.35.0" + "@typescript-eslint/visitor-keys" "8.35.0" graphemer "^1.4.0" ignore "^7.0.0" natural-compare "^1.4.0" ts-api-utils "^2.1.0" "@typescript-eslint/parser@^8.19.0": - version "8.34.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.34.0.tgz#703270426ac529304ae6988482f487c856d9c13f" - integrity sha512-vxXJV1hVFx3IXz/oy2sICsJukaBrtDEQSBiV48/YIV5KWjX1dO+bcIr/kCPrW6weKXvsaGKFNlwH0v2eYdRRbA== + version "8.35.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.35.0.tgz#20a0e17778a329a6072722f5ac418d4376b767d2" + integrity sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA== dependencies: - "@typescript-eslint/scope-manager" "8.34.0" - "@typescript-eslint/types" "8.34.0" - "@typescript-eslint/typescript-estree" "8.34.0" - "@typescript-eslint/visitor-keys" "8.34.0" + "@typescript-eslint/scope-manager" "8.35.0" + "@typescript-eslint/types" "8.35.0" + "@typescript-eslint/typescript-estree" "8.35.0" + "@typescript-eslint/visitor-keys" "8.35.0" debug "^4.3.4" "@typescript-eslint/project-service@8.34.0": @@ -3719,6 +3727,15 @@ "@typescript-eslint/types" "^8.34.0" debug "^4.3.4" +"@typescript-eslint/project-service@8.35.0": + version "8.35.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.35.0.tgz#00bd77e6845fbdb5684c6ab2d8a400a58dcfb07b" + integrity sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ== + dependencies: + "@typescript-eslint/tsconfig-utils" "^8.35.0" + "@typescript-eslint/types" "^8.35.0" + debug "^4.3.4" + "@typescript-eslint/scope-manager@8.23.0": version "8.23.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.23.0.tgz#ee3bb7546421ca924b9b7a8b62a77d388193ddec" @@ -3735,18 +3752,31 @@ "@typescript-eslint/types" "8.34.0" "@typescript-eslint/visitor-keys" "8.34.0" +"@typescript-eslint/scope-manager@8.35.0": + version "8.35.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.35.0.tgz#8ccb2ab63383544fab98fc4b542d8d141259ff4f" + integrity sha512-+AgL5+mcoLxl1vGjwNfiWq5fLDZM1TmTPYs2UkyHfFhgERxBbqHlNjRzhThJqz+ktBqTChRYY6zwbMwy0591AA== + dependencies: + "@typescript-eslint/types" "8.35.0" + "@typescript-eslint/visitor-keys" "8.35.0" + "@typescript-eslint/tsconfig-utils@8.34.0", "@typescript-eslint/tsconfig-utils@^8.34.0": version "8.34.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.0.tgz#97d0a24e89a355e9308cebc8e23f255669bf0979" integrity sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA== -"@typescript-eslint/type-utils@8.34.0": - version "8.34.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.34.0.tgz#03e7eb3776129dfd751ba1cac0c6ea4b0fab5ec6" - integrity sha512-n7zSmOcUVhcRYC75W2pnPpbO1iwhJY3NLoHEtbJwJSNlVAZuwqu05zY3f3s2SDWWDSo9FdN5szqc73DCtDObAg== +"@typescript-eslint/tsconfig-utils@8.35.0", "@typescript-eslint/tsconfig-utils@^8.35.0": + version "8.35.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.0.tgz#6e05aeb999999e31d562ceb4fe144f3cbfbd670e" + integrity sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA== + +"@typescript-eslint/type-utils@8.35.0": + version "8.35.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.35.0.tgz#0201eae9d83ffcc3451ef8c94f53ecfbf2319ecc" + integrity sha512-ceNNttjfmSEoM9PW87bWLDEIaLAyR+E6BoYJQ5PfaDau37UGca9Nyq3lBk8Bw2ad0AKvYabz6wxc7DMTO2jnNA== dependencies: - "@typescript-eslint/typescript-estree" "8.34.0" - "@typescript-eslint/utils" "8.34.0" + "@typescript-eslint/typescript-estree" "8.35.0" + "@typescript-eslint/utils" "8.35.0" debug "^4.3.4" ts-api-utils "^2.1.0" @@ -3760,6 +3790,11 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.34.0.tgz#18000f205c59c9aff7f371fc5426b764cf2890fb" integrity sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA== +"@typescript-eslint/types@8.35.0", "@typescript-eslint/types@^8.35.0": + version "8.35.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.35.0.tgz#e60d062907930e30008d796de5c4170f02618a93" + integrity sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ== + "@typescript-eslint/typescript-estree@8.23.0": version "8.23.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.23.0.tgz#f633ef08efa656e386bc44b045ffcf9537cc6924" @@ -3790,15 +3825,31 @@ semver "^7.6.0" ts-api-utils "^2.1.0" -"@typescript-eslint/utils@8.34.0", "@typescript-eslint/utils@^8.32.1": - version "8.34.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.34.0.tgz#7844beebc1153b4d3ec34135c2da53a91e076f8d" - integrity sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ== +"@typescript-eslint/typescript-estree@8.35.0": + version "8.35.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.0.tgz#86141e6c55b75bc1eaecc0781bd39704de14e52a" + integrity sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w== + dependencies: + "@typescript-eslint/project-service" "8.35.0" + "@typescript-eslint/tsconfig-utils" "8.35.0" + "@typescript-eslint/types" "8.35.0" + "@typescript-eslint/visitor-keys" "8.35.0" + debug "^4.3.4" + fast-glob "^3.3.2" + is-glob "^4.0.3" + minimatch "^9.0.4" + semver "^7.6.0" + ts-api-utils "^2.1.0" + +"@typescript-eslint/utils@8.35.0": + version "8.35.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.35.0.tgz#aaf0afab5ab51ea2f1897002907eacd9834606d5" + integrity sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg== dependencies: "@eslint-community/eslint-utils" "^4.7.0" - "@typescript-eslint/scope-manager" "8.34.0" - "@typescript-eslint/types" "8.34.0" - "@typescript-eslint/typescript-estree" "8.34.0" + "@typescript-eslint/scope-manager" "8.35.0" + "@typescript-eslint/types" "8.35.0" + "@typescript-eslint/typescript-estree" "8.35.0" "@typescript-eslint/utils@^6.0.0 || ^7.0.0 || ^8.0.0": version "8.23.0" @@ -3810,6 +3861,16 @@ "@typescript-eslint/types" "8.23.0" "@typescript-eslint/typescript-estree" "8.23.0" +"@typescript-eslint/utils@^8.32.1": + version "8.34.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.34.0.tgz#7844beebc1153b4d3ec34135c2da53a91e076f8d" + integrity sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ== + dependencies: + "@eslint-community/eslint-utils" "^4.7.0" + "@typescript-eslint/scope-manager" "8.34.0" + "@typescript-eslint/types" "8.34.0" + "@typescript-eslint/typescript-estree" "8.34.0" + "@typescript-eslint/visitor-keys@8.23.0": version "8.23.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.23.0.tgz#40405fd26a61d23f5f4c2ed0f016a47074781df8" @@ -3826,20 +3887,28 @@ "@typescript-eslint/types" "8.34.0" eslint-visitor-keys "^4.2.0" +"@typescript-eslint/visitor-keys@8.35.0": + version "8.35.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.0.tgz#93e905e7f1e94d26a79771d1b1eb0024cb159dbf" + integrity sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g== + dependencies: + "@typescript-eslint/types" "8.35.0" + eslint-visitor-keys "^4.2.1" + "@ungap/structured-clone@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@vector-im/compound-design-tokens@^4.0.0": - version "4.0.2" - resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-4.0.2.tgz#27363d26446eaa21880ab126fa51fec112e6fd86" - integrity sha512-y13bhPyJ5OzbGRl21F6+Y2adrjyK+mu67yKTx+o8MfmIpJzMSn4KkHZtcujMquWSh0e5ZAufsnk4VYvxbSpr1A== +"@vector-im/compound-design-tokens@^4.0.4": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-4.0.5.tgz#5a26437b6e28cd905e493941c2dfb6af1d439d5f" + integrity sha512-tZqcQhdp854SuBduP+A11xQ2FXk2vT7RXoklDF9jA3p5fLl83OJ66fjsqRdb5UUmfkfs2tM6fFWQiuymT9kx4Q== -"@vector-im/compound-web@^8.1.0": - version "8.1.0" - resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-8.1.0.tgz#9a99a8a9764ca5afcb231e275a9b1ff3b3e47a02" - integrity sha512-kjzrxpN03PCojcLiG08iJxqJU8ix/vx7BUdMHTWYALp92SmkVKjwuqwfz/vi7deqKVCgUc4aLmRnSYw/mDbGNw== +"@vector-im/compound-web@^8.1.2": + version "8.1.2" + resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-8.1.2.tgz#a8af8681b442e63e07b8f25204528b1b022c1db2" + integrity sha512-F9UyQBwRThwju+STz84iJy6JGWQ7UIxaprstfsGpiyS/3ror4E6m/mfwbrNjT0l3fhrhk6sRiTAMlcBRzYgdMQ== dependencies: "@floating-ui/react" "^0.27.0" "@radix-ui/react-context-menu" "^2.2.1" @@ -3851,16 +3920,16 @@ classnames "^2.5.1" vaul "^1.0.0" -"@vector-im/matrix-wysiwyg-wasm@link:../../../.cache/yarn/v6/npm-@vector-im-matrix-wysiwyg-2.38.3-cc54d8b3e9472bcd8e622126ba364ee31952cd8a-integrity/node_modules/bindings/wysiwyg-wasm": +"@vector-im/matrix-wysiwyg-wasm@link:../../../.cache/yarn/v6/npm-@vector-im-matrix-wysiwyg-2.38.4-fb0001dea01010a1e3ffc7042596e2d001ce9389-integrity/node_modules/bindings/wysiwyg-wasm": version "0.0.0" uid "" -"@vector-im/matrix-wysiwyg@2.38.3": - version "2.38.3" - resolved "https://registry.yarnpkg.com/@vector-im/matrix-wysiwyg/-/matrix-wysiwyg-2.38.3.tgz#cc54d8b3e9472bcd8e622126ba364ee31952cd8a" - integrity sha512-fqo8P55Vc/t0vxpFar9RDJN5gKEjJmzrLo+O4piDbFda6VrRoqrWAtiu0Au0g6B4hRDPKIuFupk8v9Ja7q8Hvg== +"@vector-im/matrix-wysiwyg@2.38.4": + version "2.38.4" + resolved "https://registry.yarnpkg.com/@vector-im/matrix-wysiwyg/-/matrix-wysiwyg-2.38.4.tgz#fb0001dea01010a1e3ffc7042596e2d001ce9389" + integrity sha512-X6ky+1cf33SPdEVd6iTmOKfZZ2mDJv9cz3sHtDhuclS6uitK3QE8td/pmGqBj4ek2Ia4y0mnU61LfxvMry1SMA== dependencies: - "@vector-im/matrix-wysiwyg-wasm" "link:../../../.cache/yarn/v6/npm-@vector-im-matrix-wysiwyg-2.38.3-cc54d8b3e9472bcd8e622126ba364ee31952cd8a-integrity/node_modules/bindings/wysiwyg-wasm" + "@vector-im/matrix-wysiwyg-wasm" "link:../../../.cache/yarn/v6/npm-@vector-im-matrix-wysiwyg-2.38.4-fb0001dea01010a1e3ffc7042596e2d001ce9389-integrity/node_modules/bindings/wysiwyg-wasm" "@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1": version "1.14.1" @@ -4756,9 +4825,9 @@ brace-expansion@^1.1.7: concat-map "0.0.1" brace-expansion@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" - integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + version "2.0.2" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7" + integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ== dependencies: balanced-match "^1.0.0" @@ -4934,10 +5003,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@1.0.30001721, caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001718: - version "1.0.30001721" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001721.tgz#36b90cd96901f8c98dd6698bf5c8af7d4c6872d7" - integrity sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ== +caniuse-lite@1.0.30001724, caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001718: + version "1.0.30001724" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001724.tgz#312e163553dd70d2c0fb603d74810c85d8ed94a0" + integrity sha512-WqJo7p0TbHDOythNTqYujmaJTvtYRZrjpP8TCvH6Vb9CYJerJNKamKzIWOM4BkQatWj9H2lYulpdAQNBe7QhNA== chalk@5.2.0: version "5.2.0" @@ -5301,9 +5370,9 @@ core-js-compat@^3.40.0: browserslist "^4.25.0" core-js@^3.0.0, core-js@^3.38.1: - version "3.42.0" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.42.0.tgz#edbe91f78ac8cfb6df8d997e74d368a68082fe37" - integrity sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g== + version "3.43.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.43.0.tgz#f7258b156523208167df35dea0cfd6b6ecd4ee88" + integrity sha512-N6wEbTTZSYOY2rYAn85CuvWWkCK6QweMn7/4Nr3w+gDBeBhk/x4EJeY6FPo4QzDoJZxVTv8U7CMvgWk6pOHHqA== core-util-is@~1.0.0: version "1.0.3" @@ -6846,7 +6915,7 @@ fflate@^0.4.8: resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae" integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA== -file-entry-cache@^10.1.0: +file-entry-cache@^10.1.1: version "10.1.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-10.1.1.tgz#ca46f5c4eb22cc37e4ac30214452a59c297d2119" integrity sha512-zcmsHjg2B2zjuBgjdnB+9q0+cWcgWfykIcsDkWDB4GTPtl1eXUA+gTI6sO0u01AqK3cliHryTU55/b2Ow1hfZg== @@ -7002,7 +7071,7 @@ foreachasync@^3.0.0: resolved "https://registry.yarnpkg.com/foreachasync/-/foreachasync-3.0.0.tgz#5502987dc8714be3392097f32e0071c9dee07cf6" integrity sha512-J+ler7Ta54FwwNcx6wQRDhTIbNeyDcARMkOcguEqnEdtm0jKvN3Li3PDAb2Du3ubJYEWfYL83XMROXdsXAXycw== -foreground-child@^3.1.0: +foreground-child@^3.1.0, foreground-child@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f" integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw== @@ -7247,6 +7316,18 @@ glob@^11.0.0: package-json-from-dist "^1.0.0" path-scurry "^2.0.0" +glob@^11.0.3: + version "11.0.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-11.0.3.tgz#9d8087e6d72ddb3c4707b1d2778f80ea3eaefcd6" + integrity sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA== + dependencies: + foreground-child "^3.3.1" + jackspeak "^4.1.1" + minimatch "^10.0.3" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^2.0.0" + glob@^7.1.3, glob@^7.1.4: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -7666,7 +7747,7 @@ ignore@^5.2.0: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== -ignore@^7.0.0, ignore@^7.0.4: +ignore@^7.0.0, ignore@^7.0.5: version "7.0.5" resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.5.tgz#4cb5f6cd7d4c7ab0365738c7aea888baa6d7efd9" integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg== @@ -8227,6 +8308,13 @@ jackspeak@^4.0.1: dependencies: "@isaacs/cliui" "^8.0.2" +jackspeak@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-4.1.1.tgz#96876030f450502047fc7e8c7fcf8ce8124e43ae" + integrity sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ== + dependencies: + "@isaacs/cliui" "^8.0.2" + jake@^10.8.5: version "10.9.2" resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.2.tgz#6ae487e6a69afec3a5e167628996b59f35ae2b7f" @@ -8859,9 +8947,9 @@ kleur@^3.0.3: integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== knip@^5.36.2: - version "5.60.2" - resolved "https://registry.yarnpkg.com/knip/-/knip-5.60.2.tgz#0deb8f5d72878f08a1dd8200d2655124204c5d56" - integrity sha512-TsYqEsoL3802RmhGL5MN7RLI6/03kocMYx/4BpMmwo3dSwEJxmzV7HqRxMVZr6c1llbd25+MqjgA86bv1IwsPA== + version "5.61.2" + resolved "https://registry.yarnpkg.com/knip/-/knip-5.61.2.tgz#c21df1547f4704ab488a2082c8f92520a5072b40" + integrity sha512-ZBv37zDvZj0/Xwk0e93xSjM3+5bjxgqJ0PH2GlB5tnWV0ktXtmatWLm+dLRUCT/vpO3SdGz2nNAfvVhuItUNcQ== dependencies: "@nodelib/fs.walk" "^1.2.3" fast-glob "^3.3.3" @@ -8882,6 +8970,11 @@ known-css-properties@^0.36.0: resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.36.0.tgz#5c4365f3c9549ca2e813d2e729e6c47ef6a6cb60" integrity sha512-A+9jP+IUmuQsNdsLdcg6Yt7voiMF/D4K83ew0OpJtpu+l34ef7LaohWV0Rc6KNvzw6ZDizkqfyB5JznZnzuKQA== +known-css-properties@^0.37.0: + version "0.37.0" + resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.37.0.tgz#10ebe49b9dbb6638860ff8a002fb65a053f4aec5" + integrity sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ== + language-subtag-registry@^0.3.20: version "0.3.23" resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz#23529e04d9e3b74679d70142df3fd2eb6ec572e7" @@ -8962,9 +9055,9 @@ linkifyjs@4.3.1: integrity sha512-DRSlB9DKVW04c4SUdGvKK5FR6be45lTU9M76JnngqPeeGDqPwYc0zdUErtsNVMtxPXgUWV4HbXbnC4sNyBxkYg== lint-staged@^16.0.0: - version "16.1.0" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-16.1.0.tgz#06807ef3dbbade9e4e3416897aac0ac5b99a2377" - integrity sha512-HkpQh69XHxgCjObjejBT3s2ILwNjFx8M3nw+tJ/ssBauDlIpkx2RpqWSi1fBgkXLSSXnbR3iEq1NkVtpvV+FLQ== + version "16.1.2" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-16.1.2.tgz#8cb84daa844f39c7a9790dd2c0caa327125ef059" + integrity sha512-sQKw2Si2g9KUZNY3XNvRuDq4UJqpHwF0/FQzZR2M7I5MvtpWvibikCjUVJzZdGE0ByurEl3KQNvsGetd1ty1/Q== dependencies: chalk "^5.4.1" commander "^14.0.0" @@ -9226,12 +9319,11 @@ matrix-events-sdk@0.0.1: integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== "matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": - version "37.7.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/c387f30e5c23c897877dd418545a306e6b8cf0c9" + version "37.10.0" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/de659d6431cbd4d072168f30744a1ca49d8ca4ce" dependencies: "@babel/runtime" "^7.12.5" - "@matrix-org/matrix-sdk-crypto-wasm" "^14.2.0" - "@matrix-org/olm" "3.2.15" + "@matrix-org/matrix-sdk-crypto-wasm" "^15.0.0" another-json "^0.2.0" bs58 "^6.0.0" content-type "^1.0.4" @@ -9426,6 +9518,13 @@ minimatch@^10.0.0: dependencies: brace-expansion "^2.0.1" +minimatch@^10.0.3: + version "10.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.0.3.tgz#cf7a0314a16c4d9ab73a7730a0e8e3c3502d47aa" + integrity sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw== + dependencies: + "@isaacs/brace-expansion" "^5.0.0" + minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -9727,10 +9826,10 @@ obuf@^1.0.0, obuf@^1.1.2: resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== -oidc-client-ts@3.2.1, oidc-client-ts@^3.0.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/oidc-client-ts/-/oidc-client-ts-3.2.1.tgz#d71d899dc0cddd11a8b84e41265fd79d7e0f152a" - integrity sha512-hS5AJ5s/x4bXhHvNJT1v+GGvzHUwdRWqNQQbSrp10L1IRmzfRGKQ3VWN3dstJb+oF3WtAyKezwD2+dTEIyBiAA== +oidc-client-ts@3.3.0, oidc-client-ts@^3.0.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/oidc-client-ts/-/oidc-client-ts-3.3.0.tgz#98a12e643b930825fb141634974e139d804ef972" + integrity sha512-t13S540ZwFOEZKLYHJwSfITugupW4uYLwuQSSXyKH/wHwZ+7FvgHE7gnNJh1YQIZ1Yd1hKSRjqeXGSUtS0r9JA== dependencies: jwt-decode "^4.0.0" @@ -10077,17 +10176,17 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -playwright-core@1.52.0, playwright-core@^1.51.0: - version "1.52.0" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.52.0.tgz#238f1f0c3edd4ebba0434ce3f4401900319a3dca" - integrity sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg== +playwright-core@1.53.1, playwright-core@^1.51.0: + version "1.53.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.53.1.tgz#0b6f7a2006ccb6126ffcc3e3b2fa9efda23b6638" + integrity sha512-Z46Oq7tLAyT0lGoFx4DOuB1IA9D1TPj0QkYxpPVUnGDqHHvDpCftu1J2hM2PiWsNMoZh8+LQaarAWcDfPBc6zg== -playwright@1.52.0: - version "1.52.0" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.52.0.tgz#26cb9a63346651e1c54c8805acfd85683173d4bd" - integrity sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw== +playwright@1.53.1: + version "1.53.1" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.53.1.tgz#86fb041b237a6868d163c87c4b9737fd1cac145e" + integrity sha512-LJ13YLr/ocweuwxyGf1XNFWIU4M2zUSo149Qbp+A4cpwDjsxRPj7k6H25LBrEHiEwxvRbD8HdwvQmRMSvquhYw== dependencies: - playwright-core "1.52.0" + playwright-core "1.53.1" optionalDependencies: fsevents "2.3.2" @@ -10741,19 +10840,19 @@ postcss@^8.4.40: picocolors "^1.1.1" source-map-js "^1.2.1" -postcss@^8.5.3: - version "8.5.4" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.4.tgz#d61014ac00e11d5f58458ed7247d899bd65f99c0" - integrity sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w== +postcss@^8.5.5: + version "8.5.6" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c" + integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg== dependencies: nanoid "^3.3.11" picocolors "^1.1.1" source-map-js "^1.2.1" -posthog-js@1.249.4: - version "1.249.4" - resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.249.4.tgz#449a908e7363be13b86aff59c917943147c8a4e1" - integrity sha512-Qq4cxDZ1P9BkwguuoVNTiLGQiET9vrzwjYWLS3DduKhRXqEzERLl9tOq2X8ZNPbo+D207+FILdWg/dTKUItfDg== +posthog-js@1.255.1: + version "1.255.1" + resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.255.1.tgz#c8c335f496d3062985fc00662804604cd3edb884" + integrity sha512-KMh0o9MhORhEZVjXpktXB5rJ8PfDk+poqBoTSoLzWgNjhJf6D8jcyB9jUMA6vVPfn4YeepVX5NuclDRqOwr5Mw== dependencies: core-js "^3.38.1" fflate "^0.4.8" @@ -12296,9 +12395,9 @@ stylelint-config-standard@^38.0.0: stylelint-config-recommended "^16.0.0" stylelint-scss@^6.0.0: - version "6.12.0" - resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-6.12.0.tgz#38cf41c3b8a76f34cd7267e4c30e7e66d35619c2" - integrity sha512-U7CKhi1YNkM1pXUXl/GMUXi8xKdhl4Ayxdyceie1nZ1XNIdaUgMV6OArpooWcDzEggwgYD0HP/xIgVJo9a655w== + version "6.12.1" + resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-6.12.1.tgz#7de9980a7c9acb7a3f203498e7296526cb52ffa0" + integrity sha512-UJUfBFIvXfly8WKIgmqfmkGKPilKB4L5j38JfsDd+OCg2GBdU0vGUV08Uw82tsRZzd4TbsUURVVNGeOhJVF7pA== dependencies: css-tree "^3.0.1" is-plain-object "^5.0.0" @@ -12318,13 +12417,13 @@ stylelint-value-no-unknown-custom-properties@^6.0.1: resolve "^1.22.8" stylelint@^16.13.0: - version "16.20.0" - resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-16.20.0.tgz#ec9eeddd49c20bbc397473505e63ad22f1639228" - integrity sha512-B5Myu9WRxrgKuLs3YyUXLP2H0mrbejwNxPmyADlACWwFsrL8Bmor/nTSh4OMae5sHjOz6gkSeccQH34gM4/nAw== + version "16.21.0" + resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-16.21.0.tgz#c24050d8e67621fa2c2269c082966e2c0f6a1883" + integrity sha512-ki3PpJGG7xhm3WtINoWGnlvqAmbqSexoRMbEMJzlwewSIOqPRKPlq452c22xAdEJISVi80r+I7KL9GPUiwFgbg== dependencies: - "@csstools/css-parser-algorithms" "^3.0.4" - "@csstools/css-tokenizer" "^3.0.3" - "@csstools/media-query-list-parser" "^4.0.2" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/media-query-list-parser" "^4.0.3" "@csstools/selector-specificity" "^5.0.0" "@dual-bundle/import-meta-resolve" "^4.1.0" balanced-match "^2.0.0" @@ -12335,21 +12434,21 @@ stylelint@^16.13.0: debug "^4.4.1" fast-glob "^3.3.3" fastest-levenshtein "^1.0.16" - file-entry-cache "^10.1.0" + file-entry-cache "^10.1.1" global-modules "^2.0.0" globby "^11.1.0" globjoin "^0.1.4" html-tags "^3.3.1" - ignore "^7.0.4" + ignore "^7.0.5" imurmurhash "^0.1.4" is-plain-object "^5.0.0" - known-css-properties "^0.36.0" + known-css-properties "^0.37.0" mathml-tag-names "^2.1.3" meow "^13.2.0" micromatch "^4.0.8" normalize-path "^3.0.0" picocolors "^1.1.1" - postcss "^8.5.3" + postcss "^8.5.5" postcss-resolve-nested-selector "^0.1.6" postcss-safe-parser "^7.0.1" postcss-selector-parser "^7.1.0" @@ -13637,11 +13736,11 @@ zip-stream@^6.0.1: readable-stream "^4.0.0" zod-validation-error@^3.0.3: - version "3.4.1" - resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-3.4.1.tgz#fb0a64f15d90f4aafe9ccc804331853609aad408" - integrity sha512-1KP64yqDPQ3rupxNv7oXhf7KdhHHgaqbKuspVoiN93TT0xrBjql+Svjkdjq/Qh/7GSMmgQs3AfvBT0heE35thw== + version "3.5.2" + resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-3.5.2.tgz#5af463c1acd4662e6b2610a75260931dbcb43a56" + integrity sha512-mdi7YOLtram5dzJ5aDtm1AG9+mxRma1iaMrZdYIpFO7epdKBUwLHIxTF8CPDeCQ828zAXYtizrKlEJAtzgfgrw== zod@^3.22.4: - version "3.25.57" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.57.tgz#317c8a6eb8a8460bb4b58defb19e8b50c1200e51" - integrity sha512-6tgzLuwVST5oLUxXTmBqoinKMd3JeesgbgseXeFasKKj8Q1FCZrHnbqJOyiEvr4cVAlbug+CgIsmJ8cl/pU5FA== + version "3.25.67" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.67.tgz#62987e4078e2ab0f63b491ef0c4f33df24236da8" + integrity sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==