diff --git a/.eslintrc.js b/.eslintrc.js index 892d7cdbb1..26865d55ec 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -30,6 +30,10 @@ module.exports = { ["window.innerHeight", "window.innerWidth", "window.visualViewport"], "Use UIStore to access window dimensions instead.", ), + ...buildRestrictedPropertiesOptions( + ["React.forwardRef", "*.forwardRef", "forwardRef"], + "Use ref props instead.", + ), ...buildRestrictedPropertiesOptions( ["*.mxcUrlToHttp", "*.getHttpUriForMxc"], "Use Media helper instead to centralise access for customisation.", @@ -55,6 +59,11 @@ module.exports = { "error", { paths: [ + { + name: "react", + importNames: ["forwardRef"], + message: "Use ref props instead.", + }, { name: "@testing-library/react", message: "Please use jest-matrix-react instead", diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 21a6b0e7ab..68b9f4e703 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,9 +43,9 @@ jobs: run: shell: bash steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: # Disable cache on Windows as it is slower than not caching # https://github.com/actions/setup-node/issues/975 @@ -77,7 +77,7 @@ jobs: yarn build - name: Upload Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: webapp-${{ matrix.image }} path: webapp diff --git a/.github/workflows/build_debian.yaml b/.github/workflows/build_debian.yaml index f46678512a..247e5604ee 100644 --- a/.github/workflows/build_debian.yaml +++ b/.github/workflows/build_debian.yaml @@ -14,7 +14,7 @@ jobs: R2_URL: ${{ vars.CF_R2_S3_API }} VERSION: ${{ github.ref_name }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: Download package run: | @@ -62,7 +62,7 @@ jobs: dpkg-gencontrol -v"$VERSION" -ldebian/tmp/DEBIAN/changelog dpkg-deb -Zxz --root-owner-group --build debian/tmp element-web.deb - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: element-web.deb path: element-web.deb diff --git a/.github/workflows/build_develop.yml b/.github/workflows/build_develop.yml index bbb52de5a2..9550bf9139 100644 --- a/.github/workflows/build_develop.yml +++ b/.github/workflows/build_develop.yml @@ -26,9 +26,9 @@ jobs: R2_URL: ${{ vars.CF_R2_S3_API }} R2_PUBLIC_URL: "https://element-web-develop.element.io" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: cache: "yarn" node-version: "lts/*" @@ -53,7 +53,7 @@ jobs: - run: mv dist/element-*.tar.gz dist/develop.tar.gz - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: webapp path: dist/develop.tar.gz diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 14fbd22086..8b52e6764c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -34,7 +34,7 @@ jobs: env: SITE: ${{ inputs.site || 'staging.element.io' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: Load GPG key run: | diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index ff48c3bf00..75a00af7aa 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -20,12 +20,12 @@ jobs: env: TEST_TAG: vectorim/element-web:test steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: fetch-depth: 0 # needed for docker-package to be able to calculate the version - name: Install Cosign - uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3 + uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3 if: github.event_name != 'pull_request' - name: Set up QEMU diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a301b6daf6..e7d69cf477 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -17,23 +17,23 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Fetch element-desktop - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: repository: element-hq/element-desktop path: element-desktop - name: Fetch element-web - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: path: element-web - name: Fetch matrix-js-sdk - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: repository: matrix-org/matrix-js-sdk path: matrix-js-sdk - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: cache: "yarn" cache-dependency-path: element-web/yarn.lock @@ -47,7 +47,7 @@ jobs: echo "- [Automations](automations.md)" >> docs/SUMMARY.md - name: Setup mdBook - uses: peaceiris/actions-mdbook@v2 + uses: peaceiris/actions-mdbook@ee69d230fe19748b7abf22df32acaa93833fad08 # v2 with: mdbook-version: "0.4.10" @@ -88,7 +88,7 @@ jobs: run: mdbook build - name: Upload artifact - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3 with: path: ./book @@ -104,4 +104,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4 diff --git a/.github/workflows/end-to-end-tests-netlify.yaml b/.github/workflows/end-to-end-tests-netlify.yaml index e25994ec9d..90a8c3d24b 100644 --- a/.github/workflows/end-to-end-tests-netlify.yaml +++ b/.github/workflows/end-to-end-tests-netlify.yaml @@ -25,7 +25,7 @@ jobs: actions: read steps: - name: Download HTML report - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: github-token: ${{ secrets.GITHUB_TOKEN }} run-id: ${{ github.event.workflow_run.id }} @@ -33,7 +33,7 @@ jobs: path: playwright-report - name: 📤 Deploy to Netlify - uses: matrix-org/netlify-pr-preview@v3 + uses: matrix-org/netlify-pr-preview@9805cd123fc9a7e421e35340a05e1ebc5dee46b5 # v3 with: path: playwright-report owner: ${{ github.event.workflow_run.head_repository.owner.login }} diff --git a/.github/workflows/end-to-end-tests.yaml b/.github/workflows/end-to-end-tests.yaml index bd7872ee12..b2d49d7703 100644 --- a/.github/workflows/end-to-end-tests.yaml +++ b/.github/workflows/end-to-end-tests.yaml @@ -50,11 +50,11 @@ jobs: runners-matrix: ${{ steps.runner-vars.outputs.matrix }} steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: repository: element-hq/element-web - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: cache: "yarn" node-version: "lts/*" @@ -81,7 +81,7 @@ jobs: yarn build - name: Upload Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: webapp path: webapp @@ -89,7 +89,7 @@ jobs: - name: Calculate runner variables id: runner-vars - uses: actions/github-script@v7 + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 with: script: | const numRunners = parseInt(process.env.NUM_RUNNERS, 10); @@ -129,18 +129,18 @@ jobs: - runAllTests: false project: Pinecone steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: persist-credentials: false repository: element-hq/element-web - name: 📥 Download artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: webapp path: webapp - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: cache: "yarn" cache-dependency-path: yarn.lock @@ -154,12 +154,11 @@ jobs: run: echo "version=$(yarn list --pattern @playwright/test --depth=0 --json --non-interactive --no-progress | jq -r '.data.trees[].name')" >> $GITHUB_OUTPUT - name: Cache playwright binaries - uses: actions/cache@v4 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 id: playwright-cache with: - path: | - ~/.cache/ms-playwright - key: ${{ runner.os }}-playwright-${{ steps.playwright.outputs.version }} + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.playwright.outputs.version }} - name: Install Playwright browsers if: steps.playwright-cache.outputs.cache-hit != 'true' @@ -180,25 +179,35 @@ jobs: - name: Upload blob report to GitHub Actions Artifacts if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: all-blob-reports-${{ matrix.project }}-${{ matrix.runner }} path: blob-report retention-days: 1 + downstream-modules: + name: Downstream Playwright tests [element-modules] + needs: build + if: inputs.skip != true && github.event_name == 'merge_group' + uses: element-hq/element-modules/.github/workflows/reusable-playwright-tests.yml@main + with: + webapp-artifact: webapp + complete: name: end-to-end-tests - needs: playwright + needs: + - playwright + - downstream-modules if: always() runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 if: inputs.skip != true with: persist-credentials: false repository: element-hq/element-web - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 if: inputs.skip != true with: cache: "yarn" @@ -210,7 +219,7 @@ jobs: - name: Download blob reports from GitHub Actions Artifacts if: inputs.skip != true - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: pattern: all-blob-reports-* path: all-blob-reports @@ -226,11 +235,11 @@ jobs: # Upload the HTML report even if one of our reporters fails, this can happen when stale screenshots are detected - name: Upload HTML report if: always() && inputs.skip != true - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: html-report path: playwright-report retention-days: 14 - - if: needs.playwright.result != 'skipped' && needs.playwright.result != 'success' + - if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') run: exit 1 diff --git a/.github/workflows/issue_closed.yml b/.github/workflows/issue_closed.yml index 2cffae0011..249f1eb342 100644 --- a/.github/workflows/issue_closed.yml +++ b/.github/workflows/issue_closed.yml @@ -10,7 +10,7 @@ jobs: name: Tidy closed issues runs-on: ubuntu-24.04 steps: - - uses: actions/github-script@v7 + - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 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@v7 + - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 name: Close duplicate as Not Planned if: steps.main.outputs.closeAsNotPlanned with: diff --git a/.github/workflows/netlify.yaml b/.github/workflows/netlify.yaml index cd03ca5140..a7909265c7 100644 --- a/.github/workflows/netlify.yaml +++ b/.github/workflows/netlify.yaml @@ -28,7 +28,7 @@ jobs: Exercise caution. Use test accounts. - name: 📥 Download artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: github-token: ${{ secrets.GITHUB_TOKEN }} run-id: ${{ github.event.workflow_run.id }} @@ -36,7 +36,7 @@ jobs: path: webapp - name: 📤 Deploy to Netlify - uses: matrix-org/netlify-pr-preview@v3 + uses: matrix-org/netlify-pr-preview@9805cd123fc9a7e421e35340a05e1ebc5dee46b5 # v3 with: path: webapp owner: ${{ github.event.workflow_run.head_repository.owner.login }} diff --git a/.github/workflows/pending-reviews.yaml b/.github/workflows/pending-reviews.yaml index c96ed3f17e..199eb60daa 100644 --- a/.github/workflows/pending-reviews.yaml +++ b/.github/workflows/pending-reviews.yaml @@ -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@v7 + - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 env: HS_URL: ${{ secrets.BETABOT_HS_URL }} ROOM_ID: ${{ secrets.ROOM_ID }} diff --git a/.github/workflows/playwright-image-updates.yaml b/.github/workflows/playwright-image-updates.yaml index 7681600dde..4cbdb17bbd 100644 --- a/.github/workflows/playwright-image-updates.yaml +++ b/.github/workflows/playwright-image-updates.yaml @@ -10,7 +10,7 @@ jobs: permissions: pull-requests: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: Update synapse image run: | diff --git a/.github/workflows/pull_request_base_branch.yaml b/.github/workflows/pull_request_base_branch.yaml index 6610ee4879..fbdebfbed0 100644 --- a/.github/workflows/pull_request_base_branch.yaml +++ b/.github/workflows/pull_request_base_branch.yaml @@ -8,7 +8,7 @@ jobs: name: Check PR base branch runs-on: ubuntu-24.04 steps: - - uses: actions/github-script@v7 + - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 with: script: | const baseBranch = context.payload.pull_request.base.ref; diff --git a/.github/workflows/release_prepare.yml b/.github/workflows/release_prepare.yml index 707489b08a..2f36644c2e 100644 --- a/.github/workflows/release_prepare.yml +++ b/.github/workflows/release_prepare.yml @@ -41,7 +41,7 @@ jobs: REPOS: matrix-js-sdk element-web element-desktop steps: - name: Checkout Element Desktop - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 if: inputs.element-desktop with: repository: element-hq/element-desktop @@ -51,7 +51,7 @@ jobs: fetch-tags: true token: ${{ secrets.ELEMENT_BOT_TOKEN }} - name: Checkout Element Web - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 if: inputs.element-web with: repository: element-hq/element-web @@ -61,7 +61,7 @@ jobs: fetch-tags: true token: ${{ secrets.ELEMENT_BOT_TOKEN }} - name: Checkout Matrix JS SDK - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 if: inputs.matrix-js-sdk with: repository: matrix-org/matrix-js-sdk diff --git a/.github/workflows/static_analysis.yaml b/.github/workflows/static_analysis.yaml index 8b15fdfe76..d63e0da8ed 100644 --- a/.github/workflows/static_analysis.yaml +++ b/.github/workflows/static_analysis.yaml @@ -23,9 +23,9 @@ jobs: name: "Typescript Syntax Check" runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: cache: "yarn" node-version: "lts/*" @@ -57,7 +57,7 @@ jobs: name: "Rethemendex Check" runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - run: ./res/css/rethemendex.sh @@ -67,9 +67,9 @@ jobs: name: "ESLint" runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: cache: "yarn" node-version: "lts/*" @@ -85,9 +85,9 @@ jobs: name: "Style Lint" runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: cache: "yarn" node-version: "lts/*" @@ -103,9 +103,9 @@ jobs: name: "Workflow Lint" runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: cache: "yarn" node-version: "lts/*" @@ -121,9 +121,9 @@ jobs: name: "Analyse Dead Code" runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: cache: "yarn" node-version: "lts/*" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4d4d3ce5d6..276c53c098 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -39,12 +39,12 @@ jobs: runner: [1, 2] steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: repository: ${{ inputs.matrix-js-sdk-sha && 'element-hq/element-web' || github.repository }} - name: Yarn cache - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: "lts/*" cache: "yarn" @@ -55,7 +55,7 @@ jobs: JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }} - name: Jest Cache - uses: actions/cache@v4 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 with: path: /tmp/jest_cache key: ${{ hashFiles('**/yarn.lock') }} @@ -84,7 +84,7 @@ jobs: - name: Upload Artifact if: env.ENABLE_COVERAGE == 'true' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: coverage-${{ matrix.runner }} path: | diff --git a/.github/workflows/triage-labelled.yml b/.github/workflows/triage-labelled.yml index 2cb05a8bcf..e1849e0efc 100644 --- a/.github/workflows/triage-labelled.yml +++ b/.github/workflows/triage-labelled.yml @@ -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@v7 + - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 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@v7 + - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 with: script: | github.rest.issues.addLabels({ @@ -61,7 +61,7 @@ jobs: contains(github.event.issue.labels.*.name, 'X-Needs-Info') steps: - id: add_to_project - uses: actions/add-to-project@v1.0.2 + uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2 with: project-url: ${{ env.PROJECT_URL }} github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} @@ -84,7 +84,7 @@ jobs: contains(github.event.issue.labels.*.name, 'Z-Flaky-Test') steps: - id: add_to_project - uses: actions/add-to-project@v1.0.2 + uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2 with: project-url: ${{ env.PROJECT_URL }} github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} @@ -150,15 +150,15 @@ jobs: project-url: https://github.com/orgs/element-hq/projects/41 github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} - verticals_feature: - name: Add labelled issues to Verticals Feature project + crypto: + name: Add labelled issues to Crypto project runs-on: ubuntu-24.04 if: > - contains(github.event.issue.labels.*.name, 'Team: Verticals Feature') + contains(github.event.issue.labels.*.name, 'Team: Crypto') steps: - uses: actions/add-to-project@main with: - project-url: https://github.com/orgs/element-hq/projects/57 + project-url: https://github.com/orgs/element-hq/projects/76 github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} tech_debt: diff --git a/.github/workflows/triage-stale.yml b/.github/workflows/triage-stale.yml index b52f4e59da..f76cd299cc 100644 --- a/.github/workflows/triage-stale.yml +++ b/.github/workflows/triage-stale.yml @@ -12,7 +12,7 @@ jobs: issues: write pull-requests: write steps: - - uses: actions/stale@v9 + - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9 with: operations-per-run: 100 # Flaky test issue closing diff --git a/.github/workflows/triage-unlabelled.yml b/.github/workflows/triage-unlabelled.yml index efbf80eea9..d3bda6df1f 100644 --- a/.github/workflows/triage-unlabelled.yml +++ b/.github/workflows/triage-unlabelled.yml @@ -62,7 +62,7 @@ jobs: contains(github.event.issue.labels.*.name, 'A-Element-Call')) && contains(github.event.issue.labels.*.name, 'Z-Labs') steps: - - uses: actions/github-script@v7 + - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 with: script: | github.rest.issues.removeLabel({ diff --git a/.github/workflows/update-jitsi.yml b/.github/workflows/update-jitsi.yml index f4fd13892b..da386c544d 100644 --- a/.github/workflows/update-jitsi.yml +++ b/.github/workflows/update-jitsi.yml @@ -9,9 +9,9 @@ jobs: update: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: cache: "yarn" node-version: "lts/*" diff --git a/.github/workflows/update-topics.yaml b/.github/workflows/update-topics.yaml index cd6c2fc553..5ee9f2b608 100644 --- a/.github/workflows/update-topics.yaml +++ b/.github/workflows/update-topics.yaml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-24.04 environment: Matrix steps: - - uses: actions/github-script@v7 + - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 env: HS_URL: ${{ secrets.BETABOT_HS_URL }} LOBBY_ROOM_ID: ${{ secrets.ROOM_ID }} diff --git a/.gitignore b/.gitignore index 3e9dc5e135..429b317a4f 100644 --- a/.gitignore +++ b/.gitignore @@ -25,7 +25,6 @@ electron/pub .env /coverage # Auto-generated file -/src/modules.ts /src/modules.js /build_config.yaml /book diff --git a/CHANGELOG.md b/CHANGELOG.md index 01c67c34c0..48a721c013 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,88 @@ +Changes in [1.11.101](https://github.com/element-hq/element-web/releases/tag/v1.11.101) (2025-05-20) +==================================================================================================== +## ✨ Features + +* New room list: add keyboard navigation support ([#29805](https://github.com/element-hq/element-web/pull/29805)). Contributed by @florianduros. +* Use the JoinRuleSettings component for the guest link access prompt. ([#28614](https://github.com/element-hq/element-web/pull/28614)). Contributed by @toger5. +* Add loading state to the new room list view ([#29725](https://github.com/element-hq/element-web/pull/29725)). Contributed by @langleyd. +* Make OIDC identity reset consistent with EX ([#29854](https://github.com/element-hq/element-web/pull/29854)). Contributed by @andybalaam. +* Support error code for email / phone adding unsupported (MSC4178) ([#29855](https://github.com/element-hq/element-web/pull/29855)). Contributed by @dbkr. +* Update identity reset UI (Make consistent with EX) ([#29701](https://github.com/element-hq/element-web/pull/29701)). Contributed by @andybalaam. +* Add secondary filters to the new room list ([#29818](https://github.com/element-hq/element-web/pull/29818)). Contributed by @dbkr. +* Fix battery drain from Web Audio ([#29203](https://github.com/element-hq/element-web/pull/29203)). Contributed by @mbachry. + +## 🐛 Bug Fixes + +* Fix go home shortcut on macos and change toggle action events shortcut ([#29929](https://github.com/element-hq/element-web/pull/29929)). Contributed by @florianduros. +* New room list: fix outdated message preview when space or filter change ([#29925](https://github.com/element-hq/element-web/pull/29925)). Contributed by @florianduros. +* Stop migrating to MSC4278 if the config exists. ([#29924](https://github.com/element-hq/element-web/pull/29924)). Contributed by @Half-Shot. +* Ensure consistent download file name on download from ImageView ([#29913](https://github.com/element-hq/element-web/pull/29913)). Contributed by @t3chguy. +* Add error toast when service worker registration fails ([#29895](https://github.com/element-hq/element-web/pull/29895)). Contributed by @t3chguy. +* New Room List: Prevent old tombstoned rooms from appearing in the list ([#29881](https://github.com/element-hq/element-web/pull/29881)). Contributed by @MidhunSureshR. +* Remove lag in search field ([#29885](https://github.com/element-hq/element-web/pull/29885)). Contributed by @florianduros. +* Respect UIFeature.Voip ([#29873](https://github.com/element-hq/element-web/pull/29873)). Contributed by @langleyd. +* Allow jumping to message search from spotlight ([#29850](https://github.com/element-hq/element-web/pull/29850)). Contributed by @t3chguy. + + +Changes in [1.11.100](https://github.com/element-hq/element-web/releases/tag/v1.11.100) (2025-05-06) +==================================================================================================== +## ✨ Features + +* Move rich topics out of labs / stabilise MSC3765 ([#29817](https://github.com/element-hq/element-web/pull/29817)). Contributed by @Johennes. +* Spell out that Element Web does \*not\* work on mobile. ([#29211](https://github.com/element-hq/element-web/pull/29211)). Contributed by @ara4n. +* Add message preview support to the new room list ([#29784](https://github.com/element-hq/element-web/pull/29784)). Contributed by @dbkr. +* Global configuration flag for media previews ([#29582](https://github.com/element-hq/element-web/pull/29582)). Contributed by @Half-Shot. +* New room list: add partial keyboard shortcuts support ([#29783](https://github.com/element-hq/element-web/pull/29783)). Contributed by @florianduros. +* MVVM RoomSummaryCard Topic ([#29710](https://github.com/element-hq/element-web/pull/29710)). Contributed by @MarcWadai. +* Warn on self change from settings > roles ([#28926](https://github.com/element-hq/element-web/pull/28926)). Contributed by @MarcWadai. +* New room list: new visual for invitation ([#29773](https://github.com/element-hq/element-web/pull/29773)). Contributed by @florianduros. + +## 🐛 Bug Fixes + +* Fix incorrect display of the user info display name ([#29826](https://github.com/element-hq/element-web/pull/29826)). Contributed by @langleyd. +* RoomListStore: Remove invite rooms on decline ([#29804](https://github.com/element-hq/element-web/pull/29804)). Contributed by @MidhunSureshR. +* Fix the buttons not being displayed with long preview text ([#29811](https://github.com/element-hq/element-web/pull/29811)). Contributed by @dbkr. +* New room list: fix missing/incorrect notification decoration ([#29796](https://github.com/element-hq/element-web/pull/29796)). Contributed by @florianduros. +* New Room List: Prevent potential scroll jump/flicker when switching spaces ([#29781](https://github.com/element-hq/element-web/pull/29781)). Contributed by @MidhunSureshR. +* New room list: fix incorrect decoration ([#29770](https://github.com/element-hq/element-web/pull/29770)). Contributed by @florianduros. + + +Changes in [1.11.99](https://github.com/element-hq/element-web/releases/tag/v1.11.99) (2025-04-23) +================================================================================================== +No changes, just bumping the version to accommodate a new Element Desktop release + +Changes in [1.11.98](https://github.com/element-hq/element-web/releases/tag/v1.11.98) (2025-04-22) +================================================================================================== +## ✨ Features + +* print better errors in the search view instead of a blocking modal ([#29724](https://github.com/element-hq/element-web/pull/29724)). Contributed by @Jujure. +* New room list: video room and video call decoration ([#29693](https://github.com/element-hq/element-web/pull/29693)). Contributed by @florianduros. +* Remove Secure Backup, Cross-signing and Cryptography sections in `Security & Privacy` user settings ([#29088](https://github.com/element-hq/element-web/pull/29088)). Contributed by @florianduros. +* Allow reporting a room when rejecting an invite. ([#29570](https://github.com/element-hq/element-web/pull/29570)). Contributed by @Half-Shot. +* RoomListViewModel: Reset primary and secondary filters on space change ([#29672](https://github.com/element-hq/element-web/pull/29672)). Contributed by @MidhunSureshR. +* RoomListStore: Support specific sorting requirements for muted rooms ([#29665](https://github.com/element-hq/element-web/pull/29665)). Contributed by @MidhunSureshR. +* New room list: add notification options menu ([#29639](https://github.com/element-hq/element-web/pull/29639)). Contributed by @florianduros. +* Room List: Scroll to top of the list when active room is not in the list ([#29650](https://github.com/element-hq/element-web/pull/29650)). Contributed by @MidhunSureshR. + +## 🐛 Bug Fixes + +* Fix unwanted form submit behaviour in memberlist ([#29747](https://github.com/element-hq/element-web/pull/29747)). Contributed by @MidhunSureshR. +* New room list: fix public room icon visibility when filter change ([#29737](https://github.com/element-hq/element-web/pull/29737)). Contributed by @florianduros. +* Fix custom theme support for short hex \& rgba hex strings ([#29726](https://github.com/element-hq/element-web/pull/29726)). Contributed by @t3chguy. +* New room list: minor visual fixes ([#29723](https://github.com/element-hq/element-web/pull/29723)). Contributed by @florianduros. +* Fix getOidcCallbackUrl for Element Desktop ([#29711](https://github.com/element-hq/element-web/pull/29711)). Contributed by @t3chguy. +* Fix some webp images improperly marked as animated ([#29713](https://github.com/element-hq/element-web/pull/29713)). Contributed by @Petersmit27. +* Revert deletion of hydrateSession ([#29703](https://github.com/element-hq/element-web/pull/29703)). Contributed by @Jujure. +* Fix converttoroom \& converttodm not working ([#29705](https://github.com/element-hq/element-web/pull/29705)). Contributed by @t3chguy. +* Ensure forceCloseAllModals also closes priority/static modals ([#29706](https://github.com/element-hq/element-web/pull/29706)). Contributed by @t3chguy. +* Continue button is disabled when uploading a recovery key file ([#29695](https://github.com/element-hq/element-web/pull/29695)). Contributed by @Giwayume. +* Catch errors after syncing recovery ([#29691](https://github.com/element-hq/element-web/pull/29691)). Contributed by @andybalaam. +* New room list: fix multiple visual issues ([#29673](https://github.com/element-hq/element-web/pull/29673)). Contributed by @florianduros. +* New Room List: Fix mentions filter matching rooms with any highlight ([#29668](https://github.com/element-hq/element-web/pull/29668)). Contributed by @MidhunSureshR. +* Fix truncated emoji label during emoji SAS ([#29643](https://github.com/element-hq/element-web/pull/29643)). Contributed by @florianduros. +* Remove duplicate jitsi link ([#29642](https://github.com/element-hq/element-web/pull/29642)). Contributed by @dbkr. + + Changes in [1.11.97](https://github.com/element-hq/element-web/releases/tag/v1.11.97) (2025-04-08) ================================================================================================== ## ✨ Features diff --git a/Dockerfile b/Dockerfile index f1a41c2bc7..a1813cf66d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ -# syntax=docker.io/docker/dockerfile:1.15-labs +# syntax=docker.io/docker/dockerfile:1.16-labs@sha256:bb5e2b225985193779991f3256d1901a0b3e6a0b284c7bffa0972064f4a6d458 # Builder -FROM --platform=$BUILDPLATFORM node:22-bullseye AS builder +FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:f16d8e8af67bb6361231e932b8b3e7afa040cbfed181719a450b02c3821b26c1 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 +FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:2acffd86b1bdefb8fa6b48b6e9aadf75430e8ab9c43c54c515ea7df77897f987 # Need root user to install packages & manipulate the usr directory USER root diff --git a/README.md b/README.md index 00f8d6d89c..0f8a721f90 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ guide](https://classic.yarnpkg.com/en/docs/install) if you do not have it alread 1. Install the prerequisites: `yarn install`. - If you're using the `develop` branch, then it is recommended to set up a proper development environment (see [Setting up a dev - environment](#setting-up-a-dev-environment) below). Alternatively, you + environment](./developer_guide.md#setting-up-a-dev-environment) below). Alternatively, you can use - the continuous integration release of the develop branch. 1. Configure the app by copying `config.sample.json` to `config.json` and diff --git a/docs/labs.md b/docs/labs.md index 7f69fca6e9..60f35dd4a4 100644 --- a/docs/labs.md +++ b/docs/labs.md @@ -101,10 +101,6 @@ Under the hood this stops Element Web from adding the `perParticipantE2EE` flag This is useful while we experiment with encryption and to make calling compatible with platforms that don't use encryption yet. -## Rich text in room topics (`feature_html_topic`) [In Development] - -Enables rendering of MD / HTML in room topics. - ## Enable the notifications panel in the room header (`feature_notifications`) Unreliable in encrypted rooms. diff --git a/package.json b/package.json index a686e8ab1c..f5ff367e3d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "element-web", - "version": "1.11.97", + "version": "1.11.101", "description": "Element: the future of secure communication", "author": "New Vector Ltd.", "repository": { @@ -69,19 +69,19 @@ }, "resolutions": { "**/pretty-format/react-is": "19.1.0", - "@playwright/test": "1.51.1", - "@types/react": "19.1.1", - "@types/react-dom": "19.1.2", - "oidc-client-ts": "3.2.0", + "@playwright/test": "1.52.0", + "@types/react": "19.1.6", + "@types/react-dom": "19.1.5", + "oidc-client-ts": "3.2.1", "jwt-decode": "4.0.0", - "caniuse-lite": "1.0.30001714", - "testcontainers": "10.24.2", + "caniuse-lite": "1.0.30001720", + "testcontainers": "^11.0.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0", "wrap-ansi": "npm:wrap-ansi@^7.0.0" }, "dependencies": { "@babel/runtime": "^7.12.5", - "@element-hq/element-web-module-api": "^0.1.1", + "@element-hq/element-web-module-api": "1.0.0", "@fontsource/inconsolata": "^5", "@fontsource/inter": "^5", "@formatjs/intl-segmenter": "^11.5.7", @@ -95,7 +95,7 @@ "@types/png-chunks-extract": "^1.0.2", "@types/react-virtualized": "^9.21.30", "@vector-im/compound-design-tokens": "^4.0.0", - "@vector-im/compound-web": "^7.10.1", + "@vector-im/compound-web": "^7.11.0", "@vector-im/matrix-wysiwyg": "2.38.3", "@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4", @@ -125,9 +125,9 @@ "jsrsasign": "^11.0.0", "jszip": "^3.7.0", "katex": "^0.16.0", - "linkify-react": "4.2.0", - "linkify-string": "4.2.0", - "linkifyjs": "4.2.0", + "linkify-react": "4.3.1", + "linkify-string": "4.3.1", + "linkifyjs": "4.3.1", "lodash": "^4.17.21", "maplibre-gl": "^5.0.0", "matrix-encrypt-attachment": "^1.0.3", @@ -140,7 +140,7 @@ "opus-recorder": "^8.0.3", "pako": "^2.0.3", "png-chunks-extract": "^1.0.0", - "posthog-js": "1.236.1", + "posthog-js": "1.248.1", "qrcode": "1.5.4", "re-resizable": "6.11.2", "react": "^19.0.0", @@ -153,7 +153,7 @@ "react-virtualized": "^9.22.5", "rfc4648": "^1.4.0", "sanitize-filename": "^1.6.3", - "sanitize-html": "2.15.0", + "sanitize-html": "2.17.0", "styled-components": "^6.1.17", "tar-js": "^0.3.0", "temporal-polyfill": "^0.3.0", @@ -183,7 +183,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.9.0", + "@element-hq/element-call-embedded": "0.12.0", "@element-hq/element-web-playwright-common": "^1.1.5", "@peculiar/webcrypto": "^1.4.3", "@playwright/test": "^1.50.1", @@ -215,11 +215,11 @@ "@types/node-fetch": "^2.6.2", "@types/pako": "^2.0.0", "@types/qrcode": "^1.3.5", - "@types/react": "19.1.1", + "@types/react": "19.1.6", "@types/react-beautiful-dnd": "^13.0.0", - "@types/react-dom": "19.1.2", + "@types/react-dom": "19.1.5", "@types/react-transition-group": "^4.4.0", - "@types/sanitize-html": "2.15.0", + "@types/sanitize-html": "2.16.0", "@types/semver": "^7.5.8", "@types/tar-js": "^0.3.5", "@types/ua-parser-js": "^0.7.36", @@ -265,7 +265,7 @@ "jest-raw-loader": "^1.0.1", "jsqr": "^1.4.0", "knip": "^5.36.2", - "lint-staged": "^15.0.2", + "lint-staged": "^16.0.0", "matrix-web-i18n": "^3.2.1", "mini-css-extract-plugin": "2.9.2", "minimist": "^1.2.6", @@ -294,7 +294,7 @@ "stylelint-scss": "^6.0.0", "stylelint-value-no-unknown-custom-properties": "^6.0.1", "terser-webpack-plugin": "^5.3.9", - "testcontainers": "^10.20.0", + "testcontainers": "^11.0.0", "ts-node": "^10.9.1", "typescript": "5.8.3", "util": "^0.12.5", @@ -314,5 +314,6 @@ }, "engines": { "node": ">=20.0.0" - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/patches/@types+react+19.0.10.patch b/patches/@types+react+19.1.4.patch similarity index 91% rename from patches/@types+react+19.0.10.patch rename to patches/@types+react+19.1.4.patch index 6f54d0b382..ceba85b000 100644 --- a/patches/@types+react+19.0.10.patch +++ b/patches/@types+react+19.1.4.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/@types/react/index.d.ts b/node_modules/@types/react/index.d.ts -index 2272032..18bd20a 100644 +index d3318dc..c2b2c77 100644 --- a/node_modules/@types/react/index.d.ts +++ b/node_modules/@types/react/index.d.ts @@ -134,7 +134,7 @@ declare namespace React { @@ -11,7 +11,7 @@ index 2272032..18bd20a 100644 /** * Created by {@link createRef}, or {@link useRef} when passed `null`. -@@ -941,7 +941,7 @@ declare namespace React { +@@ -945,7 +945,7 @@ declare namespace React { context: unknown; // Keep in sync with constructor signature of JSXElementConstructor and ComponentClass. @@ -20,7 +20,7 @@ index 2272032..18bd20a 100644 // We MUST keep setState() as a unified signature because it allows proper checking of the method return type. // See: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment-351013257 -@@ -1113,7 +1113,7 @@ declare namespace React { +@@ -1117,7 +1117,7 @@ declare namespace React { */ interface ComponentClass

extends StaticLifecycle { // constructor signature must match React.Component diff --git a/playwright/e2e/crypto/dehydration.spec.ts b/playwright/e2e/crypto/dehydration.spec.ts index 2f545b8d14..379fc36cf9 100644 --- a/playwright/e2e/crypto/dehydration.spec.ts +++ b/playwright/e2e/crypto/dehydration.spec.ts @@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details. import { test, expect } from "../../element-web-test"; import { isDendrite } from "../../plugins/homeserver/dendrite"; -import { completeCreateSecretStorageDialog, createBot, logIntoElement } from "./utils.ts"; +import { createBot, logIntoElement } from "./utils.ts"; import { type Client } from "../../pages/client.ts"; import { type ElementAppPage } from "../../pages/ElementAppPage.ts"; @@ -28,21 +28,27 @@ test.describe("Dehydration", () => { test.skip(isDendrite, "does not yet support dehydration v2"); test("Verify device and reset creates dehydrated device", async ({ page, user, credentials, app }, workerInfo) => { - // Verify the device by resetting the key (which will create SSSS, and dehydrated device) + // Verify the device by resetting the identity key, and then set up recovery (which will create SSSS, and dehydrated device) const securityTab = await app.settings.openUserSettings("Security & Privacy"); await expect(securityTab.getByText("Offline device enabled")).not.toBeVisible(); await app.closeDialog(); - // Verify the device by resetting the key + // Reset the identity key const settings = await app.settings.openUserSettings("Encryption"); await settings.getByRole("button", { name: "Verify this device" }).click(); await page.getByRole("button", { name: "Proceed with reset" }).click(); await page.getByRole("button", { name: "Continue" }).click(); - await page.getByRole("button", { name: "Copy" }).click(); + + // Set up recovery + await page.getByRole("button", { name: "Set up recovery" }).click(); await page.getByRole("button", { name: "Continue" }).click(); - await page.getByRole("button", { name: "Done" }).click(); + const recoveryKey = await page.getByTestId("recoveryKey").innerText(); + await page.getByRole("button", { name: "Continue" }).click(); + await page.getByRole("textbox").fill(recoveryKey); + await page.getByRole("button", { name: "Finish set up" }).click(); + await page.getByRole("button", { name: "Close" }).click(); await expectDehydratedDeviceEnabled(app); @@ -80,7 +86,7 @@ test.describe("Dehydration", () => { await expectDehydratedDeviceEnabled(app); }); - test("Reset recovery key during login re-creates dehydrated device", async ({ + test("Reset identity during login and set up recovery re-creates dehydrated device", async ({ page, homeserver, app, @@ -99,16 +105,26 @@ test.describe("Dehydration", () => { // Log in our client await logIntoElement(page, credentials); - // Oh no, we forgot our recovery key + // Oh no, we forgot our recovery key - reset our identity await page.locator(".mx_AuthPage").getByRole("button", { name: "Reset all" }).click(); - await page.locator(".mx_AuthPage").getByRole("button", { name: "Proceed with reset" }).click(); + await expect( + page.getByRole("heading", { name: "Are you sure you want to reset your identity?" }), + ).toBeVisible(); + await page.getByRole("button", { name: "Continue", exact: true }).click(); + await page.getByPlaceholder("Password").fill(credentials.password); + await page.getByRole("button", { name: "Continue" }).click(); - await completeCreateSecretStorageDialog(page, { accountPassword: credentials.password }); + // And set up recovery + const settings = await app.settings.openUserSettings("Encryption"); + await settings.getByRole("button", { name: "Set up recovery" }).click(); + await settings.getByRole("button", { name: "Continue" }).click(); + const recoveryKey = await settings.getByTestId("recoveryKey").innerText(); + await settings.getByRole("button", { name: "Continue" }).click(); + await settings.getByRole("textbox").fill(recoveryKey); + await settings.getByRole("button", { name: "Finish set up" }).click(); // There should be a brand new dehydrated device - const dehydratedDeviceIds = await getDehydratedDeviceIds(app.client); - expect(dehydratedDeviceIds.length).toBe(1); - expect(dehydratedDeviceIds[0]).not.toEqual(initialDehydratedDeviceIds[0]); + await expectDehydratedDeviceEnabled(app); }); test("'Reset cryptographic identity' removes dehydrated device", async ({ page, homeserver, app, credentials }) => { diff --git a/playwright/e2e/crypto/device-verification.spec.ts b/playwright/e2e/crypto/device-verification.spec.ts index 9be79452c4..64846ac86d 100644 --- a/playwright/e2e/crypto/device-verification.spec.ts +++ b/playwright/e2e/crypto/device-verification.spec.ts @@ -169,8 +169,8 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => { // Fill the passphrase const dialog = page.locator(".mx_Dialog"); - await dialog.locator("input").fill("new passphrase"); - await dialog.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click(); + await dialog.locator("textarea").fill("new passphrase"); + await dialog.getByRole("button", { name: "Continue", disabled: false }).click(); await page.locator(".mx_AuthPage").getByRole("button", { name: "Done" }).click(); @@ -190,10 +190,9 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => { // Fill the recovery key const dialog = page.locator(".mx_Dialog"); - await dialog.getByRole("button", { name: "use your Recovery Key" }).click(); const aliceRecoveryKey = await aliceBotClient.getRecoveryKey(); - await dialog.locator("#mx_securityKey").fill(aliceRecoveryKey.encodedPrivateKey); - await dialog.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click(); + await dialog.locator("textarea").fill(aliceRecoveryKey.encodedPrivateKey); + await dialog.getByRole("button", { name: "Continue", disabled: false }).click(); await page.locator(".mx_AuthPage").getByRole("button", { name: "Done" }).click(); diff --git a/playwright/e2e/crypto/event-shields.spec.ts b/playwright/e2e/crypto/event-shields.spec.ts index f73f190e10..e577a66467 100644 --- a/playwright/e2e/crypto/event-shields.spec.ts +++ b/playwright/e2e/crypto/event-shields.spec.ts @@ -67,8 +67,9 @@ test.describe("Cryptography", function () { // Bob has a second, not cross-signed, device const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob); - // Dismiss the toast nagging us to set up recovery otherwise it gets in the way of clicking the room list - await page.getByRole("button", { name: "Not now" }).click(); + // Dismiss the toasts nagging us, otherwise they get in the way of clicking the room list + await page.getByRole("button", { name: "Dismiss" }).click(); + await page.getByRole("button", { name: "Yes, dismiss" }).click(); await bob.sendEvent(testRoomId, null, "m.room.encrypted", { algorithm: "m.megolm.v1.aes-sha2", diff --git a/playwright/e2e/crypto/toasts.spec.ts b/playwright/e2e/crypto/toasts.spec.ts index 5b80f822a0..7b838edaac 100644 --- a/playwright/e2e/crypto/toasts.spec.ts +++ b/playwright/e2e/crypto/toasts.spec.ts @@ -8,7 +8,8 @@ import { type GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api"; import { test, expect } from "../../element-web-test"; -import { createBot, deleteCachedSecrets, logIntoElement } from "./utils"; +import { createBot, deleteCachedSecrets, disableKeyBackup, logIntoElement } from "./utils"; +import { type Bot } from "../../pages/bot"; test.describe("Key storage out of sync toast", () => { let recoveryKey: GeneratedSecretStorageKey; @@ -53,3 +54,114 @@ test.describe("Key storage out of sync toast", () => { ).toBeVisible(); }); }); + +test.describe("'Turn on key storage' toast", () => { + let botClient: Bot | undefined; + + test.beforeEach(async ({ page, homeserver, credentials, toasts }) => { + // Set up all crypto stuff. Key storage defaults to on. + + const res = await createBot(page, homeserver, credentials); + const recoveryKey = res.recoveryKey; + botClient = res.botClient; + + await logIntoElement(page, credentials, recoveryKey.encodedPrivateKey); + + // We won't be prompted for crypto setup unless we have an e2e room, so make one + await page.getByRole("button", { name: "Add room" }).click(); + await page.getByRole("menuitem", { name: "New room" }).click(); + await page.getByRole("textbox", { name: "Name" }).fill("Test room"); + await page.getByRole("button", { name: "Create room" }).click(); + + await toasts.rejectToast("Notifications"); + }); + + test("should not show toast if key storage is on", async ({ page, toasts }) => { + // Given the default situation after signing in + // Then no toast is shown (because key storage is on) + await toasts.assertNoToasts(); + + // When we reload + await page.reload(); + + // Give the toasts time to appear + await new Promise((resolve) => setTimeout(resolve, 2000)); + + // Then still no toast is shown + await toasts.assertNoToasts(); + }); + + test("should not show toast if key storage is off because we turned it off", async ({ app, page, toasts }) => { + // Given the backup is disabled because we disabled it + await disableKeyBackup(app); + + // Then no toast is shown + await toasts.assertNoToasts(); + + // When we reload + await page.reload(); + + // Give the toasts time to appear + await new Promise((resolve) => setTimeout(resolve, 2000)); + + // Then still no toast is shown + await toasts.assertNoToasts(); + }); + + test("should show toast if key storage is off but account data is missing", async ({ app, page, toasts }) => { + // Given the backup is disabled but we didn't set account data saying that is expected + await disableKeyBackup(app); + await botClient.setAccountData("m.org.matrix.custom.backup_disabled", { disabled: false }); + + // Wait for the account data setting to stick + await new Promise((resolve) => setTimeout(resolve, 2000)); + + // When we enter the app + await page.reload(); + + // Then the toast is displayed + let toast = await toasts.getToast("Turn on key storage"); + + // And when we click "Continue" + await toast.getByRole("button", { name: "Continue" }).click(); + + // Then we see the Encryption settings dialog with an option to turn on key storage + await expect(page.getByRole("checkbox", { name: "Allow key storage" })).toBeVisible(); + + // And when we close that + await page.getByRole("button", { name: "Close dialog" }).click(); + + // Then we see the toast again + toast = await toasts.getToast("Turn on key storage"); + + // And when we click "Dismiss" + await toast.getByRole("button", { name: "Dismiss" }).click(); + + // Then we see the "are you sure?" dialog + await expect( + page.getByRole("heading", { name: "Are you sure you want to keep key storage turned off?" }), + ).toBeVisible(); + + // And when we close it by clicking away + await page.getByTestId("dialog-background").click({ force: true, position: { x: 10, y: 10 } }); + + // Then we see the toast again + toast = await toasts.getToast("Turn on key storage"); + + // And when we click Dismiss and then "Go to Settings" + await toast.getByRole("button", { name: "Dismiss" }).click(); + await page.getByRole("button", { name: "Go to Settings" }).click(); + + // Then we see Encryption settings again + await expect(page.getByRole("checkbox", { name: "Allow key storage" })).toBeVisible(); + + // And when we close that, see the toast, click Dismiss, and Yes, Dismiss + await page.getByRole("button", { name: "Close dialog" }).click(); + toast = await toasts.getToast("Turn on key storage"); + await toast.getByRole("button", { name: "Dismiss" }).click(); + await page.getByRole("button", { name: "Yes, dismiss" }).click(); + + // Then the toast is gone + await toasts.assertNoToasts(); + }); +}); diff --git a/playwright/e2e/crypto/utils.ts b/playwright/e2e/crypto/utils.ts index 77f1b747a1..289b123e86 100644 --- a/playwright/e2e/crypto/utils.ts +++ b/playwright/e2e/crypto/utils.ts @@ -228,8 +228,8 @@ export async function logIntoElement(page: Page, credentials: Credentials, secur await useSecurityKey.click(); } // Fill in the recovery key - await page.locator(".mx_Dialog").locator('input[type="password"]').fill(securityKey); - await page.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click(); + await page.locator(".mx_Dialog").locator("textarea").fill(securityKey); + await page.getByRole("button", { name: "Continue", disabled: false }).click(); await page.getByRole("button", { name: "Done" }).click(); } } @@ -263,7 +263,7 @@ export async function verifySession(app: ElementAppPage, securityKey: string) { const settings = await app.settings.openUserSettings("Encryption"); await settings.getByRole("button", { name: "Verify this device" }).click(); await app.page.getByRole("button", { name: "Verify with Recovery Key" }).click(); - await app.page.locator(".mx_Dialog").locator('input[type="password"]').fill(securityKey); + await app.page.locator(".mx_Dialog").locator("textarea").fill(securityKey); await app.page.getByRole("button", { name: "Continue", disabled: false }).click(); await app.page.getByRole("button", { name: "Done" }).click(); await app.settings.closeDialog(); @@ -316,6 +316,25 @@ export async function enableKeyBackup(app: ElementAppPage): Promise { return recoveryKey; } +/** + * Open the encryption settings and disable key storage (and recovery) + * Assumes that the current device has been verified + */ +export async function disableKeyBackup(app: ElementAppPage): Promise { + const encryptionTab = await app.settings.openUserSettings("Encryption"); + + const keyStorageToggle = encryptionTab.getByRole("checkbox", { name: "Allow key storage" }); + if (await keyStorageToggle.isChecked()) { + await encryptionTab.getByRole("checkbox", { name: "Allow key storage" }).click(); + await encryptionTab.getByRole("button", { name: "Delete key storage" }).click(); + await encryptionTab.getByRole("checkbox", { name: "Allow key storage" }).isVisible(); + + // Wait for the update to account data to stick + await new Promise((resolve) => setTimeout(resolve, 2000)); + } + await app.settings.closeDialog(); +} + /** * Go through the "Set up Secure Backup" dialog (aka the `CreateSecretStorageDialog`). * diff --git a/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts b/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts index 21d22d8ccd..d3aa060a27 100644 --- a/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts +++ b/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts @@ -5,8 +5,11 @@ * Please see LICENSE files in the repository root for full details. */ +import { type Visibility } from "matrix-js-sdk/src/matrix"; +import { type Locator, type Page } from "@playwright/test"; + import { expect, test } from "../../../element-web-test"; -import type { Page } from "@playwright/test"; +import { SettingLevel } from "../../../../src/settings/SettingLevel"; test.describe("Room list filters and sort", () => { test.use({ @@ -18,10 +21,14 @@ test.describe("Room list filters and sort", () => { labsFlags: ["feature_new_room_list"], }); - function getPrimaryFilters(page: Page) { + function getPrimaryFilters(page: Page): Locator { return page.getByRole("listbox", { name: "Room list filters" }); } + function getRoomOptionsMenu(page: Page): Locator { + return page.getByRole("button", { name: "Room Options" }); + } + /** * Get the room list * @param page @@ -35,6 +42,65 @@ test.describe("Room list filters and sort", () => { await app.closeNotificationToast(); }); + test("Tombstoned rooms are not shown even when they receive updates", async ({ page, app, bot }) => { + // This bug shows up with this setting turned on + await app.settings.setValue("Spaces.allRoomsInHome", null, SettingLevel.DEVICE, true); + + /* + We will first create a room named 'Old Room' and will invite the bot user to this room. + We will also send a simple message in this room. + */ + const oldRoomId = await app.client.createRoom({ name: "Old Room" }); + await app.client.inviteUser(oldRoomId, bot.credentials.userId); + await bot.joinRoom(oldRoomId); + const response = await app.client.sendMessage(oldRoomId, "Hello!"); + + /* + At this point, we haven't done anything interesting. + So we expect 'Old Room' to show up in the room list. + */ + const roomListView = getRoomList(page); + const oldRoomTile = roomListView.getByRole("gridcell", { name: "Open room Old Room" }); + await expect(oldRoomTile).toBeVisible(); + + /* + Now let's tombstone 'Old Room'. + First we create a new room ('New Room') with the predecessor set to the old room.. + */ + const newRoomId = await bot.createRoom({ + name: "New Room", + creation_content: { + predecessor: { + event_id: response.event_id, + room_id: oldRoomId, + }, + }, + visibility: "public" as Visibility, + }); + + /* + Now we can send the tombstone event itself to the 'Old Room'. + */ + await app.client.sendStateEvent(oldRoomId, "m.room.tombstone", { + body: "This room has been replaced", + replacement_room: newRoomId, + }); + + // Let's join the replaced room. + await app.client.joinRoom(newRoomId); + + // We expect 'Old Room' to be hidden from the room list. + await expect(oldRoomTile).not.toBeVisible(); + + /* + Let's say some user in the 'Old Room' changes their display name. + This will send events to the all the rooms including 'Old Room'. + Nevertheless, the replaced room should not be shown in the room list. + */ + await bot.setDisplayName("MyNewName"); + await expect(oldRoomTile).not.toBeVisible(); + }); + test.describe("Scroll behaviour", () => { test("should scroll to the top of list when filter is applied and active room is not in filtered list", async ({ page, @@ -106,6 +172,38 @@ test.describe("Room list filters and sort", () => { await app.client.evaluate(async (client, favouriteId) => { await client.setRoomTag(favouriteId, "m.favourite", { order: 0.5 }); }, favouriteId); + + const lowPrioId = await app.client.createRoom({ name: "Low prio room" }); + await app.client.evaluate(async (client, id) => { + await client.setRoomTag(id, "m.lowpriority", { order: 0.5 }); + }, lowPrioId); + + await bot.createRoom({ + name: "invited room", + invite: [user.userId], + is_direct: true, + }); + + const mentionRoomId = await app.client.createRoom({ name: "room with mention" }); + await app.client.inviteUser(mentionRoomId, bot.credentials.userId); + await bot.joinRoom(mentionRoomId); + + const clientBot = await bot.prepareClient(); + await clientBot.evaluate( + async (client, { mentionRoomId, userId }) => { + await client.sendMessage(mentionRoomId, { + // @ts-ignore ignore usage of MsgType.text + "msgtype": "m.text", + "body": "User", + "format": "org.matrix.custom.html", + "formatted_body": `User`, + "m.mentions": { + user_ids: [userId], + }, + }); + }, + { mentionRoomId, userId: user.userId }, + ); }); test("should filter the list (with primary filters)", { tag: "@screenshot" }, async ({ page, app, user }) => { @@ -122,7 +220,7 @@ test.describe("Room list filters and sort", () => { // only one room should be visible await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible(); await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible(); - expect(await roomList.locator("role=gridcell").count()).toBe(2); + expect(await roomList.locator("role=gridcell").count()).toBe(4); await expect(primaryFilters).toMatchScreenshot("unread-primary-filters.png"); await primaryFilters.getByRole("option", { name: "Favourite" }).click(); @@ -131,37 +229,71 @@ test.describe("Room list filters and sort", () => { await primaryFilters.getByRole("option", { name: "People" }).click(); await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible(); - expect(await roomList.locator("role=gridcell").count()).toBe(1); + await expect(roomList.getByRole("gridcell", { name: "invited room" })).toBeVisible(); + expect(await roomList.locator("role=gridcell").count()).toBe(2); await primaryFilters.getByRole("option", { name: "Rooms" }).click(); await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible(); await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible(); await expect(roomList.getByRole("gridcell", { name: "empty room" })).toBeVisible(); - expect(await roomList.locator("role=gridcell").count()).toBe(3); + await expect(roomList.getByRole("gridcell", { name: "room with mention" })).toBeVisible(); + await expect(roomList.getByRole("gridcell", { name: "Low prio room" })).toBeVisible(); + expect(await roomList.locator("role=gridcell").count()).toBe(5); + + await primaryFilters.getByRole("option", { name: "Mentions" }).click(); + await expect(roomList.getByRole("gridcell", { name: "room with mention" })).toBeVisible(); + expect(await roomList.locator("role=gridcell").count()).toBe(1); + + await primaryFilters.getByRole("option", { name: "Invites" }).click(); + await expect(roomList.getByRole("gridcell", { name: "invited room" })).toBeVisible(); + expect(await roomList.locator("role=gridcell").count()).toBe(1); }); - test("unread filter should only match unread rooms that have a count", async ({ page, app, bot }) => { + test( + "unread filter should only match unread rooms that have a count", + { tag: "@screenshot" }, + async ({ page, app, bot }) => { + const roomListView = getRoomList(page); + + // Let's configure unread dm room so that we only get notification for mentions and keywords + await app.viewRoomById(unReadDmId); + await app.settings.openRoomSettings("Notifications"); + await page.getByText("@mentions & keywords").click(); + await app.settings.closeDialog(); + + // Let's open a room other than unread room or unread dm + await roomListView.getByRole("gridcell", { name: "Open room favourite room" }).click(); + + // Let's make the bot send a new message in both rooms + await bot.sendMessage(unReadDmId, "Hello!"); + await bot.sendMessage(unReadRoomId, "Hello!"); + + // Let's activate the unread filter now + await page.getByRole("option", { name: "Unread" }).click(); + + // Unread filter should only show unread room and not unread dm! + const unreadDm = roomListView.getByRole("gridcell", { name: "Open room unread room" }); + await expect(unreadDm).toBeVisible(); + await expect(unreadDm).toMatchScreenshot("unread-dm.png"); + await expect(roomListView.getByRole("gridcell", { name: "Open room unread dm" })).not.toBeVisible(); + }, + ); + + test("should sort the room list alphabetically", async ({ page }) => { const roomListView = getRoomList(page); - // Let's configure unread dm room so that we only get notification for mentions and keywords - await app.viewRoomById(unReadDmId); - await app.settings.openRoomSettings("Notifications"); - await page.getByText("@mentions & keywords").click(); - await app.settings.closeDialog(); + await getRoomOptionsMenu(page).click(); + await page.getByRole("menuitemradio", { name: "A-Z" }).click(); - // Let's open a room other than unread room or unread dm - await roomListView.getByRole("gridcell", { name: "Open room favourite room" }).click(); + await expect(roomListView.getByRole("gridcell").first()).toHaveText(/empty room/); + }); + + test("should move room to the top on message when sorting by activity", async ({ page, bot }) => { + const roomListView = getRoomList(page); - // Let's make the bot send a new message in both rooms await bot.sendMessage(unReadDmId, "Hello!"); - await bot.sendMessage(unReadRoomId, "Hello!"); - // Let's activate the unread filter now - await page.getByRole("option", { name: "Unread" }).click(); - - // Unread filter should only show unread room and not unread dm! - await expect(roomListView.getByRole("gridcell", { name: "Open room unread room" })).toBeVisible(); - await expect(roomListView.getByRole("gridcell", { name: "Open room unread dm" })).not.toBeVisible(); + await expect(roomListView.getByRole("gridcell").first()).toHaveText(/unread dm/); }); }); @@ -184,15 +316,25 @@ test.describe("Room list filters and sort", () => { }, ); - test("should render the placeholder for unread filter", { tag: "@screenshot" }, async ({ page, app, user }) => { - const primaryFilters = getPrimaryFilters(page); - await primaryFilters.getByRole("option", { name: "Unread" }).click(); + [ + { filter: "Unreads", action: "Show all chats" }, + { filter: "Mentions", action: "See all activity" }, + { filter: "Invites", action: "See all activity" }, + ].forEach(({ filter, action }) => { + test( + `should render the placeholder for ${filter} filter`, + { tag: "@screenshot" }, + async ({ page, app, user }) => { + const primaryFilters = getPrimaryFilters(page); + await primaryFilters.getByRole("option", { name: filter }).click(); - const emptyRoomList = getEmptyRoomList(page); - await expect(emptyRoomList).toMatchScreenshot("unread-empty-room-list.png"); + const emptyRoomList = getEmptyRoomList(page); + await expect(emptyRoomList).toMatchScreenshot(`${filter}-empty-room-list.png`); - await emptyRoomList.getByRole("button", { name: "show all chats" }).click(); - await expect(primaryFilters.getByRole("option", { name: "Unread" })).not.toBeChecked(); + await emptyRoomList.getByRole("button", { name: action }).click(); + await expect(primaryFilters.getByRole("option", { name: filter })).not.toBeChecked(); + }, + ); }); ["People", "Rooms", "Favourite"].forEach((filter) => { diff --git a/playwright/e2e/left-panel/room-list-panel/room-list-panel.spec.ts b/playwright/e2e/left-panel/room-list-panel/room-list-panel.spec.ts index 2ffbc608c2..8ca138a707 100644 --- a/playwright/e2e/left-panel/room-list-panel/room-list-panel.spec.ts +++ b/playwright/e2e/left-panel/room-list-panel/room-list-panel.spec.ts @@ -30,6 +30,9 @@ test.describe("Room list panel", () => { for (let i = 0; i < 20; i++) { await app.client.createRoom({ name: `room${i}` }); } + + // focus the user menu to avoid to have hover decoration + await page.getByRole("button", { name: "User menu" }).focus(); }); test("should render the room list panel", { tag: "@screenshot" }, async ({ page, app, user }) => { @@ -38,4 +41,10 @@ test.describe("Room list panel", () => { await expect(roomListView.getByRole("gridcell", { name: "Open room room19" })).toBeVisible(); await expect(roomListView).toMatchScreenshot("room-list-panel.png"); }); + + test("should respond to small screen sizes", { tag: "@screenshot" }, async ({ page }) => { + await page.setViewportSize({ width: 575, height: 600 }); + const roomListPanel = page.getByTestId("room-list-panel"); + await expect(roomListPanel).toMatchScreenshot("room-list-panel-smallscreen.png"); + }); }); diff --git a/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts b/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts index 8af053fd54..d9e1922934 100644 --- a/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts +++ b/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts @@ -29,6 +29,9 @@ test.describe("Room list", () => { test.beforeEach(async ({ page, app, user }) => { // The notification toast is displayed above the search section await app.closeNotificationToast(); + + // focus the user menu to avoid to have hover decoration + await page.getByRole("button", { name: "User menu" }).focus(); }); test.describe("Room list", () => { @@ -43,7 +46,8 @@ test.describe("Room list", () => { await expect(roomListView.getByRole("gridcell", { name: "Open room room29" })).toBeVisible(); await expect(roomListView).toMatchScreenshot("room-list.png"); - await roomListView.hover(); + // Put focus on the room list + await roomListView.getByRole("gridcell", { name: "Open room room29" }).click(); // Scroll to the end of the room list await page.mouse.wheel(0, 1000); await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible(); @@ -105,13 +109,10 @@ test.describe("Room list", () => { // It should make the room muted await page.getByRole("menuitem", { name: "Mute room" }).click(); - // Remove hover on the room list item - await roomListView.hover(); - - // Scroll to the bottom of the list - await page.getByRole("grid", { name: "Room list" }).evaluate((e) => { - e.scrollTop = e.scrollHeight; - }); + // Put focus on the room list + await roomListView.getByRole("gridcell", { name: "Open room room28" }).click(); + // Scroll to the end of the room list + await page.mouse.wheel(0, 1000); // The room decoration should have the muted icon await expect(roomItem.getByTestId("notification-decoration")).toBeVisible(); @@ -129,7 +130,8 @@ test.describe("Room list", () => { test("should scroll to the current room", async ({ page, app, user }) => { const roomListView = getRoomList(page); - await roomListView.hover(); + // Put focus on the room list + await roomListView.getByRole("gridcell", { name: "Open room room29" }).click(); // Scroll to the end of the room list await page.mouse.wheel(0, 1000); @@ -142,6 +144,98 @@ test.describe("Room list", () => { await filters.getByRole("option", { name: "People" }).click(); await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible(); }); + + test.describe("Shortcuts", () => { + test("should select the next room", async ({ page, app, user }) => { + const roomListView = getRoomList(page); + await roomListView.getByRole("gridcell", { name: "Open room room29" }).click(); + await page.keyboard.press("Alt+ArrowDown"); + + await expect(page.getByRole("heading", { name: "room28", level: 1 })).toBeVisible(); + }); + + test("should select the previous room", async ({ page, app, user }) => { + const roomListView = getRoomList(page); + await roomListView.getByRole("gridcell", { name: "Open room room28" }).click(); + await page.keyboard.press("Alt+ArrowUp"); + + await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible(); + }); + + test("should select the last room", async ({ page, app, user }) => { + const roomListView = getRoomList(page); + await roomListView.getByRole("gridcell", { name: "Open room room29" }).click(); + await page.keyboard.press("Alt+ArrowUp"); + + await expect(page.getByRole("heading", { name: "room0", level: 1 })).toBeVisible(); + }); + + test("should select the next unread room", async ({ page, app, user, bot }) => { + const roomListView = getRoomList(page); + + const roomId = await app.client.createRoom({ name: "1 notification" }); + await app.client.inviteUser(roomId, bot.credentials.userId); + await bot.joinRoom(roomId); + await bot.sendMessage(roomId, "I am a robot. Beep."); + + await roomListView.getByRole("gridcell", { name: "Open room room20" }).click(); + + await page.keyboard.press("Alt+Shift+ArrowDown"); + + await expect(page.getByRole("heading", { name: "1 notification", level: 1 })).toBeVisible(); + }); + }); + + test.describe("Keyboard navigation", () => { + test("should navigate to the room list", async ({ page, app, user }) => { + const roomListView = getRoomList(page); + + const room29 = roomListView.getByRole("gridcell", { name: "Open room room29" }); + const room28 = roomListView.getByRole("gridcell", { name: "Open room room28" }); + + // open the room + await room29.click(); + // put focus back on the room list item + await room29.click(); + await expect(room29).toBeFocused(); + + await page.keyboard.press("ArrowDown"); + await expect(room28).toBeFocused(); + await expect(room29).not.toBeFocused(); + + await page.keyboard.press("ArrowUp"); + await expect(room29).toBeFocused(); + await expect(room28).not.toBeFocused(); + }); + + test("should navigate to the notification menu", async ({ page, app, user }) => { + const roomListView = getRoomList(page); + const room29 = roomListView.getByRole("gridcell", { name: "Open room room29" }); + const moreButton = room29.getByRole("button", { name: "More options" }); + const notificationButton = room29.getByRole("button", { name: "Notification options" }); + + await room29.click(); + // put focus back on the room list item + await room29.click(); + await page.keyboard.press("Tab"); + await expect(moreButton).toBeFocused(); + await page.keyboard.press("Tab"); + await expect(notificationButton).toBeFocused(); + + // Open the menu + await notificationButton.click(); + // Wait for the menu to be open + await expect(page.getByRole("menuitem", { name: "Match default settings" })).toHaveAttribute( + "aria-selected", + "true", + ); + + // Close the menu + await page.keyboard.press("Escape"); + // Focus should be back on the room list item + await expect(room29).toBeFocused(); + }); + }); }); test.describe("Avatar decoration", () => { @@ -150,6 +244,10 @@ test.describe("Room list", () => { test("should be a public room", { tag: "@screenshot" }, async ({ page, app, user }) => { // @ts-ignore Visibility enum is not accessible await app.client.createRoom({ name: "public room", visibility: "public" }); + + // focus the user menu to avoid to have hover decoration + await page.getByRole("button", { name: "User menu" }).focus(); + const roomListView = getRoomList(page); const publicRoom = roomListView.getByRole("gridcell", { name: "public room" }); @@ -165,6 +263,10 @@ test.describe("Room list", () => { const roomListView = getRoomList(page); const videoRoom = roomListView.getByRole("gridcell", { name: "video room" }); + + // focus the user menu to avoid to have hover decoration + await page.getByRole("button", { name: "User menu" }).focus(); + await expect(videoRoom).toBeVisible(); await expect(videoRoom).toMatchScreenshot("room-list-item-video.png"); }); @@ -230,6 +332,26 @@ test.describe("Room list", () => { await expect(room).toMatchScreenshot("room-list-item-mention.png"); }); + test("should render a message preview", { tag: "@screenshot" }, async ({ page, app, user, bot }) => { + const roomListView = getRoomList(page); + + await page.getByRole("button", { name: "Room Options" }).click(); + await page.getByRole("menuitemcheckbox", { name: "Show message previews" }).click(); + + const roomId = await app.client.createRoom({ name: "activity" }); + + // focus the user menu to avoid to have hover decoration + await page.getByRole("button", { name: "User menu" }).focus(); + + await app.client.inviteUser(roomId, bot.credentials.userId); + await bot.joinRoom(roomId); + await bot.sendMessage(roomId, "I am a robot. Beep."); + + const room = roomListView.getByRole("gridcell", { name: "activity" }); + await expect(room.getByText("I am a robot. Beep.")).toBeVisible(); + await expect(room).toMatchScreenshot("room-list-item-message-preview.png"); + }); + test("should render an activity decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => { const roomListView = getRoomList(page); @@ -269,8 +391,8 @@ test.describe("Room list", () => { await room.getByRole("button", { name: "More Options" }).click(); await page.getByRole("menuitem", { name: "mark as unread" }).click(); - // Remove hover on the room list item - await roomListView.hover(); + // focus the user menu to avoid to have hover decoration + await page.getByRole("button", { name: "User menu" }).focus(); await expect(room).toMatchScreenshot("room-list-item-mark-as-unread.png"); }); diff --git a/playwright/e2e/login/login-consent.spec.ts b/playwright/e2e/login/login-consent.spec.ts index c4a4b1409f..23baf023fa 100644 --- a/playwright/e2e/login/login-consent.spec.ts +++ b/playwright/e2e/login/login-consent.spec.ts @@ -288,6 +288,43 @@ test.describe("Login", () => { await expect(h1).toBeVisible(); }); }); + + test("Can reset identity to become verified", async ({ page, homeserver, request, credentials }) => { + // Log in + const res = await request.post(`${homeserver.baseUrl}/_matrix/client/v3/keys/device_signing/upload`, { + headers: { Authorization: `Bearer ${credentials.accessToken}` }, + data: DEVICE_SIGNING_KEYS_BODY, + }); + if (!res.ok()) { + console.log(`Uploading dummy keys failed with HTTP status ${res.status}`, await res.json()); + throw new Error("Uploading dummy keys failed"); + } + + await page.goto("/"); + await login(page, homeserver, credentials); + + await expect(page.getByRole("heading", { name: "Verify this device", level: 1 })).toBeVisible(); + + // Start the reset process + await page.getByRole("button", { name: "Proceed with reset" }).click(); + + // First try cancelling and restarting + await page.getByRole("button", { name: "Cancel" }).click(); + await page.getByRole("button", { name: "Proceed with reset" }).click(); + + // Then click outside the dialog and restart + await page.getByRole("link", { name: "Powered by Matrix" }).click({ force: true }); + await page.getByRole("button", { name: "Proceed with reset" }).click(); + + // Finally we actually continue + await page.getByRole("button", { name: "Continue" }).click(); + await page.getByPlaceholder("Password").fill(credentials.password); + await page.getByRole("button", { name: "Continue" }).click(); + + // We end up at the Home screen + await expect(page).toHaveURL(/\/#\/home$/, { timeout: 10000 }); + await expect(page.getByRole("heading", { name: "Welcome Dave", exact: true })).toBeVisible(); + }); }); }); diff --git a/playwright/e2e/modules/loader.spec.ts b/playwright/e2e/modules/loader.spec.ts index e21b5c2d92..52c7a02988 100644 --- a/playwright/e2e/modules/loader.spec.ts +++ b/playwright/e2e/modules/loader.spec.ts @@ -15,6 +15,7 @@ test.describe("Module loading", () => { test.describe("Example Module", () => { test.use({ config: { + brand: "TestBrand", modules: ["/modules/example-module.js"], }, page: async ({ page }, use) => { @@ -25,11 +26,31 @@ test.describe("Module loading", () => { }, }); - test("should show alert", async ({ page }) => { - const dialogPromise = page.waitForEvent("dialog"); - await page.goto("/"); - const dialog = await dialogPromise; - expect(dialog.message()).toBe("Testing module loading successful!"); - }); + const testCases = [ + ["en", "TestBrand module loading successful!"], + ["de", "TestBrand-Module erfolgreich geladen!"], + ]; + + for (const [lang, message] of testCases) { + test.describe(`language-${lang}`, () => { + test.use({ + config: async ({ config }, use) => { + await use({ + ...config, + setting_defaults: { + language: lang, + }, + }); + }, + }); + + test("should show alert", async ({ page }) => { + const dialogPromise = page.waitForEvent("dialog"); + await page.goto("/"); + const dialog = await dialogPromise; + expect(dialog.message()).toBe(message); + }); + }); + } }); }); diff --git a/playwright/e2e/oidc/index.ts b/playwright/e2e/oidc/index.ts index 1989e8764f..02de6e2f03 100644 --- a/playwright/e2e/oidc/index.ts +++ b/playwright/e2e/oidc/index.ts @@ -37,6 +37,8 @@ export async function registerAccountMas( await page.getByRole("textbox", { name: "6-digit code" }).fill(code); await page.getByRole("button", { name: "Continue" }).click(); + await page.getByRole("textbox", { name: "Display Name" }).fill(username); + await page.getByRole("button", { name: "Continue" }).click(); await expect(page.getByText("Allow access to your account?")).toBeVisible(); await page.getByRole("button", { name: "Continue" }).click(); } diff --git a/playwright/e2e/oidc/oidc-native.spec.ts b/playwright/e2e/oidc/oidc-native.spec.ts index 2592311554..e985e58d09 100644 --- a/playwright/e2e/oidc/oidc-native.spec.ts +++ b/playwright/e2e/oidc/oidc-native.spec.ts @@ -88,7 +88,6 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => { await expect(page.getByText("Welcome")).toBeVisible(); await page.goto("about:blank"); - // @ts-expect-error const result = await mas.manage("kill-sessions", userId); expect(result.output).toContain("Ended 1 active OAuth 2.0 session"); diff --git a/playwright/e2e/release-announcement/releaseAnnouncement.spec.ts b/playwright/e2e/release-announcement/releaseAnnouncement.spec.ts index c2ebd5b853..812b66b796 100644 --- a/playwright/e2e/release-announcement/releaseAnnouncement.spec.ts +++ b/playwright/e2e/release-announcement/releaseAnnouncement.spec.ts @@ -15,20 +15,34 @@ test.describe("Release announcement", () => { feature_release_announcement: true, }, }, - labsFlags: ["threadsActivityCentre"], + room: async ({ app, user }, use) => { + const roomId = await app.client.createRoom({ + name: "Test room", + }); + await app.viewRoomById(roomId); + await use({ roomId }); + }, }); - test("should display the release announcement process", { tag: "@screenshot" }, async ({ page, app, util }) => { - // The TAC release announcement should be displayed - await util.assertReleaseAnnouncementIsVisible("Threads Activity Centre"); - // Hide the release announcement - await util.markReleaseAnnouncementAsRead("Threads Activity Centre"); - await util.assertReleaseAnnouncementIsNotVisible("Threads Activity Centre"); + test( + "should display the pinned messages release announcement", + { tag: "@screenshot" }, + async ({ page, app, room, util }) => { + await app.toggleRoomInfoPanel(); - await page.reload(); - // Wait for EW to load - await expect(page.getByRole("navigation", { name: "Spaces" })).toBeVisible(); - // Check that once the release announcement has been marked as viewed, it does not appear again - await util.assertReleaseAnnouncementIsNotVisible("Threads Activity Centre"); - }); + const name = "All new pinned messages"; + + // The release announcement should be displayed + await util.assertReleaseAnnouncementIsVisible(name); + // Hide the release announcement + await util.markReleaseAnnouncementAsRead(name); + await util.assertReleaseAnnouncementIsNotVisible(name); + + await page.reload(); + await app.toggleRoomInfoPanel(); + await expect(page.getByRole("menuitem", { name: "Pinned messages" })).toBeVisible(); + // Check that once the release announcement has been marked as viewed, it does not appear again + await util.assertReleaseAnnouncementIsNotVisible(name); + }, + ); }); diff --git a/playwright/e2e/right-panel/right-panel.spec.ts b/playwright/e2e/right-panel/right-panel.spec.ts index 12b188c9e3..0fbd306d86 100644 --- a/playwright/e2e/right-panel/right-panel.spec.ts +++ b/playwright/e2e/right-panel/right-panel.spec.ts @@ -11,6 +11,7 @@ import { type Locator, type Page } from "@playwright/test"; import { test, expect } from "../../element-web-test"; import { checkRoomSummaryCard, viewRoomSummaryByName } from "./utils"; import { isDendrite } from "../../plugins/homeserver/dendrite"; +import { Bot } from "../../pages/bot"; const ROOM_NAME = "Test room"; const ROOM_NAME_LONG = @@ -21,20 +22,23 @@ const ROOM_NAME_LONG = "officia deserunt mollit anim id est laborum."; const SPACE_NAME = "Test space"; const NAME = "Alice"; +const LONG_NAME = "Bob long long long long long long long long long long long long long long long name"; + const ROOM_ADDRESS_LONG = "loremIpsumDolorSitAmetConsecteturAdipisicingElitSedDoEiusmodTemporIncididuntUtLaboreEtDoloreMagnaAliqua"; function getMemberTileByName(page: Page, name: string): Locator { - return page.locator(`.mx_MemberTileView, [title="${name}"]`); + return page.locator(".mx_MemberListView .mx_MemberTileView_name").filter({ hasText: name }); } test.describe("RightPanel", () => { + let testRoomId: string; test.use({ displayName: NAME, }); test.beforeEach(async ({ app, user }) => { - await app.client.createRoom({ name: ROOM_NAME }); + testRoomId = await app.client.createRoom({ name: ROOM_NAME }); await app.client.createSpace({ name: SPACE_NAME }); }); @@ -77,10 +81,12 @@ test.describe("RightPanel", () => { await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("with-leave-room.png"); }); - test("should handle clicking add widgets", async ({ page, app }) => { + test("should handle clicking add widgets", { tag: "@screenshot" }, async ({ page, app }) => { await viewRoomSummaryByName(page, app, ROOM_NAME); await page.getByRole("menuitem", { name: "Extensions" }).click(); + await expect(page.getByTestId("right-panel")).toMatchScreenshot("with-extensions.png"); + await page.getByRole("button", { name: "Add extensions" }).click(); await expect(page.locator(".mx_IntegrationManager")).toBeVisible(); }); @@ -134,6 +140,37 @@ test.describe("RightPanel", () => { await page.getByLabel("Room info").nth(1).click(); await checkRoomSummaryCard(page, ROOM_NAME); }); + + test( + "should handle viewing long room member name", + { tag: "@screenshot" }, + async ({ page, homeserver, app }) => { + const bobLongName = new Bot(page, homeserver, { displayName: LONG_NAME }); + await bobLongName.prepareClient(); + await app.client.inviteUser(testRoomId, bobLongName.credentials.userId); + await bobLongName.joinRoom(testRoomId); + + await viewRoomSummaryByName(page, app, ROOM_NAME); + + await page.locator(".mx_RightPanel").getByRole("menuitem", { name: "People" }).click(); + await expect(page.locator(".mx_MemberListView")).toBeVisible(); + + await getMemberTileByName(page, LONG_NAME).click(); + await expect(page.locator(".mx_UserInfo")).toBeVisible(); + await expect(page.locator(".mx_UserInfo_profile").getByText(LONG_NAME)).toBeVisible(); + + await expect(page.locator(".mx_UserInfo")).toMatchScreenshot("with-long-name.png", { + mask: [page.locator(".mx_UserInfo_profile_mxid")], + css: ` + /* Use monospace font for consistent mask width */ + .mx_UserInfo_profile_mxid { + font-family: Inconsolata !important; + } + `, + }); + }, + ); + test.describe("room reporting", () => { test.skip(isDendrite, "Dendrite does not implement room reporting"); test("should handle reporting a room", { tag: "@screenshot" }, async ({ page, app }) => { diff --git a/playwright/e2e/settings/encryption-user-tab/encryption-tab.spec.ts b/playwright/e2e/settings/encryption-user-tab/encryption-tab.spec.ts index 427c801ef4..ed1daecf35 100644 --- a/playwright/e2e/settings/encryption-user-tab/encryption-tab.spec.ts +++ b/playwright/e2e/settings/encryption-user-tab/encryption-tab.spec.ts @@ -19,126 +19,162 @@ import { test.describe("Encryption tab", () => { test.use({ displayName: "Alice" }); - let recoveryKey: GeneratedSecretStorageKey; - let expectedBackupVersion: string; + test.describe("when encryption is set up", () => { + let recoveryKey: GeneratedSecretStorageKey; + let expectedBackupVersion: string; - test.beforeEach(async ({ page, homeserver, credentials }) => { - // The bot bootstraps cross-signing, creates a key backup and sets up a recovery key - const res = await createBot(page, homeserver, credentials); - recoveryKey = res.recoveryKey; - expectedBackupVersion = res.expectedBackupVersion; - }); + test.beforeEach(async ({ page, homeserver, credentials }) => { + // The bot bootstraps cross-signing, creates a key backup and sets up a recovery key + const res = await createBot(page, homeserver, credentials); + recoveryKey = res.recoveryKey; + expectedBackupVersion = res.expectedBackupVersion; + }); - test( - "should show a 'Verify this device' button if the device is unverified", - { tag: "@screenshot" }, - async ({ page, app, util }) => { - const dialog = await util.openEncryptionTab(); - const content = util.getEncryptionTabContent(); + test( + "should show a 'Verify this device' button if the device is unverified", + { tag: "@screenshot" }, + async ({ page, app, util }) => { + const dialog = await util.openEncryptionTab(); + const content = util.getEncryptionTabContent(); - // The user's device is in an unverified state, therefore the only option available to them here is to verify it - const verifyButton = dialog.getByRole("button", { name: "Verify this device" }); - await expect(verifyButton).toBeVisible(); - await expect(content).toMatchScreenshot("verify-device-encryption-tab.png"); - await verifyButton.click(); + // The user's device is in an unverified state, therefore the only option available to them here is to verify it + const verifyButton = dialog.getByRole("button", { name: "Verify this device" }); + await expect(verifyButton).toBeVisible(); + await expect(content).toMatchScreenshot("verify-device-encryption-tab.png"); + await verifyButton.click(); - await util.verifyDevice(recoveryKey); + await util.verifyDevice(recoveryKey); - await expect(content).toMatchScreenshot("default-tab.png", { - mask: [content.getByTestId("deviceId"), content.getByTestId("sessionKey")], - }); + await expect(content).toMatchScreenshot("default-tab.png", { + mask: [content.getByTestId("deviceId"), content.getByTestId("sessionKey")], + }); - // Check that our device is now cross-signed - await checkDeviceIsCrossSigned(app); + // Check that our device is now cross-signed + await checkDeviceIsCrossSigned(app); - // Check that the current device is connected to key backup - // The backup decryption key should be in cache also, as we got it directly from the 4S - await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true); - }, - ); + // Check that the current device is connected to key backup + // The backup decryption key should be in cache also, as we got it directly from the 4S + await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true); + }, + ); - // Test what happens if the cross-signing secrets are in secret storage but are not cached in the local DB. - // - // This can happen if we verified another device and secret-gossiping failed, or the other device itself lacked the secrets. - // We simulate this case by deleting the cached secrets in the indexedDB. - test( - "should prompt to enter the recovery key when the secrets are not cached locally", - { tag: "@screenshot" }, - async ({ page, app, util }) => { + // Test what happens if the cross-signing secrets are in secret storage but are not cached in the local DB. + // + // This can happen if we verified another device and secret-gossiping failed, or the other device itself lacked the secrets. + // We simulate this case by deleting the cached secrets in the indexedDB. + test( + "should prompt to enter the recovery key when the secrets are not cached locally", + { tag: "@screenshot" }, + async ({ page, app, util }) => { + await verifySession(app, recoveryKey.encodedPrivateKey); + // We need to delete the cached secrets + await deleteCachedSecrets(page); + + await util.openEncryptionTab(); + // We ask the user to enter the recovery key + const dialog = util.getEncryptionTabContent(); + const enterKeyButton = dialog.getByRole("button", { name: "Enter recovery key" }); + await expect(enterKeyButton).toBeVisible(); + await expect(dialog).toMatchScreenshot("out-of-sync-recovery.png"); + await enterKeyButton.click(); + + // Fill the recovery key + await util.enterRecoveryKey(recoveryKey); + await expect(dialog).toMatchScreenshot("default-tab.png", { + mask: [dialog.getByTestId("deviceId"), dialog.getByTestId("sessionKey")], + }); + + // Check that our device is now cross-signed + await checkDeviceIsCrossSigned(app); + + // Check that the current device is connected to key backup + // The backup decryption key should be in cache also, as we got it directly from the 4S + await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true); + }, + ); + + test("should display the reset identity panel when the user clicks on 'Forgot recovery key?'", async ({ + page, + app, + util, + }) => { await verifySession(app, recoveryKey.encodedPrivateKey); // We need to delete the cached secrets await deleteCachedSecrets(page); + // The "Key storage is out sync" section is displayed and the user click on the "Forgot recovery key?" button await util.openEncryptionTab(); - // We ask the user to enter the recovery key const dialog = util.getEncryptionTabContent(); - const enterKeyButton = dialog.getByRole("button", { name: "Enter recovery key" }); - await expect(enterKeyButton).toBeVisible(); - await expect(dialog).toMatchScreenshot("out-of-sync-recovery.png"); - await enterKeyButton.click(); + await dialog.getByRole("button", { name: "Forgot recovery key?" }).click(); - // Fill the recovery key - await util.enterRecoveryKey(recoveryKey); - await expect(dialog).toMatchScreenshot("default-tab.png", { - mask: [dialog.getByTestId("deviceId"), dialog.getByTestId("sessionKey")], - }); + // The user is prompted to reset their identity + await expect( + dialog.getByText("Forgot your recovery key? You’ll need to reset your identity."), + ).toBeVisible(); + }); - // Check that our device is now cross-signed - await checkDeviceIsCrossSigned(app); + test("should warn before turning off key storage", { tag: "@screenshot" }, async ({ page, app, util }) => { + await verifySession(app, recoveryKey.encodedPrivateKey); + await util.openEncryptionTab(); - // Check that the current device is connected to key backup - // The backup decryption key should be in cache also, as we got it directly from the 4S - await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true); - }, - ); + await page.getByRole("checkbox", { name: "Allow key storage" }).click(); - test("should display the reset identity panel when the user clicks on 'Forgot recovery key?'", async ({ - page, - app, - util, - }) => { - await verifySession(app, recoveryKey.encodedPrivateKey); - // We need to delete the cached secrets - await deleteCachedSecrets(page); + await expect( + page.getByRole("heading", { name: "Are you sure you want to turn off key storage and delete it?" }), + ).toBeVisible(); - // The "Key storage is out sync" section is displayed and the user click on the "Forgot recovery key?" button - await util.openEncryptionTab(); - const dialog = util.getEncryptionTabContent(); - await dialog.getByRole("button", { name: "Forgot recovery key?" }).click(); + await expect(util.getEncryptionTabContent()).toMatchScreenshot("delete-key-storage-confirm.png"); - // The user is prompted to reset their identity - await expect(dialog.getByText("Forgot your recovery key? You’ll need to reset your identity.")).toBeVisible(); + const deleteRequestPromises = [ + page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.master")), + page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.self_signing")), + page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.user_signing")), + page.waitForRequest((req) => req.url().endsWith("/account_data/m.megolm_backup.v1")), + page.waitForRequest((req) => req.url().endsWith("/account_data/m.secret_storage.default_key")), + page.waitForRequest((req) => req.url().includes("/account_data/m.secret_storage.key.")), + ]; + + await page.getByRole("button", { name: "Delete key storage" }).click(); + + await expect(page.getByRole("checkbox", { name: "Allow key storage" })).not.toBeChecked(); + + for (const prom of deleteRequestPromises) { + const request = await prom; + expect(request.method()).toBe("PUT"); + expect(request.postData()).toBe(JSON.stringify({})); + } + }); }); - test("should warn before turning off key storage", { tag: "@screenshot" }, async ({ page, app, util }) => { - await verifySession(app, recoveryKey.encodedPrivateKey); - await util.openEncryptionTab(); + test.describe("when encryption is not set up", () => { + test("'Verify this device' allows us to become verified", async ({ + page, + user, + credentials, + app, + }, workerInfo) => { + const settings = await app.settings.openUserSettings("Encryption"); - await page.getByRole("checkbox", { name: "Allow key storage" }).click(); + // Initially, our device is not verified + await expect(settings.getByRole("heading", { name: "Device not verified" })).toBeVisible(); - await expect( - page.getByRole("heading", { name: "Are you sure you want to turn off key storage and delete it?" }), - ).toBeVisible(); + // We will reset our identity + await settings.getByRole("button", { name: "Verify this device" }).click(); + await page.getByRole("button", { name: "Proceed with reset" }).click(); - await expect(util.getEncryptionTabContent()).toMatchScreenshot("delete-key-storage-confirm.png"); + // First try cancelling and restarting + await page.getByRole("button", { name: "Cancel" }).click(); + await page.getByRole("button", { name: "Proceed with reset" }).click(); - const deleteRequestPromises = [ - page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.master")), - page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.self_signing")), - page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.user_signing")), - page.waitForRequest((req) => req.url().endsWith("/account_data/m.megolm_backup.v1")), - page.waitForRequest((req) => req.url().endsWith("/account_data/m.secret_storage.default_key")), - page.waitForRequest((req) => req.url().includes("/account_data/m.secret_storage.key.")), - ]; + // Then click outside the dialog and restart + await page.locator("li").filter({ hasText: "Encryption" }).click({ force: true }); + await page.getByRole("button", { name: "Proceed with reset" }).click(); - await page.getByRole("button", { name: "Delete key storage" }).click(); + // Finally we actually continue + await page.getByRole("button", { name: "Continue" }).click(); - await expect(page.getByRole("checkbox", { name: "Allow key storage" })).not.toBeChecked(); - - for (const prom of deleteRequestPromises) { - const request = await prom; - expect(request.method()).toBe("PUT"); - expect(request.postData()).toBe(JSON.stringify({})); - } + // Now we are verified, so we see the Key storage toggle + await expect(settings.getByRole("heading", { name: "Key storage" })).toBeVisible(); + }); }); }); diff --git a/playwright/e2e/share-dialog/share-by-url.spec.ts b/playwright/e2e/share-dialog/share-by-url.spec.ts new file mode 100644 index 0000000000..c5b9174782 --- /dev/null +++ b/playwright/e2e/share-dialog/share-by-url.spec.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { test, expect } from "../../element-web-test"; + +test.describe("share from URL", () => { + test.use({ + displayName: "Alice", + room: async ({ app }, use) => { + const roomId = await app.client.createRoom({ name: "A test room" }); + await use({ roomId }); + }, + }); + + test("should share message when users navigates to share URL", async ({ page, user, room, app }) => { + await page.goto("/#/share?msg=Hello+world"); + // The forward message dialog doesn't update as new infomation arrives via sync, which means sometimes + // this is just says, "Empty room". For the same reason, we can't reliably write a test for loading the + // app straight away with a /#/share url as the room doesn't appear until the client syncs.] + // Ideally we should fix the forward dialog to update and eliminate races, until then, there is only one + // room so we click the first button. + await page.getByRole("listitem" /*, { name: "A test room" }*/).getByRole("button", { name: "Send" }).click(); + await page.keyboard.press("Escape"); + await app.viewRoomByName("A test room"); + const lastMessage = page.locator(".mx_RoomView_MessageList .mx_EventTile_last"); + await expect(lastMessage).toBeVisible(); + const lastMessageText = await lastMessage.locator(".mx_EventTile_body").innerText(); + await expect(lastMessageText).toBe("Hello world"); + }); +}); diff --git a/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts b/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts index 683577dce4..eec28099a5 100644 --- a/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts +++ b/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts @@ -19,7 +19,6 @@ test.describe("Threads Activity Centre", { tag: "@no-firefox" }, () => { test.use({ displayName: "Alice", botCreateOpts: { displayName: "Other User" }, - labsFlags: ["threadsActivityCentre"], }); test( diff --git a/playwright/e2e/timeline/media-preview-settings.spec.ts b/playwright/e2e/timeline/media-preview-settings.spec.ts new file mode 100644 index 0000000000..e32a7dbc82 --- /dev/null +++ b/playwright/e2e/timeline/media-preview-settings.spec.ts @@ -0,0 +1,139 @@ +/* +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 * as fs from "node:fs"; +import { type EventType, type MsgType, type RoomJoinRulesEventContent } from "matrix-js-sdk/src/types"; + +import { test, expect } from "../../element-web-test"; + +const MEDIA_FILE = fs.readFileSync("playwright/sample-files/riot.png"); + +test.describe("Media preview settings", () => { + test.use({ + displayName: "Alan", + botCreateOpts: { + displayName: "Bob", + }, + room: async ({ app, page, homeserver, bot, user }, use) => { + const mxc = (await bot.uploadContent(MEDIA_FILE, { name: "image.png", type: "image/png" })).content_uri; + const roomId = await bot.createRoom({ + name: "Test room", + invite: [user.userId], + initial_state: [{ type: "m.room.avatar", content: { url: mxc }, state_key: "" }], + }); + await bot.sendEvent(roomId, null, "m.room.message" as EventType, { + msgtype: "m.image" as MsgType, + body: "image.png", + url: mxc, + }); + + await use({ roomId }); + }, + }); + + test("should be able to hide avatars of inviters", { tag: "@screenshot" }, async ({ page, app, room, user }) => { + let settings = await app.settings.openUserSettings("Preferences"); + await settings.getByLabel("Hide avatars of room and inviter").click(); + await app.closeDialog(); + await app.viewRoomById(room.roomId); + await expect( + page.getByRole("complementary").filter({ hasText: "Do you want to join Test room" }), + ).toMatchScreenshot("invite-no-avatar.png", { + // Hide the mxid, which is not stable. + css: ` + .mx_RoomPreviewBar_inviter_mxid { + display: none !important; + } + `, + }); + await expect( + page.getByRole("tree", { name: "Rooms" }).getByRole("treeitem", { name: "Test room" }), + ).toMatchScreenshot("invite-room-tree-no-avatar.png"); + + // And then go back to being visible + settings = await app.settings.openUserSettings("Preferences"); + await settings.getByLabel("Hide avatars of room and inviter").click(); + await app.closeDialog(); + await page.goto("#/home"); + await app.viewRoomById(room.roomId); + await expect( + page.getByRole("complementary").filter({ hasText: "Do you want to join Test room" }), + ).toMatchScreenshot("invite-with-avatar.png", { + // Hide the mxid, which is not stable. + css: ` + .mx_RoomPreviewBar_inviter_mxid { + display: none !important; + } + `, + }); + await expect( + page.getByRole("tree", { name: "Rooms" }).getByRole("treeitem", { name: "Test room" }), + ).toMatchScreenshot("invite-room-tree-with-avatar.png"); + }); + + test("should be able to hide media in rooms globally", async ({ page, app, room, user }) => { + const settings = await app.settings.openUserSettings("Preferences"); + await settings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always hide" }).click(); + await app.closeDialog(); + await app.viewRoomById(room.roomId); + await page.getByRole("button", { name: "Accept" }).click(); + await expect(page.getByText("Show image")).toBeVisible(); + }); + test("should be able to hide media in non-private rooms globally", async ({ page, app, room, user, bot }) => { + await bot.sendStateEvent(room.roomId, "m.room.join_rules", { + join_rule: "public", + }); + const settings = await app.settings.openUserSettings("Preferences"); + await settings.getByLabel("Show media in timeline").getByLabel("In private rooms").click(); + await app.closeDialog(); + await app.viewRoomById(room.roomId); + await page.getByRole("button", { name: "Accept" }).click(); + await expect(page.getByText("Show image")).toBeVisible(); + for (const joinRule of ["invite", "knock", "restricted"] as RoomJoinRulesEventContent["join_rule"][]) { + await bot.sendStateEvent(room.roomId, "m.room.join_rules", { + join_rule: joinRule, + } satisfies RoomJoinRulesEventContent); + await expect(page.getByText("Show image")).not.toBeVisible(); + } + }); + test("should be able to show media in rooms globally", async ({ page, app, room, user }) => { + const settings = await app.settings.openUserSettings("Preferences"); + await settings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always show" }).click(); + await app.closeDialog(); + await app.viewRoomById(room.roomId); + await page.getByRole("button", { name: "Accept" }).click(); + await expect(page.getByText("Show image")).not.toBeVisible(); + }); + test("should be able to hide media in an individual room", async ({ page, app, room, user }) => { + const settings = await app.settings.openUserSettings("Preferences"); + await settings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always show" }).click(); + await app.closeDialog(); + + await app.viewRoomById(room.roomId); + await page.getByRole("button", { name: "Accept" }).click(); + + const roomSettings = await app.settings.openRoomSettings("General"); + await roomSettings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always hide" }).click(); + await app.closeDialog(); + + await expect(page.getByText("Show image")).toBeVisible(); + }); + test("should be able to show media in an individual room", async ({ page, app, room, user }) => { + const settings = await app.settings.openUserSettings("Preferences"); + await settings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always hide" }).click(); + await app.closeDialog(); + + await app.viewRoomById(room.roomId); + await page.getByRole("button", { name: "Accept" }).click(); + + const roomSettings = await app.settings.openRoomSettings("General"); + await roomSettings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always show" }).click(); + await app.closeDialog(); + + await expect(page.getByText("Show image")).not.toBeVisible(); + }); +}); diff --git a/playwright/sample-files/example-module.js b/playwright/sample-files/example-module.js index cb9b80a93b..561dea5fd3 100644 --- a/playwright/sample-files/example-module.js +++ b/playwright/sample-files/example-module.js @@ -6,11 +6,19 @@ Please see LICENSE files in the repository root for full details. */ export default class ExampleModule { - static moduleApiVersion = "^0.1.0"; + static moduleApiVersion = "^1.0.0"; constructor(api) { this.api = api; + + this.api.i18n.register({ + key: { + en: "%(brand)s module loading successful!", + de: "%(brand)s-Module erfolgreich geladen!", + }, + }); } async load() { - alert("Testing module loading successful!"); + const brand = this.api.config.get("brand"); + alert(this.api.i18n.translate("key", { brand })); } } diff --git a/playwright/snapshots/app-loading/feature-detection.spec.ts/unsupported-browser-linux.png b/playwright/snapshots/app-loading/feature-detection.spec.ts/unsupported-browser-linux.png index 9a7760bfd0..6a72dec4e2 100644 Binary files a/playwright/snapshots/app-loading/feature-detection.spec.ts/unsupported-browser-linux.png and b/playwright/snapshots/app-loading/feature-detection.spec.ts/unsupported-browser-linux.png differ diff --git a/playwright/snapshots/chat-export/html-export.spec.ts/html-export-linux.png b/playwright/snapshots/chat-export/html-export.spec.ts/html-export-linux.png index 7a108b441c..26c506097a 100644 Binary files a/playwright/snapshots/chat-export/html-export.spec.ts/html-export-linux.png and b/playwright/snapshots/chat-export/html-export.spec.ts/html-export-linux.png differ diff --git a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-with-user-pill-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-with-user-pill-linux.png index 95b10570a0..1a66050e5f 100644 Binary files a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-with-user-pill-linux.png and b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-with-user-pill-linux.png differ diff --git a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-without-user-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-without-user-linux.png index 8f2a488c00..86cb11aad9 100644 Binary files a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-without-user-linux.png and b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-dm-without-user-linux.png differ diff --git a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png index 9bc23433a5..043e35b6be 100644 Binary files a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png and b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-with-user-pill-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/Favourite-empty-room-list-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/Favourite-empty-room-list-linux.png index 8cf4a3c97b..6c71c11b1d 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/Favourite-empty-room-list-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/Favourite-empty-room-list-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/Invites-empty-room-list-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/Invites-empty-room-list-linux.png new file mode 100644 index 0000000000..c7396da41d Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/Invites-empty-room-list-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/Mentions-empty-room-list-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/Mentions-empty-room-list-linux.png new file mode 100644 index 0000000000..15620d3612 Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/Mentions-empty-room-list-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/People-empty-room-list-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/People-empty-room-list-linux.png index 3f700037cb..7b73e0b819 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/People-empty-room-list-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/People-empty-room-list-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/Rooms-empty-room-list-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/Rooms-empty-room-list-linux.png index e01564eeb4..eb3add5733 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/Rooms-empty-room-list-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/Rooms-empty-room-list-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/Unreads-empty-room-list-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/Unreads-empty-room-list-linux.png new file mode 100644 index 0000000000..70ed4bb782 Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/Unreads-empty-room-list-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/default-empty-room-list-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/default-empty-room-list-linux.png index 45d2a775ea..6781c1d364 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/default-empty-room-list-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/default-empty-room-list-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/filter-menu-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/filter-menu-linux.png new file mode 100644 index 0000000000..fe056af3d3 Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/filter-menu-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/room-panel-empty-room-list-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/room-panel-empty-room-list-linux.png index 6bc9d4696e..34924cf69f 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/room-panel-empty-room-list-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/room-panel-empty-room-list-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unread-dm-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unread-dm-linux.png new file mode 100644 index 0000000000..2f12ee4e41 Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unread-dm-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unread-empty-room-list-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unread-empty-room-list-linux.png deleted file mode 100644 index 94b09ac14f..0000000000 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unread-empty-room-list-linux.png and /dev/null differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unread-primary-filters-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unread-primary-filters-linux.png index fd60bb3d53..f0cda0b577 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unread-primary-filters-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unread-primary-filters-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unselected-primary-filters-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unselected-primary-filters-linux.png index 1cff3fea46..7d63f923b0 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unselected-primary-filters-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unselected-primary-filters-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-header-compose-menu-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-header-compose-menu-linux.png index 8382d3b184..9f501a58d4 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-header-compose-menu-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-header-compose-menu-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-header-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-header-linux.png index 6c871b6b9c..46ff1a53be 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-header-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-header-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-header-space-menu-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-header-space-menu-linux.png index 49cf6ef08a..c706e71b0f 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-header-space-menu-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-header-space-menu-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-space-header-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-space-header-linux.png index bdb7f8fa2a..3a4aea566e 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-space-header-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-space-header-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-linux.png index 6f2daa3017..9b130b73c4 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-smallscreen-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-smallscreen-linux.png new file mode 100644 index 0000000000..92b81245a2 Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-smallscreen-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-activity-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-activity-linux.png index 2a75d0d91c..aa73d79988 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-activity-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-activity-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-hover-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-hover-linux.png index f4aa4d56a9..fba408c922 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-hover-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-hover-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-hover-silent-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-hover-silent-linux.png index 3a2c6783fa..36b7304a01 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-hover-silent-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-hover-silent-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-invited-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-invited-linux.png index 4d9fd3ccd2..1f2b691b4a 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-invited-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-invited-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-mark-as-unread-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-mark-as-unread-linux.png index cfa08c2008..310912e50d 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-mark-as-unread-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-mark-as-unread-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-mention-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-mention-linux.png index 28d5719733..9fa531f5b1 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-mention-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-mention-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-message-preview-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-message-preview-linux.png new file mode 100644 index 0000000000..dac349eb2d Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-message-preview-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-notification-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-notification-linux.png index 60f4916270..144604ffeb 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-notification-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-notification-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png index 6257aa7af8..99bb312695 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-linux.png index cad0f83b4a..c91ebf3b3a 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-selection-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-selection-linux.png index fb8aabd6bf..4b3ac052ca 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-selection-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-selection-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-public-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-public-linux.png index e83fee3ed2..e951f77ef2 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-public-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-public-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-silent-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-silent-linux.png index 72f0c42425..57c5cb1eb7 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-silent-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-silent-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-video-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-video-linux.png index 41881886f6..16ea458274 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-video-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-video-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-linux.png index 1f7a84c9a6..f2b625f498 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-scrolled-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-scrolled-linux.png index 87c88e8e46..a1752e30d3 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-scrolled-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-scrolled-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-rtldisplayname-linux.png index 4bf016b825..35523b7db8 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/basic-message-ltr-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-rtldisplayname-linux.png index ee69990523..40a096409e 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/basic-message-rtl-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-rtldisplayname-linux.png index eea2b47469..557613e7e6 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/edited-message-ltr-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-rtldisplayname-linux.png index 4d2e9c593d..bd26e84628 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/edited-message-rtl-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-ltr-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-ltr-rtldisplayname-linux.png index 1bb3582578..055ad23a81 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/emote-ltr-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/emote-ltr-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-rtldisplayname-linux.png index 0e65830b51..cfb905b689 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/emote-rich-ltr-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-rtldisplayname-linux.png index 26bd9f7523..ea29e98a75 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/emote-rich-rtl-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/emote-rtl-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/emote-rtl-rtldisplayname-linux.png index 3f50a1406f..d84780f530 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/emote-rtl-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/emote-rtl-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/reply-message-ltr-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/reply-message-ltr-rtldisplayname-linux.png index 37096f025c..b59d960f4f 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/reply-message-ltr-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/reply-message-ltr-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-rtldisplayname-linux.png b/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-rtldisplayname-linux.png index 52ced56c83..f6eaea241a 100644 Binary files a/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-rtldisplayname-linux.png and b/playwright/snapshots/messages/messages.spec.ts/reply-message-trl-rtldisplayname-linux.png differ diff --git a/playwright/snapshots/oidc/oidc-native.spec.ts/token-expired-linux.png b/playwright/snapshots/oidc/oidc-native.spec.ts/token-expired-linux.png index 17479bb870..e85b83f2f1 100644 Binary files a/playwright/snapshots/oidc/oidc-native.spec.ts/token-expired-linux.png and b/playwright/snapshots/oidc/oidc-native.spec.ts/token-expired-linux.png differ diff --git a/playwright/snapshots/polls/polls.spec.ts/Polls-Timeline-tile-no-votes-linux.png b/playwright/snapshots/polls/polls.spec.ts/Polls-Timeline-tile-no-votes-linux.png index 6818add73e..7b7f296be1 100644 Binary files a/playwright/snapshots/polls/polls.spec.ts/Polls-Timeline-tile-no-votes-linux.png and b/playwright/snapshots/polls/polls.spec.ts/Polls-Timeline-tile-no-votes-linux.png differ diff --git a/playwright/snapshots/register/register.spec.ts/email-prompt-linux.png b/playwright/snapshots/register/register.spec.ts/email-prompt-linux.png index dd458f3157..9bc2ed101b 100644 Binary files a/playwright/snapshots/register/register.spec.ts/email-prompt-linux.png and b/playwright/snapshots/register/register.spec.ts/email-prompt-linux.png differ diff --git a/playwright/snapshots/register/register.spec.ts/server-picker-linux.png b/playwright/snapshots/register/register.spec.ts/server-picker-linux.png index 16b686e101..db3ec188d1 100644 Binary files a/playwright/snapshots/register/register.spec.ts/server-picker-linux.png and b/playwright/snapshots/register/register.spec.ts/server-picker-linux.png differ diff --git a/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-All-new-pinned-messages-linux.png b/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-All-new-pinned-messages-linux.png new file mode 100644 index 0000000000..f466c17d64 Binary files /dev/null and b/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-All-new-pinned-messages-linux.png differ diff --git a/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-Threads-Activity-Centre-linux.png b/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-Threads-Activity-Centre-linux.png deleted file mode 100644 index fee99165ab..0000000000 Binary files a/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-Threads-Activity-Centre-linux.png and /dev/null differ diff --git a/playwright/snapshots/right-panel/file-panel.spec.ts/empty-linux.png b/playwright/snapshots/right-panel/file-panel.spec.ts/empty-linux.png index c1fced1938..2dbd40ffd9 100644 Binary files a/playwright/snapshots/right-panel/file-panel.spec.ts/empty-linux.png and b/playwright/snapshots/right-panel/file-panel.spec.ts/empty-linux.png differ diff --git a/playwright/snapshots/right-panel/right-panel.spec.ts/room-report-dialog-linux.png b/playwright/snapshots/right-panel/right-panel.spec.ts/room-report-dialog-linux.png index 3beae421d4..627071591c 100644 Binary files a/playwright/snapshots/right-panel/right-panel.spec.ts/room-report-dialog-linux.png and b/playwright/snapshots/right-panel/right-panel.spec.ts/room-report-dialog-linux.png differ diff --git a/playwright/snapshots/right-panel/right-panel.spec.ts/with-extensions-linux.png b/playwright/snapshots/right-panel/right-panel.spec.ts/with-extensions-linux.png new file mode 100644 index 0000000000..8333d55b46 Binary files /dev/null and b/playwright/snapshots/right-panel/right-panel.spec.ts/with-extensions-linux.png differ diff --git a/playwright/snapshots/right-panel/right-panel.spec.ts/with-long-name-linux.png b/playwright/snapshots/right-panel/right-panel.spec.ts/with-long-name-linux.png new file mode 100644 index 0000000000..9a3479a1e6 Binary files /dev/null and b/playwright/snapshots/right-panel/right-panel.spec.ts/with-long-name-linux.png differ diff --git a/playwright/snapshots/room/invites.spec.ts/Invites-reject-dialog-linux.png b/playwright/snapshots/room/invites.spec.ts/Invites-reject-dialog-linux.png index 71f3e420ab..22ef895d1a 100644 Binary files a/playwright/snapshots/room/invites.spec.ts/Invites-reject-dialog-linux.png and b/playwright/snapshots/room/invites.spec.ts/Invites-reject-dialog-linux.png differ diff --git a/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-linux.png b/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-linux.png index e0d8b80639..3b4031063c 100644 Binary files a/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-linux.png and b/playwright/snapshots/settings/account-user-settings-tab.spec.ts/account-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png index 88d7919dcc..20518942b0 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png index cc3c32ee95..a847075a4d 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-12px-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/message-layout-panel.spec.ts/message-layout-panel-bubble-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/message-layout-panel.spec.ts/message-layout-panel-bubble-linux.png index 6e0cc3e06f..a913d4ab19 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/message-layout-panel.spec.ts/message-layout-panel-bubble-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/message-layout-panel.spec.ts/message-layout-panel-bubble-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/message-layout-panel.spec.ts/message-layout-panel-modern-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/message-layout-panel.spec.ts/message-layout-panel-modern-linux.png index 8e29fd26b8..b2167e0a12 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/message-layout-panel.spec.ts/message-layout-panel-modern-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/message-layout-panel.spec.ts/message-layout-panel-modern-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-dark-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-dark-linux.png index eb249d0b24..1acda5968e 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-dark-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/theme-panel-dark-linux.png differ diff --git a/playwright/snapshots/settings/encryption-user-tab/advanced.spec.ts/encryption-details-linux.png b/playwright/snapshots/settings/encryption-user-tab/advanced.spec.ts/encryption-details-linux.png index 6a95f36da7..e18821b774 100644 Binary files a/playwright/snapshots/settings/encryption-user-tab/advanced.spec.ts/encryption-details-linux.png and b/playwright/snapshots/settings/encryption-user-tab/advanced.spec.ts/encryption-details-linux.png differ diff --git a/playwright/snapshots/settings/encryption-user-tab/advanced.spec.ts/reset-cryptographic-identity-linux.png b/playwright/snapshots/settings/encryption-user-tab/advanced.spec.ts/reset-cryptographic-identity-linux.png index 18213b5375..6e0b105c24 100644 Binary files a/playwright/snapshots/settings/encryption-user-tab/advanced.spec.ts/reset-cryptographic-identity-linux.png and b/playwright/snapshots/settings/encryption-user-tab/advanced.spec.ts/reset-cryptographic-identity-linux.png differ diff --git a/playwright/snapshots/settings/encryption-user-tab/encryption-tab.spec.ts/default-tab-linux.png b/playwright/snapshots/settings/encryption-user-tab/encryption-tab.spec.ts/default-tab-linux.png index bf57e8c875..c999365534 100644 Binary files a/playwright/snapshots/settings/encryption-user-tab/encryption-tab.spec.ts/default-tab-linux.png and b/playwright/snapshots/settings/encryption-user-tab/encryption-tab.spec.ts/default-tab-linux.png differ diff --git a/playwright/snapshots/settings/encryption-user-tab/encryption-tab.spec.ts/delete-key-storage-confirm-linux.png b/playwright/snapshots/settings/encryption-user-tab/encryption-tab.spec.ts/delete-key-storage-confirm-linux.png index 10ece913d4..237c853a1b 100644 Binary files a/playwright/snapshots/settings/encryption-user-tab/encryption-tab.spec.ts/delete-key-storage-confirm-linux.png and b/playwright/snapshots/settings/encryption-user-tab/encryption-tab.spec.ts/delete-key-storage-confirm-linux.png differ diff --git a/playwright/snapshots/settings/encryption-user-tab/encryption-tab.spec.ts/out-of-sync-recovery-linux.png b/playwright/snapshots/settings/encryption-user-tab/encryption-tab.spec.ts/out-of-sync-recovery-linux.png index ba4ccfe18e..0ad192f2c2 100644 Binary files a/playwright/snapshots/settings/encryption-user-tab/encryption-tab.spec.ts/out-of-sync-recovery-linux.png and b/playwright/snapshots/settings/encryption-user-tab/encryption-tab.spec.ts/out-of-sync-recovery-linux.png differ diff --git a/playwright/snapshots/settings/encryption-user-tab/encryption-tab.spec.ts/verify-device-encryption-tab-linux.png b/playwright/snapshots/settings/encryption-user-tab/encryption-tab.spec.ts/verify-device-encryption-tab-linux.png index dcc1f25008..0bb5fad763 100644 Binary files a/playwright/snapshots/settings/encryption-user-tab/encryption-tab.spec.ts/verify-device-encryption-tab-linux.png and b/playwright/snapshots/settings/encryption-user-tab/encryption-tab.spec.ts/verify-device-encryption-tab-linux.png differ diff --git a/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/change-key-1-encryption-tab-linux.png b/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/change-key-1-encryption-tab-linux.png index 8d6f2a74f4..a42cb394d7 100644 Binary files a/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/change-key-1-encryption-tab-linux.png and b/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/change-key-1-encryption-tab-linux.png differ diff --git a/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/change-key-2-encryption-tab-linux.png b/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/change-key-2-encryption-tab-linux.png index e8def33311..ec0207a227 100644 Binary files a/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/change-key-2-encryption-tab-linux.png and b/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/change-key-2-encryption-tab-linux.png differ diff --git a/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/default-recovery-linux.png b/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/default-recovery-linux.png index 9c23f7ea20..1f34a3f2e0 100644 Binary files a/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/default-recovery-linux.png and b/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/default-recovery-linux.png differ diff --git a/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-1-encryption-tab-linux.png b/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-1-encryption-tab-linux.png index a1f286ac73..3835cfe727 100644 Binary files a/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-1-encryption-tab-linux.png and b/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-1-encryption-tab-linux.png differ diff --git a/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-2-encryption-tab-linux.png b/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-2-encryption-tab-linux.png index 0d8aa341c0..6c3e9eab2c 100644 Binary files a/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-2-encryption-tab-linux.png and b/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-2-encryption-tab-linux.png differ diff --git a/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-3-encryption-tab-linux.png b/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-3-encryption-tab-linux.png index 00a96ce522..cb0bc78e00 100644 Binary files a/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-3-encryption-tab-linux.png and b/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-key-3-encryption-tab-linux.png differ diff --git a/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-recovery-linux.png b/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-recovery-linux.png index e7dcea9436..e26d001a90 100644 Binary files a/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-recovery-linux.png and b/playwright/snapshots/settings/encryption-user-tab/recovery.spec.ts/set-up-recovery-linux.png differ diff --git a/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png b/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png index 833dc3a8c7..c326c3f8e4 100644 Binary files a/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png and b/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png differ diff --git a/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png b/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png index 91d402ed8a..5f92c00fc3 100644 Binary files a/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png and b/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png differ diff --git a/playwright/snapshots/settings/quick-settings-menu.spec.ts/quick-settings-linux.png b/playwright/snapshots/settings/quick-settings-menu.spec.ts/quick-settings-linux.png index b40d183830..c6605b5b64 100644 Binary files a/playwright/snapshots/settings/quick-settings-menu.spec.ts/quick-settings-linux.png and b/playwright/snapshots/settings/quick-settings-menu.spec.ts/quick-settings-linux.png differ diff --git a/playwright/snapshots/settings/security-user-settings-tab.spec.ts/Security-user-settings-tab-with-posthog-enable-b5d89-csLearnMoreDialog-should-be-rendered-properly-1-linux.png b/playwright/snapshots/settings/security-user-settings-tab.spec.ts/Security-user-settings-tab-with-posthog-enable-b5d89-csLearnMoreDialog-should-be-rendered-properly-1-linux.png index 748bdd5c21..9cd18bee3e 100644 Binary files a/playwright/snapshots/settings/security-user-settings-tab.spec.ts/Security-user-settings-tab-with-posthog-enable-b5d89-csLearnMoreDialog-should-be-rendered-properly-1-linux.png and b/playwright/snapshots/settings/security-user-settings-tab.spec.ts/Security-user-settings-tab-with-posthog-enable-b5d89-csLearnMoreDialog-should-be-rendered-properly-1-linux.png differ diff --git a/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-no-avatar-linux.png b/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-no-avatar-linux.png new file mode 100644 index 0000000000..377282a2c8 Binary files /dev/null and b/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-no-avatar-linux.png differ diff --git a/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-room-tree-no-avatar-linux.png b/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-room-tree-no-avatar-linux.png new file mode 100644 index 0000000000..29d129f73f Binary files /dev/null and b/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-room-tree-no-avatar-linux.png differ diff --git a/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-room-tree-with-avatar-linux.png b/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-room-tree-with-avatar-linux.png new file mode 100644 index 0000000000..452f08d3e2 Binary files /dev/null and b/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-room-tree-with-avatar-linux.png differ diff --git a/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-with-avatar-linux.png b/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-with-avatar-linux.png new file mode 100644 index 0000000000..93459507d0 Binary files /dev/null and b/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-with-avatar-linux.png differ diff --git a/playwright/snapshots/user-menu/user-menu.spec.ts/user-menu-linux.png b/playwright/snapshots/user-menu/user-menu.spec.ts/user-menu-linux.png index 73c9ce49ac..79335b66ae 100644 Binary files a/playwright/snapshots/user-menu/user-menu.spec.ts/user-menu-linux.png and b/playwright/snapshots/user-menu/user-menu.spec.ts/user-menu-linux.png differ diff --git a/playwright/snapshots/user-view/user-view.spec.ts/user-info-linux.png b/playwright/snapshots/user-view/user-view.spec.ts/user-info-linux.png index dc832f31c9..a4f6a476f6 100644 Binary files a/playwright/snapshots/user-view/user-view.spec.ts/user-info-linux.png and b/playwright/snapshots/user-view/user-view.spec.ts/user-info-linux.png differ diff --git a/res/apple-app-site-association b/res/apple-app-site-association index 94869effab..0235e6ada1 100644 --- a/res/apple-app-site-association +++ b/res/apple-app-site-association @@ -5,8 +5,7 @@ "appIDs": [ "7J4U792NQT.im.vector.app", "7J4U792NQT.io.element.elementx", - "7J4U792NQT.io.element.elementx.nightly", - "7J4U792NQT.io.element.elementx.pr" + "7J4U792NQT.io.element.elementx.nightly" ], "components": [ { @@ -28,8 +27,7 @@ "apps": [ "7J4U792NQT.im.vector.app", "7J4U792NQT.io.element.elementx", - "7J4U792NQT.io.element.elementx.nightly", - "7J4U792NQT.io.element.elementx.pr" + "7J4U792NQT.io.element.elementx.nightly" ] } } diff --git a/res/css/_common.pcss b/res/css/_common.pcss index 75180013f6..3eed8c93c6 100644 --- a/res/css/_common.pcss +++ b/res/css/_common.pcss @@ -593,6 +593,7 @@ legend { .mx_Dialog button:not( .mx_EncryptionUserSettingsTab button, + .mx_EncryptionCard button, .mx_UserProfileSettings button, .mx_ShareDialog button, .mx_UnpinAllDialog button, @@ -600,6 +601,7 @@ legend { .mx_Dialog_nonDialogButton, .mx_AccessibleButton, .mx_IdentityServerPicker button, + .mx_AccessSecretStorageDialog button, [class|="maplibregl"] ), .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton, .mx_AccessibleButton), diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 0c04a6751d..e212163784 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -131,6 +131,7 @@ @import "./views/dialogs/_BugReportDialog.pcss"; @import "./views/dialogs/_ChangelogDialog.pcss"; @import "./views/dialogs/_CompoundDialog.pcss"; +@import "./views/dialogs/_ConfirmKeyStorageOffDialog.pcss"; @import "./views/dialogs/_ConfirmSpaceUserActionDialog.pcss"; @import "./views/dialogs/_ConfirmUserActionDialog.pcss"; @import "./views/dialogs/_CreateRoomDialog.pcss"; @@ -280,6 +281,8 @@ @import "./views/rooms/RoomListPanel/_RoomListPanel.pcss"; @import "./views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss"; @import "./views/rooms/RoomListPanel/_RoomListSearch.pcss"; +@import "./views/rooms/RoomListPanel/_RoomListSecondaryFilters.pcss"; +@import "./views/rooms/RoomListPanel/_RoomListSkeleton.pcss"; @import "./views/rooms/_AppsDrawer.pcss"; @import "./views/rooms/_Autocomplete.pcss"; @import "./views/rooms/_AuxPanel.pcss"; @@ -379,6 +382,7 @@ @import "./views/settings/tabs/user/_AppearanceUserSettingsTab.pcss"; @import "./views/settings/tabs/user/_HelpUserSettingsTab.pcss"; @import "./views/settings/tabs/user/_KeyboardUserSettingsTab.pcss"; +@import "./views/settings/tabs/user/_MediaPreviewAccountSettings.pcss"; @import "./views/settings/tabs/user/_MjolnirUserSettingsTab.pcss"; @import "./views/settings/tabs/user/_PreferencesUserSettingsTab.pcss"; @import "./views/settings/tabs/user/_SecurityUserSettingsTab.pcss"; diff --git a/res/css/structures/_LeftPanel.pcss b/res/css/structures/_LeftPanel.pcss index c76fd5da02..c1886b6b80 100644 --- a/res/css/structures/_LeftPanel.pcss +++ b/res/css/structures/_LeftPanel.pcss @@ -28,6 +28,12 @@ Please see LICENSE files in the repository root for full details. --collapsedWidth: 68px; } +.mx_LeftPanel_newRoomList { + /* Thew new rooms list is not designed to be collapsed to just icons. */ + /* 224 + 68(spaces bar) was deemed by design to be a good minimum for the left panel. */ + --collapsedWidth: 224px; +} + .mx_LeftPanel_wrapper { display: flex; flex-direction: row; diff --git a/res/css/structures/_SpacePanel.pcss b/res/css/structures/_SpacePanel.pcss index 3436e5b3a7..64044c4c5c 100644 --- a/res/css/structures/_SpacePanel.pcss +++ b/res/css/structures/_SpacePanel.pcss @@ -30,6 +30,11 @@ Please see LICENSE files in the repository root for full details. width: 68px; } + &.newUi { + background-color: var(--cpd-color-bg-canvas-default); + border-right: 1px solid var(--cpd-color-bg-subtle-primary); + } + .mx_SpacePanel_toggleCollapse { position: absolute; width: 18px; @@ -399,6 +404,11 @@ Please see LICENSE files in the repository root for full details. display: block; } } + + &.newUi .mx_UserMenu { + margin-top: var(--cpd-space-4x); + border-bottom: none; + } } .mx_SpacePanel_contextMenu { diff --git a/res/css/structures/_ToastContainer.pcss b/res/css/structures/_ToastContainer.pcss index 0ab0816392..cf1d7fd4a9 100644 --- a/res/css/structures/_ToastContainer.pcss +++ b/res/css/structures/_ToastContainer.pcss @@ -79,6 +79,11 @@ Please see LICENSE files in the repository root for full details. background-color: $primary-content; } + &.mx_Toast_icon_key_storage::after { + mask-image: url("@vector-im/compound-design-tokens/icons/settings-solid.svg"); + background-color: $primary-content; + } + &.mx_Toast_icon_labs::after { mask-image: url("$(res)/img/element-icons/flask.svg"); background-color: $secondary-content; diff --git a/res/css/views/dialogs/_ConfirmKeyStorageOffDialog.pcss b/res/css/views/dialogs/_ConfirmKeyStorageOffDialog.pcss new file mode 100644 index 0000000000..5ac53c7b70 --- /dev/null +++ b/res/css/views/dialogs/_ConfirmKeyStorageOffDialog.pcss @@ -0,0 +1,16 @@ +/* +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. +*/ + +.mx_ConfirmKeyStorageOffDialog { + .mx_Dialog_border { + width: 600px; + } + + .mx_EncryptionCard { + text-align: center; + } +} diff --git a/res/css/views/dialogs/_SpotlightDialog.pcss b/res/css/views/dialogs/_SpotlightDialog.pcss index d00acd6786..592431c2f1 100644 --- a/res/css/views/dialogs/_SpotlightDialog.pcss +++ b/res/css/views/dialogs/_SpotlightDialog.pcss @@ -412,7 +412,8 @@ Please see LICENSE files in the repository root for full details. .mx_SpotlightDialog_joinRoomAlias, .mx_SpotlightDialog_explorePublicRooms, .mx_SpotlightDialog_explorePublicSpaces, - .mx_SpotlightDialog_startGroupChat { + .mx_SpotlightDialog_startGroupChat, + .mx_SpotlightDialog_searchMessages { padding-left: $spacing-32; position: relative; @@ -451,22 +452,14 @@ Please see LICENSE files in the repository root for full details. mask-image: url("$(res)/img/element-icons/group-members.svg"); } + .mx_SpotlightDialog_searchMessages::before { + mask-image: url("$(res)/img/element-icons/room/search-inset.svg"); + } + .mx_SpotlightDialog_otherSearches_messageSearchText { font-size: $font-15px; line-height: $font-24px; } - - .mx_SpotlightDialog_otherSearches_messageSearchIcon { - display: inline-block; - width: 24px; - height: 24px; - background-color: $secondary-content; - vertical-align: text-bottom; - mask-repeat: no-repeat; - mask-position: center; - mask-size: contain; - mask-image: url("$(res)/img/element-icons/room/search-inset.svg"); - } } .mx_SpotlightDialog_result_details { diff --git a/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss b/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss index 83b9fe96b4..943ec3a41f 100644 --- a/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss +++ b/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss @@ -7,62 +7,14 @@ Please see LICENSE files in the repository root for full details. */ .mx_AccessSecretStorageDialog { - .mx_AccessSecretStorageDialog_titleWithIcon { - &::before { - content: ""; - display: inline-block; - width: 24px; - height: 24px; - margin-inline-end: $spacing-8; - position: relative; - top: 5px; - background-color: $primary-content; - } - - &.mx_AccessSecretStorageDialog_resetBadge::before { - /* The image isn't capable of masking, so we use a background instead. */ - background-image: url("@vector-im/compound-design-tokens/icons/error-solid.svg"); - background-size: 24px; - background-color: transparent; - } - - &.mx_AccessSecretStorageDialog_secureBackupTitle::before { - mask-image: url("$(res)/img/feather-customised/secure-backup.svg"); - } - - &.mx_AccessSecretStorageDialog_securePhraseTitle::before { - mask-image: url("$(res)/img/feather-customised/secure-phrase.svg"); - } + &.mx_EncryptionCard { + /* override some styles that we don't need */ + border: 0px none; + box-shadow: none; + padding: 0px; } .mx_AccessSecretStorageDialog_primaryContainer { - .mx_AccessSecretStorageDialog_passPhraseInput { - width: 300px; - border: 1px solid $accent; - border-radius: 5px; - } - - .mx_AccessSecretStorageDialog_keyStatus { - height: 30px; - } - - .mx_AccessSecretStorageDialog_recoveryKeyEntry { - display: flex; - align-items: center; - - .mx_AccessSecretStorageDialog_recoveryKeyEntry_textInput { - flex-grow: 1; - } - - .mx_AccessSecretStorageDialog_recoveryKeyEntry_entryControlSeparatorText { - margin: $spacing-16; - } - - .mx_AccessSecretStorageDialog_recoveryKeyEntry_fileInput { - display: none; - } - } - .mx_AccessSecretStorageDialog_recoveryKeyFeedback { &::before { content: ""; @@ -76,15 +28,6 @@ Please see LICENSE files in the repository root for full details. margin-inline-end: 5px; } - &.mx_AccessSecretStorageDialog_recoveryKeyFeedback--valid { - color: $accent; - - &::before { - mask-image: url("@vector-im/compound-design-tokens/icons/check.svg"); - background-color: $accent; - } - } - &.mx_AccessSecretStorageDialog_recoveryKeyFeedback--invalid { color: $alert; @@ -94,46 +37,9 @@ Please see LICENSE files in the repository root for full details. } } } + } - .mx_Dialog_buttons { - $spacingStart: $spacing-24; /* 16px icon + 8px padding */ - - text-align: initial; - display: flex; - flex-flow: column; - gap: 14px; - - .mx_Dialog_buttons_additive { - float: none; - - .mx_AccessSecretStorageDialog_reset { - position: relative; - padding-inline-start: $spacingStart; - /* To avoid bold styling inherent with elements */ - font-weight: inherit; - - &::before { - content: ""; - display: inline-block; - position: absolute; - height: 16px; - width: 16px; - left: 0; - top: 2px; /* alignment */ - background-image: url("@vector-im/compound-design-tokens/icons/error-solid.svg"); - background-size: contain; - } - - .mx_AccessSecretStorageDialog_reset_link { - color: $alert; - } - } - } - - .mx_Dialog_buttons_row { - gap: $spacing-16; /* TODO: needs normalization */ - padding-inline-start: $spacingStart; - } - } + .mx_EncryptionCard_buttons { + margin-top: var(--cpd-space-20x); } } diff --git a/res/css/views/right_panel/_ExtensionsCard.pcss b/res/css/views/right_panel/_ExtensionsCard.pcss index 0dbfc056cd..c98fa3e9dc 100644 --- a/res/css/views/right_panel/_ExtensionsCard.pcss +++ b/res/css/views/right_panel/_ExtensionsCard.pcss @@ -7,12 +7,11 @@ Please see LICENSE files in the repository root for full details. */ .mx_ExtensionsCard { - --cpd-separator-inset: var(--cpd-space-4x); - --cpd-separator-spacing: var(--cpd-space-4x); - + --cpd-separator-spacing: var(--cpd-space-6x); + --AddExtension-overlap: -76px; .mx_AutoHideScrollbar { padding: 0 var(--cpd-space-4x); - margin-top: var(--cpd-space-3x); + margin-top: var(--cpd-space-6x); box-sizing: border-box; /* Styling for the "Add extensions" button */ @@ -128,6 +127,11 @@ Please see LICENSE files in the repository root for full details. .mx_EmptyState::before { /* Overlap the Add extensions button */ - top: -76px; + top: var(--AddExtension-overlap); + } + + .mx_EmptyState { + /* Stop empty state scrolling */ + height: calc(100% + var(--AddExtension-overlap)); } } diff --git a/res/css/views/right_panel/_UserInfo.pcss b/res/css/views/right_panel/_UserInfo.pcss index 7fccd6e2d1..3030b93c03 100644 --- a/res/css/views/right_panel/_UserInfo.pcss +++ b/res/css/views/right_panel/_UserInfo.pcss @@ -109,6 +109,15 @@ Please see LICENSE files in the repository root for full details. font-size: $font-20px; line-height: $font-25px; + /* E2E icon wrapper */ + .mx_Flex > span { + display: inline-block; + } + } + + .mx_UserInfo_profile_name { + min-height: 30px; + /* limit to 2 lines, show an ellipsis if it overflows */ /* this looks webkit specific but is supported by Firefox 68+ */ display: -webkit-box; @@ -118,15 +127,6 @@ Please see LICENSE files in the repository root for full details. overflow: hidden; word-break: break-all; text-overflow: ellipsis; - - /* E2E icon wrapper */ - .mx_Flex > span { - display: inline-block; - } - } - - .mx_UserInfo_profile_name { - height: 30px; } .mx_UserInfo_profile_mxid { diff --git a/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss b/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss index ee228ec262..ac58a69bef 100644 --- a/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss +++ b/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss @@ -7,23 +7,19 @@ /** * The RoomListItemView has the following structure: - * button----------------------------------------| - * | <-12px-> container--------------------------| - * | | room avatar <-12px-> content-----| - * | | | room_name | - * | | | ----------| <-- border - * |---------------------------------------------| + * button--------------------------------------------------| + * | <-12px-> container------------------------------------| + * | | room avatar <-8px-> content----------------| + * | | | room_name <- 20px ->| + * | | | --------------------| <-- border + * |-------------------------------------------------------| */ .mx_RoomListItemView { all: unset; cursor: pointer; - &:hover { - background-color: var(--cpd-color-bg-action-secondary-hovered); - } - .mx_RoomListItemView_container { - padding-left: var(--cpd-space-2x); + padding-left: var(--cpd-space-3x); font: var(--cpd-font-body-md-regular); height: 100%; @@ -34,40 +30,47 @@ border-bottom: var(--cpd-border-width-0-5) solid var(--cpd-color-bg-subtle-secondary); box-sizing: border-box; min-width: 0; + padding-right: var(--cpd-space-5x); + + .mx_RoomListItemView_text { + min-width: 0; + } .mx_RoomListItemView_roomName { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + + .mx_RoomListItemView_messagePreview { + font: var(--cpd-font-body-sm-regular); + color: var(--cpd-color-text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } } } } -.mx_RoomListItemView_menu_open { +.mx_RoomListItemView_hover { background-color: var(--cpd-color-bg-action-secondary-hovered); +} - .mx_RoomListItemView_content { - padding-right: var(--cpd-space-1-5x); - } +.mx_RoomListItemView_menu_open .mx_RoomListItemView_container .mx_RoomListItemView_content { + /** + * The figma uses 16px padding (--cpd-space-4x) but due to https://github.com/element-hq/compound-web/issues/331 + * the icon size of the menu is 18px instead of 20px with a different internal padding + * We need to use 18px to align the icon with the others icons + * 18px is not available in compound spacing + */ + padding-right: 18px; } .mx_RoomListItemView_selected { background-color: var(--cpd-color-bg-action-secondary-pressed); } -.mx_RoomListItemView_notification_decoration { - .mx_RoomListItemView_content { - padding-right: var(--cpd-space-2x); - } -} - -.mx_RoomListItemView_empty { - .mx_RoomListItemView_content { - padding-right: var(--cpd-space-3x); - } -} - .mx_RoomListItemView_bold .mx_RoomListItemView_roomName { font: var(--cpd-font-body-md-semibold); } diff --git a/res/css/views/rooms/RoomListPanel/_RoomListSearch.pcss b/res/css/views/rooms/RoomListPanel/_RoomListSearch.pcss index 8a97086df8..f6f98e1efe 100644 --- a/res/css/views/rooms/RoomListPanel/_RoomListSearch.pcss +++ b/res/css/views/rooms/RoomListPanel/_RoomListSearch.pcss @@ -21,6 +21,7 @@ flex: 1; font: var(--cpd-font-body-md-regular); color: var(--cpd-color-text-secondary); + min-width: 0; span { flex: 1; @@ -28,6 +29,17 @@ kbd { font-family: inherit; } + + /* Shrink and truncate the search text */ + white-space: nowrap; + overflow: hidden; + .mx_RoomListSearch_search_text { + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-align: start; + } } } diff --git a/res/css/views/rooms/RoomListPanel/_RoomListSecondaryFilters.pcss b/res/css/views/rooms/RoomListPanel/_RoomListSecondaryFilters.pcss new file mode 100644 index 0000000000..0fa8dc12ae --- /dev/null +++ b/res/css/views/rooms/RoomListPanel/_RoomListSecondaryFilters.pcss @@ -0,0 +1,12 @@ +/* + * 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. + */ + +.mx_RoomListSecondaryFilters { + font: var(--cpd-font-body-md-medium); + margin: var(--cpd-space-2x); + margin-left: var(--cpd-space-1x); +} diff --git a/res/css/views/rooms/RoomListPanel/_RoomListSkeleton.pcss b/res/css/views/rooms/RoomListPanel/_RoomListSkeleton.pcss new file mode 100644 index 0000000000..2e644cbba1 --- /dev/null +++ b/res/css/views/rooms/RoomListPanel/_RoomListSkeleton.pcss @@ -0,0 +1,24 @@ +/* + * 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. + */ + +.mx_RoomListSkeleton { + position: relative; + margin-left: 4px; + height: 100%; + + &::before { + background-color: var(--cpd-color-bg-subtle-secondary); + width: 100%; + height: 100%; + + content: ""; + position: absolute; + mask-repeat: repeat-y; + mask-size: auto 96px; + mask-image: url("$(res)/img/element-icons/roomlist/room-list-item-skeleton.svg"); + } +} diff --git a/res/css/views/rooms/_RoomSublist.pcss b/res/css/views/rooms/_RoomSublist.pcss index 7b0da5608e..3361bce4bb 100644 --- a/res/css/views/rooms/_RoomSublist.pcss +++ b/res/css/views/rooms/_RoomSublist.pcss @@ -404,8 +404,7 @@ Please see LICENSE files in the repository root for full details. height: 240px; &::before { - background: $roomsublist-skeleton-ui-bg; - + background-color: var(--cpd-color-bg-subtle-secondary); width: 100%; height: 100%; diff --git a/res/css/views/settings/_JoinRuleSettings.pcss b/res/css/views/settings/_JoinRuleSettings.pcss index 485434f0da..fcb21fca96 100644 --- a/res/css/views/settings/_JoinRuleSettings.pcss +++ b/res/css/views/settings/_JoinRuleSettings.pcss @@ -53,6 +53,14 @@ Please see LICENSE files in the repository root for full details. display: block; } + &.mx_StyledRadioButton_disabled { + opacity: 0.5; + } + + &.mx_StyledRadioButton_disabled + span { + opacity: 0.5; + } + & + span { display: inline-block; margin-left: 34px; @@ -71,3 +79,7 @@ Please see LICENSE files in the repository root for full details. font: var(--cpd-font-body-md-regular); margin-top: var(--cpd-space-2x); } + +.mx_JoinRuleSettings_recommended { + color: $accent-1000; +} diff --git a/res/css/views/settings/tabs/user/_MediaPreviewAccountSettings.pcss b/res/css/views/settings/tabs/user/_MediaPreviewAccountSettings.pcss new file mode 100644 index 0000000000..68187854c1 --- /dev/null +++ b/res/css/views/settings/tabs/user/_MediaPreviewAccountSettings.pcss @@ -0,0 +1,28 @@ +/* +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. +*/ + +.mx_MediaPreviewAccountSetting_Radio { + margin: var(--cpd-space-1x) 0; +} + +.mx_MediaPreviewAccountSetting { + margin-top: var(--cpd-space-1x); +} + +.mx_MediaPreviewAccountSetting_RadioHelp { + margin-top: 0; + margin-bottom: var(--cpd-space-1x); +} + +.mx_MediaPreviewAccountSetting_Form { + width: 100%; +} + +.mx_MediaPreviewAccountSetting_ToggleSwitch { + font: var(--cpd-font-body-md-medium); + letter-spacing: var(--cpd-font-letter-spacing-body-md); +} diff --git a/res/img/element-icons/roomlist/room-list-item-skeleton.svg b/res/img/element-icons/roomlist/room-list-item-skeleton.svg new file mode 100644 index 0000000000..adf56e4ed8 --- /dev/null +++ b/res/img/element-icons/roomlist/room-list-item-skeleton.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/res/themes/light-high-contrast/css/_light-high-contrast.pcss b/res/themes/light-high-contrast/css/_light-high-contrast.pcss index 213c641440..94774bc5b8 100644 --- a/res/themes/light-high-contrast/css/_light-high-contrast.pcss +++ b/res/themes/light-high-contrast/css/_light-high-contrast.pcss @@ -163,7 +163,8 @@ $accent-1400: var(--cpd-color-green-1400); &.mx_SpotlightDialog_startChat::before, &.mx_SpotlightDialog_joinRoomAlias::before, &.mx_SpotlightDialog_explorePublicRooms::before, - &.mx_SpotlightDialog_startGroupChat::before { + &.mx_SpotlightDialog_startGroupChat::before, + &.mx_SpotlightDialog_searchMessages::before { background-color: $background !important; } diff --git a/res/welcome.html b/res/welcome.html index ef2d43bd8f..9fdd60a7c0 100644 --- a/res/welcome.html +++ b/res/welcome.html @@ -3,7 +3,7 @@ * voodoo where we have to set display: none by default */ - h1::after { + .mx_Header_title::after { content: "!"; } diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 344059fee4..f19a8591cb 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -82,19 +82,10 @@ declare global { mxMatrixClientPeg: IMatrixClientPeg; mxReactSdkConfig: DeepReadonly; - // Needed for Safari, unknown to TypeScript - webkitAudioContext: typeof AudioContext; - // https://docs.microsoft.com/en-us/previous-versions/hh772328(v=vs.85) // we only ever check for its existence, so we can ignore its actual type MSStream?: unknown; - // https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1029#issuecomment-869224737 - // https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas - OffscreenCanvas?: { - new (width: number, height: number): OffscreenCanvas; - }; - mxContentMessages: ContentMessages; mxToastStore: ToastStore; mxDeviceListener: DeviceListener; @@ -139,6 +130,11 @@ declare global { interface Electron { on(channel: ElectronChannel, listener: (event: Event, ...args: any[]) => void): void; send(channel: ElectronChannel, ...args: any[]): void; + initialise(): Promise<{ + protocol: string; + sessionId: string; + config: IConfigOptions; + }>; } interface DesktopCapturerSource { @@ -156,31 +152,10 @@ declare global { fetchWindowIcons?: boolean; } - interface Document { - // Safari & IE11 only have this prefixed: we used prefixed versions - // previously so let's continue to support them for now - webkitExitFullscreen(): Promise; - msExitFullscreen(): Promise; - readonly webkitFullscreenElement: Element | null; - readonly msFullscreenElement: Element | null; - } - - interface Navigator { - userLanguage?: string; - } - interface StorageEstimate { usageDetails?: { [key: string]: number }; } - interface Element { - // Safari & IE11 only have this prefixed: we used prefixed versions - // previously so let's continue to support them for now - webkitRequestFullScreen(options?: FullscreenOptions): Promise; - msRequestFullscreen(options?: FullscreenOptions): Promise; - // scrollIntoView(arg?: boolean | _ScrollIntoViewOptions): void; - } - // https://github.com/microsoft/TypeScript/issues/28308#issuecomment-650802278 interface AudioWorkletProcessor { readonly port: MessagePort; @@ -239,11 +214,4 @@ declare global { var mx_rage_store: IndexedDBLogStore; } -// add method which is missing from the node typing -declare module "url" { - interface Url { - format(): string; - } -} - /* eslint-enable @typescript-eslint/naming-convention */ diff --git a/src/@types/matrix-js-sdk.d.ts b/src/@types/matrix-js-sdk.d.ts index 92b76c4c4d..c81c5377bf 100644 --- a/src/@types/matrix-js-sdk.d.ts +++ b/src/@types/matrix-js-sdk.d.ts @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024, 2025 New Vector Ltd. Copyright 2024 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial @@ -14,6 +14,7 @@ import type { EncryptedFile } from "matrix-js-sdk/src/types"; import type { EmptyObject } from "matrix-js-sdk/src/matrix"; import type { DeviceClientInformation } from "../utils/device/types.ts"; import type { UserWidget } from "../utils/WidgetUtils-types.ts"; +import { type MediaPreviewConfig } from "./media_preview.ts"; // Extend Matrix JS SDK types via Typescript declaration merging to support unspecced event fields and types declare module "matrix-js-sdk/src/types" { @@ -87,6 +88,8 @@ declare module "matrix-js-sdk/src/types" { "m.accepted_terms": { accepted: string[]; }; + + "io.element.msc4278.media_preview_config": MediaPreviewConfig; } export interface AudioContent { diff --git a/src/@types/media_preview.ts b/src/@types/media_preview.ts new file mode 100644 index 0000000000..d340e64caf --- /dev/null +++ b/src/@types/media_preview.ts @@ -0,0 +1,33 @@ +/* +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. +*/ + +export enum MediaPreviewValue { + /** + * Media previews should be enabled. + */ + On = "on", + /** + * Media previews should only be enabled for rooms with non-public join rules. + */ + Private = "private", + /** + * Media previews should be disabled. + */ + Off = "off", +} + +export const MEDIA_PREVIEW_ACCOUNT_DATA_TYPE = "io.element.msc4278.media_preview_config"; +export interface MediaPreviewConfig extends Record { + /** + * Media preview setting for thumbnails of media in rooms. + */ + media_previews: MediaPreviewValue; + /** + * Media preview settings for avatars of rooms we have been invited to. + */ + invite_avatars: MediaPreviewValue.On | MediaPreviewValue.Off; +} diff --git a/src/@types/react.d.ts b/src/@types/react.d.ts index d094890467..61a4c59992 100644 --- a/src/@types/react.d.ts +++ b/src/@types/react.d.ts @@ -6,16 +6,9 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { type PropsWithChildren } from "react"; - -import type React from "react"; +import { type ComponentType } from "react"; declare module "react" { - // Fix forwardRef types for Generic components - https://stackoverflow.com/a/58473012 - function forwardRef( - render: (props: PropsWithChildren

, ref: React.ForwardedRef) => React.ReactElement | null, - ): (props: P & React.RefAttributes) => React.ReactElement | null; - // Fix lazy types - https://stackoverflow.com/a/71017028 function lazy>(factory: () => Promise<{ default: T }>): T; diff --git a/src/AddThreepid.ts b/src/AddThreepid.ts index 7d2c4cefb5..877a0c5c4a 100644 --- a/src/AddThreepid.ts +++ b/src/AddThreepid.ts @@ -79,6 +79,8 @@ export default class AddThreepid { } catch (err) { if (err instanceof MatrixError && err.errcode === "M_THREEPID_IN_USE") { throw new UserFriendlyError("settings|general|email_address_in_use", { cause: err }); + } else if (err instanceof MatrixError && err.errcode === "M_THREEPID_MEDIUM_NOT_SUPPORTED") { + throw new UserFriendlyError("settings|general|email_adding_unsupported_by_hs", { cause: err }); } // Otherwise, just blurt out the same error throw err; @@ -121,6 +123,8 @@ export default class AddThreepid { * @param {string} phoneCountry The ISO 2 letter code of the country to resolve phoneNumber in * @param {string} phoneNumber The national or international formatted phone number to add * @return {Promise} Resolves when the text message has been sent. Then call haveMsisdnToken(). + * + * @throws {UserFriendlyError} An appropriate user-friendly error if the verification code could not be sent. */ public async addMsisdn(phoneCountry: string, phoneNumber: string): Promise { try { @@ -136,6 +140,10 @@ export default class AddThreepid { } catch (err) { if (err instanceof MatrixError && err.errcode === "M_THREEPID_IN_USE") { throw new UserFriendlyError("settings|general|msisdn_in_use", { cause: err }); + } else if (err instanceof MatrixError && err.errcode === "M_THREEPID_MEDIUM_NOT_SUPPORTED") { + throw new UserFriendlyError("settings|general|msisdn_adding_unsupported_by_hs", { cause: err }); + } else if (err instanceof MatrixError && err.errcode === "M_INVALID_PARAM") { + throw new UserFriendlyError("settings|general|invalid_phone_number", { cause: err }); } // Otherwise, just blurt out the same error throw err; diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts index c7b7825fe6..87635a421c 100644 --- a/src/BasePlatform.ts +++ b/src/BasePlatform.ts @@ -72,7 +72,7 @@ export default abstract class BasePlatform { protected _favicon?: Favicon; protected constructor() { - dis.register(this.onAction); + dis.register(this.onAction.bind(this)); this.startUpdateCheck = this.startUpdateCheck.bind(this); } @@ -85,14 +85,14 @@ export default abstract class BasePlatform { */ public abstract getDefaultDeviceDisplayName(): string; - protected onAction = (payload: ActionPayload): void => { + protected onAction(payload: ActionPayload): void { switch (payload.action) { case "on_client_not_viable": case Action.OnLoggedOut: this.setNotificationCount(0); break; } - }; + } // Used primarily for Analytics public abstract getHumanReadableName(): string; diff --git a/src/CreateCrossSigning.ts b/src/CreateCrossSigning.ts index db9bc3e3fe..c8e7aa3e73 100644 --- a/src/CreateCrossSigning.ts +++ b/src/CreateCrossSigning.ts @@ -64,7 +64,7 @@ export async function uiAuthCallback( }; const { finished } = Modal.createDialog(InteractiveAuthDialog, { - title: _t("encryption|bootstrap_title"), + title: "", matrixClient, makeRequest, aestheticsForStagePhases: { diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index 751e71dd9f..e13d296bc1 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -97,6 +97,7 @@ export default class DeviceListener { this.client.on(CryptoEvent.DevicesUpdated, this.onDevicesUpdated); this.client.on(CryptoEvent.UserTrustStatusChanged, this.onUserTrustStatusChanged); this.client.on(CryptoEvent.KeysChanged, this.onCrossSingingKeysChanged); + this.client.on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatusChanged); this.client.on(ClientEvent.AccountData, this.onAccountData); this.client.on(ClientEvent.Sync, this.onSync); this.client.on(RoomStateEvent.Events, this.onRoomStateEvents); @@ -132,7 +133,7 @@ export default class DeviceListener { this.dismissedThisDeviceToast = false; this.keyBackupInfo = null; this.keyBackupFetchedAt = null; - this.keyBackupStatusChecked = false; + this.cachedKeyBackupUploadActive = undefined; this.ourDeviceIdsAtStart = null; this.displayingToastsForDeviceIds = new Set(); this.client = undefined; @@ -157,6 +158,13 @@ export default class DeviceListener { this.recheck(); } + /** + * Set the account data "m.org.matrix.custom.backup_disabled" to { "disabled": true }. + */ + public async recordKeyBackupDisabled(): Promise { + await this.client?.setAccountData(BACKUP_DISABLED_ACCOUNT_DATA_KEY, { disabled: true }); + } + private async ensureDeviceIdsAtStartPopulated(): Promise { if (this.ourDeviceIdsAtStart === null) { this.ourDeviceIdsAtStart = await this.getDeviceIds(); @@ -192,6 +200,11 @@ export default class DeviceListener { this.recheck(); }; + private onKeyBackupStatusChanged = (): void => { + this.cachedKeyBackupUploadActive = undefined; + this.recheck(); + }; + private onCrossSingingKeysChanged = (): void => { this.recheck(); }; @@ -201,11 +214,13 @@ export default class DeviceListener { // * migrated SSSS to symmetric // * uploaded keys to secret storage // * completed secret storage creation + // * disabled key backup // which result in account data changes affecting checks below. if ( ev.getType().startsWith("m.secret_storage.") || ev.getType().startsWith("m.cross_signing.") || - ev.getType() === "m.megolm_backup.v1" + ev.getType() === "m.megolm_backup.v1" || + ev.getType() === BACKUP_DISABLED_ACCOUNT_DATA_KEY ) { this.recheck(); } @@ -324,7 +339,16 @@ export default class DeviceListener { (await crypto.getDeviceVerificationStatus(cli.getSafeUserId(), cli.deviceId!))?.crossSigningVerified, ); - const allSystemsReady = crossSigningReady && secretStorageReady && allCrossSigningSecretsCached; + const keyBackupUploadActive = await this.isKeyBackupUploadActive(); + const backupDisabled = await this.recheckBackupDisabled(cli); + + // We warn if key backup upload is turned off and we have not explicitly + // said we are OK with that. + const keyBackupIsOk = keyBackupUploadActive || backupDisabled; + + const allSystemsReady = + crossSigningReady && keyBackupIsOk && secretStorageReady && allCrossSigningSecretsCached; + await this.reportCryptoSessionStateToAnalytics(cli); if (this.dismissedThisDeviceToast || allSystemsReady) { @@ -353,14 +377,19 @@ export default class DeviceListener { crossSigningStatus.privateKeysCachedLocally, ); showSetupEncryptionToast(SetupKind.KEY_STORAGE_OUT_OF_SYNC); + } else if (!keyBackupIsOk) { + logSpan.info("Key backup upload is unexpectedly turned off: showing TURN_ON_KEY_STORAGE toast"); + showSetupEncryptionToast(SetupKind.TURN_ON_KEY_STORAGE); } else if (defaultKeyId === null) { - // the user just hasn't set up 4S yet: prompt them to do so (unless they've explicitly said no to key storage) - const disabledEvent = cli.getAccountData(BACKUP_DISABLED_ACCOUNT_DATA_KEY); - if (!disabledEvent?.getContent().disabled) { + // The user just hasn't set up 4S yet: if they have key + // backup, prompt them to turn on recovery too. (If not, they + // have explicitly opted out, so don't hassle them.) + if (keyBackupUploadActive) { logSpan.info("No default 4S key: showing SET_UP_RECOVERY toast"); showSetupEncryptionToast(SetupKind.SET_UP_RECOVERY); } else { logSpan.info("No default 4S key but backup disabled: no toast needed"); + hideSetupEncryptionToast(); } } else { // some other condition... yikes! Show the 'set up encryption' toast: this is what we previously did @@ -443,6 +472,16 @@ export default class DeviceListener { this.displayingToastsForDeviceIds = newUnverifiedDeviceIds; } + /** + * Fetch the account data for `backup_disabled`. If this is the first time, + * fetch it from the server (in case the initial sync has not finished). + * Otherwise, fetch it from the store as normal. + */ + private async recheckBackupDisabled(cli: MatrixClient): Promise { + const backupDisabled = await cli.getAccountDataFromServer(BACKUP_DISABLED_ACCOUNT_DATA_KEY); + return !!backupDisabled?.disabled; + } + /** * Reports current recovery state to analytics. * Checks if the session is verified and if the recovery is correctly set up (i.e all secrets known locally and in 4S). @@ -512,18 +551,42 @@ export default class DeviceListener { * trigger an auto-rageshake). */ private checkKeyBackupStatus = async (): Promise => { - if (this.keyBackupStatusChecked || !this.client) { - return; - } - const activeKeyBackupVersion = await this.client.getCrypto()?.getActiveSessionBackupVersion(); - // if key backup is enabled, no need to check this ever again (XXX: why only when it is enabled?) - this.keyBackupStatusChecked = !!activeKeyBackupVersion; - - if (!activeKeyBackupVersion) { + if (!(await this.isKeyBackupUploadActive())) { dis.dispatch({ action: Action.ReportKeyBackupNotEnabled }); } }; - private keyBackupStatusChecked = false; + + /** + * Is key backup enabled? Use a cached answer if we have one. + */ + private isKeyBackupUploadActive = async (): Promise => { + if (!this.client) { + // To preserve existing behaviour, if there is no client, we + // pretend key backup upload is on. + // + // Someone looking to improve this code could try throwing an error + // here since we don't expect client to be undefined. + return true; + } + + const crypto = this.client.getCrypto(); + if (!crypto) { + // If there is no crypto, there is no key backup + return false; + } + + // If we've already cached the answer, return it. + if (this.cachedKeyBackupUploadActive !== undefined) { + return this.cachedKeyBackupUploadActive; + } + + // Fetch the answer and cache it + const activeKeyBackupVersion = await crypto.getActiveSessionBackupVersion(); + this.cachedKeyBackupUploadActive = !!activeKeyBackupVersion; + + return this.cachedKeyBackupUploadActive; + }; + private cachedKeyBackupUploadActive: boolean | undefined = undefined; private onRecordClientInformationSettingChange: CallbackFn = ( _originalSettingName, diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index f704657c32..1d17710dab 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024, 2025 New Vector Ltd. Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Copyright 2019 The Matrix.org Foundation C.I.C. Copyright 2017, 2018 New Vector Ltd @@ -294,6 +294,10 @@ export interface EventRenderOpts { disableBigEmoji?: boolean; stripReplyFallback?: boolean; forComposerQuote?: boolean; + /** + * Should inline media be rendered? + */ + mediaIsVisible?: boolean; } function analyseEvent(content: IContent, highlights: Optional, opts: EventRenderOpts = {}): EventAnalysis { @@ -302,6 +306,20 @@ function analyseEvent(content: IContent, highlights: Optional, opts: E sanitizeParams = composerSanitizeHtmlParams; } + if (opts.mediaIsVisible === false && sanitizeParams.transformTags?.["img"]) { + // Prevent mutating the source of sanitizeParams. + sanitizeParams = { + ...sanitizeParams, + transformTags: { + ...sanitizeParams.transformTags, + img: (tagName) => { + // Remove element + return { tagName, attribs: {} }; + }, + }, + }; + } + try { const isFormattedBody = content.format === "org.matrix.custom.html" && typeof content.formatted_body === "string"; @@ -462,10 +480,6 @@ export function topicToHtml( ref?: LegacyRef, allowExtendedHtml = false, ): ReactNode { - if (!SettingsStore.getValue("feature_html_topic")) { - htmlTopic = undefined; - } - let isFormattedTopic = !!htmlTopic; let topicHasEmoji = false; let safeTopic = ""; diff --git a/src/LegacyCallHandler.tsx b/src/LegacyCallHandler.tsx index 97cb478512..9c3e7073d5 100644 --- a/src/LegacyCallHandler.tsx +++ b/src/LegacyCallHandler.tsx @@ -679,7 +679,7 @@ export default class LegacyCallHandler extends TypedEventEmitter { - SettingsStore.setValue("fallbackICEServerAllowed", null, SettingLevel.DEVICE, allow); - }, }, undefined, true, ); + + finished.then(([allow]) => { + SettingsStore.setValue("fallbackICEServerAllowed", null, SettingLevel.DEVICE, allow); + }); } private showMediaCaptureError(call: MatrixCall): void { diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index c1559886b4..5641f936ae 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -469,12 +469,15 @@ type TryAgainFunction = () => void; * @param tryAgain OPTIONAL function to call on try again button from error dialog */ function onFailedDelegatedAuthLogin(description: string | ReactNode, tryAgain?: TryAgainFunction): void { - Modal.createDialog(ErrorDialog, { + const { finished } = Modal.createDialog(ErrorDialog, { title: _t("auth|oidc|error_title"), description, button: _t("action|try_again"), + }); + + finished.then(([shouldTryAgain]) => { // if we have a tryAgain callback, call it the primary 'try again' button was clicked in the dialog - onFinished: tryAgain ? (shouldTryAgain?: boolean) => shouldTryAgain && tryAgain() : undefined, + if (shouldTryAgain) tryAgain?.(); }); } @@ -1112,7 +1115,9 @@ export async function onLoggedOut(): Promise { * @param {object} opts Options for how to clear storage. * @returns {Promise} promise which resolves once the stores have been cleared */ -async function clearStorage(opts?: { deleteEverything?: boolean }): Promise { +export async function clearStorage(opts?: { deleteEverything?: boolean }): Promise { + logger.info(`Clearing storage, deleteEverything=${opts?.deleteEverything}`); + if (window.localStorage) { // get the currently defined device language, if set, so we can restore it later const language = SettingsStore.getValueAt(SettingLevel.DEVICE, "language", null, true, true); diff --git a/src/Linkify.tsx b/src/Linkify.tsx index 27dd4783be..846bf8e82d 100644 --- a/src/Linkify.tsx +++ b/src/Linkify.tsx @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024, 2025 New Vector Ltd. Copyright 2024 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial @@ -12,7 +12,6 @@ import { merge } from "lodash"; import _Linkify from "linkify-react"; import { _linkifyString, ELEMENT_URL_PATTERN, options as linkifyMatrixOptions } from "./linkify-matrix"; -import SettingsStore from "./settings/SettingsStore"; import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks"; import { mediaFromMxc } from "./customisations/Media"; import { PERMITTED_URL_SCHEMES } from "./utils/UrlUtils"; @@ -47,10 +46,7 @@ export const transformTags: NonNullable = { // Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag // because transformTags is used _before_ we filter by allowedSchemesByTag and // we don't want to allow images with `https?` `src`s. - // We also drop inline images (as if they were not present at all) when the "show - // images" preference is disabled. Future work might expose some UI to reveal them - // like standalone image events have. - if (!src || !SettingsStore.getValue("showImages")) { + if (!src) { return { tagName, attribs: {} }; } @@ -78,7 +74,6 @@ export const transformTags: NonNullable = { if (requestedHeight) { attribs.style += "height: 100%;"; } - attribs.src = mediaFromMxc(src).getThumbnailOfSourceHttp(width, height)!; return { tagName, attribs }; }, diff --git a/src/Modal.tsx b/src/Modal.tsx index ba5c6702bd..e2873783ea 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -10,7 +10,6 @@ Please see LICENSE files in the repository root for full details. import React, { StrictMode } from "react"; import { createRoot, type Root } from "react-dom/client"; import classNames from "classnames"; -import { type IDeferred, defer } from "matrix-js-sdk/src/utils"; import { TypedEventEmitter } from "matrix-js-sdk/src/matrix"; import { Glass, TooltipProvider } from "@vector-im/compound-web"; @@ -30,12 +29,25 @@ export type ComponentType = }> | React.ComponentType; -// Generic type which returns the props of the Modal component with the onFinished being optional. +/** + * The parameter types of the `onFinished` callback property exposed by the component which forms the + * body of the dialog. + * + * @typeParam C - The type of the React component which forms the body of the dialog. + */ +type OnFinishedParams = Parameters["onFinished"]>; + +/** + * The properties exposed by the `props` argument to {@link Modal.createDialog}: the same as + * those exposed by the underlying component, with the exception of `onFinished`, which is provided by + * `createDialog`. + * + * @typeParam C - The type of the React component which forms the body of the dialog. + */ export type ComponentProps = Defaultize< Omit, "onFinished">, C["defaultProps"] -> & - Partial, "onFinished">>; +>; export interface IModal { elem: React.ReactNode; @@ -43,15 +55,44 @@ export interface IModal { beforeClosePromise?: Promise; closeReason?: ModalCloseReason; onBeforeClose?(reason?: ModalCloseReason): Promise; - onFinished: ComponentProps["onFinished"]; - close(...args: Parameters["onFinished"]>): void; + + /** + * Run the {@link deferred} with the given arguments, and close this modal. + * + * This method is passed as the `onFinished` callback to the underlying component, + * as well as being returned by {@link Modal.createDialog} to the caller. + */ + close(...args: OnFinishedParams | []): void; + hidden?: boolean; - deferred?: IDeferred["onFinished"]>>; + + /** A deferred to resolve when the dialog closes, with the results as provided by + * the call to {@link close} (normally from the `onFinished` callback). + */ + deferred?: PromiseWithResolvers | []>; } +/** The result of {@link Modal.createDialog}. + * + * @typeParam C - The type of the React component which forms the body of the dialog. + */ export interface IHandle { - finished: Promise["onFinished"]>>; - close(...args: Parameters["onFinished"]>): void; + /** + * A promise which will resolve when the dialog closes. + * + * If the dialog body component calls the `onFinished` property, or the caller calls {@link close}, + * the promise resolves with an array holding the arguments to that call. + * + * If the dialog is closed by clicking in the background, the promise resolves with an empty array. + */ + finished: Promise | []>; + + /** + * A function which, if called, will close the dialog. + * + * @param args - Arguments to return to {@link finished}. + */ + close(...args: OnFinishedParams): void; } interface IOptions { @@ -164,7 +205,6 @@ export class ModalManager extends TypedEventEmitter["finished"]; } { const modal = { - onFinished: props?.onFinished, onBeforeClose: options?.onBeforeClose, className, @@ -196,8 +235,7 @@ export class ModalManager extends TypedEventEmitter; - // never call this from onFinished() otherwise it will loop - const [closeDialog, onFinishedProm] = this.getCloseFn(modal, props); + const [closeDialog, onFinishedProm] = this.getCloseFn(modal); // don't attempt to reuse the same AsyncWrapper for different dialogs, // otherwise we'll get confused. @@ -214,13 +252,10 @@ export class ModalManager extends TypedEventEmitter( - modal: IModal, - props?: ComponentProps, - ): [IHandle["close"], IHandle["finished"]] { - modal.deferred = defer["onFinished"]>>(); + private getCloseFn(modal: IModal): [IHandle["close"], IHandle["finished"]] { + modal.deferred = Promise.withResolvers | []>(); return [ - async (...args: Parameters["onFinished"]>): Promise => { + async (...args: OnFinishedParams): Promise => { if (modal.beforeClosePromise) { await modal.beforeClosePromise; } else if (modal.onBeforeClose) { @@ -232,7 +267,6 @@ export class ModalManager extends TypedEventEmitter= 0) { this.modals.splice(i, 1); @@ -280,7 +314,8 @@ export class ModalManager extends TypedEventEmitter import('./MyComponent'))` * - * @param props properties to pass to the displayed component. (We will also pass an 'onFinished' property.) + * @param props properties to pass to the displayed component. (We will also pass an `onFinished` property; when + * called, that property will close the dialog and return the results to the caller via {@link IHandle.finished}.) * * @param className CSS class to apply to the modal wrapper * @@ -295,7 +330,7 @@ export class ModalManager extends TypedEventEmitter( component: C, diff --git a/src/Registration.tsx b/src/Registration.tsx index ea0264fab3..22eb6e15ff 100644 --- a/src/Registration.tsx +++ b/src/Registration.tsx @@ -62,14 +62,14 @@ export async function startAnyRegistrationFlow( , ] : [], - onFinished: (proceed) => { - if (proceed) { - dis.dispatch({ action: "start_login", screenAfterLogin: options.screen_after }); - } else if (options.go_home_on_cancel) { - dis.dispatch({ action: Action.ViewHomePage }); - } else if (options.go_welcome_on_cancel) { - dis.dispatch({ action: "view_welcome_page" }); - } - }, + }); + modal.finished.then(([proceed]) => { + if (proceed) { + dis.dispatch({ action: "start_login", screenAfterLogin: options.screen_after }); + } else if (options.go_home_on_cancel) { + dis.dispatch({ action: Action.ViewHomePage }); + } else if (options.go_welcome_on_cancel) { + dis.dispatch({ action: "view_welcome_page" }); + } }); } diff --git a/src/SlidingSyncManager.ts b/src/SlidingSyncManager.ts index 88b839312d..815e438da7 100644 --- a/src/SlidingSyncManager.ts +++ b/src/SlidingSyncManager.ts @@ -49,7 +49,7 @@ import { SlidingSyncState, } from "matrix-js-sdk/src/sliding-sync"; import { logger } from "matrix-js-sdk/src/logger"; -import { defer, sleep } from "matrix-js-sdk/src/utils"; +import { sleep } from "matrix-js-sdk/src/utils"; // how long to long poll for const SLIDING_SYNC_TIMEOUT_MS = 20 * 1000; @@ -184,7 +184,7 @@ export class SlidingSyncManager { public slidingSync?: SlidingSync; private client?: MatrixClient; - private configureDefer = defer(); + private configureDefer = Promise.withResolvers(); public static get instance(): SlidingSyncManager { return SlidingSyncManager.internalInstance; diff --git a/src/WorkerManager.ts b/src/WorkerManager.ts index a8a95ca727..1d403be88c 100644 --- a/src/WorkerManager.ts +++ b/src/WorkerManager.ts @@ -6,14 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { defer, type IDeferred } from "matrix-js-sdk/src/utils"; - import { type WorkerPayload } from "./workers/worker"; export class WorkerManager { private readonly worker: Worker; private seq = 0; - private pendingDeferredMap = new Map>(); + private pendingDeferredMap = new Map>(); public constructor(worker: Worker) { this.worker = worker; @@ -30,7 +28,7 @@ export class WorkerManager { public call(request: Request): Promise { const seq = this.seq++; - const deferred = defer(); + const deferred = Promise.withResolvers(); this.pendingDeferredMap.set(seq, deferred); this.worker.postMessage({ seq, ...request }); return deferred.promise; diff --git a/src/accessibility/KeyboardShortcuts.ts b/src/accessibility/KeyboardShortcuts.ts index da0097f4b2..ec5b8312b9 100644 --- a/src/accessibility/KeyboardShortcuts.ts +++ b/src/accessibility/KeyboardShortcuts.ts @@ -521,7 +521,8 @@ export const KEYBOARD_SHORTCUTS: IKeyboardShortcuts = { [KeyBindingAction.GoToHome]: { default: { ctrlKey: true, - altKey: true, + altKey: !IS_MAC, + shiftKey: IS_MAC, key: Key.H, }, displayName: _td("keyboard|go_home_view"), @@ -586,7 +587,7 @@ export const KEYBOARD_SHORTCUTS: IKeyboardShortcuts = { default: { ctrlKey: true, shiftKey: true, - key: Key.H, + key: Key.J, }, displayName: _td("keyboard|toggle_hidden_events"), }, diff --git a/src/accessibility/Toolbar.tsx b/src/accessibility/Toolbar.tsx index 317ff8b936..5583292e48 100644 --- a/src/accessibility/Toolbar.tsx +++ b/src/accessibility/Toolbar.tsx @@ -6,18 +6,20 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { forwardRef } from "react"; +import React, { type Ref, type JSX } from "react"; import { RovingTabIndexProvider } from "./RovingTabIndex"; import { getKeyBindingsManager } from "../KeyBindingsManager"; import { KeyBindingAction } from "./KeyboardShortcuts"; -interface IProps extends Omit, "onKeyDown"> {} +interface IProps extends Omit, "onKeyDown"> { + ref?: Ref; +} // This component implements the Toolbar design pattern from the WAI-ARIA Authoring Practices guidelines. // https://www.w3.org/TR/wai-aria-practices-1.1/#toolbar // All buttons passed in children must use RovingTabIndex to set `onFocus`, `isActive`, `ref` -const Toolbar = forwardRef(({ children, ...props }, ref) => { +const Toolbar = ({ children, ref, ...props }: IProps): JSX.Element => { const onKeyDown = (ev: React.KeyboardEvent): void => { const target = ev.target as HTMLElement; // Don't interfere with input default keydown behaviour @@ -55,6 +57,6 @@ const Toolbar = forwardRef(({ children, ...props }, ref) )} ); -}); +}; export default Toolbar; diff --git a/src/accessibility/context_menu/ContextMenuButton.tsx b/src/accessibility/context_menu/ContextMenuButton.tsx index 8096203d0e..df16d15821 100644 --- a/src/accessibility/context_menu/ContextMenuButton.tsx +++ b/src/accessibility/context_menu/ContextMenuButton.tsx @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { forwardRef, type Ref } from "react"; +import React, { type Ref, type JSX } from "react"; import AccessibleButton, { type ButtonProps } from "../../components/views/elements/AccessibleButton"; @@ -16,13 +16,19 @@ type Props = ButtonProps & { label?: string; // whether the context menu is currently open isExpanded: boolean; + ref?: Ref; }; // Semantic component for representing the AccessibleButton which launches a -export const ContextMenuButton = forwardRef(function ( - { label, isExpanded, children, onClick, onContextMenu, ...props }: Props, - ref: Ref, -) { +export const ContextMenuButton = function ({ + label, + isExpanded, + children, + onClick, + onContextMenu, + ref, + ...props +}: Props): JSX.Element { return ( ); -}); +}; diff --git a/src/accessibility/context_menu/ContextMenuTooltipButton.tsx b/src/accessibility/context_menu/ContextMenuTooltipButton.tsx index f58fbea171..b0d0156c0f 100644 --- a/src/accessibility/context_menu/ContextMenuTooltipButton.tsx +++ b/src/accessibility/context_menu/ContextMenuTooltipButton.tsx @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { forwardRef, type Ref } from "react"; +import React, { type JSX } from "react"; import AccessibleButton, { type ButtonProps } from "../../components/views/elements/AccessibleButton"; @@ -18,10 +18,14 @@ type Props = ButtonProps & { }; // Semantic component for representing the AccessibleButton which launches a -export const ContextMenuTooltipButton = forwardRef(function ( - { isExpanded, children, onClick, onContextMenu, ...props }: Props, - ref: Ref, -) { +export const ContextMenuTooltipButton = function ({ + isExpanded, + children, + onClick, + onContextMenu, + ref, + ...props +}: Props): JSX.Element { return ( ); -}); +}; diff --git a/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx b/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx index 9b9683b798..a2fef4e1c9 100644 --- a/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx +++ b/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx @@ -49,15 +49,8 @@ export default function NewRecoveryMethodDialog({ onFinished }: NewRecoveryMetho if (isKeyBackupEnabled) { onFinished(); } else { - Modal.createDialog( - RestoreKeyBackupDialog, - { - onFinished, - }, - undefined, - false, - true, - ); + const { finished } = Modal.createDialog(RestoreKeyBackupDialog, {}, undefined, false, true); + finished.then(onFinished); } } diff --git a/src/audio/BackgroundAudio.ts b/src/audio/BackgroundAudio.ts index c90016eef9..eda10c0904 100644 --- a/src/audio/BackgroundAudio.ts +++ b/src/audio/BackgroundAudio.ts @@ -48,6 +48,12 @@ export class BackgroundAudio { source.buffer = this.sounds[url]; source.loop = loop; source.connect(this.audioContext.destination); + + await this.audioContext.resume(); + source.onended = () => { + this.audioContext.suspend(); + }; + source.start(); return source; } diff --git a/src/audio/Playback.ts b/src/audio/Playback.ts index 58b8a23c22..54d2c710d0 100644 --- a/src/audio/Playback.ts +++ b/src/audio/Playback.ts @@ -9,7 +9,6 @@ Please see LICENSE files in the repository root for full details. import EventEmitter from "events"; import { SimpleObservable } from "matrix-widget-api"; import { logger } from "matrix-js-sdk/src/logger"; -import { defer } from "matrix-js-sdk/src/utils"; import { UPDATE_EVENT } from "../stores/AsyncStore"; import { arrayFastResample } from "../utils/arrays"; @@ -158,42 +157,27 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte // 5mb logger.log("Audio file too large: processing through

before rendering but I think this is the better */ interface ITextualCompletionProps { - title?: string; - subtitle?: string; - description?: string; - className?: string; + "title"?: string; + "subtitle"?: string; + "description"?: string; + "className"?: string; + "aria-selected"?: boolean; + "ref"?: Ref; } -export const TextualCompletion = forwardRef((props, ref) => { - const { title, subtitle, description, className, "aria-selected": ariaSelectedAttribute, ...restProps } = props; +export const TextualCompletion = (props: ITextualCompletionProps): JSX.Element => { + const { + title, + subtitle, + description, + className, + "aria-selected": ariaSelectedAttribute, + ref, + ...restProps + } = props; return (
((props {description}
); -}); +}; interface IPillCompletionProps extends ITextualCompletionProps { children?: React.ReactNode; } -export const PillCompletion = forwardRef((props, ref) => { +export const PillCompletion = (props: IPillCompletionProps): JSX.Element => { const { title, subtitle, @@ -51,6 +61,7 @@ export const PillCompletion = forwardRef((props, ref) className, children, "aria-selected": ariaSelectedAttribute, + ref, ...restProps } = props; return ( @@ -67,4 +78,4 @@ export const PillCompletion = forwardRef((props, ref) {description}
); -}); +}; diff --git a/src/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx index 9cd5564795..39accdc8da 100644 --- a/src/autocomplete/UserProvider.tsx +++ b/src/autocomplete/UserProvider.tsx @@ -127,7 +127,7 @@ export default class UserProvider extends AutocompleteProvider { suffix: selection.beginning && range!.start === 0 ? ": " : " ", href: makeUserPermalink(user.userId), component: ( - + ), diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 3686d52bca..3bd2518c8a 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -379,13 +379,14 @@ export default class LeftPanel extends React.Component { } public render(): React.ReactNode { + const useNewRoomList = SettingsStore.getValue("feature_new_room_list"); const containerClasses = classNames({ mx_LeftPanel: true, + mx_LeftPanel_newRoomList: useNewRoomList, mx_LeftPanel_minimized: this.props.isMinimized, }); const roomListClasses = classNames("mx_LeftPanel_actualRoomListContainer", "mx_AutoHideScrollbar"); - const useNewRoomList = SettingsStore.getValue("feature_new_room_list"); if (useNewRoomList) { return (
diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index cf68f85b4c..26e127f21f 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -259,9 +259,11 @@ class LoggedInView extends React.Component { private createResizer(): Resizer { let panelSize: number | null; let panelCollapsed: boolean; + const useNewRoomList = SettingsStore.getValue("feature_new_room_list"); + // TODO decrease this once Spaces launches as it'll no longer need to include the 56px Community Panel + const toggleSize = useNewRoomList ? 224 : 206 - 50; const collapseConfig: ICollapseConfig = { - // TODO decrease this once Spaces launches as it'll no longer need to include the 56px Community Panel - toggleSize: 206 - 50, + toggleSize, onCollapsed: (collapsed) => { panelCollapsed = collapsed; if (collapsed) { @@ -697,10 +699,18 @@ class LoggedInView extends React.Component { "mx_MatrixChat--with-avatar": this.state.backgroundImage, }); + const useNewRoomList = SettingsStore.getValue("feature_new_room_list"); + + const leftPanelWrapperClasses = classNames({ + mx_LeftPanel_wrapper: true, + mx_LeftPanel_newRoomList: useNewRoomList, + }); + const audioFeedArraysForCalls = this.state.activeCalls.map((call) => { return ; }); + const shouldUseMinimizedUI = !useNewRoomList && this.props.collapseLhs; return (
{
- -
- + +
+ {!useNewRoomList && ( + + )} - + {!useNewRoomList && }
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 83d6f5f54c..e61713ca69 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -13,20 +13,21 @@ import { EventType, HttpApiEvent, type MatrixClient, - type MatrixEvent, + MatrixEvent, + MsgType, type RoomType, SyncState, type SyncStateData, type TimelineEvents, } from "matrix-js-sdk/src/matrix"; -import { defer, type IDeferred, type QueryDict } from "matrix-js-sdk/src/utils"; +import { type QueryDict } from "matrix-js-sdk/src/utils"; import { logger } from "matrix-js-sdk/src/logger"; import { throttle } from "lodash"; import { CryptoEvent, type KeyBackupInfo } from "matrix-js-sdk/src/crypto-api"; import { TooltipProvider } from "@vector-im/compound-web"; - // what-input helps improve keyboard accessibility import "what-input"; +import sanitizeHtml from "sanitize-html"; import PosthogTrackers from "../../PosthogTrackers"; import { DecryptionFailureTracker } from "../../DecryptionFailureTracker"; @@ -50,6 +51,7 @@ import ThemeController from "../../settings/controllers/ThemeController"; import { startAnyRegistrationFlow } from "../../Registration"; import ResizeNotifier from "../../utils/ResizeNotifier"; import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils"; +import { makeRoomPermalink } from "../../utils/permalinks/Permalinks"; import ThemeWatcher, { ThemeWatcherEvent } from "../../settings/watchers/ThemeWatcher"; import { FontWatcher } from "../../settings/watchers/FontWatcher"; import { storeRoomAliasInCache } from "../../RoomAliasCache"; @@ -94,7 +96,6 @@ import VerificationRequestToast from "../views/toasts/VerificationRequestToast"; import PerformanceMonitor, { PerformanceEntryNames } from "../../performance"; import UIStore, { UI_EVENTS } from "../../stores/UIStore"; import SoftLogout from "./auth/SoftLogout"; -import { makeRoomPermalink } from "../../utils/permalinks/Permalinks"; import { copyPlaintext } from "../../utils/strings"; import { PosthogAnalytics } from "../../PosthogAnalytics"; import { initSentry } from "../../sentry"; @@ -107,6 +108,7 @@ import Views from "../../Views"; import { type FocusNextType, type ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; import { type ViewHomePagePayload } from "../../dispatcher/payloads/ViewHomePagePayload"; import { type AfterLeaveRoomPayload } from "../../dispatcher/payloads/AfterLeaveRoomPayload"; +import { type AfterForgetRoomPayload } from "../../dispatcher/payloads/AfterForgetRoomPayload"; import { type DoAfterSyncPreparedPayload } from "../../dispatcher/payloads/DoAfterSyncPreparedPayload"; import { type ViewStartChatOrReusePayload } from "../../dispatcher/payloads/ViewStartChatOrReusePayload"; import { leaveRoomBehaviour } from "../../utils/leave-behaviour"; @@ -123,7 +125,7 @@ import { viewUserDeviceSettings } from "../../actions/handlers/viewUserDeviceSet import GenericToast from "../views/toasts/GenericToast"; import RovingSpotlightDialog from "../views/dialogs/spotlight/SpotlightDialog"; import { findDMForUser } from "../../utils/dm/findDMForUser"; -import { Linkify } from "../../HtmlUtils"; +import { getHtmlText, Linkify } from "../../HtmlUtils"; import { NotificationLevel } from "../../stores/notifications/NotificationLevel"; import { type UserTab } from "../views/dialogs/UserTab"; import { shouldSkipSetupEncryption } from "../../utils/crypto/shouldSkipSetupEncryption"; @@ -135,6 +137,10 @@ import { LoginSplashView } from "./auth/LoginSplashView"; import { cleanUpDraftsIfRequired } from "../../DraftCleaner"; import { InitialCryptoSetupStore } from "../../stores/InitialCryptoSetupStore"; import { setTheme } from "../../theme"; +import { type OpenForwardDialogPayload } from "../../dispatcher/payloads/OpenForwardDialogPayload"; +import { ShareFormat, type SharePayload } from "../../dispatcher/payloads/SharePayload"; +import Markdown from "../../Markdown"; +import { sanitizeHtmlParams } from "../../Linkify"; // legacy export export { default as Views } from "../../Views"; @@ -215,7 +221,7 @@ export default class MatrixChat extends React.PureComponent { }; private firstSyncComplete = false; - private firstSyncPromise: IDeferred; + private firstSyncPromise: PromiseWithResolvers; private screenAfterLogin?: IScreen; private tokenLogin?: boolean; @@ -254,7 +260,7 @@ export default class MatrixChat extends React.PureComponent { // Used by _viewRoom before getting state from sync this.firstSyncComplete = false; - this.firstSyncPromise = defer(); + this.firstSyncPromise = Promise.withResolvers(); if (this.props.config.sync_timeline_limit) { MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit; @@ -779,6 +785,9 @@ export default class MatrixChat extends React.PureComponent { case Action.ViewHomePage: this.viewHome(payload.justRegistered); break; + case Action.Share: + this.viewShare(payload.format, payload.msg); + break; case Action.ViewStartChatOrReuse: this.chatCreateOrReuse(payload.user_id); break; @@ -1114,6 +1123,58 @@ export default class MatrixChat extends React.PureComponent { }); } + private viewShare(format: ShareFormat, msg: string): void { + // Wait for the first sync so we can present possible rooms to share into + this.firstSyncPromise.promise.then(() => { + this.notifyNewScreen("share"); + let rawEvent; + switch (format) { + case ShareFormat.Html: { + rawEvent = { + type: "m.room.message", + content: { + msgtype: MsgType.Text, + body: getHtmlText(msg), + format: "org.matrix.custom.html", + formatted_body: sanitizeHtml(msg, sanitizeHtmlParams), + }, + origin_server_ts: Date.now(), + }; + break; + } + case ShareFormat.Markdown: { + const html = new Markdown(msg).toHTML({ externalLinks: true }); + rawEvent = { + type: "m.room.message", + content: { + msgtype: MsgType.Text, + body: msg, + format: "org.matrix.custom.html", + formatted_body: html, + }, + origin_server_ts: Date.now(), + }; + break; + } + default: + rawEvent = { + type: "m.room.message", + content: { + msgtype: MsgType.Text, + body: msg, + }, + origin_server_ts: Date.now(), + }; + } + const event = new MatrixEvent(rawEvent); + dis.dispatch({ + action: Action.OpenForwardDialog, + event: event, + permalinkCreator: null, + }); + }); + } + private async createRoom(defaultPublic = false, defaultName?: string, type?: RoomType): Promise { const modal = Modal.createDialog(CreateRoomDialog, { type, @@ -1229,7 +1290,7 @@ export default class MatrixChat extends React.PureComponent { const warnings = this.leaveRoomWarnings(roomId); const isSpace = roomToLeave?.isSpaceRoom(); - Modal.createDialog(QuestionDialog, { + const { finished } = Modal.createDialog(QuestionDialog, { title: isSpace ? _t("space|leave_dialog_action") : _t("action|leave_room"), description: ( @@ -1245,16 +1306,17 @@ export default class MatrixChat extends React.PureComponent { ), button: _t("action|leave"), danger: warnings.length > 0, - onFinished: async (shouldLeave) => { - if (shouldLeave) { - await leaveRoomBehaviour(cli, roomId); + }); - dis.dispatch({ - action: Action.AfterLeaveRoom, - room_id: roomId, - }); - } - }, + finished.then(async ([shouldLeave]) => { + if (shouldLeave) { + await leaveRoomBehaviour(cli, roomId); + + dis.dispatch({ + action: Action.AfterLeaveRoom, + room_id: roomId, + }); + } }); } @@ -1268,10 +1330,12 @@ export default class MatrixChat extends React.PureComponent { dis.dispatch({ action: Action.ViewHomePage }); } - // We have to manually update the room list because the forgotten room will not - // be notified to us, therefore the room list will have no other way of knowing - // the room is forgotten. - if (room) RoomListStore.instance.manualRoomUpdate(room, RoomUpdateCause.RoomRemoved); + if (room) { + // Legacy room list store needs to be told to manually remove this room + RoomListStore.instance.manualRoomUpdate(room, RoomUpdateCause.RoomRemoved); + // New room list store will remove the room on the following dispatch + dis.dispatch({ action: Action.AfterForgetRoom, room }); + } }) .catch((err) => { const errCode = err.errcode || _td("error|unknown_error_code"); @@ -1470,11 +1534,11 @@ export default class MatrixChat extends React.PureComponent { // since we're about to start the client and therefore about to do the first sync // We resolve the existing promise with the new one to update any existing listeners if (!this.firstSyncComplete) { - const firstSyncPromise = defer(); + const firstSyncPromise = Promise.withResolvers(); this.firstSyncPromise.resolve(firstSyncPromise.promise); this.firstSyncPromise = firstSyncPromise; } else { - this.firstSyncPromise = defer(); + this.firstSyncPromise = Promise.withResolvers(); } this.firstSyncComplete = false; const cli = MatrixClientPeg.safeGet(); @@ -1558,7 +1622,7 @@ export default class MatrixChat extends React.PureComponent { }); }); cli.on(HttpApiEvent.NoConsent, function (message, consentUri) { - Modal.createDialog( + const { finished } = Modal.createDialog( QuestionDialog, { title: _t("terms|tac_title"), @@ -1569,16 +1633,16 @@ export default class MatrixChat extends React.PureComponent { ), button: _t("terms|tac_button"), cancelButton: _t("action|dismiss"), - onFinished: (confirmed) => { - if (confirmed) { - const wnd = window.open(consentUri, "_blank")!; - wnd.opener = null; - } - }, }, undefined, true, ); + finished.then(([confirmed]) => { + if (confirmed) { + const wnd = window.open(consentUri, "_blank")!; + wnd.opener = null; + } + }); }); DecryptionFailureTracker.instance @@ -1738,6 +1802,20 @@ export default class MatrixChat extends React.PureComponent { dis.dispatch({ action: Action.CreateChat, }); + } else if (screen === "share") { + if (params && params["msg"] !== undefined) { + dis.dispatch({ + action: Action.Share, + msg: params["msg"], + format: params["format"], + }); + } + // if we weren't already coming at this from an existing screen + // and we're logged in, then explicitly default to home. + // if we're not logged in, then the login flow will do the right thing. + if (!this.state.currentRoomId && !this.state.currentUserId) { + this.viewHome(); + } } else if (screen === "settings") { dis.fire(Action.ViewUserSettings); } else if (screen === "welcome") { diff --git a/src/components/structures/MatrixClientContextProvider.tsx b/src/components/structures/MatrixClientContextProvider.tsx index e22d6bfcde..7d555f5809 100644 --- a/src/components/structures/MatrixClientContextProvider.tsx +++ b/src/components/structures/MatrixClientContextProvider.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type PropsWithChildren, useEffect, useState } from "react"; +import React, { type PropsWithChildren, useEffect, useState, type JSX } from "react"; import { type MatrixClient } from "matrix-js-sdk/src/matrix"; import { CryptoEvent } from "matrix-js-sdk/src/crypto-api"; import { logger } from "matrix-js-sdk/src/logger"; @@ -85,7 +85,7 @@ interface Props { * A React component which exposes a {@link MatrixClientContext} and a {@link LocalDeviceVerificationStateContext} * to its children. */ -export function MatrixClientContextProvider(props: PropsWithChildren): React.JSX.Element { +export function MatrixClientContextProvider(props: PropsWithChildren): JSX.Element { const verificationState = useLocalVerificationState(props.client); return ( diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index 21e2a2ee71..94a5d34d60 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type ChangeEvent } from "react"; +import React from "react"; import { type Room, type RoomState, RoomStateEvent, RoomMember, type MatrixEvent } from "matrix-js-sdk/src/matrix"; import { throttle } from "lodash"; @@ -15,7 +15,7 @@ import dis from "../../dispatcher/dispatcher"; import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases"; import RightPanelStore from "../../stores/right-panel/RightPanelStore"; import MatrixClientContext from "../../contexts/MatrixClientContext"; -import RoomSummaryCard from "../views/right_panel/RoomSummaryCard"; +import RoomSummaryCardView from "../views/right_panel/RoomSummaryCardView"; import WidgetCard from "../views/right_panel/WidgetCard"; import UserInfo from "../views/right_panel/UserInfo"; import ThirdPartyMemberInfo from "../views/rooms/ThirdPartyMemberInfo"; @@ -49,8 +49,9 @@ interface RoomlessProps extends BaseProps { interface RoomProps extends BaseProps { room: Room; permalinkCreator: RoomPermalinkCreator; - onSearchChange?: (e: ChangeEvent) => void; + onSearchChange?: (term: string) => void; onSearchCancel?: () => void; + searchTerm?: string; } type Props = XOR; @@ -254,12 +255,13 @@ export default class RightPanel extends React.Component { case RightPanelPhases.RoomSummary: if (!!this.props.room) { card = ( - ); @@ -280,7 +282,7 @@ export default class RightPanel extends React.Component { } return ( -
); @@ -997,28 +1012,6 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n ); } - let messageSearchSection: JSX.Element | undefined; - if (filter === null) { - messageSearchSection = ( -
-

- {_t("spotlight_dialog|message_search_section_title")} -

-
- {_t( - "spotlight_dialog|search_messages_hint", - {}, - { icon: () =>
}, - )} -
-
- ); - } - content = ( <> {peopleSection} @@ -1031,7 +1024,6 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n {hiddenResultsSection} {otherSearchesSection} {groupChatSection} - {messageSearchSection} ); } else { diff --git a/src/components/views/directory/NetworkDropdown.tsx b/src/components/views/directory/NetworkDropdown.tsx index 82714915cc..e46b89578a 100644 --- a/src/components/views/directory/NetworkDropdown.tsx +++ b/src/components/views/directory/NetworkDropdown.tsx @@ -222,10 +222,10 @@ export const NetworkDropdown: React.FC = ({ protocols, config, setConfig const [ok, newServer] = await finished; if (!ok) return; - if (!allServers.includes(newServer)) { - setUserDefinedServers([...userDefinedServers, newServer]); + if (!allServers.includes(newServer!)) { + setUserDefinedServers([...userDefinedServers, newServer!]); setConfig({ - roomServer: newServer, + roomServer: newServer!, }); } }} diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx index b5d13bf8e7..a3e8a57c47 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -10,8 +10,6 @@ import React, { type JSX, type ComponentProps, type ComponentPropsWithoutRef, - forwardRef, - type FunctionComponent, type ReactElement, type KeyboardEvent, type Ref, @@ -100,6 +98,8 @@ type Props = { * Whether the tooltip should be disabled. */ disableTooltip?: TooltipProps["disabled"]; + + ref?: Ref; }; export type ButtonProps = Props & Omit, keyof Props>; @@ -119,28 +119,26 @@ type RenderedElementProps = React.InputHTMLAttributes( - { - element, - onClick, - children, - kind, - disabled, - className, - onKeyDown, - onKeyUp, - triggerOnMouseDown, - title, - caption, - placement = "right", - onTooltipOpenChange, - disableTooltip, - role = "button", - tabIndex = 0, - ...restProps - }: ButtonProps, - ref: Ref, -): JSX.Element { +const AccessibleButton = function AccessibleButton({ + element, + onClick, + children, + kind, + disabled, + className, + onKeyDown, + onKeyUp, + triggerOnMouseDown, + title, + caption, + placement = "right", + onTooltipOpenChange, + disableTooltip, + role = "button", + tabIndex = 0, + ref, + ...restProps +}: ButtonProps): JSX.Element { const newProps = { ...restProps, tabIndex, @@ -226,10 +224,7 @@ const AccessibleButton = forwardRef(function { ref?: Ref; diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index b4c3b72334..e3ee6f20d5 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type JSX, createRef, type CSSProperties, useRef, useState } from "react"; +import React, { type JSX, createRef, type CSSProperties, useRef, useState, useMemo } from "react"; import FocusLock from "react-focus-lock"; import { type MatrixEvent, parseErrorResponse } from "matrix-js-sdk/src/matrix"; @@ -33,6 +33,7 @@ import AccessibleButton from "./AccessibleButton"; import Modal from "../../../Modal"; import ErrorDialog from "../dialogs/ErrorDialog"; import { FileDownloader } from "../../../utils/FileDownloader"; +import { MediaEventHelper } from "../../../utils/MediaEventHelper.ts"; // Max scale to keep gaps around the image const MAX_SCALE = 0.95; @@ -549,7 +550,7 @@ export default class ImageView extends React.Component { title={_t("lightbox|rotate_right")} onClick={this.onRotateClockwiseClick} /> - + {contextMenuButton} { } } -function DownloadButton({ url, fileName }: { url: string; fileName?: string }): JSX.Element { +function DownloadButton({ + url, + fileName, + mxEvent, +}: { + url: string; + fileName?: string; + mxEvent?: MatrixEvent; +}): JSX.Element { const downloader = useRef(new FileDownloader()).current; const [loading, setLoading] = useState(false); const blobRef = useRef(undefined); + const mediaEventHelper = useMemo(() => (mxEvent ? new MediaEventHelper(mxEvent) : undefined), [mxEvent]); function showError(e: unknown): void { Modal.createDialog(ErrorDialog, { @@ -625,7 +635,7 @@ function DownloadButton({ url, fileName }: { url: string; fileName?: string }): async function downloadBlob(blob: Blob): Promise { await downloader.download({ blob, - name: fileName ?? _t("common|image"), + name: mediaEventHelper?.fileName ?? fileName ?? _t("common|image"), }); setLoading(false); } diff --git a/src/components/views/elements/LanguageDropdown.tsx b/src/components/views/elements/LanguageDropdown.tsx index c4a72c4f4f..7bf50f7806 100644 --- a/src/components/views/elements/LanguageDropdown.tsx +++ b/src/components/views/elements/LanguageDropdown.tsx @@ -11,7 +11,6 @@ import React, { type ReactElement } from "react"; import classNames from "classnames"; import * as languageHandler from "../../../languageHandler"; -import SettingsStore from "../../../settings/SettingsStore"; import { _t } from "../../../languageHandler"; import Spinner from "./Spinner"; import Dropdown from "./Dropdown"; @@ -29,7 +28,7 @@ function languageMatchesSearchQuery(query: string, language: Languages[0]): bool interface IProps { className?: string; onOptionChange: (language: string) => void; - value?: string; + value: string; disabled?: boolean; } @@ -103,17 +102,6 @@ export default class LanguageDropdown extends React.Component { return
{language.labelInTargetLanguage}
; }) as NonEmptyArray; - // default value here too, otherwise we need to handle null / undefined - // values between mounting and the initial value propagating - let language = SettingsStore.getValue("language", null, /*excludeDefault:*/ true); - let value: string | undefined; - if (language) { - value = this.props.value || language; - } else { - language = navigator.language || navigator.userLanguage; - value = this.props.value || language; - } - return ( { onOptionChange={this.props.onOptionChange} onSearchChange={this.onSearchChange} searchEnabled={true} - value={value} + value={this.props.value} label={_t("language_dropdown_label")} disabled={this.props.disabled} > diff --git a/src/components/views/elements/PollCreateDialog.tsx b/src/components/views/elements/PollCreateDialog.tsx index 8e23074ca5..41a3bc9dfb 100644 --- a/src/components/views/elements/PollCreateDialog.tsx +++ b/src/components/views/elements/PollCreateDialog.tsx @@ -167,18 +167,18 @@ export default class PollCreateDialog extends ScrollableBaseModal this.props.onFinished(true)) .catch((e) => { console.error("Failed to post poll:", e); - Modal.createDialog(QuestionDialog, { + const { finished } = Modal.createDialog(QuestionDialog, { title: _t("poll|failed_send_poll_title"), description: _t("poll|failed_send_poll_description"), button: _t("action|try_again"), cancelButton: _t("action|cancel"), - onFinished: (tryAgain: boolean) => { - if (!tryAgain) { - this.cancel(); - } else { - this.setState({ busy: false, canSubmit: true }); - } - }, + }); + finished.then(([tryAgain]) => { + if (!tryAgain) { + this.cancel(); + } else { + this.setState({ busy: false, canSubmit: true }); + } }); }); } diff --git a/src/components/views/elements/ServerPicker.tsx b/src/components/views/elements/ServerPicker.tsx index 6740534711..430f205377 100644 --- a/src/components/views/elements/ServerPicker.tsx +++ b/src/components/views/elements/ServerPicker.tsx @@ -28,9 +28,10 @@ interface IProps { const showPickerDialog = ( title: string | undefined, serverConfig: ValidatedServerConfig, - onFinished: (config: ValidatedServerConfig) => void, + onFinished: (config?: ValidatedServerConfig) => void, ): void => { - Modal.createDialog(ServerPickerDialog, { title, serverConfig, onFinished }); + const { finished } = Modal.createDialog(ServerPickerDialog, { title, serverConfig }); + finished.then(([config]) => onFinished(config)); }; const onHelpClick = (): void => { diff --git a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx index a267999f5b..88a1ab1bd6 100644 --- a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx +++ b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx @@ -10,7 +10,6 @@ import React, { type ReactElement } from "react"; import Dropdown from "../../views/elements/Dropdown"; import PlatformPeg from "../../../PlatformPeg"; -import SettingsStore from "../../../settings/SettingsStore"; import { _t, getUserLanguage } from "../../../languageHandler"; import Spinner from "./Spinner"; import { type NonEmptyArray } from "../../../@types/common"; @@ -105,17 +104,6 @@ export default class SpellCheckLanguagesDropdown extends React.Component< return
{language.label}
; }) as NonEmptyArray; - // default value here too, otherwise we need to handle null / undefined; - // values between mounting and the initial value propagating - let language = SettingsStore.getValue("language", null, /*excludeDefault:*/ true); - let value: string | undefined; - if (language) { - value = this.props.value || language; - } else { - language = navigator.language || navigator.userLanguage; - value = this.props.value || language; - } - return ( diff --git a/src/components/views/location/Marker.tsx b/src/components/views/location/Marker.tsx index 13faa8cb68..8502580c2e 100644 --- a/src/components/views/location/Marker.tsx +++ b/src/components/views/location/Marker.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type ReactNode, useState } from "react"; +import React, { type JSX, type ReactNode, type Ref, useState } from "react"; import classNames from "classnames"; import { type RoomMember } from "matrix-js-sdk/src/matrix"; import LocationIcon from "@vector-im/compound-design-tokens/assets/web/icons/location-pin-solid"; @@ -21,6 +21,7 @@ interface Props { // use member text color as background useMemberColor?: boolean; tooltip?: ReactNode; + ref?: Ref; } /** @@ -55,7 +56,7 @@ const OptionalTooltip: React.FC<{ /** * Generic location marker */ -const Marker = React.forwardRef(({ id, roomMember, useMemberColor, tooltip }, ref) => { +const Marker = ({ id, roomMember, useMemberColor, tooltip, ref }: Props): JSX.Element => { const memberColorClass = useMemberColor && roomMember ? getUserNameColorClass(roomMember.userId) : ""; return (
(({ id, roomMember, useMem
); -}); +}; export default Marker; diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index b3185921bb..f8efd8381e 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { forwardRef, useCallback, useContext, useMemo } from "react"; +import React, { type Ref, useCallback, useContext, useMemo, type JSX } from "react"; import type { MatrixEvent, RoomMember } from "matrix-js-sdk/src/matrix"; import { ConnectionState, type ElementCall } from "../../../models/Call"; @@ -37,60 +37,69 @@ interface ActiveCallEventProps { buttonKind: AccessibleButtonKind; buttonDisabledTooltip?: string; onButtonClick: ((ev: ButtonEvent) => void) | null; + ref?: Ref; } -const ActiveCallEvent = forwardRef( - ({ mxEvent, call, participatingMembers, buttonText, buttonKind, buttonDisabledTooltip, onButtonClick }, ref) => { - const senderName = useMemo(() => mxEvent.sender?.name ?? mxEvent.getSender(), [mxEvent]); +const ActiveCallEvent = ({ + mxEvent, + call, + participatingMembers, + buttonText, + buttonKind, + buttonDisabledTooltip, + onButtonClick, + ref, +}: ActiveCallEventProps): JSX.Element => { + const senderName = useMemo(() => mxEvent.sender?.name ?? mxEvent.getSender(), [mxEvent]); - const facePileMembers = useMemo(() => participatingMembers.slice(0, MAX_FACES), [participatingMembers]); - const facePileOverflow = participatingMembers.length > facePileMembers.length; + const facePileMembers = useMemo(() => participatingMembers.slice(0, MAX_FACES), [participatingMembers]); + const facePileOverflow = participatingMembers.length > facePileMembers.length; - return ( -
-
- -
-
- - {_t("timeline|m.call|video_call_started_text", { name: senderName })} - - - -
- {call && } - - {buttonText} - + return ( +
+
+ +
+
+ + {_t("timeline|m.call|video_call_started_text", { name: senderName })} + + +
+ {call && } + + {buttonText} +
- ); - }, -); +
+ ); +}; interface ActiveLoadedCallEventProps { mxEvent: MatrixEvent; call: ElementCall; + ref?: Ref; } -const ActiveLoadedCallEvent = forwardRef(({ mxEvent, call }, ref) => { +const ActiveLoadedCallEvent = ({ mxEvent, call, ref }: ActiveLoadedCallEventProps): JSX.Element => { const connectionState = useConnectionState(call); const participatingMembers = useParticipatingMembers(call); const joinCallButtonDisabledTooltip = useJoinCallButtonDisabledTooltip(call); @@ -141,16 +150,17 @@ const ActiveLoadedCallEvent = forwardRef(({ mxE onButtonClick={onButtonClick} /> ); -}); +}; interface CallEventProps { mxEvent: MatrixEvent; + ref?: Ref; } /** * An event tile representing an active or historical Element call. */ -export const CallEvent = forwardRef(({ mxEvent }, ref) => { +export const CallEvent = ({ mxEvent, ref }: CallEventProps): JSX.Element => { const client = useContext(MatrixClientContext); const call = useCall(mxEvent.getRoomId()!); const latestEvent = client @@ -187,4 +197,4 @@ export const CallEvent = forwardRef(({ mxEvent }, ref) => { } return ; -}); +}; diff --git a/src/components/views/messages/DecryptionFailureBody.tsx b/src/components/views/messages/DecryptionFailureBody.tsx index 1158749571..f75a7c48f8 100644 --- a/src/components/views/messages/DecryptionFailureBody.tsx +++ b/src/components/views/messages/DecryptionFailureBody.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import classNames from "classnames"; -import React, { forwardRef, type ForwardRefExoticComponent, useContext } from "react"; +import React, { type JSX, useContext } from "react"; import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; import { DecryptionFailureCode } from "matrix-js-sdk/src/crypto-api"; import { BlockIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; @@ -16,7 +16,7 @@ import { _t } from "../../../languageHandler"; import { type IBodyProps } from "./IBodyProps"; import { LocalDeviceVerificationStateContext } from "../../../contexts/LocalDeviceVerificationStateContext"; -function getErrorMessage(mxEvent: MatrixEvent, isVerified: boolean | undefined): string | React.JSX.Element { +function getErrorMessage(mxEvent: MatrixEvent, isVerified: boolean | undefined): string | JSX.Element { switch (mxEvent.decryptionFailureReason) { case DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE: return _t("timeline|decryption_failure|blocked"); @@ -72,7 +72,7 @@ function errorClassName(mxEvent: MatrixEvent): string | null { } // A placeholder element for messages that could not be decrypted -export const DecryptionFailureBody = forwardRef(({ mxEvent }, ref): React.JSX.Element => { +export const DecryptionFailureBody = ({ mxEvent, ref }: IBodyProps): JSX.Element => { const verificationState = useContext(LocalDeviceVerificationStateContext); const classes = classNames("mx_DecryptionFailureBody", "mx_EventTile_content", errorClassName(mxEvent)); @@ -81,4 +81,4 @@ export const DecryptionFailureBody = forwardRef(({ m {getErrorMessage(mxEvent, verificationState)}
); -}) as ForwardRefExoticComponent; +}; diff --git a/src/components/views/messages/EncryptionEvent.tsx b/src/components/views/messages/EncryptionEvent.tsx index e612a5be4d..5c5f1f0dc2 100644 --- a/src/components/views/messages/EncryptionEvent.tsx +++ b/src/components/views/messages/EncryptionEvent.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type JSX, forwardRef } from "react"; +import React, { type JSX, type Ref, type ReactNode } from "react"; import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; import type { RoomEncryptionEventContent } from "matrix-js-sdk/src/types"; @@ -22,9 +22,10 @@ import { useIsEncrypted } from "../../../hooks/useIsEncrypted.ts"; interface IProps { mxEvent: MatrixEvent; timestamp?: JSX.Element; + ref?: Ref; } -const EncryptionEvent = forwardRef(({ mxEvent, timestamp }, ref) => { +const EncryptionEvent = ({ mxEvent, timestamp, ref }: IProps): ReactNode => { const cli = useMatrixClientContext(); const roomId = mxEvent.getRoomId()!; const isRoomEncrypted = useIsEncrypted(cli, cli.getRoom(roomId) || undefined); @@ -80,6 +81,6 @@ const EncryptionEvent = forwardRef(({ mxEvent, timestamp timestamp={timestamp} /> ); -}); +}; export default EncryptionEvent; diff --git a/src/components/views/messages/EventContentBody.tsx b/src/components/views/messages/EventContentBody.tsx index 3e51691c24..fce5428e8c 100644 --- a/src/components/views/messages/EventContentBody.tsx +++ b/src/components/views/messages/EventContentBody.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { memo, forwardRef, useContext, useMemo } from "react"; +import React, { memo, useContext, useMemo, type Ref } from "react"; import { type IContent, type MatrixEvent, MsgType, PushRuleKind } from "matrix-js-sdk/src/matrix"; import parse from "html-react-parser"; import { PushProcessor } from "matrix-js-sdk/src/pushprocessor"; @@ -28,6 +28,7 @@ import { import MatrixClientContext from "../../../contexts/MatrixClientContext.tsx"; import { useSettingValue } from "../../../hooks/useSettings.ts"; import { filterBoolean } from "../../../utils/arrays.ts"; +import { useMediaVisible } from "../../../hooks/useMediaVisible.ts"; /** * Returns a RegExp pattern for the keyword in the push rule of the given Matrix event, if any @@ -138,6 +139,7 @@ interface Props extends ReplacerOptions { * Whether to include the `dir="auto"` attribute on the rendered element */ includeDir?: boolean; + ref?: Ref; } /** @@ -147,50 +149,50 @@ interface Props extends ReplacerOptions { * Returns a div or span depending on `as`, the `dir` on a `div` is always set to `"auto"` but set by `includeDir` otherwise. */ const EventContentBody = memo( - forwardRef( - ({ as, mxEvent, stripReply, content, linkify, highlights, includeDir = true, ...options }, ref) => { - const enableBigEmoji = useSettingValue("TextualBody.enableBigEmoji"); + ({ as, mxEvent, stripReply, content, linkify, highlights, includeDir = true, ref, ...options }: Props) => { + const enableBigEmoji = useSettingValue("TextualBody.enableBigEmoji"); + const [mediaIsVisible] = useMediaVisible(mxEvent?.getId(), mxEvent?.getRoomId()); - const replacer = useReplacer(content, mxEvent, options); - const linkifyOptions = useMemo( - () => ({ - render: replacerToRenderFunction(replacer), + const replacer = useReplacer(content, mxEvent, options); + const linkifyOptions = useMemo( + () => ({ + render: replacerToRenderFunction(replacer), + }), + [replacer], + ); + + const isEmote = content.msgtype === MsgType.Emote; + + const { strippedBody, formattedBody, emojiBodyElements, className } = useMemo( + () => + bodyToNode(content, highlights, { + disableBigEmoji: isEmote || !enableBigEmoji, + // Part of Replies fallback support + stripReplyFallback: stripReply, + mediaIsVisible, }), - [replacer], - ); + [content, mediaIsVisible, enableBigEmoji, highlights, isEmote, stripReply], + ); - const isEmote = content.msgtype === MsgType.Emote; + if (as === "div") includeDir = true; // force dir="auto" on divs - const { strippedBody, formattedBody, emojiBodyElements, className } = useMemo( - () => - bodyToNode(content, highlights, { - disableBigEmoji: isEmote || !enableBigEmoji, - // Part of Replies fallback support - stripReplyFallback: stripReply, - }), - [content, enableBigEmoji, highlights, isEmote, stripReply], - ); + const As = as; + const body = formattedBody ? ( + + {parse(formattedBody, { + replace: replacer, + })} + + ) : ( + + {applyReplacerOnString(emojiBodyElements || strippedBody, replacer)} + + ); - if (as === "div") includeDir = true; // force dir="auto" on divs + if (!linkify) return body; - const As = as; - const body = formattedBody ? ( - - {parse(formattedBody, { - replace: replacer, - })} - - ) : ( - - {applyReplacerOnString(emojiBodyElements || strippedBody, replacer)} - - ); - - if (!linkify) return body; - - return {body}; - }, - ), + return {body}; + }, ); export default EventContentBody; diff --git a/src/components/views/messages/EventTileBubble.tsx b/src/components/views/messages/EventTileBubble.tsx index 56fbc83b86..4569115c0d 100644 --- a/src/components/views/messages/EventTileBubble.tsx +++ b/src/components/views/messages/EventTileBubble.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type JSX, forwardRef, type ReactNode } from "react"; +import React, { type JSX, type ReactNode, type Ref } from "react"; import classNames from "classnames"; interface IProps { @@ -15,19 +15,18 @@ interface IProps { timestamp?: JSX.Element; subtitle?: ReactNode; children?: JSX.Element; + ref?: Ref; } -const EventTileBubble = forwardRef( - ({ className, title, timestamp, subtitle, children }, ref) => { - return ( -
-
{title}
- {subtitle &&
{subtitle}
} - {children} - {timestamp} -
- ); - }, -); +const EventTileBubble = ({ className, title, timestamp, subtitle, children, ref }: IProps): JSX.Element => { + return ( +
+
{title}
+ {subtitle &&
{subtitle}
} + {children} + {timestamp} +
+ ); +}; export default EventTileBubble; diff --git a/src/components/views/messages/HiddenBody.tsx b/src/components/views/messages/HiddenBody.tsx index 9cabbe43dc..20410017be 100644 --- a/src/components/views/messages/HiddenBody.tsx +++ b/src/components/views/messages/HiddenBody.tsx @@ -6,23 +6,18 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React from "react"; -import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; +import React, { type JSX } from "react"; import { _t } from "../../../languageHandler"; import { type IBodyProps } from "./IBodyProps"; -interface IProps { - mxEvent: MatrixEvent; -} - /** * A message hidden from the user pending moderation. * * Note: This component must not be used when the user is the author of the message * or has a sufficient powerlevel to see the message. */ -const HiddenBody = React.forwardRef(({ mxEvent }, ref) => { +const HiddenBody = ({ mxEvent, ref }: IBodyProps): JSX.Element => { let text; const visibility = mxEvent.messageVisibility(); switch (visibility.visible) { @@ -42,6 +37,6 @@ const HiddenBody = React.forwardRef(({ mxEvent }, ref) {text} ); -}); +}; export default HiddenBody; diff --git a/src/components/views/messages/HideActionButton.tsx b/src/components/views/messages/HideActionButton.tsx index 8d2baf0220..0c9817b2a6 100644 --- a/src/components/views/messages/HideActionButton.tsx +++ b/src/components/views/messages/HideActionButton.tsx @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024, 2025 New Vector Ltd. Copyright 2021 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial @@ -25,7 +25,7 @@ interface IProps { * Quick action button for marking a media event as hidden. */ export const HideActionButton: React.FC = ({ mxEvent }) => { - const [mediaIsVisible, setVisible] = useMediaVisible(mxEvent.getId()!); + const [mediaIsVisible, setVisible] = useMediaVisible(mxEvent.getId(), mxEvent.getRoomId()); if (!mediaIsVisible) { return; diff --git a/src/components/views/messages/IBodyProps.ts b/src/components/views/messages/IBodyProps.ts index 59f5030c75..37aae37de6 100644 --- a/src/components/views/messages/IBodyProps.ts +++ b/src/components/views/messages/IBodyProps.ts @@ -6,7 +6,6 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { type LegacyRef } from "react"; import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; import type React from "react"; @@ -44,7 +43,7 @@ export interface IBodyProps { // helper function to access relations for this event getRelationsForEvent?: GetRelationsForEvent; - ref?: React.RefObject | LegacyRef; + ref?: React.RefObject; // Set to `true` to disable interactions (e.g. video controls) and to remove controls from the tab order. // This may be useful when displaying a preview of the event. diff --git a/src/components/views/messages/MBeaconBody.tsx b/src/components/views/messages/MBeaconBody.tsx index 0b9528db50..48294296be 100644 --- a/src/components/views/messages/MBeaconBody.tsx +++ b/src/components/views/messages/MBeaconBody.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type JSX, type ForwardRefExoticComponent, useCallback, useContext, useEffect, useState } from "react"; +import React, { type JSX, useCallback, useContext, useEffect, useState } from "react"; import { type Beacon, BeaconEvent, @@ -122,7 +122,7 @@ const useHandleBeaconRedaction = ( }, [event, onBeforeBeaconInfoRedaction]); }; -const MBeaconBody = React.forwardRef(({ mxEvent, getRelationsForEvent }, ref) => { +const MBeaconBody = ({ mxEvent, getRelationsForEvent, ref }: IBodyProps): JSX.Element => { const { beacon, isLive, latestLocationState, waitingToStart } = useBeaconState(mxEvent); const mapId = useUniqueId(mxEvent.getId()!); @@ -225,6 +225,6 @@ const MBeaconBody = React.forwardRef(({ mxEvent, get )}
); -}) as ForwardRefExoticComponent; +}; export default MBeaconBody; diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index 7c42ea4462..79f840ce39 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -686,7 +686,7 @@ export class MImageBodyInner extends React.Component { // Wrap MImageBody component so we can use a hook here. const MImageBody: React.FC = (props) => { - const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId()!); + const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId()); return ; }; diff --git a/src/components/views/messages/MImageReplyBody.tsx b/src/components/views/messages/MImageReplyBody.tsx index 788ca2d7e9..b73f8f77c3 100644 --- a/src/components/views/messages/MImageReplyBody.tsx +++ b/src/components/views/messages/MImageReplyBody.tsx @@ -38,7 +38,7 @@ class MImageReplyBodyInner extends MImageBodyInner { } } const MImageReplyBody: React.FC = (props) => { - const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId()!); + const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId()); return ; }; diff --git a/src/components/views/messages/MPollEndBody.tsx b/src/components/views/messages/MPollEndBody.tsx index a3a42a7843..95f8a53f2a 100644 --- a/src/components/views/messages/MPollEndBody.tsx +++ b/src/components/views/messages/MPollEndBody.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { useEffect, useState, useContext, type ForwardRefExoticComponent } from "react"; +import React, { useEffect, useState, useContext, type JSX } from "react"; import { MatrixEvent, M_TEXT } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; @@ -85,7 +85,7 @@ const usePollStartEvent = (event: MatrixEvent): { pollStartEvent?: MatrixEvent; return { pollStartEvent, isLoadingPollStartEvent }; }; -export const MPollEndBody = React.forwardRef(({ mxEvent, ...props }, ref) => { +export const MPollEndBody = ({ mxEvent, ref, ...props }: IBodyProps): JSX.Element => { const cli = useMatrixClientContext(); const { pollStartEvent, isLoadingPollStartEvent } = usePollStartEvent(mxEvent); @@ -105,4 +105,4 @@ export const MPollEndBody = React.forwardRef(({ mxEvent, ...pro
); -}) as ForwardRefExoticComponent; +}; diff --git a/src/components/views/messages/MStickerBody.tsx b/src/components/views/messages/MStickerBody.tsx index 365ebc652a..3a922d35aa 100644 --- a/src/components/views/messages/MStickerBody.tsx +++ b/src/components/views/messages/MStickerBody.tsx @@ -79,7 +79,7 @@ class MStickerBodyInner extends MImageBodyInner { } const MStickerBody: React.FC = (props) => { - const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId()!); + const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId()); return ; }; diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx index 58ba7575fe..6a36dae6a8 100644 --- a/src/components/views/messages/MVideoBody.tsx +++ b/src/components/views/messages/MVideoBody.tsx @@ -342,7 +342,7 @@ class MVideoBodyInner extends React.PureComponent { // Wrap MVideoBody component so we can use a hook here. const MVideoBody: React.FC = (props) => { - const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId()!); + const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId()); return ; }; diff --git a/src/components/views/messages/RedactedBody.tsx b/src/components/views/messages/RedactedBody.tsx index 9048d176ca..ddad4d2e4d 100644 --- a/src/components/views/messages/RedactedBody.tsx +++ b/src/components/views/messages/RedactedBody.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type ForwardRefExoticComponent, useContext } from "react"; +import React, { useContext, type JSX } from "react"; import { type MatrixClient } from "matrix-js-sdk/src/matrix"; import { _t } from "../../../languageHandler"; @@ -15,7 +15,7 @@ import { formatFullDate } from "../../../DateUtils"; import SettingsStore from "../../../settings/SettingsStore"; import { type IBodyProps } from "./IBodyProps"; -const RedactedBody = React.forwardRef(({ mxEvent }, ref) => { +const RedactedBody = ({ mxEvent, ref }: IBodyProps): JSX.Element => { const cli: MatrixClient = useContext(MatrixClientContext); let text = _t("timeline|self_redaction"); const unsigned = mxEvent.getUnsigned(); @@ -37,6 +37,6 @@ const RedactedBody = React.forwardRef(({ mxEvent }, ref) => { {text} ); -}) as ForwardRefExoticComponent; +}; export default RedactedBody; diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index b51d2fa3f3..d0107b31ec 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -235,7 +235,7 @@ export default class TextualBody extends React.Component { scalarClient?.connect().then(() => { const completeUrl = scalarClient.getStarterLink(starterLink); const integrationsUrl = integrationManager!.uiUrl; - Modal.createDialog(QuestionDialog, { + const { finished } = Modal.createDialog(QuestionDialog, { title: _t("timeline|scalar_starter_link|dialog_title"), description: (
@@ -243,18 +243,19 @@ export default class TextualBody extends React.Component {
), button: _t("action|continue"), - onFinished(confirmed) { - if (!confirmed) { - return; - } - const width = window.screen.width > 1024 ? 1024 : window.screen.width; - const height = window.screen.height > 800 ? 800 : window.screen.height; - const left = (window.screen.width - width) / 2; - const top = (window.screen.height - height) / 2; - const features = `height=${height}, width=${width}, top=${top}, left=${left},`; - const wnd = window.open(completeUrl, "_blank", features)!; - wnd.opener = null; - }, + }); + + finished.then(([confirmed]) => { + if (!confirmed) { + return; + } + const width = window.screen.width > 1024 ? 1024 : window.screen.width; + const height = window.screen.height > 800 ? 800 : window.screen.height; + const left = (window.screen.width - width) / 2; + const top = (window.screen.height - height) / 2; + const features = `height=${height}, width=${width}, top=${top}, left=${left},`; + const wnd = window.open(completeUrl, "_blank", features)!; + wnd.opener = null; }); }); }; diff --git a/src/components/views/messages/UnknownBody.tsx b/src/components/views/messages/UnknownBody.tsx index e72f8d2327..26e4ea5fca 100644 --- a/src/components/views/messages/UnknownBody.tsx +++ b/src/components/views/messages/UnknownBody.tsx @@ -7,16 +7,15 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { forwardRef, type ForwardRefExoticComponent } from "react"; +import React, { type JSX } from "react"; import { type IBodyProps } from "./IBodyProps"; -export default forwardRef(({ mxEvent, children }, ref) => { +export default ({ mxEvent, ref }: IBodyProps): JSX.Element => { const text = mxEvent.getContent().body; return (
{text} - {children}
); -}) as ForwardRefExoticComponent; +}; diff --git a/src/components/views/right_panel/BaseCard.tsx b/src/components/views/right_panel/BaseCard.tsx index 90534947a0..a564a60695 100644 --- a/src/components/views/right_panel/BaseCard.tsx +++ b/src/components/views/right_panel/BaseCard.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { forwardRef, type ReactNode, type KeyboardEvent, type Ref, type MouseEvent } from "react"; +import React, { type ReactNode, type KeyboardEvent, type Ref, type MouseEvent } from "react"; import classNames from "classnames"; import { IconButton, Text } from "@vector-im/compound-web"; import CloseIcon from "@vector-im/compound-design-tokens/assets/web/icons/close"; @@ -44,106 +44,102 @@ function closeRightPanel(ev: MouseEvent): void { RightPanelStore.instance.popCard(); } -const BaseCard: React.FC = forwardRef( - ( - { - closeLabel, - onClose, - onBack, - className, - id, - ariaLabelledBy, - role, - hideHeaderButtons, - header, - footer, - withoutScrollContainer, - children, - onKeyDown, - closeButtonRef, - }, - ref, - ) => { - let backButton; - const cardHistory = RightPanelStore.instance.roomPhaseHistory; - if (cardHistory.length > 1 && !hideHeaderButtons) { - const prevCard = cardHistory[cardHistory.length - 2]; - const onBackClick = (ev: MouseEvent): void => { - onBack?.(ev); - RightPanelStore.instance.popCard(); - }; - const label = backLabelForPhase(prevCard.phase) ?? _t("action|back"); - backButton = ( - - - - ); - } - - let closeButton; - if (!hideHeaderButtons) { - closeButton = ( - - - - ); - } - - if (!withoutScrollContainer) { - children = {children}; - } - - const shouldRenderHeader = header || !hideHeaderButtons; - - return ( - -
- {shouldRenderHeader && ( -
- {backButton} - {typeof header === "string" ? ( -
- - {header} - -
- ) : ( - (header ??
) - )} - {closeButton} -
- )} - {children} - {footer &&
{footer}
} -
- +const BaseCard: React.FC = ({ + closeLabel, + onClose, + onBack, + className, + id, + ariaLabelledBy, + role, + hideHeaderButtons, + header, + footer, + withoutScrollContainer, + children, + onKeyDown, + closeButtonRef, + ref, +}: IProps) => { + let backButton; + const cardHistory = RightPanelStore.instance.roomPhaseHistory; + if (cardHistory.length > 1 && !hideHeaderButtons) { + const prevCard = cardHistory[cardHistory.length - 2]; + const onBackClick = (ev: MouseEvent): void => { + onBack?.(ev); + RightPanelStore.instance.popCard(); + }; + const label = backLabelForPhase(prevCard.phase) ?? _t("action|back"); + backButton = ( + + + ); - }, -); + } + + let closeButton; + if (!hideHeaderButtons) { + closeButton = ( + + + + ); + } + + if (!withoutScrollContainer) { + children = {children}; + } + + const shouldRenderHeader = header || !hideHeaderButtons; + + return ( + +
+ {shouldRenderHeader && ( +
+ {backButton} + {typeof header === "string" ? ( +
+ + {header} + +
+ ) : ( + (header ??
) + )} + {closeButton} +
+ )} + {children} + {footer &&
{footer}
} +
+ + ); +}; export default BaseCard; diff --git a/src/components/views/right_panel/RoomSummaryCard.tsx b/src/components/views/right_panel/RoomSummaryCardView.tsx similarity index 56% rename from src/components/views/right_panel/RoomSummaryCard.tsx rename to src/components/views/right_panel/RoomSummaryCardView.tsx index 7894ec0d89..d994e697c6 100644 --- a/src/components/views/right_panel/RoomSummaryCard.tsx +++ b/src/components/views/right_panel/RoomSummaryCardView.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type JSX, type ChangeEvent, useContext, useEffect, useRef, useState } from "react"; +import React, { useEffect, useState, type JSX } from "react"; import classNames from "classnames"; import { MenuItem, @@ -38,81 +38,30 @@ import PublicIcon from "@vector-im/compound-design-tokens/assets/web/icons/publi import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error"; import ErrorSolidIcon from "@vector-im/compound-design-tokens/assets/web/icons/error-solid"; import ChevronDownIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-down"; -import { EventType, JoinRule, type Room, RoomStateEvent } from "matrix-js-sdk/src/matrix"; +import { JoinRule, type Room } from "matrix-js-sdk/src/matrix"; -import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import { useIsEncrypted } from "../../../hooks/useIsEncrypted"; -import BaseCard from "./BaseCard"; -import { _t } from "../../../languageHandler"; -import RoomAvatar from "../avatars/RoomAvatar"; -import defaultDispatcher from "../../../dispatcher/dispatcher"; -import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases"; -import Modal from "../../../Modal"; -import { ShareDialog } from "../dialogs/ShareDialog"; -import { useEventEmitterState } from "../../../hooks/useEventEmitter"; -import { E2EStatus } from "../../../utils/ShieldUtils"; -import { type RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; -import { TimelineRenderingType } from "../../../contexts/RoomContext"; -import RoomName from "../elements/RoomName"; -import ExportDialog from "../dialogs/ExportDialog"; -import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; -import PosthogTrackers from "../../../PosthogTrackers"; -import { PollHistoryDialog } from "../dialogs/PollHistoryDialog"; -import { Flex } from "../../utils/Flex"; -import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore"; -import { DefaultTagID } from "../../../stores/room-list/models"; -import { tagRoom } from "../../../utils/room/tagRoom"; -import { canInviteTo } from "../../../utils/room/canInviteTo"; -import { inviteToRoom } from "../../../utils/room/inviteToRoom"; -import { useAccountData } from "../../../hooks/useAccountData"; -import { useRoomState } from "../../../hooks/useRoomState"; -import { Linkify, topicToHtml } from "../../../HtmlUtils"; -import { Box } from "../../utils/Box"; -import { useDispatcher } from "../../../hooks/useDispatcher"; -import { Action } from "../../../dispatcher/actions"; -import { Key } from "../../../Keyboard"; -import { useTransition } from "../../../hooks/useTransition"; -import { isVideoRoom as calcIsVideoRoom } from "../../../utils/video-rooms"; -import { usePinnedEvents } from "../../../hooks/usePinnedEvents"; +import BaseCard from "./BaseCard.tsx"; +import { _t } from "../../../languageHandler.tsx"; +import RoomAvatar from "../avatars/RoomAvatar.tsx"; +import { E2EStatus } from "../../../utils/ShieldUtils.ts"; +import { type RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks.ts"; +import RoomName from "../elements/RoomName.tsx"; +import { Flex } from "../../utils/Flex.tsx"; +import { Linkify, topicToHtml } from "../../../HtmlUtils.tsx"; +import { Box } from "../../utils/Box.tsx"; import { ReleaseAnnouncement } from "../../structures/ReleaseAnnouncement.tsx"; -import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; -import { ReportRoomDialog } from "../dialogs/ReportRoomDialog.tsx"; +import { useRoomSummaryCardViewModel } from "../../viewmodels/right_panel/RoomSummaryCardViewModel.tsx"; import { useRoomTopicViewModel } from "../../viewmodels/right_panel/RoomSummaryCardTopicViewModel.tsx"; interface IProps { room: Room; permalinkCreator: RoomPermalinkCreator; - onSearchChange?: (e: ChangeEvent) => void; + onSearchChange?: (term: string) => void; onSearchCancel?: () => void; focusRoomSearch?: boolean; + searchTerm?: string; } -const onRoomMembersClick = (): void => { - RightPanelStore.instance.pushCard({ phase: RightPanelPhases.MemberList }, true); -}; - -const onRoomThreadsClick = (): void => { - RightPanelStore.instance.pushCard({ phase: RightPanelPhases.ThreadPanel }, true); -}; - -const onRoomFilesClick = (): void => { - RightPanelStore.instance.pushCard({ phase: RightPanelPhases.FilePanel }, true); -}; - -const onRoomExtensionsClick = (): void => { - RightPanelStore.instance.pushCard({ phase: RightPanelPhases.Extensions }, true); -}; - -const onRoomPinsClick = (): void => { - PosthogTrackers.trackInteraction("PinnedMessageRoomInfoButton"); - RightPanelStore.instance.pushCard({ phase: RightPanelPhases.PinnedMessages }, true); -}; - -const onRoomSettingsClick = (ev: Event): void => { - defaultDispatcher.dispatch({ action: "open_room_settings" }); - PosthogTrackers.trackInteraction("WebRightPanelRoomInfoSettingsButton", ev); -}; - const RoomTopic: React.FC> = ({ room }): JSX.Element | null => { const vm = useRoomTopicViewModel(room); @@ -143,6 +92,7 @@ const RoomTopic: React.FC> = ({ room }): JSX.Element | null } const content = vm.expanded ? {body} : body; + return ( > = ({ room }): JSX.Element | null ); }; -const RoomSummaryCard: React.FC = ({ +const RoomSummaryCardView: React.FC = ({ room, permalinkCreator, onSearchChange, onSearchCancel, focusRoomSearch, + searchTerm = "", }) => { - const cli = useContext(MatrixClientContext); + const vm = useRoomSummaryCardViewModel(room, permalinkCreator, onSearchCancel); - const onShareRoomClick = (): void => { - Modal.createDialog(ShareDialog, { - target: room, - }); - }; - - const onRoomExportClick = async (): Promise => { - Modal.createDialog(ExportDialog, { - room, - }); - }; - - const onRoomPollHistoryClick = (): void => { - Modal.createDialog(PollHistoryDialog, { - room, - matrixClient: cli, - permalinkCreator, - }); - }; - - const onLeaveRoomClick = (): void => { - defaultDispatcher.dispatch({ - action: "leave_room", - room_id: room.roomId, - }); - }; - const onReportRoomClick = async (): Promise => { - const [leave] = await Modal.createDialog(ReportRoomDialog, { - roomId: room.roomId, - }).finished; - if (leave) { - defaultDispatcher.dispatch({ - action: "leave_room", - room_id: room.roomId, - }); - } - }; - - const isRoomEncrypted = useIsEncrypted(cli, room); - const roomContext = useScopedRoomContext("e2eStatus", "timelineRenderingType"); - const e2eStatus = roomContext.e2eStatus; - const isVideoRoom = calcIsVideoRoom(room); - - const roomState = useRoomState(room); - const directRoomsList = useAccountData>(room.client, EventType.Direct); - const [isDirectMessage, setDirectMessage] = useState(false); + // The search field is controlled and onSearchChange is debounced in RoomView, + // so we need to set the value of the input right away + const [searchValue, setSearchValue] = useState(searchTerm); useEffect(() => { - for (const [, dmRoomList] of Object.entries(directRoomsList)) { - if (dmRoomList.includes(room?.roomId ?? "")) { - setDirectMessage(true); - break; - } - } - }, [room, directRoomsList]); + setSearchValue(searchTerm); + }, [searchTerm]); - const searchInputRef = useRef(null); - useDispatcher(defaultDispatcher, (payload) => { - if (payload.action === Action.FocusMessageSearch) { - searchInputRef.current?.focus(); - } - }); - // Clear the search field when the user leaves the search view - useTransition( - (prevTimelineRenderingType) => { - if ( - prevTimelineRenderingType === TimelineRenderingType.Search && - roomContext.timelineRenderingType !== TimelineRenderingType.Search && - searchInputRef.current - ) { - searchInputRef.current.value = ""; - } - }, - [roomContext.timelineRenderingType], - ); - - const alias = room.getCanonicalAlias() || room.getAltAliases()[0] || ""; const roomInfo = (
@@ -280,34 +162,34 @@ const RoomSummaryCard: React.FC = ({ size="sm" weight="semibold" className="mx_RoomSummaryCard_alias text-secondary" - title={alias} + title={vm.alias} > - {alias} + {vm.alias} - {!isDirectMessage && roomState.getJoinRule() === JoinRule.Public && ( + {!vm.isDirectMessage && vm.roomJoinRule === JoinRule.Public && ( {_t("common|public_room")} )} - {isRoomEncrypted && e2eStatus !== E2EStatus.Warning && ( + {vm.isRoomEncrypted && vm.e2eStatus !== E2EStatus.Warning && ( {_t("common|encrypted")} )} - {!e2eStatus && ( + {!vm.isRoomEncrypted && ( {_t("common|unencrypted")} )} - {e2eStatus === E2EStatus.Warning && ( + {vm.e2eStatus === E2EStatus.Warning && ( {_t("common|not_trusted")} @@ -319,29 +201,20 @@ const RoomSummaryCard: React.FC = ({
); - const pinCount = usePinnedEvents(room).length; - - const roomTags = useEventEmitterState(RoomListStore.instance, LISTS_UPDATE_EVENT, () => - RoomListStore.instance.getTagsForRoom(room), - ); - const canInviteToState = useEventEmitterState(room, RoomStateEvent.Update, () => canInviteTo(room)); - const isFavorite = roomTags.includes(DefaultTagID.Favourite); - const header = onSearchChange && ( e.preventDefault()}> { - if (searchInputRef.current && e.key === Key.ESCAPE) { - searchInputRef.current.value = ""; - onSearchCancel?.(); - } + onChange={(e) => { + setSearchValue(e.currentTarget.value); + onSearchChange(e.currentTarget.value); }} + value={searchValue} + className="mx_no_textinput" + ref={vm.searchInputRef} + autoFocus={focusRoomSearch} + onKeyDown={vm.onUpdateSearchInput} /> ); @@ -362,21 +235,21 @@ const RoomSummaryCard: React.FC = ({ tagRoom(room, DefaultTagID.Favourite)} + checked={vm.isFavorite} + onSelect={vm.onFavoriteToggleClick} /> inviteToRoom(room)} + disabled={!vm.canInviteToState} + onSelect={vm.onInviteToRoomClick} /> - - - {!isVideoRoom && ( + + + {!vm.isVideoRoom && ( <> = ({ - {pinCount} + {vm.pinCount}
- + )} - + - {!isVideoRoom && ( + {!vm.isVideoRoom && ( <> )} - +
@@ -433,14 +310,14 @@ const RoomSummaryCard: React.FC = ({ Icon={ErrorIcon} kind="critical" label={_t("action|report_room")} - onSelect={onReportRoomClick} + onSelect={vm.onReportRoomClick} />
@@ -448,4 +325,4 @@ const RoomSummaryCard: React.FC = ({ ); }; -export default RoomSummaryCard; +export default RoomSummaryCardView; diff --git a/src/components/views/rooms/Autocomplete.tsx b/src/components/views/rooms/Autocomplete.tsx index 31d32da53c..a7a211e828 100644 --- a/src/components/views/rooms/Autocomplete.tsx +++ b/src/components/views/rooms/Autocomplete.tsx @@ -10,7 +10,6 @@ import React, { createRef, type RefObject } from "react"; import classNames from "classnames"; import { flatMap } from "lodash"; import { type Room } from "matrix-js-sdk/src/matrix"; -import { defer } from "matrix-js-sdk/src/utils"; import Autocompleter, { type ICompletion, @@ -177,7 +176,7 @@ export default class Autocomplete extends React.PureComponent { } } - const deferred = defer(); + const deferred = Promise.withResolvers(); this.setState( { completions, diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 1fa2dab9a8..a744464cba 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { createRef, forwardRef, type JSX, type MouseEvent, type ReactNode } from "react"; +import React, { createRef, type JSX, type Ref, type MouseEvent, type ReactNode } from "react"; import classNames from "classnames"; import { EventStatus, @@ -229,6 +229,8 @@ export interface EventTileProps { // The following properties are used by EventTilePreview to disable tab indexes within the event tile hideTimestamp?: boolean; inhibitInteraction?: boolean; + + ref?: Ref; } interface IState { @@ -1502,13 +1504,13 @@ export class UnwrappedEventTile extends React.Component } // Wrap all event tiles with the tile error boundary so that any throws even during construction are captured -const SafeEventTile = forwardRef((props, ref) => { +const SafeEventTile = (props: EventTileProps): JSX.Element => { return ( - <><> - - + + + ); -}); +}; export default SafeEventTile; function E2ePadlockUnencrypted(props: Omit): JSX.Element { diff --git a/src/components/views/rooms/LinkPreviewGroup.tsx b/src/components/views/rooms/LinkPreviewGroup.tsx index 84943c24e8..69c98cb6c9 100644 --- a/src/components/views/rooms/LinkPreviewGroup.tsx +++ b/src/components/views/rooms/LinkPreviewGroup.tsx @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024, 2025 New Vector Ltd. Copyright 2021 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial @@ -17,6 +17,7 @@ import AccessibleButton from "../elements/AccessibleButton"; import { _t } from "../../../languageHandler"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { useAsyncMemo } from "../../../hooks/useAsyncMemo"; +import { useMediaVisible } from "../../../hooks/useMediaVisible"; const INITIAL_NUM_PREVIEWS = 2; @@ -29,6 +30,7 @@ interface IProps { const LinkPreviewGroup: React.FC = ({ links, mxEvent, onCancelClick }) => { const cli = useContext(MatrixClientContext); const [expanded, toggleExpanded] = useStateToggle(); + const [mediaVisible] = useMediaVisible(mxEvent.getId(), mxEvent.getRoomId()); const ts = mxEvent.getTs(); const previews = useAsyncMemo<[string, IPreviewUrlResponse][]>( @@ -55,7 +57,13 @@ const LinkPreviewGroup: React.FC = ({ links, mxEvent, onCancelClick }) = return (
{showPreviews.map(([link, preview], i) => ( - + {i === 0 ? ( { @@ -69,7 +69,7 @@ export default class LinkPreviewWidget extends React.Component { // FIXME: do we want to factor out all image displaying between this and MImageBody - especially for lightboxing? let image: string | null = p["og:image"] ?? null; - if (!SettingsStore.getValue("showImages")) { + if (!this.props.mediaVisible) { image = null; // Don't render a button to show the image, just hide it outright } const imageMaxWidth = 100; diff --git a/src/components/views/rooms/MemberList/MemberListView.tsx b/src/components/views/rooms/MemberList/MemberListView.tsx index dd04a89209..0b5629685c 100644 --- a/src/components/views/rooms/MemberList/MemberListView.tsx +++ b/src/components/views/rooms/MemberList/MemberListView.tsx @@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details. */ import { Form } from "@vector-im/compound-web"; -import React from "react"; +import React, { type JSX } from "react"; import { List, type ListRowProps } from "react-virtualized/dist/commonjs/List"; import { AutoSizer } from "react-virtualized"; @@ -33,7 +33,7 @@ const MemberListView: React.FC = (props: IProps) => { const totalRows = vm.members.length; - const getRowComponent = (item: MemberWithSeparator): React.JSX.Element => { + const getRowComponent = (item: MemberWithSeparator): JSX.Element => { if (item === SEPARATOR) { return
; } else if (item.member) { @@ -64,7 +64,7 @@ const MemberListView: React.FC = (props: IProps) => { } }; - const rowRenderer = ({ key, index, style }: ListRowProps): React.JSX.Element => { + const rowRenderer = ({ key, index, style }: ListRowProps): JSX.Element => { if (index === totalRows) { // We've rendered all the members, // now we render an empty div to add some space to the end of the list. diff --git a/src/components/views/rooms/MemberList/tiles/common/E2EIconView.tsx b/src/components/views/rooms/MemberList/tiles/common/E2EIconView.tsx index c25d40fc58..359bb74b23 100644 --- a/src/components/views/rooms/MemberList/tiles/common/E2EIconView.tsx +++ b/src/components/views/rooms/MemberList/tiles/common/E2EIconView.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React from "react"; +import React, { type JSX } from "react"; import { Tooltip } from "@vector-im/compound-web"; import VerifiedIcon from "@vector-im/compound-design-tokens/assets/web/icons/verified"; import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error-solid"; @@ -14,7 +14,7 @@ import { _t } from "../../../../../../languageHandler"; import { E2EStatus } from "../../../../../../utils/ShieldUtils"; import { crossSigningUserTitles } from "../../../E2EIcon"; -function getIconFromStatus(status: E2EStatus): React.JSX.Element | undefined { +function getIconFromStatus(status: E2EStatus): JSX.Element | undefined { switch (status) { case E2EStatus.Normal: return undefined; diff --git a/src/components/views/rooms/MemberList/tiles/common/PresenceIconView.tsx b/src/components/views/rooms/MemberList/tiles/common/PresenceIconView.tsx index 855aecf626..ac31084adf 100644 --- a/src/components/views/rooms/MemberList/tiles/common/PresenceIconView.tsx +++ b/src/components/views/rooms/MemberList/tiles/common/PresenceIconView.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React from "react"; +import React, { type JSX } from "react"; import OnlineOrUnavailableIcon from "@vector-im/compound-design-tokens/assets/web/icons/presence-solid-8x8"; import OfflineIcon from "@vector-im/compound-design-tokens/assets/web/icons/presence-outline-8x8"; import DNDIcon from "@vector-im/compound-design-tokens/assets/web/icons/presence-strikethrough-8x8"; @@ -19,7 +19,7 @@ interface Props { export const BUSY_PRESENCE_NAME = new UnstableValue("busy", "org.matrix.msc3026.busy"); -function getIconForPresenceState(state: string): React.JSX.Element { +function getIconForPresenceState(state: string): JSX.Element { switch (state) { case "online": return ; diff --git a/src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx b/src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx index c324e8ccbd..8e44a0a3b9 100644 --- a/src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { forwardRef } from "react"; +import React, { type Ref, type JSX, type ReactNode } from "react"; import classNames from "classnames"; import { formatCount } from "../../../../utils/FormattingUtils"; @@ -26,6 +26,8 @@ interface Props { * for the difference between the two. */ forceDot?: boolean; + children?: ReactNode; + ref?: Ref; } interface ClickableProps extends Props { @@ -45,61 +47,66 @@ interface ClickableProps extends Props { * notifications in the room list, it may have a green badge with the number of unread notifications, * but somewhere else it may just have a green dot as a more compact representation of the same information. */ -export const StatelessNotificationBadge = forwardRef>( - ({ symbol, count, level, knocked, forceDot = false, ...props }, ref) => { - const hideBold = useSettingValue("feature_hidebold"); +export const StatelessNotificationBadge = ({ + symbol, + count, + level, + knocked, + forceDot = false, + ...props +}: XOR): JSX.Element => { + const hideBold = useSettingValue("feature_hidebold"); - // Don't show a badge if we don't need to - if ((level === NotificationLevel.None || (hideBold && level == NotificationLevel.Activity)) && !knocked) { - return <>; - } + // Don't show a badge if we don't need to + if ((level === NotificationLevel.None || (hideBold && level == NotificationLevel.Activity)) && !knocked) { + return <>; + } - const hasUnreadCount = level >= NotificationLevel.Notification && (!!count || !!symbol); + const hasUnreadCount = level >= NotificationLevel.Notification && (!!count || !!symbol); - const isEmptyBadge = symbol === null && count === 0; + const isEmptyBadge = symbol === null && count === 0; - if (symbol === null && count > 0) { - symbol = formatCount(count); - } + if (symbol === null && count > 0) { + symbol = formatCount(count); + } - // We show a dot if either: - // * The props force us to, or - // * It's just an activity-level notification or (in theory) lower and the room isn't knocked - const badgeType = - forceDot || (level <= NotificationLevel.Activity && !knocked) - ? "dot" - : !symbol || symbol.length < 3 - ? "badge_2char" - : "badge_3char"; + // We show a dot if either: + // * The props force us to, or + // * It's just an activity-level notification or (in theory) lower and the room isn't knocked + const badgeType = + forceDot || (level <= NotificationLevel.Activity && !knocked) + ? "dot" + : !symbol || symbol.length < 3 + ? "badge_2char" + : "badge_3char"; - const classes = classNames({ - "mx_NotificationBadge": true, - "mx_NotificationBadge_visible": isEmptyBadge || knocked ? true : hasUnreadCount, - "mx_NotificationBadge_level_notification": level == NotificationLevel.Notification, - "mx_NotificationBadge_level_highlight": level >= NotificationLevel.Highlight, - "mx_NotificationBadge_knocked": knocked, + const classes = classNames({ + "mx_NotificationBadge": true, + "mx_NotificationBadge_visible": isEmptyBadge || knocked ? true : hasUnreadCount, + "mx_NotificationBadge_level_notification": level == NotificationLevel.Notification, + "mx_NotificationBadge_level_highlight": level >= NotificationLevel.Highlight, + "mx_NotificationBadge_knocked": knocked, - // Exactly one of mx_NotificationBadge_dot, mx_NotificationBadge_2char, mx_NotificationBadge_3char - "mx_NotificationBadge_dot": badgeType === "dot", - "mx_NotificationBadge_2char": badgeType === "badge_2char", - "mx_NotificationBadge_3char": badgeType === "badge_3char", - // Badges with text should always use light colors - "cpd-theme-light": badgeType !== "dot", - }); - - if (props.onClick) { - return ( - - {symbol} - {props.children} - - ); - } + // Exactly one of mx_NotificationBadge_dot, mx_NotificationBadge_2char, mx_NotificationBadge_3char + "mx_NotificationBadge_dot": badgeType === "dot", + "mx_NotificationBadge_2char": badgeType === "badge_2char", + "mx_NotificationBadge_3char": badgeType === "badge_3char", + // Badges with text should always use light colors + "cpd-theme-light": badgeType !== "dot", + }); + if (props.onClick) { return ( -
+ {symbol} -
+ {props.children} +
); - }, -); + } + + return ( +
+ {symbol} +
+ ); +}; diff --git a/src/components/views/rooms/NotificationDecoration.tsx b/src/components/views/rooms/NotificationDecoration.tsx index 0bf506d324..a93f1428fd 100644 --- a/src/components/views/rooms/NotificationDecoration.tsx +++ b/src/components/views/rooms/NotificationDecoration.tsx @@ -64,7 +64,7 @@ export function NotificationDecoration({ diff --git a/src/components/views/rooms/RoomHeader/CallGuestLinkButton.tsx b/src/components/views/rooms/RoomHeader/CallGuestLinkButton.tsx index 7675f66070..16ef5c2234 100644 --- a/src/components/views/rooms/RoomHeader/CallGuestLinkButton.tsx +++ b/src/components/views/rooms/RoomHeader/CallGuestLinkButton.tsx @@ -14,10 +14,11 @@ import { EventType, JoinRule, type Room } from "matrix-js-sdk/src/matrix"; import Modal from "../../../../Modal"; import { ShareDialog } from "../../dialogs/ShareDialog"; import { _t } from "../../../../languageHandler"; -import SettingsStore from "../../../../settings/SettingsStore"; import { calculateRoomVia } from "../../../../utils/permalinks/Permalinks"; import BaseDialog from "../../dialogs/BaseDialog"; import { useGuestAccessInformation } from "../../../../hooks/room/useGuestAccessInformation"; +import JoinRuleSettings from "../../settings/JoinRuleSettings"; +import SettingsStore from "../../../../settings/SettingsStore"; /** * Display a button to open a dialog to share a link to the call using a element call guest spa url (`element_call:guest_spa_url` in the EW config). @@ -114,33 +115,32 @@ export const JoinRuleDialog: React.FC<{ "", ); // Show the dialog for a bit to give the user feedback - setTimeout(() => onFinished(), 500); + setTimeout(() => onFinished(), 1000); }, [isUpdating, onFinished, room.client, room.roomId], ); return ( -

{_t("update_room_access_modal|description")}

-
- {askToJoinEnabled && canInvite && ( - - )} - -
+

{_t("update_room_access_modal|description", {}, { b: (sub) => {sub} })}

+

+ {_t("update_room_access_modal|revert_access_description", {}, { b: (sub) => {sub} })} +

+ { + await changeJoinRule(newRule).catch(() => { + return false; + }); + return true; + }} + closeSettingsFn={() => {}} + onError={(error: unknown) => logger.error("Could not generate change access level:", error)} + />

{_t("update_room_access_modal|dont_change_description")}

); diff --git a/src/components/views/rooms/RoomListPanel/RoomList.tsx b/src/components/views/rooms/RoomListPanel/RoomList.tsx index 628723246f..f5f610f5ae 100644 --- a/src/components/views/rooms/RoomListPanel/RoomList.tsx +++ b/src/components/views/rooms/RoomListPanel/RoomList.tsx @@ -11,6 +11,10 @@ import { AutoSizer, List, type ListRowProps } from "react-virtualized"; import { type RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel"; import { _t } from "../../../../languageHandler"; import { RoomListItemView } from "./RoomListItemView"; +import { RovingTabIndexProvider } from "../../../../accessibility/RovingTabIndex"; +import { getKeyBindingsManager } from "../../../../KeyBindingsManager"; +import { KeyBindingAction } from "../../../../accessibility/KeyboardShortcuts"; +import { Landmark, LandmarkNavigation } from "../../../../accessibility/LandmarkNavigation"; interface RoomListProps { /** @@ -32,21 +36,45 @@ export function RoomList({ vm: { rooms, activeIndex } }: RoomListProps): JSX.Ele // The first div is needed to make the virtualized list take all the remaining space and scroll correctly return ( -
- - {({ height, width }) => ( - - )} - -
+ + {({ onKeyDownHandler }) => ( +
{ + const navAction = getKeyBindingsManager().getNavigationAction(ev); + if ( + navAction === KeyBindingAction.NextLandmark || + navAction === KeyBindingAction.PreviousLandmark + ) { + LandmarkNavigation.findAndFocusNextLandmark( + Landmark.ROOM_LIST, + navAction === KeyBindingAction.PreviousLandmark, + ); + ev.stopPropagation(); + ev.preventDefault(); + return; + } + onKeyDownHandler(ev); + }} + > + + {({ height, width }) => ( + + )} + +
+ )} +
); } diff --git a/src/components/views/rooms/RoomListPanel/RoomListHeaderView.tsx b/src/components/views/rooms/RoomListPanel/RoomListHeaderView.tsx index ba4fdc5ca4..752f065fd4 100644 --- a/src/components/views/rooms/RoomListPanel/RoomListHeaderView.tsx +++ b/src/components/views/rooms/RoomListPanel/RoomListHeaderView.tsx @@ -21,6 +21,7 @@ import { type RoomListHeaderViewState, useRoomListHeaderViewModel, } from "../../../viewmodels/roomlist/RoomListHeaderViewModel"; +import { RoomListOptionsMenu } from "./RoomListOptionsMenu"; /** * The header view for the room list @@ -42,14 +43,17 @@ export function RoomListHeaderView(): JSX.Element {

{vm.title}

{vm.displaySpaceMenu && } - {/* If we don't display the compose menu, it means that the user can only send DM */} - {vm.displayComposeMenu ? ( - - ) : ( - vm.createChatRoom(e.nativeEvent)}> - - - )} + + + {/* If we don't display the compose menu, it means that the user can only send DM */} + {vm.displayComposeMenu ? ( + + ) : ( + vm.createChatRoom(e.nativeEvent)}> + + + )} + ); } diff --git a/src/components/views/rooms/RoomListPanel/RoomListItemMenuView.tsx b/src/components/views/rooms/RoomListPanel/RoomListItemMenuView.tsx index 5f97e44378..07b7be4b43 100644 --- a/src/components/views/rooms/RoomListPanel/RoomListItemMenuView.tsx +++ b/src/components/views/rooms/RoomListPanel/RoomListItemMenuView.tsx @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -import React, { type ComponentProps, forwardRef, type JSX, useState } from "react"; +import React, { type ComponentProps, type JSX, type Ref, useState } from "react"; import { IconButton, Menu, MenuItem, Separator, ToggleMenuItem, Tooltip } from "@vector-im/compound-web"; import MarkAsReadIcon from "@vector-im/compound-design-tokens/assets/web/icons/mark-as-read"; import MarkAsUnreadIcon from "@vector-im/compound-design-tokens/assets/web/icons/mark-as-unread"; @@ -47,7 +47,7 @@ export function RoomListItemMenuView({ room, setMenuOpen }: RoomListItemMenuView const vm = useRoomListItemMenuViewModel(room); return ( - + {vm.showMoreOptionsMenu && } {vm.showNotificationMenu && } @@ -147,22 +147,22 @@ function MoreOptionsMenu({ vm, setMenuOpen }: MoreOptionsMenuProps): JSX.Element ); } -interface MoreOptionsButtonProps extends ComponentProps {} +interface MoreOptionsButtonProps extends ComponentProps { + ref?: Ref; +} /** * A button to trigger the more options menu. */ -export const MoreOptionsButton = forwardRef( - function MoreOptionsButton(props, ref) { - return ( - - - - - - ); - }, -); +export const MoreOptionsButton = function MoreOptionsButton(props: MoreOptionsButtonProps): JSX.Element { + return ( + + + + + + ); +}; interface NotificationMenuProps { /** @@ -238,15 +238,17 @@ interface NotificationButtonProps extends ComponentProps { * Whether the room is muted. */ isRoomMuted: boolean; + ref?: Ref; } /** * A button to trigger the notification menu. */ -export const NotificationButton = forwardRef(function MoreOptionsButton( - { isRoomMuted, ...props }, +export const NotificationButton = function MoreOptionsButton({ + isRoomMuted, ref, -) { + ...props +}: NotificationButtonProps): JSX.Element { return ( @@ -254,4 +256,4 @@ export const NotificationButton = forwardRef ); -}); +}; diff --git a/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx b/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx index 36839d75b0..ac4d72ec57 100644 --- a/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx +++ b/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -import React, { type JSX, memo, useState } from "react"; +import React, { type JSX, memo, useCallback, useRef, useState } from "react"; import { type Room } from "matrix-js-sdk/src/matrix"; import classNames from "classnames"; @@ -14,8 +14,9 @@ import { Flex } from "../../../utils/Flex"; import { RoomListItemMenuView } from "./RoomListItemMenuView"; import { NotificationDecoration } from "../NotificationDecoration"; import { RoomAvatarView } from "../../avatars/RoomAvatarView"; +import { useRovingTabIndex } from "../../../../accessibility/RovingTabIndex"; -interface RoomListItemViewPropsProps extends React.HTMLAttributes { +interface RoomListItemViewProps extends React.HTMLAttributes { /** * The room to display */ @@ -33,23 +34,25 @@ export const RoomListItemView = memo(function RoomListItemView({ room, isSelected, ...props -}: RoomListItemViewPropsProps): JSX.Element { +}: RoomListItemViewProps): JSX.Element { + const buttonRef = useRef(null); + const [onFocus, isActive, ref] = useRovingTabIndex(buttonRef); + const vm = useRoomListItemViewModel(room); - const [isHover, setIsHover] = useState(false); + const [isHover, setIsHoverWithDelay] = useIsHover(); const [isMenuOpen, setIsMenuOpen] = useState(false); // The compound menu in RoomListItemMenuView needs to be rendered when the hover menu is shown // Using display: none; and then display:flex when hovered in CSS causes the menu to be misaligned - const showHoverDecoration = (isMenuOpen || isHover) && vm.showHoverMenu; - - const isNotificationDecorationVisible = !showHoverDecoration && vm.showNotificationDecoration; + const showHoverDecoration = isMenuOpen || isHover; + const showHoverMenu = showHoverDecoration && vm.showHoverMenu; return ( ); }); + +/** + * Custom hook to manage the hover state of the room list item + * If the timeout is set, it will set the hover state after the timeout + * If the timeout is not set, it will set the hover state immediately + * When the set method is called, it will clear any existing timeout + * + * @returns {boolean} isHover - The hover state + */ +function useIsHover(): [boolean, (value: boolean, timeout?: number) => void] { + const [isHover, setIsHover] = useState(false); + // Store the timeout ID + const timeoutRef = useRef(undefined); + + const setIsHoverWithDelay = useCallback((value: boolean, timeout?: number): void => { + // Clear the timeout if it exists + clearTimeout(timeoutRef.current); + + // No delay, set the value immediately + if (timeout === undefined) { + setIsHover(value); + return; + } + + // Set a timeout to set the value after the delay + timeoutRef.current = setTimeout(() => setIsHover(value), timeout); + }, []); + + return [isHover, setIsHoverWithDelay]; +} diff --git a/src/components/views/rooms/RoomListPanel/RoomListOptionsMenu.tsx b/src/components/views/rooms/RoomListPanel/RoomListOptionsMenu.tsx new file mode 100644 index 0000000000..a78c1dd994 --- /dev/null +++ b/src/components/views/rooms/RoomListPanel/RoomListOptionsMenu.tsx @@ -0,0 +1,74 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { IconButton, Menu, MenuTitle, CheckboxMenuItem, Tooltip, RadioMenuItem } from "@vector-im/compound-web"; +import React, { type Ref, type JSX, useState, useCallback } from "react"; +import FilterIcon from "@vector-im/compound-design-tokens/assets/web/icons/filter"; + +import { _t } from "../../../../languageHandler"; +import { SortOption } from "../../../viewmodels/roomlist/useSorter"; +import { type RoomListHeaderViewState } from "../../../viewmodels/roomlist/RoomListHeaderViewModel"; + +interface MenuTriggerProps extends React.ComponentProps { + ref?: Ref; +} + +const MenuTrigger = ({ ref, ...props }: MenuTriggerProps): JSX.Element => ( + + + + + +); + +interface Props { + /** + * The view model for the room list view + */ + vm: RoomListHeaderViewState; +} + +export function RoomListOptionsMenu({ vm }: Props): JSX.Element { + const [open, setOpen] = useState(false); + + const onActivitySelected = useCallback(() => { + vm.sort(SortOption.Activity); + }, [vm]); + + const onAtoZSelected = useCallback(() => { + vm.sort(SortOption.AToZ); + }, [vm]); + + return ( + } + > + + + + + + + ); +} diff --git a/src/components/views/rooms/RoomListPanel/RoomListSearch.tsx b/src/components/views/rooms/RoomListPanel/RoomListSearch.tsx index 6809841075..26f0ff8bae 100644 --- a/src/components/views/rooms/RoomListPanel/RoomListSearch.tsx +++ b/src/components/views/rooms/RoomListPanel/RoomListSearch.tsx @@ -55,7 +55,7 @@ export function RoomListSearch({ activeSpace }: RoomListSearchProps): JSX.Elemen onClick={() => defaultDispatcher.fire(Action.OpenSpotlight)} > - {_t("action|search")} + {_t("action|search")} {IS_MAC ? "⌘ K" : _t(ALTERNATE_KEY_NAME[Key.CONTROL]) + " K"} diff --git a/src/components/views/rooms/RoomListPanel/RoomListView.tsx b/src/components/views/rooms/RoomListPanel/RoomListView.tsx index f4800f7009..f6d99625aa 100644 --- a/src/components/views/rooms/RoomListPanel/RoomListView.tsx +++ b/src/components/views/rooms/RoomListPanel/RoomListView.tsx @@ -18,11 +18,18 @@ import { RoomListPrimaryFilters } from "./RoomListPrimaryFilters"; export function RoomListView(): JSX.Element { const vm = useRoomListViewModel(); const isRoomListEmpty = vm.rooms.length === 0; - + let listBody; + if (vm.isLoadingRooms) { + listBody =
; + } else if (isRoomListEmpty) { + listBody = ; + } else { + listBody = ; + } return ( <> - {isRoomListEmpty ? : } + {listBody} ); } diff --git a/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx index 2ea87fc68e..c84f130f34 100644 --- a/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type JSX, type ForwardedRef, forwardRef, type RefObject, useMemo } from "react"; +import React, { type JSX, type RefObject, useMemo, type ReactNode } from "react"; import classNames from "classnames"; import type EditorStateTransfer from "../../../../utils/EditorStateTransfer"; @@ -21,15 +21,13 @@ import { type ComposerFunctions } from "./types"; interface ContentProps { disabled?: boolean; composerFunctions: ComposerFunctions; + ref?: RefObject; } -const Content = forwardRef(function Content( - { disabled = false, composerFunctions }: ContentProps, - forwardRef: ForwardedRef, -) { - useWysiwygEditActionHandler(disabled, forwardRef as RefObject, composerFunctions); +const Content = function Content({ disabled = false, composerFunctions, ref }: ContentProps): ReactNode { + useWysiwygEditActionHandler(disabled, ref, composerFunctions); return null; -}); +}; interface EditWysiwygComposerProps { disabled?: boolean; diff --git a/src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx index fb399dda28..9b21eef545 100644 --- a/src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type JSX, type ForwardedRef, forwardRef, type RefObject, useMemo } from "react"; +import React, { type JSX, type RefObject, useMemo, type ReactNode } from "react"; import { type IEventRelation } from "matrix-js-sdk/src/matrix"; import { useWysiwygSendActionHandler } from "./hooks/useWysiwygSendActionHandler"; @@ -22,15 +22,13 @@ import { ComposerContext, getDefaultContextValue } from "./ComposerContext"; interface ContentProps { disabled?: boolean; composerFunctions: ComposerFunctions; + ref?: RefObject; } -const Content = forwardRef(function Content( - { disabled = false, composerFunctions }: ContentProps, - forwardRef: ForwardedRef, -) { - useWysiwygSendActionHandler(disabled, forwardRef as RefObject, composerFunctions); +const Content = function Content({ disabled = false, composerFunctions, ref }: ContentProps): ReactNode { + useWysiwygSendActionHandler(disabled, ref, composerFunctions); return null; -}); +}; export interface SendWysiwygComposerProps { initialContent?: string; diff --git a/src/components/views/rooms/wysiwyg_composer/components/Editor.tsx b/src/components/views/rooms/wysiwyg_composer/components/Editor.tsx index d46318af04..d418a0df19 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/Editor.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/Editor.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import classNames from "classnames"; -import React, { type CSSProperties, forwardRef, memo, type RefObject, type ReactNode } from "react"; +import React, { type CSSProperties, memo, type RefObject, type ReactNode } from "react"; import { useIsExpanded } from "../hooks/useIsExpanded"; import { useSelection } from "../hooks/useSelection"; @@ -19,44 +19,36 @@ interface EditorProps { placeholder?: string; leftComponent?: ReactNode; rightComponent?: ReactNode; + ref?: RefObject; } -export const Editor = memo( - forwardRef(function Editor( - { disabled, placeholder, leftComponent, rightComponent }: EditorProps, - ref, - ) { - const isExpanded = useIsExpanded(ref as RefObject, HEIGHT_BREAKING_POINT); - const { onFocus, onBlur, onInput } = useSelection(); +export const Editor = memo(function Editor({ disabled, placeholder, leftComponent, rightComponent, ref }: EditorProps) { + const isExpanded = useIsExpanded(ref, HEIGHT_BREAKING_POINT); + const { onFocus, onBlur, onInput } = useSelection(); - return ( -
- {leftComponent} -
-
-
- {rightComponent} + return ( +
+ {leftComponent} +
+
- ); - }), -); + {rightComponent} +
+ ); +}); diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx index 56ac579035..9446a32a7b 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type JSX, type ForwardedRef, forwardRef, type FunctionComponent } from "react"; +import React, { type JSX, type Ref, type FunctionComponent } from "react"; import { type FormattingFunctions, type MappedSuggestion } from "@vector-im/matrix-wysiwyg"; import { logger } from "matrix-js-sdk/src/logger"; @@ -39,6 +39,8 @@ interface WysiwygAutocompleteProps { * Handler purely for the at-room mentions special case */ handleAtRoomMention: FormattingFunctions["mentionAtRoom"]; + + ref?: Ref; } /** @@ -48,69 +50,70 @@ interface WysiwygAutocompleteProps { * * @param props.ref - the ref will be attached to the rendered `` component */ -const WysiwygAutocomplete = forwardRef( - ( - { suggestion, handleMention, handleCommand, handleAtRoomMention }: WysiwygAutocompleteProps, - ref: ForwardedRef, - ): JSX.Element | null => { - const { room } = useScopedRoomContext("room"); - const client = useMatrixClientContext(); +const WysiwygAutocomplete = ({ + suggestion, + handleMention, + handleCommand, + handleAtRoomMention, + ref, +}: WysiwygAutocompleteProps): JSX.Element | null => { + const { room } = useScopedRoomContext("room"); + const client = useMatrixClientContext(); - function handleConfirm(completion: ICompletion): void { - if (client === undefined || room === undefined) { - return; - } - - switch (completion.type) { - case "command": { - // TODO determine if utils in SlashCommands.tsx are required. - // Trim the completion as some include trailing spaces, but we always insert a - // trailing space in the rust model anyway - handleCommand(completion.completion.trim()); - return; - } - case "at-room": { - handleAtRoomMention(getMentionAttributes(completion, client, room)); - return; - } - case "room": - case "user": { - if (typeof completion.href === "string") { - handleMention( - completion.href, - getMentionDisplayText(completion, client), - getMentionAttributes(completion, client, room), - ); - } - return; - } - // TODO - handle "community" type - default: - return; - } + function handleConfirm(completion: ICompletion): void { + if (client === undefined || room === undefined) { + return; } - if (!room) return null; + switch (completion.type) { + case "command": { + // TODO determine if utils in SlashCommands.tsx are required. + // Trim the completion as some include trailing spaces, but we always insert a + // trailing space in the rust model anyway + handleCommand(completion.completion.trim()); + return; + } + case "at-room": { + handleAtRoomMention(getMentionAttributes(completion, client, room)); + return; + } + case "room": + case "user": { + if (typeof completion.href === "string") { + handleMention( + completion.href, + getMentionDisplayText(completion, client), + getMentionAttributes(completion, client, room), + ); + } + return; + } + // TODO - handle "community" type + default: + return; + } + } - const autoCompleteQuery = buildQuery(suggestion); - // debug for https://github.com/vector-im/element-web/issues/26037 - logger.log(`## 26037 ## Rendering Autocomplete for WysiwygAutocomplete with query: "${autoCompleteQuery}"`); + if (!room) return null; - // TODO - determine if we show all of the /command suggestions, there are some options in the - // list which don't seem to make sense in this context, specifically /html and /plain - return ( -
- -
- ); - }, -); + const autoCompleteQuery = buildQuery(suggestion); + // debug for https://github.com/vector-im/element-web/issues/26037 + logger.log(`## 26037 ## Rendering Autocomplete for WysiwygAutocomplete with query: "${autoCompleteQuery}"`); + + // TODO - determine if we show all of the /command suggestions, there are some options in the + // list which don't seem to make sense in this context, specifically /html and /plain + return ( +
+ +
+ ); +}; (WysiwygAutocomplete as FunctionComponent).displayName = "WysiwygAutocomplete"; diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useIsExpanded.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useIsExpanded.ts index 0041cac557..d5f9850277 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useIsExpanded.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useIsExpanded.ts @@ -8,10 +8,10 @@ Please see LICENSE files in the repository root for full details. import { type RefObject, useEffect, useState } from "react"; -export function useIsExpanded(ref: RefObject, breakingPoint: number): boolean { +export function useIsExpanded(ref: RefObject | undefined, breakingPoint: number): boolean { const [isExpanded, setIsExpanded] = useState(false); useEffect(() => { - if (ref.current) { + if (ref?.current) { const editor = ref.current; const resizeObserver = new ResizeObserver((entries) => { requestAnimationFrame(() => { diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts index 68d38eebcd..c105996a05 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygEditActionHandler.ts @@ -22,7 +22,7 @@ import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext. export function useWysiwygEditActionHandler( disabled: boolean, - composerElement: RefObject, + composerElement: RefObject | undefined, composerFunctions: ComposerFunctions, ): void { const roomContext = useScopedRoomContext("timelineRenderingType"); @@ -33,7 +33,7 @@ export function useWysiwygEditActionHandler( (payload: ActionPayload) => { // don't let the user into the composer if it is disabled - all of these branches lead // to the cursor being in the composer - if (disabled || !composerElement.current) return; + if (disabled || !composerElement?.current) return; const context = payload.context ?? TimelineRenderingType.Room; diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts index 89379db1ad..fcab8f4547 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useWysiwygSendActionHandler.ts @@ -22,7 +22,7 @@ import { useScopedRoomContext } from "../../../../../contexts/ScopedRoomContext. export function useWysiwygSendActionHandler( disabled: boolean, - composerElement: RefObject, + composerElement: RefObject | undefined, composerFunctions: ComposerFunctions, ): void { const roomContext = useScopedRoomContext("timelineRenderingType"); diff --git a/src/components/views/settings/EventIndexPanel.tsx b/src/components/views/settings/EventIndexPanel.tsx index 2cfcd70aed..fe19aa0580 100644 --- a/src/components/views/settings/EventIndexPanel.tsx +++ b/src/components/views/settings/EventIndexPanel.tsx @@ -119,15 +119,14 @@ export default class EventIndexPanel extends React.Component { - const { close } = Modal.createDialog(SeshatResetDialog, { - onFinished: async (success): Promise => { - if (success) { - await SettingsStore.setValue("enableEventIndexing", null, SettingLevel.DEVICE, false); - await EventIndexPeg.deleteEventIndex(); - await this.onEnable(); - close(); - } - }, + const { finished, close } = Modal.createDialog(SeshatResetDialog); + finished.then(async ([success]) => { + if (success) { + await SettingsStore.setValue("enableEventIndexing", null, SettingLevel.DEVICE, false); + await EventIndexPeg.deleteEventIndex(); + await this.onEnable(); + close(); + } }); }; diff --git a/src/components/views/settings/JoinRuleSettings.tsx b/src/components/views/settings/JoinRuleSettings.tsx index ea2123729d..43f8ab9ff6 100644 --- a/src/components/views/settings/JoinRuleSettings.tsx +++ b/src/components/views/settings/JoinRuleSettings.tsx @@ -36,6 +36,9 @@ export interface JoinRuleSettingsProps { onError(error: unknown): void; beforeChange?(joinRule: JoinRule): Promise; // if returns false then aborts the change aliasWarning?: ReactNode; + disabledOptions?: Set; + hiddenOptions?: Set; + recommendedOption?: JoinRule; } const JoinRuleSettings: React.FC = ({ @@ -45,6 +48,9 @@ const JoinRuleSettings: React.FC = ({ onError, beforeChange, closeSettingsFn, + disabledOptions, + hiddenOptions, + recommendedOption, }) => { const cli = room.client; @@ -147,7 +153,7 @@ const JoinRuleSettings: React.FC = ({ } }); - closeSettingsFn(); + closeSettingsFn?.(); // switch to the new room in the background dis.dispatch({ @@ -170,18 +176,26 @@ const JoinRuleSettings: React.FC = ({ {_t("room_settings|security|join_rule_upgrade_required")} ); + const withRecommendLabel = (label: string, rule: JoinRule): React.ReactNode => + rule === recommendedOption ? ( + <> + {label} ({_t("common|recommended")}) + + ) : ( + label + ); const definitions: IDefinition[] = [ { value: JoinRule.Invite, - label: _t("room_settings|security|join_rule_invite"), + label: withRecommendLabel(_t("room_settings|security|join_rule_invite"), JoinRule.Invite), description: _t("room_settings|security|join_rule_invite_description"), checked: joinRule === JoinRule.Invite || (joinRule === JoinRule.Restricted && !restrictedAllowRoomIds?.length), }, { value: JoinRule.Public, - label: _t("common|public"), + label: withRecommendLabel(_t("common|public"), JoinRule.Public), description: ( <> {_t("room_settings|security|join_rule_public_description")} @@ -292,7 +306,7 @@ const JoinRuleSettings: React.FC = ({ value: JoinRule.Restricted, label: ( <> - {_t("room_settings|security|join_rule_restricted")} + {withRecommendLabel(_t("room_settings|security|join_rule_restricted"), JoinRule.Restricted)} {preferredRestrictionVersion && upgradeRequiredPill} ), @@ -303,11 +317,11 @@ const JoinRuleSettings: React.FC = ({ } if (askToJoinEnabled && (roomSupportsKnock || preferredKnockVersion)) { - definitions.push({ + definitions.splice(Math.max(0, definitions.length - 1), 0, { value: JoinRule.Knock, label: ( <> - {_t("room_settings|security|join_rule_knock")} + {withRecommendLabel(_t("room_settings|security|join_rule_knock"), JoinRule.Knock)} {preferredKnockVersion && upgradeRequiredPill} ), @@ -397,7 +411,9 @@ const JoinRuleSettings: React.FC = ({ name="joinRule" value={joinRule} onChange={onChange} - definitions={definitions} + definitions={definitions + .map((d) => (disabledOptions?.has(d.value) ? { ...d, disabled: true } : d)) + .filter((d) => !hiddenOptions?.has(d.value))} disabled={disabled} className="mx_JoinRuleSettings_radioButton" /> diff --git a/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx b/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx index 507fc31425..dfa188cf69 100644 --- a/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx +++ b/src/components/views/settings/devices/DeviceExpandDetailsButton.tsx @@ -15,7 +15,7 @@ import AccessibleButton, { type ButtonProps } from "../../elements/AccessibleBut type Props = Omit< ButtonProps, - "aria-label" | "title" | "kind" | "className" | "element" + "aria-label" | "title" | "kind" | "className" | "element" | "ref" > & { isExpanded: boolean; }; diff --git a/src/components/views/settings/devices/FilteredDeviceList.tsx b/src/components/views/settings/devices/FilteredDeviceList.tsx index c9a266e4b9..fd30091106 100644 --- a/src/components/views/settings/devices/FilteredDeviceList.tsx +++ b/src/components/views/settings/devices/FilteredDeviceList.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type ForwardedRef, forwardRef } from "react"; +import React, { type JSX, type Ref } from "react"; import { type IPusher, PUSHER_DEVICE_ID, type LocalNotificationSettings } from "matrix-js-sdk/src/matrix"; import { _t } from "../../../../languageHandler"; @@ -47,6 +47,7 @@ interface Props { * Changes sign out button to be a manage button */ delegatedAuthAccountUrl?: string; + ref?: Ref; } const isDeviceSelected = ( @@ -237,152 +238,148 @@ const DeviceListItem: React.FC<{ * Filtered list of devices * Sorted by latest activity descending */ -export const FilteredDeviceList = forwardRef( - ( - { - devices, - pushers, - localNotificationSettings, - filter, - expandedDeviceIds, - signingOutDeviceIds, - selectedDeviceIds, - onFilterChange, - onDeviceExpandToggle, - saveDeviceName, - onSignOutDevices, - onRequestDeviceVerification, - setPushNotifications, - setSelectedDeviceIds, - supportsMSC3881, - delegatedAuthAccountUrl, - }: Props, - ref: ForwardedRef, - ) => { - const sortedDevices = getFilteredSortedDevices(devices, filter); +export const FilteredDeviceList = ({ + devices, + pushers, + localNotificationSettings, + filter, + expandedDeviceIds, + signingOutDeviceIds, + selectedDeviceIds, + onFilterChange, + onDeviceExpandToggle, + saveDeviceName, + onSignOutDevices, + onRequestDeviceVerification, + setPushNotifications, + setSelectedDeviceIds, + supportsMSC3881, + delegatedAuthAccountUrl, + ref, +}: Props): JSX.Element => { + const sortedDevices = getFilteredSortedDevices(devices, filter); - function getPusherForDevice(device: ExtendedDevice): IPusher | undefined { - return pushers.find((pusher) => pusher[PUSHER_DEVICE_ID.name] === device.device_id); + function getPusherForDevice(device: ExtendedDevice): IPusher | undefined { + return pushers.find((pusher) => pusher[PUSHER_DEVICE_ID.name] === device.device_id); + } + + const toggleSelection = (deviceId: ExtendedDevice["device_id"]): void => { + if (isDeviceSelected(deviceId, selectedDeviceIds)) { + // remove from selection + setSelectedDeviceIds(selectedDeviceIds.filter((id) => id !== deviceId)); + } else { + setSelectedDeviceIds([...selectedDeviceIds, deviceId]); } + }; - const toggleSelection = (deviceId: ExtendedDevice["device_id"]): void => { - if (isDeviceSelected(deviceId, selectedDeviceIds)) { - // remove from selection - setSelectedDeviceIds(selectedDeviceIds.filter((id) => id !== deviceId)); - } else { - setSelectedDeviceIds([...selectedDeviceIds, deviceId]); - } - }; + const options: FilterDropdownOption[] = [ + { id: ALL_FILTER_ID, label: _t("settings|sessions|filter_all") }, + { + id: DeviceSecurityVariation.Verified, + label: _t("common|verified"), + description: _t("settings|sessions|filter_verified_description"), + }, + { + id: DeviceSecurityVariation.Unverified, + label: _t("common|unverified"), + description: _t("settings|sessions|filter_unverified_description"), + }, + { + id: DeviceSecurityVariation.Inactive, + label: _t("settings|sessions|filter_inactive"), + description: _t("settings|sessions|filter_inactive_description", { + inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS, + }), + }, + ]; - const options: FilterDropdownOption[] = [ - { id: ALL_FILTER_ID, label: _t("settings|sessions|filter_all") }, - { - id: DeviceSecurityVariation.Verified, - label: _t("common|verified"), - description: _t("settings|sessions|filter_verified_description"), - }, - { - id: DeviceSecurityVariation.Unverified, - label: _t("common|unverified"), - description: _t("settings|sessions|filter_unverified_description"), - }, - { - id: DeviceSecurityVariation.Inactive, - label: _t("settings|sessions|filter_inactive"), - description: _t("settings|sessions|filter_inactive_description", { - inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS, - }), - }, - ]; + const onFilterOptionChange = (filterId: DeviceFilterKey): void => { + onFilterChange(filterId === ALL_FILTER_ID ? undefined : (filterId as FilterVariation)); + }; - const onFilterOptionChange = (filterId: DeviceFilterKey): void => { - onFilterChange(filterId === ALL_FILTER_ID ? undefined : (filterId as FilterVariation)); - }; + const isAllSelected = selectedDeviceIds.length >= sortedDevices.length; + const toggleSelectAll = (): void => { + if (isAllSelected) { + setSelectedDeviceIds([]); + } else { + setSelectedDeviceIds(sortedDevices.map((device) => device.device_id)); + } + }; - const isAllSelected = selectedDeviceIds.length >= sortedDevices.length; - const toggleSelectAll = (): void => { - if (isAllSelected) { - setSelectedDeviceIds([]); - } else { - setSelectedDeviceIds(sortedDevices.map((device) => device.device_id)); - } - }; + const isSigningOut = !!signingOutDeviceIds.length; - const isSigningOut = !!signingOutDeviceIds.length; - - return ( -
- - {selectedDeviceIds.length ? ( - <> - onSignOutDevices(selectedDeviceIds)} - className="mx_FilteredDeviceList_headerButton" - > - {isSigningOut && } - {_t("action|sign_out")} - - setSelectedDeviceIds([])} - className="mx_FilteredDeviceList_headerButton" - > - {_t("action|cancel")} - - - ) : ( - - id="device-list-filter" - label={_t("settings|sessions|filter_label")} - value={filter || ALL_FILTER_ID} - onOptionChange={onFilterOptionChange} - options={options} - selectedLabel={_t("action|show")} - /> - )} - - {!!sortedDevices.length ? ( - + return ( +
+ + {selectedDeviceIds.length ? ( + <> + onSignOutDevices(selectedDeviceIds)} + className="mx_FilteredDeviceList_headerButton" + > + {isSigningOut && } + {_t("action|sign_out")} + + setSelectedDeviceIds([])} + className="mx_FilteredDeviceList_headerButton" + > + {_t("action|cancel")} + + ) : ( - onFilterChange(undefined)} /> + + id="device-list-filter" + label={_t("settings|sessions|filter_label")} + value={filter || ALL_FILTER_ID} + onOptionChange={onFilterOptionChange} + options={options} + selectedLabel={_t("action|show")} + /> )} -
    - {sortedDevices.map((device) => ( - onDeviceExpandToggle(device.device_id)} - onSignOutDevice={() => onSignOutDevices([device.device_id])} - saveDeviceName={(deviceName: string) => saveDeviceName(device.device_id, deviceName)} - onRequestDeviceVerification={ - onRequestDeviceVerification - ? () => onRequestDeviceVerification(device.device_id) - : undefined - } - setPushNotifications={setPushNotifications} - toggleSelected={() => toggleSelection(device.device_id)} - supportsMSC3881={supportsMSC3881} - delegatedAuthAccountUrl={delegatedAuthAccountUrl} - /> - ))} -
-
- ); - }, -); + + {!!sortedDevices.length ? ( + + ) : ( + onFilterChange(undefined)} /> + )} +
    + {sortedDevices.map((device) => ( + onDeviceExpandToggle(device.device_id)} + onSignOutDevice={() => onSignOutDevices([device.device_id])} + saveDeviceName={(deviceName: string) => saveDeviceName(device.device_id, deviceName)} + onRequestDeviceVerification={ + onRequestDeviceVerification + ? () => onRequestDeviceVerification(device.device_id) + : undefined + } + setPushNotifications={setPushNotifications} + toggleSelected={() => toggleSelection(device.device_id)} + supportsMSC3881={supportsMSC3881} + delegatedAuthAccountUrl={delegatedAuthAccountUrl} + /> + ))} +
+
+ ); +}; diff --git a/src/components/views/settings/devices/deleteDevices.tsx b/src/components/views/settings/devices/deleteDevices.tsx index e2fc6c1abd..894e23e0f7 100644 --- a/src/components/views/settings/devices/deleteDevices.tsx +++ b/src/components/views/settings/devices/deleteDevices.tsx @@ -11,7 +11,6 @@ import { type AuthDict, type IAuthData } from "matrix-js-sdk/src/interactive-aut import { _t } from "../../../../languageHandler"; import Modal from "../../../../Modal"; -import { type InteractiveAuthCallback } from "../../../structures/InteractiveAuth"; import { SSOAuthEntry } from "../../auth/InteractiveAuthEntryComponents"; import InteractiveAuthDialog from "../../dialogs/InteractiveAuthDialog"; @@ -24,7 +23,7 @@ const makeDeleteRequest = export const deleteDevicesWithInteractiveAuth = async ( matrixClient: MatrixClient, deviceIds: string[], - onFinished: InteractiveAuthCallback, + onFinished: (success?: boolean) => Promise, ): Promise => { if (!deviceIds.length) { return; @@ -32,7 +31,7 @@ export const deleteDevicesWithInteractiveAuth = async ( try { await makeDeleteRequest(matrixClient, deviceIds)(null); // no interactive auth needed - await onFinished(true, undefined); + await onFinished(true); } catch (error) { if (!(error instanceof MatrixError) || error.httpStatus !== 401 || !error.data?.flows) { // doesn't look like an interactive-auth failure @@ -62,16 +61,16 @@ export const deleteDevicesWithInteractiveAuth = async ( continueKind: "danger", }, }; - Modal.createDialog(InteractiveAuthDialog, { + const { finished } = Modal.createDialog(InteractiveAuthDialog, { title: _t("common|authentication"), matrixClient: matrixClient, authData: error.data as IAuthData, - onFinished, makeRequest: makeDeleteRequest(matrixClient, deviceIds), aestheticsForStagePhases: { [SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics, [SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics, }, }); + finished.then(([success]) => onFinished(success)); } }; diff --git a/src/components/views/settings/encryption/ResetIdentityBody.tsx b/src/components/views/settings/encryption/ResetIdentityBody.tsx index f2c339ca4d..a6b0b2c12e 100644 --- a/src/components/views/settings/encryption/ResetIdentityBody.tsx +++ b/src/components/views/settings/encryption/ResetIdentityBody.tsx @@ -9,7 +9,7 @@ import { Button, InlineSpinner, VisualList, VisualListItem } from "@vector-im/co import CheckIcon from "@vector-im/compound-design-tokens/assets/web/icons/check"; import InfoIcon from "@vector-im/compound-design-tokens/assets/web/icons/info"; import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error-solid"; -import React, { type JSX, useState, type MouseEventHandler } from "react"; +import React, { type JSX, useState } from "react"; import { _t } from "../../../../languageHandler"; import { EncryptionCard } from "./EncryptionCard"; @@ -22,7 +22,8 @@ interface ResetIdentityBodyProps { /** * Called when the identity is reset. */ - onFinish: MouseEventHandler; + onReset: () => void; + /** * Called when the cancel button is clicked. */ @@ -36,22 +37,26 @@ interface ResetIdentityBodyProps { } /** - * "compromised" is shown when the user chooses 'reset' explicitly in settings, usually because they believe their - * identity has been compromised. + * The variant of the panel to show. This affects the message displayed to the user. + * + * "compromised" is shown when the user chose 'Reset cryptographic identity' explicitly in settings, usually because + * they believe their identity has been compromised. * * "sync_failed" is shown when the user tried to recover their identity but the process failed, probably because * the required information is missing from recovery. * - * "forgot" is shown when the user has just forgotten their passphrase. + * "forgot" is shown when the user chose 'Forgot recovery key?' during `SetupEncryptionToast`. + * + * "confirm" is shown when the user chose 'Reset all' during `SetupEncryptionBody`. */ -export type ResetIdentityBodyVariant = "compromised" | "forgot" | "sync_failed"; +export type ResetIdentityBodyVariant = "compromised" | "forgot" | "sync_failed" | "confirm"; /** * User interface component allowing the user to reset their cryptographic identity. * * Used by {@link ResetIdentityPanel}. */ -export function ResetIdentityBody({ onCancelClick, onFinish, variant }: ResetIdentityBodyProps): JSX.Element { +export function ResetIdentityBody({ onCancelClick, onReset, variant }: ResetIdentityBodyProps): JSX.Element { const matrixClient = useMatrixClientContext(); // After the user clicks "Continue", we disable the button so it can't be @@ -78,12 +83,12 @@ export function ResetIdentityBody({ onCancelClick, onFinish, variant }: ResetIde diff --git a/src/components/views/settings/tabs/user/MediaPreviewAccountSettings.tsx b/src/components/views/settings/tabs/user/MediaPreviewAccountSettings.tsx new file mode 100644 index 0000000000..f6a2b7cf38 --- /dev/null +++ b/src/components/views/settings/tabs/user/MediaPreviewAccountSettings.tsx @@ -0,0 +1,150 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import React, { type ChangeEventHandler, useCallback } from "react"; +import { Field, HelpMessage, InlineField, Label, RadioInput, Root } from "@vector-im/compound-web"; + +import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch"; +import { type MediaPreviewConfig, MediaPreviewValue } from "../../../../../@types/media_preview"; +import { _t } from "../../../../../languageHandler"; +import { useSettingValue } from "../../../../../hooks/useSettings"; +import SettingsStore from "../../../../../settings/SettingsStore"; +import { SettingLevel } from "../../../../../settings/SettingLevel"; + +export const MediaPreviewAccountSettings: React.FC<{ roomId?: string }> = ({ roomId }) => { + const currentMediaPreview = useSettingValue("mediaPreviewConfig", roomId); + + const changeSetting = useCallback( + (newValue: MediaPreviewConfig) => { + SettingsStore.setValue( + "mediaPreviewConfig", + roomId ?? null, + roomId ? SettingLevel.ROOM_ACCOUNT : SettingLevel.ACCOUNT, + newValue, + ); + }, + [roomId], + ); + + const avatarOnChange = useCallback( + (c: boolean) => { + changeSetting({ + ...currentMediaPreview, + // Switch is inverted. "Hide avatars..." + invite_avatars: c ? MediaPreviewValue.Off : MediaPreviewValue.On, + }); + }, + [changeSetting, currentMediaPreview], + ); + + const mediaPreviewOnChangeOff = useCallback>( + (event) => { + if (!event.target.checked) { + return; + } + changeSetting({ + ...currentMediaPreview, + media_previews: MediaPreviewValue.Off, + }); + }, + [changeSetting, currentMediaPreview], + ); + + const mediaPreviewOnChangePrivate = useCallback>( + (event) => { + if (!event.target.checked) { + return; + } + changeSetting({ + ...currentMediaPreview, + media_previews: MediaPreviewValue.Private, + }); + }, + [changeSetting, currentMediaPreview], + ); + + const mediaPreviewOnChangeOn = useCallback>( + (event) => { + if (!event.target.checked) { + return; + } + changeSetting({ + ...currentMediaPreview, + media_previews: MediaPreviewValue.On, + }); + }, + [changeSetting, currentMediaPreview], + ); + + return ( + + {!roomId && ( + + )} + {/* Explict label here because htmlFor is not supported for linking to radiogroups */} + + + + {_t("settings|media_preview|media_preview_description")} + + + } + > + + + {!roomId && ( + + } + > + + + )} + + } + > + + + + + ); +}; diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx index e7929348d6..fa72cfe28e 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx @@ -32,6 +32,7 @@ import SpellCheckSettings from "../../SpellCheckSettings"; import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch"; import * as TimezoneHandler from "../../../../../TimezoneHandler"; import { type BooleanSettingKey } from "../../../../../settings/Settings.tsx"; +import { MediaPreviewAccountSettings } from "./MediaPreviewAccountSettings.tsx"; interface IProps { closeSettingsFn(success: boolean): void; @@ -116,7 +117,7 @@ const SpellCheckSection: React.FC = () => { }; export default class PreferencesUserSettingsTab extends React.Component { - private static ROOM_LIST_SETTINGS: BooleanSettingKey[] = ["breadcrumbs", "showAvatarsOnInvites"]; + private static ROOM_LIST_SETTINGS: BooleanSettingKey[] = ["breadcrumbs"]; private static SPACES_SETTINGS: BooleanSettingKey[] = ["Spaces.allRoomsInHome"]; @@ -146,7 +147,6 @@ export default class PreferencesUserSettingsTab extends React.Component + + + + {this.renderGroup(PreferencesUserSettingsTab.ROOM_DIRECTORY_SETTINGS)} diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index c5d9385d7b..ab5b941cde 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -9,7 +9,6 @@ Please see LICENSE files in the repository root for full details. import React, { lazy, Suspense, useCallback, useContext, useEffect, useRef, useState } from "react"; import { type MatrixClient } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; -import { defer } from "matrix-js-sdk/src/utils"; import { _t } from "../../../../../languageHandler"; import Modal from "../../../../../Modal"; @@ -98,9 +97,9 @@ const useSignOut = ( const url = getManageDeviceUrl(delegatedAuthAccountUrl, deviceId); window.open(url, "_blank"); } else { - const deferredSuccess = defer(); + const deferredSuccess = Promise.withResolvers(); await deleteDevicesWithInteractiveAuth(matrixClient, deviceIds, async (success) => { - deferredSuccess.resolve(success); + deferredSuccess.resolve(!!success); }); success = await deferredSuccess.promise; } @@ -203,7 +202,8 @@ const SessionManagerTab: React.FC<{ const shouldShowOtherSessions = otherSessionsCount > 0; const onVerifyCurrentDevice = (): void => { - Modal.createDialog(SetupEncryptionDialog, { onFinished: refreshDevices }); + const { finished } = Modal.createDialog(SetupEncryptionDialog); + finished.then(refreshDevices); }; const onTriggerDeviceVerification = useCallback( @@ -212,14 +212,14 @@ const SessionManagerTab: React.FC<{ return; } const verificationRequestPromise = requestDeviceVerification(deviceId); - Modal.createDialog(VerificationRequestDialog, { + const { finished } = Modal.createDialog(VerificationRequestDialog, { verificationRequestPromise, member: currentUserMember, - onFinished: async (): Promise => { - const request = await verificationRequestPromise; - request.cancel(); - await refreshDevices(); - }, + }); + finished.then(async () => { + const request = await verificationRequestPromise; + request.cancel(); + await refreshDevices(); }); }, [requestDeviceVerification, refreshDevices, currentUserMember], diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index 321f6c9e36..9f0d534767 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -363,6 +363,8 @@ const SpacePanel: React.FC = () => { } }); + const newRoomListEnabled = useSettingValue("feature_new_room_list"); + return ( {({ onKeyDownHandler, onDragEndHandler }) => ( @@ -378,7 +380,10 @@ const SpacePanel: React.FC = () => { }} >
+
); } diff --git a/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentreButton.tsx b/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentreButton.tsx index bb04df275b..9d43655c60 100644 --- a/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentreButton.tsx +++ b/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentreButton.tsx @@ -6,7 +6,7 @@ * Please see LICENSE files in the repository root for full details. */ -import React, { type ComponentProps, forwardRef } from "react"; +import React, { type ComponentProps, type Ref, type JSX } from "react"; import ThreadsSolidIcon from "@vector-im/compound-design-tokens/assets/web/icons/threads-solid"; import classNames from "classnames"; import { IconButton, Text, Tooltip } from "@vector-im/compound-web"; @@ -28,44 +28,46 @@ interface ThreadsActivityCentreButtonProps extends ComponentProps; } /** * A button to open the thread activity centre. */ -export const ThreadsActivityCentreButton = forwardRef( - function ThreadsActivityCentreButton( - { displayLabel, notificationLevel, disableTooltip, ...props }, - ref, - ): React.JSX.Element { - // Disable tooltip when the label is displayed - const openTooltip = disableTooltip || displayLabel ? false : undefined; +export const ThreadsActivityCentreButton = function ThreadsActivityCentreButton({ + displayLabel, + notificationLevel, + disableTooltip, + ref, + ...props +}: ThreadsActivityCentreButtonProps): JSX.Element { + // Disable tooltip when the label is displayed + const openTooltip = disableTooltip || displayLabel ? false : undefined; - return ( - - - <> - - {/* This is dirty, but we need to add the label to the indicator icon */} - {displayLabel && ( - - {_t("common|threads")} - - )} - - - - ); - }, -); + return ( + + + <> + + {/* This is dirty, but we need to add the label to the indicator icon */} + {displayLabel && ( + + {_t("common|threads")} + + )} + + + + ); +}; diff --git a/src/components/views/toasts/VerificationRequestToast.tsx b/src/components/views/toasts/VerificationRequestToast.tsx index e3975c148b..12843c0b87 100644 --- a/src/components/views/toasts/VerificationRequestToast.tsx +++ b/src/components/views/toasts/VerificationRequestToast.tsx @@ -124,18 +124,16 @@ export default class VerificationRequestToast extends React.PureComponent { - request.cancel(); - }, }, undefined, /* priority = */ false, /* static = */ true, ); + finished.then(() => request.cancel()); } await request.accept(); } catch (err) { diff --git a/src/components/views/voip/LegacyCallView.tsx b/src/components/views/voip/LegacyCallView.tsx index c972cb21d4..255b1ab0f4 100644 --- a/src/components/views/voip/LegacyCallView.tsx +++ b/src/components/views/voip/LegacyCallView.tsx @@ -66,26 +66,15 @@ interface IState { } function getFullScreenElement(): Element | null { - return ( - document.fullscreenElement || - // moz omitted because firefox supports this unprefixed now (webkit here for safari) - document.webkitFullscreenElement || - document.msFullscreenElement - ); + return document.fullscreenElement; } function requestFullscreen(element: Element): void { - const method = - element.requestFullscreen || - // moz omitted since firefox supports unprefixed now - element.webkitRequestFullScreen || - element.msRequestFullscreen; - if (method) method.call(element); + element.requestFullscreen(); } function exitFullscreen(): void { - const exitMethod = document.exitFullscreen || document.webkitExitFullscreen || document.msExitFullscreen; - if (exitMethod) exitMethod.call(document); + document.exitFullscreen(); } export default class LegacyCallView extends React.Component { diff --git a/src/components/views/voip/LegacyCallView/LegacyCallViewButtons.tsx b/src/components/views/voip/LegacyCallView/LegacyCallViewButtons.tsx index d9fc85c943..0e81b5278e 100644 --- a/src/components/views/voip/LegacyCallView/LegacyCallViewButtons.tsx +++ b/src/components/views/voip/LegacyCallView/LegacyCallViewButtons.tsx @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { createRef, useState, forwardRef } from "react"; +import React, { createRef, useState, type Ref, type FC } from "react"; import classNames from "classnames"; import { type MatrixCall } from "matrix-js-sdk/src/webrtc/call"; @@ -41,31 +41,40 @@ type ButtonProps = Omit, "title" | "element"> & { offLabel?: string; forceHide?: boolean; onHover?: (hovering: boolean) => void; + ref?: Ref; }; -const LegacyCallViewToggleButton = forwardRef( - ({ children, state: isOn, className, onLabel, offLabel, forceHide, onHover, ...props }, ref) => { - const classes = classNames("mx_LegacyCallViewButtons_button", className, { - mx_LegacyCallViewButtons_button_on: isOn, - mx_LegacyCallViewButtons_button_off: !isOn, - }); +const LegacyCallViewToggleButton: FC = ({ + children, + state: isOn, + className, + onLabel, + offLabel, + forceHide, + onHover, + ref, + ...props +}) => { + const classes = classNames("mx_LegacyCallViewButtons_button", className, { + mx_LegacyCallViewButtons_button_on: isOn, + mx_LegacyCallViewButtons_button_off: !isOn, + }); - const title = forceHide ? undefined : isOn ? onLabel : offLabel; + const title = forceHide ? undefined : isOn ? onLabel : offLabel; - return ( - - {children} - - ); - }, -); + return ( + + {children} + + ); +}; interface IDropdownButtonProps extends ButtonProps { deviceKinds: MediaDeviceKindEnum[]; diff --git a/src/contexts/MatrixClientContext.tsx b/src/contexts/MatrixClientContext.tsx index 0810465789..1783c9a12f 100644 --- a/src/contexts/MatrixClientContext.tsx +++ b/src/contexts/MatrixClientContext.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type ComponentClass, createContext, forwardRef, useContext } from "react"; +import React, { type ComponentClass, createContext, useContext } from "react"; import { type MatrixClient } from "matrix-js-sdk/src/matrix"; // This context is available to components under LoggedInView, @@ -24,22 +24,16 @@ export function useMatrixClientContext(): MatrixClient { return useContext(MatrixClientContext); } -const matrixHOC = ( - ComposedComponent: ComponentClass, -): (( - props: Omit & React.RefAttributes>, -) => React.ReactElement | null) => { - type ComposedComponentInstance = InstanceType; - - // eslint-disable-next-line react-hooks/rules-of-hooks - - const TypedComponent = ComposedComponent; - - return forwardRef>((props, ref) => { +const matrixHOC = + ( + ComposedComponent: ComponentClass, + ): (( + props: Omit & React.RefAttributes>, + ) => React.ReactElement | null) => + (props) => { const client = useContext(MatrixClientContext); // @ts-ignore - return ; - }); -}; + return ; + }; export const withMatrixClientHOC = matrixHOC; diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index d3a6bc38a1..e6616a7e21 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -26,6 +26,11 @@ export enum Action { */ ViewUser = "view_user", + /** + * Share a text message by forwarding it to a room selected by the user + */ + Share = "share", + /** * Open the user settings. No additional payload information required. * Optionally can include an OpenToTabPayload. @@ -235,6 +240,12 @@ export enum Action { */ AfterLeaveRoom = "after_leave_room", + /** + * Dispatched after a room has been successfully forgotten + * Should be used with AfterForgetRoomPayload. + */ + AfterForgetRoom = "after_forget_room", + /** * Used to defer actions until after sync is complete * LifecycleStore will emit deferredAction payload after 'MatrixActions.sync' @@ -362,7 +373,7 @@ export enum Action { View3pidInvite = "view_3pid_invite", /** - * Opens right panel room summary and focuses the search input + * Opens right panel room summary and focuses the search input. Use with a FocusMessageSearchPayload. */ FocusMessageSearch = "focus_search", diff --git a/src/dispatcher/payloads/AfterForgetRoomPayload.ts b/src/dispatcher/payloads/AfterForgetRoomPayload.ts new file mode 100644 index 0000000000..ba480e7ff7 --- /dev/null +++ b/src/dispatcher/payloads/AfterForgetRoomPayload.ts @@ -0,0 +1,16 @@ +/* + * 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 { type Action } from "../actions"; +import { type ActionPayload } from "../payloads"; + +export interface AfterForgetRoomPayload extends ActionPayload { + action: Action.AfterForgetRoom; + room: Room; +} diff --git a/src/dispatcher/payloads/FocusMessageSearchPayload.ts b/src/dispatcher/payloads/FocusMessageSearchPayload.ts new file mode 100644 index 0000000000..a06f7f5174 --- /dev/null +++ b/src/dispatcher/payloads/FocusMessageSearchPayload.ts @@ -0,0 +1,15 @@ +/* +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 Action } from "../actions"; +import { type ActionPayload } from "../payloads"; + +export interface FocusMessageSearchPayload extends ActionPayload { + action: Action.FocusMessageSearch; + + initialText?: string; +} diff --git a/src/dispatcher/payloads/SharePayload.ts b/src/dispatcher/payloads/SharePayload.ts new file mode 100644 index 0000000000..0aa44fe12f --- /dev/null +++ b/src/dispatcher/payloads/SharePayload.ts @@ -0,0 +1,29 @@ +/* +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 ActionPayload } from "../payloads"; +import { type Action } from "../actions"; + +export enum ShareFormat { + Text = "text", + Html = "html", + Markdown = "md", +} + +export interface SharePayload extends ActionPayload { + action: Action.Share; + + /** + * The format of message to be shared (optional) + */ + format: ShareFormat; + + /** + * The message to be shared. + */ + msg: string; +} diff --git a/src/events/EventTileFactory.tsx b/src/events/EventTileFactory.tsx index 12e9f869f3..f1fc224471 100644 --- a/src/events/EventTileFactory.tsx +++ b/src/events/EventTileFactory.tsx @@ -69,7 +69,7 @@ export interface EventTileTypeProps } type FactoryProps = Omit; -type Factory = (ref: Optional>, props: X) => JSX.Element; +type Factory = (ref: React.RefObject | undefined, props: X) => JSX.Element; export const MessageEventFactory: Factory = (ref, props) => ; const LegacyCallEventFactory: Factory = (ref, props) => ( diff --git a/src/hooks/room/useRoomCall.tsx b/src/hooks/room/useRoomCall.tsx index e0dfbdcdeb..8d7667aa7d 100644 --- a/src/hooks/room/useRoomCall.tsx +++ b/src/hooks/room/useRoomCall.tsx @@ -10,7 +10,7 @@ import { type Room } from "matrix-js-sdk/src/matrix"; import React, { type ReactNode, useCallback, useEffect, useMemo, useState } from "react"; import { CallType } from "matrix-js-sdk/src/webrtc/call"; -import { useFeatureEnabled } from "../useSettings"; +import { useFeatureEnabled, useSettingValue } from "../useSettings"; import SdkConfig from "../../SdkConfig"; import { useEventEmitter, useEventEmitterState } from "../useEventEmitter"; import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../LegacyCallHandler"; @@ -33,7 +33,6 @@ import { Action } from "../../dispatcher/actions"; import { CallStore, CallStoreEvent } from "../../stores/CallStore"; import { isVideoRoom } from "../../utils/video-rooms"; import { useGuestAccessInformation } from "./useGuestAccessInformation"; -import SettingsStore from "../../settings/SettingsStore"; import { UIFeature } from "../../settings/UIFeature"; import { BetaPill } from "../../components/views/beta/BetaCard"; import { type InteractionName } from "../../PosthogTrackers"; @@ -102,6 +101,8 @@ export const useRoomCall = ( } => { // settings const groupCallsEnabled = useFeatureEnabled("feature_group_calls"); + const widgetsFeatureEnabled = useSettingValue(UIFeature.Widgets); + const voipFeatureEnabled = useSettingValue(UIFeature.Voip); const useElementCallExclusively = useMemo(() => { return SdkConfig.get("element_call").use_exclusively; }, []); @@ -285,8 +286,8 @@ export const useRoomCall = ( // We hide the voice call button if it'd have the same effect as the video call button let hideVoiceCallButton = isManagedHybridWidgetEnabled(room) || !callOptions.includes(PlatformCallType.LegacyCall); let hideVideoCallButton = false; - // We hide both buttons if they require widgets but widgets are disabled. - if (memberCount > 2 && !SettingsStore.getValue(UIFeature.Widgets)) { + // We hide both buttons if they require widgets but widgets are disabled, or if the Voip feature is disabled. + if ((memberCount > 2 && !widgetsFeatureEnabled) || !voipFeatureEnabled) { hideVoiceCallButton = true; hideVideoCallButton = true; } diff --git a/src/hooks/useMediaVisible.ts b/src/hooks/useMediaVisible.ts index e244d500cf..f367e87c4f 100644 --- a/src/hooks/useMediaVisible.ts +++ b/src/hooks/useMediaVisible.ts @@ -6,30 +6,52 @@ Please see LICENSE files in the repository root for full details. */ import { useCallback } from "react"; +import { JoinRule } from "matrix-js-sdk/src/matrix"; import { SettingLevel } from "../settings/SettingLevel"; import { useSettingValue } from "./useSettings"; import SettingsStore from "../settings/SettingsStore"; +import { useMatrixClientContext } from "../contexts/MatrixClientContext"; +import { MediaPreviewValue } from "../@types/media_preview"; +import { useRoomState } from "./useRoomState"; + +const PRIVATE_JOIN_RULES: JoinRule[] = [JoinRule.Invite, JoinRule.Knock, JoinRule.Restricted]; /** * Should the media event be visible in the client, or hidden. * @param eventId The eventId of the media event. * @returns A boolean describing the hidden status, and a function to set the visiblity. */ -export function useMediaVisible(eventId: string): [boolean, (visible: boolean) => void] { - const defaultShowImages = useSettingValue("showImages", SettingLevel.DEVICE); - const eventVisibility = useSettingValue("showMediaEventIds", SettingLevel.DEVICE); +export function useMediaVisible(eventId?: string, roomId?: string): [boolean, (visible: boolean) => void] { + const mediaPreviewSetting = useSettingValue("mediaPreviewConfig", roomId); + const client = useMatrixClientContext(); + const eventVisibility = useSettingValue("showMediaEventIds"); + const joinRule = useRoomState(client.getRoom(roomId) ?? undefined, (state) => state.getJoinRule()); const setMediaVisible = useCallback( (visible: boolean) => { SettingsStore.setValue("showMediaEventIds", null, SettingLevel.DEVICE, { ...eventVisibility, - [eventId]: visible, + [eventId!]: visible, }); }, [eventId, eventVisibility], ); + const roomIsPrivate = joinRule ? PRIVATE_JOIN_RULES.includes(joinRule) : false; + + const explicitEventVisiblity = eventId ? eventVisibility[eventId] : undefined; // Always prefer the explicit per-event user preference here. - const imgIsVisible = eventVisibility[eventId] ?? defaultShowImages; - return [imgIsVisible, setMediaVisible]; + if (explicitEventVisiblity !== undefined) { + return [explicitEventVisiblity, setMediaVisible]; + } else if (mediaPreviewSetting.media_previews === MediaPreviewValue.Off) { + return [false, setMediaVisible]; + } else if (mediaPreviewSetting.media_previews === MediaPreviewValue.On) { + return [true, setMediaVisible]; + } else if (mediaPreviewSetting.media_previews === MediaPreviewValue.Private) { + return [roomIsPrivate, setMediaVisible]; + } else { + // Invalid setting. + console.warn("Invalid media visibility setting", mediaPreviewSetting.media_previews); + return [false, setMediaVisible]; + } } diff --git a/src/hooks/useTransition.ts b/src/hooks/useTransition.ts deleted file mode 100644 index d5e8df5f5d..0000000000 --- a/src/hooks/useTransition.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2024 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -// Based on https://stackoverflow.com/a/61680184 - -import { type DependencyList, useEffect, useRef } from "react"; - -export const useTransition = (callback: (...params: D) => void, deps: D): void => { - const func = useRef<(...params: D) => void>(callback); - - useEffect(() => { - func.current = callback; - }, [callback]); - - const args = useRef(null); - - useEffect(() => { - if (args.current !== null) func.current(...args.current); - args.current = deps; - // eslint-disable-next-line react-compiler/react-compiler,react-hooks/exhaustive-deps - }, deps); -}; diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index c3fa1053bc..f454a2f1e4 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -159,6 +159,7 @@ "view_message": "Zobrazit zprávu", "view_source": "Zobrazit zdroj", "yes": "Ano", + "yes_dismiss": "Ano, zamítnout", "zoom_in": "Přiblížit", "zoom_out": "Oddálit" }, @@ -388,6 +389,7 @@ "fallback_button": "Zahájit autentizaci", "mas_cross_signing_reset_cta": "Přejděte na svůj účet", "mas_cross_signing_reset_description": "Resetujte svou identitu prostřednictvím svého poskytovatele účtu a poté se vraťte a klikněte na \"Opakovat\".", + "mas_cross_signing_reset_title": "Přejděte na svůj účet a obnovte svou identitu", "msisdn": "Na číslo %(msisdn)s byla odeslána textová zpráva", "msisdn_token_incorrect": "Neplatný token", "msisdn_token_prompt": "Prosím zadejte kód z této zprávy:", @@ -527,6 +529,7 @@ "message_timestamp_invalid": "Neplatné časové razítko", "microphone": "Mikrofon", "model": "Model", + "moderation_and_safety": "Moderování a bezpečnost", "modern": "Moderní", "mute": "Ztlumit", "n_members": { @@ -910,22 +913,13 @@ "empty_room_was_name": "Prázdná místnost (dříve %(oldName)s)", "encryption": { "access_secret_storage_dialog": { - "enter_phrase_or_key_prompt": "Pro pokračování zadejte bezpečnostní frázi nebo .", + "alternatives": "Pokud máte bezpečnostní klíč nebo bezpečnostní frázi, bude to také fungovat.", "key_validation_text": { - "invalid_security_key": "Neplatný klíč pro obnovení", - "recovery_key_is_correct": "To vypadá dobře!", - "wrong_file_type": "Špatný typ souboru", - "wrong_security_key": "Nesprávný klíč pro obnovení" + "wrong_security_key": "Zadaný klíč pro obnovení není správný." }, - "reset_title": "Resetovat vše", - "reset_warning_1": "Udělejte to, pouze pokud nemáte žádné jiné zařízení, se kterým byste mohli dokončit ověření.", - "reset_warning_2": "Pokud vše resetujete, začnete bez důvěryhodných relací, bez důvěryhodných uživatelů a možná nebudete moci zobrazit minulé zprávy.", + "privacy_warning": "Ujistěte se, že tuto obrazovku nikdo nevidí!", "restoring": "Obnovení klíčů ze zálohy", - "security_key_title": "Klíč pro obnovení", - "security_phrase_incorrect_error": "Nelze získat přístup k zabezpečenému úložišti. Ověřte, zda jste zadali správnou bezpečnostní frázi.", - "security_phrase_title": "Bezpečnostní fráze", - "separator": "%(securityKey)s nebo %(recoveryFile)s", - "use_security_key_prompt": "Pokračujte pomocí klíče pro obnovení." + "security_key_title": "Klíč pro obnovení" }, "bootstrap_title": "Příprava klíčů", "cancel_entering_passphrase_description": "Chcete určitě zrušit zadávání přístupové fráze?", @@ -981,6 +975,8 @@ "setup_secure_backup": { "explainer": "Před odhlášením si zazálohujte klíče abyste o ně nepřišli." }, + "turn_on_key_storage": "Zapnout úložiště klíčů", + "turn_on_key_storage_description": "Bezpečně uložte svou kryptografickou identitu a klíče zpráv na serveru. To vám umožní zobrazit historii zpráv na všech nových zařízeních.", "udd": { "interactive_verification_button": "Interaktivní ověření pomocí emoji", "other_ask_verify_text": "Požádejte tohoto uživatele, aby ověřil svou relaci, nebo jí níže můžete ověřit manuálně.", @@ -994,7 +990,6 @@ "accepting": "Přijímání…", "after_new_login": { "device_verified": "Zařízení ověřeno", - "reset_confirmation": "Opravdu chcete resetovat ověřovací klíče?", "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í" @@ -1065,8 +1060,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_reset_warning_1": "Resetování ověřovacích klíčů nelze vrátit zpět. Po jejich resetování nebudete mít přístup ke starým zašifrovaným zprávám a všem přátelům, kteří vás dříve ověřili, se zobrazí bezpečnostní varování, dokud se u nich znovu neověříte.", - "verify_reset_warning_2": "Pokračujte pouze v případě, že jste si jisti, že jste ztratili všechna ostatní zařízení a klíč pro obnovení.", "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", @@ -2100,12 +2093,14 @@ }, "uploading_single_file": "Nahrávání %(filename)s" }, + "video_room": "Tato místnost je video místnost", "waiting_for_join_subtitle": "Jakmile se pozvaní uživatelé připojí k %(brand)s, budete moci chatovat a místnost bude koncově šifrovaná", "waiting_for_join_title": "Čekání na připojení uživatelů k %(brand)s" }, "room_list": { "add_room_label": "Přidat místnost", "add_space_label": "Přidat prostor", + "appearance": "Vzhled", "breadcrumbs_empty": "Žádné nedávno navštívené místnosti", "breadcrumbs_label": "Nedávno navštívené místnosti", "empty": { @@ -2114,11 +2109,14 @@ "no_chats_description_no_room_rights": "Začněte tím, že někomu pošlete zprávu", "no_favourites": "Zatím nemáte oblíbený chat", "no_favourites_description": "Chat si můžete přidat do oblíbených v nastavení chatu", + "no_invites": "Nemáte žádné nepřečtené pozvánky", + "no_mentions": "Nemáte žádné nepřečtené zmínky", "no_people": "Zatím s nikým nemáte přímé chaty", "no_people_description": "Můžete zrušit výběr filtrů, abyste viděli ostatní chaty", "no_rooms": "Ještě nejste v žádné místnosti", "no_rooms_description": "Můžete zrušit výběr filtrů, abyste viděli své další chaty", "no_unread": "Gratulujeme! Nemáte žádné nepřečtené zprávy", + "show_activity": "Zobrazit veškerou aktivitu", "show_chats": "Zobrazit všechny chaty" }, "failed_add_tag": "Nepodařilo se přidat štítek %(tagName)s k místnosti", @@ -2126,6 +2124,8 @@ "failed_set_dm_tag": "Nepodařilo se nastavit značku přímé zprávy", "filters": { "favourite": "Oblíbené", + "invites": "Pozvánky", + "mentions": "Zmínky", "people": "Lidé", "rooms": "Místnosti", "unread": "Nepřečtené" @@ -2156,15 +2156,22 @@ "more_options": "Více možností", "open_room": "Otevřít místnost %(roomName)s" }, + "room_options": "Možnosti místnosti", "show_less": "Zobrazit méně", + "show_message_previews": "Zobrazit náhledy zpráv", "show_n_more": { "other": "Zobrazit %(count)s dalších", "one": "Zobrazit %(count)s další" }, "show_previews": "Zobrazovat náhledy zpráv", + "sort": "Řadit", "sort_by": "Řadit dle", "sort_by_activity": "Aktivity", "sort_by_alphabet": "A–Z", + "sort_type": { + "activity": "Aktivita", + "atoz": "A–Z" + }, "sort_unread_first": "Zobrazovat místnosti s nepřečtenými zprávami jako první", "space_menu": { "home": "Domov prostoru", @@ -2452,6 +2459,10 @@ "recent_changes_heading": "Nedávné změny, které dosud nebyly přijaty", "title": "Server neodpovídá" }, + "service_worker_error": { + "description": "%(brand)s vyžaduje service worker pro načítání ověřených médií z úložiště obsahu Matrixu. Váš prohlížeč to nepodporuje, takže může dojít k selhání načtení médií.", + "title": "Nepodařilo se načíst service worker" + }, "seshat": { "error_initialising": "Inicializace vyhledávání zpráv se nezdařila, zkontrolujte svá nastavení", "reset_button": "Resetovat úložiště událostí", @@ -2545,6 +2556,8 @@ "session_key": "Klíč relace:", "title": "Rozšířené" }, + "confirm_key_storage_off": "Opravdu chcete ponechat úložiště klíčů vypnuté?", + "confirm_key_storage_off_description": "Pokud se odhlásíte ze všech svých zařízení, ztratíte historii zpráv a budete muset znovu ověřit všechny své stávající kontakty. Další informace", "delete_key_storage": { "breadcrumb_page": "Smazat úložiště klíčů", "confirm": "Smazat úložiště klíčů", @@ -2630,6 +2643,7 @@ "discovery_needs_terms_title": "Umožněte lidem, aby vás našli", "display_name": "Zobrazovaný název", "display_name_error": "Nelze nastavit zobrazovaný název", + "email_adding_unsupported_by_hs": "Tento domovský server nepodporuje přidávání e-mailových adres do vašeho účtu.", "email_address_in_use": "Tato e-mailová adresa je již používána", "email_address_label": "E-mailová adresa", "email_not_verified": "Vaše e-mailová adresa ještě nebyla ověřena", @@ -2654,7 +2668,9 @@ "error_share_msisdn_discovery": "Nepovedlo se nasdílet telefonní číslo", "identity_server_no_token": "Nebyl nalezen žádný přístupový token identity", "identity_server_not_set": "Server identit není nastaven", + "invalid_phone_number": "Zadané telefonní číslo se nezdá být platné.", "language_section": "Jazyk", + "msisdn_adding_unsupported_by_hs": "Tento domovský server nepodporuje přidávání telefonních čísel do vašeho účtu.", "msisdn_in_use": "Toto telefonní číslo je již používáno", "msisdn_label": "Telefonní číslo", "msisdn_verification_field_label": "Ověřovací kód", @@ -2673,12 +2689,10 @@ "unable_to_load_msisdns": "Nelze načíst telefonní čísla", "username": "Uživatelské jméno" }, - "image_thumbnails": "Zobrazovat náhledy obrázků", "inline_url_previews_default": "Nastavit povolení náhledů URL adres jako výchozí", "inline_url_previews_room": "Povolit náhledy URL adres pro členy této místnosti jako výchozí", "inline_url_previews_room_account": "Povolit náhledy URL adres pro tuto místnost (ovlivňuje pouze vás)", "insert_trailing_colon_mentions": "Vložit dvojtečku za zmínku o uživateli na začátku zprávy", - "invite_avatars": "Zobrazit avatary místností, do kterých jste byli pozváni", "jump_to_bottom_on_send": "Po odeslání zprávy přejít na konec časové osy", "key_backup": { "backup_in_progress": "Klíče se zálohují (první záloha může trvat pár minut).", @@ -2737,6 +2751,14 @@ "labs_mjolnir": { "dialog_title": "Nastavení: Ignorovaní uživatelé" }, + "media_preview": { + "hide_avatars": "Skrýt avatary místnosti a zvoucího", + "hide_media": "Vždy skrýt", + "media_preview_description": "Skryté médium lze vždy zobrazit klepnutím na něj", + "media_preview_label": "Zobrazit média na časové ose", + "show_in_private": "V soukromých místnostech", + "show_media": "Vždy zobrazit" + }, "notifications": { "default_setting_description": "Toto nastavení se ve výchozím stavu použije pro všechny vaše místnosti.", "default_setting_section": "Chci být upozorňován na (Výchozí nastavení)", @@ -3221,7 +3243,7 @@ "heading_without_query": "Hledat", "join_button_text": "Vstoupit do %(roomAddress)s", "keyboard_scroll_hint": "K pohybu použijte ", - "message_search_section_title": "Další vyhledávání", + "messages_label": "Zprávy", "other_rooms_in_space": "Další místnosti v %(spaceName)s", "public_rooms_label": "Veřejné místnosti", "public_spaces_label": "Veřejné prostory", @@ -3231,7 +3253,6 @@ "result_may_be_hidden_privacy_warning": "Některé výsledky mohou být z důvodu ochrany soukromí skryté", "result_may_be_hidden_warning": "Některé výsledky mohou být skryté", "search_dialog": "Dialogové okno hledání", - "search_messages_hint": "Pro vyhledávání zpráv hledejte tuto ikonu v horní části místnosti ", "spaces_title": "Prostory, ve kterých se nacházíte", "start_group_chat_button": "Zahájit skupinový chat" }, @@ -3280,9 +3301,7 @@ "threads_activity_centre": { "header": "Aktivita vláken", "no_rooms_with_threads_notifs": "Zatím nemáte k dispozici místnosti s upozorněními na vlákna.", - "no_rooms_with_unread_threads": "Zatím nemáte místnosti s nepřečtenými vlákny.", - "release_announcement_description": "Oznámení o vláknech se přesunula, od nynějška je najdete zde.", - "release_announcement_header": "Centrum aktivity vláken" + "no_rooms_with_unread_threads": "Zatím nemáte místnosti s nepřečtenými vlákny." }, "time": { "about_day_ago": "před jedním dnem", @@ -3793,10 +3812,11 @@ "unavailable": "Nedostupné" }, "update_room_access_modal": { - "description": "Chcete-li vytvořit sdílený odkaz, musíte hostům povolit, aby se k této místnosti připojili. Může se tak stát, že místnost bude méně bezpečná. Po dokončení hovoru můžete místnost opět učinit soukromou.", - "dont_change_description": "Případně můžete hovor uskutečnit v oddělené místnosti.", + "description": "Chcete-li vytvořit sdílený odkaz, nastavte tuto místnost veřejnou nebo povolte možnost, aby uživatelé požádali o vstup. To umožňuje hostům připojit se, aniž by byli pozváni.", + "dont_change_description": "Pokud nechcete změnit přístup k této místnosti, můžete pro odkaz na hovor vytvořit novou místnost.", "no_change": "Nechci měnit úroveň přístupu.", - "title": "Změna úrovně přístupu do místnosti" + "revert_access_description": "(Tuto hodnotu lze vrátit na předchozí hodnotu v Nastavení místnosti: Zabezpečení a soukromí / Přístup)", + "title": "Povolit hostům vstup do této místnosti" }, "upload_failed_generic": "Soubor '%(fileName)s' se nepodařilo nahrát.", "upload_failed_size": "Soubor '%(fileName)s' je větší než povoluje limit domovského serveru", diff --git a/src/i18n/strings/cy.json b/src/i18n/strings/cy.json index afb4d7da19..d73c13a384 100644 --- a/src/i18n/strings/cy.json +++ b/src/i18n/strings/cy.json @@ -3,9 +3,29 @@ "emoji_picker": "Dewisydd Emoji", "jump_first_invite": "Symud i'r gwahoddiad cyntaf.", "message_composer": "Neges cyfansoddwr", + "n_unread_messages": { + "%(count)s neges heb eu darllen": "other", + "1 neges heb ei darllen": "one" + }, + "n_unread_messages_mentions": { + "%(count)s crybwylliadau heb eu darllen": "zero", + "1 crybwylliad heb ei ddarllen": "one", + "%(count)s grybwylliad heb eu darllen": "two", + "%(count)s crybwylliad heb eu darllen": "other" + }, "recent_rooms": "Ystafelloedd diweddar", "room_messsage_not_sent": "Agor ystafell %(roomName)s gyda neges heb ei gosod.", "room_n_unread_invite": "Agor gwahoddiad i ystafell %(roomName)s.", + "room_n_unread_messages": { + "Ystafell agored%(roomName)s gyda %(count)s negeseuon heb eu darllen.": "zero", + "Ystafell agored %(roomName)s gydag 1 neges heb ei darllen.": "one", + "Ystafell agored%(roomName)s gyda %(count)s neges heb eu darllen.": "other" + }, + "room_n_unread_messages_mentions": { + "Ystafell agored %(roomName)s gyda %(count)s negeseuon heb eu darllen gan gynnwys crybwylliadau.": "zero", + "Ystafell agored %(roomName)s gydag 1 crybwylliad heb ei ddarllen.": "one", + "Ystafell agored %(roomName)s gyda %(count)s neges heb eu darllen gan gynnwys crybwylliadau.": "other" + }, "room_name": "Ystafell %(matere)s", "room_status_bar": "Bar statws ystafell", "seek_bar_label": "Bar chwilio sain", @@ -141,6 +161,7 @@ "view_message": "Gweld neges", "view_source": "Gweld Ffynhonnell", "yes": "Iawn", + "yes_dismiss": "Iawn, diystyru", "zoom_in": "Chwyddo mewn", "zoom_out": "Chwyddo allan" }, @@ -370,6 +391,7 @@ "fallback_button": "Dechrau dilysu", "mas_cross_signing_reset_cta": "Mynd i'ch cyfrif", "mas_cross_signing_reset_description": "Ailosodwch eich hunaniaeth trwy ddarparwr eich cyfrif ac yna dewch yn ôl a chlicio \"Ailgynnig\".", + "mas_cross_signing_reset_title": "Ewch i'ch cyfrif i ailosod eich hunaniaeth", "msisdn": "Mae neges destun wedi'i hanfon at %(msisdn)s", "msisdn_token_incorrect": "Tocyn yn anghywir", "msisdn_token_prompt": "Rhowch y cod sydd ynddo:", @@ -505,6 +527,7 @@ "message_timestamp_invalid": "Stamp amser annilys", "microphone": "Meicroffôn", "model": "Model", + "moderation_and_safety": "Cymedroli a diogelwch", "modern": "Modern", "mute": "Tewi", "name": "Enw", @@ -872,22 +895,11 @@ "empty_room_was_name": "Ystafell wag (roedd yn %(oldName)s)", "encryption": { "access_secret_storage_dialog": { - "enter_phrase_or_key_prompt": "Rhowch eich Ymadrodd Diogelwch neu i barhau.", "key_validation_text": { - "invalid_security_key": "Allwedd Adfer Annilys", - "recovery_key_is_correct": "Edrych yn dda!", - "wrong_file_type": "Math anghywir o ffeil", "wrong_security_key": "Allwedd Adfer Anghywir" }, - "reset_title": "Ailosod popeth", - "reset_warning_1": "Gwnewch hyn dim ond os nad oes gennych unrhyw ddyfais arall gyda chi i gwblhau'r dilysu.", - "reset_warning_2": "Os byddwch yn ailosod popeth, byddwch yn ailgychwyn heb unrhyw sesiynau dibynadwy, dim defnyddwyr dibynadwy, ac efallai na fyddwch yn gallu gweld negeseuon blaenorol.", "restoring": "Adfer allweddi o'r copi wrth gefn", - "security_key_title": "Allwedd Adfer", - "security_phrase_incorrect_error": "Methu cyrchu storfa gyfrinachol. Gwiriwch eich bod wedi rhoi'r Ymadrodd Diogelwch cywir.", - "security_phrase_title": "Ymadrodd Diogelwch", - "separator": "%(securityKey)s neu %(recoveryFile)s", - "use_security_key_prompt": "Defnyddiwch eich Allwedd Adfer i barhau." + "security_key_title": "Allwedd Adfer" }, "bootstrap_title": "Gosod allweddi", "cancel_entering_passphrase_description": "Ydych chi'n siŵr eich bod am ddiddymu'r cyfrinymadrodd?", @@ -926,8 +938,8 @@ "title": "Dull Adfer Newydd", "warning": "Os na wnaethoch chi osod y dull adfer newydd, mae'n bosibl bod ymosodwr yn ceisio cael mynediad i'ch cyfrif. Newidiwch eich cyfrinair cyfrif a gosodwch ddull adfer newydd ar unwaith yn y Gosodiadau." }, - "pinned_identity_changed": "Mae'n ymddangos bod hunaniaeth %(displayName)s ( %(userId)s ) wedi newid. Dysgu rhagor", - "pinned_identity_changed_no_displayname": "Mae'n ymddangos bod hunaniaeth %(userId)s wedi newid. Dysgu rhagor", + "pinned_identity_changed": "Cafodd hunaniaeth (%(userId)s) %(displayName)s ei ailosod. Dysgu rhagor", + "pinned_identity_changed_no_displayname": "Cafodd hunaniaeth %(userId)s ei ailosod. Dysgu rhagor", "recovery_method_removed": { "description_1": "Mae'r sesiwn hon wedi canfod bod eich Ymadrodd Diogelwch a'ch allwedd ar gyfer Negeseuon Diogel wedi'u dileu.", "description_2": "Os gwnaethoch hyn yn ddamweiniol, gallwch osod Negeseuon Diogel ar y sesiwn hon a fydd yn ail-amgryptio hanes negeseuon y sesiwn hon gyda dull adfer newydd.", @@ -943,6 +955,8 @@ "setup_secure_backup": { "explainer": "Gwnewch gopi wrth gefn o'ch allweddi cyn allgofnodi er mwyn osgoi eu colli." }, + "turn_on_key_storage": "Troi storfa allweddi ymlaen", + "turn_on_key_storage_description": "Cadwch eich hunaniaeth cryptograffig a'ch allweddi neges yn ddiogel ar y gweinydd. Bydd hyn yn caniatáu ichi weld hanes eich neges ar unrhyw ddyfeisiau newydd. %1$s.", "udd": { "interactive_verification_button": "Dilyswch yn rhyngweithiol trwy emoji", "other_ask_verify_text": "Gofynnwch i'r defnyddiwr hwn wirio ei sesiwn, neu ei wirio â llaw isod.", @@ -956,7 +970,6 @@ "accepting": "Yn derbyn…", "after_new_login": { "device_verified": "Dyfais wedi'i dilysu", - "reset_confirmation": "Ailosod allweddi dilysu mewn gwirionedd?", "skip_verification": "Hepgor dilysu am y tro", "unable_to_verify": "Methu â gwirio'r ddyfais hon", "verify_this_device": "Dilyswch y ddyfais hon" @@ -1027,8 +1040,6 @@ "verify_emoji_prompt": "Gwiriwch trwy gymharu emoji unigryw.", "verify_emoji_prompt_qr": "Os na allwch sganio'r cod uchod, gwiriwch trwy gymharu emoji unigryw.", "verify_later": "Byddaf yn gwirio yn ddiweddarach", - "verify_reset_warning_1": "Nid oes modd dadwneud ailosod eich allweddi dilysu. Ar ôl ailosod, ni fydd gennych fynediad i hen negeseuon wedi'u hamgryptio, a bydd unrhyw ffrindiau sydd wedi'ch dilysu o'r blaen yn gweld rhybuddion diogelwch nes i chi ail-ddilysu gyda nhw.", - "verify_reset_warning_2": "Mynd ymlaen dim ond os ydych yn siŵr eich bod wedi colli eich holl ddyfeisiau eraill a'ch Allwedd Adfer.", "verify_using_device": "Gwiriwch gyda dyfais arall", "verify_using_key": "Dilyswch gydag Allwedd Adfer", "verify_using_key_or_phrase": "Dilyswch gydag Allwedd Adfer neu Ymadrodd", @@ -1038,8 +1049,8 @@ "waiting_other_user": "Yn aros i %(displayName)s wirio…" }, "verification_requested_toast_title": "Gofynnwyd am ddilysiad", - "verified_identity_changed": "Mae hunaniaeth %(displayName)s ( %(userId)s ) a ddilyswyd wedi newid. Dysgu rhagor", - "verified_identity_changed_no_displayname": "Mae hunaniaeth ddilysedig %(userId)s wedi newid. Dysgu rhagor", + "verified_identity_changed": "Cafodd hunaniaeth (%(userId)s) %(displayName)s ei ailosod. Dysgu rhagor", + "verified_identity_changed_no_displayname": "Cafodd hunaniaeth %(userId)s ei ailosod. Dysgu rhagor", "verify_toast_description": "Efallai na fydd defnyddwyr eraill yn ymddiried ynddo", "verify_toast_title": "Gwiriwch y sesiwn hon", "withdraw_verification_action": "Tynnu'r dilysiad yn ôl" @@ -1617,7 +1628,7 @@ "mark_all_read": "Marcio'r cyfan wedi'u darllen", "mentions_and_keywords": "@crybwylliadau ac allweddeiriau", "mentions_and_keywords_description": "Dim ond gyda chyfeiriadau ac allweddeiriau fel y'u gosodwyd yn eich gosodiadau y cewch eich hysbysu", - "mentions_keywords": "Crybwyll ac allweddeiriau", + "mentions_keywords": "Crybwylliadau ac allweddeiriau", "message_didnt_send": "Heb anfon y neges. Cliciwch am wybodaeth.", "mute_description": "Fyddwch chi ddim yn cael unrhyw hysbysiadau", "mute_room": "Tewi'r ystafell" @@ -1974,12 +1985,14 @@ "upload": { "uploading_single_file": "Wrthi'n llwytho %(filematere)s" }, + "video_room": "Mae'r ystafell hon yn ystafell fideo", "waiting_for_join_subtitle": "Unwaith y bydd defnyddwyr sydd wedi'u gwahodd wedi ymuno â %(brand)s, byddwch yn gallu sgwrsio a bydd yr ystafell yn cael ei hamgryptio pen-i-ben", "waiting_for_join_title": "Yn aros i ddefnyddwyr ymuno â %(brand)s" }, "room_list": { "add_room_label": "Ychwanegu ystafell", "add_space_label": "Ychwanegu gofod", + "appearance": "Gwedd", "breadcrumbs_empty": "Dim ystafelloedd yr ymwelwyd â nhw yn ddiweddar", "breadcrumbs_label": "Ymwelwyd ag ystafelloedd yn ddiweddar", "empty": { @@ -1988,11 +2001,14 @@ "no_chats_description_no_room_rights": "Dechreuwch trwy anfon neges at rywun", "no_favourites": "Nid oes gennych hoff sgwrs eto", "no_favourites_description": "Gallwch ychwanegu sgwrs at eich ffefrynnau yn y gosodiadau sgwrsio", + "no_invites": "Does gennych chi ddim gwahoddiadau heb eu darllen", + "no_mentions": "Does gennych chi ddim crybwylliadau heb eu darllen", "no_people": "Nid oes gennych chi sgyrsiau uniongyrchol gydag unrhyw un eto", "no_people_description": "Gallwch ddad-ddewis hidlwyr er mwyn gweld eich sgyrsiau eraill", "no_rooms": "Nid ydych mewn unrhyw ystafell eto", "no_rooms_description": "Gallwch ddad-ddewis hidlwyr er mwyn gweld eich sgyrsiau eraill", "no_unread": "Llongyfarchiadau! Nid oes gennych unrhyw negeseuon heb eu darllen", + "show_activity": "Gweld yr holl weithgarwch", "show_chats": "Dangos pob sgwrs" }, "failed_add_tag": "Wedi methu ag ychwanegu tag %(tagName)s i'r ystafell", @@ -2000,6 +2016,8 @@ "failed_set_dm_tag": "Wedi methu gosod tag neges uniongyrchol", "filters": { "favourite": "Ffefrynnau", + "invites": "Gwahoddiadau", + "mentions": "Crybwylliadau", "people": "Pobl", "rooms": "Ystafelloedd", "unread": "Heb ei ddarllen" @@ -2022,11 +2040,18 @@ "more_options": "Rhagor o Ddewisiadau", "open_room": "Agor ystafell %(roomName)s" }, + "room_options": "Dewisiadau Ystafelloedd", "show_less": "Dangos llai", + "show_message_previews": "Dangos rhagolygon negeseuon", "show_previews": "Dangos rhagolwg o negeseuon", + "sort": "Trefnu", "sort_by": "Trefnu yn ôl", "sort_by_activity": "Gweithgaredd", "sort_by_alphabet": "A-Z", + "sort_type": { + "activity": "Gweithgaredd", + "atoz": "A-Z" + }, "sort_unread_first": "Dangos ystafelloedd gyda negeseuon heb eu darllen yn gyntaf", "space_menu": { "home": "Cartref gofod", @@ -2298,6 +2323,10 @@ "recent_changes_heading": "Newidiadau diweddar sydd heb eu derbyn eto", "title": "Nid yw'r gweinydd yn ymateb" }, + "service_worker_error": { + "description": "Mae angen gweithiwr gwasanaeth ar %(brand)s i lwytho cyfryngau dilys o storfeydd cynnwys Matrix. Nid yw hyn yn cael ei gefnogi gan eich porwr felly efallai y byddwch yn profi bod cyfryngau'n methu â llwytho.", + "title": "Methwyd llwytho gweithiwr gwasanaeth" + }, "seshat": { "error_initialising": "Nid oedd modd cychwyn chwilio negeseuon, gwiriwch eich gosodiadau am ragor o wybodaeth", "reset_button": "Ailosod storfa digwyddiad", @@ -2391,6 +2420,8 @@ "session_key": "Allwedd sesiwn:", "title": "Uwch" }, + "confirm_key_storage_off": "Ydych chi'n siŵr eich bod chi eisiau cadw storfa allweddi wedi'i diffodd?", + "confirm_key_storage_off_description": "Os byddwch chi'n allgofnodi o'ch holl ddyfeisiau byddwch chi'n colli'ch hanes negeseuon a bydd angen i chi wirio'ch holl gysylltiadau presennol eto. Dysgu mwy", "delete_key_storage": { "breadcrumb_page": "Dileu storfa allweddi", "confirm": "Dileu storfa allweddi", @@ -2476,6 +2507,7 @@ "discovery_needs_terms_title": "Gadewch i bobl ddod o hyd i chi", "display_name": "Enw Dangos", "display_name_error": "Methu gosod enw dangos", + "email_adding_unsupported_by_hs": "Nid yw'r gweinydd cartref hwn yn cefnogi ychwanegu cyfeiriadau e-bost at eich cyfrif.", "email_address_in_use": "Mae'r cyfeiriad e-bost hwn eisoes yn cael ei ddefnyddio", "email_address_label": "Cyfeiriad e-bost", "email_not_verified": "Nid yw eich cyfeiriad e-bost wedi'i wirio eto", @@ -2500,7 +2532,9 @@ "error_share_msisdn_discovery": "Methu rhannu rhif ffôn", "identity_server_no_token": "Heb ganfod docyn mynediad adnabod", "identity_server_not_set": "Heb osod gweinydd hunaniaeth", + "invalid_phone_number": "Nid yw'n ymddangos bod y rhif ffôn a ddarparwyd yn ddilys.", "language_section": "Iaith", + "msisdn_adding_unsupported_by_hs": "Nid yw'r gweinydd cartref hwn yn cefnogi ychwanegu rhifau ffôn at eich cyfrif.", "msisdn_in_use": "Mae'r rhif ffôn hwn eisoes yn cael ei ddefnyddio", "msisdn_label": "Rhif Ffôn", "msisdn_verification_field_label": "Cod dilysu", @@ -2519,12 +2553,10 @@ "unable_to_load_msisdns": "Methu llwytho rhifau ffôn", "username": "Enw defnyddiwr" }, - "image_thumbnails": "Dangos rhagolygon / lluniau bach ar gyfer delweddau", "inline_url_previews_default": "Galluogi rhagolygon URL mewnol fel rhagosodiad", "inline_url_previews_room": "Galluogi rhagolygon URL fel rhagosodiad ar gyfer cyfranogwyr yn yr ystafell hon", "inline_url_previews_room_account": "Galluogi rhagolygon URL ar gyfer yr ystafell hon (yn effeithio arnoch chi yn unig)", "insert_trailing_colon_mentions": "Mewnosod colon sy'n llusgo ar ôl i'r defnyddiwr sôn amdano ar ddechrau neges", - "invite_avatars": "Dangos afatarau ystafelloedd rydych wedi'ch gwahodd iddyn nhw", "jump_to_bottom_on_send": "Symud i waelod y llinell amser pan fyddwch chi'n anfon neges", "key_backup": { "backup_in_progress": "Mae copi wrth gefn o'ch allweddi (gallai'r copi wrth gefn cyntaf gymryd ychydig funudau).", @@ -2583,6 +2615,14 @@ "labs_mjolnir": { "dialog_title": "Gosodiadau: Defnyddwyr wedi'u Hanwybyddu" }, + "media_preview": { + "hide_avatars": "Cuddio afatarau o'r ystafell a'r gwahoddwr", + "hide_media": "Cuddio bob tro", + "media_preview_description": "Mae modd dangos cyfrwng cudd trwy dapio arno", + "media_preview_label": "Dangos cyfryngau mewn llinell amser", + "show_in_private": "Mewn ystafelloedd preifat", + "show_media": "Dangos bob tro" + }, "notifications": { "default_setting_description": "Bydd y gosodiad hwn yn cael ei osod fel rhagosodiad i'ch holl ystafelloedd.", "default_setting_section": "Rwyf am gael fy hysbysu am (Gosodiad Rhagosodedig)", @@ -3026,7 +3066,7 @@ "heading_without_query": "Chwilio am", "join_button_text": "Ymuno â %(roomAddress)s", "keyboard_scroll_hint": "Defnydd i sgrolio", - "message_search_section_title": "Chwiliadau eraill", + "messages_label": "Negeseuon", "other_rooms_in_space": "Ystafelloedd eraill yn %(spaceName)s", "public_rooms_label": "Ystafelloedd cyhoeddus", "public_spaces_label": "Gofodau cyhoeddus", @@ -3036,7 +3076,6 @@ "result_may_be_hidden_privacy_warning": "Gall rhai canlyniadau gael eu cuddio er preifatrwydd", "result_may_be_hidden_warning": "Efallai y bydd rhai canlyniadau wedi'u cuddio", "search_dialog": "Deialog Chwilio", - "search_messages_hint": "I chwilio negeseuon, edrychwch am yr eicon hwn ar frig ystafell", "spaces_title": "Gofodau rydych chi ynddyn nhw", "start_group_chat_button": "Dechreuwch sgwrs grŵp" }, @@ -3081,9 +3120,7 @@ "threads_activity_centre": { "header": "Gweithgaredd edafedd", "no_rooms_with_threads_notifs": "Nid oes gennych chi ystafelloedd gyda hysbysiadau edafedd eto.", - "no_rooms_with_unread_threads": "Nid oes gennych chi ystafelloedd ag edafedd heb eu darllen eto.", - "release_announcement_description": "Mae hysbysiadau edafedd wedi symud, dewch o hyd iddyn nhw yma o hyn ymlaen.", - "release_announcement_header": "Canolfan Gweithgareddau Edafedd" + "no_rooms_with_unread_threads": "Nid oes gennych chi ystafelloedd ag edafedd heb eu darllen eto." }, "time": { "about_day_ago": "tua diwrnod yn ôl", @@ -3432,6 +3469,7 @@ "description": "I greu dolen rhannu, mae angen i chi ganiatáu i westeion ymuno â'r ystafell hon. Gall hyn wneud yr ystafell yn llai diogel. Pan fyddwch wedi gorffen gyda'r alwad, gallwch wneud yr ystafell yn breifat eto.", "dont_change_description": "Fel arall, gallwch gynnal yr alwad mewn ystafell ar wahân.", "no_change": "Dydw i ddim eisiau newid y lefel mynediad.", + "revert_access_description": "(Mae modddychwelyd hwn i'r gwerth blaenorol yng Ngosodiadau'r Ystafell: Diogelwch a Phreifatrwydd / Mynediad )", "title": "Newid lefel mynediad yr ystafell" }, "upload_failed_generic": "Methodd y ffeil '%(fileName)s' â llwytho i fyny.", diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index becdaeb381..e1de098662 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -157,6 +157,7 @@ "view_message": "Nachricht anzeigen", "view_source": "Rohdaten anzeigen", "yes": "Ja", + "yes_dismiss": "Ja, verwerfen", "zoom_in": "Vergrößern", "zoom_out": "Verkleinern" }, @@ -386,6 +387,7 @@ "fallback_button": "Authentifizierung beginnen", "mas_cross_signing_reset_cta": "Gehen Sie zu Ihren Konto", "mas_cross_signing_reset_description": "Setzen Sie Ihre Identität über Ihren Kontoanbieter zurück. Kommen Sie dann zurück und klicken Sie auf „Wiederholen“.", + "mas_cross_signing_reset_title": "Gehen sie zu ihrem Konto, um ihre Identität zurückzusetzen", "msisdn": "Eine Textnachricht wurde an %(msisdn)s gesendet", "msisdn_token_incorrect": "Token fehlerhaft", "msisdn_token_prompt": "Bitte gib den darin enthaltenen Code ein:", @@ -525,6 +527,7 @@ "message_timestamp_invalid": "Ungültiger Zeitstempel", "microphone": "Mikrofon", "model": "Modell", + "moderation_and_safety": "Moderation und Sicherheit", "modern": "Modern", "mute": "Stummschalten", "n_members": { @@ -781,7 +784,7 @@ "cross_signing_public_keys_on_device_status": "Überkreuzsignierung öffentlicher Schlüssel:", "cross_signing_ready": "Kreuzsignatur ist einsatzbereit.", "cross_signing_status": "Status der Kreuzsignatur", - "cross_signing_untrusted": "Ihr Konto verfügt über eine Cross-Signing-Identität im geheimen Speicher, diese wird von dieser Sitzung jedoch noch nicht als vertrauenswürdig eingestuft.", + "cross_signing_untrusted": "Ihr Konto hat eine Cross-Signing-Identität im sicheren Speicher, der von dieser Sitzung jedoch noch nicht als vertrauenswürdig eingestuft wird.", "crypto_not_available": "Das kryptografische Modul ist nicht verfügbar", "key_backup_active_version": "Aktive Backup Version:", "key_backup_active_version_none": "Keine", @@ -908,22 +911,11 @@ "empty_room_was_name": "Leerer Raum (war %(oldName)s)", "encryption": { "access_secret_storage_dialog": { - "enter_phrase_or_key_prompt": "Um fortzufahren, geben Sie Ihre Sicherheitsphrase ein oder , um fortzufahren.", "key_validation_text": { - "invalid_security_key": "Ungültiger Wiederherstellungsschlüssel", - "recovery_key_is_correct": "Sieht gut aus!", - "wrong_file_type": "Falscher Dateityp", "wrong_security_key": "Falscher Wiederherstellungsschlüssel" }, - "reset_title": "Alles zurücksetzen", - "reset_warning_1": "Verwende es nur, wenn du kein Gerät, mit dem du dich verifizieren kannst, bei dir hast.", - "reset_warning_2": "Wenn du alles zurücksetzt, beginnst du ohne verifizierte Sitzungen und Benutzende von Neuem und siehst eventuell keine alten Nachrichten.", "restoring": "Schlüssel aus der Sicherung wiederherstellen", - "security_key_title": "Wiederherstellungsschlüssel", - "security_phrase_incorrect_error": "Zugriff auf sicheren Speicher nicht möglich. Bitte überprüfe, ob du die richtige Sicherheitsphrase eingegeben hast.", - "security_phrase_title": "Sicherheitsphrase", - "separator": "%(securityKey)s oder %(recoveryFile)s", - "use_security_key_prompt": "Verwenden Sie Ihren Wiederherstellungsschlüssel, um fortzufahren." + "security_key_title": "Wiederherstellungsschlüssel" }, "bootstrap_title": "Schlüssel werden eingerichtet", "cancel_entering_passphrase_description": "Bist du sicher, dass du die Eingabe der Passphrase abbrechen möchtest?", @@ -979,6 +971,8 @@ "setup_secure_backup": { "explainer": "Um deine Schlüssel nicht zu verlieren, musst du sie vor der Abmeldung sichern." }, + "turn_on_key_storage": "Schlüsselspeicher einschalten", + "turn_on_key_storage_description": "Die kryptografische Identität und die Schlüssel deiner Nachrichten sicher auf dem Server speichern. So kann der Nachrichtenverlauf auf allen neuen Geräten angezeigt werden.", "udd": { "interactive_verification_button": "Interaktiv per Emoji verifizieren", "other_ask_verify_text": "Bitte diesen Nutzer, seine Sitzung zu verifizieren, oder verifiziere diese unten manuell.", @@ -992,7 +986,6 @@ "accepting": "Annehmen…", "after_new_login": { "device_verified": "Gerät verifiziert", - "reset_confirmation": "Willst du deine Verifizierungsschlüssel wirklich zurücksetzen?", "skip_verification": "Verifizierung vorläufig überspringen", "unable_to_verify": "Gerät konnte nicht verifiziert werden", "verify_this_device": "Dieses Gerät verifizieren" @@ -1063,8 +1056,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_reset_warning_1": "Das Zurücksetzen deiner Sicherheitsschlüssel kann nicht rückgängig gemacht werden. Nach dem Zurücksetzen wirst du alte Nachrichten nicht mehr lesen können und Freunde, die dich vorher verifiziert haben werden Sicherheitswarnungen bekommen, bis du dich erneut mit ihnen verifizierst.", - "verify_reset_warning_2": "Bitte fahren Sie nur fort, wenn Sie sicher sind, dass Sie Ihren Zugang zu allen anderen Geräte und Ihren Wiederherstellungsschlüssel verloren haben.", "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", @@ -1973,7 +1964,7 @@ }, "room_is_public": "Dieser Raum ist öffentlich" }, - "header_avatar_open_settings_label": "Chatroomeinstellungen öffnen", + "header_avatar_open_settings_label": "Raumeinstellungen öffnen", "header_face_pile_tooltip": "Personen", "header_untrusted_label": "Nicht vertrauenswürdig", "inaccessible": "Dieser Raum oder Space ist im Moment nicht zugänglich.", @@ -2100,6 +2091,7 @@ "room_list": { "add_room_label": "Raum hinzufügen", "add_space_label": "Space hinzufügen", + "appearance": "Erscheinungsbild", "breadcrumbs_empty": "Keine kürzlich besuchten Räume", "breadcrumbs_label": "Kürzlich besuchte Räume", "empty": { @@ -2108,11 +2100,14 @@ "no_chats_description_no_room_rights": "Beginnen Sie damit, jemandem eine Nachricht zu senden", "no_favourites": "Sie haben noch keinen Lieblingschat", "no_favourites_description": "Sie können einen Chat in den Chat-Einstellungen zu Ihren Favoriten hinzufügen", + "no_invites": "Sie haben keine ungelesenen Einladungen", + "no_mentions": "Sie haben keine ungelesenen Erwähnungen", "no_people": "Sie haben noch keine direkten Chats", "no_people_description": "Sie können Filter deaktivieren, um Ihre anderen Chats anzuzeigen", "no_rooms": "Sie sind noch in keinem Raum", "no_rooms_description": "Sie können Filter deaktivieren, um Ihre anderen Chats anzuzeigen", "no_unread": "Glückwunsch! Sie haben keine ungelesenen Nachrichten", + "show_activity": "Alle Aktivitäten ansehen", "show_chats": "Alle Chats anzeigen" }, "failed_add_tag": "Fehler beim Hinzufügen des \"%(tagName)s\"-Tags an dem Raum", @@ -2120,6 +2115,8 @@ "failed_set_dm_tag": "Fehler beim Setzen der Nachrichtenmarkierung", "filters": { "favourite": "Favoriten", + "invites": "Einladungen", + "mentions": "Erwähnungen", "people": "Personen", "rooms": "Räume", "unread": "Ungelesene" @@ -2150,15 +2147,22 @@ "more_options": "Weitere Optionen", "open_room": "Raum öffnen %(roomName)s" }, + "room_options": "Chatroomoptionen", "show_less": "Weniger anzeigen", + "show_message_previews": "Nachrichtenvorschau anzeigen", "show_n_more": { "other": "%(count)s weitere anzeigen", "one": "%(count)s weitere anzeigen" }, "show_previews": "Nachrichtenvorschau anzeigen", + "sort": "Sortieren", "sort_by": "Sortieren nach", "sort_by_activity": "Aktivität", "sort_by_alphabet": "A–Z", + "sort_type": { + "activity": "Aktivität", + "atoz": "A–Z" + }, "sort_unread_first": "Räume mit ungelesenen Nachrichten zuerst zeigen", "space_menu": { "home": "Space-Übersicht", @@ -2446,6 +2450,10 @@ "recent_changes_heading": "Letzte Änderungen, die noch nicht eingegangen sind", "title": "Server reagiert nicht" }, + "service_worker_error": { + "description": "%(brand)s benötigt einen Service Worker zum Laden authentifizierter Medien aus Matrix-Inhaltsrepositorys. Dies wird von Ihrem Browser nicht unterstützt, sodass Medien möglicherweise nicht geladen werden können.", + "title": "Service Worker konnte nicht geladen werden" + }, "seshat": { "error_initialising": "Initialisierung der Suche fehlgeschlagen, für weitere Informationen öffne deine Einstellungen", "reset_button": "Ereignisspeicher zurück setzen", @@ -2539,6 +2547,8 @@ "session_key": "Sitzungsschlüssel:", "title": "Advanced" }, + "confirm_key_storage_off": "Soll die Speicherung der Schlüssel wirklich deaktiviert sein?", + "confirm_key_storage_off_description": "Wenn Sie sich von all Ihren Geräten abmelden, verlieren Sie Ihren Nachrichtenverlauf und müssen alle Ihre vorhandenen Kontakte erneut überprüfen. Erfahre mehr ", "delete_key_storage": { "breadcrumb_page": "Schlüsselspeicher löschen", "confirm": "Schlüsselspeicher löschen", @@ -2624,6 +2634,7 @@ "discovery_needs_terms_title": "Lassen Sie sich von anderen finden", "display_name": "Anzeigename", "display_name_error": "Anzeigename konnte nicht gesetzt werden", + "email_adding_unsupported_by_hs": "Dieser Homeserver unterstützt das Hinzufügen von E-Mail-Adressen zu Ihrem Konto nicht.", "email_address_in_use": "Diese E-Mail-Adresse wird bereits verwendet", "email_address_label": "E-Mail-Adresse", "email_not_verified": "Deine E-Mail-Adresse wurde noch nicht verifiziert", @@ -2648,7 +2659,9 @@ "error_share_msisdn_discovery": "Teilen der Telefonnummer nicht möglich", "identity_server_no_token": "Kein Identitäts-Zugangs-Token gefunden", "identity_server_not_set": "Kein Identitäts-Server festgelegt", + "invalid_phone_number": "Die angegebene Telefonnummer scheint nicht gültig zu sein.", "language_section": "Sprache", + "msisdn_adding_unsupported_by_hs": "Dieser Homeserver unterstützt das Hinzufügen von Telefonnummern zu Ihrem Konto nicht.", "msisdn_in_use": "Diese Telefonnummer wird bereits verwendet", "msisdn_label": "Telefonnummer", "msisdn_verification_field_label": "Bestätigungscode", @@ -2667,12 +2680,10 @@ "unable_to_load_msisdns": "Telefonnummern können nicht geladen werden", "username": "Benutzername" }, - "image_thumbnails": "Vorschauen für Bilder", "inline_url_previews_default": "URL-Vorschau standardmäßig aktivieren", "inline_url_previews_room": "URL-Vorschau für Raummitglieder", "inline_url_previews_room_account": "URL-Vorschau für dich in diesem Raum", "insert_trailing_colon_mentions": "Doppelpunkt nach Erwähnungen einfügen", - "invite_avatars": "Zeigen Sie Avatare von Chatrooms an, zu denen Sie eingeladen wurden", "jump_to_bottom_on_send": "Nach Senden einer Nachricht im Verlauf nach unten springen", "key_backup": { "backup_in_progress": "Deine Schlüssel werden gesichert (Das erste Backup könnte ein paar Minuten in Anspruch nehmen).", @@ -2731,6 +2742,14 @@ "labs_mjolnir": { "dialog_title": "Einstellungen: Blockierte Benutzer" }, + "media_preview": { + "hide_avatars": "Verstecken Sie die Avatare von Chatroom und Einladung", + "hide_media": "Verstecke Sie sich immer", + "media_preview_description": "Ein verstecktes Medium kann immer angezeigt werden, indem Sie darauf tippen", + "media_preview_label": "Medien in der Zeitleiste anzeigen", + "show_in_private": "In privaten Chatrooms", + "show_media": "Immer anzeigen" + }, "notifications": { "default_setting_description": "Diese Einstellung wird standardmäßig für all deine Räume übernommen.", "default_setting_section": "Ich möchte benachrichtigt werden für (Standardeinstellung)", @@ -3214,7 +3233,7 @@ "heading_without_query": "Suche nach", "join_button_text": "%(roomAddress)s betreten", "keyboard_scroll_hint": "Benutze zum Scrollen", - "message_search_section_title": "Andere Suchen", + "messages_label": "Nachrichten", "other_rooms_in_space": "Andere Räume in %(spaceName)s", "public_rooms_label": "Öffentliche Räume", "public_spaces_label": "Öffentliche Spaces", @@ -3224,7 +3243,6 @@ "result_may_be_hidden_privacy_warning": "Einige Vorschläge könnten aus Gründen der Privatsphäre ausgeblendet sein", "result_may_be_hidden_warning": "Einige Ergebnisse können ausgeblendet sein", "search_dialog": "Suchdialog", - "search_messages_hint": "Wenn du Nachrichten durchsuchen willst, klicke auf das Icon oberhalb des Raumes", "spaces_title": "Spaces, in denen du Mitglied bist", "start_group_chat_button": "Gruppenunterhaltung beginnen" }, @@ -3273,9 +3291,7 @@ "threads_activity_centre": { "header": "Thread-Aktivität", "no_rooms_with_threads_notifs": "Sie haben noch keine Chatrooms mit Thread-Benachrichtigungen.", - "no_rooms_with_unread_threads": "Sie haben noch keine Chatrooms mit ungelesenen Threads.", - "release_announcement_description": "Die Thread-Benachrichtigungen wurden verschoben. Sie finden sie ab sofort hier.", - "release_announcement_header": "Thread Aktivitätszentrum" + "no_rooms_with_unread_threads": "Sie haben noch keine Chatrooms mit ungelesenen Threads." }, "time": { "about_day_ago": "vor etwa einem Tag", @@ -3786,10 +3802,11 @@ "unavailable": "Nicht verfügbar" }, "update_room_access_modal": { - "description": "Um einen Link zum Teilen zu erstellen, müssen Sie Gästen erlauben, diesem Chatroom beizutreten. Dadurch kann der Chatroom weniger sicher werden. Wenn Sie mit dem Anruf fertig sind, können Sie den Raum wieder privat machen.", - "dont_change_description": "Sie können den Anruf auch in einem separaten Chatroom führen.", + "description": "Um einen Link zum Teilen zu erstellen, mache diesen Chatroom öffentlich oder erlaube Beitrittsanfragen. Auf diese Weise können Gäste beitreten, ohne eingeladen zu werden.", + "dont_change_description": "Andernfalls, wenn Sie den Zugang dieses Raumes nicht anpassen möchten, können sie einen neuen Raum für den Anruflink erstellen.", "no_change": "Ich möchte die Zugriffsebene nicht ändern.", - "title": "Ändern Sie die Zugriffsebene des Chatrooms." + "revert_access_description": "(Dies kann in den Raumeinstellungen auf den vorherigen Wert zurückgesetzt werden: Sicherheit & Datenschutz/Zutritt)", + "title": "Gastbenutzern erlauben, diesem Raum beizutreten" }, "upload_failed_generic": "Die Datei „%(fileName)s“ konnte nicht hochgeladen werden.", "upload_failed_size": "Die Datei „%(fileName)s“ überschreitet das Hochladelimit deines Heim-Servers", diff --git a/src/i18n/strings/el.json b/src/i18n/strings/el.json index 7270df46c2..662d56b6d5 100644 --- a/src/i18n/strings/el.json +++ b/src/i18n/strings/el.json @@ -9,15 +9,16 @@ "one": "1 μη αναγνωσμένη αναφορά.", "other": "%(count)s μη αναγνωσμένα μηνύματα συμπεριλαμβανομένων των αναφορών." }, - "room_name": "Δωμάτιο %(name)s", + "recent_rooms": "Πρόσφατες αίθουσες", + "room_name": "Αίθουσα %(name)s", "unread_messages": "Μη αναγνωσμένα μηνύματα.", "user_menu": "Μενού χρήστη" }, - "a11y_jump_first_unread_room": "Μετάβαση στο πρώτο μη αναγνωσμένο δωμάτιο.", + "a11y_jump_first_unread_room": "Μετάβαση στην πρώτη μη αναγνωσμένη αίθουσα.", "action": { "accept": "Αποδοχή", "add": "Προσθήκη", - "add_existing_room": "Προσθήκη υπάρχοντος δωματίου", + "add_existing_room": "Προσθήκη υπάρχουσας αίθουσας", "add_people": "Προσθήκη ατόμων", "apply": "Εφαρμογή", "approve": "Έγκριση", @@ -37,7 +38,7 @@ "copy": "Αντιγραφή", "copy_link": "Αντιγραφή συνδέσμου", "create": "Δημιουργία", - "create_a_room": "Δημιουργήστε ένα δωμάτιο", + "create_a_room": "Δημιουργήστε μία αίθουσα", "create_account": "Δημιουργία Λογαριασμού", "decline": "Απόρριψη", "delete": "Διαγραφή", @@ -50,8 +51,8 @@ "edit": "Επεξεργασία", "enable": "Ενεργοποίηση", "expand": "Επέκταση", - "explore_public_rooms": "Εξερευνήστε δημόσια δωμάτια", - "explore_rooms": "Εξερευνήστε δωμάτια", + "explore_public_rooms": "Εξερευνήστε δημόσιες αίθουσες", + "explore_rooms": "Εξερευνήστε αίθουσες", "export": "Εξαγωγή", "forward": "Προώθηση", "go": "Μετάβαση", @@ -67,14 +68,14 @@ "join": "Συμμετοχή", "learn_more": "Μάθετε περισσότερα", "leave": "Αποχώρηση", - "leave_room": "Αποχώρηση από το δωμάτιο", + "leave_room": "Αποχώρηση από την αίθουσα", "logout": "Αποσύνδεση", "manage": "Διαχειριστείτε", "maximise": "Μεγιστοποίηση", "mention": "Αναφορά", "minimise": "Ελαχιστοποίηση", - "new_room": "Νέο δωμάτιο", - "new_video_room": "Νέο δωμάτιο βίντεο", + "new_room": "Νέα αίθουσα", + "new_video_room": "Νέα αίθουσα βίντεο", "next": "Επόμενο", "no": "Όχι", "ok": "Εντάξει", @@ -206,7 +207,7 @@ }, "misconfigured_body": "Ζητήστε από τον %(brand)s διαχειριστή σας να ελέγξει τις ρυθμίσεις σας για λανθασμένες ή διπλότυπες καταχωρίσεις.", "misconfigured_title": "Οι παράμετροι του %(brand)s σας είναι λανθασμένα ρυθμισμένοι", - "msisdn_field_description": "Άλλοι χρήστες μπορούν να σας προσκαλέσουν σε δωμάτια χρησιμοποιώντας τα στοιχεία επικοινωνίας σας", + "msisdn_field_description": "Άλλοι χρήστες μπορούν να σας προσκαλέσουν σε αίθουσες χρησιμοποιώντας τα στοιχεία επικοινωνίας σας", "msisdn_field_label": "Τηλέφωνο", "msisdn_field_number_invalid": "Αυτός ο αριθμός τηλεφώνου δε φαίνεται σωστός, ελέγξτε και δοκιμάστε ξανά", "msisdn_field_required_invalid": "Εισάγετε αριθμό τηλεφώνου", @@ -281,7 +282,7 @@ "sso_complete_in_browser_dialog_title": "Μεταβείτε στο πρόγραμμα περιήγησής σας για να ολοκληρώσετε τη σύνδεση", "sso_failed_missing_storage": "Ζητήσαμε από το πρόγραμμα περιήγησης να θυμάται τον διακομιστή που χρησιμοποιείτε για να συνδέεστε, αλλά το πρόγραμμα περιήγησης δεν το έχει αποθηκεύσει. Πηγαίνετε στην σελίδα σύνδεσεις για να προσπαθήσετε ξανά.", "sso_or_username_password": "%(ssoButtons)s Ή %(usernamePassword)s", - "sync_footer_subtitle": "Εάν έχετε συμμετάσχει σε πολλά δωμάτια, αυτό μπορεί να διαρκέσει λίγο", + "sync_footer_subtitle": "Εάν έχετε συμμετάσχει σε πολλές αίθουσες, αυτό μπορεί να διαρκέσει λίγο", "uia": { "code": "Κωδικός", "email_auth_header": "Ελέγξτε το email σας για να συνεχίσετε", @@ -306,12 +307,12 @@ "username_in_use": "Κάποιος έχει ήδη αυτό το όνομα χρήστη, δοκιμάστε ένα άλλο." }, "bug_reporting": { - "additional_context": "Εάν υπάρχουν πρόσθετες ππληροφορίες που θα βοηθούσαν στην ανάλυση του ζητήματος, όπως τι κάνατε εκείνη τη στιγμή, αναγνωριστικά δωματίων, αναγνωριστικά χρηστών κ.λπ., συμπεριλάβετε τα εδώ.", + "additional_context": "Εάν υπάρχουν πρόσθετες πληροφορίες που θα βοηθούσαν στην ανάλυση του ζητήματος, όπως τι κάνατε εκείνη τη στιγμή, αναγνωριστικά αιθουσών, αναγνωριστικά χρηστών κ.λπ., συμπεριλάβετέ τα εδώ.", "before_submitting": "Προτού υποβάλετε αρχεία καταγραφής, πρέπει να δημιουργήσετε ένα ζήτημα GitHub για να περιγράψετε το πρόβλημά σας.", "collecting_information": "Συγκέντρωση πληροφοριών σχετικά με την έκδοση της εφαρμογής", "collecting_logs": "Συγκέντρωση πληροφοριών", "create_new_issue": "Παρακαλούμε δημιουργήστε ένα νέο issue στο GitHub ώστε να μπορέσουμε να διερευνήσουμε αυτό το σφάλμα.", - "description": "Τα αρχεία καταγραφής εντοπισμού σφαλμάτων περιέχουν δεδομένα χρήσης εφαρμογών, συμπεριλαμβανομένου του ονόματος χρήστη σας, των αναγνωριστικών ή των ψευδωνύμων των δωματίων που έχετε επισκεφτεί, των στοιχείων διεπαφής χρήστη με τα οποία αλληλεπιδράσατε τελευταία και των ονομάτων χρήστη άλλων χρηστών. Δεν περιέχουν μηνύματα.", + "description": "Τα αρχεία καταγραφής εντοπισμού σφαλμάτων περιέχουν δεδομένα χρήσης εφαρμογών, συμπεριλαμβανομένου του ονόματος χρήστη σας, των αναγνωριστικών ή των ψευδωνύμων των αιθουσών που έχετε επισκεφτεί, των στοιχείων διεπαφής χρήστη με τα οποία αλληλεπιδράσατε τελευταία και των ονομάτων χρήστη άλλων χρηστών. Δεν περιέχουν μηνύματα.", "download_logs": "Λήψη αρχείων καταγραφής", "downloading_logs": "Λήψη αρχείων καταγραφής", "error_empty": "Πείτε μας τι πήγε στραβά ή, καλύτερα, δημιουργήστε ένα ζήτημα στο GitHub που να περιγράφει το πρόβλημα.", @@ -422,7 +423,7 @@ "offline": "Εκτός σύνδεσης", "on": "Ενεργό", "options": "Επιλογές", - "orphan_rooms": "Άλλα δωμάτια", + "orphan_rooms": "Άλλες αίθουσες", "password": "Κωδικός πρόσβασης", "people": "Άτομα", "preferences": "Προτιμήσεις", @@ -430,19 +431,19 @@ "preview_message": "Είσαι ο καλύτερος!", "privacy": "Ιδιωτικότητα", "private": "Ιδιωτικό", - "private_room": "Ιδιωτικό δωμάτιο", + "private_room": "Ιδιωτική αίθουσα", "private_space": "Ιδιωτικός χώρος", "profile": "Προφίλ", "public": "Δημόσιο", - "public_room": "Δημόσιο δωμάτιο", + "public_room": "Δημόσια αίθουσα", "public_space": "Δημόσιος χώρος", "qr_code": "Κωδικός QR", "random": "Τυχαία", "reactions": "Αντιδράσεις", "report_a_bug": "Αναφορά σφάλματος", - "room": "Δωμάτιο", - "room_name": "Όνομα δωματίου", - "rooms": "Δωμάτια", + "room": "Αίθουσα", + "room_name": "Όνομα αίθουσας", + "rooms": "Αίθουσες", "secure_backup": "Ασφαλές αντίγραφο ασφαλείας", "select_all": "Επιλογή όλων", "server": "Διακομιστής", @@ -461,10 +462,10 @@ "theme": "Θέμα", "thread": "Νήμα", "threads": "Νήμα εκτέλεσης", - "timeline": "Χρονοδιάγραμμα", + "timeline": "Χρονολόγιο", "unencrypted": "Μη κρυπτογραφημένο", "unmute": "Άρση σίγασης", - "unnamed_room": "Ανώνυμο δωμάτιο", + "unnamed_room": "Ανώνυμη αίθουσα", "unnamed_space": "Χώρος χωρίς όνομα", "unverified": "Μη επαληθευμένη", "user": "Χρήστης", @@ -474,13 +475,13 @@ "verified": "Επαληθευμένη", "version": "Έκδοση", "video": "Βίντεο", - "video_room": "Δωμάτια βίντεο", + "video_room": "Αίθουσα βίντεο", "view_message": "Προβολή μηνύματος", "warning": "Προειδοποίηση" }, "composer": { "autocomplete": { - "@room_description": "Ειδοποιήστε όλο το δωμάτιο", + "@room_description": "Ειδοποιήστε όλη την αίθουσα", "command_a11y": "Αυτόματη συμπλήρωση εντολών", "command_description": "Εντολές", "emoji_a11y": "Αυτόματη συμπλήρωση Emoji", @@ -542,7 +543,7 @@ "join_rule_public_label": "Οποιοσδήποτε θα μπορεί να βρει και να εγγραφεί σε αυτό το δωμάτιο.", "join_rule_public_parent_space_label": "Οποιοσδήποτε θα μπορεί να βρει και να εγγραφεί σε αυτόν τον χώρο, όχι μόνο μέλη του .", "join_rule_restricted": "Ορατό στα μέλη του χώρου", - "join_rule_restricted_label": "Όλοι στο θα μπορούν να βρουν και να συμμετάσχουν σε αυτό το δωμάτιο.", + "join_rule_restricted_label": "Όλοι στο θα μπορούν να βρουν και να συμμετάσχουν σε αυτήν την αίθουσα.", "name_validation_required": "Εισάγετε ένα όνομα για το δωμάτιο", "room_visibility_label": "Ορατότητα δωματίου", "title_private_room": "Δημιουργήστε ένα ιδιωτικό δωμάτιο", @@ -628,15 +629,15 @@ "id": "ID: ", "invalid_json": "Δε μοιάζει με έγκυρο JSON.", "level": "Επίπεδο", - "main_timeline": "Κύριο χρονοδιάγραμμα", + "main_timeline": "Κύριο χρονολόγιο", "no_receipt_found": "Δεν βρέθηκε απόδειξη", "notification_state": "Η κατάσταση ειδοποίησης είναι %(notificationState)s", "notifications_debug": "Αποσφαλμάτωση ειδοποιήσεων", "number_of_users": "Αριθμός χρηστών", "original_event_source": "Αρχική πηγή συμβάντος", - "room_encrypted": "Το δωμάτιο είναι κρυπτογραφημένο ✅", + "room_encrypted": "Η αίθουσα είναι κρυπτογραφημένη ✅", "room_id": "ID δωματίου: %(roomId)s", - "room_not_encrypted": "Το δωμάτιο δεν είναι κρυπτογραφημένο 🚨", + "room_not_encrypted": "Η αίθουσα δεν είναι κρυπτογραφημένη 🚨", "room_notifications_dot": "Σημείο: ", "room_notifications_highlight": "Αποκορύφωμα: ", "room_notifications_last_event": "Τελευταίο γεγονός:", @@ -663,14 +664,14 @@ "setting_definition": "Ορισμός ρύθμισης:", "setting_id": "Ρύθμιση αναγνωριστικού", "settings_explorer": "Εξερεύνηση ρυθμίσεων", - "show_hidden_events": "Εμφάνιση κρυφών συμβάντων στη γραμμή χρόνου", + "show_hidden_events": "Εμφάνιση κρυφών συμβάντων στο χρονολόγιο", "spaces": { "one": "<χώρος>", "other": "<%(count)s χώροι>" }, "state_key": "Κλειδί κατάστασης", "thread_root_id": "Thread Root ID: %(threadRootId)s", - "threads_timeline": "Χρονοδιάγραμμα νημάτων", + "threads_timeline": "Χρονολόγιο νημάτων", "title": "Εργαλεία προγραμματιστή", "toggle_event": "μεταβολή συμβάντος", "toolbox": "Εργαλειοθήκη", @@ -681,12 +682,12 @@ "user_read_up_to_private_ignore_synthetic": "Ο χρήστης διάβασε ως (m.read.private;ignoreSynthetic): ", "value": "Τιμή", "value_colon": "Τιμή:", - "value_in_this_room": "Τιμή σε αυτό το δωμάτιο", - "value_this_room_colon": "Τιμή σε αυτό το δωμάτιο:", + "value_in_this_room": "Τιμή σε αυτήν την αίθουσα", + "value_this_room_colon": "Τιμή σε αυτήν την αίθουσα:", "values_explicit": "Αξίες σε σαφής επίπεδα", "values_explicit_colon": "Τιμές σε σαφή επίπεδα:", "values_explicit_room": "Αξίες σε σαφής επίπεδα σε αυτό το δωμάτιο", - "values_explicit_this_room_colon": "Τιμές σε σαφή επίπεδα σε αυτό το δωμάτιο:", + "values_explicit_this_room_colon": "Τιμές σε ρητά επίπεδα σε αυτήν την αίθουσα:", "view_servers_in_room": "Προβολή διακομιστών στο δωμάτιο", "view_source_decrypted_event_source": "Αποκρυπτογραφημένη πηγή συμβάντος", "widget_screenshots": "Ενεργοποίηση στιγμιότυπων οθόνης μικροεφαρμογών σε υποστηριζόμενες μικροεφαρμογές" @@ -712,21 +713,11 @@ "empty_room": "Άδειο δωμάτιο", "encryption": { "access_secret_storage_dialog": { - "enter_phrase_or_key_prompt": "Εισαγάγετε τη Φράση Ασφαλείας ή για να συνεχίσετε.", "key_validation_text": { - "invalid_security_key": "Μη έγκυρο Κλειδί Ασφαλείας", - "recovery_key_is_correct": "Φαίνεται καλό!", - "wrong_file_type": "Λάθος τύπος αρχείου", "wrong_security_key": "Λάθος Κλειδί Ασφαλείας" }, - "reset_title": "Επαναφορά όλων", - "reset_warning_1": "Κάντε αυτό μόνο όταν δεν έχετε άλλη συσκευή για να ολοκληρώσετε την επαλήθευση.", - "reset_warning_2": "Εάν επαναφέρετε τα πάντα, θα κάνετε επανεκκίνηση χωρίς αξιόπιστες συνεδρίες, χωρίς αξιόπιστους χρήστες και ενδέχεται να μην μπορείτε να δείτε προηγούμενα μηνύματα.", "restoring": "Επαναφορά κλειδιών από αντίγραφο ασφαλείας", - "security_key_title": "Κλειδί Ασφαλείας", - "security_phrase_incorrect_error": "Δεν είναι δυνατή η πρόσβαση στον κρυφό χώρο αποθήκευσης. Βεβαιωθείτε ότι έχετε εισαγάγει τη σωστή Φράση Ασφαλείας.", - "security_phrase_title": "Φράση Ασφαλείας", - "use_security_key_prompt": "Χρησιμοποιήστε το Κλειδί Ασφαλείας σας για να συνεχίσετε." + "security_key_title": "Κλειδί Ασφαλείας" }, "bootstrap_title": "Ρύθμιση κλειδιών", "cancel_entering_passphrase_description": "Είστε σίγουρος/η ότι θέλετε να ακυρώσετε την εισαγωγή κωδικού;", @@ -734,7 +725,7 @@ "confirm_encryption_setup_body": "Κάντε κλικ στο κουμπί παρακάτω για να επιβεβαιώσετε τη ρύθμιση της κρυπτογράφησης.", "confirm_encryption_setup_title": "Επιβεβαιώστε τη ρύθμιση κρυπτογράφησης", "cross_signing_room_normal": "Αυτό το δωμάτιο έχει κρυπτογράφηση από άκρο σε άκρο", - "cross_signing_room_verified": "Όλοι σε αυτό το δωμάτιο έχουν επαληθευτεί", + "cross_signing_room_verified": "Όλοι σε αυτήν την αίθουσα έχουν επαληθευτεί", "cross_signing_room_warning": "Κάποιος χρησιμοποιεί μια άγνωστη συνεδρία", "cross_signing_user_normal": "Δεν έχετε επαληθεύσει αυτόν τον χρήστη.", "cross_signing_user_verified": "Έχετε επαληθεύσει αυτόν τον χρήστη. Αυτός ο χρήστης έχει επαληθεύσει όλες τις συνεδρίες του.", @@ -782,7 +773,6 @@ "accepting": "Αποδοχή …", "after_new_login": { "device_verified": "Η συσκευή επαληθεύτηκε", - "reset_confirmation": "Είστε σίγουρος ότι θέλετε να επαναφέρετε τα κλειδιά επαλήθευσης;", "skip_verification": "Παράβλεψη επαλήθευσης προς το παρόν", "unable_to_verify": "Αδυναμία επαλήθευσης αυτής της συσκευής", "verify_this_device": "Επαληθεύστε αυτήν τη συσκευή" @@ -845,7 +835,6 @@ "verify_emoji_prompt": "Επαληθεύστε συγκρίνοντας μοναδικά emoji.", "verify_emoji_prompt_qr": "Εάν δεν μπορείτε να σαρώσετε τον παραπάνω κώδικα, επαληθεύστε το συγκρίνοντας μοναδικά emoji.", "verify_later": "Θα επαληθεύσω αργότερα", - "verify_reset_warning_1": "Δεν είναι δυνατή η αναίρεση της επαναφοράς των κλειδιών επαλήθευσης. Μετά την επαναφορά, δε θα έχετε πρόσβαση σε παλιά κρυπτογραφημένα μηνύματα και όλοι οι φίλοι που σας έχουν προηγουμένως επαληθεύσει θα βλέπουν προειδοποιήσεις ασφαλείας μέχρι να επαληθεύσετε ξανά μαζί τους.", "verify_using_device": "Επαλήθευση με άλλη συσκευή", "verify_using_key": "Επαλήθευση με Κλειδί ασφαλείας", "verify_using_key_or_phrase": "Επαλήθευση με Κλειδί Ασφαλείας ή Φράση Ασφαλείας", @@ -1156,13 +1145,13 @@ "page_up": "Σελίδα προς τα πάνω", "prev_room": "Προηγούμενο δωμάτιο ή ΑΜ", "prev_unread_room": "Προηγούμενο μη αναγνωσμένο δωμάτιο ή ΑΜ", - "room_list_collapse_section": "Σύμπτυξη ενότητας λίστας δωματίων", + "room_list_collapse_section": "Σύμπτυξη ενότητας λίστας αιθουσών", "room_list_expand_section": "Ανάπτυξη ενότητας λίστας δωματίων", "room_list_navigate_down": "Πλοήγηση προς τα κάτω στη λίστα δωματίων", "room_list_navigate_up": "Πλοήγηση προς τα πάνω στη λίστα δωματίων", "room_list_select_room": "Επιλέξτε δωμάτιο από τη λίστα δωματίων", - "scroll_down_timeline": "Κύλιση προς τα κάτω στη γραμμή χρόνου", - "scroll_up_timeline": "Κύλιση προς τα πάνω στη γραμμή χρόνου", + "scroll_down_timeline": "Κύλιση προς τα κάτω στο χρονολόγιο", + "scroll_up_timeline": "Κύλιση προς τα πάνω στο χρονολόγιο", "search": "Αναζήτηση (πρέπει να είναι ενεργοποιημένη)", "send_sticker": "Αποστολή αυτοκόλλητου", "shift": "Shift", @@ -1228,9 +1217,9 @@ "video_rooms_a_new_way_to_chat": "Ένας νέος τρόπος για συνομιλία μέσω φωνής και βίντεο με το %(brand)s.", "video_rooms_always_on_voip_channels": "Οι αίθουσες βίντεο είναι πάντα-ενεργά κανάλια VoIP ενσωματωμένα σε ένα δωμάτιο στο %(brand)s", "video_rooms_beta": "Οι αίθουσες βίντεο είναι μια λειτουργία beta", - "video_rooms_faq1_answer": "Χρησιμοποιήστε το κουμπί “+” στην ενότητα δωματίων του αριστερού πάνελ.", + "video_rooms_faq1_answer": "Χρησιμοποιήστε το κουμπί “+” στην ενότητα αιθουσών του αριστερού πάνελ.", "video_rooms_faq1_question": "Πώς μπορώ να δημιουργήσω ένα δωμάτιο βίντεο;", - "video_rooms_faq2_answer": "Ναι, το χρονοδιάγραμμα της συνομιλίας εμφανίζεται δίπλα στο βίντεο.", + "video_rooms_faq2_answer": "Ναι, το χρονολόγιο της συνομιλίας εμφανίζεται δίπλα στο βίντεο.", "video_rooms_faq2_question": "Μπορώ να χρησιμοποιήσω τη συνομιλία κειμένου παράλληλα με τη βιντεοκλήση;", "wysiwyg_composer": "Συντάκτης εμπλουτισμένου κειμένου" }, @@ -1249,7 +1238,7 @@ "lists_description_1": "Η εγγραφή σε μια λίστα απαγορεύσων θα σας κάνει να εγγραφείτε σε αυτήν!", "lists_description_2": "Εάν αυτό δεν είναι αυτό που θέλετε, χρησιμοποιήστε ένα διαφορετικό εργαλείο για να αγνοήσετε τους χρήστες.", "lists_heading": "Εγγεγραμμένες λίστες", - "lists_new_label": "Ταυτότητα δωματίου ή διεύθυνση της λίστας απαγορευσων", + "lists_new_label": "Ταυτότητα αίθουσας ή διεύθυνση της λίστας απαγορεύσεων", "no_lists": "Δεν είστε εγγεγραμμένοι σε καμία λίστα", "personal_empty": "Δεν έχετε αγνοήσει κανέναν.", "personal_heading": "Προσωπική λίστα απαγορεύσεων", @@ -1478,14 +1467,16 @@ "pinned_messages": { "limits": { "other": "Μπορείτε να καρφιτσώσετε μόνο έως %(count)s μικρεοεφαρμογές" - } + }, + "view": "Εμφάνιση στο χρονολόγιο" }, "pinned_messages_button": "Καρφιτσωμένο", "poll": { "final_result": { "one": "Τελικό αποτέλεσμα με βάση %(count)s ψήφο", "other": "Τελικό αποτέλεσμα με βάση %(count)s ψήφους" - } + }, + "view_in_timeline": "Εμφάνιση δημοσκόπισης στο χρονολόγιο" }, "thread_list": { "context_menu_label": "Επιλογές νήματος συζήτησης" @@ -1544,13 +1535,13 @@ "inaccessible": "Αυτό το δωμάτιο ή ο χώρος δεν είναι προσβάσιμος αυτήν τη στιγμή.", "inaccessible_name": "Το %(roomName)s δεν είναι προσβάσιμο αυτή τη στιγμή.", "inaccessible_subtitle_1": "Δοκιμάστε ξανά αργότερα ή ζητήστε από έναν διαχειριστή δωματίου ή χώρου να ελέγξει εάν έχετε πρόσβαση.", - "inaccessible_subtitle_2": "Το %(errcode)s επιστράφηκε κατά την προσπάθεια πρόσβασης στο δωμάτιο ή στο χώρο. Εάν πιστεύετε ότι βλέπετε αυτό το μήνυμα κατά λάθος, υποβάλετε μια αναφορά σφάλματος.", + "inaccessible_subtitle_2": "Το %(errcode)s επιστράφηκε κατά την προσπάθεια πρόσβασης στην αίθουσα ή στο χώρο. Εάν πιστεύετε ότι βλέπετε αυτό το μήνυμα κατά λάθος, υποβάλετε μια αναφορά σφάλματος.", "intro": { "dm_caption": "Μόνο οι δυο σας συμμετέχετε σε αυτήν τη συνομιλία, εκτός εάν κάποιος από εσάς προσκαλέσει κάποιον να συμμετάσχει.", "enable_encryption_prompt": "Ενεργοποιήστε την κρυπτογράφηση στις ρυθμίσεις.", "no_avatar_label": "Προσθέστε μια φωτογραφία, ώστε οι χρήστες να μπορούν εύκολα να εντοπίσουν το δωμάτιό σας.", "no_topic": "Προσθέστε ένα θέμα για να βοηθήσετε τους χρήστες να γνωρίζουν περί τίνος πρόκειται.", - "private_unencrypted_warning": "Τα προσωπικά σας μηνύματα είναι συνήθως κρυπτογραφημένα, αλλά αυτό το δωμάτιο δεν είναι. Συνήθως αυτό οφείλεται σε μια μη υποστηριζόμενη συσκευή ή μέθοδο που χρησιμοποιείται, όπως προσκλήσεις μέσω email.", + "private_unencrypted_warning": "Τα προσωπικά σας μηνύματα είναι συνήθως κρυπτογραφημένα, αλλά αυτή η αίθουσα δεν είναι. Συνήθως αυτό οφείλεται σε μια μη υποστηριζόμενη συσκευή ή μέθοδο που χρησιμοποιείται, όπως προσκλήσεις μέσω email.", "room_invite": "Προσκαλέστε μόνο σε αυτό το δωμάτιο", "start_of_dm_history": "Αυτή είναι η αρχή του ιστορικού των άμεσων μηνυμάτων σας με .", "start_of_room": "Αυτή είναι η αρχή του .", @@ -1581,7 +1572,7 @@ "kick_reason": "Αιτία: %(reason)s", "kicked_by": "Αφαιρεθήκατε από %(memberName)s", "kicked_from_room_by": "Αφαιρεθήκατε από το %(roomName)s από τον %(memberName)s", - "leave_error_title": "Σφάλμα στην έξοδο από το δωμάτιο", + "leave_error_title": "Σφάλμα κατά την έξοδο από την αίθουσα", "leave_server_notices_description": "Αυτό το δωμάτιο χρησιμοποιείται για σημαντικά μηνύματα από τον κεντρικό διακομιστή, επομένως δεν μπορείτε να το αφήσετε.", "leave_server_notices_title": "Δεν είναι δυνατή η έξοδος από την αίθουσα ειδοποιήσεων διακομιστή", "leave_unexpected_error": "Μη αναμενόμενο σφάλμα διακομιστή κατά την προσπάθεια εξόδου από το δωμάτιο", @@ -1593,6 +1584,9 @@ "not_found_title": "Αυτό το δωμάτιο ή ο χώρος δεν υπάρχει.", "not_found_title_name": "Το %(roomName)s δεν υπάρχει.", "peek_join_prompt": "Κάνετε προεπισκόπηση στο %(roomName)s. Θέλετε να συμμετάσχετε;", + "pinned_message_banner": { + "go_to_message": "Εμφάνιση καρφιτσωμένου μηνύματος στο χρονολόγιο." + }, "rejoin_button": "Επανασύνδεση", "status_bar": { "delete_all": "Διαγραφή όλων", @@ -1610,7 +1604,7 @@ "other": "Έχετε %(count)s μη αναγνωσμένες ειδοποιήσεις σε προηγούμενη έκδοση αυτού του δωματίου." }, "upgrade_error_description": "Επανελέγξτε ότι ο διακομιστής σας υποστηρίζει την έκδοση δωματίου που επιλέξατε και προσπαθήστε ξανά.", - "upgrade_error_title": "Σφάλμα αναβάθμισης δωματίου", + "upgrade_error_title": "Σφάλμα αναβάθμισης αίθουσας", "upgrade_warning_bar": "Η αναβάθμιση αυτού του δωματίου θα τερματίσει το δωμάτιο και θα δημιουργήσει ένα αναβαθμισμένο δωμάτιο με το ίδιο όνομα.", "upgrade_warning_bar_admins": "Μόνο οι διαχειριστές δωματίων θα βλέπουν αυτήν την προειδοποίηση", "upgrade_warning_bar_unstable": "Αυτό το δωμάτιο τρέχει την έκδοση , την οποία ο κεντρικός διακομιστής έχει επισημάνει ως ασταθής.", @@ -1729,7 +1723,7 @@ "local_aliases_explainer_room": "Ορίστε διευθύνσεις για αυτό το δωμάτιο, ώστε οι χρήστες να μπορούν να το βρίσκουν μέσω του κεντρικού σας διακομιστή (%(localDomain)s)", "local_aliases_explainer_space": "Ορίστε διευθύνσεις για αυτόν τον χώρο, ώστε οι χρήστες να μπορούν να τον βρίσκουν μέσω του κεντρικού σας διακομιστή (%(localDomain)s)", "local_aliases_section": "Τοπική Διεύθυνση", - "name_field_label": "Όνομα Δωματίου", + "name_field_label": "Όνομα Αίθουσας", "new_alias_placeholder": "Νέα δημοσιευμένη διεύθυνση (π.χ. #alias:server)", "no_aliases_room": "Αυτό το δωμάτιο δεν έχει τοπικές διευθύνσεις", "no_aliases_space": "Αυτός ο χώρος δεν έχει τοπικές διευθύνσεις", @@ -1812,7 +1806,7 @@ "encryption_permanent": "Αφού ενεργοποιηθεί, η κρυπτογράφηση δεν μπορεί να απενεργοποιηθεί.", "error_join_rule_change_title": "Αποτυχία ενημέρωσης των κανόνων συμμετοχής", "error_join_rule_change_unknown": "Άγνωστο σφάλμα", - "guest_access_warning": "Τα άτομα με υποστηριζόμενους πελάτες θα μπορούν να εγγραφούν στο δωμάτιο χωρίς να έχουν εγγεγραμμένο λογαριασμό.", + "guest_access_warning": "Τα άτομα με υποστηριζόμενες εφαρμογές θα μπορούν να συμμετάσχουν στην αίθουσα χωρίς να έχουν εγγεγραμμένο λογαριασμό.", "history_visibility_invited": "Μόνο μέλη (από τη στιγμή που προσκλήθηκαν)", "history_visibility_joined": "Μόνο μέλη (από τη στιγμή που έγιναν μέλη)", "history_visibility_legend": "Ποιος μπορεί να διαβάσει το ιστορικό;", @@ -1832,7 +1826,7 @@ "join_rule_restricted_dialog_empty_warning": "Καταργείτε όλους τους χώρους. Η πρόσβαση θα είναι προεπιλεγμένη μόνο για πρόσκληση", "join_rule_restricted_dialog_filter_placeholder": "Αναζήτηση χώρων", "join_rule_restricted_dialog_heading_other": "Άλλοι χώροι ή δωμάτια που ίσως δε γνωρίζετε", - "join_rule_restricted_dialog_heading_room": "Χώροι που γνωρίζετε ότι περιέχουν αυτό το δωμάτιο", + "join_rule_restricted_dialog_heading_room": "Χώροι που γνωρίζετε ότι περιέχουν αυτήν την αίθουσα", "join_rule_restricted_dialog_heading_space": "Χώροι που γνωρίζετε ότι περιέχουν αυτόν το χώρο", "join_rule_restricted_dialog_heading_unknown": "Πιθανότατα αυτά είναι μέρος στα οποία συμμετέχουν και άλλοι διαχειριστές δωματίου.", "join_rule_restricted_dialog_title": "Επιλέξτε χώρους", @@ -1846,7 +1840,7 @@ }, "join_rule_restricted_upgrade_description": "Αυτή η αναβάθμιση θα επιτρέψει σε μέλη επιλεγμένων Χώρων πρόσβαση σε αυτό το δωμάτιο χωρίς πρόσκληση.", "join_rule_restricted_upgrade_warning": "Αυτό το δωμάτιο βρίσκεται σε ορισμένους Χώρους στους οποίους δεν είστε διαχειριστής. Σε αυτούς τους Χώρους, το παλιό δωμάτιο θα εξακολουθεί να εμφανίζεται, αλλά τα άτομα θα κληθούν να συμμετάσχουν στο νέο.", - "join_rule_upgrade_awaiting_room": "Φόρτωση νέου δωματίου", + "join_rule_upgrade_awaiting_room": "Φόρτωση νέας αίθουσας", "join_rule_upgrade_required": "Απαιτείται αναβάθμιση", "join_rule_upgrade_sending_invites": { "one": "Αποστολή πρόσκλησης...", @@ -1886,7 +1880,7 @@ "error_missing_user_id_request": "Λείπει το user_id στο αίτημα", "error_permission": "Δεν έχετε την άδεια να το κάνετε αυτό σε αυτό το δωμάτιο.", "error_power_level_invalid": "Το επίπεδο δύναμης πρέπει να είναι ένας θετικός ακέραιος.", - "error_room_not_visible": "Το δωμάτιο %(roomId)s δεν είναι ορατό", + "error_room_not_visible": "Η αίθουσα %(roomId)s δεν είναι ορατή", "error_room_unknown": "Αυτό το δωμάτιο δεν αναγνωρίζεται.", "error_send_request": "Δεν ήταν δυνατή η αποστολή αιτήματος." }, @@ -1927,7 +1921,7 @@ }, "settings": { "all_rooms_home": "Εμφάνιση όλων των δωματίων στην Αρχική", - "all_rooms_home_description": "Όλα τα δωμάτια στα οποία συμμετέχετε θα εμφανίζονται στην Αρχική σελίδα.", + "all_rooms_home_description": "Όλες οι αίθουσες στις οποίες συμμετέχετε θα εμφανίζονται στην Αρχική σελίδα.", "always_show_message_timestamps": "Εμφάνιση πάντα της ένδειξης ώρας στα μηνύματα", "appearance": { "custom_font": "Χρήση μιας γραμματοσειρά συστήματος", @@ -1942,7 +1936,7 @@ "layout_bubbles": "Συννεφάκια μηνυμάτων", "layout_irc": "IRC (Πειραματικό)", "match_system_theme": "Αντιστοίχιση θέματος συστήματος", - "timeline_image_size": "Μέγεθος εικόνας στη γραμμή χρόνου" + "timeline_image_size": "Μέγεθος εικόνας στο χρονολόγιο" }, "automatic_language_detection_syntax_highlight": "Ενεργοποίηση αυτόματης ανίχνευσης γλώσσας για επισήμανση σύνταξης", "autoplay_gifs": "Αυτόματη αναπαραγωγή GIFs", @@ -1976,7 +1970,7 @@ "deactivate_confirm_content_3": "Κανείς δε θα μπορεί να επαναχρησιμοποιήσει το όνομα χρήστη σας (MXID), συμπεριλαμβανομένου εσάς: αυτό το όνομα χρήστη θα παραμείνει μη διαθέσιμο", "deactivate_confirm_content_4": "Θα αποχωρήσετε από όλα τα δωμάτια και τις συνομιλίες σας", "deactivate_confirm_content_5": "Θα αφαιρεθείς από τον διακομιστή ταυτότητας: οι φίλοι σου δεν θα μπορούν πλέον να σε βρίσκουν με το email ή τον αριθμό τηλεφώνου σου", - "deactivate_confirm_content_6": "Τα παλιά σου μηνύματα θα εξακολουθούν να είναι ορατά σε άτομα που τα έλαβαν, όπως τα email που έστειλες στο παρελθόν. Θα 'θελες να αποκρύψεις τα απεσταλμένα σου μηνύματα από άτομα που συμμετέχουν στα δωμάτια στο μέλλον;", + "deactivate_confirm_content_6": "Τα παλιά σας μηνύματα θα εξακολουθούν να είναι ορατά σε άτομα που τα έλαβαν, όπως τα email που στείλατε στο παρελθόν. Θα θέλατε να αποκρύψετε τα απεσταλμένα σας μηνύματα από άτομα που συμμετέχουν στις αίθουσες στο μέλλον;", "deactivate_confirm_continue": "Επιβεβαίωση απενεργοποίησης λογαριασμού", "deactivate_confirm_erase_label": "Απόκρυψη των μηνυμάτων μου από νέους συμμετέχοντες", "deactivate_section": "Απενεργοποίηση λογαριασμού", @@ -2022,12 +2016,11 @@ "remove_msisdn_prompt": "Κατάργηση %(phone)s;", "spell_check_locale_placeholder": "Επιλογή τοπικών ρυθμίσεων" }, - "image_thumbnails": "Εμφάνιση προεπισκοπήσεων/μικρογραφιών για εικόνες", "inline_url_previews_default": "Ενεργοποιήστε τις ενσωματωμένες προεπισκοπήσεις URL από προεπιλογή", "inline_url_previews_room": "Ενεργοποιήστε τις προεπισκοπήσεις URL από προεπιλογή για τους συμμετέχοντες σε αυτό το δωμάτιο", "inline_url_previews_room_account": "Ενεργοποίηση προεπισκόπισης URL για αυτό το δωμάτιο (επηρεάζει μόνο εσάς)", "insert_trailing_colon_mentions": "Εισαγάγετε άνω και κάτω τελεία μετά την αναφορά του χρήστη στην αρχή ενός μηνύματος", - "jump_to_bottom_on_send": "Μεταβείτε στο τέλος του χρονοδιαγράμματος όταν στέλνετε ένα μήνυμα", + "jump_to_bottom_on_send": "Μεταβείτε στο τέλος του χρονολογίου όταν στέλνετε ένα μήνυμα", "key_backup": { "backup_in_progress": "Δημιουργούνται αντίγραφα ασφαλείας των κλειδιών σας (το πρώτο αντίγραφο ασφαλείας μπορεί να διαρκέσει μερικά λεπτά).", "backup_success": "Επιτυχία!", @@ -2071,6 +2064,9 @@ "keyboard": { "title": "Πληκτρολόγιο" }, + "media_preview": { + "media_preview_label": "Εμφάνιση μέσων στο χρονολόγιο" + }, "notifications": { "default_setting_description": "Αυτή η ρύθμιση θα εφαρμοστεί από προεπιλογή σε όλα τα δωμάτιά σου.", "default_setting_section": "Θέλω να ειδοποιούμαι για (Προεπιλεγμένη Ρύθμιση)", @@ -2117,12 +2113,12 @@ "rule_contains_user_name": "Μηνύματα που περιέχουν το όνομα χρήστη μου", "rule_encrypted": "Κρυπτογραφημένα μηνύματα σε ομαδικές συνομιλίες", "rule_encrypted_room_one_to_one": "Κρυπτογραφημένα μηνύματα σε συνομιλίες ένας προς έναν", - "rule_invite_for_me": "Όταν με προσκαλούν σ' ένα δωμάτιο", + "rule_invite_for_me": "Όταν με προσκαλούν σε μία αίθουσα", "rule_message": "Μηνύματα σε ομαδικές συνομιλίες", "rule_room_one_to_one": "Μηνύματα σε 1-προς-1 συνομιλίες", "rule_roomnotif": "Μηνύματα που περιέχουν @δωμάτιο", "rule_suppress_notices": "Μηνύματα από bots", - "rule_tombstone": "Όταν τα δωμάτια αναβαθμίζονται", + "rule_tombstone": "Όταν οι αίθουσες αναβαθμίζονται", "show_message_desktop_notification": "Εμφάνιση του μηνύματος στην ειδοποίηση στον υπολογιστή", "voip": "Κλήσεις ήχου και Βίντεο" }, @@ -2230,9 +2226,9 @@ "start_automatically": "Αυτόματη έναρξη μετά τη σύνδεση", "use_12_hour_format": "Εμφάνιση χρονικών σημάνσεων σε 12ωρη μορφή ώρας (π.χ. 2:30 μ.μ.)", "use_command_enter_send_message": "Χρησιμοποιήστε Command + Enter για να στείλετε ένα μήνυμα", - "use_command_f_search": "Χρησιμοποιήστε το Command + F για αναζήτηση στο χρονοδιάγραμμα", + "use_command_f_search": "Χρησιμοποιήστε τα πλήκτρα Command + F για αναζήτηση στο χρονολόγιο", "use_control_enter_send_message": "Χρησιμοποιήστε Ctrl + Enter για να στείλετε ένα μήνυμα", - "use_control_f_search": "Χρησιμοποιήστε τα πλήκτρα Ctrl + F για αναζήτηση στο χρονοδιάγραμμα", + "use_control_f_search": "Χρησιμοποιήστε τα πλήκτρα Ctrl + F για αναζήτηση στο χρονολόγιο", "voip": { "allow_p2p": "Να επιτρέπεται η χρήση Peer-to-Peer για κλήσεις 1:1", "allow_p2p_description": "Όταν είναι ενεργό, το άλλο άτομο ενδέχεται να μπορεί να δει τη διεύθυνση IP σου", @@ -2295,8 +2291,8 @@ "invite_3pid_needs_is_error": "Χρησιμοποιήστε έναν διακομιστή ταυτοτήτων για να προσκαλέσετε μέσω email. Μπορείτε να κάνετε διαχείριση στις Ρυθμίσεις.", "invite_3pid_use_default_is_title": "Χρησιμοποιήστε ένα διακομιστή ταυτοτήτων", "invite_3pid_use_default_is_title_description": "Χρησιμοποιήστε έναν διακομιστή ταυτοτήτων για να προσκαλέσετε μέσω email. Πατήστε συνέχεια για να χρησιμοποιήσετε τον προεπιλεγμένο διακομιστή ταυτοτήτων (%(defaultIdentityServerName)s) ή μπείτε στην διαχείριση στις Ρυθμίσεις.", - "join": "Σύνδεση στο δωμάτιο με την δοθείσα διεύθυνση", - "jumptodate": "Μεταβείτε στη δεδομένη ημερομηνία στη γραμμή χρόνου", + "join": "Σύνδεση στην αίθουσα με την δοθείσα διεύθυνση", + "jumptodate": "Μεταβείτε στη δεδομένη ημερομηνία στο χρονολόγιο", "jumptodate_invalid_input": "Αδυναμία κατανόησης της δοθείσας ημερομηνίας (%(inputDate)s). Προσπαθήστε να χρησιμοποιήσετε την μορφή YYYY-MM-DD.", "lenny": "Προ-εισάγει ( ͡° ͜ʖ ͡°) σε ένα μήνυμα απλού κειμένου", "me": "Εμφανίζει την ενέργεια", @@ -2323,7 +2319,7 @@ "spoiler": "Στέλνει το δοθέν μήνυμα ως spoiler", "tableflip": "Προ-εισάγει (╯°□°)╯︵ ┻━┻ σε ένα μήνυμα απλού κειμένου", "topic": "Λαμβάνει ή θέτει το θέμα του δωματίου", - "topic_none": "Το δωμάτιο αυτό δεν έχει κανένα θέμα.", + "topic_none": "Αυτή η αίθουσα δεν έχει κάποιο θέμα.", "topic_room_error": "Αποτυχία λήψης θέματος δωματίου: Αδυναμία εύρεσης δωματίου (%(roomId)s", "unban": "Άρση αποκλεισμού χρήστη με το συγκεκριμένο αναγνωριστικό", "unflip": "Προ-εισάγει ┬──┬ ノ( ゜-゜ノ) σε ένα μήνυμα απλού κειμένου", @@ -2362,7 +2358,7 @@ "space_dropdown_title": "Προσθήκη υπάρχοντος χώρου" }, "context_menu": { - "devtools_open_timeline": "Εμφάνιση χρονοδιαγράμματος δωματίου (develtools)", + "devtools_open_timeline": "Εμφάνιση χρονολογίου αίθουσας (develtools)", "explore": "Εξερευνήστε δωμάτια", "home": "Αρχική σελίδα χώρου", "manage_and_explore": "Διαχειριστείτε και εξερευνήστε δωμάτια", @@ -2439,7 +2435,6 @@ "heading_without_query": "Αναζήτηση για", "join_button_text": "Συμμετοχή στο %(roomAddress)s", "keyboard_scroll_hint": "Χρησιμοποιήστε τα για κύλιση", - "message_search_section_title": "'Άλλες αναζητήσεις", "other_rooms_in_space": "Άλλα δωμάτιο στο %(spaceName)s", "public_rooms_label": "Δημόσια δωμάτια", "recent_searches_section_title": "Πρόσφατες αναζητήσεις", @@ -2447,7 +2442,6 @@ "result_may_be_hidden_privacy_warning": "Ορισμένα αποτελέσματα ενδέχεται να είναι κρυφά για λόγους απορρήτου", "result_may_be_hidden_warning": "Ορισμένα αποτελέσματα ενδέχεται να είναι κρυμμένα", "search_dialog": "Παράθυρο Αναζήτησης", - "search_messages_hint": "Για να αναζητήσετε μηνύματα, βρείτε αυτό το εικονίδιο στην κορυφή ενός δωματίου ", "spaces_title": "Χώροι που ανήκετε", "start_group_chat_button": "Ξεκινήστε μια ομαδική συνομιλία" }, @@ -2542,9 +2536,9 @@ "historical_messages_unavailable": "Δεν μπορείτε να δείτε προηγούμενα μηνύματα", "io.element.widgets.layout": "%(senderName)s έχει ενημερώσει τη διάταξη του δωματίου", "load_error": { - "no_permission": "Προσπαθήσατε να φορτώσετε ένα συγκεκριμένο σημείο στο χρονολόγιο του δωματίου, αλλά δεν έχετε δικαίωμα να δείτε το εν λόγω μήνυμα.", + "no_permission": "Έγινε προσπάθεια φόρτωσης ενός συγκεκριμένου σημείου στο χρονολόγιο της αίθουσας, αλλά δεν έχετε δικαίωμα να δείτε το εν λόγω μήνυμα.", "title": "Δεν ήταν δυνατή η φόρτωση της θέσης του χρονολόγιου", - "unable_to_find": "Προσπαθήσατε να φορτώσετε ένα συγκεκριμένο σημείο στο χρονολόγιο του δωματίου, αλλά δεν καταφέρατε να το βρείτε." + "unable_to_find": "Έγινε προσπάθεια φόρτωσης ενός συγκεκριμένου σημείου στο χρονολόγιο της αίθουσας, αλλά δε βρέθηκε." }, "m.audio": { "error_downloading_audio": "Σφάλμα λήψης ήχου", @@ -2696,7 +2690,7 @@ "user_from_to": "%(userId)s από %(fromPowerLevel)s σε %(toPowerLevel)s" }, "m.room.server_acl": { - "all_servers_banned": "🎉 Όλοι οι διακομιστές αποκλείστηκαν από την συμμετοχή! Αυτό το δωμάτιο δεν μπορεί να χρησιμοποιηθεί πλέον.", + "all_servers_banned": "🎉 Όλοι οι διακομιστές αποκλείστηκαν από την συμμετοχή! Αυτή η αίθουσα δεν μπορεί να χρησιμοποιηθεί πλέον.", "changed": "Ο %(senderDisplayName)s άλλαξε τα ACLs του διακομιστή για αυτό το δωμάτιο.", "set": "Ο %(senderDisplayName)s όρισε τα ACLs του διακομιστή για αυτό το δωμάτιο." }, @@ -2716,7 +2710,7 @@ "added": "Προστέθηκε η μικροεφαρμογή %(widgetName)s από τον/την %(senderName)s", "jitsi_ended": "Η τηλεδιάσκεψη τερματίστηκε από %(senderName)s", "jitsi_join_right_prompt": "Συμμετάσχετε στην τηλεδιάσκεψη από την κάρτα πληροφοριών στα δεξιά", - "jitsi_join_top_prompt": "Συμμετάσχετε στην τηλεδιάσκεψη από την κορυφή του δωματίου αυτού", + "jitsi_join_top_prompt": "Συμμετάσχετε στην τηλεδιάσκεψη από την κορυφή αυτής της αίθουσας", "jitsi_started": "Η τηλεδιάσκεψη ξεκίνησε από %(senderName)s", "jitsi_updated": "Η τηλεδιάσκεψη ενημερώθηκε από %(senderName)s", "modified": "Έγινε αλλαγή στη μικροεφαρμογή %(widgetName)s από τον/την %(senderName)s", @@ -3010,14 +3004,14 @@ "confirm_keep_state_explainer": "Καταργήστε την επιλογή εάν θέλετε επίσης να καταργήσετε τα μηνύματα συστήματος σε αυτόν τον χρήστη (π.χ. αλλαγή μέλους, αλλαγή προφίλ…)", "confirm_keep_state_label": "Διατήρηση μηνυμάτων συστήματος", "confirm_title": "Καταργήστε πρόσφατα μηνύματα από %(user)s", - "no_recent_messages_description": "Δοκιμάστε να κάνετε κύλιση στη γραμμή χρόνου για να δείτε αν υπάρχουν παλαιότερα.", + "no_recent_messages_description": "Δοκιμάστε να κάνετε κύλιση στο χρονολόγιο για να δείτε αν υπάρχουν παλαιότερα.", "no_recent_messages_title": "Δε βρέθηκαν πρόσφατα μηνύματα από %(user)s" }, "redact_button": "Κατάργηση πρόσφατων μηνυμάτων", "revoke_invite": "Ανάκληση πρόσκλησης", - "room_encrypted": "Τα μηνύματα σε αυτό το δωμάτιο είναι κρυπτογραφημένα από άκρο σε άκρο.", + "room_encrypted": "Τα μηνύματα σε αυτήν την αίθουσα είναι κρυπτογραφημένα από άκρο σε άκρο.", "room_encrypted_detail": "Τα μηνύματά σας είναι ασφαλή και μόνο εσείς και ο παραλήπτης έχετε τα μοναδικά κλειδιά για να τα ξεκλειδώσετε.", - "room_unencrypted": "Τα μηνύματα σε αυτό το δωμάτιο δεν είναι κρυπτογραφημένα από άκρο σε άκρο.", + "room_unencrypted": "Τα μηνύματα σε αυτήν την αίθουσα δεν είναι κρυπτογραφημένα από άκρο σε άκρο.", "room_unencrypted_detail": "Σε κρυπτογραφημένα δωμάτια, τα μηνύματά σας είναι ασφαλή και μόνο εσείς και ο παραλήπτης έχετε τα μοναδικά κλειδιά για να τα ξεκλειδώσετε.", "share_button": "Κοινή χρήση Συνδέσμου με Χρήστη", "unban_button_space": "Αναίρεση αποκλεισμού από τον χώρο", @@ -3123,7 +3117,7 @@ "capability": { "always_on_screen_generic": "Παραμονή στην οθόνη σας ενώ τρέχετε", "always_on_screen_viewing_another_room": "Παραμονή στην οθόνη σας όταν βλέπετε άλλο δωμάτιο, όταν τρέχετε", - "any_room": "Τα παραπάνω, αλλά και σε οποιοδήποτε δωμάτιο είστε μέλος ή προσκεκλημένοι", + "any_room": "Τα παραπάνω, αλλά και σε οποιαδήποτε αίθουσα είστε μέλος ή προσκεκλημένοι", "byline_empty_state_key": "με ένα κενό κλειδί κατάστασης", "byline_state_key": "με κλειδί κατάστασης %(stateKey)s", "capability": "Η %(capability)s ικανότητα", @@ -3165,22 +3159,22 @@ "send_emotes_this_room": "Στείλτε emotes, ως εσείς, σε αυτό το δωμάτιο", "send_event_type_active_room": "Στείλετε %(eventType)s γεγονότα, ως εσείς, στο ενεργό δωμάτιό σας", "send_event_type_this_room": "Στείλτε %(eventType)s γεγονότα σε αυτό το δωμάτιο", - "send_files_active_room": "Στείλτε γενικά αρχεία, ως εσείς, στο ενεργό δωμάτιό σας", - "send_files_this_room": "Στείλτε γενικά αρχεία, ως εσείς, σε αυτό το δωμάτιο", - "send_images_active_room": "Στείλτε εικόνες, ως εσείς, στο ενεργό δωμάτιό σας", - "send_images_this_room": "Στείλτε εικόνες, ως εσείς, σε αυτό το δωμάτιο", - "send_messages_active_room": "Στείλτε μηνύματα, ως εσείς, στο ενεργό δωμάτιό σας", - "send_messages_this_room": "Στείλτε μηνύματα, ως εσείς, σε αυτό το δωμάτιο", + "send_files_active_room": "Στείλτε γενικά αρχεία, ως εσείς, στην ενεργή σας αίθουσα", + "send_files_this_room": "Στείλτε γενικά αρχεία, ως εσείς, σε αυτήν την αίθουσα", + "send_images_active_room": "Στείλτε εικόνες, ως εσείς, στην ενεργή σας αίθουσα", + "send_images_this_room": "Στείλτε εικόνες, ως εσείς, σε αυτήν την αίθουσα", + "send_messages_active_room": "Στείλτε μηνύματα, ως εσείς, στην ενεργή σας αίθουσα", + "send_messages_this_room": "Στείλτε μηνύματα, ως εσείς, σε αυτήν την αίθουσα", "send_msgtype_active_room": "Στείλτε %(msgtype)s μηνύματα, ώς εσείς, στο ενεργό δωμάτιό σας", "send_msgtype_this_room": "Στείλτε %(msgtype)s μηνύματα, ως εσείς, σε αυτό το δωμάτιο", - "send_stickers_active_room": "Στείλτε αυτοκόλλητα στο ενεργό δωμάτιο", - "send_stickers_active_room_as_you": "Στείλτε αυτοκόλλητα στο ενεργό δωμάτιό σας", - "send_stickers_this_room": "Στείλτε αυτοκόλλητα σε αυτό το δωμάτιο", + "send_stickers_active_room": "Στείλτε αυτοκόλλητα στην ενεργή σας αίθουσα", + "send_stickers_active_room_as_you": "Στείλτε αυτοκόλλητα στην ενεργή σας αίθουσας, ως εσείς", + "send_stickers_this_room": "Στείλτε αυτοκόλλητα σε αυτήν την αίθουσα", "send_stickers_this_room_as_you": "Στείλτε αυτοκόλλητα σε αυτό το δωμάτιο", - "send_text_messages_active_room": "Στείλτε μηνύματα κειμένου, ως εσείς, στο ενεργό δωμάτιό σας", - "send_text_messages_this_room": "Στείλτε μηνύματα κειμένου, ως εσείς, σε αυτό το δωμάτιο", - "send_videos_active_room": "Στείλτε βίντεο, ως εσείς, στο ενεργό δωμάτιό σας", - "send_videos_this_room": "Στείλτε βίντεο, ως εσείς, σε αυτό το δωμάτιο", + "send_text_messages_active_room": "Στείλτε μηνύματα κειμένου, ως εσείς, στην ενεργή σας αίθουσα", + "send_text_messages_this_room": "Στείλτε μηνύματα κειμένου, ως εσείς, σε αυτήν την αίθουσα", + "send_videos_active_room": "Στείλτε βίντεο, ως εσείς, στην ενεργή σας αίθουσα", + "send_videos_this_room": "Στείλτε βίντεο, ως εσείς, σε αυτήν την αίθουσα", "specific_room": "Τα παραπάνω, αλλά και μέσα ", "switch_room": "Αλλάξτε το δωμάτιο που βλέπετε", "switch_room_message_user": "Αλλάξτε το δωμάτιο, το μήνυμα ή τον χρήστη που βλέπετε" diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c7472cd7c9..2a61785608 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -162,6 +162,7 @@ "view_message": "View message", "view_source": "View Source", "yes": "Yes", + "yes_dismiss": "Yes, dismiss", "zoom_in": "Zoom in", "zoom_out": "Zoom out" }, @@ -389,8 +390,9 @@ "email_resend_prompt": "Did not receive it? Resend it", "email_resent": "Resent!", "fallback_button": "Start authentication", - "mas_cross_signing_reset_cta": "Go to your account", - "mas_cross_signing_reset_description": "Reset your identity through your account provider and then come back and click “Retry”.", + "mas_cross_signing_reset_cta": "Continue to account", + "mas_cross_signing_reset_description": "You're about to go to your %(serverName)s account to reset your identity. Once you have completed reset on your account, please return here and click Retry.", + "mas_cross_signing_reset_title": "Go to your account to reset your identity", "msisdn": "A text message has been sent to %(msisdn)s", "msisdn_token_incorrect": "Token incorrect", "msisdn_token_prompt": "Please enter the code it contains:", @@ -531,6 +533,7 @@ "message_timestamp_invalid": "Invalid timestamp", "microphone": "Microphone", "model": "Model", + "moderation_and_safety": "Moderation and safety", "modern": "Modern", "mute": "Mute", "n_members": { @@ -914,22 +917,13 @@ "empty_room_was_name": "Empty room (was %(oldName)s)", "encryption": { "access_secret_storage_dialog": { - "enter_phrase_or_key_prompt": "Enter your Security Phrase or to continue.", + "alternatives": "If you have a security key or security phrase, this will work too.", "key_validation_text": { - "invalid_security_key": "Invalid Recovery Key", - "recovery_key_is_correct": "Looks good!", - "wrong_file_type": "Wrong file type", - "wrong_security_key": "Wrong Recovery Key" + "wrong_security_key": "The recovery key you entered is not correct." }, - "reset_title": "Reset everything", - "reset_warning_1": "Only do this if you have no other device to complete verification with.", - "reset_warning_2": "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.", + "privacy_warning": "Make sure nobody can see this screen!", "restoring": "Restoring keys from backup", - "security_key_title": "Recovery Key", - "security_phrase_incorrect_error": "Unable to access secret storage. Please verify that you entered the correct Security Phrase.", - "security_phrase_title": "Security Phrase", - "separator": "%(securityKey)s or %(recoveryFile)s", - "use_security_key_prompt": "Use your Recovery Key to continue." + "security_key_title": "Recovery key" }, "bootstrap_title": "Setting up keys", "cancel_entering_passphrase_description": "Are you sure you want to cancel entering passphrase?", @@ -985,6 +979,8 @@ "setup_secure_backup": { "explainer": "Back up your keys before signing out to avoid losing them." }, + "turn_on_key_storage": "Turn on key storage", + "turn_on_key_storage_description": "Store your cryptographic identity and message keys securely on the server. This will allow you to view your message history on any new devices.", "udd": { "interactive_verification_button": "Interactively verify by emoji", "other_ask_verify_text": "Ask this user to verify their session, or manually verify it below.", @@ -998,7 +994,6 @@ "accepting": "Accepting…", "after_new_login": { "device_verified": "Device verified", - "reset_confirmation": "Really reset verification keys?", "skip_verification": "Skip verification for now", "unable_to_verify": "Unable to verify this device", "verify_this_device": "Verify this device" @@ -1069,8 +1064,6 @@ "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.", "verify_later": "I'll verify later", - "verify_reset_warning_1": "Resetting your verification keys cannot be undone. After resetting, you won't have access to old encrypted messages, and any friends who have previously verified you will see security warnings until you re-verify with them.", - "verify_reset_warning_2": "Please only proceed if you're sure you've lost all of your other devices and your Recovery Key.", "verify_using_device": "Verify with another device", "verify_using_key": "Verify with Recovery Key", "verify_using_key_or_phrase": "Verify with Recovery Key or Phrase", @@ -2107,6 +2100,7 @@ "room_list": { "add_room_label": "Add room", "add_space_label": "Add space", + "appearance": "Appearance", "breadcrumbs_empty": "No recently visited rooms", "breadcrumbs_label": "Recently visited rooms", "empty": { @@ -2115,11 +2109,14 @@ "no_chats_description_no_room_rights": "Get started by messaging someone", "no_favourites": "You don't have favourite chat yet", "no_favourites_description": "You can add a chat to your favourites in the chat settings", + "no_invites": "You don't have any unread invites", + "no_mentions": "You don't have any unread mentions", "no_people": "You don’t have direct chats with anyone yet", "no_people_description": "You can deselect filters in order to see your other chats", "no_rooms": "You’re not in any room yet", "no_rooms_description": "You can deselect filters in order to see your other chats", "no_unread": "Congrats! You don’t have any unread messages", + "show_activity": "See all activity", "show_chats": "Show all chats" }, "failed_add_tag": "Failed to add tag %(tagName)s to room", @@ -2127,6 +2124,8 @@ "failed_set_dm_tag": "Failed to set direct message tag", "filters": { "favourite": "Favourites", + "invites": "Invites", + "mentions": "Mentions", "people": "People", "rooms": "Rooms", "unread": "Unreads" @@ -2157,15 +2156,22 @@ "more_options": "More Options", "open_room": "Open room %(roomName)s" }, + "room_options": "Room Options", "show_less": "Show less", + "show_message_previews": "Show message previews", "show_n_more": { "one": "Show %(count)s more", "other": "Show %(count)s more" }, "show_previews": "Show previews of messages", + "sort": "Sort", "sort_by": "Sort by", "sort_by_activity": "Activity", "sort_by_alphabet": "A-Z", + "sort_type": { + "activity": "Activity", + "atoz": "A-Z" + }, "sort_unread_first": "Show rooms with unread messages first", "space_menu": { "home": "Space home", @@ -2453,6 +2459,10 @@ "recent_changes_heading": "Recent changes that have not yet been received", "title": "Server isn't responding" }, + "service_worker_error": { + "description": "%(brand)s requires a service worker for loading authenticated media from Matrix content repositories. This is not supported by your browser so you may experience media failing to load.", + "title": "Failed to load service worker" + }, "seshat": { "error_initialising": "Message search initialisation failed, check your settings for more information", "reset_button": "Reset event store", @@ -2546,6 +2556,8 @@ "session_key": "Session key:", "title": "Advanced" }, + "confirm_key_storage_off": "Are you sure you want to keep key storage turned off?", + "confirm_key_storage_off_description": "If you sign out of all your devices you will lose your message history and will need to verify all your existing contacts again. Learn more", "delete_key_storage": { "breadcrumb_page": "Delete key storage", "confirm": "Delete key storage", @@ -2631,6 +2643,7 @@ "discovery_needs_terms_title": "Let people find you", "display_name": "Display Name", "display_name_error": "Unable to set display name", + "email_adding_unsupported_by_hs": "This homeserver does not support adding email addresses to your account.", "email_address_in_use": "This email address is already in use", "email_address_label": "Email Address", "email_not_verified": "Your email address hasn't been verified yet", @@ -2655,7 +2668,9 @@ "error_share_msisdn_discovery": "Unable to share phone number", "identity_server_no_token": "No identity access token found", "identity_server_not_set": "Identity server not set", + "invalid_phone_number": "The phone number supplied does not appear to be valid.", "language_section": "Language", + "msisdn_adding_unsupported_by_hs": "This homeserver does not support adding phone numbers to your account.", "msisdn_in_use": "This phone number is already in use", "msisdn_label": "Phone Number", "msisdn_verification_field_label": "Verification code", @@ -2674,12 +2689,10 @@ "unable_to_load_msisdns": "Unable to load phone numbers", "username": "Username" }, - "image_thumbnails": "Show previews/thumbnails for images", "inline_url_previews_default": "Enable inline URL previews by default", "inline_url_previews_room": "Enable URL previews by default for participants in this room", "inline_url_previews_room_account": "Enable URL previews for this room (only affects you)", "insert_trailing_colon_mentions": "Insert a trailing colon after user mentions at the start of a message", - "invite_avatars": "Show avatars of rooms you have been invited to", "jump_to_bottom_on_send": "Jump to the bottom of the timeline when you send a message", "key_backup": { "backup_in_progress": "Your keys are being backed up (the first backup could take a few minutes).", @@ -2738,6 +2751,14 @@ "labs_mjolnir": { "dialog_title": "Settings: Ignored Users" }, + "media_preview": { + "hide_avatars": "Hide avatars of room and inviter", + "hide_media": "Always hide", + "media_preview_description": "A hidden media can always be shown by tapping on it", + "media_preview_label": "Show media in timeline", + "show_in_private": "In private rooms", + "show_media": "Always show" + }, "notifications": { "default_setting_description": "This setting will be applied by default to all your rooms.", "default_setting_section": "I want to be notified for (Default Setting)", @@ -3222,7 +3243,7 @@ "heading_without_query": "Search for", "join_button_text": "Join %(roomAddress)s", "keyboard_scroll_hint": "Use to scroll", - "message_search_section_title": "Other searches", + "messages_label": "Messages", "other_rooms_in_space": "Other rooms in %(spaceName)s", "public_rooms_label": "Public rooms", "public_spaces_label": "Public spaces", @@ -3232,7 +3253,6 @@ "result_may_be_hidden_privacy_warning": "Some results may be hidden for privacy", "result_may_be_hidden_warning": "Some results may be hidden", "search_dialog": "Search Dialog", - "search_messages_hint": "To search messages, look for this icon at the top of a room ", "spaces_title": "Spaces you're in", "start_group_chat_button": "Start a group chat" }, @@ -3281,9 +3301,7 @@ "threads_activity_centre": { "header": "Threads activity", "no_rooms_with_threads_notifs": "You don't have rooms with thread notifications yet.", - "no_rooms_with_unread_threads": "You don't have rooms with unread threads yet.", - "release_announcement_description": "Threads notifications have moved, find them here from now on.", - "release_announcement_header": "Threads Activity Centre" + "no_rooms_with_unread_threads": "You don't have rooms with unread threads yet." }, "time": { "about_day_ago": "about a day ago", @@ -3794,10 +3812,11 @@ "unavailable": "Unavailable" }, "update_room_access_modal": { - "description": "To create a share link, you need to allow guests to join this room. This may make the room less secure. When you're done with the call, you can make the room private again.", - "dont_change_description": "Alternatively, you can hold the call in a separate room.", + "description": "To create a share link, make this room public or enable the option for users to ask to join. This allows guests to join without being invited.", + "dont_change_description": "If you don't want to change the access of this room, you can create a new room for the call link.", "no_change": "I don't want to change the access level.", - "title": "Change the room access level" + "revert_access_description": "(This can be reverted to the previous value in the Room Settings: Security & Privacy / Access)", + "title": "Allow guest users to join this room" }, "upload_failed_generic": "The file '%(fileName)s' failed to upload.", "upload_failed_size": "The file '%(fileName)s' exceeds this homeserver's size limit for uploads", diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json index 4af53c91b1..a6f28167ea 100644 --- a/src/i18n/strings/eo.json +++ b/src/i18n/strings/eo.json @@ -598,19 +598,10 @@ "encryption": { "access_secret_storage_dialog": { "key_validation_text": { - "invalid_security_key": "Nevalida Sekureca ŝlosilo", - "recovery_key_is_correct": "Ŝajnas bona!", - "wrong_file_type": "Neĝusta dosiertipo", "wrong_security_key": "Malĝusta Sekureca ŝlosilo" }, - "reset_title": "Restarigi ĉion", - "reset_warning_1": "Faru tion ĉi nur se vi ne havas alian aparaton, per kiu vi kontrolus ceterajn.", - "reset_warning_2": "Se vi restarigos ĉion, vi rekomencos sen fidataj salutaĵoj, uzantoj, kaj eble ne povos vidi antaŭajn mesaĝojn.", "restoring": "Rehavo de ŝlosiloj el savkopio", - "security_key_title": "Sekureca ŝlosilo", - "security_phrase_incorrect_error": "Ne povas akiri sekretandeponejon. Bonvolu kontroli, ĉu vi enigis la ĝustan Sekurecan frazon.", - "security_phrase_title": "Sekureca frazo", - "use_security_key_prompt": "Uzu vian sekurecan ŝlosilon por daŭrigi." + "security_key_title": "Sekureca ŝlosilo" }, "bootstrap_title": "Agordo de klavoj", "cancel_entering_passphrase_description": "Ĉu vi certe volas nuligi enigon de pasfrazo?", @@ -1702,7 +1693,6 @@ "remove_email_prompt": "Ĉu forigi %(email)s?", "remove_msisdn_prompt": "Ĉu forigi %(phone)s?" }, - "image_thumbnails": "Montri antaŭrigardojn/bildetojn por bildoj", "inline_url_previews_default": "Ŝalti entekstan antaŭrigardon al retadresoj", "inline_url_previews_room": "Ŝalti URL-antaŭrigardon por anoj de ĉi tiu ĉambro", "inline_url_previews_room_account": "Ŝalti URL-antaŭrigardon en ĉi tiu ĉambro (nur por vi)", diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json index c08d82a19f..6abbdbe819 100644 --- a/src/i18n/strings/es.json +++ b/src/i18n/strings/es.json @@ -741,22 +741,11 @@ "empty_room_was_name": "Sala vacía (antes era %(oldName)s)", "encryption": { "access_secret_storage_dialog": { - "enter_phrase_or_key_prompt": "Escribe tu frase de seguridad o para continuar.", "key_validation_text": { - "invalid_security_key": "Clave de seguridad inválida", - "recovery_key_is_correct": "¡Se ve bien!", - "wrong_file_type": "Tipo de archivo incorrecto", "wrong_security_key": "Clave de seguridad incorrecta" }, - "reset_title": "Restablecer todo", - "reset_warning_1": "Solo haz esto si no tienes ningún otro dispositivo con el que completar la verificación.", - "reset_warning_2": "Si restableces todo, volverás a empezar sin sesiones ni usuarios de confianza, y puede que no puedas ver mensajes anteriores.", "restoring": "Restaurando las claves desde copia de seguridad", - "security_key_title": "Clave de seguridad", - "security_phrase_incorrect_error": "No se ha podido acceder al almacenamiento seguro. Por favor, comprueba que la frase de seguridad es correcta.", - "security_phrase_title": "Frase de seguridad", - "separator": "%(securityKey)s o %(recoveryFile)s", - "use_security_key_prompt": "Usa tu llave de seguridad para continuar." + "security_key_title": "Clave de seguridad" }, "bootstrap_title": "Configurando claves", "cancel_entering_passphrase_description": "¿Estas seguro que quieres cancelar el ingresar tu contraseña de recuperación?", @@ -813,7 +802,6 @@ "accepting": "Aceptando…", "after_new_login": { "device_verified": "Dispositivo verificado", - "reset_confirmation": "¿De verdad quieres restablecer las claves de verificación?", "skip_verification": "Saltar la verificación por ahora", "unable_to_verify": "No se ha podido verificar el dispositivo", "verify_this_device": "Verificar este dispositivo" @@ -883,7 +871,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_reset_warning_1": "Una vez restableces las claves de verificación, no lo podrás deshacer. Después de restablecerlas, no podrás acceder a los mensajes cifrados antiguos, y cualquier persona que te haya verificado verá avisos de seguridad hasta que vuelvas a hacer la verificación con ella.", "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", @@ -2147,7 +2134,6 @@ "remove_msisdn_prompt": "¿Eliminar %(phone)s?", "spell_check_locale_placeholder": "Elige un idioma" }, - "image_thumbnails": "Mostrar vistas previas para las imágenes", "inline_url_previews_default": "Activar la vista previa de URLs en línea por defecto", "inline_url_previews_room": "Activar la vista previa de URLs por defecto para los participantes de esta sala", "inline_url_previews_room_account": "Activar la vista previa de URLs en esta sala (solo para ti)", @@ -2603,7 +2589,6 @@ "heading_without_query": "Buscar", "join_button_text": "Unirte a %(roomAddress)s", "keyboard_scroll_hint": "Usa para desplazarte", - "message_search_section_title": "Otras búsquedas", "other_rooms_in_space": "Otras salas en %(spaceName)s", "public_rooms_label": "Salas públicas", "recent_searches_section_title": "Búsquedas recientes", @@ -2612,7 +2597,6 @@ "result_may_be_hidden_privacy_warning": "Algunos resultados pueden estar ocultos por motivos de privacidad", "result_may_be_hidden_warning": "Algunos resultados pueden estar ocultos", "search_dialog": "Ventana de búsqueda", - "search_messages_hint": "Para buscar contenido de mensajes, usa este icono en la parte de arriba de una sala: ", "spaces_title": "Tus espacios", "start_group_chat_button": "Crear conversación en grupo" }, diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index 170ff24587..666ed60825 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -157,6 +157,7 @@ "view_message": "Vaata sõnumit", "view_source": "Vaata lähtekoodi", "yes": "Jah", + "yes_dismiss": "Jah, loobu", "zoom_in": "Suumi sisse", "zoom_out": "Suumi välja" }, @@ -386,6 +387,7 @@ "fallback_button": "Alusta autentimist", "mas_cross_signing_reset_cta": "Mine oma kasutajakonto andmete juurde", "mas_cross_signing_reset_description": "Lähtesta oma võrguidentiteet oma teenusepakkuja abil ning tule siis siia tagasi ja vajuta „Proovi uuesti“.", + "mas_cross_signing_reset_title": "Oma võrguidentiteedi lähtestamiseks ava oma kasutajakonto vaade", "msisdn": "Saatsime tekstisõnumi telefoninumbrile %(msisdn)s", "msisdn_token_incorrect": "Vigane tunnusluba", "msisdn_token_prompt": "Palun sisesta seal kuvatud kood:", @@ -525,6 +527,7 @@ "message_timestamp_invalid": "Vigane ajatempel", "microphone": "Mikrofon", "model": "Mudel", + "moderation_and_safety": "Modereerimine ja turvalisus", "modern": "Moodne", "mute": "Summuta", "n_members": { @@ -908,22 +911,13 @@ "empty_room_was_name": "Tühi jututuba (varasema nimega %(oldName)s)", "encryption": { "access_secret_storage_dialog": { - "enter_phrase_or_key_prompt": "Jätkamiseks sisesta oma turvafraas või .", + "alternatives": "Kui sul on turvavõti või turvafraas, siis need toimivad ka.", "key_validation_text": { - "invalid_security_key": "Vigane taastevõti", - "recovery_key_is_correct": "Tundub õige!", - "wrong_file_type": "Vale failitüüp", - "wrong_security_key": "Vale taastevõti" + "wrong_security_key": "Sinu sisestatud taastevõti pole korrektne." }, - "reset_title": "Alusta kõigega algusest", - "reset_warning_1": "Toimi nii vaid siis, kui sul pole jäänud ühtegi seadet, millega verifitseerimist lõpuni teha.", - "reset_warning_2": "Kui sa kõik krüptoseosed lähtestad, siis sul esimese hooga pole ühtegi usaldusväärseks tunnistatud sessiooni ega kasutajat ning ilmselt ei saa sa lugeda vanu sõnumeid.", + "privacy_warning": "Palun vaata, et keegi teine ei näeks seda ekraanivaadet!", "restoring": "Taastan võtmed varundusest", - "security_key_title": "Taastevõti", - "security_phrase_incorrect_error": "Ei õnnestu saada ligipääsu turvahoidlale. Palun kontrolli, et sa oleksid sisestanud õige turvafraasi.", - "security_phrase_title": "Turvafraas", - "separator": "%(securityKey)s või %(recoveryFile)s", - "use_security_key_prompt": "Jätkamiseks kasuta oma taastevõtit." + "security_key_title": "Taastevõti" }, "bootstrap_title": "Võtame krüptovõtmed kasutusele", "cancel_entering_passphrase_description": "Kas oled kindel et sa soovid katkestada paroolifraasi sisestamise?", @@ -979,6 +973,8 @@ "setup_secure_backup": { "explainer": "Vältimaks nende kaotamist, varunda krüptovõtmed enne väljalogimist." }, + "turn_on_key_storage": "Võta krüptovõtmete hoidla kasutusele", + "turn_on_key_storage_description": "Salvesta oma krüptoidentiteet ja sõnumite krüptovõtmed turvaliselt serveris. See tagab, et sinu sõnumite ajalugu on alati loetav, ka kõikides uutes seadmetes.", "udd": { "interactive_verification_button": "Verifitseeri interaktiivselt emoji abil", "other_ask_verify_text": "Palu nimetatud kasutajal verifitseerida see sessioon või tee seda alljärgnevaga käsitsi.", @@ -992,7 +988,6 @@ "accepting": "Nõustun …", "after_new_login": { "device_verified": "Seade on verifitseeritud", - "reset_confirmation": "Kas tõesti kustutame kõik verifitseerimisvõtmed?", "skip_verification": "Jäta verifitseerimine praegu vahele", "unable_to_verify": "Selle seadme verifitseerimine ei õnnestunud", "verify_this_device": "Verifitseeri see seade" @@ -1063,8 +1058,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_reset_warning_1": "Verifitseerimisvõtmete kustutamist ei saa hiljem tagasi võtta. Peale seda sul puudub ligipääs vanadele krüptitud sõnumitele ja kõik sinu verifitseeritud sõbrad-tuttavad näevad turvahoiatusi seni kuni sa uuesti nad verifitseerid.", - "verify_reset_warning_2": "Palun jätka ainult siis, kui sa oled kaotanud ligipääsu oma kõikidele muudele seadmetele ning oma taastevõtmele.", "verify_using_device": "Verifitseeri teise seadmega", "verify_using_key": "Verifitseeri taastevõtmega", "verify_using_key_or_phrase": "Verifitseeri taastevõtme või -fraasiga", @@ -2101,6 +2094,7 @@ "room_list": { "add_room_label": "Lisa jututuba", "add_space_label": "Lisa kogukonnakeskus", + "appearance": "Välimus", "breadcrumbs_empty": "Hiljuti külastatud jututubasid ei leidu", "breadcrumbs_label": "Hiljuti külastatud jututoad", "empty": { @@ -2109,11 +2103,14 @@ "no_chats_description_no_room_rights": "Alusta sellest, et leia mõni vestluspartner", "no_favourites": "Sa pole veel ühtegi vestlust märkinud lemmikuks", "no_favourites_description": "Vestluse saad märkida lemmikuks tema seadistustest", + "no_invites": "Sul pole lugemata kutseid", + "no_mentions": "Sul pole lugemata mainimisi", "no_people": "Sul pole veel ühtegi otsevestlust kellegagi", "no_people_description": "Kõikide muude vestluste nägemiseks eemalda otsingufiltrid", "no_rooms": "Sa veel ei osale mitte üheski jututoas", "no_rooms_description": "Kõikide oma muude vestluste nägemiseks eemalda otsingufiltrid", "no_unread": "Õnnitlused! Sul pole ühtegi lugemata sõnumit", + "show_activity": "Vaata kõiki tegevusi", "show_chats": "Näita kõiki vestlusi" }, "failed_add_tag": "Sildi %(tagName)s lisamine jututoale ebaõnnestus", @@ -2121,6 +2118,8 @@ "failed_set_dm_tag": "Otsevestluse sildi seadmine ei õnnestunud", "filters": { "favourite": "Lemmikud", + "invites": "Kutsed", + "mentions": "Mainimised", "people": "Inimesed", "rooms": "Jututoad", "unread": "Lugemata" @@ -2151,15 +2150,22 @@ "more_options": "Täiendavad seadistused", "open_room": "Ava jututuba: %(roomName)s" }, + "room_options": "Jututoa valikud", "show_less": "Näita vähem", + "show_message_previews": "Näita sõnumite eelvaateid", "show_n_more": { "one": "Näita veel %(count)s vestlust", "other": "Näita veel %(count)s vestlust" }, "show_previews": "Näita sõnumite eelvaateid", + "sort": "Järjesta", "sort_by": "Järjestamisviis", "sort_by_activity": "Aktiivsuse alusel", "sort_by_alphabet": "Tähestiku järjekorras", + "sort_type": { + "activity": "Aktiivsuse alusel", + "atoz": "Tähestiku alusel" + }, "sort_unread_first": "Näita lugemata sõnumitega jututubasid esimesena", "space_menu": { "home": "Kogukonna avaleht", @@ -2447,6 +2453,10 @@ "recent_changes_heading": "Hiljutised muudatused, mis pole veel alla laetud või saabunud", "title": "Server ei vasta päringutele" }, + "service_worker_error": { + "description": "Selleks, et autenditud meediafailide laadimine Matrixi sisuhoidlast toimiks, eeldab %(brand)s taustal toimiva teenuse töötleja kasutamist. See funktsionaalsus pole sinu veebibrauseris toetatud ja meediafailid võivad jääda laadimata.", + "title": "Teenuse töötleja laadimine ei õnnestunud" + }, "seshat": { "error_initialising": "Sõnumite otsingu ettevalmistamine ei õnnestunud, lisateavet leiad rakenduse seadistustest", "reset_button": "Lähtesta sündmuste andmekogu", @@ -2540,6 +2550,8 @@ "session_key": "Sessioonivõti:", "title": "Täiendav teave" }, + "confirm_key_storage_off": "Kas sa oled kindel, et tahad krüptovõtmete hoidlat mitte kasutada?", + "confirm_key_storage_off_description": "Kui sa logid välja kõikidest oma seadmetest, siis kaotad ligipääsu oma sõnumite ajaloole ning pead kõik olemasolevad kontaktid uuesti verifitseerima. Lisateave", "delete_key_storage": { "breadcrumb_page": "Kustuta krüptovõtmete hoidla", "confirm": "Kustuta krüptovõtmete hoidla", @@ -2625,6 +2637,7 @@ "discovery_needs_terms_title": "Võimalda teistel Matrixi võrgu kasutajatel sind leida", "display_name": "Kuvatav nimi", "display_name_error": "Kuvatava nime määramine ei õnnestu", + "email_adding_unsupported_by_hs": "See koduserver ei toeta kasutajakonto juurde e-posti aadressi lisamise võimalust.", "email_address_in_use": "See e-posti aadress on juba kasutusel", "email_address_label": "E-posti aadress", "email_not_verified": "Sinu e-posti aadress pole veel verifitseeritud", @@ -2649,7 +2662,9 @@ "error_share_msisdn_discovery": "Telefoninumbri jagamine ei õnnestunud", "identity_server_no_token": "Ei leidu tunnusluba isikutuvastusserveri jaoks", "identity_server_not_set": "Isikutuvastusserver on määramata", + "invalid_phone_number": "Sisestatud telefoninumber ei tundu olema korrektne", "language_section": "Keel", + "msisdn_adding_unsupported_by_hs": "See koduserver ei toeta kasutajakonto juurde telefoninumbri lisamise võimalust.", "msisdn_in_use": "See telefoninumber on juba kasutusel", "msisdn_label": "Telefoninumber", "msisdn_verification_field_label": "Verifikatsioonikood", @@ -2668,12 +2683,10 @@ "unable_to_load_msisdns": "Telefoninumbrite laadimine ei õnnestu", "username": "Kasutajanimi" }, - "image_thumbnails": "Näita piltide eelvaateid või väikepilte", "inline_url_previews_default": "Luba URL'ide vaikimisi eelvaated", "inline_url_previews_room": "Luba URL'ide vaikimisi eelvaated selles jututoas osalejate jaoks", "inline_url_previews_room_account": "Luba URL'ide eelvaated selle jututoa jaoks (mõjutab vaid sind)", "insert_trailing_colon_mentions": "Mainimiste järel näita sõnumi alguses koolonit", - "invite_avatars": "Näita nende jututubade tunnuspilte, kuhu oled saanud kutse", "jump_to_bottom_on_send": "Sõnumi saatmiseks hüppa ajajoone lõppu", "key_backup": { "backup_in_progress": "Sinu krüptovõtmeid varundatakse (esimese varukoopia tegemine võib võtta paar minutit).", @@ -2732,6 +2745,14 @@ "labs_mjolnir": { "dialog_title": "Seadistused: Eiratud kasutajad" }, + "media_preview": { + "hide_avatars": "Peida jututoa ja kutsuja tunnuspildid", + "hide_media": "Peida alati", + "media_preview_description": "Peidetud meediumi saad alati näha temal klõpsides", + "media_preview_label": "Näita ajajoonel meediat", + "show_in_private": "Privaatsetes jututubades", + "show_media": "Alati" + }, "notifications": { "default_setting_description": "See seadistus kehtib vaikimisi kõikides sinu jututubades.", "default_setting_section": "Soovin teavitusi (vaikimisi seadistused)", @@ -3215,7 +3236,7 @@ "heading_without_query": "Otsingusõna", "join_button_text": "Liitu %(roomAddress)s jututoaga", "keyboard_scroll_hint": "Kerimiseks kasuta ", - "message_search_section_title": "Muud otsingud", + "messages_label": "Sõnumid", "other_rooms_in_space": "Muud jututoad %(spaceName)s kogukonnad", "public_rooms_label": "Avalikud jututoad", "public_spaces_label": "Avalikud kogukonnad", @@ -3225,7 +3246,6 @@ "result_may_be_hidden_privacy_warning": "Mõned tulemused võivad privaatsusseadistuste tõttu olla peidetud", "result_may_be_hidden_warning": "Mõned tulemused võivad olla peidetud", "search_dialog": "Otsinguvaade", - "search_messages_hint": "Sõnumite otsimiseks klõpsi ikooni jututoa ülaosas", "spaces_title": "Kogukonnad, mille liige sa oled", "start_group_chat_button": "Alusta rühmavestlust" }, @@ -3274,9 +3294,7 @@ "threads_activity_centre": { "header": "Jutulõngade ülevaade", "no_rooms_with_threads_notifs": "Pole veel ühtegi jutulõngakohase teavitusega jututuba.", - "no_rooms_with_unread_threads": "Pole veel ühtegi lugemata jutulõngaga jututuba.", - "release_announcement_description": "Jutulõngade teavitused leiduvad nüüd uues kohas. Nüüd leiad nad siit.", - "release_announcement_header": "Jutulõngade ülevaade" + "no_rooms_with_unread_threads": "Pole veel ühtegi lugemata jutulõngaga jututuba." }, "time": { "about_day_ago": "umbes päev tagasi", @@ -3787,10 +3805,11 @@ "unavailable": "Ei ole saadaval" }, "update_room_access_modal": { - "description": "Osalemiseks mõeldud lingi loomiseks on vaja, et külalised saavad selle jututoaga liituda. See aga võib muuta jututoa vähem turvaliseks. Kui sa oled aga kõne lõpetanud, siis saad soovi korral jututoa uuesti muuta privaatseks.", - "dont_change_description": "Teise võimalusena saad helistada eraldi jututoas", + "description": "Kui soovid luua jagatavat linki, siis pead selle jututoa tegema avalikuks või pead seadistustest lubama huvilistel paluda võimalust liitumiseks. See võimaldab külalistel liituda ilma kutseta.", + "dont_change_description": "Kui sa ei soovi selle jututoa ligipääsuõigusi muuta, siis teise võimalusena saad helistamiseks luua eraldi jututoa", "no_change": "Ma ei soovi muuta õigusi jututoas.", - "title": "Muuda õigusi jututoas" + "revert_access_description": "(Eelmise väärtuse saad taastada jututoa seadistustest: Turvalisus & Privaatsus / Ligipääs)", + "title": "Luba külalistel selle jututoaga liituda" }, "upload_failed_generic": "Faili '%(fileName)s' üleslaadimine ei õnnestunud.", "upload_failed_size": "Faili '%(fileName)s' suurus ületab serveris seadistatud üleslaadimise piiri", diff --git a/src/i18n/strings/fa.json b/src/i18n/strings/fa.json index 8f9f85c7b3..7b375d4db0 100644 --- a/src/i18n/strings/fa.json +++ b/src/i18n/strings/fa.json @@ -557,21 +557,11 @@ "empty_room_was_name": "اتاق خالی (نام قبلی: %(oldName)s)", "encryption": { "access_secret_storage_dialog": { - "enter_phrase_or_key_prompt": "عبارت امنیتی خود را وارد کنید و یا .", "key_validation_text": { - "invalid_security_key": "کلید امنیتی نامعتبر است", - "recovery_key_is_correct": "به نظر خوب میاد!", - "wrong_file_type": "نوع فایل اشتباه است", "wrong_security_key": "کلید امنیتی اشتباه است" }, - "reset_title": "همه چیز را بازراه‌اندازی (reset) کنید", - "reset_warning_1": "این کار را فقط درصورتی انجام دهید که دستگاه دیگری برای تکمیل فرآیند تأیید ندارید.", - "reset_warning_2": "اگر همه موارد را بازراه‌اندازی (reset) کنید، دیگر هیچ نشست تائید شده‌ای و هیچ کاربر تائيد‌ شده‌ای نخواهید داشت و ممکن است نتوانید پیام‌های گذشته‌ی خود را مشاهده نمائید.", "restoring": "بازیابی کلیدها از نسخه پشتیبان", - "security_key_title": "کلید امنیتی", - "security_phrase_incorrect_error": "دسترسی به حافظه نهان امکان‌پذیر نیست. لطفاً تأیید کنید که عبارت امنیتی صحیح را وارد کرده‌اید.", - "security_phrase_title": "عبارت امنیتی", - "use_security_key_prompt": "برای ادامه از کلید امنیتی خود استفاده کنید." + "security_key_title": "کلید امنیتی" }, "bootstrap_title": "تنظیم کلیدها", "cancel_entering_passphrase_description": "آیا مطمئن هستید که می خواهید وارد کردن عبارت امنیتی را لغو کنید؟", @@ -1501,7 +1491,6 @@ "remove_email_prompt": "%(email)s را پاک می‌کنید؟", "remove_msisdn_prompt": "%(phone)s را پاک می‌کنید؟" }, - "image_thumbnails": "پیش‌نمایش تصاویر را نشان بده", "inline_url_previews_default": "فعال‌سازی پیش‌نمایش URL به صورت پیش‌فرض", "inline_url_previews_room": "امکان پیش‌نمایش URL را به صورت پیش‌فرض برای اعضای این اتاق فعال کن", "inline_url_previews_room_account": "فعال‌سازی پیش‌نمایش URL برای این اتاق (تنها شما را تحت تاثیر قرار می‌دهد)", diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index a22378a860..291b460f2e 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -12,6 +12,7 @@ "one": "Yksi lukematon maininta." }, "recent_rooms": "Viimeisimmät huoneet", + "room_n_unread_invite": "Avaa huoneen %(roomName)s kutsu.", "room_name": "Huone %(name)s", "room_status_bar": "Huoneen tilapalkki", "seek_bar_label": "Äänen siirtymispalkki", @@ -90,7 +91,7 @@ "open": "Avaa", "open_menu": "Avaa valikko", "pause": "Keskeytä", - "pin": "Nuppineula", + "pin": "Kiinnitä", "play": "Toista", "proceed": "Jatka", "quote": "Lainaa", @@ -103,6 +104,7 @@ "reply": "Vastaa", "reply_in_thread": "Vastaa ketjuun", "report_content": "Ilmoita sisällöstä", + "report_room": "Ilmoita huoneesta", "resend": "Lähetä uudelleen", "reset": "Palauta alkutilaan", "resume": "Jatka", @@ -262,7 +264,7 @@ "error_other_device_already_signed_in": "Sinun ei tarvitse tehdä mitään muuta.", "error_other_device_already_signed_in_title": "Toinen laitteesi on jo kirjautunut sisään", "error_rate_limited": "Liikaa yrityksiä lyhyessä ajassa. Odota hetki, ennen kuin yrität uudelleen.", - "error_unexpected": "Tapahtui odottamaton virhe.", + "error_unexpected": "Tapahtui odottamaton virhe. Toisen laitteen yhdistämispyyntö on peruttu.", "error_unsupported_protocol": "Tämä laite ei tue kirjautumista toiseen laitteeseen QR-koodilla.", "error_unsupported_protocol_title": "Toinen laite ei ole yhteensopiva", "error_user_cancelled": "Kirjautuminen peruutettiin toisella laitteella.", @@ -706,6 +708,9 @@ "default_cover_photo": "Oletuskansikuva © Jesús Roncero, käytössä CC-BY-SA 4.0:n ehtojen mukaisesti.", "twemoji_colr": "twemoji-colr-fontti © Mozilla Foundation, käytössä Apache 2.0:n ehtojen mukaisesti." }, + "decline_invitation_dialog": { + "reason_description": "Kerro syy huoneen ilmoittamiseen." + }, "desktop_default_device_name": "%(brand)sin työpöytäversio: %(platformName)s", "devtools": { "active_widgets": "Aktiiviset sovelmat", @@ -740,6 +745,7 @@ "room_notifications_sender": "Lähettäjä: ", "room_notifications_total": "Yhteensä: ", "room_notifications_type": "Tyyppi: ", + "room_status": "Huoneen tila", "save_setting_values": "Tallenna asetusarvot", "server_info": "Palvelimen tiedot", "server_versions": "Palvelinversiot", @@ -783,18 +789,11 @@ "empty_room_was_name": "Tyhjä huone (oli %(oldName)s)", "encryption": { "access_secret_storage_dialog": { - "enter_phrase_or_key_prompt": "Kirjoita turvalause tai jatkaaksesi.", "key_validation_text": { - "invalid_security_key": "Virheellinen turva-avain", - "recovery_key_is_correct": "Hyvältä näyttää!", - "wrong_file_type": "Väärä tiedostotyyppi", - "wrong_security_key": "Väärä turva-avain" + "wrong_security_key": "Väärä palautusavain" }, "restoring": "Palautetaan avaimia varmuuskopiosta", - "security_key_title": "Turva-avain", - "security_phrase_title": "Turvalause", - "separator": "%(securityKey)s tai %(recoveryFile)s", - "use_security_key_prompt": "Käytä turva-avain jatkaaksesi." + "security_key_title": "Palautusavain" }, "bootstrap_title": "Otetaan avaimet käyttöön", "cancel_entering_passphrase_description": "Haluatko varmasti peruuttaa salasanan syöttämisen?", @@ -807,6 +806,7 @@ "cross_signing_user_normal": "Et ole varmentanut tätä käyttäjää.", "cross_signing_user_verified": "Olet varmentanut tämän käyttäjän. Tämä käyttäjä on varmentanut kaikki istuntonsa.", "cross_signing_user_warning": "Tämä käyttäjä ei ole varmentanut kaikkia istuntojaan.", + "enter_recovery_key": "Kirjoita palautusavain", "event_shield_reason_authenticity_not_guaranteed": "Tämän salatun viestin aitoutta ei voida taata tällä laitteella.", "event_shield_reason_mismatched_sender_key": "Salattu varmentamattoman istunnon toimesta", "export_unsupported": "Selaimesi ei tue vaadittuja kryptografisia laajennuksia", @@ -832,6 +832,7 @@ "reset_all_button": "Unohtanut tai kadottanut kaikki palautustavat? Nollaa kaikki", "set_up_recovery": "Määritä palautus", "set_up_recovery_later": "Ei nyt", + "set_up_recovery_toast_description": "Luo palautusavain, jota voit käyttää salatun viestihistorian palauttamiseen, jos menetät pääsyn laitteisiisi.", "set_up_toast_description": "Suojaudu salattuihin viesteihin ja tietoihin pääsyn menettämiseltä", "set_up_toast_title": "Määritä turvallinen varmuuskopio", "setup_secure_backup": { @@ -990,7 +991,8 @@ "image": "Kuva", "poll": "Kysely", "video": "Video" - } + }, + "preview": "%(prefix)s: %(preview)s" }, "export_chat": { "cancelled": "Vienti peruttu", @@ -1032,9 +1034,11 @@ "media_omitted": "Media jätetty pois", "media_omitted_file_size": "Media jätetty pois – tiedoston kokoraja ylitetty", "messages": "Viestit", + "next_page": "Seuraava viestiryhmä", "num_messages": "Viestien määrä", "num_messages_min_max": "Viestimäärän täytyy olla luku väliltä %(min)s…%(max)s", "number_of_messages": "Määritä viestien lukumäärä", + "previous_page": "Edellinen viestiryhmä", "processing": "Käsitellään…", "processing_event_n": "Käsitellään tapahtumaa %(number)s / %(total)s", "size_limit": "Kokoraja", @@ -1307,7 +1311,7 @@ "sliding_sync": "Liukuvan synkronoinnin tila", "sliding_sync_description": "Työn alla, käytöstä poistaminen ei ole mahdollista.", "sliding_sync_disabled_notice": "Poista käytöstä kirjautumalla ulos ja takaisin sisään", - "sliding_sync_server_no_support": "Palvelimellasi ei ole natiivitukea", + "sliding_sync_server_no_support": "Palvelimellasi ei ole tukea", "under_active_development": "Aktiivisen kehityksen kohteena.", "video_rooms": "Videohuoneet", "video_rooms_a_new_way_to_chat": "Uusi tapa keskustella äänen ja videon välityksellä %(brand)sissä.", @@ -1409,6 +1413,7 @@ "toast_description": "%(brand)s on mobiiliselaimissa kokeellinen. Paremman kokemuksen ja uusimmat ominaisuudet saat ilmaisella mobiilisovelluksellamme.", "toast_title": "Parempi kokemus sovelluksella" }, + "name_and_id": "%(name)s (%(userId)s)", "no_more_results": "Ei enempää tuloksia", "notif_panel": { "empty_description": "Sinulla ei ole näkyviä ilmoituksia.", @@ -1435,7 +1440,8 @@ "mentions_and_keywords_description": "Vastaanota ilmoitukset maininnoista ja asiasanoista asetuksissa määrittämälläsi tavalla", "mentions_keywords": "Maininnat ja avainsanat", "message_didnt_send": "Viestiä ei lähetetty. Lisätietoa napsauttamalla.", - "mute_description": "Et saa ilmoituksia" + "mute_description": "Et saa ilmoituksia", + "mute_room": "Mykistä huone" }, "notifier": { "m.key.verification.request": "%(name)s pyytää varmennusta" @@ -1529,6 +1535,7 @@ }, "redact": { "confirm_button": "Varmista poistaminen", + "confirm_description": "Haluatko varmasti poistaa tämän tapahtuman?", "error": "Et voi poistaa tätä viestiä. (%(code)s)", "ongoing": "Poistetaan…", "reason_label": "Syy (valinnainen)" @@ -1544,6 +1551,9 @@ "spam_or_propaganda": "Roskapostitusta tai propagandaa", "toxic_behaviour": "Myrkyllinen käyttäytyminen" }, + "report_room": { + "reason_label": "Kuvaile syy" + }, "restore_key_backup_dialog": { "count_of_decryption_failures": "%(failedCount)s istunnon purkaminen epäonnistui!", "count_of_successfully_restored_keys": "%(sessionCount)s avaimen palautus onnistui", @@ -1568,6 +1578,7 @@ "extensions_empty_title": "Paranna tuottavuutta työkaluilla, widgeteillä ja boteilla", "files_button": "Tiedostot", "pinned_messages": { + "empty_title": "Kiinnitä tärkeät viestit, jotta ne löytyvät helposti", "header": { "one": "1 kiinnitetty viesti", "other": "%(count)s kiinnitettyä viestiä" @@ -1742,7 +1753,9 @@ "pinned_message_badge": "Kiinnitetty viesti", "pinned_message_banner": { "button_view_all": "Näytä kaikki", - "description": "Tässä huoneessa on kiinnitettyjä viestejä. Napsauta nähdäksesi ne." + "description": "Tässä huoneessa on kiinnitettyjä viestejä. Napsauta nähdäksesi ne.", + "go_to_message": "Näytä kiinnitetty viesti aikajanalla.", + "title": "%(index)s/%(length)s kiinnitettyä viestiä" }, "read_topic": "Lue aihe napsauttamalla", "rejecting": "Hylätään kutsua…", @@ -1784,15 +1797,21 @@ "other": "Lähetetään %(filename)s ja %(count)s muuta" }, "uploading_single_file": "Lähetetään %(filename)s" - } + }, + "video_room": "Tämä huone on videohuone" }, "room_list": { "add_room_label": "Lisää huone", "add_space_label": "Lisää avaruus", + "appearance": "Ulkoasu", "breadcrumbs_empty": "Ei hiljattain vierailtuja huoneita", "breadcrumbs_label": "Hiljattain vieraillut huoneet", "empty": { + "no_chats": "Ei keskusteluja vielä", + "no_chats_description": "Aloita lähettämällä viestejä jollekin henkilölle tai luomalla huone", + "no_chats_description_no_room_rights": "Aloita lähettämällä viesti jollekin", "no_favourites": "Sinulla ei ole vielä suosikkikeskustelua", + "no_rooms": "Et ole vielä missään huoneessa", "no_unread": "Onnittelut! Sinulla ei ole lukemattomia viestejä", "show_chats": "Näytä kaikki keskustelut" }, @@ -1813,7 +1832,8 @@ "more_options": { "copy_link": "Kopioi huoneen linkki", "leave_room": "Poistu huoneesta", - "mark_read": "Merkitse luetuksi" + "mark_read": "Merkitse luetuksi", + "mark_unread": "Merkitse lukemattomaksi" }, "notification_options": "Ilmoitusasetukset", "open_space_menu": "Avaa avaruusvalikko", @@ -1825,6 +1845,7 @@ "open_room": "Avoin huone %(roomName)s" }, "show_less": "Näytä vähemmän", + "show_message_previews": "Näytä viestien esikatselut", "show_n_more": { "one": "Näytä %(count)s lisää", "other": "Näytä %(count)s lisää" @@ -1833,6 +1854,9 @@ "sort_by": "Lajittelutapa", "sort_by_activity": "Aktiivisuus", "sort_by_alphabet": "A-Ö", + "sort_type": { + "atoz": "A-Ö" + }, "sort_unread_first": "Näytä ensimmäisenä huoneet, joissa on lukemattomia viestejä", "space_menu": { "space_settings": "Avaruuden asetukset" @@ -2050,8 +2074,9 @@ }, "join_rule_upgrade_upgrading_room": "Päivitetään huonetta", "public_without_alias_warning": "Lisää osoite linkittääksesi tähän huoneeseen.", + "publish_room": "Aseta tämä huone näkyväksi julkisten huoneiden hakemistossa.", "publish_space": "Tee tämä avaruus näkyväksi julkisten huoneiden hakemistossa.", - "strict_encryption": "Älä lähetä salattuja viestejä vahvistamattomiin istuntoihin tässä huoneessa tässä istunnossa", + "strict_encryption": "Lähetä viestejä vain vahvistetuille käyttäjille.", "title": "Tietoturva ja yksityisyys" }, "title": "Huoneen asetukset — %(roomName)s", @@ -2162,6 +2187,7 @@ "code_block_line_numbers": "Näytä rivinumerot koodilohkoissa", "emoji_autocomplete": "Näytä emoji-ehdotuksia kirjoittaessa", "enable_markdown": "Ota Markdown käyttöön", + "enable_markdown_description": "Aloita viestit kirjoittamalla /plain lähettääksesi ilman markdownia.", "encryption": { "advanced": { "details_title": "Salauksen tiedot", @@ -2175,14 +2201,20 @@ "device_not_verified_title": "Laitetta ei ole vahvistettu", "dialog_title": "Asetukset: Salaus", "recovery": { + "change_recovery_confirm_button": "Vahvista uusi palautusavain", "change_recovery_confirm_title": "Anna uusi palautusavain", + "change_recovery_key": "Vaihda palautusavain", "change_recovery_key_title": "Vaihdetaanko palautusavain?", "description": "Palauta kryptografinen identiteettisi ja viestihistoriasi palautusavaimella, jos olet kadottanut kaikki olemassa olevat laitteesi.", "enter_key_error": "Kirjoittamasi palautusavain ei ole oikein.", + "enter_recovery_key": "Kirjoita palautusavain", "forgot_recovery_key": "Unohditko palautusavaimen?", "save_key_description": "Älä jaa tätä kenenkään kanssa!", + "save_key_title": "Palautusavain", "set_up_recovery": "Määritä palautus", "set_up_recovery_confirm_title": "Anna palautusavain vahvistaaksesi", + "set_up_recovery_save_key_title": "Tallenna palautusavain turvalliseen paikkaan", + "set_up_recovery_secondary_description": "Kun napsautat Jatka, sinulle luodaan palautusavain.", "title": "Palautuminen" }, "title": "Salaus" @@ -2204,6 +2236,7 @@ "avatar_remove_progress": "Poistetaan kuva...", "avatar_save_progress": "Lähetetään kuva…", "avatar_upload_error_text": "Tiedostomuoto ei ole tuettu tai kuva on suurempi kuin %(size)s.", + "avatar_upload_error_text_generic": "Tiedostomuotoa ei ehkä tueta.", "confirm_adding_email_body": "Napsauta alapuolella olevaa painiketta lisätäksesi tämän sähköpostiosoitteen.", "confirm_adding_email_title": "Vahvista sähköpostin lisääminen", "deactivate_confirm_body": "Haluatko varmasti poistaa tilisi pysyvästi?", @@ -2225,6 +2258,7 @@ "discovery_needs_terms_title": "Anna ihmisten löytää sinut", "display_name": "Näyttönimi", "display_name_error": "Näyttönimeä ei voi asettaa", + "email_adding_unsupported_by_hs": "Tämä kotipalvelin ei tue sähköpostiosoitteiden lisäämistä tiliisi.", "email_address_in_use": "Tämä sähköpostiosoite on jo käytössä", "email_address_label": "Sähköpostiosoite", "email_not_verified": "Sähköpostiosoitettasi ei ole vielä varmistettu", @@ -2239,6 +2273,7 @@ "error_invalid_email_detail": "Tämä ei vaikuta olevan kelvollinen sähköpostiosoite", "error_msisdn_verification": "Puhelinnumeron vahvistaminen epäonnistui.", "error_password_change_403": "Salasanan vaihtaminen epäonnistui. Onko salasanasi oikein?", + "error_password_change_http": "%(errorMessage)s (HTTP-tila %(httpStatus)s)", "error_password_change_title": "Virhe salasanan vaihtamisessa", "error_password_change_unknown": "Tuntematon salasananvaihtovirhe (%(stringifiedError)s)", "error_remove_3pid": "Yhteystietojen poistaminen epäonnistui", @@ -2247,7 +2282,9 @@ "error_share_email_discovery": "Sähköpostiosoitetta ei voi jakaa", "error_share_msisdn_discovery": "Puhelinnumeroa ei voi jakaa", "identity_server_not_set": "Identiteettipalvelinta ei ole asetettu", + "invalid_phone_number": "Annettu puhelinnumero ei vaikuta olevan kelvollinen.", "language_section": "Kieli", + "msisdn_adding_unsupported_by_hs": "Tämä kotipalvelin ei tue puhelinnumeroiden lisäämistä tilillesi.", "msisdn_in_use": "Puhelinnumero on jo käytössä", "msisdn_label": "Puhelinnumero", "msisdn_verification_field_label": "Varmennuskoodi", @@ -2265,7 +2302,6 @@ "unable_to_load_msisdns": "Puhelinnumeroita ei voi ladata", "username": "Käyttäjätunnus" }, - "image_thumbnails": "Näytä kuvien esikatselut/pienoiskuvat", "inline_url_previews_default": "Ota linkkien esikatselu käyttöön oletusarvoisesti", "inline_url_previews_room": "Ota linkkien esikatselu käyttöön kaikille huoneen jäsenille", "inline_url_previews_room_account": "Ota linkkien esikatselut käyttöön tässä huoneessa (koskee ainoastaan sinua)", @@ -2323,6 +2359,12 @@ "labs_mjolnir": { "dialog_title": "Asetukset: Ohitetut käyttäjät" }, + "media_preview": { + "hide_media": "Piilota aina", + "media_preview_description": "Piilotetun median voi aina näyttää napauttamalla sitä", + "media_preview_label": "Näytä media aikajanalla", + "show_media": "Näytä aina" + }, "notifications": { "desktop_notification_message_preview": "Näytä viestin esikatselu työpöytäilmoituksessa", "dialog_title": "Asetukset: Ilmoitukset", @@ -2449,6 +2491,7 @@ "device_verified_description": "Tämä istunto on valmis turvallista viestintää varten.", "device_verified_description_current": "Nykyinen istuntosi on valmis turvalliseen viestintään.", "dialog_title": "Asetukset: Istunnot", + "error_set_name": "Istunnon nimen asettaminen epäonnistui", "filter_all": "Kaikki", "filter_inactive": "Passiivinen", "filter_inactive_description": "Passiivinen %(inactiveAgeDays)s päivää tai pidempään", @@ -2461,6 +2504,7 @@ "inactive_sessions_list_description": "Harkitse vanhoista (%(inactiveAgeDays)s tai useamman päivän ikäisistä), käyttämättömistä istunnoista uloskirjautumista.", "ip": "IP-osoite", "last_activity": "Viimeisin toiminta", + "manage": "Hallitse tätä istuntoa", "mobile_session": "Mobiili-istunto", "n_sessions_selected": { "one": "%(count)s istunto valittu", @@ -2512,6 +2556,7 @@ "verify_session": "Vahvista istunto", "web_session": "Web-istunto" }, + "show_avatar_changes": "Näytä profiilikuvan muutokset", "show_breadcrumbs": "Näytä oikotiet viimeksi katsottuihin huoneisiin huoneluettelon yläpuolella", "show_chat_effects": "Näytä keskustelutehosteet (animaatiot, kun saat esim. konfettia)", "show_displayname_changes": "Näytä näyttönimien muutokset", @@ -2758,7 +2803,7 @@ "heading_without_query": "Etsittävät kohteet", "join_button_text": "Liity %(roomAddress)s", "keyboard_scroll_hint": "Käytä vierittääksesi", - "message_search_section_title": "Muut haut", + "messages_label": "Viestit", "other_rooms_in_space": "Muut huoneet avaruudessa %(spaceName)s", "public_rooms_label": "Julkiset huoneet", "public_spaces_label": "Julkiset avaruudet", @@ -2766,7 +2811,6 @@ "recently_viewed_section_title": "Äskettäin katsottu", "result_may_be_hidden_privacy_warning": "Jotkin tulokset saatetaan piilottaa tietosuojan takia", "result_may_be_hidden_warning": "Jotkin tulokset saatetaan piilottaa", - "search_messages_hint": "Etsi viesteistä huoneen yläosassa olevalla kuvakkeella ", "spaces_title": "Avaruudet, joissa olet", "start_group_chat_button": "Aloita ryhmäkeskustelu" }, @@ -2853,9 +2897,11 @@ "historical_event_user_not_joined": "Sinulla ei ole pääsyä tähän viestiin", "unable_to_decrypt": "Viestin salausta ei voi purkaa" }, + "disambiguated_profile": "%(displayName)s (%(matrixId)s)", "download_action_decrypting": "Puretaan salausta", "download_action_downloading": "Ladataan", "download_failed": "Lataus epäonnistui", + "download_failed_description": "Tiedostoa ladattaessa tapahtui virhe", "edits": { "tooltip_label": "Muokattu %(date)s. Napsauta nähdäksesi muokkaukset.", "tooltip_sub": "Napsauta nähdäksesi muokkaukset", @@ -3277,6 +3323,9 @@ "toast_title": "Päivitä %(brand)s", "unavailable": "Ei saatavilla" }, + "update_room_access_modal": { + "title": "Salli vieraskäyttäjien liittyä tähän huoneeseen" + }, "upload_failed_generic": "Tiedoston '%(fileName)s' lähettäminen ei onnistunut.", "upload_failed_size": "Tiedoston '%(fileName)s' koko ylittää tämän kotipalvelimen lähetettyjen tiedostojen ylärajan", "upload_failed_title": "Lähetys epäonnistui", @@ -3286,6 +3335,7 @@ "error_files_too_large": "Tiedostot ovat liian isoja lähetettäväksi. Tiedoston kokoraja on %(limit)s.", "error_some_files_too_large": "Osa tiedostoista on liian isoja lähetettäväksi. Tiedoston kokoraja on %(limit)s.", "error_title": "Lähetysvirhe", + "not_image": "Valitsemasi tiedosto ei ole kelvollinen kuvatiedosto.", "title": "Lähetä tiedostot", "title_progress": "Lähettää tiedostoa (%(current)s / %(total)s)", "upload_all_button": "Lähetä kaikki palvelimelle", diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 22de0284ec..9992d5e51f 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -157,6 +157,7 @@ "view_message": "Afficher le message", "view_source": "Voir la source", "yes": "Oui", + "yes_dismiss": "Oui, rejeter", "zoom_in": "Zoomer", "zoom_out": "Dé-zoomer" }, @@ -386,6 +387,7 @@ "fallback_button": "Commencer l’authentification", "mas_cross_signing_reset_cta": "Accédez à votre compte", "mas_cross_signing_reset_description": "Réinitialisez votre identité par l’intermédiaire de votre fournisseur de compte, puis revenez et cliquez sur « Réessayer ».", + "mas_cross_signing_reset_title": "Accédez à votre compte pour réinitialiser votre identité", "msisdn": "Un message a été envoyé à %(msisdn)s", "msisdn_token_incorrect": "Jeton incorrect", "msisdn_token_prompt": "Merci de saisir le code qu’il contient :", @@ -525,6 +527,7 @@ "message_timestamp_invalid": "Horodatage non valide", "microphone": "Micro", "model": "Modèle", + "moderation_and_safety": "Modération et sécurité", "modern": "Moderne", "mute": "Mettre en sourdine", "n_members": { @@ -908,22 +911,11 @@ "empty_room_was_name": "Salon vide (précédemment %(oldName)s)", "encryption": { "access_secret_storage_dialog": { - "enter_phrase_or_key_prompt": "Saisissez votre phrase de sécurité ou pour continuer.", "key_validation_text": { - "invalid_security_key": "Clé de récupération incorrecte", - "recovery_key_is_correct": "Ça a l’air correct !", - "wrong_file_type": "Mauvais type de fichier", "wrong_security_key": "Clé de récupération incorrecte" }, - "reset_title": "Tout réinitialiser", - "reset_warning_1": "Poursuivez seulement si vous n’avez aucun autre appareil avec lequel procéder à la vérification.", - "reset_warning_2": "Si vous réinitialisez tout, vous allez repartir sans session et utilisateur de confiance. Vous pourriez ne pas voir certains messages passés.", "restoring": "Restauration des clés depuis la sauvegarde", - "security_key_title": "Clé de récupération", - "security_phrase_incorrect_error": "Impossible d’accéder à l’espace de stockage sécurisé. Merci de vérifier que vous avez saisi la bonne phrase de sécurité.", - "security_phrase_title": "Phrase de sécurité", - "separator": "%(securityKey)s ou %(recoveryFile)s", - "use_security_key_prompt": "Utilisez votre clé de récupération pour continuer." + "security_key_title": "Clé de récupération" }, "bootstrap_title": "Configuration des clés", "cancel_entering_passphrase_description": "Souhaitez-vous vraiment annuler la saisie de la phrase de passe ?", @@ -979,6 +971,8 @@ "setup_secure_backup": { "explainer": "Sauvegardez vos clés avant de vous déconnecter pour éviter de les perdre." }, + "turn_on_key_storage": "Activer le stockage des clés", + "turn_on_key_storage_description": "Stockez votre identité cryptographique et vos clés de message en toute sécurité sur le serveur. Cela vous permettra de consulter l'historique de vos messages sur tous les nouveaux appareils.", "udd": { "interactive_verification_button": "Vérifier de façon interactive avec des émojis", "other_ask_verify_text": "Demandez à cet utilisateur de vérifier sa session, ou vérifiez-la manuellement ci-dessous.", @@ -992,7 +986,6 @@ "accepting": "Acceptation…", "after_new_login": { "device_verified": "Appareil vérifié", - "reset_confirmation": "Réinitialiser les clés de vérification, c’est certain ?", "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" @@ -1063,8 +1056,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_reset_warning_1": "La réinitialisation de vos clés de vérification ne peut pas être annulé. Après la réinitialisation, vous n’aurez plus accès à vos anciens messages chiffrés, et tous les amis que vous aviez précédemment vérifiés verront des avertissement de sécurité jusqu'à ce vous les vérifiiez à nouveau.", - "verify_reset_warning_2": "Veuillez ne continuer que si vous êtes certain d’avoir perdu tous vos autres appareils et votre clé de récupération.", "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", @@ -2100,6 +2091,7 @@ "room_list": { "add_room_label": "Ajouter un salon", "add_space_label": "Ajouter un espace", + "appearance": "Apparence", "breadcrumbs_empty": "Aucun salon visité récemment", "breadcrumbs_label": "Salons visités récemment", "empty": { @@ -2108,11 +2100,14 @@ "no_chats_description_no_room_rights": "Commencez par envoyer un message à quelqu'un", "no_favourites": "Vous n'avez pas encore de discussion favorite", "no_favourites_description": "Vous pouvez ajouter une discussion à vos favoris dans les paramètres de discussion", + "no_invites": "Vous n'avez aucune invitation non lue", + "no_mentions": "Vous n'avez aucune mention non lue", "no_people": "Vous n'avez encore de discussions", "no_people_description": "Veuillez désélectionner des filtres pour voir vos discussions", "no_rooms": "Vous n’êtes membre d’aucun salon", "no_rooms_description": "Veuillez désélectionner des filtres pour voir vos discussions", "no_unread": "Félicitations ! Vous n'avez aucun message non lu", + "show_activity": "Voir toutes les activités", "show_chats": "Afficher toutes les discussions" }, "failed_add_tag": "Échec de l’ajout de l’étiquette %(tagName)s au salon", @@ -2120,6 +2115,8 @@ "failed_set_dm_tag": "Échec de l’ajout de l’étiquette de conversation privée", "filters": { "favourite": "Favoris", + "invites": "Invitations", + "mentions": "Mentions", "people": "Personnes", "rooms": "Salons", "unread": "Non-lus" @@ -2150,15 +2147,22 @@ "more_options": "Plus d’options", "open_room": "Ouvrir salon %(roomName)s" }, + "room_options": "Options du salon", "show_less": "En voir moins", + "show_message_previews": "Afficher les aperçus des messages", "show_n_more": { "other": "En afficher %(count)s de plus", "one": "En afficher %(count)s de plus" }, "show_previews": "Afficher un aperçu des messages", + "sort": "Trier", "sort_by": "Trier par", "sort_by_activity": "Activité", "sort_by_alphabet": "A-Z", + "sort_type": { + "activity": "Activité", + "atoz": "A-Z" + }, "sort_unread_first": "Afficher les salons non lus en premier", "space_menu": { "home": "Accueil de l’espace", @@ -2446,6 +2450,10 @@ "recent_changes_heading": "Changements récents qui n’ont pas encore été reçus", "title": "Le serveur ne répond pas" }, + "service_worker_error": { + "description": "%(brand)s nécessite un service worker pour charger les médias authentifiés à partir des référentiels de contenu Matrix. Ceci n'est pas pris en charge par votre navigateur. Il est donc possible que le contenu multimédia ne se charge pas.", + "title": "Échec du chargement du service worker" + }, "seshat": { "error_initialising": "Échec de l’initialisation de la recherche de messages, vérifiez vos paramètres pour plus d’information", "reset_button": "Réinitialiser le magasin d’évènements", @@ -2539,6 +2547,8 @@ "session_key": "Clé de session :", "title": "Avancé" }, + "confirm_key_storage_off": "Êtes-vous sûr de vouloir désactiver le stockage des clés ?", + "confirm_key_storage_off_description": "Si vous vous déconnectez de tous vos appareils, vous perdrez l'historique de vos messages et vous devrez vérifier à nouveau tous vos contacts existants. En savoir plus ", "delete_key_storage": { "breadcrumb_page": "Désactiver la sauvegarde", "confirm": "Désactiver la sauvegarde", @@ -2624,6 +2634,7 @@ "discovery_needs_terms_title": "Laissez les gens vous trouver", "display_name": "Nom d'affichage", "display_name_error": "Impossible de définir le nom d'affichage", + "email_adding_unsupported_by_hs": "Ce serveur d'accueil ne prend pas en charge l'ajout d'adresses e-mail à votre compte.", "email_address_in_use": "Cette adresse e-mail est déjà utilisée", "email_address_label": "Adresse e-mail", "email_not_verified": "Votre adresse e-mail n’a pas encore été vérifiée", @@ -2648,7 +2659,9 @@ "error_share_msisdn_discovery": "Impossible de partager le numéro de téléphone", "identity_server_no_token": "Aucun jeton d’accès d’identité trouvé", "identity_server_not_set": "Serveur d'identité non défini", + "invalid_phone_number": "Le numéro de téléphone fourni est invalide.", "language_section": "Langue", + "msisdn_adding_unsupported_by_hs": "Ce serveur d'accueil ne prend pas en charge l'ajout de numéros de téléphone à votre compte.", "msisdn_in_use": "Ce numéro de téléphone est déjà utilisé", "msisdn_label": "Numéro de téléphone", "msisdn_verification_field_label": "Code de vérification", @@ -2667,12 +2680,10 @@ "unable_to_load_msisdns": "Impossible de charger les numéros de téléphone", "username": "Nom d’utilisateur" }, - "image_thumbnails": "Afficher les aperçus/vignettes pour les images", "inline_url_previews_default": "Activer l’aperçu des URL par défaut", "inline_url_previews_room": "Activer l’aperçu des URL par défaut pour les participants de ce salon", "inline_url_previews_room_account": "Activer l’aperçu des URL pour ce salon (n’affecte que vous)", "insert_trailing_colon_mentions": "Insérer deux-points après les mentions de l'utilisateur au début d'un message", - "invite_avatars": "Afficher les avatars des salons dans lesquels vous avez été invité", "jump_to_bottom_on_send": "Sauter en bas du fil de discussion lorsque vous envoyez un message", "key_backup": { "backup_in_progress": "Vous clés sont en cours de sauvegarde (la première sauvegarde peut prendre quelques minutes).", @@ -2731,6 +2742,14 @@ "labs_mjolnir": { "dialog_title": "Paramètres : Utilisateurs ignorés" }, + "media_preview": { + "hide_avatars": "Masquer les avatars des salons et des invitations", + "hide_media": "Toujours masquer", + "media_preview_description": "Un média masqué peut toujours être affiché en cliquant dessus", + "media_preview_label": "Afficher les médias dans les discussions", + "show_in_private": "Dans les salons privés", + "show_media": "Toujours afficher" + }, "notifications": { "default_setting_description": "Ce réglage sera appliqué par défaut à tous vos salons.", "default_setting_section": "Je veux être notifié pour (réglage par défaut)", @@ -3214,7 +3233,7 @@ "heading_without_query": "Recherche de", "join_button_text": "Rejoindre %(roomAddress)s", "keyboard_scroll_hint": "Utilisez pour faire défiler", - "message_search_section_title": "Autres recherches", + "messages_label": "Messages", "other_rooms_in_space": "Autres salons dans %(spaceName)s", "public_rooms_label": "Salons publics", "public_spaces_label": "Espaces publics", @@ -3224,7 +3243,6 @@ "result_may_be_hidden_privacy_warning": "Certains résultats pourraient être masqués pour des raisons de confidentialité", "result_may_be_hidden_warning": "Certains résultats peuvent être cachés", "search_dialog": "Fenêtre de recherche", - "search_messages_hint": "Pour chercher des messages, repérez cette icône en haut à droite d'un salon ", "spaces_title": "Espaces où vous êtes", "start_group_chat_button": "Démarrer une conversation de groupe" }, @@ -3273,9 +3291,7 @@ "threads_activity_centre": { "header": "Activité des fils de discussions", "no_rooms_with_threads_notifs": "Vous n’avez pas encore de salons avec des notifications de fil de discussion.", - "no_rooms_with_unread_threads": "Vous n'avez pas encore de salons contenant des fils de discussion non lus.", - "release_announcement_description": "Les notifications des fils de discussion ont été déplacées. À partir de maintenant, retrouvez-les ici.", - "release_announcement_header": "Centre d'activité des fils de discussions" + "no_rooms_with_unread_threads": "Vous n'avez pas encore de salons contenant des fils de discussion non lus." }, "time": { "about_day_ago": "il y a environ un jour", @@ -3786,10 +3802,11 @@ "unavailable": "Indisponible" }, "update_room_access_modal": { - "description": "Pour créer un lien de partage, vous devez autoriser les invités à rejoindre ce salon. Cela peut rendre le salon moins sûr. Lorsque vous aurez terminé l'appel, vous pourrez redéfinir la confidentialité du salon.", - "dont_change_description": "Vous pouvez également prendre l'appel dans un salon séparé.", + "description": "Pour créer un lien de partage, rendez ce salon publique ou activer l 'option permettant aux utilisateurs de demander à rejoindre. Cela permet aux invités de participer sans être invités.", + "dont_change_description": "Si vous ne souhaitez pas modifier l'accès à ce salon, vous pouvez créer un nouveau salon pour le lien d'appel.", "no_change": "Je ne souhaite pas modifier le niveau d'accès.", - "title": "Modifier le niveau d'accès du salon" + "revert_access_description": "(Ceci peut être rétabli à sa valeur précédente dans les paramètres du salon : Sécurité et confidentialité/Accès)", + "title": "Autoriser les utilisateurs invités à rejoindre ce salon" }, "upload_failed_generic": "Le fichier « %(fileName)s » n’a pas pu être envoyé.", "upload_failed_size": "Le fichier « %(fileName)s » dépasse la taille limite autorisée par ce serveur pour les envois", diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index 46f1ca23f2..be37d40bc6 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -669,22 +669,11 @@ "empty_room_was_name": "Sala baleira (era %(oldName)s)", "encryption": { "access_secret_storage_dialog": { - "enter_phrase_or_key_prompt": "Escribe a túa Frase de Seguridade ou para continuar.", "key_validation_text": { - "invalid_security_key": "Chave de Seguridade non válida", - "recovery_key_is_correct": "Pinta ben!", - "wrong_file_type": "Tipo de ficheiro erróneo", "wrong_security_key": "Chave de Seguridade incorrecta" }, - "reset_title": "Restablecer todo", - "reset_warning_1": "Fai isto únicamente se non tes outro dispositivo co que completar a verificación.", - "reset_warning_2": "Se restableces todo, volverás a comezar sen sesións verificadas, usuarias de confianza, e poderías non poder ver as mensaxes anteriores.", "restoring": "Restablecendo chaves desde a copia", - "security_key_title": "Chave de Seguridade", - "security_phrase_incorrect_error": "Non se puido acceder ao almacenaxe segredo. Comproba que escribiches correctamente a Frase de Seguridade.", - "security_phrase_title": "Frase de seguridade", - "separator": "%(securityKey)s ou %(recoveryFile)s", - "use_security_key_prompt": "Usa a túa Chave de Seguridade para continuar." + "security_key_title": "Chave de Seguridade" }, "bootstrap_title": "Configurando as chaves", "cancel_entering_passphrase_description": "¿Estás seguro de que non queres escribir a frase de paso?", @@ -741,7 +730,6 @@ "accepting": "Aceptando…", "after_new_login": { "device_verified": "Dispositivo verificado", - "reset_confirmation": "Queres restablecer as chaves de verificación?", "skip_verification": "Omitir a verificación por agora", "unable_to_verify": "Non se puido verificar este dispositivo", "verify_this_device": "Verifica este dispositivo" @@ -804,7 +792,6 @@ "verify_emoji_prompt": "Verficación por comparación de emoticonas.", "verify_emoji_prompt_qr": "Se non podes escanear o código superior, verifica comparando as emoticonas.", "verify_later": "Verificarei máis tarde", - "verify_reset_warning_1": "O restablecemento das chaves de seguridade non se pode desfacer. Tras o restablecemento, non terás acceso ás antigas mensaxes cifradas, e calquera amizade que verificaras con anterioridade vai ver un aviso de seguridade ata que volvades a verificarvos mutuamente.", "verify_using_device": "Verifica usando outro dispositivo", "verify_using_key": "Verificar coa Chave de Seguridade", "verify_using_key_or_phrase": "Verificar coa Chave ou Frase de Seguridade", @@ -1971,7 +1958,6 @@ "remove_msisdn_prompt": "Eliminar %(phone)s?", "spell_check_locale_placeholder": "Elixe o idioma" }, - "image_thumbnails": "Mostrar miniaturas/vista previa das imaxes", "inline_url_previews_default": "Activar por defecto as vistas previas en liña de URL", "inline_url_previews_room": "Activar a vista previa de URL por defecto para as participantes nesta sala", "inline_url_previews_room_account": "Activar vista previa de URL nesta sala (só che afecta a ti)", @@ -2373,7 +2359,6 @@ "heading_without_query": "Buscar", "join_button_text": "Unirse a %(roomAddress)s", "keyboard_scroll_hint": "Usa para desprazarte", - "message_search_section_title": "Outras buscas", "other_rooms_in_space": "Outras salas en %(spaceName)s", "public_rooms_label": "Salas públicas", "recent_searches_section_title": "Buscas recentes", @@ -2382,7 +2367,6 @@ "result_may_be_hidden_privacy_warning": "Algúns resultados poden estar agochados por privacidade", "result_may_be_hidden_warning": "Algúns resultados poderían estar agochados", "search_dialog": "Diálogo de busca", - "search_messages_hint": "Para buscar mensaxes, busca esta icona arriba de todo na sala ", "spaces_title": "Espazos nos que estás", "start_group_chat_button": "Inicia un chat en grupo" }, diff --git a/src/i18n/strings/he.json b/src/i18n/strings/he.json index f13860415a..22a1cecc89 100644 --- a/src/i18n/strings/he.json +++ b/src/i18n/strings/he.json @@ -568,16 +568,10 @@ "encryption": { "access_secret_storage_dialog": { "key_validation_text": { - "invalid_security_key": "מפתח אבטחה לא חוקי", - "recovery_key_is_correct": "נראה טוב!", - "wrong_file_type": "סוג קובץ שגוי", "wrong_security_key": "מפתח אבטחה שגוי" }, "restoring": "שחזור מפתחות מגיבוי", - "security_key_title": "מפתח אבטחה", - "security_phrase_incorrect_error": "אין אפשרות לגשת לאחסון הסודי. אנא אשר שהזנת את ביטוי האבטחה הנכון.", - "security_phrase_title": "ביטוי אבטחה", - "use_security_key_prompt": "השתמש במפתח האבטחה שלך כדי להמשיך." + "security_key_title": "מפתח אבטחה" }, "bootstrap_title": "מגדיר מפתחות", "cancel_entering_passphrase_description": "האם אתם בטוחים שהינכם רוצים לבטל?", @@ -1621,7 +1615,6 @@ "remove_email_prompt": "הסר כתובות %(email)s ?", "remove_msisdn_prompt": "הסר מספרי %(phone)s ?" }, - "image_thumbnails": "הראה תצוגה מקדימה\\ממוזערת של תמונות", "inline_url_previews_default": "אפשר צפייה של תצוגת קישורים בצאט כברירת מחדל", "inline_url_previews_room": "אפשר לחברים בחדר זה לצפות בתצוגת קישורים", "inline_url_previews_room_account": "הראה תצוגה מקדימה של קישורים בחדר זה (משפיע רק עליכם)", diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 70a26d40aa..76f822fee4 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -15,12 +15,12 @@ "room_messsage_not_sent": "A(z) %(roomName)s szoba megnyitása nem beállított üzenettel.", "room_n_unread_invite": "A(z) %(roomName)s szoba meghívásának megnyitása.", "room_n_unread_messages": { - "one": "A(z) %(roomName)s szoba megnyitása 1 olvasatlan üzenettel.", - "other": "A(z) %(roomName)s szoba megnyitása %(count)s olvasatlan üzenettel." + "A(z) %(roomName)s szoba megnyitása 1 olvasatlan üzenettel.": "one", + "A(z) %(roomName)s szoba megnyitása %(count)s olvasatlan üzenettel.": "other" }, "room_n_unread_messages_mentions": { - "one": "A(z) %(roomName)s szoba megnyitása 1 olvasatlan megemlítéssel.", - "other": "A(z) %(roomName)s szoba megnyitása %(count)s olvasatlan megemlítéssel." + "A(z) %(roomName)s szoba megnyitása 1 olvasatlan megemlítéssel.": "one", + "A(z) %(roomName)s szoba megnyitása %(count)s olvasatlan megemlítéssel.": "other" }, "room_name": "Szoba: %(name)s", "room_status_bar": "Szoba állapotsora", @@ -157,6 +157,7 @@ "view_message": "Üzenet megjelenítése", "view_source": "Forrás megjelenítése", "yes": "Igen", + "yes_dismiss": "Igen, elvetés", "zoom_in": "Nagyítás", "zoom_out": "Kicsinyítés" }, @@ -314,12 +315,12 @@ "confirm_new_password": "Új jelszó megerősítése", "devices_logout_success": "Az összes eszközéről kijelentkezett és leküldéses értesítéseket sem fog kapni. Az értesítések újbóli engedélyezéséhez újra be kell jelentkezni az egyes eszközökön.", "other_devices_logout_warning_1": "A kijelentkezéssel az üzeneteket titkosító kulcsokat az eszközök törlik magukról ami elérhetetlenné teheti a régi titkosított csevegéseket.", - "other_devices_logout_warning_2": "Ha szeretné megtartani a hozzáférést a titkosított szobákban lévő csevegésekhez, állítson be Kulcs mentést vagy exportálja ki a kulcsokat valamelyik eszközéről mielőtt továbblép.", + "other_devices_logout_warning_2": "Ha szeretné megtartani a hozzáférést a titkosított szobákban lévő csevegésekhez, állítson be kulcsmentést vagy exportálja a kulcsokat valamelyik eszközéről mielőtt továbblép.", "password_not_entered": "Új jelszót kell megadni.", "passwords_mismatch": "Az új jelszavaknak meg kell egyezniük egymással.", "rate_limit_error": "Rövid idő alatt túl sok próbálkozás. Várjon egy kicsit mielőtt újra próbálkozik.", "rate_limit_error_with_time": "Rövid idő alatt túl sok próbálkozás. Próbálkozzon ennyi idő múlva: %(timeout)s.", - "reset_successful": "A jelszava alaphelyzetbe lett állítva.", + "reset_successful": "A jelszava vissza lett állítva.", "return_to_login": "Vissza a bejelentkezési képernyőre", "sign_out_other_devices": "Kijelentkezés az összes eszközről" }, @@ -329,7 +330,7 @@ "reset_password_email_field_required_invalid": "E-mail-cím megadása (ezen a Matrix-kiszolgálón kötelező)", "reset_password_email_not_associated": "Úgy tűnik, hogy ez az e-mail-cím nincs összekötve Matrix-azonosítóval ezen a Matrix-kiszolgálón.", "reset_password_email_not_found_title": "Az e-mail-cím nem található", - "reset_password_title": "Jelszó megváltoztatása", + "reset_password_title": "Jelszó visszaállítása", "server_picker_custom": "Másik Matrix-kiszolgáló", "server_picker_description": "Használhatja az egyéni kiszolgáló lehetőséget, hogy egy másik Matrix-kiszolgáló címének megadásával jelentkezzen be. Ezzel használhatja az %(brand)s klienst egy már létező Matrix-fiókkal, egy másik Matrix-kiszolgálón.", "server_picker_description_matrix.org": "Csatlakozzon több millió felhasználóhoz ingyen, a legnagyobb nyilvános kiszolgálón", @@ -385,7 +386,8 @@ "email_resent": "Újraküldve!", "fallback_button": "Hitelesítés indítása", "mas_cross_signing_reset_cta": "Ugrás a fiókjához", - "mas_cross_signing_reset_description": "Állítsa vissza személyazonosságát a fiókszolgáltatón keresztül, majd térjen vissza, és kattintson az „Újra próbálkozás” gombra.", + "mas_cross_signing_reset_description": "Állítsa alaphelyzetbe személyazonosságát a fiókszolgáltatón keresztül, majd térjen vissza, és kattintson az „Újra” gombra.", + "mas_cross_signing_reset_title": "Ugorjon a fiókjához a személyazonossága alaphelyzetbe állításához", "msisdn": "Szöveges üzenet küldve ide: %(msisdn)s", "msisdn_token_incorrect": "Helytelen token", "msisdn_token_prompt": "Adja meg a benne lévő kódot:", @@ -407,7 +409,7 @@ "unsupported_auth_msisdn": "Ez a kiszolgáló nem támogatja a telefonszámmal történő hitelesítést.", "username_field_required_invalid": "Felhasználónév megadása", "username_in_use": "Ez a felhasználónév már foglalt, próbáljon ki másikat.", - "verify_email_explainer": "Mielőtt beállíthatja a jelszót, tudnunk kell, hogy tényleg az, akinek mondja magát. Kattintson a hivatkozásra az e-mailben, melyet épp most küldtünk ide: %(email)s", + "verify_email_explainer": "Mielőtt visszaállíthatja a jelszót, tudnunk kell, hogy tényleg az, akinek mondja magát. Kattintson a hivatkozásra az e-mailben, melyet épp most küldtünk ide: %(email)s", "verify_email_heading": "Erősítse meg az e-mailt a továbblépéshez" }, "bug_reporting": { @@ -525,6 +527,7 @@ "message_timestamp_invalid": "Érvénytelen időbélyeg", "microphone": "Mikrofon", "model": "Modell", + "moderation_and_safety": "Moderálás és biztonság", "modern": "Modern", "mute": "Némítás", "n_members": { @@ -905,22 +908,11 @@ "empty_room_was_name": "Üres szoba (%(oldName)s volt)", "encryption": { "access_secret_storage_dialog": { - "enter_phrase_or_key_prompt": "Adja meg a biztonsági jelmondatot vagy a folytatáshoz.", "key_validation_text": { - "invalid_security_key": "Érvénytelen helyreállítási kulcs", - "recovery_key_is_correct": "Jónak tűnik!", - "wrong_file_type": "A fájltípus hibás", "wrong_security_key": "Hibás helyreállítási kulcs" }, - "reset_title": "Minden alaphelyzetbe állítása", - "reset_warning_1": "Csak akkor tegye meg, ha egyetlen másik eszköze sincs az ellenőrzés elvégzéséhez.", - "reset_warning_2": "Ha mindent alaphelyzetbe állít, akkor nem lesz megbízható munkamenete, nem lesznek megbízható felhasználók és a régi üzenetekhez sem biztos, hogy hozzáfér majd.", "restoring": "Kulcsok helyreállítása mentésből", - "security_key_title": "Helyreállítási kulcs", - "security_phrase_incorrect_error": "A biztonsági tárolóhoz nem lehet hozzáférni. Kérjük ellenőrizze, hogy jó Biztonsági jelmondatot adott-e meg.", - "security_phrase_title": "Biztonsági jelmondat", - "separator": "%(securityKey)s vagy %(recoveryFile)s", - "use_security_key_prompt": "A folytatáshoz használja a helyreállítási kulcsot." + "security_key_title": "Helyreállítási kulcs" }, "bootstrap_title": "Kulcsok beállítása", "cancel_entering_passphrase_description": "Biztos, hogy megszakítja a jelmondat bevitelét?", @@ -976,6 +968,8 @@ "setup_secure_backup": { "explainer": "Mentse a kulcsait a kiszolgálóra kijelentkezés előtt, hogy ne veszítse el azokat." }, + "turn_on_key_storage": "A kulcstárolás bekapcsolása", + "turn_on_key_storage_description": "Tárolja biztonságosan a kriptográfiai azonosító- és üzenetkulcsokat a kiszolgálón. Ez lehetővé teszi az üzenetek előzményeinek megtekintését bármely új eszközön.", "udd": { "interactive_verification_button": "Interaktív ellenőrzés emodzsikkal", "other_ask_verify_text": "Kérje meg a felhasználót, hogy hitelesítse a munkamenetét, vagy ellenőrizze kézzel lentebb.", @@ -989,7 +983,6 @@ "accepting": "Elfogadás…", "after_new_login": { "device_verified": "Eszköz ellenőrizve", - "reset_confirmation": "Biztos, hogy lecseréli az ellenőrzési kulcsokat?", "skip_verification": "Ellenőrzés kihagyása most", "unable_to_verify": "Ennek az eszköznek az ellenőrzése nem lehetséges", "verify_this_device": "Az eszköz ellenőrzése" @@ -1060,8 +1053,6 @@ "verify_emoji_prompt": "Ellenőrzés egyedi emodzsik összehasonlításával.", "verify_emoji_prompt_qr": "Ha nem tudod beolvasni az alábbi kódot, ellenőrizd az egyedi emodzsik összehasonlításával.", "verify_later": "Később ellenőrzöm", - "verify_reset_warning_1": "Az ellenőrzéshez használt kulcsok alaphelyzetbe állítását nem lehet visszavonni. A visszaállítás után nem fog hozzáférni a régi titkosított üzenetekhez, és minden ismerőse, aki eddig ellenőrizte a személyazonosságát, biztonsági figyelmeztetést fog látni, amíg újra nem ellenőrzi.", - "verify_reset_warning_2": "Csak akkor folytassa, ha biztos benne, hogy elvesztette a hozzáférést a többi eszközéhez és helyreállítási kulcsához.", "verify_using_device": "Ellenőrizze egy másik eszközzel", "verify_using_key": "Ellenőrzés helyreállítási kulccsal", "verify_using_key_or_phrase": "Ellenőrzés helyreállítási kulccsal vagy jelmondattal", @@ -1516,7 +1507,7 @@ "render_reaction_images_description": "Néha „egyéni emodzsiknak” nevezik.", "report_to_moderators": "Jelentés a moderátoroknak", "report_to_moderators_description": "A moderálást támogató szobákban a problémás tartalmat a „Jelentés” gombbal lehet a moderátorok felé jelezni.", - "sliding_sync": "Csúszó szinkronizációs mód", + "sliding_sync": "Csúszóablakos szinkronizálási mód", "sliding_sync_description": "Aktív fejlesztés alatt, nem kapcsolható ki.", "sliding_sync_disabled_notice": "A kikapcsoláshoz ki-, és bejelentkezés szükséges", "sliding_sync_server_no_support": "A kiszolgálója nem támogatja", @@ -1966,7 +1957,7 @@ "inaccessible": "Ez a szoba vagy tér jelenleg elérhetetlen.", "inaccessible_name": "%(roomName)s jelenleg nem érhető el.", "inaccessible_subtitle_1": "Próbálkozzon később vagy kérje meg a szoba vagy tér adminisztrátorát, hogy nézze meg van-e hozzáférése.", - "inaccessible_subtitle_2": "Amikor a szobát vagy teret próbáltuk elérni ezt a hibaüzenetet kaptuk: %(errcode)s. Ha úgy gondolja, hogy ez egy hiba legyen szívesnyisson egy hibajegyet.", + "inaccessible_subtitle_2": "A szoba vagy tér elérésekor ez a hibaüzenetet érkezett: %(errcode)s. Ha úgy gondolja, hogy az üzenetet egy hiba miatt látja, nyisson egy hibajegyet.", "intro": { "dm_caption": "Csak önök ketten vannak ebben a beszélgetésben, hacsak valamelyikőjük nem hív meg valakit, hogy csatlakozzon.", "enable_encryption_prompt": "Titkosítás bekapcsolása a beállításokban.", @@ -2036,7 +2027,7 @@ "button_view_all": "Összes megtekintése", "description": "Ez a szoba rögzített üzeneteket tartalmaz. Kattintson ide a megtekintésükhöz.", "go_to_message": "Tekintse meg a rögzített üzenetet az idővonalon.", - "title": "%(index)s rögzített %(length)s üzenetből" + "title": "%(index)s. / %(length)s rögzített üzenet" }, "read_topic": "Kattintson a téma elolvasásához", "rejecting": "Meghívó elutasítása…", @@ -2056,7 +2047,7 @@ "homeserver_blocked": "Az üzenete nem lett elküldve, mert a Matrix-kiszolgáló adminisztrátora letiltotta. A szolgáltatás használatának folytatásához vegye fel a kapcsolatot a szolgáltatás adminisztrátorával.", "monthly_user_limit_reached": "Az üzenete nem lett elküldve, mert ez a Matrix-kiszolgáló elérte a havi aktív felhasználói korlátot. A szolgáltatás használatának folytatásához vegye fel a kapcsolatot a szolgáltatás adminisztrátorával.", "requires_consent_agreement": "Nem tudsz üzenetet küldeni amíg nem olvasod el és nem fogadod el a felhasználási feltételeket.", - "retry_all": "Mind újraküldése", + "retry_all": "Összes újraküldése", "select_messages_to_retry": "Újraküldéshez vagy törléshez kiválaszthatja az üzeneteket egyenként vagy az összeset együtt", "server_connectivity_lost_description": "Az elküldött üzenetek tárolva lesznek, amíg a kapcsolata újra elérhető nem lesz.", "server_connectivity_lost_title": "A kapcsolat megszakadt a kiszolgálóval.", @@ -2078,12 +2069,14 @@ }, "uploading_single_file": "%(filename)s feltöltése" }, + "video_room": "Ez a szoba egy videószoba", "waiting_for_join_subtitle": "Miután a meghívott felhasználók csatlakoztak ide: %(brand)s, beszélgethet, és a szoba végponttól végpontig titkosítva lesz", "waiting_for_join_title": "Várakozás a felhasználók csatlakozására ide: %(brand)s" }, "room_list": { "add_room_label": "Szoba hozzáadása", "add_space_label": "Tér hozzáadása", + "appearance": "Megjelenés", "breadcrumbs_empty": "Nincsenek nemrégiben meglátogatott szobák", "breadcrumbs_label": "Nemrég meglátogatott szobák", "empty": { @@ -2092,11 +2085,14 @@ "no_chats_description_no_room_rights": "Kezdje azzal, hogy üzenetet küld valakinek", "no_favourites": "Még nincs kedvenc csevegése", "no_favourites_description": "A csevegési beállításokban adhat hozzá csevegést a kedvencekhez", + "no_invites": "Nincs olvasatlan meghívója", + "no_mentions": "Nincs olvasatlan említése", "no_people": "Még nincs közvetlen csevegése senkivel", "no_people_description": "Kikapcsolhatja a szűrőket a többi csevegés megtekintéséhez", "no_rooms": "Még nincs egy szobában sem", "no_rooms_description": "Kikapcsolhatja a szűrőket a többi csevegés megtekintéséhez", "no_unread": "Gratulálunk! Nincsenek olvasatlan üzenetei.", + "show_activity": "Összes tevékenység megtekintése", "show_chats": "Összes csevegés megjelenítése" }, "failed_add_tag": "Nem sikerült hozzáadni a szobához ezt: %(tagName)s", @@ -2104,6 +2100,8 @@ "failed_set_dm_tag": "Nem sikerült a közvetlen beszélgetés címkét beállítani", "filters": { "favourite": "Kedvencek", + "invites": "Meghívók", + "mentions": "Említések", "people": "Emberek", "rooms": "Szobák", "unread": "Olvasatlan" @@ -2132,14 +2130,21 @@ "more_options": "További lehetőségek", "open_room": "A(z) %(roomName)s szoba megnyitása" }, + "room_options": "Szobabeállítások", "show_less": "Kevesebb megjelenítése", + "show_message_previews": "Üzenetelőnézetek megjelenítése", "show_n_more": { "Még %(count)s megjelenítése": "one" }, "show_previews": "Üzenet-előnézet megjelenítése", + "sort": "Rendezés", "sort_by": "Rendezés", "sort_by_activity": "Aktivitás", "sort_by_alphabet": "A-Z", + "sort_type": { + "activity": "Tevékenység", + "atoz": "A-Z" + }, "sort_unread_first": "Olvasatlan üzeneteket tartalmazó szobák megjelenítése elöl", "space_menu": { "home": "Tér kezdőlapja", @@ -2426,6 +2431,10 @@ "recent_changes_heading": "A legutóbbi változások, amelyek még nem érkeztek meg", "title": "A kiszolgáló nem válaszol" }, + "service_worker_error": { + "description": "Az %(brand)s ún. service workert igényel a hitelesített médiafájlok Matrix tartalomtárolókból történő betöltéséhez. Ezt a böngészője nem támogatja, ezért előfordulhat, hogy a médiafájlok nem töltődnek be.", + "title": "Nem sikerült betölteni a service workert" + }, "seshat": { "error_initialising": "Az üzenetkeresés előkészítése sikertelen, további információkért ellenőrizze a beállításait", "reset_button": "Az eseménytároló alaphelyzetbe állítása", @@ -2503,22 +2512,24 @@ "breadcrumb_second_description": "Elveszít minden olyan üzenetelőzményt, amely csak a kiszolgálón van tárolva.", "breadcrumb_third_description": "Újra ellenőriznie kell az összes meglévő eszközét és névjegyét", "breadcrumb_title": "Biztos, hogy alaphelyzetbe állítja a személyazonosságát?", - "breadcrumb_title_forgot": "Elfelejtette a helyreállítási kulcsot? Újra be kell állítania a személyazonosságát.", - "breadcrumb_title_sync_failed": "A kulcstár szinkronizálása sikertelen. Vissza kell állítania személyazonosságát.", + "breadcrumb_title_forgot": "Elfelejtette a helyreállítási kulcsot? Alaphelyzetbe kell állítania a személyazonosságát.", + "breadcrumb_title_sync_failed": "A kulcstároló szinkronizálása sikertelen. Alaphelyzetbe kell állítania személyazonosságát.", "breadcrumb_warning": "Csak akkor tegye ezt, ha úgy gondolja, hogy fiókját feltörték.", "details_title": "Titkosítás részletei", - "do_not_close_warning": "Ne zárja be ezt az ablakot, amíg a visszaállítás be nem fejeződik", + "do_not_close_warning": "Ne zárja be ezt az ablakot, amíg az alaphelyzetbe állítás be nem fejeződik", "export_keys": "Kulcsok exportálása", "import_keys": "Kulcsok importálása", "other_people_device_description": "Figyelmeztetés: azok a felhasználók, akikkel nem ellenőrizték kölcsönösen egymást (például emodzsik használatával), nem kapják meg a titkosított üzeneteket. Ezenkívül az ellenőrzött felhasználók nem ellenőrzött eszközei sem kapják meg a titkosított üzeneteket.", "other_people_device_label": "Titkosított szobákban csak az ellenőrzött felhasználók kapják meg az üzeneteket", "other_people_device_title": "Mások eszközei", "reset_identity": "Kriptográfiai személyazonosság alaphelyzetbe állítása", - "reset_in_progress": "Visszaállítás folyamatban…", + "reset_in_progress": "Alaphelyzetbe állítás folyamatban…", "session_id": "Munkamenet-azonosító:", "session_key": "Munkamenetkulcs:", "title": "Speciális" }, + "confirm_key_storage_off": "Biztosan kikapcsolva szeretné tartani a kulcstárolást?", + "confirm_key_storage_off_description": "Ha kijelentkezik az összes eszközéről, elveszíti üzenetelőzményeit, és újra ellenőriznie kell az összes meglévő kapcsolatát. Tudjon meg többet", "delete_key_storage": { "breadcrumb_page": "Kulcstároló törlése", "confirm": "Kulcstároló törlése", @@ -2604,6 +2615,7 @@ "discovery_needs_terms_title": "Hagyja, hogy mások megtalálhassák", "display_name": "Megjelenítendő név", "display_name_error": "Nem sikerült beállítani a megjelenítési nevet", + "email_adding_unsupported_by_hs": "Ez a Matrix-kiszolgáló nem támogatja az e-mail-címek hozzáadását a fiókjához.", "email_address_in_use": "Ez az e-mail-cím már használatban van", "email_address_label": "E-mail cím", "email_not_verified": "Az e-mail-címe még nincs ellenőrizve", @@ -2628,7 +2640,9 @@ "error_share_msisdn_discovery": "A telefonszámot nem sikerült megosztani", "identity_server_no_token": "Nem található személyazonosság-hozzáférési kulcs", "identity_server_not_set": "Az azonosítási kiszolgáló nincs megadva", + "invalid_phone_number": "A megadott telefonszám nem tűnik érvényesnek.", "language_section": "Nyelv", + "msisdn_adding_unsupported_by_hs": "Ez a Matrix-kiszolgáló nem támogatja a telefonszámok hozzáadását a fiókjához.", "msisdn_in_use": "Ez a telefonszám már használatban van", "msisdn_label": "Telefonszám", "msisdn_verification_field_label": "Ellenőrző kód", @@ -2647,18 +2661,16 @@ "unable_to_load_msisdns": "Nem sikerült betölteni a telefonszámokat", "username": "Felhasználónév" }, - "image_thumbnails": "Előnézet/bélyegkép megjelenítése a képekhez", "inline_url_previews_default": "Beágyazott webcím-előnézetek alapértelmezett engedélyezése", "inline_url_previews_room": "Webcím-előnézetek alapértelmezett engedélyezése a szobatagok számára", "inline_url_previews_room_account": "Webcím-előnézetek engedélyezése ebben a szobában (csak Önt érinti)", "insert_trailing_colon_mentions": "Záró kettőspont beszúrása egy felhasználó üzenet elején való megemlítésekor", - "invite_avatars": "Azon szobák profilképének megjelenítése, melybe meghívták", "jump_to_bottom_on_send": "Üzenetküldés után az idővonal aljára ugrás", "key_backup": { "backup_in_progress": "A kulcsaid mentése folyamatban van (az első mentés több percig is eltarthat).", "backup_starting": "Mentés indul…", "backup_success": "Sikeres!", - "cannot_create_backup": "Kulcs mentés sikertelen", + "cannot_create_backup": "Kulcsmentés sikertelen", "create_title": "Kulcsmentés készítése", "setup_secure_backup": { "backup_setup_success_description": "A kulcsai nem kerülnek elmentésre erről az eszközről.", @@ -2711,6 +2723,14 @@ "labs_mjolnir": { "dialog_title": "Beállítások: Figyelmen kívül hagyott felhasználók" }, + "media_preview": { + "hide_avatars": "A szoba és a meghívó profilképének elrejtése", + "hide_media": "Elrejtés mindig", + "media_preview_description": "A rejtett médiatartalmak koppintással jeleníthetők meg", + "media_preview_label": "Média megjelenítése az idővonalon", + "show_in_private": "Privát szobákban", + "show_media": "Megjelenítés mindig" + }, "notifications": { "default_setting_description": "Ez a beállítás alapértelmezés szerint az összes szobájára érvényes lesz.", "default_setting_section": "Szeretnék értesítést kapni az alábbiakról (Alapértelmezett beállítás)", @@ -3189,7 +3209,7 @@ "heading_without_query": "Keresés:", "join_button_text": "Belépés ide: %(roomAddress)s", "keyboard_scroll_hint": "Görgetés ezekkel: ", - "message_search_section_title": "Más keresések", + "messages_label": "Üzenetek", "other_rooms_in_space": "Más szobák itt: %(spaceName)s", "public_rooms_label": "Nyilvános szobák", "public_spaces_label": "Nyilvános terek", @@ -3199,7 +3219,6 @@ "result_may_be_hidden_privacy_warning": "Adatvédelmi okokból néhány találat rejtve lehet", "result_may_be_hidden_warning": "Néhány találat rejtve lehet", "search_dialog": "Keresési párbeszédablak", - "search_messages_hint": "Az üzenetek kereséséhez keresse ezt az ikont a szoba tetején: ", "spaces_title": "Terek, amelynek tagja", "start_group_chat_button": "Csoportos csevegés indítása" }, @@ -3240,16 +3259,14 @@ "error_start_thread_existing_relation": "Nem lehet üzenetszálat indítani olyan eseményről ami már rendelkezik kapcsolattal", "mark_all_read": "Az összes megjelölése olvasottként", "my_threads": "Saját üzenetszálak", - "my_threads_description": "Minden üzenetszál megjelenítése, amelyben részt vesz", + "my_threads_description": "Összes olyan üzenetszál megjelenítése, amelyben részt vesz", "open_thread": "Üzenetszál megnyitása", "show_thread_filter": "Megjelenítés:" }, "threads_activity_centre": { - "header": "Üzenetszál aktivitás", + "header": "Üzenetszál-tevékenységek", "no_rooms_with_threads_notifs": "Még nincsenek olyan szobái, amelyek üzenetszál értesítéseket tartalmaznak.", - "no_rooms_with_unread_threads": "Nincsenek még olvasatlan üzenetszálakkal rendelkező szobái.", - "release_announcement_description": "Az üzenetszálak értesítései átkerültek, mostantól itt találja őket.", - "release_announcement_header": "Üzenetszál Tevékenységi Központ" + "no_rooms_with_unread_threads": "Nincsenek még olvasatlan üzenetszálakkal rendelkező szobái." }, "time": { "about_day_ago": "egy napja", @@ -3757,10 +3774,11 @@ "unavailable": "Elérhetetlen" }, "update_room_access_modal": { - "description": "Megosztási hivatkozás létrehozásához engedélyeznie kell a vendégek számára, hogy csatlakozzanak ehhez a szobához. Ez kevésbé biztonságossá teheti a szobát. Ha befejezte a hívást, ismét priváttá teheti a szobát.", - "dont_change_description": "Alternatív megoldásként a hívást külön szobában is tarthatja.", + "description": "Megosztási hivatkozás létrehozásához tegye nyilvánossá ezt a szobát, vagy engedélyezze azt a lehetőséget, hogy a felhasználók csatlakozást kérhessenek. Ez lehetővé teszi a vendégek számára, hogy meghívás nélkül csatlakozzanak.", + "dont_change_description": "Ha nem szeretné módosítani a szoba hozzáférését, létrehozhat egy új szobát a híváshoz.", "no_change": "Nem akarom megváltoztatni a hozzáférési szintet.", - "title": "A szoba hozzáférési szintjének módosítása" + "revert_access_description": "(Ez visszaállítható az előző értékre a Szoba beállításaiban: Biztonság és adatvédelem / Hozzáférés)", + "title": "Vendégfelhasználók csatlakozásának engedélyezése ehhez a szobához" }, "upload_failed_generic": "A(z) „%(fileName)s” fájl feltöltése sikertelen.", "upload_failed_size": "A(z) „%(fileName)s” mérete túllépi a Matrix-kiszolgáló által megengedett korlátot", diff --git a/src/i18n/strings/id.json b/src/i18n/strings/id.json index 03c5a4e729..4df0b7707b 100644 --- a/src/i18n/strings/id.json +++ b/src/i18n/strings/id.json @@ -1,6 +1,8 @@ { "a11y": { + "emoji_picker": "Pemilih emoji", "jump_first_invite": "Pergi ke undangan pertama.", + "message_composer": "Komposer pesan", "n_unread_messages": { "one": "1 pesan yang belum dibaca.", "other": "%(count)s pesan yang belum dibaca." @@ -9,7 +11,18 @@ "one": "1 sebutan yang belum dibaca.", "other": "%(count)s pesan yang belum dibaca termasuk sebutan." }, + "recent_rooms": "Ruangan terkini", + "room_messsage_not_sent": "Buka ruangan %(roomName)s dengan pesan yang belum diatur.", + "room_n_unread_invite": "Buka undangan ruangan %(roomName)s.", + "room_n_unread_messages": { + "other": "Buka ruangan %(roomName)s dengan %(count)s pesan yang belum dibaca." + }, + "room_n_unread_messages_mentions": { + "other": "Buka ruangan %(roomName)s dengan %(count)s pesan yang belum dibaca termasuk sebutan." + }, "room_name": "Ruangan %(name)s", + "room_status_bar": "Bilah status ruangan", + "seek_bar_label": "Bilah pencarian audio", "unread_messages": "Pesan yang belum dibaca.", "user_menu": "Menu pengguna" }, @@ -40,6 +53,8 @@ "create_a_room": "Buat sebuah ruangan", "create_account": "Buat Akun", "decline": "Tolak", + "decline_and_block": "Tolak dan blokir", + "decline_invite": "Tolak undangan", "delete": "Hapus", "deny": "Tolak", "disable": "Nonaktifkan", @@ -59,6 +74,7 @@ "go": "Mulai", "go_back": "Kembali ke sebelumnya", "got_it": "Mengerti", + "hide": "Sembunyikan", "hide_advanced": "Sembunyikan lanjutan", "hold": "Jeda", "ignore": "Abaikan", @@ -75,12 +91,14 @@ "maximise": "Maksimalkan", "mention": "Sebutkan", "minimise": "Minimalkan", + "new_message": "Pesan baru", "new_room": "Ruangan baru", "new_video_room": "Ruangan video baru", "next": "Lanjut", "no": "Tidak", "ok": "Oke", "open": "Buka", + "open_menu": "Buka menu", "pause": "Jeda", "pin": "Sematkan", "play": "Mainkan", @@ -95,6 +113,7 @@ "reply": "Balas", "reply_in_thread": "Balas di utasan", "report_content": "Laporkan Konten", + "report_room": "Laporkan ruangan", "resend": "Kirim Ulang", "reset": "Atur Ulang", "resume": "Lanjutkan", @@ -104,6 +123,7 @@ "save": "Simpan", "search": "Cari", "send_report": "Kirimkan laporan", + "set_avatar": "Atur foto profil", "share": "Bagikan", "show": "Tampilkan", "show_advanced": "Tampilkan lanjutan", @@ -127,6 +147,7 @@ "update": "Perbarui", "upgrade": "Tingkatkan", "upload": "Unggah", + "upload_file": "Unggah berkas", "verify": "Lakukan verifikasi", "view": "Pratinjau", "view_all": "Tampilkan semua", @@ -134,6 +155,7 @@ "view_message": "Tampilkan pesan", "view_source": "Tampilkan Sumber", "yes": "Ya", + "yes_dismiss": "Ya, abaikan", "zoom_in": "Perbesar", "zoom_out": "Perkecil" }, @@ -221,6 +243,7 @@ }, "misconfigured_body": "Tanyakan admin %(brand)s Anda untuk memeriksa konfigurasi Anda untuk entri yang tidak benar atau entri duplikat.", "misconfigured_title": "%(brand)s Anda telah diatur dengan salah", + "mobile_create_account_title": "Anda akan membuat akun di %(hsName)s", "msisdn_field_description": "Pengguna lain dapat mengundang Anda ke ruangan menggunakan detail kontak Anda", "msisdn_field_label": "Ponsel", "msisdn_field_number_invalid": "Nomor teleponnya tidak terlihat benar, mohon periksa dan coba lagi", @@ -228,6 +251,7 @@ "no_hs_url_provided": "Tidak ada URL homeserver yang disediakan", "oidc": { "error_title": "Kami tidak dapat memasukkan Anda", + "generic_auth_error": "Terjadi kesalahan saat autentikasi. Buka laman masuk dan coba lagi.", "missing_or_invalid_stored_state": "Kami menanyakan browser ini untuk mengingat homeserver apa yang Anda gunakan untuk membantu Anda masuk, tetapi sayangnya browser ini melupakannya. Pergi ke halaman masuk dan coba lagi." }, "password_field_keep_going_prompt": "Lanjutkan…", @@ -237,11 +261,40 @@ "phone_label": "Ponsel", "phone_optional_label": "Nomor telepon (opsional)", "qr_code_login": { + "check_code_explainer": "Ini akan memverifikasi bahwa koneksi ke perangkat Anda yang lain aman.", + "check_code_heading": "Masukkan nomor yang ditampilkan di perangkat Anda yang lain", + "check_code_input_label": "Kode 2 digit", + "check_code_mismatch": "Angkanya tidak cocok", "completing_setup": "Menyelesaikan penyiapan perangkat baru Anda", - "error_unexpected": "Sebuah kesalahan terjadi secara tidak terduga.", - "scan_code_instruction": "Pindai kode QR di bawah dengan perangkat Anda yang sudah keluar dari akun.", - "scan_qr_code": "Pindai kode QR", - "select_qr_code": "Pilih '%(scanQRCode)s'", + "error_etag_missing": "Terjadi kesalahan yang tidak terduga. Hal ini mungkin disebabkan oleh pengaya peramban, server proksi atau kesalahan konfigurasi server.", + "error_expired": "Masa masuk sudah habis. Silakan coba lagi.", + "error_expired_title": "Proses masuk tidak selesai tepat waktu", + "error_insecure_channel_detected": "Koneksi aman tidak dapat dibuat ke perangkat baru. Perangkat Anda yang ada masih aman dan Anda tidak perlu khawatir tentang mereka.", + "error_insecure_channel_detected_instructions": "Sekarang apa?", + "error_insecure_channel_detected_instructions_1": "Coba masuk ke perangkat lain lagi dengan kode QR jika ini adalah masalah jaringan", + "error_insecure_channel_detected_instructions_2": "Jika Anda mengalami masalah yang sama, coba jaringan Wi-Fi yang berbeda atau gunakan data seluler Anda daripada Wi-Fi", + "error_insecure_channel_detected_instructions_3": "Jika tidak berhasil, masuk secara manual", + "error_insecure_channel_detected_title": "Koneksi tidak aman", + "error_other_device_already_signed_in": "Anda tidak perlu melakukan hal lain.", + "error_other_device_already_signed_in_title": "Perangkat Anda yang lain sudah masuk", + "error_rate_limited": "Terlalu banyak percobaan dalam waktu yang singkat. Tunggu beberapa waktu sebelum mencoba lagi.", + "error_unexpected": "Terjadi kesalahan yang tidak terduga. Permintaan untuk menghubungkan perangkat Anda yang lain telah dibatalkan.", + "error_unsupported_protocol": "Perangkat ini tidak mendukung masuk ke perangkat lain dengan kode QR.", + "error_unsupported_protocol_title": "Perangkat lain tidak kompatibel", + "error_user_cancelled": "Proses masuk dibatalkan di perangkat lain.", + "error_user_cancelled_title": "Permintaan masuk dibatalkan", + "error_user_declined": "Anda menolak permintaan dari perangkat Anda yang lain untuk masuk.", + "error_user_declined_title": "Masuk ditolak", + "follow_remaining_instructions": "Ikuti petunjuk selanjutnya untuk memverifikasi perangkat Anda yang lain", + "open_element_other_device": "Buka %(brand)s di perangkat Anda yang lain", + "point_the_camera": "Arahkan kamera ke kode QR yang ditampilkan di sini", + "scan_code_instruction": "Pindai kode QR dengan perangkat lain", + "scan_qr_code": "Masuk dengan kode QR", + "security_code": "Kode keamanan", + "security_code_prompt": "Jika diminta, masukkan kode di bawah ini pada perangkat Anda yang lain.", + "select_qr_code": "Pilih \"%(scanQRCode)s\"", + "unsupported_explainer": "Penyedia akun Anda tidak mendukung masuk ke perangkat baru dengan kode QR.", + "unsupported_heading": "Kode QR tidak didukung", "waiting_for_device": "Menunggu perangkat untuk masuk" }, "register_action": "Buat Akun", @@ -330,6 +383,9 @@ "email_resend_prompt": "Belum menerima? Kirim ulang", "email_resent": "Dikirimkan ulang!", "fallback_button": "Mulai autentikasi", + "mas_cross_signing_reset_cta": "Buka akun Anda", + "mas_cross_signing_reset_description": "Atur ulang identitas Anda melalui penyedia akun Anda, kemudian kembali dan klik “Coba lagi”.", + "mas_cross_signing_reset_title": "Buka akun Anda untuk mengatur ulang identitas Anda", "msisdn": "Sebuah pesan teks telah dikirim ke %(msisdn)s", "msisdn_token_incorrect": "Token salah", "msisdn_token_prompt": "Silakan masukkan kode yang berisi:", @@ -364,6 +420,15 @@ "download_logs": "Unduh catatan", "downloading_logs": "Mengunduh catatan", "error_empty": "Mohon beri tahu kami apa saja yang salah atau, lebih baik, buat sebuah issue GitHub yang menjelaskan masalahnya.", + "failed_download_logs": "Gagal mengunduh log pengawakutuan: ", + "failed_send_logs_causes": { + "disallowed_app": "Laporan pengawakutuan Anda ditolak. Server rageshake tidak mendukung aplikasi ini.", + "rejected_generic": "Laporan bug Anda ditolak. Server rageshake menolak isi laporan karena suatu kebijakan.", + "rejected_recovery_key": "Laporan pengawakutuan Anda ditolak karena alasan keamanan, karena berisi kunci pemulihan.", + "rejected_version": "Laporan pengawakutuan Anda ditolak karena versi yang Anda jalankan terlalu lawas.", + "server_unknown_error": "Server rageshake mengalami kesalahan yang tidak diketahui dan tidak dapat menangani laporan.", + "unknown_error": "Gagal mengirim log." + }, "github_issue": "Masalah GitHub", "introduction": "Jika Anda mengirim sebuah kutu via GitHub, catatan pengawakutu dapat membantu kami melacak masalahnya. ", "log_request": "Untuk membantu kami mencegahnya di masa mendatang, silakan kirimkan kami catatan.", @@ -403,6 +468,7 @@ "access_token": "Token Akses", "accessibility": "Aksesibilitas", "advanced": "Tingkat Lanjut", + "all_chats": "Semua Obrolan", "analytics": "Analitik", "and_n_others": { "one": "dan satu lainnya...", @@ -417,6 +483,7 @@ "beta": "Beta", "camera": "Kamera", "cameras": "Kamera", + "cancel": "Batalkan", "capabilities": "Kemampuan", "copied": "Disalin!", "credits": "Kredit", @@ -455,8 +522,10 @@ "matrix": "Matrix", "message": "Pesan", "message_layout": "Tata letak pesan", + "message_timestamp_invalid": "Stempel waktu tidak valid", "microphone": "Mikrofon", "model": "Model", + "moderation_and_safety": "Moderasi dan keamanan", "modern": "Modern", "mute": "Bisukan", "n_members": { @@ -492,10 +561,13 @@ "qr_code": "Kode QR", "random": "Sembarangan", "reactions": "Reaksi", + "recommended": "Disarankan", "report_a_bug": "Laporkan sebuah bug", "room": "Ruangan", "room_name": "Nama ruangan", "rooms": "Ruangan", + "save": "Simpan", + "saved": "Disimpan", "saving": "Menyimpan…", "secure_backup": "Cadangan Aman", "select_all": "Pilih semua", @@ -522,6 +594,7 @@ "unnamed_room": "Ruangan Tanpa Nama", "unnamed_space": "Space Tidak Dinamai", "unverified": "Belum diverifikasi", + "updating": "Memperbarui...", "user": "Pengguna", "user_avatar": "Gambar profil", "username": "Nama Pengguna", @@ -577,6 +650,7 @@ "placeholder_reply_encrypted": "Kirim sebuah balasan terenkripsi…", "placeholder_thread": "Balas ke utasan…", "placeholder_thread_encrypted": "Balas ke utasan yang terenkripsi…", + "poll_button": "Pemungutan suara", "poll_button_no_perms_description": "Anda tidak memiliki izin untuk memulai sebuah poll di ruangan ini.", "poll_button_no_perms_title": "Izin Dibutuhkan", "replying_title": "Membalas", @@ -649,6 +723,7 @@ "private_space_description": "Sebuah space pribadi untuk Anda dan tim Anda", "public_description": "Space terbuka untuk siapa saja, baik untuk komunitas", "public_heading": "Space publik Anda", + "search_public_button": "Cari ruangan publik", "setup_rooms_community_description": "Mari kita buat ruangan untuk masing-masing.", "setup_rooms_community_heading": "Apa saja yang Anda ingin bahas di %(spaceName)s?", "setup_rooms_description": "Anda juga dapat menambahkan lebih banyak nanti, termasuk yang sudah ada.", @@ -672,12 +747,58 @@ "twemoji": "Gambar emoji Twemoji © Twitter, Inc dan kontributor lainnya digunakan di bawah ketentuan CC-BY 4.0.", "twemoji_colr": "Fon twemoji-colr © Mozilla Foundation digunakan di bawah ketentuan Apache 2.0." }, + "decline_invitation_dialog": { + "confirm": "Apakah Anda yakin ingin menolak undangan untuk bergabung dengan \"%(roomName)s\"?", + "ignore_user_help": "Anda tidak akan melihat pesan atau undangan ruangan dari pengguna ini.", + "reason_description": "Jelaskan alasan untuk melaporkan ruangan.", + "report_room_description": "Laporkan ruangan ini ke penyedia akun Anda.", + "title": "Tolak undangan" + }, + "desktop_default_device_name": "%(brand)s Desktop: %(platformName)s", "devtools": { "active_widgets": "Widget Aktif", "category_other": "Lainnya", "category_room": "Ruangan", "caution_colon": "Peringatan:", "client_versions": "Versi Klien", + "crypto": { + "4s_public_key_in_account_data": "dalam data akun", + "4s_public_key_not_in_account_data": "tidak ditemukan", + "4s_public_key_status": "Kunci publik penyimpanan rahasia:", + "backup_key_cached": "dalam tembolok secara lokal", + "backup_key_cached_status": "Kunci cadangan dalam tembolok:", + "backup_key_not_stored": "tidak disimpan", + "backup_key_stored": "dalam penyimpanan rahasia", + "backup_key_stored_status": "Kunci cadangan disimpan:", + "backup_key_unexpected_type": "jenis tidak terduga", + "backup_key_well_formed": "terbentuk dengan baik", + "cross_signing": "Penandatanganan silang", + "cross_signing_cached": "dalam tembolok secara lokal", + "cross_signing_not_ready": "Penandatanganan silang belum disiapkan.", + "cross_signing_private_keys_in_storage": "dalam penyimpanan rahasia", + "cross_signing_private_keys_in_storage_status": "Kunci pribadi penandatanganan silang:", + "cross_signing_private_keys_not_in_storage": "tidak ditemukan dalam penyimpanan", + "cross_signing_public_keys_on_device": "dalam memori", + "cross_signing_public_keys_on_device_status": "Kunci publik penandatanganan silang:", + "cross_signing_ready": "Penandatanganan silang siap digunakan.", + "cross_signing_status": "Status penandatanganan silang:", + "cross_signing_untrusted": "Akun Anda memiliki identitas penandatanganan silang dalam penyimpanan rahasia, tetapi belum dipercaya oleh sesi ini.", + "crypto_not_available": "Modul kriptografi tidak tersedia", + "key_backup_active_version": "Versi cadangan aktif:", + "key_backup_active_version_none": "Tidak ada", + "key_backup_inactive_warning": "Kunci Anda tidak dicadangkan dari sesi ini.", + "key_backup_latest_version": "Versi cadangan terbaru di server:", + "key_storage": "Penyimpanan Kunci", + "master_private_key_cached_status": "Kunci pribadi utama:", + "not_found": "tidak ditemukan", + "not_found_locally": "tidak ditemukan secara lokal", + "secret_storage_not_ready": "belum siap", + "secret_storage_ready": "siap", + "secret_storage_status": "Penyimpanan rahasia:", + "self_signing_private_key_cached_status": "Kunci pribadi penandatanganan sendiri:", + "title": "Enkripsi ujung ke ujung", + "user_signing_private_key_cached_status": "Kunci pribadi penandatanganan pengguna:" + }, "developer_mode": "Mode pengembang", "developer_tools": "Alat Pengembang", "edit_setting": "Edit pengaturan", @@ -732,6 +853,9 @@ "setting_colon": "Pengaturan:", "setting_definition": "Definisi pengaturan:", "setting_id": "ID Pengaturan", + "settings": { + "elementCallUrl": "URL Element Call" + }, "settings_explorer": "Penelusur pengaturan", "show_hidden_events": "Tampilkan peristiwa tersembunyi di lini masa", "spaces": { @@ -784,22 +908,13 @@ "empty_room_was_name": "Ruangan kosong (sebelumnya %(oldName)s)", "encryption": { "access_secret_storage_dialog": { - "enter_phrase_or_key_prompt": "Masukkan Frasa Keamanan Anda atau untuk melanjutkan.", + "alternatives": "Jika Anda memiliki kunci keamanan atau frasa keamanan, ini juga bisa digunakan.", "key_validation_text": { - "invalid_security_key": "Kunci Keamanan tidak absah", - "recovery_key_is_correct": "Kelihatannya bagus!", - "wrong_file_type": "Tipe file salah", - "wrong_security_key": "Kunci Keamanan salah" + "wrong_security_key": "Kunci pemulihan yang Anda masukkan salah." }, - "reset_title": "Atur ulang semuanya", - "reset_warning_1": "Hanya lakukan ini jika Anda tidak memiliki perangkat yang lain untuk menyelesaikan verifikasi.", - "reset_warning_2": "Jika Anda mengatur ulang semuanya, Anda dengan mulai ulang dengan tidak ada sesi yang dipercayai, tidak ada pengguna yang dipercayai, dan mungkin tidak dapat melihat pesan-pesan lama.", + "privacy_warning": "Pastikan tidak ada yang bisa melihat layar ini!", "restoring": "Memulihkan kunci-kunci dari cadangan", - "security_key_title": "Kunci Keamanan", - "security_phrase_incorrect_error": "Tidak dapat mengakses penyimpanan rahasia. Periksa jika Anda memasukkan Frasa Keamanan yang benar.", - "security_phrase_title": "Frasa Keamanan", - "separator": "%(securityKey)s atau %(recoveryFile)s", - "use_security_key_prompt": "Gunakan Kunci Keamanan Anda untuk melanjutkan." + "security_key_title": "Kunci pemulihan" }, "bootstrap_title": "Menyiapkan kunci", "cancel_entering_passphrase_description": "Apakah Anda yakin untuk membatalkan pemasukkan frasa sandi?", @@ -812,14 +927,18 @@ "cross_signing_user_normal": "Anda belum memverifikasi pengguna ini.", "cross_signing_user_verified": "Anda telah memverifikasi pengguna ini. Pengguna ini telah memverifikasi semua sesinya.", "cross_signing_user_warning": "Pengguna ini belum memverifikasi semua sesinya.", + "enter_recovery_key": "Masukkan kunci pemulihan", "event_shield_reason_authenticity_not_guaranteed": "Keaslian pesan terenkripsi ini tidak dapat dijamin pada perangkat ini.", "event_shield_reason_mismatched_sender_key": "Terenkripsi oleh sesi yang belum diverifikasi", "event_shield_reason_unknown_device": "Dienkripsi oleh perangkat yang tidak dikenal atau dihapus.", "event_shield_reason_unsigned_device": "Dienkripsi oleh perangkat yang tidak diverifikasi oleh pemiliknya.", "event_shield_reason_unverified_identity": "Dienkripsi oleh pengguna yang tidak diverifikasi.", "export_unsupported": "Browser Anda tidak mendukung ekstensi kriptografi yang dibutuhkan", + "forgot_recovery_key": "Lupa kunci pemulihan?", "import_invalid_keyfile": "Bukan keyfile %(brand)s yang absah", "import_invalid_passphrase": "Pemeriksaan autentikasi gagal: kata sandi salah?", + "key_storage_out_of_sync": "Penyimpanan kunci Anda tidak tersinkron.", + "key_storage_out_of_sync_description": "Konfirmasikan kunci pemulihan Anda untuk mempertahankan akses ke penyimpanan kunci dan riwayat pesan Anda.", "messages_not_secure": { "cause_1": "Homeserver Anda", "cause_2": "Homeserver pengguna yang Anda memverifikasi", @@ -834,6 +953,8 @@ "title": "Metode Pemulihan Baru", "warning": "Jika Anda tidak menyetel metode pemulihan yang baru, sebuah penyerang mungkin mencoba mengakses akun Anda. Ubah kata sandi akun Anda dan segera tetapkan metode pemulihan yang baru di Pengaturan." }, + "pinned_identity_changed": "Identitas (%(userId)s) %(displayName)s tampaknya telah berubah. Pelajari lebih lanjut", + "pinned_identity_changed_no_displayname": "Identitas %(userId)s tampaknya telah berubah. Pelajari lebih lanjut", "recovery_method_removed": { "description_1": "Sesi ini telah mendeteksi bahwa Frasa Keamanan dan kunci untuk Pesan Aman Anda telah dihapus.", "description_2": "Jika Anda melakukan ini secara tidak sengaja, Anda dapat mengatur Pesan Aman pada sesi ini yang akan mengenkripsi ulang riwayat pesan sesi ini dengan metode pemulihan baru.", @@ -841,11 +962,16 @@ "warning": "Jika Anda tidak menghapus metode pemulihan, sebuah penyerang mungkin mencoba mengakses akun Anda. Ubah kata sandi akun Anda dan segera tetapkan metode pemulihan baru di Pengaturan." }, "reset_all_button": "Lupa atau kehilangan semua metode pemulihan? Atur ulang semuanya", + "set_up_recovery": "Siapkan pemulihan", + "set_up_recovery_later": "Tidak sekarang", + "set_up_recovery_toast_description": "Buat kunci pemulihan yang dapat digunakan untuk memulihkan riwayat pesan terenkripsi jika Anda kehilangan akses ke perangkat Anda.", "set_up_toast_description": "Lindungi dari kehilangan akses ke pesan & data terenkripsi", "set_up_toast_title": "Siapkan Cadangan Aman", "setup_secure_backup": { "explainer": "Cadangkan kunci Anda sebelum keluar untuk menghindari kehilangannya." }, + "turn_on_key_storage": "Aktifkan penyimpanan kunci", + "turn_on_key_storage_description": "Simpan identitas kriptografi dan kunci pesan Anda secara aman di server. Ini akan memungkinkan Anda untuk melihat riwayat pesan Anda di perangkat baru mana pun.", "udd": { "interactive_verification_button": "Verifikasi secara interaktif sengan emoji", "other_ask_verify_text": "Tanyakan pengguna ini untuk memverifikasi sesinya, atau verifikasi secara manual di bawah.", @@ -859,7 +985,6 @@ "accepting": "Menerima…", "after_new_login": { "device_verified": "Perangkat telah diverifikasi", - "reset_confirmation": "Benar-benar ingin mengatur ulang kunci-kunci verifikasi?", "skip_verification": "Lewatkan verifikasi untuk sementara", "unable_to_verify": "Tidak dapat memverifikasi perangkat ini", "verify_this_device": "Verifikasi perangkat ini" @@ -881,7 +1006,7 @@ "incoming_sas_dialog_waiting": "Menunggu pengguna untuk konfirmasi…", "incoming_sas_user_dialog_text_1": "Verifikasi pengguna ini untuk menandainya sebagai terpercaya. Mempercayai pengguna memberikan Anda ketenangan saat menggunakan pesan terenkripsi secara ujung ke ujung.", "incoming_sas_user_dialog_text_2": "Memverifikasi pengguna ini akan menandai sesinya sebagai terpercaya, dan juga menandai sesi Anda sebagai terpercaya kepadanya.", - "no_key_or_device": "Sepertinya Anda tidak memiliki Kunci Keamanan atau perangkat lainnya yang Anda dapat gunakan untuk memverifikasi. Perangkat ini tidak dapat mengakses ke pesan terenkripsi lama. Untuk membuktikan identitas Anda, kunci verifikasi harus diatur ulang.", + "no_key_or_device": "Sepertinya Anda tidak memiliki Kunci Pemulihan atau perangkat lain yang dapat Anda verifikasi. Perangkat ini tidak akan dapat mengakses pesan terenkripsi lama. Untuk memverifikasi identitas Anda di perangkat ini, Anda harus mengatur ulang kunci verifikasi Anda.", "no_support_qr_emoji": "Perangkat yang Anda sedang verifikasi tidak mendukung pemindaian kode QR atau verifikasi emoji, yang didukung oleh %(brand)s. Coba menggunakan klien yang lain.", "other_party_cancelled": "Pengguna yang lain membatalkan proses verifikasi ini.", "prompt_encrypted": "Verifikasi semua pengguna di sebuah ruangan untuk memastikan keamanannya.", @@ -894,6 +1019,7 @@ "qr_reciprocate_same_shield_device": "Hampir selesai! Apakah perangkat lain Anda menampilkan perisai yang sama?", "qr_reciprocate_same_shield_user": "Hampir selesai! Apakah %(displayName)s menampilkan perisai yang sama?", "request_toast_accept": "Verifikasi Sesi", + "request_toast_accept_user": "Verifikasi Pengguna", "request_toast_decline_counter": "Abaikan (%(counter)s)", "request_toast_detail": "%(deviceId)s dari %(ip)s", "reset_proceed_prompt": "Lanjutkan dengan mengatur ulang", @@ -919,7 +1045,7 @@ "unverified_sessions_toast_description": "Periksa untuk memastikan akun Anda aman", "unverified_sessions_toast_reject": "Nanti", "unverified_sessions_toast_title": "Anda memiliki sesi yang belum diverifikasi", - "verification_description": "Verifikasi identitas Anda untuk mengakses pesan-pesan terenkripsi Anda dan buktikan identitas Anda kepada lainnya.", + "verification_description": "Verifikasi identitas Anda untuk mengakses pesan terenkripsi dan membuktikan identitas Anda kepada orang lain. Jika Anda juga menggunakan ponsel, harap buka aplikasi di sana sebelum melanjutkan.", "verification_dialog_title_device": "Verifikasi perangkat lain", "verification_dialog_title_user": "Permintaan Verifikasi", "verification_skip_warning": "Tanpa memverifikasi, Anda tidak akan memiliki akses ke semua pesan Anda dan tampak tidak dipercayai kepada lainnya.", @@ -929,19 +1055,20 @@ "verify_emoji_prompt": "Verifikasi dengan membandingkan emoji unik.", "verify_emoji_prompt_qr": "Jika Anda tidak dapat memindai kode di atas, verifikasi dengan membandingkan emoji yang unik.", "verify_later": "Saya verifikasi nanti", - "verify_reset_warning_1": "Mengatur ulang kunci verifikasi Anda tidak dapat dibatalkan. Setelah mengatur ulang, Anda tidak akan memiliki akses ke pesan terenkripsi lama, dan semua orang yang sebelumnya telah memverifikasi Anda akan melihat peringatan keamanan sampai Anda memverifikasi ulang dengan mereka.", - "verify_reset_warning_2": "Hanya lanjutkan jika Anda yakin Anda telah kehilangan semua perangkat lainnya dan kunci keamanan Anda.", "verify_using_device": "Verifikasi dengan perangkat lain", "verify_using_key": "Verifikasi dengan Kunci Keamanan", - "verify_using_key_or_phrase": "Verifikasi dengan Kunci Keamanan atau Frasa", + "verify_using_key_or_phrase": "Verifikasi dengan Kunci atau Frasa Keamanan", "waiting_for_user_accept": "Menunggu untuk %(displayName)s untuk menerima…", "waiting_other_device": "Menunggu Anda untuk verifikasi di perangkat Anda yang lain…", "waiting_other_device_details": "Menunggu Anda untuk memverifikasi perangkat Anda yang lain, %(deviceName)s (%(deviceId)s)…", "waiting_other_user": "Menunggu %(displayName)s untuk memverifikasi…" }, "verification_requested_toast_title": "Verifikasi diminta", + "verified_identity_changed": "Identitas terverifikasi %(displayName)s (%(userId)s) telah berubah. Pelajari lebih lanjut", + "verified_identity_changed_no_displayname": "Identitas terverifikasi %(userId)s telah berubah. Pelajari lebih lanjut", "verify_toast_description": "Pengguna yang lain mungkin tidak mempercayainya", - "verify_toast_title": "Verifikasi sesi ini" + "verify_toast_title": "Verifikasi sesi ini", + "withdraw_verification_action": "Tolak verifikasi" }, "error": { "admin_contact": "Mohon hubungi administrator layanan Anda untuk melanjutkan menggunakan layanannya.", @@ -982,6 +1109,13 @@ "unknown_error_code": "kode kesalahan tidak diketahui", "update_power_level": "Gagal untuk mengubah tingkat daya" }, + "error_app_open_in_another_tab": "%(brand)s telah dibuka di tab lain.", + "error_app_open_in_another_tab_title": "%(brand)s sedang terhubung di tab lain", + "error_app_opened_in_another_window": "%(brand)s sedang dibuka di jendela lain. Klik \"%(label)s\" untuk menggunakan %(brand)s di sini dan putuskan sambungan di jendela lainnya.", + "error_database_closed_description": { + "for_desktop": "Diska Anda mungkin sudah penuh. Mohon bersihkan beberapa ruang dan muat ulang.", + "for_web": "Jika Anda menghapus data penelusuran, maka pesan ini akan muncul. %(brand)s mungkin juga terbuka di tab lain, atau diska Anda penuh. Tolong bersihkan beberapa ruang dan muat ulang" + }, "error_database_closed_title": "%(brand)s berhenti bekerja", "error_dialog": { "copy_room_link_failed": { @@ -1014,7 +1148,15 @@ "you": "Anda bereaksi %(reaction)s ke %(message)s" }, "m.sticker": "%(senderName)s: %(stickerName)s", - "m.text": "%(senderName)s: %(message)s" + "m.text": "%(senderName)s: %(message)s", + "prefix": { + "audio": "Audio", + "file": "Berkas", + "image": "Gambar", + "poll": "Pemugutan suara", + "video": "Video" + }, + "preview": "%(prefix)s: %(preview)s" }, "export_chat": { "cancelled": "Ekspor Dibatalkan", @@ -1106,6 +1248,7 @@ "change": "Ubah server identitas", "change_prompt": "Putuskan hubungan dari server identitas dan hubungkan ke ?", "change_server_prompt": "Jika Anda tidak ingin menggunakan untuk menemukan dan dapat ditemukan oleh kontak yang Anda tahu, masukkan server identitas yang lain di bawah.", + "changed": "Server identitas Anda telah diubah", "checking": "Memeriksa server", "description_connected": "Anda saat ini menggunakan untuk menemukan dan dapat ditemukan oleh kontak yang Anda tahu. Anda dapat mengubah server identitas di bawah.", "description_disconnected": "Anda saat ini tidak menggunakan sebuah server identitas. Untuk menemukan dan dapat ditemukan oleh kontak yang Anda tahu, tambahkan satu di bawah.", @@ -1137,7 +1280,20 @@ "other": "Dalam %(spaceName)s dan %(count)s space lainnya." }, "incompatible_browser": { - "title": "Peramban tidak didukung" + "continue": "Tetap lanjutkan", + "description": "%(brand)s menggunakan beberapa fitur peramban yang tidak tersedia di peramban Anda saat ini. %(detail)s", + "detail_can_continue": "Jika Anda melanjutkan, beberapa fitur mungkin berhenti bekerja dan ada risiko kehilangan data di masa mendatang.", + "detail_no_continue": "Coba perbarui peramban ini jika Anda tidak menggunakan versi terbaru dan coba lagi.", + "learn_more": "Pelajari lebih lanjut", + "linux": "Linux", + "macos": "Mac", + "supported_browsers": "Untuk pengalaman terbaik, gunakan Chrome, Firefox, Edge, atau Safari.", + "title": "Peramban tidak didukung", + "use_desktop_heading": "Gunakan %(brand)s Desktop sebagai gantinya", + "use_mobile_heading": "Gunakan %(brand)s di ponsel sebagai gantinya", + "use_mobile_heading_after_desktop": "Atau gunakan aplikasi ponsel kami", + "windows_64bit": "Windows (64-bit)", + "windows_arm_64bit": "Windows (ARM 64-bit)" }, "info_tooltip_title": "Informasi", "integration_manager": { @@ -1146,6 +1302,7 @@ "error_connecting_heading": "Tidak dapat menghubungkan ke manajer integrasi", "explainer": "Manajer integrasi menerima data pengaturan, dan dapat mengubah widget, mengirimkan undangan ruangan, dan mengatur tingkat daya dengan sepengetahuan Anda.", "manage_title": "Kelola integrasi", + "toggle_label": "Aktifkan pengelola integrasi", "use_im": "Gunakan sebuah manajer integrasi untuk mengelola bot, widget, dan paket stiker.", "use_im_default": "Gunakan manajer integrasi (%(serverName)s) untuk mengelola bot, widget, dan paket stiker." }, @@ -1177,6 +1334,8 @@ "error_permissions_space": "Anda tidak memiliki izin untuk mengundang seseorang ke space ini.", "error_profile_undisclosed": "Pengguna mungkin atau mungkin tidak ada", "error_transfer_multiple_target": "Sebuah panggilan dapat dipindah ke sebuah pengguna.", + "error_unfederated_room": "Ruangan ini tidak terfederasi. Anda tidak dapat mengundang orang dari server eksternal.", + "error_unfederated_space": "Space ini tidak terfederasi. Anda tidak dapat mengundang orang dari server eksternal.", "error_unknown": "Kesalahan server yang tidak diketahui", "error_user_not_found": "Pengguna tidak ada", "error_version_unsupported_room": "Homeserver penggunanya tidak mendukung versi ruangannya.", @@ -1259,12 +1418,14 @@ "navigate_next_message_edit": "Pergi ke pesan berikutnya untuk diedit", "navigate_prev_history": "Ruangan atau space yang dikunjungi sebelumnya", "navigate_prev_message_edit": "Pergi ke pesan sebelumnya untuk diedit", + "next_landmark": "Pergi ke tengara berikutnya", "next_room": "Ruangan atau pesan langsung berikutnya", "next_unread_room": "Ruangan atau pesan langsung berikutnya yang belum dibaca", "number": "[nomor]", "open_user_settings": "Buka pengaturan pengguna", "page_down": "Halaman Bawah", "page_up": "Halaman Atas", + "prev_landmark": "Pergi ke tengara sebelumnya", "prev_room": "Ruangan atau pesan langsung sebelumnya", "prev_unread_room": "Ruangan atau pesan langsung sebelumnya yang belum dibaca", "room_list_collapse_section": "Tutup bagian daftar ruangan", @@ -1309,8 +1470,12 @@ "dynamic_room_predecessors": "Pendahulu ruang dinamis", "dynamic_room_predecessors_description": "Aktifkan MSC3946 (untuk mendukung arsip ruangan yang datang terlambat)", "element_call_video_rooms": "Ruangan video Element Call", + "exclude_insecure_devices": "Kecualikan perangkat tidak aman ketika mengirim/menerima pesan", + "exclude_insecure_devices_description": "Jika mode ini diaktifkan, pesan terenkripsi tidak akan dibagikan dengan perangkat yang tidak terverifikasi, dan pesan dari perangkat yang tidak terverifikasi akan ditampilkan sebagai kesalahan. Perlu diperhatikan bahwa jika Anda mengaktifkan mode ini, Anda mungkin tidak dapat berkomunikasi dengan pengguna yang belum memverifikasi perangkat mereka.", "experimental_description": "Merasa eksperimental? Coba ide terkini kami dalam pengembangan. Fitur ini belum selesai; mereka mungkin tidak stabil, mungkin berubah, atau dihapus sama sekali. Pelajari lebih lanjut.", "experimental_section": "Pratinjau awal", + "extended_profiles_msc_support": "Memerlukan server Anda untuk mendukung MSC4133", + "feature_disable_call_per_sender_encryption": "Nonaktifkan enkripsi per pengirim untuk Element Call", "feature_wysiwyg_composer_description": "Menggunakan teks kaya daripada Markdown dalam komposer pesan.", "group_calls": "Pengalaman panggilan grup baru", "group_developer": "Pengembang", @@ -1322,6 +1487,8 @@ "group_rooms": "Ruangan", "group_spaces": "Space", "group_themes": "Tema", + "group_threads": "Utas", + "group_ui": "Antarmuka pengguna", "group_voip": "Suara & Video", "group_widgets": "Widget", "hidebold": "Sembunyikan titik notifikasi (hanya tampilkan lencana penghitung)", @@ -1337,10 +1504,12 @@ "location_share_live_description": "Penerapan sementara. Lokasi tetap berada di riwayat ruangan.", "mjolnir": "Cara baru mengabaikan orang", "msc3531_hide_messages_pending_moderation": "Memperbolehkan moderator untuk menyembunyikan pesan yang akan dimoderasikan.", + "new_room_list": "Aktifkan daftar ruangan baru", "notification_settings": "Pengaturan Notifikasi Baru", "notification_settings_beta_caption": "Perkenalkan cara yang lebih sederhana untuk mengubah pengaturan notifikasi Anda. Sesuaikan %(brand)s Anda, sesuai keinginan Anda.", "notification_settings_beta_title": "Pengaturan Notifikasi", "notifications": "Aktifkan panel notifikasi di tajuk ruangan", + "release_announcement": "Pengumuman rilis", "render_reaction_images": "Render gambar khusus dalam reaksi", "render_reaction_images_description": "Terkadang disebut sebagai \"emoji khusus\".", "report_to_moderators": "Laporkan ke moderator", @@ -1348,7 +1517,7 @@ "sliding_sync": "Mode Sinkronisasi Geser", "sliding_sync_description": "Dalam pengembangan aktif, tidak dapat dinonaktifkan.", "sliding_sync_disabled_notice": "Keluar dan masuk kembali ke akun untuk menonaktifkan", - "sliding_sync_server_no_support": "Server Anda belum mendukungnya", + "sliding_sync_server_no_support": "Server Anda tidak memiliki dukungan", "under_active_development": "Dalam pengembangan aktif.", "unrealiable_e2e": "Tidak dapat diandalkan di ruangan terenkripsi", "video_rooms": "Ruangan video", @@ -1400,6 +1569,8 @@ "last_person_warning": "Anda adalah satu-satunya di sini. Jika Anda keluar, tidak ada siapa saja dapat bergabung di masa mendatang, termasuk Anda.", "leave_room_question": "Anda yakin ingin meninggalkan ruangan '%(roomName)s'?", "leave_space_question": "Apakah Anda yakin untuk keluar dari space '%(spaceName)s'?", + "room_leave_admin_warning": "Anda adalah administrator satu-satunya di ruangan ini. Jika Anda keluar, tidak ada siapa pun yang dapat mengubah pengaturan ruangan atau melakukan tindakan penting lainnya.", + "room_leave_mod_warning": "Anda adalah moderator satu-satunya di ruangan ini. Jika Anda keluar, tidak ada siapa pun yang dapat mengubah pengaturan ruangan atau melakukan tindakan penting lainnya.", "room_rejoin_warning": "Ruangan ini tidak publik. Anda tidak dapat bergabung lagi tanpa sebuah undangan.", "space_rejoin_warning": "Space ini tidak publik. Anda tidak dapat bergabung lagi tanpa sebuah undangan." }, @@ -1457,12 +1628,18 @@ "toggle_attribution": "Alih atribusi" }, "member_list": { + "count": { + "other": "%(count)s Anggota" + }, "filter_placeholder": "Saring anggota ruangan", "invite_button_no_perms_tooltip": "Anda tidak memiliki izin untuk mengundang pengguna", + "invited_label": "Diundang", + "no_matches": "Tidak ada kecocokan", "power_label": "%(userName)s (tingkat daya %(powerLevelNumber)s)" }, "member_list_back_action_label": "Anggota ruangan", "message_edit_dialog_title": "Editan pesan", + "migrating_crypto": "Bersabarlah. Kami sedang memperbarui Element untuk membuat enkripsi lebih cepat dan andal.", "mobile_guide": { "toast_accept": "Gunakan aplikasi", "toast_description": "%(brand)s bersifat eksperimental pada peramban web ponsel. Untuk pengalaman yang lebih baik dan fitur-fitur terkini, gunakan aplikasi natif gratis kami.", @@ -1480,6 +1657,7 @@ "class_global": "Global", "class_other": "Lainnya", "default": "Bawaan", + "default_settings": "Cocokkan pengaturan bawaan", "email_pusher_app_display_name": "Notifikasi Surel", "enable_prompt_toast_description": "Aktifkan notifikasi desktop", "enable_prompt_toast_title": "Notifikasi", @@ -1487,12 +1665,19 @@ "error_change_title": "Ubah pengaturan notifikasi", "keyword": "Kata kunci", "keyword_new": "Kata kunci baru", + "level_activity": "Aktivitas", + "level_highlight": "Sorotan", + "level_muted": "Dibisukan", + "level_none": "Tidak ada", + "level_notification": "Notifikasi", + "level_unsent": "Pengiriman dibatalkan", "mark_all_read": "Tandai semua sebagai dibaca", "mentions_and_keywords": "@sebutan & kata kunci", "mentions_and_keywords_description": "Dapatkan notifikasi hanya dengan sebutan dan kata kunci yang diatur di pengaturan Anda", - "mentions_keywords": "Sebutan & kata kunci", + "mentions_keywords": "Sebutan dan kata kunci", "message_didnt_send": "Pesan tidak terkirim. Klik untuk informasi.", - "mute_description": "Anda tidak akan mendapatkan notifikasi apa pun" + "mute_description": "Anda tidak akan mendapatkan notifikasi apa pun", + "mute_room": "Bisukan ruangan" }, "notifier": { "m.key.verification.request": "%(name)s meminta verifikasi" @@ -1573,7 +1758,8 @@ "online": "Daring", "online_for": "Daring selama %(duration)s", "unknown": "Tidak Dikenal", - "unknown_for": "Tidak diketahui untuk %(duration)s" + "unknown_for": "Tidak diketahui untuk %(duration)s", + "unreachable": "Server pengguna tidak dapat dijangkau" }, "quick_settings": { "all_settings": "Semua pengaturan", @@ -1596,6 +1782,7 @@ "report_content": { "description": "Melaporkan pesan ini akan mengirimkan ID peristiwa yang untuk ke administrator homeserver Anda. Jika pesan-pesan di ruangan ini terenkripsi, maka administrator homeserver Anda tidak akan dapat membaca teks pesan atau menampilkan file atau gambar apa saja.", "disagree": "Tidak Setuju", + "error_create_room_moderation_bot": "Tidak dapat membuat ruangan dengan bot moderasi", "hide_messages_from_user": "Periksa jika Anda ingin menyembunyikan semua pesan saat ini dan pesan baru dari pengguna ini.", "ignore_user": "Abaikan pengguna", "illegal_content": "Konten Ilegal", @@ -1603,6 +1790,8 @@ "nature": "Harap pilih sifat dan jelaskan apa yang membuat pesan ini kasar.", "nature_disagreement": "Apa yang ditulis pengguna itu salah.\nIni akan dilaporkan ke moderator ruangan.", "nature_illegal": "Pengguna ini menampilkan kelakuan yang ilegal, misalnya dengan doxing orang lain atau ancaman kekerasan.\nIni akan dilaporkan ke moderator ruangan yang mungkin melaporkannya juga ke otoritas hukum.", + "nature_nonstandard_admin": "Ruang ini didedikasikan untuk konten ilegal atau toksik atau moderator gagal memoderasi konten ilegal atau toksik.\nHal ini akan dilaporkan kepada administrator %(homeserver)s.", + "nature_nonstandard_admin_encrypted": "Ruang ini didedikasikan untuk konten ilegal atau toksik atau moderator gagal memoderasi konten ilegal atau toksik.\nHal ini akan dilaporkan kepada administrator %(homeserver)s. Administrator TIDAK akan dapat membaca konten terenkripsi di ruangan ini.", "nature_other": "Alasan yang lain. Mohon jelaskan masalahnya.\nIni akan dilaporkan ke moderator ruangan.", "nature_spam": "Pengguna ini spam ruangan dengan iklan, tautan ke iklan atau ke propaganda.\nIni akan dilaporkan ke moderator ruangan.", "nature_toxic": "Pengguna ini menampilkan kelakuan yang toksik, misalnya dengan menghina pengguna lain atau membagikan konten dewasa di ruangan ramah keluarga atau merusak aturan ruangan.\nIni akan dilaporkan ke moderator ruangan.", @@ -1612,6 +1801,10 @@ "spam_or_propaganda": "Spam atau propaganda", "toxic_behaviour": "Kelakukan Toxic" }, + "report_room": { + "description": "Laporkan ruangan ini ke admin homeserver Anda. Ini akan mengirimkan ID unik ruangan, tetapi jika pesan dienkripsi, administrator tidak akan dapat membacanya atau melihat file terbagi.", + "reason_label": "Jelaskan alasannya" + }, "restore_key_backup_dialog": { "count_of_decryption_failures": "Gagal untuk mendekripsi %(failedCount)s sesi!", "count_of_successfully_restored_keys": "Berhasil memulihkan %(sessionCount)s kunci", @@ -1624,26 +1817,48 @@ "key_backup_warning": "Peringatan: Anda seharusnya menyiapkan cadangan kunci di komputer yang dipercayai.", "key_fetch_in_progress": "Mendapatkan kunci- dari server…", "key_forgotten_text": "Jika Anda lupa Kunci Keamanan, Anda dapat ", - "key_is_invalid": "Bukan Kunci Keamanan yang absah", - "key_is_valid": "Ini sepertinya Kunci Keamanan yang absah!", + "key_is_invalid": "Bukan Kunci Pemulihan yang valid", + "key_is_valid": "Ini sepertinya Kunci Pemulihan yang valid!", "keys_restored_title": "Kunci-kunci terpulihkan", "load_error_content": "Tidak dapat memuat status cadangan", "load_keys_progress": "%(completed)s dari %(total)s kunci dipulihkan", "no_backup_error": "Tidak ada cadangan yang ditemukan!", - "phrase_forgotten_text": "Jika Anda lupa Frasa Keamanan, Anda dapat menggunakan Kunci Keamanan Anda atau siapkan opsi pemulihan baru", - "recovery_key_mismatch_description": "Cadangan tidak dapat didekripsikan dengan Kunci Keamanan ini: mohon periksa jika Anda memasukkan Kunci Keamanan yang benar.", + "phrase_forgotten_text": "Jika Anda lupa Frasa Keamanan Anda, Anda dapat menggunakan Kunci Pemulihan Anda atau mengatur opsi pemulihan baru", + "recovery_key_mismatch_description": "Cadangan tidak dapat didekripsi dengan Kunci Pemulihan ini: mohon pastikan Anda memasukkan Kunci Pemulihan yang benar.", "recovery_key_mismatch_title": "Kunci Keamanan tidak cocok", "restore_failed_error": "Tidak dapat memulihkan cadangan" }, "right_panel": { - "add_integrations": "Tambahkan widget, jembatan & bot", + "add_integrations": "Tambahkan ekstensi", + "add_topic": "Tambahkan topik", + "extensions_button": "Ekstensi", + "extensions_empty_description": "Pilih “%(addIntegrations)s” untuk menelusuri dan menambahkan ekstensi ke ruangan ini", + "extensions_empty_title": "Tingkatkan produktivitas dengan lebih banyak alat, widget, dan bot", "files_button": "File", "pinned_messages": { + "empty_description": "Pilih pesan dan pilih “%(pinAction)s” untuk disertakan di sini.", + "empty_title": "Sematkan pesan penting agar mudah ditemukan", + "header": { + "other": "%(count)s Pesan tersemat" + }, "limits": { "other": "Anda hanya dapat memasang pin sampai %(count)s widget" - } + }, + "menu": "Buka menu", + "release_announcement": { + "close": "Oke", + "description": "Temukan semua pesan yang disematkan di sini. Arahkan pesan apa pun dan pilih “Sematkan” untuk menambahkannya.", + "title": "Semua pesan baru yang disematkan" + }, + "reply_thread": "Balas ke pesan dalam utas", + "unpin_all": { + "button": "Lepas sematan semua pesan", + "content": "Pastikan Anda benar-benar ingin melepaskan sematan semua pesan tersemat. Tindakan ini tidak dapat diurungkan.", + "title": "Hapus sematan semua pesan?" + }, + "view": "Lihat di lini masa" }, - "pinned_messages_button": "Disematkan", + "pinned_messages_button": "Pesan yang disematkan", "poll": { "active_heading": "Pemungutan suara yang aktif", "empty_active": "Tidak ada pemungutan suara yang aktif di ruangan ini", @@ -1668,7 +1883,7 @@ "view_in_timeline": "Tampilkan pemungutan suara di lini masa", "view_poll": "Tampilkan pemungutan suara" }, - "polls_button": "Riwayat pemungutan suara", + "polls_button": "Pemungutan suara", "room_summary_card": { "title": "Informasi ruangan" }, @@ -1697,6 +1912,7 @@ "forget": "Lupakan Ruangan", "low_priority": "Prioritas Rendah", "mark_read": "Tandai sebagai dibaca", + "mark_unread": "Tandai sebagai belum dibaca", "notifications_default": "Sesuai dengan pengaturan bawaan", "notifications_mute": "Bisukan ruangan", "title": "Opsi ruangan", @@ -1744,6 +1960,8 @@ }, "room_is_public": "Ruangan ini publik" }, + "header_avatar_open_settings_label": "Buka pengaturan ruangan", + "header_face_pile_tooltip": "Alihkan daftar anggota", "header_untrusted_label": "Tidak dipercaya", "inaccessible": "Ruangan atau space ini tidak dapat diakses pada saat ini.", "inaccessible_name": "%(roomName)s tidak dapat diakses sekarang.", @@ -1769,7 +1987,7 @@ "invite_email_mismatch_suggestion": "Bagikan email ini di Pengaturan untuk mendapatkan undangan secara langsung di %(brand)s.", "invite_sent_to_email": "Undangan ini telah dikirim ke %(email)s", "invite_sent_to_email_room": "Undangan ke %(roomName)s ini terkirim ke %(email)s", - "invite_subtitle": " mengundang Anda", + "invite_subtitle": "Diundang oleh ", "invite_this_room": "Undang ke ruangan ini", "invite_title": "Apakah Anda ingin bergabung %(roomName)s?", "inviter_unknown": "Tidak Dikenal", @@ -1812,11 +2030,23 @@ "not_found_title": "Ruangan atau space ini tidak ada.", "not_found_title_name": "%(roomName)s tidak ada.", "peek_join_prompt": "Anda melihat tampilan %(roomName)s. Ingin bergabung?", + "pinned_message_badge": "Pesan yang disematkan", + "pinned_message_banner": { + "button_close_list": "Tutup daftar", + "button_view_all": "Lihat semua", + "description": "Ruangan ini memiliki pesan yang disematkan. Klik untuk melihatnya.", + "go_to_message": "Lihat pesan yang disematkan di lini masa.", + "title": "%(index)s dari %(length)s Pesan yang disematkan" + }, "read_topic": "Klik untuk membaca topik", "rejecting": "Menolak undangan…", "rejoin_button": "Bergabung Ulang", "search": { "all_rooms_button": "Cari semua ruangan", + "placeholder": "Cari pesan...", + "summary": { + "other": "%(count)s hasil ditemukan untuk “”" + }, "this_room_button": "Cari ruangan ini" }, "status_bar": { @@ -1849,37 +2079,90 @@ }, "uploading_single_file": "Mengunggah %(filename)s" }, + "video_room": "Ruangan ini adalah ruangan video", "waiting_for_join_subtitle": "Setelah pengguna yang diundang telah bergabung ke %(brand)s, Anda akan dapat bercakapan dan ruangan akan terenkripsi secara ujung ke ujung", "waiting_for_join_title": "Menunggu pengguna untuk bergabung ke %(brand)s" }, "room_list": { "add_room_label": "Tambahkan ruangan", "add_space_label": "Tambahkan space", + "appearance": "Penampilan", "breadcrumbs_empty": "Tidak ada ruangan yang baru saja dilihat", "breadcrumbs_label": "Ruangan yang baru saja dilihat", + "empty": { + "no_chats": "Belum ada obrolan", + "no_chats_description": "Mulailah dengan mengirim pesan kepada seseorang atau dengan membuat ruangan", + "no_chats_description_no_room_rights": "Mulailah dengan mengirim pesan kepada seseorang", + "no_favourites": "Anda belum memiliki obrolan favorit", + "no_favourites_description": "Anda dapat menambahkan obrolan ke favorit Anda di pengaturan obrolan", + "no_invites": "Anda tidak memiliki undangan yang belum dibaca", + "no_mentions": "Anda tidak memiliki sebutan yang belum dibaca", + "no_people": "Anda belum memiliki obrolan langsung dengan siapa pun", + "no_people_description": "Anda dapat membatalkan pilihan saringan untuk melihat percakapan Anda yang lain", + "no_rooms": "Anda belum berada di ruangan mana pun", + "no_rooms_description": "Anda dapat membatalkan pilihan saringan untuk melihat percakapan Anda yang lain", + "no_unread": "Selamat! Anda tidak memiliki pesan yang belum dibaca", + "show_activity": "Lihat semua aktivitas", + "show_chats": "Tampilkan semua obrolan" + }, "failed_add_tag": "Gagal menambahkan tag %(tagName)s ke ruangan", "failed_remove_tag": "Gagal menghapus tanda %(tagName)s dari ruangan", "failed_set_dm_tag": "Gagal menetapkan tanda pesan langsung", + "filters": { + "favourite": "Favorit", + "invites": "Undangan", + "mentions": "Sebutan", + "people": "Orang", + "rooms": "Ruangan", + "unread": "Belum dibaca" + }, "home_menu_label": "Opsi Beranda", "join_public_room_label": "Bergabung dengan ruangan publik", "joining_rooms_status": { "one": "Saat ini bergabung dengan %(count)s ruangan", "other": "Saat ini bergabung dengan %(count)s ruangan" }, + "list_title": "Daftar ruangan", + "more_options": { + "copy_link": "Salin tautan ruangan", + "favourited": "Difavorit", + "leave_room": "Tinggalkan ruangan", + "low_priority": "Prioritas rendah", + "mark_read": "Tandai sebagai dibaca", + "mark_unread": "Tandai sebagai belum dibaca" + }, "notification_options": "Opsi notifikasi", + "open_space_menu": "Buka menu space", + "primary_filters": "Filter daftar ruangan", "redacting_messages_status": { "one": "Saat ini menghapus pesan-pesan di %(count)s ruangan", "other": "Saat ini menghapus pesan-pesan di %(count)s ruangan" }, + "room": { + "more_options": "Opsi Lainnya", + "open_room": "Buka ruangan %(roomName)s" + }, + "room_options": "Opsi Ruangan", "show_less": "Tampilkan lebih sedikit", + "show_message_previews": "Tampilkan pratinjau pesan", "show_n_more": { "one": "Tampilkan %(count)s lagi", "other": "Tampilkan %(count)s lagi" }, "show_previews": "Tampilkan tampilan pesan", + "sort": "Urutkan", "sort_by": "Sortir berdasarkan", "sort_by_activity": "Aktivitas", + "sort_by_alphabet": "A-Z", + "sort_type": { + "activity": "Aktivitas", + "atoz": "A-Z" + }, "sort_unread_first": "Tampilkan ruangan dengan pesan yang belum dibaca dahulu", + "space_menu": { + "home": "Beranda space", + "space_settings": "Pengaturan Space" + }, "space_menu_label": "Menu %(spaceName)s", "sublist_options": "Tampilkan daftar opsi", "suggested_rooms_heading": "Ruangan yang Disarankan" @@ -1951,6 +2234,8 @@ "error_deleting_alias_description": "Terjadi sebuah kesalahan menghapus alamat. Itu mungkin sudah tidak ada atau ada kesalahan sementara.", "error_deleting_alias_description_forbidden": "Anda tidak memiliki izin untuk menghapus alamatnya.", "error_deleting_alias_title": "Terjadi kesalahan menghapus alamat", + "error_publishing": "Tidal dapat menerbitkan ruangan", + "error_publishing_detail": "Terjadi kesalahan saat menerbitkan ruangan ini", "error_save_space_settings": "Gagal untuk menyimpan pengaturan space.", "error_updating_alias_description": "Terjadi sebuah kesalahan memperbarui alamat alternatif ruangan. Ini mungkin tidak diperbolehkan oleh servernya atau ada kegagalan sementara.", "error_updating_canonical_alias_description": "Terjadi sebuah kesalahan memperbarui alamat utama ruangan. Ini mungkin tidak diperbolehkan oleh servernya atau ada kegagalan sementara.", @@ -2104,8 +2389,9 @@ }, "join_rule_upgrade_upgrading_room": "Meningkatkan ruangan", "public_without_alias_warning": "Untuk menautkan ruangan ini, mohon tambahkan sebuah alamat.", + "publish_room": "Buat ruangan ini terlihat di direktori ruangan publik.", "publish_space": "Buat ruang ini terlihat di direktori ruangan publik.", - "strict_encryption": "Jangan kirim pesan terenkripsi ke sesi yang belum diverifikasi di ruangan ini dari sesi ini", + "strict_encryption": "Hanya kirim pesan ke pengguna terverifikasi.", "title": "Keamanan & Privasi" }, "title": "Pengaturan Ruangan — %(roomName)s", @@ -2159,6 +2445,10 @@ "recent_changes_heading": "Perubahan terbaru yang belum diterima", "title": "Server tidak merespon" }, + "service_worker_error": { + "description": "%(brand)s memerlukan pekerja layanan untuk memuat media yang diautentikasi dari repositori konten Matrix. Ini tidak didukung oleh peramban Anda sehingga Anda mungkin mengalami kegagalan pemuatan media.", + "title": "Gagal memuat pekerja layanan" + }, "seshat": { "error_initialising": "Initialisasi pencarian pesan gagal, periksa pengaturan Anda untuk informasi lanjut", "reset_button": "Atur ulang penyimpanan peristiwa", @@ -2175,6 +2465,8 @@ "access_token_detail": "Token akses Anda memberikan akses penuh ke akun Anda. Jangan bagikan dengan siapa pun.", "brand_version": "Versi %(brand)s:", "clear_cache_reload": "Hapus cache dan muat ulang", + "crypto_version": "Versi kripto:", + "dialog_title": "Pengaturan: Bantuan & Tentang", "help_link": "Untuk bantuan dengan menggunakan %(brand)s, klik di sini.", "homeserver": "Homeserver adalah %(homeserverUrl)s", "identity_server": "Server identitas adalah %(identityServerUrl)s", @@ -2183,21 +2475,34 @@ } }, "settings": { + "account": { + "dialog_title": "Pengaturan: Akun", + "title": "Akun" + }, "all_rooms_home": "Tampilkan semua ruangan di Beranda", "all_rooms_home_description": "Semua ruangan yang Anda bergabung akan ditampilkan di Beranda.", "always_show_message_timestamps": "Selalu tampilkan stempel waktu pesan", "appearance": { + "bundled_emoji_font": "Gunakan fon emoji yang dibundel", + "compact_layout": "Tampilkan teks dan pesan ringkas", + "compact_layout_description": "Tata letak modern harus dipilih untuk menggunakan fitur ini.", "custom_font": "Gunakan sebuah font sistem", "custom_font_description": "Atur sebuah nama font yang terinstal di sistem Anda & %(brand)s akan mencoba menggunakannya.", "custom_font_name": "Nama font sistem", "custom_font_size": "Gunakan ukuran kustom", - "custom_theme_error_downloading": "Terjadi kesalahan saat mengunduh informasi tema.", + "custom_theme_add": "Tambahkan tema kustom", + "custom_theme_downloading": "Mengunduh tema kustom...", + "custom_theme_error_downloading": "Terjadi kesalahan mengunduh tema", + "custom_theme_help": "Masukkan URL tema kustom yang ingin Anda terapkan.", "custom_theme_invalid": "Skema tema tidak absah.", + "dialog_title": "Pengaturan: Penampilan", "font_size": "Ukuran font", + "font_size_default": "%(fontSize)s (bawaan)", + "high_contrast": "Kontras tinggi", "image_size_default": "Bawaan", "image_size_large": "Besar", "layout_bubbles": "Gelembung pesan", - "layout_irc": "IRC (Eksperimental)", + "layout_irc": "IRC (eksperimental)", "match_system_theme": "Sesuaikan dengan tema sistem", "timeline_image_size": "Ukuran gambar di lini masa" }, @@ -2208,9 +2513,80 @@ "code_block_expand_default": "Buka blok kode secara bawaan", "code_block_line_numbers": "Tampilkan nomor barisan di blok kode", "disable_historical_profile": "Tampilkan foto profil dan nama saat ini untuk pengguna dalam riwayat pesan", + "discovery": { + "title": "Cara menemukan Anda" + }, "emoji_autocomplete": "Aktifkan saran emoji saat mengetik", "enable_markdown": "Aktifkan Markdown", "enable_markdown_description": "Mulai pesan dengan /plain untuk mengirim tanpa Markdown.", + "encryption": { + "advanced": { + "breadcrumb_first_description": "Detail akun, kontak, preferensi, dan daftar obrolan Anda akan disimpan", + "breadcrumb_page": "Atur ulang enkripsi", + "breadcrumb_second_description": "Anda akan kehilangan semua riwayat pesan yang hanya disimpan di server", + "breadcrumb_third_description": "Anda perlu memverifikasi ulang semua perangkat dan kontak yang ada", + "breadcrumb_title": "Apakah Anda yakin ingin mengatur ulang identitas Anda?", + "breadcrumb_title_forgot": "Lupa kunci pemulihan Anda? Anda harus mengatur ulang identitas Anda.", + "breadcrumb_title_sync_failed": "Gagal menyinkronkan penyimpanan kunci. Anda perlu mengatur ulang identitas Anda.", + "breadcrumb_warning": "Lakukan ini hanya jika Anda yakin akun Anda telah terkompromi.", + "details_title": "Detail enkripsi", + "do_not_close_warning": "Jangan tutup jendela ini sampai pengaturan ulang selesai", + "export_keys": "Ekspor kunci", + "import_keys": "Impor kunci", + "other_people_device_description": "Secara bawaan dalam ruangan terenkripsi, jangan kirim pesan terenkripsi kepada siapa pun sampai Anda memverifikasinya", + "other_people_device_label": "Jangan pernah mengirim pesan terenkripsi ke perangkat yang tidak terverifikasi", + "other_people_device_title": "Perangkat orang lain", + "reset_identity": "Atur ulang identitas kriptografi", + "reset_in_progress": "Pengaturan ulang sedang berlangsung...", + "session_id": "ID sesi:", + "session_key": "Kunci sesi:", + "title": "Lanjutan" + }, + "confirm_key_storage_off": "Apakah Anda yakin ingin tetap menonaktifkan penyimpanan kunci?", + "confirm_key_storage_off_description": "Jika Anda keluar dari semua perangkat, Anda akan kehilangan riwayat pesan dan perlu memverifikasi semua kontak yang ada lagi. Pelajari lebih lanjut", + "delete_key_storage": { + "breadcrumb_page": "Hapus penyimpanan kunci", + "confirm": "Hapus penyimpanan kunci", + "description": "Menghapus penyimpanan kunci akan menghapus identitas kriptografi dan kunci pesan Anda dari server dan menonaktifkan fitur keamanan berikut:", + "list_first": "Anda tidak akan memiliki riwayat pesan terenkripsi di perangkat baru", + "list_second": "Anda akan kehilangan akses ke pesan terenkripsi jika Anda keluar dari %(brand)s di mana pun", + "title": "Apakah Anda yakin ingin mematikan penyimpanan kunci dan menghapusnya?" + }, + "device_not_verified_button": "Verifikasi perangkat ini", + "device_not_verified_description": "Anda perlu memverifikasi perangkat ini untuk melihat pengaturan enkripsi Anda.", + "device_not_verified_title": "Perangkat tidak diverifikasi", + "dialog_title": "Pengaturan: Enkripsi", + "key_storage": { + "allow_key_storage": "Izinkan penyimpanan kunci", + "description": "Simpan identitas kriptografi dan kunci pesan Anda secara aman di server. Hal ini akan memungkinkan Anda untuk melihat riwayat pesan Anda di perangkat baru. Pelajari lebih lanjut", + "title": "Penyimpanan kunci" + }, + "recovery": { + "change_recovery_confirm_button": "Konfirmasikan kunci pemulihan baru", + "change_recovery_confirm_description": "Masukkan kunci pemulihan baru Anda di bawah ini untuk menyelesaikannya. Kunci lama Anda tidak akan berfungsi lagi.", + "change_recovery_confirm_title": "Masukkan kunci pemulihan baru Anda", + "change_recovery_key": "Ubah kunci pemulihan", + "change_recovery_key_description": "Tuliskan kunci pemulihan baru ini di tempat yang aman. Kemudian klik Lanjutkan untuk mengonfirmasi perubahan.", + "change_recovery_key_title": "Ubah kunci pemulihan?", + "description": "Pulihkan identitas kriptografi dan riwayat pesan Anda dengan kunci pemulihan jika Anda kehilangan semua perangkat yang ada.", + "enter_key_error": "Kunci pemulihan yang Anda masukkan salah.", + "enter_recovery_key": "Masukkan kunci pemulihan", + "forgot_recovery_key": "Lupa kunci pemulihan?", + "key_storage_warning": "Penyimpanan kunci Anda tidak tersinkron. Klik tombol di bawah ini untuk memperbaiki masalah.", + "save_key_description": "Jangan bagikan ini kepada siapa pun!", + "save_key_title": "Kunci pemulihan", + "set_up_recovery": "Siapkan pemulihan", + "set_up_recovery_confirm_button": "Selesaikan penyiapan", + "set_up_recovery_confirm_description": "Masukkan kunci pemulihan yang ditunjukkan pada layar sebelumnya untuk menyelesaikan penyiapan pemulihan.", + "set_up_recovery_confirm_title": "Masukkan kunci pemulihan Anda untuk mengonfirmasi", + "set_up_recovery_description": "Penyimpanan kunci Anda dilindungi oleh kunci pemulihan. Jika Anda memerlukan kunci pemulihan baru setelah penyiapan, Anda dapat membuatnya kembali dengan memilih '%(changeRecoveryKeyButton)s'.", + "set_up_recovery_save_key_description": "Catatlah kunci pemulihan ini di tempat yang aman, seperti pengelola kata sandi, catatan terenkripsi, atau brankas fisik.", + "set_up_recovery_save_key_title": "Simpan kunci pemulihan Anda di tempat yang aman", + "set_up_recovery_secondary_description": "Setelah mengeklik lanjutkan, kami akan membuat kunci pemulihan untuk Anda.", + "title": "Pemulihan" + }, + "title": "Enkripsi" + }, "general": { "account_management_section": "Manajemen akun", "account_section": "Akun", @@ -2223,6 +2599,14 @@ "add_msisdn_dialog_title": "Tambahkan Nomor Telepon", "add_msisdn_instructions": "Sebuah teks pesan telah dikirim ke +%(msisdn)s. Silakan masukkan kode verifikasinya.", "add_msisdn_misconfigured": "Aliran penambahan/pengaitan MSISDN tidak diatur dengan benar", + "allow_spellcheck": "Izinkan pemeriksaan ejaan", + "application_language": "Bahasa aplikasi", + "application_language_reload_hint": "Aplikasi akan dimuat ulang setelah memilih bahasa lain", + "avatar_remove_progress": "Menghapus gambar...", + "avatar_save_progress": "Mengunggah gambar...", + "avatar_upload_error_text": "Format berkas tidak didukung atau gambar lebih besar dari %(size)s.", + "avatar_upload_error_text_generic": "Format berkas mungkin tidak didukung.", + "avatar_upload_error_title": "Gambar avatar tidak dapat diunggah", "confirm_adding_email_body": "Klik tombol di bawah untuk mengkonfirmasi penambahan alamat email ini.", "confirm_adding_email_title": "Konfirmasi penambahan email", "deactivate_confirm_body": "Apakah Anda yakin ingin menonaktifkan akun Anda? Ini tidak dapat dibatalkan.", @@ -2238,10 +2622,14 @@ "deactivate_confirm_erase_label": "Sembunyikan pesan saya dari orang baru bergabung", "deactivate_section": "Nonaktifkan Akun", "deactivate_warning": "Menonaktifkan akun Anda adalah aksi yang permanen — hati-hati!", - "discovery_email_empty": "Opsi penemuan akan tersedia setelah Anda telah menambahkan sebuah email di atas.", + "discovery_email_empty": "Opsi penemuan akan muncul setelah Anda menambahkan surel.", "discovery_email_verification_instructions": "Verifikasi tautannya di kotak masuk Anda", - "discovery_msisdn_empty": "Opsi penemuan akan tersedia setelah Anda telah menambahkan sebuah nomor telepon di atas.", + "discovery_msisdn_empty": "Opsi penemuan akan muncul setelah Anda telah menambahkan nomor telepon.", "discovery_needs_terms": "Terima Ketentuan Layanannya server identitas %(serverName)s untuk mengizinkan Anda untuk dapat ditemukan dengan alamat email atau nomor telepon.", + "discovery_needs_terms_title": "Biarkan orang lain menemukan Anda", + "display_name": "Nama Tampilan", + "display_name_error": "Tidak dapat mengatur nama tampilan", + "email_adding_unsupported_by_hs": "Homeserver ini tidak mendukung penambahan alamat surel ke akun Anda.", "email_address_in_use": "Alamat email ini telah dipakai", "email_address_label": "Alamat Email", "email_not_verified": "Alamat email Anda belum diverifikasi", @@ -2266,7 +2654,9 @@ "error_share_msisdn_discovery": "Tidak dapat membagikan nomor telepon", "identity_server_no_token": "Tidak ada token akses identitas yang ditemukan", "identity_server_not_set": "Server identitas tidak diatur", - "language_section": "Bahasa dan wilayah", + "invalid_phone_number": "Nomor telepon yang diberikan tampaknya tidak valid.", + "language_section": "Bahasa", + "msisdn_adding_unsupported_by_hs": "Homeserver ini tidak mendukung penambahan nomor telepon ke akun Anda.", "msisdn_in_use": "Nomor telepon ini telah dipakai", "msisdn_label": "Nomor Telepon", "msisdn_verification_field_label": "Kode verifikasi", @@ -2275,11 +2665,16 @@ "oidc_manage_button": "Kelola akun", "password_change_section": "Atur kata sandi akun baru…", "password_change_success": "Kata sandi Anda berhasil diubah.", + "personal_info": "Info pribadi", + "profile_subtitle": "Ini adalah bagaimana Anda akan terlihat kepada orang lain dalam aplikasi.", + "profile_subtitle_oidc": "Akun Anda dikelola secara terpisah oleh penyedia identitas sehingga beberapa informasi pribadi Anda tidak dapat diubah di sini.", "remove_email_prompt": "Hapus %(email)s?", "remove_msisdn_prompt": "Hapus %(phone)s?", - "spell_check_locale_placeholder": "Pilih locale" + "spell_check_locale_placeholder": "Pilih locale", + "unable_to_load_emails": "Tidak dapat memuat alamat surel", + "unable_to_load_msisdns": "Tidak dapat memuat nomor telepon", + "username": "Nama pengguna" }, - "image_thumbnails": "Tampilkan gambar mini untuk gambar", "inline_url_previews_default": "Aktifkan tampilan URL secara bawaan", "inline_url_previews_room": "Aktifkan tampilan URL secara bawaan untuk anggota di ruangan ini", "inline_url_previews_room_account": "Aktifkan tampilan URL secara bawaan (hanya memengaruhi Anda)", @@ -2301,13 +2696,13 @@ "enter_phrase_description": "Masukkan frasa keamanan yang hanya Anda tahu, yang digunakan untuk mengamankan data Anda. Supaya aman, jangan menggunakan ulang kata sandi akun Anda.", "enter_phrase_title": "Masukkan sebuah Frasa Keamanan", "enter_phrase_to_confirm": "Masukkan Frasa Keamanan sekali lagi untuk mengkonfirmasinya.", - "generate_security_key_description": "Kami akan membuat sebuah Kunci Keamanan untuk Anda simpan di tempat yang aman, seperti manajer sandi atau brankas.", - "generate_security_key_title": "Buat sebuah Kunci Keamanan", + "generate_security_key_description": "Kami akan membuat Kunci Pemulihan untuk Anda simpan di tempat yang aman, seperti pengelola kata sandi atau brankas.", + "generate_security_key_title": "Buat Kunci Pemulihan", "pass_phrase_match_failed": "Itu tidak cocok.", "pass_phrase_match_success": "Mereka cocok!", "phrase_strong_enough": "Hebat! Frasa Keamanan ini kelihatannya kuat.", "secret_storage_query_failure": "Tidak dapat menanyakan status penyimpanan rahasia", - "security_key_safety_reminder": "Simpan Kunci Keamanan Anda di tempat yang aman, seperti manajer sandi atau sebuah brankas, yang digunakan untuk mengamankan data terenkripsi Anda.", + "security_key_safety_reminder": "Simpan Kunci Pemulihan Anda di tempat yang aman, seperti pengelola kata sandi atau brankas, karena digunakan untuk melindungi data terenkripsi Anda.", "set_phrase_again": "Pergi kembali untuk menyiapkannya lagi.", "settings_reminder": "Anda juga dapat menyiapkan Cadangan Aman & kelola kunci Anda di Pengaturan.", "title_confirm_phrase": "Konfirmasi Frasa Keamanan", @@ -2315,7 +2710,7 @@ "title_set_phrase": "Atur sebuah Frasa Keamanan", "unable_to_setup": "Tidak dapat menyiapkan penyimpanan rahasia", "use_different_passphrase": "Gunakan frasa sandi yang berbeda?", - "use_phrase_only_you_know": "Gunakan frasa rahasia yang hanya Anda tahu, dan simpan sebuah Kunci Keamanan untuk menggunakannya untuk cadangan secara opsional." + "use_phrase_only_you_know": "Gunakan frasa rahasia yang hanya Anda ketahui, dan secara opsional simpan Kunci Pemulihan untuk digunakan untuk cadangan." } }, "key_export_import": { @@ -2333,12 +2728,28 @@ "phrase_strong_enough": "Hebat! Frasa keamanan ini kelihatannya kuat" }, "keyboard": { + "dialog_title": "Pengaturan: Papan Ketik", "title": "Papan tik" }, + "labs": { + "dialog_title": "Pengaturan: Uji Coba" + }, + "labs_mjolnir": { + "dialog_title": "Pengaturan: Pengguna yang Diabaikan" + }, + "media_preview": { + "hide_avatars": "Sembunyikan avatar ruangan dan pengundang", + "hide_media": "Selalu sembunyikan", + "media_preview_description": "Media tersembunyi selalu dapat ditampilkan dengan mengetuknya", + "media_preview_label": "Tampilkan media di lini masa", + "show_in_private": "Di ruangan privat", + "show_media": "Selalu tampilkan" + }, "notifications": { "default_setting_description": "Pengaturan ini akan diterapkan secara bawaan ke semua ruangan Anda.", "default_setting_section": "Saya ingin diberi tahu (Pengaturan Bawaan)", "desktop_notification_message_preview": "Tampilkan tampilan pesan di notifikasi desktop", + "dialog_title": "Pengaturan: Notifikasi", "email_description": "Terima surel ikhtisar notifikasi yang terlewat", "email_section": "Kirim surel ikhtisar", "email_select": "Pilih surel mana yang ingin dikirimkan ikhtisar. Kelola surel Anda di .", @@ -2397,12 +2808,15 @@ "code_blocks_heading": "Blok kode", "compact_modern": "Gunakan tata letak 'Modern' yang lebih kecil", "composer_heading": "Komposer", + "default_timezone": "Bawaan peramban (%(timezone)s)", + "dialog_title": "Pengaturan: Preferensi", "enable_hardware_acceleration": "Aktifkan akselerasi perangkat keras", "enable_tray_icon": "Tampilkan ikon baki dan minimalkan window ke ikonnya jika ditutup", "keyboard_heading": "Pintasan keyboard", "keyboard_view_shortcuts_button": "Untuk melihat semua shortcut keyboard, klik di sini.", "media_heading": "Gambar, GIF, dan video", "presence_description": "Bagikan aktivitas dan status Anda dengan orang lain.", + "publish_timezone": "Terbitkan zona waktu di profil publik", "rm_lifetime": "Delay Penanda Bacaan (md)", "rm_lifetime_offscreen": "Delay Penanda Bacaan diluar layar (md)", "room_directory_heading": "Direktori ruangan", @@ -2410,7 +2824,8 @@ "show_avatars_pills": "Tampilkan avatar di sebutan pengguna, ruangan, dan peristiwa", "show_polls_button": "Tampilkan tombol pemungutan suara", "surround_text": "Kelilingi teks yang dipilih saat mengetik karakter khusus", - "time_heading": "Tampilkan waktu" + "time_heading": "Tampilkan waktu", + "user_timezone": "Atur zona waktu" }, "prompt_invite": "Tanyakan sebelum mengirim undangan ke ID Matrix yang mungkin tidak absah", "replace_plain_emoji": "Ganti emoji teks biasa secara otomatis", @@ -2419,6 +2834,9 @@ "bulk_options_accept_all_invites": "Terima semua %(invitedRooms)s undangan", "bulk_options_reject_all_invites": "Tolak semua %(invitedRooms)s undangan", "bulk_options_section": "Opsi massal", + "dehydrated_device_description": "Fitur perangkat luring memungkinkan Anda menerima pesan terenkripsi bahkan ketika Anda tidak masuk ke perangkat apa pun", + "dehydrated_device_enabled": "Perangkat luring diaktifkan", + "dialog_title": "Pengaturan: Keamanan & Privasi", "e2ee_default_disabled_warning": "Admin server Anda telah menonaktifkan enkripsi ujung ke ujung secara bawaan di ruangan privat & Pesan Langsung.", "enable_message_search": "Aktifkan pencarian pesan di ruangan terenkripsi", "encryption_section": "Enkripsi", @@ -2446,7 +2864,7 @@ "message_search_unsupported_web": "%(brand)s tidak dapat menyimpan pesan terenkripsi secara lokal dengan aman saat dijalankan di browser. Gunakan %(brand)s Desktop supaya pesan terenkripsi dapat muncul di hasil pencarian.", "record_session_details": "Rekam nama, versi, dan URL klien untuk dapat mengenal sesi dengan lebih mudah dalam pengelola sesi", "send_analytics": "Kirim data analitik", - "strict_encryption": "Jangan kirim pesan terenkripsi ke sesi yang belum diverifikasi dari sesi ini" + "strict_encryption": "Hanya kirim pesan ke pengguna terverifikasi" }, "send_read_receipts": "Kirim laporan dibaca", "send_read_receipts_unsupported": "Server Anda tidak mendukung penonaktifkan pengiriman laporan dibaca.", @@ -2477,6 +2895,7 @@ "device_unverified_description_current": "Verifikasi sesi Anda saat ini untuk perpesanan aman yang ditingkatkan.", "device_verified_description": "Sesi ini siap untuk perpesanan yang aman.", "device_verified_description_current": "Sesi Anda saat ini siap untuk perpesanan aman.", + "dialog_title": "Pengaturan: Sesi", "error_pusher_state": "Gagal menetapkan keadaan pendorong", "error_set_name": "Gagal mengatur nama sesi", "filter_all": "Semua", @@ -2493,6 +2912,7 @@ "inactive_sessions_list_description": "Pertimbangkan untuk mengeluarkan sesi lama (%(inactiveAgeDays)s hari atau lebih) yang Anda tidak gunakan lagi.", "ip": "Alamat IP", "last_activity": "Aktivitas terakhir", + "manage": "Kelola sesi ini", "mobile_session": "Sesi ponsel", "n_sessions_selected": { "one": "%(count)s sesi dipilih", @@ -2516,9 +2936,10 @@ "security_recommendations_description": "Tingkatkan keamanan akun Anda dengan mengikuti saran berikut.", "session_id": "ID Sesi", "show_details": "Tampilkan detail", - "sign_in_with_qr": "Masuk dengan kode QR", + "sign_in_with_qr": "Tautkan perangkat baru", "sign_in_with_qr_button": "Tampilkan kode QR", - "sign_in_with_qr_description": "Anda dapat menggunakan perangkat ini untuk masuk ke perangkat yang baru dengan sebuah kode QR. Anda harus memindai kode QR yang ditampilkan di perangkat ini dengan perangkat Anda yang telah keluar dari akun.", + "sign_in_with_qr_description": "Gunakan kode QR untuk masuk ke akun di perangkat lain dan menyiapkan perpesanan aman.", + "sign_in_with_qr_unsupported": "Tidak didukung oleh penyedia akun Anda", "sign_out": "Keluarkan sesi ini", "sign_out_all_other_sessions": "Keluar dari semua sesi lain (%(otherSessionsCount)s)", "sign_out_confirm_description": { @@ -2558,7 +2979,9 @@ "show_redaction_placeholder": "Tampilkan sebuah penampung untuk pesan terhapus", "show_stickers_button": "Tampilkan tombol stiker", "show_typing_notifications": "Tampilkan notifikasi pengetikan", + "showbold": "Tampilkan semua aktivitas dalam daftar ruangan (titik atau jumlah pesan belum dibaca)", "sidebar": { + "dialog_title": "Pengaturan: Bilah Samping", "metaspaces_favourites_description": "Kelompokkan semua ruangan dan orang favorit Anda di satu tempat.", "metaspaces_home_all_rooms": "Tampilkan semua ruangan", "metaspaces_home_all_rooms_description": "Tampilkan semua ruangan di Beranda, walaupun mereka berada di sebuah space.", @@ -2567,10 +2990,14 @@ "metaspaces_orphans_description": "Kelompokkan semua ruangan yang tidak ada di sebuah space di satu tempat.", "metaspaces_people_description": "Kelompokkan semua orang di satu tempat.", "metaspaces_subsection": "Space yang ditampilkan", + "metaspaces_video_rooms": "Ruangan dan konferensi video", + "metaspaces_video_rooms_description": "Kelompokkan semua ruangan dan konferensi video privat.", + "metaspaces_video_rooms_description_invite_extension": "Dalam konferensi, Anda dapat mengundang orang-orang di luar Matrix.", "spaces_explainer": "Space adalah cara untuk mengelompokkan ruangan dan orang-orang. Di samping space yang Anda berada, Anda juga dapat menggunakan beberapa yang sudah dibuat sebelumnya.", "title": "Bilah Samping" }, "start_automatically": "Mulai setelah login sistem secara otomatis", + "tac_only_notifications": "Hanya tampilkan notifikasi dalam pusat aktivitas utas", "use_12_hour_format": "Tampilkan stempel waktu dalam format 12 jam (mis. 2:30pm)", "use_command_enter_send_message": "Gunakan ⌘ + Enter untuk mengirim pesan", "use_command_f_search": "Gunakan ⌘ + F untuk cari di lini masa", @@ -2584,6 +3011,7 @@ "audio_output_empty": "Tidak ada output audio yang terdeteksi", "auto_gain_control": "Kendali suara otomatis", "connection_section": "Koneksi", + "dialog_title": "Pengaturan: Suara & Video", "echo_cancellation": "Pembatalan gema", "enable_fallback_ice_server": "Perbolehkan server bantuan panggilan cadangan (%(server)s)", "enable_fallback_ice_server_description": "Hanya diterapkan jika homeserver Anda tidak menyediakan satu. Alamat IP Anda akan dibagikan selama panggilan berlangsung.", @@ -2602,8 +3030,12 @@ "warning": "PERINGATAN: " }, "share": { + "link_copied": "Tautan disalin", "permalink_message": "Tautan ke pesan yang dipilih", "permalink_most_recent": "Tautan ke pesan terkini", + "share_call": "Tautan undangan konferensi", + "share_call_subtitle": "Tautan bagi pengguna eksternal untuk bergabung ke panggilan tanpa akun Matrix:", + "title_link": "Bagikan Tautan", "title_message": "Bagikan Pesan Ruangan", "title_room": "Bagikan Ruangan", "title_user": "Bagikan Pengguna" @@ -2691,6 +3123,7 @@ "view": "Menampilkan ruangan dengan alamat yang ditentukan", "whois": "Menampilkan informasi tentang sebuah pengguna" }, + "sliding_sync_legacy_no_longer_supported": "Sinkronisasi geser lama tidak lagi didukung: silakan keluar dan masuk kembali untuk mengaktifkan bendera sinkronisasi geser yang baru", "space": { "add_existing_room_space": { "create": "Ingin menambahkan sebuah ruangan yang baru saja?", @@ -2789,21 +3222,22 @@ }, "create_new_room_button": "Buat ruangan baru", "failed_querying_public_rooms": "Gagal melakukan kueri ruangan publik", + "failed_querying_public_spaces": "Gagal melakukan kueri space publik", "group_chat_section_title": "Opsi lain", "heading_with_query": "Gunakan \"%(query)s\" untuk mencari", "heading_without_query": "Cari", "join_button_text": "Bergabung dengan %(roomAddress)s", "keyboard_scroll_hint": "Gunakan untuk menggulirkan", - "message_search_section_title": "Pencarian lainnya", + "messages_label": "Pesan", "other_rooms_in_space": "Ruangan lainnya di %(spaceName)s", "public_rooms_label": "Ruangan publik", + "public_spaces_label": "Space publik", "recent_searches_section_title": "Pencarian terkini", "recently_viewed_section_title": "Baru saja dilihat", "remove_filter": "Hapus saringan pencarian untuk %(filter)s", "result_may_be_hidden_privacy_warning": "Beberapa hasil mungkin disembunyikan untuk privasi", "result_may_be_hidden_warning": "Beberapa hasil mungkin tersembunyi", "search_dialog": "Dialog Pencarian", - "search_messages_hint": "Untuk mencari pesan-pesan, lihat ikon ini di atas ruangan ", "spaces_title": "Space yang Anda berada", "start_group_chat_button": "Mulai sebuah grup obrolan" }, @@ -2840,12 +3274,20 @@ "one": "%(count)s balasan", "other": "%(count)s balasan" }, + "empty_description": "Gunakan “%(replyInThread)s” ketika berada di atas pesan.", + "empty_title": "Utas membantu percakapan Anda sesuai dengan topik dan mudah untuk dilacak.", "error_start_thread_existing_relation": "Tidak dapat membuat utasan dari sebuah peristiwa dengan relasi yang sudah ada", + "mark_all_read": "Tandai semua sebagai dibaca", "my_threads": "Utasan saya", "my_threads_description": "Menampilkan semua utasan yang Anda berpartisipasi", "open_thread": "Buka utasan", "show_thread_filter": "Tampilkan:" }, + "threads_activity_centre": { + "header": "Aktivitas utas", + "no_rooms_with_threads_notifs": "Anda belum memiliki ruangan dengan notifikasi utas.", + "no_rooms_with_unread_threads": "Anda belum memiliki ruangan dengan utas yang belum dibaca." + }, "time": { "about_day_ago": "1 hari yang lalu", "about_hour_ago": "1 jam yang lalu", @@ -2887,9 +3329,21 @@ }, "creation_summary_dm": "%(creator)s membuat pesan langsung ini.", "creation_summary_room": "%(creator)s membuat dan mengatur ruangan ini.", + "decryption_failure": { + "blocked": "Pengirim telah mencegah Anda menerima pesan ini", + "historical_event_no_key_backup": "Riwayat pesan tidak tersedia di perangkat ini", + "historical_event_unverified_device": "Anda harus memverifikasi perangkat ini untuk mengakses riwayat pesan", + "historical_event_user_not_joined": "Anda tidak memiliki akses ke pesan ini", + "sender_identity_previously_verified": "Identitas terverifikasi telah berubah", + "sender_unsigned_device": "Dienkripsi oleh perangkat yang tidak diverifikasi oleh pemiliknya.", + "unable_to_decrypt": "Tidak dapat mendekripsi pesan" + }, "disambiguated_profile": "%(displayName)s (%(matrixId)s)", "download_action_decrypting": "Mendekripsi", "download_action_downloading": "Mengunduh", + "download_failed": "Pengunduhan gagal", + "download_failed_description": "Terjadi kesalahan saat mengunduh berkas ini", + "e2e_state": "Keadaan enkripsi ujung ke ujung", "edits": { "tooltip_label": "Diedit di %(date)s. Klik untuk melihat editan.", "tooltip_sub": "Klik untuk melihat editan", @@ -2900,6 +3354,7 @@ "historical_messages_unavailable": "Anda tidak dapat melihat pesan-pesan awal", "in_room_name": " di %(room)s", "io.element.widgets.layout": "%(senderName)s telah memperbarui tata letak ruangan", + "late_event_separator": "Awalnya dikirim %(dateTime)s", "load_error": { "no_permission": "Mencoba memuat titik spesifik di lini masa ruangan ini, tetapi Anda tidak memiliki izin untuk menampilkan pesannya.", "title": "Gagal untuk memuat posisi lini masa", @@ -2942,7 +3397,7 @@ }, "m.file": { "error_decrypting": "Terjadi kesalahan mendekripsi lampiran", - "error_invalid": "File tidak absah%(extra)s" + "error_invalid": "Berkas tidak valid" }, "m.image": { "error": "Tidak dapat menampilkan gambar karena kesalahan", @@ -3043,6 +3498,7 @@ "left_reason": "%(targetName)s keluar dari ruangan ini: %(reason)s", "no_change": "%(senderName)s tidak membuat perubahan", "reject_invite": "%(targetName)s menolak undangannya", + "reject_invite_reason": "%(targetName)s menolak undangan: %(reason)s", "remove_avatar": "%(senderName)s menghilangkan foto profilnya", "remove_name": "%(senderName)s menghilangkan nama tampilannya (%(oldDisplayName)s)", "set_avatar": "%(senderName)s mengatur foto profil", @@ -3079,11 +3535,13 @@ }, "m.room.tombstone": "%(senderDisplayName)s meningkatkan ruangan ini.", "m.room.topic": { - "changed": "%(senderDisplayName)s telah mengubah topik menjadi \"%(topic)s\"." + "changed": "%(senderDisplayName)s telah mengubah topik menjadi \"%(topic)s\".", + "removed": "%(senderDisplayName)s menghapus topik." }, "m.sticker": "%(senderDisplayName)s mengirim sebuah stiker.", "m.video": { - "error_decrypting": "Terjadi kesalahan mendekripsi video" + "error_decrypting": "Terjadi kesalahan mendekripsi video", + "show_video": "Tampilkan video" }, "m.widget": { "added": "Widget %(widgetName)s ditambahkan oleh %(senderName)s", @@ -3102,6 +3560,8 @@ "label": "Aksi Pesan", "view_in_room": "Tampilkan di ruangan" }, + "message_timestamp_received_at": "Diterima pada: %(dateTime)s", + "message_timestamp_sent_at": "Dikirim pada: %(dateTime)s", "mjolnir": { "changed_rule_glob": "%(senderName)s memperbarui sebuah peraturan pencekalan yang sebelumnya berisi %(oldGlob)s ke %(newGlob)s untuk %(reason)s", "changed_rule_rooms": "%(senderName)s mengubah sebuah peraturan pencekalan ruangan yang sebelumnya berisi %(oldGlob)s ke %(newGlob)s untuk %(reason)s", @@ -3129,7 +3589,8 @@ "reactions": { "add_reaction_prompt": "Tambahkan reaksi", "custom_reaction_fallback_label": "Reaksi khusus", - "label": "%(reactors)s berekasi dengan %(content)s" + "label": "%(reactors)s berekasi dengan %(content)s", + "tooltip_caption": "bereaksi dengan %(shortName)s" }, "read_receipt_title": { "one": "Dilihat oleh %(count)s orang", @@ -3314,6 +3775,10 @@ "truncated_list_n_more": { "other": "Dan %(count)s lagi..." }, + "unsupported_browser": { + "description": "Jika Anda lanjut, beberapa fitur dapat berhenti bekerja dan ada risiko kehilangan data di masa mendatang. Perbarui peramban Anda untuk terus menggunakan %(brand)s.", + "title": "%(brand)s tidak mendukung peramban ini" + }, "unsupported_server_description": "Server ini menjalankan sebuah versi Matrix yang lama. Tingkatkan ke Matrix %(version)s untuk menggunakan %(brand)s tanpa eror.", "unsupported_server_title": "Server Anda tidak didukung", "update": { @@ -3331,6 +3796,13 @@ "toast_title": "Perbarui %(brand)s", "unavailable": "Tidak Tersedia" }, + "update_room_access_modal": { + "description": "Untuk membuat tautan berbagi, jadikan ruangan ini publik atau aktifkan opsi bagi pengguna untuk meminta bergabung. Hal ini memungkinkan tamu untuk bergabung tanpa diundang.", + "dont_change_description": "Jika Anda tidak ingin mengubah akses ruangan ini, Anda dapat membuat ruang baru untuk tautan panggilan.", + "no_change": "Saya tidak ingin mengubah tingkat akses.", + "revert_access_description": "(Ini dapat dikembalikan ke nilai sebelumnya di Pengaturan Ruangan: Keamanan & Privasi / Akses)", + "title": "Izinkan pengguna tamu untuk bergabung dengan ruangan ini" + }, "upload_failed_generic": "File '%(fileName)s' gagal untuk diunggah.", "upload_failed_size": "File '%(fileName)s' melebihi batas ukuran unggahan file homeserver", "upload_failed_title": "Unggahan Gagal", @@ -3340,6 +3812,7 @@ "error_files_too_large": "File-file ini terlalu besar untuk diunggah. Batas ukuran unggahan file adalah %(limit)s.", "error_some_files_too_large": "Beberapa file terlalu besar untuk diunggah. Batas ukuran unggahan file adalah %(limit)s.", "error_title": "Kesalahan saat Mengunggah", + "not_image": "Berkas yang Anda pilih bukan berkas gambar yang valid.", "title": "Unggah file", "title_progress": "Mengunggah file (%(current)s dari %(total)s)", "upload_all_button": "Unggah semua", @@ -3371,6 +3844,7 @@ "error_mute_user": "Gagal untuk membisukan pengguna", "error_revoke_3pid_invite_description": "Tidak dapat menghapus undangan. Server ini mungkin mengalami masalah sementara atau Anda tidak memiliki izin yang dibutuhkan untuk menghapus undangannya.", "error_revoke_3pid_invite_title": "Gagal untuk menghapus undangan", + "ignore_button": "Abaikan", "ignore_confirm_description": "Semua pesan dan undangan dari pengguna ini akan disembunyikan. Apakah Anda yakin ingin mengabaikan?", "ignore_confirm_title": "Abaikan %(user)s", "invited_by": "Diundang oleh %(sender)s", @@ -3398,23 +3872,27 @@ "no_recent_messages_description": "Coba gulir ke atas di lini masa untuk melihat apa ada pesan-pesan sebelumnya.", "no_recent_messages_title": "Tidak ada pesan terkini dari %(user)s yang ditemukan" }, - "redact_button": "Hapus pesan terkini", + "redact_button": "Hapus pesan", "revoke_invite": "Hapus undangan", "room_encrypted": "Pesan di ruangan ini terenkripsi secara ujung ke ujung.", "room_encrypted_detail": "Pesan Anda diamankan dan hanya Anda dan penerimanya mempunyai kunci yang unik untuk mengaksesnya.", "room_unencrypted": "Pesan di ruangan ini tidak dienkripsi secara ujung ke ujung.", "room_unencrypted_detail": "Di ruangan terenkripsi, pesan Anda diamankan dan hanya Anda dan penerimanya mempunyai kunci yang unik untuk mengaksesnya.", - "share_button": "Bagikan Tautan ke Pengguna", + "send_message": "Kirim pesan", + "share_button": "Bagikan profil", "unban_button_room": "Batalkan cekalan dari ruangan", "unban_button_space": "Batalkan cekalan dari space", "unban_room_confirm_title": "Batalkan cekalan dari %(roomName)s", "unban_space_everything": "Batalkan pencekalan dari semuanya yang saya dapat melakukan", "unban_space_specific": "Batalkan pencekalan dari beberapa hal yang saya dapat melakukan", "unban_space_warning": "Mereka tidak dapat mengakses apa saja yang Anda bukan admin di sana.", + "unignore_button": "Batalkan pengabaian", + "verification_unavailable": "Verifikasi pengguna tidak tersedia", "verify_button": "Verifikasi Pengguna", "verify_explainer": "Untuk keamanan lebih, verifikasi pengguna ini dengan memeriksa kode satu kali di kedua perangkat Anda." }, "user_menu": { + "link_new_device": "Tautkan perangkat baru", "settings": "Semua pengaturan", "switch_theme_dark": "Ubah ke mode gelap", "switch_theme_light": "Ubah ke mode terang" @@ -3438,6 +3916,7 @@ "camera_disabled": "Kamera Anda dimatikan", "camera_enabled": "Kamera Anda masih nyala", "cannot_call_yourself_description": "Anda tidak dapat melakukan panggilan dengan diri sendiri.", + "close_lobby": "Tutup lobi", "connecting": "Menghubungkan", "connection_lost": "Koneksi ke server telah hilang", "connection_lost_description": "Anda tidak dapat membuat panggilan tanpa terhubung ke server.", @@ -3451,14 +3930,23 @@ "disabled_no_perms_start_video_call": "Anda tidak memiliki izin untuk memulai panggilan video", "disabled_no_perms_start_voice_call": "Anda tidak memiliki izin untuk memulai panggilan suara", "disabled_ongoing_call": "Panggilan sedang berlangsung", + "element_call": "Element Call", "enable_camera": "Nyalakan kamera", "enable_microphone": "Suarakan mikrofon", "expand": "Kembali ke panggilan", + "get_call_link": "Bagikan tautan panggilan", "hangup": "Akhiri", "hide_sidebar_button": "Sembunyikan sisi bilah", "input_devices": "Perangkat masukan", + "jitsi_call": "Konferensi Jitsi", "join_button_tooltip_call_full": "Maaf — panggilan ini saat ini penuh", + "legacy_call": "Panggilan Lawas", "maximise": "Penuhi layar", + "maximise_call": "Maksimalkan panggilan", + "metaspace_video_rooms": { + "conference_room_section": "Konferensi" + }, + "minimise_call": "Minimalkan panggilan", "misconfigured_server": "Panggilan gagal karena servernya tidak dikonfigurasi dengan benar", "misconfigured_server_description": "Mohon tanyakan ke administrator homeserver Anda (%(homeserverDomain)s) untuk mengkonfigurasikan server TURN supaya panggilan dapat bekerja dengan benar.", "misconfigured_server_fallback": "Secara alternatif, Anda dapat menggunakan server publik di , tetapi ini tidak akan selalu tersedia, dan akan membagikan alamat IP Anda dengan server itu. Anda juga dapat mengelola ini di Pengaturan.", @@ -3506,6 +3994,7 @@ "user_is_presenting": "%(sharerName)s sedang mempresentasi", "video_call": "Panggilan video", "video_call_started": "Panggilan video dimulai", + "video_call_using": "Panggilan video menggunakan:", "voice_call": "Panggilan suara", "you_are_presenting": "Anda sedang mempresentasi" }, @@ -3605,7 +4094,7 @@ "error_need_to_be_logged_in": "Anda harus masuk.", "error_unable_start_audio_stream_description": "Tidak dapat memulai penyiaran audio.", "error_unable_start_audio_stream_title": "Gagal untuk memulai siaran langsung", - "modal_data_warning": "Data di layar ini dibagikan dengan %(widgetDomain)s", + "modal_data_warning": "Data di bawah ini dibagikan dengan %(widgetDomain)s", "modal_title_default": "Widget Modal", "no_name": "Aplikasi Tidak Diketahui", "open_id_permissions_dialog": { @@ -3614,7 +4103,7 @@ "title": "Izinkan widget ini untuk memverifikasi identitas Anda" }, "popout": "Widget popout", - "set_room_layout": "Tetapkan tata letak ruangan saya untuk semuanya", + "set_room_layout": "Tetapkan tata letak untuk semua orang", "shared_data_avatar": "URL foto profil Anda", "shared_data_device_id": "ID perangkat Anda", "shared_data_lang": "Bahasa Anda", diff --git a/src/i18n/strings/is.json b/src/i18n/strings/is.json index d0b3d34373..f57cdf156e 100644 --- a/src/i18n/strings/is.json +++ b/src/i18n/strings/is.json @@ -673,17 +673,10 @@ "encryption": { "access_secret_storage_dialog": { "key_validation_text": { - "invalid_security_key": "Ógildur öryggislykill", - "recovery_key_is_correct": "Lítur vel út!", - "wrong_file_type": "Röng skráartegund", "wrong_security_key": "Rangur öryggislykill" }, - "reset_title": "Frumstilla allt", "restoring": "Endurheimti lykla úr öryggisafriti", - "security_key_title": "Öryggislykill", - "security_phrase_title": "Öryggisfrasi", - "separator": "%(securityKey)s eða %(recoveryFile)s", - "use_security_key_prompt": "Notaðu öryggislykilinn þinn til að halda áfram." + "security_key_title": "Öryggislykill" }, "bootstrap_title": "Set upp dulritunarlykla", "cancel_entering_passphrase_description": "Viltu örugglega hætta við að setja inn lykilfrasa?", @@ -729,7 +722,6 @@ "accepting": "Samþykki…", "after_new_login": { "device_verified": "Tæki er sannreynt", - "reset_confirmation": "Viltu í alvörunni endurstilla sannvottunarlyklana?", "skip_verification": "Sleppa sannvottun í bili", "unable_to_verify": "Tókst ekki að sannreyna þetta tæki", "verify_this_device": "Sannreyna þetta tæki" @@ -1862,7 +1854,6 @@ "remove_msisdn_prompt": "Fjarlægja %(phone)s?", "spell_check_locale_placeholder": "Veldu staðfærslu" }, - "image_thumbnails": "Birta forskoðun/smámyndir fyrir myndir", "inline_url_previews_default": "Sjálfgefið virkja forskoðun innfelldra vefslóða", "inline_url_previews_room": "Virkja forskoðun vefslóða sjálfgefið fyrir þátttakendur í þessari spjallrás", "inline_url_previews_room_account": "Virkja forskoðun vefslóða fyrir þessa spjallrás (einungis fyrir þig)", @@ -2292,7 +2283,6 @@ "heading_without_query": "Leita að", "join_button_text": "Taka þátt í %(roomAddress)s", "keyboard_scroll_hint": "Notaðu til að skruna", - "message_search_section_title": "Aðrar leitir", "other_rooms_in_space": "Aðrar spjallrásir í %(spaceName)s", "public_rooms_label": "Almenningsspjallrásir", "recent_searches_section_title": "Nýlegar leitir", diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 7e8d9a727c..66ea31ad9c 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -794,22 +794,11 @@ "empty_room_was_name": "Stanza vuota (era %(oldName)s)", "encryption": { "access_secret_storage_dialog": { - "enter_phrase_or_key_prompt": "Inserisci la tua frase di sicurezza o per continuare.", "key_validation_text": { - "invalid_security_key": "Chiave di sicurezza non valida", - "recovery_key_is_correct": "Sembra giusta!", - "wrong_file_type": "Tipo di file errato", "wrong_security_key": "Chiave di sicurezza sbagliata" }, - "reset_title": "Reimposta tutto", - "reset_warning_1": "Fallo solo se non hai altri dispositivi con cui completare la verifica.", - "reset_warning_2": "Se reimposti tutto, ricomincerai senza sessioni fidate, senza utenti fidati e potresti non riuscire a vedere i messaggi passati.", "restoring": "Ripristino delle chiavi dal backup", - "security_key_title": "Chiave di sicurezza", - "security_phrase_incorrect_error": "Impossibile accedere all'archivio segreto. Verifica di avere inserito la password di sicurezza giusta.", - "security_phrase_title": "Frase di sicurezza", - "separator": "%(securityKey)s o %(recoveryFile)s", - "use_security_key_prompt": "Usa la tua chiave di sicurezza per continuare." + "security_key_title": "Chiave di sicurezza" }, "bootstrap_title": "Configurazione chiavi", "cancel_entering_passphrase_description": "Sei sicuro di volere annullare l'inserimento della frase?", @@ -869,7 +858,6 @@ "accepting": "Accettazione…", "after_new_login": { "device_verified": "Dispositivo verificato", - "reset_confirmation": "Reimpostare le chiavi di verifica?", "skip_verification": "Salta la verifica per adesso", "unable_to_verify": "Impossibile verificare questo dispositivo", "verify_this_device": "Verifica questo dispositivo" @@ -939,8 +927,6 @@ "verify_emoji_prompt": "Verifica confrontando emoji specifici.", "verify_emoji_prompt_qr": "Se non riesci a scansionare il codice sopra, verifica confrontando emoji specifiche.", "verify_later": "Verificherò dopo", - "verify_reset_warning_1": "La reimpostazione delle chiavi di verifica non può essere annullata. Dopo averlo fatto, non avrai accesso ai vecchi messaggi cifrati, e gli amici che ti avevano verificato in precedenza vedranno avvisi di sicurezza fino a quando non ti ri-verifichi con loro.", - "verify_reset_warning_2": "Procedi solo se sei sicuro di avere perso tutti gli altri tuoi dispositivi e la chiave di sicurezza.", "verify_using_device": "Verifica con un altro dispositivo", "verify_using_key": "Verifica con chiave di sicurezza", "verify_using_key_or_phrase": "Verifica con chiave di sicurezza o frase", @@ -2318,7 +2304,6 @@ "remove_msisdn_prompt": "Rimuovere %(phone)s?", "spell_check_locale_placeholder": "Scegli una lingua" }, - "image_thumbnails": "Mostra anteprime/miniature per le immagini", "inline_url_previews_default": "Attiva le anteprime URL in modo predefinito", "inline_url_previews_room": "Attiva le anteprime URL in modo predefinito per i partecipanti in questa stanza", "inline_url_previews_room_account": "Attiva le anteprime URL in questa stanza (riguarda solo te)", @@ -2834,7 +2819,6 @@ "heading_without_query": "Cerca", "join_button_text": "Entra in %(roomAddress)s", "keyboard_scroll_hint": "Usa per scorrere", - "message_search_section_title": "Altre ricerche", "other_rooms_in_space": "Altre stanze in %(spaceName)s", "public_rooms_label": "Stanze pubbliche", "public_spaces_label": "Spazi pubblici", @@ -2844,7 +2828,6 @@ "result_may_be_hidden_privacy_warning": "Alcuni risultati potrebbero essere nascosti per privacy", "result_may_be_hidden_warning": "Alcuni risultati potrebbero essere nascosti", "search_dialog": "Finestra di ricerca", - "search_messages_hint": "Per cercare messaggi, trova questa icona in cima ad una stanza ", "spaces_title": "Spazi in cui sei", "start_group_chat_button": "Inizia una conversazione di gruppo" }, diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json index bcb9ee59b6..e4e61e8907 100644 --- a/src/i18n/strings/ja.json +++ b/src/i18n/strings/ja.json @@ -753,22 +753,11 @@ "empty_room_was_name": "空のルーム(以前の名前は%(oldName)s)", "encryption": { "access_secret_storage_dialog": { - "enter_phrase_or_key_prompt": "続行するにはセキュリティーフレーズを入力するか、してください。", "key_validation_text": { - "invalid_security_key": "セキュリティーキーが正しくありません", - "recovery_key_is_correct": "問題ありません!", - "wrong_file_type": "正しくないファイルの種類", "wrong_security_key": "正しくないセキュリティーキー" }, - "reset_title": "全てリセット", - "reset_warning_1": "認証を行える端末がない場合のみ行ってください。", - "reset_warning_2": "全てをリセットすると、履歴とメッセージが消去され、信頼済の端末、信頼済のユーザーが取り消されます。また、過去のメッセージを表示できなくなる可能性があります。", "restoring": "バックアップから鍵を復元", - "security_key_title": "セキュリティーキー", - "security_phrase_incorrect_error": "機密ストレージにアクセスできません。正しいセキュリティーフレーズを入力したことを確認してください。", - "security_phrase_title": "セキュリティーフレーズ", - "separator": "%(securityKey)sまたは%(recoveryFile)s", - "use_security_key_prompt": "続行するにはセキュリティーキーを使用してください。" + "security_key_title": "セキュリティーキー" }, "bootstrap_title": "鍵のセットアップ", "cancel_entering_passphrase_description": "パスフレーズの入力をキャンセルしてよろしいですか?", @@ -825,7 +814,6 @@ "accepting": "承認しています…", "after_new_login": { "device_verified": "端末が認証されました", - "reset_confirmation": "本当に認証鍵をリセットしますか?", "skip_verification": "認証をスキップ", "unable_to_verify": "この端末を認証できません", "verify_this_device": "この端末を認証" @@ -892,8 +880,6 @@ "verify_emoji_prompt": "絵文字の並びを比較して認証。", "verify_emoji_prompt_qr": "上記のコードをスキャンできない場合は、絵文字による確認を行ってください。", "verify_later": "後で認証", - "verify_reset_warning_1": "認証鍵のリセットは取り消せません。リセットすると、以前の暗号化されたメッセージにはアクセスできなくなります。また、あなたのアカウントを認証した連絡先には、再認証するまで、セキュリティーに関する警告が表示されます。", - "verify_reset_warning_2": "全ての端末とセキュリティーキーを紛失してしまったことが確かである場合にのみ、続行してください。", "verify_using_device": "別の端末で認証", "verify_using_key": "セキュリティーキーで認証", "verify_using_key_or_phrase": "セキュリティーキーあるいはセキュリティーフレーズで認証", @@ -2113,7 +2099,6 @@ "remove_msisdn_prompt": "%(phone)sを削除しますか?", "spell_check_locale_placeholder": "ロケールを選択" }, - "image_thumbnails": "画像のプレビューまたはサムネイルを表示", "inline_url_previews_default": "既定でインラインURLプレビューを有効にする", "inline_url_previews_room": "このルームの参加者のために既定でURLプレビューを有効にする", "inline_url_previews_room_account": "このルームのURLプレビューを有効にする(あなたにのみ適用)", @@ -2581,7 +2566,6 @@ "heading_without_query": "検索", "join_button_text": "%(roomAddress)sに参加", "keyboard_scroll_hint": "でスクロール", - "message_search_section_title": "その他の検索", "other_rooms_in_space": "%(spaceName)sの他のルーム", "public_rooms_label": "公開ルーム", "recent_searches_section_title": "最近の検索", @@ -2590,7 +2574,6 @@ "result_may_be_hidden_privacy_warning": "プライバシーの観点から表示していない結果があります", "result_may_be_hidden_warning": "いくつかの結果が表示されていない可能性があります", "search_dialog": "検索ダイアログ", - "search_messages_hint": "メッセージを検索する場合は、ルームの上に表示されるアイコンをクリックしてください。", "spaces_title": "参加しているスペース", "start_group_chat_button": "グループチャットを開始" }, diff --git a/src/i18n/strings/ka.json b/src/i18n/strings/ka.json index 883d6fa4ac..514e069bfa 100644 --- a/src/i18n/strings/ka.json +++ b/src/i18n/strings/ka.json @@ -658,22 +658,11 @@ "empty_room_was_name": "ცარიელი ოთახი (იყო%(oldName)s )", "encryption": { "access_secret_storage_dialog": { - "enter_phrase_or_key_prompt": "შეიყვანეთ თქვენი უსაფრთხოების ფრაზა ან გააგრძელოს.", "key_validation_text": { - "invalid_security_key": "უსაფრთხოების გასაღები არასწორია", - "recovery_key_is_correct": "კარგად გამოიყურება!", - "wrong_file_type": "არასწორი ფაილის ტიპი", "wrong_security_key": "არასწორი უსაფრთხოების გასაღები" }, - "reset_title": "გადააყენე ყველაფერი", - "reset_warning_1": "გააკეთეთ ეს მხოლოდ იმ შემთხვევაში, თუ სხვა მოწყობილობა არ გაქვთ დადასტურების დასასრულებლად.", - "reset_warning_2": "თუ ყველაფერს გადატვირთავთ, გადატვირთავთ სანდო სესიების გარეშე, სანდო მომხმარებლების გარეშე და შესაძლოა ვერ დაინახოთ წარსული შეტყობინებები.", "restoring": "გასაღებების აღდგენა სარეზერვო ასლიდან", - "security_key_title": "უსაფრთხოების გასაღები", - "security_phrase_incorrect_error": "საიდუმლო მეხსიერებაზე წვდომა შეუძლებელია. გთხოვთ, დაადასტუროთ, რომ შეიყვანეთ უსაფრთხოების სწორი ფრაზა.", - "security_phrase_title": "უსაფრთხოების ფრაზა", - "separator": "%(securityKey)sან%(recoveryFile)s", - "use_security_key_prompt": "გამოიყენეთ თქვენი უსაფრთხოების გასაღები გასაგრძელებლად." + "security_key_title": "უსაფრთხოების გასაღები" }, "bootstrap_title": "გასაღებების დაყენება", "cancel_entering_passphrase_description": "დარწმუნებული ხართ, რომ გსურთ გააუქმოთ პაროლის შეყვანა?", @@ -733,7 +722,6 @@ "accepting": "მიღება…", "after_new_login": { "device_verified": "მოწყობილობა დადასტურებულია", - "reset_confirmation": "ნამდვილად გადააყენეთ დადასტურების გასაღებები?", "skip_verification": "ამ დროისთვის გამოტოვეთ დადასტურება", "unable_to_verify": "ამ მოწყობილობის დადასტურება შეუძლებელია", "verify_this_device": "დაადასტურეთ ეს მოწყობილობა" @@ -803,8 +791,6 @@ "verify_emoji_prompt": "გადაამოწმეთ უნიკალური emoji-ების შედარებით.", "verify_emoji_prompt_qr": "თუ ზემოთ მოცემულ კოდს ვერ სკანირებთ, გადაამოწმეთ უნიკალური emoji-ების შედარებით.", "verify_later": "მოგვიანებით გადავამოწმებ", - "verify_reset_warning_1": "თქვენი დამადასტურებელი გასაღებების გადაყენება შეუძლებელია. გადატვირთვის შემდეგ, თქვენ არ გექნებათ წვდომა ძველ დაშიფრულ შეტყობინებებზე და ყველა მეგობარი, ვინც ადრე დაგიდასტურდათ, დაინახავს უსაფრთხოების გაფრთხილებებს, სანამ მათთან ხელახლა გადაამოწმებთ.", - "verify_reset_warning_2": "გთხოვთ გააგრძელოთ მხოლოდ იმ შემთხვევაში, თუ დარწმუნებული ხართ, რომ დაკარგეთ ყველა თქვენი სხვა მოწყობილობა და უსაფრთხოების გასაღები.", "verify_using_device": "გადაამოწმეთ სხვა მოწყობილობით", "verify_using_key": "გადაამოწმეთ უსაფრთხოების გასაღებით", "verify_using_key_or_phrase": "გადაამოწმეთ უსაფრთხოების გასაღებით ან ფრაზით", @@ -1755,7 +1741,6 @@ "remove_msisdn_prompt": "ამოღება%(phone)s ?", "spell_check_locale_placeholder": "აირჩიეთ ლოკალი" }, - "image_thumbnails": "სურათების გადახედვის/მინიატურების ჩვენება", "inline_url_previews_default": "ნაგულისხმევად ჩართული URL-ის გადახედვის ჩართვა", "inline_url_previews_room": "URL-ის გადახედვის ჩართვა ნაგულისხმევად ამ ოთახში მონაწილეებისთვის", "inline_url_previews_room_account": "ამ ოთახისთვის URL-ის გადახედვის ჩართვა (მხოლოდ თქვენზე მოქმედებს)", diff --git a/src/i18n/strings/lo.json b/src/i18n/strings/lo.json index dd1b8e951a..782980078d 100644 --- a/src/i18n/strings/lo.json +++ b/src/i18n/strings/lo.json @@ -661,21 +661,11 @@ "empty_room": "ຫ້ອງຫວ່າງ", "encryption": { "access_secret_storage_dialog": { - "enter_phrase_or_key_prompt": "ກະລຸນາໃສ່ປະໂຫຍກຄວາມປອດໄພຂອງທ່ານ ຫຼື ເພື່ອສືບຕໍ່.", "key_validation_text": { - "invalid_security_key": "ກະແຈຄວາມປອດໄພບໍ່ຖືກຕ້ອງ", - "recovery_key_is_correct": "ດີ!", - "wrong_file_type": "ປະເພດໄຟລ໌ບໍ່ຖຶກຕ້ອງ", "wrong_security_key": "ກະແຈຄວາມປອດໄພບໍ່ຖຶກຕ້ອງ" }, - "reset_title": "ຕັ້ງຄ່າໃໝ່ທຸກຢ່າງ", - "reset_warning_1": "ເຮັດແນວນີ້ກໍ່ຕໍ່ເມື່ອທ່ານບໍ່ມີອຸປະກອນອື່ນເພື່ອການຢັ້ງຢືນດ້ວຍ.", - "reset_warning_2": "ຖ້າທ່ານຕັ້ງຄ່າຄືນໃໝ່ທຸກຢ່າງ, ທ່ານຈະຣີສະຕາດໂດຍບໍ່ມີລະບົບທີ່ເຊື່ອຖືໄດ້, ບໍ່ມີຜູ້ໃຊ້ທີ່ເຊື່ອຖືໄດ້ ແລະ ອາດຈະບໍ່ເຫັນຂໍ້ຄວາມທີ່ຜ່ານມາ.", "restoring": "ການຟື້ນຟູລະຫັດຈາກການສໍາຮອງຂໍ້ມູນ", - "security_key_title": "ກະແຈຄວາມປອດໄພ", - "security_phrase_incorrect_error": "ບໍ່ສາມາດເຂົ້າເຖິງບ່ອນເກັບຂໍ້ມູນລັບໄດ້. ກະລຸນາກວດສອບວ່າທ່ານໃສ່ປະໂຫຍກຄວາມປອດໄພທີ່ຖືກຕ້ອງ.", - "security_phrase_title": "ປະໂຫຍກລະຫັດຄວາມປອດໄພ", - "use_security_key_prompt": "ໃຊ້ກະແຈຄວາມປອດໄພຂອງທ່ານເພື່ອສືບຕໍ່." + "security_key_title": "ກະແຈຄວາມປອດໄພ" }, "bootstrap_title": "ການຕັ້ງຄ່າກະແຈ", "cancel_entering_passphrase_description": "ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການຍົກເລີກການໃສ່ປະໂຫຍກລະຫັດຜ່ານ?", @@ -731,7 +721,6 @@ "accepting": "ກຳລັງຍອມຮັບ…", "after_new_login": { "device_verified": "ຢັ້ງຢືນອຸປະກອນແລ້ວ", - "reset_confirmation": "ຕັ້ງຄ່າຢືນຢັນກະແຈຄືນໃໝ່ບໍ?", "skip_verification": "ຂ້າມການຢັ້ງຢືນດຽວນີ້", "unable_to_verify": "ບໍ່ສາມາດຢັ້ງຢືນອຸປະກອນນີ້ໄດ້", "verify_this_device": "ຢັ້ງຢືນອຸປະກອນນີ້" @@ -793,7 +782,6 @@ "verify_emoji_prompt": "ຢັ້ງຢືນໂດຍການປຽບທຽບ emoji ທີ່ເປັນເອກະລັກ.", "verify_emoji_prompt_qr": "ຖ້າທ່ານບໍ່ສາມາດສະແກນລະຫັດຂ້າງເທິງໄດ້, ໃຫ້ກວດສອບໂດຍການປຽບທຽບອີໂມຈິທີ່ເປັນເອກະລັກ.", "verify_later": "ຂ້ອຍຈະກວດສອບພາຍຫຼັງ", - "verify_reset_warning_1": "ການຕັ້ງຄ່າລະຫັດຢືນຢັນຂອງທ່ານບໍ່ສາມາດຍົກເລີກໄດ້. ຫຼັງຈາກການຕັ້ງຄ່າແລ້ວ, ທ່ານຈະບໍ່ສາມາດເຂົ້າເຖິງຂໍ້ຄວາມທີ່ເຂົ້າລະຫັດເກົ່າໄດ້, ແລະ ໝູ່ເພື່ອນທີ່ຢືນຢັນໄປກ່ອນໜ້ານີ້ ທ່ານຈະເຫັນຄຳເຕືອນຄວາມປອດໄພຈົນກວ່າທ່ານຈະຢືນຢັນກັບພວກມັນຄືນໃໝ່.", "verify_using_device": "ຢັ້ງຢືນດ້ວຍອຸປະກອນອື່ນ", "verify_using_key": "ຢືນຢັນດ້ວຍກະແຈຄວາມປອດໄພ", "verify_using_key_or_phrase": "ຢືນຢັນດ້ວຍກະແຈຄວາມປອດໄພ ຫຼືປະໂຫຍກ", @@ -1931,7 +1919,6 @@ "remove_email_prompt": "ລຶບ %(email)s ອອກບໍ?", "remove_msisdn_prompt": "ລຶບ %(phone)sອອກບໍ?" }, - "image_thumbnails": "ສະແດງຕົວຢ່າງ/ຮູບຕົວຢ່າງສຳລັບຮູບພາບ", "inline_url_previews_default": "ເປີດໃຊ້ການສະແດງຕົວຢ່າງ URL ໃນແຖວຕາມຄ່າເລີ່ມຕົ້ນ", "inline_url_previews_room": "ເປີດໃຊ້ການສະແດງຕົວຢ່າງ URL ໂດຍຄ່າເລີ່ມຕົ້ນສໍາລັບຜູ້ເຂົ້າຮ່ວມໃນຫ້ອງນີ້", "inline_url_previews_room_account": "ເປີດໃຊ້ຕົວຢ່າງ URL ສໍາລັບຫ້ອງນີ້ (ມີຜົນຕໍ່ທ່ານເທົ່ານັ້ນ)", @@ -2286,13 +2273,11 @@ "heading_with_query": "ໃຊ້ \"%(query)s\" ເພື່ອຊອກຫາ", "join_button_text": "ເຂົ້າຮ່ວມ %(roomAddress)s", "keyboard_scroll_hint": "ໃຊ້ ເພື່ອເລື່ອນ", - "message_search_section_title": "ການຄົ້ນຫາອື່ນໆ", "other_rooms_in_space": "ຫ້ອງອື່ນໆ%(spaceName)s", "public_rooms_label": "ຫ້ອງສາທາລະນະ", "recent_searches_section_title": "ການຄົ້ນຫາທີ່ຜ່ານມາ", "recently_viewed_section_title": "ເບິ່ງເມື່ອບໍ່ດົນມານີ້", "search_dialog": "ຊອກຫາ ກ່ອງໂຕ້ຕອບ", - "search_messages_hint": "ເພື່ອຊອກຫາຂໍ້ຄວາມ, ຊອກຫາໄອຄອນນີ້ຢູ່ເທິງສຸດຂອງຫ້ອງ ", "spaces_title": "ຊ່ອງທີ່ທ່ານຢູ່" }, "stickers": { diff --git a/src/i18n/strings/lt.json b/src/i18n/strings/lt.json index 7c1ebaa47e..101e8fbc0c 100644 --- a/src/i18n/strings/lt.json +++ b/src/i18n/strings/lt.json @@ -499,19 +499,10 @@ "encryption": { "access_secret_storage_dialog": { "key_validation_text": { - "invalid_security_key": "Klaidingas Saugumo Raktas", - "recovery_key_is_correct": "Atrodo gerai!", - "wrong_file_type": "Netinkamas failo tipas", "wrong_security_key": "Netinkamas Saugumo Raktas" }, - "reset_title": "Iš naujo nustatyti viską", - "reset_warning_1": "Taip darykite tik tuo atveju, jei neturite kito prietaiso, kuriuo galėtumėte užbaigti patikrinimą.", - "reset_warning_2": "Jei viską nustatysite iš naujo, paleisite iš naujo be patikimų seansų, be patikimų vartotojų ir galbūt negalėsite matyti ankstesnių žinučių.", "restoring": "Raktų atkūrimas iš atsarginės kopijos", - "security_key_title": "Saugumo Raktas", - "security_phrase_incorrect_error": "Nepavyksta pasiekti slaptosios saugyklos. Prašome patvirtinti kad teisingai įvedėte Saugumo Frazę.", - "security_phrase_title": "Slaptafrazė", - "use_security_key_prompt": "Naudokite Saugumo Raktą kad tęsti." + "security_key_title": "Saugumo Raktas" }, "bootstrap_title": "Raktų nustatymas", "cancel_entering_passphrase_description": "Ar tikrai norite atšaukti slaptafrazės įvedimą?", @@ -1484,7 +1475,6 @@ "remove_email_prompt": "Pašalinti %(email)s?", "remove_msisdn_prompt": "Pašalinti %(phone)s?" }, - "image_thumbnails": "Rodyti vaizdų peržiūras/miniatiūras", "inline_url_previews_default": "Įjungti URL nuorodų peržiūras kaip numatytasias", "inline_url_previews_room": "Įjungti URL nuorodų peržiūras kaip numatytasias šiame kambaryje esantiems dalyviams", "inline_url_previews_room_account": "Įjungti URL nuorodų peržiūras šiame kambaryje (įtakoja tik jus)", diff --git a/src/i18n/strings/lv.json b/src/i18n/strings/lv.json index 3c112b89f5..739e9a81b0 100644 --- a/src/i18n/strings/lv.json +++ b/src/i18n/strings/lv.json @@ -833,22 +833,11 @@ "empty_room_was_name": "Tukša istaba (bija %(oldName)s)", "encryption": { "access_secret_storage_dialog": { - "enter_phrase_or_key_prompt": "Ievadiet savu drošības frāzi vai , lai turpinātu.", "key_validation_text": { - "invalid_security_key": "Kļūdaina drošības atslēga", - "recovery_key_is_correct": "Izskatās labi!", - "wrong_file_type": "Nepareizs datnes veids", "wrong_security_key": "Nepareiza drošības atslēga" }, - "reset_title": "Atiestatīt visu", - "reset_warning_1": "Dariet to tikai tad, ja jums nav citas ierīces, ar kuru pabeigt verifikāciju.", - "reset_warning_2": "Ja viss tiks attiestatīts, tiks atkal uzsākts bez uzticamām sesijām un lietotājiem un varētu nebūt iespējams redzēt iepriekšējās ziņas.", "restoring": "Atslēgu atjaunošana no rezerves kopijas", - "security_key_title": "Drošības atslēga", - "security_phrase_incorrect_error": "Nebija iespējams piekļūt slepenajai krātuvei. Lūgums pārliecināties, ka ir ievadīta pareiza drošības vārdkopa.", - "security_phrase_title": "Slepenā frāze", - "separator": "%(securityKey)s vai %(recoveryFile)s", - "use_security_key_prompt": "Izmantojiet savu drošības atslēgu, lai turpinātu." + "security_key_title": "Drošības atslēga" }, "bootstrap_title": "Atslēgu iestatīšana", "cancel_entering_passphrase_description": "Vai tiešām atcelt paroles vārdkopas ievadīšanu?", @@ -908,7 +897,6 @@ "accepting": "Apstiprina…", "after_new_login": { "device_verified": "Ierīce ir verificēta", - "reset_confirmation": "Vai tiešām atiestatīt verifikācijas atslēgas?", "skip_verification": "Pagaidām izlaist verifikāciju", "unable_to_verify": "Neizdevās verificēt šo ierīci", "verify_this_device": "Verificēt šo ierīci" @@ -979,8 +967,6 @@ "verify_emoji_prompt": "Apliecināt ar vienreizēju emocijzīmu salīdzināšanu.", "verify_emoji_prompt_qr": "Ja nevar nolasīt augstāk esošo kodu, var apliecināt ar vienreizēju emocijzīmju salīdzināšanu.", "verify_later": "Es verificēšu vēlāk", - "verify_reset_warning_1": "Verifikācijas atslēgu atiestatīšanu nevar atsaukt. Pēc atiestatīšanas jūs nevarēsit piekļūt vecām šifrētām ziņām, un visi draugi, kas jūs iepriekš ir verificējuši, redzēs drošības brīdinājumus, līdz veiksit atkārtotu verifikāciju.", - "verify_reset_warning_2": "Lūdzu, turpiniet tikai tad, ja esat pārliecināts, ka esat zaudējis visas pārējās ierīces un drošības atslēgu.", "verify_using_device": "Verificēt ar citu ierīci", "verify_using_key": "Verificēt ar drošības atslēgu", "verify_using_key_or_phrase": "Verificēt, izmantojot drošības atslēgu vai frāzi", @@ -2235,7 +2221,6 @@ "remove_msisdn_prompt": "Dzēst %(phone)s?", "spell_check_locale_placeholder": "Izvēlieties lokalizāciju" }, - "image_thumbnails": "Rādīt attēlu priekšskatījumus/sīktēlus", "inline_url_previews_default": "Iespējot URL priekšskatījumus pēc noklusējuma", "inline_url_previews_room": "Iespējot URL priekšskatījumus pēc noklusējuma visiem šīs istabas dalībniekiem", "inline_url_previews_room_account": "Iespējot URL priekšskatījumus šajā istabā (ietekmē tikai Tevi)", @@ -2766,7 +2751,6 @@ "heading_with_query": "Izmantot \"%(query)s\" meklēšanai", "heading_without_query": "Meklēt", "keyboard_scroll_hint": "Lietojiet ritināšanai", - "message_search_section_title": "Citi meklējumi", "other_rooms_in_space": "Citas istabas %(spaceName)s", "public_rooms_label": "Publiskas istabas", "public_spaces_label": "Publiskās telpas", @@ -2775,7 +2759,6 @@ "remove_filter": "Noņemt meklēšanas filtru %(filter)s", "result_may_be_hidden_privacy_warning": "Daži rezultāti var būt slēpti dēļ privātuma", "result_may_be_hidden_warning": "Atsevišķi rezultāti var būt slēpti", - "search_messages_hint": "Lai meklētu ziņas, istabas augšpusē meklējiet šo ikonu ", "spaces_title": "Telpas, kurās atrodaties", "start_group_chat_button": "Uzsākt grupas tērzēšanu" }, diff --git a/src/i18n/strings/mg_MG.json b/src/i18n/strings/mg_MG.json index d702724488..1ec5ed76c5 100644 --- a/src/i18n/strings/mg_MG.json +++ b/src/i18n/strings/mg_MG.json @@ -790,22 +790,11 @@ "empty_room_was_name": "Efitra banga (dia%(oldName)s )", "encryption": { "access_secret_storage_dialog": { - "enter_phrase_or_key_prompt": "Ampidiro ny fehezan-teny fiarovana anao na hanohy.", "key_validation_text": { - "invalid_security_key": "Fanalahidy fiarovana tsy mety", - "recovery_key_is_correct": "Toa tsara!", - "wrong_file_type": "Karazana rakitra diso", "wrong_security_key": "Fanalahidin'ny fiarovana diso" }, - "reset_title": "Avereno ny zava-drehetra", - "reset_warning_1": "Ataovy izany raha tsy manana fitaovana hafa hamitana ny fanamarinana ianao.", - "reset_warning_2": "Raha averinao daholo ny zava-drehetra dia hanomboka tsy misy fivoriana azo itokisana, tsy misy mpampiasa azo itokisana, ary mety tsy hahita hafatra taloha.", "restoring": "Famerenana ny fanalahidy avy amin'ny vakorakitra", - "security_key_title": "Kitendry fiarovana", - "security_phrase_incorrect_error": "Tsy afaka miditra amin'ny fitahirizana miafina. Azafady, hamarino fa nampiditra ny fehezan-teny fiarovana marina ianao.", - "security_phrase_title": "Andian-teny fiarovana", - "separator": "%(securityKey)sna%(recoveryFile)s", - "use_security_key_prompt": "Ampiasao ny lakilen'ny fiarovana anao hanohizana." + "security_key_title": "Kitendry fiarovana" }, "bootstrap_title": "Fametrahana fanalahidy", "cancel_entering_passphrase_description": "Tena te-hanafoana ny fampidirana fehezanteny ve ianao?", @@ -865,7 +854,6 @@ "accepting": "Manaiky…", "after_new_login": { "device_verified": "Voamarina ny fitaovana", - "reset_confirmation": "Avereno tokoa ny fanalahidiny fanamarinana?", "skip_verification": "Alefaso ny fanamarinana amin'izao fotoana izao", "unable_to_verify": "Tsy afaka manamarina ity fitaovana ity", "verify_this_device": "Hamarino ity fitaovana ity" @@ -935,8 +923,6 @@ "verify_emoji_prompt": "Hamarino amin'ny fampitahana emoji tokana.", "verify_emoji_prompt_qr": "Raha tsy azonao atao ny mijery ny kaody etsy ambony dia hamarino amin'ny fampitahana emoji tokana.", "verify_later": "Hamariniko avy eo", - "verify_reset_warning_1": "Tsy azo atao ny mamerina ny fanalahidiny fanamarinanao. Aoriany famerenanao dia tsy ho afaka miditra amin'ny hafatra miafina taloha ianao, ary izay namana nanamarina anao teo aloha dia hahita fampitandremana momba ny fiarovana mandra-panamarinanao azy ireo indray.", - "verify_reset_warning_2": "Tohizo ihany azafady raha azonao antoka fa very daholo ny fitaovanao hafa rehetra sy ny Key Security.", "verify_using_device": "Hamarino amin'ny fitaovana hafa", "verify_using_key": "Hamarino aminy fanalahidiny voaharo", "verify_using_key_or_phrase": "Hamarino amin'ny fanalahidy na rakin-tsoratra voaharo", @@ -2302,7 +2288,6 @@ "remove_msisdn_prompt": "Esory%(phone)s ?", "spell_check_locale_placeholder": "Misafidiana toerana iray" }, - "image_thumbnails": "Asehoy ny topi-maso/sary kely hoan'ny sary", "inline_url_previews_default": "Alefaso ny fijerena URL an-tserasera aminy alàlan'ny default", "inline_url_previews_room": "Alefaso amin'ny alàlany ara-pototra hoany mpandray anjara amin'ity efitrano ity ny fijerena URL", "inline_url_previews_room_account": "Alefaso ny fijerena URL ho an'ity efitrano ity (miantraika aminao ihany)", @@ -2818,7 +2803,6 @@ "heading_without_query": "Hitady ny", "join_button_text": "anjara%(roomAddress)s", "keyboard_scroll_hint": "Ampiasao ny horonan-taratasy", - "message_search_section_title": "Fikarohana hafa", "other_rooms_in_space": "Efitrano hafa ao%(spaceName)s", "public_rooms_label": "Efitranom-bahoaka", "public_spaces_label": "Toeram-bahoaka", @@ -2828,7 +2812,6 @@ "result_may_be_hidden_privacy_warning": "Mety hafenina hoany fiainana manokana ny valiny sasany", "result_may_be_hidden_warning": "Mety hafenina ny valiny sasany", "search_dialog": "Fikarohana Dialog", - "search_messages_hint": "Raha hikaroka hafatra dia tadiavo ity kisary ity eo an-tampon'ny efitrano iray", "spaces_title": "Toerana misy anao", "start_group_chat_button": "Manomboha resaka vondrona" }, diff --git a/src/i18n/strings/nb_NO.json b/src/i18n/strings/nb_NO.json index 75c88b8c4c..cab5ac8a05 100644 --- a/src/i18n/strings/nb_NO.json +++ b/src/i18n/strings/nb_NO.json @@ -157,6 +157,7 @@ "view_message": "Se melding", "view_source": "Vis kilde", "yes": "Ja", + "yes_dismiss": "Ja, avvis", "zoom_in": "Forstørr", "zoom_out": "Forminske" }, @@ -386,6 +387,7 @@ "fallback_button": "Begynn autentisering", "mas_cross_signing_reset_cta": "Gå til kontoen din", "mas_cross_signing_reset_description": "Tilbakestill identiteten din gjennom kontoleverandøren din, og kom deretter tilbake og klikk \"Prøv på nytt\".", + "mas_cross_signing_reset_title": "Gå til kontoen din for å tilbakestille identiteten din", "msisdn": "En SMS har blitt sendt til %(msisdn)s", "msisdn_token_incorrect": "Sjetongen er feil", "msisdn_token_prompt": "Vennligst skriv inn koden den inneholder:", @@ -525,6 +527,7 @@ "message_timestamp_invalid": "Ugyldig tidsstempel", "microphone": "Mikrofon", "model": "Modell", + "moderation_and_safety": "Moderasjon og sikkerhet", "modern": "Moderne", "mute": "Demp", "n_members": { @@ -908,22 +911,13 @@ "empty_room_was_name": "Tomt rom (var %(oldName)s)", "encryption": { "access_secret_storage_dialog": { - "enter_phrase_or_key_prompt": "Skriv inn sikkerhetsfrasen eller for å fortsette.", + "alternatives": "Hvis du har en sikkerhetsnøkkel eller en sikkerhetsfrase, vil dette også fungere.", "key_validation_text": { - "invalid_security_key": "Ugyldig gjenopprettingsnøkkel", - "recovery_key_is_correct": "Ser bra ut!", - "wrong_file_type": "Feil filtype", - "wrong_security_key": "Feil gjenopprettingsnøkkel" + "wrong_security_key": "Gjenopprettingsnøkkelen du skrev inn er ikke riktig." }, - "reset_title": "Tilbakestill alt", - "reset_warning_1": "Gjør dette bare hvis du ikke har noen annen enhet å fullføre verifiseringen med.", - "reset_warning_2": "Hvis du tilbakestiller alt, starter du på nytt uten klarerte økter, ingen klarerte brukere, og du kan kanskje ikke se tidligere meldinger.", + "privacy_warning": "Sørg for at ingen kan se denne skjermen!", "restoring": "Gjenoppretter nøkler fra sikkerhetskopi", - "security_key_title": "Gjenopprettingsnøkkel", - "security_phrase_incorrect_error": "Får ikke tilgang til hemmelig lagring. Kontroller at du har skrevet inn riktig sikkerhetsfrase.", - "security_phrase_title": "Sikkerhetsfrase", - "separator": "%(securityKey)s eller %(recoveryFile)s", - "use_security_key_prompt": "Bruk sikkerhetsnøkkelen din for å fortsette." + "security_key_title": "Gjenopprettingsnøkkel" }, "bootstrap_title": "Setter opp nøkler", "cancel_entering_passphrase_description": "Er du sikker på at du vil avbryte inntasting av passordfrase?", @@ -979,6 +973,8 @@ "setup_secure_backup": { "explainer": "Ta sikkerhetskopi av nøklene dine før du logger av for å unngå å miste dem." }, + "turn_on_key_storage": "Slå på nøkkeloppbevaring", + "turn_on_key_storage_description": "Lagre din kryptografiske identitet og nøkler til meldinger sikkert på serveren. Dette gjør at du kan se historikken for meldinger på alle nye enheter.", "udd": { "interactive_verification_button": "Interaktiv verifisering ved hjelp av emoji", "other_ask_verify_text": "Be denne brukeren om å bekrefte sesjonen sin, eller bekreft den manuelt nedenfor.", @@ -992,7 +988,6 @@ "accepting": "Aksepterer …", "after_new_login": { "device_verified": "Enhet verifisert", - "reset_confirmation": "Vil du virkelig tilbakestille bekreftelsesnøkler?", "skip_verification": "Hopp over verifisering for nå", "unable_to_verify": "Kan ikke verifisere denne enheten", "verify_this_device": "Verifiser denne enheten" @@ -1063,8 +1058,6 @@ "verify_emoji_prompt": "Bekreft ved å sammenligne unike emoji.", "verify_emoji_prompt_qr": "Hvis du ikke kan skanne koden ovenfor, bekreft ved å sammenligne unike emoji.", "verify_later": "Jeg bekrefter senere", - "verify_reset_warning_1": "Tilbakestilling av bekreftelsesnøklene kan ikke angres. Etter tilbakestilling har du ikke tilgang til gamle krypterte meldinger, og eventuelle venner som tidligere har bekreftet deg, vil se sikkerhetsadvarsler til du bekrefter med dem på nytt.", - "verify_reset_warning_2": "Fortsett bare hvis du er sikker på at du har mistet alle de andre enhetene og sikkerhetsnøkkelen din.", "verify_using_device": "Bekreft med en annen enhet", "verify_using_key": "Bekreft med gjenopprettingsnøkkel", "verify_using_key_or_phrase": "Bekreft med gjenopprettingsnøkkel eller -frase", @@ -2101,6 +2094,7 @@ "room_list": { "add_room_label": "Legg til et rom", "add_space_label": "Legg til område", + "appearance": "Utseende", "breadcrumbs_empty": "Ingen nylig besøkte rom", "breadcrumbs_label": "Nylig besøkte rom", "empty": { @@ -2109,11 +2103,14 @@ "no_chats_description_no_room_rights": "Kom i gang med å sende meldinger til noen", "no_favourites": "Du har ikke favorittchat ennå", "no_favourites_description": "Du kan legge til en chat til dine favoritter i chat-innstillingene", + "no_invites": "Du har ingen uleste invitasjoner", + "no_mentions": "Du har ingen uleste omtaler", "no_people": "Du har ikke direkte chatter med noen ennå", "no_people_description": "Du kan fjerne merket for filtre for å se de andre chattene dine", "no_rooms": "Du er ikke med i noen rom ennå", "no_rooms_description": "Du kan fjerne merket for filtre for å se de andre chattene dine", "no_unread": "Gratulerer! Du har ingen uleste meldinger", + "show_activity": "Se alle aktiviteter", "show_chats": "Vis alle chatter" }, "failed_add_tag": "Kunne ikke legge til tagg %(tagName)s til rom", @@ -2121,6 +2118,8 @@ "failed_set_dm_tag": "Kan ikke sette kode på direktemeldingen", "filters": { "favourite": "Favoritter", + "invites": "Invitasjoner", + "mentions": "Omtaler", "people": "Personer", "rooms": "Rom", "unread": "Uleste" @@ -2151,14 +2150,21 @@ "more_options": "Flere alternativer", "open_room": "Åpne rom %(roomName)s" }, + "room_options": "Rominnstillinger", "show_less": "Vis mindre", + "show_message_previews": "Aktiver forhåndsvisning av meldinger", "show_n_more": { "Vis %(count)s til": "Vis %(count)s mer" }, "show_previews": "Vis forhåndsvisninger av meldinger", + "sort": "Sorter", "sort_by": "Sorter etter", "sort_by_activity": "Aktivitet", "sort_by_alphabet": "A-Å", + "sort_type": { + "activity": "Aktivitet", + "atoz": "A-Å" + }, "sort_unread_first": "Vis rom med uleste meldinger først", "space_menu": { "home": "Område hjem", @@ -2446,6 +2452,10 @@ "recent_changes_heading": "Nylige endringer som ennå ikke er mottatt", "title": "Serveren svarer ikke" }, + "service_worker_error": { + "description": "%(brand)s krever en tjenestearbeider for å laste inn autentiserte medier fra Matrix-innholdslagre. Dette støttes ikke av nettleseren din, så du kan oppleve at media ikke lastes inn.", + "title": "Kunne ikke laste inn servicearbeider" + }, "seshat": { "error_initialising": "Initialisering av meldingssøk mislyktes, sjekk innstillingene dine for mer informasjon", "reset_button": "Tilbakestill hendelseslageret", @@ -2539,6 +2549,8 @@ "session_key": "Sesjonsnøkkel:", "title": "Avansert" }, + "confirm_key_storage_off": "Er du sikker på at du vil ha nøkkeloppbevaring skrudd av?", + "confirm_key_storage_off_description": "Hvis du logger deg av alle enhetene dine, mister du meldingshistorikken og må bekrefte alle eksisterende kontakter igjen. Lær mer ", "delete_key_storage": { "breadcrumb_page": "Slett nøkkellagring", "confirm": "Slett nøkkellagring", @@ -2624,6 +2636,7 @@ "discovery_needs_terms_title": "La folk finne deg", "display_name": "Visningsnavn", "display_name_error": "Kan ikke angi visningsnavn", + "email_adding_unsupported_by_hs": "Denne hjemmeserveren støtter ikke å legge til flere e-postadresser til kontoen din.", "email_address_in_use": "Denne e-postadressen er allerede i bruk", "email_address_label": "E-postadresse", "email_not_verified": "E-postadressen din er ikke verifisert ennå", @@ -2648,7 +2661,9 @@ "error_share_msisdn_discovery": "Kan ikke dele telefonnummer", "identity_server_no_token": "Ingen identitetstilgangstoken funnet", "identity_server_not_set": "Identitetsserver er ikke angitt", + "invalid_phone_number": "Telefonnummeret som er oppgitt ser ikke ut til å være gyldig.", "language_section": "Språk", + "msisdn_adding_unsupported_by_hs": "Denne hjemmeserveren støtter ikke å legge til telefonnumre til kontoen din.", "msisdn_in_use": "Dette mobilnummeret er allerede i bruk", "msisdn_label": "Telefonnummer", "msisdn_verification_field_label": "Verifikasjonskode", @@ -2667,12 +2682,10 @@ "unable_to_load_msisdns": "Kan ikke laste inn telefonnumre", "username": "Brukernavn" }, - "image_thumbnails": "Vis forhåndsvisninger for bilder", "inline_url_previews_default": "Skru på URL-forhåndsvisninger inni meldinger som standard", "inline_url_previews_room": "Skru på URL-forhåndsvisninger som standard for deltakerne i dette rommet", "inline_url_previews_room_account": "Skru på URL-forhåndsvisninger for dette rommet (Påvirker bare deg)", "insert_trailing_colon_mentions": "Sett inn et etterfølgende kolon etter at brukeromtaler i starten av en melding", - "invite_avatars": "Vis avatarer til rom du har blitt invitert til", "jump_to_bottom_on_send": "Gå til bunnen av tidslinjen når du vil sende en melding", "key_backup": { "backup_in_progress": "Nøklene dine blir sikkerhetskopiert (den første sikkerhetskopieringen kan ta noen minutter).", @@ -2731,6 +2744,14 @@ "labs_mjolnir": { "dialog_title": "Innstillinger: Ignorerte brukere" }, + "media_preview": { + "hide_avatars": "Skjul avatarer for rom og inviterer", + "hide_media": "Skjul alltid", + "media_preview_description": "Et skjult medium kan alltid vises ved å trykke på det", + "media_preview_label": "Vis media i tidslinjen", + "show_in_private": "I private rom", + "show_media": "Vis alltid" + }, "notifications": { "default_setting_description": "Denne innstillingen vil bli brukt som standard for alle rommene dine.", "default_setting_section": "Jeg ønsker å bli varslet for (standardinnstilling)", @@ -3214,7 +3235,7 @@ "heading_without_query": "Søk etter", "join_button_text": "Bli med i %(roomAddress)s", "keyboard_scroll_hint": "Bruk for å scrolle", - "message_search_section_title": "Andre søk", + "messages_label": "Meldinger", "other_rooms_in_space": "Andre rom i %(spaceName)s", "public_rooms_label": "Offentlige rom", "public_spaces_label": "Offentlige områder", @@ -3224,7 +3245,6 @@ "result_may_be_hidden_privacy_warning": "Noen resultater kan være skjult av personvernhensyn", "result_may_be_hidden_warning": "Noen resultater kan være skjult", "search_dialog": "Søkedialog", - "search_messages_hint": "For å søke i meldinger, se etter dette ikonet øverst i et rom ", "spaces_title": "Områder du er i", "start_group_chat_button": "Start en gruppechat" }, @@ -3273,9 +3293,7 @@ "threads_activity_centre": { "header": "Trådaktivitet", "no_rooms_with_threads_notifs": "Du har ikke rom med trådvarsler ennå.", - "no_rooms_with_unread_threads": "Du har ikke rom med uleste tråder ennå.", - "release_announcement_description": "Trådenotifikasjoner er flyttet, du finner dem her fra nå av.", - "release_announcement_header": "Aktivitetssenter for tråder" + "no_rooms_with_unread_threads": "Du har ikke rom med uleste tråder ennå." }, "time": { "about_day_ago": "cirka 1 dag siden", @@ -3783,10 +3801,11 @@ "unavailable": "Ikke tilgjengelig" }, "update_room_access_modal": { - "description": "Hvis du vil opprette en delingslenke, må du tillate gjester å bli med i dette rommet. Dette kan gjøre rommet mindre sikkert. Når du er ferdig med samtalen, kan du gjøre rommet privat igjen.", - "dont_change_description": "Alternativt kan du holde samtalen i et eget rom.", + "description": "For å opprette en delingslenke, lag dette rommet offentlig eller aktiver alternativet for brukere å be om å bli med Dette lar gjester bli med uten å bli invitert.", + "dont_change_description": "Hvis du ikke ønsker å endre tilgangen til dette rommet, kan du opprette et nytt rom for lenken.", "no_change": "Jeg vil ikke endre tilgangsnivået.", - "title": "Endre tilgangsnivået til rommet" + "revert_access_description": "(Dette kan tilbakestilles til den forrige verdien i Rominnstillinger: Sikkerhet og personvern / Tilgang)", + "title": "Tillat gjestebrukere å bli med i dette rommet" }, "upload_failed_generic": "Filen '%(fileName)s' kunne ikke lastes opp.", "upload_failed_size": "Filen \"%(fileName)s\" er større enn hjemmeserverens grense for opplastninger", diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index 2cad3b3875..c8cfd1e8ba 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -692,22 +692,11 @@ "empty_room_was_name": "Lege ruimte (was %(oldName)s)", "encryption": { "access_secret_storage_dialog": { - "enter_phrase_or_key_prompt": "Voer uw veiligheidswachtwoord in of om door te gaan.", "key_validation_text": { - "invalid_security_key": "Ongeldige veiligheidssleutel", - "recovery_key_is_correct": "Ziet er goed uit!", - "wrong_file_type": "Verkeerd bestandstype", "wrong_security_key": "Verkeerde veiligheidssleutel" }, - "reset_title": "Alles opnieuw instellen", - "reset_warning_1": "Doe dit alleen als u geen ander apparaat hebt om de verificatie mee uit te voeren.", - "reset_warning_2": "Als u alles reset zult u opnieuw opstarten zonder vertrouwde sessies, zonder vertrouwde personen, en zult u misschien geen oude berichten meer kunnen zien.", "restoring": "Sleutels herstellen vanaf back-up", - "security_key_title": "Veiligheidssleutel", - "security_phrase_incorrect_error": "Geen toegang tot geheime opslag. Controleer of u het juiste veiligheidswachtwoord hebt ingevoerd.", - "security_phrase_title": "Veiligheidswachtwoord", - "separator": "%(securityKey)s of %(recoveryFile)s", - "use_security_key_prompt": "Gebruik uw veiligheidssleutel om verder te gaan." + "security_key_title": "Veiligheidssleutel" }, "bootstrap_title": "Sleutelconfiguratie", "cancel_entering_passphrase_description": "Weet je zeker, dat je het invoeren van je wachtwoord wilt afbreken?", @@ -764,7 +753,6 @@ "accepting": "Toestaan…", "after_new_login": { "device_verified": "Apparaat geverifieerd", - "reset_confirmation": "Echt je verificatiesleutels resetten?", "skip_verification": "Verificatie voorlopig overslaan", "unable_to_verify": "Kan dit apparaat niet verifiëren", "verify_this_device": "Verifieer dit apparaat" @@ -826,7 +814,6 @@ "verify_emoji_prompt": "Verifieer door unieke emoji te vergelijken.", "verify_emoji_prompt_qr": "Als je bovenstaande code niet kan scannen, verifieer dan door unieke emoji te vergelijken.", "verify_later": "Ik verifieer het later", - "verify_reset_warning_1": "Het resetten van je verificatiesleutels kan niet ongedaan worden gemaakt. Na het resetten heb je geen toegang meer tot oude versleutelde berichten, en vrienden die je eerder hebben geverifieerd zullen veiligheidswaarschuwingen zien totdat je opnieuw bij hen geverifieert bent.", "verify_using_device": "Verifieer met andere apparaat", "verify_using_key": "Verifieer met veiligheidssleutel", "verify_using_key_or_phrase": "Verifieer met veiligheidssleutel of -wachtwoord", @@ -1988,7 +1975,6 @@ "remove_msisdn_prompt": "%(phone)s verwijderen?", "spell_check_locale_placeholder": "Kies een landinstelling" }, - "image_thumbnails": "Miniaturen voor afbeeldingen tonen", "inline_url_previews_default": "Inline URL-voorvertoning standaard inschakelen", "inline_url_previews_room": "URL-voorvertoning voor alle deelnemers aan deze kamer standaard inschakelen", "inline_url_previews_room_account": "URL-voorvertoning in dit kamer inschakelen (geldt alleen voor jou)", @@ -2418,7 +2404,6 @@ "heading_without_query": "Zoeken naar", "join_button_text": "%(roomAddress)s toetreden", "keyboard_scroll_hint": "Gebruik om te scrollen", - "message_search_section_title": "Andere zoekopdrachten", "other_rooms_in_space": "Andere kamers in %(spaceName)s", "public_rooms_label": "Publieke kamers", "recent_searches_section_title": "Recente zoekopdrachten", @@ -2427,7 +2412,6 @@ "result_may_be_hidden_privacy_warning": "Sommige resultaten kunnen om privacyredenen verborgen zijn", "result_may_be_hidden_warning": "Sommige resultaten zijn mogelijk verborgen", "search_dialog": "Dialoogvenster Zoeken", - "search_messages_hint": "Om berichten te zoeken, zoek naar dit icoon bovenaan een kamer ", "spaces_title": "Spaces waar u in zit", "start_group_chat_button": "Start een groepsgesprek" }, diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index d567b6f8e1..42eb0de4af 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -388,6 +388,7 @@ "fallback_button": "Rozpocznij uwierzytelnienie", "mas_cross_signing_reset_cta": "Przejdź do swojego konta", "mas_cross_signing_reset_description": "Zresetuj swoją tożsamość poprzez dostawcę swojego konta, wróć i kliknij „Ponów”.", + "mas_cross_signing_reset_title": "Przejdź do swojego konta, aby zresetować swoją tożsamość", "msisdn": "Wysłano wiadomość tekstową do %(msisdn)s", "msisdn_token_incorrect": "Niepoprawny token", "msisdn_token_prompt": "Wpisz kod, który jest tam zawarty:", @@ -528,6 +529,7 @@ "message_timestamp_invalid": "Nieprawidłowy znacznik czasu", "microphone": "Mikrofon", "model": "Model", + "moderation_and_safety": "Moderacja i bezpieczeństwo", "modern": "Współczesny", "mute": "Wycisz", "n_members": { @@ -914,22 +916,11 @@ "empty_room_was_name": "Pusty pokój (poprzednio %(oldName)s)", "encryption": { "access_secret_storage_dialog": { - "enter_phrase_or_key_prompt": "Wprowadź swoją frazę bezpieczeństwa lub , aby kontynuować.", "key_validation_text": { - "invalid_security_key": "Błędny klucz przywracania", - "recovery_key_is_correct": "Wygląda dobrze!", - "wrong_file_type": "Błędny typ pliku", "wrong_security_key": "Błędny klucz przywracania" }, - "reset_title": "Resetuj wszystko", - "reset_warning_1": "Zrób to tylko wtedy, gdy nie masz innego urządzenia, za pomocą którego mógłbyś zakończyć weryfikację.", - "reset_warning_2": "Jeśli zresetujesz wszystko, stracisz wszystkie sesje zaufane, użytkowników zaufanych i możliwe, że nie będziesz w stanie przeglądać historii czatu.", "restoring": "Przywracanie kluczy z kopii zapasowej", - "security_key_title": "Klucz przywracania", - "security_phrase_incorrect_error": "Nie można uzyskać dostępu do sekretnego magazynu. Upewnij się, że wprowadzono poprawne Hasło bezpieczeństwa.", - "security_phrase_title": "Hasło bezpieczeństwa", - "separator": "%(securityKey)s lub %(recoveryFile)s", - "use_security_key_prompt": "Użyj klucza przywracania, aby kontynuować." + "security_key_title": "Klucz przywracania" }, "bootstrap_title": "Konfigurowanie kluczy", "cancel_entering_passphrase_description": "Czy na pewno chcesz anulować wpisywanie hasła?", @@ -998,7 +989,6 @@ "accepting": "Akceptowanie…", "after_new_login": { "device_verified": "Urządzenie zweryfikowane", - "reset_confirmation": "Czy na pewno zresetować klucze weryfikacyjne?", "skip_verification": "Pomiń weryfikację na razie", "unable_to_verify": "Nie można zweryfikować tego urządzenia", "verify_this_device": "Zweryfikuj to urządzenie" @@ -1069,8 +1059,6 @@ "verify_emoji_prompt": "Zweryfikuj, porównując unikalne emotikony.", "verify_emoji_prompt_qr": "Jeśli nie jesteś w stanie skanować kodu powyżej, zweryfikuj porównując emoji.", "verify_later": "Zweryfikuję później", - "verify_reset_warning_1": "Zresetowanie kluczy weryfikacyjnych nie może być cofnięte. Po zresetowaniu, nie będziesz mieć dostępu do starych wiadomości szyfrowanych, a wszyscy znajomi, którzy wcześniej Cię zweryfikowali, będą widzieć ostrzeżenia do czasu ponownej weryfikacji.", - "verify_reset_warning_2": "Kontynuuj tylko wtedy, gdy masz pewność, że utraciłeś wszystkie inne urządzenia i swój klucz przywracania.", "verify_using_device": "Zweryfikuj innym urządzeniem", "verify_using_key": "Zweryfikuj kluczem przywracania", "verify_using_key_or_phrase": "Zweryfikuj kluczem przywracania lub frazą", @@ -2117,6 +2105,7 @@ "room_list": { "add_room_label": "Dodaj pokój", "add_space_label": "Dodaj przestrzeń", + "appearance": "Wygląd", "breadcrumbs_empty": "Brak ostatnio odwiedzonych pokojów", "breadcrumbs_label": "Ostatnio odwiedzane pokoje", "empty": { @@ -2167,16 +2156,23 @@ "more_options": "Więcej opcji", "open_room": "Pokój otwarty %(roomName)s" }, + "room_options": "Opcje pokoju", "show_less": "Pokaż mniej", + "show_message_previews": "Pokaż podglądy wiadomości", "show_n_more": { "one": "Pokaż %(count)s więcej", "few": "Pokaż %(count)s więcej", "many": "Pokaż %(count)s więcej" }, "show_previews": "Pokazuj podgląd wiadomości", + "sort": "Sortuj", "sort_by": "Sortuj według", "sort_by_activity": "Aktywności", "sort_by_alphabet": "A-Z", + "sort_type": { + "activity": "Aktywność", + "atoz": "A-Z" + }, "sort_unread_first": "Pokazuj najpierw pokoje z nieprzeczytanymi wiadomościami", "space_menu": { "home": "Główna przestrzeni", @@ -2464,6 +2460,10 @@ "recent_changes_heading": "Najnowsze zmiany nie zostały jeszcze wprowadzone", "title": "Serwer nie odpowiada" }, + "service_worker_error": { + "description": "%(brand)s wymaga workera usługi, aby móc załadować zabezpieczone media z repozytoriów Matrix. Ta funkcja nie jest obsługiwana przez Twoją przeglądarkę, więc niektóre media mogą się nie załadować.", + "title": "Nie udało się załadować workera usługi" + }, "seshat": { "error_initialising": "Wystąpił błąd inicjalizacji wyszukiwania wiadomości, sprawdź swoje ustawienia po więcej informacji", "reset_button": "Resetuj bank wydarzeń", @@ -2642,6 +2642,7 @@ "discovery_needs_terms_title": "Pozwól ludziom Cię znaleźć", "display_name": "Wyświetlana nazwa", "display_name_error": "Nie można ustawić wyświetlanej nazwy", + "email_adding_unsupported_by_hs": "Ten serwer domowy nie obsługuje dodawania adresów e-mail do Twojego konta.", "email_address_in_use": "Podany adres e-mail jest już w użyciu", "email_address_label": "Adres e-mail", "email_not_verified": "Twój adres e-mail nie został jeszcze zweryfikowany", @@ -2666,7 +2667,9 @@ "error_share_msisdn_discovery": "Nie udało się udostępnić numeru telefonu", "identity_server_no_token": "Nie znaleziono tokena dostępu tożsamości", "identity_server_not_set": "Serwer tożsamości nie jest ustawiony", + "invalid_phone_number": "Podany numer telefonu nie wydaje się być poprawny.", "language_section": "Język", + "msisdn_adding_unsupported_by_hs": "Ten serwer domowy nie obsługuje dodawania numerów telefonu do Twojego konta.", "msisdn_in_use": "Ten numer telefonu jest już zajęty", "msisdn_label": "Numer telefonu", "msisdn_verification_field_label": "Kod weryfikacyjny", @@ -2685,12 +2688,10 @@ "unable_to_load_msisdns": "Nie można załadować numerów telefonu", "username": "Nazwa użytkownika" }, - "image_thumbnails": "Pokaż podgląd/miniatury obrazów", "inline_url_previews_default": "Włącz domyślny podgląd URL w tekście", "inline_url_previews_room": "Włącz domyślny podgląd URL dla uczestników w tym pokoju", "inline_url_previews_room_account": "Włącz podgląd URL dla tego pokoju (dotyczy tylko Ciebie)", "insert_trailing_colon_mentions": "Wstawiaj dwukropek po wzmiance użytkownika na początku wiadomości", - "invite_avatars": "Pokaż awatary pokoi, do których zostałeś zaproszony", "jump_to_bottom_on_send": "Przejdź na dół osi czasu po wysłaniu wiadomości", "key_backup": { "backup_in_progress": "Tworzy się kopia zapasowa Twoich kluczy (pierwsza kopia może potrwać kilka minut).", @@ -2749,6 +2750,14 @@ "labs_mjolnir": { "dialog_title": "Ustawienia: Ignorowani użytkownicy" }, + "media_preview": { + "hide_avatars": "Ukryj awatary pokoju i zapraszającego", + "hide_media": "Zawsze ukrywaj", + "media_preview_description": "Ukryte media można zawsze wyświetlić, dotykając ich", + "media_preview_label": "Pokaż media na osi czasu", + "show_in_private": "W pokojach prywatnych", + "show_media": "Zawsze pokazuj" + }, "notifications": { "default_setting_description": "To ustawienie zastosuje się do wszystkich Twoich pokoi.", "default_setting_section": "Chce otrzymywać powiadomienia (Domyślne ustawienie)", @@ -3232,7 +3241,7 @@ "heading_without_query": "Szukaj", "join_button_text": "Dołącz %(roomAddress)s", "keyboard_scroll_hint": "Użyj , aby przewijać", - "message_search_section_title": "Inne wyszukiwania", + "messages_label": "Wiadomości", "other_rooms_in_space": "Inne pokoje w %(spaceName)s", "public_rooms_label": "Pokoje publiczne", "public_spaces_label": "Przestrzenie publiczne", @@ -3242,7 +3251,6 @@ "result_may_be_hidden_privacy_warning": "Niektóre wyniki zostały ukryte dla ochrony prywatności", "result_may_be_hidden_warning": "Niektóre wyniki mogą być ukryte", "search_dialog": "Pasek wyszukiwania", - "search_messages_hint": "Aby szukać wiadomości, poszukaj tej ikony na górze pokoju ", "spaces_title": "Przestrzenie, w których jesteś", "start_group_chat_button": "Rozpocznij czat grupowy" }, @@ -3280,7 +3288,7 @@ "other": "%(count)s odpowiedzi" }, "empty_description": "Użyj „%(replyInThread)s” po najechaniu kursorem na wiadomość", - "empty_title": "Wątki pomagają utrzymać tematykę rozmów i łatwo za nimi podążyć.", + "empty_title": "Wątki pomagają utrzymać tematykę rozmów i łatwo za nimi podążać.", "error_start_thread_existing_relation": "Nie można utworzyć wątku z wydarzenia z istniejącą relacją", "mark_all_read": "Oznacz wszystkie jako przeczytane", "my_threads": "Moje wątki", @@ -3291,9 +3299,7 @@ "threads_activity_centre": { "header": "Aktywność wątków", "no_rooms_with_threads_notifs": "Nie masz jeszcze pokoi z powiadomieniami w wątku.", - "no_rooms_with_unread_threads": "Nie masz jeszcze pokoi z nieprzeczytanymi wątkami.", - "release_announcement_description": "Powiadomienia w wątkach zostały przeniesione, teraz znajdziesz je tutaj.", - "release_announcement_header": "Centrum aktywności wątków" + "no_rooms_with_unread_threads": "Nie masz jeszcze pokoi z nieprzeczytanymi wątkami." }, "time": { "about_day_ago": "około dzień temu", @@ -3811,10 +3817,11 @@ "unavailable": "Niedostępny" }, "update_room_access_modal": { - "description": "Aby utworzyć link udostępniania, musisz zezwolić gościom na dołączenie do tego pokoju. Może to zmniejszyć bezpieczeństwo pokoju. Gdy zakończysz połączenie, możesz ustawić pokój jako prywatny z powrotem.", - "dont_change_description": "Możesz również zadzwonić w innym pokoju.", + "description": "Aby utworzyć link udostępniania, ustaw ten pokój jako publiczny lub włącz opcję umożliwiającą użytkownikom poprosić o dołączenie. Dzięki temu goście będą mogli dołączyć bez zaproszenia.", + "dont_change_description": "Jeśli nie chcesz zmieniać uprawnień dostępu tego pokoju, możesz utworzyć link połączenia w nowym pokoju.", "no_change": "Nie chce zmieniać poziomu uprawnień.", - "title": "Zmień poziom dostępu pokoju" + "revert_access_description": "(Przywróć poprzednią wartość w ustawieniach pokoju: Bezpieczeństwo i prywatność / Dostęp)", + "title": "Zezwól gościom na dołączenie do tego pokoju" }, "upload_failed_generic": "Nie udało się przesłać pliku '%(fileName)s'.", "upload_failed_size": "Plik '%(fileName)s' przekracza limit rozmiaru dla tego serwera głównego", diff --git a/src/i18n/strings/pt.json b/src/i18n/strings/pt.json index 93e5108019..db6df857a6 100644 --- a/src/i18n/strings/pt.json +++ b/src/i18n/strings/pt.json @@ -64,7 +64,8 @@ "go": "Ir", "go_back": "Voltar", "got_it": "Entendi", - "hide_advanced": "Ocultar avançado", + "hide": "Ocultar", + "hide_advanced": "Ocultar avançadas", "hold": "Espera", "ignore": "Ignorar", "import": "Importar", @@ -80,12 +81,14 @@ "maximise": "Maximizar", "mention": "Mencionar", "minimise": "Minimizar", + "new_message": "Nova mensagem", "new_room": "Nova sala", "new_video_room": "Nova sala de vídeo", "next": "Próximo", "no": "Não", "ok": "OK", "open": "Abrir", + "open_menu": "Abrir menu", "pause": "Pausar", "pin": "Fixar", "play": "Reproduzir", @@ -404,6 +407,15 @@ "download_logs": "Descarrega os registos", "downloading_logs": "Descarregando registos", "error_empty": "Diz-nos o que correu mal ou, melhor ainda, cria uma questão no GitHub que descreva o problema.", + "failed_download_logs": "Falha ao descarregar os registos de depuração: ", + "failed_send_logs_causes": { + "disallowed_app": "O teu relatório de erro foi rejeitado. O servidor rageshake não suporta esta aplicação.", + "rejected_generic": "O teu relatório de bug foi rejeitado. O servidor do rageshake rejeitou o conteúdo do relatório devido a uma política.", + "rejected_recovery_key": "O teu relatório de erro foi rejeitado por razões de segurança, pois continha uma chave de recuperação.", + "rejected_version": "O teu relatório de erro foi rejeitado porque a versão que estás a executar é demasiado antiga.", + "server_unknown_error": "O servidor do rageshake encontrou um erro desconhecido e não pôde processar o relatório.", + "unknown_error": "Não conseguiste enviar os registos." + }, "github_issue": "Problema no GitHub", "introduction": "Se submeteste um erro através do GitHub, os registos de depuração podem ajudar-nos a localizar o problema. ", "log_request": "Para nos ajudar a evitar esta situação no futuro, envia-nos os registos para .", @@ -443,6 +455,7 @@ "access_token": "Token de acesso", "accessibility": "Acessibilidade", "advanced": "Avançado", + "all_chats": "Todas as conversas", "analytics": "Análise", "and_n_others": { "other": "e %(count)s outros...", @@ -872,22 +885,11 @@ "empty_room_was_name": "Sala vazia (era %(oldName)s)", "encryption": { "access_secret_storage_dialog": { - "enter_phrase_or_key_prompt": "Introduz a tua frase de segurança ou para continuar.", "key_validation_text": { - "invalid_security_key": "Chave de recuperação inválida", - "recovery_key_is_correct": "Parece bom!", - "wrong_file_type": "Tipo de ficheiro errado", "wrong_security_key": "Chave de recuperação errada" }, - "reset_title": "Repor tudo", - "reset_warning_1": "Faz isto apenas se não tiveres outro dispositivo para completar a verificação.", - "reset_warning_2": "Se reiniciares tudo, irás reiniciar sem sessões de confiança, sem utilizadores de confiança e poderás não conseguir ver mensagens anteriores.", "restoring": "Restaurar chaves a partir de uma cópia de segurança", - "security_key_title": "Chave de recuperação", - "security_phrase_incorrect_error": "Não é possível aceder ao armazenamento secreto. Verifica se introduziste a frase de segurança correcta.", - "security_phrase_title": "Frase de segurança", - "separator": "%(securityKey)s ou %(recoveryFile)s", - "use_security_key_prompt": "Utiliza a tua chave de recuperação para continuar." + "security_key_title": "Chave de recuperação" }, "bootstrap_title": "A configurar chaves", "cancel_entering_passphrase_description": "Tem a certeza que quer cancelar a introdução da frase-passe?", @@ -926,8 +928,8 @@ "title": "Novo método de recuperação", "warning": "Se não tiveres definido o novo método de recuperação, um atacante pode estar a tentar aceder à tua conta. Altera a palavra-passe da tua conta e define imediatamente um novo método de recuperação nas Definições." }, - "pinned_identity_changed": "A identidade de %(displayName)s (%(userId)s ) parece ter mudado. Saber mais", - "pinned_identity_changed_no_displayname": "A identidade de %(userId)s parece ter mudado. Saiba mais", + "pinned_identity_changed": "A identidade de %(displayName)s (%(userId)s ) foi alterada. Saber mais", + "pinned_identity_changed_no_displayname": "A identidade de %(userId)s foi alterada. Saber mais", "recovery_method_removed": { "description_1": "Esta sessão detectou que a tua frase de segurança e a chave para as mensagens seguras foram removidas.", "description_2": "Se o fizeste acidentalmente, podes configurar as Mensagens seguras nesta sessão, o que criptografará novamente o histórico de mensagens desta sessão com um novo método de recuperação.", @@ -956,7 +958,6 @@ "accepting": "Aceitando...", "after_new_login": { "device_verified": "Dispositivo verificado", - "reset_confirmation": "Repõe mesmo as chaves de verificação?", "skip_verification": "Ignora a verificação por enquanto", "unable_to_verify": "Não é possível verificar este dispositivo", "verify_this_device": "Verifica este dispositivo" @@ -1027,8 +1028,6 @@ "verify_emoji_prompt": "Verifica comparando o emoji único.", "verify_emoji_prompt_qr": "Se não conseguires ler o código acima, verifica-o comparando emojis.", "verify_later": "Verificarei mais tarde", - "verify_reset_warning_1": "A reposição das tuas chaves de verificação não pode ser anulada. Após a reposição, não terás acesso a mensagens encriptadas antigas e todos os amigos que te tenham verificado anteriormente verão avisos de segurança até voltares a verificar com eles.", - "verify_reset_warning_2": "Só avances se tiveres a certeza de que perdeste todos os teus outros dispositivos e a tua chave de recuperação.", "verify_using_device": "Verifica com outro dispositivo", "verify_using_key": "Verifica com a chave de recuperação", "verify_using_key_or_phrase": "Verifica com a chave ou frase de recuperação", @@ -1039,7 +1038,7 @@ }, "verification_requested_toast_title": "Verificação solicitada", "verified_identity_changed": "%(displayName)s(%(userId)s) mudou a tua identidade verificada. Saber mais", - "verified_identity_changed_no_displayname": "A identidade verificada de %(userId)s foi alterada. Saber mais", + "verified_identity_changed_no_displayname": "A identidade de %(userId)s foi alterada. Saber mais", "verify_toast_description": "Outros utilizadores podem não confiar nisto", "verify_toast_title": "Verifica esta sessão", "withdraw_verification_action": "Retirar verificação" @@ -1222,6 +1221,7 @@ "change": "Altera o servidor de identidade", "change_prompt": "Desligar do servidor de identidade e ligar a em vez disso?", "change_server_prompt": "Se não pretenderes utilizar para descobrir e ser descoberto pelos contactos existentes que conheces, introduz outro servidor de identidade abaixo.", + "changed": "O teu servidor de identidade foi alterado", "checking": "A verificar o servidor", "description_connected": "Atualmente, estás a utilizar para descobrires e seres descoberto pelos contactos existentes que conheces. Podes alterar o teu servidor de identidade abaixo.", "description_disconnected": "Atualmente, não estás a utilizar um servidor de identidade. Para descobrires e seres descoberto pelos contactos existentes que conheces, adiciona um abaixo.", @@ -1264,7 +1264,9 @@ "title": "%(brand)s não suporta este browser", "use_desktop_heading": "Utiliza antes o %(brand)s Desktop", "use_mobile_heading": "Utiliza antes o %(brand)s no telemóvel", - "use_mobile_heading_after_desktop": "Ou utiliza a nossa aplicação móvel" + "use_mobile_heading_after_desktop": "Ou utiliza a nossa aplicação móvel", + "windows_64bit": "Windows (64 bits)", + "windows_arm_64bit": "Windows (ARM 64-bit)" }, "info_tooltip_title": "Informação", "integration_manager": { @@ -1273,6 +1275,7 @@ "error_connecting_heading": "Não é possível conectar-se ao gerenciador de integração", "explainer": "Os gestores de integração recebem dados de configuração e podem modificar widgets, enviar convites para salas e definir níveis de potência em teu nome.", "manage_title": "Gere as integrações", + "toggle_label": "Ativar o gestor de integração", "use_im": "Utiliza um gestor de integração para gerir bots, widgets e pacotes de autocolantes.", "use_im_default": "Utiliza um gestor de integração (%(serverName)s) para gerir bots, widgets e pacotes de autocolantes." }, @@ -2056,20 +2059,54 @@ "add_space_label": "Adiciona espaço", "breadcrumbs_empty": "Nenhuma sala visitada recentemente", "breadcrumbs_label": "Salas visitadas recentemente", + "empty": { + "no_chats": "Ainda sem conversas", + "no_chats_description": "Começa a enviar mensagens a alguém ou a crie uma sala", + "no_chats_description_no_room_rights": "Começa por enviar uma mensagem a alguém", + "no_favourites": "Ainda não tem um conversa favorita", + "no_favourites_description": "Pode adicionar uma conversa aos seus favoritos nas definições de conversa", + "no_people": "Ainda não tem conversas diretas com ninguém", + "no_people_description": "Pode desseleccionar filtros para veres as suas outras conversas", + "no_rooms": "Você ainda não está em nenhuma sala", + "no_rooms_description": "Pode desmarcar filtros para ver as suas outras conversas", + "no_unread": "Parabéns! Não tens nenhuma mensagem por ler", + "show_chats": "Mostra todas as conversas" + }, "failed_add_tag": "Falha ao adicionar %(tagName)s à sala", "failed_remove_tag": "Não foi possível remover a marcação %(tagName)s desta sala", "failed_set_dm_tag": "Falha ao definir a etiqueta de mensagem direta", + "filters": { + "favourite": "Favoritos", + "people": "Pessoas", + "rooms": "Salas", + "unread": "Não lido" + }, "home_menu_label": "Opções de casa", "join_public_room_label": "Participa na sala pública", "joining_rooms_status": { "one": "Atualmente ingressando%(count)s sala", "other": "Atualmente ingressando%(count)s salas" }, + "list_title": "Lista de salas", + "more_options": { + "copy_link": "Copiar link da sala", + "favourited": "Adicionado aos favoritos", + "leave_room": "Sair da sala", + "low_priority": "Baixa prioridade", + "mark_read": "Marcar como lido", + "mark_unread": "Marcar como não lido" + }, "notification_options": "Opções de notificação", + "open_space_menu": "Abrir menu do espaço", + "primary_filters": "Filtros da lista de salas", "redacting_messages_status": { "one": "Atualmente removendo mensagens na %(count)s sala", "other": "Atualmente removendo mensagens em %(count)s salas" }, + "room": { + "more_options": "Mais opções", + "open_room": "Abrir a sala %(roomName)s" + }, "show_less": "Mostrar menos", "show_n_more": { "one": "Mostrar %(count)s mais", @@ -2080,6 +2117,10 @@ "sort_by_activity": "Atividade", "sort_by_alphabet": "A-Z", "sort_unread_first": "Mostra primeiro as salas com mensagens não lidas", + "space_menu": { + "home": "Casa do espaço", + "space_settings": "Configurações de espaço" + }, "space_menu_label": "%(spaceName)s menu", "sublist_options": "Lista de opções", "suggested_rooms_heading": "Salas sugeridas" @@ -2113,7 +2154,7 @@ "upgrade_dwarning_ialog_title_public": "Atualizar sala pública", "upgrade_warning_dialog_description": "A atualização de uma divisão é uma ação avançada e é normalmente recomendada quando uma divisão está instável devido a erros, funcionalidades em falta ou vulnerabilidades de segurança.", "upgrade_warning_dialog_explainer": "Tem em atenção que a atualização fará uma nova versão da sala. Todas as mensagens actuais permanecerão nesta sala arquivada.", - "upgrade_warning_dialog_footer": "Actualizarás este quarto de para .", + "upgrade_warning_dialog_footer": "Esta sala será atualizada de para .", "upgrade_warning_dialog_invite_label": "Convida automaticamente os membros desta sala para a nova sala", "upgrade_warning_dialog_report_bug_prompt": "Normalmente, isto só afecta a forma como a sala é processada no servidor. Se estiveres a ter problemas com o teu %(brand)s, por favor reporta um erro.", "upgrade_warning_dialog_report_bug_prompt_link": "Normalmente, isto apenas afecta a forma como a sala é processada no servidor. Se estiveres a ter problemas com o teu %(brand)s, por favor reporta um bug.", @@ -2253,7 +2294,7 @@ "encrypted_room_public_confirm_description_1": "Não é recomendável tornar públicas as salas encriptadas. Isso significa que qualquer pessoa pode encontrar e entrar na sala, pelo que qualquer pessoa pode ler as mensagens. Não terás nenhum dos benefícios da encriptação. Encriptar mensagens numa sala pública tornará a receção e o envio de mensagens mais lento.", "encrypted_room_public_confirm_description_2": "Para evitar estes problemas, cria uma nova sala pública para a conversa que pretendes ter.", "encrypted_room_public_confirm_title": "Tens a certeza de que queres tornar pública esta sala encriptada?", - "encryption_forced": "O teu servidor requer que a encriptação seja desactivada.", + "encryption_forced": "O servidor requer que a encriptação seja desativada.", "encryption_permanent": "Uma vez activada, a encriptação não pode ser desactivada.", "error_join_rule_change_title": "Falha ao atualizar as regras de adesão", "error_join_rule_change_unknown": "Falha desconhecida", @@ -2308,7 +2349,7 @@ "public_without_alias_warning": "Para estabelecer uma ligação a esta sala, adiciona um endereço.", "publish_room": "Torna esta sala visível no diretório público de salas.", "publish_space": "Torna este espaço visível no diretório de salas públicas.", - "strict_encryption": "Nunca envia mensagens encriptadas para sessões não verificadas nesta sala a partir desta sessão", + "strict_encryption": "Enviar mensagens apenas a utilizadores verificados.", "title": "Segurança e privacidade" }, "title": "Configurações da sala -%(roomName)s", @@ -2442,20 +2483,35 @@ "breadcrumb_title_forgot": "Esqueceu-se da sua chave de recuperação? Terá de repor a sua identidade.", "breadcrumb_warning": "Faz isto apenas se acreditares que a tua conta foi comprometida.", "details_title": "Detalhes da encriptação", + "do_not_close_warning": "Não feches esta janela até que a reposição esteja concluída", "export_keys": "Exportar chaves", "import_keys": "Importar chaves", "other_people_device_description": "Por predefinição, nas salas encriptadas, não envies mensagens encriptadas a ninguém até as teres verificado", "other_people_device_label": "Nunca enviar mensagens encriptadas para dispositivos não verificados", "other_people_device_title": "Dispositivos de outras pessoas", "reset_identity": "Redefinir identidade criptográfica", + "reset_in_progress": "Reposição em curso...", "session_id": "ID da sessão:", "session_key": "Chave da sessão:", "title": "Avançadas" }, + "delete_key_storage": { + "breadcrumb_page": "Excluir armazenamento de chaves", + "confirm": "Excluir armazenamento de chaves", + "description": "A exclusão do armazenamento de chaves removerá sua identidade criptográfica e chaves de mensagem do servidor e desativará os seguintes recursos de segurança:", + "list_first": "Você não terá histórico de mensagens criptografadas em novos dispositivos", + "list_second": "Irá perder o acesso às suas mensagens encriptadas se se desconectar de %(brand)s em qualquer lugar", + "title": "Tem certeza de que deseja desativar o armazenamento de chaves e excluí-lo?" + }, "device_not_verified_button": "Verificar este dispositivo", "device_not_verified_description": "Você precisa verificar este dispositivo para visualizar suas configurações de criptografia.", "device_not_verified_title": "Dispositivo não verificado", "dialog_title": "Configurações: Encriptação", + "key_storage": { + "allow_key_storage": "Permitir armazenamento de chaves", + "description": "Armazene sua identidade criptográfica e chaves de mensagem com segurança no servidor. Isso permitirá que você visualize seu histórico de mensagens em quaisquer novos dispositivos. Saiba mais", + "title": "Armazenamento de chaves" + }, "recovery": { "change_recovery_confirm_button": "Confirmar nova chave de recuperação", "change_recovery_confirm_description": "Introduz a tua nova chave de recuperação abaixo para terminar. A tua chave antiga deixará de funcionar.", @@ -2567,7 +2623,6 @@ "unable_to_load_msisdns": "Não é possível carregar números de telefone", "username": "Nome de utilizador" }, - "image_thumbnails": "Mostrar pré-visualizações/miniaturas de imagens", "inline_url_previews_default": "Ativar pré-visualizações de URL embutidas por predefinição", "inline_url_previews_room": "Ativar pré-visualizações de URL por defeito para os participantes nesta sala", "inline_url_previews_room_account": "Ativar pré-visualizações URL para esta sala (só te afeta a ti)", @@ -2749,7 +2804,7 @@ "message_search_unsupported_web": "%(brand)s não é possível armazenar mensagens encriptadas em cache com segurança localmente durante a execução num navegador da Web. Usa %(brand)s Desktop para que as mensagens encriptadas apareçam nos resultados da pesquisa.", "record_session_details": "Grava o nome do cliente, a versão e o URL para reconhecer mais facilmente as sessões no gestor de sessões", "send_analytics": "Envia dados analíticos", - "strict_encryption": "Nunca envies mensagens encriptadas para sessões não verificadas a partir desta sessão" + "strict_encryption": "Enviar mensagens apenas a utilizadores verificados" }, "send_read_receipts": "Enviar recibos lidos", "send_read_receipts_unsupported": "O teu servidor não suporta a desativação do envio de recibos de leitura.", @@ -3008,6 +3063,7 @@ "view": "Visualiza a sala com o endereço indicado", "whois": "Apresenta informações sobre um utilizador" }, + "sliding_sync_legacy_no_longer_supported": "A Sliding Sync Legacy não é mais suportada: faça logout e login novamente para ativar a nova opção de Sliding Sync", "space": { "add_existing_room_space": { "create": "Queres acrescentar uma nova sala em vez disso?", @@ -3112,7 +3168,6 @@ "heading_without_query": "Pesquisar por", "join_button_text": "Aderir %(roomAddress)s", "keyboard_scroll_hint": "Utiliza para te deslocares", - "message_search_section_title": "Outras pesquisas", "other_rooms_in_space": "Outras salas em %(spaceName)s", "public_rooms_label": "Salas públicas", "public_spaces_label": "Espaços públicos", @@ -3122,7 +3177,6 @@ "result_may_be_hidden_privacy_warning": "Alguns resultados podem estar ocultos por motivos de privacidade", "result_may_be_hidden_warning": "Alguns resultados podem estar ocultos", "search_dialog": "Diálogo de pesquisa", - "search_messages_hint": "Para procurar mensagens, procura este ícone na parte superior de uma sala ", "spaces_title": "Espaços em que te encontras", "start_group_chat_button": "Inicia uma conversa de grupo" }, @@ -3171,9 +3225,7 @@ "threads_activity_centre": { "header": "Atividade de tópicos", "no_rooms_with_threads_notifs": "Ainda não tens salas com notificações de tópicos.", - "no_rooms_with_unread_threads": "Ainda não tens salas com tópicos não lidos.", - "release_announcement_description": "As notificações de tópicos foram movidas, encontre-as aqui a partir de agora.", - "release_announcement_header": "Centro de Actividades Tópicos" + "no_rooms_with_unread_threads": "Ainda não tens salas com tópicos não lidos." }, "time": { "about_day_ago": "há cerca de um dia", @@ -3385,6 +3437,7 @@ "left_reason": "%(targetName)s saiu da sala:%(reason)s", "no_change": "%(senderName)s não fez nenhuma alteração", "reject_invite": "%(targetName)s rejeitou o convite", + "reject_invite_reason": "%(targetName)s rejeitou o convite: %(reason)s", "remove_avatar": "%(senderName)s removeu a sua fotografia de perfil", "remove_name": "%(senderName)s removeu o seu nome de apresentação (%(oldDisplayName)s)", "set_avatar": "%(senderName)s colocou uma foto de perfil", @@ -3979,7 +4032,7 @@ "error_need_to_be_logged_in": "Você tem que estar logado.", "error_unable_start_audio_stream_description": "Não é possível iniciar a transmissão de áudio.", "error_unable_start_audio_stream_title": "Falha ao iniciar a transmissão ao vivo", - "modal_data_warning": "Os dados neste ecrã são partilhados com %(widgetDomain)s", + "modal_data_warning": "Os dados abaixo são partilhados com %(widgetDomain)s", "modal_title_default": "Widget Modal", "no_name": "Aplicação desconhecida", "open_id_permissions_dialog": { diff --git a/src/i18n/strings/pt_BR.json b/src/i18n/strings/pt_BR.json index ab70778c80..c0f38b3163 100644 --- a/src/i18n/strings/pt_BR.json +++ b/src/i18n/strings/pt_BR.json @@ -12,6 +12,16 @@ "one": "1 menção não lida." }, "recent_rooms": "Salas recentes", + "room_messsage_not_sent": "Abra a sala %(roomName)s com uma mensagem não enviada.", + "room_n_unread_invite": "Abra o convite da sala %(roomName)s.", + "room_n_unread_messages": { + "one": "Sala aberta %(roomName)s com 1 mensagem não lida.", + "other": "Sala aberta %(roomName)s com mensagens %(count)s não lidas." + }, + "room_n_unread_messages_mentions": { + "one": "Sala aberta %(roomName)s com 1 menção não lida.", + "other": "Sala aberta %(roomName)s com mensagens %(count)s não lidas, incluindo menções." + }, "room_name": "Sala %(name)s", "room_status_bar": "Barra de status da sala", "seek_bar_label": "Barra de busca de áudio", @@ -35,7 +45,7 @@ "click": "Clicar", "click_to_copy": "Clique para copiar", "close": "Fechar", - "collapse": "Colapsar", + "collapse": "Recolher", "complete": "Concluir", "confirm": "Confirmar", "continue": "Continuar", @@ -45,6 +55,8 @@ "create_a_room": "Criar uma sala", "create_account": "Criar Conta", "decline": "Recusar", + "decline_and_block": "Recusar e bloquear", + "decline_invite": "Recusar convite", "delete": "Excluir", "deny": "Negar", "disable": "Desativar", @@ -60,10 +72,11 @@ "explore_public_rooms": "Explorar salas públicas", "explore_rooms": "Explorar salas", "export": "Exportar", - "forward": "Avançar", + "forward": "Encaminhar", "go": "Próximo", "go_back": "Voltar", "got_it": "Ok, entendi", + "hide": "Ocultar", "hide_advanced": "Esconder configurações avançadas", "hold": "Pausar", "ignore": "Bloquear", @@ -80,12 +93,14 @@ "maximise": "Maximizar", "mention": "Mencionar", "minimise": "Minimizar", + "new_message": "Nova mensagem", "new_room": "Nova sala", "new_video_room": "Nova sala de vídeo", "next": "Próximo", "no": "Não", "ok": "Ok", "open": "Abrir", + "open_menu": "Abrir menu", "pause": "Pausar", "pin": "Alfinete", "play": "Reproduzir", @@ -100,6 +115,7 @@ "reply": "Responder", "reply_in_thread": "Responder no tópico", "report_content": "Denunciar conteúdo", + "report_room": "Reportar sala", "resend": "Reenviar", "reset": "Redefinir", "resume": "Retomar", @@ -109,6 +125,7 @@ "save": "Salvar", "search": "Buscar", "send_report": "Enviar relatório", + "set_avatar": "Definir foto do perfil", "share": "Compartilhar", "show": "Mostrar", "show_advanced": "Mostrar configurações avançadas", @@ -140,6 +157,7 @@ "view_message": "Ver mensagem", "view_source": "Ver código-fonte", "yes": "Sim", + "yes_dismiss": "Sim, dispensar", "zoom_in": "Aumentar zoom", "zoom_out": "Diminuir zoom" }, @@ -227,6 +245,7 @@ }, "misconfigured_body": "Entre em contato com o administrador do %(brand)s para verificar se há entradas inválidas ou duplicadas nas suas configurações.", "misconfigured_title": "O %(brand)s está mal configurado", + "mobile_create_account_title": "Você está prestes a criar uma conta em %(hsName)s", "msisdn_field_description": "Outros usuários podem convidá-lo para salas usando seus detalhes de contato", "msisdn_field_label": "Telefone", "msisdn_field_number_invalid": "Esse número de telefone não é válido, verifique e tente novamente", @@ -244,15 +263,40 @@ "phone_label": "Telefone", "phone_optional_label": "Número de celular (opcional)", "qr_code_login": { + "check_code_explainer": "Isso verificará se a conexão com seu outro dispositivo é segura.", + "check_code_heading": "Digite o número mostrado em seu outro dispositivo", "check_code_input_label": "Código de 2 dígitos", "check_code_mismatch": "Os números não coincidem", "completing_setup": "Concluindo a configuração do seu novo dispositivo", + "error_etag_missing": "Ocorreu um erro inesperado. Isso pode ser devido a uma extensão do navegador, servidor proxy ou configuração incorreta do servidor.", + "error_expired": "O login expirou. Por favor, tente novamente.", + "error_expired_title": "O login não foi concluído a tempo", + "error_insecure_channel_detected": "Não foi possível estabelecer uma conexão segura com o novo dispositivo. Seus dispositivos existentes ainda estão seguros e você não precisa se preocupar com eles.", "error_insecure_channel_detected_instructions": "E agora?", + "error_insecure_channel_detected_instructions_1": "Tente entrar no outro dispositivo novamente com um código QR, caso seja um problema de rede", + "error_insecure_channel_detected_instructions_2": "Se o problema persistir, tente uma rede Wi-Fi diferente ou use seus dados móveis em vez de Wi-Fi", + "error_insecure_channel_detected_instructions_3": "Se isso não funcionar, faça login manualmente", + "error_insecure_channel_detected_title": "Conexão não segura", + "error_other_device_already_signed_in": "Você não precisa fazer mais nada.", + "error_other_device_already_signed_in_title": "Seu outro dispositivo já está conectado", + "error_rate_limited": "Muitas tentativas em um curto espaço de tempo. Aguarde algum tempo antes de tentar novamente.", "error_unexpected": "Ocorreu um erro inesperado. A solicitação para conectar seu outro dispositivo foi cancelada.", + "error_unsupported_protocol": "Este dispositivo não suporta o login em outro dispositivo com um código QR.", + "error_unsupported_protocol_title": "Outro dispositivo não compatível", + "error_user_cancelled": "O login foi cancelado no outro dispositivo.", + "error_user_cancelled_title": "Solicitação de login cancelada", + "error_user_declined": "Você ou o provedor da conta recusaram a solicitação de login.", "error_user_declined_title": "Login recusado", + "follow_remaining_instructions": "Siga as instruções restantes", + "open_element_other_device": "Abra %(brand)s em seu outro dispositivo", + "point_the_camera": "Escaneie o código QR mostrado aqui", "scan_code_instruction": "Digitalize o código QR com outro dispositivo", "scan_qr_code": "Iniciar sessão com código QR", + "security_code": "Código de segurança", + "security_code_prompt": "Se solicitado, insira o código abaixo em seu outro dispositivo.", "select_qr_code": "Selecione \"%(scanQRCode)s”", + "unsupported_explainer": "Seu provedor de conta não suporta o login em um novo dispositivo com um código QR.", + "unsupported_heading": "Código QR não suportado", "waiting_for_device": "Aguardando o login do dispositivo" }, "register_action": "Criar Conta", @@ -322,7 +366,7 @@ "clear_data_description": "Apagar todos os dados desta sessão é uma ação permanente. Mensagens criptografadas serão perdidas, a não ser que as chaves delas tenham sido copiadas para o backup.", "clear_data_title": "Limpar todos os dados nesta sessão?" }, - "soft_logout_heading": "Você está desconectada/o", + "soft_logout_heading": "Você está desconectado", "soft_logout_intro_password": "Digite sua senha para entrar e recuperar o acesso à sua conta.", "soft_logout_intro_sso": "Entre e recupere o acesso à sua conta.", "soft_logout_intro_unsupported_auth": "Você não pôde se conectar na sua conta. Entre em contato com o administrador do servidor para obter mais informações.", @@ -341,6 +385,9 @@ "email_resend_prompt": "Não recebeu? Reenvie", "email_resent": "Reenviar!", "fallback_button": "Iniciar autenticação", + "mas_cross_signing_reset_cta": "Vá para sua conta", + "mas_cross_signing_reset_description": "Redefina sua identidade por meio do provedor da conta e, em seguida, volte e clique em “Tentar novamente”.", + "mas_cross_signing_reset_title": "Acesse sua conta para redefinir sua identidade", "msisdn": "Uma mensagem de texto foi enviada para %(msisdn)s", "msisdn_token_incorrect": "Token incorreto", "msisdn_token_prompt": "Por favor, entre com o código que está na mensagem:", @@ -370,25 +417,34 @@ "before_submitting": "Antes de enviar os relatórios, você deve criar um bilhete de erro no GitHub para descrever seu problema.", "collecting_information": "Coletando informação sobre a versão do app", "collecting_logs": "Coletando logs", - "create_new_issue": "Por favor, crie um novo bilhete de erro no GitHub para que possamos investigar esta falha.", + "create_new_issue": "Por favor, crie um novo ticket de erro no GitHub para que possamos investigar esta falha.", "description": "Os logs de depuração contêm dados de uso do aplicativo, incluindo seu nome de usuário, os IDs ou aliases das salas que você visitou, com quais elementos da IU você interagiu pela última vez e os nomes de usuários de outros usuários. Eles não contêm mensagens.", - "download_logs": "Baixar relatórios", - "downloading_logs": "Baixando relatórios", - "error_empty": "Por favor, diga-nos o que aconteceu de errado ou, ainda melhor, crie um bilhete de erro no GitHub que descreva o problema.", - "github_issue": "Bilhete de erro no GitHub", + "download_logs": "Baixar logs", + "downloading_logs": "Baixando logs", + "error_empty": "Por favor, diga-nos o que aconteceu de errado ou, ainda melhor, crie um ticket de erro no GitHub que descreva o problema.", + "failed_download_logs": "Falha ao baixar os logs: ", + "failed_send_logs_causes": { + "disallowed_app": "Seu relatório de bug foi rejeitado. O servidor rageshake não suporta esse aplicativo.", + "rejected_generic": "Seu relatório de bug foi rejeitado. O servidor rageshake rejeitou o conteúdo do relatório devido a uma política.", + "rejected_recovery_key": "Seu relatório de bug foi rejeitado por motivos de segurança, pois continha uma chave de recuperação.", + "rejected_version": "Seu relatório de bug foi rejeitado porque a versão que você está executando é muito antiga.", + "server_unknown_error": "O servidor rageshake encontrou um erro desconhecido e não conseguiu lidar com o relatório.", + "unknown_error": "Falha ao enviar logs." + }, + "github_issue": "Ticket de erro no GitHub", "introduction": "Se você enviou um bug pelo GitHub, os logs de depuração podem nos ajudar a rastrear o problema. ", - "log_request": "Para nos ajudar a evitar isso no futuro, envie-nos os relatórios.", - "logs_sent": "Relatórios enviados", + "log_request": "Para nos ajudar a evitar que isso aconteça no futuro, envie-nos logs.", + "logs_sent": "Logs enviados", "matrix_security_issue": "Para relatar um problema de segurança relacionado à tecnologia Matrix, leia a Política de Divulgação de Segurança da Matrix.org.", - "preparing_download": "Preparando os relatórios para download", - "preparing_logs": "Preparando para enviar relatórios", - "send_logs": "Enviar relatórios", - "submit_debug_logs": "Enviar relatórios de erros", + "preparing_download": "Preparando para baixar logs", + "preparing_logs": "Preparando para enviar logs", + "send_logs": "Enviar logs", + "submit_debug_logs": "Enviar logs de depuração", "textarea_label": "Notas", "thank_you": "Obrigado!", "title": "Relato de Erros", "unsupported_browser": "Lembrete: seu navegador não é compatível; portanto, sua experiência pode ser imprevisível.", - "uploading_logs": "Enviando relatórios", + "uploading_logs": "Enviando logs", "waiting_for_server": "Aguardando a resposta do servidor" }, "cannot_invite_without_identity_server": "Não é possível convidar usuários por e-mail sem um servidor de identidade. Você pode se conectar a um em “Configurações”.", @@ -414,6 +470,7 @@ "access_token": "Símbolo de acesso", "accessibility": "Acessibilidade", "advanced": "Avançado", + "all_chats": "Todos os bate-papos", "analytics": "Análise", "and_n_others": { "one": "e um outro...", @@ -449,7 +506,7 @@ "forward_message": "Encaminhar", "general": "Geral", "go_to_settings": "Ir para as configurações", - "guest": "Convidada(o)", + "guest": "Convidado", "help": "Ajuda", "historical": "Histórico", "home": "Início", @@ -467,8 +524,10 @@ "matrix": "Matrix", "message": "Mensagem", "message_layout": "Aparência da mensagem", + "message_timestamp_invalid": "Data/hora inválido", "microphone": "Microfone", "model": "Modelo", + "moderation_and_safety": "Moderação e segurança", "modern": "Moderno", "mute": "Mudo", "n_members": { @@ -504,10 +563,13 @@ "qr_code": "Código QR", "random": "Aleatório", "reactions": "Reações", + "recommended": "Recomendado", "report_a_bug": "Reportar um erro", "room": "Sala", "room_name": "Nome da sala", "rooms": "Salas", + "save": "Salvar", + "saved": "Salvo", "saving": "Salvando...", "secure_backup": "Backup online", "select_all": "Selecionar tudo", @@ -534,6 +596,7 @@ "unnamed_room": "Sala sem nome", "unnamed_space": "Espaço sem nome", "unverified": "Não verificado", + "updating": "Atualizando…", "user": "Usuário", "user_avatar": "Foto de perfil", "username": "Nome de usuário", @@ -554,9 +617,11 @@ "notification_a11y": "Notificação do preenchimento automático", "notification_description": "Notificação da sala", "room_a11y": "Preenchimento automático de sala", + "space_a11y": "Preenchimento automático de espaço", "user_a11y": "Preenchimento automático de usuário", "user_description": "Usuários" }, + "close_sticker_picker": "Ocultar stickers", "edit_composer_label": "Editar mensagem", "format_bold": "Negrito", "format_code_block": "Bloco de código", @@ -571,12 +636,15 @@ "format_strikethrough": "Riscado", "format_underline": "Sublinhar", "format_unordered_list": "Lista com marcadores", + "formatting_toolbar_label": "Formatação", "link_modal": { "link_field_label": "Ligação", "text_field_label": "Texto", "title_create": "Criar uma ligação", "title_edit": "Editar ligação" }, + "mode_plain": "Ocultar formatação", + "mode_rich_text": "Mostrar formatação", "no_perms_notice": "Você não tem permissão para digitar nesta sala", "placeholder": "Digite uma mensagem…", "placeholder_encrypted": "Digite uma mensagem criptografada…", @@ -584,6 +652,7 @@ "placeholder_reply_encrypted": "Digite sua resposta criptografada…", "placeholder_thread": "Responder ao tópico…", "placeholder_thread_encrypted": "Responder ao tópico criptografado…", + "poll_button": "Enquete", "poll_button_no_perms_description": "Você não tem permissão para iniciar enquetes nesta sala.", "poll_button_no_perms_title": "Permissão necessária", "replying_title": "Em resposta a", @@ -592,8 +661,12 @@ "send_button_title": "Enviar mensagem", "send_button_voice_message": "Enviar uma mensagem de voz", "send_voice_message": "Enviar uma mensagem de voz", - "stop_voice_message": "Parar a gravação" + "stop_voice_message": "Parar a gravação", + "voice_message_button": "Mensagem de voz" }, + "console_dev_note": "Se você sabe o que está fazendo, o Element é de código aberto, não deixe de conferir nosso GitHub (https://github.com/vector-im/element-web/) e contribuir!", + "console_scam_warning": "Se alguém lhe disse para copiar/colar algo aqui, há uma grande probabilidade de você estar sendo enganado!", + "console_wait": "Aguarde!", "create_room": { "action_create_room": "Criar sala", "action_create_video_room": "Criar sala de vídeo", @@ -626,22 +699,45 @@ "add_details_prompt": "Adicione alguns detalhes para ajudar as pessoas a reconhecê-lo.", "add_details_prompt_2": "Você pode mudá-los a qualquer instante.", "add_existing_rooms_description": "Escolha salas ou conversas para adicionar. Este é apenas um espaço para você, ninguém será informado. Você pode adicionar mais posteriormente.", + "add_existing_rooms_heading": "O que você deseja organizar?", "address_label": "Endereço", "address_placeholder": "e.g. meu-espaco", + "creating": "Criando...", + "creating_rooms": "Criando salas...", + "done_action": "Ir para o meu espaço", + "done_action_first_room": "Ir para minha primeira sala", + "explainer": "Os espaços são uma nova forma de agrupar salas e pessoas. Que tipo de espaço você quer criar? Você pode mudar isso mais tarde.", "failed_create_initial_rooms": "Falha ao criar salas de espaço iniciais", + "failed_invite_users": "Falha ao convidar os seguintes usuários para seu espaço: %(csvUsers)s", "invite_teammates_by_username": "Convidar por nome de usuário", + "invite_teammates_description": "Certifique-se de que as pessoas certas tenham acesso. Você pode convidar mais tarde.", + "invite_teammates_heading": "Convide seus colegas de equipe", "inviting_users": "Convidando...", "label": "Criar um espaço", "name_required": "Por favor entre o nome do espaço", "personal_space": "Apenas eu", - "private_description": "Somente convite, melhor para si mesmo(a) ou para equipes", + "personal_space_description": "Um espaço privado para organizar suas salas", + "private_description": "Somente para convidados, melhor para você ou para equipes", "private_heading": "O seu espaço privado", + "private_personal_description": "Certifique-se de que as pessoas certas tenham acesso a %(name)s", + "private_personal_heading": "Com quem você está trabalhando?", + "private_space": "Eu e meus colegas de equipe", + "private_space_description": "Um espaço privado para você e seus colegas de equipe", "public_description": "Abra espaços para todos, especialmente para comunidades", "public_heading": "O seu espaço público", "search_public_button": "Pesquise espaços públicos", + "setup_rooms_community_description": "Vamos criar uma sala para cada um deles.", + "setup_rooms_community_heading": "O que você gostaria de discutir em %(spaceName)s?", + "setup_rooms_description": "Você também pode adicionar mais tarde, incluindo os já existentes.", + "setup_rooms_private_description": "Criaremos salas para cada um deles.", + "setup_rooms_private_heading": "Em quais projetos sua equipe está trabalhando?", + "share_description": "No momento é só você, será ainda melhor com outros.", + "share_heading": "Compartilhar %(name)s", "skip_action": "Ignorar por enquanto", + "subspace_adding": "Adicionando...", "subspace_beta_notice": "Adicionar um espaço à um espaço que você gerencia.", "subspace_dropdown_title": "Criar um espaço", + "subspace_existing_space_prompt": "Em vez disso, quer adicionar um espaço existente?", "subspace_join_rule_invite_description": "Apenas convidados poderão encontrar e entrar neste espaço.", "subspace_join_rule_invite_only": "Espaço privado (apenas com convite)", "subspace_join_rule_label": "Visibilidade do Espaço", @@ -653,12 +749,58 @@ "twemoji": "A arte do emoji Twemoji é © Twitter, Inc e outros colaboradores usados sob os termos da CC-BY 4.0. ", "twemoji_colr": "A fonte twemoji-colr é © Mozilla Foundation usada sob os termos de Apache 2.0." }, + "decline_invitation_dialog": { + "confirm": "Tem certeza de que deseja recusar o convite para participar de \"%(roomName)s“?", + "ignore_user_help": "Você não verá nenhuma mensagem ou convite para salas desse usuário.", + "reason_description": "Descreva o motivo da denúncia da sala.", + "report_room_description": "Denuncie esta sala ao provedor da sua conta.", + "title": "Recusar convite" + }, + "desktop_default_device_name": "%(brand)sÁrea de trabalho: %(platformName)s", "devtools": { "active_widgets": "Widgets ativados", "category_other": "Outros", "category_room": "Sala", "caution_colon": "Atenção:", "client_versions": "Versões do cliente", + "crypto": { + "4s_public_key_in_account_data": "nos dados de conta", + "4s_public_key_not_in_account_data": "não encontrado", + "4s_public_key_status": "Chave pública do armazenamento secreto:", + "backup_key_cached": "armazenado localmente", + "backup_key_cached_status": "Backup da chave em cache:", + "backup_key_not_stored": "não armazenado", + "backup_key_stored": "em armazenamento secreto", + "backup_key_stored_status": "Backup da chave armazenada:", + "backup_key_unexpected_type": "tipo inesperado", + "backup_key_well_formed": "bem formado", + "cross_signing": "Assinatura cruzada", + "cross_signing_cached": "armazenado localmente", + "cross_signing_not_ready": "A assinatura cruzada não está configurada.", + "cross_signing_private_keys_in_storage": "em armazenamento secreto", + "cross_signing_private_keys_in_storage_status": "Chaves privadas de assinatura cruzada:", + "cross_signing_private_keys_not_in_storage": "não encontrado no armazenamento", + "cross_signing_public_keys_on_device": "na memória", + "cross_signing_public_keys_on_device_status": "Chaves públicas de assinatura cruzada:", + "cross_signing_ready": "A assinatura cruzada está pronta para ser usada.", + "cross_signing_status": "Status de assinatura cruzada:", + "cross_signing_untrusted": "Sua conta tem uma identidade de assinatura cruzada no armazenamento secreto, mas ela ainda não é confiável para esta sessão.", + "crypto_not_available": "O módulo criptográfico não está disponível", + "key_backup_active_version": "Versão de backup ativo:", + "key_backup_active_version_none": "Nenhuma", + "key_backup_inactive_warning": "Suas chaves não estão sendo copiadas nesta sessão.", + "key_backup_latest_version": "Versão mais recente do backup no servidor:", + "key_storage": "Armazenamento de chaves", + "master_private_key_cached_status": "Chave privada principal:", + "not_found": "não encontrado", + "not_found_locally": "não encontrado localmente", + "secret_storage_not_ready": "não está pronto", + "secret_storage_ready": "pronto", + "secret_storage_status": "Armazenamento secreto:", + "self_signing_private_key_cached_status": "Chave privada auto-assinada:", + "title": "Criptografia de ponta a ponta", + "user_signing_private_key_cached_status": "Chave privada de assinatura do usuário:" + }, "developer_mode": "Modo desenvolvedor", "developer_tools": "Ferramentas do desenvolvedor", "edit_setting": "Editar configuração", @@ -678,6 +820,8 @@ "id": "ID: ", "invalid_json": "Não parece um JSON válido.", "level": "Nível", + "low_bandwidth_mode": "Modo de baixa largura de banda", + "low_bandwidth_mode_description": "Requer servidor doméstico compatível.", "main_timeline": "Linha do tempo principal", "no_receipt_found": "Nenhum recibo encontrado", "notification_state": "O estado da notificação é%(notificationState)s", @@ -712,6 +856,9 @@ "setting_colon": "Configuração:", "setting_definition": "Definição da configuração:", "setting_id": "ID da configuração", + "settings": { + "elementCallUrl": "URL de chamada do Element" + }, "settings_explorer": "Explorador de configurações", "show_hidden_events": "Mostrar eventos ocultos nas conversas", "spaces": { @@ -722,6 +869,7 @@ "thread_root_id": "ID raiz do tópico: %(threadRootId)s", "threads_timeline": "Linha do tempo dos tópicos", "title": "Ferramentas de desenvolvimento", + "toggle_event": "alternar evento", "toolbox": "Ferramentas", "use_at_own_risk": "Esta interface de usuário NÃO verifica os tipos de valores. Use por sua conta e risco.", "user_read_up_to": "O usuário leu até: ", @@ -738,6 +886,7 @@ "values_explicit_this_room_colon": "Valores em níveis explícitos nessa sala:", "view_servers_in_room": "Exibir servidores na sala", "view_source_decrypted_event_source": "Fonte de evento descriptografada", + "view_source_decrypted_event_source_unavailable": "Fonte descriptografada indisponível", "widget_screenshots": "Ativar capturas de tela do widget em widgets suportados" }, "dialog_close_label": "Fechar caixa de diálogo", @@ -762,17 +911,13 @@ "empty_room_was_name": "Sala vazia (era %(oldName)s)", "encryption": { "access_secret_storage_dialog": { + "alternatives": "Se você tiver uma chave de segurança ou frase de segurança, isso também funcionará.", "key_validation_text": { - "invalid_security_key": "Chave de Segurança inválida", - "recovery_key_is_correct": "Muito bem!", - "wrong_file_type": "Tipo errado de arquivo", - "wrong_security_key": "Chave de Segurança errada" + "wrong_security_key": "A chave de recuperação que você inseriu não está correta." }, + "privacy_warning": "Certifique-se de que ninguém possa ver essa tela!", "restoring": "Restaurando chaves do backup", - "security_key_title": "Chave de Segurança", - "security_phrase_incorrect_error": "Não foi possível acessar o armazenamento secreto. Verifique se você digitou a Frase de Segurança correta.", - "security_phrase_title": "Frase de segurança", - "use_security_key_prompt": "Use sua Chave de Segurança para continuar." + "security_key_title": "Chave de recuperação" }, "bootstrap_title": "Configurar chaves", "cancel_entering_passphrase_description": "Tem certeza que quer cancelar a introdução da frase de senha?", @@ -785,14 +930,18 @@ "cross_signing_user_normal": "Você não confirmou este usuário.", "cross_signing_user_verified": "Você confirmou este usuário. Este usuário confirmou todas as próprias sessões.", "cross_signing_user_warning": "Este usuário não confirmou todas as próprias sessões.", + "enter_recovery_key": "Insira a chave de recuperação", "event_shield_reason_authenticity_not_guaranteed": "A autenticidade desta mensagem criptografada não pode ser garantida neste aparelho.", "event_shield_reason_mismatched_sender_key": "Criptografada por uma sessão não confirmada", "event_shield_reason_unknown_device": "Criptografado por um dispositivo desconhecido ou excluído.", "event_shield_reason_unsigned_device": "Criptografado por um dispositivo não verificado por seu proprietário.", "event_shield_reason_unverified_identity": "Criptografado por um usuário não verificado.", "export_unsupported": "O seu navegador não suporta as extensões de criptografia necessárias", + "forgot_recovery_key": "Esqueceu a chave de recuperação?", "import_invalid_keyfile": "Não é um arquivo de chave válido do %(brand)s", "import_invalid_passphrase": "Falha ao checar a autenticação: senha incorreta?", + "key_storage_out_of_sync": "Seu armazenamento de chaves está fora de sincronia.", + "key_storage_out_of_sync_description": "Confirme sua chave de recuperação para manter o acesso ao seu armazenamento de chaves e histórico de mensagens.", "messages_not_secure": { "cause_1": "Seu servidor local", "cause_2": "O servidor doméstico do usuário que você está verificando está conectado", @@ -807,18 +956,27 @@ "title": "Nova opção de recuperação", "warning": "Se você não definiu a nova opção de recuperação, um invasor pode estar tentando acessar sua conta. Altere a senha da sua conta e defina uma nova opção de recuperação imediatamente nas Configurações." }, + "pinned_identity_changed": "A identidade de %(displayName)s (%(userId)s) parece ter mudado. Saiba mais", + "pinned_identity_changed_no_displayname": "A identidade de %(userId)s parece ter mudado. Saiba mais", "recovery_method_removed": { "description_1": "Esta sessão detectou que a sua Frase de Segurança e a chave para mensagens seguras foram removidas.", "description_2": "Se você fez isso acidentalmente, você pode configurar Mensagens Seguras nesta sessão, o que vai re-criptografar o histórico de mensagens desta sessão com uma nova opção de recuperação.", "title": "Opção de recuperação removida", "warning": "Se você não excluiu a opção de recuperação, um invasor pode estar tentando acessar sua conta. Altere a senha da sua conta e defina imediatamente uma nova opção de recuperação nas Configurações." }, + "reset_all_button": "Esqueceu ou perdeu todos os métodos de recuperação? Redefinir tudo", + "set_up_recovery": "Configurar a recuperação", + "set_up_recovery_later": "Agora não", + "set_up_recovery_toast_description": "Gere uma chave de recuperação que possa ser usada para restaurar seu histórico de mensagens criptografadas caso você perca o acesso aos seus dispositivos.", "set_up_toast_description": "Proteja-se contra a perda de acesso a mensagens e dados criptografados", "set_up_toast_title": "Configurar o backup online", "setup_secure_backup": { "explainer": "Faça o backup das suas chaves antes de sair, para evitar perdê-las." }, + "turn_on_key_storage": "Ativar o armazenamento de chaves", + "turn_on_key_storage_description": "Armazene sua identidade criptográfica e as chaves de mensagem com segurança no servidor. Isso permitirá que você visualize seu histórico de mensagens em qualquer novo dispositivo.", "udd": { + "interactive_verification_button": "Verificar interativamente por emoji", "other_ask_verify_text": "Peça a este usuário para confirmar a sessão dele, ou confirme-a manualmente abaixo.", "other_new_session_text": "%(name)s (%(userId)s) entrou em uma nova sessão sem confirmá-la:", "own_ask_verify_text": "Confirme suas outras sessões usando uma das opções abaixo.", @@ -830,7 +988,6 @@ "accepting": "Aceitando…", "after_new_login": { "device_verified": "Dispositivo verificado", - "reset_confirmation": "Você realmente deseja redefinir as chaves de verificação?", "skip_verification": "Ignorar a verificação por enquanto", "unable_to_verify": "Não foi possível verificar este dispositivo", "verify_this_device": "Verifique este dispositivo" @@ -865,6 +1022,7 @@ "qr_reciprocate_same_shield_device": "Quase lá! O seu outro dispositivo está mostrando o mesmo escudo?", "qr_reciprocate_same_shield_user": "Quase lá! Este escudo também aparece para %(displayName)s?", "request_toast_accept": "Verificar sessão", + "request_toast_accept_user": "Verificar usuário", "request_toast_decline_counter": "Ignorar (%(counter)s)", "request_toast_detail": "%(deviceId)s de %(ip)s", "reset_proceed_prompt": "Prosseguir com a reposição", @@ -893,11 +1051,13 @@ "verification_description": "Verifique sua identidade para acessar mensagens criptografadas e provar sua identidade para outras pessoas.", "verification_dialog_title_device": "Verifique outro dispositivo", "verification_dialog_title_user": "Solicitação de confirmação", + "verification_skip_warning": "Sem verificar, você não terá acesso a todas as suas mensagens e poderá aparecer como não confiável para outras pessoas.", "verification_success_with_backup": "O seu novo dispositivo agora está verificado. Ele tem acesso às suas mensagens criptografadas, e outros usuários o verão como confiável.", + "verification_success_without_backup": "Seu novo dispositivo agora foi verificado. Outros usuários o verão como confiável.", "verify_emoji": "Confirmar por emojis", "verify_emoji_prompt": "Confirmar comparando emojis únicos.", "verify_emoji_prompt_qr": "Se você não consegue escanear o código acima, confirme comparando emojis únicos.", - "verify_reset_warning_1": "A redefinição de suas chaves de verificação não pode ser desfeita. Após a redefinição, você não terá acesso às mensagens criptografadas antigas e todos os amigos que tenham verificado você anteriormente verão avisos de segurança até que você verifique novamente com eles.", + "verify_later": "Vou verificar mais tarde", "verify_using_device": "Verifique com outro dispositivo", "verify_using_key": "Verifique com a chave de segurança", "verify_using_key_or_phrase": "Verificar com chave de segurança ou frase", @@ -906,8 +1066,12 @@ "waiting_other_device_details": "Esperando que você verifique em seu outro dispositivo, %(deviceName)s (%(deviceId)s)...", "waiting_other_user": "Aguardando %(displayName)s confirmar…" }, + "verification_requested_toast_title": "Verificação solicitada", + "verified_identity_changed": "A identidade verificada de %(displayName)s (%(userId)s) foi alterada. Saiba mais", + "verified_identity_changed_no_displayname": "A identidade verificada de %(userId)s foi alterada. Saiba mais", "verify_toast_description": "Outras(os) usuárias(os) podem não confiar nela", - "verify_toast_title": "Confirmar esta sessão" + "verify_toast_title": "Confirmar esta sessão", + "withdraw_verification_action": "Retirar verificação" }, "error": { "admin_contact": "Por favor, entre em contato com o seu administrador de serviços para continuar usando este serviço.", @@ -916,9 +1080,11 @@ "cannot_load_config": "Incapaz de carregar arquivo de configuração: por favor atualize a página para tentar de novo.", "connection": "Ocorreu um problema de comunicação com o servidor local. Tente novamente mais tarde.", "dialog_description_default": "Ocorreu um erro.", + "download_media": "Falha ao baixar a mídia de origem, nenhum URL de origem foi encontrado", "edit_history_unsupported": "O seu servidor local não parece suportar este recurso.", "failed_copy": "Não foi possível copiar", "hs_blocked": "Este servidor local foi bloqueado pelo seu administrador.", + "invalid_configuration_mixed_server": "Configuração inválida: um default_hs_url não pode ser especificado junto com default_server_name ou default_server_config", "invalid_configuration_no_server": "Configuração inválida: nenhum servidor default especificado.", "invalid_json": "Sua configuração do Element contém JSON inválido. Por favor corrija o problema e recarregue a página.", "invalid_json_detail": "A mensagem do parser é: %(message)s", @@ -940,11 +1106,15 @@ "storage_evicted_description_1": "Alguns dados de sessão, incluindo chaves de mensagens criptografadas, estão faltando. Desconecte-se e entre novamente para resolver isso, o que restaurará as chaves do backup.", "storage_evicted_description_2": "O seu navegador provavelmente removeu esses dados quando o espaço de armazenamento ficou insuficiente.", "storage_evicted_title": "Dados de sessão ausentes", + "sync": "Não foi possível conectar ao Homeserver. Tentando novamente…", "tls": "Não foi possível conectar ao Servidor de Base. Por favor, confira sua conectividade à internet, garanta que o certificado SSL do Servidor de Base é confiável, e que uma extensão do navegador não esteja bloqueando as requisições de rede.", "unknown": "Erro desconhecido", "unknown_error_code": "código de erro desconhecido", "update_power_level": "Não foi possível alterar o nível de permissão" }, + "error_app_open_in_another_tab": "Mude para a outra guia para se conectar em %(brand)s. Essa guia agora pode ser fechada.", + "error_app_open_in_another_tab_title": "%(brand)s está conectado em outra guia", + "error_app_opened_in_another_window": "%(brand)s está aberto em outra janela. Clique em \"%(label)s\" para usar %(brand)s aqui e desconectar a outra janela.", "error_database_closed_description": { "for_desktop": "Seu disco pode estar cheio. Por favor, libere algum espaço e recarregue.", "for_web": "Se você limpou os dados de navegação, esta mensagem é esperada. %(brand)s também pode estar aberto em outra guia ou seu disco está cheio. Por favor, libere algum espaço e recarregue" @@ -981,10 +1151,20 @@ "you": "Você reagiu %(reaction)s a %(message)s" }, "m.sticker": "%(senderName)s: %(stickerName)s", - "m.text": "%(senderName)s: %(message)s" + "m.text": "%(senderName)s: %(message)s", + "prefix": { + "audio": "Áudio", + "file": "Arquivo", + "image": "Imagem", + "poll": "Enquete", + "video": "Vídeo" + }, + "preview": "%(prefix)s: %(preview)s" }, "export_chat": { "cancelled": "Exportação cancelada", + "cancelled_detail": "A exportação foi cancelada com sucesso", + "confirm_stop": "Tem certeza de que deseja parar de exportar seus dados? Se o fizer, terá de começar de novo.", "creating_html": "Criando HTML...", "creating_output": "Criando saída...", "creator_summary": "%(creatorName)s criou esta sala.", @@ -997,6 +1177,7 @@ "one": "%(count)s evento exportado em %(seconds)s segundos", "other": "%(count)s eventos exportados em %(seconds)s segundos" }, + "exporting_your_data": "Exportando seus dados", "fetched_n_events": { "one": "Buscado %(count)s evento até agora", "other": "Buscado %(count)s eventos até agora" @@ -1011,6 +1192,7 @@ }, "fetching_events": "Buscando eventos...", "file_attached": "Arquivo Anexado", + "format": "Formato", "from_the_beginning": "Do começo", "generating_zip": "Gerando um ZIP", "html": "HTML", @@ -1025,11 +1207,15 @@ "num_messages_min_max": "Número de mensagens pode ser apenas um número entre %(min)s e %(max)s", "number_of_messages": "Especifique um número de mensagens", "previous_page": "Grupo anterior de mensagens", + "processing": "Processando...", "processing_event_n": "Processando evento %(number)s de %(total)s", "select_option": "Selecione uma das opções abaixo para exportar bate-papos da sua linha do tempo", "size_limit": "Limite de Tamanho", "size_limit_min_max": "O tamanho pode ser apenas um número entre %(min)s MB e %(max)s MB", + "size_limit_postfix": "MB", "starting_export": "Iniciando exportação…", + "successful": "Exportação bem-sucedida", + "successful_detail": "Sua exportação foi bem-sucedida. Encontre na sua pasta de Downloads.", "text": "Texto Simples", "title": "Exportar bate-papo", "topic": "Tópico: %(topic)s", @@ -1040,7 +1226,9 @@ "can_contact_label": "Vocês podem me contactar se tiverem quaisquer perguntas subsequentes", "comment_label": "Comentário", "existing_issue_link": "Por favor, consulte os erros conhecidos no Github antes de enviar o seu. Se ninguém tiver mencionado o seu erro, informe-nos sobre um erro novo .", - "pro_type": "DICA: se você nos informar um erro, envie relatórios de erro para nos ajudar a rastrear o problema.", + "may_contact_label": "Você pode entrar em contato comigo se quiser acompanhar ou me deixar testar as próximas ideias", + "platform_username": "Sua plataforma e nome de usuário serão registrados para nos ajudar a usar seu feedback o máximo possível.", + "pro_type": "DICA: se você nos informar um erro, envie relatórios de erro para nos ajudar a rastrear o problema.", "send_feedback_action": "Enviar comentário", "sent": "Comentário enviado" }, @@ -1052,7 +1240,9 @@ }, "forward": { "filter_placeholder": "Procurar por salas ou pessoas", + "message_preview_heading": "Pré-visualização da mensagem", "no_perms_title": "Você não tem permissão para fazer isso", + "open_room": "Sala aberta", "send_label": "Enviar", "sending": "Enviando", "sent": "Enviado" @@ -1061,6 +1251,7 @@ "change": "Alterar o servidor de identidade", "change_prompt": "Desconectar-se do servidor de identidade e conectar-se em em vez disso?", "change_server_prompt": "Se você não quiser usar para descobrir e ser detectável pelos contatos existentes, digite outro servidor de identidade abaixo.", + "changed": "Seu servidor de identidade foi alterado", "checking": "Verificando servidor", "description_connected": "No momento, você está usando para descobrir e ser descoberto pelos contatos existentes que você conhece. Você pode alterar seu servidor de identidade abaixo.", "description_disconnected": "No momento, você não está usando um servidor de identidade. Para descobrir e ser descoberto pelos contatos existentes, adicione um abaixo.", @@ -1092,18 +1283,34 @@ "other": "Em %(spaceName)s e %(count)s outros espaços." }, "incompatible_browser": { - "title": "Browser insuportado" + "continue": "Continuar mesmo assim", + "description": "%(brand)s usa alguns recursos do navegador que não estão disponíveis no seu navegador atual. %(detail)s", + "detail_can_continue": "Se você continuar, alguns recursos podem parar de funcionar e há o risco de você perder dados no futuro.", + "detail_no_continue": "Tente atualizar este navegador se não estiver usando a versão mais recente e tente novamente.", + "learn_more": "Saber mais", + "linux": "Linux", + "macos": "Mac", + "supported_browsers": "", + "title": "Browser insuportado", + "use_desktop_heading": "Use %(brand)s Desktop em vez disso", + "use_mobile_heading": "Em vez disso %(brand)s, use no celular", + "use_mobile_heading_after_desktop": "Ou use nosso aplicativo móvel", + "windows_64bit": "Windows (64 bits)", + "windows_arm_64bit": "Windows (ARM 64 bits)" }, "info_tooltip_title": "Informação", "integration_manager": { + "connecting": "Conectando-se ao gerenciador de integração...", "error_connecting": "Ou o gerenciador de integrações está indisponível, ou ele não conseguiu acessar o seu servidor.", "error_connecting_heading": "Não foi possível conectar ao gerenciador de integrações", "explainer": "O gerenciador de integrações recebe dados de configuração e pode modificar widgets, enviar convites para salas e definir níveis de permissão em seu nome.", "manage_title": "Gerenciar integrações", + "toggle_label": "Habilitar o gerenciador de integração", "use_im": "Use o gerenciador de integrações para gerenciar bots, widgets e pacotes de figurinhas.", "use_im_default": "Use o gerenciador de integrações em (%(serverName)s) para gerenciar bots, widgets e pacotes de figurinhas." }, "integrations": { + "disabled_dialog_description": "Habilite '%(manageIntegrations)s' em Configurações para fazer isto.", "disabled_dialog_title": "As integrações estão desativadas", "impossible_dialog_description": "Seu %(brand)s não permite que você use o gerenciador de integrações para fazer isso. Entre em contato com o administrador.", "impossible_dialog_title": "As integrações não estão permitidas" @@ -1130,9 +1337,11 @@ "error_permissions_space": "Você não tem permissão para convidar pessoas para este espaço.", "error_profile_undisclosed": "O usuário pode ou não existir", "error_transfer_multiple_target": "Uma chamada só pode ser transferida para um único usuário.", + "error_unfederated_room": "Esta sala não é federada. Você não pode convidar pessoas de servidores externos.", + "error_unfederated_space": "Esse espaço não é federado. Você não pode convidar pessoas de servidores externos.", "error_unknown": "Erro de servidor desconhecido", "error_user_not_found": "O usuário não existe", - "error_version_unsupported_room": "O servidor desta(e) usuária(o) não suporta a versão desta sala.", + "error_version_unsupported_room": "O servidor doméstico do usuário não é compatível com a versão da sala.", "error_version_unsupported_space": "O servidor doméstico do usuário não é compatível com a versão do espaço.", "failed_generic": "A operação falhou", "failed_title": "Falha ao enviar o convite", @@ -1174,6 +1383,9 @@ "activate_button": "Apertar no botão selecionado", "alt": "Alt", "autocomplete_cancel": "Cancelar o preenchimento automático", + "autocomplete_force": "Forçar conclusão", + "autocomplete_navigate_next": "Próxima sugestão de preenchimento automático", + "autocomplete_navigate_prev": "Sugestão anterior de preenchimento automático", "backspace": "Backspace", "cancel_reply": "Cancelar resposta à mensagem", "category_autocomplete": "Preencher automaticamente", @@ -1181,13 +1393,17 @@ "category_navigation": "Navegação", "category_room_list": "Lista de salas", "close_dialog_menu": "Fechar caixa de diálogo ou menu", + "composer_jump_end": "Ir para o final do compositor", + "composer_jump_start": "Ir para o início do compositor", "composer_navigate_next_history": "Navegue para a próxima mensagem no histórico do compositor", "composer_navigate_prev_history": "Navegue até a mensagem anterior no histórico do compositor", "composer_new_line": "Nova linha", "composer_redo": "Refazer edição", "composer_toggle_bold": "Negrito", + "composer_toggle_code_block": "Alternar para bloco de código", "composer_toggle_italics": "Itálico", - "composer_toggle_quote": "Citar", + "composer_toggle_link": "Alternar para link", + "composer_toggle_quote": "Alternar para citação", "composer_undo": "Desfazer edição", "control": "Ctrl", "dismiss_read_marker_and_jump_bottom": "Ignorar o marcador de leitura e ir para o final", @@ -1200,43 +1416,74 @@ "jump_last_message": "Ir para a última mensagem", "jump_room_search": "Ir para a pesquisa de salas", "jump_to_read_marker": "Ir para a mensagem não lida mais antiga", + "keyboard_shortcuts_tab": "Abra esta guia de configurações", "navigate_next_history": "Próxima sala ou espaço visitado recentemente", + "navigate_next_message_edit": "Navegar até a próxima mensagem para editar", "navigate_prev_history": "Sala ou espaço visitado recentemente", + "navigate_prev_message_edit": "Navegar até a mensagem anterior para editar", + "next_landmark": "Ir para o próximo ponto de referência", + "next_room": "Próxima sala ou mensagem privada", + "next_unread_room": "Próxima sala não lida ou mensagem direta", "number": "[número]", "open_user_settings": "Abrir as configurações do usuário", "page_down": "Página para baixo", "page_up": "Página para cima", + "prev_landmark": "Ir para o ponto de referência anterior", + "prev_room": "Sala anterior ou mensagem privada", + "prev_unread_room": "Sala anterior não lida ou mensagem privada", "room_list_collapse_section": "Esconder seção da lista de salas", "room_list_expand_section": "Mostrar seção da lista de salas", + "room_list_navigate_down": "Navegar para baixo na lista de salas", + "room_list_navigate_up": "Navegar para cima na lista de salas", "room_list_select_room": "Selecionar sala da lista de salas", + "scroll_down_timeline": "Rola para baixo no histórico", + "scroll_up_timeline": "Rola para cima no histórico", "search": "Pesquisar (deve estar ativado)", "send_sticker": "Enviar uma figurinha", "shift": "Shift", "space": "Barra de espaço", "switch_to_space": "Mudar para o espaço por número", + "toggle_hidden_events": "Ativar a visibilidade de eventos ocultos", "toggle_microphone_mute": "Ativar/desativar som do microfone", "toggle_right_panel": "Alternar o painel na direita", + "toggle_space_panel": "Alternar para painel de espaço", "toggle_top_left_menu": "Alternar o menu superior esquerdo", + "toggle_webcam_mute": "Ligar/desligar a webcam", "upload_file": "Enviar um arquivo" }, "labs": { "allow_screen_share_only_mode": "Permitir somente o modo de compartilhamento de tela", "ask_to_join": "Habilitar pedir para participar", "automatic_debug_logs": "Enviar automaticamente logs de depuração em qualquer erro", + "automatic_debug_logs_decryption": "Enviar automaticamente logs sobre erros de descriptografia", + "automatic_debug_logs_key_backup": "Envie automaticamente logs quando o backup da chave não estiver funcionando", + "beta_description": "O que vem por aí no %(brand)s? Os laboratórios são a melhor maneira de obter informações antecipadas, testar novos recursos e ajudar a moldá-los antes do lançamento.", "beta_feature": "Este é um recurso beta", "beta_feedback_leave_button": "Para sair do beta, vá nas suas configurações.", + "beta_feedback_title": "Comentários beta para %(featureName)s", + "beta_section": "Próximos recursos", "bridge_state": "Exibir informações sobre integrações nas configurações das salas", "bridge_state_channel": "Canal: ", "bridge_state_creator": "Esta integração foi disponibilizada por .", "bridge_state_manager": "Esta integração é desenvolvida por .", "bridge_state_workspace": "Espaço de trabalho: ", + "click_for_info": "Clique para mais informações", "currently_experimental": "Atualmente experimental.", "custom_themes": "Permite adicionar temas personalizados", "dynamic_room_predecessors": "Predecessores de salas dinâmicas", + "dynamic_room_predecessors_description": "Habilitar MSC3946 (para dar suporte a arquivos de chat para quem chegar mais tarde)", + "element_call_video_rooms": "Salas de vídeo Element Call", + "exclude_insecure_devices": "Excluir dispositivos inseguros ao enviar/receber mensagens", + "exclude_insecure_devices_description": "Quando esse modo estiver ativado, as mensagens criptografadas não serão compartilhadas com dispositivos não verificados e as mensagens de dispositivos não verificados serão mostradas como um erro. Observe que, se você ativar esse modo, talvez não consiga se comunicar com usuários que não verificaram seus dispositivos.", + "experimental_description": "Está se sentindo experimental? Experimente nossas últimas ideias em desenvolvimento. Esses recursos não estão finalizados; eles podem ser instáveis, podem mudar ou podem ser descartados completamente. Saiba mais .", + "experimental_section": "Pré-visualizações antecipadas", + "extended_profiles_msc_support": "Requer que seu servidor ofereça suporte ao MSC4133", + "feature_disable_call_per_sender_encryption": "Desativar a criptografia por remetente para Element Call", "feature_wysiwyg_composer_description": "Use rich text em vez de Markdown no compositor de mensagens.", "group_calls": "Nova experiência de chamada em grupo", "group_developer": "Desenvolvedor", "group_encryption": "Criptografia", + "group_experimental": "Experimental", "group_messaging": "Mensagens", "group_moderation": "Moderação", "group_profile": "Perfil", @@ -1244,30 +1491,47 @@ "group_spaces": "Espaços", "group_themes": "Temas", "group_threads": "Tópicos", + "group_ui": "Interface do usuário", "group_voip": "Voz e vídeo", + "group_widgets": "Widgets", "hidebold": "Ocultar ponto de notificação (exibir somente emblemas de contadores)", "html_topic": "Mostrar representação HTML dos tópicos da sala", "join_beta": "Participe da versão beta", + "join_beta_reload": "Aderir à versão beta irá recarregar %(brand)s.", "jump_to_date": "Ir para a data (adiciona cabeçalhos /jumptodate e pular para a data)", "jump_to_date_msc_support": "Requer que seu servidor suporte MSC3030", "latex_maths": "Renderizar fórmulas matemáticas LaTeX em mensagens", + "leave_beta": "Sair da versão beta", + "leave_beta_reload": "Sair da versão beta irá recarregar %(brand)s.", "location_share_live": "Compartilhamento de localização ao vivo", "location_share_live_description": "Implementação temporária. As localizações persistem no histórico da sala.", "mjolnir": "Novas formas de ignorar as pessoas", "msc3531_hide_messages_pending_moderation": "Permitir que os moderadores ocultem mensagens com moderação pendente.", + "new_room_list": "Habilitar nova lista de salas", "notification_settings": "Novas configurações de notificação", + "notification_settings_beta_caption": "Apresentamos uma maneira mais simples de alterar suas configurações de notificação. Personalize o seu %(brand)s, da forma que quiser.", + "notification_settings_beta_title": "Configurações de notificação", + "notifications": "Ative o painel de notificações no cabeçalho da sala", + "release_announcement": "Anúncio de lançamento", + "render_reaction_images": "Renderizar imagens personalizadas em reações", + "render_reaction_images_description": "Às vezes chamados de “emojis personalizados”.", "report_to_moderators": "Reportar aos moderadores", + "report_to_moderators_description": "Em salas que aceitam moderação, o botão \"Denunciar\" permitirá que você denuncie abusos aos moderadores da sala.", "sliding_sync": "Modo Sliding Sync", "sliding_sync_description": "Em desenvolvimento ativo, não pode ser desativado.", + "sliding_sync_disabled_notice": "Saia e entre novamente para desativar", "sliding_sync_server_no_support": "Seu servidor não tem suporte", "under_active_development": "Em desenvolvimento ativo.", + "unrealiable_e2e": "Não confiável em salas criptografadas", "video_rooms": "Salas de vídeo", "video_rooms_a_new_way_to_chat": "Uma nova maneira de conversar por voz e vídeo em %(brand)s .", "video_rooms_always_on_voip_channels": "As salas de vídeo são canais VoIP sempre ativos incorporados em uma sala em %(brand)s .", + "video_rooms_beta": "As salas de vídeo são um recurso beta", "video_rooms_faq1_answer": "Use o botão “+” na seção da sala do painel esquerdo.", "video_rooms_faq1_question": "Como posso criar uma sala de vídeo?", "video_rooms_faq2_answer": "Sim, a linha do tempo do chat é exibida ao lado do vídeo.", "video_rooms_faq2_question": "Posso usar o chat de texto junto com a videochamada?", + "video_rooms_feedbackSubheading": "Obrigado por experimentar a versão beta. Forneça o máximo de detalhes que você puder para que possamos aprimorá-la.", "wysiwyg_composer": "Editor de texto rico" }, "labs_mjolnir": { @@ -1287,6 +1551,7 @@ "lists_heading": "Listas inscritas", "lists_new_label": "ID da sala ou endereço da lista de banidos", "no_lists": "Você não está inscrito em nenhuma lista", + "personal_description": "Sua lista pessoal de banimento contém todos os usuários/servidores dos quais você pessoalmente não deseja receber mensagens. Depois de ignorar o primeiro usuário/servidor, uma nova sala aparecerá na sua lista de salas chamada '%(myBanList)s' - permaneça nessa sala para manter a lista de banimento em vigor.", "personal_empty": "Você não bloqueou ninguém.", "personal_heading": "Lista pessoal de banidos", "personal_new_label": "Servidor ou ID de usuário para bloquear", @@ -1304,9 +1569,12 @@ }, "language_dropdown_label": "Menu suspenso de idiomas", "leave_room_dialog": { + "last_person_warning": "Você é a única pessoa aqui. Se você sair, ninguém poderá se juntar no futuro, inclusive você.", "leave_room_question": "Tem certeza de que deseja sair da sala '%(roomName)s'?", "leave_space_question": "Tem certeza de que deseja sair desse espaço '%(spaceName)s'?", - "room_rejoin_warning": "Esta sala não é pública. Você não poderá voltar sem ser convidada/o.", + "room_leave_admin_warning": "Você é o único administrador nesta sala. Se você sair, ninguém poderá alterar as configurações da sala ou realizar outras ações importantes.", + "room_leave_mod_warning": "Você é o único moderador nesta sala. Se você sair, ninguém poderá alterar as configurações da sala ou realizar outras ações importantes.", + "room_rejoin_warning": "Esta sala não é pública. Você não poderá entrar novamente sem um convite.", "space_rejoin_warning": "Este espaço não é público. Você não poderá entrar novamente sem um convite." }, "left_panel": { @@ -1314,34 +1582,87 @@ }, "lightbox": { "rotate_left": "Girar para a esquerda", - "rotate_right": "Girar para a direita" + "rotate_right": "Girar para a direita", + "title": "Visualização da imagem" }, "location_sharing": { + "MapStyleUrlNotConfigured": "Esse​•​servidor​•​doméstico​•​não​•​está​•​configurado​•​para​•​exibir​•​mapas.", + "MapStyleUrlNotReachable": "Esse​•​servidor​•​doméstico​•​não​•​está​•​configurado​•​corretamente​•​para​•​exibir​•​mapas,​•​ou​•​o​•​servidor​•​de​•​mapas​•​configurado​•​pode​•​estar​•​inacessível.", + "WebGLNotEnabled": "O​•​WebGL​•​é​•​necessário​•​para​•​exibir​•​mapas,​•​ative-o​•​nas​•​configurações​•​do​•​seu​•​navegador.", + "click_drop_pin": "Clique para soltar um alfinete", + "click_move_pin": "Clique​•​para​•​mover​•​o​•​alfinete", + "close_sidebar": "Fechar​•​barra​•​lateral", + "error_fetch_location": "Não​•​foi​•​possível​•​obter​•​a​•​localização", + "error_no_perms_description": "Você​•​precisa​•​ter​•​as​•​permissões​•​corretas​•​para​•​compartilhar​•​locais​•​nesta​•​sala.", + "error_no_perms_title": "Você​•​não​•​tem​•​permissão​•​para​•​compartilhar​•​locais", + "error_send_description": "%(brand)s​•​não​•​pôde​•​enviar​•​a​•​sua​•​localização.​•​Tente​•​novamente​•​mais​•​tarde.", "error_send_title": "Não foi possível enviar sua localização", + "error_sharing_live_location": "Ocorreu​•​um​•​erro​•​ao​•​compartilhar​•​sua​•​localização​•​ao​•​vivo", + "error_stopping_live_location": "Ocorreu​•​um​•​erro​•​ao​•​interromper​•​sua​•​localização​•​ao​•​vivo", + "expand_map": "Expandir​•​mapa", + "failed_generic": "Falha​•​ao​•​buscar​•​sua​•​localização.​•​Tente​•​novamente​•​mais​•​tarde.", + "failed_load_map": "Não​•​é​•​possível​•​carregar​•​o​•​mapa", + "failed_permission": "%(brand)s teve​•​sua​•​permissão​•​negada​•​para​•​buscar​•​sua​•​localização.​•​Permita​•​o​•​acesso​•​à​•​localização​•​nas​•​configurações​•​do​•​seu​•​navegador.", + "failed_timeout": "Tempo​•​limite​•​esgotado​•​ao​•​tentar​•​buscar​•​sua​•​localização.​•​Tente​•​novamente​•​mais​•​tarde.", + "failed_unknown": "Erro​•​desconhecido​•​ao​•​buscar​•​local.​•​Tente​•​novamente​•​mais​•​tarde.", "find_my_location": "Encontrar minha localização", + "live_description": "Localização​•​ao​•​vivo​•​de​•​%(displayName)s", "live_enable_description": "Observação: este é um recurso de laboratório que usa uma implementação temporária. Isso significa que você não poderá excluir seu histórico de localização, e usuários avançados poderão ver seu histórico de localização mesmo depois que você parar de compartilhar sua localização ao vivo com esta sala.", + "live_enable_heading": "Compartilhamento​•​de​•​localização​•​ao​•​vivo", + "live_location_active": "Você está compartilhando sua localização ao vivo", + "live_location_enabled": "Localização ao vivo ativada", + "live_location_ended": "Localização​•​ao​•​vivo​•​encerrada", + "live_location_error": "Erro​•​de​•​localização​•​ao​•​vivo", + "live_locations_empty": "Sem​•​locais​•​ao​•​vivo", + "live_share_button": "Compartilhe​•​por​•​%(duration)s", + "live_toggle_label": "Ativar​•​o​•​compartilhamento​•​de​•​localização​•​ao​•​vivo", + "live_until": "Ao​•​vivo​•​até​•​%(expiryTime)s", + "live_update_time": "Atualizado​•​%(humanizedUpdateTime)s", + "loading_live_location": "Carregando​•​local​•​ao​•​vivo...", "location_not_available": "Local não disponível", - "share_button": "Compartilhar localização" + "map_feedback": "Feedback​•​do​•​mapa", + "mapbox_logo": "Logotipo​•​da​•​Mapbox", + "reset_bearing": "Redefinir​•​o​•​rumo​•​para​•​o​•​norte", + "share_button": "Compartilhar localização", + "share_type_live": "Minha​•​localização​•​ao​•​vivo", + "share_type_own": "Minha​•​localização​•​atual", + "share_type_pin": "Solte​•​um​•​alfinete", + "share_type_prompt": "Que​•​tipo​•​de​•​localização​•​você​•​deseja​•​compartilhar?", + "toggle_attribution": "Alternar​•​atribuição" }, "member_list": { + "count": { + "one": "%(count)s membro", + "other": "%(count)s membros" + }, "filter_placeholder": "Pesquisar participantes da sala", + "invite_button_no_perms_tooltip": "Você não tem permissão para convidar usuários", + "invited_label": "Convidado", + "no_matches": "Sem correspondências", "power_label": "%(userName)s (nível de permissão %(powerLevelNumber)s)" }, "member_list_back_action_label": "Membros da sala", "message_edit_dialog_title": "Edições na mensagem", + "migrating_crypto": "Aguente firme. Estamos atualizando %(brand)s para tornar a criptografia mais rápida e confiável.", "mobile_guide": { "toast_accept": "Usar o aplicativo", + "toast_description": "%(brand)s é experimental em um navegador da Web móvel. Para uma melhor experiência e os recursos mais recentes, use nosso aplicativo nativo gratuito.", "toast_title": "Use o aplicativo para ter uma experiência melhor" }, + "name_and_id": "%(name)s (%(userId)s)", "no_more_results": "Não há mais resultados", "notif_panel": { - "empty_description": "Não há notificações." + "empty_description": "Não há notificações.", + "empty_heading": "Isso é tudo, pessoal!" }, "notifications": { "all_messages": "Todas as mensagens novas", "all_messages_description": "Seja notificado para cada mensagem", + "class_global": "Global", "class_other": "Outros", "default": "Padrão", + "default_settings": "Corresponder às configurações padrão", + "email_pusher_app_display_name": "Notificações por e-mail", "enable_prompt_toast_description": "Ativar notificações na área de trabalho", "enable_prompt_toast_title": "Notificações", "enable_prompt_toast_title_from_message_send": "Não perca uma resposta", @@ -1349,13 +1670,18 @@ "keyword": "Palavra-chave", "keyword_new": "Nova palavra-chave", "level_activity": "Atividade recente", + "level_highlight": "Destaque", + "level_muted": "Silenciado", "level_none": "Nenhuma", + "level_notification": "Notificação", + "level_unsent": "Não enviado", "mark_all_read": "Marcar tudo como lido", "mentions_and_keywords": "@menções e palavras-chave", "mentions_and_keywords_description": "Receba notificações apenas com menções e palavras-chave conforme definido em suas configurações", - "mentions_keywords": "Menções e palavras-chave", + "mentions_keywords": "Menções e palavras-chave!", "message_didnt_send": "A mensagem não foi enviada. Clique para mais informações.", - "mute_description": "Você não receberá nenhuma notificação" + "mute_description": "Você não receberá nenhuma notificação", + "mute_room": "Silenciar sala" }, "notifier": { "m.key.verification.request": "%(name)s está solicitando confirmação" @@ -1364,23 +1690,41 @@ "create_room": "Criar um chat de grupo", "explore_rooms": "Explorar salas públicas", "has_avatar_label": "Ótimo, agora as pessoas identificarão você", + "intro_byline": "Seja dono de suas conversas.", "intro_welcome": "Boas-vindas ao %(appName)s", "no_avatar_label": "Adicione uma foto para as pessoas identificarem você.", "send_dm": "Enviar uma mensagem", "welcome_detail": "Agora, vamos começar", "welcome_user": "Boas-vindas, %(name)s" }, + "pill": { + "permalink_other_room": "Mensagem em %(room)s", + "permalink_this_room": "Mensagem de %(user)s" + }, "poll": { + "create_poll_action": "Criar Enquete", "create_poll_title": "Criar enquete", + "disclosed_notes": "Os participantes veem os resultados assim que votam", + "edit_poll_title": "Editar enquete", + "end_description": "Tem certeza de que deseja encerrar esta enquete? Isso mostrará os resultados finais da enquete e impedirá que as pessoas possam votar.", "end_message": "A enquete terminou. Resposta principal: %(topAnswer)s", "end_message_no_votes": "A enquete terminou. Nenhum voto foi dado.", + "end_title": "Encerrar Enquete", + "error_ending_description": "Desculpe, a enquete não terminou. Por favor, tente novamente.", + "error_ending_title": "Falha ao finalizar a enquete", "error_voting_description": "Desculpe, seu voto não foi registrado. Por favor, tente novamente.", "error_voting_title": "Voto não registrado", "failed_send_poll_description": "Desculpe, a enquete que você tentou criar não foi publicada.", "failed_send_poll_title": "Falha ao postar enquete", + "notes": "Os resultados só são revelados quando você encerra a enquete", + "options_add_button": "Adicionar opção", + "options_heading": "Criar opções", + "options_label": "Opção %(number)s", + "options_placeholder": "Escrever uma opção", "topic_heading": "Qual é a pergunta ou o tópico da sua enquete?", "topic_label": "Pergunta ou tópico", "topic_placeholder": "Escreva algo…", + "total_decryption_errors": "Devido a erros de descriptografia, alguns votos podem não ser contados", "total_n_votes": { "one": "%(count)s votos expressos. Vote para ver os resultados", "other": "%(count)s votos expressos. Vote para ver os resultados" @@ -1389,7 +1733,13 @@ "one": "Com base na votação de %(count)s", "other": "Com base em %(count)s votos" }, - "total_no_votes": "Sem votos expressos" + "total_no_votes": "Sem votos expressos", + "total_not_ended": "Os resultados ficarão visíveis quando a enquete terminar", + "type_closed": "Enquete encerrada", + "type_heading": "Tipo de enquete", + "type_open": "Enquete aberta", + "unable_edit_description": "Desculpe, você não pode editar uma enquete após os votos terem sido registrados.", + "unable_edit_title": "Não é possível editar a enquete" }, "power_level": { "admin": "Administrador/a", @@ -1400,6 +1750,7 @@ "moderator": "Moderador/a", "restricted": "Restrito" }, + "powered_by_matrix": "Desenvolvido por Matrix", "powered_by_matrix_with_logo": "Chat descentralizado e encriptado & colaboração, powered by $matrixLogo", "presence": { "away": "Ausente", @@ -1408,10 +1759,11 @@ "idle_for": "Inativo há %(duration)s", "offline": "Offline", "offline_for": "Offline há %(duration)s", - "online": "Conectada/o", + "online": "Disponível", "online_for": "Online há %(duration)s", "unknown": "Desconhecido", - "unknown_for": "Status desconhecido há %(duration)s" + "unknown_for": "Status desconhecido há %(duration)s", + "unreachable": "Servidor do usuário inacessível" }, "quick_settings": { "all_settings": "Todas as configurações", @@ -1425,52 +1777,103 @@ }, "redact": { "confirm_button": "Confirmar a remoção", + "confirm_description": "Tem certeza de que deseja remover (excluir) este evento?", + "confirm_description_state": "Observe que remover alterações na sala desta forma pode desfazer a alteração.", "error": "Você não pode apagar esta mensagem. (%(code)s)", "ongoing": "Removendo…", "reason_label": "Motivo (opcional)" }, "report_content": { - "description": "Reportar esta mensagem enviará o seu 'event ID' único para o/a administrador/a do seu Homeserver. Se as mensagens nesta sala são criptografadas, o/a administrador/a não conseguirá ler o texto da mensagem nem ver nenhuma imagem ou arquivo.", + "description": "Denunciar essa mensagem enviará seu “ID de evento” exclusivo ao administrador do seu servidor doméstico. Se as mensagens nesta sala forem criptografadas, o administrador do servidor doméstico não poderá ler o texto da mensagem nem visualizar arquivos ou imagens.", + "disagree": "Discordo", + "error_create_room_moderation_bot": "Não é possível criar espaço com o bot de moderação", + "hide_messages_from_user": "Marque se você deseja ocultar todas as mensagens atuais e futuras desse usuário.", + "ignore_user": "Ignorar usuário", + "illegal_content": "Conteúdo ilegal", "missing_reason": "Por favor, descreva porque você está reportando.", + "nature": "Escolha uma natureza e descreva o que torna essa mensagem abusiva.", + "nature_disagreement": "O que esse usuário está escrevendo está errado.\nIsso será relatado aos moderadores da sala.", + "nature_illegal": "Este usuário está exibindo comportamento ilegal, por exemplo, expondo pessoas (doxxing) ou ameaçando de violência.\nIsso será relatado aos moderadores da sala, que podem encaminhar isso às autoridades legais.", + "nature_nonstandard_admin": "Esta sala é dedicada a conteúdo ilegal ou tóxico ou os moderadores não conseguem moderar conteúdo ilegal ou tóxico.\nIsso será relatado aos administradores do %(homeserver)s.", + "nature_nonstandard_admin_encrypted": "Esta sala é dedicada a conteúdo ilegal ou tóxico ou os moderadores não conseguem moderar conteúdo ilegal ou tóxico.\nIsso será relatado aos administradores do %(homeserver)s. Os administradores NÃO poderão ler o conteúdo criptografado desta sala.", + "nature_other": "Qualquer outro motivo. Descreva o problema.\nIsso será relatado aos moderadores da sala.", + "nature_spam": "Esse usuário está enviando spam para a sala com anúncios, links para anúncios ou propaganda.\nIsso será relatado aos moderadores da sala.", + "nature_toxic": "Esse usuário está exibindo comportamento tóxico, por exemplo, insultando outros usuários ou compartilhando conteúdo somente para adultos em um sala para menores de idade ou violando as regras desta sala. Isso será relatado aos moderadores da sala.", "other_label": "Outros", - "report_content_to_homeserver": "Denunciar conteúdo ao administrador do seu servidor principal" + "report_content_to_homeserver": "Denunciar conteúdo ao administrador do seu servidor principal", + "report_entire_room": "Denunciar a sala inteira", + "spam_or_propaganda": "Spam ou propaganda", + "toxic_behaviour": "Comportamento tóxico" + }, + "report_room": { + "description": "Denuncie esta sala ao administrador do seu servidor. Isso enviará a ID exclusiva da sala, mas se as mensagens forem criptografadas, o administrador não poderá lê-las nem visualizar os arquivos compartilhados.", + "reason_label": "Descreva o motivo" }, "restore_key_backup_dialog": { "count_of_decryption_failures": "Falha ao descriptografar as sessões de %(failedCount)s!", "count_of_successfully_restored_keys": "%(sessionCount)s chaves foram restauradas com sucesso", - "enter_key_description": "Acesse o seu histórico de mensagens seguras e configure as mensagens seguras, ao inserir a sua Chave de Segurança.", - "enter_key_title": "Digite a Chave de Segurança", + "enter_key_description": "Acesse o seu histórico de mensagens seguras e configure as mensagens seguras, ao inserir a sua Chave de Recuperação.", + "enter_key_title": "Digite a Chave de Recuperação", "enter_phrase_description": "Acesse o seu histórico de mensagens seguras e configure mensagens seguras digitando a sua Frase de Segurança.", "enter_phrase_title": "Digite a Frase de Segurança", "incorrect_security_phrase_dialog": "O backup não pôde ser descriptografado com esta Frase de Segurança: verifique se você digitou a Frase de Segurança correta.", "incorrect_security_phrase_title": "Frase de Segurança incorreta", "key_backup_warning": "Atenção: você só deve configurar o backup de chave em um computador de sua confiança.", - "key_forgotten_text": "Se você esqueceu a sua Chave de Segurança, você pode ", - "key_is_invalid": "Chave de Segurança inválida", - "key_is_valid": "Essa Chave de Segurança é válida!", + "key_fetch_in_progress": "Obtendo chaves do servidor…", + "key_forgotten_text": "Se você esqueceu a sua Chave de Recuperação, você pode ", + "key_is_invalid": "Chave de Recuperação inválida", + "key_is_valid": "Essa Chave de Recuperação é válida!", "keys_restored_title": "Chaves restauradas", "load_error_content": "Não foi possível carregar o status do backup", "load_keys_progress": "%(completed)s de %(total)s chaves restauradas", "no_backup_error": "Nenhum backup encontrado!", - "phrase_forgotten_text": "Se você esqueceu a sua Frase de Segurança, você pode usar a sua Chave de Segurança ou definir novas opções de recuperação", - "recovery_key_mismatch_description": "Não foi possível descriptografar o backup com esta chave de segurança: verifique se você digitou a chave de segurança correta.", - "recovery_key_mismatch_title": "Incompatibilidade da Chave de Segurança", + "phrase_forgotten_text": "Se você esqueceu a sua Frase de Segurança, você pode usar a sua Chave de Recuperação ou definir novas opções de recuperação", + "recovery_key_mismatch_description": "O backup não pôde ser descriptografado com essa chave de recuperação: verifique se você inseriu a chave de recuperação correta.", + "recovery_key_mismatch_title": "Incompatibilidade da chave de recuperação", "restore_failed_error": "Não foi possível restaurar o backup" }, "right_panel": { "add_integrations": "Adicionar extensões", "add_topic": "Adicionar tópico", + "extensions_button": "Extensões", + "extensions_empty_description": "Selecione “%(addIntegrations)s” para navegar e adicionar extensões a esta sala", + "extensions_empty_title": "Aumente a produtividade com mais ferramentas, widgets e bots", "files_button": "Arquivos", "pinned_messages": { + "empty_description": "Selecione uma mensagem e escolha “%(pinAction)s” para incluí-la aqui.", + "empty_title": "Fixe mensagens importantes para que elas possam ser facilmente descobertas", + "header": { + "one": "1 Mensagem fixada", + "other": "%(count)s Mensagens fixadas" + }, "limits": { "other": "Você pode fixar até %(count)s widgets" - } + }, + "menu": "Abrir menu", + "release_announcement": { + "close": "OK", + "description": "Encontre todas as mensagens fixadas aqui. Passe o mouse sobre qualquer mensagem e selecione “Fixar” para adicioná-la.", + "title": "Todas as novas mensagens fixadas" + }, + "reply_thread": "Responder a uma mensagem de discussão", + "unpin_all": { + "button": "Desfixar todas as mensagens", + "content": "Certifique-se de que você realmente deseja remover todas as mensagens fixadas. Essa ação não pode ser desfeita.", + "title": "Desfixar todas as mensagens?" + }, + "view": "Ver no histórico" }, + "pinned_messages_button": "Mensagens fixadas", "poll": { + "active_heading": "Enquetes ativas", + "empty_active": "Não há enquetes ativas nesta sala", + "empty_active_load_more": "Não há enquetes ativas. Carregue mais enquetes para ver as enquetes dos meses anteriores", "empty_active_load_more_n_days": { "one": "Não há pesquisas ativas no último dia. Carregue mais enquetes para ver as enquetes dos meses anteriores", "other": "Não há pesquisas ativas para os últimos %(count)s dias. Carregue mais enquetes para ver as enquetes dos meses anteriores" }, + "empty_past": "Não há enquetes anteriores nesta sala", + "empty_past_load_more": "Não há enquetes anteriores. Carregue mais enquetes para ver as enquetes dos meses anteriores", "empty_past_load_more_n_days": { "one": "Não há pesquisas anteriores para o dia anterior. Carregue mais enquetes para ver as enquetes dos meses anteriores", "other": "Não há pesquisas anteriores nos últimos %(count)s dias. Carregue mais enquetes para ver as enquetes dos meses anteriores" @@ -1478,7 +1881,16 @@ "final_result": { "one": "Resultado final baseado em %(count)s votos", "other": "Resultado final baseado em %(count)s votos" - } + }, + "load_more": "Carregar mais enquetes", + "loading": "Carregando enquetes", + "past_heading": "Enquetes anteriores", + "view_in_timeline": "Exibir enquete no histórico", + "view_poll": "Ver enquete" + }, + "polls_button": "Enquetes", + "room_summary_card": { + "title": "Informação da sala" }, "thread_list": { "context_menu_label": "Opções de tópico" @@ -1488,30 +1900,52 @@ } }, "room": { + "3pid_invite_email_not_found_account": "Este convite foi enviado para %(email)s que não está associado à sua conta", "3pid_invite_email_not_found_account_room": "Este convite para %(roomName)s foi enviado para %(email)s, que não está associado à sua conta", + "3pid_invite_error_description": "Um erro (%(errcode)s) foi retornado ao tentar validar seu convite. Você pode tentar passar essas informações para a pessoa que o convidou.", "3pid_invite_error_invite_action": "Tentar entrar mesmo assim", "3pid_invite_error_invite_subtitle": "Você só pode participar com um convite válido.", + "3pid_invite_error_public_subtitle": "Você ainda pode entrar aqui.", + "3pid_invite_error_title": "Algo deu errado com seu convite.", "3pid_invite_error_title_room": "Ocorreu um erro no seu convite para %(roomName)s", "3pid_invite_no_is_subtitle": "Use um servidor de identidade em Configurações para receber convites diretamente no %(brand)s.", + "banned_by": "Você foi banido por %(memberName)s", "banned_from_room_by": "Você foi banido de %(roomName)s por %(memberName)s", "context_menu": { "copy_link": "Copiar link da sala", "favourite": "Favoritar", "forget": "Esquecer Sala", "low_priority": "Baixa prioridade", + "mark_read": "Marcar como lido", + "mark_unread": "Marcar como não lido", + "notifications_default": "Corresponder à configuração padrão", + "notifications_mute": "Silenciar sala", "title": "Opções da Sala", "unfavourite": "Favoritado" }, + "creating_room_text": "Estamos​•​criando​•​uma​•​sala​•​com​•​%(names)s", "dm_invite_action": "Começar a conversa", "dm_invite_subtitle": " quer conversar", "dm_invite_title": "Deseja conversar com %(user)s?", "drop_file_prompt": "Arraste um arquivo aqui para enviar", "edit_topic": "Editar tópico", + "error_3pid_invite_email_lookup": "Não foi possível encontrar o usuário por e-mail", + "error_cancel_knock_title": "Falha ao cancelar", + "error_join_403": "Você precisa de um convite para acessar esta sala.", + "error_join_404_1": "Você tentou entrar usando um ID de sala sem fornecer uma lista de servidores pelos quais ingressar. Os IDs das salas são identificadores internos e não podem ser usados para entrar em uma sala sem informações adicionais.", + "error_join_404_2": "Se você souber o endereço de uma sala, tente entrar por lá.", "error_join_404_invite": "A pessoa que o convidou já saiu ou o servidor dela está offline.", "error_join_404_invite_same_hs": "A pessoa que o convidou já saiu.", "error_join_connection": "Ocorreu um erro ao entrar.", + "error_join_incompatible_version_1": "Desculpe, seu servidor doméstico é muito antigo para participar aqui.", "error_join_incompatible_version_2": "Por favor, entre em contato com o administrador do seu homeserver.", "error_join_title": "Falha ao entrar", + "error_jump_to_date": "Servidor​•​retornou%(statusCode)s​•​com​•​código​•​de​•​erro%(errorCode)s", + "error_jump_to_date_connection": "Ocorreu um erro de rede ao tentar localizar e ir para a data especificada. Seu servidor doméstico pode estar inativo ou houve apenas um problema temporário com sua conexão com a Internet. Tente novamente. Se isso continuar, entre em contato com o administrador do servidor doméstico.", + "error_jump_to_date_details": "Detalhes​•​do​•​erro", + "error_jump_to_date_not_found": "Não​•​foi​•​possível​•​encontrar​•​um​•​evento​•​com​•​data​•​anterior​•​a​•​%(dateString)s.​•​Tente​•​escolher​•​uma​•​data​•​anterior.", + "error_jump_to_date_send_logs_prompt": "Envie​•​os​•​registros​•​de​•​​•​depuração​•​​•​para​•​nos​•​ajudar​•​a​•​rastrear​•​o​•​problema.", + "error_jump_to_date_title": "Não​•​foi​•​possível​•​encontrar​•​o​•​evento​•​nessa​•​data", "face_pile_summary": { "one": "%(count)s pessoa que você conhece já entrou", "other": "%(count)s pessoas que você conhece já entraram" @@ -1524,6 +1958,7 @@ "face_pile_tooltip_shortcut_joined": "Incluindo você, %(commaSeparatedMembers)s", "failed_reject_invite": "Não foi possível recusar o convite", "forget_room": "Esquecer esta sala", + "forget_space": "Esqueça este espaço", "header": { "n_people_asking_to_join": { "one": "Pedindo para participar", @@ -1531,10 +1966,17 @@ }, "room_is_public": "Esta sala é pública" }, + "header_avatar_open_settings_label": "Abrir configurações de sala", + "header_face_pile_tooltip": "Pessoas", + "header_untrusted_label": "Não confiável", + "inaccessible": "Esta sala ou espaço não está acessível neste momento.", "inaccessible_name": "%(roomName)s não está acessível neste momento.", + "inaccessible_subtitle_1": "Tente novamente mais tarde ou peça a um administrador de sala ou espaço para verificar se você tem acesso.", + "inaccessible_subtitle_2": "%(errcode)s foi retornado ao tentar acessar a sala ou o espaço. Se você acha que está vendo essa mensagem por engano, envie uma comunicação de bug.", "intro": { "dm_caption": "Apenas vocês dois estão nesta conversa, a menos que algum de vocês convide mais alguém.", "enable_encryption_prompt": "Ative a criptografia nas configurações.", + "encrypted_3pid_dm_pending_join": "Depois que todos entrarem, você poderá conversar", "no_avatar_label": "Adicione uma imagem para que as pessoas possam identificar facilmente sua sala.", "no_topic": "Adicione uma descrição para ajudar as pessoas a saber do que se trata essa conversa.", "private_unencrypted_warning": "Suas mensagens privadas normalmente são criptografadas, mas esta sala não é. Isto acontece normalmente por conta de um dispositivo ou método usado sem suporte, como convites via email, por exemplo.", @@ -1549,6 +1991,7 @@ "you_created": "Você criou esta sala." }, "invite_email_mismatch_suggestion": "Compartilhe este e-mail em Configurações para receber convites diretamente no %(brand)s.", + "invite_sent_to_email": "Este convite foi enviado para %(email)s", "invite_sent_to_email_room": "Este convite para %(roomName)s foi enviado para %(email)s", "invite_subtitle": "Convidado por ", "invite_this_room": "Convidar para esta sala", @@ -1556,6 +1999,7 @@ "inviter_unknown": "Desconhecido", "invites_you_text": " convida você", "join_button_account": "Inscrever-se", + "join_failed_needs_invite": "Para ver %(roomName)s, você precisa de um convite", "join_the_discussion": "Participar da discussão", "join_title": "Entre na sala para participar", "join_title_account": "Participar da conversa com uma conta", @@ -1565,30 +2009,66 @@ "jump_read_marker": "Ir diretamente para a primeira das mensagens não lidas.", "jump_to_bottom_button": "Ir para as mensagens recentes", "jump_to_date": "Ir para Data", + "jump_to_date_beginning": "O​•​começo​•​da​•​sala", + "jump_to_date_prompt": "Escolha​•​uma​•​data​•​para​•​ir", "kick_reason": "Razão: %(reason)s", + "kicked_by": "Você foi removido por %(memberName)s", + "kicked_from_room_by": "Você foi removido de %(roomName)s por %(memberName)s", + "knock_cancel_action": "Cancelar solicitação", + "knock_denied_subtitle": "Como seu acesso foi negado, você não pode voltar a menos que seja convidado pelo administrador ou moderador do grupo.", + "knock_denied_title": "Seu acesso foi negado", + "knock_message_field_placeholder": "Mensagem (opcional)", + "knock_prompt": "Pedir para participar?", + "knock_prompt_name": "Pedir para participar de %(roomName)s?", "knock_send_action": "Solicitar acesso", + "knock_sent": "Solicitação de adesão enviada", + "knock_sent_subtitle": "Sua solicitação de adesão está pendente.", "knock_subtitle": "Você precisa ter acesso a esta sala para visualizar ou participar da conversa. Você pode enviar uma solicitação de adesão abaixo.", "leave_error_title": "Erro ao sair da sala", "leave_server_notices_description": "Esta sala é usada para mensagens importantes do Homeserver, então você não pode sair dela.", "leave_server_notices_title": "Não é possível sair da sala Avisos do Servidor", "leave_unexpected_error": "Erro inesperado no servidor, ao tentar sair da sala", "link_email_to_receive_3pid_invite": "Vincule esse e-mail à sua conta em Configurações, para receber convites diretamente no %(brand)s.", + "loading_preview": "Carregando pré-visualização", "no_peek_join_prompt": "%(roomName)s não pode ser visualizado. Deseja participar?", + "no_peek_no_name_join_prompt": "Não há pré-visualização, você gostaria de participar?", + "not_found_subtitle": "Tem certeza de que está no lugar certo?", + "not_found_title": "Esta sala ou espaço não existe.", "not_found_title_name": "%(roomName)s não existe.", "peek_join_prompt": "Você está visualizando %(roomName)s. Deseja participar?", + "pinned_message_badge": "Mensagem fixada", + "pinned_message_banner": { + "button_close_list": "Fechar lista", + "button_view_all": "Ver tudo", + "description": "Esta sala tem mensagens fixadas. Clique para visualizá-las.", + "go_to_message": "Veja a mensagem fixada no histórico.", + "title": "%(index)s de %(length)s mensagens fixadas" + }, "read_topic": "Clique para ler o tópico", + "rejecting": "Rejeitando o convite...", "rejoin_button": "Entrar novamente", "search": { "all_rooms_button": "Pesquisar todos as salas", - "placeholder": "Pesquisar mensagens..." + "placeholder": "Pesquisar mensagens...", + "summary": { + "one": "1 resultado encontrado para “”", + "other": "%(count)s resultados encontrados para “”" + }, + "this_room_button": "Pesquise nesta sala" }, "status_bar": { + "delete_all": "Excluir​•​tudo", "exceeded_resource_limit": "Sua mensagem não foi enviada porque este servidor local excedeu o limite de recursos. Por favor, entre em contato com o seu administrador de serviços para continuar usando o serviço.", + "homeserver_blocked": "Sua​•​mensagem​•​não​•​foi​•​enviada​•​porque​•​esse​•​servidor​•​doméstico​•​foi​•​bloqueado​•​pelo​•​administrador.​•​Entre​•​em​•​contato​•​com​•​o​•​administrador​•​do​•​serviço​•​​•​para​•​continuar​•​usando​•​o​•​serviço.", "monthly_user_limit_reached": "Sua mensagem não foi enviada porque este homeserver atingiu seu Limite de usuário ativo mensal. Por favor, entre em contato com o seu administrador de serviços para continuar usando o serviço.", "requires_consent_agreement": "Você não pode enviar nenhuma mensagem até revisar e concordar com nossos termos e condições.", + "retry_all": "Repetir​•​tudo", + "select_messages_to_retry": "Você​•​pode​•​selecionar​•​todas​•​as​•​mensagens​•​ou​•​mensagens​•​individuais​•​para​•​tentar​•​novamente​•​ou​•​excluir", "server_connectivity_lost_description": "Imagens enviadas ficarão armazenadas até que sua conexão seja reestabelecida.", - "server_connectivity_lost_title": "A conexão com o servidor foi perdida. Verifique sua conexão de internet." + "server_connectivity_lost_title": "A conexão com o servidor foi perdida. Verifique sua conexão de internet.", + "some_messages_not_sent": "Algumas​•​de​•​suas​•​mensagens​•​não​•​foram​•​enviadas" }, + "unknown_status_code_for_timeline_jump": "código​•​de​•​status​•​desconhecido", "unread_notifications_predecessor": { "other": "Você tem %(count)s notificações não lidas em uma versão anterior desta sala.", "one": "Você tem %(count)s notificações não lidas em uma versão anterior desta sala." @@ -1605,31 +2085,92 @@ "other": "Enviando o arquivo %(filename)s e %(count)s outros arquivos" }, "uploading_single_file": "Enviando o arquivo %(filename)s" - } + }, + "video_room": "Esta sala é uma sala de vídeo", + "waiting_for_join_subtitle": "Uma​•​vez​•​que​•​os​•​usuários​•​convidados​•​tenham​•​entrado​•​em​•​%(brand)s,​•​você​•​poderá​•​conversar​•​e​•​a​•​sala​•​será​•​criptografada​•​de​•​ponta​•​a​•​ponta", + "waiting_for_join_title": "Aguardando​•​que​•​os​•​usuários​•​se​•​juntem​•​a​•​%(brand)s" }, "room_list": { "add_room_label": "Adicionar sala", "add_space_label": "Adicionar espaço", + "appearance": "Aparência", "breadcrumbs_empty": "Nenhuma sala foi visitada recentemente", "breadcrumbs_label": "Salas visitadas recentemente", + "empty": { + "no_chats": "Ainda não há conversas.", + "no_chats_description": "Comece enviando uma mensagem para alguém ou criando uma sala", + "no_chats_description_no_room_rights": "Comece enviando uma mensagem para alguém", + "no_favourites": "Você ainda não tem o bate-papo favorito", + "no_favourites_description": "Você pode adicionar um bate-papo aos seus favoritos nas configurações de bate-papo", + "no_invites": "Você não tem nenhum convite não lido", + "no_mentions": "Você não tem nenhuma menção não lida", + "no_people": "Você ainda não tem conversas diretas com ninguém", + "no_people_description": "Você pode desmarcar os filtros para ver suas outras conversas", + "no_rooms": "Você não está em nenhuma sala ainda", + "no_rooms_description": "Você pode desmarcar os filtros para ver suas outras conversas.", + "no_unread": "Parabéns! Você não tem nenhuma mensagem não lida", + "show_activity": "Ver todas as atividades", + "show_chats": "Mostrar todas as conversas" + }, "failed_add_tag": "Falha ao adicionar a tag %(tagName)s para a sala", "failed_remove_tag": "Falha ao remover a tag %(tagName)s da sala", + "failed_set_dm_tag": "Falha ao definir a marca de mensagem direta", + "filters": { + "favourite": "Favoritos", + "invites": "Convites", + "mentions": "Menções", + "people": "Pessoas", + "rooms": "Salas", + "unread": "Não lido" + }, "home_menu_label": "Opções do Início", "join_public_room_label": "Entrar na sala pública", "joining_rooms_status": { "one": "Entrando na %(count)s sala", "other": "Entrando atualmente em %(count)s salas" }, + "list_title": "Lista de salas", + "more_options": { + "copy_link": "Copiar link da sala", + "favourited": "Favoritado", + "leave_room": "Sair da sala", + "low_priority": "Baixa prioridade", + "mark_read": "Marcar como lido", + "mark_unread": "Marcar como não lido" + }, "notification_options": "Alterar notificações", + "open_space_menu": "Abrir menu do espaço", + "primary_filters": "Filtros da lista de salas", + "redacting_messages_status": { + "one": "Atualmente removendo mensagens em %(count)s sala", + "other": "Atualmente removendo mensagens em %(count)s salas" + }, + "room": { + "more_options": "Mais opções", + "open_room": "Abrir sala %(roomName)s" + }, + "room_options": "Opções da Sala", "show_less": "Mostrar menos", + "show_message_previews": "Mostrar prévias de mensagens", "show_n_more": { "other": "Mostrar %(count)s a mais", "one": "Mostrar %(count)s a mais" }, "show_previews": "Mostrar pré-visualizações de mensagens", + "sort": "Ordenar", "sort_by": "Classificar por", "sort_by_activity": "Atividade recente", + "sort_by_alphabet": "De A a Z", + "sort_type": { + "activity": "Atividade", + "atoz": "A-Z" + }, "sort_unread_first": "Mostrar salas não lidas em primeiro", + "space_menu": { + "home": "Espaço", + "space_settings": "Configurações de espaço" + }, + "space_menu_label": "Menu de %(spaceName)s", "sublist_options": "Opções da Lista", "suggested_rooms_heading": "Salas sugeridas" }, @@ -1643,11 +2184,14 @@ "error_upgrade_title": "Falha ao atualizar a sala", "information_section_room": "Informação da sala", "information_section_space": "Informações do espaço", + "room_id": "ID interno da sala", "room_predecessor": "Ler mensagens antigas em %(roomName)s.", "room_upgrade_button": "Atualizar a versão desta sala", "room_upgrade_warning": "Aviso : atualizar um quarto irá não migre automaticamente os membros da sala para a nova versão da sala. Publicaremos um link para a nova sala na versão antiga da sala - os membros da sala terão que clicar neste link para entrar na nova sala.", "room_version": "Versão da sala:", "room_version_section": "Versão da sala", + "space_predecessor": "Exibir a versão mais antiga de %(spaceName)s.", + "space_upgrade_button": "Atualize este espaço para a versão de sala recomendada", "unfederated": "Esta sala não é acessível para servidores Matrix remotos", "upgrade_button": "Atualize essa sala para versão %(version)s", "upgrade_dialog_description": "Atualizar esta sala irá fechar a instância atual da sala e, em seu lugar, criar uma sala atualizada com o mesmo nome. Para oferecer a melhor experiência possível aos integrantes da sala, nós iremos:", @@ -1660,7 +2204,10 @@ "upgrade_warning_dialog_description": "Atualizar uma sala é uma ação avançada e geralmente é recomendada quando uma sala está instável devido a erros, recursos ausentes ou vulnerabilidades de segurança.", "upgrade_warning_dialog_explainer": "Por favor, note que a atualização criará uma nova versão do quarto . Todas as mensagens atuais permanecerão nesta sala arquivada.", "upgrade_warning_dialog_footer": "Você atualizará esta sala de para .", + "upgrade_warning_dialog_invite_label": "Convide automaticamente membros desta sala para a nova", + "upgrade_warning_dialog_report_bug_prompt": "Isso geralmente afeta apenas como a sala é processada no servidor. Se você tiver problemas com o %(brand)s, informe um erro.", "upgrade_warning_dialog_report_bug_prompt_link": "Isso geralmente afeta apenas como a sala é processada no servidor. Se você tiver problemas com o %(brand)s, informe um erro.", + "upgrade_warning_dialog_title": "Atualizar a sala", "upgrade_warning_dialog_title_private": "Atualizar a sala privada" }, "alias_not_specified": "não especificado", @@ -1671,9 +2218,13 @@ }, "delete_avatar_label": "Remover foto de perfil", "general": { + "alias_field_has_domain_invalid": "Separador de domínio ausente, por exemplo, (:domain.org)", + "alias_field_has_localpart_invalid": "Nome da sala ou separador ausente, por exemplo, (my-room:domain.org)", + "alias_field_matches_invalid": "Este endereço não aponta para esta sala", "alias_field_placeholder_default": "por exemplo: minha-sala", "alias_field_required_invalid": "Por favor, digite um endereço", "alias_field_safe_localpart_invalid": "Alguns caracteres não são permitidos", + "alias_field_taken_invalid": "Este endereço tem um servidor inválido ou já está em uso", "alias_field_taken_invalid_domain": "Este endereço já está em uso", "alias_field_taken_valid": "Este endereço está disponível para uso", "alias_heading": "Endereço da sala", @@ -1690,6 +2241,8 @@ "error_deleting_alias_description": "Ocorreu um erro ao remover esse endereço. Ele pode não mais existir ou houve um problema temporário.", "error_deleting_alias_description_forbidden": "Você não tem permissão para excluir este endereço.", "error_deleting_alias_title": "Erro ao remover o endereço", + "error_publishing": "Não é possível publicar a sala", + "error_publishing_detail": "Houve um erro ao publicar esta sala", "error_save_space_settings": "Falha ao salvar as configurações desse espaço.", "error_updating_alias_description": "Ocorreu um erro ao atualizar o endereço alternativo da sala. Isso pode não ser permitido pelo servidor ou houve um problema temporário.", "error_updating_canonical_alias_description": "Ocorreu um erro ao atualizar o endereço principal da sala. Isso pode não ser permitido pelo servidor ou houve um problema temporário.", @@ -1723,9 +2276,19 @@ "notification_sound": "Som de notificação", "settings_link": "Receba notificações conforme configurado em suas configurações", "sounds_section": "Sons", + "upload_sound_label": "Carregar som personalizado", "uploaded_sound": "Som enviado" }, + "people": { + "knock_empty": "Nenhuma solicitação", + "knock_section": "Pedir para participar", + "see_less": "Ver menos", + "see_more": "Ver mais" + }, "permissions": { + "add_privileged_user_description": "Dê mais privilégios a um ou mais usuários nesta sala", + "add_privileged_user_filter_placeholder": "Pesquisar usuários nesta sala...", + "add_privileged_user_heading": "Adicione usuários privilegiados", "ban": "Banir usuários", "ban_reason": "Razão", "banned_by": "Banido por %(displayName)s", @@ -1764,7 +2327,7 @@ "permissions_section": "Permissões", "permissions_section_description_room": "Selecione os cargos necessários para alterar várias partes da sala", "permissions_section_description_space": "Selecionar os cargos necessários para alterar certas partes do espaço", - "privileged_users_section": "Usuárias/os privilegiadas/os", + "privileged_users_section": "Usuários privilegiados", "redact": "Remover mensagens enviadas por outros", "send_event_type": "Enviar eventos de %(eventType)s", "state_default": "Alterar configurações", @@ -1785,7 +2348,7 @@ "error_join_rule_change_title": "Falha ao atualizar as regras de entrada", "error_join_rule_change_unknown": "Falha desconhecida", "guest_access_warning": "Pessoas com clientes suportados poderão entrar na sala sem ter uma conta registrada.", - "history_visibility_invited": "Apenas participantes (desde que foram convidadas/os)", + "history_visibility_invited": "Somente membros (desde que tenham sido convidados)", "history_visibility_joined": "Apenas participantes (desde que entraram na sala)", "history_visibility_legend": "Quem pode ler o histórico da sala?", "history_visibility_shared": "Apenas participantes (a partir do momento em que esta opção for selecionada)", @@ -1794,12 +2357,23 @@ "join_rule_description": "Decida quem pode entrar em %(roomName)s.", "join_rule_invite": "Privado (convite apenas)", "join_rule_invite_description": "Apenas pessoas convidadas podem entrar.", + "join_rule_knock": "Pedir para participar", + "join_rule_knock_description": "As pessoas não podem participar a menos que o acesso seja concedido.", "join_rule_public_description": "Todos podem encontrar e entrar.", "join_rule_restricted": "Membros do espaço", "join_rule_restricted_description": "Qualquer um em um espaço pode encontrar e se juntar. Edite quais espaços podem ser acessados aqui.", "join_rule_restricted_description_active_space": "Qualquer um em pode encontrar e se juntar. Você pode selecionar outros espaços também.", "join_rule_restricted_description_prompt": "Qualquer um em um espaço pode encontrar e se juntar. Você pode selecionar múltiplos espaços.", "join_rule_restricted_description_spaces": "Espaço com acesso", + "join_rule_restricted_dialog_description": "Decida quais espaços podem acessar essa sala. Se um espaço for selecionado, seus membros poderão encontrar e participar de .", + "join_rule_restricted_dialog_empty_warning": "Você está removendo todos os espaços. O acesso será definido como padrão somente para convidados", + "join_rule_restricted_dialog_filter_placeholder": "Pesquisar espaços", + "join_rule_restricted_dialog_heading_known": "Outros espaços que você conhece", + "join_rule_restricted_dialog_heading_other": "Outros espaços ou salas que você talvez não conheça", + "join_rule_restricted_dialog_heading_room": "Espaços que você conhece que contêm esta sala", + "join_rule_restricted_dialog_heading_space": "Espaços que você conhece que contêm esse espaço", + "join_rule_restricted_dialog_heading_unknown": "Provavelmente são aquelas das quais outros administradores de salas fazem parte.", + "join_rule_restricted_dialog_title": "Selecione espaços", "join_rule_restricted_n_more": { "other": "e %(count)s mais", "one": "& %(count)s mais" @@ -1822,7 +2396,9 @@ }, "join_rule_upgrade_upgrading_room": "Atualizando sala", "public_without_alias_warning": "Para criar um link para esta sala, antes adicione um endereço.", - "strict_encryption": "Nunca envie mensagens criptografadas a partir desta sessão para sessões não confirmadas nessa sala", + "publish_room": "Torne esta sala visível no diretório de salas públicas.", + "publish_space": "Torne este espaço visível no diretório de salas públicas.", + "strict_encryption": "Envie mensagens somente para usuários verificados.", "title": "Segurança e privacidade" }, "title": "Configurações da sala - %(roomName)s", @@ -1839,6 +2415,12 @@ "history_visibility_anyone_space_description": "Permite que pessoas vejam seu espaço antes de entrarem.", "history_visibility_anyone_space_recommendation": "Recomendado para espaços públicos.", "title": "Visibilidade" + }, + "voip": { + "call_type_section": "Tipo de chamada", + "enable_element_call_caption": "%(brand)s é criptografado de ponta a ponta, mas atualmente está limitado a um número menor de usuários.", + "enable_element_call_label": "Ativar %(brand)s como uma opção de chamada adicional nesta sala", + "enable_element_call_no_permissions_tooltip": "Você não tem permissões suficientes para alterar isso." } }, "room_summary_card_back_action_label": "Informação da sala", @@ -1870,8 +2452,16 @@ "recent_changes_heading": "Alterações recentes que ainda não foram recebidas", "title": "O servidor não está respondendo" }, + "service_worker_error": { + "description": "%(brand)s requer um operador de serviço para carregar mídia autenticada de repositórios de conteúdo Matrix. Isso não é suportado pelo seu navegador, portanto, pode ocorrer uma falha no carregamento de mídia.", + "title": "Falha ao carregar o operador de serviço" + }, "seshat": { "error_initialising": "Falha na inicialização da pesquisa por mensagem, confire suas configurações para mais informações", + "reset_button": "Redefinir armazenamento de eventos", + "reset_description": "Você provavelmente não deseja redefinir seu armazenamento de índice de eventos", + "reset_explainer": "Se você fizer isso, observe que nenhuma de suas mensagens será excluída, mas a experiência de pesquisa poderá ser prejudicada por alguns instantes enquanto o índice é recriado", + "reset_title": "Redefinir armazenamento de eventos?", "warning_kind_files": "Esta versão do %(brand)s não permite visualizar alguns arquivos criptografados", "warning_kind_files_app": "Use o app para Computador para ver todos os arquivos criptografados", "warning_kind_search": "Esta versão do %(brand)s não permite buscar mensagens criptografadas", @@ -1882,33 +2472,45 @@ "access_token_detail": "Seu token de acesso dá acesso total à sua conta. Não o compartilhe com ninguém.", "brand_version": "versão do %(brand)s:", "clear_cache_reload": "Limpar cache e recarregar", + "crypto_version": "Versão Crypto:", "dialog_title": "Configurações: Ajuda e Sobre", "help_link": "Para obter ajuda com o uso do %(brand)s, clique aqui.", + "homeserver": "O servidor doméstico é %(homeserverUrl)s", + "identity_server": "O servidor de identidade é %(identityServerUrl)s", "title": "Ajuda e sobre", "versions": "Versões" } }, "settings": { "account": { - "dialog_title": "Configurações: Conta" + "dialog_title": "Configurações: Conta", + "title": "Conta" }, "all_rooms_home": "Mostrar todas as salas no Início", "all_rooms_home_description": "Todas as salas que você estiver presente aparecerão no Início.", "always_show_message_timestamps": "Sempre mostrar as datas das mensagens", "appearance": { + "bundled_emoji_font": "Use a fonte de emoji incluída no pacote", + "compact_layout": "Mostrar texto e mensagens compactas", + "compact_layout_description": "O layout moderno deve ser selecionado para usar esse recurso.", "custom_font": "Usar uma fonte do sistema", "custom_font_description": "Defina o nome de uma fonte instalada no seu sistema e o %(brand)s tentará usá-la.", "custom_font_name": "Nome da fonte do sistema", "custom_font_size": "Usar tamanho personalizado", + "custom_theme_add": "Adicionar tema personalizado", + "custom_theme_downloading": "Baixando tema personalizado...", "custom_theme_error_downloading": "Erro ao baixar o tema", + "custom_theme_help": "Insira o URL de um tema personalizado que você deseja aplicar.", "custom_theme_invalid": "Esquema inválido de tema.", "dialog_title": "Configurações: Aparência", "font_size": "Tamanho da fonte", + "font_size_default": "%(fontSize)s (padrão)", + "high_contrast": "Alto contraste", "image_size_default": "Padrão", "image_size_large": "Grande", "layout_bubbles": "Balões de mensagem", "layout_irc": "IRC (experimental)", - "match_system_theme": "Se adaptar ao tema do sistema", + "match_system_theme": "Se adaptar ao padrão do sistema", "timeline_image_size": "Tamanho da imagem na linha do tempo" }, "automatic_language_detection_syntax_highlight": "Ativar detecção automática de idioma para ressaltar erros de ortografia", @@ -1918,9 +2520,80 @@ "code_block_expand_default": "Expandir blocos de código por padrão", "code_block_line_numbers": "Mostrar o número da linha em blocos de código", "disable_historical_profile": "Mostrar a foto e o nome do perfil atual dos usuários no histórico de mensagens", + "discovery": { + "title": "Como encontrar você" + }, "emoji_autocomplete": "Ativar sugestões de emojis ao digitar", "enable_markdown": "Habilitar markdown", - "enable_markdown_description": "Inicie mensagens com /plain para enviar sem marcação.", + "enable_markdown_description": "Inicie as mensagens com /plain para enviar sem markdown.", + "encryption": { + "advanced": { + "breadcrumb_first_description": "Os detalhes da sua conta, contatos, preferências e lista de bate-papo serão mantidos", + "breadcrumb_page": "Redefinir criptografia", + "breadcrumb_second_description": "Você perderá qualquer histórico de mensagens armazenado somente no servidor", + "breadcrumb_third_description": "Você precisará verificar todos os seus dispositivos e contatos existentes novamente.", + "breadcrumb_title": "Tem certeza de que deseja redefinir sua identidade?", + "breadcrumb_title_forgot": "Esqueceu sua chave de recuperação? Você precisará redefinir sua identidade.", + "breadcrumb_title_sync_failed": "Falha ao sincronizar o armazenamento de chaves. Você precisa redefinir sua identidade.", + "breadcrumb_warning": "Faça isso somente se você acreditar que sua conta foi comprometida.", + "details_title": "Detalhes da criptografia", + "do_not_close_warning": "Não feche essa janela até que a redefinição seja concluída", + "export_keys": "Exportar chaves", + "import_keys": "Importar chaves", + "other_people_device_description": "Por padrão, em salas criptografadas, não envie mensagens criptografadas para ninguém até que você as tenha verificado", + "other_people_device_label": "Nunca envie mensagens criptografadas para dispositivos não verificados", + "other_people_device_title": "Dispositivos de outras pessoas", + "reset_identity": "Redefinir identidade criptográfica", + "reset_in_progress": "Redefinição em andamento...", + "session_id": "ID da sessão:", + "session_key": "Chave da sessão:", + "title": "Avançado" + }, + "confirm_key_storage_off": "Tem certeza de que deseja manter o armazenamento de chaves desativado?", + "confirm_key_storage_off_description": "Se você sair de todos os seus dispositivos, perderá o histórico de mensagens e precisará verificar todos os seus contatos existentes novamente. Saiba mais", + "delete_key_storage": { + "breadcrumb_page": "Excluir armazenamento de chaves", + "confirm": "Excluir armazenamento de chaves", + "description": "A exclusão do armazenamento de chaves removerá sua identidade criptográfica e as chaves de mensagem do servidor e desativará os seguintes recursos de segurança:", + "list_first": "Você não terá histórico de mensagens criptografadas em novos dispositivos", + "list_second": "Você perderá o acesso às suas mensagens criptografadas se estiver desconectado de %(brand)s em todos os lugares", + "title": "Tem certeza de que deseja desativar o armazenamento de chaves e excluí-lo?" + }, + "device_not_verified_button": "Verificar este dispositivo", + "device_not_verified_description": "Você precisa verificar este dispositivo para visualizar suas configurações de criptografia.", + "device_not_verified_title": "Dispositivo não verificado", + "dialog_title": "Configurações: Criptografia", + "key_storage": { + "allow_key_storage": "Permitir o armazenamento de chaves", + "description": "Armazene sua identidade criptográfica e chaves de mensagem com segurança no servidor. Isso permitirá que você visualize seu histórico de mensagens em quaisquer novos dispositivos. Saber mais", + "title": "Armazenamento de chaves" + }, + "recovery": { + "change_recovery_confirm_button": "Confirme a nova chave de recuperação", + "change_recovery_confirm_description": "Insira sua nova chave de recuperação abaixo para concluir. Seu antigo não funcionará mais.", + "change_recovery_confirm_title": "Insira sua nova chave de recuperação", + "change_recovery_key": "Alterar chave de recuperação", + "change_recovery_key_description": "Anote esta nova chave de recuperação em algum lugar seguro. Então clique em Continue para confirmar a alteração.", + "change_recovery_key_title": "Alterar a chave de recuperação?", + "description": "Recupere sua identidade criptográfica e histórico de mensagens com uma chave de recuperação se você tiver perdido todos os seus dispositivos existentes.", + "enter_key_error": "A chave de recuperação que você inseriu não está correta.", + "enter_recovery_key": "Insira a chave de recuperação", + "forgot_recovery_key": "Esqueceu a chave de recuperação?", + "key_storage_warning": "Seu armazenamento de chaves está fora de sincronia. Clique em um dos botões abaixo para corrigir o problema.", + "save_key_description": "Não compartilhe isso com ninguém!", + "save_key_title": "Chave de recuperação", + "set_up_recovery": "Configurar a recuperação", + "set_up_recovery_confirm_button": "Concluir a configuração", + "set_up_recovery_confirm_description": "Digite a chave de recuperação mostrada na tela anterior para concluir a configuração da recuperação.", + "set_up_recovery_confirm_title": "Digite sua chave de recuperação para confirmar", + "set_up_recovery_description": "Seu armazenamento de chaves é protegido por uma chave de recuperação. Se precisar de uma nova chave de recuperação após a configuração, você pode recriá-la selecionando '%(changeRecoveryKeyButton)s'.", + "set_up_recovery_save_key_description": "Anote essa chave de recuperação em algum lugar seguro, como um gerenciador de senhas, uma nota criptografada ou um cofre físico.", + "set_up_recovery_save_key_title": "Salve sua chave de recuperação", + "set_up_recovery_secondary_description": "Depois de clicar em continuar, geraremos uma chave de recuperação para você.", + "title": "Recuperação" + }, + "title": "Criptografia" + }, "general": { "account_management_section": "Gerenciamento da Conta", "account_section": "Conta", @@ -1933,6 +2606,14 @@ "add_msisdn_dialog_title": "Adicionar número de telefone", "add_msisdn_instructions": "Digite o código de confirmação enviado por mensagem de texto para +%(msisdn)s.", "add_msisdn_misconfigured": "O fluxo de adição/vinculação com MSISDN está mal configurado", + "allow_spellcheck": "Permitir verificação ortográfica", + "application_language": "Idioma do aplicativo", + "application_language_reload_hint": "O aplicativo será recarregado após selecionar outro idioma", + "avatar_remove_progress": "Removendo a imagem...", + "avatar_save_progress": "Enviando imagem ...", + "avatar_upload_error_text": "O formato do arquivo não é suportado ou a imagem é maior que %(size)s.", + "avatar_upload_error_text_generic": "O formato do arquivo pode não ser suportado.", + "avatar_upload_error_title": "Não foi possível carregar a imagem de perfil", "confirm_adding_email_body": "Clique no botão abaixo para confirmar a adição deste endereço de e-mail.", "confirm_adding_email_title": "Confirmar a inclusão de e-mail", "deactivate_confirm_body": "Tem certeza de que deseja desativar sua conta? Isso é irreversível.", @@ -1952,7 +2633,10 @@ "discovery_email_verification_instructions": "Verifique o link na sua caixa de e-mails", "discovery_msisdn_empty": "As opções de descoberta aparecerão depois que você adicionar um número de telefone.", "discovery_needs_terms": "Concorde com os Termos de Serviço do servidor de identidade (%(serverName)s), para que você possa ser descoberto por endereço de e-mail ou por número de celular.", + "discovery_needs_terms_title": "Deixe que as pessoas encontrem você", "display_name": "Nome de exibição", + "display_name_error": "Não foi possível definir o nome de exibição", + "email_adding_unsupported_by_hs": "Este servidor doméstico não suporta a adição de endereços de e-mail à sua conta.", "email_address_in_use": "Este endereço de email já está em uso", "email_address_label": "Endereço de e-mail", "email_not_verified": "Seu endereço de e-mail ainda não foi confirmado", @@ -1977,7 +2661,9 @@ "error_share_msisdn_discovery": "Não foi possível compartilhar o número de celular", "identity_server_no_token": "Nenhum token de acesso à identidade foi encontrado", "identity_server_not_set": "Servidor de identidade não definido", + "invalid_phone_number": "O número de telefone fornecido não parece ser válido.", "language_section": "Idioma", + "msisdn_adding_unsupported_by_hs": "Este servidor doméstico não suporta a adição de números de telefone à sua conta.", "msisdn_in_use": "Este número de telefone já está em uso", "msisdn_label": "Número de telefone", "msisdn_verification_field_label": "Código de confirmação", @@ -1986,12 +2672,16 @@ "oidc_manage_button": "Gerenciar conta", "password_change_section": "Defina uma nova senha da conta...", "password_change_success": "Sua senha foi alterada com sucesso.", + "personal_info": "Informações pessoais", "profile_subtitle": "É assim que você aparece para outras pessoas no aplicativo.", + "profile_subtitle_oidc": "Sua conta é gerenciada separadamente por um provedor de identidade e, portanto, algumas de suas informações pessoais não podem ser alteradas aqui.", "remove_email_prompt": "Remover %(email)s?", "remove_msisdn_prompt": "Remover %(phone)s?", - "spell_check_locale_placeholder": "Escolha um local" + "spell_check_locale_placeholder": "Escolha um local", + "unable_to_load_emails": "Não é possível carregar endereços de e-mail", + "unable_to_load_msisdns": "Não é possível carregar números de telefone", + "username": "Nome de usuário" }, - "image_thumbnails": "Mostrar miniaturas e resumos para imagens", "inline_url_previews_default": "Ativar, por padrão, a visualização de resumo de links", "inline_url_previews_room": "Ativar, para todos os participantes desta sala, a visualização de links", "inline_url_previews_room_account": "Ativar, para esta sala, a visualização de links (só afeta você)", @@ -2014,7 +2704,7 @@ "enter_phrase_title": "Digite uma frase de segurança", "enter_phrase_to_confirm": "Digite sua frase de segurança uma segunda vez para confirmá-la.", "generate_security_key_description": "Geraremos uma chave de segurança para você armazenar em algum lugar seguro, como um gerenciador de senhas ou um cofre.", - "generate_security_key_title": "Gerar uma Chave de Segurança", + "generate_security_key_title": "Gerar uma chave de recuperação", "pass_phrase_match_failed": "Isto não corresponde.", "pass_phrase_match_success": "Isto corresponde!", "phrase_strong_enough": "Ótimo! Essa frase de segurança parece ser segura o suficiente.", @@ -2023,11 +2713,11 @@ "set_phrase_again": "Voltar para configurar novamente.", "settings_reminder": "Você também pode configurar o Backup online & configurar as suas senhas nas Configurações.", "title_confirm_phrase": "Confirme a frase de segurança", - "title_save_key": "Salve sua Chave de Segurança", + "title_save_key": "Salve sua chave de recuperação", "title_set_phrase": "Defina uma frase de segurança", "unable_to_setup": "Não foi possível definir o armazenamento secreto", "use_different_passphrase": "Usar uma frase secreta diferente?", - "use_phrase_only_you_know": "Use uma frase secreta que apenas você conhece, e opcionalmente salve uma Chave de Segurança para acessar o backup." + "use_phrase_only_you_know": "Use uma frase secreta que só você conhece e, opcionalmente, salve uma Chave de Recuperação para usar como backup." } }, "key_export_import": { @@ -2054,6 +2744,14 @@ "labs_mjolnir": { "dialog_title": "Configurações: Usuários ignorados" }, + "media_preview": { + "hide_avatars": "Ocultar avatares da sala e do convidador", + "hide_media": "Ocultar sempre", + "media_preview_description": "Uma mídia oculta sempre pode ser exibida tocando nela", + "media_preview_label": "Mostrar mídia na linha do tempo", + "show_in_private": "Em salas privadas", + "show_media": "Mostrar sempre" + }, "notifications": { "default_setting_description": "Essa configuração será aplicada por padrão a todas as suas salas.", "default_setting_section": "Quero ser notificado sobre (configuração padrão)", @@ -2101,7 +2799,7 @@ "rule_contains_user_name": "Mensagens contendo meu nome de usuário", "rule_encrypted": "Mensagens criptografadas em salas", "rule_encrypted_room_one_to_one": "Mensagens criptografadas em conversas individuais", - "rule_invite_for_me": "Quando eu for convidada(o) a uma sala", + "rule_invite_for_me": "Quando sou convidado para uma sala", "rule_message": "Mensagens em salas", "rule_room_one_to_one": "Mensagens em conversas individuais", "rule_roomnotif": "Mensagens contendo @room", @@ -2117,6 +2815,7 @@ "code_blocks_heading": "Blocos de código", "compact_modern": "Usar um layout \"moderno\" mais compacto", "composer_heading": "Campo de texto", + "default_timezone": "Navegador padrão (%(timezone)s)", "dialog_title": "Configurações: Preferências", "enable_hardware_acceleration": "Habilitar aceleração de hardware", "enable_tray_icon": "Mostrar o ícone da bandeja e minimizar a janela ao fechar", @@ -2124,6 +2823,7 @@ "keyboard_view_shortcuts_button": "Para ver todos os atalhos do teclado, clique aqui.", "media_heading": "Imagens, GIFs e vídeos", "presence_description": "Compartilhe sua atividade e status com outras pessoas.", + "publish_timezone": "Publique o fuso horário no perfil público", "rm_lifetime": "Duração do marcador de leitura (ms)", "rm_lifetime_offscreen": "Vida útil do marcador de leitura fora da tela (ms)", "room_directory_heading": "Diretório de salas", @@ -2131,7 +2831,8 @@ "show_avatars_pills": "Mostrar avatares em menções de usuários, salas e eventos", "show_polls_button": "Mostrar botão de enquetes", "surround_text": "Circule o texto selecionado ao digitar caracteres especiais", - "time_heading": "Exibindo tempo" + "time_heading": "Exibindo tempo", + "user_timezone": "Definir fuso horário" }, "prompt_invite": "Avisar antes de enviar convites para IDs da Matrix potencialmente inválidas", "replace_plain_emoji": "Substituir automaticamente os emojis em texto", @@ -2140,6 +2841,8 @@ "bulk_options_accept_all_invites": "Aceite todos os convites de %(invitedRooms)s", "bulk_options_reject_all_invites": "Recusar todos os %(invitedRooms)s convites", "bulk_options_section": "Opções em massa", + "dehydrated_device_description": "O recurso de dispositivo offline permite que você receba mensagens criptografadas mesmo quando você não está conectado a nenhum dispositivo", + "dehydrated_device_enabled": "Dispositivo offline habilitado", "dialog_title": "Configurações: Segurança e Privacidade", "e2ee_default_disabled_warning": "O administrador do servidor desativou a criptografia de ponta a ponta por padrão em salas privadas e em conversas.", "enable_message_search": "Ativar busca de mensagens em salas criptografadas", @@ -2168,7 +2871,7 @@ "message_search_unsupported_web": "%(brand)s não consegue pesquisar as mensagens criptografadas armazenadas localmente em um navegador de internet. Use o %(brand)s para Computador para que as mensagens criptografadas sejam exibidas nos resultados de buscas.", "record_session_details": "Registre o nome, a versão e o URL do cliente para reconhecer as sessões com mais facilidade no gerenciador de sessões", "send_analytics": "Enviar dados analíticos", - "strict_encryption": "Nunca envie mensagens criptografadas a partir desta sessão para sessões não confirmadas" + "strict_encryption": "Envie mensagens somente para usuários verificados" }, "send_read_receipts": "Enviar confirmações de leitura", "send_read_receipts_unsupported": "Seu servidor não suporta a desativação do envio de confirmações de leitura.", @@ -2216,6 +2919,7 @@ "inactive_sessions_list_description": "Considere sair de sessões antigas (%(inactiveAgeDays)sdias ou mais) que você não usa mais.", "ip": "Endereço de IP", "last_activity": "Última atividade", + "manage": "Gerenciar esta sessão", "mobile_session": "Sessão móvel", "n_sessions_selected": { "one": "%(count)ssessão selecionada", @@ -2242,6 +2946,7 @@ "sign_in_with_qr": "Vincular novo dispositivo", "sign_in_with_qr_button": "Mostrar código QR", "sign_in_with_qr_description": "Use um código QR para fazer login em outro dispositivo e configurar mensagens seguras.", + "sign_in_with_qr_unsupported": "Não é suportado pelo provedor da sua conta", "sign_out": "Sair desta sessão", "sign_out_all_other_sessions": "Sair de todas as outras sessões (%(otherSessionsCount)s)", "sign_out_confirm_description": { @@ -2281,6 +2986,7 @@ "show_redaction_placeholder": "Mostrar um marcador para as mensagens removidas", "show_stickers_button": "Mostrar o botão de figurinhas", "show_typing_notifications": "Mostrar quando alguém estiver digitando", + "showbold": "Mostrar todas as atividades na lista de salas (pontos ou número de mensagens não lidas)", "sidebar": { "dialog_title": "Configurações: Barra lateral", "metaspaces_favourites_description": "Agrupe todas as suas salas e pessoas favoritas em um só lugar.", @@ -2291,6 +2997,9 @@ "metaspaces_orphans_description": "Agrupe todas as suas salas que não fazem parte de um espaço em um só lugar.", "metaspaces_people_description": "Agrupe todas as suas pessoas em um só lugar.", "metaspaces_subsection": "Espaços para mostrar", + "metaspaces_video_rooms": "Salas de vídeo e conferências", + "metaspaces_video_rooms_description": "Agrupar todas as salas de vídeo e conferências privadas.", + "metaspaces_video_rooms_description_invite_extension": "Nas conferências, você pode convidar pessoas fora da Matrix.", "spaces_explainer": "Os espaços são formas de agrupar salas e pessoas. Além dos espaços em que você está, você também pode usar alguns pré-construídos.", "title": "Barra lateral" }, @@ -2328,14 +3037,19 @@ "warning": "AVISO:" }, "share": { + "link_copied": "Link copiado", "permalink_message": "Link da mensagem selecionada", "permalink_most_recent": "Link da mensagem mais recente", + "share_call": "Link de convite para conferência", + "share_call_subtitle": "Link para usuários externos participarem da chamada sem uma conta Matrix:", + "title_link": "Compartilhar link", "title_message": "Compartilhar Mensagem da Sala", "title_room": "Compartilhar sala", "title_user": "Compartilhar usuário" }, "slash_command": { "addwidget": "Adiciona um widget personalizado na sala por meio de um link", + "addwidget_iframe_missing_src": "iframe não tem atributo src", "addwidget_invalid_protocol": "Forneça o link de um widget com https:// ou http://", "addwidget_missing_url": "Forneça o link de um widget ou de um código de incorporação", "addwidget_no_permissions": "Você não pode modificar widgets nesta sala.", @@ -2349,15 +3063,18 @@ "command_error": "Erro de comando", "converttodm": "Converte a sala para uma conversa", "converttoroom": "Converte a conversa para uma sala", + "could_not_find_room": "Não foi possível encontrar a sala", "deop": "Retira o nível de moderador do usuário com o ID informado", "devtools": "Abre a caixa de diálogo Ferramentas do desenvolvedor", "discardsession": "Força a atual sessão da comunidade em uma sala criptografada a ser descartada", "error_invalid_rendering_type": "Erro de comando: Não é possível manipular o tipo (%(renderingType)s)", + "error_invalid_room": "O comando falhou: Não foi possível encontrar a sala (%(roomId)s)", "error_invalid_runfn": "Erro de comando: Não é possível manipular o comando de barra.", + "error_invalid_user_in_room": "Não foi possível encontrar o usuário na sala", "help": "Exibe a lista de comandos com usos e descrições", "help_dialog_title": "Ajuda com Comandos", "holdcall": "Pausa a chamada na sala atual", - "html": "Envia uma mensagem como HTML, sem formatação", + "html": "Envia uma mensagem como html, sem interpretá-la como markdown", "ignore": "Bloqueia um usuário, escondendo as mensagens dele de você", "ignore_dialog_description": "Agora você está bloqueando %(userId)s", "ignore_dialog_title": "Usuário bloqueado", @@ -2365,6 +3082,7 @@ "invite_3pid_needs_is_error": "Use um servidor de identidade para convidar pessoas por e-mail. Gerencie nas Configurações.", "invite_3pid_use_default_is_title": "Usar um servidor de identidade", "invite_3pid_use_default_is_title_description": "Use um servidor de identidade para convidar por e-mail. Clique em continuar para usar o servidor de identidade padrão (%(defaultIdentityServerName)s) ou gerencie nas Configurações.", + "invite_failed": "O usuário (%(user)s) não foi convidado para %(roomId)s, mas o utilitário de convite não apresentou nenhum erro", "join": "Entra em uma sala com o endereço fornecido", "jumptodate": "Ir para a data especificada na linha do tempo", "jumptodate_invalid_input": "Não foi possível entender a data fornecida (%(inputDate)s). Tente usando o formato AAAA-MM-DD.", @@ -2378,10 +3096,10 @@ "no_active_call": "Nenhuma chamada ativa nesta sala", "op": "Define o nível de permissões de um usuário", "part_unknown_alias": "Endereço da sala não reconhecido: %(roomAlias)s", - "plain": "Envia uma mensagem de texto sem formatação", + "plain": "Envia uma mensagem como texto simples, sem interpretá-la como markdown", "query": "Abre um chat com determinada pessoa", "query_not_found_phone_number": "Não foi possível encontrar o ID Matrix pelo número de telefone", - "rageshake": "Envia um relatório de erro", + "rageshake": "Enviar um relatório de bug com logs", "rainbow": "Envia a mensagem colorida como arco-íris", "rainbowme": "Envia o emoji colorido como um arco-íris", "remove": "Remove desta sala o usuário com o ID determinado", @@ -2409,46 +3127,79 @@ "upgraderoom": "Atualiza a sala para uma nova versão", "upgraderoom_permission_error": "Você não tem as permissões necessárias para usar este comando.", "usage": "Uso", + "view": "Visualizações da sala com o endereço fornecido", "whois": "Exibe informação sobre um usuário" }, + "sliding_sync_legacy_no_longer_supported": "A sliding sync antiga não é mais suportada: saia e entre novamente para ativar o novo sinalizador de sliding sync", "space": { "add_existing_room_space": { + "create": "Quer​•​adicionar​•​uma​•​nova​•​sala?", "create_prompt": "Criar uma nova sala", "dm_heading": "Conversas", + "error_heading": "Nem​•​todos​•​os​•​selecionados​•​foram​•​adicionados", "progress_text": { "one": "Adicionando sala…", "other": "Adicionando salas… (%(progress)s de %(count)s)" }, "space_dropdown_label": "Seleção de Espaços", - "space_dropdown_title": "Adicionar salas existentes" + "space_dropdown_title": "Adicionar salas existentes", + "subspace_moved_note": "A​•​adição​•​de​•​espaços​•​foi​•​movida." }, "add_existing_subspace": { "create_button": "Criar um novo espaço", + "create_prompt": "Quer​•​adicionar​•​um​•​novo​•​espaço?", "filter_placeholder": "Procure por espaços", "space_dropdown_title": "Adicionar espaço existente" }, "context_menu": { + "devtools_open_timeline": "Veja​•​o​•​cronograma​•​da​•​sala​•​(devtools)", "explore": "Explorar salas", + "home": "Espaço​•​doméstico", + "manage_and_explore": "Gerencie​•​e​•​explore​•​salas", "options": "Opções do espaço" }, + "failed_load_rooms": "Falha​•​ao​•​carregar​•​a​•​lista​•​de​•​salas.", + "failed_remove_rooms": "Falha​•​ao​•​remover​•​algumas​•​salas.​•​Tente​•​novamente​•​mais​•​tarde", + "incompatible_server_hierarchy": "Seu​•​servidor​•​não​•​suporta​•​a​•​exibição​•​de​•​hierarquias​•​de​•​espaço.", "invite": "Convidar pessoas", "invite_description": "Convidar com email ou nome de usuário", "invite_link": "Compartilhar link de convite", "joining_space": "Juntando-se", "landing_welcome": "Boas-vindas ao ", "leave_dialog_action": "Sair do espaço", + "leave_dialog_description": "Você​•​está​•​prestes​•​a​•​deixar​•​.", + "leave_dialog_only_admin_room_warning": "Você​•​é​•​o​•​único​•​administrador​•​de​•​algumas​•​das​•​salas​•​ou​•​espaços​•​que​•​deseja​•​deixar.​•​Deixá-los​•​os​•​deixará​•​sem​•​administradores.", + "leave_dialog_only_admin_warning": "Você​•​é​•​o​•​único​•​administrador​•​deste​•​espaço.​•​Sair​•​dele​•​significa​•​que​•​ninguém​•​terá​•​controle​•​sobre​•​ele.", "leave_dialog_option_all": "Sair de todas as salas", + "leave_dialog_option_intro": "Você​•​gostaria​•​de​•​deixar​•​as​•​salas​•​neste​•​espaço?", "leave_dialog_option_none": "Não saia de nenhuma sala", "leave_dialog_option_specific": "Sair de algumas salas", "leave_dialog_public_rejoin_warning": "Você não poderá entrar novamente a menos que seja convidado novamente.", + "leave_dialog_title": "Sair​•de ​%(spaceName)s", + "mark_suggested": "Marcar​•​como​•​sugerido", + "no_search_result_hint": "Você​•​pode​•​tentar​•​uma​•​pesquisa​•​diferente​•​ou​•​verificar​•​se​•​há​•​erros​•​de​•​digitação.", + "preferences": { + "sections_section": "Seções​•​para​•​mostrar", + "show_people_in_space": "Isso​•​agrupa​•​seus​•​bate-papos​•​com​•​membros​•​desse​•​espaço.​•​Desativar​•​isso​•​ocultará​•​esses​•​bate-papos​•​da​•​sua​•​visão​•​de%(spaceName)s." + }, "room_filter_placeholder": "Buscar salas", "search_children": "Pesquisar %(spaceName)s", "search_placeholder": "Pesquisar nomes e descrições", + "select_room_below": "Selecione​•​uma​•​sala​•​abaixo​•​primeiro", "share_public": "Compartilhar o seu espaço público", - "unmark_suggested": "Marcar como não sugerido" + "suggested": "Sugerido", + "suggested_tooltip": "Esta​•​sala​•​é​•​sugerida​•​como​•​uma​•​boa​•​opção​•​para​•​entrar", + "title_when_query_available": "Resultados", + "title_when_query_unavailable": "Salas​•​e​•​espaços", + "unmark_suggested": "Marcar como não sugerido", + "user_lacks_permission": "Você​•​não​•​tem​•​permissão" + }, + "space_settings": { + "title": "Configurações - %(spaceName)s" }, "spaces": { "error_no_permission_add_room": "Você não tem permissão para adicionar salas neste espaço", + "error_no_permission_add_space": "Você não tem permissão para adicionar espaços a este espaço", "error_no_permission_create_room": "Você não tem permissão para criar novas salas neste espaço", "error_no_permission_invite": "Você não tem permissão para convidar pessoas para este espaço" }, @@ -2461,26 +3212,41 @@ "network_dropdown_available_invalid": "Não foi possível encontrar este servidor ou sua lista de salas", "network_dropdown_available_invalid_forbidden": "Você não tem a permissão para ver a lista de salas deste servidor", "network_dropdown_available_valid": "Muito bem", + "network_dropdown_remove_server_adornment": "Remover servidor “%(roomServer)s”", "network_dropdown_required_invalid": "Digite um nome de servidor", + "network_dropdown_selected_label": "Mostrar: Salas Matrix", + "network_dropdown_selected_label_instance": "Mostrar: %(instance)s salas do servidor (%(server)s )", "network_dropdown_your_server_description": "Seu servidor" } }, "spotlight_dialog": { + "cant_find_person_helpful_hint": "Se você não conseguir ver quem está procurando, envie seu link de convite.", "cant_find_room_helpful_hint": "Se você não conseguir encontrar a sala que está procurando, peça um convite ou crie uma nova sala.", + "copy_link_text": "Copiar link de convite", "count_of_members": { "one": "%(count)s Membro", "other": "%(count)s Membros" }, "create_new_room_button": "Criar nova sala", + "failed_querying_public_rooms": "Falha ao consultar salas públicas", + "failed_querying_public_spaces": "Falha ao consultar espaços públicos", + "group_chat_section_title": "Outras opções", "heading_with_query": "Use \"%(query)s\" para pesquisar", "heading_without_query": "Pesquisar por", "join_button_text": "Junte-se a %(roomAddress)s", - "message_search_section_title": "Outras pesquisas", + "keyboard_scroll_hint": "Use para rolar", + "messages_label": "Mensagens", + "other_rooms_in_space": "Outras salas em %(spaceName)s", "public_rooms_label": "Salas públicas", + "public_spaces_label": "Espaços públicos", "recent_searches_section_title": "Pesquisas recentes", "recently_viewed_section_title": "Visualizado recentemente", + "remove_filter": "Remover filtro de pesquisa para %(filter)s", + "result_may_be_hidden_privacy_warning": "Alguns resultados podem estar ocultos por questões de privacidade", + "result_may_be_hidden_warning": "Alguns resultados podem estar ocultos", "search_dialog": "Diálogo de pesquisa", - "search_messages_hint": "Para pesquisar mensagens, procure esse ícone na parte superior de uma sala " + "spaces_title": "Espaços em que você está", + "start_group_chat_button": "Inicie um bate-papo em grupo" }, "stickers": { "empty": "No momento, você não tem pacotes de figurinhas ativados", @@ -2497,14 +3263,15 @@ "integration_manager": "Use bots, integrações, widgets e pacotes de figurinhas", "intro": "Para continuar, você precisa aceitar os termos deste serviço.", "summary_identity_server_1": "Encontre outras pessoas por telefone ou e-mail", - "summary_identity_server_2": "Seja encontrada/o por número de celular ou por e-mail", + "summary_identity_server_2": "Seja encontrado por telefone ou e-mail", "tac_button": "Revise os termos e condições", "tac_description": "Para continuar usando o servidor local %(homeserverDomain)s, você deve ler e concordar com nossos termos e condições.", "tac_title": "Termos e Condições", "tos": "Termos de serviço" }, "theme": { - "light_high_contrast": "Claro (alto contraste)" + "light_high_contrast": "Claro (alto contraste)", + "match_system": "Padrão do sistema" }, "thread_view_back_action_label": "Voltar ao tópico", "threads": { @@ -2526,9 +3293,7 @@ "threads_activity_centre": { "header": "Atividade de tópicos", "no_rooms_with_threads_notifs": "Você ainda não tem salas com notificações de tópicos.", - "no_rooms_with_unread_threads": "Você ainda não tem salas com tópicos não lidos.", - "release_announcement_description": "As notificações de tópicos foram movidas, encontre-as aqui a partir de agora.", - "release_announcement_header": "Centro de Atividades de Tópicos" + "no_rooms_with_unread_threads": "Você ainda não tem salas com tópicos não lidos." }, "time": { "about_day_ago": "há aproximadamente um dia", @@ -2562,7 +3327,12 @@ "context_menu": { "collapse_reply_thread": "Recolher tópico de resposta", "external_url": "Link do código-fonte", - "resent_unsent_reactions": "Reenviar %(unsentCount)s reações" + "open_in_osm": "Abrir no OpenStreetMap", + "report": "Denunciar", + "resent_unsent_reactions": "Reenviar %(unsentCount)s reações", + "show_url_preview": "Mostrar pré-visualização", + "view_related_event": "Exibir evento relacionado", + "view_source": "Ver fonte" }, "creation_summary_dm": "%(creator)s criou esta conversa.", "creation_summary_room": "%(creator)s criou e configurou esta sala.", @@ -2570,10 +3340,17 @@ "blocked": "O remetente bloqueou você de receber esta mensagem porque seu dispositivo não foi verificado", "historical_event_no_key_backup": "Mensagens históricas não estão disponíveis neste dispositivo", "historical_event_unverified_device": "Você precisa verificar este dispositivo para acessar as mensagens históricas", - "historical_event_user_not_joined": "Você não tem acesso a esta mensagem" + "historical_event_user_not_joined": "Você não tem acesso a esta mensagem", + "sender_identity_previously_verified": "A identidade verificada do remetente foi alterada", + "sender_unsigned_device": "Enviado de um dispositivo inseguro.", + "unable_to_decrypt": "Não foi possível descriptografar a mensagem" }, + "disambiguated_profile": "%(displayName)s (%(matrixId)s)", "download_action_decrypting": "Decriptando", "download_action_downloading": "Baixando", + "download_failed": "Falha no download", + "download_failed_description": "Ocorreu um erro ao baixar esse arquivo", + "e2e_state": "Estado da criptografia de ponta a ponta", "edits": { "tooltip_label": "Editado em %(date)s. Clique para ver edições.", "tooltip_sub": "Clicar para ver edições", @@ -2582,24 +3359,34 @@ "error_no_renderer": "Este evento não pôde ser exibido", "error_rendering_message": "Não foi possível carregar esta mensagem", "historical_messages_unavailable": "Você não pode ver as mensagens anteriores", + "in_room_name": " em %(room)s", "io.element.widgets.layout": "%(senderName)s atualizou o layout da sala", + "late_event_separator": "Enviado originalmente em %(dateTime)s", "load_error": { "no_permission": "Não foi possível carregar um trecho específico da conversa desta sala, porque parece que você não tem permissão para ler a mensagem em questão.", "title": "Não foi possível carregar um trecho da conversa", "unable_to_find": "Não foi possível carregar um trecho específico da conversa desta sala." }, "m.audio": { + "error_downloading_audio": "Erro ao baixar o áudio", "error_processing_audio": "Erro ao processar a mensagem de áudio", - "error_processing_voice_message": "Erro ao processar a mensagem de voz" + "error_processing_voice_message": "Erro ao processar a mensagem de voz", + "unnamed_audio": "Áudio sem nome" + }, + "m.beacon_info": { + "view_live_location": "Ver localização ao vivo" }, "m.call": { + "video_call_ended": "Chamada de vídeo encerrada", "video_call_started": "Chamada de vídeo iniciada em %(roomName)s.", + "video_call_started_text": "%(name)s iniciou uma chamada de vídeo", "video_call_started_unsupported": "Chamada de vídeo iniciada em %(roomName)s. (não compatível com este navegador)" }, "m.call.hangup": { "dm": "Chamada encerrada" }, "m.call.invite": { + "answered_elsewhere": "Respondido em outro lugar", "call_back_prompt": "Chamar de volta", "declined": "Chamada recusada", "failed_connect_media": "Não foi possível conectar-se à mídia", @@ -2617,10 +3404,12 @@ }, "m.file": { "error_decrypting": "Erro ao descriptografar o anexo", - "error_invalid": "Arquivo inválido %(extra)s" + "error_invalid": "Arquivo inválido" }, "m.image": { + "error": "Não é possível mostrar a imagem devido a um erro", "error_decrypting": "Erro ao descriptografar a imagem", + "error_downloading": "Erro ao baixar a imagem", "sent": "%(senderDisplayName)s enviou uma imagem.", "show_image": "Mostrar imagem" }, @@ -2629,7 +3418,9 @@ "you_started": "Você enviou uma solicitação de confirmação" }, "m.location": { - "full": "%(senderName)s compartilhou sua localização" + "full": "%(senderName)s compartilhou sua localização", + "location": "Compartilhou um local: ", + "self_location": "Compartilhou sua localização: " }, "m.poll": { "count_of_votes": { @@ -2638,6 +3429,7 @@ } }, "m.poll.end": { + "ended": "Encerrou uma enquete", "sender_ended": "%(senderName)s encerrou uma enquete" }, "m.poll.start": "%(senderName)s começou uma enquete - %(pollQuestion)s", @@ -2664,13 +3456,16 @@ }, "m.room.create": { "continuation": "Esta sala é uma continuação de outra conversa.", - "see_older_messages": "Clique aqui para ver as mensagens mais antigas." + "see_older_messages": "Clique aqui para ver as mensagens mais antigas.", + "unknown_predecessor": "Não foi possível encontrar a versão antiga desta sala (ID da sala:%(roomId)s) e não recebemos 'via_servers' para procurá-la.", + "unknown_predecessor_guess_server": "Não foi possível encontrar a versão antiga desta sala (ID da sala:%(roomId)s) e não recebemos 'via_servers' para procurá-la. É possível que adivinhar o servidor a partir do ID da sala funcione. Se você quiser tentar, clique neste link:" }, "m.room.encryption": { "disable_attempt": "A tentativa de desativar a criptografia foi ignorada", "disabled": "Criptografia desativada", "enabled": "As mensagens nesta sala são criptografadas de ponta a ponta. Quando as pessoas ingressam, você pode verificá-las em seus perfis, basta tocar na foto do perfil.", "enabled_dm": "As mensagens aqui são criptografadas de ponta a ponta. Verifique %(displayName)s no perfil deles - toque na foto do perfil.", + "enabled_local": "As mensagens neste bate-papo serão criptografadas de ponta a ponta.", "parameters_changed": "Alguns parâmetros de criptografia foram modificados.", "unsupported": "A criptografia usada nesta sala não é suportada." }, @@ -2710,6 +3505,7 @@ "left_reason": "%(targetName)s saiu da sala: %(reason)s", "no_change": "%(senderName)s não fez mudanças", "reject_invite": "%(targetName)s rejeitou o convite", + "reject_invite_reason": "%(targetName)s rejeitou o convite: %(reason)s", "remove_avatar": "%(senderName)s removeu sua foto de perfil", "remove_name": "%(senderName)s removeu seu nome de exibição (%(oldDisplayName)s)", "set_avatar": "%(senderName)s definiu sua foto de perfil", @@ -2746,11 +3542,13 @@ }, "m.room.tombstone": "%(senderDisplayName)s atualizou esta sala.", "m.room.topic": { - "changed": "%(senderDisplayName)s alterou a descrição para \"%(topic)s\"." + "changed": "%(senderDisplayName)s alterou a descrição para \"%(topic)s\".", + "removed": "%(senderDisplayName)s removeu o tópico." }, "m.sticker": "%(senderDisplayName)s enviou uma figurinha.", "m.video": { - "error_decrypting": "Erro ao descriptografar o vídeo" + "error_decrypting": "Erro ao descriptografar o vídeo", + "show_video": "Mostrar vídeo" }, "m.widget": { "added": "O widget %(widgetName)s foi criado por %(senderName)s", @@ -2763,10 +3561,14 @@ "removed": "O widget %(widgetName)s foi removido por %(senderName)s" }, "mab": { + "collapse_reply_chain": "Recolher citações", "copy_link_thread": "Copiar ligação para o tópico", + "expand_reply_chain": "Expandir citações", "label": "Ações da mensagem", "view_in_room": "Ver na sala" }, + "message_timestamp_received_at": "Recebido em: %(dateTime)s", + "message_timestamp_sent_at": "Enviado em: %(dateTime)s", "mjolnir": { "changed_rule_glob": "%(senderName)s alterou uma regra que bania o que correspondia a %(oldGlob)s para corresponder a %(newGlob)s devido à %(reason)s", "changed_rule_rooms": "%(senderName)s alterou uma regra que bania salas que correspondiam a %(oldGlob)s para corresponder a %(newGlob)s devido à %(reason)s", @@ -2787,14 +3589,21 @@ "updated_rule_servers": "%(senderName)s atualizou a regra que bane servidores que correspondem a %(glob)s devido à %(reason)s", "updated_rule_users": "%(senderName)s atualizou a regra de banimento de usuários correspondendo a %(glob)s devido à %(reason)s" }, + "no_permission_messages_before_invite": "Você não tem permissão para ver as mensagens de antes de ser convidado.", + "no_permission_messages_before_join": "Você não tem permissão para ver as mensagens de antes de entrar.", + "pending_moderation": "Mensagem pendente de moderação", + "pending_moderation_reason": "Mensagem pendente de moderação: %(reason)s", "reactions": { "add_reaction_prompt": "Adicionar reação", - "label": "%(reactors)s reagiram com %(content)s" + "custom_reaction_fallback_label": "Reação personalizada", + "label": "%(reactors)s reagiram com %(content)s", + "tooltip_caption": "Reagiu com %(shortName)s" }, "read_receipt_title": { "one": "Visto por %(count)s pessoa", "other": "Visto por %(count)s pessoas" }, + "read_receipts_label": "Confirmações de leitura", "redacted": { "tooltip": "Mensagem apagada em %(date)s" }, @@ -2856,12 +3665,12 @@ "one": "%(severalUsers)s tiveram os convites retirados" }, "invited": { - "other": "foi convidada/o %(count)s vezes", - "one": "foi convidada/o" + "one": "foi convidado", + "other": "foi convidado %(count)s vezes" }, "invited_multiple": { - "other": "foram convidadas/os %(count)s vezes", - "one": "foram convidadas/os" + "one": "foram convidados", + "other": "foram convidados %(count)s vezes" }, "joined": { "other": "%(oneUser)s entrou %(count)s vezes", @@ -2961,6 +3770,7 @@ "one_user": "%(displayName)s está digitando…", "two_users": "%(names)s e %(lastPerson)s estão digitando…" }, + "undecryptable_tooltip": "Esta mensagem não pôde ser descriptografada", "url_preview": { "close": "Fechar a visualização", "show_n_more": { @@ -2972,11 +3782,17 @@ "truncated_list_n_more": { "other": "E %(count)s mais..." }, + "unsupported_browser": { + "description": "Se você continuar, alguns recursos podem parar de funcionar e há o risco de você perder dados no futuro. Atualize seu navegador para continuar usando %(brand)s.", + "title": "%(brand)s não suporta este navegador" + }, "unsupported_server_description": "Este servidor está usando uma versão mais antiga do Matrix. Atualize para o Matrix %(version)s para usar %(brand)s sem erros.", "unsupported_server_title": "Seu servidor não é compatível", "update": { "changelog": "Registro de alterações", "check_action": "Verificar atualizações", + "checking": "Verificando se há uma atualização...", + "downloading": "Baixando a atualização...", "error_encountered": "Erro encontrado (%(errorDetail)s).", "error_unable_load_commit": "Não foi possível carregar os detalhes do envio: %(msg)s", "new_version_available": "Nova versão disponível. Atualize agora.", @@ -2987,6 +3803,13 @@ "toast_title": "Atualizar o %(brand)s", "unavailable": "Indisponível" }, + "update_room_access_modal": { + "description": "Para criar um link de compartilhamento, torne esta sala pública ou habilite a opção para que os usuários peçam para entrar. Isso permite que os convidados entrem sem serem convidados.", + "dont_change_description": "Se não quiser alterar o acesso a essa sala, você pode criar uma nova sala para o link de chamada.", + "no_change": "Eu não quero mudar o nível de acesso.", + "revert_access_description": "(Isso pode ser revertido para o valor anterior nas configurações da sala: Segurança e privacidade / Acesso)", + "title": "Permitir que usuários convidados entrem nesta sala" + }, "upload_failed_generic": "O envio do arquivo '%(fileName)s' falhou.", "upload_failed_size": "O arquivo '%(fileName)s' excede o limite de tamanho deste homeserver para uploads", "upload_failed_title": "O envio falhou", @@ -2996,6 +3819,7 @@ "error_files_too_large": "Esses arquivos são muito grandes para serem enviados. O limite do tamanho de arquivos é %(limit)s.", "error_some_files_too_large": "Alguns arquivos são muito grandes para serem enviados. O limite do tamanho de arquivos é %(limit)s.", "error_title": "Erro no envio", + "not_image": "O arquivo que você escolheu não é um arquivo de imagem válido.", "title": "Enviar arquivos", "title_progress": "Enviar arquivos (%(current)s de %(total)s)", "upload_all_button": "Enviar tudo", @@ -3006,6 +3830,8 @@ }, "user_info": { "admin_tools_section": "Ferramentas de administração", + "ban_button_room": "Banir da sala", + "ban_button_space": "Banir do espaço", "ban_room_confirm_title": "Banir de %(roomName)s", "ban_space_everything": "Bani-los de tudo que eu faço", "ban_space_specific": "Bani-los de coisas específicas que faço", @@ -3016,14 +3842,25 @@ "demote_self_confirm_description_space": "Você não poderá desfazer esta mudança, já que está tirando seu próprio cargo, e se você for o último usuário com privilégios neste espaço será impossível ganhá-los novamente.", "demote_self_confirm_room": "Você não poderá desfazer essa alteração, já que está rebaixando sua própria permissão. Se você for a última pessoa nesta sala, será impossível recuperar a permissão atual.", "demote_self_confirm_title": "Reduzir seu próprio privilégio?", + "disinvite_button_room": "Desconvidar da sala", "disinvite_button_room_name": "Cancelar convite de %(roomName)s", + "disinvite_button_space": "Desconvidar do espaço", "error_ban_user": "Não foi possível banir o usuário", "error_deactivate": "Falha ao desativar o usuário", - "error_mute_user": "Não foi possível remover notificações da/do usuária/o", + "error_kicking_user": "Falha ao remover usuário", + "error_mute_user": "Falha ao silenciar o usuário", "error_revoke_3pid_invite_description": "Não foi possível revogar o convite. O servidor pode estar com um problema temporário ou você não tem permissões suficientes para revogar o convite.", "error_revoke_3pid_invite_title": "Falha ao revogar o convite", + "ignore_button": "Ignorar", + "ignore_confirm_description": "Todas as mensagens e convites desse usuário serão ocultados. Você tem certeza de que deseja ignorá-lo?", + "ignore_confirm_title": "Ignorar %(user)s", "invited_by": "Convidado por %(sender)s", "jump_to_rr_button": "Ir para a confirmação de leitura", + "kick_button_room": "Remover da sala", + "kick_button_room_name": "Remover de %(roomName)s", + "kick_button_space": "Remover do espaço", + "kick_button_space_everything": "Remova-os de tudo que eu puder", + "kick_space_specific": "Remover de coisas específicas que eu posso fazer", "kick_space_warning": "Eles ainda poderão acessar tudo o que você não for administrador.", "promote_warning": "Você não poderá desfazer essa alteração, pois está promovendo o usuário ao mesmo nível de permissão que você.", "redact": { @@ -3031,7 +3868,13 @@ "other": "Apagar %(count)s mensagens para todos", "one": "Remover 1 mensagem" }, + "confirm_description_1": { + "one": "Você está prestes a remover a %(count)s mensagem de %(user)s. Isso os removerá permanentemente para todos na conversa. Você deseja continuar?", + "other": "Você está prestes a remover %(count)s mensagens de %(user)s. Isso os removerá permanentemente para todos na conversa. Você deseja continuar?" + }, "confirm_description_2": "Quando há muitas mensagens, isso pode levar algum tempo. Por favor, não recarregue o seu cliente enquanto isso.", + "confirm_keep_state_explainer": "Desmarque se você também deseja remover as mensagens do sistema desse usuário (por exemplo, alteração de associação, alteração de perfil...)", + "confirm_keep_state_label": "Preservar as mensagens do sistema", "confirm_title": "Apagar mensagens de %(user)s na sala", "no_recent_messages_description": "Tente rolar para cima na conversa para ver se há mensagens anteriores.", "no_recent_messages_title": "Nenhuma mensagem recente de %(user)s foi encontrada" @@ -3044,14 +3887,19 @@ "room_unencrypted_detail": "Em salas criptografadas, suas mensagens estão seguras e apenas você e a pessoa que a recebe têm as chaves únicas que permitem a sua leitura.", "send_message": "Enviar mensagem", "share_button": "Compartilhar perfil", + "unban_button_room": "Desbanir da sala", + "unban_button_space": "Desbanir do espaço", "unban_room_confirm_title": "Desbanir de %(roomName)s", "unban_space_everything": "Desbani-los de tudo que eu faço", "unban_space_specific": "Desbani-los de coisas específicas que eu faço", "unban_space_warning": "Eles não poderão acessar o que você não é administrador.", + "unignore_button": "Designorar", + "verification_unavailable": "Verificação de usuário indisponível", "verify_button": "Confirmar usuário", "verify_explainer": "Para maior segurança, confirme este usuário comparando um código único em ambos os aparelhos." }, "user_menu": { + "link_new_device": "Vincular novo dispositivo", "settings": "Todas as configurações", "switch_theme_dark": "Alternar para o modo escuro", "switch_theme_light": "Alternar para o modo claro" @@ -3075,6 +3923,7 @@ "camera_disabled": "Sua câmera está desligada", "camera_enabled": "Sua câmera ainda está habilitada", "cannot_call_yourself_description": "Você não pode iniciar uma chamada consigo mesmo.", + "close_lobby": "Fechar lobby", "connecting": "Conectando", "connection_lost": "A conectividade com o servidor foi perdida", "connection_lost_description": "Você não pode fazer chamadas sem uma conexão com o servidor.", @@ -3088,14 +3937,23 @@ "disabled_no_perms_start_video_call": "Você não tem permissão para iniciar chamadas de vídeo", "disabled_no_perms_start_voice_call": "Você não tem permissão para iniciar chamadas de voz", "disabled_ongoing_call": "Chamada em andamento", + "element_call": "Element", "enable_camera": "Ligar câmera", "enable_microphone": "Habilitar microfone", "expand": "Retornar para a chamada", + "get_call_link": "Compartilhar link de chamada", "hangup": "Desligar", "hide_sidebar_button": "Esconder a barra lateral", "input_devices": "Dispositivos de entrada", + "jitsi_call": "Jitsi", "join_button_tooltip_call_full": "Desculpe, esta chamada está lotada no momento", + "legacy_call": "Legacy", "maximise": "Preencher tela", + "maximise_call": "Maximizar chamada", + "metaspace_video_rooms": { + "conference_room_section": "Conferências" + }, + "minimise_call": "Minimizar chamada", "misconfigured_server": "A chamada falhou por conta de má configuração no servidor", "misconfigured_server_description": "Por favor, peça ao administrador do seu servidor (%(homeserverDomain)s) para configurar um servidor TURN, de modo que as chamadas funcionem de maneira estável.", "misconfigured_server_fallback": "Como alternativa, você pode tentar usar o servidor público em, mas isso não será tão confiável e ele compartilhará seu endereço IP com esse servidor. Você também pode gerenciar isso em Configurações.", @@ -3143,6 +4001,7 @@ "user_is_presenting": "%(sharerName)s está apresentando", "video_call": "Chamada de vídeo", "video_call_started": "Videochamada iniciada", + "video_call_using": "Chamada de vídeo usando:", "voice_call": "Chamada de voz", "you_are_presenting": "Você está apresentando" }, @@ -3242,7 +4101,7 @@ "error_need_to_be_logged_in": "Você precisa estar logado.", "error_unable_start_audio_stream_description": "Não foi possível iniciar a transmissão de áudio.", "error_unable_start_audio_stream_title": "Falha ao iniciar a transmissão ao vivo", - "modal_data_warning": "Dados nessa tela são compartilhados com %(widgetDomain)s", + "modal_data_warning": "Os dados abaixo são compartilhados com %(widgetDomain)s", "modal_title_default": "Popup do widget", "no_name": "App desconhecido", "open_id_permissions_dialog": { @@ -3277,6 +4136,7 @@ "l33t": "Substituições previsíveis como '@' em vez de 'a' não ajudam muito", "longerKeyboardPattern": "Use um padrão de teclas em diferentes direções e sentido", "noNeed": "Não há necessidade de símbolos, dígitos ou letras maiúsculas", + "pwned": "Se​•​você​•​usar​•​essa​•​senha​•​em​•​outro​•​lugar,​•​deverá​•​alterá-la.", "recentYears": "Evite anos recentes", "repeated": "Evite palavras e caracteres repetidos", "reverseWords": "Palavras invertidas não são muito mais difíceis de adivinhar", @@ -3290,6 +4150,7 @@ "extendedRepeat": "Repetições como \"abcabcabc\" são apenas um pouco mais difíceis de adivinhar que \"abc\"", "keyPattern": "Padrões de teclado curtos são fáceis de adivinhar", "namesByThemselves": "Nomes e sobrenomes por si só são fáceis de adivinhar", + "pwned": "Sua​•​senha​•​foi​•​exposta​•​por​•​uma​•​violação​•​de​•​dados​•​na​•​Internet.", "recentYears": "Os últimos anos são fáceis de adivinhar", "sequences": "Sequências como abc ou 6543 são fáceis de adivinhar", "similarToCommon": "Isto é similar a uma senha muito comum", @@ -3297,6 +4158,7 @@ "straightRow": "Linhas retas de teclas são fáceis de adivinhar", "topHundred": "Esta é uma das top-100 senhas mais comuns", "topTen": "Esta é uma das top-10 senhas mais comuns", + "userInputs": "Não deve haver nenhum dado pessoal ou relacionado à página.", "wordByItself": "Uma palavra por si só é fácil de adivinhar" } } diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index d56984fa0d..108d2e0cd8 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -871,22 +871,11 @@ "empty_room_was_name": "Пустая комната (без %(oldName)s)", "encryption": { "access_secret_storage_dialog": { - "enter_phrase_or_key_prompt": "Введите секретную фразу или , чтобы продолжить.", "key_validation_text": { - "invalid_security_key": "Неверный ключ восстановления", - "recovery_key_is_correct": "Выглядит неплохо!", - "wrong_file_type": "Неправильный тип файла", "wrong_security_key": "Неправильный ключ восстановления" }, - "reset_title": "Сбросить всё", - "reset_warning_1": "Делайте это только в том случае, если у вас нет другого устройства для завершения проверки.", - "reset_warning_2": "Если вы сбросите все настройки, вы перезагрузитесь без доверенных сеансов, без доверенных пользователей, и скорее всего не сможете просматривать прошлые сообщения.", "restoring": "Восстановление ключей из резервной копии", - "security_key_title": "Ключ восстановления", - "security_phrase_incorrect_error": "Невозможно получить доступ к секретному хранилищу. Убедитесь, что вы ввели правильную секретную фразу.", - "security_phrase_title": "Мнемоническая фраза", - "separator": "%(securityKey)s или %(recoveryFile)s", - "use_security_key_prompt": "Используйте ключ восстановления, чтобы продолжить." + "security_key_title": "Ключ восстановления" }, "bootstrap_title": "Настройка ключей", "cancel_entering_passphrase_description": "Вы уверены, что хотите отменить ввод кодовой фразы?", @@ -952,7 +941,6 @@ "accepting": "Принятие…", "after_new_login": { "device_verified": "Сеанс заверен", - "reset_confirmation": "Действительно сбросить ключи подтверждения?", "skip_verification": "Пока пропустить проверку", "unable_to_verify": "Невозможно заверить этот сеанс", "verify_this_device": "Заверьте этот сеанс" @@ -1023,8 +1011,6 @@ "verify_emoji_prompt": "Подтверждение сравнением уникальных смайлов.", "verify_emoji_prompt_qr": "Если вы не можете отсканировать код выше, попробуйте сравнить уникальные смайлы.", "verify_later": "Я заверю позже", - "verify_reset_warning_1": "Сброс ключей проверки нельзя отменить. После сброса вы не сможете получить доступ к старым зашифрованным сообщениям, а друзья, которые ранее проверили вас, будут видеть предупреждения о безопасности, пока вы не пройдете повторную проверку.", - "verify_reset_warning_2": "Продолжайте, только если вы уверены, что потеряли все остальные устройства и ключ безопасности.", "verify_using_device": "Сверить с другим сеансом", "verify_using_key": "Заверить бумажным ключом", "verify_using_key_or_phrase": "Проверка с помощью ключа безопасности или фразы", @@ -2538,7 +2524,6 @@ "remove_msisdn_prompt": "Удалить %(phone)s?", "spell_check_locale_placeholder": "Выберите регион" }, - "image_thumbnails": "Предпросмотр/миниатюры для изображений", "inline_url_previews_default": "Предпросмотр ссылок по умолчанию", "inline_url_previews_room": "Включить предпросмотр ссылок для участников этой комнаты по умолчанию", "inline_url_previews_room_account": "Включить предпросмотр ссылок в этой комнате (влияет только на вас)", @@ -3063,7 +3048,6 @@ "heading_without_query": "Поиск", "join_button_text": "Присоединиться к %(roomAddress)s", "keyboard_scroll_hint": "Используйте для прокрутки", - "message_search_section_title": "Другие поиски", "other_rooms_in_space": "Прочие комнаты в %(spaceName)s", "public_rooms_label": "Публичные комнаты", "public_spaces_label": "Публичное пространство", @@ -3073,7 +3057,6 @@ "result_may_be_hidden_privacy_warning": "Некоторые результаты могут быть скрыты из-за конфиденциальности", "result_may_be_hidden_warning": "Некоторые результаты могут быть скрыты", "search_dialog": "Окно поиска", - "search_messages_hint": "Для поиска сообщений найдите этот значок в верхней части комнаты", "spaces_title": "Ваши пространства", "start_group_chat_button": "Начать групповой чат" }, diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json index 4e8222d2cd..07e7ffeeb1 100644 --- a/src/i18n/strings/sk.json +++ b/src/i18n/strings/sk.json @@ -161,6 +161,7 @@ "view_message": "Zobraziť správu", "view_source": "Zobraziť zdroj", "yes": "Áno", + "yes_dismiss": "Áno, zamietnuť", "zoom_in": "Priblížiť", "zoom_out": "Oddialiť" }, @@ -390,6 +391,7 @@ "fallback_button": "Spustiť overenie", "mas_cross_signing_reset_cta": "Prejdite do svojho účtu", "mas_cross_signing_reset_description": "Obnovte svoju totožnosť prostredníctvom poskytovateľa účtu a potom sa vráťte späť a kliknite na tlačidlo „Skúsiť znova“.", + "mas_cross_signing_reset_title": "Prejdite do svojho účtu a obnovte svoju totožnosť", "msisdn": "Na číslo %(msisdn)s bola odoslaná textová správa", "msisdn_token_incorrect": "Neplatný token", "msisdn_token_prompt": "Prosím, zadajte kód z tejto správy:", @@ -530,6 +532,7 @@ "message_timestamp_invalid": "Neplatná časová značka", "microphone": "Mikrofón", "model": "Model", + "moderation_and_safety": "Moderovanie a bezpečnosť", "modern": "Moderný", "mute": "Umlčať", "n_members": { @@ -917,22 +920,13 @@ "empty_room_was_name": "Prázdna miestnosť (bola %(oldName)s)", "encryption": { "access_secret_storage_dialog": { - "enter_phrase_or_key_prompt": "Zadajte svoju bezpečnostnú frázu alebo pre pokračovanie.", + "alternatives": "Ak máte bezpečnostný kľúč alebo bezpečnostnú frázu, bude to fungovať tiež.", "key_validation_text": { - "invalid_security_key": "Neplatný kľúč na obnovenie", - "recovery_key_is_correct": "Vyzerá to super!", - "wrong_file_type": "Nesprávny typ súboru", - "wrong_security_key": "Nesprávny kľúč na obnovenie" + "wrong_security_key": "Zadaný kľúč na obnovenie nie je správny." }, - "reset_title": "Obnoviť všetko", - "reset_warning_1": "Urobte to len vtedy, ak nemáte iné zariadenie, pomocou ktorého by ste mohli dokončiť overenie.", - "reset_warning_2": "Ak všetko obnovíte, reštartujete bez dôveryhodných relácií, bez dôveryhodných používateľov a možno nebudete môcť vidieť predchádzajúce správy.", + "privacy_warning": "Uistite sa, že túto obrazovku nikto neuvidí!", "restoring": "Obnovenie kľúčov zo zálohy", - "security_key_title": "Kľúč na obnovenie", - "security_phrase_incorrect_error": "Nie je možné získať prístup k tajnému úložisku. Skontrolujte, či ste zadali správnu bezpečnostnú frázu.", - "security_phrase_title": "Bezpečnostná fráza", - "separator": "%(securityKey)s alebo %(recoveryFile)s", - "use_security_key_prompt": "Ak chcete pokračovať, použite kľúč na obnovenie." + "security_key_title": "Kľúč na obnovenie" }, "bootstrap_title": "Príprava kľúčov", "cancel_entering_passphrase_description": "Naozaj chcete zrušiť zadávanie prístupovej frázy?", @@ -988,6 +982,8 @@ "setup_secure_backup": { "explainer": "Zálohujte si šifrovacie kľúče pred odhlásením, aby ste zabránili ich strate." }, + "turn_on_key_storage": "Zapnúť úložisko kľúčov", + "turn_on_key_storage_description": "Uložte svoju kryptografickú totožnosť a kľúče správ bezpečne na serveri. To vám umožní zobraziť históriu správ na všetkých nových zariadeniach.", "udd": { "interactive_verification_button": "Interaktívne overte pomocou emotikonov", "other_ask_verify_text": "Poproste tohto používateľa, aby si overil svoju reláciu alebo ju nižšie manuálne overte.", @@ -1001,7 +997,6 @@ "accepting": "Akceptovanie…", "after_new_login": { "device_verified": "Zariadenie overené", - "reset_confirmation": "Skutočne vynulovať overovacie kľúče?", "skip_verification": "Vynechať zatiaľ overovanie", "unable_to_verify": "Nie je možné overiť toto zariadenie", "verify_this_device": "Overiť toto zariadenie" @@ -1072,8 +1067,6 @@ "verify_emoji_prompt": "Overenie porovnaním jedinečnej kombinácie emotikonov.", "verify_emoji_prompt_qr": "Ak sa vám nepodarí naskenovať uvedený kód, overte pomocou porovnania jedinečných emotikonov.", "verify_later": "Overím to neskôr", - "verify_reset_warning_1": "Vynulovanie overovacích kľúčov sa nedá vrátiť späť. Po vynulovaní nebudete mať prístup k starým zašifrovaným správam a všetci priatelia, ktorí vás predtým overili, uvidia bezpečnostné upozornenia, kým sa u nich znovu neoveríte.", - "verify_reset_warning_2": "Pokračujte prosím iba vtedy, ak ste si istí, že ste stratili všetky ostatné zariadenia aj váš kľúč na obnovenie.", "verify_using_device": "Overiť pomocou iného zariadenia", "verify_using_key": "Overiť pomocou kľúča na obnovenie", "verify_using_key_or_phrase": "Overiť pomocou kľúča na obnovenie alebo frázy", @@ -2131,6 +2124,7 @@ "room_list": { "add_room_label": "Pridať miestnosť", "add_space_label": "Pridať priestor", + "appearance": "Vzhľad", "breadcrumbs_empty": "Žiadne nedávno navštívené miestnosti", "breadcrumbs_label": "Nedávno navštívené miestnosti", "empty": { @@ -2139,11 +2133,14 @@ "no_chats_description_no_room_rights": "Začnite tým, že niekomu napíšete správu", "no_favourites": "Zatiaľ nemáte obľúbenú konverzáciu", "no_favourites_description": "V nastaveniach konverzácií môžete pridať konverzáciu medzi obľúbené", + "no_invites": "Nemáte žiadne neprečítané pozvánky", + "no_mentions": "Nemáte žiadne neprečítané zmienky", "no_people": "Zatiaľ s nikým nemáte priame konverzácie", "no_people_description": "Môžete zrušiť výber filtrov, aby ste videli svoje ostatné konverzácie", "no_rooms": "Zatiaľ ešte nie ste v žiadnej miestnosti", "no_rooms_description": "Môžete zrušiť výber filtrov, aby ste videli svoje ostatné konverzácie", "no_unread": "Gratulujeme! Nemáte žiadne neprečítané správy", + "show_activity": "Zobraziť všetku aktivitu", "show_chats": "Zobraziť všetky konverzácie" }, "failed_add_tag": "Miestnosti sa nepodarilo pridať značku %(tagName)s", @@ -2151,6 +2148,8 @@ "failed_set_dm_tag": "Nepodarilo sa nastaviť značku priamej správy", "filters": { "favourite": "Obľúbené", + "invites": "Pozvánky", + "mentions": "Zmienky", "people": "Ľudia", "rooms": "Miestnosti", "unread": "Neprečítané" @@ -2183,16 +2182,23 @@ "more_options": "Viac možností", "open_room": "Otvoriť miestnosť %(roomName)s" }, + "room_options": "Možnosti miestnosti", "show_less": "Zobraziť menej", + "show_message_previews": "Zobraziť náhľady správ", "show_n_more": { "one": "Zobraziť %(count)s ďalšiu", "few": "Zobraziť %(count)s ďalšie", "other": "Zobraziť %(count)s ďalších" }, "show_previews": "Zobraziť náhľady správ", + "sort": "Zoradiť", "sort_by": "Zoradiť podľa", "sort_by_activity": "Aktivity", "sort_by_alphabet": "A-Z", + "sort_type": { + "activity": "Aktivita", + "atoz": "A-Z" + }, "sort_unread_first": "Najprv ukázať miestnosti s neprečítanými správami", "space_menu": { "home": "Domov priestoru", @@ -2483,6 +2489,10 @@ "recent_changes_heading": "Nedávne zmeny, ktoré ešte neboli prijaté", "title": "Server neodpovedá" }, + "service_worker_error": { + "description": "%(brand)s vyžaduje proces služby na načítanie overených médií z úložísk obsahu Matrix. Váš prehliadač to nepodporuje, takže sa môže vyskytnúť zlyhanie načítania médií.", + "title": "Nepodarilo sa načítať proces služby" + }, "seshat": { "error_initialising": "Inicializácia vyhľadávania správ zlyhala, pre viac informácií skontrolujte svoje nastavenia", "reset_button": "Obnoviť úložisko udalostí", @@ -2576,6 +2586,8 @@ "session_key": "Kľúč relácie:", "title": "Pokročilé" }, + "confirm_key_storage_off": "Naozaj chcete ponechať úložisko kľúčov vypnuté?", + "confirm_key_storage_off_description": "Ak sa zo všetkých zariadení odhlásite, stratíte históriu správ a budete musieť znova overiť všetky existujúce kontakty. Prečítajte si viac ", "delete_key_storage": { "breadcrumb_page": "Odstrániť úložisko kľúčov", "confirm": "Odstrániť úložisko kľúčov", @@ -2661,6 +2673,7 @@ "discovery_needs_terms_title": "Umožnite ľuďom, aby vás našli", "display_name": "Zobrazované meno", "display_name_error": "Nedá sa nastaviť zobrazované meno", + "email_adding_unsupported_by_hs": "Tento domovský server nepodporuje pridávanie e-mailových adries do vášho účtu.", "email_address_in_use": "Táto emailová adresa sa už používa", "email_address_label": "Emailová adresa", "email_not_verified": "Vaša emailová adresa nebola zatiaľ overená", @@ -2685,7 +2698,9 @@ "error_share_msisdn_discovery": "Nepodarilo sa zdieľanie telefónneho čísla", "identity_server_no_token": "Nenašiel sa prístupový token totožnosti", "identity_server_not_set": "Server totožnosti nie je nastavený", + "invalid_phone_number": "Poskytnuté telefónne číslo sa nezdá byť platné.", "language_section": "Jazyk", + "msisdn_adding_unsupported_by_hs": "Tento domovský server nepodporuje pridávanie telefónnych čísel do vášho účtu.", "msisdn_in_use": "Toto telefónne číslo sa už používa", "msisdn_label": "Telefónne číslo", "msisdn_verification_field_label": "Overovací kód", @@ -2704,12 +2719,10 @@ "unable_to_load_msisdns": "Nie je možné načítať telefónne čísla", "username": "Používateľské meno" }, - "image_thumbnails": "Zobrazovať ukážky/náhľady obrázkov", "inline_url_previews_default": "Predvolene povoliť náhľady URL adries", "inline_url_previews_room": "Predvolene povoliť náhľady URL adries pre členov tejto miestnosti", "inline_url_previews_room_account": "Povoliť náhľady URL adries pre túto miestnosť (ovplyvňuje len vás)", "insert_trailing_colon_mentions": "Vložiť na koniec dvojbodku za zmienkou používateľa na začiatku správy", - "invite_avatars": "Zobraziť obrázky miestností, do ktorých ste boli pozvaní", "jump_to_bottom_on_send": "Skok na koniec časovej osi pri odosielaní správy", "key_backup": { "backup_in_progress": "Zálohovanie kľúčov máte aktívne (prvé zálohovanie môže trvať niekoľko minút).", @@ -2768,6 +2781,14 @@ "labs_mjolnir": { "dialog_title": "Nastavenia: Ignorovaní používatelia" }, + "media_preview": { + "hide_avatars": "Skryť obrázky miestnosti a pozývateľa", + "hide_media": "Vždy skryť", + "media_preview_description": "Skryté médium sa dá vždy zobraziť ťuknutím naň", + "media_preview_label": "Zobraziť médiá na časovej osi", + "show_in_private": "V súkromných miestnostiach", + "show_media": "Vždy zobraziť" + }, "notifications": { "default_setting_description": "Toto nastavenie sa predvolene použije pre všetky vaše miestnosti.", "default_setting_section": "Chcem byť upozornený na (predvolené nastavenie)", @@ -3256,7 +3277,7 @@ "heading_without_query": "Hľadať", "join_button_text": "Pripojiť sa k %(roomAddress)s", "keyboard_scroll_hint": "Na posúvanie použite ", - "message_search_section_title": "Iné vyhľadávania", + "messages_label": "Správy", "other_rooms_in_space": "Ostatné miestnosti v %(spaceName)s", "public_rooms_label": "Verejné miestnosti", "public_spaces_label": "Verejné priestory", @@ -3266,7 +3287,6 @@ "result_may_be_hidden_privacy_warning": "Niektoré výsledky môžu byť skryté kvôli ochrane súkromia", "result_may_be_hidden_warning": "Niektoré výsledky môžu byť skryté", "search_dialog": "Vyhľadávacie dialógové okno", - "search_messages_hint": "Ak chcete vyhľadávať správy, nájdite túto ikonu v hornej časti miestnosti ", "spaces_title": "Priestory, v ktorých sa nachádzate", "start_group_chat_button": "Začať skupinovú konverzáciu" }, @@ -3316,9 +3336,7 @@ "threads_activity_centre": { "header": "Aktivita vlákien", "no_rooms_with_threads_notifs": "Zatiaľ nemáte miestnosti s upozorneniami na vlákna.", - "no_rooms_with_unread_threads": "Zatiaľ nemáte miestnosti s neprečítanými vláknami.", - "release_announcement_description": "Upozornenia na vlákna sa presunuli, odteraz ich nájdete tu.", - "release_announcement_header": "Centrum aktivity vlákien" + "no_rooms_with_unread_threads": "Zatiaľ nemáte miestnosti s neprečítanými vláknami." }, "time": { "about_day_ago": "asi pred jedným dňom", @@ -3863,10 +3881,11 @@ "unavailable": "Nedostupné" }, "update_room_access_modal": { - "description": "Ak chcete vytvoriť odkaz na zdieľanie, musíte povoliť hosťom pripojiť sa k tejto miestnosti. To môže spôsobiť, že miestnosť bude menej bezpečná. Keď ukončíte hovor, môžete miestnosť opäť nastaviť na súkromnú.", - "dont_change_description": "Prípadne môžete hovor uskutočniť v samostatnej miestnosti.", + "description": "Ak chcete vytvoriť odkaz na zdieľanie, nastavte túto miestnosť ako verejnú alebo povolte používateľom možnosť požiadať o pripojenie. To umožní hosťom pripojiť sa bez pozvánky.", + "dont_change_description": "Ak nechcete zmeniť prístup do tejto miestnosti, môžete vytvoriť novú miestnosť pre odkaz na hovor.", "no_change": "Nechcem zmeniť úroveň prístupu.", - "title": "Zmeniť úroveň prístupu do miestnosti" + "revert_access_description": "(Túto hodnotu je možné vrátiť na predchádzajúcu hodnotu v nastaveniach miestnosti: Zabezpečenie a súkromie / Prístup)", + "title": "Povoliť hosťom pripojiť sa k tejto miestnosti" }, "upload_failed_generic": "Nepodarilo sa nahrať súbor „%(fileName)s“.", "upload_failed_size": "Veľkosť súboru „%(fileName)s“ prekračuje limit veľkosti súboru nahrávania na tento domovský server", diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 3428b65785..15ea9a3df8 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -755,22 +755,11 @@ "empty_room_was_name": "Dhomë e zbrazët (qe %(oldName)s)", "encryption": { "access_secret_storage_dialog": { - "enter_phrase_or_key_prompt": "Që të vazhdohet, jepni Frazën tuaj të Sigurisë, ose .", "key_validation_text": { - "invalid_security_key": "Kyç Sigurie i Pavlefshëm", - "recovery_key_is_correct": "Mirë duket!", - "wrong_file_type": "Lloj i gabuar kartele", "wrong_security_key": "Kyç Sigurie i Gabuar" }, - "reset_title": "Kthe gjithçka te parazgjedhjet", - "reset_warning_1": "Bëjeni këtë vetëm nëse s’keni pajisje tjetër me të cilën të plotësoni verifikimin.", - "reset_warning_2": "Nëse riktheni gjithçka te parazgjedhjet, do të rifilloni pa sesione të besuara, pa përdorues të besuar, dhe mund të mos jeni në gjendje të shihni mesazhe të dikurshëm.", "restoring": "Po rikthehen kyçesh nga kopjeruajtje", - "security_key_title": "Kyç Sigurie", - "security_phrase_incorrect_error": "S’arrihet të hyhet në depozitë të fshehtë. Ju lutemi, verifikoni se keni dhënë Frazën e saktë të Sigurisë.", - "security_phrase_title": "Frazë Sigurie", - "separator": "%(securityKey)s ose %(recoveryFile)s", - "use_security_key_prompt": "Që të vazhdohet përdorni Kyçin tuaj të Sigurisë." + "security_key_title": "Kyç Sigurie" }, "bootstrap_title": "Ujdisje kyçesh", "cancel_entering_passphrase_description": "Jeni i sigurt se doni të anulohet dhënie frazëkalimi?", @@ -827,7 +816,6 @@ "accepting": "Po pranohet…", "after_new_login": { "device_verified": "Pajisja u verifikua", - "reset_confirmation": "Të kthehen vërtet te parazgjedhjet kyçet e verifikimit?", "skip_verification": "Anashkaloje verifikimin hëpërhë", "unable_to_verify": "S’arrihet të verifikohet kjo pajisje", "verify_this_device": "Verifikoni këtë pajisje" @@ -897,8 +885,6 @@ "verify_emoji_prompt": "Verifikoje duke krahasuar emoji unik.", "verify_emoji_prompt_qr": "Nëse s’e skanoni dot kodin më sipër, verifikojeni duke krahasuar emoji unik.", "verify_later": "Do ta verifikoj më vonë", - "verify_reset_warning_1": "Rikthimi te parazgjedhjet i kyçeve tuaj të verifikimit s’mund të zhbëhet. Pas rikthimit te parazgjedhjet, s’do të mund të hyni dot te mesazhe të dikurshëm të fshehtëzuar dhe, cilido shok që ju ka verifikuar më parë, do të shohë një sinjalizim sigurie deri sa të ribëni verifikimin me ta.", - "verify_reset_warning_2": "Ju lutemi, ecni më tej vetëm nëse jeni i sigurt se keni humbur krejt pajisjet tuaja të tjera dhe Kyçin tuaj të Sigurisë.", "verify_using_device": "Verifikojeni me pajisje tjetër", "verify_using_key": "Verifikoje me Kyç Sigurie", "verify_using_key_or_phrase": "Verifikojeni me Kyç ose Frazë Sigurie", @@ -2177,7 +2163,6 @@ "remove_msisdn_prompt": "Të hiqet %(phone)s?", "spell_check_locale_placeholder": "Zgjidhni vendore" }, - "image_thumbnails": "Shfaq për figurat paraparje/miniatura", "inline_url_previews_default": "Aktivizo, si parazgjedhje, paraparje URL-sh brendazi", "inline_url_previews_room": "Aktivizo, si parazgjedhje, paraparje URL-sh për pjesëmarrësit në këtë dhomë", "inline_url_previews_room_account": "Aktivizo paraparje URL-sh për këtë dhomë (prek vetëm ju)", @@ -2651,7 +2636,6 @@ "heading_without_query": "Kërkoni për", "join_button_text": "Hyni te %(roomAddress)s", "keyboard_scroll_hint": "Përdorni për rrëshqitje", - "message_search_section_title": "Kërkime të tjera", "other_rooms_in_space": "Dhoma të tjera në %(spaceName)s", "public_rooms_label": "Dhoma publike", "recent_searches_section_title": "Kërkime së fundi", @@ -2660,7 +2644,6 @@ "result_may_be_hidden_privacy_warning": "Disa përfundime mund të jenë fshehur, për arsye privatësie", "result_may_be_hidden_warning": "Disa përfundime mund të jenë të fshehura", "search_dialog": "Dialog Kërkimi", - "search_messages_hint": "Që të kërkoni te mesazhet, shihni për këtë ikonë në krye të një dhome ", "spaces_title": "Hapësira ku jeni i pranishëm", "start_group_chat_button": "Nisni një fjalosje grupi" }, diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json index 10c8909c29..1715f2276f 100644 --- a/src/i18n/strings/sv.json +++ b/src/i18n/strings/sv.json @@ -525,6 +525,7 @@ "message_timestamp_invalid": "Ogiltig tidsstämpel", "microphone": "Mikrofon", "model": "Modell", + "moderation_and_safety": "Moderering och säkerhet", "modern": "Modernt", "mute": "Tysta", "n_members": { @@ -775,27 +776,27 @@ "cross_signing_cached": "cachad lokalt", "cross_signing_not_ready": "Korssignering är inte konfigurerad.", "cross_signing_private_keys_in_storage": "i hemlig lagring", - "cross_signing_private_keys_in_storage_status": "Korssignering av privata nycklar:", + "cross_signing_private_keys_in_storage_status": "Privata nycklar för korssignering:", "cross_signing_private_keys_not_in_storage": "hittades inte i lagring", "cross_signing_public_keys_on_device": "i minnet", "cross_signing_public_keys_on_device_status": "Korssignerade publika nycklar:", "cross_signing_ready": "Korssignering är klar för användning.", "cross_signing_status": "Korssigneringsstatus:", - "cross_signing_untrusted": "Ditt konto har en korssigneringsidentitet i hemlig lagring, men det är ännu inte betrodd av den här sessionen.", + "cross_signing_untrusted": "Ditt konto har en korssigneringsidentitet i hemlig lagring, men den är ännu inte betrodd av den här sessionen.", "crypto_not_available": "Kryptografisk modul är inte tillgänglig", - "key_backup_active_version": "Aktiv backupversion:", + "key_backup_active_version": "Aktiv säkerhetskopiaversion:", "key_backup_active_version_none": "Ingen", "key_backup_inactive_warning": "Dina nycklar säkerhetskopieras inte från den här sessionen.", - "key_backup_latest_version": "Senaste backupversionen på servern:", + "key_backup_latest_version": "Senast version av säkerhetskopia på servern:", "key_storage": "Nyckellagring", "master_private_key_cached_status": "Privat huvudnyckel:", - "not_found": "Hittades inte", + "not_found": "hittades inte", "not_found_locally": "hittades inte lokalt", - "secret_storage_not_ready": "Inte redo", - "secret_storage_ready": "Klar", + "secret_storage_not_ready": "inte klar", + "secret_storage_ready": "klar", "secret_storage_status": "Hemlig lagring:", "self_signing_private_key_cached_status": "Självsignerande privat nyckel:", - "title": "End-to-end-kryptering", + "title": "Totalsträckskryptering", "user_signing_private_key_cached_status": "Privat nyckel för användarsignering:" }, "developer_mode": "Utvecklarläge", @@ -908,22 +909,11 @@ "empty_room_was_name": "Tomt rum (var %(oldName)s)", "encryption": { "access_secret_storage_dialog": { - "enter_phrase_or_key_prompt": "Ange din säkerhetsfras eller för att fortsätta.", "key_validation_text": { - "invalid_security_key": "Ogiltig säkerhetsnyckel", - "recovery_key_is_correct": "Ser bra ut!", - "wrong_file_type": "Fel filtyp", "wrong_security_key": "Fel säkerhetsnyckel" }, - "reset_title": "Återställ allt", - "reset_warning_1": "Gör detta endast om du inte har någon annan enhet att slutföra verifikationen med.", - "reset_warning_2": "Om du återställer allt så kommer du att börja om utan betrodda sessioner eller betrodda användare, och kommer kanske inte kunna se gamla meddelanden.", "restoring": "Återställer nycklar från säkerhetskopia", - "security_key_title": "Säkerhetsnyckel", - "security_phrase_incorrect_error": "Kan inte komma åt hemlig lagring. Vänligen verifiera att du angav rätt säkerhetsfras.", - "security_phrase_title": "Säkerhetsfras", - "separator": "%(securityKey)s eller %(recoveryFile)s", - "use_security_key_prompt": "Använd din säkerhetsnyckel för att fortsätta." + "security_key_title": "Säkerhetsnyckel" }, "bootstrap_title": "Sätter upp nycklar", "cancel_entering_passphrase_description": "Är du säker på att du vill avbryta inmatning av lösenfrasen?", @@ -992,7 +982,6 @@ "accepting": "Accepterar…", "after_new_login": { "device_verified": "Enhet verifierad", - "reset_confirmation": "Återställ verkligen verifieringsnycklar?", "skip_verification": "Hoppa över verifiering för tillfället", "unable_to_verify": "Kunde inte verifiera den här enheten", "verify_this_device": "Verifiera den här enheten" @@ -1063,8 +1052,6 @@ "verify_emoji_prompt": "Verifiera genom att jämföra unika emojier.", "verify_emoji_prompt_qr": "Om du inte kan skanna koden ovan, verifiera genom att jämföra unika emojier.", "verify_later": "Jag verifierar senare", - "verify_reset_warning_1": "Återställning av dina verifieringsnycklar kan inte ångras. Efter återställning så kommer du inte att komma åt dina krypterade meddelanden, och alla vänner som tidigare har verifierat dig kommer att se säkerhetsvarningar tills du återverifierar med dem.", - "verify_reset_warning_2": "Fortsätt bara om du är säker på att du har förlorat alla dina övriga enheter och din säkerhetsnyckel.", "verify_using_device": "Verifiera med annan enhet", "verify_using_key": "Verifiera med säkerhetsnyckel", "verify_using_key_or_phrase": "Verifiera med säkerhetsnyckel eller -fras", @@ -2100,6 +2087,7 @@ "room_list": { "add_room_label": "Lägg till rum", "add_space_label": "Lägg till utrymme", + "appearance": "Utseende", "breadcrumbs_empty": "Inga nyligen besökta rum", "breadcrumbs_label": "Nyligen besökta rum", "empty": { @@ -2150,7 +2138,9 @@ "more_options": "Fler alternativ", "open_room": "Öppet rummet %(roomName)s" }, + "room_options": "Rumsalternativ", "show_less": "Visa mindre", + "show_message_previews": "Visa förhandsgranskningar av meddelanden", "show_n_more": { "other": "Visa %(count)s till", "one": "Visa %(count)s till" @@ -2595,7 +2585,7 @@ "add_msisdn_instructions": "Ett SMS har skickats till +%(msisdn)s. Ange verifieringskoden som det innehåller.", "add_msisdn_misconfigured": "Flöde för tilläggning/bindning med MSISDN är felkonfigurerat", "allow_spellcheck": "Tillåt stavningskontroll", - "application_language": "Språk för ansökan", + "application_language": "Applikationsspråk", "application_language_reload_hint": "Appen laddas om efter att ha valt ett annat språk", "avatar_remove_progress": "Tar bort bild...", "avatar_save_progress": "Laddar upp bild...", @@ -2667,12 +2657,10 @@ "unable_to_load_msisdns": "Det går inte att läsa in telefonnummer", "username": "Användarnamn" }, - "image_thumbnails": "Visa förhandsgranskning/miniatyr för bilder", "inline_url_previews_default": "Aktivera inbäddad URL-förhandsgranskning som standard", "inline_url_previews_room": "Aktivera URL-förhandsgranskning som standard för deltagare i detta rum", "inline_url_previews_room_account": "Aktivera URL-förhandsgranskning för detta rum (påverkar bara dig)", "insert_trailing_colon_mentions": "Infoga kolon efter användaromnämnande på början av ett meddelande", - "invite_avatars": "Visa avatarer av rum du har blivit inbjuden till", "jump_to_bottom_on_send": "Hoppa till botten av tidslinjen när du skickar ett meddelande", "key_backup": { "backup_in_progress": "Dina nycklar säkerhetskopieras (den första säkerhetskopieringen kan ta några minuter).", @@ -2731,6 +2719,14 @@ "labs_mjolnir": { "dialog_title": "Inställningar: Ignorerade användare" }, + "media_preview": { + "hide_avatars": "Dölj avatarer för rum och inbjudare", + "hide_media": "Dölj alltid", + "media_preview_description": "En dold media kan alltid visas genom att trycka på den", + "media_preview_label": "Visa media i tidslinjen", + "show_in_private": "I privata rum", + "show_media": "Visa alltid" + }, "notifications": { "default_setting_description": "Denna inställning kommer att tillämpas som standard för alla dina rum.", "default_setting_section": "Jag vill bli meddelad för (Standardinställning)", @@ -3214,7 +3210,6 @@ "heading_without_query": "Sök efter", "join_button_text": "Gå med i %(roomAddress)s", "keyboard_scroll_hint": "Använd för att skrolla", - "message_search_section_title": "Andra sökningar", "other_rooms_in_space": "Andra rum i %(spaceName)s", "public_rooms_label": "Offentliga rum", "public_spaces_label": "Offentliga utrymmen", @@ -3224,7 +3219,6 @@ "result_may_be_hidden_privacy_warning": "Vissa resultat kan vara dolda av sekretesskäl", "result_may_be_hidden_warning": "Vissa resultat kanske är dolda", "search_dialog": "Sökdialog", - "search_messages_hint": "För att söka efter meddelanden, leta efter den här ikonen på toppen av ett rum ", "spaces_title": "Utrymmen du är med i", "start_group_chat_button": "Starta en gruppchatt" }, @@ -3273,9 +3267,7 @@ "threads_activity_centre": { "header": "Aktivitet för trådar", "no_rooms_with_threads_notifs": "Du har inga rum med trådaviseringar ännu.", - "no_rooms_with_unread_threads": "Du har inga rum med olästa trådar än.", - "release_announcement_description": "Meddelanden om trådar har flyttats, du hittar dem här från och med nu.", - "release_announcement_header": "Aktivitetscenter för Trådar" + "no_rooms_with_unread_threads": "Du har inga rum med olästa trådar än." }, "time": { "about_day_ago": "cirka en dag sedan", diff --git a/src/i18n/strings/tr.json b/src/i18n/strings/tr.json index c97e5569a1..20cc904f17 100644 --- a/src/i18n/strings/tr.json +++ b/src/i18n/strings/tr.json @@ -883,22 +883,11 @@ "empty_room_was_name": "Boş oda (%(oldName)s idi)", "encryption": { "access_secret_storage_dialog": { - "enter_phrase_or_key_prompt": "Devam etmek için Güvenlik İfadenizi girin veya .", "key_validation_text": { - "invalid_security_key": "Geçersiz Güvenlik Anahtarı", - "recovery_key_is_correct": "Güzel görünüyor!", - "wrong_file_type": "Yanlış dosya türü", "wrong_security_key": "Yanlış Güvenlik Anahtarı" }, - "reset_title": "Hepsini sıfırla", - "reset_warning_1": "Bunu yalnızca doğrulamayı tamamlamak için başka bir cihazınız yoksa yapın.", - "reset_warning_2": "Her şeyi sıfırlarsanız, güvenilen oturumlar ve güvenilen kullanıcılar olmadan yeniden başlarsınız ve geçmiş mesajları göremeyebilirsiniz.", "restoring": "Anahtarları yedekten geri yükle", - "security_key_title": "Güvenlik anahtarı", - "security_phrase_incorrect_error": "Gizli depolama alanına erişilemiyor. Lütfen doğru Güvenlik İfadesini girdiğinizi doğrulayın.", - "security_phrase_title": "Güvenlik İfadesi", - "separator": "%(securityKey)s veya %(recoveryFile)s", - "use_security_key_prompt": "Devam etmek için Güvenlik Anahtarınızı kullanın." + "security_key_title": "Güvenlik anahtarı" }, "bootstrap_title": "Anahtarları ayarla", "cancel_entering_passphrase_description": "Parola girmeyi iptal etmek istediğinizden emin misiniz?", @@ -967,7 +956,6 @@ "accepting": "Kabul ediyorum…", "after_new_login": { "device_verified": "Cihaz doğrulandı", - "reset_confirmation": "Doğrulama anahtarlarını sıfırlamak istediğinizden emin misiniz?", "skip_verification": "Şimdilik doğrulamayı atla", "unable_to_verify": "Bu cihaz doğrulanamıyor", "verify_this_device": "Bu cihazı doğrulayın" @@ -1038,8 +1026,6 @@ "verify_emoji_prompt": "Eşsiz emoji eşleştirme ile doğrulama.", "verify_emoji_prompt_qr": "Yukarıdaki kodu tarayamıyorsanız benzersiz emojiyi karşılaştırarak doğrulayın.", "verify_later": "Daha sonra doğrulayacağım", - "verify_reset_warning_1": "Doğrulama anahtarlarınızı sıfırlama işlemi geri alınamaz. Sıfırladıktan sonra eski şifrelenmiş mesajlara erişiminiz olmaz ve sizi daha önce doğrulamış olan arkadaşlarınız, siz onlarla yeniden doğrulayana kadar güvenlik uyarıları görür.", - "verify_reset_warning_2": "Lütfen yalnızca diğer tüm cihazlarınızı ve Güvenlik Anahtarınızı kaybettiğinizden eminseniz devam edin.", "verify_using_device": "Başka bir cihazla doğrula", "verify_using_key": "Güvenlik Anahtarı ile Doğrula", "verify_using_key_or_phrase": "Güvenlik Anahtarı veya İfadesiyle Doğrula", @@ -2607,7 +2593,6 @@ "unable_to_load_msisdns": "Telefon numaraları yüklenemiyor", "username": "Kullanıcı Adı" }, - "image_thumbnails": "Fotoğraflar için ön izleme/küçük resim göster", "inline_url_previews_default": "Varsayılan olarak satır içi URL önizlemeleri aç", "inline_url_previews_room": "Bu odadaki katılımcılar için URL önizlemeyi varsayılan olarak açık hale getir", "inline_url_previews_room_account": "Bu oda için URL önizlemeyi aç (sadece sizi etkiler)", @@ -3152,7 +3137,6 @@ "heading_without_query": "Ara", "join_button_text": "Katıl %(roomAddress)s", "keyboard_scroll_hint": "Kaydırmak için kullanın", - "message_search_section_title": "Diğer aramalar", "other_rooms_in_space": "%(spaceName)s alanındaki diğer odalar", "public_rooms_label": "Herkese açık odalar", "public_spaces_label": "Herkese açık alanlar", @@ -3162,7 +3146,6 @@ "result_may_be_hidden_privacy_warning": "Bazı sonuçlar gizlilik için gizlenmiş olabilir", "result_may_be_hidden_warning": "Bazı sonuçlar gizlenmiş olabilir", "search_dialog": "Arama İletişim Kutusu", - "search_messages_hint": "Mesajları aramak için odanın üst kısmında bu simgeye tıklayın ", "spaces_title": "Bulunduğunuz alanlar", "start_group_chat_button": "Grup sohbeti başlat" }, @@ -3211,9 +3194,7 @@ "threads_activity_centre": { "header": "Mesaj dizileri etkinliği", "no_rooms_with_threads_notifs": "Henüz mesaj dizisi bildirimleri olan odalarınız yok.", - "no_rooms_with_unread_threads": "Henüz okunmamış mesaj dizilerinin bulunduğu odalarınız yok.", - "release_announcement_description": "Mesaj dizisi bildirimleri taşındı, bundan sonra burada bulabilirsiniz.", - "release_announcement_header": "Mesaj Dizileri Etkinlik Merkezi" + "no_rooms_with_unread_threads": "Henüz okunmamış mesaj dizilerinin bulunduğu odalarınız yok." }, "time": { "about_day_ago": "yaklaşık bir gün önce", diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json index 619336e936..614cfca299 100644 --- a/src/i18n/strings/uk.json +++ b/src/i18n/strings/uk.json @@ -388,6 +388,7 @@ "fallback_button": "Почати автентифікацію", "mas_cross_signing_reset_cta": "Перейти до свого облікового запису", "mas_cross_signing_reset_description": "Скиньте свою особистість через постачальника облікового запису, а потім поверніться та натисніть «Повторити».", + "mas_cross_signing_reset_title": "Перейдіть до свого облікового запису, щоб скинути свою ідентичність", "msisdn": "Текстове повідомлення надіслано на %(msisdn)s", "msisdn_token_incorrect": "Хибний токен", "msisdn_token_prompt": "Введіть отриманий код:", @@ -527,6 +528,7 @@ "message_timestamp_invalid": "Недійсна позначка часу", "microphone": "Мікрофон", "model": "Модель", + "moderation_and_safety": "Модерування й безпека", "modern": "Сучасний", "mute": "Стишити", "n_members": { @@ -911,22 +913,11 @@ "empty_room_was_name": "Порожня кімната (були %(oldName)s)", "encryption": { "access_secret_storage_dialog": { - "enter_phrase_or_key_prompt": "Щоб продовжити, введіть свою фразу безпеки або .", "key_validation_text": { - "invalid_security_key": "Недійсний ключ відновлення", - "recovery_key_is_correct": "Виглядає файно!", - "wrong_file_type": "Тип файлу не підтримується", "wrong_security_key": "Неправильний ключ відновлення" }, - "reset_title": "Скинути все", - "reset_warning_1": "Робіть це лише якщо у вас немає іншого пристрою для виконання перевірки.", - "reset_warning_2": "Якщо ви скинете все, то почнете заново без довірених сеансів, користувачів і доступу до минулих повідомлень.", "restoring": "Відновлення ключів із резервної копії", - "security_key_title": "Ключ відновлення", - "security_phrase_incorrect_error": "Не вдалося зайти до таємного сховища. Переконайтеся, що ввели правильну фразу безпеки.", - "security_phrase_title": "Фраза безпеки", - "separator": "%(securityKey)s або %(recoveryFile)s", - "use_security_key_prompt": "Щоб продовжити, скористайтеся ключем відновлення." + "security_key_title": "Ключ відновлення" }, "bootstrap_title": "Налаштовування ключів", "cancel_entering_passphrase_description": "Ви точно хочете скасувати введення парольної фрази?", @@ -995,7 +986,6 @@ "accepting": "Прийняття…", "after_new_login": { "device_verified": "Пристрій звірено", - "reset_confirmation": "Точно скинути ключі звірки?", "skip_verification": "На разі пропустити звірку", "unable_to_verify": "Не вдалося звірити цей пристрій", "verify_this_device": "Звірити цей пристрій" @@ -1066,8 +1056,6 @@ "verify_emoji_prompt": "Звірити порівнянням унікальних емодзі.", "verify_emoji_prompt_qr": "Якщо ви не можете сканувати зазначений код, звірте порівнянням унікальних емодзі.", "verify_later": "Звірю пізніше", - "verify_reset_warning_1": "Скидання ключів звірки неможливо скасувати. Після скидання, ви втратите доступ до старих зашифрованих повідомлень, а всі друзі, які раніше вас звіряли, бачитимуть застереження безпеки, поки ви не проведете звірку з ними знову.", - "verify_reset_warning_2": "Будь ласка, продовжуйте лише в разі втрати всіх своїх інших пристроїв та ключа відновлення.", "verify_using_device": "Звірити за допомогою іншого пристрою", "verify_using_key": "Верифікувати за допомогою ключа відновлення", "verify_using_key_or_phrase": "Верифікувати за допомогою фрази або ключа відновлення", @@ -1801,7 +1789,7 @@ "hide_messages_from_user": "Виберіть, чи хочете ви сховати всі поточні та майбутні повідомлення від цього користувача.", "ignore_user": "Нехтувати користувача", "illegal_content": "Протиправний вміст", - "missing_reason": "Будь ласка, вкажіть, чому ви скаржитеся.", + "missing_reason": "Будь ласка, вкажіть причину скарги.", "nature": "Оберіть природу й опишіть, що образливого в цьому повідомленні.", "nature_disagreement": "Те, що пише цей користувач, неправильно.\nПро це буде повідомлено модераторам кімнати.", "nature_illegal": "Користувач порушує закон, наприклад зливає особисті дані людей чи погрожує насиллям.\nМодератори кімнати отримають скаргу на це й зможуть передати її правоохоронцям.", @@ -2107,6 +2095,7 @@ "room_list": { "add_room_label": "Додати кімнату", "add_space_label": "Додати простір", + "appearance": "Вигляд", "breadcrumbs_empty": "Немає недавно відвіданих кімнат", "breadcrumbs_label": "Недавно відвідані кімнати", "empty": { @@ -2157,15 +2146,22 @@ "more_options": "Інші опції", "open_room": "Відкрити кімнату %(roomName)s" }, + "room_options": "Параметри кімнати", "show_less": "Згорнути", + "show_message_previews": "Показати попередній перегляд повідомлень", "show_n_more": { "other": "Показати ще %(count)s", "one": "Показати ще %(count)s" }, "show_previews": "Показувати попередній перегляд повідомлень", + "sort": "Сортувати", "sort_by": "Упорядкувати за", "sort_by_activity": "Активністю", "sort_by_alphabet": "А-Я", + "sort_type": { + "activity": "Діяльність", + "atoz": "А-Я" + }, "sort_unread_first": "Спочатку показувати кімнати з непрочитаними повідомленнями", "space_menu": { "home": "Домівка простору", @@ -2631,6 +2627,7 @@ "discovery_needs_terms_title": "Дайте змогу людям знаходити вас", "display_name": "Показуване ім'я", "display_name_error": "Не вдалося встановити показуване ім'я", + "email_adding_unsupported_by_hs": "Цей домашній сервер не підтримує додавання адрес електронної пошти до вашого облікового запису.", "email_address_in_use": "Ця е-пошта вже використовується", "email_address_label": "Адреса е-пошти", "email_not_verified": "Ваша адреса е-пошти ще не підтверджена", @@ -2655,7 +2652,9 @@ "error_share_msisdn_discovery": "Не вдалося надіслати телефонний номер", "identity_server_no_token": "Токен доступу до ідентифікації не знайдено", "identity_server_not_set": "Сервер ідентифікації не налаштовано", + "invalid_phone_number": "Вказано недійсний номер телефону.", "language_section": "Мова", + "msisdn_adding_unsupported_by_hs": "Цей домашній сервер не підтримує додавання телефонних номерів до вашого облікового запису.", "msisdn_in_use": "Цей телефонний номер вже використовується", "msisdn_label": "Телефонний номер", "msisdn_verification_field_label": "Код перевірки", @@ -2674,12 +2673,10 @@ "unable_to_load_msisdns": "Не вдалося завантажити номери телефонів", "username": "Ім'я користувача" }, - "image_thumbnails": "Показувати попередній перегляд зображень", "inline_url_previews_default": "Увімкнути вбудований перегляд гіперпосилань за умовчанням", "inline_url_previews_room": "Увімкнути попередній перегляд гіперпосилань за умовчанням для учасників цієї кімнати", "inline_url_previews_room_account": "Увімкнути попередній перегляд гіперпосилань в цій кімнаті (стосується тільки вас)", "insert_trailing_colon_mentions": "Додавати двокрапку після згадки користувача на початку повідомлення", - "invite_avatars": "Показувати аватари кімнат, до яких вас запросили", "jump_to_bottom_on_send": "Переходити вниз стрічки під час надсилання повідомлення", "key_backup": { "backup_in_progress": "Створюється резервна копія ваших ключів (перше копіювання може тривати кілька хвилин).", @@ -2738,6 +2735,14 @@ "labs_mjolnir": { "dialog_title": "Налаштування: Ігноровані користувачі" }, + "media_preview": { + "hide_avatars": "Сховати аватари кімнати та запрошувача", + "hide_media": "Завжди ховати", + "media_preview_description": "Сховане медіа завжди можна переглянути, натиснувши на нього", + "media_preview_label": "Показувати медіа у стрічці", + "show_in_private": "У приватних кімнатах", + "show_media": "Завжди показувати" + }, "notifications": { "default_setting_description": "Цей параметр буде застосовано усталеним до всіх ваших кімнат.", "default_setting_section": "Я хочу отримувати сповіщення про (типове налаштування)", @@ -3221,7 +3226,7 @@ "heading_without_query": "Пошук", "join_button_text": "Приєднатися до %(roomAddress)s", "keyboard_scroll_hint": "Використовуйте , щоб прокручувати", - "message_search_section_title": "Інші пошуки", + "messages_label": "Повідомлення", "other_rooms_in_space": "Інші кімнати в %(spaceName)s", "public_rooms_label": "Загальнодоступні кімнати", "public_spaces_label": "Загальнодоступні простори", @@ -3231,7 +3236,6 @@ "result_may_be_hidden_privacy_warning": "Деякі результати можуть бути приховані через приватність", "result_may_be_hidden_warning": "Деякі результати можуть бути приховані", "search_dialog": "Вікно пошуку", - "search_messages_hint": "Шукайте повідомлення за допомогою піктограми вгорі кімнати", "spaces_title": "Ваші простори", "start_group_chat_button": "Розпочати нову бесіду" }, @@ -3280,9 +3284,7 @@ "threads_activity_centre": { "header": "Діяльність у гілках", "no_rooms_with_threads_notifs": "У вас ще немає кімнат зі сповіщеннями в гілках.", - "no_rooms_with_unread_threads": "У вас ще немає кімнат з непрочитаними гілками.", - "release_announcement_description": "Сповіщення гілок переміщено, відтепер вони тут.", - "release_announcement_header": "Центр діяльності в гілках" + "no_rooms_with_unread_threads": "У вас ще немає кімнат з непрочитаними гілками." }, "time": { "about_day_ago": "близько доби тому", @@ -3793,10 +3795,11 @@ "unavailable": "Недоступний" }, "update_room_access_modal": { - "description": "Щоб створити посилання для спільного доступу, потрібно дозволити гостям приєднуватися до цієї кімнати. Рівень безпеки кімнати стане нижчим. Після завершення виклику, ви можете знову зробити кімнату приватною.", - "dont_change_description": "Як варіант, ви можете провести виклик в окремій кімнаті.", + "description": "Щоб створити посилання для поширення, зробіть кімнату загальнодоступною або дозвольте користувачам надсилати запит приєднатися. Це дозволить гостям приєднуватися без запрошення.", + "dont_change_description": "Якщо ви не хочете змінювати доступ до цієї кімнати, ви можете створити нову кімнату для посилання на виклик.", "no_change": "Я не хочу змінювати рівень доступу.", - "title": "Змінити рівень доступу до кімнати" + "revert_access_description": "(Це значення можна повернути до попереднього в налаштуваннях кімнати: Безпека та приватність / Доступ)", + "title": "Дозволити гостьовим користувачам приєднуватися до цієї кімнати" }, "upload_failed_generic": "Не вдалося вивантажити файл '%(fileName)s'.", "upload_failed_size": "Файл '%(fileName)s' перевищує ліміт розміру для відвантажень домашнього сервера", diff --git a/src/i18n/strings/vi.json b/src/i18n/strings/vi.json index fa300eac2d..a0c91663ff 100644 --- a/src/i18n/strings/vi.json +++ b/src/i18n/strings/vi.json @@ -716,22 +716,11 @@ "empty_room_was_name": "Phòng trống (trước kia là %(oldName)s)", "encryption": { "access_secret_storage_dialog": { - "enter_phrase_or_key_prompt": "Nhập Chuỗi bảo mật hoặc của bạn để tiếp tục.", "key_validation_text": { - "invalid_security_key": "Khóa bảo mật không hợp lệ", - "recovery_key_is_correct": "Có vẻ tốt!", - "wrong_file_type": "Loại tệp sai", "wrong_security_key": "Khóa bảo mật sai" }, - "reset_title": "Đặt lại mọi thứ", - "reset_warning_1": "Chỉ thực hiện việc này nếu bạn không có thiết bị nào khác để hoàn tất quá trình xác thực.", - "reset_warning_2": "Nếu bạn đặt lại mọi thứ, bạn sẽ khởi động lại mà không có phiên nào đáng tin cậy, không có người dùng đáng tin cậy và có thể không xem được các tin nhắn trước đây.", "restoring": "Khôi phục khóa từ sao lưu", - "security_key_title": "Chìa khóa bảo mật", - "security_phrase_incorrect_error": "Không thể truy cập bộ nhớ bí mật. Vui lòng xác minh rằng bạn đã nhập đúng Cụm từ bảo mật.", - "security_phrase_title": "Cụm từ Bảo mật", - "separator": "%(securityKey)s hay %(recoveryFile)s", - "use_security_key_prompt": "Sử dụng Khóa bảo mật của bạn để tiếp tục." + "security_key_title": "Chìa khóa bảo mật" }, "bootstrap_title": "Đang thiết lập khóa bảo mật", "cancel_entering_passphrase_description": "Bạn có chắc chắn muốn hủy nhập cụm mật khẩu không?", @@ -788,7 +777,6 @@ "accepting": "Đang chấp nhận…", "after_new_login": { "device_verified": "Thiết bị được xác thực", - "reset_confirmation": "Thực sự đặt lại các khóa xác minh?", "skip_verification": "Bỏ qua xác thực ngay bây giờ", "unable_to_verify": "Không thể xác thực thiết bị này", "verify_this_device": "Xác thực thiết bị này" @@ -858,8 +846,6 @@ "verify_emoji_prompt": "Xác thực bằng cách so sánh biểu tượng cảm xúc độc đáo.", "verify_emoji_prompt_qr": "Nếu bạn không thể quét mã ở trên, hãy xác thực bằng cách so sánh biểu tượng cảm xúc duy nhất.", "verify_later": "Tôi sẽ xác thực sau", - "verify_reset_warning_1": "Sẽ không thể hoàn tác lại việc đặt lại các khóa xác thực của bạn. Sau khi đặt lại, bạn sẽ không có quyền truy cập vào các tin nhắn đã được mã hóa cũ, và bạn bè đã được xác thực trước đó bạn sẽ thấy các cảnh báo bảo mật cho đến khi bạn xác thực lại với họ.", - "verify_reset_warning_2": "Chỉ tiếp tục nếu bạn chắc chắn là mình đã mất mọi thiết bị khác và khóa bảo mật.", "verify_using_device": "Xác thực bằng thiết bị khác", "verify_using_key": "Xác thực bằng Khóa Bảo mật", "verify_using_key_or_phrase": "Xác thực bằng Khóa hoặc Chuỗi Bảo mật", @@ -2087,7 +2073,6 @@ "remove_msisdn_prompt": "Xóa %(phone)s?", "spell_check_locale_placeholder": "Chọn vùng miền" }, - "image_thumbnails": "Hiển thị bản xem trước / hình thu nhỏ cho hình ảnh", "inline_url_previews_default": "Bật xem trước nội dung liên kết theo mặc định", "inline_url_previews_room": "Bật xem trước nội dung liên kết cho mọi người trong phòng này", "inline_url_previews_room_account": "Bật xem trước nội dung liên kết trong phòng này (chỉ với bạn)", @@ -2564,14 +2549,12 @@ "heading_without_query": "Tìm", "join_button_text": "Tham gia %(roomAddress)s", "keyboard_scroll_hint": "Dùng để cuộn", - "message_search_section_title": "Các tìm kiếm khác", "other_rooms_in_space": "Các phòng khác trong %(spaceName)s", "public_rooms_label": "Các phòng công cộng", "recent_searches_section_title": "Các tìm kiếm gần đây", "recently_viewed_section_title": "Được xem gần đây", "result_may_be_hidden_privacy_warning": "Một số kết quả có thể bị ẩn để đảm bảo quyền riêng tư", "result_may_be_hidden_warning": "Một số kết quả có thể bị ẩn", - "search_messages_hint": "Để tìm các tin nhắn, hãy tìm biểu tượng này ở đầu phòng ", "spaces_title": "Các Space bạn đang trong đó", "start_group_chat_button": "Bắt đầu cuộc trò chuyện nhóm" }, diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json index ba999dcab9..43e1ac008c 100644 --- a/src/i18n/strings/zh_Hans.json +++ b/src/i18n/strings/zh_Hans.json @@ -726,22 +726,11 @@ "empty_room_was_name": "空房间(曾是%(oldName)s)", "encryption": { "access_secret_storage_dialog": { - "enter_phrase_or_key_prompt": "输入安全短语或以继续。", "key_validation_text": { - "invalid_security_key": "安全密钥无效", - "recovery_key_is_correct": "看着不错!", - "wrong_file_type": "错误文件类型", "wrong_security_key": "安全密钥错误" }, - "reset_title": "全部重置", - "reset_warning_1": "当你没有其他设备可以用于完成验证时,方可执行此操作。", - "reset_warning_2": "如果你全部重置,你将会在没有受信任的会话重新开始、没有受信任的用户,且可能会看不到过去的消息。", "restoring": "从备份恢复密钥", - "security_key_title": "安全密钥", - "security_phrase_incorrect_error": "无法访问秘密存储。请确认你输入了正确的安全短语。", - "security_phrase_title": "安全短语", - "separator": "%(securityKey)s或%(recoveryFile)s", - "use_security_key_prompt": "使用你的安全密钥以继续。" + "security_key_title": "安全密钥" }, "bootstrap_title": "设置密钥", "cancel_entering_passphrase_description": "你确定要取消输入口令词组吗?", @@ -798,7 +787,6 @@ "accepting": "正在接受……", "after_new_login": { "device_verified": "设备已验证", - "reset_confirmation": "确实要重置验证密钥?", "skip_verification": "暂时跳过验证", "unable_to_verify": "无法验证此设备", "verify_this_device": "验证此设备" @@ -862,7 +850,6 @@ "verify_emoji_prompt": "通过比较唯一的表情符号来验证。", "verify_emoji_prompt_qr": "如果你不能扫描以上代码,请通过比较唯一的表情符号来验证。", "verify_later": "我稍后进行验证", - "verify_reset_warning_1": "无法撤消重置验证密钥的操作。重置后,你将无法访问旧的加密消息,任何之前验证过你的朋友将看到安全警告,直到你再次和他们进行验证。", "verify_using_device": "使用其他设备进行验证", "verify_using_key": "使用安全密钥进行验证", "verify_using_key_or_phrase": "使用安全密钥或短语进行验证", @@ -2088,7 +2075,6 @@ "remove_msisdn_prompt": "删除 %(phone)s 吗?", "spell_check_locale_placeholder": "选择区域设置" }, - "image_thumbnails": "显示图片的预览图", "inline_url_previews_default": "默认启用行内URL预览", "inline_url_previews_room": "对此房间的所有参与者默认启用URL预览", "inline_url_previews_room_account": "对此房间启用URL预览(仅影响你)", @@ -2519,7 +2505,6 @@ "heading_without_query": "搜索", "join_button_text": "加入%(roomAddress)s", "keyboard_scroll_hint": "用来滚动", - "message_search_section_title": "其他搜索", "other_rooms_in_space": "%(spaceName)s 中的其他房间", "public_rooms_label": "公共房间", "public_spaces_label": "公共空间", @@ -2529,7 +2514,6 @@ "result_may_be_hidden_privacy_warning": "为保护隐私,一些结果可能被隐藏", "result_may_be_hidden_warning": "一些结果可能被隐藏", "search_dialog": "搜索对话", - "search_messages_hint": "要搜索消息,请在房间顶部查找此图标", "spaces_title": "你所在的空间", "start_group_chat_button": "发起群聊天" }, diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 6d9c02a0bd..18cc169d97 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -784,22 +784,11 @@ "empty_room_was_name": "空的聊天室(曾為 %(oldName)s)", "encryption": { "access_secret_storage_dialog": { - "enter_phrase_or_key_prompt": "輸入您的安全密語或以繼續。", "key_validation_text": { - "invalid_security_key": "無效的安全金鑰", - "recovery_key_is_correct": "看起來真棒!", - "wrong_file_type": "錯誤的檔案類型", "wrong_security_key": "錯誤的安全金鑰" }, - "reset_title": "重設所有東西", - "reset_warning_1": "當您沒有其他裝置可以完成驗證時,才執行此動作。", - "reset_warning_2": "如果您重設所有東西,您將會在沒有受信任的工作階段、沒有受信任的使用者的情況下重新啟動,且可能會看不到過去的訊息。", "restoring": "從備份還原金鑰", - "security_key_title": "安全金鑰", - "security_phrase_incorrect_error": "無法存取祕密儲存空間。請確認您輸入了正確的安全密語。", - "security_phrase_title": "安全密語", - "separator": "%(securityKey)s 或 %(recoveryFile)s", - "use_security_key_prompt": "使用您的安全金鑰以繼續。" + "security_key_title": "安全金鑰" }, "bootstrap_title": "正在產生金鑰", "cancel_entering_passphrase_description": "您確定要取消輸入安全密語嗎?", @@ -856,7 +845,6 @@ "accepting": "正在接受…", "after_new_login": { "device_verified": "裝置已驗證", - "reset_confirmation": "真的要重設驗證金鑰?", "skip_verification": "暫時略過驗證", "unable_to_verify": "無法驗證此裝置", "verify_this_device": "驗證此裝置" @@ -926,8 +914,6 @@ "verify_emoji_prompt": "透過比對獨特的表情符號來進行驗證。", "verify_emoji_prompt_qr": "如果您無法掃描上面的條碼,請透過比較獨特的表情符號驗證。", "verify_later": "我稍後驗證", - "verify_reset_warning_1": "重設您的驗證金鑰將無法復原。重設後,您將無法存取舊的加密訊息,之前任何驗證過您的朋友也會看到安全警告,直到您重新驗證。", - "verify_reset_warning_2": "請僅在您確定遺失了您其他所有裝置與安全金鑰時才繼續。", "verify_using_device": "用另一台裝置驗證", "verify_using_key": "使用安全金鑰進行驗證", "verify_using_key_or_phrase": "使用安全金鑰或密語進行驗證", @@ -2255,7 +2241,6 @@ "remove_msisdn_prompt": "移除 %(phone)s?", "spell_check_locale_placeholder": "選擇語系" }, - "image_thumbnails": "顯示圖片的預覽/縮圖", "inline_url_previews_default": "預設啟用行內網址預覽", "inline_url_previews_room": "對此聊天室中的參與者預設啟用網址預覽", "inline_url_previews_room_account": "對此聊天室啟用網址預覽(僅影響您)", @@ -2766,7 +2751,6 @@ "heading_without_query": "搜尋", "join_button_text": "加入 %(roomAddress)s", "keyboard_scroll_hint": "使用 捲動", - "message_search_section_title": "其他搜尋", "other_rooms_in_space": "其他在 %(spaceName)s 中的聊天室", "public_rooms_label": "公開聊天室", "recent_searches_section_title": "近期搜尋", @@ -2775,7 +2759,6 @@ "result_may_be_hidden_privacy_warning": "出於隱私考量,可能會隱藏一些結果", "result_may_be_hidden_warning": "某些結果可能會被隱藏", "search_dialog": "搜尋對話方塊", - "search_messages_hint": "要搜尋訊息,請在聊天室頂部尋找此圖示 ", "spaces_title": "您所在的聊天空間", "start_group_chat_button": "開始群組聊天" }, diff --git a/src/languageHandler.tsx b/src/languageHandler.tsx index 865430fb9e..175f9c149f 100644 --- a/src/languageHandler.tsx +++ b/src/languageHandler.tsx @@ -113,6 +113,10 @@ export function _td(s: TranslationKey): TranslationKey { return s; } +function isValidTranslation(translated: string): boolean { + return typeof translated === "string" && !translated.startsWith("missing translation:"); +} + /** * to improve screen reader experience translations that are not in the main page language * eg a translation that fell back to english from another language @@ -124,30 +128,26 @@ export function _td(s: TranslationKey): TranslationKey { * */ const translateWithFallback = (text: string, options?: IVariables): { translated: string; isFallback?: boolean } => { const translated = counterpart.translate(text, { ...options, fallbackLocale: counterpart.getLocale() }); - if (!translated || translated.startsWith("missing translation:")) { - const fallbackTranslated = counterpart.translate(text, { ...options, locale: FALLBACK_LOCALE }); - if ( - (!fallbackTranslated || fallbackTranslated.startsWith("missing translation:")) && - process.env.NODE_ENV !== "development" - ) { - // Even the translation via FALLBACK_LOCALE failed; this can happen if - // - // 1. The string isn't in the translations dictionary, usually because you're in develop - // and haven't run yarn i18n - // 2. Loading the translation resources over the network failed, which can happen due to - // to network or if the client tried to load a translation that's been removed from the - // server. - // - // At this point, its the lesser evil to show the untranslated text, which - // will be in English, so the user can still make out *something*, rather than an opaque - // "missing translation" error. - // - // Don't do this in develop so people remember to run yarn i18n. - return { translated: text, isFallback: true }; - } + if (isValidTranslation(translated)) { + return { translated }; + } + + const fallbackTranslated = counterpart.translate(text, { ...options, locale: FALLBACK_LOCALE }); + if (isValidTranslation(fallbackTranslated)) { return { translated: fallbackTranslated, isFallback: true }; } - return { translated }; + + // Even the translation via FALLBACK_LOCALE failed; this can happen if + // + // 1. The string isn't in the translations dictionary, usually because you're in develop + // and haven't run yarn i18n + // 2. Loading the translation resources over the network failed, which can happen due to + // to network or if the client tried to load a translation that's been removed from the + // server. + // + // At this point, its the lesser evil to show the i18n key which will be in English but not human-friendly, + // so the user can still make out *something*, rather than an opaque possibly-untranslated "missing translation" error. + return { translated: text, isFallback: true }; }; // Wrapper for counterpart's translation function so that it handles nulls and undefineds properly @@ -454,55 +454,35 @@ type Languages = { [lang: string]: string; }; -export function setLanguage(preferredLangs: string | string[]): Promise { - if (!Array.isArray(preferredLangs)) { - preferredLangs = [preferredLangs]; +export async function setLanguage(...preferredLangs: string[]): Promise { + PlatformPeg.get()?.setLanguage(preferredLangs); + + const availableLanguages = await getLangsJson(); + let chosenLanguage = preferredLangs.find((lang) => availableLanguages.hasOwnProperty(lang)); + if (!chosenLanguage) { + // Fallback to en_EN if none is found + chosenLanguage = "en"; + logger.error("Unable to find an appropriate language, preferred: ", preferredLangs); } - const plaf = PlatformPeg.get(); - if (plaf) { - plaf.setLanguage(preferredLangs); + const languageData = await getLanguageRetry(i18nFolder + availableLanguages[chosenLanguage]); + + counterpart.registerTranslations(chosenLanguage, languageData); + counterpart.setLocale(chosenLanguage); + + await SettingsStore.setValue("language", null, SettingLevel.DEVICE, chosenLanguage); + // Adds a lot of noise to test runs, so disable logging there. + if (process.env.NODE_ENV !== "test") { + logger.log("set language to " + chosenLanguage); } - let langToUse: string; - let availLangs: Languages; - return getLangsJson() - .then((result) => { - availLangs = result; + // Set 'en' as fallback language: + if (chosenLanguage !== "en") { + const fallbackLanguageData = await getLanguageRetry(i18nFolder + availableLanguages["en"]); + counterpart.registerTranslations("en", fallbackLanguageData); + } - for (let i = 0; i < preferredLangs.length; ++i) { - if (availLangs.hasOwnProperty(preferredLangs[i])) { - langToUse = preferredLangs[i]; - break; - } - } - if (!langToUse) { - // Fallback to en_EN if none is found - langToUse = "en"; - logger.error("Unable to find an appropriate language"); - } - - return getLanguageRetry(i18nFolder + availLangs[langToUse]); - }) - .then(async (langData): Promise => { - counterpart.registerTranslations(langToUse, langData); - await registerCustomTranslations(); - counterpart.setLocale(langToUse); - await SettingsStore.setValue("language", null, SettingLevel.DEVICE, langToUse); - // Adds a lot of noise to test runs, so disable logging there. - if (process.env.NODE_ENV !== "test") { - logger.log("set language to " + langToUse); - } - - // Set 'en' as fallback language: - if (langToUse !== "en") { - return getLanguageRetry(i18nFolder + availLangs["en"]); - } - }) - .then(async (langData): Promise => { - if (langData) counterpart.registerTranslations("en", langData); - await registerCustomTranslations(); - }); + await registerCustomTranslations(); } type Language = { @@ -529,8 +509,7 @@ export async function getAllLanguagesWithLabels(): Promise { export function getLanguagesFromBrowser(): readonly string[] { if (navigator.languages && navigator.languages.length) return navigator.languages; - if (navigator.language) return [navigator.language]; - return [navigator.userLanguage || "en"]; + return [navigator.language ?? "en"]; } export function getLanguageFromBrowser(): string { diff --git a/src/models/Call.ts b/src/models/Call.ts index 60ff2b2ffa..70c7e4779f 100644 --- a/src/models/Call.ts +++ b/src/models/Call.ts @@ -85,15 +85,9 @@ export enum ConnectionState { export const isConnected = (state: ConnectionState): boolean => state === ConnectionState.Connected || state === ConnectionState.Disconnecting; -export enum Layout { - Tile = "tile", - Spotlight = "spotlight", -} - export enum CallEvent { ConnectionState = "connection_state", Participants = "participants", - Layout = "layout", Close = "close", Destroy = "destroy", } @@ -104,7 +98,6 @@ interface CallEventHandlerMap { participants: Map>, prevParticipants: Map>, ) => void; - [CallEvent.Layout]: (layout: Layout) => void; [CallEvent.Close]: () => void; [CallEvent.Destroy]: () => void; } @@ -658,14 +651,6 @@ export class ElementCall extends Call { private settingsStoreCallEncryptionWatcher?: string; private terminationTimer?: number; - private _layout = Layout.Tile; - public get layout(): Layout { - return this._layout; - } - protected set layout(value: Layout) { - this._layout = value; - this.emit(CallEvent.Layout, value); - } public get presented(): boolean { return super.presented; @@ -686,7 +671,6 @@ export class ElementCall extends Call { const params = new URLSearchParams({ embed: "true", // We're embedding EC within another application // Template variables are used, so that this can be configured using the widget data. - preload: "$preload", // We want it to load in the background. skipLobby: "$skipLobby", // Skip the lobby in case we show a lobby component of our own. returnToLobby: "$returnToLobby", // Returns to the lobby (instead of blank screen) when the call ends. (For video rooms) perParticipantE2EE: "$perParticipantE2EE", @@ -756,17 +740,13 @@ export class ElementCall extends Call { } // Creates a new widget if there isn't any widget of typ Call in this room. - // Defaults for creating a new widget are: skipLobby = false, preload = false + // Defaults for creating a new widget are: skipLobby = false // When there is already a widget the current widget configuration will be used or can be overwritten - // by passing the according parameters (skipLobby, preload). - // - // `preload` is deprecated. We used it for optimizing EC by using a custom EW call lobby and preloading the iframe. - // now it should always be false. + // by passing the according parameters (skipLobby). private static createOrGetCallWidget( roomId: string, client: MatrixClient, skipLobby: boolean | undefined, - preload: boolean | undefined, returnToLobby: boolean | undefined, ): IApp { const ecWidget = WidgetStore.instance.getApps(roomId).find((app) => WidgetType.CALL.matches(app.type)); @@ -777,9 +757,6 @@ export class ElementCall extends Call { if (skipLobby !== undefined) { overwrites.skipLobby = skipLobby; } - if (preload !== undefined) { - overwrites.preload = preload; - } if (returnToLobby !== undefined) { overwrites.returnToLobby = returnToLobby; } @@ -804,7 +781,6 @@ export class ElementCall extends Call { {}, { skipLobby: skipLobby ?? false, - preload: preload ?? false, returnToLobby: returnToLobby ?? false, }, ), @@ -870,7 +846,6 @@ export class ElementCall extends Call { room.roomId, room.client, undefined, - undefined, isVideoRoom(room), ); return new ElementCall(session, availableOrCreatedWidget, room.client); @@ -880,7 +855,7 @@ export class ElementCall extends Call { } public static create(room: Room, skipLobby = false): void { - ElementCall.createOrGetCallWidget(room.roomId, room.client, skipLobby, false, isVideoRoom(room)); + ElementCall.createOrGetCallWidget(room.roomId, room.client, skipLobby, isVideoRoom(room)); } protected async sendCallNotify(): Promise { @@ -912,19 +887,6 @@ export class ElementCall extends Call { audioInput: MediaDeviceInfo | null, videoInput: MediaDeviceInfo | null, ): Promise { - // The JoinCall action is only send if the widget is waiting for it. - if (this.widget.data?.preload) { - try { - await this.messaging!.transport.send(ElementWidgetActions.JoinCall, { - audioInput: audioInput?.label ?? null, - videoInput: videoInput?.label ?? null, - }); - } catch (e) { - throw new Error(`Failed to join call in room ${this.roomId}: ${e}`); - } - } - this.messaging!.on(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout); - this.messaging!.on(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout); this.messaging!.on(`action:${ElementWidgetActions.HangupCall}`, this.onHangup); this.messaging!.once(`action:${ElementWidgetActions.Close}`, this.onClose); this.messaging!.on(`action:${ElementWidgetActions.DeviceMute}`, this.onDeviceMute); @@ -968,8 +930,6 @@ export class ElementCall extends Call { } public setDisconnected(): void { - this.messaging!.off(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout); - this.messaging!.off(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout); this.messaging!.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup); this.messaging!.off(`action:${ElementWidgetActions.DeviceMute}`, this.onDeviceMute); super.setDisconnected(); @@ -994,15 +954,6 @@ export class ElementCall extends Call { if (this.session.memberships.length === 0 && !this.presented && !this.room.isCallRoom()) this.destroy(); }; - /** - * Sets the call's layout. - * @param layout The layout to switch to. - */ - public async setLayout(layout: Layout): Promise { - const action = layout === Layout.Tile ? ElementWidgetActions.TileLayout : ElementWidgetActions.SpotlightLayout; - await this.messaging!.transport.send(action, {}); - } - private readonly onMembershipChanged = (): void => this.updateParticipants(); private updateParticipants(): void { @@ -1046,18 +997,6 @@ export class ElementCall extends Call { this.close(); }; - private readonly onTileLayout = async (ev: CustomEvent): Promise => { - ev.preventDefault(); - this.layout = Layout.Tile; - this.messaging!.transport.reply(ev.detail, {}); // ack - }; - - private readonly onSpotlightLayout = async (ev: CustomEvent): Promise => { - ev.preventDefault(); - this.layout = Layout.Spotlight; - this.messaging!.transport.reply(ev.detail, {}); // ack - }; - public clean(): Promise { return Promise.resolve(); } diff --git a/src/modules/Api.ts b/src/modules/Api.ts index ad87088840..23abadf529 100644 --- a/src/modules/Api.ts +++ b/src/modules/Api.ts @@ -5,7 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import type { Api, RuntimeModuleConstructor, Config } from "@element-hq/element-web-module-api"; +import { createRoot, type Root } from "react-dom/client"; + +import type { Api, RuntimeModuleConstructor } from "@element-hq/element-web-module-api"; import { ModuleRunner } from "./ModuleRunner.ts"; import AliasCustomisations from "../customisations/Alias.ts"; import { RoomListCustomisations } from "../customisations/RoomList.ts"; @@ -17,7 +19,8 @@ import * as MediaCustomisations from "../customisations/Media.ts"; import UserIdentifierCustomisations from "../customisations/UserIdentifier.ts"; import { WidgetPermissionCustomisations } from "../customisations/WidgetPermissions.ts"; import { WidgetVariableCustomisations } from "../customisations/WidgetVariables.ts"; -import SdkConfig from "../SdkConfig.ts"; +import { ConfigApi } from "./ConfigApi.ts"; +import { I18nApi } from "./I18nApi.ts"; const legacyCustomisationsFactory = (baseCustomisations: T) => { let used = false; @@ -28,17 +31,6 @@ const legacyCustomisationsFactory = (baseCustomisations: T) => }; }; -class ConfigApi { - public get(): Config; - public get(key: K): Config[K]; - public get(key?: K): Config | Config[K] { - if (key === undefined) { - return SdkConfig.get() as Config; - } - return SdkConfig.get(key); - } -} - /** * Implementation of the @element-hq/element-web-module-api runtime module API. */ @@ -65,6 +57,12 @@ class ModuleApi implements Api { /* eslint-enable @typescript-eslint/naming-convention */ public readonly config = new ConfigApi(); + public readonly i18n = new I18nApi(); + public readonly rootNode = document.getElementById("matrixchat")!; + + public createRoot(element: Element): Root { + return createRoot(element); + } } export type ModuleApiType = ModuleApi; diff --git a/src/modules/ConfigApi.ts b/src/modules/ConfigApi.ts new file mode 100644 index 0000000000..512a1c4abe --- /dev/null +++ b/src/modules/ConfigApi.ts @@ -0,0 +1,20 @@ +/* +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 { ConfigApi as IConfigApi, Config } from "@element-hq/element-web-module-api"; +import SdkConfig from "../SdkConfig.ts"; + +export class ConfigApi implements IConfigApi { + public get(): Config; + public get(key: K): Config[K]; + public get(key?: K): Config | Config[K] { + if (key === undefined) { + return SdkConfig.get() as Config; + } + return SdkConfig.get(key); + } +} diff --git a/src/modules/I18nApi.ts b/src/modules/I18nApi.ts new file mode 100644 index 0000000000..43c101eca6 --- /dev/null +++ b/src/modules/I18nApi.ts @@ -0,0 +1,47 @@ +/* +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 I18nApi as II18nApi, type Variables, type Translations } from "@element-hq/element-web-module-api"; +import counterpart from "counterpart"; + +import { _t, getCurrentLanguage, type TranslationKey } from "../languageHandler.tsx"; + +export class I18nApi implements II18nApi { + /** + * Read the current language of the user in IETF Language Tag format + */ + public get language(): string { + return getCurrentLanguage(); + } + + /** + * Register translations for the module, may override app's existing translations + */ + public register(translations: Partial): void { + const langs: Record> = {}; + for (const key in translations) { + for (const lang in translations[key]) { + langs[lang] = langs[lang] || {}; + langs[lang][key] = translations[key][lang]; + } + } + + // Finally, tell counterpart about our translations + for (const lang in langs) { + counterpart.registerTranslations(lang, langs[lang]); + } + } + + /** + * Perform a translation, with optional variables + * @param key - The key to translate + * @param variables - Optional variables to interpolate into the translation + */ + public translate(key: TranslationKey, variables?: Variables): string { + return _t(key, variables); + } +} diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 0eb55b5996..1c649054ad 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -10,6 +10,7 @@ Please see LICENSE files in the repository root for full details. import React, { type ReactNode } from "react"; import { UNSTABLE_MSC4133_EXTENDED_PROFILES } from "matrix-js-sdk/src/matrix"; +import { type MediaPreviewConfig } from "../@types/media_preview.ts"; import { _t, _td, type TranslationKey } from "../languageHandler"; import DeviceIsolationModeController from "./controllers/DeviceIsolationModeController.ts"; import { @@ -45,6 +46,7 @@ import { type Json, type JsonValue } from "../@types/json.ts"; import { type RecentEmojiData } from "../emojipicker/recent.ts"; import { type Assignable } from "../@types/common.ts"; import { SortingAlgorithm } from "../stores/room-list-v3/skip-list/sorters/index.ts"; +import MediaPreviewConfigController from "./controllers/MediaPreviewConfigController.ts"; export const defaultWatchManager = new WatchManager(); @@ -313,8 +315,6 @@ export interface Settings { "showHiddenEventsInTimeline": IBaseSetting; "lowBandwidth": IBaseSetting; "fallbackICEServerAllowed": IBaseSetting; - "showImages": IBaseSetting; - "showAvatarsOnInvites": IBaseSetting; "RoomList.preferredSorting": IBaseSetting; "RoomList.showMessagePreview": IBaseSetting; "RightPanel.phasesGlobal": IBaseSetting; @@ -350,6 +350,7 @@ export interface Settings { "Electron.alwaysShowMenuBar": IBaseSetting; "Electron.showTrayIcon": IBaseSetting; "Electron.enableHardwareAcceleration": IBaseSetting; + "mediaPreviewConfig": IBaseSetting; "Developer.elementCallUrl": IBaseSetting; } @@ -428,6 +429,11 @@ export const SETTINGS: Settings = { supportedLevelsAreOrdered: true, default: false, }, + "mediaPreviewConfig": { + controller: new MediaPreviewConfigController(), + supportedLevels: LEVELS_ROOM_SETTINGS, + default: MediaPreviewConfigController.default, + }, "feature_report_to_moderators": { isFeature: true, labsGroup: LabGroup.Moderation, @@ -1129,16 +1135,6 @@ export const SETTINGS: Settings = { default: null, controller: new FallbackIceServerController(), }, - "showImages": { - supportedLevels: LEVELS_ACCOUNT_SETTINGS, - displayName: _td("settings|image_thumbnails"), - default: true, - }, - "showAvatarsOnInvites": { - supportedLevels: LEVELS_ACCOUNT_SETTINGS, - displayName: _td("settings|invite_avatars"), - default: true, - }, "RoomList.preferredSorting": { supportedLevels: [SettingLevel.DEVICE], default: SortingAlgorithm.Recency, @@ -1392,7 +1388,6 @@ export const SETTINGS: Settings = { displayName: _td("settings|preferences|enable_hardware_acceleration"), default: true, }, - "Developer.elementCallUrl": { supportedLevels: [SettingLevel.DEVICE], displayName: _td("devtools|settings|elementCallUrl"), diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts index 7dac777399..aaa836b6fc 100644 --- a/src/settings/SettingsStore.ts +++ b/src/settings/SettingsStore.ts @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024, 2025 New Vector Ltd. Copyright 2019, 2020 The Matrix.org Foundation C.I.C. Copyright 2017 Travis Ralston @@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details. import { logger } from "matrix-js-sdk/src/logger"; import { type ReactNode } from "react"; -import { ClientEvent, SyncState } from "matrix-js-sdk/src/matrix"; +import { ClientEvent } from "matrix-js-sdk/src/matrix"; import DeviceSettingsHandler from "./handlers/DeviceSettingsHandler"; import RoomDeviceSettingsHandler from "./handlers/RoomDeviceSettingsHandler"; @@ -38,6 +38,7 @@ import { Action } from "../dispatcher/actions"; import PlatformSettingsHandler from "./handlers/PlatformSettingsHandler"; import ReloadOnChangeController from "./controllers/ReloadOnChangeController"; import { MatrixClientPeg } from "../MatrixClientPeg"; +import { MediaPreviewValue } from "../@types/media_preview"; // Convert the settings to easier to manage objects for the handlers const defaultSettings: Record = {}; @@ -666,35 +667,25 @@ export default class SettingsStore { const client = MatrixClientPeg.safeGet(); - const doMigration = async (): Promise => { - logger.info("Performing one-time settings migration of URL previews in E2EE rooms"); + while (!client.isInitialSyncComplete()) { + await new Promise((r) => client.once(ClientEvent.Sync, r)); + } - const roomAccounthandler = LEVEL_HANDLERS[SettingLevel.ROOM_ACCOUNT]; + logger.info("Performing one-time settings migration of URL previews in E2EE rooms"); - for (const room of client.getRooms()) { - // We need to use the handler directly because this setting is no longer supported - // at this level at all - const val = roomAccounthandler.getValue("urlPreviewsEnabled_e2ee", room.roomId); + const roomAccounthandler = LEVEL_HANDLERS[SettingLevel.ROOM_ACCOUNT]; - if (val !== undefined) { - await SettingsStore.setValue("urlPreviewsEnabled_e2ee", room.roomId, SettingLevel.ROOM_DEVICE, val); - } + for (const room of client.getRooms()) { + // We need to use the handler directly because this setting is no longer supported + // at this level at all + const val = roomAccounthandler.getValue("urlPreviewsEnabled_e2ee", room.roomId); + + if (val !== undefined) { + await SettingsStore.setValue("urlPreviewsEnabled_e2ee", room.roomId, SettingLevel.ROOM_DEVICE, val); } + } - localStorage.setItem(MIGRATION_DONE_FLAG, "true"); - }; - - const onSync = (state: SyncState): void => { - if (state === SyncState.Prepared) { - client.removeListener(ClientEvent.Sync, onSync); - - doMigration().catch((e) => { - logger.error("Failed to migrate URL previews in E2EE rooms:", e); - }); - } - }; - - client.on(ClientEvent.Sync, onSync); + localStorage.setItem(MIGRATION_DONE_FLAG, "true"); } /** @@ -715,6 +706,35 @@ export default class SettingsStore { localStorage.setItem(MIGRATION_DONE_FLAG, "true"); } + /** + * Migrate the setting for visible images to a setting. + * + * @param isFreshLogin True if the user has just logged in, false if a previous session is being restored. + */ + private static async migrateMediaControlsToSetting(isFreshLogin: boolean): Promise { + if (isFreshLogin) return; + const client = MatrixClientPeg.safeGet(); + + while (!client.isInitialSyncComplete()) { + await new Promise((r) => client.once(ClientEvent.Sync, r)); + } + // Never migrate if the config already exists. + if (client.getAccountData("io.element.msc4278.media_preview_config")) { + return; + } + logger.info("Performing one-time settings migration of show images and invite avatars to account data"); + const handler = LEVEL_HANDLERS[SettingLevel.ACCOUNT]; + const showImages = handler.getValue("showImages", null); + const showAvatarsOnInvites = handler.getValue("showAvatarsOnInvites", null); + + if (typeof showImages === "boolean" || typeof showAvatarsOnInvites === "boolean") { + this.setValue("mediaPreviewConfig", null, SettingLevel.ACCOUNT, { + invite_avatars: showAvatarsOnInvites === false ? MediaPreviewValue.Off : MediaPreviewValue.On, + media_previews: showImages === false ? MediaPreviewValue.Off : MediaPreviewValue.On, + }); + } // else, we don't set anything and use the server value + } + /** * Runs or queues any setting migrations needed. */ @@ -724,7 +744,9 @@ export default class SettingsStore { // (so around October 2024). // The consequences of missing the migration are only that URL previews will // be disabled in E2EE rooms. - SettingsStore.migrateURLPreviewsE2EE(isFreshLogin); + SettingsStore.migrateURLPreviewsE2EE(isFreshLogin).catch((e) => { + logger.error("Failed to migrate URL previews in E2EE rooms:", e); + }); // This can be removed once enough users have run a version of Element with // this migration. @@ -732,6 +754,13 @@ export default class SettingsStore { // will now be hidden again, so this fails safely. SettingsStore.migrateShowImagesToSettings(); + // This can be removed once enough users have run a version of Element with + // this migration. + // The consequences of missing the migration are that the previously set + // media controls for this user will be missing + SettingsStore.migrateMediaControlsToSetting(isFreshLogin).catch((e) => { + logger.error("Failed to migrate media config settings", e); + }); // Dev notes: to add your migration, just add a new `migrateMyFeature` function, call it, and // add a comment to note when it can be removed. return; diff --git a/src/settings/controllers/MatrixClientBackedController.ts b/src/settings/controllers/MatrixClientBackedController.ts index c7816222d5..b305ad5fa2 100644 --- a/src/settings/controllers/MatrixClientBackedController.ts +++ b/src/settings/controllers/MatrixClientBackedController.ts @@ -26,7 +26,7 @@ export default abstract class MatrixClientBackedController extends SettingContro MatrixClientBackedController._matrixClient = client; for (const instance of MatrixClientBackedController.instances) { - instance.initMatrixClient(client, oldClient); + instance.initMatrixClient?.(client, oldClient); } } @@ -40,5 +40,5 @@ export default abstract class MatrixClientBackedController extends SettingContro return MatrixClientBackedController._matrixClient; } - protected abstract initMatrixClient(newClient: MatrixClient, oldClient?: MatrixClient): void; + protected initMatrixClient?(newClient: MatrixClient, oldClient?: MatrixClient): void; } diff --git a/src/settings/controllers/MediaPreviewConfigController.ts b/src/settings/controllers/MediaPreviewConfigController.ts new file mode 100644 index 0000000000..cb8b9b34aa --- /dev/null +++ b/src/settings/controllers/MediaPreviewConfigController.ts @@ -0,0 +1,100 @@ +/* +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 IContent } from "matrix-js-sdk/src/matrix"; +import { type AccountDataEvents } from "matrix-js-sdk/src/types"; + +import { + MEDIA_PREVIEW_ACCOUNT_DATA_TYPE, + type MediaPreviewConfig, + MediaPreviewValue, +} from "../../@types/media_preview.ts"; +import { type SettingLevel } from "../SettingLevel.ts"; +import MatrixClientBackedController from "./MatrixClientBackedController.ts"; + +/** + * Handles media preview settings provided by MSC4278. + * This uses both account-level and room-level account data. + */ +export default class MediaPreviewConfigController extends MatrixClientBackedController { + public static readonly default: AccountDataEvents[typeof MEDIA_PREVIEW_ACCOUNT_DATA_TYPE] = { + media_previews: MediaPreviewValue.On, + invite_avatars: MediaPreviewValue.On, + }; + + private static getValidSettingData(content: IContent): Partial { + const mediaPreviews: MediaPreviewConfig["media_previews"] = content.media_previews; + const inviteAvatars: MediaPreviewConfig["invite_avatars"] = content.invite_avatars; + const validMediaPreviews = Object.values(MediaPreviewValue); + const validInviteAvatars = [MediaPreviewValue.Off, MediaPreviewValue.On]; + return { + invite_avatars: validInviteAvatars.includes(inviteAvatars) ? inviteAvatars : undefined, + media_previews: validMediaPreviews.includes(mediaPreviews) ? mediaPreviews : undefined, + }; + } + + public constructor() { + super(); + } + + private getValue = (roomId?: string): MediaPreviewConfig => { + const source = roomId ? this.client?.getRoom(roomId) : this.client; + const accountData = + source?.getAccountData(MEDIA_PREVIEW_ACCOUNT_DATA_TYPE)?.getContent() ?? {}; + + const calculatedConfig = MediaPreviewConfigController.getValidSettingData(accountData); + + // Save an account data fetch if we have all the values. + if (calculatedConfig.invite_avatars && calculatedConfig.media_previews) { + return calculatedConfig as MediaPreviewConfig; + } + + // We're missing some keys. + if (roomId) { + const globalConfig = this.getValue(); + return { + invite_avatars: + calculatedConfig.invite_avatars ?? + globalConfig.invite_avatars ?? + MediaPreviewConfigController.default.invite_avatars, + media_previews: + calculatedConfig.media_previews ?? + globalConfig.media_previews ?? + MediaPreviewConfigController.default.media_previews, + }; + } + return { + invite_avatars: calculatedConfig.invite_avatars ?? MediaPreviewConfigController.default.invite_avatars, + media_previews: calculatedConfig.media_previews ?? MediaPreviewConfigController.default.media_previews, + }; + }; + + public getValueOverride(_level: SettingLevel, roomId: string | null): MediaPreviewConfig { + return this.getValue(roomId ?? undefined); + } + + public get settingDisabled(): false { + // No homeserver support is required for this MSC. + return false; + } + + public async beforeChange( + _level: SettingLevel, + roomId: string | null, + newValue: MediaPreviewConfig, + ): Promise { + if (!this.client) { + return false; + } + if (roomId) { + await this.client.setRoomAccountData(roomId, MEDIA_PREVIEW_ACCOUNT_DATA_TYPE, newValue); + return true; + } + await this.client.setAccountData(MEDIA_PREVIEW_ACCOUNT_DATA_TYPE, newValue); + return true; + } +} diff --git a/src/settings/handlers/AccountSettingsHandler.ts b/src/settings/handlers/AccountSettingsHandler.ts index 6afd560867..3ea9db158e 100644 --- a/src/settings/handlers/AccountSettingsHandler.ts +++ b/src/settings/handlers/AccountSettingsHandler.ts @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024, 2025 New Vector Ltd. Copyright 2019, 2020 The Matrix.org Foundation C.I.C. Copyright 2017 Travis Ralston @@ -8,13 +8,13 @@ Please see LICENSE files in the repository root for full details. */ import { type AccountDataEvents, ClientEvent, type MatrixClient, type MatrixEvent } from "matrix-js-sdk/src/matrix"; -import { defer } from "matrix-js-sdk/src/utils"; import { isEqual } from "lodash"; import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler"; import { objectClone, objectKeyChanges } from "../../utils/objects"; import { SettingLevel } from "../SettingLevel"; import { type WatchManager } from "../WatchManager"; +import { MEDIA_PREVIEW_ACCOUNT_DATA_TYPE } from "../../@types/media_preview"; const BREADCRUMBS_LEGACY_EVENT_TYPE = "im.vector.riot.breadcrumb_rooms"; const BREADCRUMBS_EVENT_TYPE = "im.vector.setting.breadcrumbs"; @@ -68,6 +68,8 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa } else if (event.getType() === RECENT_EMOJI_EVENT_TYPE) { const val = event.getContent()["enabled"]; this.watchers.notifyUpdate("recent_emoji", null, SettingLevel.ACCOUNT, val); + } else if (event.getType() === MEDIA_PREVIEW_ACCOUNT_DATA_TYPE) { + this.watchers.notifyUpdate("mediaPreviewConfig", null, SettingLevel.ROOM_ACCOUNT, event.getContent()); } }; @@ -159,7 +161,7 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa // Attach a deferred *before* setting the account data to ensure we catch any requests // which race between different lines. - const deferred = defer(); + const deferred = Promise.withResolvers(); const handler = (event: MatrixEvent): void => { if (event.getType() !== eventType || !isEqual(event.getContent()[field], value)) return; @@ -173,7 +175,7 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa await deferred.promise; } - public setValue(settingName: string, roomId: string, newValue: any): Promise { + public async setValue(settingName: string, roomId: string, newValue: any): Promise { switch (settingName) { // Special case URL previews case "urlPreviewsEnabled": @@ -199,7 +201,9 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa // Special case analytics case "pseudonymousAnalyticsOptIn": return this.setAccountData(ANALYTICS_EVENT_TYPE, "pseudonymousAnalyticsOptIn", newValue); - + case "mediaPreviewConfig": + // Handled in MediaPreviewConfigController. + return; default: return this.setAccountData(DEFAULT_SETTINGS_EVENT_TYPE, settingName, newValue); } diff --git a/src/settings/handlers/RoomAccountSettingsHandler.ts b/src/settings/handlers/RoomAccountSettingsHandler.ts index 045ef5cfe0..8fdf412adf 100644 --- a/src/settings/handlers/RoomAccountSettingsHandler.ts +++ b/src/settings/handlers/RoomAccountSettingsHandler.ts @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024, 2025 New Vector Ltd. Copyright 2019, 2020 The Matrix.org Foundation C.I.C. Copyright 2017 Travis Ralston @@ -8,12 +8,12 @@ Please see LICENSE files in the repository root for full details. */ import { type MatrixClient, type MatrixEvent, type Room, RoomEvent } from "matrix-js-sdk/src/matrix"; -import { defer } from "matrix-js-sdk/src/utils"; import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler"; import { objectClone, objectKeyChanges } from "../../utils/objects"; import { SettingLevel } from "../SettingLevel"; import { type WatchManager } from "../WatchManager"; +import { MEDIA_PREVIEW_ACCOUNT_DATA_TYPE } from "../../@types/media_preview"; const ALLOWED_WIDGETS_EVENT_TYPE = "im.vector.setting.allowed_widgets"; const DEFAULT_SETTINGS_EVENT_TYPE = "im.vector.web.settings"; @@ -56,6 +56,8 @@ export default class RoomAccountSettingsHandler extends MatrixClientBackedSettin } } else if (event.getType() === ALLOWED_WIDGETS_EVENT_TYPE) { this.watchers.notifyUpdate("allowedWidgets", roomId, SettingLevel.ROOM_ACCOUNT, event.getContent()); + } else if (event.getType() === MEDIA_PREVIEW_ACCOUNT_DATA_TYPE) { + this.watchers.notifyUpdate("mediaPreviewConfig", roomId, SettingLevel.ROOM_ACCOUNT, event.getContent()); } }; @@ -96,7 +98,7 @@ export default class RoomAccountSettingsHandler extends MatrixClientBackedSettin await this.client.setRoomAccountData(roomId, eventType, content); - const deferred = defer(); + const deferred = Promise.withResolvers(); const handler = (event: MatrixEvent, room: Room): void => { if (room.roomId !== roomId || event.getType() !== eventType) return; if (field !== null && event.getContent()[field] !== value) return; @@ -108,7 +110,7 @@ export default class RoomAccountSettingsHandler extends MatrixClientBackedSettin await deferred.promise; } - public setValue(settingName: string, roomId: string, newValue: any): Promise { + public async setValue(settingName: string, roomId: string, newValue: any): Promise { switch (settingName) { // Special case URL previews case "urlPreviewsEnabled": @@ -117,7 +119,9 @@ export default class RoomAccountSettingsHandler extends MatrixClientBackedSettin // Special case allowed widgets case "allowedWidgets": return this.setRoomAccountData(roomId, ALLOWED_WIDGETS_EVENT_TYPE, null, newValue); - + case "mediaPreviewConfig": + // Handled in MediaPreviewConfigController. + return; default: return this.setRoomAccountData(roomId, DEFAULT_SETTINGS_EVENT_TYPE, settingName, newValue); } diff --git a/src/settings/handlers/RoomSettingsHandler.ts b/src/settings/handlers/RoomSettingsHandler.ts index c574ea1ea4..a31ccb87e9 100644 --- a/src/settings/handlers/RoomSettingsHandler.ts +++ b/src/settings/handlers/RoomSettingsHandler.ts @@ -14,7 +14,6 @@ import { RoomStateEvent, type StateEvents, } from "matrix-js-sdk/src/matrix"; -import { defer } from "matrix-js-sdk/src/utils"; import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler"; import { objectClone, objectKeyChanges } from "../../utils/objects"; @@ -98,7 +97,7 @@ export default class RoomSettingsHandler extends MatrixClientBackedSettingsHandl const { event_id: eventId } = await this.client.sendStateEvent(roomId, eventType, content); - const deferred = defer(); + const deferred = Promise.withResolvers(); const handler = (event: MatrixEvent): void => { if (event.getId() !== eventId) return; this.client.off(RoomStateEvent.Events, handler); diff --git a/src/slash-commands/utils.ts b/src/slash-commands/utils.ts index f1be124151..d9cf15d16a 100644 --- a/src/slash-commands/utils.ts +++ b/src/slash-commands/utils.ts @@ -49,16 +49,14 @@ export const singleMxcUpload = async (cli: MatrixClient): Promise const file = (ev as HTMLInputEvent).target.files?.[0]; if (!file) return; - Modal.createDialog(UploadConfirmDialog, { - file, - onFinished: async (shouldContinue): Promise => { - if (shouldContinue) { - const { content_uri: uri } = await cli.uploadContent(file); - resolve(uri); - } else { - resolve(null); - } - }, + const { finished } = Modal.createDialog(UploadConfirmDialog, { file }); + finished.then(async ([shouldContinue]) => { + if (shouldContinue) { + const { content_uri: uri } = await cli.uploadContent(file); + resolve(uri); + } else { + resolve(null); + } }); }; diff --git a/src/stores/ModalWidgetStore.ts b/src/stores/ModalWidgetStore.ts index 10f16d24d2..2a2fbdd8da 100644 --- a/src/stores/ModalWidgetStore.ts +++ b/src/stores/ModalWidgetStore.ts @@ -61,18 +61,18 @@ export class ModalWidgetStore extends AsyncStoreWithClient { widgetDefinition: { ...requestData }, widgetRoomId, sourceWidgetId: sourceWidget.id, - onFinished: (success, data) => { - this.closeModalWidget(sourceWidget, widgetRoomId, success && data ? data : { "m.exited": true }); - - this.openSourceWidgetId = null; - this.openSourceWidgetRoomId = null; - this.modalInstance = null; - }, }, undefined, /* priority = */ false, /* static = */ true, ); + this.modalInstance!.finished.then(([success, data]) => { + this.closeModalWidget(sourceWidget, widgetRoomId, success && data ? data : { "m.exited": true }); + + this.openSourceWidgetId = null; + this.openSourceWidgetRoomId = null; + this.modalInstance = null; + }); }; public closeModalWidget = ( diff --git a/src/stores/ReleaseAnnouncementStore.ts b/src/stores/ReleaseAnnouncementStore.ts index 05362dde5a..0b287d44b9 100644 --- a/src/stores/ReleaseAnnouncementStore.ts +++ b/src/stores/ReleaseAnnouncementStore.ts @@ -17,7 +17,7 @@ import { Features } from "../settings/Settings"; /** * The features are shown in the array order. */ -const FEATURES = ["threadsActivityCentre", "pinningMessageList"] as const; +const FEATURES = ["pinningMessageList"] as const; /** * All the features that can be shown in the release announcements. */ diff --git a/src/stores/SetupEncryptionStore.ts b/src/stores/SetupEncryptionStore.ts index 0bb7a53257..9e324b6496 100644 --- a/src/stores/SetupEncryptionStore.ts +++ b/src/stores/SetupEncryptionStore.ts @@ -29,7 +29,6 @@ export enum Phase { Done = 3, // final done stage, but still showing UX ConfirmSkip = 4, Finished = 5, // UX can be closed - ConfirmReset = 6, } /** @@ -220,38 +219,6 @@ export class SetupEncryptionStore extends EventEmitter { this.emit("update"); } - public reset(): void { - this.phase = Phase.ConfirmReset; - this.emit("update"); - } - - public async resetConfirm(): Promise { - try { - // If we've gotten here, the user presumably lost their - // secret storage key if they had one. Start by resetting - // secret storage and setting up a new recovery key, then - // create new cross-signing keys once that succeeds. - await accessSecretStorage( - async (): Promise => { - this.phase = Phase.Finished; - }, - { - forceReset: true, - resetCrossSigning: true, - }, - ); - } catch (e) { - logger.error("Error resetting cross-signing", e); - this.phase = Phase.Intro; - } - this.emit("update"); - } - - public returnAfterReset(): void { - this.phase = Phase.Intro; - this.emit("update"); - } - public done(): void { this.phase = Phase.Finished; this.emit("update"); diff --git a/src/stores/room-list-v3/RoomListStoreV3.ts b/src/stores/room-list-v3/RoomListStoreV3.ts index e8eac5dff9..92de63285e 100644 --- a/src/stores/room-list-v3/RoomListStoreV3.ts +++ b/src/stores/room-list-v3/RoomListStoreV3.ts @@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details. */ import { logger } from "matrix-js-sdk/src/logger"; -import { EventType } from "matrix-js-sdk/src/matrix"; +import { EventType, KnownMembership } from "matrix-js-sdk/src/matrix"; import type { EmptyObject, Room, RoomState } from "matrix-js-sdk/src/matrix"; import type { MatrixDispatcher } from "../../dispatcher/dispatcher"; @@ -16,7 +16,6 @@ import { AsyncStoreWithClient } from "../AsyncStoreWithClient"; import SettingsStore from "../../settings/SettingsStore"; import { VisibilityProvider } from "../room-list/filters/VisibilityProvider"; import defaultDispatcher from "../../dispatcher/dispatcher"; -import { LISTS_UPDATE_EVENT } from "../room-list/RoomListStore"; import { RoomSkipList } from "./skip-list/RoomSkipList"; import { RecencySorter } from "./skip-list/sorters/RecencySorter"; import { AlphabeticSorter } from "./skip-list/sorters/AlphabeticSorter"; @@ -35,6 +34,7 @@ import { type Sorter, SortingAlgorithm } from "./skip-list/sorters"; import { SettingLevel } from "../../settings/SettingLevel"; import { MARKED_UNREAD_TYPE_STABLE, MARKED_UNREAD_TYPE_UNSTABLE } from "../../utils/notifications"; import { getChangedOverrideRoomMutePushRules } from "../room-list/utils/roomMute"; +import { Action } from "../../dispatcher/actions"; /** * These are the filters passed to the room skip list. @@ -49,6 +49,15 @@ const FILTERS = [ new LowPriorityFilter(), ]; +export enum RoomListStoreV3Event { + // The event/channel which is called when the room lists have been changed. + ListsUpdate = "lists_update", + // The event which is called when the room list is loaded. + ListsLoaded = "lists_loaded", +} + +export const LISTS_UPDATE_EVENT = RoomListStoreV3Event.ListsUpdate; +export const LISTS_LOADED_EVENT = RoomListStoreV3Event.ListsLoaded; /** * This store allows for fast retrieval of the room list in a sorted and filtered manner. * This is the third such implementation hence the "V3". @@ -76,6 +85,13 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient { return rooms; } + /** + * Check whether the initial list of rooms has loaded. + */ + public get isLoadingRooms(): boolean { + return !this.roomSkipList?.initialized; + } + /** * Get a list of sorted rooms. */ @@ -127,6 +143,7 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient { await SpaceStore.instance.storeReadyPromise; const rooms = this.getRooms(); this.roomSkipList.seed(rooms); + this.emit(LISTS_LOADED_EVENT); this.emit(LISTS_UPDATE_EVENT); } @@ -194,14 +211,29 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient { case "MatrixActions.Room.myMembership": { const oldMembership = getEffectiveMembership(payload.oldMembership); const newMembership = getEffectiveMembershipTag(payload.room, payload.membership); - if (oldMembership === EffectiveMembership.Join && newMembership === EffectiveMembership.Leave) { + + // If the user is kicked, re-insert the room and do nothing more. + const ownUserId = this.matrixClient.getSafeUserId(); + const isKicked = (payload.room as Room).getMember(ownUserId)?.isKicked(); + if (isKicked) { + this.addRoomAndEmit(payload.room); + return; + } + + // If the user has left this room, remove it from the skiplist. + if ( + (payload.oldMembership === KnownMembership.Invite || + payload.oldMembership === KnownMembership.Join) && + payload.membership === KnownMembership.Leave + ) { this.roomSkipList.removeRoom(payload.room); this.emit(LISTS_UPDATE_EVENT); return; } + + // If we're joining an upgraded room, we'll want to make sure we don't proliferate + // the dead room in the list. if (oldMembership !== EffectiveMembership.Join && newMembership === EffectiveMembership.Join) { - // If we're joining an upgraded room, we'll want to make sure we don't proliferate - // the dead room in the list. const roomState: RoomState = payload.room.currentState; const predecessor = roomState.findPredecessor(this.msc3946ProcessDynamicPredecessor); if (predecessor) { @@ -210,7 +242,15 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient { else logger.warn(`Unable to find predecessor room with id ${predecessor.roomId}`); } } - this.addRoomAndEmit(payload.room); + + this.addRoomAndEmit(payload.room, true); + break; + } + + case Action.AfterForgetRoom: { + const room = payload.room; + this.roomSkipList.removeRoom(room); + this.emit(LISTS_UPDATE_EVENT); break; } } @@ -234,7 +274,7 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient { logger.warn(`${roomId} was found in DMs but the room is not in the store`); continue; } - this.roomSkipList!.addRoom(room); + this.roomSkipList!.reInsertRoom(room); needsEmit = true; } } @@ -248,7 +288,7 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient { .map((id) => this.matrixClient?.getRoom(id)) .filter((room) => !!room); for (const room of rooms) { - this.roomSkipList!.addRoom(room); + this.roomSkipList!.reInsertRoom(room); needsEmit = true; } break; @@ -277,10 +317,21 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient { /** * Add a room to the skiplist and emit an update. * @param room The room to add to the skiplist + * @param isNewRoom Set this to true if this a new room that the isn't already in the skiplist */ - private addRoomAndEmit(room: Room): void { + private addRoomAndEmit(room: Room, isNewRoom = false): void { if (!this.roomSkipList) throw new Error("roomSkipList hasn't been created yet!"); - this.roomSkipList.addRoom(room); + if (isNewRoom) { + if (!VisibilityProvider.instance.isRoomVisible(room)) { + logger.info( + `RoomListStoreV3: Refusing to add new room ${room.roomId} because isRoomVisible returned false.`, + ); + return; + } + this.roomSkipList.addNewRoom(room); + } else { + this.roomSkipList.reInsertRoom(room); + } this.emit(LISTS_UPDATE_EVENT); } diff --git a/src/stores/room-list-v3/skip-list/RoomSkipList.ts b/src/stores/room-list-v3/skip-list/RoomSkipList.ts index 5de15eaa46..93c898ee21 100644 --- a/src/stores/room-list-v3/skip-list/RoomSkipList.ts +++ b/src/stores/room-list-v3/skip-list/RoomSkipList.ts @@ -90,15 +90,34 @@ export class RoomSkipList implements Iterable { } /** - * Adds a given room to the correct sorted position in the list. - * If the room is already present in the list, it is first removed. + * Re-inserts a room that is already in the skiplist. + * This method does nothing if the room isn't already in the skiplist. + * @param room the room to add */ - public addRoom(room: Room): void { - /** - * Remove this room from the skip list if necessary. - */ + public reInsertRoom(room: Room): void { + if (!this.roomNodeMap.has(room.roomId)) { + return; + } this.removeRoom(room); + this.addNewRoom(room); + } + /** + * Adds a new room to the skiplist. + * This method will throw an error if the room is already in the skiplist. + * @param room the room to add + */ + public addNewRoom(room: Room): void { + if (this.roomNodeMap.has(room.roomId)) { + throw new Error(`Can't add room to skiplist: ${room.roomId} is already in the skiplist!`); + } + this.insertRoom(room); + } + + /** + * Adds a given room to the correct sorted position in the list. + */ + private insertRoom(room: Room): void { const newNode = new RoomNode(room); newNode.checkIfRoomBelongsToActiveSpace(); newNode.applyFilters(this.filters); diff --git a/src/stores/spaces/SpaceStore.ts b/src/stores/spaces/SpaceStore.ts index ac4ffdaf0c..015c9fa0fd 100644 --- a/src/stores/spaces/SpaceStore.ts +++ b/src/stores/spaces/SpaceStore.ts @@ -21,7 +21,6 @@ import { } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; import { logger } from "matrix-js-sdk/src/logger"; -import { defer } from "matrix-js-sdk/src/utils"; import { AsyncStoreWithClient } from "../AsyncStoreWithClient"; import defaultDispatcher from "../../dispatcher/dispatcher"; @@ -153,7 +152,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { private _enabledMetaSpaces: MetaSpace[] = []; /** Whether the feature flag is set for MSC3946 */ private _msc3946ProcessDynamicPredecessor: boolean = SettingsStore.getValue("feature_dynamic_room_predecessors"); - private _storeReadyDeferred = defer(); + private _storeReadyDeferred = Promise.withResolvers(); public constructor() { super(defaultDispatcher, {}); diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index 31d1d6edac..d4c0f8930c 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -498,9 +498,9 @@ export class StopGapWidgetDriver extends WidgetDriver { if (results.length >= limit) break; if (since !== undefined && ev.getId() === since) break; - if (ev.getType() !== eventType || ev.isState()) continue; + if (ev.getType() !== eventType) continue; if (eventType === EventType.RoomMessage && msgtype && msgtype !== ev.getContent()["msgtype"]) continue; - if (ev.getStateKey() !== undefined && stateKey !== undefined && ev.getStateKey() !== stateKey) continue; + if (stateKey !== undefined && ev.getStateKey() !== stateKey) continue; results.push(ev); } @@ -557,19 +557,17 @@ export class StopGapWidgetDriver extends WidgetDriver { observer.update({ state: OpenIDRequestState.PendingUserConfirmation }); - Modal.createDialog(WidgetOpenIDPermissionsDialog, { + const { finished } = Modal.createDialog(WidgetOpenIDPermissionsDialog, { widget: this.forWidget, widgetKind: this.forWidgetKind, inRoomId: this.inRoomId, - - onFinished: async (confirm): Promise => { - if (!confirm) { - return observer.update({ state: OpenIDRequestState.Blocked }); - } - - return observer.update({ state: OpenIDRequestState.Allowed, token: await getToken() }); - }, }); + const [confirm] = await finished; + if (!confirm) { + observer.update({ state: OpenIDRequestState.Blocked }); + } else { + observer.update({ state: OpenIDRequestState.Allowed, token: await getToken() }); + } } public async navigate(uri: string): Promise { diff --git a/src/toasts/AnalyticsToast.tsx b/src/toasts/AnalyticsToast.tsx index 63e0757520..351ccdf4b2 100644 --- a/src/toasts/AnalyticsToast.tsx +++ b/src/toasts/AnalyticsToast.tsx @@ -34,34 +34,36 @@ const onReject = (): void => { }; const onLearnMoreNoOptIn = (): void => { - showAnalyticsLearnMoreDialog({ - onFinished: (buttonClicked?: ButtonClicked) => { - if (buttonClicked === ButtonClicked.Primary) { - // user clicked "Enable" - onAccept(); - } - // otherwise, the user either clicked "Cancel", or closed the dialog without making a choice, - // leave the toast open - }, + const { finished } = showAnalyticsLearnMoreDialog({ primaryButton: _t("action|enable"), }); + + finished.then(([buttonClicked]) => { + if (buttonClicked === ButtonClicked.Primary) { + // user clicked "Enable" + onAccept(); + } + // otherwise, the user either clicked "Cancel", or closed the dialog without making a choice, + // leave the toast open + }); }; const onLearnMorePreviouslyOptedIn = (): void => { - showAnalyticsLearnMoreDialog({ - onFinished: (buttonClicked?: ButtonClicked) => { - if (buttonClicked === ButtonClicked.Primary) { - // user clicked "That's fine" - onAccept(); - } else if (buttonClicked === ButtonClicked.Cancel) { - // user clicked "Stop" - onReject(); - } - // otherwise, the user closed the dialog without making a choice, leave the toast open - }, + const { finished } = showAnalyticsLearnMoreDialog({ primaryButton: _t("analytics|accept_button"), cancelButton: _t("action|stop"), }); + + finished.then(([buttonClicked]) => { + if (buttonClicked === ButtonClicked.Primary) { + // user clicked "That's fine" + onAccept(); + } else if (buttonClicked === ButtonClicked.Cancel) { + // user clicked "Stop" + onReject(); + } + // otherwise, the user closed the dialog without making a choice, leave the toast open + }); }; const TOAST_KEY = "analytics"; diff --git a/src/toasts/SetupEncryptionToast.ts b/src/toasts/SetupEncryptionToast.ts index 2b50ccd267..802da5b895 100644 --- a/src/toasts/SetupEncryptionToast.ts +++ b/src/toasts/SetupEncryptionToast.ts @@ -24,6 +24,7 @@ import { type OpenToTabPayload } from "../dispatcher/payloads/OpenToTabPayload"; import { Action } from "../dispatcher/actions"; import { UserTab } from "../components/views/dialogs/UserTab"; import defaultDispatcher from "../dispatcher/dispatcher"; +import ConfirmKeyStorageOffDialog from "../components/views/dialogs/ConfirmKeyStorageOffDialog"; const TOAST_KEY = "setupencryption"; @@ -37,6 +38,8 @@ const getTitle = (kind: Kind): string => { return _t("encryption|verify_toast_title"); case Kind.KEY_STORAGE_OUT_OF_SYNC: return _t("encryption|key_storage_out_of_sync"); + case Kind.TURN_ON_KEY_STORAGE: + return _t("encryption|turn_on_key_storage"); } }; @@ -49,6 +52,8 @@ const getIcon = (kind: Kind): string | undefined => { case Kind.VERIFY_THIS_SESSION: case Kind.KEY_STORAGE_OUT_OF_SYNC: return "verification_warning"; + case Kind.TURN_ON_KEY_STORAGE: + return "key_storage"; } }; @@ -62,6 +67,8 @@ const getSetupCaption = (kind: Kind): string => { return _t("action|verify"); case Kind.KEY_STORAGE_OUT_OF_SYNC: return _t("encryption|enter_recovery_key"); + case Kind.TURN_ON_KEY_STORAGE: + return _t("action|continue"); } }; @@ -87,6 +94,8 @@ const getSecondaryButtonLabel = (kind: Kind): string => { return _t("encryption|verification|unverified_sessions_toast_reject"); case Kind.KEY_STORAGE_OUT_OF_SYNC: return _t("encryption|forgot_recovery_key"); + case Kind.TURN_ON_KEY_STORAGE: + return _t("action|dismiss"); } }; @@ -100,6 +109,8 @@ const getDescription = (kind: Kind): string => { return _t("encryption|verify_toast_description"); case Kind.KEY_STORAGE_OUT_OF_SYNC: return _t("encryption|key_storage_out_of_sync_description"); + case Kind.TURN_ON_KEY_STORAGE: + return _t("encryption|turn_on_key_storage_description"); } }; @@ -123,6 +134,10 @@ export enum Kind { * Prompt the user to enter their recovery key */ KEY_STORAGE_OUT_OF_SYNC = "key_storage_out_of_sync", + /** + * Prompt the user to turn on key storage + */ + TURN_ON_KEY_STORAGE = "turn_on_key_storage", } /** @@ -143,6 +158,13 @@ export const showToast = (kind: Kind): void => { const onPrimaryClick = async (): Promise => { if (kind === Kind.VERIFY_THIS_SESSION) { Modal.createDialog(SetupEncryptionDialog, {}, undefined, /* priority = */ false, /* static = */ true); + } else if (kind == Kind.TURN_ON_KEY_STORAGE) { + // Open the user settings dialog to the encryption tab + const payload: OpenToTabPayload = { + action: Action.ViewUserSettings, + initialTabId: UserTab.Encryption, + }; + defaultDispatcher.dispatch(payload); } else { const modal = Modal.createDialog( Spinner, @@ -161,7 +183,7 @@ export const showToast = (kind: Kind): void => { } }; - const onSecondaryClick = (): void => { + const onSecondaryClick = async (): Promise => { if (kind === Kind.KEY_STORAGE_OUT_OF_SYNC) { // Open the user settings dialog to the encryption tab and start the flow to reset encryption const payload: OpenToTabPayload = { @@ -170,6 +192,15 @@ export const showToast = (kind: Kind): void => { props: { initialEncryptionState: "reset_identity_forgot" }, }; defaultDispatcher.dispatch(payload); + } else if (kind === Kind.TURN_ON_KEY_STORAGE) { + // The user clicked "Dismiss": offer them "Are you sure?" + const modal = Modal.createDialog(ConfirmKeyStorageOffDialog, undefined, "mx_ConfirmKeyStorageOffDialog"); + const [dismissed] = await modal.finished; + if (dismissed) { + const deviceListener = DeviceListener.sharedInstance(); + await deviceListener.recordKeyBackupDisabled(); + deviceListener.dismissEncryptionSetup(); + } } else { DeviceListener.sharedInstance().dismissEncryptionSetup(); } diff --git a/src/toasts/UpdateToast.tsx b/src/toasts/UpdateToast.tsx index 0abc95c066..4ced8144c1 100644 --- a/src/toasts/UpdateToast.tsx +++ b/src/toasts/UpdateToast.tsx @@ -32,27 +32,27 @@ export const showToast = (version: string, newVersion: string, releaseNotes?: st let acceptLabel = _t("update|see_changes_button"); if (releaseNotes) { onAccept = () => { - Modal.createDialog(QuestionDialog, { + const { finished } = Modal.createDialog(QuestionDialog, { title: _t("update|release_notes_toast_title"), description:
{releaseNotes}
, button: _t("action|update"), - onFinished: (update) => { - if (update && PlatformPeg.get()) { - PlatformPeg.get()!.installUpdate(); - } - }, + }); + finished.then(([update]) => { + if (update && PlatformPeg.get()) { + PlatformPeg.get()!.installUpdate(); + } }); }; } else if (checkVersion(version) && checkVersion(newVersion)) { onAccept = () => { - Modal.createDialog(ChangelogDialog, { + const { finished } = Modal.createDialog(ChangelogDialog, { version, newVersion, - onFinished: (update) => { - if (update && PlatformPeg.get()) { - PlatformPeg.get()!.installUpdate(); - } - }, + }); + finished.then(([update]) => { + if (update && PlatformPeg.get()) { + PlatformPeg.get()!.installUpdate(); + } }); }; } else { diff --git a/src/utils/DialogOpener.ts b/src/utils/DialogOpener.ts index 8304d6c2aa..9eadf63adf 100644 --- a/src/utils/DialogOpener.ts +++ b/src/utils/DialogOpener.ts @@ -119,7 +119,7 @@ export class DialogOpener { break; case Action.OpenAddToExistingSpaceDialog: { const space = payload.space; - Modal.createDialog( + const { finished } = Modal.createDialog( AddExistingToSpaceDialog, { onCreateRoomClick: (ev: ButtonEvent) => { @@ -128,14 +128,14 @@ export class DialogOpener { }, onAddSubspaceClick: () => showAddExistingSubspace(space), space, - onFinished: (added: boolean) => { - if (added && SdkContextClass.instance.roomViewStore.getRoomId() === space.roomId) { - defaultDispatcher.fire(Action.UpdateSpaceHierarchy); - } - }, }, "mx_AddExistingToSpaceDialog_wrapper", ); + finished.then(([added]) => { + if (added && SdkContextClass.instance.roomViewStore.getRoomId() === space.roomId) { + defaultDispatcher.fire(Action.UpdateSpaceHierarchy); + } + }); break; } } diff --git a/src/utils/MultiInviter.ts b/src/utils/MultiInviter.ts index 4f5bda879d..f8310de8bd 100644 --- a/src/utils/MultiInviter.ts +++ b/src/utils/MultiInviter.ts @@ -8,7 +8,6 @@ Please see LICENSE files in the repository root for full details. import { MatrixError, type MatrixClient, EventType, type EmptyObject } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; -import { defer, type IDeferred } from "matrix-js-sdk/src/utils"; import { logger } from "matrix-js-sdk/src/logger"; import { AddressType, getAddressType } from "../UserAddress"; @@ -51,7 +50,7 @@ export default class MultiInviter { private _fatal = false; private completionStates: CompletionStates = {}; // State of each address (invited or error) private errors: Record = {}; // { address: {errorText, errcode} } - private deferred: IDeferred | null = null; + private deferred: PromiseWithResolvers | null = null; private reason: string | undefined; /** @@ -93,7 +92,7 @@ export default class MultiInviter { }; } } - this.deferred = defer(); + this.deferred = Promise.withResolvers(); this.inviteMore(0); return this.deferred.promise; diff --git a/src/utils/StorageManager.ts b/src/utils/StorageManager.ts index 3b63f0a636..478d30af6f 100644 --- a/src/utils/StorageManager.ts +++ b/src/utils/StorageManager.ts @@ -92,8 +92,7 @@ export async function checkConsistency(): Promise<{ if (dataInLocalStorage && cryptoInited && !dataInCryptoStore) { healthy = false; error( - "Data exists in local storage and crypto is marked as initialised " + - " but no data found in crypto store. " + + "Data exists in local storage and crypto is marked as initialised but no data found in crypto store. " + "IndexedDB storage has likely been evicted by the browser!", ); } diff --git a/src/utils/Timer.ts b/src/utils/Timer.ts index 6b120fa30a..c5d9084c31 100644 --- a/src/utils/Timer.ts +++ b/src/utils/Timer.ts @@ -6,8 +6,6 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { type IDeferred, defer } from "matrix-js-sdk/src/utils"; - /** A countdown timer, exposing a promise api. A timer starts in a non-started state, @@ -22,7 +20,7 @@ a new one through `clone()` or `cloneIfRun()`. export default class Timer { private timerHandle?: number; private startTs?: number; - private deferred!: IDeferred; + private deferred!: PromiseWithResolvers; public constructor(private timeout: number) { this.setNotStarted(); @@ -31,7 +29,7 @@ export default class Timer { private setNotStarted(): void { this.timerHandle = undefined; this.startTs = undefined; - this.deferred = defer(); + this.deferred = Promise.withResolvers(); this.deferred.promise = this.deferred.promise.finally(() => { this.timerHandle = undefined; }); diff --git a/src/utils/exportUtils/HtmlExport.tsx b/src/utils/exportUtils/HtmlExport.tsx index 6a75e8b222..c20ea8762f 100644 --- a/src/utils/exportUtils/HtmlExport.tsx +++ b/src/utils/exportUtils/HtmlExport.tsx @@ -13,7 +13,6 @@ import { renderToStaticMarkup } from "react-dom/server"; import { logger } from "matrix-js-sdk/src/logger"; import escapeHtml from "escape-html"; import { TooltipProvider } from "@vector-im/compound-web"; -import { defer } from "matrix-js-sdk/src/utils"; import Exporter from "./Exporter"; import { mediaFromMxc } from "../../customisations/Media"; @@ -302,7 +301,7 @@ export default class HTMLExporter extends Exporter { if (hasAvatar) await this.saveAvatarIfNeeded(mxEv); // We have to wait for the component to be rendered before we can get the markup // so pass a deferred as a ref to the component. - const deferred = defer(); + const deferred = Promise.withResolvers(); const EventTile = this.getEventTile(mxEv, continuation, deferred.resolve); let eventTileMarkup: string; diff --git a/src/utils/leave-behaviour.ts b/src/utils/leave-behaviour.ts index ec24903ad4..2ad6e7d4fe 100644 --- a/src/utils/leave-behaviour.ts +++ b/src/utils/leave-behaviour.ts @@ -171,20 +171,20 @@ export async function leaveRoomBehaviour( } export const leaveSpace = (space: Room): void => { - Modal.createDialog( + const { finished } = Modal.createDialog( LeaveSpaceDialog, { space, - onFinished: async (leave: boolean, rooms: Room[]): Promise => { - if (!leave) return; - await bulkSpaceBehaviour(space, rooms, (room) => leaveRoomBehaviour(space.client, room.roomId)); - - dis.dispatch({ - action: Action.AfterLeaveRoom, - room_id: space.roomId, - }); - }, }, "mx_LeaveSpaceDialog_wrapper", ); + finished.then(async ([leave, rooms]) => { + if (!leave) return; + await bulkSpaceBehaviour(space, rooms!, (room) => leaveRoomBehaviour(space.client, room.roomId)); + + dis.dispatch({ + action: Action.AfterLeaveRoom, + room_id: space.roomId, + }); + }); }; diff --git a/src/utils/space.tsx b/src/utils/space.tsx index 1a9b104c4b..230c783322 100644 --- a/src/utils/space.tsx +++ b/src/utils/space.tsx @@ -75,7 +75,7 @@ export const showCreateNewRoom = async (space: Room, type?: RoomType): Promise { }; export const showAddExistingSubspace = (space: Room): void => { - Modal.createDialog( + const { finished } = Modal.createDialog( AddExistingSubspaceDialog, { space, onCreateSubspaceClick: () => showCreateNewSubspace(space), - onFinished: (added: boolean) => { - if (added && SdkContextClass.instance.roomViewStore.getRoomId() === space.roomId) { - defaultDispatcher.fire(Action.UpdateSpaceHierarchy); - } - }, }, "mx_AddExistingToSpaceDialog_wrapper", ); + finished.then(([added]) => { + if (added && SdkContextClass.instance.roomViewStore.getRoomId() === space.roomId) { + defaultDispatcher.fire(Action.UpdateSpaceHierarchy); + } + }); }; export const showCreateNewSubspace = (space: Room): void => { - Modal.createDialog( + const { finished } = Modal.createDialog( CreateSubspaceDialog, { space, onAddExistingSpaceClick: () => showAddExistingSubspace(space), - onFinished: (added: boolean) => { - if (added && SdkContextClass.instance.roomViewStore.getRoomId() === space.roomId) { - defaultDispatcher.fire(Action.UpdateSpaceHierarchy); - } - }, }, "mx_CreateSubspaceDialog_wrapper", ); + finished.then(([added]) => { + if (added && SdkContextClass.instance.roomViewStore.getRoomId() === space.roomId) { + defaultDispatcher.fire(Action.UpdateSpaceHierarchy); + } + }); }; export const bulkSpaceBehaviour = async ( diff --git a/src/vector/index.ts b/src/vector/index.ts index af557c241b..943ed49c6a 100644 --- a/src/vector/index.ts +++ b/src/vector/index.ts @@ -49,6 +49,8 @@ function checkBrowserFeatures(): boolean { window.Modernizr.addTest("promiseprototypefinally", () => typeof window.Promise?.prototype?.finally === "function"); // ES2020: http://262.ecma-international.org/#sec-promise.allsettled window.Modernizr.addTest("promiseallsettled", () => typeof window.Promise?.allSettled === "function"); + // ES2024: https://2ality.com/2024/05/proposal-promise-with-resolvers.html + window.Modernizr.addTest("promisewithresolvers", () => typeof window.Promise?.withResolvers === "function"); // ES2018: https://262.ecma-international.org/9.0/#sec-get-regexp.prototype.dotAll window.Modernizr.addTest( "regexpdotall", @@ -160,21 +162,18 @@ async function start(): Promise { // now that the config is ready, try to persist logs const persistLogsPromise = setupLogStorage(); - // Load modules & plugins before language to ensure any custom translations are respected, and any app - // startup functionality is run - const loadModulesPromise = loadModules(); - await settled(loadModulesPromise); - const loadPluginsPromise = loadPlugins(); - await settled(loadPluginsPromise); - // Load language after loading config.json so that settingsDefaults.language can be applied const loadLanguagePromise = loadLanguage(); // as quickly as we possibly can, set a default theme... const loadThemePromise = loadTheme(); - // await things settling so that any errors we have to render have features like i18n running await settled(loadThemePromise, loadLanguagePromise); + const loadModulesPromise = loadModules(); + await settled(loadModulesPromise); + const loadPluginsPromise = loadPlugins(); + await settled(loadPluginsPromise); + let acceptBrowser = supportedBrowser; if (!acceptBrowser && window.localStorage) { acceptBrowser = Boolean(window.localStorage.getItem("mx_accepts_unsupported_browser")); diff --git a/src/vector/init.tsx b/src/vector/init.tsx index 1169a6df28..e481e34b97 100644 --- a/src/vector/init.tsx +++ b/src/vector/init.tsx @@ -75,7 +75,7 @@ export async function loadLanguage(): Promise { langs = [prefLang]; } try { - await languageHandler.setLanguage(langs); + await languageHandler.setLanguage(...langs); document.documentElement.setAttribute("lang", languageHandler.getCurrentLanguage()); } catch (e) { logger.error("Unable to set language", e); diff --git a/src/vector/mobile_guide/index.html b/src/vector/mobile_guide/index.html index 30e26c619d..d58842d6a6 100644 --- a/src/vector/mobile_guide/index.html +++ b/src/vector/mobile_guide/index.html @@ -782,6 +782,8 @@

Go to Desktop Site +
+ Please note the Desktop site does not work on mobile.

diff --git a/src/vector/platform/ElectronPlatform.tsx b/src/vector/platform/ElectronPlatform.tsx index 3be158b18d..c8b544daf1 100644 --- a/src/vector/platform/ElectronPlatform.tsx +++ b/src/vector/platform/ElectronPlatform.tsx @@ -17,7 +17,6 @@ import { type OidcRegistrationClientMetadata, } from "matrix-js-sdk/src/matrix"; import React from "react"; -import { secureRandomString } from "matrix-js-sdk/src/randomstring"; import { logger } from "matrix-js-sdk/src/logger"; import BasePlatform, { UpdateCheckStatus, type UpdateStatus } from "../../BasePlatform"; @@ -97,8 +96,10 @@ function getUpdateCheckStatus(status: boolean | string): UpdateStatus { export default class ElectronPlatform extends BasePlatform { private readonly ipc = new IPCManager("ipcCall", "ipcReply"); private readonly eventIndexManager: BaseEventIndexManager = new SeshatIndexManager(); - // this is the opaque token we pass to the HS which when we get it in our callback we can resolve to a profile - private readonly ssoID: string = secureRandomString(32); + private readonly initialised: Promise; + private protocol!: string; + private sessionId!: string; + private config!: IConfigOptions; public constructor() { super(); @@ -186,13 +187,21 @@ export default class ElectronPlatform extends BasePlatform { await this.ipc.call("callDisplayMediaCallback", source ?? { id: "", name: "", thumbnailURL: "" }); }); - void this.ipc.call("startSSOFlow", this.ssoID); - BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate); + + this.initialised = this.initialise(); + } + + private async initialise(): Promise { + const { protocol, sessionId, config } = await window.electron!.initialise(); + this.protocol = protocol; + this.sessionId = sessionId; + this.config = config; } public async getConfig(): Promise { - return this.ipc.call("getConfig"); + await this.initialised; + return this.config; } private onBreadcrumbsUpdate = (): void => { @@ -391,7 +400,7 @@ export default class ElectronPlatform extends BasePlatform { public getSSOCallbackUrl(fragmentAfterLogin?: string): URL { const url = super.getSSOCallbackUrl(fragmentAfterLogin); url.protocol = "element"; - url.searchParams.set(SSO_ID_KEY, this.ssoID); + url.searchParams.set(SSO_ID_KEY, this.sessionId); return url; } @@ -469,7 +478,7 @@ export default class ElectronPlatform extends BasePlatform { } public getOidcClientState(): string { - return `:${SSO_ID_KEY}:${this.ssoID}`; + return `:${SSO_ID_KEY}:${this.sessionId}`; } /** @@ -477,7 +486,7 @@ export default class ElectronPlatform extends BasePlatform { */ public getOidcCallbackUrl(): URL { const url = super.getOidcCallbackUrl(); - url.protocol = "io.element.desktop"; + url.protocol = this.protocol; // Trim the double slash into a single slash to comply with https://datatracker.ietf.org/doc/html/rfc8252#section-7.1 if (url.href.startsWith(`${url.protocol}//`)) { url.href = url.href.replace("://", ":/"); diff --git a/src/vector/platform/IPCManager.ts b/src/vector/platform/IPCManager.ts index eb0f411025..dc183f0cfb 100644 --- a/src/vector/platform/IPCManager.ts +++ b/src/vector/platform/IPCManager.ts @@ -5,7 +5,6 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { defer, type IDeferred } from "matrix-js-sdk/src/utils"; import { logger } from "matrix-js-sdk/src/logger"; import { type ElectronChannel } from "../../@types/global"; @@ -17,7 +16,7 @@ interface IPCPayload { } export class IPCManager { - private pendingIpcCalls: { [ipcCallId: number]: IDeferred } = {}; + private pendingIpcCalls: { [ipcCallId: number]: PromiseWithResolvers } = {}; private nextIpcCallId = 0; public constructor( @@ -33,7 +32,7 @@ export class IPCManager { public async call(name: string, ...args: any[]): Promise { // TODO this should be moved into the preload.js file. const ipcCallId = ++this.nextIpcCallId; - const deferred = defer(); + const deferred = Promise.withResolvers(); this.pendingIpcCalls[ipcCallId] = deferred; // Maybe add a timeout to these? Probably not necessary. window.electron!.send(this.sendChannel, { id: ipcCallId, name, args }); diff --git a/src/vector/platform/WebPlatform.ts b/src/vector/platform/WebPlatform.ts index 316f479e73..46b6a61f83 100644 --- a/src/vector/platform/WebPlatform.ts +++ b/src/vector/platform/WebPlatform.ts @@ -18,6 +18,10 @@ import { Action } from "../../dispatcher/actions"; import { type CheckUpdatesPayload } from "../../dispatcher/payloads/CheckUpdatesPayload"; import { parseQs } from "../url_utils"; import { _t } from "../../languageHandler"; +import ToastStore from "../../stores/ToastStore.ts"; +import GenericToast from "../../components/views/toasts/GenericToast.tsx"; +import SdkConfig from "../../SdkConfig.ts"; +import type { ActionPayload } from "../../dispatcher/payloads.ts"; const POKE_RATE_MS = 10 * 60 * 1000; // 10 min @@ -32,32 +36,59 @@ function getNormalizedAppVersion(version: string): string { export default class WebPlatform extends BasePlatform { private static readonly VERSION = process.env.VERSION!; // baked in by Webpack + private readonly registerServiceWorkerPromise: Promise; public constructor() { super(); // Register the service worker in the background - this.tryRegisterServiceWorker().catch((e) => console.error("Error registering/updating service worker:", e)); + this.registerServiceWorkerPromise = this.registerServiceWorker(); + this.registerServiceWorkerPromise.catch((e) => { + console.error("Error registering/updating service worker:", e); + }); } - private async tryRegisterServiceWorker(): Promise { - if (!("serviceWorker" in navigator)) { - return; // not available on this platform - don't try to register the service worker - } + protected onAction(payload: ActionPayload): void { + super.onAction(payload); + switch (payload.action) { + case "client_started": + // Defer drawing the toast until the client is started as the lifecycle methods reset the ToastStore right before + this.registerServiceWorkerPromise.catch(this.handleServiceWorkerRegistrationError); + break; + } + } + + private async registerServiceWorker(): Promise { // sw.js is exported by webpack, sourced from `/src/serviceworker/index.ts` const registration = await navigator.serviceWorker.register("sw.js"); if (!registration) { - // Registration didn't work for some reason - assume failed and ignore. - // This typically happens in Jest. - return; + throw new Error("Service worker registration failed"); } - navigator.serviceWorker.addEventListener("message", this.onServiceWorkerPostMessage.bind(this)); + navigator.serviceWorker.addEventListener("message", this.onServiceWorkerPostMessage); await registration.update(); } - private onServiceWorkerPostMessage(event: MessageEvent): void { + private handleServiceWorkerRegistrationError = (): void => { + const key = "service_worker_error"; + const brand = SdkConfig.get().brand; + ToastStore.sharedInstance().addOrReplaceToast({ + key, + title: _t("service_worker_error|title"), + props: { + description: _t("service_worker_error|description", { brand }), + primaryLabel: _t("action|ok"), + onPrimaryClick: () => { + ToastStore.sharedInstance().dismissToast(key); + }, + }, + component: GenericToast, + priority: 95, + }); + }; + + private onServiceWorkerPostMessage = (event: MessageEvent): void => { try { if (event.data?.["type"] === "userinfo" && event.data?.["responseKey"]) { const userId = localStorage.getItem("mx_user_id"); @@ -73,7 +104,7 @@ export default class WebPlatform extends BasePlatform { } catch (e) { console.error("Error responding to service worker: ", e); } - } + }; public getHumanReadableName(): string { return "Web Platform"; // no translation required: only used for analytics diff --git a/test/components/views/dialogs/security/ResetIdentityDialog-test.tsx b/test/components/views/dialogs/security/ResetIdentityDialog-test.tsx new file mode 100644 index 0000000000..5de2a0a043 --- /dev/null +++ b/test/components/views/dialogs/security/ResetIdentityDialog-test.tsx @@ -0,0 +1,63 @@ +/* +Copyright 2024 New Vector Ltd. +Copyright 2018-2022 The Matrix.org Foundation C.I.C. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import React, { act } from "react"; +import { render } from "jest-matrix-react"; +import { type CryptoApi } from "matrix-js-sdk/src/crypto-api"; +import { type Mocked } from "jest-mock"; +import { type MatrixClient } from "matrix-js-sdk/src/matrix"; + +import { getMockClientWithEventEmitter } from "../../../../test-utils"; +import { ResetIdentityDialog } from "../../../../../src/components/views/dialogs/ResetIdentityDialog"; + +describe("ResetIdentityDialog", () => { + afterEach(() => { + jest.resetAllMocks(); + jest.restoreAllMocks(); + }); + + it("should call onReset and onFinished when we click Continue", async () => { + const client = mockClient(); + + const onFinished = jest.fn(); + const onReset = jest.fn(); + const dialog = render(); + + await act(async () => dialog.getByRole("button", { name: "Continue" }).click()); + + expect(onReset).toHaveBeenCalled(); + expect(onFinished).toHaveBeenCalled(); + + expect(client.getCrypto()?.resetEncryption).toHaveBeenCalled(); + }); + + it("should call onFinished when we click Cancel", async () => { + const client = mockClient(); + + const onFinished = jest.fn(); + const onReset = jest.fn(); + const dialog = render(); + + await act(async () => dialog.getByRole("button", { name: "Cancel" }).click()); + + expect(onFinished).toHaveBeenCalled(); + + expect(onReset).not.toHaveBeenCalled(); + expect(client.getCrypto()?.resetEncryption).not.toHaveBeenCalled(); + }); +}); + +function mockClient(): Mocked { + const mockCrypto = { + resetEncryption: jest.fn().mockResolvedValue(null), + } as unknown as Mocked; + + return getMockClientWithEventEmitter({ + getCrypto: jest.fn().mockReturnValue(mockCrypto), + }); +} diff --git a/test/components/views/dialogs/security/SetupEncryptionDialog-test.tsx b/test/components/views/dialogs/security/SetupEncryptionDialog-test.tsx new file mode 100644 index 0000000000..885366c4e2 --- /dev/null +++ b/test/components/views/dialogs/security/SetupEncryptionDialog-test.tsx @@ -0,0 +1,88 @@ +/* +Copyright 2024 New Vector Ltd. +Copyright 2018-2022 The Matrix.org Foundation C.I.C. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import React, { act } from "react"; +import { render, screen } from "jest-matrix-react"; +import { type Mocked } from "jest-mock"; +import { type CryptoApi } from "matrix-js-sdk/src/crypto-api"; + +import SetupEncryptionDialog from "../../../../../src/components/views/dialogs/security/SetupEncryptionDialog"; +import { getMockClientWithEventEmitter } from "../../../../test-utils"; +import { Phase, SetupEncryptionStore } from "../../../../../src/stores/SetupEncryptionStore"; +import Modal from "../../../../../src/Modal"; + +describe("SetupEncryptionDialog", () => { + afterEach(() => { + jest.resetAllMocks(); + jest.restoreAllMocks(); + }); + + it("should launch a dialog when I say Proceed, then be finished when I reset", async () => { + mockClient(); + const store = new SetupEncryptionStore(); + jest.spyOn(SetupEncryptionStore, "sharedInstance").mockReturnValue(store); + + // Given when you open the reset dialog we immediately reset + jest.spyOn(Modal, "createDialog").mockImplementation((_, props) => { + // Simulate doing the reset in the dialog + props?.onReset(); + + return { + close: jest.fn(), + finished: Promise.resolve([]), + }; + }); + + // When we launch the dialog and set it ready to start + const onFinished = jest.fn(); + render(); + await act(async () => await store.fetchKeyInfo()); + expect(store.phase).toBe(Phase.Intro); + + // And we hit the Proceed with reset button. + // (The createDialog mock above simulates the user doing the reset) + await act(async () => screen.getByRole("button", { name: "Proceed with reset" }).click()); + + // Then the phase has been set to Finished + expect(store.phase).toBe(Phase.Finished); + }); +}); + +function mockClient() { + const mockCrypto = { + getDeviceVerificationStatus: jest.fn().mockResolvedValue({ + crossSigningVerified: false, + }), + getUserDeviceInfo: jest.fn().mockResolvedValue(new Map()), + isCrossSigningReady: jest.fn().mockResolvedValue(true), + isSecretStorageReady: jest.fn().mockResolvedValue(true), + userHasCrossSigningKeys: jest.fn(), + getActiveSessionBackupVersion: jest.fn(), + getCrossSigningStatus: jest.fn().mockReturnValue({ + publicKeysOnDevice: true, + privateKeysInSecretStorage: true, + privateKeysCachedLocally: { + masterKey: true, + selfSigningKey: true, + userSigningKey: true, + }, + }), + getSessionBackupPrivateKey: jest.fn(), + isEncryptionEnabledInRoom: jest.fn(), + getKeyBackupInfo: jest.fn().mockResolvedValue(null), + getVerificationRequestsToDeviceInProgress: jest.fn().mockReturnValue([]), + } as unknown as Mocked; + + const userId = "@user:server"; + + getMockClientWithEventEmitter({ + getCrypto: jest.fn().mockReturnValue(mockCrypto), + getUserId: jest.fn().mockReturnValue(userId), + secretStorage: { isStored: jest.fn().mockReturnValue({}) }, + }); +} diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index 9ead26987a..8243fe5083 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -164,6 +164,7 @@ export function createTestClient(): MatrixClient { getVisibleRooms: jest.fn().mockReturnValue([]), loginFlows: jest.fn(), on: eventEmitter.on.bind(eventEmitter), + once: eventEmitter.once.bind(eventEmitter), off: eventEmitter.off.bind(eventEmitter), removeListener: eventEmitter.removeListener.bind(eventEmitter), emit: eventEmitter.emit.bind(eventEmitter), @@ -309,6 +310,8 @@ export function createTestClient(): MatrixClient { pushProcessor: { getPushRuleById: jest.fn(), }, + search: jest.fn().mockResolvedValue({}), + processRoomEventsSearch: jest.fn().mockResolvedValue({ highlights: [], results: [] }), } as unknown as MatrixClient; client.reEmitter = new ReEmitter(client); diff --git a/test/unit-tests/ContentMessages-test.ts b/test/unit-tests/ContentMessages-test.ts index 2b08cd85e0..80b400568e 100644 --- a/test/unit-tests/ContentMessages-test.ts +++ b/test/unit-tests/ContentMessages-test.ts @@ -14,7 +14,6 @@ import { type UploadResponse, } from "matrix-js-sdk/src/matrix"; import { type ImageInfo } from "matrix-js-sdk/src/types"; -import { defer } from "matrix-js-sdk/src/utils"; import encrypt, { type IEncryptedFile } from "matrix-encrypt-attachment"; import ContentMessages, { UploadCanceledError, uploadFile } from "../../src/ContentMessages"; @@ -338,7 +337,7 @@ describe("ContentMessages", () => { describe("cancelUpload", () => { it("should cancel in-flight upload", async () => { - const deferred = defer(); + const deferred = Promise.withResolvers(); mocked(client.uploadContent).mockReturnValue(deferred.promise); const file1 = new File([], "file1"); const prom = contentMessages.sendContentToRoom(file1, roomId, undefined, client, undefined); diff --git a/test/unit-tests/DeviceListener-test.ts b/test/unit-tests/DeviceListener-test.ts index f58eccf586..62221d0665 100644 --- a/test/unit-tests/DeviceListener-test.ts +++ b/test/unit-tests/DeviceListener-test.ts @@ -24,7 +24,7 @@ import { } from "matrix-js-sdk/src/crypto-api"; import { type CryptoSessionStateChange } from "@matrix-org/analytics-events/types/typescript/CryptoSessionStateChange"; -import DeviceListener from "../../src/DeviceListener"; +import DeviceListener, { BACKUP_DISABLED_ACCOUNT_DATA_KEY } from "../../src/DeviceListener"; import { MatrixClientPeg } from "../../src/MatrixClientPeg"; import * as SetupEncryptionToast from "../../src/toasts/SetupEncryptionToast"; import * as UnverifiedSessionToast from "../../src/toasts/UnverifiedSessionToast"; @@ -118,6 +118,7 @@ describe("DeviceListener", () => { getDeviceId: jest.fn().mockReturnValue(deviceId), setAccountData: jest.fn(), getAccountData: jest.fn(), + getAccountDataFromServer: jest.fn(), deleteAccountData: jest.fn(), getCrypto: jest.fn().mockReturnValue(mockCrypto), secretStorage: { @@ -309,6 +310,8 @@ describe("DeviceListener", () => { it("hides setup encryption toast when cross signing and secret storage are ready", async () => { mockCrypto!.isCrossSigningReady.mockResolvedValue(true); mockCrypto!.isSecretStorageReady.mockResolvedValue(true); + mockCrypto!.getActiveSessionBackupVersion.mockResolvedValue("1"); + await createAndStart(); expect(SetupEncryptionToast.hideToast).toHaveBeenCalled(); }); @@ -377,6 +380,7 @@ describe("DeviceListener", () => { it("hides the out-of-sync toast when one of the secrets is missing", async () => { mockCrypto!.isSecretStorageReady.mockResolvedValue(true); + mockCrypto!.getActiveSessionBackupVersion.mockResolvedValue("1"); // First show the toast mockCrypto!.getCrossSigningStatus.mockResolvedValue({ @@ -414,6 +418,7 @@ describe("DeviceListener", () => { it("shows set up recovery toast when user has a key backup available", async () => { // non falsy response mockCrypto.getKeyBackupInfo.mockResolvedValue({} as unknown as KeyBackupInfo); + mockCrypto.getActiveSessionBackupVersion.mockResolvedValue("1"); mockClient.secretStorage.getDefaultKeyId.mockResolvedValue(null); await createAndStart(); @@ -444,8 +449,13 @@ describe("DeviceListener", () => { it("dispatches keybackup event when key backup is not enabled", async () => { mockCrypto.getActiveSessionBackupVersion.mockResolvedValue(null); + mockClient.getAccountDataFromServer.mockImplementation((eventType) => + eventType === BACKUP_DISABLED_ACCOUNT_DATA_KEY ? ({ disabled: true } as any) : null, + ); await createAndStart(); - expect(mockDispatcher.dispatch).toHaveBeenCalledWith({ action: Action.ReportKeyBackupNotEnabled }); + expect(mockDispatcher.dispatch).toHaveBeenCalledWith({ + action: Action.ReportKeyBackupNotEnabled, + }); }); it("does not check key backup status again after check is complete", async () => { @@ -461,6 +471,137 @@ describe("DeviceListener", () => { }); }); + it("sets backup_disabled account data when we call recordKeyBackupDisabled", async () => { + const instance = await createAndStart(); + await instance.recordKeyBackupDisabled(); + + expect(mockClient.setAccountData).toHaveBeenCalledWith("m.org.matrix.custom.backup_disabled", { + disabled: true, + }); + }); + + describe("when crypto is in use and set up", () => { + beforeEach(() => { + // Encryption is in use + mockClient.getRooms.mockReturnValue([{ roomId: "!room1" }, { roomId: "!room2" }] as unknown as Room[]); + jest.spyOn(mockClient.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true); + + // The device is verified + mockCrypto.getDeviceVerificationStatus.mockResolvedValue( + new DeviceVerificationStatus({ crossSigningVerified: true }), + ); + }); + + describe("but key storage is off", () => { + beforeEach(() => { + // There is no active key backup/storage + mockCrypto.getActiveSessionBackupVersion.mockResolvedValue(null); + }); + + it("shows the 'Turn on key storage' toast if we never explicitly turned off key storage", async () => { + // Given key backup is off but the account data saying we turned it off is not set + // (m.org.matrix.custom.backup_disabled) + mockClient.getAccountData.mockReturnValue(undefined); + + // When we launch the DeviceListener + await createAndStart(); + + // Then the toast is displayed + expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith( + SetupEncryptionToast.Kind.TURN_ON_KEY_STORAGE, + ); + }); + + it("shows the 'Turn on key storage' toast if we turned on key storage", async () => { + // Given key backup is off but the account data says we turned it on (this should not happen - the + // account data should only be updated if we turn on key storage) + mockClient.getAccountData.mockImplementation((eventType) => + eventType === BACKUP_DISABLED_ACCOUNT_DATA_KEY + ? new MatrixEvent({ content: { disabled: false } }) + : undefined, + ); + + // When we launch the DeviceListener + await createAndStart(); + + // Then the toast is displayed + expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith( + SetupEncryptionToast.Kind.TURN_ON_KEY_STORAGE, + ); + }); + + it("does not show the 'Turn on key storage' toast if we turned off key storage", async () => { + // Given key backup is off but the account data saying we turned it off is set + mockClient.getAccountDataFromServer.mockImplementation((eventType) => + eventType === BACKUP_DISABLED_ACCOUNT_DATA_KEY ? ({ disabled: true } as any) : null, + ); + + // When we launch the DeviceListener + await createAndStart(); + + // Then the toast is not displayed + expect(SetupEncryptionToast.showToast).not.toHaveBeenCalledWith( + SetupEncryptionToast.Kind.TURN_ON_KEY_STORAGE, + ); + }); + }); + + describe("and key storage is on", () => { + beforeEach(() => { + // There is an active key backup/storage + mockCrypto.getActiveSessionBackupVersion.mockResolvedValue("1"); + }); + + it("does not show the 'Turn on key storage' toast if we never explicitly turned off key storage", async () => { + // Given key backup is on and the account data saying we turned it off is not set + mockClient.getAccountData.mockReturnValue(undefined); + + // When we launch the DeviceListener + await createAndStart(); + + // Then the toast is not displayed + expect(SetupEncryptionToast.showToast).not.toHaveBeenCalledWith( + SetupEncryptionToast.Kind.TURN_ON_KEY_STORAGE, + ); + }); + + it("does not show the 'Turn on key storage' toast if we turned on key storage", async () => { + // Given key backup is on and the account data says we turned it on + mockClient.getAccountData.mockImplementation((eventType) => + eventType === BACKUP_DISABLED_ACCOUNT_DATA_KEY + ? new MatrixEvent({ content: { disabled: false } }) + : undefined, + ); + + // When we launch the DeviceListener + await createAndStart(); + + // Then the toast is not displayed + expect(SetupEncryptionToast.showToast).not.toHaveBeenCalledWith( + SetupEncryptionToast.Kind.TURN_ON_KEY_STORAGE, + ); + }); + + it("does not show the 'Turn on key storage' toast if we turned off key storage", async () => { + // Given key backup is on but the account data saying we turned it off is set (this should never + // happen - it should only be set when we turn off key storage or dismiss the toast) + mockClient.getAccountData.mockImplementation((eventType) => + eventType === BACKUP_DISABLED_ACCOUNT_DATA_KEY + ? new MatrixEvent({ content: { disabled: true } }) + : undefined, + ); + + // When we launch the DeviceListener + await createAndStart(); + + // Then the toast is not displayed + expect(SetupEncryptionToast.showToast).not.toHaveBeenCalledWith( + SetupEncryptionToast.Kind.TURN_ON_KEY_STORAGE, + ); + }); + }); + }); + describe("unverified sessions toasts", () => { const currentDevice = new Device({ deviceId, userId: userId, algorithms: [], keys: new Map() }); const device2 = new Device({ deviceId: "d2", userId: userId, algorithms: [], keys: new Map() }); @@ -995,6 +1136,8 @@ describe("DeviceListener", () => { }); it("shows the 'set up recovery' toast if user has not set up 4S", async () => { + mockCrypto!.getActiveSessionBackupVersion.mockResolvedValue("1"); + await createAndStart(); expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith(SetupEncryptionToast.Kind.SET_UP_RECOVERY); diff --git a/test/unit-tests/HtmlUtils-test.tsx b/test/unit-tests/HtmlUtils-test.tsx index 16546e69dc..97c8da6013 100644 --- a/test/unit-tests/HtmlUtils-test.tsx +++ b/test/unit-tests/HtmlUtils-test.tsx @@ -12,14 +12,11 @@ import parse from "html-react-parser"; import { bodyToHtml, bodyToNode, formatEmojis, topicToHtml } from "../../src/HtmlUtils"; import SettingsStore from "../../src/settings/SettingsStore"; +import { getMockClientWithEventEmitter } from "../test-utils"; import { SettingLevel } from "../../src/settings/SettingLevel"; import SdkConfig from "../../src/SdkConfig"; describe("topicToHtml", () => { - afterEach(() => { - SettingsStore.reset(); - }); - function getContent() { return screen.getByRole("contentinfo").children[0].innerHTML; } @@ -35,19 +32,16 @@ describe("topicToHtml", () => { }); it("converts literal HTML topic to HTML", async () => { - SettingsStore.setValue("feature_html_topic", null, SettingLevel.DEVICE, true); render(
{topicToHtml("pizza", undefined, null, false)}
); expect(getContent()).toEqual("<b>pizza</b>"); }); it("converts true HTML topic to HTML", async () => { - SettingsStore.setValue("feature_html_topic", null, SettingLevel.DEVICE, true); render(
{topicToHtml("**pizza**", "pizza", null, false)}
); expect(getContent()).toEqual("pizza"); }); it("converts true HTML topic with emoji to HTML", async () => { - SettingsStore.setValue("feature_html_topic", null, SettingLevel.DEVICE, true); render(
{topicToHtml("**pizza** 🍕", "pizza 🍕", null, false)}
); expect(getContent()).toEqual('pizza 🍕'); }); @@ -231,6 +225,37 @@ describe("bodyToNode", () => { expect(asFragment()).toMatchSnapshot(); }); + it.each([[true], [false]])("should handle inline media when mediaIsVisible is %s", (mediaIsVisible) => { + const cli = getMockClientWithEventEmitter({ + mxcUrlToHttp: jest.fn().mockReturnValue("https://example.org/img"), + }); + const { className, formattedBody } = bodyToNode( + { + "body": "![foo](mxc://going/knowwhere) Hello there", + "format": "org.matrix.custom.html", + "formatted_body": `foo Hello there`, + "m.relates_to": { + "m.in_reply_to": { + event_id: "$eventId", + }, + }, + "msgtype": "m.text", + }, + [], + { + mediaIsVisible, + }, + ); + + const { asFragment } = render( + , + ); + expect(asFragment()).toMatchSnapshot(); + // We do not want to download untrusted media. + // eslint-disable-next-line no-restricted-properties + expect(cli.mxcUrlToHttp).toHaveBeenCalledTimes(mediaIsVisible ? 1 : 0); + }); + afterEach(() => { jest.resetAllMocks(); }); diff --git a/test/unit-tests/Modal-test.ts b/test/unit-tests/Modal-test.ts index d542fa3843..121c027b01 100644 --- a/test/unit-tests/Modal-test.ts +++ b/test/unit-tests/Modal-test.ts @@ -9,6 +9,7 @@ Please see LICENSE files in the repository root for full details. import Modal from "../../src/Modal"; import QuestionDialog from "../../src/components/views/dialogs/QuestionDialog"; import defaultDispatcher from "../../src/dispatcher/dispatcher"; +import { flushPromises } from "../test-utils"; describe("Modal", () => { test("forceCloseAllModals should close all open modals", () => { @@ -23,7 +24,7 @@ describe("Modal", () => { expect(Modal.hasDialogs()).toBe(false); }); - test("open modals should be closed on logout", () => { + test("open modals should be closed on logout", async () => { const modal1OnFinished = jest.fn(); const modal2OnFinished = jest.fn(); @@ -31,18 +32,18 @@ describe("Modal", () => { title: "Test dialog 1", description: "This is a test dialog", button: "Word", - onFinished: modal1OnFinished, - }); + }).finished.then(modal1OnFinished); Modal.createDialog(QuestionDialog, { title: "Test dialog 2", description: "This is a test dialog", button: "Word", - onFinished: modal2OnFinished, - }); + }).finished.then(modal2OnFinished); defaultDispatcher.dispatch({ action: "logout" }, true); + await flushPromises(); + expect(modal1OnFinished).toHaveBeenCalled(); expect(modal2OnFinished).toHaveBeenCalled(); }); diff --git a/test/unit-tests/SupportedBrowser-test.ts b/test/unit-tests/SupportedBrowser-test.ts index c95d2000b3..0eb2b42ed6 100644 --- a/test/unit-tests/SupportedBrowser-test.ts +++ b/test/unit-tests/SupportedBrowser-test.ts @@ -66,17 +66,17 @@ describe("SupportedBrowser", () => { // Safari 18.0 on macOS Sonoma "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15", // Latest Firefox on macOS Sonoma - "Mozilla/5.0 (Macintosh; Intel Mac OS X 14.7; rv:137.0) Gecko/20100101 Firefox/137.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 14.7; rv:139.0) Gecko/20100101 Firefox/139.0", // Latest Edge on Windows - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/134.0.3124.72", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.3240.92", // Latest Edge on macOS - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/134.0.3124.72", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.3240.92", // Latest Firefox on Windows - "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0", // Latest Firefox on Linux - "Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:137.0) Gecko/20100101 Firefox/137.0", + "Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0", // Latest Chrome on Windows - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36", ])("should not warn for supported browsers", testUserAgentFactory()); it.each([ diff --git a/test/unit-tests/__snapshots__/HtmlUtils-test.tsx.snap b/test/unit-tests/__snapshots__/HtmlUtils-test.tsx.snap index 09ab44bfcb..018a6721c1 100644 --- a/test/unit-tests/__snapshots__/HtmlUtils-test.tsx.snap +++ b/test/unit-tests/__snapshots__/HtmlUtils-test.tsx.snap @@ -64,3 +64,30 @@ exports[`bodyToNode should generate big emoji for an emoji-only reply to a messa `; + +exports[`bodyToNode should handle inline media when mediaIsVisible is false 1`] = ` + + + + foo Hello there + + +`; + +exports[`bodyToNode should handle inline media when mediaIsVisible is true 1`] = ` + + + + foo Hello there + + +`; diff --git a/test/unit-tests/audio/Playback-test.ts b/test/unit-tests/audio/Playback-test.ts index 0aeea5c832..7f14685f45 100644 --- a/test/unit-tests/audio/Playback-test.ts +++ b/test/unit-tests/audio/Playback-test.ts @@ -47,7 +47,7 @@ describe("Playback", () => { beforeEach(() => { jest.spyOn(logger, "error").mockRestore(); mockAudioBuffer.getChannelData.mockClear().mockReturnValue(mockChannelData); - mockAudioContext.decodeAudioData.mockReset().mockImplementation((_b, callback) => callback(mockAudioBuffer)); + mockAudioContext.decodeAudioData.mockReset().mockResolvedValue(mockAudioBuffer); mockAudioContext.resume.mockClear().mockResolvedValue(undefined); mockAudioContext.suspend.mockClear().mockResolvedValue(undefined); mocked(decodeOgg).mockClear().mockResolvedValue(new ArrayBuffer(1)); @@ -131,8 +131,8 @@ describe("Playback", () => { const buffer = new ArrayBuffer(8); const decodingError = new Error("test"); mockAudioContext.decodeAudioData - .mockImplementationOnce((_b, _callback, error) => error(decodingError)) - .mockImplementationOnce((_b, callback) => callback(mockAudioBuffer)); + .mockRejectedValueOnce(decodingError) + .mockResolvedValueOnce(mockAudioBuffer); const playback = new Playback(buffer); diff --git a/test/unit-tests/audio/compat-test.ts b/test/unit-tests/audio/compat-test.ts new file mode 100644 index 0000000000..da90d61711 --- /dev/null +++ b/test/unit-tests/audio/compat-test.ts @@ -0,0 +1,15 @@ +/* +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 { createAudioContext } from "../../../src/audio/compat"; + +describe("createAudioContext", () => { + it("should throw if AudioContext is not supported", () => { + window.AudioContext = undefined as any; + expect(createAudioContext).toThrow("Unsupported browser"); + }); +}); diff --git a/test/unit-tests/components/structures/MatrixChat-test.tsx b/test/unit-tests/components/structures/MatrixChat-test.tsx index 8633b3cfcf..62fcf40f4d 100644 --- a/test/unit-tests/components/structures/MatrixChat-test.tsx +++ b/test/unit-tests/components/structures/MatrixChat-test.tsx @@ -21,7 +21,7 @@ import { completeAuthorizationCodeGrant } from "matrix-js-sdk/src/oidc/authorize import { logger } from "matrix-js-sdk/src/logger"; import { OidcError } from "matrix-js-sdk/src/oidc/error"; import { type BearerTokenResponse } from "matrix-js-sdk/src/oidc/validate"; -import { defer, type IDeferred, sleep } from "matrix-js-sdk/src/utils"; +import { sleep } from "matrix-js-sdk/src/utils"; import { CryptoEvent, type DeviceVerificationStatus, @@ -68,6 +68,9 @@ import AutoDiscoveryUtils from "../../../../src/utils/AutoDiscoveryUtils"; import { type ValidatedServerConfig } from "../../../../src/utils/ValidatedServerConfig"; import Modal from "../../../../src/Modal.tsx"; import { SetupEncryptionStore } from "../../../../src/stores/SetupEncryptionStore.ts"; +import { ShareFormat } from "../../../../src/dispatcher/payloads/SharePayload.ts"; +import { clearStorage } from "../../../../src/Lifecycle"; +import RoomListStore from "../../../../src/stores/room-list/RoomListStore.ts"; jest.mock("matrix-js-sdk/src/oidc/authorize", () => ({ completeAuthorizationCodeGrant: jest.fn(), @@ -85,7 +88,7 @@ describe("", () => { const deviceId = "qwertyui"; const accessToken = "abc123"; const refreshToken = "def456"; - let bootstrapDeferred: IDeferred; + let bootstrapDeferred: PromiseWithResolvers; // reused in createClient mock below const getMockClientMethods = () => ({ ...mockClientMethodsUser(userId), @@ -154,6 +157,7 @@ describe("", () => { whoami: jest.fn(), logout: jest.fn(), getDeviceId: jest.fn(), + forget: () => Promise.resolve(), }); let mockClient: Mocked; const serverConfig = { @@ -217,6 +221,9 @@ describe("", () => { }; beforeEach(async () => { + await clearStorage(); + Lifecycle.setSessionLockNotStolen(); + localStorage.clear(); jest.restoreAllMocks(); defaultProps = { @@ -248,7 +255,7 @@ describe("", () => { {} as ValidatedServerConfig, ); - bootstrapDeferred = defer(); + bootstrapDeferred = Promise.withResolvers(); await clearAllModals(); }); @@ -344,10 +351,6 @@ describe("", () => { }, }); - jest.spyOn(logger, "error").mockClear(); - }); - - beforeEach(() => { loginClient = getMockClientWithEventEmitter(getMockClientMethods()); // this is used to create a temporary client during login jest.spyOn(MatrixJs, "createClient").mockReturnValue(loginClient); @@ -675,6 +678,34 @@ describe("", () => { jest.restoreAllMocks(); }); + describe("forget_room", () => { + it("should dispatch after_forget_room action on successful forget", async () => { + await clearAllModals(); + await getComponentAndWaitForReady(); + + // Mock out the old room list store + jest.spyOn(RoomListStore.instance, "manualRoomUpdate").mockImplementation(async () => {}); + + // Register a mock function to the dispatcher + const fn = jest.fn(); + defaultDispatcher.register(fn); + + // Forge the room + defaultDispatcher.dispatch({ + action: "forget_room", + room_id: roomId, + }); + + // On success, we expect the following action to have been dispatched. + await waitFor(() => { + expect(fn).toHaveBeenCalledWith({ + action: Action.AfterForgetRoom, + room: room, + }); + }); + }); + }); + describe("leave_room", () => { beforeEach(async () => { await clearAllModals(); @@ -783,6 +814,108 @@ describe("", () => { }); }); }); + + it("should open forward dialog when text message shared", async () => { + await getComponentAndWaitForReady(); + defaultDispatcher.dispatch({ action: Action.Share, format: ShareFormat.Text, msg: "Hello world" }); + await waitFor(() => { + expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ + action: Action.OpenForwardDialog, + event: expect.any(MatrixEvent), + permalinkCreator: null, + }); + }); + const forwardCall = mocked(defaultDispatcher.dispatch).mock.calls.find( + ([call]) => call.action === Action.OpenForwardDialog, + ); + + const payload = forwardCall?.[0]; + + expect(payload!.event.getContent()).toEqual({ + msgtype: MatrixJs.MsgType.Text, + body: "Hello world", + }); + }); + + it("should open forward dialog when html message shared", async () => { + await getComponentAndWaitForReady(); + defaultDispatcher.dispatch({ action: Action.Share, format: ShareFormat.Html, msg: "Hello world" }); + await waitFor(() => { + expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ + action: Action.OpenForwardDialog, + event: expect.any(MatrixEvent), + permalinkCreator: null, + }); + }); + const forwardCall = mocked(defaultDispatcher.dispatch).mock.calls.find( + ([call]) => call.action === Action.OpenForwardDialog, + ); + + const payload = forwardCall?.[0]; + + expect(payload!.event.getContent()).toEqual({ + msgtype: MatrixJs.MsgType.Text, + format: "org.matrix.custom.html", + body: expect.stringContaining("Hello world"), + formatted_body: expect.stringContaining("Hello world"), + }); + }); + + it("should open forward dialog when markdown message shared", async () => { + await getComponentAndWaitForReady(); + defaultDispatcher.dispatch({ + action: Action.Share, + format: ShareFormat.Markdown, + msg: "Hello *world*", + }); + await waitFor(() => { + expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ + action: Action.OpenForwardDialog, + event: expect.any(MatrixEvent), + permalinkCreator: null, + }); + }); + const forwardCall = mocked(defaultDispatcher.dispatch).mock.calls.find( + ([call]) => call.action === Action.OpenForwardDialog, + ); + + const payload = forwardCall?.[0]; + + expect(payload!.event.getContent()).toEqual({ + msgtype: MatrixJs.MsgType.Text, + format: "org.matrix.custom.html", + body: "Hello *world*", + formatted_body: "Hello world", + }); + }); + + it("should strip malicious tags from shared html message", async () => { + await getComponentAndWaitForReady(); + defaultDispatcher.dispatch({ + action: Action.Share, + format: ShareFormat.Html, + msg: `evil