Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 56b60e845e | |||
| 87509b567a | |||
| 32c7c33e2b | |||
| 736e638e68 | |||
| 7e1590cb0e | |||
| 6562f5ac20 | |||
| e0df420ade | |||
| 7e48f2b6b6 | |||
| a9f4ccaa02 | |||
| 6f00235432 | |||
| 5014f0b411 | |||
| 1415354f2a | |||
| dd8612d76b | |||
| c31444bfda | |||
| 88d4f369eb | |||
| e225c23fba | |||
| 7f39bb61ec | |||
| 75083c2e80 | |||
| 65eb4ce1d3 | |||
| 2d28b79432 | |||
| 6bedb1525d | |||
| 6a233b513a | |||
| 4cd5991cac | |||
| 2f238ed300 | |||
| 15af27b906 | |||
| 55d07e1703 | |||
| b5d8e63c6d | |||
| c8d937655b | |||
| ca3060af69 | |||
| 479b451916 | |||
| b89de61e12 | |||
| 3c6d341357 | |||
| 7f0c0ca599 | |||
| e165d0db1c | |||
| 7007c2095c | |||
| d79becc2a9 | |||
| e3dceb3718 | |||
| 1a77f6126d | |||
| dae1cbf590 | |||
| c1689ad226 | |||
| 03129e8f10 | |||
| 78cb26201b | |||
| 38a0d28453 | |||
| b4ba350770 | |||
| db2e958823 | |||
| 25a8591791 | |||
| 570e263ca1 | |||
| a2e18a61a8 | |||
| 2146ff6f23 | |||
| 115e3cd3d8 | |||
| 155c0b6a0c | |||
| 841f12bd46 | |||
| 6f41ac58bc | |||
| 902cdb0810 | |||
| 4fd752fe29 | |||
| d83a2d2b93 | |||
| fb9d5db6ec | |||
| 0ca23af4a9 | |||
| 5db72f7fab | |||
| d193434b30 | |||
| 1bbe4802e4 | |||
| 169ae025bf | |||
| e666fa33f3 | |||
| c9f375a02b |
@@ -18,9 +18,10 @@
|
||||
/playwright/e2e/settings/encryption-user-tab/ @element-hq/element-crypto-web-reviewers
|
||||
|
||||
|
||||
/src/models/Call.ts @element-hq/element-call-reviewers
|
||||
/src/call-types.ts @element-hq/element-call-reviewers
|
||||
/src/components/views/voip @element-hq/element-call-reviewers
|
||||
/src/models/Call.ts @element-hq/element-call-reviewers
|
||||
/src/call-types.ts @element-hq/element-call-reviewers
|
||||
/src/components/views/voip @element-hq/element-call-reviewers
|
||||
/playwright/e2e/voip/element-call.spec.ts @element-hq/element-call-reviewers
|
||||
|
||||
# Ignore translations as those will be updated by GHA for Localazy download
|
||||
/src/i18n/strings
|
||||
|
||||
@@ -10,8 +10,7 @@ concurrency:
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
# develop pushes and repository_dispatch handled in build_develop.yaml
|
||||
env:
|
||||
# These must be set for fetchdep.sh to get the right branch
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
# This must be set for fetchdep.sh to get the right branch
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
permissions: {} # No permissions required
|
||||
jobs:
|
||||
@@ -45,7 +44,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
with:
|
||||
# Disable cache on Windows as it is slower than not caching
|
||||
# https://github.com/actions/setup-node/issues/975
|
||||
@@ -56,15 +55,7 @@ jobs:
|
||||
- run: yarn config set network-timeout 300000
|
||||
|
||||
- name: Fetch layered build
|
||||
id: layered_build
|
||||
env:
|
||||
# tell layered.sh to check out the right sha of the JS-SDK & EW, if they were given one
|
||||
JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }}
|
||||
run: |
|
||||
scripts/layered.sh
|
||||
JSSDK_SHA=$(git -C matrix-js-sdk rev-parse --short=12 HEAD)
|
||||
VECTOR_SHA=$(git rev-parse --short=12 HEAD)
|
||||
echo "VERSION=$VECTOR_SHA--js-$JSSDK_SHA" >> $GITHUB_OUTPUT
|
||||
run: ./scripts/layered.sh
|
||||
|
||||
- name: Copy config
|
||||
run: cp element.io/develop/config.json config.json
|
||||
@@ -72,9 +63,7 @@ jobs:
|
||||
- name: Build
|
||||
env:
|
||||
CI_PACKAGE: true
|
||||
VERSION: "${{ steps.layered_build.outputs.VERSION }}"
|
||||
run: |
|
||||
yarn build
|
||||
run: VERSION=$(scripts/get-version-from-git.sh) yarn build
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
repository: matrix-org/matrix-js-sdk
|
||||
path: matrix-js-sdk
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
with:
|
||||
cache: "yarn"
|
||||
cache-dependency-path: element-web/yarn.lock
|
||||
|
||||
@@ -54,21 +54,16 @@ jobs:
|
||||
with:
|
||||
repository: element-hq/element-web
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
|
||||
- name: Fetch layered build
|
||||
id: layered_build
|
||||
env:
|
||||
# tell layered.sh to check out the right sha of the JS-SDK & EW, if they were given one
|
||||
JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }}
|
||||
run: |
|
||||
scripts/layered.sh
|
||||
JSSDK_SHA=$(git -C matrix-js-sdk rev-parse --short=12 HEAD)
|
||||
VECTOR_SHA=$(git rev-parse --short=12 HEAD)
|
||||
echo "VERSION=$VECTOR_SHA--js-$JSSDK_SHA" >> $GITHUB_OUTPUT
|
||||
run: scripts/layered.sh
|
||||
|
||||
- name: Copy config
|
||||
run: cp element.io/develop/config.json config.json
|
||||
@@ -76,9 +71,7 @@ jobs:
|
||||
- name: Build
|
||||
env:
|
||||
CI_PACKAGE: true
|
||||
VERSION: "${{ steps.layered_build.outputs.VERSION }}"
|
||||
run: |
|
||||
yarn build
|
||||
run: VERSION=$(scripts/get-version-from-git.sh) yarn build
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
@@ -89,7 +82,7 @@ jobs:
|
||||
|
||||
- name: Calculate runner variables
|
||||
id: runner-vars
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
script: |
|
||||
const numRunners = parseInt(process.env.NUM_RUNNERS, 10);
|
||||
@@ -140,7 +133,7 @@ jobs:
|
||||
name: webapp
|
||||
path: webapp
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
with:
|
||||
cache: "yarn"
|
||||
cache-dependency-path: yarn.lock
|
||||
@@ -207,7 +200,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
repository: element-hq/element-web
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
if: inputs.skip != true
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
name: Tidy closed issues
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
id: main
|
||||
with:
|
||||
# PAT needed as the GITHUB_TOKEN won't be able to see cross-references from other orgs (matrix-org)
|
||||
@@ -142,7 +142,7 @@ jobs:
|
||||
});
|
||||
}
|
||||
}
|
||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
name: Close duplicate as Not Planned
|
||||
if: steps.main.outputs.closeAsNotPlanned
|
||||
with:
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
URL: "https://github.com/pulls?q=is%3Apr+is%3Aopen+repo%3Amatrix-org%2Fmatrix-js-sdk+repo%3Amatrix-org%2Fmatrix-react-sdk+repo%3Aelement-hq%2Felement-web+repo%3Aelement-hq%2Felement-desktop+review-requested%3A%40me+sort%3Aupdated-desc+"
|
||||
RELEASE_BLOCKERS_URL: "https://github.com/pulls?q=is%3Aopen+repo%3Amatrix-org%2Fmatrix-js-sdk+repo%3Amatrix-org%2Fmatrix-react-sdk+repo%3Aelement-hq%2Felement-web+repo%3Aelement-hq%2Felement-desktop+sort%3Aupdated-desc+label%3AX-Release-Blocker+"
|
||||
steps:
|
||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
env:
|
||||
HS_URL: ${{ secrets.BETABOT_HS_URL }}
|
||||
ROOM_ID: ${{ secrets.ROOM_ID }}
|
||||
|
||||
@@ -8,7 +8,7 @@ jobs:
|
||||
name: Check PR base branch
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
script: |
|
||||
const baseBranch = context.payload.pull_request.base.ref;
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
repository: element-hq/element-web
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
|
||||
@@ -12,8 +12,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
# These must be set for fetchdep.sh to get the right branch
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
# This must be set for fetchdep.sh to get the right branch
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
permissions: {} # No permissions required
|
||||
@@ -25,7 +24,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
@@ -70,7 +69,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
@@ -88,7 +87,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
@@ -106,7 +105,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
@@ -124,7 +123,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
repository: ${{ inputs.matrix-js-sdk-sha && 'element-hq/element-web' || github.repository }}
|
||||
|
||||
- name: Yarn cache
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
cache: "yarn"
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
contains(github.event.issue.labels.*.name, 'A-Rich-Text-Editor') ||
|
||||
contains(github.event.issue.labels.*.name, 'A-Element-Call')
|
||||
steps:
|
||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.addLabels({
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
contains(github.event.issue.labels.*.name, 'good first issue') ||
|
||||
contains(github.event.issue.labels.*.name, 'Hacktoberfest')
|
||||
steps:
|
||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.addLabels({
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9
|
||||
- uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10
|
||||
with:
|
||||
operations-per-run: 100
|
||||
|
||||
|
||||
@@ -5,44 +5,25 @@ on:
|
||||
types: [unlabeled]
|
||||
permissions: {}
|
||||
jobs:
|
||||
Move_Unabeled_Issue_On_Project_Board:
|
||||
move_no_longer_needs_info_issues:
|
||||
name: Move no longer X-Needs-Info issues to Triaged
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
repository-projects: read
|
||||
if: >
|
||||
${{
|
||||
!contains(github.event.issue.labels.*.name, 'X-Needs-Info') }}
|
||||
env:
|
||||
BOARD_NAME: "Issue triage"
|
||||
OWNER: ${{ github.repository_owner }}
|
||||
REPO: ${{ github.event.repository.name }}
|
||||
ISSUE: ${{ github.event.issue.number }}
|
||||
!contains(github.event.issue.labels.*.name, 'X-Needs-Info')
|
||||
steps:
|
||||
- name: Check if issue is already in "${{ env.BOARD_NAME }}"
|
||||
run: |
|
||||
json=$(curl -s -H 'Content-Type: application/json' -H "Authorization: bearer ${{ secrets.GITHUB_TOKEN }}" -X POST -d '{"query": "query($issue: Int!, $owner: String!, $repo: String!) { repository(owner: $owner, name: $repo) { issue(number: $issue) { projectCards { nodes { project { name } isArchived } } } } } ", "variables" : "{ \"issue\": '${ISSUE}', \"owner\": \"'${OWNER}'\", \"repo\": \"'${REPO}'\" }" }' https://api.github.com/graphql)
|
||||
if echo $json | jq '.data.repository.issue.projectCards.nodes | length'; then
|
||||
if [[ $(echo $json | jq '.data.repository.issue.projectCards.nodes[0].project.name') =~ "${BOARD_NAME}" ]]; then
|
||||
if [[ $(echo $json | jq '.data.repository.issue.projectCards.nodes[0].isArchived') == 'true' ]]; then
|
||||
echo "Issue is already in Project '$BOARD_NAME', but is archived - skipping workflow";
|
||||
echo "SKIP_ACTION=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "Issue is already in Project '$BOARD_NAME', proceeding";
|
||||
echo "ALREADY_IN_BOARD=true" >> $GITHUB_ENV
|
||||
fi
|
||||
else
|
||||
echo "Issue is not in project '$BOARD_NAME', cancelling this workflow"
|
||||
echo "ALREADY_IN_BOARD=false" >> $GITHUB_ENV
|
||||
fi
|
||||
fi
|
||||
- name: Move issue
|
||||
uses: alex-page/github-project-automation-plus@303f24a24c67ce7adf565a07e96720faf126fe36
|
||||
if: ${{ env.ALREADY_IN_BOARD == 'true' && env.SKIP_ACTION != 'true' }}
|
||||
- id: set_fields
|
||||
uses: nipe0324/update-project-v2-item-field@c4af58452d1c5a788c1ea4f20e073fa722ec4a6b #v2.0.2
|
||||
with:
|
||||
project: Issue triage
|
||||
column: Triaged
|
||||
repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
project-url: ${{ env.PROJECT_URL }}
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
skip-update-script: |
|
||||
const isIssue = item.type === 'ISSUE'
|
||||
const status = item.fieldValues['Status']
|
||||
return !isIssue || status !== 'Needs info'
|
||||
field-name: Status
|
||||
field-value: "Triaged"
|
||||
env:
|
||||
PROJECT_URL: https://github.com/orgs/element-hq/projects/120
|
||||
|
||||
remove_Z-Labs_label:
|
||||
name: Remove Z-Labs label when features behind labs flags are removed
|
||||
@@ -62,7 +43,7 @@ jobs:
|
||||
contains(github.event.issue.labels.*.name, 'A-Element-Call')) &&
|
||||
contains(github.event.issue.labels.*.name, 'Z-Labs')
|
||||
steps:
|
||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.removeLabel({
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
environment: Matrix
|
||||
steps:
|
||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
env:
|
||||
HS_URL: ${{ secrets.BETABOT_HS_URL }}
|
||||
LOBBY_ROOM_ID: ${{ secrets.ROOM_ID }}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import type { ArgTypes, Preview, Decorator } from "@storybook/react-vite";
|
||||
import { addons } from "storybook/preview-api";
|
||||
import type { ArgTypes, Preview, Decorator, ReactRenderer, StrictArgs } from "@storybook/react-vite";
|
||||
|
||||
import "../res/css/shared.pcss";
|
||||
import "./preview.css";
|
||||
import React, { useLayoutEffect } from "react";
|
||||
import { FORCE_RE_RENDER } from "storybook/internal/core-events";
|
||||
import { setLanguage } from "../src/shared-components/utils/i18n";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
import { StoryContext } from "storybook/internal/csf";
|
||||
|
||||
export const globalTypes = {
|
||||
theme: {
|
||||
@@ -59,29 +58,9 @@ const withThemeProvider: Decorator = (Story, context) => {
|
||||
);
|
||||
};
|
||||
|
||||
const LanguageSwitcher: React.FC<{
|
||||
language: string;
|
||||
}> = ({ language }) => {
|
||||
useLayoutEffect(() => {
|
||||
const changeLanguage = async (language: string) => {
|
||||
await setLanguage(language);
|
||||
// Force the component to re-render to apply the new language
|
||||
addons.getChannel().emit(FORCE_RE_RENDER);
|
||||
};
|
||||
changeLanguage(language);
|
||||
}, [language]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const withLanguageProvider: Decorator = (Story, context) => {
|
||||
return (
|
||||
<>
|
||||
<LanguageSwitcher language={context.globals.language} />
|
||||
<Story />
|
||||
</>
|
||||
);
|
||||
};
|
||||
async function languageLoader(context: StoryContext<ReactRenderer, StrictArgs>): Promise<void> {
|
||||
await setLanguage(context.globals.language);
|
||||
}
|
||||
|
||||
const withTooltipProvider: Decorator = (Story) => {
|
||||
return (
|
||||
@@ -93,7 +72,7 @@ const withTooltipProvider: Decorator = (Story) => {
|
||||
|
||||
const preview: Preview = {
|
||||
tags: ["autodocs"],
|
||||
decorators: [withThemeProvider, withLanguageProvider, withTooltipProvider],
|
||||
decorators: [withThemeProvider, withTooltipProvider],
|
||||
parameters: {
|
||||
options: {
|
||||
storySort: {
|
||||
@@ -108,6 +87,7 @@ const preview: Preview = {
|
||||
test: "error",
|
||||
},
|
||||
},
|
||||
loaders: [languageLoader],
|
||||
};
|
||||
|
||||
export default preview;
|
||||
|
||||
@@ -1,3 +1,30 @@
|
||||
Changes in [1.12.1](https://github.com/element-hq/element-web/releases/tag/v1.12.1) (2025-10-07)
|
||||
================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* New Room List: Change the order of filters to match those on mobile ([#30905](https://github.com/element-hq/element-web/pull/30905)). Contributed by @langleyd.
|
||||
* New Room List: Don't clear filters on space change ([#30903](https://github.com/element-hq/element-web/pull/30903)). Contributed by @langleyd.
|
||||
* Add release announcement for the sounds ([#30900](https://github.com/element-hq/element-web/pull/30900)). Contributed by @langleyd.
|
||||
* Rich Text Editor: Add emoji suggestion support ([#30873](https://github.com/element-hq/element-web/pull/30873)). Contributed by @langleyd.
|
||||
* feat: Disable session lock when running in element-desktop ([#30643](https://github.com/element-hq/element-web/pull/30643)). Contributed by @kaylendog.
|
||||
* Improve invite dialog ui - Part 1 ([#30764](https://github.com/element-hq/element-web/pull/30764)). Contributed by @florianduros.
|
||||
* Update Message Sound for Element ([#30804](https://github.com/element-hq/element-web/pull/30804)). Contributed by @beatdemon.
|
||||
* Add new and improved ringtone ([#30761](https://github.com/element-hq/element-web/pull/30761)). Contributed by @Half-Shot.
|
||||
* Disable RTE formatting buttons when the content contains a slash command ([#30802](https://github.com/element-hq/element-web/pull/30802)). Contributed by @langleyd.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* New Room List: Improve robustness of keyboard navigation ([#30888](https://github.com/element-hq/element-web/pull/30888)). Contributed by @langleyd.
|
||||
* Fix a11y issue on list in invite dialog ([#30878](https://github.com/element-hq/element-web/pull/30878)). Contributed by @florianduros.
|
||||
* Switch Export and Import Icons to match intuition ([#30805](https://github.com/element-hq/element-web/pull/30805)). Contributed by @micartey.
|
||||
* Hide breadcrumb option when new room list is enabled ([#30869](https://github.com/element-hq/element-web/pull/30869)). Contributed by @florianduros.
|
||||
* Avoid creating multiple call objects for the same widget ([#30839](https://github.com/element-hq/element-web/pull/30839)). Contributed by @robintown.
|
||||
* Add a test for #29882, which is fixed by matrix-org/matrix-js-sdk#5016 ([#30835](https://github.com/element-hq/element-web/pull/30835)). Contributed by @andybalaam.
|
||||
* fix: use `help_encryption_url` of config instead of hardcoded `https://element.io/help#encryption5` ([#30746](https://github.com/element-hq/element-web/pull/30746)). Contributed by @florianduros.
|
||||
* Fix html export when feature\_jump\_to\_date is enabled ([#30828](https://github.com/element-hq/element-web/pull/30828)). Contributed by @langleyd.
|
||||
* Fix #30439: "Forgot recovery key" should go to "reset" ([#30771](https://github.com/element-hq/element-web/pull/30771)). Contributed by @andybalaam.
|
||||
|
||||
|
||||
Changes in [1.12.0](https://github.com/element-hq/element-web/releases/tag/v1.12.0) (2025-09-23)
|
||||
================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# syntax=docker.io/docker/dockerfile:1.17-labs@sha256:9187104f31e3a002a8a6a3209ea1f937fb7486c093cbbde1e14b0fa0d7e4f1b5
|
||||
# syntax=docker.io/docker/dockerfile:1.18-labs@sha256:79cdc14e1c220efb546ad14a8ebc816e3277cd72d27195ced5bebdd226dd1025
|
||||
|
||||
# Builder
|
||||
FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:f7f28d1962d93cc096ea6327378d990284757fec281ce48e42436e7b4b167fa2 AS builder
|
||||
FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:f8c398a3ad2612293e8827915c056ed0f5cc708b0f676274bb6c732e3c10f93d 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:0d019e980f83728002de7a6d8819d0d4af7179046d3946b8b37749953fbb28e6
|
||||
FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:14b127ed799301a21a1798516443c675237120c76b9a738d43c5e4747de4b1c9
|
||||
|
||||
# Need root user to install packages & manipulate the usr directory
|
||||
USER root
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# MVVM
|
||||
|
||||
_Deprecated_, see [MVVM.md](./MVVM.md) for the current version.
|
||||
|
||||
General description of the pattern can be found [here](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel). But the gist of it is that you divide your code into three sections:
|
||||
|
||||
1. Model: This is where the business logic and data resides.
|
||||
2. View Model: This code exists to provide the logic necessary for the UI. It directly uses the Model code.
|
||||
3. View: This is the UI code itself and depends on the view model.
|
||||
|
||||
If you do MVVM right, your view should be dumb i.e it gets data from the view model and merely displays it.
|
||||
|
||||
### Practical guidelines for MVVM in element-web
|
||||
|
||||
#### Model
|
||||
|
||||
This is anywhere your data or business logic comes from. If your view model is accessing something simple exposed from `matrix-js-sdk`, then the sdk is your model. If you're using something more high level in element-web to get your data/logic (eg: `MemberListStore`), then that becomes your model.
|
||||
|
||||
#### View Model
|
||||
|
||||
1. View model is always a custom react hook named like `useFooViewModel()`.
|
||||
2. The return type of your view model (known as view state) must be defined as a typescript interface:
|
||||
```ts
|
||||
inteface FooViewState {
|
||||
somethingUseful: string;
|
||||
somethingElse: BarType;
|
||||
update: () => Promise<void>
|
||||
...
|
||||
}
|
||||
```
|
||||
3. Any react state that your UI needs must be in the view model.
|
||||
|
||||
#### View
|
||||
|
||||
1. Views are simple react components (eg: `FooView`).
|
||||
2. Views usually start by calling the view model hook, eg:
|
||||
```tsx
|
||||
const FooView: React.FC<IProps> = (props: IProps) => {
|
||||
const vm = useFooViewModel();
|
||||
....
|
||||
return(
|
||||
<div>
|
||||
{vm.somethingUseful}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
3. Views are also allowed to accept the view model as a prop, eg:
|
||||
```tsx
|
||||
const FooView: React.FC<IProps> = ({ vm }: IProps) => {
|
||||
....
|
||||
return(
|
||||
<div>
|
||||
{vm.somethingUseful}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
4. Multiple views can share the same view model if necessary.
|
||||
|
||||
### Benefits
|
||||
|
||||
1. MVVM forces a separation of concern i.e we will no longer have large react components that have a lot of state and rendering code mixed together. This improves code readability and makes it easier to introduce changes.
|
||||
2. Introduces the possibility of code reuse. You can reuse an old view model with a new view or vice versa.
|
||||
3. Adding to the point above, in future you could import element-web view models to your project and supply your own views thus creating something similar to the [hydrogen sdk](https://github.com/element-hq/hydrogen-web/blob/master/doc/SDK.md).
|
||||
|
||||
### Example
|
||||
|
||||
We started experimenting with MVVM in the redesigned memberlist, you can see the code [here](https://github.com/vector-im/element-web/blob/develop/src/components/views/rooms/MemberList/MemberListView.tsx).
|
||||
@@ -10,58 +10,80 @@ If you do MVVM right, your view should be dumb i.e it gets data from the view mo
|
||||
|
||||
### Practical guidelines for MVVM in element-web
|
||||
|
||||
A first documentation and implementation of MVVM was done in [MVVM-v1.md](MVVM-v1.md). This v1 version is now deprecated and this document describes the current implementation.
|
||||
|
||||
#### Model
|
||||
|
||||
This is anywhere your data or business logic comes from. If your view model is accessing something simple exposed from `matrix-js-sdk`, then the sdk is your model. If you're using something more high level in element-web to get your data/logic (eg: `MemberListStore`), then that becomes your model.
|
||||
|
||||
#### View Model
|
||||
|
||||
1. View model is always a custom react hook named like `useFooViewModel()`.
|
||||
2. The return type of your view model (known as view state) must be defined as a typescript interface:
|
||||
```ts
|
||||
inteface FooViewState {
|
||||
somethingUseful: string;
|
||||
somethingElse: BarType;
|
||||
update: () => Promise<void>
|
||||
...
|
||||
}
|
||||
```
|
||||
3. Any react state that your UI needs must be in the view model.
|
||||
|
||||
#### View
|
||||
|
||||
1. Views are simple react components (eg: `FooView`).
|
||||
2. Views usually start by calling the view model hook, eg:
|
||||
1. Located in [`shared-components`](https://github.com/element-hq/element-web/tree/develop/src/shared-components). Develop it in storybook!
|
||||
2. Views are simple react components (eg: `FooView`).
|
||||
3. Views use [useSyncExternalStore](https://react.dev/reference/react/useSyncExternalStore) internally where the view model is the external store.
|
||||
4. Views should define the interface of the view model they expect:
|
||||
|
||||
```tsx
|
||||
const FooView: React.FC<IProps> = (props: IProps) => {
|
||||
const vm = useFooViewModel();
|
||||
....
|
||||
return(
|
||||
<div>
|
||||
{vm.somethingUseful}
|
||||
</div>
|
||||
);
|
||||
// Snapshot is the return type of your view model
|
||||
interface FooViewSnapshot {
|
||||
value: string;
|
||||
}
|
||||
|
||||
// To call function on the view model
|
||||
interface FooViewActions {
|
||||
doSomething: () => void;
|
||||
}
|
||||
|
||||
// ViewModel is a type defining the methods needed for `useSyncExternalStore`
|
||||
// https://github.com/element-hq/element-web/blob/develop/src/shared-components/ViewModel.ts
|
||||
type FooViewModel = ViewModel<FooViewSnapshot> & FooViewActions;
|
||||
|
||||
interface FooViewProps {
|
||||
vm: FooViewModel;
|
||||
}
|
||||
|
||||
function FooView({ vm }: FooViewProps) {
|
||||
// useViewModel is a helper function that uses useSyncExternalStore under the hood
|
||||
const { value } = useViewModel(vm);
|
||||
return (
|
||||
<button type="button" onClick={() => vm.doSomething()}>
|
||||
{value}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
3. Views are also allowed to accept the view model as a prop, eg:
|
||||
```tsx
|
||||
const FooView: React.FC<IProps> = ({ vm }: IProps) => {
|
||||
....
|
||||
return(
|
||||
<div>
|
||||
{vm.somethingUseful}
|
||||
</div>
|
||||
);
|
||||
|
||||
5. Multiple views can share the same view model if necessary.
|
||||
6. A full example is available [here](https://github.com/element-hq/element-web/blob/develop/src/shared-components/audio/AudioPlayerView/AudioPlayerView.tsx)
|
||||
|
||||
#### View Model
|
||||
|
||||
1. A View model is a class extending [`BaseViewModel`](https://github.com/element-hq/element-web/blob/develop/src/viewmodels/base/BaseViewModel.ts).
|
||||
2. Implements the interface defined in the view (e.g `FooViewModel` in the example above).
|
||||
3. View models define a snapshot type that defines the data the view will consume. The snapshot is immutable and can only be changed by calling `this.snapshot.set(...)` in the view model. This will trigger a re-render in the view.
|
||||
|
||||
```ts
|
||||
interface Props {
|
||||
propsValue: string;
|
||||
}
|
||||
|
||||
class FooViewModel extends BaseViewModel<FooViewSnapshot, Props> implements FooViewModel {
|
||||
constructor(props: Props) {
|
||||
// Call super with initial snapshot
|
||||
super(props, { value: "initial" });
|
||||
}
|
||||
|
||||
public doSomething() {
|
||||
// Call this.snapshot.set to update the snapshot
|
||||
this.snapshot.set({ value: "changed" });
|
||||
}
|
||||
}
|
||||
```
|
||||
4. Multiple views can share the same view model if necessary.
|
||||
|
||||
4. A full example is available [here](https://github.com/element-hq/element-web/blob/develop/src/viewmodels/audio/AudioPlayerViewModel.ts)
|
||||
|
||||
### Benefits
|
||||
|
||||
1. MVVM forces a separation of concern i.e we will no longer have large react components that have a lot of state and rendering code mixed together. This improves code readability and makes it easier to introduce changes.
|
||||
2. Introduces the possibility of code reuse. You can reuse an old view model with a new view or vice versa.
|
||||
3. Adding to the point above, in future you could import element-web view models to your project and supply your own views thus creating something similar to the [hydrogen sdk](https://github.com/element-hq/hydrogen-web/blob/master/doc/SDK.md).
|
||||
|
||||
### Example
|
||||
|
||||
We started experimenting with MVVM in the redesigned memberlist, you can see the code [here](https://github.com/vector-im/element-web/blob/develop/src/components/views/rooms/MemberList/MemberListView.tsx).
|
||||
|
||||
@@ -41,7 +41,7 @@ const config: Config = {
|
||||
"recorderWorkletFactory": "<rootDir>/__mocks__/empty.js",
|
||||
"^fetch-mock$": "<rootDir>/node_modules/fetch-mock",
|
||||
},
|
||||
transformIgnorePatterns: ["/node_modules/(?!(mime|matrix-js-sdk)).+$"],
|
||||
transformIgnorePatterns: ["/node_modules/(?!(mime|matrix-js-sdk|uuid|p-retry|is-network-error)).+$"],
|
||||
collectCoverageFrom: [
|
||||
"<rootDir>/src/**/*.{js,ts,tsx}",
|
||||
// getSessionLock is piped into a different JS context via stringification, and the coverage functionality is
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "element-web",
|
||||
"version": "1.12.0",
|
||||
"version": "1.12.1",
|
||||
"description": "Element: the future of secure communication",
|
||||
"author": "New Vector Ltd.",
|
||||
"repository": {
|
||||
@@ -75,11 +75,11 @@
|
||||
"resolutions": {
|
||||
"**/pretty-format/react-is": "19.1.1",
|
||||
"@playwright/test": "1.54.2",
|
||||
"@types/react": "19.1.12",
|
||||
"@types/react": "19.1.13",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"oidc-client-ts": "3.3.0",
|
||||
"jwt-decode": "4.0.0",
|
||||
"caniuse-lite": "1.0.30001724",
|
||||
"caniuse-lite": "1.0.30001741",
|
||||
"testcontainers": "^11.0.0",
|
||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
|
||||
"wrap-ansi": "npm:wrap-ansi@^7.0.0"
|
||||
@@ -98,7 +98,7 @@
|
||||
"@types/png-chunks-extract": "^1.0.2",
|
||||
"@vector-im/compound-design-tokens": "^6.0.0",
|
||||
"@vector-im/compound-web": "^8.1.2",
|
||||
"@vector-im/matrix-wysiwyg": "2.39.0",
|
||||
"@vector-im/matrix-wysiwyg": "2.40.0",
|
||||
"@zxcvbn-ts/core": "^3.0.4",
|
||||
"@zxcvbn-ts/language-common": "^3.0.4",
|
||||
"@zxcvbn-ts/language-en": "^3.0.2",
|
||||
@@ -134,7 +134,7 @@
|
||||
"maplibre-gl": "^5.0.0",
|
||||
"matrix-encrypt-attachment": "^1.0.3",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-js-sdk": "38.3.0",
|
||||
"matrix-js-sdk": "38.4.0",
|
||||
"matrix-widget-api": "^1.10.0",
|
||||
"memoize-one": "^6.0.0",
|
||||
"mime": "^4.0.4",
|
||||
@@ -142,7 +142,7 @@
|
||||
"opus-recorder": "^8.0.3",
|
||||
"pako": "^2.0.3",
|
||||
"png-chunks-extract": "^1.0.0",
|
||||
"posthog-js": "1.261.0",
|
||||
"posthog-js": "1.265.1",
|
||||
"qrcode": "1.5.4",
|
||||
"re-resizable": "6.11.2",
|
||||
"react": "^19.0.0",
|
||||
@@ -159,7 +159,7 @@
|
||||
"tar-js": "^0.3.0",
|
||||
"temporal-polyfill": "^0.3.0",
|
||||
"ua-parser-js": "1.0.40",
|
||||
"uuid": "^11.0.0",
|
||||
"uuid": "^13.0.0",
|
||||
"what-input": "^5.2.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -184,7 +184,7 @@
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@casualbot/jest-sonar-reporter": "2.2.7",
|
||||
"@element-hq/element-call-embedded": "0.15.0",
|
||||
"@element-hq/element-call-embedded": "0.16.0",
|
||||
"@element-hq/element-web-playwright-common": "^1.4.6",
|
||||
"@peculiar/webcrypto": "^1.4.3",
|
||||
"@playwright/test": "^1.50.1",
|
||||
@@ -223,7 +223,7 @@
|
||||
"@types/node-fetch": "^2.6.2",
|
||||
"@types/pako": "^2.0.0",
|
||||
"@types/qrcode": "^1.3.5",
|
||||
"@types/react": "19.1.12",
|
||||
"@types/react": "19.1.13",
|
||||
"@types/react-beautiful-dnd": "^13.0.0",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"@types/react-transition-group": "^4.4.0",
|
||||
@@ -232,7 +232,6 @@
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/tar-js": "^0.3.5",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.19.0",
|
||||
"@typescript-eslint/parser": "^8.19.0",
|
||||
"babel-jest": "^29.0.0",
|
||||
|
||||
@@ -24,7 +24,7 @@ const startDMWithBob = async (page: Page, bob: Bot) => {
|
||||
await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
|
||||
await page.getByRole("menuitem", { name: "Start chat" }).click();
|
||||
await page.getByTestId("invite-dialog-input").fill(bob.credentials.userId);
|
||||
await page.locator(".mx_InviteDialog_tile_nameStack_name").getByText("Bob").click();
|
||||
await page.getByRole("option", { name: bob.credentials.displayName }).click();
|
||||
await expect(
|
||||
page.locator(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name").getByText("Bob"),
|
||||
).toBeVisible();
|
||||
|
||||
@@ -146,8 +146,8 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
);
|
||||
|
||||
// Confirm that the bot user scanned successfully
|
||||
await expect(infoDialog.getByText("Almost there! Is your other device showing the same shield?")).toBeVisible();
|
||||
await infoDialog.getByRole("button", { name: "Yes" }).click();
|
||||
await expect(infoDialog.getByText("Confirm that you see a green shield on your other device")).toBeVisible();
|
||||
await infoDialog.getByRole("button", { name: "Yes, I see a green shield" }).click();
|
||||
await infoDialog.getByRole("button", { name: "Got it" }).click();
|
||||
|
||||
// wait for the bot to see we have finished
|
||||
@@ -201,6 +201,30 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
await enterRecoveryKeyAndCheckVerified(page, app, recoveryKey);
|
||||
});
|
||||
|
||||
test("After cancelling verify with another device, I can try again #29882", async ({ page, app, credentials }) => {
|
||||
// Regression test for https://github.com/element-hq/element-web/issues/29882
|
||||
|
||||
// Log in without verifying
|
||||
await logIntoElement(page, credentials);
|
||||
const authPage = page.locator(".mx_AuthPage");
|
||||
await authPage.getByRole("button", { name: "Skip verification for now" }).click();
|
||||
await authPage.getByRole("button", { name: "I'll verify later" }).click();
|
||||
await page.waitForSelector(".mx_MatrixChat");
|
||||
|
||||
// Start to verify with "Use another device" but cancel
|
||||
const settings = await app.settings.openUserSettings("Encryption");
|
||||
await settings.getByRole("button", { name: "Verify this device" }).click();
|
||||
await page.getByRole("button", { name: "Use another device" }).click();
|
||||
await page.locator("#mx_Dialog_Container").getByRole("button", { name: "Close dialog" }).click();
|
||||
|
||||
// Start again
|
||||
await settings.getByRole("button", { name: "Verify this device" }).click();
|
||||
|
||||
// We should be offered to use another device again.
|
||||
// (In the bug, we were immediately told that verification has been cancelled.)
|
||||
await expect(page.getByRole("button", { name: "Use another device" })).toBeVisible();
|
||||
});
|
||||
|
||||
/** Helper for the three tests above which verify by recovery key */
|
||||
async function enterRecoveryKeyAndCheckVerified(page: Page, app: ElementAppPage, recoveryKey: string) {
|
||||
await page.getByRole("button", { name: "Use recovery key" }).click();
|
||||
@@ -246,7 +270,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
// it should contain the device ID of the requesting device
|
||||
await expect(toast.getByText(`${aliceBotClient.credentials.deviceId} from `)).toBeVisible();
|
||||
// Accept
|
||||
await toast.getByRole("button", { name: "Verify Session" }).click();
|
||||
await toast.getByRole("button", { name: "Start verification" }).click();
|
||||
|
||||
/* Click 'Start' to start SAS verification */
|
||||
await page.getByRole("button", { name: "Start" }).click();
|
||||
@@ -261,10 +285,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
/* And we're all done! */
|
||||
const infoDialog = page.locator(".mx_InfoDialog");
|
||||
await infoDialog.getByRole("button", { name: "They match" }).click();
|
||||
// We don't assert the full string as the device name is unset on Synapse but set to the user ID on Dendrite
|
||||
await expect(infoDialog.getByText(`You've successfully verified`)).toContainText(
|
||||
`(${aliceBotClient.credentials.deviceId})`,
|
||||
);
|
||||
await expect(infoDialog.getByText("Device verified")).toBeVisible();
|
||||
await infoDialog.getByRole("button", { name: "Got it" }).click();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -50,11 +50,9 @@ test.describe("Invite dialog", function () {
|
||||
await expect(other.locator(".mx_InviteDialog_identityServer")).toBeVisible();
|
||||
|
||||
// Assert that the bot id is rendered properly
|
||||
await expect(
|
||||
other.locator(".mx_InviteDialog_tile_nameStack_userId").getByText(bot.credentials.userId),
|
||||
).toBeVisible();
|
||||
await expect(other.getByRole("option", { name: botName }).getByText(bot.credentials.userId)).toBeVisible();
|
||||
|
||||
await other.locator(".mx_InviteDialog_tile_nameStack_name").getByText(botName).click();
|
||||
await other.getByRole("option", { name: botName }).click();
|
||||
|
||||
await expect(
|
||||
other.locator(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name").getByText(botName),
|
||||
@@ -94,10 +92,8 @@ test.describe("Invite dialog", function () {
|
||||
|
||||
await other.getByTestId("invite-dialog-input").fill(bot.credentials.userId);
|
||||
|
||||
await expect(
|
||||
other.locator(".mx_InviteDialog_tile_nameStack").getByText(bot.credentials.userId),
|
||||
).toBeVisible();
|
||||
await other.locator(".mx_InviteDialog_tile_nameStack").getByText(botName).click();
|
||||
await expect(other.getByRole("option", { name: botName }).getByText(bot.credentials.userId)).toBeVisible();
|
||||
await other.getByRole("option", { name: botName }).click();
|
||||
|
||||
await expect(
|
||||
other.locator(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name").getByText(botName),
|
||||
|
||||
@@ -322,6 +322,13 @@ test.describe("Room list filters and sort", () => {
|
||||
return page.getByTestId("empty-room-list");
|
||||
}
|
||||
|
||||
test("should render the primary filters", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const primaryFilters = getPrimaryFilters(page);
|
||||
await expect(primaryFilters).toMatchScreenshot("collapsed-primary-filters.png");
|
||||
await getFilterExpandButton(page).click();
|
||||
await expect(primaryFilters).toMatchScreenshot("expanded-primary-filters.png");
|
||||
});
|
||||
|
||||
test(
|
||||
"should render the default placeholder when there is no filter",
|
||||
{ tag: "@screenshot" },
|
||||
|
||||
@@ -252,6 +252,26 @@ test.describe("Room list", () => {
|
||||
// Focus should be back on the notification button
|
||||
await expect(notificationButton).toBeFocused();
|
||||
});
|
||||
|
||||
test("should navigate to the top and then bottom of the room list", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
const topRoom = roomListView.getByRole("option", { name: "Open room room29" });
|
||||
|
||||
// open the room
|
||||
await topRoom.click();
|
||||
// put focus back on the room list item
|
||||
await topRoom.click();
|
||||
await expect(topRoom).toBeFocused();
|
||||
|
||||
await page.keyboard.press("End");
|
||||
const bottomRoom = roomListView.getByRole("option", { name: "Open room room0" });
|
||||
await expect(bottomRoom).toBeFocused();
|
||||
|
||||
await page.keyboard.press("Home");
|
||||
const topRoomAgain = roomListView.getByRole("option", { name: "Open room room29" });
|
||||
await expect(topRoomAgain).toBeFocused();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -228,7 +228,7 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
*/
|
||||
async function verifyUsingOtherDevice(deviceToVerifyPage: Page, alreadyVerifiedDevicePage: Page) {
|
||||
await deviceToVerifyPage.getByRole("button", { name: "Use another device" }).click();
|
||||
await alreadyVerifiedDevicePage.getByRole("button", { name: "Verify session" }).click();
|
||||
await alreadyVerifiedDevicePage.getByRole("button", { name: "Start verification" }).click();
|
||||
await alreadyVerifiedDevicePage.getByRole("button", { name: "Start" }).click();
|
||||
await alreadyVerifiedDevicePage.getByRole("button", { name: "They match" }).click();
|
||||
await deviceToVerifyPage.getByRole("button", { name: "They match" }).click();
|
||||
|
||||
@@ -32,20 +32,26 @@ test.describe("Release announcement", () => {
|
||||
// dismiss the toast so the announcement appears
|
||||
await page.getByRole("button", { name: "Dismiss" }).click();
|
||||
|
||||
const name = "Chats has a new look!";
|
||||
const newSoundsName = "We’ve refreshed your sounds";
|
||||
// The new sounds release announcement should be displayed
|
||||
await util.assertReleaseAnnouncementIsVisible(newSoundsName);
|
||||
// Hide the new sounds release announcement
|
||||
const newSoundsDialog = util.getReleaseAnnouncement(newSoundsName);
|
||||
await newSoundsDialog.getByRole("button", { name: "OK" }).click();
|
||||
|
||||
// The release announcement should be displayed
|
||||
await util.assertReleaseAnnouncementIsVisible(name);
|
||||
// Hide the release announcement
|
||||
const dialog = util.getReleaseAnnouncement(name);
|
||||
const newRoomListName = "Chats has a new look!";
|
||||
// The new room list release announcement should be displayed
|
||||
await util.assertReleaseAnnouncementIsVisible(newRoomListName);
|
||||
// Hide the new room list release announcement
|
||||
const dialog = util.getReleaseAnnouncement(newRoomListName);
|
||||
await dialog.getByRole("button", { name: "Next" }).click();
|
||||
|
||||
await util.assertReleaseAnnouncementIsNotVisible(name);
|
||||
await util.assertReleaseAnnouncementIsNotVisible(newRoomListName);
|
||||
|
||||
await page.reload();
|
||||
await expect(page.getByRole("button", { name: "Room options" })).toBeVisible();
|
||||
// Check that once the release announcement has been marked as viewed, it does not appear again
|
||||
await util.assertReleaseAnnouncementIsNotVisible(name);
|
||||
// Check that once the release announcements has been marked as viewed, it does not appear again
|
||||
await util.assertReleaseAnnouncementIsNotVisible(newRoomListName);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
/*
|
||||
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 { EventType, Preset } from "matrix-js-sdk/src/matrix";
|
||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import type { Credentials } from "../../plugins/homeserver";
|
||||
import type { Bot } from "../../pages/bot";
|
||||
|
||||
function assertCommonCallParameters(
|
||||
url: URLSearchParams,
|
||||
hash: URLSearchParams,
|
||||
user: Credentials,
|
||||
room: { roomId: string },
|
||||
): void {
|
||||
expect(url.has("widgetId")).toEqual(true);
|
||||
expect(url.has("parentUrl")).toEqual(true);
|
||||
|
||||
expect(hash.get("perParticipantE2EE")).toEqual("false");
|
||||
expect(hash.get("userId")).toEqual(user.userId);
|
||||
expect(hash.get("deviceId")).toEqual(user.deviceId);
|
||||
expect(hash.get("roomId")).toEqual(room.roomId);
|
||||
expect(hash.get("preload")).toEqual("false");
|
||||
}
|
||||
|
||||
async function sendRTCState(bot: Bot, roomId: string, notification?: "ring" | "notification") {
|
||||
const resp = await bot.sendStateEvent(
|
||||
roomId,
|
||||
"org.matrix.msc3401.call.member",
|
||||
{
|
||||
application: "m.call",
|
||||
call_id: "",
|
||||
device_id: "OiDFxsZrjz",
|
||||
expires: 180000000,
|
||||
foci_preferred: [
|
||||
{
|
||||
livekit_alias: roomId,
|
||||
livekit_service_url: "https://example.org",
|
||||
type: "livekit",
|
||||
},
|
||||
],
|
||||
focus_active: {
|
||||
focus_selection: "oldest_membership",
|
||||
type: "livekit",
|
||||
},
|
||||
scope: "m.room",
|
||||
},
|
||||
`_@${bot.credentials.userId}_OiDFxsZrjz_m.call`,
|
||||
);
|
||||
if (!notification) {
|
||||
return;
|
||||
}
|
||||
await bot.sendEvent(roomId, null, "org.matrix.msc4075.rtc.notification", {
|
||||
"lifetime": 30000,
|
||||
"m.mentions": {
|
||||
room: true,
|
||||
user_ids: [],
|
||||
},
|
||||
"m.relates_to": {
|
||||
event_id: resp.event_id,
|
||||
rel_type: "org.matrix.msc4075.rtc.notification.parent",
|
||||
},
|
||||
"notification_type": notification,
|
||||
"sender_ts": 1758611895996,
|
||||
});
|
||||
}
|
||||
|
||||
test.describe("Element Call", () => {
|
||||
test.use({
|
||||
config: {
|
||||
element_call: {
|
||||
use_exclusively: false,
|
||||
},
|
||||
features: {
|
||||
feature_group_calls: true,
|
||||
},
|
||||
},
|
||||
displayName: "Alice",
|
||||
botCreateOpts: {
|
||||
autoAcceptInvites: true,
|
||||
displayName: "Bob",
|
||||
},
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page, user, app }) => {
|
||||
// Mock a widget page. It doesn't need to actually be Element Call.
|
||||
await page.route("/widget.html", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
body: "<p> Hello world </p>",
|
||||
});
|
||||
});
|
||||
await app.settings.setValue(
|
||||
"Developer.elementCallUrl",
|
||||
null,
|
||||
SettingLevel.DEVICE,
|
||||
new URL("/widget.html#", page.url()).toString(),
|
||||
);
|
||||
});
|
||||
|
||||
test.describe("Group Chat", () => {
|
||||
test.use({
|
||||
room: async ({ page, app, user, bot }, use) => {
|
||||
const roomId = await app.client.createRoom({ name: "TestRoom", invite: [bot.credentials.userId] });
|
||||
await use({ roomId });
|
||||
},
|
||||
});
|
||||
test("should be able to start a video call", async ({ page, user, room, app }) => {
|
||||
await app.viewRoomById(room.roomId);
|
||||
await expect(page.getByText("Bob joined the room")).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "Video call" }).click();
|
||||
await page.getByRole("menuitem", { name: "Element Call" }).click();
|
||||
|
||||
const frameUrlStr = await page.locator("iframe").getAttribute("src");
|
||||
await expect(frameUrlStr).toBeDefined();
|
||||
// Ensure we set the correct parameters for ECall.
|
||||
const url = new URL(frameUrlStr);
|
||||
const hash = new URLSearchParams(url.hash.slice(1));
|
||||
assertCommonCallParameters(url.searchParams, hash, user, room);
|
||||
expect(hash.get("intent")).toEqual("start_call");
|
||||
expect(hash.get("skipLobby")).toEqual(null);
|
||||
});
|
||||
|
||||
test("should be able to skip lobby by holding down shift", async ({ page, user, bot, room, app }) => {
|
||||
await app.viewRoomById(room.roomId);
|
||||
await expect(page.getByText("Bob joined the room")).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "Video call" }).click();
|
||||
await page.keyboard.down("Shift");
|
||||
await page.getByRole("menuitem", { name: "Element Call" }).click();
|
||||
await page.keyboard.up("Shift");
|
||||
|
||||
const frameUrlStr = await page.locator("iframe").getAttribute("src");
|
||||
await expect(frameUrlStr).toBeDefined();
|
||||
const url = new URL(frameUrlStr);
|
||||
const hash = new URLSearchParams(url.hash.slice(1));
|
||||
assertCommonCallParameters(url.searchParams, hash, user, room);
|
||||
expect(hash.get("intent")).toEqual("start_call");
|
||||
expect(hash.get("skipLobby")).toEqual("true");
|
||||
});
|
||||
|
||||
test("should be able to join a call in progress", async ({ page, user, bot, room, app }) => {
|
||||
await app.viewRoomById(room.roomId);
|
||||
// Allow bob to create a call
|
||||
await app.client.setPowerLevel(room.roomId, bot.credentials.userId, 50);
|
||||
await expect(page.getByText("Bob joined the room")).toBeVisible();
|
||||
// Fake a start of a call
|
||||
await sendRTCState(bot, room.roomId);
|
||||
const button = page.getByTestId("join-call-button");
|
||||
await expect(button).toBeInViewport({ timeout: 5000 });
|
||||
// And test joining
|
||||
await button.click();
|
||||
const frameUrlStr = await page.locator("iframe").getAttribute("src");
|
||||
console.log(frameUrlStr);
|
||||
await expect(frameUrlStr).toBeDefined();
|
||||
const url = new URL(frameUrlStr);
|
||||
const hash = new URLSearchParams(url.hash.slice(1));
|
||||
assertCommonCallParameters(url.searchParams, hash, user, room);
|
||||
|
||||
expect(hash.get("intent")).toEqual("join_existing");
|
||||
expect(hash.get("skipLobby")).toEqual(null);
|
||||
});
|
||||
|
||||
[true, false].forEach((skipLobbyToggle) => {
|
||||
test(
|
||||
`should be able to join a call via incoming call toast (skipLobby=${skipLobbyToggle})`,
|
||||
{ tag: ["@screenshot"] },
|
||||
async ({ page, user, bot, room, app }) => {
|
||||
await app.viewRoomById(room.roomId);
|
||||
// Allow bob to create a call
|
||||
await app.client.setPowerLevel(room.roomId, bot.credentials.userId, 50);
|
||||
await expect(page.getByText("Bob joined the room")).toBeVisible();
|
||||
// Fake a start of a call
|
||||
await sendRTCState(bot, room.roomId, "notification");
|
||||
const toast = page.locator(".mx_Toast_toast");
|
||||
const button = toast.getByRole("button", { name: "Join" });
|
||||
if (skipLobbyToggle) {
|
||||
await toast.getByRole("switch").check();
|
||||
await expect(toast).toMatchScreenshot("incoming-call-group-video-toast-checked.png");
|
||||
} else {
|
||||
await toast.getByRole("switch").uncheck();
|
||||
await expect(toast).toMatchScreenshot("incoming-call-group-video-toast-unchecked.png");
|
||||
}
|
||||
|
||||
// And test joining
|
||||
await button.click();
|
||||
const frameUrlStr = await page.locator("iframe").getAttribute("src");
|
||||
console.log(frameUrlStr);
|
||||
await expect(frameUrlStr).toBeDefined();
|
||||
const url = new URL(frameUrlStr);
|
||||
const hash = new URLSearchParams(url.hash.slice(1));
|
||||
assertCommonCallParameters(url.searchParams, hash, user, room);
|
||||
|
||||
expect(hash.get("intent")).toEqual("join_existing");
|
||||
expect(hash.get("skipLobby")).toEqual(skipLobbyToggle.toString());
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("DMs", () => {
|
||||
test.use({
|
||||
room: async ({ page, app, user, bot }, use) => {
|
||||
const roomId = await app.client.createRoom({
|
||||
preset: "trusted_private_chat" as Preset.TrustedPrivateChat,
|
||||
invite: [bot.credentials.userId],
|
||||
});
|
||||
await app.client.setAccountData("m.direct" as EventType.Direct, {
|
||||
[bot.credentials.userId]: [roomId],
|
||||
});
|
||||
await use({ roomId });
|
||||
},
|
||||
});
|
||||
|
||||
test("should be able to start a video call", async ({ page, user, room, app }) => {
|
||||
await app.viewRoomById(room.roomId);
|
||||
await expect(page.getByText("Bob joined the room")).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "Video call" }).click();
|
||||
await page.getByRole("menuitem", { name: "Element Call" }).click();
|
||||
const frameUrlStr = await page.locator("iframe").getAttribute("src");
|
||||
|
||||
await expect(frameUrlStr).toBeDefined();
|
||||
const url = new URL(frameUrlStr);
|
||||
const hash = new URLSearchParams(url.hash.slice(1));
|
||||
assertCommonCallParameters(url.searchParams, hash, user, room);
|
||||
expect(hash.get("intent")).toEqual("start_call_dm");
|
||||
expect(hash.get("skipLobby")).toEqual(null);
|
||||
});
|
||||
|
||||
test("should be able to skip lobby by holding down shift", async ({ page, user, room, app }) => {
|
||||
await app.viewRoomById(room.roomId);
|
||||
await expect(page.getByText("Bob joined the room")).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "Video call" }).click();
|
||||
await page.keyboard.down("Shift");
|
||||
await page.getByRole("menuitem", { name: "Element Call" }).click();
|
||||
await page.keyboard.up("Shift");
|
||||
const frameUrlStr = await page.locator("iframe").getAttribute("src");
|
||||
|
||||
await expect(frameUrlStr).toBeDefined();
|
||||
const url = new URL(frameUrlStr);
|
||||
const hash = new URLSearchParams(url.hash.slice(1));
|
||||
assertCommonCallParameters(url.searchParams, hash, user, room);
|
||||
expect(hash.get("intent")).toEqual("start_call_dm");
|
||||
expect(hash.get("skipLobby")).toEqual("true");
|
||||
});
|
||||
|
||||
test("should be able to join a call in progress", async ({ page, user, bot, room, app }) => {
|
||||
await app.viewRoomById(room.roomId);
|
||||
// Allow bob to create a call
|
||||
await expect(page.getByText("Bob joined the room")).toBeVisible();
|
||||
// Fake a start of a call
|
||||
await sendRTCState(bot, room.roomId);
|
||||
const button = page.getByTestId("join-call-button");
|
||||
await expect(button).toBeInViewport({ timeout: 5000 });
|
||||
// And test joining
|
||||
await button.click();
|
||||
const frameUrlStr = await page.locator("iframe").getAttribute("src");
|
||||
console.log(frameUrlStr);
|
||||
await expect(frameUrlStr).toBeDefined();
|
||||
const url = new URL(frameUrlStr);
|
||||
const hash = new URLSearchParams(url.hash.slice(1));
|
||||
assertCommonCallParameters(url.searchParams, hash, user, room);
|
||||
|
||||
expect(hash.get("intent")).toEqual("join_existing_dm");
|
||||
expect(hash.get("skipLobby")).toEqual(null);
|
||||
});
|
||||
|
||||
[true, false].forEach((skipLobbyToggle) => {
|
||||
test(
|
||||
`should be able to join a call via incoming call toast (skipLobby=${skipLobbyToggle})`,
|
||||
{ tag: ["@screenshot"] },
|
||||
async ({ page, user, bot, room, app }) => {
|
||||
await app.viewRoomById(room.roomId);
|
||||
// Allow bob to create a call
|
||||
await expect(page.getByText("Bob joined the room")).toBeVisible();
|
||||
// Fake a start of a call
|
||||
await sendRTCState(bot, room.roomId, "ring");
|
||||
const toast = page.locator(".mx_Toast_toast");
|
||||
const button = toast.getByRole("button", { name: "Join" });
|
||||
if (skipLobbyToggle) {
|
||||
await toast.getByRole("switch").check();
|
||||
await expect(toast).toMatchScreenshot("incoming-call-dm-video-toast-checked.png");
|
||||
} else {
|
||||
await toast.getByRole("switch").uncheck();
|
||||
await expect(toast).toMatchScreenshot("incoming-call-dm-video-toast-unchecked.png");
|
||||
}
|
||||
|
||||
// And test joining
|
||||
await button.click();
|
||||
const frameUrlStr = await page.locator("iframe").getAttribute("src");
|
||||
console.log(frameUrlStr);
|
||||
await expect(frameUrlStr).toBeDefined();
|
||||
const url = new URL(frameUrlStr);
|
||||
const hash = new URLSearchParams(url.hash.slice(1));
|
||||
assertCommonCallParameters(url.searchParams, hash, user, room);
|
||||
|
||||
expect(hash.get("intent")).toEqual("join_existing_dm");
|
||||
expect(hash.get("skipLobby")).toEqual(skipLobbyToggle.toString());
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Video Rooms", () => {
|
||||
test.use({
|
||||
config: {
|
||||
features: {
|
||||
feature_video_rooms: true,
|
||||
feature_element_call_video_rooms: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
test("should be able to create and join a video room", async ({ page, user }) => {
|
||||
await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
|
||||
await page.getByRole("menuitem", { name: "New video room" }).click();
|
||||
await page.getByRole("textbox", { name: "Name" }).fill("Test room");
|
||||
await page.getByRole("button", { name: "Create video room" }).click();
|
||||
await expect(page).toHaveURL(new RegExp(`/#/room/`));
|
||||
const roomId = new URL(page.url()).hash.slice("#/room/".length);
|
||||
|
||||
const frameUrlStr = await page.locator("iframe").getAttribute("src");
|
||||
await expect(frameUrlStr).toBeDefined();
|
||||
// Ensure we set the correct parameters for ECall.
|
||||
const url = new URL(frameUrlStr);
|
||||
const hash = new URLSearchParams(url.hash.slice(1));
|
||||
assertCommonCallParameters(url.searchParams, hash, user, { roomId });
|
||||
expect(hash.get("intent")).toEqual("join_existing");
|
||||
expect(hash.get("skipLobby")).toEqual("false");
|
||||
expect(hash.get("returnToLobby")).toEqual("true");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -469,6 +469,27 @@ export class Client {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a power level to one or multiple users.
|
||||
* Will apply changes atop of current power level event.
|
||||
* @param roomId - the room to update power levels in
|
||||
* @param userId - the ID of the user or users to update power levels of
|
||||
* @param powerLevel - the numeric power level to update given users to
|
||||
*/
|
||||
public async setPowerLevel(
|
||||
roomId: string,
|
||||
userId: string | string[],
|
||||
powerLevel: number,
|
||||
): Promise<ISendEventResponse> {
|
||||
const client = await this.prepareClient();
|
||||
return client.evaluate(
|
||||
async (client, { roomId, userId, powerLevel }) => {
|
||||
return client.setPowerLevel(roomId, userId, powerLevel);
|
||||
},
|
||||
{ roomId, userId, powerLevel },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Leaves the given room.
|
||||
* @param roomId ID of the room to leave
|
||||
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 266 KiB After Width: | Height: | Size: 260 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -10,7 +10,7 @@ import {
|
||||
type StartedPostgreSqlContainer,
|
||||
} from "@element-hq/element-web-playwright-common/lib/testcontainers";
|
||||
|
||||
const TAG = "main@sha256:a29fa92aca82fd4cdf6b84abaa14935f111f281f9bffeb30fdb8fe2353c0108c";
|
||||
const TAG = "main@sha256:09f64cd1633f1c82756b8e7d83cec4575b15782709674b0a69a4ad2a931e4e4f";
|
||||
|
||||
/**
|
||||
* MatrixAuthenticationServiceContainer which freezes the docker digest to
|
||||
|
||||
@@ -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:40ef11b61d70bda94266324d159cc7d807e64b26ad03788e386d5084abb3c198";
|
||||
const TAG = "develop@sha256:52fe74457880905aeae689034c40ec773a0dbe63b263f6c30c209b116852fc06";
|
||||
|
||||
/**
|
||||
* SynapseContainer which freezes the docker digest to stabilise tests,
|
||||
|
||||
@@ -602,6 +602,7 @@ legend {
|
||||
.mx_AccessibleButton,
|
||||
.mx_IdentityServerPicker button,
|
||||
.mx_AccessSecretStorageDialog button,
|
||||
.mx_InviteDialog_section button,
|
||||
[class|="maplibregl"]
|
||||
),
|
||||
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton, .mx_AccessibleButton),
|
||||
@@ -643,7 +644,8 @@ legend {
|
||||
.mx_ThemeChoicePanel_CustomTheme button,
|
||||
.mx_UnpinAllDialog button,
|
||||
.mx_ShareDialog button,
|
||||
.mx_EncryptionUserSettingsTab button
|
||||
.mx_EncryptionUserSettingsTab button,
|
||||
.mx_InviteDialog_section button
|
||||
):focus,
|
||||
.mx_Dialog input[type="submit"]:focus,
|
||||
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton, .mx_AccessibleButton):focus,
|
||||
|
||||
@@ -68,21 +68,6 @@ Please see LICENSE files in the repository root for full details.
|
||||
.mx_InviteDialog_section {
|
||||
padding-bottom: $spacing-4;
|
||||
|
||||
h3 {
|
||||
font-size: $font-12px;
|
||||
color: $muted-fg-color;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
> p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
> span {
|
||||
color: $primary-content;
|
||||
}
|
||||
|
||||
.mx_InviteDialog_section_showMore {
|
||||
margin: 7px 18px;
|
||||
display: block;
|
||||
@@ -194,10 +179,13 @@ Please see LICENSE files in the repository root for full details.
|
||||
.mx_InviteDialog_userSections {
|
||||
flex-grow: 1;
|
||||
padding-inline-end: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: var(--cpd-space-3x);
|
||||
gap: var(--cpd-space-3x);
|
||||
|
||||
.mx_InviteDialog_section {
|
||||
padding-bottom: 0;
|
||||
margin-top: $spacing-12;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -249,7 +237,6 @@ Please see LICENSE files in the repository root for full details.
|
||||
}
|
||||
|
||||
.mx_InviteDialog_userSections {
|
||||
margin-top: $spacing-4;
|
||||
overflow-y: auto;
|
||||
padding: 0 45px $spacing-4 0;
|
||||
}
|
||||
@@ -325,48 +312,6 @@ Please see LICENSE files in the repository root for full details.
|
||||
gap: $spacing-8 $spacing-12;
|
||||
align-items: center;
|
||||
|
||||
&.mx_InviteDialog_tile--room {
|
||||
/* mx_InviteDialog_tile_avatarStack, mx_InviteDialog_tile_nameStack, time */
|
||||
grid-template-columns: min-content auto auto;
|
||||
padding: $spacing-4 $spacing-8;
|
||||
|
||||
&:hover {
|
||||
background-color: $header-panel-bg-color;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.mx_InviteDialog_tile--room_selected {
|
||||
border-radius: 36px;
|
||||
background-color: var(--cpd-color-bg-success-subtle);
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
mask-image: url("@vector-im/compound-design-tokens/icons/check.svg");
|
||||
mask-size: 100%;
|
||||
mask-repeat: no-repeat;
|
||||
position: absolute;
|
||||
top: 6px; /* 50% */
|
||||
left: 6px; /* 50% */
|
||||
background-color: $primary-content;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_InviteDialog_tile--room_time {
|
||||
margin-inline-start: auto;
|
||||
width: max-content;
|
||||
font-size: $font-12px;
|
||||
color: $muted-fg-color;
|
||||
}
|
||||
|
||||
.mx_InviteDialog_tile--room_highlight {
|
||||
font-weight: 900;
|
||||
}
|
||||
}
|
||||
|
||||
&.mx_InviteDialog_tile--inviterError {
|
||||
grid-template-columns: max-content auto; /* max-content = avatar width */
|
||||
margin-bottom: $spacing-24;
|
||||
@@ -388,15 +333,11 @@ Please see LICENSE files in the repository root for full details.
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.mx_InviteDialog_tile_avatarStack,
|
||||
.mx_InviteDialog_tile--room_selected {
|
||||
.mx_InviteDialog_tile_avatarStack {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mx_InviteDialog_tile_avatarStack {
|
||||
grid-row-start: 1;
|
||||
grid-column-start: 1;
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Echoes a version based on the git hashes of the element-web, react-sdk & js-sdk checkouts, for the case where
|
||||
# Echoes a version based on the git hashes of the element-web & js-sdk checkouts, for the case where
|
||||
# these dependencies are git checkouts.
|
||||
|
||||
set -e
|
||||
|
||||
# Since the deps are fetched from git, we can rev-parse
|
||||
# Since the deps are fetched from git & linked, we can rev-parse
|
||||
JSSDK_SHA=$(git -C node_modules/matrix-js-sdk rev-parse --short=12 HEAD)
|
||||
VECTOR_SHA=$(git rev-parse --short=12 HEAD) # use the ACTUAL SHA rather than assume develop
|
||||
echo $VECTOR_SHA-js-$JSSDK_SHA
|
||||
echo "$VECTOR_SHA-js-$JSSDK_SHA"
|
||||
|
||||
@@ -508,4 +508,19 @@ export default abstract class BasePlatform {
|
||||
* Begin update polling, if applicable
|
||||
*/
|
||||
public startUpdater(): void {}
|
||||
|
||||
/**
|
||||
* Checks if the current session is lock-free, i.e., no other instance is holding the session lock.
|
||||
* Platforms that support session locking should override this method.
|
||||
* @returns {boolean} True if the session is lock-free, false otherwise.
|
||||
*/
|
||||
public abstract checkSessionLockFree(): boolean;
|
||||
/**
|
||||
* Attempts to acquire a session lock for this instance.
|
||||
* If another instance is detected, calls the provided callback.
|
||||
* Platforms that support session locking should override this method.
|
||||
* @param _onNewInstance Callback to invoke if a new instance is detected.
|
||||
* @returns {Promise<boolean>} True if the lock was acquired, false otherwise.
|
||||
*/
|
||||
public abstract getSessionLock(_onNewInstance: () => Promise<void>): Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -390,6 +390,9 @@ export default class LegacyCallHandler extends TypedEventEmitter<LegacyCallHandl
|
||||
|
||||
const [urlPrefix, loop] = audioInfo[audioId];
|
||||
const source = await this.backgroundAudio.pickFormatAndPlay(urlPrefix, ["mp3", "ogg"], loop);
|
||||
if (this.playingSources[audioId]) {
|
||||
logger.warn(`${logPrefix} Already playing audio ${audioId}!`);
|
||||
}
|
||||
this.playingSources[audioId] = source;
|
||||
logger.debug(`${logPrefix} playing audio successfully`);
|
||||
}
|
||||
|
||||
@@ -130,7 +130,6 @@ import { NotificationLevel } from "../../stores/notifications/NotificationLevel"
|
||||
import { type UserTab } from "../views/dialogs/UserTab";
|
||||
import { shouldSkipSetupEncryption } from "../../utils/crypto/shouldSkipSetupEncryption";
|
||||
import { Filter } from "../views/dialogs/spotlight/Filter";
|
||||
import { checkSessionLockFree, getSessionLock } from "../../utils/SessionLock";
|
||||
import { SessionLockStolenView } from "./auth/SessionLockStolenView";
|
||||
import { ConfirmSessionLockTheftView } from "./auth/ConfirmSessionLockTheftView";
|
||||
import { LoginSplashView } from "./auth/LoginSplashView";
|
||||
@@ -314,7 +313,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
private async initSession(): Promise<void> {
|
||||
// The Rust Crypto SDK will break if two Element instances try to use the same datastore at once, so
|
||||
// make sure we are the only Element instance in town (on this browser/domain).
|
||||
if (!(await getSessionLock(() => this.onSessionLockStolen()))) {
|
||||
const platform = PlatformPeg.get();
|
||||
if (platform && !(await platform.getSessionLock(() => this.onSessionLockStolen()))) {
|
||||
// we failed to get the lock. onSessionLockStolen should already have been called, so nothing left to do.
|
||||
return;
|
||||
}
|
||||
@@ -479,7 +479,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
// mounted.
|
||||
if (!this.sessionLoadStarted) {
|
||||
this.sessionLoadStarted = true;
|
||||
if (!checkSessionLockFree()) {
|
||||
const platform = PlatformPeg.get();
|
||||
if (platform && !platform.checkSessionLockFree()) {
|
||||
// another instance holds the lock; confirm its theft before proceeding
|
||||
setTimeout(() => this.setState({ view: Views.CONFIRM_LOCK_THEFT }), 0);
|
||||
} else {
|
||||
|
||||
@@ -2609,7 +2609,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
<CallView
|
||||
room={this.state.room}
|
||||
resizing={this.state.resizing}
|
||||
skipLobby={this.context.roomViewStore.skipCallLobby() ?? false}
|
||||
role="main"
|
||||
onClose={this.onCallClose}
|
||||
/>
|
||||
|
||||
@@ -133,13 +133,13 @@ export function ListView<Item, Context = any>(props: IListViewProps<Item, Contex
|
||||
}
|
||||
if (items[clampedIndex]) {
|
||||
const key = getItemKey(items[clampedIndex]);
|
||||
setTabIndexKey(key);
|
||||
isScrollingToItem.current = true;
|
||||
virtuosoHandleRef.current?.scrollIntoView({
|
||||
index: clampedIndex,
|
||||
align: align,
|
||||
behavior: "auto",
|
||||
done: () => {
|
||||
setTabIndexKey(key);
|
||||
isScrollingToItem.current = false;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -15,8 +15,6 @@ import RoomListStoreV3, {
|
||||
type RoomsResult,
|
||||
} from "../../../stores/room-list-v3/RoomListStoreV3";
|
||||
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
import SpaceStore from "../../../stores/spaces/SpaceStore";
|
||||
import { UPDATE_SELECTED_SPACE } from "../../../stores/spaces";
|
||||
|
||||
/**
|
||||
* Provides information about a primary filter.
|
||||
@@ -50,9 +48,9 @@ const filterKeyToNameMap: Map<FilterKey, TranslationKey> = new Map([
|
||||
[FilterKey.UnreadFilter, _td("room_list|filters|unread")],
|
||||
[FilterKey.PeopleFilter, _td("room_list|filters|people")],
|
||||
[FilterKey.RoomsFilter, _td("room_list|filters|rooms")],
|
||||
[FilterKey.FavouriteFilter, _td("room_list|filters|favourite")],
|
||||
[FilterKey.MentionsFilter, _td("room_list|filters|mentions")],
|
||||
[FilterKey.InvitesFilter, _td("room_list|filters|invites")],
|
||||
[FilterKey.FavouriteFilter, _td("room_list|filters|favourite")],
|
||||
[FilterKey.LowPriorityFilter, _td("room_list|filters|low_priority")],
|
||||
]);
|
||||
|
||||
@@ -74,9 +72,6 @@ export function useFilteredRooms(): FilteredRooms {
|
||||
setRoomsResult(newRooms);
|
||||
}, []);
|
||||
|
||||
// Reset filters when active space changes
|
||||
useEventEmitter(SpaceStore.instance, UPDATE_SELECTED_SPACE, () => setPrimaryFilter(undefined));
|
||||
|
||||
const filterUndefined = (array: (FilterKey | undefined)[]): FilterKey[] =>
|
||||
array.filter((f) => f !== undefined) as FilterKey[];
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import { type Beacon, BeaconEvent, LocationAssetType } from "matrix-js-sdk/src/m
|
||||
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||
import { humanizeTime } from "../../../utils/humanize";
|
||||
import { preventDefaultWrapper } from "../../../utils/NativeEventUtils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import MemberAvatar from "../avatars/MemberAvatar";
|
||||
@@ -19,6 +18,7 @@ import BeaconStatus from "./BeaconStatus";
|
||||
import { BeaconDisplayStatus } from "./displayStatus";
|
||||
import StyledLiveBeaconIcon from "./StyledLiveBeaconIcon";
|
||||
import ShareLatestLocation from "./ShareLatestLocation";
|
||||
import { humanizeTime } from "../../../shared-components/utils/humanize";
|
||||
|
||||
interface Props {
|
||||
beacon: Beacon;
|
||||
|
||||
@@ -35,7 +35,7 @@ const RoomCallBannerInner: React.FC<RoomCallBannerProps> = ({ roomId, call }) =>
|
||||
action: Action.ViewRoom,
|
||||
room_id: roomId,
|
||||
view_call: true,
|
||||
skipLobby: "shiftKey" in ev ? ev.shiftKey : false,
|
||||
skipLobby: ("shiftKey" in ev && ev.shiftKey) || undefined,
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -17,6 +17,7 @@ import { EncryptionCardButtons } from "../settings/encryption/EncryptionCardButt
|
||||
import { type OpenToTabPayload } from "../../../dispatcher/payloads/OpenToTabPayload";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { UserTab } from "./UserTab";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
|
||||
interface Props {
|
||||
onFinished: (dismissed: boolean) => void;
|
||||
@@ -60,7 +61,7 @@ export default class ConfirmKeyStorageOffDialog extends React.Component<Props> {
|
||||
a: (sub) => (
|
||||
<>
|
||||
<br />
|
||||
<a href="https://element.io/help#encryption5" target="_blank" rel="noreferrer noopener">
|
||||
<a href={SdkConfig.get("help_encryption_url")} target="_blank" rel="noreferrer noopener">
|
||||
{sub} <PopOutIcon />
|
||||
</a>
|
||||
</>
|
||||
|
||||
@@ -24,7 +24,6 @@ import { getDefaultIdentityServerUrl, setToDefaultIdentityServer } from "../../.
|
||||
import { buildActivityScores, buildMemberScores, compareMembers } from "../../../utils/SortMembers";
|
||||
import { abbreviateUrl } from "../../../utils/UrlUtils";
|
||||
import IdentityAuthClient from "../../../IdentityAuthClient";
|
||||
import { humanizeTime } from "../../../utils/humanize";
|
||||
import { type IInviteResult, inviteMultipleToRoom, showAnyInviteErrors } from "../../../RoomInvite";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { DefaultTagID } from "../../../stores/room-list/models";
|
||||
@@ -65,6 +64,8 @@ import AskInviteAnywayDialog, { type UnknownProfiles } from "./AskInviteAnywayDi
|
||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||
import { type UserProfilesStore } from "../../../stores/UserProfilesStore";
|
||||
import InviteProgressBody from "./InviteProgressBody.tsx";
|
||||
import { RichList } from "../../../shared-components/rich-list/RichList";
|
||||
import { RichItem } from "../../../shared-components/rich-list/RichItem";
|
||||
|
||||
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
||||
/* eslint-disable camelcase */
|
||||
@@ -163,7 +164,6 @@ interface IDMRoomTileProps {
|
||||
member: Member;
|
||||
lastActiveTs?: number;
|
||||
onToggle(member: Member): void;
|
||||
highlightWord: string;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
@@ -176,54 +176,8 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
||||
this.props.onToggle(this.props.member);
|
||||
};
|
||||
|
||||
private highlightName(str: string): ReactNode {
|
||||
if (!this.props.highlightWord) return str;
|
||||
|
||||
// We convert things to lowercase for index searching, but pull substrings from
|
||||
// the submitted text to preserve case. Note: we don't need to htmlEntities the
|
||||
// string because React will safely encode the text for us.
|
||||
const lowerStr = str.toLowerCase();
|
||||
const filterStr = this.props.highlightWord.toLowerCase();
|
||||
|
||||
const result: JSX.Element[] = [];
|
||||
|
||||
let i = 0;
|
||||
let ii: number;
|
||||
while ((ii = lowerStr.indexOf(filterStr, i)) >= 0) {
|
||||
// Push any text we missed (first bit/middle of text)
|
||||
if (ii > i) {
|
||||
// Push any text we aren't highlighting (middle of text match, or beginning of text)
|
||||
result.push(<span key={i + "begin"}>{str.substring(i, ii)}</span>);
|
||||
}
|
||||
|
||||
i = ii; // copy over ii only if we have a match (to preserve i for end-of-text matching)
|
||||
|
||||
// Highlight the word the user entered
|
||||
const substr = str.substring(i, filterStr.length + i);
|
||||
result.push(
|
||||
<span className="mx_InviteDialog_tile--room_highlight" key={i + "bold"}>
|
||||
{substr}
|
||||
</span>,
|
||||
);
|
||||
i += substr.length;
|
||||
}
|
||||
|
||||
// Push any text we missed (end of text)
|
||||
if (i < str.length) {
|
||||
result.push(<span key={i + "end"}>{str.substring(i)}</span>);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
let timestamp: JSX.Element | undefined;
|
||||
if (this.props.lastActiveTs) {
|
||||
const humanTs = humanizeTime(this.props.lastActiveTs);
|
||||
timestamp = <span className="mx_InviteDialog_tile--room_time">{humanTs}</span>;
|
||||
}
|
||||
|
||||
const avatarSize = "36px";
|
||||
const avatarSize = "32px";
|
||||
const avatar = (this.props.member as ThreepidMember).isEmail ? (
|
||||
<EmailPillAvatarIcon width={avatarSize} height={avatarSize} />
|
||||
) : (
|
||||
@@ -241,40 +195,23 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
||||
/>
|
||||
);
|
||||
|
||||
let checkmark: JSX.Element | undefined;
|
||||
if (this.props.isSelected) {
|
||||
// To reduce flickering we put the 'selected' room tile above the real avatar
|
||||
checkmark = <div className="mx_InviteDialog_tile--room_selected" />;
|
||||
}
|
||||
|
||||
// To reduce flickering we put the checkmark on top of the actual avatar (prevents
|
||||
// the browser from reloading the image source when the avatar remounts).
|
||||
const stackedAvatar = (
|
||||
<span className="mx_InviteDialog_tile_avatarStack">
|
||||
{avatar}
|
||||
{checkmark}
|
||||
</span>
|
||||
);
|
||||
|
||||
const userIdentifier = UserIdentifierCustomisations.getDisplayUserIdentifier(this.props.member.userId, {
|
||||
withDisplayName: true,
|
||||
});
|
||||
|
||||
const caption = (this.props.member as ThreepidMember).isEmail
|
||||
? _t("invite|email_caption")
|
||||
: this.highlightName(userIdentifier || this.props.member.userId);
|
||||
: userIdentifier || this.props.member.userId;
|
||||
|
||||
return (
|
||||
<AccessibleButton className="mx_InviteDialog_tile mx_InviteDialog_tile--room" onClick={this.onClick}>
|
||||
{stackedAvatar}
|
||||
<span className="mx_InviteDialog_tile_nameStack">
|
||||
<div className="mx_InviteDialog_tile_nameStack_name">
|
||||
{this.highlightName(this.props.member.name)}
|
||||
</div>
|
||||
<div className="mx_InviteDialog_tile_nameStack_userId">{caption}</div>
|
||||
</span>
|
||||
{timestamp}
|
||||
</AccessibleButton>
|
||||
<RichItem
|
||||
avatar={avatar}
|
||||
title={this.props.member.name}
|
||||
description={caption}
|
||||
timestamp={this.props.lastActiveTs}
|
||||
onClick={this.onClick}
|
||||
selected={this.props.isSelected}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1048,8 +985,13 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
|
||||
if (sourceMembers.length === 0 && !hasAdditionalMembers) {
|
||||
return (
|
||||
<div className="mx_InviteDialog_section">
|
||||
<h3>{sectionName}</h3>
|
||||
<p>{_t("common|no_results")}</p>
|
||||
<RichList
|
||||
title={sectionName}
|
||||
titleAttributes={{ "role": "heading", "aria-level": 3 }}
|
||||
isEmpty={true}
|
||||
>
|
||||
{_t("common|no_results")}
|
||||
</RichList>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1084,14 +1026,15 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
|
||||
lastActiveTs={lastActive(r)}
|
||||
key={r.user.userId}
|
||||
onToggle={this.toggleMember}
|
||||
highlightWord={this.state.filterText}
|
||||
isSelected={this.state.targets.some((t) => t.userId === r.userId)}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<div className="mx_InviteDialog_section">
|
||||
<h3>{sectionName}</h3>
|
||||
{tiles}
|
||||
<RichList title={sectionName} titleAttributes={{ "role": "heading", "aria-level": 3 }}>
|
||||
{tiles}
|
||||
</RichList>
|
||||
{showMore}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,8 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { type VerificationRequest } from "matrix-js-sdk/src/crypto-api";
|
||||
import { VerificationPhase, VerificationRequestEvent, type VerificationRequest } from "matrix-js-sdk/src/crypto-api";
|
||||
import { VerificationMethod } from "matrix-js-sdk/src/types";
|
||||
import { type User } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
@@ -23,7 +24,17 @@ interface IProps {
|
||||
}
|
||||
|
||||
interface IState {
|
||||
// The VerificationRequest that is ongoing. This can be replaced if a
|
||||
// promise was supplied in the props and it completes.
|
||||
verificationRequest?: VerificationRequest;
|
||||
|
||||
// What phase the VerificationRequest is at. This is part of
|
||||
// verificationRequest but we have it as independent state because we need
|
||||
// to update when it changes.
|
||||
//
|
||||
// We listen to the `Change` event on verificationRequest and update phase
|
||||
// when that fires.
|
||||
phase?: VerificationPhase;
|
||||
}
|
||||
|
||||
export default class VerificationRequestDialog extends React.Component<IProps, IState> {
|
||||
@@ -31,22 +42,51 @@ export default class VerificationRequestDialog extends React.Component<IProps, I
|
||||
super(props);
|
||||
this.state = {
|
||||
verificationRequest: this.props.verificationRequest,
|
||||
phase: this.props.verificationRequest?.phase,
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
// Listen to when the verificationRequest changes, so we can keep our
|
||||
// phase up-to-date.
|
||||
this.state.verificationRequest?.on(VerificationRequestEvent.Change, this.onRequestChange);
|
||||
|
||||
this.props.verificationRequestPromise?.then((r) => {
|
||||
this.setState({ verificationRequest: r });
|
||||
// The request promise completed, so we have a new request
|
||||
|
||||
// Stop listening to the old request (if we have one, which normally we won't)
|
||||
this.state.verificationRequest?.off(VerificationRequestEvent.Change, this.onRequestChange);
|
||||
|
||||
// And start listening to the new one
|
||||
r.on(VerificationRequestEvent.Change, this.onRequestChange);
|
||||
|
||||
this.setState({ verificationRequest: r, phase: r.phase });
|
||||
});
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
// Stop listening for changes to the request when we close
|
||||
this.state.verificationRequest?.off(VerificationRequestEvent.Change, this.onRequestChange);
|
||||
}
|
||||
|
||||
/**
|
||||
* The verificationRequest changed, so we need to make sure we update our
|
||||
* state to have the correct phase.
|
||||
*
|
||||
* Note: this is called when verificationRequest changes in some way, not
|
||||
* when we replace verificationRequest with some new request.
|
||||
*/
|
||||
private readonly onRequestChange = (): void => {
|
||||
this.setState((prevState) => ({
|
||||
phase: prevState.verificationRequest?.phase,
|
||||
}));
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const request = this.state.verificationRequest;
|
||||
const otherUserId = request?.otherUserId;
|
||||
const member = this.props.member || (otherUserId ? MatrixClientPeg.safeGet().getUser(otherUserId) : null);
|
||||
const title = request?.isSelfVerification
|
||||
? _t("encryption|verification|verification_dialog_title_device")
|
||||
: _t("encryption|verification|verification_dialog_title_user");
|
||||
const title = this.dialogTitle(request);
|
||||
|
||||
if (!member) return null;
|
||||
|
||||
@@ -60,7 +100,7 @@ export default class VerificationRequestDialog extends React.Component<IProps, I
|
||||
>
|
||||
<EncryptionPanel
|
||||
layout="dialog"
|
||||
verificationRequest={this.props.verificationRequest}
|
||||
verificationRequest={this.state.verificationRequest}
|
||||
verificationRequestPromise={this.props.verificationRequestPromise}
|
||||
onClose={this.props.onFinished}
|
||||
member={member}
|
||||
@@ -69,4 +109,33 @@ export default class VerificationRequestDialog extends React.Component<IProps, I
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
|
||||
private dialogTitle(request?: VerificationRequest): string {
|
||||
if (request?.isSelfVerification) {
|
||||
switch (request.phase) {
|
||||
case VerificationPhase.Ready:
|
||||
return _t("encryption|verification|verification_dialog_title_choose");
|
||||
case VerificationPhase.Done:
|
||||
return _t("encryption|verification|verification_dialog_title_verified");
|
||||
case VerificationPhase.Started:
|
||||
switch (request.chosenMethod) {
|
||||
case VerificationMethod.Reciprocate:
|
||||
return _t("encryption|verification|verification_dialog_title_confirm_green_shield");
|
||||
case VerificationMethod.Sas:
|
||||
return _t("encryption|verification|verification_dialog_title_compare_emojis");
|
||||
default:
|
||||
return _t("encryption|verification|verification_dialog_title_device");
|
||||
}
|
||||
case VerificationPhase.Unsent:
|
||||
case VerificationPhase.Requested:
|
||||
return _t("encryption|verification|verification_dialog_title_start_on_other_device");
|
||||
case VerificationPhase.Cancelled:
|
||||
return _t("encryption|verification|verification_dialog_title_failed");
|
||||
default:
|
||||
return _t("encryption|verification|verification_dialog_title_device");
|
||||
}
|
||||
} else {
|
||||
return _t("encryption|verification|verification_dialog_title_user");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,7 +320,7 @@ export default class DateSeparator extends React.Component<IProps, IState> {
|
||||
const label = this.getLabel();
|
||||
|
||||
let dateHeaderContent: JSX.Element;
|
||||
if (this.state.jumpToDateEnabled) {
|
||||
if (this.state.jumpToDateEnabled && !this.props.forExport) {
|
||||
dateHeaderContent = this.renderJumpToDateMenu();
|
||||
} else {
|
||||
dateHeaderContent = (
|
||||
|
||||
@@ -43,7 +43,7 @@ const EncryptionInfo: React.FC<IProps> = ({
|
||||
}: IProps) => {
|
||||
let content: JSX.Element;
|
||||
if (waitingForOtherParty && isSelfVerification) {
|
||||
content = <div>{_t("encryption|verification|self_verification_hint")}</div>;
|
||||
content = <div>{_t("encryption|verification|once_accepted_can_continue")}</div>;
|
||||
} else if (waitingForOtherParty || waitingForNetwork) {
|
||||
let text: string;
|
||||
if (waitingForOtherParty) {
|
||||
|
||||
@@ -126,7 +126,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
||||
) : null;
|
||||
return (
|
||||
<div>
|
||||
{_t("encryption|verification|qr_or_sas_header")}
|
||||
{_t("encryption|verification|verify_by_completing_one_of")}
|
||||
<div className="mx_VerificationPanel_QRPhase_startOptions">
|
||||
{qrBlockDialog}
|
||||
{or}
|
||||
@@ -224,7 +224,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
||||
private renderQRReciprocatePhase(): JSX.Element {
|
||||
const { member, request } = this.props;
|
||||
const description = request.isSelfVerification
|
||||
? _t("encryption|verification|qr_reciprocate_same_shield_device")
|
||||
? _t("encryption|verification|qr_reciprocate_check_again_device")
|
||||
: _t("encryption|verification|qr_reciprocate_same_shield_user", {
|
||||
displayName: (member as User).displayName || (member as RoomMember).name || member.userId,
|
||||
});
|
||||
@@ -236,19 +236,18 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
||||
<p>{description}</p>
|
||||
<E2EIcon isUser={true} status={E2EStatus.Verified} size={128} hideTooltip={true} />
|
||||
<div className="mx_VerificationPanel_reciprocateButtons">
|
||||
<AccessibleButton
|
||||
kind="danger"
|
||||
disabled={this.state.reciprocateButtonClicked}
|
||||
onClick={this.onReciprocateNoClick}
|
||||
>
|
||||
{_t("action|no")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
disabled={this.state.reciprocateButtonClicked}
|
||||
onClick={this.onReciprocateYesClick}
|
||||
>
|
||||
{_t("action|yes")}
|
||||
{_t("encryption|verification|qr_reciprocate_yes")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
disabled={this.state.reciprocateButtonClicked}
|
||||
onClick={this.onReciprocateNoClick}
|
||||
>
|
||||
{_t("encryption|verification|qr_reciprocate_no")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
@@ -260,12 +259,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
||||
</p>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="mx_UserInfo_container mx_VerificationPanel_reciprocate_section">
|
||||
<h3>{_t("encryption|verification|scan_qr")}</h3>
|
||||
{body}
|
||||
</div>
|
||||
);
|
||||
return <div className="mx_UserInfo_container mx_VerificationPanel_reciprocate_section">{body}</div>;
|
||||
}
|
||||
|
||||
private renderVerifiedPhase(): JSX.Element {
|
||||
@@ -282,18 +276,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
||||
|
||||
let description: string;
|
||||
if (request.isSelfVerification) {
|
||||
const device = this.state.otherDeviceDetails;
|
||||
if (!device) {
|
||||
// This can happen if the device is logged out while we're still showing verification
|
||||
// UI for it.
|
||||
logger.warn("Verified device we don't know about: " + this.props.request.otherDeviceId);
|
||||
description = _t("encryption|verification|successful_own_device");
|
||||
} else {
|
||||
description = _t("encryption|verification|successful_device", {
|
||||
deviceName: device.displayName,
|
||||
deviceId: device.deviceId,
|
||||
});
|
||||
}
|
||||
description = _t("encryption|verification|now_you_can");
|
||||
} else {
|
||||
description = _t("encryption|verification|successful_user", {
|
||||
displayName: (member as User).displayName || (member as RoomMember).name || member.userId,
|
||||
@@ -313,35 +296,9 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
||||
}
|
||||
|
||||
private renderCancelledPhase(): JSX.Element {
|
||||
const { member, request } = this.props;
|
||||
|
||||
let startAgainInstruction: string;
|
||||
if (request.isSelfVerification) {
|
||||
startAgainInstruction = _t("encryption|verification|prompt_self");
|
||||
} else {
|
||||
startAgainInstruction = _t("encryption|verification|prompt_user");
|
||||
}
|
||||
|
||||
let text: string;
|
||||
if (request.cancellationCode === "m.timeout") {
|
||||
text = _t("encryption|verification|timed_out") + ` ${startAgainInstruction}`;
|
||||
} else if (request.cancellingUserId === request.otherUserId) {
|
||||
if (request.isSelfVerification) {
|
||||
text = _t("encryption|verification|cancelled_self");
|
||||
} else {
|
||||
text = _t("encryption|verification|cancelled_user", {
|
||||
displayName: (member as User).displayName || (member as RoomMember).name || member.userId,
|
||||
});
|
||||
}
|
||||
text = `${text} ${startAgainInstruction}`;
|
||||
} else {
|
||||
text = _t("encryption|verification|cancelled") + ` ${startAgainInstruction}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_UserInfo_container">
|
||||
<h3>{_t("common|verification_cancelled")}</h3>
|
||||
<p>{text}</p>
|
||||
<p>{_t("encryption|verification|cancelled_verification")}</p>
|
||||
|
||||
<AccessibleButton kind="primary" className="mx_UserInfo_wideButton" onClick={this.props.onClose}>
|
||||
{_t("action|got_it")}
|
||||
|
||||
@@ -129,6 +129,7 @@ export default function RoomHeader({
|
||||
disabled={!!videoCallDisabledReason}
|
||||
color="primary"
|
||||
aria-label={videoCallDisabledReason ?? _t("action|join")}
|
||||
data-testId="join-call-button"
|
||||
>
|
||||
{_t("action|join")}
|
||||
</Button>
|
||||
|
||||
@@ -43,6 +43,7 @@ function Button({ label, keyCombo, onClick, actionState, icon }: ButtonProps): J
|
||||
element="button"
|
||||
onClick={onClick as (e: ButtonEvent) => void}
|
||||
aria-label={label}
|
||||
disabled={actionState === "disabled"}
|
||||
className={classNames("mx_FormattingButtons_Button", {
|
||||
mx_FormattingButtons_active: actionState === "reversed",
|
||||
mx_FormattingButtons_Button_hover: actionState === "enabled",
|
||||
@@ -64,55 +65,59 @@ function Button({ label, keyCombo, onClick, actionState, icon }: ButtonProps): J
|
||||
interface FormattingButtonsProps {
|
||||
composer: FormattingFunctions;
|
||||
actionStates: AllActionStates;
|
||||
/**
|
||||
* Whether all buttons should be disabled
|
||||
*/
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function FormattingButtons({ composer, actionStates }: FormattingButtonsProps): JSX.Element {
|
||||
export function FormattingButtons({ composer, actionStates, disabled }: FormattingButtonsProps): JSX.Element {
|
||||
const composerContext = useComposerContext();
|
||||
const isInList = actionStates.unorderedList === "reversed" || actionStates.orderedList === "reversed";
|
||||
return (
|
||||
<div className="mx_FormattingButtons">
|
||||
<Button
|
||||
actionState={actionStates.bold}
|
||||
actionState={disabled ? "disabled" : actionStates.bold}
|
||||
label={_t("composer|format_bold")}
|
||||
keyCombo={{ ctrlOrCmdKey: true, key: "b" }}
|
||||
onClick={() => composer.bold()}
|
||||
icon={<BoldIcon className="mx_FormattingButtons_Icon" />}
|
||||
/>
|
||||
<Button
|
||||
actionState={actionStates.italic}
|
||||
actionState={disabled ? "disabled" : actionStates.italic}
|
||||
label={_t("composer|format_italic")}
|
||||
keyCombo={{ ctrlOrCmdKey: true, key: "i" }}
|
||||
onClick={() => composer.italic()}
|
||||
icon={<ItalicIcon className="mx_FormattingButtons_Icon" />}
|
||||
/>
|
||||
<Button
|
||||
actionState={actionStates.underline}
|
||||
actionState={disabled ? "disabled" : actionStates.underline}
|
||||
label={_t("composer|format_underline")}
|
||||
keyCombo={{ ctrlOrCmdKey: true, key: "u" }}
|
||||
onClick={() => composer.underline()}
|
||||
icon={<UnderlineIcon className="mx_FormattingButtons_Icon" />}
|
||||
/>
|
||||
<Button
|
||||
actionState={actionStates.strikeThrough}
|
||||
actionState={disabled ? "disabled" : actionStates.strikeThrough}
|
||||
label={_t("composer|format_strikethrough")}
|
||||
onClick={() => composer.strikeThrough()}
|
||||
icon={<StrikeThroughIcon className="mx_FormattingButtons_Icon" />}
|
||||
/>
|
||||
<Button
|
||||
actionState={actionStates.unorderedList}
|
||||
actionState={disabled ? "disabled" : actionStates.unorderedList}
|
||||
label={_t("composer|format_unordered_list")}
|
||||
onClick={() => composer.unorderedList()}
|
||||
icon={<BulletedListIcon className="mx_FormattingButtons_Icon" />}
|
||||
/>
|
||||
<Button
|
||||
actionState={actionStates.orderedList}
|
||||
actionState={disabled ? "disabled" : actionStates.orderedList}
|
||||
label={_t("composer|format_ordered_list")}
|
||||
onClick={() => composer.orderedList()}
|
||||
icon={<NumberedListIcon className="mx_FormattingButtons_Icon" />}
|
||||
/>
|
||||
{isInList && (
|
||||
<Button
|
||||
actionState={actionStates.indent}
|
||||
actionState={disabled ? "disabled" : actionStates.indent}
|
||||
label={_t("composer|format_increase_indent")}
|
||||
onClick={() => composer.indent()}
|
||||
icon={<IndentIcon className="mx_FormattingButtons_Icon" />}
|
||||
@@ -120,33 +125,33 @@ export function FormattingButtons({ composer, actionStates }: FormattingButtonsP
|
||||
)}
|
||||
{isInList && (
|
||||
<Button
|
||||
actionState={actionStates.unindent}
|
||||
actionState={disabled ? "disabled" : actionStates.unindent}
|
||||
label={_t("composer|format_decrease_indent")}
|
||||
onClick={() => composer.unindent()}
|
||||
icon={<UnIndentIcon className="mx_FormattingButtons_Icon" />}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
actionState={actionStates.quote}
|
||||
actionState={disabled ? "disabled" : actionStates.quote}
|
||||
label={_t("action|quote")}
|
||||
onClick={() => composer.quote()}
|
||||
icon={<QuoteIcon className="mx_FormattingButtons_Icon" />}
|
||||
/>
|
||||
<Button
|
||||
actionState={actionStates.inlineCode}
|
||||
actionState={disabled ? "disabled" : actionStates.inlineCode}
|
||||
label={_t("composer|format_inline_code")}
|
||||
keyCombo={{ ctrlOrCmdKey: true, key: "e" }}
|
||||
onClick={() => composer.inlineCode()}
|
||||
icon={<InlineCodeIcon className="mx_FormattingButtons_Icon" />}
|
||||
/>
|
||||
<Button
|
||||
actionState={actionStates.codeBlock}
|
||||
actionState={disabled ? "disabled" : actionStates.codeBlock}
|
||||
label={_t("composer|format_code_block")}
|
||||
onClick={() => composer.codeBlock()}
|
||||
icon={<CodeBlockIcon className="mx_FormattingButtons_Icon" />}
|
||||
/>
|
||||
<Button
|
||||
actionState={actionStates.link}
|
||||
actionState={disabled ? "disabled" : actionStates.link}
|
||||
label={_t("composer|format_link")}
|
||||
onClick={() => openLinkModal(composer, composerContext, actionStates.link === "reversed")}
|
||||
icon={<LinkIcon className="mx_FormattingButtons_Icon" />}
|
||||
|
||||
@@ -60,6 +60,7 @@ export function PlainTextComposer({
|
||||
handleCommand,
|
||||
handleMention,
|
||||
handleAtRoomMention,
|
||||
handleEmoji,
|
||||
} = usePlainTextListeners(initialContent, onChange, onSend, eventRelation, isAutoReplaceEmojiEnabled);
|
||||
const composerFunctions = useComposerFunctions(editorRef, setContent);
|
||||
usePlainTextInitialization(initialContent, editorRef);
|
||||
@@ -84,6 +85,7 @@ export function PlainTextComposer({
|
||||
handleMention={handleMention}
|
||||
handleCommand={handleCommand}
|
||||
handleAtRoomMention={handleAtRoomMention}
|
||||
handleEmoji={handleEmoji}
|
||||
/>
|
||||
<Editor
|
||||
ref={editorRef}
|
||||
|
||||
@@ -40,6 +40,12 @@ interface WysiwygAutocompleteProps {
|
||||
*/
|
||||
handleAtRoomMention: FormattingFunctions["mentionAtRoom"];
|
||||
|
||||
/**
|
||||
* This handler will be called with the emoji character on clicking
|
||||
* an emoji in the autocomplete list or pressing enter on a selected item
|
||||
*/
|
||||
handleEmoji: FormattingFunctions["emoji"];
|
||||
|
||||
ref?: Ref<Autocomplete>;
|
||||
}
|
||||
|
||||
@@ -55,6 +61,7 @@ const WysiwygAutocomplete = ({
|
||||
handleMention,
|
||||
handleCommand,
|
||||
handleAtRoomMention,
|
||||
handleEmoji,
|
||||
ref,
|
||||
}: WysiwygAutocompleteProps): JSX.Element | null => {
|
||||
const { room } = useScopedRoomContext("room");
|
||||
@@ -89,7 +96,14 @@ const WysiwygAutocomplete = ({
|
||||
return;
|
||||
}
|
||||
// TODO - handle "community" type
|
||||
case "community": {
|
||||
return; // no-op until we decide how to handle community in the wysiwyg composer
|
||||
}
|
||||
default:
|
||||
{
|
||||
// similar to the cider editor we handle emoji and other plain text replacement in the default case
|
||||
handleEmoji(completion.completion);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import { parsePermalink } from "../../../../../utils/permalinks/Permalinks";
|
||||
import { isNotNull } from "../../../../../Typeguards";
|
||||
import { useSettingValue } from "../../../../../hooks/useSettings";
|
||||
import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext.tsx";
|
||||
import { useContainsCommand } from "../hooks/useContainsCommand.ts";
|
||||
|
||||
interface WysiwygComposerProps {
|
||||
disabled?: boolean;
|
||||
@@ -83,6 +84,9 @@ export const WysiwygComposer = memo(function WysiwygComposer({
|
||||
}
|
||||
}, [onChange, messageContent, disabled]);
|
||||
|
||||
// Disable formatting buttons if the message content contains a slash command
|
||||
const disableFormatting = useContainsCommand(content, room);
|
||||
|
||||
useEffect(() => {
|
||||
function handleClick(e: Event): void {
|
||||
e.preventDefault();
|
||||
@@ -123,8 +127,9 @@ export const WysiwygComposer = memo(function WysiwygComposer({
|
||||
handleMention={wysiwyg.mention}
|
||||
handleAtRoomMention={wysiwyg.mentionAtRoom}
|
||||
handleCommand={wysiwyg.command}
|
||||
handleEmoji={wysiwyg.emoji}
|
||||
/>
|
||||
<FormattingButtons composer={wysiwyg} actionStates={actionStates} />
|
||||
<FormattingButtons composer={wysiwyg} actionStates={actionStates} disabled={disableFormatting} />
|
||||
<Editor
|
||||
ref={ref}
|
||||
disabled={!isReady}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
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 Room } from "matrix-js-sdk/src/matrix";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
|
||||
import CommandProvider from "../../../../../autocomplete/CommandProvider";
|
||||
|
||||
/**
|
||||
* A hook which determines if the given content contains a slash command.
|
||||
* @returns true if the content contains a slash command, false otherwise.
|
||||
* @param content The content to check for commands.
|
||||
* @param room The current room.
|
||||
*/
|
||||
export function useContainsCommand(content: string | null, room: Room | undefined): boolean {
|
||||
const [contentContainsCommands, setContentContainsCommands] = useState(false);
|
||||
const providerRef = useRef<CommandProvider | null>(null);
|
||||
const currentRoomIdRef = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!room || !content) {
|
||||
setContentContainsCommands(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create or reuse CommandProvider for the current room
|
||||
if (!providerRef.current || currentRoomIdRef.current !== room.roomId) {
|
||||
providerRef.current = new CommandProvider(room);
|
||||
currentRoomIdRef.current = room.roomId;
|
||||
}
|
||||
|
||||
const provider = providerRef.current;
|
||||
provider
|
||||
.getCompletions(content, { start: 0, end: 0 })
|
||||
.then((results) => {
|
||||
if (results.length > 0) {
|
||||
setContentContainsCommands(true);
|
||||
} else {
|
||||
setContentContainsCommands(false);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// If there's an error getting completions, assume no commands
|
||||
setContentContainsCommands(false);
|
||||
});
|
||||
}, [content, room]);
|
||||
|
||||
return contentContainsCommands;
|
||||
}
|
||||
@@ -60,6 +60,7 @@ export function usePlainTextListeners(
|
||||
handleMention: (link: string, text: string, attributes: AllowedMentionAttributes) => void;
|
||||
handleAtRoomMention: (attributes: AllowedMentionAttributes) => void;
|
||||
handleCommand: (text: string) => void;
|
||||
handleEmoji: (emoji: string) => void;
|
||||
onSelect: (event: SyntheticEvent<HTMLDivElement>) => void;
|
||||
suggestion: MappedSuggestion | null;
|
||||
} {
|
||||
@@ -95,8 +96,15 @@ export function usePlainTextListeners(
|
||||
// For separation of concerns, the suggestion handling is kept in a separate hook but is
|
||||
// nested here because we do need to be able to update the `content` state in this hook
|
||||
// when a user selects a suggestion from the autocomplete menu
|
||||
const { suggestion, onSelect, handleCommand, handleMention, handleAtRoomMention, handleEmojiReplacement } =
|
||||
useSuggestion(ref, setText, isAutoReplaceEmojiEnabled);
|
||||
const {
|
||||
suggestion,
|
||||
onSelect,
|
||||
handleCommand,
|
||||
handleMention,
|
||||
handleAtRoomMention,
|
||||
handleEmojiSuggestion,
|
||||
handleEmojiReplacement,
|
||||
} = useSuggestion(ref, setText, isAutoReplaceEmojiEnabled);
|
||||
|
||||
const onInput = useCallback(
|
||||
(event: SyntheticEvent<HTMLDivElement, InputEvent | ClipboardEvent>) => {
|
||||
@@ -178,5 +186,6 @@ export function usePlainTextListeners(
|
||||
handleCommand,
|
||||
handleMention,
|
||||
handleAtRoomMention,
|
||||
handleEmoji: handleEmojiSuggestion,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ export function useSuggestion(
|
||||
handleMention: (href: string, displayName: string, attributes: AllowedMentionAttributes) => void;
|
||||
handleAtRoomMention: (attributes: AllowedMentionAttributes) => void;
|
||||
handleCommand: (text: string) => void;
|
||||
handleEmojiSuggestion: (text: string) => void;
|
||||
handleEmojiReplacement: () => void;
|
||||
onSelect: (event: SyntheticEvent<HTMLDivElement>) => void;
|
||||
suggestion: MappedSuggestion | null;
|
||||
@@ -86,11 +87,15 @@ export function useSuggestion(
|
||||
|
||||
const handleEmojiReplacement = (): void => processEmojiReplacement(suggestionData, setSuggestionData, setText);
|
||||
|
||||
const handleEmojiSuggestion = (emoji: string): void =>
|
||||
processTextReplacement(emoji, suggestionData, setSuggestionData, setText);
|
||||
|
||||
return {
|
||||
suggestion: suggestionData?.mappedSuggestion ?? null,
|
||||
handleCommand,
|
||||
handleMention,
|
||||
handleAtRoomMention,
|
||||
handleEmojiSuggestion,
|
||||
handleEmojiReplacement,
|
||||
onSelect,
|
||||
};
|
||||
@@ -260,10 +265,31 @@ export function processEmojiReplacement(
|
||||
setText: (text?: string) => void,
|
||||
): void {
|
||||
// if we do not have a suggestion of the correct type, return early
|
||||
if (suggestionData === null || suggestionData.mappedSuggestion.type !== `custom`) {
|
||||
if (suggestionData?.mappedSuggestion?.type !== `custom`) {
|
||||
return;
|
||||
}
|
||||
const { node, mappedSuggestion } = suggestionData;
|
||||
|
||||
processTextReplacement(suggestionData.mappedSuggestion.text, suggestionData, setSuggestionData, setText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the relevant part of the editor text, replacing the suggestionData selection with the replacement text.
|
||||
* @param replacementText - the text that we will insert into the DOM
|
||||
* @param suggestionData - representation of the part of the DOM that will be replaced
|
||||
* @param setSuggestionData - setter function to set the suggestion state
|
||||
* @param setText - setter function to set the content of the composer
|
||||
*/
|
||||
export function processTextReplacement(
|
||||
replacementText: string,
|
||||
suggestionData: SuggestionState,
|
||||
setSuggestionData: React.Dispatch<React.SetStateAction<SuggestionState>>,
|
||||
setText: (text?: string) => void,
|
||||
): void {
|
||||
// if we do not have suggestion data return early
|
||||
if (suggestionData === null) {
|
||||
return;
|
||||
}
|
||||
const { node } = suggestionData;
|
||||
const existingContent = node.textContent;
|
||||
|
||||
if (existingContent == null) {
|
||||
@@ -273,7 +299,7 @@ export function processEmojiReplacement(
|
||||
// replace the emoticon with the suggesed emoji
|
||||
const newContent =
|
||||
existingContent.slice(0, suggestionData.startOffset) +
|
||||
mappedSuggestion.text +
|
||||
replacementText +
|
||||
existingContent.slice(suggestionData.endOffset);
|
||||
|
||||
node.textContent = newContent;
|
||||
@@ -405,6 +431,8 @@ export function getMappedSuggestion(text: string, isAutoReplaceEmojiEnabled?: bo
|
||||
case "#":
|
||||
case "@":
|
||||
return { keyChar: firstChar, text: restOfString, type: "mention" };
|
||||
case ":":
|
||||
return { keyChar: firstChar, text: restOfString, type: "emoji" };
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ function EncryptionDetails({ onResetIdentityClick }: EncryptionDetails): JSX.Ele
|
||||
<Button
|
||||
size="sm"
|
||||
kind="secondary"
|
||||
Icon={ShareIcon}
|
||||
Icon={DownloadIcon}
|
||||
onClick={() =>
|
||||
Modal.createDialog(
|
||||
lazy(
|
||||
@@ -89,7 +89,7 @@ function EncryptionDetails({ onResetIdentityClick }: EncryptionDetails): JSX.Ele
|
||||
<Button
|
||||
size="sm"
|
||||
kind="secondary"
|
||||
Icon={DownloadIcon}
|
||||
Icon={ShareIcon}
|
||||
onClick={() =>
|
||||
Modal.createDialog(
|
||||
lazy(
|
||||
|
||||
@@ -13,6 +13,7 @@ import { SettingsSection } from "../shared/SettingsSection";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { SettingsHeader } from "../SettingsHeader";
|
||||
import { useKeyStoragePanelViewModel } from "../../../viewmodels/settings/encryption/KeyStoragePanelViewModel";
|
||||
import SdkConfig from "../../../../SdkConfig";
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
@@ -55,7 +56,7 @@ export const KeyStoragePanel: React.FC<Props> = ({ onKeyStorageDisableClick }) =
|
||||
}
|
||||
subHeading={_t("settings|encryption|key_storage|description", undefined, {
|
||||
a: (sub) => (
|
||||
<a href="https://element.io/help#encryption5" target="_blank" rel="noreferrer noopener">
|
||||
<a href={SdkConfig.get("help_encryption_url")} target="_blank" rel="noreferrer noopener">
|
||||
{sub}
|
||||
</a>
|
||||
),
|
||||
|
||||
@@ -265,7 +265,7 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
||||
</SettingsSubsection>
|
||||
|
||||
<SettingsSubsection heading={_t("settings|preferences|room_list_heading")}>
|
||||
{this.renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS)}
|
||||
{!newRoomListEnabled && this.renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS)}
|
||||
{/* The settings is on device level where the other room list settings are on account level */}
|
||||
{newRoomListEnabled && (
|
||||
<SettingsFlag name="RoomList.showMessagePreview" level={SettingLevel.DEVICE} />
|
||||
|
||||
@@ -68,6 +68,7 @@ import { ThreadsActivityCentre } from "./threads-activity-centre/";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { Landmark, LandmarkNavigation } from "../../../accessibility/LandmarkNavigation";
|
||||
import { KeyboardShortcut } from "../settings/KeyboardShortcut";
|
||||
import { ReleaseAnnouncement } from "../../structures/ReleaseAnnouncement";
|
||||
|
||||
const useSpaces = (): [Room[], MetaSpace[], Room[], SpaceKey] => {
|
||||
const invites = useEventEmitterState<Room[]>(SpaceStore.instance, UPDATE_INVITED_SPACES, () => {
|
||||
@@ -379,61 +380,72 @@ const SpacePanel: React.FC = () => {
|
||||
onDragEndHandler();
|
||||
}}
|
||||
>
|
||||
<nav
|
||||
className={classNames("mx_SpacePanel", {
|
||||
collapsed: isPanelCollapsed,
|
||||
newUi: newRoomListEnabled,
|
||||
})}
|
||||
onKeyDown={(ev) => {
|
||||
const navAction = getKeyBindingsManager().getNavigationAction(ev);
|
||||
if (
|
||||
navAction === KeyBindingAction.NextLandmark ||
|
||||
navAction === KeyBindingAction.PreviousLandmark
|
||||
) {
|
||||
LandmarkNavigation.findAndFocusNextLandmark(
|
||||
Landmark.ACTIVE_SPACE_BUTTON,
|
||||
navAction === KeyBindingAction.PreviousLandmark,
|
||||
);
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
return;
|
||||
}
|
||||
onKeyDownHandler(ev);
|
||||
}}
|
||||
ref={ref}
|
||||
aria-label={_t("common|spaces")}
|
||||
<ReleaseAnnouncement
|
||||
feature="newNotificationSounds"
|
||||
header={_t("settings|notifications|sounds_release_announcement|title")}
|
||||
description={_t("settings|notifications|sounds_release_announcement|description")}
|
||||
closeLabel={_t("action|ok")}
|
||||
displayArrow={false}
|
||||
placement="right-start"
|
||||
>
|
||||
<UserMenu isPanelCollapsed={isPanelCollapsed}>
|
||||
<AccessibleButton
|
||||
className={classNames("mx_SpacePanel_toggleCollapse", { expanded: !isPanelCollapsed })}
|
||||
onClick={() => setPanelCollapsed(!isPanelCollapsed)}
|
||||
title={isPanelCollapsed ? _t("action|expand") : _t("action|collapse")}
|
||||
caption={
|
||||
<KeyboardShortcut
|
||||
value={{ ctrlOrCmdKey: true, shiftKey: true, key: "d" }}
|
||||
className="mx_SpacePanel_Tooltip_KeyboardShortcut"
|
||||
/>
|
||||
<nav
|
||||
className={classNames("mx_SpacePanel", {
|
||||
collapsed: isPanelCollapsed,
|
||||
newUi: newRoomListEnabled,
|
||||
})}
|
||||
onKeyDown={(ev) => {
|
||||
const navAction = getKeyBindingsManager().getNavigationAction(ev);
|
||||
if (
|
||||
navAction === KeyBindingAction.NextLandmark ||
|
||||
navAction === KeyBindingAction.PreviousLandmark
|
||||
) {
|
||||
LandmarkNavigation.findAndFocusNextLandmark(
|
||||
Landmark.ACTIVE_SPACE_BUTTON,
|
||||
navAction === KeyBindingAction.PreviousLandmark,
|
||||
);
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
return;
|
||||
}
|
||||
/>
|
||||
</UserMenu>
|
||||
<Droppable droppableId="top-level-spaces">
|
||||
{(provided, snapshot) => (
|
||||
<InnerSpacePanel
|
||||
{...provided.droppableProps}
|
||||
isPanelCollapsed={isPanelCollapsed}
|
||||
setPanelCollapsed={setPanelCollapsed}
|
||||
isDraggingOver={snapshot.isDraggingOver}
|
||||
innerRef={provided.innerRef}
|
||||
>
|
||||
{provided.placeholder}
|
||||
</InnerSpacePanel>
|
||||
)}
|
||||
</Droppable>
|
||||
onKeyDownHandler(ev);
|
||||
}}
|
||||
ref={ref}
|
||||
aria-label={_t("common|spaces")}
|
||||
>
|
||||
<UserMenu isPanelCollapsed={isPanelCollapsed}>
|
||||
<AccessibleButton
|
||||
className={classNames("mx_SpacePanel_toggleCollapse", {
|
||||
expanded: !isPanelCollapsed,
|
||||
})}
|
||||
onClick={() => setPanelCollapsed(!isPanelCollapsed)}
|
||||
title={isPanelCollapsed ? _t("action|expand") : _t("action|collapse")}
|
||||
caption={
|
||||
<KeyboardShortcut
|
||||
value={{ ctrlOrCmdKey: true, shiftKey: true, key: "d" }}
|
||||
className="mx_SpacePanel_Tooltip_KeyboardShortcut"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</UserMenu>
|
||||
<Droppable droppableId="top-level-spaces">
|
||||
{(provided, snapshot) => (
|
||||
<InnerSpacePanel
|
||||
{...provided.droppableProps}
|
||||
isPanelCollapsed={isPanelCollapsed}
|
||||
setPanelCollapsed={setPanelCollapsed}
|
||||
isDraggingOver={snapshot.isDraggingOver}
|
||||
innerRef={provided.innerRef}
|
||||
>
|
||||
{provided.placeholder}
|
||||
</InnerSpacePanel>
|
||||
)}
|
||||
</Droppable>
|
||||
|
||||
<ThreadsActivityCentre displayButtonLabel={!isPanelCollapsed} />
|
||||
<ThreadsActivityCentre displayButtonLabel={!isPanelCollapsed} />
|
||||
|
||||
<QuickSettingsButton isPanelCollapsed={isPanelCollapsed} />
|
||||
</nav>
|
||||
<QuickSettingsButton isPanelCollapsed={isPanelCollapsed} />
|
||||
</nav>
|
||||
</ReleaseAnnouncement>
|
||||
</DragDropContext>
|
||||
)}
|
||||
</RovingTabIndexProvider>
|
||||
|
||||
@@ -177,7 +177,7 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
|
||||
detail={detail}
|
||||
primaryLabel={
|
||||
request.isSelfVerification || !request.roomId
|
||||
? _t("encryption|verification|request_toast_accept")
|
||||
? _t("encryption|verification|request_toast_start_verification")
|
||||
: _t("encryption|verification|request_toast_accept_user")
|
||||
}
|
||||
onPrimaryClick={this.accept}
|
||||
|
||||
@@ -119,7 +119,7 @@ export default class VerificationShowSas extends React.Component<IProps, IState>
|
||||
</div>
|
||||
);
|
||||
sasCaption = this.props.isSelf
|
||||
? _t("encryption|verification|sas_emoji_caption_self")
|
||||
? _t("encryption|verification|confirm_the_emojis")
|
||||
: _t("encryption|verification|sas_emoji_caption_user");
|
||||
} else if (this.props.sas.decimal) {
|
||||
const numberBlocks = this.props.sas.decimal.map((num, i) => <span key={i}>{num}</span>);
|
||||
|
||||
@@ -21,12 +21,11 @@ interface JoinCallViewProps {
|
||||
room: Room;
|
||||
resizing: boolean;
|
||||
call: Call;
|
||||
skipLobby?: boolean;
|
||||
role?: AriaRole;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call, skipLobby, role, onClose }) => {
|
||||
const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call, role, onClose }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
useTypedEventEmitter(call, CallEvent.Close, onClose);
|
||||
|
||||
@@ -35,12 +34,6 @@ const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call, skipLobby,
|
||||
call.clean();
|
||||
}, [call]);
|
||||
|
||||
useEffect(() => {
|
||||
// Always update the widget data so that we don't ignore "skipLobby" accidentally.
|
||||
call.widget.data ??= {};
|
||||
call.widget.data.skipLobby = skipLobby;
|
||||
}, [call.widget, skipLobby]);
|
||||
|
||||
const disconnectAllOtherCalls: () => Promise<void> = useCallback(async () => {
|
||||
// The stickyPromise has to resolve before the widget actually becomes sticky.
|
||||
// We only let the widget become sticky after disconnecting all other active calls.
|
||||
@@ -69,7 +62,6 @@ const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call, skipLobby,
|
||||
interface CallViewProps {
|
||||
room: Room;
|
||||
resizing: boolean;
|
||||
skipLobby?: boolean;
|
||||
role?: AriaRole;
|
||||
/**
|
||||
* Callback for when the user closes the call.
|
||||
@@ -77,19 +69,8 @@ interface CallViewProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const CallView: FC<CallViewProps> = ({ room, resizing, skipLobby, role, onClose }) => {
|
||||
export const CallView: FC<CallViewProps> = ({ room, resizing, role, onClose }) => {
|
||||
const call = useCall(room.roomId);
|
||||
|
||||
return (
|
||||
call && (
|
||||
<JoinCallView
|
||||
room={room}
|
||||
resizing={resizing}
|
||||
call={call}
|
||||
skipLobby={skipLobby}
|
||||
role={role}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)
|
||||
);
|
||||
return call && <JoinCallView room={room} resizing={resizing} call={call} role={role} onClose={onClose} />;
|
||||
};
|
||||
|
||||
@@ -229,7 +229,7 @@ export const useRoomCall = (
|
||||
if (widget && promptPinWidget) {
|
||||
WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Top);
|
||||
} else {
|
||||
placeCall(room, CallType.Voice, callPlatformType, evt?.shiftKey ?? false);
|
||||
placeCall(room, CallType.Voice, callPlatformType, evt?.shiftKey || undefined);
|
||||
}
|
||||
},
|
||||
[promptPinWidget, room, widget],
|
||||
@@ -240,7 +240,9 @@ export const useRoomCall = (
|
||||
if (widget && promptPinWidget) {
|
||||
WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Top);
|
||||
} else {
|
||||
placeCall(room, CallType.Video, callPlatformType, evt?.shiftKey ?? false);
|
||||
// If we have pressed shift then always skip the lobby, otherwise `undefined` will defer
|
||||
// to the defaults of the call implementation.
|
||||
placeCall(room, CallType.Video, callPlatformType, evt?.shiftKey || undefined);
|
||||
}
|
||||
},
|
||||
[widget, promptPinWidget, room],
|
||||
|
||||
@@ -656,6 +656,7 @@
|
||||
"poll_button_no_perms_description": "Nemáte oprávnění zahajovat hlasování v této místnosti.",
|
||||
"poll_button_no_perms_title": "Vyžaduje oprávnění",
|
||||
"replying_title": "Odpovídá",
|
||||
"room_unencrypted": "Zprávy v této místnosti nejsou koncově šifrované.",
|
||||
"room_upgraded_link": "Konverzace pokračuje zde.",
|
||||
"room_upgraded_notice": "Tato místnost byla nahrazena a už není používaná.",
|
||||
"send_button_title": "Poslat zprávu",
|
||||
@@ -719,6 +720,7 @@
|
||||
"personal_space_description": "Soukromý prostor pro uspořádání vašich místností",
|
||||
"private_description": "Pouze pozvat, nejlepší pro sebe nebo pro týmy",
|
||||
"private_heading": "Váš soukromý prostor",
|
||||
"private_only_heading": "Váš prostor",
|
||||
"private_personal_description": "Zajistěte, aby do %(name)s měli přístup správní lidé",
|
||||
"private_personal_heading": "S kým pracujete?",
|
||||
"private_space": "Já a moji spolupracovníci",
|
||||
@@ -969,7 +971,6 @@
|
||||
"title": "Záloha klíčů byla odstraněna",
|
||||
"warning": "Pokud jste způsob obnovy neodstranili vy, mohou se pokoušet k vašemu účtu dostat útočníci. Změňte si raději ihned heslo a nastavte nový způsob obnovy v Nastavení."
|
||||
},
|
||||
"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_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_title": "Nastavení zabezpečené zálohy",
|
||||
@@ -992,16 +993,18 @@
|
||||
"after_new_login": {
|
||||
"device_verified": "Zařízení ověřeno",
|
||||
"skip_verification": "Prozatím přeskočit ověřování",
|
||||
"unable_to_verify": "Nelze ověřit toto zařízení",
|
||||
"verify_this_device": "Ověřit toto zařízení"
|
||||
},
|
||||
"cancelled": "Zrušili jste proces ověření.",
|
||||
"cancelled_self": "Ověřování na jiném zařízení jste zrušili.",
|
||||
"cancelled_user": "%(displayName)s zrušil(a) proces ověření.",
|
||||
"cancelling": "Rušení…",
|
||||
"cant_confirm": "Nemůžete potvrdit?",
|
||||
"complete_action": "OK",
|
||||
"complete_description": "Uživatel úspěšně ověřen.",
|
||||
"complete_title": "Ověřeno!",
|
||||
"confirm_identity_description": "Ověřte toto zařízení a nastavte zabezpečené zasílání zpráv.",
|
||||
"confirm_identity_title": "Potvrďte svou totožnost",
|
||||
"error_starting_description": "Nepodařilo se zahájit chat s druhým uživatelem.",
|
||||
"error_starting_title": "Chyba při zahájení ověření",
|
||||
"explainer": "Bezpečné zprávy s tímto uživatelem jsou koncově šifrované a nikdo další je nemůže číst.",
|
||||
@@ -1027,7 +1030,6 @@
|
||||
"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.",
|
||||
"prompt_encrypted": "Ověřit všechny uživatele v místnosti, abyste se přesvědčili o bezpečnosti.",
|
||||
@@ -1043,7 +1045,6 @@
|
||||
"request_toast_accept_user": "Ověřit uživatele",
|
||||
"request_toast_decline_counter": "Ignorovat (%(counter)s)",
|
||||
"request_toast_detail": "%(deviceId)s z %(ip)s",
|
||||
"reset_proceed_prompt": "Pokračovat v resetování",
|
||||
"sas_caption_self": "Ověřte toto zařízení tak, že potvrdíte, že se na jeho obrazovce zobrazí následující číslo.",
|
||||
"sas_caption_user": "Ověřte uživatele zkontrolováním, že se na obrazovce objevila stejná čísla.",
|
||||
"sas_description": "Pokud na žádném zařízení nemáte kameru, porovnejte jedinečnou kombinaci emoji",
|
||||
@@ -1066,7 +1067,8 @@
|
||||
"unverified_sessions_toast_description": "Zkontrolujte, zda je váš účet v bezpečí",
|
||||
"unverified_sessions_toast_reject": "Později",
|
||||
"unverified_sessions_toast_title": "Máte neověřené relace",
|
||||
"verification_description": "Ověřte svou identitu, abyste získali přístup k šifrovaným zprávám a prokázali svou identitu ostatním. Pokud používáte také mobilní zařízení, před pokračováním otevřete aplikaci.",
|
||||
"use_another_device": "Použít jiné zařízení",
|
||||
"use_recovery_key": "Použít klíč pro obnovení",
|
||||
"verification_dialog_title_device": "Ověřit jiné zařízení",
|
||||
"verification_dialog_title_user": "Požadavek na ověření",
|
||||
"verification_skip_warning": "Bez ověření nebudete mít přístup ke všem svým zprávám a můžete se ostatním jevit jako nedůvěryhodní.",
|
||||
@@ -1076,9 +1078,6 @@
|
||||
"verify_emoji_prompt": "Ověření porovnáním několika emoji.",
|
||||
"verify_emoji_prompt_qr": "Pokud vám skenování kódů nefunguje, ověřte se porovnáním emoji.",
|
||||
"verify_later": "Ověřím se později",
|
||||
"verify_using_device": "Ověřit pomocí jiného zařízení",
|
||||
"verify_using_key": "Ověřit pomocí klíče pro obnovení",
|
||||
"verify_using_key_or_phrase": "Ověření pomocí klíče pro obnovení nebo fráze",
|
||||
"waiting_for_user_accept": "Čekáme, než %(displayName)s výzvu přijme…",
|
||||
"waiting_other_device": "Čekáme na ověření na jiném zařízení…",
|
||||
"waiting_other_device_details": "Čekáme na ověření na vašem dalším zařízení, %(deviceName)s (%(deviceId)s)…",
|
||||
@@ -1128,6 +1127,7 @@
|
||||
"tls": "Nelze se připojit k domovskému serveru – zkontrolujte prosím své připojení, prověřte, zda je <a>SSL certifikát</a> vašeho domovského serveru důvěryhodný, a že některé z rozšíření prohlížeče neblokuje komunikaci.",
|
||||
"unknown": "Neznámá chyba",
|
||||
"unknown_error_code": "neznámý kód chyby",
|
||||
"update_history_visibility": "Nepodařilo se změnit viditelnost historie",
|
||||
"update_power_level": "Nepodařilo se změnit úroveň oprávnění"
|
||||
},
|
||||
"error_app_open_in_another_tab": "Přepněte na jiný panel a připojte se k %(brand)s. Tuto kartu nyní můžete zavřít.",
|
||||
@@ -2001,7 +2001,9 @@
|
||||
"inaccessible_subtitle_1": "Zkuste to později nebo požádejte správce místnosti či prostoru, aby zkontroloval, zda máte přístup.",
|
||||
"inaccessible_subtitle_2": "Při pokusu o přístup do místnosti nebo prostoru bylo vráceno %(errcode)s. Pokud si myslíte, že se vám tato zpráva zobrazuje chybně, pošlete prosím <issueLink>hlášení o chybě</issueLink>.",
|
||||
"intro": {
|
||||
"display_topic": "Téma: <topic/>",
|
||||
"dm_caption": "V této konverzaci jste pouze vy dva, dokud někdo z vás nepozve někoho dalšího.",
|
||||
"edit_topic": "Téma: <topic/> (<a>upravit</a>)",
|
||||
"enable_encryption_prompt": "Povolte šifrování v nastavení.",
|
||||
"encrypted_3pid_dm_pending_join": "Jakmile se všichni připojí, budete moci konverzovat",
|
||||
"no_avatar_label": "Přidejte fotografii, aby lidé mohli snadno najít váši místnost.",
|
||||
@@ -2065,7 +2067,9 @@
|
||||
"pinned_message_banner": {
|
||||
"button_close_list": "Zavřít seznam",
|
||||
"button_view_all": "Zobrazit vše",
|
||||
"description": "Tato místnost má připnuté zprávy. Kliknutím je zobrazíte.",
|
||||
"description": "Připnuté zprávy",
|
||||
"go_to_newest_message": "Zobrazit připnutou zprávu na časové ose a nejnovější připnutou zprávu zde",
|
||||
"go_to_next_message": "Zobrazit připnutou zprávu na časové ose a další nejstarší připnutou zprávu zde",
|
||||
"title": "<bold>%(index)sz%(length)s</bold> Připnuté zprávy"
|
||||
},
|
||||
"read_topic": "Klikněte pro přečtení tématu",
|
||||
@@ -2176,6 +2180,26 @@
|
||||
"one": "Momentálně se odstraňují zprávy v %(count)s místnosti",
|
||||
"other": "Momentálně se odstraňují zprávy v %(count)s místnostech"
|
||||
},
|
||||
"release_announcement": {
|
||||
"done": "Hotovo",
|
||||
"filter": {
|
||||
"description": "Filtrujte své chaty jediným kliknutím. Rozbalením zobrazíte další filtry.",
|
||||
"title": "Nové rychlé filtry"
|
||||
},
|
||||
"intro": {
|
||||
"description": "Seznam chatů byl aktualizován, aby byl přehlednější a jednodušší na používání.",
|
||||
"title": "Chaty mají nový vzhled!"
|
||||
},
|
||||
"next": "Další",
|
||||
"settings": {
|
||||
"description": "Chcete-li zobrazit nebo skrýt náhledy zpráv, přejděte do Všechna nastavení > Předvolby > Seznam místností",
|
||||
"title": "Některá nastavení byla přesunuta"
|
||||
},
|
||||
"sort": {
|
||||
"description": "Změňte pořadí svých chatů z nejnovějších na A-Z",
|
||||
"title": "Seřaďte si chaty"
|
||||
}
|
||||
},
|
||||
"room": {
|
||||
"more_options": "Více možností",
|
||||
"open_room": "Otevřít místnost %(roomName)s"
|
||||
@@ -2365,6 +2389,10 @@
|
||||
"users_default": "Výchozí role"
|
||||
},
|
||||
"security": {
|
||||
"cannot_change_to_private_due_to_missing_history_visiblity_permissions": {
|
||||
"description": "Nemáte oprávnění měnit viditelnost historie místnosti. To je nebezpečné, protože by to mohlo umožnit uživatelům, kteří nejsou členy, číst zprávy.",
|
||||
"title": "Nelze nastavit místnost jako soukromou"
|
||||
},
|
||||
"enable_encryption_confirm_description": "Po zapnutí již nelze šifrování v této místnosti vypnout. Zprávy v šifrovaných místnostech mohou číst jen členové místnosti, server se k obsahu nedostane. Šifrování místností nepodporuje většina botů a propojení. <a>Více informací o šifrování.</a>",
|
||||
"enable_encryption_confirm_title": "Povolit šifrování?",
|
||||
"enable_encryption_public_room_confirm_description_1": "<b>Nedoporučuje se šifrovat veřejné místnosti.</b>Veřejné místnosti může najít a připojit se k nim kdokoli, takže si v nich může číst zprávy kdokoli. Nezískáte tak žádnou z výhod šifrování a nebudete ho moci později vypnout. Šifrování zpráv ve veřejné místnosti zpomalí příjem a odesílání zpráv.",
|
||||
@@ -2382,7 +2410,7 @@
|
||||
"history_visibility_joined": "Pouze členové (od chvíle jejich vstupu)",
|
||||
"history_visibility_legend": "Kdo může číst historii?",
|
||||
"history_visibility_shared": "Pouze členové (od chvíle vybrání této volby)",
|
||||
"history_visibility_warning": "Změny viditelnosti historie této místnosti ovlivní jenom nové zprávy. Viditelnost starších zpráv zůstane, jaká byla v době jejich odeslání.",
|
||||
"history_visibility_warning": "Viditelnost stávající historie se nezmění.",
|
||||
"history_visibility_world_readable": "Kdokoliv",
|
||||
"join_rule_description": "Rozhodněte, kdo se může připojit k místnosti %(roomName)s.",
|
||||
"join_rule_invite": "Soukromý (pouze pro pozvané)",
|
||||
@@ -2425,6 +2453,7 @@
|
||||
"other": "Aktualizace prostorů... (%(progress)s z %(count)s)"
|
||||
},
|
||||
"join_rule_upgrade_upgrading_room": "Aktualizace místnosti",
|
||||
"join_rule_world_readable_description": "Změna toho, kdo může vstoupit do místnosti, změní také viditelnost budoucích zpráv.",
|
||||
"public_without_alias_warning": "Přidejte prosím místnosti adresu aby na ní šlo odkazovat.",
|
||||
"publish_room": "Zviditelněte tuto místnost ve veřejném adresáři místností.",
|
||||
"publish_space": "Zviditelněte tento prostor ve veřejném adresáři místností.",
|
||||
@@ -2563,6 +2592,7 @@
|
||||
"breadcrumb_second_description": "Ztratíte veškerou historii zpráv, která je uložena pouze na serveru",
|
||||
"breadcrumb_third_description": "Budete muset znovu ověřit všechna svá stávající zařízení a kontakty",
|
||||
"breadcrumb_title": "Opravdu chcete obnovit svou identitu?",
|
||||
"breadcrumb_title_cant_confirm": "Musíte resetovat svou totožnost",
|
||||
"breadcrumb_title_forgot": "Zapomněli jste klíč pro obnovení? Budete muset obnovit svou identitu.",
|
||||
"breadcrumb_title_sync_failed": "Synchronizace úložiště klíčů se nezdařila. Musíte obnovit svou identitu.",
|
||||
"breadcrumb_warning": "Udělejte to pouze v případě, že se domníváte, že váš účet byl napaden.",
|
||||
@@ -3964,6 +3994,7 @@
|
||||
"connection_lost": "Došlo ke ztrátě připojení k serveru",
|
||||
"connection_lost_description": "Bez připojení k serveru nelze uskutečňovat hovory.",
|
||||
"consulting": "Konzultace s %(transferTarget)s. <a>Převod na %(transferee)s</a>",
|
||||
"decline_call": "Odmítnout",
|
||||
"default_device": "Výchozí zařízení",
|
||||
"dial": "Vytočit",
|
||||
"dialpad": "Číselník",
|
||||
@@ -4015,6 +4046,7 @@
|
||||
"show_sidebar_button": "Zobrazit postranní panel",
|
||||
"silence": "Ztlumit zvonění",
|
||||
"silenced": "Oznámení ztlumena",
|
||||
"skip_lobby_toggle_option": "Připojte se ihned",
|
||||
"start_screenshare": "Začít sdílet obrazovku",
|
||||
"stop_screenshare": "Ukončit sdílení obrazovky",
|
||||
"too_many_calls": "Přiliš mnoho hovorů",
|
||||
|
||||
@@ -644,7 +644,7 @@
|
||||
"mode_plain": "Formatierung ausblenden",
|
||||
"mode_rich_text": "Formatierung anzeigen",
|
||||
"no_perms_notice": "Du darfst in diesem Chat nichts schreiben",
|
||||
"placeholder": "Eine unverschlüsselte Nachricht senden...",
|
||||
"placeholder": "Unverschlüsselte Nachricht senden...",
|
||||
"placeholder_encrypted": "Nachricht senden …",
|
||||
"placeholder_reply": "Eine unverschlüsselte Antwort senden…",
|
||||
"placeholder_reply_encrypted": "Antwort senden…",
|
||||
@@ -718,6 +718,7 @@
|
||||
"personal_space_description": "Ein privater Space zum Organisieren deiner Chats",
|
||||
"private_description": "Nur für Eingeladene – optimal für dich selbst oder Teams",
|
||||
"private_heading": "Dein privater Space",
|
||||
"private_only_heading": "Dein Space",
|
||||
"private_personal_description": "Stelle sicher, dass die richtigen Personen Zugriff auf %(name)s haben",
|
||||
"private_personal_heading": "Für wen ist dieser Space gedacht?",
|
||||
"private_space": "Für mich und meine Kollegen",
|
||||
@@ -968,7 +969,6 @@
|
||||
"title": "Wiederherstellungsmethode gelöscht",
|
||||
"warning": "Wenn du die Wiederherstellungsmethode nicht gelöscht hast, kann ein Angreifer versuchen, Zugang zu deinem Konto zu bekommen. Ändere dein Passwort und richte sofort eine neue Wiederherstellungsmethode in den Einstellungen ein."
|
||||
},
|
||||
"reset_all_button": "Hast du alle Wiederherstellungsmethoden vergessen? <a>Setze sie hier zurück</a>",
|
||||
"set_up_recovery": "Wiederherstellung einrichten",
|
||||
"set_up_recovery_toast_description": "Erzeuge einen Wiederherstellungsschlüssel. Er wird verwendet, um den verschlüsselten Nachrichtenverlauf wiederherzustellen, falls du den Zugriff auf deine Geräte verlierst.",
|
||||
"set_up_toast_title": "Schlüsselsicherung einrichten",
|
||||
@@ -991,7 +991,6 @@
|
||||
"after_new_login": {
|
||||
"device_verified": "Gerät verifiziert",
|
||||
"skip_verification": "Verifizierung vorläufig überspringen",
|
||||
"unable_to_verify": "Gerät konnte nicht verifiziert werden",
|
||||
"verify_this_device": "Dieses Gerät verifizieren"
|
||||
},
|
||||
"cancelled": "Du hast die Verifikation abgebrochen.",
|
||||
@@ -1026,7 +1025,6 @@
|
||||
"text": "Gib die Geräte-ID und den Fingerabdruck eines deiner eigenen Geräte zur Verifizierung ein. HINWEIS: Dadurch kann das andere Gerät Nachrichten in deinem Namen senden und empfangen. WENN DICH JEMAND AUFGEFORDERT HAT, HIER ETWAS EINZUFÜGEN, WIRST DU WAHRSCHEINLICH BETROGEN!",
|
||||
"wrong_fingerprint": "Das Gerät „%(deviceId)s” kann nicht verifiziert werden – der angegebene Fingerabdruck „%(fingerprint)s” stimmt nicht mit dem Fingerabdruck des Geräts „%(fprint)s” überein"
|
||||
},
|
||||
"no_key_or_device": "Es sieht so aus, als hättest du weder einen Wiederherstellungsschlüssel noch andere Geräte, mit denen du dich verifizieren kannst. Dieses Gerät kann daher nicht auf den verschlüsselten Nachrichtenverlauf zugreifen. Um deine Identität auf diesem Gerät zu bestätigen, muss deine kryptografische Identität zurückgesetzt werden.",
|
||||
"no_support_qr_emoji": "Das Gerät unterstützt weder Verifizieren mittels QR-Code noch Emoji-Verifizierung. %(brand)s benötigt dies jedoch. Bitte verwende eine andere Anwendung.",
|
||||
"other_party_cancelled": "Die Gegenstelle hat die Überprüfung abgebrochen.",
|
||||
"prompt_encrypted": "Verifiziere alle Nutzer in einem Chat um vollständige Sicherheit zu gewährleisten.",
|
||||
@@ -1042,7 +1040,6 @@
|
||||
"request_toast_accept_user": "Benutzer verifizieren",
|
||||
"request_toast_decline_counter": "Ignoriere (%(counter)s)",
|
||||
"request_toast_detail": "%(deviceId)s von %(ip)s",
|
||||
"reset_proceed_prompt": "Mit Zurücksetzen fortfahren",
|
||||
"sas_caption_self": "Verifiziere dieses Gerät, indem du überprüfst, dass die folgende Zahl auf dem Bildschirm erscheint.",
|
||||
"sas_caption_user": "Verifiziere diesen Nutzer, indem du bestätigst, dass die folgende Nummer auf dessen Bildschirm erscheint.",
|
||||
"sas_description": "Vergleiche eine einmalige Reihe von Emojis, sofern du an keinem Gerät eine Kamera hast",
|
||||
@@ -1065,7 +1062,6 @@
|
||||
"unverified_sessions_toast_description": "Überprüfe, um dein Konto sicher zu halten",
|
||||
"unverified_sessions_toast_reject": "Später",
|
||||
"unverified_sessions_toast_title": "Du hast nicht verifizierte Sitzungen",
|
||||
"verification_description": "Verifiziere deine Identität, um auf verschlüsselte Nachrichten zuzugreifen und dich gegenüber anderen Nutzern auszuweisen. Solltest du auch ein mobiles Gerät verwenden, öffne dort die App bevor du fortfährst.",
|
||||
"verification_dialog_title_device": "Anderes Gerät verifizieren",
|
||||
"verification_dialog_title_user": "Verifizierungsanfrage",
|
||||
"verification_skip_warning": "Ohne dich zu verifizieren wirst du keinen Zugriff auf alle deine Nachrichten haben und könntest für andere als nicht vertrauenswürdig erscheinen.",
|
||||
@@ -1075,9 +1071,6 @@
|
||||
"verify_emoji_prompt": "Durch den Vergleich einzigartiger Emojis verifizieren.",
|
||||
"verify_emoji_prompt_qr": "Wenn du obigen Code nicht erfassen kannst, verifiziere stattdessen durch den Vergleich von Emojis.",
|
||||
"verify_later": "Später verifizieren",
|
||||
"verify_using_device": "Mit anderem Gerät verifizieren",
|
||||
"verify_using_key": "Mit Wiederherstellungsschlüssel verifizieren",
|
||||
"verify_using_key_or_phrase": "Mit Wiederherstellungsschlüssel oder Wiederherstellungsphrase verifizieren",
|
||||
"waiting_for_user_accept": "Warte auf die Annahme von %(displayName)s …",
|
||||
"waiting_other_device": "Warten darauf, dass du das auf deinem anderen Gerät bestätigst…",
|
||||
"waiting_other_device_details": "Warten, dass du auf deinem anderen Gerät %(deviceName)s (%(deviceId)s) verifizierst…",
|
||||
@@ -1127,6 +1120,7 @@
|
||||
"tls": "Verbindung zum Heim-Server fehlgeschlagen – bitte überprüfe die Internetverbindung und stelle sicher, dass dem <a>SSL-Zertifikat deines Heimservers</a> vertraut wird und dass Anfragen nicht durch eine Browser-Erweiterung blockiert werden.",
|
||||
"unknown": "Unbekannter Fehler",
|
||||
"unknown_error_code": "Unbekannter Fehlercode",
|
||||
"update_history_visibility": "Die Sichtbarkeit des Nachrichtenverlaufs konnte nicht geändert werden",
|
||||
"update_power_level": "Ändern der Berechtigungsstufe fehlgeschlagen"
|
||||
},
|
||||
"error_app_open_in_another_tab": "Wechsle zu einem anderen Tab um mit %(brand)s zu verbinden. Dieser Tab kann jetzt geschlossen werden.",
|
||||
@@ -2383,6 +2377,10 @@
|
||||
"users_default": "Standard-Rolle"
|
||||
},
|
||||
"security": {
|
||||
"cannot_change_to_private_due_to_missing_history_visiblity_permissions": {
|
||||
"description": "Du hast keine Berechtigung, die Sichtbarkeit des Nachrichtenverlaufs des Chats zu ändern. Das ist gefährlich, weil es nicht angemeldeten Nutzern ermöglichen könnte, Nachrichten zu lesen.",
|
||||
"title": "Chat kann nicht \"privat\" gemacht werden"
|
||||
},
|
||||
"enable_encryption_confirm_description": "Die Verschlüsselung für einen Chat kann nicht mehr deaktiviert werden sobald sie aktiv ist. Nachrichten in einem verschlüsselten Chat können nur noch von Mitgliedern, aber nicht mehr vom Server gelesen werden. Einige Bots und Brücken werden vielleicht nicht mehr funktionieren. <a>Erfahre mehr über Verschlüsselung.</a>",
|
||||
"enable_encryption_confirm_title": "Verschlüsselung aktivieren?",
|
||||
"enable_encryption_public_room_confirm_description_1": "<b>Verschlüsselung wird für öffentliche Chats nicht empfohlen.</b> Jeder kann öffentliche Chats finden und ihnen beitreten, also kann auch jeder die Nachrichten lesen. Die Verschlüsselung bringt keine Vorteile und lässt sich später auch nicht mehr deaktivieren. Außerdem verlangsamt die Verschlüsselung das Senden und Empfangen von Nachrichten in öffentlichen Chats.",
|
||||
@@ -2400,7 +2398,7 @@
|
||||
"history_visibility_joined": "Mitglieder (ab Betreten)",
|
||||
"history_visibility_legend": "Wer kann den bisherigen Verlauf lesen?",
|
||||
"history_visibility_shared": "Mitglieder",
|
||||
"history_visibility_warning": "Änderungen an der Sichtbarkeit des Verlaufs gelten nur für zukünftige Nachrichten. Die Sichtbarkeit des existierenden Verlaufs bleibt unverändert.",
|
||||
"history_visibility_warning": "Die Sichtbarkeit des existierenden Nachrichtenverlaufs bleibt unverändert.",
|
||||
"history_visibility_world_readable": "Alle",
|
||||
"join_rule_description": "Entscheide, wer %(roomName)s betreten kann.",
|
||||
"join_rule_invite": "Privat (Betreten mit Einladung)",
|
||||
@@ -2443,6 +2441,7 @@
|
||||
"other": "Spaces aktualisieren … (%(progress)s von %(count)s)"
|
||||
},
|
||||
"join_rule_upgrade_upgrading_room": "Chat-Version wird aktualisiert",
|
||||
"join_rule_world_readable_description": "Wenn du änderst, wer dem Chat beitreten darf, ändert sich auch, wer zukünftige Nachrichten sehen kann.",
|
||||
"public_without_alias_warning": "Um den Chat zu verlinken, füge bitte eine Adresse hinzu.",
|
||||
"publish_room": "Veröffentliche diesen Chat im Chat Verzeichnis.",
|
||||
"publish_space": "Veröffentliche diesen Space im Chat Verzeichnis.",
|
||||
|
||||
@@ -811,7 +811,6 @@
|
||||
"title": "Η Μέθοδος Ανάκτησης Καταργήθηκε",
|
||||
"warning": "Εάν δεν καταργήσατε τη μέθοδο ανάκτησης, ένας εισβολέας μπορεί να προσπαθεί να αποκτήσει πρόσβαση στον λογαριασμό σας. Αλλάξτε τον κωδικό πρόσβασης του λογαριασμού σας και ορίστε μια νέα μέθοδο ανάκτησης αμέσως στις Ρυθμίσεις."
|
||||
},
|
||||
"reset_all_button": "Ξεχάσατε ή χάσατε όλες τις μεθόδους ανάκτησης; <a>Επαναφορά όλων</a>",
|
||||
"set_up_toast_title": "Ρυθμίστε το αντίγραφο ασφαλείας",
|
||||
"setup_secure_backup": {
|
||||
"explainer": "Δημιουργήστε αντίγραφα ασφαλείας των κλειδιών σας πριν αποσυνδεθείτε για να μην τα χάσετε."
|
||||
@@ -830,7 +829,6 @@
|
||||
"after_new_login": {
|
||||
"device_verified": "Η συσκευή επαληθεύτηκε",
|
||||
"skip_verification": "Παράβλεψη επαλήθευσης προς το παρόν",
|
||||
"unable_to_verify": "Αδυναμία επαλήθευσης αυτής της συσκευής",
|
||||
"verify_this_device": "Επαληθεύστε αυτήν τη συσκευή"
|
||||
},
|
||||
"cancelled": "Ακυρώσατε την επαλήθευση.",
|
||||
@@ -850,7 +848,6 @@
|
||||
"incoming_sas_dialog_waiting": "Αναμονή επιβεβαίωσης από τον συνεργάτη…",
|
||||
"incoming_sas_user_dialog_text_1": "Επαληθεύστε αυτόν τον χρήστη για να τον επισημάνετε ως αξιόπιστο. Η εμπιστοσύνη των χρηστών σάς προσφέρει επιπλέον ηρεμία όταν χρησιμοποιείτε μηνύματα με κρυπτογράφηση από άκρο σε άκρο.",
|
||||
"incoming_sas_user_dialog_text_2": "Η επαλήθευση αυτού του χρήστη θα επισημάνει τη συνεδρία του ως αξιόπιστη και θα επισημάνει επίσης τη συνεδρία σας ως αξιόπιστη σε αυτόν.",
|
||||
"no_key_or_device": "Φαίνεται ότι δεν έχετε Κλειδί Ανάκτησης ή άλλες συσκευές με τις οποίες μπορείτε να κάνετε επαλήθευση. Αυτή η συσκευή δεν θα έχει πρόσβαση σε παλιά κρυπτογραφημένα μηνύματα. Για να επαληθεύσετε την ταυτότητά σας σε αυτήν τη συσκευή, θα πρέπει να επαναφέρετε τα κλειδιά επαλήθευσης.",
|
||||
"no_support_qr_emoji": "Η συσκευή που προσπαθείτε να επαληθεύσετε δεν υποστηρίζει τη σάρωση κωδικού QR ή επαλήθευσης emoji, κάτι που υποστηρίζει το %(brand)s. Δοκιμάστε με διαφορετικό πρόγραμμα-πελάτη.",
|
||||
"other_party_cancelled": "Το άλλο μέρος ακύρωσε την επαλήθευση.",
|
||||
"prompt_encrypted": "Επαληθεύστε όλους τους χρήστες σε ένα δωμάτιο για να βεβαιωθείτε ότι είναι ασφαλές.",
|
||||
@@ -865,7 +862,6 @@
|
||||
"request_toast_accept": "Επαλήθευση Συνεδρίας",
|
||||
"request_toast_decline_counter": "Παράβλεψη (%(counter)s)",
|
||||
"request_toast_detail": "%(deviceId)s από %(ip)s",
|
||||
"reset_proceed_prompt": "Προχωρήστε με την επαναφορά",
|
||||
"sas_caption_self": "Επαληθεύστε αυτήν τη συσκευή επιβεβαιώνοντας ότι ο ακόλουθος αριθμός εμφανίζεται στην οθόνη της.",
|
||||
"sas_caption_user": "Επαληθεύστε αυτόν τον χρήστη επιβεβαιώνοντας ότι ο ακόλουθος αριθμός εμφανίζεται στην οθόνη του.",
|
||||
"sas_description": "Συγκρίνετε ένα μοναδικό σύνολο emoji εάν δεν έχετε κάμερα σε καμία από τις δύο συσκευές",
|
||||
@@ -888,7 +884,6 @@
|
||||
"unverified_sessions_toast_description": "Ελέγξτε για να βεβαιωθείτε ότι ο λογαριασμός σας είναι ασφαλής",
|
||||
"unverified_sessions_toast_reject": "Αργότερα",
|
||||
"unverified_sessions_toast_title": "Έχετε μη επαληθευμένες συνεδρίες",
|
||||
"verification_description": "Επαληθεύστε την ταυτότητά σας για να αποκτήσετε πρόσβαση σε κρυπτογραφημένα μηνύματα και να αποδείξετε την ταυτότητά σας σε άλλους. Εάν χρησιμοποιείτε επίσης κινητή συσκευή, ανοίξτε την εφαρμογή εκεί πριν προχωρήσετε.",
|
||||
"verification_dialog_title_device": "Επαλήθευση άλλης συσκευής",
|
||||
"verification_dialog_title_user": "Αίτημα επαλήθευσης",
|
||||
"verification_skip_warning": "Χωρίς επαλήθευση, δε θα έχετε πρόσβαση σε όλα τα μηνύματά σας και ενδέχεται να φαίνεστε ως αναξιόπιστος στους άλλους.",
|
||||
@@ -898,9 +893,6 @@
|
||||
"verify_emoji_prompt": "Επαληθεύστε συγκρίνοντας μοναδικά emoji.",
|
||||
"verify_emoji_prompt_qr": "Εάν δεν μπορείτε να σαρώσετε τον παραπάνω κώδικα, επαληθεύστε το συγκρίνοντας μοναδικά emoji.",
|
||||
"verify_later": "Θα επαληθεύσω αργότερα",
|
||||
"verify_using_device": "Επαλήθευση με άλλη συσκευή",
|
||||
"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)…",
|
||||
|
||||
@@ -598,7 +598,6 @@
|
||||
"user": "User",
|
||||
"user_avatar": "Profile picture",
|
||||
"username": "Username",
|
||||
"verification_cancelled": "Verification cancelled",
|
||||
"verified": "Verified",
|
||||
"version": "Version",
|
||||
"video": "Video",
|
||||
@@ -993,9 +992,7 @@
|
||||
"skip_verification": "Skip verification for now",
|
||||
"verify_this_device": "Verify this device"
|
||||
},
|
||||
"cancelled": "You cancelled verification.",
|
||||
"cancelled_self": "You cancelled verification on your other device.",
|
||||
"cancelled_user": "%(displayName)s cancelled verification.",
|
||||
"cancelled_verification": "Either the request timed out, the request was denied, or there was a verification mismatch.",
|
||||
"cancelling": "Cancelling…",
|
||||
"cant_confirm": "Can't confirm?",
|
||||
"complete_action": "Got It",
|
||||
@@ -1003,6 +1000,7 @@
|
||||
"complete_title": "Verified!",
|
||||
"confirm_identity_description": "Verify this device to set up secure messaging",
|
||||
"confirm_identity_title": "Confirm your identity",
|
||||
"confirm_the_emojis": "Confirm that the emojis below match those shown on your other device.",
|
||||
"error_starting_description": "We were unable to start a chat with the other user.",
|
||||
"error_starting_title": "Error starting verification",
|
||||
"explainer": "Secure messages with this user are end-to-end encrypted and not able to be read by third parties.",
|
||||
@@ -1029,36 +1027,32 @@
|
||||
"wrong_fingerprint": "Unable to verify device '%(deviceId)s' - the supplied fingerprint '%(fingerprint)s' does not match the device fingerprint, '%(fprint)s'"
|
||||
},
|
||||
"no_support_qr_emoji": "The device you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.",
|
||||
"now_you_can": "Now you can read or send messages securely, and anyone you chat with can also trust this device.",
|
||||
"once_accepted_can_continue": "Once accepted you'll be able to continue with the verification.",
|
||||
"other_party_cancelled": "The other party cancelled the verification.",
|
||||
"prompt_encrypted": "Verify all users in a room to ensure it's secure.",
|
||||
"prompt_self": "Start verification again from the notification.",
|
||||
"prompt_unencrypted": "In encrypted rooms, verify all users to ensure it's secure.",
|
||||
"prompt_user": "Start verification again from their profile.",
|
||||
"qr_or_sas": "%(qrCode)s or %(emojiCompare)s",
|
||||
"qr_or_sas_header": "Verify this device by completing one of the following:",
|
||||
"qr_prompt": "Scan this unique code",
|
||||
"qr_reciprocate_same_shield_device": "Almost there! Is your other device showing the same shield?",
|
||||
"qr_reciprocate_check_again_device": "Check again on your other device to finish verification.",
|
||||
"qr_reciprocate_no": "No, I don't see a green shield",
|
||||
"qr_reciprocate_same_shield_user": "Almost there! Is %(displayName)s showing the same shield?",
|
||||
"request_toast_accept": "Verify Session",
|
||||
"qr_reciprocate_yes": "Yes, I see a green shield",
|
||||
"request_toast_accept_user": "Verify User",
|
||||
"request_toast_decline_counter": "Ignore (%(counter)s)",
|
||||
"request_toast_detail": "%(deviceId)s from %(ip)s",
|
||||
"request_toast_start_verification": "Start Verification",
|
||||
"sas_caption_self": "Verify this device by confirming the following number appears on its screen.",
|
||||
"sas_caption_user": "Verify this user by confirming the following number appears on their screen.",
|
||||
"sas_description": "Compare a unique set of emoji if you don't have a camera on either device",
|
||||
"sas_emoji_caption_self": "Confirm the emoji below are displayed on both devices, in the same order:",
|
||||
"sas_emoji_caption_user": "Verify this user by confirming the following emoji appear on their screen.",
|
||||
"sas_match": "They match",
|
||||
"sas_no_match": "They don't match",
|
||||
"sas_prompt": "Compare unique emoji",
|
||||
"scan_qr": "Verify by scanning",
|
||||
"scan_qr_explainer": "Ask %(displayName)s to scan your code:",
|
||||
"self_verification_hint": "To proceed, please accept the verification request on your other device.",
|
||||
"start_button": "Start Verification",
|
||||
"successful_device": "You've successfully verified %(deviceName)s (%(deviceId)s)!",
|
||||
"successful_own_device": "You've successfully verified your device!",
|
||||
"successful_user": "You've successfully verified %(displayName)s!",
|
||||
"timed_out": "Verification timed out.",
|
||||
"unsupported_method": "Unable to find a supported verification method.",
|
||||
"unverified_session_toast_accept": "Yes, it was me",
|
||||
"unverified_session_toast_title": "New login. Was this you?",
|
||||
@@ -1067,11 +1061,18 @@
|
||||
"unverified_sessions_toast_title": "You have unverified sessions",
|
||||
"use_another_device": "Use another device",
|
||||
"use_recovery_key": "Use recovery key",
|
||||
"verification_dialog_title_choose": "Choose how to verify",
|
||||
"verification_dialog_title_compare_emojis": "Compare emojis",
|
||||
"verification_dialog_title_confirm_green_shield": "Confirm that you see a green shield on your other device",
|
||||
"verification_dialog_title_device": "Verify other device",
|
||||
"verification_dialog_title_failed": "Verification failed",
|
||||
"verification_dialog_title_start_on_other_device": "Start verification on the other device",
|
||||
"verification_dialog_title_user": "Verification Request",
|
||||
"verification_dialog_title_verified": "Device verified",
|
||||
"verification_skip_warning": "Without verifying, you won't have access to all your messages and may appear as untrusted to others.",
|
||||
"verification_success_with_backup": "Your new device is now verified. It has access to your encrypted messages, and other users will see it as trusted.",
|
||||
"verification_success_without_backup": "Your new device is now verified. Other users will see it as trusted.",
|
||||
"verify_by_completing_one_of": "Verify by completing one of the following:",
|
||||
"verify_emoji": "Verify by emoji",
|
||||
"verify_emoji_prompt": "Verify by comparing unique emoji.",
|
||||
"verify_emoji_prompt_qr": "If you can't scan the code above, verify by comparing unique emoji.",
|
||||
@@ -2859,6 +2860,10 @@
|
||||
"rule_suppress_notices": "Messages sent by bot",
|
||||
"rule_tombstone": "When rooms are upgraded",
|
||||
"show_message_desktop_notification": "Show message in desktop notification",
|
||||
"sounds_release_announcement": {
|
||||
"description": "Your notification ping and call ringer have been updated—clearer, quicker, and less disruptive",
|
||||
"title": "We’ve refreshed your sounds"
|
||||
},
|
||||
"voip": "Audio and Video calls"
|
||||
},
|
||||
"preferences": {
|
||||
|
||||
@@ -633,7 +633,6 @@
|
||||
"title": "Rehava metodo foriĝis",
|
||||
"warning": "Se vi ne forigis la rehavan metodon, eble atakanto provas aliri vian konton. Vi tuj ŝanĝu la pasvorton de via konto, kaj agordu novan rehavan metodon en la agordoj."
|
||||
},
|
||||
"reset_all_button": "Ĉu vi forgesis aŭ perdis ĉiujn manierojn de rehavo? <a>Restarigu ĉion</a>",
|
||||
"set_up_toast_title": "Agordi Sekuran savkopiadon",
|
||||
"setup_secure_backup": {
|
||||
"explainer": "Savkopiu viajn ŝlosilojn antaŭ adiaŭo, por ilin ne perdi."
|
||||
@@ -668,7 +667,6 @@
|
||||
"qr_prompt": "Skanu ĉi tiun unikan kodon",
|
||||
"qr_reciprocate_same_shield_user": "Preskaŭ finite! Ĉu %(displayName)s montras la saman ŝildon?",
|
||||
"request_toast_detail": "%(deviceId)s de %(ip)s",
|
||||
"reset_proceed_prompt": "Procedu por restarigi",
|
||||
"sas_caption_user": "Kontrolu ĉu tiun uzanton per konfirmo, ke la jena numero aperis sur ĝia ekrano.",
|
||||
"sas_description": "Komparu unikan aron de bildsignoj se vi ne havas kameraon sur la alia aparato",
|
||||
"sas_emoji_caption_user": "Kontrolu ĉi tiun uzanton per konfirmo, ke la jenaj bildsignoj aperis sur ĝia ekrano.",
|
||||
@@ -688,7 +686,6 @@
|
||||
"unverified_sessions_toast_description": "Kontrolu por certigi sekurecon de via konto",
|
||||
"unverified_sessions_toast_reject": "Pli poste",
|
||||
"unverified_sessions_toast_title": "Vi havas nekontrolitajn salutaĵojn",
|
||||
"verification_description": "Kontrolu vian identecon por aliri ĉifritajn mesaĝojn kaj pruvi vian identecon al aliuloj.",
|
||||
"verification_dialog_title_user": "Kontrolpeto",
|
||||
"verification_skip_warning": "Sen kontrolado, vi ne havos aliron al ĉiuj viaj mesaĝoj kaj povas aperi kiel nefidinda al aliaj.",
|
||||
"verification_success_with_backup": "Via nova aparato nun estas kontrolita. Ĝi havas aliron al viaj ĉifritaj mesaĝoj, kaj aliaj vidos ĝin kiel fidinda.",
|
||||
@@ -697,9 +694,6 @@
|
||||
"verify_emoji_prompt": "Kontrolu per komparo de unikaj bildsignoj.",
|
||||
"verify_emoji_prompt_qr": "Se vi ne povas skani la supran kodon, kontrolu per komparo de unikaj bildsignoj.",
|
||||
"verify_later": "Kontrolu poste",
|
||||
"verify_using_device": "Kontrolu per alia aparato",
|
||||
"verify_using_key": "Kontrolu per Sekureca ŝlosilo",
|
||||
"verify_using_key_or_phrase": "Kontrolu per Sekureca ŝlosilo aŭ frazo",
|
||||
"waiting_for_user_accept": "Atendante akcepton de %(displayName)s…",
|
||||
"waiting_other_user": "Atendas kontrolon de %(displayName)s…"
|
||||
},
|
||||
|
||||
@@ -780,7 +780,6 @@
|
||||
"title": "Método de recuperación eliminado",
|
||||
"warning": "Si no eliminó el método de recuperación, es posible que un atacante esté intentando acceder a su cuenta. Cambie la contraseña de su cuenta y configure un nuevo método de recuperación inmediatamente en Configuración."
|
||||
},
|
||||
"reset_all_button": "¿Has olvidado o perdido todos los métodos de recuperación? <a>Restablecer todo</a>",
|
||||
"set_up_toast_title": "Configurar copia de seguridad segura",
|
||||
"setup_secure_backup": {
|
||||
"explainer": "Haz copia de seguridad de tus claves antes de cerrar sesión para evitar perderlas."
|
||||
@@ -799,7 +798,6 @@
|
||||
"after_new_login": {
|
||||
"device_verified": "Dispositivo verificado",
|
||||
"skip_verification": "Saltar la verificación por ahora",
|
||||
"unable_to_verify": "No se ha podido verificar el dispositivo",
|
||||
"verify_this_device": "Verificar este dispositivo"
|
||||
},
|
||||
"cancelled": "Has cancelado la verificación.",
|
||||
@@ -819,7 +817,6 @@
|
||||
"incoming_sas_dialog_waiting": "Esperando a que la otra persona confirme…",
|
||||
"incoming_sas_user_dialog_text_1": "Verifica a este usuario para marcarlo como de confianza. Confiar en usuarios aporta tranquilidad en los mensajes cifrados de extremo a extremo.",
|
||||
"incoming_sas_user_dialog_text_2": "Verificar este usuario marcará su sesión como de confianza, y también marcará tu sesión como de confianza para él.",
|
||||
"no_key_or_device": "Parece que no tienes una clave de seguridad u otros dispositivos para la verificación. Este dispositivo no podrá acceder los mensajes cifrados antiguos. Para verificar tu identidad en este dispositivo, tendrás que restablecer tus claves de verificación.",
|
||||
"no_support_qr_emoji": "El dispositivo que estás intentando verificar no es compatible con el escaneo de códigos QR o la verificación con emojis, que son las opciones que %(brand)s ofrece. Prueba con otra aplicación distinta.",
|
||||
"other_party_cancelled": "El otro lado canceló la verificación.",
|
||||
"prompt_encrypted": "Verifica a todos los usuarios de una sala para asegurar que es segura.",
|
||||
@@ -834,7 +831,6 @@
|
||||
"request_toast_accept": "Verificar sesión",
|
||||
"request_toast_decline_counter": "Ignorar (%(counter)s)",
|
||||
"request_toast_detail": "%(deviceId)s desde %(ip)s",
|
||||
"reset_proceed_prompt": "Continuar y restablecer",
|
||||
"sas_caption_self": "Verifica este dispositivo confirmando que el siguiente número aparece en pantalla.",
|
||||
"sas_caption_user": "Verifica a este usuario confirmando que este número aparece en su pantalla.",
|
||||
"sas_description": "Compara un conjunto de emojis si no tienes cámara en ninguno de los dispositivos",
|
||||
@@ -857,7 +853,6 @@
|
||||
"unverified_sessions_toast_description": "Revisa que tu cuenta esté segura",
|
||||
"unverified_sessions_toast_reject": "Más tarde",
|
||||
"unverified_sessions_toast_title": "Tienes sesiones sin verificar",
|
||||
"verification_description": "Verifica tu identidad para leer tus mensajes cifrados y probar a las demás personas que realmente eres tú.",
|
||||
"verification_dialog_title_device": "Verificar otro dispositivo",
|
||||
"verification_dialog_title_user": "Solicitud de verificación",
|
||||
"verification_skip_warning": "Si decides no verificar, no tendrás acceso a todos tus mensajes y puede que le aparezcas a los demás como «no confiado».",
|
||||
@@ -867,9 +862,6 @@
|
||||
"verify_emoji_prompt": "Verifica comparando emoji únicos.",
|
||||
"verify_emoji_prompt_qr": "Si no puedes escanear el código de arriba, verifica comparando emoji únicos.",
|
||||
"verify_later": "La verificaré en otro momento",
|
||||
"verify_using_device": "Verificar con otro dispositivo",
|
||||
"verify_using_key": "Verificar con una clave de seguridad",
|
||||
"verify_using_key_or_phrase": "Verificar con una clave o frase de seguridad",
|
||||
"waiting_for_user_accept": "Esperando a que %(displayName)s acepte…",
|
||||
"waiting_other_device": "Esperando a que verifiques en tu otro dispositivo…",
|
||||
"waiting_other_device_details": "Esperando a que verifiques en tu otro dispositivo, %(deviceName)s (%(deviceId)s)…",
|
||||
|
||||
@@ -718,6 +718,7 @@
|
||||
"personal_space_description": "Privaatne kogukonnakeskus jututubade koondamiseks",
|
||||
"private_description": "Liitumine vaid kutse alusel, sobib sulle ja sinu lähematele kaaslastele",
|
||||
"private_heading": "Sinu privaatne kogukonnakeskus",
|
||||
"private_only_heading": "Sinu kogukond",
|
||||
"private_personal_description": "Palun kontrolli, et vajalikel inimestel oleks ligipääs siia - %(name)s",
|
||||
"private_personal_heading": "Kellega sa koos töötad?",
|
||||
"private_space": "Mina ja minu kaasteelised",
|
||||
@@ -968,7 +969,6 @@
|
||||
"title": "Taastemeetod on eemaldatud",
|
||||
"warning": "Kui sa ei ole ise taastamise meetodeid eemaldanud, siis võib olla tegemist ründega sinu konto vastu. Palun vaheta koheselt oma kasutajakonto salasõna ning määra seadistustes uus taastemeetod."
|
||||
},
|
||||
"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_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_title": "Võta kasutusele turvaline varundus",
|
||||
@@ -991,7 +991,6 @@
|
||||
"after_new_login": {
|
||||
"device_verified": "Seade on verifitseeritud",
|
||||
"skip_verification": "Jäta verifitseerimine praegu vahele",
|
||||
"unable_to_verify": "Selle seadme verifitseerimine ei õnnestunud",
|
||||
"verify_this_device": "Verifitseeri see seade"
|
||||
},
|
||||
"cancelled": "Sina tühistasid verifitseerimise.",
|
||||
@@ -1026,7 +1025,6 @@
|
||||
"text": "Verifitseerimiseks sisesta ühe oma seadme tunnus ja sõrmejälg. Palun arvesta, et see võimaldab muul seadmel saata ja vastu võtta sõnumeid esinedes sinuna. KUI KEEGI PALUS SUL SIIA MIDAGI KOPEERIDA, SIIS ON SEE KAHTLANE JA ILMSELT PROOVITAKSE SIND PETTA!",
|
||||
"wrong_fingerprint": "„%(deviceId)s“ seadme verifitseerimine ei õnnestunud - lisatud sõrmejälg „%(fingerprint)s“ ja seadme sõrmejälg „%(fprint)s“ pole samad"
|
||||
},
|
||||
"no_key_or_device": "Tundub, et sul ei ole ei taastevõtit ega muid seadmeid, mida saaksid verifitseerimiseks kasutada. Siin seadmes ei saa lugeda vanu krüptitud sõnumeid. Enda tuvastamiseks selles seadmes pead oma vanad verifitseerimisvõtmed kustutama.",
|
||||
"no_support_qr_emoji": "See seade, mida sa tahad verifitseerida ei toeta QR-koodi ega emoji-põhist verifitseerimist, aga just neid %(brand)s oskab kasutada. Proovi mõne muu Matrix'i kliendiga.",
|
||||
"other_party_cancelled": "Teine osapool tühistas verifitseerimise.",
|
||||
"prompt_encrypted": "Tagamaks, et jututuba on turvaline, verifitseeri kõik selle kasutajad.",
|
||||
@@ -1042,7 +1040,6 @@
|
||||
"request_toast_accept_user": "Verifitseeri kasutaja",
|
||||
"request_toast_decline_counter": "Eira (%(counter)s)",
|
||||
"request_toast_detail": "%(deviceId)s ip-aadressil %(ip)s",
|
||||
"reset_proceed_prompt": "Jätka kustutamisega",
|
||||
"sas_caption_self": "Verifitseeri see seade tehes kindlaks, et järgnev number kuvatakse tema ekraanil.",
|
||||
"sas_caption_user": "Verifitseeri see kasutaja tehes kindlaks, et järgnev number kuvatakse tema ekraanil.",
|
||||
"sas_description": "Kui sul mõlemas seadmes pole kaamerat, siis võrdle unikaalset emoji'de komplekti",
|
||||
@@ -1065,7 +1062,6 @@
|
||||
"unverified_sessions_toast_description": "Tagamaks, et su konto on sinu kontrolli all, vaata andmed üle",
|
||||
"unverified_sessions_toast_reject": "Hiljem",
|
||||
"unverified_sessions_toast_title": "Sul on verifitseerimata sessioone",
|
||||
"verification_description": "Tagamaks ligipääsu oma krüptitud sõnumitele ja tõestamaks oma isikut teistele kasutajatale, verifitseeri end. Kui kasutad mobiilirakendust, siis palun ava see enne jätkamist.",
|
||||
"verification_dialog_title_device": "Verifitseeri oma teine seade",
|
||||
"verification_dialog_title_user": "Verifitseerimispäring",
|
||||
"verification_skip_warning": "Ilma verifitseerimiseta sul puudub ligipääs kõikidele oma sõnumitele ning teised ei näe sinu kasutajakontot usaldusväärsena.",
|
||||
@@ -1075,9 +1071,6 @@
|
||||
"verify_emoji_prompt": "Verifitseeri unikaalsete emoji'de võrdlemise teel.",
|
||||
"verify_emoji_prompt_qr": "Kui sa ei saa skaneerida eespool kuvatud koodi, siis verifitseeri unikaalsete emoji'de võrdlemise teel.",
|
||||
"verify_later": "Ma verifitseerin hiljem",
|
||||
"verify_using_device": "Verifitseeri teise seadmega",
|
||||
"verify_using_key": "Verifitseeri taastevõtmega",
|
||||
"verify_using_key_or_phrase": "Verifitseeri taastevõtme või -fraasiga",
|
||||
"waiting_for_user_accept": "Ootan, et %(displayName)s nõustuks…",
|
||||
"waiting_other_device": "Ootan, et sa verifitseeriksid oma teises seadmes…",
|
||||
"waiting_other_device_details": "Ootan, et sa verifitseerid oma teises seadmes: %(deviceName)s (%(deviceId)s)…",
|
||||
@@ -1127,6 +1120,7 @@
|
||||
"tls": "Ei sa ühendust koduserveriga. Palun kontrolli, et sinu <a>koduserveri SSL sertifikaat</a> oleks usaldusväärne ning mõni brauseri lisamoodul ei blokeeri päringuid.",
|
||||
"unknown": "Teadmata viga",
|
||||
"unknown_error_code": "tundmatu veakood",
|
||||
"update_history_visibility": "Ei õnnestunud muuta ajaloo nähtavust",
|
||||
"update_power_level": "Õiguste muutmine ei õnnestunud"
|
||||
},
|
||||
"error_app_open_in_another_tab": "%(brand)s'i kasutamiseks ava teine vahekaart. Selle vahekaardi võid kinni panna.",
|
||||
@@ -2384,6 +2378,10 @@
|
||||
"users_default": "Vaikimisi roll"
|
||||
},
|
||||
"security": {
|
||||
"cannot_change_to_private_due_to_missing_history_visiblity_permissions": {
|
||||
"description": "Sul pole õigusi selle jututoa ajaloo nähtavuse muutmiseks. Kuna võib tekkida olukord, kus ka mitteliitunud kasutajad saavad lugeda sõnumeid, siis on selline tegevus ka ohtlik.",
|
||||
"title": "Ei õnnestu muuta jututuba privaatseks"
|
||||
},
|
||||
"enable_encryption_confirm_description": "Kui kord juba kasutusele võetud, siis krüptimist enam hiljem ära lõpetada ei saa. Krüptitud sõnumeid ei saa lugeda ei vaheapealses veebiliikluses ega serveris ja vaid jututoa liikmed saavad neid lugeda. Krüptimise kasutusele võtmine võib takistada nii robotite kui sõnumisildade tööd. <a>Lisateave krüptimise kohta.</a>",
|
||||
"enable_encryption_confirm_title": "Kas võtame krüptimise kasutusele?",
|
||||
"enable_encryption_public_room_confirm_description_1": "<b>Me ei soovita avalikes jututubades krüptimise kasutamist.</b> Kuna kõik huvilised saavad vabalt leida avalikke jututube ning nendega liituda, siis saavad nad niikuinii ka neis leiduvaid sõnumeid lugeda. Olemuselt puuduvad sellises olukorras krüptimise eelised ning sa ei saa hiljem krüptimist välja lülitada. Avalike jututubade sõnumite krüptimine teeb ka sõnumite saatmise ja vastuvõtmise aeglasemaks.",
|
||||
@@ -2401,7 +2399,7 @@
|
||||
"history_visibility_joined": "Ainult liikmetele (alates liitumisest)",
|
||||
"history_visibility_legend": "Kes võivad lugeda ajalugu?",
|
||||
"history_visibility_shared": "Ainult liikmetele (alates selle seadistuse kasutuselevõtmisest)",
|
||||
"history_visibility_warning": "Kui muudad seda, kes saavad selle jututoa ajalugu lugeda, siis kehtib see vaid tulevaste sõnumite kohta. Senise ajaloo nähtavus sellega ei muutu.",
|
||||
"history_visibility_warning": "Senise ajaloo nähtavus ei muutu.",
|
||||
"history_visibility_world_readable": "Kõik kasutajad",
|
||||
"join_rule_description": "Vali, kes saavad liituda %(roomName)s jututoaga.",
|
||||
"join_rule_invite": "Privaatne jututuba (eeldab kutset)",
|
||||
@@ -2444,6 +2442,7 @@
|
||||
"other": "Uuendan kogukonnakeskuseid... (%(progress)s / %(count)s)"
|
||||
},
|
||||
"join_rule_upgrade_upgrading_room": "Uuendan jututoa versiooni",
|
||||
"join_rule_world_readable_description": "Kui muudad seda, kes võib jututoaga liituda, siis muutub ka tulevaste sõnumite nähtavus.",
|
||||
"public_without_alias_warning": "Sellele jututoale viitamiseks palun lisa talle aadress.",
|
||||
"publish_room": "Tee see jututuba nähtavaks avalikus jututubade kataloogis.",
|
||||
"publish_space": "Tee see kogukond nähtavaks avalikus jututubade kataloogis.",
|
||||
|
||||
@@ -593,7 +593,6 @@
|
||||
"title": "روش بازیابی حذف شد",
|
||||
"warning": "اگر متد بازیابی را حذف نکردهاید، ممکن است حملهکنندهای سعی در دسترسی به حسابکاربری شما داشته باشد. گذرواژه حساب کاربری خود را تغییر داده و فورا یک روش بازیابی را از بخش تنظیمات خود تنظیم کنید."
|
||||
},
|
||||
"reset_all_button": "همه روشهای بازیابی را فراموش کرده یا از دست دادهاید؟ <a>بازراهاندازی (reset) همه</a>",
|
||||
"set_up_toast_title": "پشتیبانگیری امن را انجام دهید",
|
||||
"setup_secure_backup": {
|
||||
"explainer": "پیش از خروج از حساب کاربری، از کلیدهای خود پشتیبان بگیرید تا آنها را از دست ندهید."
|
||||
@@ -645,7 +644,6 @@
|
||||
"unverified_session_toast_title": "ورود جدید. آیا شما بودید؟",
|
||||
"unverified_sessions_toast_description": "برای کسب اطمینان از امنبودن حساب کاربری خود، لطفا بررسی فرمائید",
|
||||
"unverified_sessions_toast_reject": "بعداً",
|
||||
"verification_description": "با تائید هویت خود به پیامهای رمزشده دسترسی یافته و هویت خود را به دیگران ثابت میکنید.",
|
||||
"verification_dialog_title_user": "درخواست تأیید",
|
||||
"verify_emoji": "تأیید توسط شکلک",
|
||||
"verify_emoji_prompt": "با مقایسه شکلک تأیید کنید.",
|
||||
|
||||
@@ -826,7 +826,6 @@
|
||||
"title": "Palautustapa poistettu",
|
||||
"warning": "Jos et poistanut palautustapaa, hyökkääjä saattaa yrittää käyttää tiliäsi. Vaihda tilisi salasana ja aseta uusi palautustapa asetuksissa välittömästi."
|
||||
},
|
||||
"reset_all_button": "Unohtanut tai kadottanut kaikki palautustavat? <a>Nollaa kaikki</a>",
|
||||
"set_up_recovery": "Määritä palautus",
|
||||
"set_up_recovery_toast_description": "Luo palautusavain, jota voit käyttää salatun viestihistorian palauttamiseen, jos menetät pääsyn laitteisiisi.",
|
||||
"set_up_toast_title": "Määritä turvallinen varmuuskopio",
|
||||
@@ -846,7 +845,6 @@
|
||||
"after_new_login": {
|
||||
"device_verified": "Laite vahvistettu",
|
||||
"skip_verification": "Ohita vahvistus toistaiseksi",
|
||||
"unable_to_verify": "Tätä laitetta ei voitu vahvistaa",
|
||||
"verify_this_device": "Vahvista tämä laite"
|
||||
},
|
||||
"cancelled": "Peruutit varmennuksen.",
|
||||
@@ -905,9 +903,6 @@
|
||||
"verify_emoji_prompt": "Varmenna vertaamalla uniikkia emojia.",
|
||||
"verify_emoji_prompt_qr": "Jos et pysty skannaamaan yläpuolella olevaa koodia, varmenna vertaamalla emojia.",
|
||||
"verify_later": "Vahvistan myöhemmin",
|
||||
"verify_using_device": "Vahvista toisella laitteella",
|
||||
"verify_using_key": "Vahvista palautusavaimella",
|
||||
"verify_using_key_or_phrase": "Vahvista turva-avaimella tai turvalauseella",
|
||||
"waiting_for_user_accept": "Odotetaan, että %(displayName)s hyväksyy…",
|
||||
"waiting_other_device": "Odotetaan vahvistustasi toiselta laitteelta…",
|
||||
"waiting_other_device_details": "Odotetaan vahvistustasi toiselta laitteelta, %(deviceName)s (%(deviceId)s)…",
|
||||
|
||||
@@ -718,6 +718,7 @@
|
||||
"personal_space_description": "Un espace privé pour organiser vos salons",
|
||||
"private_description": "Sur invitation, idéal pour vous-même ou les équipes",
|
||||
"private_heading": "Votre espace privé",
|
||||
"private_only_heading": "Votre Espace",
|
||||
"private_personal_description": "Assurez-vous que les bonnes personnes aient accès à %(name)s",
|
||||
"private_personal_heading": "Avec qui travaillez-vous ?",
|
||||
"private_space": "Moi et mon équipe",
|
||||
@@ -968,7 +969,6 @@
|
||||
"title": "Méthode de récupération supprimée",
|
||||
"warning": "Si vous n’avez pas supprimé la méthode de récupération, un attaquant peut être en train d’essayer d’accéder à votre compte. Modifiez le mot de passe de votre compte et configurez une nouvelle méthode de récupération dans les réglages."
|
||||
},
|
||||
"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_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_title": "Configurer la sauvegarde sécurisée",
|
||||
@@ -991,16 +991,18 @@
|
||||
"after_new_login": {
|
||||
"device_verified": "Appareil vérifié",
|
||||
"skip_verification": "Ignorer la vérification pour l’instant",
|
||||
"unable_to_verify": "Impossible de vérifier cet appareil",
|
||||
"verify_this_device": "Vérifier cet appareil"
|
||||
},
|
||||
"cancelled": "Vous avez annulé la vérification.",
|
||||
"cancelled_self": "Vous avez annulé la vérification dans votre autre appareil.",
|
||||
"cancelled_user": "%(displayName)s a annulé la vérification.",
|
||||
"cancelling": "Annulation…",
|
||||
"cant_confirm": "Confirmation impossible ?",
|
||||
"complete_action": "Compris",
|
||||
"complete_description": "Vous avez vérifié cet utilisateur avec succès.",
|
||||
"complete_title": "Vérifié !",
|
||||
"confirm_identity_description": "Vérifier cet appareil pour configurer votre messagerie sécurisée",
|
||||
"confirm_identity_title": "Confirmez votre identité",
|
||||
"error_starting_description": "Nous n’avons pas pu démarrer une conversation avec l’autre utilisateur.",
|
||||
"error_starting_title": "Erreur en démarrant la vérification",
|
||||
"explainer": "Les messages sécurisés avec cet utilisateur sont chiffrés de bout en bout et ne peuvent être lus par d’autres personnes.",
|
||||
@@ -1026,7 +1028,6 @@
|
||||
"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.",
|
||||
"prompt_encrypted": "Vérifiez tous les utilisateurs d’un salon pour vous assurer qu’il est sécurisé.",
|
||||
@@ -1042,7 +1043,6 @@
|
||||
"request_toast_accept_user": "Vérifier l'utilisateur",
|
||||
"request_toast_decline_counter": "Ignorer (%(counter)s)",
|
||||
"request_toast_detail": "%(deviceId)s depuis %(ip)s",
|
||||
"reset_proceed_prompt": "Faire la réinitialisation",
|
||||
"sas_caption_self": "Vérifiez cet appareil en confirmant que le nombre suivant s’affiche sur son écran.",
|
||||
"sas_caption_user": "Vérifier cet utilisateur en confirmant que le nombre suivant apparaît sur leur écran.",
|
||||
"sas_description": "Comparez une liste unique d’émojis si vous n’avez d’appareil photo sur aucun des deux appareils",
|
||||
@@ -1065,7 +1065,8 @@
|
||||
"unverified_sessions_toast_description": "Vérifiez pour assurer la sécurité de votre compte",
|
||||
"unverified_sessions_toast_reject": "Plus tard",
|
||||
"unverified_sessions_toast_title": "Vous avez des sessions non vérifiées",
|
||||
"verification_description": "Vérifiez votre identité pour accéder aux messages chiffrés et prouver votre identité aux autres. Si vous utilisez également un appareil mobile, veuillez ouvrir l’application avant de continuer.",
|
||||
"use_another_device": "Utiliser un autre appareil",
|
||||
"use_recovery_key": "Utiliser la clé de récupération",
|
||||
"verification_dialog_title_device": "Vérifier un autre appareil",
|
||||
"verification_dialog_title_user": "Demande de vérification",
|
||||
"verification_skip_warning": "Sans vérification, vous n’aurez pas accès à tous vos messages et vous n’apparaîtrez pas comme de confiance aux autres.",
|
||||
@@ -1075,9 +1076,6 @@
|
||||
"verify_emoji_prompt": "Vérifier en comparant des émojis uniques.",
|
||||
"verify_emoji_prompt_qr": "Si vous ne pouvez pas scanner le code ci-dessus, vérifiez en comparant des émojis uniques.",
|
||||
"verify_later": "Je ferai la vérification plus tard",
|
||||
"verify_using_device": "Vérifier avec un autre appareil",
|
||||
"verify_using_key": "Vérifier avec la clé de récupération",
|
||||
"verify_using_key_or_phrase": "Vérifier avec une clé de récupération ou une phrase",
|
||||
"waiting_for_user_accept": "En attente d’acceptation par %(displayName)s…",
|
||||
"waiting_other_device": "En attente de votre vérification sur votre autre appareil…",
|
||||
"waiting_other_device_details": "En attente de votre vérification sur votre autre appareil, %(deviceName)s (%(deviceId)s)…",
|
||||
@@ -1127,6 +1125,7 @@
|
||||
"tls": "Impossible de se connecter au serveur d’accueil - veuillez vérifier votre connexion, assurez-vous que le <a>certificat SSL de votre serveur d’accueil</a> est un certificat de confiance, et qu’aucune extension du navigateur ne bloque les requêtes.",
|
||||
"unknown": "Erreur inconnue",
|
||||
"unknown_error_code": "code d’erreur inconnu",
|
||||
"update_history_visibility": "Echec lors de la modification de la visibilité de l'historique",
|
||||
"update_power_level": "Échec du changement de rang"
|
||||
},
|
||||
"error_app_open_in_another_tab": "Vous pouvez fermer cet onglet déconnecté, et aller à l'autre onglet %(brand)s.",
|
||||
@@ -2383,6 +2382,10 @@
|
||||
"users_default": "Rôle par défaut"
|
||||
},
|
||||
"security": {
|
||||
"cannot_change_to_private_due_to_missing_history_visiblity_permissions": {
|
||||
"description": "Vous n\"avez pas les autorisations nécessaires pour modifier l\"historique du salon. Ceci est dangereux, car cela pourrait permettre aux utilisateurs non présents de lire les messages.",
|
||||
"title": "Impossible de rendre le salon privé"
|
||||
},
|
||||
"enable_encryption_confirm_description": "Le chiffrement du salon ne peut pas être désactivé après son activation. Les messages d’un salon chiffré ne peuvent pas être vus par le serveur, seulement par les membres du salon. Activer le chiffrement peut empêcher certains robots et certaines passerelles de fonctionner correctement. <a>En savoir plus sur le chiffrement.</a>",
|
||||
"enable_encryption_confirm_title": "Activer le chiffrement ?",
|
||||
"enable_encryption_public_room_confirm_description_1": "<b>Il n'est pas recommandé d’ajouter le chiffrement aux salons publics.</b> Tout le monde peut trouver et rejoindre les salons publics, donc tout le monde peut lire les messages qui s’y trouvent. Vous n’aurez aucun des avantages du chiffrement, et vous ne pourrez pas le désactiver plus tard. Chiffrer les messages dans un salon public ralentira la réception et l’envoi de messages.",
|
||||
@@ -2400,7 +2403,7 @@
|
||||
"history_visibility_joined": "Seulement les membres (depuis leur arrivée)",
|
||||
"history_visibility_legend": "Qui peut lire l’historique ?",
|
||||
"history_visibility_shared": "Seulement les membres (depuis la sélection de cette option)",
|
||||
"history_visibility_warning": "Les modifications concernant l'accès à l’historique ne s'appliqueront qu’aux futurs messages de ce salon. La visibilité de l’historique existant ne sera pas modifiée.",
|
||||
"history_visibility_warning": "La visibilité de l’historique existant ne sera pas modifiée.",
|
||||
"history_visibility_world_readable": "N’importe qui",
|
||||
"join_rule_description": "Choisir qui peut rejoindre %(roomName)s.",
|
||||
"join_rule_invite": "Privé (sur invitation)",
|
||||
@@ -2443,6 +2446,7 @@
|
||||
"other": "Mise-à-jour des espaces… (%(progress)s sur %(count)s)"
|
||||
},
|
||||
"join_rule_upgrade_upgrading_room": "Mise-à-jour du salon",
|
||||
"join_rule_world_readable_description": "Changer qui peut rejoindre la salon modifiera également la visibilité des futurs messages.",
|
||||
"public_without_alias_warning": "Pour créer un lien vers ce salon, ajoutez une adresse.",
|
||||
"publish_room": "Rendez ce salon visible dans l’annuaire des salons publics.",
|
||||
"publish_space": "Rendez cet espace visible dans le répertoires des salons publics.",
|
||||
@@ -2581,6 +2585,7 @@
|
||||
"breadcrumb_second_description": "Vous perdrez l’historique de vos messages",
|
||||
"breadcrumb_third_description": "Vous devrez vérifier à nouveau tous vos appareils et tous vos contacts",
|
||||
"breadcrumb_title": "Êtes-vous sûr de vouloir réinitialiser votre identité ?",
|
||||
"breadcrumb_title_cant_confirm": "Vous devez réinitialiser votre identité",
|
||||
"breadcrumb_title_forgot": "Vous avez oublié votre clé de récupération ? Vous devez réinitialiser votre identité.",
|
||||
"breadcrumb_title_sync_failed": "Impossible de synchroniser le stockage des clés. Vous devez réinitialiser votre identité.",
|
||||
"breadcrumb_warning": "Ne faites cela que si vous pensez que votre compte a été compromis.",
|
||||
@@ -3981,6 +3986,7 @@
|
||||
"connection_lost": "La connexion au serveur a été perdue",
|
||||
"connection_lost_description": "Vous ne pouvez pas passer d’appels sans connexion au serveur.",
|
||||
"consulting": "Consultation avec %(transferTarget)s. <a>Transfert à %(transferee)s</a>",
|
||||
"decline_call": "Refuser",
|
||||
"default_device": "Appareil par défaut",
|
||||
"dial": "Composer",
|
||||
"dialpad": "Pavé numérique",
|
||||
@@ -4032,6 +4038,7 @@
|
||||
"show_sidebar_button": "Afficher la barre latérale",
|
||||
"silence": "Mettre l’appel en sourdine",
|
||||
"silenced": "Notifications silencieuses",
|
||||
"skip_lobby_toggle_option": "Rejoignez immédiatement",
|
||||
"start_screenshare": "Commencer à partager mon écran",
|
||||
"stop_screenshare": "Arrêter de partager mon écran",
|
||||
"too_many_calls": "Trop d’appels",
|
||||
|
||||