Merge remote-tracking branch 'origin/develop' into hs/update-toggles-to-use-consistent-style
8
.github/workflows/docker.yaml
vendored
@ -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: .
|
||||
|
||||
2
.github/workflows/end-to-end-tests.yaml
vendored
@ -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 || '' }}
|
||||
|
||||
2
.github/workflows/tests.yml
vendored
@ -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
|
||||
|
||||
7
.github/workflows/update-topics.yaml
vendored
@ -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",
|
||||
|
||||
50
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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
23
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",
|
||||
|
||||
@ -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<ReactNode>)
|
||||
// constructor signature must match React.Component
|
||||
- | (new(props: P) => Component<any, any>);
|
||||
+ | (new(props: P, context?: any) => Component<any, any>);
|
||||
|
||||
/**
|
||||
* 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<P = {}, S = ComponentState> extends StaticLifecycle<P, S> {
|
||||
// constructor signature must match React.Component
|
||||
- new(props: P): Component<P, S>;
|
||||
+ new(props: P, context?: any): Component<P, S>;
|
||||
/**
|
||||
* Ignored by React.
|
||||
* @deprecated Only kept in types for backwards compatibility. Will be removed in a future major release.
|
||||
@ -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");
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
36
playwright/e2e/mobile-guide/mobile-guide.spec.ts
Normal file
@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
160
playwright/e2e/modules/custom-component.spec.ts
Normal file
@ -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),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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<Expectations>({
|
||||
`;
|
||||
}
|
||||
|
||||
if (options?.hideJumpToBottomButton) {
|
||||
css += `
|
||||
.mx_JumpToBottomButton {
|
||||
display: none !important;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
if (options?.css) {
|
||||
css += options.css;
|
||||
}
|
||||
|
||||
74
playwright/sample-files/custom-component-module.js
Normal file
@ -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() {}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 271 KiB After Width: | Height: | Size: 275 KiB |
@ -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<string>();
|
||||
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<void> {
|
||||
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;
|
||||
@ -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,
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 */
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
3
src/@types/matrix-js-sdk.d.ts
vendored
@ -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 {
|
||||
|
||||
@ -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<void> {
|
||||
await this.client?.setAccountData(RECOVERY_ACCOUNT_DATA_KEY, { enabled: false });
|
||||
}
|
||||
|
||||
private async ensureDeviceIdsAtStartPopulated(): Promise<void> {
|
||||
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<boolean> {
|
||||
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).
|
||||
|
||||
@ -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
|
||||
|
||||
@ -176,10 +176,11 @@ export async function withSecretStorageKeyCache<T>(func: () => Promise<T>): 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<void>, 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<void>, 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({
|
||||
|
||||
@ -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<IProps, IState> {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
// 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 (
|
||||
<div>
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderPhaseDone(): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<p>{_t("settings|key_backup|backup_in_progress")}</p>
|
||||
<DialogButtons primaryButton={_t("action|ok")} onPrimaryButtonClick={this.onDone} hasCancel={false} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 = (
|
||||
<div>
|
||||
<p>{_t("settings|key_backup|cannot_create_backup")}</p>
|
||||
<DialogButtons
|
||||
primaryButton={_t("action|retry")}
|
||||
onPrimaryButtonClick={this.createBackup}
|
||||
hasCancel={true}
|
||||
onCancel={this.onCancel}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
switch (this.state.phase) {
|
||||
case Phase.BackingUp:
|
||||
content = this.renderBusyPhase();
|
||||
break;
|
||||
case Phase.Done:
|
||||
content = this.renderPhaseDone();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_CreateKeyBackupDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={this.titleForPhase(this.state.phase)}
|
||||
hasCancel={[Phase.Done].includes(this.state.phase)}
|
||||
>
|
||||
<div>{content}</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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<IProps, IState> {
|
||||
public static defaultProps: Partial<IProps> = {
|
||||
forceReset: false,
|
||||
resetCrossSigning: false,
|
||||
};
|
||||
private recoveryKey?: GeneratedSecretStorageKey;
|
||||
private recoveryKeyNode = createRef<HTMLElement>();
|
||||
@ -211,7 +211,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
||||
private bootstrapSecretStorage = async (): Promise<void> => {
|
||||
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<IProp
|
||||
createSecretStorageKey: async () => 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 {
|
||||
|
||||
@ -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<IPr
|
||||
|
||||
private onSetupClick = (): void => {
|
||||
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 {
|
||||
|
||||
@ -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<IProps> = ({ parent, onFileDrop }) => {
|
||||
const FileDropTarget: React.FC<IProps> = ({ parent, onFileDrop, room }) => {
|
||||
const [state, setState] = useState<IState>({
|
||||
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<IProps> = ({ parent, onFileDrop }) => {
|
||||
parent?.removeEventListener("dragenter", onDragEnter);
|
||||
parent?.removeEventListener("dragleave", onDragLeave);
|
||||
};
|
||||
}, [parent, onFileDrop]);
|
||||
}, [parent, onFileDrop, hasPermission]);
|
||||
|
||||
if (state.dragging) {
|
||||
if (hasPermission && state.dragging) {
|
||||
return (
|
||||
<div className="mx_FileDropTarget">
|
||||
<img src={UploadBigSvg} className="mx_FileDropTarget_image" alt="" />
|
||||
|
||||
@ -316,7 +316,7 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement {
|
||||
<ErrorBoundary>
|
||||
<RoomHeader room={room} />
|
||||
<main className="mx_RoomView_body" ref={props.roomView} aria-label={_t("room|room_content")}>
|
||||
<FileDropTarget parent={props.roomView.current} onFileDrop={props.onFileDrop} />
|
||||
<FileDropTarget parent={props.roomView.current} onFileDrop={props.onFileDrop} room={room} />
|
||||
<div className="mx_RoomView_timeline">
|
||||
<ScrollPanel className="mx_RoomView_messagePanel" resizeNotifier={props.resizeNotifier}>
|
||||
{encryptionTile}
|
||||
@ -2564,7 +2564,11 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
{auxPanel}
|
||||
{pinnedMessageBanner}
|
||||
<main className={timelineClasses}>
|
||||
<FileDropTarget parent={this.roomView.current} onFileDrop={this.onFileDrop} />
|
||||
<FileDropTarget
|
||||
parent={this.roomView.current}
|
||||
onFileDrop={this.onFileDrop}
|
||||
room={this.state.room}
|
||||
/>
|
||||
{topUnreadMessagesBar}
|
||||
{jumpToBottom}
|
||||
{messagePanel}
|
||||
|
||||
@ -29,6 +29,7 @@ export class Tab<T extends string> {
|
||||
* @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<T extends string> {
|
||||
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<T extends string> {
|
||||
}
|
||||
|
||||
function TabLabel<T extends string>({ tab, isActive, showToolip, onClick }: ITabLabelProps<T>): JSX.Element {
|
||||
const classes = classNames("mx_TabbedView_tabLabel", {
|
||||
const classes = classNames("mx_TabbedView_tabLabel", tab.labelClassName, {
|
||||
mx_TabbedView_tabLabel_active: isActive,
|
||||
});
|
||||
|
||||
|
||||
@ -388,7 +388,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||
|
||||
timeline = (
|
||||
<>
|
||||
<FileDropTarget parent={this.card.current} onFileDrop={this.onFileDrop} />
|
||||
<FileDropTarget parent={this.card.current} onFileDrop={this.onFileDrop} room={this.props.room} />
|
||||
<TimelinePanel
|
||||
key={this.state.thread.id}
|
||||
ref={this.timelinePanel}
|
||||
|
||||
@ -0,0 +1,108 @@
|
||||
/*
|
||||
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, { useContext, useEffect, useState, useCallback } from "react";
|
||||
import { logger } from "@sentry/browser";
|
||||
import { type RoomMember, type Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import Modal from "../../../Modal";
|
||||
import ErrorDialog from "../../views/dialogs/ErrorDialog";
|
||||
import QuestionDialog from "../../views/dialogs/QuestionDialog";
|
||||
import { warnSelfDemote } from "../../views/right_panel/UserInfo";
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export interface UserInfoPowerLevelState {
|
||||
/**
|
||||
* default power level value of the selected user
|
||||
*/
|
||||
powerLevelUsersDefault: number;
|
||||
/**
|
||||
* The new power level to apply
|
||||
*/
|
||||
selectedPowerLevel: number;
|
||||
/**
|
||||
* Method to call When power level selection change
|
||||
*/
|
||||
onPowerChange: (powerLevel: number) => 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<unknown> => {
|
||||
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: (
|
||||
<div>
|
||||
{_t("user_info|promote_warning")}
|
||||
<br />
|
||||
{_t("common|are_you_sure")}
|
||||
</div>
|
||||
),
|
||||
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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
|
||||
@ -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<IProps, IState> {
|
||||
};
|
||||
|
||||
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<IProps, IState> {
|
||||
</div>
|
||||
);
|
||||
|
||||
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 = (
|
||||
<div>
|
||||
<div className="mx_Dialog_content" id="mx_Dialog_content">
|
||||
{description}
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={setupButtonCaption}
|
||||
primaryButton={_t("common|go_to_settings")}
|
||||
hasCancel={false}
|
||||
onPrimaryButtonClick={this.onSetRecoveryMethodClick}
|
||||
focus={true}
|
||||
|
||||
@ -7,6 +7,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 { ClientEvent, type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { Toast } from "@vector-im/compound-web";
|
||||
import React, { type JSX, useState } from "react";
|
||||
import UserProfileIcon from "@vector-im/compound-design-tokens/assets/web/icons/user-profile";
|
||||
@ -44,6 +45,7 @@ import { UserTab } from "./UserTab";
|
||||
import { type NonEmptyArray } from "../../../@types/common";
|
||||
import { SDKContext, type SdkContextClass } from "../../../contexts/SDKContext";
|
||||
import { useSettingValue } from "../../../hooks/useSettings";
|
||||
import { NoChange, useEventEmitterAsyncState, type AsyncStateCallbackResult } from "../../../hooks/useEventEmitter";
|
||||
import { ToastContext, useActiveToast } from "../../../contexts/ToastContext";
|
||||
import { EncryptionUserSettingsTab, type State } from "../settings/tabs/user/EncryptionUserSettingsTab";
|
||||
|
||||
@ -100,6 +102,26 @@ export default function UserSettingsDialog(props: IProps): JSX.Element {
|
||||
const [showMsc4108QrCode, setShowMsc4108QrCode] = useState(props.showMsc4108QrCode);
|
||||
const [initialEncryptionState, setInitialEncryptionState] = useState(props.initialEncryptionState);
|
||||
|
||||
// If the user doesn't have Recovery set up (no default Secret Storage key),
|
||||
// we show an indicator on the Encryption tab.
|
||||
const showSetupRecoveryIndicator = useEventEmitterAsyncState(
|
||||
props.sdkContext.client,
|
||||
ClientEvent.AccountData,
|
||||
async (event?: MatrixEvent): AsyncStateCallbackResult<boolean> => {
|
||||
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<Tab<UserTab>> => {
|
||||
const tabs: Tab<UserTab>[] = [];
|
||||
|
||||
@ -196,6 +218,7 @@ export default function UserSettingsDialog(props: IProps): JSX.Element {
|
||||
<KeyIcon />,
|
||||
<EncryptionUserSettingsTab initialState={initialEncryptionState} />,
|
||||
"UserSettingsEncryption",
|
||||
showSetupRecoveryIndicator ? "mx_SettingsDialog_tabLabelsAlert" : undefined,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@ -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<boolean>(false);
|
||||
const blobRef = useRef<Blob>(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 (
|
||||
<AccessibleButton
|
||||
className="mx_ImageView_button mx_ImageView_button_download"
|
||||
|
||||
@ -10,6 +10,7 @@ import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import React, { type JSX } from "react";
|
||||
import classNames from "classnames";
|
||||
import { DownloadIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { type MediaEventHelper } from "../../../utils/MediaEventHelper";
|
||||
import { RovingAccessibleButton } from "../../../accessibility/RovingTabIndex";
|
||||
@ -18,6 +19,7 @@ import { _t, _td, type TranslationKey } from "../../../languageHandler";
|
||||
import { FileDownloader } from "../../../utils/FileDownloader";
|
||||
import Modal from "../../../Modal";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import ModuleApi from "../../../modules/Api";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
@ -29,6 +31,7 @@ interface IProps {
|
||||
}
|
||||
|
||||
interface IState {
|
||||
canDownload: null | boolean;
|
||||
loading: boolean;
|
||||
blob?: Blob;
|
||||
tooltip: TranslationKey;
|
||||
@ -40,9 +43,29 @@ export default class DownloadActionButton extends React.PureComponent<IProps, IS
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
const moduleHints = ModuleApi.customComponents.getHintsForMessage(props.mxEvent);
|
||||
const downloadState: Pick<IState, "canDownload"> = { 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<IProps, IS
|
||||
spinner = <Spinner w={18} h={18} />;
|
||||
}
|
||||
|
||||
if (this.state.canDownload === null) {
|
||||
spinner = <Spinner w={18} h={18} />;
|
||||
}
|
||||
|
||||
if (this.state.canDownload === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const classes = classNames({
|
||||
mx_MessageActionBar_iconButton: true,
|
||||
mx_MessageActionBar_downloadButton: true,
|
||||
|
||||
@ -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 <PowerLevelEditor user={user} room={room} roomPermissions={roomPermissions} />;
|
||||
} else {
|
||||
const powerLevelUsersDefault = powerLevels.users_default || 0;
|
||||
const powerLevel = user.powerLevel;
|
||||
const role = textualPowerLevel(powerLevel, powerLevelUsersDefault);
|
||||
return (
|
||||
<div className="mx_UserInfo_profileField">
|
||||
<div className="mx_UserInfo_roleDescription">{role}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
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<unknown> => {
|
||||
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: (
|
||||
<div>
|
||||
{_t("user_info|promote_warning")}
|
||||
<br />
|
||||
{_t("common|are_you_sure")}
|
||||
</div>
|
||||
),
|
||||
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 (
|
||||
<div className="mx_UserInfo_profileField">
|
||||
<PowerSelector
|
||||
label={undefined}
|
||||
value={selectedPowerLevel}
|
||||
maxValue={roomPermissions.modifyLevelMax}
|
||||
usersDefault={powerLevelUsersDefault}
|
||||
onChange={onPowerChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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 = (
|
||||
<PowerLevelSection
|
||||
powerLevels={powerLevels}
|
||||
user={member as RoomMember}
|
||||
room={room}
|
||||
roomPermissions={roomPermissions}
|
||||
/>
|
||||
<PowerLevelSection user={member as RoomMember} room={room} roomPermissions={roomPermissions} />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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 <PowerLevelEditor vm={vm} roomPermissions={roomPermissions} />;
|
||||
}
|
||||
|
||||
const powerLevel = user.powerLevel;
|
||||
const role = textualPowerLevel(powerLevel, vm.powerLevelUsersDefault);
|
||||
return (
|
||||
<div className="mx_UserInfo_profileField">
|
||||
<div className="mx_UserInfo_roleDescription">{role}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const PowerLevelEditor: React.FC<{
|
||||
vm: UserInfoPowerLevelState;
|
||||
roomPermissions: IRoomPermissions;
|
||||
}> = ({ vm, roomPermissions }) => {
|
||||
return (
|
||||
<div className="mx_UserInfo_profileField">
|
||||
<PowerSelector
|
||||
label={undefined}
|
||||
value={vm.selectedPowerLevel}
|
||||
maxValue={roomPermissions.modifyLevelMax}
|
||||
usersDefault={vm.powerLevelUsersDefault}
|
||||
onChange={vm.onPowerChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -76,7 +76,17 @@ const E2EIcon: React.FC<Props> = ({
|
||||
if (onClick) {
|
||||
content = <AccessibleButton onClick={onClick} className={classes} style={style} />;
|
||||
} else {
|
||||
content = <div className={classes} style={style} />;
|
||||
// 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 = (
|
||||
<div className={classes} style={style}>
|
||||
<div className="mx_E2EIcon_normal" />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
content = <div className={classes} style={style} />;
|
||||
}
|
||||
}
|
||||
|
||||
if (!e2eTitle || hideTooltip) {
|
||||
|
||||
@ -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<RoomListItemContextMenuViewProps>): JSX.Element {
|
||||
const vm = useRoomListItemMenuViewModel(room);
|
||||
|
||||
return (
|
||||
<ContextMenu
|
||||
title={_t("room_list|room|more_options")}
|
||||
showTitle={false}
|
||||
// To not mess with the roving tab index of the button
|
||||
hasAccessibleAlternative={true}
|
||||
trigger={children}
|
||||
onOpenChange={setMenuOpen}
|
||||
>
|
||||
<MoreOptionContent vm={vm} />
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
@ -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={<MoreOptionsButton size="24px" />}
|
||||
>
|
||||
<MoreOptionContent vm={vm} />
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
interface MoreOptionContentProps {
|
||||
/**
|
||||
* The view model state for the menu.
|
||||
*/
|
||||
vm: RoomListItemMenuViewState;
|
||||
}
|
||||
|
||||
export function MoreOptionContent({ vm }: MoreOptionContentProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{vm.canMarkAsRead && (
|
||||
<MenuItem
|
||||
Icon={MarkAsReadIcon}
|
||||
@ -143,7 +157,7 @@ function MoreOptionsMenu({ vm, setMenuOpen }: MoreOptionsMenuProps): JSX.Element
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
hideChevron={true}
|
||||
/>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -154,7 +168,7 @@ interface MoreOptionsButtonProps extends ComponentProps<typeof IconButton> {
|
||||
/**
|
||||
* 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 (
|
||||
<Tooltip label={_t("room_list|room|more_options")}>
|
||||
<IconButton aria-label={_t("room_list|room|more_options")} {...props}>
|
||||
@ -244,7 +258,7 @@ interface NotificationButtonProps extends ComponentProps<typeof IconButton> {
|
||||
/**
|
||||
* A button to trigger the notification menu.
|
||||
*/
|
||||
export const NotificationButton = function MoreOptionsButton({
|
||||
const NotificationButton = function MoreOptionsButton({
|
||||
isRoomMuted,
|
||||
ref,
|
||||
...props
|
||||
|
||||
@ -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<HTMLButtonElement> {
|
||||
/**
|
||||
@ -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 = (
|
||||
<button
|
||||
ref={ref}
|
||||
className={classNames("mx_RoomListItemView", {
|
||||
@ -92,17 +99,7 @@ export const RoomListItemView = memo(function RoomListItemView({
|
||||
{showHoverMenu ? (
|
||||
<RoomListItemMenuView
|
||||
room={room}
|
||||
setMenuOpen={(isOpen) => {
|
||||
if (isOpen) {
|
||||
setIsMenuOpen(isOpen);
|
||||
} else {
|
||||
// To avoid icon blinking when closing the menu, we delay the state update
|
||||
setTimeout(() => setIsMenuOpen(isOpen), 0);
|
||||
// After closing the menu, we need to set the focus back to the button
|
||||
// 10ms because the focus moves to the body and we put back the focus on the button
|
||||
setTimeout(() => buttonRef.current?.focus(), 10);
|
||||
}
|
||||
}}
|
||||
setMenuOpen={(isOpen) => (isOpen ? setIsMenuOpen(true) : closeMenu())}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
@ -120,6 +117,24 @@ export const RoomListItemView = memo(function RoomListItemView({
|
||||
</Flex>
|
||||
</button>
|
||||
);
|
||||
|
||||
if (!vm.showContextMenu) return content;
|
||||
|
||||
return (
|
||||
<RoomListItemContextMenuView
|
||||
room={room}
|
||||
setMenuOpen={(isOpen) => {
|
||||
if (isOpen) {
|
||||
// To avoid icon blinking when the context menu is re-opened
|
||||
setTimeout(() => setIsMenuOpen(true), 0);
|
||||
} else {
|
||||
closeMenu();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</RoomListItemContextMenuView>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@ -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 (
|
||||
<Heading className="mx_SettingsHeader" as="h2" size="sm" weight="semibold">
|
||||
{label} {hasRecommendedTag && <span>{_t("common|recommended")}</span>}
|
||||
<Heading className={classes} as="h2" size="sm" weight="semibold">
|
||||
{label}
|
||||
</Heading>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -161,7 +161,7 @@ export default function NotificationSettings2(): JSX.Element {
|
||||
description={_t("settings|notifications|play_sound_for_description")}
|
||||
>
|
||||
<LabelledCheckbox
|
||||
label="People"
|
||||
label={_t("common|people")}
|
||||
value={settings.sound.people !== undefined}
|
||||
disabled={disabled || settings.defaultLevels.dm === RoomNotifState.MentionsOnly}
|
||||
onChange={(value) => {
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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<T>(
|
||||
useEventEmitter(emitter, eventName, handler);
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* The return value of the callback function for `useEventEmitterAsyncState`.
|
||||
*/
|
||||
export type AsyncStateCallbackResult<T> = Promise<T | NoChange>;
|
||||
|
||||
/**
|
||||
* 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<T, Events extends string, Arguments extends ListenerMap<Events>>(
|
||||
emitter: TypedEventEmitter<Events, Arguments> | undefined,
|
||||
eventName: string | symbol,
|
||||
fn: Mapper<AsyncStateCallbackResult<T>>,
|
||||
deps: DependencyList,
|
||||
initialValue: T,
|
||||
): T;
|
||||
export function useEventEmitterAsyncState<T, Events extends string, Arguments extends ListenerMap<Events>>(
|
||||
emitter: TypedEventEmitter<Events, Arguments> | undefined,
|
||||
eventName: string | symbol,
|
||||
fn: Mapper<AsyncStateCallbackResult<T>>,
|
||||
deps: DependencyList,
|
||||
initialValue?: T,
|
||||
): T | undefined;
|
||||
export function useEventEmitterAsyncState<T, Events extends string, Arguments extends ListenerMap<Events>>(
|
||||
emitter: TypedEventEmitter<Events, Arguments> | undefined,
|
||||
eventName: string | symbol,
|
||||
fn: Mapper<AsyncStateCallbackResult<T>>,
|
||||
deps: DependencyList,
|
||||
initialValue?: T,
|
||||
): T | undefined {
|
||||
const [value, setValue] = useState<T | undefined>(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 {}
|
||||
|
||||
@ -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 <strong>%(notificationState)s</strong>",
|
||||
"notifications_debug": "Ladění oznámení",
|
||||
@ -967,7 +970,6 @@
|
||||
},
|
||||
"reset_all_button": "Zapomněli nebo ztratili jste všechny metody obnovy? <a>Resetovat vše</a>",
|
||||
"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",
|
||||
|
||||
@ -962,7 +962,6 @@
|
||||
},
|
||||
"reset_all_button": "Wedi anghofio neu golli pob dull adfer? <a>Ailosod y cyfan</a>",
|
||||
"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",
|
||||
|
||||
@ -964,7 +964,6 @@
|
||||
},
|
||||
"reset_all_button": "Hast du alle Wiederherstellungsmethoden vergessen? <a>Setze sie hier zurück</a>",
|
||||
"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",
|
||||
|
||||
@ -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": "Ακολουθήστε τις οδηγίες που στάλθηκαν στο <b>%(email)s</b>",
|
||||
"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": "Το <b>%(homeserver)s</b> θα σας στείλει έναν σύνδεσμο επαλήθευσης για να επαναφέρετε τον κωδικό πρόσβασής σας.",
|
||||
"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 και ξεχάσετε τον κωδικό πρόσβασης, ενδέχεται να <b>χάσετε οριστικά την πρόσβαση στον λογαριασμό σας</b>.",
|
||||
@ -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": "Έχετε ήδη λογαριασμό; <a>Συνδεθείτε εδώ</a>",
|
||||
"sign_in_or_register": "Συνδεθείτε ή Δημιουργήστε Λογαριασμό",
|
||||
"sign_in_or_register_description": "Χρησιμοποιήστε τον λογαριασμό σας ή δημιουργήστε νέο για να συνεχίσετε.",
|
||||
"sign_in_prompt": "Έχετε λογαριασμό; <a>Συνδεθείτε</a>",
|
||||
"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": "Δεν το λάβατε; <a>Στείλτε το ξανά</a>",
|
||||
"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 που μόλις στείλαμε στη διεύθυνση <b>%(email)s</b>",
|
||||
"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": "Οποιοσδήποτε θα μπορεί να βρει και να εγγραφεί σε αυτόν τον χώρο, όχι μόνο μέλη του <SpaceName/>.",
|
||||
"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. <default> Χρησιμοποιήστε τον προεπιλεγμένο (%(defaultIdentityServerName)s)</default> ή διαμορφώστε στις <settings>Ρυθμίσεις</settings>.",
|
||||
"email_use_is": "Χρησιμοποιήστε έναν διακομιστή ταυτότητας για πρόσκληση μέσω email. Διαχείριση στις <settings>Ρυθμίσεις</settings>.",
|
||||
"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": "<Items/> και ένα ακόμα",
|
||||
@ -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": "Έχετε βάλει την κλήση σε αναμονή <a>Επαναφορά</a>",
|
||||
"call_held_switch": "Έχετε βάλει την κλήση σε αναμονή <a>Switch</a>",
|
||||
"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": "Παρακαλείστε να ρωτήσετε τον διαχειριστή του κεντρικού διακομιστή σας (<code>%(homeserverDomain)s</code>) να ρυθμίσουν έναν διακομιστή πρωτοκόλλου TURN ώστε οι κλήσεις να λειτουργούν απρόσκοπτα.",
|
||||
"misconfigured_server_fallback": "Εναλλακτικά, μπορείτε να δοκιμάσετε να χρησιμοποιήσετε τον δημόσιο διακομιστή στη διεύθυνση <server/> , αλλά αυτό δεν θα είναι τόσο αξιόπιστο και θα κοινοποιήσει τη διεύθυνση 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": "Η χρήση αυτής της μικροεφαρμογής ενδέχεται να μοιράζεται δεδομένα <helpIcon /> με %(widgetDomain)s.",
|
||||
"shared_data_warning_im": "Η χρήση αυτής της μικροεφαρμογής μπορεί να μοιραστεί δεδομένα <helpIcon /> με το %(widgetDomain)s και τον διαχειριστή πρόσθετων.",
|
||||
"shared_data_widget_id": "Ταυτότητα μικροεφαρμογής",
|
||||
"unencrypted_warning": "Οι μικροεοεφαρμογές δε χρησιμοποιούν κρυπτογράφηση μηνυμάτων.",
|
||||
"unmaximise": "Απο-μεγιστοποίηση",
|
||||
"unpin_to_view_right_panel": "Ξεκαρφιτσώστε αυτήν τη μικροεφαρμογή για να την προβάλετε σε αυτόν τον πίνακα"
|
||||
},
|
||||
"zxcvbn": {
|
||||
|
||||
@ -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 <a>your config</a> 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? <a>Reset all</a>",
|
||||
"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": "<strong>Settings:</strong> 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": {
|
||||
|
||||
@ -968,7 +968,6 @@
|
||||
},
|
||||
"reset_all_button": "Unustasid või oled kaotanud kõik võimalused ligipääsu taastamiseks? <a>Lähtesta kõik ühe korraga</a>",
|
||||
"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",
|
||||
|
||||
@ -829,7 +829,6 @@
|
||||
},
|
||||
"reset_all_button": "Unohtanut tai kadottanut kaikki palautustavat? <a>Nollaa kaikki</a>",
|
||||
"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",
|
||||
|
||||
@ -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 <strong>%(notificationState)s</strong>",
|
||||
"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 ? <a>Tout réinitialiser</a>",
|
||||
"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"
|
||||
},
|
||||
|
||||
@ -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: <strong>%(notificationState)s</strong>",
|
||||
"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? <a>Minden alaphelyzetbe állítása</a>",
|
||||
"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"
|
||||
},
|
||||
|
||||
@ -965,7 +965,6 @@
|
||||
},
|
||||
"reset_all_button": "Lupa atau kehilangan semua metode pemulihan? <a>Atur ulang semuanya</a>",
|
||||
"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",
|
||||
|
||||
@ -968,7 +968,6 @@
|
||||
},
|
||||
"reset_all_button": "Glemt eller mistet alle gjenopprettingsmetoder?<a> Tilbakestill alt</a>",
|
||||
"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",
|
||||
|
||||
@ -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? <a>Zaloguj się tutaj</a>",
|
||||
"sign_in_instead_prompt": "Masz już konto? <a>Zaloguj się tutaj </a>",
|
||||
"sign_in_or_register": "Zaloguj się lub utwórz konto",
|
||||
"sign_in_or_register_description": "Użyj konta lub utwórz nowe, aby kontynuować.",
|
||||
"sign_in_prompt": "Posiadasz już konto? <a>Zaloguj się</a>",
|
||||
@ -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? <a>Resetuj wszystko</a>",
|
||||
"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": {
|
||||
|
||||
@ -936,7 +936,6 @@
|
||||
},
|
||||
"reset_all_button": "Esqueceste-te ou perdeste todos os métodos de recuperação? <a>Repor tudo</a>",
|
||||
"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",
|
||||
|
||||