diff --git a/.eslintrc.js b/.eslintrc.js index 342dbde484a..14afc41c070 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -76,6 +76,9 @@ module.exports = { group: [ "matrix-js-sdk/src/**", "!matrix-js-sdk/src/matrix", + "!matrix-js-sdk/src/crypto-api", + "!matrix-js-sdk/src/types", + "!matrix-js-sdk/src/testing", "matrix-js-sdk/lib", "matrix-js-sdk/lib/", "matrix-js-sdk/lib/**", @@ -104,13 +107,9 @@ module.exports = { "!matrix-js-sdk/src/extensible_events_v1/PollResponseEvent", "!matrix-js-sdk/src/extensible_events_v1/PollEndEvent", "!matrix-js-sdk/src/extensible_events_v1/InvalidEventError", - "!matrix-js-sdk/src/crypto-api", - "!matrix-js-sdk/src/crypto-api/verification", "!matrix-js-sdk/src/crypto", "!matrix-js-sdk/src/crypto/algorithms", - "!matrix-js-sdk/src/crypto/api", "!matrix-js-sdk/src/crypto/aes", - "!matrix-js-sdk/src/crypto/backup", "!matrix-js-sdk/src/crypto/olmlib", "!matrix-js-sdk/src/crypto/crypto", "!matrix-js-sdk/src/crypto/keybackup", @@ -120,13 +119,6 @@ module.exports = { "!matrix-js-sdk/src/crypto/CrossSigning", "!matrix-js-sdk/src/crypto/recoverykey", "!matrix-js-sdk/src/crypto/dehydration", - "!matrix-js-sdk/src/crypto/verification", - "!matrix-js-sdk/src/crypto/verification/SAS", - "!matrix-js-sdk/src/crypto/verification/QRCode", - "!matrix-js-sdk/src/crypto/verification/request", - "!matrix-js-sdk/src/crypto/verification/request/VerificationRequest", - "!matrix-js-sdk/src/common-crypto", - "!matrix-js-sdk/src/common-crypto/CryptoBackend", "!matrix-js-sdk/src/oidc", "!matrix-js-sdk/src/oidc/discovery", "!matrix-js-sdk/src/oidc/authorize", diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index a34327aa5e5..d4997def594 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,25 +2,7 @@ ## Checklist -- [ ] Tests written for new code (and old code if feasible) -- [ ] Linter and other CI checks pass -- [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-react-sdk/blob/develop/CONTRIBUTING.md)) - - +- [ ] Tests written for new code (and old code if feasible). +- [ ] New or updated `public`/`exported` symbols have accurate [TSDoc](https://tsdoc.org/) documentation. +- [ ] Linter and other CI checks pass. +- [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-react-sdk/blob/develop/CONTRIBUTING.md)). diff --git a/.github/workflows/end-to-end-tests.yaml b/.github/workflows/end-to-end-tests.yaml index 54ddb400065..3228fe91b35 100644 --- a/.github/workflows/end-to-end-tests.yaml +++ b/.github/workflows/end-to-end-tests.yaml @@ -156,7 +156,10 @@ jobs: merge-multiple: true - name: Merge into HTML Report - run: yarn playwright merge-reports --reporter=html,github ./all-blob-reports + run: yarn playwright merge-reports --reporter=html,github,./playwright/flaky-reporter.ts ./all-blob-reports + env: + # Only pass creds to the flaky-reporter on main branch runs + GITHUB_TOKEN: ${{ github.event.workflow_run.head_branch == 'develop' && secrets.ELEMENT_BOT_TOKEN || '' }} - name: Upload HTML report uses: actions/upload-artifact@v4 diff --git a/.github/workflows/netlify.yaml b/.github/workflows/netlify.yaml index ea393830d69..21c6f22df00 100644 --- a/.github/workflows/netlify.yaml +++ b/.github/workflows/netlify.yaml @@ -13,7 +13,7 @@ jobs: environment: Netlify steps: - name: 📝 Create Deployment - uses: bobheadxi/deployments@88ce5600046c82542f8246ac287d0a53c461bca3 # v1 + uses: bobheadxi/deployments@648679e8e4915b27893bd7dbc35cb504dc915bc8 # v1 id: deployment with: step: start diff --git a/.github/workflows/static_analysis.yaml b/.github/workflows/static_analysis.yaml index cae9e9dfd38..070ac5f8544 100644 --- a/.github/workflows/static_analysis.yaml +++ b/.github/workflows/static_analysis.yaml @@ -56,6 +56,13 @@ jobs: i18n_lint: name: "i18n Check" uses: matrix-org/matrix-web-i18n/.github/workflows/i18n_check.yml@main + with: + hardcoded-words: "Element" + allowed-hardcoded-keys: | + console_dev_note + labs|element_call_video_rooms + labs|feature_disable_call_per_sender_encryption + voip|element_call rethemendex_lint: name: "Rethemendex Check" diff --git a/CHANGELOG.md b/CHANGELOG.md index a8743130ba2..28af8e3187d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,104 @@ +Changes in [3.96.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.96.1) (2024-03-28) +===================================================================================================== +## 🐛 Bug Fixes + +* Revert "Make EC widget theme reactive - Update widget url when the theme changes" ([#12383](https://github.com/matrix-org/matrix-react-sdk/pull/12383)) in order to fix widgets that require authentication. + + +Changes in [3.96.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.96.0) (2024-03-26) +===================================================================================================== +## ✨ Features + +* Change user permission by using a new apply button ([#12346](https://github.com/matrix-org/matrix-react-sdk/pull/12346)). Contributed by @florianduros. +* Mark as Unread ([#12254](https://github.com/matrix-org/matrix-react-sdk/pull/12254)). Contributed by @dbkr. +* Refine the colors of some more components ([#12343](https://github.com/matrix-org/matrix-react-sdk/pull/12343)). Contributed by @robintown. +* TAC: Order rooms by most recent after notification level ([#12329](https://github.com/matrix-org/matrix-react-sdk/pull/12329)). Contributed by @florianduros. +* Make EC widget theme reactive - Update widget url when the theme changes ([#12295](https://github.com/matrix-org/matrix-react-sdk/pull/12295)). Contributed by @toger5. +* Refine styles of menus, toasts, popovers, and modals ([#12332](https://github.com/matrix-org/matrix-react-sdk/pull/12332)). Contributed by @robintown. +* Element Call: fix widget shown while its still loading (`waitForIframeLoad=false`) ([#12292](https://github.com/matrix-org/matrix-react-sdk/pull/12292)). Contributed by @toger5. +* Improve Forward Dialog a11y by switching to roving tab index interactions ([#12306](https://github.com/matrix-org/matrix-react-sdk/pull/12306)). Contributed by @t3chguy. +* Call guest access link creation to join calls as a non registered user via the EC SPA ([#12259](https://github.com/matrix-org/matrix-react-sdk/pull/12259)). Contributed by @toger5. +* Use `strong` element to semantically denote visually emphasised content ([#12320](https://github.com/matrix-org/matrix-react-sdk/pull/12320)). Contributed by @t3chguy. +* Handle up/down arrow keys as well as left/right for horizontal toolbars for improved a11y ([#12305](https://github.com/matrix-org/matrix-react-sdk/pull/12305)). Contributed by @t3chguy. + +## 🐛 Bug Fixes + +* [Backport staging] Remove the glass border from modal spinners ([#12369](https://github.com/matrix-org/matrix-react-sdk/pull/12369)). Contributed by @RiotRobot. +* Fix incorrect check for private read receipt support ([#12348](https://github.com/matrix-org/matrix-react-sdk/pull/12348)). Contributed by @tulir. +* TAC: Fix hover state when expanded ([#12337](https://github.com/matrix-org/matrix-react-sdk/pull/12337)). Contributed by @florianduros. +* Fix the image view ([#12341](https://github.com/matrix-org/matrix-react-sdk/pull/12341)). Contributed by @robintown. +* Use correct push rule to evaluate room-wide mentions ([#12318](https://github.com/matrix-org/matrix-react-sdk/pull/12318)). Contributed by @t3chguy. +* Reset power selector on API failure to prevent state mismatch ([#12319](https://github.com/matrix-org/matrix-react-sdk/pull/12319)). Contributed by @t3chguy. +* Fix spotlight opening in TAC ([#12315](https://github.com/matrix-org/matrix-react-sdk/pull/12315)). Contributed by @florianduros. + + +Changes in [3.95.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.95.0) (2024-03-14) +===================================================================================================== +## 🐛 Bug Fixes + +* Update `@vector-im/compound-design-tokens` in package.json ([#12340](https://github.com/matrix-org/matrix-react-sdk/pull/12340)). + +Changes in [3.94.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.94.0) (2024-03-12) +===================================================================================================== +## ✨ Features + +* Refine styles of controls to match Compound ([#12299](https://github.com/matrix-org/matrix-react-sdk/pull/12299)). Contributed by @robintown. +* Hide the archived section ([#12286](https://github.com/matrix-org/matrix-react-sdk/pull/12286)). Contributed by @dbkr. +* Add theme data to EC widget Url ([#12279](https://github.com/matrix-org/matrix-react-sdk/pull/12279)). Contributed by @toger5. +* Update MSC2965 OIDC Discovery implementation ([#12245](https://github.com/matrix-org/matrix-react-sdk/pull/12245)). Contributed by @t3chguy. +* Use green dot for activity notification in `LegacyRoomHeader` ([#12270](https://github.com/matrix-org/matrix-react-sdk/pull/12270)). Contributed by @florianduros. + +## 🐛 Bug Fixes + +* Fix requests for senders to submit auto-rageshakes ([#12304](https://github.com/matrix-org/matrix-react-sdk/pull/12304)). Contributed by @richvdh. +* fix automatic DM avatar with functional members ([#12157](https://github.com/matrix-org/matrix-react-sdk/pull/12157)). Contributed by @HarHarLinks. +* Feeds event with relation to unknown to the widget ([#12283](https://github.com/matrix-org/matrix-react-sdk/pull/12283)). Contributed by @maheichyk. +* Fix TAC opening with keyboard ([#12285](https://github.com/matrix-org/matrix-react-sdk/pull/12285)). Contributed by @florianduros. +* Allow screenshot update docker to run multiple test files ([#12291](https://github.com/matrix-org/matrix-react-sdk/pull/12291)). Contributed by @dbkr. +* Fix alignment of user menu avatar ([#12289](https://github.com/matrix-org/matrix-react-sdk/pull/12289)). Contributed by @dbkr. +* Fix buttons of widget in a room ([#12288](https://github.com/matrix-org/matrix-react-sdk/pull/12288)). Contributed by @florianduros. +* ModuleAPI: `overwrite_login` action was not stopping the existing client resulting in the action failing with rust-sdk ([#12272](https://github.com/matrix-org/matrix-react-sdk/pull/12272)). Contributed by @BillCarsonFr. + + +Changes in [3.93.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.93.0) (2024-02-27) +===================================================================================================== +## 🦖 Deprecations + +* Enable custom themes to theme Compound ([#12240](https://github.com/matrix-org/matrix-react-sdk/pull/12240)). Contributed by @robintown. +* Remove welcome bot `welcome_user_id` support ([#12153](https://github.com/matrix-org/matrix-react-sdk/pull/12153)). Contributed by @t3chguy. + +## ✨ Features + +* Ignore activity in TAC ([#12269](https://github.com/matrix-org/matrix-react-sdk/pull/12269)). Contributed by @florianduros. +* Use browser's font size instead of hardcoded `16px` as root font size ([#12246](https://github.com/matrix-org/matrix-react-sdk/pull/12246)). Contributed by @florianduros. +* Revert "Use Compound primary colors for most actions" ([#12264](https://github.com/matrix-org/matrix-react-sdk/pull/12264)). Contributed by @florianduros. +* Revert "Refine menu, toast, and popover colors" ([#12263](https://github.com/matrix-org/matrix-react-sdk/pull/12263)). Contributed by @florianduros. +* Fix Native OIDC for Element Desktop ([#12253](https://github.com/matrix-org/matrix-react-sdk/pull/12253)). Contributed by @t3chguy. +* Improve client metadata used for OIDC dynamic registration ([#12257](https://github.com/matrix-org/matrix-react-sdk/pull/12257)). Contributed by @t3chguy. +* Refine menu, toast, and popover colors ([#12247](https://github.com/matrix-org/matrix-react-sdk/pull/12247)). Contributed by @robintown. +* Call the AsJson forms of import and exportRoomKeys ([#12233](https://github.com/matrix-org/matrix-react-sdk/pull/12233)). Contributed by @andybalaam. +* Use Compound primary colors for most actions ([#12241](https://github.com/matrix-org/matrix-react-sdk/pull/12241)). Contributed by @robintown. +* Enable redirected media by default ([#12142](https://github.com/matrix-org/matrix-react-sdk/pull/12142)). Contributed by @turt2live. +* Reduce TAC width by `16px` ([#12239](https://github.com/matrix-org/matrix-react-sdk/pull/12239)). Contributed by @florianduros. +* Pop out of Threads Activity Centre ([#12136](https://github.com/matrix-org/matrix-react-sdk/pull/12136)). Contributed by @florianduros. +* Use new semantic tokens for username colors ([#12209](https://github.com/matrix-org/matrix-react-sdk/pull/12209)). Contributed by @robintown. + +## 🐛 Bug Fixes + +* [Backport staging] Fix spurious session corruption error ([#12287](https://github.com/matrix-org/matrix-react-sdk/pull/12287)). Contributed by @RiotRobot. +* Fix the space panel getting bigger when gaining a scroll bar ([#12267](https://github.com/matrix-org/matrix-react-sdk/pull/12267)). Contributed by @dbkr. +* Fix gradients spacings on the space panel ([#12262](https://github.com/matrix-org/matrix-react-sdk/pull/12262)). Contributed by @dbkr. +* Remove hardcoded `Element` in tac labs description ([#12266](https://github.com/matrix-org/matrix-react-sdk/pull/12266)). Contributed by @florianduros. +* Fix branding in "migrating crypto" message ([#12265](https://github.com/matrix-org/matrix-react-sdk/pull/12265)). Contributed by @richvdh. +* Use h1 as first heading in dialogs ([#12250](https://github.com/matrix-org/matrix-react-sdk/pull/12250)). Contributed by @dbkr. +* Fix forced lowercase username in login/registration flows ([#9329](https://github.com/matrix-org/matrix-react-sdk/pull/9329)). Contributed by @vrifox. +* Update the TAC indicator on event decryption ([#12243](https://github.com/matrix-org/matrix-react-sdk/pull/12243)). Contributed by @dbkr. +* Fix OIDC delegated auth account url check ([#12242](https://github.com/matrix-org/matrix-react-sdk/pull/12242)). Contributed by @t3chguy. +* New Header edgecase fixes: Close lobby button not shown, disable join button in various places, more... ([#12235](https://github.com/matrix-org/matrix-react-sdk/pull/12235)). Contributed by @toger5. +* Fix TAC button alignment when expanded ([#12238](https://github.com/matrix-org/matrix-react-sdk/pull/12238)). Contributed by @florianduros. +* Fix tooltip behaviour in TAC ([#12236](https://github.com/matrix-org/matrix-react-sdk/pull/12236)). Contributed by @florianduros. + + Changes in [3.92.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.92.0) (2024-02-13) ===================================================================================================== ## ✨ Features diff --git a/docs/playwright.md b/docs/playwright.md index d6f37f147e7..1d00e9781a0 100644 --- a/docs/playwright.md +++ b/docs/playwright.md @@ -85,7 +85,7 @@ to be left with some stray containers if, for example, you terminate a test such that the `after()` does not run and also exit Playwright uncleanly. All the containers it starts are prefixed, so they are easy to recognise. They can be removed safely. -After each test run, logs from the Synapse instances are saved in `playwright/synapselogs` +After each test run, logs from the Synapse instances are saved in `playwright/logs/synapse` with each instance in a separate directory named after its ID. These logs are removed at the start of each test run. diff --git a/package.json b/package.json index 0205783b36d..af4c78194f3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.92.0", + "version": "3.96.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -61,19 +61,21 @@ }, "resolutions": { "@types/react-dom": "17.0.21", - "@types/react": "17.0.68" + "@types/react": "17.0.68", + "oidc-client-ts": "3.0.1", + "jwt-decode": "4.0.0" }, "dependencies": { "@babel/runtime": "^7.12.5", - "@matrix-org/analytics-events": "^0.10.0", + "@matrix-org/analytics-events": "^0.19.0", "@matrix-org/emojibase-bindings": "^1.1.2", "@matrix-org/matrix-wysiwyg": "2.17.0", "@matrix-org/olm": "3.2.15", - "@matrix-org/react-sdk-module-api": "^2.3.0", + "@matrix-org/react-sdk-module-api": "^2.4.0", "@matrix-org/spec": "^1.7.0", "@sentry/browser": "^7.0.0", "@testing-library/react-hooks": "^8.0.1", - "@vector-im/compound-design-tokens": "^1.0.0", + "@vector-im/compound-design-tokens": "^1.2.0", "@vector-im/compound-web": "^3.1.1", "@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4", @@ -81,14 +83,14 @@ "await-lock": "^2.1.0", "blurhash": "^2.0.3", "classnames": "^2.2.6", - "commonmark": "^0.30.0", + "commonmark": "^0.31.0", "counterpart": "^0.18.6", "diff-dom": "^5.0.0", "diff-match-patch": "^1.0.5", "emojibase-regex": "15.3.0", "escape-html": "^1.0.3", "file-saver": "^2.0.5", - "filesize": "10.1.0", + "filesize": "10.1.1", "gfm.css": "^1.1.2", "glob-to-regexp": "^0.4.1", "graphemer": "^1.4.0", @@ -110,11 +112,11 @@ "matrix-widget-api": "^1.5.0", "memoize-one": "^6.0.0", "minimist": "^1.2.5", - "oidc-client-ts": "^2.2.4", + "oidc-client-ts": "^3.0.1", "opus-recorder": "^8.0.3", "pako": "^2.0.3", "png-chunks-extract": "^1.0.0", - "posthog-js": "1.100.0", + "posthog-js": "1.116.6", "proposal-temporal": "^0.9.0", "qrcode": "1.5.3", "re-resizable": "^6.9.0", @@ -126,15 +128,15 @@ "react-transition-group": "^4.4.1", "rfc4648": "^1.4.0", "sanitize-filename": "^1.6.3", - "sanitize-html": "2.11.0", + "sanitize-html": "2.13.0", "tar-js": "^0.3.0", "ua-parser-js": "^1.0.2", "uuid": "^9.0.0", "what-input": "^5.2.10" }, "devDependencies": { - "@action-validator/cli": "^0.5.3", - "@action-validator/core": "^0.5.3", + "@action-validator/cli": "^0.6.0", + "@action-validator/core": "^0.6.0", "@axe-core/playwright": "^4.8.1", "@babel/cli": "^7.12.10", "@babel/core": "^7.12.10", @@ -170,7 +172,7 @@ "@types/katex": "^0.16.0", "@types/lodash": "^4.14.168", "@types/modernizr": "^3.5.3", - "@types/node": "^16", + "@types/node": "18", "@types/node-fetch": "^2.6.2", "@types/pako": "^2.0.0", "@types/qrcode": "^1.3.5", @@ -178,18 +180,17 @@ "@types/react-beautiful-dnd": "^13.0.0", "@types/react-dom": "17.0.21", "@types/react-transition-group": "^4.4.0", - "@types/sanitize-html": "2.9.5", + "@types/sanitize-html": "2.11.0", "@types/sdp-transform": "^2.4.6", "@types/tar-js": "^0.3.2", "@types/ua-parser-js": "^0.7.36", "@types/uuid": "^9.0.2", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", - "allchange": "^1.1.0", - "axe-core": "4.8.3", + "@typescript-eslint/eslint-plugin": "^7.0.0", + "@typescript-eslint/parser": "^7.0.0", + "axe-core": "4.9.0", "babel-jest": "^29.0.0", "blob-polyfill": "^7.0.0", - "eslint": "8.56.0", + "eslint": "8.57.0", "eslint-config-google": "^0.14.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-deprecate": "0.8.4", @@ -199,7 +200,7 @@ "eslint-plugin-matrix-org": "1.2.1", "eslint-plugin-react": "^7.28.0", "eslint-plugin-react-hooks": "^4.3.0", - "eslint-plugin-unicorn": "^50.0.0", + "eslint-plugin-unicorn": "^51.0.0", "express": "^4.18.2", "fake-indexeddb": "^5.0.2", "fetch-mock-jest": "^1.5.1", @@ -211,18 +212,18 @@ "jest-raw-loader": "^1.0.1", "jsqr": "^1.4.0", "mailhog": "^4.16.0", - "matrix-web-i18n": "^3.1.5", + "matrix-web-i18n": "^3.2.1", "mocha-junit-reporter": "^2.2.0", "node-fetch": "2", "postcss-scss": "^4.0.4", - "prettier": "3.2.4", + "prettier": "3.2.5", "raw-loader": "^4.0.2", "rimraf": "^5.0.0", "stylelint": "^16.1.0", "stylelint-config-standard": "^36.0.0", "stylelint-scss": "^6.0.0", "ts-node": "^10.9.1", - "typescript": "5.3.3" + "typescript": "5.4.3" }, "peerDependencies": { "postcss": "^8.4.19", diff --git a/playwright/.gitignore b/playwright/.gitignore index 1d4efea520d..0d50077f5ad 100644 --- a/playwright/.gitignore +++ b/playwright/.gitignore @@ -1,6 +1,6 @@ /test-results/ /html-report/ -/synapselogs/ +/logs/ # Only commit snapshots from Linux /snapshots/**/*.png !/snapshots/**/*-linux.png diff --git a/playwright/Dockerfile b/playwright/Dockerfile index 24c2cfa2ee9..f13d7a2c686 100644 --- a/playwright/Dockerfile +++ b/playwright/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/playwright:v1.41.1-jammy +FROM mcr.microsoft.com/playwright:v1.42.1-jammy WORKDIR /work/matrix-react-sdk VOLUME ["/work/element-web/node_modules"] diff --git a/playwright/docker-entrypoint.sh b/playwright/docker-entrypoint.sh index 4d2354dfa4a..7bc5d2c9d84 100644 --- a/playwright/docker-entrypoint.sh +++ b/playwright/docker-entrypoint.sh @@ -5,4 +5,4 @@ set -e yarn link yarn --cwd ../element-web install yarn --cwd ../element-web link matrix-react-sdk -npx playwright test --update-snapshots --reporter line --project='Legacy Crypto' $1 +npx playwright test --update-snapshots --reporter line --project='Legacy Crypto' $@ diff --git a/playwright/e2e/audio-player/audio-player.spec.ts b/playwright/e2e/audio-player/audio-player.spec.ts index 11b7b53d505..4581801db5b 100644 --- a/playwright/e2e/audio-player/audio-player.spec.ts +++ b/playwright/e2e/audio-player/audio-player.spec.ts @@ -95,6 +95,10 @@ test.describe("Audio player", () => { .mx_MessageTimestamp { display: none !important; } + /* The MAB showing up on hover is not needed for the test */ + .mx_MessageActionBar { + display: none !important; + } `, mask: [page.locator(".mx_AudioPlayer_seek")], }; diff --git a/playwright/e2e/crypto/crypto.spec.ts b/playwright/e2e/crypto/crypto.spec.ts index d917a408398..957be587111 100644 --- a/playwright/e2e/crypto/crypto.spec.ts +++ b/playwright/e2e/crypto/crypto.spec.ts @@ -121,9 +121,6 @@ test.describe("Cryptography", function () { botCreateOpts: { displayName: "Bob", autoAcceptInvites: false, - // XXX: We use a custom prefix here to coerce the Rust Crypto SDK to prefer `@user` in race resolution - // by using a prefix that is lexically after `@user` in the alphabet. - userIdPrefix: "zzz_", }, }); @@ -152,6 +149,12 @@ test.describe("Cryptography", function () { await app.client.bootstrapCrossSigning(aliceCredentials); } + await page.route("**/_matrix/client/v3/keys/signatures/upload", async (route) => { + // We delay this API otherwise the `Setting up keys` may happen too quickly and cause flakiness + await new Promise((resolve) => setTimeout(resolve, 500)); + await route.continue(); + }); + await app.settings.openUserSettings("Security & Privacy"); await page.getByRole("button", { name: "Set up Secure Backup" }).click(); diff --git a/playwright/e2e/crypto/utils.ts b/playwright/e2e/crypto/utils.ts index 5ea7beccbd1..d43e4c7f941 100644 --- a/playwright/e2e/crypto/utils.ts +++ b/playwright/e2e/crypto/utils.ts @@ -22,8 +22,8 @@ import type { Verifier, EmojiMapping, VerifierEvent, -} from "matrix-js-sdk/src/crypto-api/verification"; -import type { ISasEvent } from "matrix-js-sdk/src/crypto/verification/SAS"; + ShowSasCallbacks, +} from "matrix-js-sdk/src/crypto-api"; import { Credentials, HomeserverInstance } from "../../plugins/homeserver"; import { Client } from "../../pages/client"; import { ElementAppPage } from "../../pages/ElementAppPage"; @@ -36,9 +36,7 @@ import { ElementAppPage } from "../../pages/ElementAppPage"; export async function waitForVerificationRequest(client: Client): Promise> { return client.evaluateHandle((cli) => { return new Promise((resolve) => { - console.log("~~"); const onVerificationRequestEvent = async (request: VerificationRequest) => { - console.log("@@", request); await request.accept(); resolve(request); }; @@ -65,7 +63,7 @@ export function handleSasVerification(verifier: JSHandle): Promise((resolve) => { - const onShowSas = (event: ISasEvent) => { + const onShowSas = (event: ShowSasCallbacks) => { verifier.off("show_sas" as VerifierEvent, onShowSas); event.confirm(); resolve(event.sas.emoji); diff --git a/playwright/e2e/crypto/verification.spec.ts b/playwright/e2e/crypto/verification.spec.ts index 55d65a9b087..6819606b642 100644 --- a/playwright/e2e/crypto/verification.spec.ts +++ b/playwright/e2e/crypto/verification.spec.ts @@ -15,9 +15,9 @@ limitations under the License. */ import jsQR from "jsqr"; +import { type Preset, type Visibility } from "matrix-js-sdk/src/matrix"; import type { JSHandle, Locator, Page } from "@playwright/test"; -import type { Preset, Visibility } from "matrix-js-sdk/src/matrix"; import type { VerificationRequest, Verifier } from "matrix-js-sdk/src/crypto-api"; import { test, expect } from "../../element-web-test"; import { diff --git a/playwright/e2e/knock/knock-into-room.spec.ts b/playwright/e2e/knock/knock-into-room.spec.ts index 21c5a145e3f..5ee366fcf25 100644 --- a/playwright/e2e/knock/knock-into-room.spec.ts +++ b/playwright/e2e/knock/knock-into-room.spec.ts @@ -16,7 +16,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import type { Visibility } from "matrix-js-sdk/src/matrix"; +import { type Visibility } from "matrix-js-sdk/src/matrix"; + import { test, expect } from "../../element-web-test"; import { waitForRoom } from "../utils"; import { Filter } from "../../pages/Spotlight"; @@ -227,8 +228,8 @@ test.describe("Knock Into Room", () => { await expect(roomPreviewBar.getByRole("button", { name: "Request access" })).toBeVisible(); await expect( - page.getByRole("group", { name: "Historical" }).getByRole("treeitem", { name: "Cybersecurity" }), - ).toBeVisible(); + page.getByRole("group", { name: "Rooms" }).getByRole("treeitem", { name: "Cybersecurity" }), + ).not.toBeVisible(); }); test("should knock into the room then knock is cancelled by another user and room is forgotten", async ({ diff --git a/playwright/e2e/login/overwrite_login.spec.ts b/playwright/e2e/login/overwrite_login.spec.ts new file mode 100644 index 00000000000..b047cfa3ddd --- /dev/null +++ b/playwright/e2e/login/overwrite_login.spec.ts @@ -0,0 +1,53 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { test, expect } from "../../element-web-test"; +import { logIntoElement } from "../crypto/utils"; + +test.describe("Overwrite login action", () => { + test("Try replace existing login with new one", async ({ page, app, credentials, homeserver }) => { + await logIntoElement(page, homeserver, credentials); + + const userMenu = await app.openUserMenu(); + await expect(userMenu.getByText(credentials.userId)).toBeVisible(); + + const bobRegister = await homeserver.registerUser("BobOverwrite", "p@ssword1!", "BOB"); + + // just assert that it's a different user + expect(credentials.userId).not.toBe(bobRegister.userId); + + const clientCredentials /* IMatrixClientCreds */ = { + homeserverUrl: homeserver.config.baseUrl, + ...bobRegister, + }; + + // Trigger the overwrite login action + await app.client.evaluate(async (cli, clientCredentials) => { + // @ts-ignore - raw access to the dispatcher to simulate the action + window.mxDispatcher.dispatch( + { + action: "overwrite_login", + credentials: clientCredentials, + }, + true, + ); + }, clientCredentials); + + // It should be now another user!! + const newUserMenu = await app.openUserMenu(); + await expect(newUserMenu.getByText(bobRegister.userId)).toBeVisible(); + }); +}); diff --git a/playwright/e2e/oidc/index.ts b/playwright/e2e/oidc/index.ts new file mode 100644 index 00000000000..61b9aa688bf --- /dev/null +++ b/playwright/e2e/oidc/index.ts @@ -0,0 +1,104 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { API, Messages } from "mailhog"; +import { Page } from "@playwright/test"; + +import { test as base, expect } from "../../element-web-test"; +import { MatrixAuthenticationService } from "../../plugins/matrix-authentication-service"; +import { StartHomeserverOpts } from "../../plugins/homeserver"; + +export const test = base.extend<{ + masPrepare: MatrixAuthenticationService; + mas: MatrixAuthenticationService; +}>({ + // There's a bit of a chicken and egg problem between MAS & Synapse where they each need to know how to reach each other + // so spinning up a MAS is split into the prepare & start stage: prepare mas -> homeserver -> start mas to disentangle this. + masPrepare: async ({ context }, use) => { + const mas = new MatrixAuthenticationService(context); + await mas.prepare(); + await use(mas); + }, + mas: [ + async ({ masPrepare: mas, homeserver, mailhog }, use, testInfo) => { + await mas.start(homeserver, mailhog.instance); + await use(mas); + await mas.stop(testInfo); + }, + { auto: true }, + ], + startHomeserverOpts: async ({ masPrepare }, use) => { + await use({ + template: "mas-oidc", + variables: { + MAS_PORT: masPrepare.port, + }, + }); + }, + config: async ({ homeserver, startHomeserverOpts, context }, use) => { + const issuer = `http://localhost:${(startHomeserverOpts as StartHomeserverOpts).variables["MAS_PORT"]}/`; + const wellKnown = { + "m.homeserver": { + base_url: homeserver.config.baseUrl, + }, + "org.matrix.msc2965.authentication": { + issuer, + account: `${issuer}account`, + }, + }; + + // Ensure org.matrix.msc2965.authentication is in well-known + await context.route("https://localhost/.well-known/matrix/client", async (route) => { + await route.fulfill({ json: wellKnown }); + }); + + await use({ + default_server_config: wellKnown, + }); + }, +}); + +export { expect }; + +export async function registerAccountMas( + page: Page, + mailhog: API, + username: string, + email: string, + password: string, +): Promise { + await expect(page.getByText("Please sign in to continue:")).toBeVisible(); + + await page.getByRole("link", { name: "Create Account" }).click(); + await page.getByRole("textbox", { name: "Username" }).fill(username); + await page.getByRole("textbox", { name: "Email address" }).fill(email); + await page.getByRole("textbox", { name: "Password", exact: true }).fill(password); + await page.getByRole("textbox", { name: "Confirm Password" }).fill(password); + await page.getByRole("button", { name: "Continue" }).click(); + + let messages: Messages; + await expect(async () => { + messages = await mailhog.messages(); + expect(messages.items).toHaveLength(1); + }).toPass(); + expect(messages.items[0].to).toEqual(`${username} <${email}>`); + const [code] = messages.items[0].text.match(/(\d{6})/); + + await page.getByRole("textbox", { name: "6-digit code" }).fill(code); + 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-aware.spec.ts b/playwright/e2e/oidc/oidc-aware.spec.ts new file mode 100644 index 00000000000..2df450243ac --- /dev/null +++ b/playwright/e2e/oidc/oidc-aware.spec.ts @@ -0,0 +1,42 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { test, expect, registerAccountMas } from "."; +import { isDendrite } from "../../plugins/homeserver/dendrite"; + +test.describe("OIDC Aware", () => { + test.skip(isDendrite, "does not yet support MAS"); + test.slow(); // trace recording takes a while here + + test("can register an account and manage it", async ({ context, page, homeserver, mailhog, app }) => { + await page.goto("/#/login"); + await page.getByRole("button", { name: "Continue" }).click(); + await registerAccountMas(page, mailhog.api, "alice", "alice@email.com", "Pa$sW0rD!"); + + // Eventually, we should end up at the home screen. + await expect(page).toHaveURL(/\/#\/home$/, { timeout: 10000 }); + await expect(page.getByRole("heading", { name: "Welcome alice", exact: true })).toBeVisible(); + + // Open settings and navigate to account management + await app.settings.openUserSettings("General"); + const newPagePromise = context.waitForEvent("page"); + await page.getByRole("button", { name: "Manage account" }).click(); + + // Assert new tab opened + const newPage = await newPagePromise; + await expect(newPage.getByText("Primary email")).toBeVisible(); + }); +}); diff --git a/playwright/e2e/oidc/oidc-native.spec.ts b/playwright/e2e/oidc/oidc-native.spec.ts new file mode 100644 index 00000000000..61795a85e56 --- /dev/null +++ b/playwright/e2e/oidc/oidc-native.spec.ts @@ -0,0 +1,74 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { test, expect, registerAccountMas } from "."; +import { isDendrite } from "../../plugins/homeserver/dendrite"; + +test.describe("OIDC Native", () => { + test.skip(isDendrite, "does not yet support MAS"); + test.slow(); // trace recording takes a while here + + test.use({ + labsFlags: ["feature_oidc_native_flow"], + }); + + test("can register the oauth2 client and an account", async ({ context, page, homeserver, mailhog, app, mas }) => { + const tokenUri = `http://localhost:${mas.port}/oauth2/token`; + const tokenApiPromise = page.waitForRequest( + (request) => request.url() === tokenUri && request.postDataJSON()["grant_type"] === "authorization_code", + ); + + await page.goto("/#/login"); + await page.getByRole("button", { name: "Continue" }).click(); + await registerAccountMas(page, mailhog.api, "alice", "alice@email.com", "Pa$sW0rD!"); + + // Eventually, we should end up at the home screen. + await expect(page).toHaveURL(/\/#\/home$/, { timeout: 10000 }); + await expect(page.getByRole("heading", { name: "Welcome alice", exact: true })).toBeVisible(); + + const tokenApiRequest = await tokenApiPromise; + expect(tokenApiRequest.postDataJSON()["grant_type"]).toBe("authorization_code"); + + const deviceId = await page.evaluate(() => window.localStorage.mx_device_id); + + await app.settings.openUserSettings("General"); + const newPagePromise = context.waitForEvent("page"); + await page.getByRole("button", { name: "Manage account" }).click(); + await app.settings.closeDialog(); + + // Assert MAS sees the session as OIDC Native + const newPage = await newPagePromise; + await newPage.getByText("Sessions").click(); + await newPage.getByText(deviceId).click(); + await expect(newPage.getByText("Element")).toBeVisible(); + await expect(newPage.getByText("oauth2_session:")).toBeVisible(); + await expect(newPage.getByText("http://localhost:8080/")).toBeVisible(); + await newPage.close(); + + // Assert logging out revokes both tokens + const revokeUri = `http://localhost:${mas.port}/oauth2/revoke`; + const revokeAccessTokenPromise = page.waitForRequest( + (request) => request.url() === revokeUri && request.postDataJSON()["token_type_hint"] === "access_token", + ); + const revokeRefreshTokenPromise = page.waitForRequest( + (request) => request.url() === revokeUri && request.postDataJSON()["token_type_hint"] === "refresh_token", + ); + const locator = await app.settings.openUserMenu(); + await locator.getByRole("menuitem", { name: "Sign out", exact: true }).click(); + await revokeAccessTokenPromise; + await revokeRefreshTokenPromise; + }); +}); diff --git a/playwright/e2e/one-to-one-chat/one-to-one-chat.spec.ts b/playwright/e2e/one-to-one-chat/one-to-one-chat.spec.ts index cfc0cac5e9b..39b30fbab54 100644 --- a/playwright/e2e/one-to-one-chat/one-to-one-chat.spec.ts +++ b/playwright/e2e/one-to-one-chat/one-to-one-chat.spec.ts @@ -43,8 +43,8 @@ test.describe("1:1 chat room", () => { // wait till the room was left await expect( - page.getByRole("group", { name: "Historical" }).locator(".mx_RoomTile").getByText(user2.displayName), - ).toBeVisible(); + page.getByRole("group", { name: "Rooms" }).locator(".mx_RoomTile").getByText(user2.displayName), + ).not.toBeVisible(); // open new 1:1 chat room await page.goto(`/#/user/${user2.userId}?action=chat`); diff --git a/playwright/e2e/presence/presence.spec.ts b/playwright/e2e/presence/presence.spec.ts index 62c14b7ed5b..861181ba56a 100644 --- a/playwright/e2e/presence/presence.spec.ts +++ b/playwright/e2e/presence/presence.spec.ts @@ -23,7 +23,9 @@ test.describe("Presence tests", () => { }); test.describe("bob unreachable", () => { - test("renders unreachable presence state correctly", async ({ page, app, user, bot: bob }) => { + // This is failing on CI (https://github.com/element-hq/element-web/issues/27270) + // but not locally, so debugging this is going to be tricky. Let's disable it for now. + test.skip("renders unreachable presence state correctly", async ({ page, app, user, bot: bob }) => { await app.client.createRoom({ name: "My Room", invite: [bob.credentials.userId] }); await app.viewRoomByName("My Room"); diff --git a/playwright/e2e/register/email.spec.ts b/playwright/e2e/register/email.spec.ts index 5b093f5036b..3ab408ae2f8 100644 --- a/playwright/e2e/register/email.spec.ts +++ b/playwright/e2e/register/email.spec.ts @@ -15,25 +15,17 @@ limitations under the License. */ import { test, expect } from "../../element-web-test"; -import { MailHogServer } from "../../plugins/mailhog"; import { isDendrite } from "../../plugins/homeserver/dendrite"; test.describe("Email Registration", async () => { test.skip(isDendrite, "not yet wired up"); test.use({ - // eslint-disable-next-line no-empty-pattern - mailhog: async ({}, use) => { - const mailhog = new MailHogServer(); - const instance = await mailhog.start(); - await use(instance); - await mailhog.stop(); - }, startHomeserverOpts: ({ mailhog }, use) => use({ template: "email", variables: { - SMTP_HOST: "{{HOST_DOCKER_INTERNAL}}", // This will get replaced in synapseStart + SMTP_HOST: "host.containers.internal", SMTP_PORT: mailhog.instance.smtpPort, }, }), diff --git a/playwright/e2e/room/room-header.spec.ts b/playwright/e2e/room/room-header.spec.ts index 2d0af8a6df9..a3c5e8c8bc3 100644 --- a/playwright/e2e/room/room-header.spec.ts +++ b/playwright/e2e/room/room-header.spec.ts @@ -18,6 +18,7 @@ import { Page } from "@playwright/test"; import { test, expect } from "../../element-web-test"; import { ElementAppPage } from "../../pages/ElementAppPage"; +import type { Container } from "../../../src/stores/widgets/types"; test.describe("Room Header", () => { test.use({ @@ -227,7 +228,7 @@ test.describe("Room Header", () => { { widgets: { [id]: { - container: "top", + container: "top" as Container, index: 1, width: 100, height: 0, diff --git a/playwright/e2e/room/room.spec.ts b/playwright/e2e/room/room.spec.ts index 43edeaab386..5b60e6f3bbe 100644 --- a/playwright/e2e/room/room.spec.ts +++ b/playwright/e2e/room/room.spec.ts @@ -92,6 +92,10 @@ test.describe("Room Directory", () => { // Display Room B await app.viewRoomById(roomBId); + + // Let the app settle to avoid flakiness + await page.waitForTimeout(500); + // Display Room A await app.viewRoomById(roomAId); diff --git a/playwright/e2e/room_options/marked_unread.spec.ts b/playwright/e2e/room_options/marked_unread.spec.ts new file mode 100644 index 00000000000..799acf22500 --- /dev/null +++ b/playwright/e2e/room_options/marked_unread.spec.ts @@ -0,0 +1,61 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { test, expect } from "../../element-web-test"; + +const TEST_ROOM_NAME = "The mark unread test room"; + +test.describe("Mark as Unread", () => { + test.use({ + displayName: "Tom", + botCreateOpts: { + displayName: "BotBob", + autoAcceptInvites: true, + }, + }); + + test("should mark a room as unread", async ({ page, app, bot }) => { + const roomId = await app.client.createRoom({ + name: TEST_ROOM_NAME, + }); + const dummyRoomId = await app.client.createRoom({ + name: "Room of no consequence", + }); + await app.client.inviteUser(roomId, bot.credentials.userId); + await bot.joinRoom(roomId); + await bot.sendMessage(roomId, "I am a robot. Beep."); + + // Regular notification on new message + await expect(page.getByLabel(TEST_ROOM_NAME + " 1 unread message.")).toBeVisible(); + await expect(page).toHaveTitle("Element [1]"); + + await page.goto("/#/room/" + roomId); + + // should now be read, since we viewed the room (we have to assert the page title: + // the room badge isn't visible since we're viewing the room) + await expect(page).toHaveTitle("Element | " + TEST_ROOM_NAME); + + // navigate away from the room again + await page.goto("/#/room/" + dummyRoomId); + + const roomTile = page.getByLabel(TEST_ROOM_NAME); + await roomTile.focus(); + await roomTile.getByRole("button", { name: "Room options" }).click(); + await page.getByRole("menuitem", { name: "Mark as unread" }).click(); + + expect(page.getByLabel(TEST_ROOM_NAME + " Unread messages.")).toBeVisible(); + }); +}); diff --git a/playwright/e2e/settings/appearance-user-settings-tab.spec.ts b/playwright/e2e/settings/appearance-user-settings-tab.spec.ts index 3f220acc076..df091f45a8c 100644 --- a/playwright/e2e/settings/appearance-user-settings-tab.spec.ts +++ b/playwright/e2e/settings/appearance-user-settings-tab.spec.ts @@ -73,48 +73,18 @@ test.describe("Appearance user settings tab", () => { await expect(page.locator(".mx_RoomView_body[data-layout='bubble']")).toBeVisible(); }); - test("should support changing font size by clicking the font slider", async ({ page, app, user }) => { + test("should support changing font size by using the font size dropdown", async ({ page, app, user }) => { await app.settings.openUserSettings("Appearance"); const tab = page.getByTestId("mx_AppearanceUserSettingsTab"); - const fontSliderSection = tab.locator(".mx_FontScalingPanel_fontSlider"); + const fontDropdown = tab.locator(".mx_FontScalingPanel_Dropdown"); + await expect(fontDropdown.getByLabel("Font size")).toBeVisible(); - await expect(fontSliderSection.getByLabel("Font size")).toBeVisible(); + // Default browser font size is 16px and the select value is 0 + // -4 value is 12px + await fontDropdown.getByLabel("Font size").selectOption({ value: "-4" }); - const slider = fontSliderSection.getByRole("slider"); - // Click the left position of the slider - await slider.click({ position: { x: 0, y: 10 } }); - - const MIN_FONT_SIZE = 11; - // Assert that the smallest font size is selected - await expect(fontSliderSection.locator(`input[value='${MIN_FONT_SIZE}']`)).toBeVisible(); - await expect( - fontSliderSection.locator("output .mx_Slider_selection_label", { hasText: String(MIN_FONT_SIZE) }), - ).toBeVisible(); - - await expect(fontSliderSection).toMatchScreenshot(`font-slider-${MIN_FONT_SIZE}.png`); - - // Click the right position of the slider - await slider.click({ position: { x: 572, y: 10 } }); - - const MAX_FONT_SIZE = 21; - // Assert that the largest font size is selected - await expect(fontSliderSection.locator(`input[value='${MAX_FONT_SIZE}']`)).toBeVisible(); - await expect( - fontSliderSection.locator("output .mx_Slider_selection_label", { hasText: String(MAX_FONT_SIZE) }), - ).toBeVisible(); - - await expect(fontSliderSection).toMatchScreenshot(`font-slider-${MAX_FONT_SIZE}.png`); - }); - - test("should disable font size slider when custom font size is used", async ({ page, app, user }) => { - await app.settings.openUserSettings("Appearance"); - - const panel = page.getByTestId("mx_FontScalingPanel"); - await panel.locator("label", { hasText: "Use custom size" }).click(); - - // Assert that the font slider is disabled - await expect(panel.locator(".mx_FontScalingPanel_fontSlider input[disabled]")).toBeVisible(); + await expect(page).toMatchScreenshot("window-12px.png"); }); test("should support enabling compact group (modern) layout", async ({ page, app, user }) => { diff --git a/playwright/e2e/settings/roles-permissions-room-settings-tab.spec.ts b/playwright/e2e/settings/roles-permissions-room-settings-tab.spec.ts new file mode 100644 index 00000000000..8d8c2ebffa7 --- /dev/null +++ b/playwright/e2e/settings/roles-permissions-room-settings-tab.spec.ts @@ -0,0 +1,58 @@ +/* + * + * Copyright 2024 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * / + */ + +import { Locator } from "@playwright/test"; + +import { test, expect } from "../../element-web-test"; + +test.describe("Roles & Permissions room settings tab", () => { + const roomName = "Test room"; + + test.use({ + displayName: "Alice", + }); + + let settings: Locator; + + test.beforeEach(async ({ user, app }) => { + await app.client.createRoom({ name: roomName }); + await app.viewRoomByName(roomName); + settings = await app.settings.openRoomSettings("Roles & Permissions"); + }); + + test("should be able to change the role of a user", async ({ page, app, user }) => { + const privilegedUserSection = settings.locator(".mx_SettingsFieldset").first(); + const applyButton = privilegedUserSection.getByRole("button", { name: "Apply" }); + + // Alice is admin (100) and the Apply button should be disabled + await expect(applyButton).toBeDisabled(); + let combobox = privilegedUserSection.getByRole("combobox", { name: user.userId }); + await expect(combobox).toHaveValue("100"); + + // Change the role of Alice to Moderator (50) + await combobox.selectOption("Moderator"); + await expect(combobox).toHaveValue("50"); + await applyButton.click(); + + // Reload and check Alice is still Moderator (50) + await page.reload(); + settings = await app.settings.openRoomSettings("Roles & Permissions"); + combobox = privilegedUserSection.getByRole("combobox", { name: user.userId }); + await expect(combobox).toHaveValue("50"); + }); +}); diff --git a/playwright/e2e/spaces/threads-activity-centre/index.ts b/playwright/e2e/spaces/threads-activity-centre/index.ts index b0fcd4648a8..fadf079ecaa 100644 --- a/playwright/e2e/spaces/threads-activity-centre/index.ts +++ b/playwright/e2e/spaces/threads-activity-centre/index.ts @@ -30,30 +30,30 @@ import { ElementAppPage } from "../../../pages/ElementAppPage"; * - Invite the bot to both rooms and ensure that it has joined */ export const test = base.extend<{ - roomAlphaName?: string; - roomAlpha: { name: string; roomId: string }; - roomBetaName?: string; - roomBeta: { name: string; roomId: string }; + room1Name?: string; + room1: { name: string; roomId: string }; + room2Name?: string; + room2: { name: string; roomId: string }; msg: MessageBuilder; util: Helpers; }>({ displayName: "Mae", botCreateOpts: { displayName: "Other User" }, - roomAlphaName: "Room Alpha", - roomAlpha: async ({ roomAlphaName: name, app, user, bot }, use) => { + room1Name: "Room 1", + room1: async ({ room1Name: name, app, user, bot }, use) => { const roomId = await app.client.createRoom({ name, invite: [bot.credentials.userId] }); await use({ name, roomId }); }, - roomBetaName: "Room Beta", - roomBeta: async ({ roomBetaName: name, app, user, bot }, use) => { + room2Name: "Room 2", + room2: async ({ room2Name: name, app, user, bot }, use) => { const roomId = await app.client.createRoom({ name, invite: [bot.credentials.userId] }); await use({ name, roomId }); }, msg: async ({ page, app, util }, use) => { await use(new MessageBuilder(page, app, util)); }, - util: async ({ roomAlpha, roomBeta, page, app, bot }, use) => { + util: async ({ room1, room2, page, app, bot }, use) => { await use(new Helpers(page, app, bot)); }, }); @@ -265,6 +265,13 @@ export class Helpers { return this.getTacButton().click(); } + /** + * Hover over the Threads Activity Centre button + */ + hoverTacButton() { + return this.getTacButton().hover(); + } + /** * Click on a room in the Threads Activity Centre * @param name - room name @@ -276,8 +283,12 @@ export class Helpers { /** * Assert that the threads activity centre button has no indicator */ - assertNoTacIndicator() { - return expect(this.getTacButton()).toMatchScreenshot("tac-no-indicator.png"); + async assertNoTacIndicator() { + // Assert by checkng neither of the known indicators are visible first. This will wait + // if it takes a little time to disappear, but the screenshot comparison won't. + await expect(this.getTacButton().locator("[data-indicator='success']")).not.toBeVisible(); + await expect(this.getTacButton().locator("[data-indicator='critical']")).not.toBeVisible(); + await expect(this.getTacButton()).toMatchScreenshot("tac-no-indicator.png"); } /** @@ -330,23 +341,27 @@ export class Helpers { * @param room1 * @param room2 * @param msg - MessageBuilder + * @param hasMention - whether to include a mention in the first message */ async populateThreads( room1: { name: string; roomId: string }, room2: { name: string; roomId: string }, msg: MessageBuilder, + hasMention = true, ) { - await this.receiveMessages(room2, [ - "Msg1", - msg.threadedOff("Msg1", { - "body": "User", - "format": "org.matrix.custom.html", - "formatted_body": "User", - "m.mentions": { - user_ids: ["@user:localhost"], - }, - }), - ]); + if (hasMention) { + await this.receiveMessages(room2, [ + "Msg1", + msg.threadedOff("Msg1", { + "body": "User", + "format": "org.matrix.custom.html", + "formatted_body": "User", + "m.mentions": { + user_ids: ["@user:localhost"], + }, + }), + ]); + } await this.receiveMessages(room2, ["Msg2", msg.threadedOff("Msg2", "Resp2")]); await this.receiveMessages(room1, ["Msg3", msg.threadedOff("Msg3", "Resp3")]); } @@ -364,6 +379,13 @@ export class Helpers { expandSpacePanel() { return this.page.getByRole("button", { name: "Expand" }).click(); } + + /** + * Clicks the button to mark all threads as read in the current room + */ + clickMarkAllThreadsRead() { + return this.page.getByLabel("Mark all as read").click(); + } } export { expect }; diff --git a/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts b/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts index 1b237c0b534..13361a70a27 100644 --- a/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts +++ b/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts @@ -17,6 +17,7 @@ */ import { expect, test } from "."; +import { CommandOrControl } from "../../utils"; test.describe("Threads Activity Centre", () => { test.use({ @@ -34,7 +35,7 @@ test.describe("Threads Activity Centre", () => { await expect(util.getSpacePanel()).toMatchScreenshot("tac-button-expanded.png"); }); - test("should not show indicator when there is no thread", async ({ roomAlpha: room1, util }) => { + test("should not show indicator when there is no thread", async ({ room1, util }) => { // No indicator should be shown await util.assertNoTacIndicator(); @@ -45,11 +46,7 @@ test.describe("Threads Activity Centre", () => { await util.assertNoTacIndicator(); }); - test("should show a notification indicator when there is a message in a thread", async ({ - roomAlpha: room1, - util, - msg, - }) => { + test("should show a notification indicator when there is a message in a thread", async ({ room1, util, msg }) => { await util.goTo(room1); await util.receiveMessages(room1, ["Msg1", msg.threadedOff("Msg1", "Resp1")]); @@ -57,11 +54,7 @@ test.describe("Threads Activity Centre", () => { await util.assertNotificationTac(); }); - test("should show a highlight indicator when there is a mention in a thread", async ({ - roomAlpha: room1, - util, - msg, - }) => { + test("should show a highlight indicator when there is a mention in a thread", async ({ room1, util, msg }) => { await util.goTo(room1); await util.receiveMessages(room1, [ "Msg1", @@ -79,7 +72,7 @@ test.describe("Threads Activity Centre", () => { await util.assertHighlightIndicator(); }); - test("should show the rooms with unread threads", async ({ roomAlpha: room1, roomBeta: room2, util, msg }) => { + test("should show the rooms with unread threads", async ({ room1, room2, util, msg }) => { await util.goTo(room2); await util.populateThreads(room1, room2, msg); // The indicator should be shown @@ -96,7 +89,7 @@ test.describe("Threads Activity Centre", () => { await expect(util.getTacPanel()).toMatchScreenshot("tac-panel-mix-unread.png"); }); - test("should update with a thread is read", async ({ roomAlpha: room1, roomBeta: room2, util, msg }) => { + test("should update with a thread is read", async ({ room1, room2, util, msg }) => { await util.goTo(room2); await util.populateThreads(room1, room2, msg); @@ -118,4 +111,53 @@ test.describe("Threads Activity Centre", () => { ]); await expect(util.getTacPanel()).toMatchScreenshot("tac-panel-notification-unread.png"); }); + + test("should order by recency after notification level", async ({ room1, room2, util, msg }) => { + await util.goTo(room2); + await util.populateThreads(room1, room2, msg, false); + + await util.openTac(); + await util.assertRoomsInTac([ + { room: room1.name, notificationLevel: "notification" }, + { room: room2.name, notificationLevel: "notification" }, + ]); + }); + + test("should block the Spotlight to open when the TAC is opened", async ({ util, page }) => { + const toggleSpotlight = () => page.keyboard.press(`${CommandOrControl}+k`); + + // Sanity check + // Open and close the spotlight + await toggleSpotlight(); + await expect(page.locator(".mx_SpotlightDialog")).toBeVisible(); + await toggleSpotlight(); + + await util.openTac(); + // The spotlight should not be opened + await toggleSpotlight(); + await expect(page.locator(".mx_SpotlightDialog")).not.toBeVisible(); + }); + + test("should have the correct hover state", async ({ util, page }) => { + await util.hoverTacButton(); + await expect(util.getSpacePanel()).toMatchScreenshot("tac-hovered.png"); + + // Expand the space panel, hover the button and take a screenshot + await util.expandSpacePanel(); + await util.hoverTacButton(); + await expect(util.getSpacePanel()).toMatchScreenshot("tac-hovered-expanded.png"); + }); + + test("should mark all threads as read", async ({ room1, room2, util, msg, page }) => { + await util.receiveMessages(room1, ["Msg1", msg.threadedOff("Msg1", "Resp1")]); + + await util.assertNotificationTac(); + + await util.openTac(); + await util.clickRoomInTac(room1.name); + + util.clickMarkAllThreadsRead(); + + await util.assertNoTacIndicator(); + }); }); diff --git a/playwright/e2e/timeline/timeline.spec.ts b/playwright/e2e/timeline/timeline.spec.ts index 67d7bb84d2e..2ca507fc9ec 100644 --- a/playwright/e2e/timeline/timeline.spec.ts +++ b/playwright/e2e/timeline/timeline.spec.ts @@ -1032,6 +1032,16 @@ test.describe("Timeline", () => { "et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut " + "aliquip"; + const newDisplayName = `${LONG_STRING} 2`; + + // Set the display name to "LONG_STRING 2" in order to avoid screenshot tests from failing + // due to the generated random mxid being displayed inside the GELS summary. + // Note that we set it here as the test was failing on CI (but not locally!) if the name + // was changed afterwards. This is quite concerning, but maybe better than just disabling the + // whole test? + // https://github.com/element-hq/element-web/issues/27109 + await app.client.setDisplayName(newDisplayName); + // Create a bot with a long display name const bot = new Bot(page, homeserver, { displayName: LONG_STRING, @@ -1049,13 +1059,9 @@ test.describe("Timeline", () => { await expect( page .locator(".mx_GenericEventListSummary_summary") - .getByText(OLD_NAME + " created and configured the room."), + .getByText(newDisplayName + " created and configured the room."), ).toBeVisible(); - // Set the display name to "LONG_STRING 2" in order to avoid screenshot tests from failing - // due to the generated random mxid being displayed inside the GELS summary. - await app.client.setDisplayName(`${LONG_STRING} 2`); - // Have the bot send a long message await bot.sendMessage(testRoomId, { body: LONG_STRING, diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts index 4e7d3a5b650..a524c139f6d 100644 --- a/playwright/element-web-test.ts +++ b/playwright/element-web-test.ts @@ -24,7 +24,7 @@ import type { IConfigOptions } from "../src/IConfigOptions"; import { Credentials, Homeserver, HomeserverInstance, StartHomeserverOpts } from "./plugins/homeserver"; import { Synapse } from "./plugins/homeserver/synapse"; import { Dendrite, Pinecone } from "./plugins/homeserver/dendrite"; -import { Instance } from "./plugins/mailhog"; +import { Instance, MailHogServer } from "./plugins/mailhog"; import { ElementAppPage } from "./pages/ElementAppPage"; import { OAuthServer } from "./plugins/oauth_server"; import { Crypto } from "./pages/crypto"; @@ -66,12 +66,32 @@ export const test = base.extend< TestOptions & { axe: AxeBuilder; checkA11y: () => Promise; - // The contents of the config.json to send + + /** + * The contents of the config.json to send when the client requests it. + */ config: typeof CONFIG_JSON; - // The options with which to run the `homeserver` fixture + + /** + * The options with which to run the {@link #homeserver} fixture. + */ startHomeserverOpts: StartHomeserverOpts | string; + homeserver: HomeserverInstance; oAuthServer: { port: number }; + + /** + * The displayname to use for the user registered in {@link #credentials}. + * + * To set it, call `test.use({ displayName: "myDisplayName" })` in the test file or `describe` block. + * See {@link https://playwright.dev/docs/api/class-test#test-use}. + */ + displayName?: string; + + /** + * A test fixture which registers a test user on the {@link #homeserver} and supplies the details + * of the registered user. + */ credentials: CredentialsWithDisplayName; /** @@ -83,10 +103,21 @@ export const test = base.extend< */ pageWithCredentials: Page; + /** + * A (rather poorly-named) test fixture which registers a user per {@link #credentials}, stores + * the credentials into localStorage per {@link #homeserver}, and then loads the front page of the + * app. + */ user: CredentialsWithDisplayName; - displayName?: string; + + /** + * The same as {@link https://playwright.dev/docs/api/class-fixtures#fixtures-page|`page`}, + * but wraps the returned `Page` in a class of utilities for interacting with the Element-Web UI, + * {@link ElementAppPage}. + */ app: ElementAppPage; - mailhog?: { api: mailhog.API; instance: Instance }; + + mailhog: { api: mailhog.API; instance: Instance }; crypto: Crypto; room?: { roomId: string }; toasts: Toasts; @@ -234,6 +265,14 @@ export const test = base.extend< await use(bot); }, + // eslint-disable-next-line no-empty-pattern + mailhog: async ({}, use) => { + const mailhog = new MailHogServer(); + const instance = await mailhog.start(); + await use(instance); + await mailhog.stop(); + }, + slidingSyncProxy: async ({ page, user, homeserver }, use) => { const proxy = new SlidingSyncProxy(homeserver.config.dockerUrl); const proxyInstance = await proxy.start(); diff --git a/playwright/flaky-reporter.ts b/playwright/flaky-reporter.ts new file mode 100644 index 00000000000..3d358bb74d1 --- /dev/null +++ b/playwright/flaky-reporter.ts @@ -0,0 +1,85 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Flaky test reporter, creating & updating GitHub issues + * Only intended to run from within GitHub Actions + */ + +import type { Reporter, TestCase } from "@playwright/test/reporter"; + +const REPO = "element-hq/element-web"; +const LABEL = "Z-Flaky-Test"; +const ISSUE_TITLE_PREFIX = "Flaky playwright test: "; + +class FlakyReporter implements Reporter { + private flakes = new Set(); + + public onTestEnd(test: TestCase): void { + const title = `${test.location.file.split("playwright/e2e/")[1]}: ${test.title}`; + if (test.outcome() === "flaky") { + this.flakes.add(title); + } + } + + public async onExit(): Promise { + if (this.flakes.size === 0) { + console.log("No flakes found"); + return; + } + + console.log("Found flakes: "); + for (const flake of this.flakes) { + console.log(flake); + } + + const { GITHUB_TOKEN, GITHUB_API_URL, GITHUB_SERVER_URL, GITHUB_REPOSITORY, GITHUB_RUN_ID } = process.env; + if (!GITHUB_TOKEN) return; + + const body = `${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}`; + + const headers = { Authorization: `Bearer ${GITHUB_TOKEN}` }; + // Fetch all existing issues with the flaky-test label. + const issuesRequest = await fetch(`${GITHUB_API_URL}/repos/${REPO}/issues?labels=${LABEL}`, { headers }); + const issues = await issuesRequest.json(); + for (const flake of this.flakes) { + const title = ISSUE_TITLE_PREFIX + "`" + flake + "`"; + const existingIssue = issues.find((issue) => issue.title === title); + + if (existingIssue) { + console.log(`Found issue ${existingIssue.number} for ${flake}, adding comment...`); + await fetch(`${existingIssue.url}/comments`, { + method: "POST", + headers, + body: JSON.stringify({ body }), + }); + } else { + console.log(`Creating new issue for ${flake}...`); + await fetch(`${GITHUB_API_URL}/repos/${REPO}/issues`, { + method: "POST", + headers, + body: JSON.stringify({ + title, + body, + labels: [LABEL], + }), + }); + } + } + } +} + +export default FlakyReporter; diff --git a/playwright/global.d.ts b/playwright/global.d.ts index 166bfbe9931..9663b9310fe 100644 --- a/playwright/global.d.ts +++ b/playwright/global.d.ts @@ -31,3 +31,10 @@ declare global { matrixcs: typeof Matrix; } } + +// Workaround for lack of strict mode not resolving complex types correctly +declare module "matrix-js-sdk/src/http-api/index.ts" { + interface UploadResponse { + json(): Promise; + } +} diff --git a/playwright/pages/ElementAppPage.ts b/playwright/pages/ElementAppPage.ts index be80cf62808..ac9b4ffef80 100644 --- a/playwright/pages/ElementAppPage.ts +++ b/playwright/pages/ElementAppPage.ts @@ -21,12 +21,29 @@ import { Client } from "./client"; import { Timeline } from "./timeline"; import { Spotlight } from "./Spotlight"; +/** + * A set of utility methods for interacting with the Element-Web UI. + */ export class ElementAppPage { public constructor(public readonly page: Page) {} - public settings = new Settings(this.page); - public client: Client = new Client(this.page); - public timeline: Timeline = new Timeline(this.page); + // We create these lazily on first access to avoid calling setup code which might cause conflicts, + // e.g. the network routing code in the client subfixture. + private _settings?: Settings; + public get settings(): Settings { + if (!this._settings) this._settings = new Settings(this.page); + return this._settings; + } + private _client?: Client; + public get client(): Client { + if (!this._client) this._client = new Client(this.page); + return this._client; + } + private _timeline?: Timeline; + public get timeline(): Timeline { + if (!this._timeline) this._timeline = new Timeline(this.page); + return this._timeline; + } /** * Open the top left user menu, returning a Locator to the resulting context menu. diff --git a/playwright/pages/bot.ts b/playwright/pages/bot.ts index d6b729420cd..333d895dfe9 100644 --- a/playwright/pages/bot.ts +++ b/playwright/pages/bot.ts @@ -16,8 +16,8 @@ limitations under the License. import { JSHandle, Page } from "@playwright/test"; import { uniqueId } from "lodash"; +import { type MatrixClient } from "matrix-js-sdk/src/matrix"; -import type { MatrixClient } from "matrix-js-sdk/src/matrix"; import type { Logger } from "matrix-js-sdk/src/logger"; import type { SecretStorageKeyDescription } from "matrix-js-sdk/src/secret-storage"; import type { Credentials, HomeserverInstance } from "../plugins/homeserver"; diff --git a/playwright/pages/client.ts b/playwright/pages/client.ts index 054b946845c..94ee5d88130 100644 --- a/playwright/pages/client.ts +++ b/playwright/pages/client.ts @@ -31,7 +31,10 @@ import type { Visibility, UploadOpts, Upload, + StateEvents, + TimelineEvents, } from "matrix-js-sdk/src/matrix"; +import type { RoomMessageEventContent } from "matrix-js-sdk/src/types"; import { Credentials } from "../plugins/homeserver"; export class Client { @@ -97,7 +100,12 @@ export class Client { const client = await this.prepareClient(); return client.evaluate( async (client, { roomId, threadId, eventType, content }) => { - return client.sendEvent(roomId, threadId, eventType, content); + return client.sendEvent( + roomId, + threadId, + eventType as keyof TimelineEvents, + content as TimelineEvents[keyof TimelineEvents], + ); }, { roomId, threadId, eventType, content }, ); @@ -124,7 +132,7 @@ export class Client { const client = await this.prepareClient(); return client.evaluate( (client, { roomId, content, threadId }) => { - return client.sendMessage(roomId, threadId, content); + return client.sendMessage(roomId, threadId, content as RoomMessageEventContent); }, { roomId, @@ -407,7 +415,7 @@ export class Client { const client = await this.prepareClient(); return client.evaluate( async (client, { roomId, eventType, content, stateKey }) => { - return client.sendStateEvent(roomId, eventType, content, stateKey); + return client.sendStateEvent(roomId, eventType as keyof StateEvents, content, stateKey); }, { roomId, eventType, content, stateKey }, ); diff --git a/playwright/plugins/docker/index.ts b/playwright/plugins/docker/index.ts index 7b6793eaed4..2b193c2fbd6 100644 --- a/playwright/plugins/docker/index.ts +++ b/playwright/plugins/docker/index.ts @@ -19,6 +19,37 @@ import * as crypto from "crypto"; import * as childProcess from "child_process"; import * as fse from "fs-extra"; +/** + * @param cmd - command to execute + * @param args - arguments to pass to executed command + * @param suppressOutput - whether to suppress the stdout and stderr resulting from this command. + * @return Promise which resolves to an object containing the string value of what was + * written to stdout and stderr by the executed command. + */ +const exec = (cmd: string, args: string[], suppressOutput = false): Promise<{ stdout: string; stderr: string }> => { + return new Promise((resolve, reject) => { + if (!suppressOutput) { + const log = ["Running command:", cmd, ...args, "\n"].join(" "); + // When in CI mode we combine reports from multiple runners into a single HTML report + // which has separate files for stdout and stderr, so we print the executed command to both + process.stdout.write(log); + if (process.env.CI) process.stderr.write(log); + } + const { stdout, stderr } = childProcess.execFile(cmd, args, { encoding: "utf8" }, (err, stdout, stderr) => { + if (err) reject(err); + resolve({ stdout, stderr }); + if (!suppressOutput) { + process.stdout.write("\n"); + if (process.env.CI) process.stderr.write("\n"); + } + }); + if (!suppressOutput) { + stdout.pipe(process.stdout); + stderr.pipe(process.stderr); + } + }); +}; + export class Docker { public id: string; @@ -26,9 +57,10 @@ export class Docker { const userInfo = os.userInfo(); const params = opts.params ?? []; - if (params?.includes("-v") && userInfo.uid >= 0) { + const isPodman = await Docker.isPodman(); + if (params.includes("-v") && userInfo.uid >= 0) { // Run the docker container as our uid:gid to prevent problems with permissions. - if (await Docker.isPodman()) { + if (isPodman) { // Note: this setup is for podman rootless containers. // In podman, run as root in the container, which maps to the current @@ -45,75 +77,57 @@ export class Docker { } } + // Make host.containers.internal work to allow the container to talk to other services via host ports. + if (isPodman) { + params.push("--network"); + params.push("slirp4netns:allow_host_loopback=true"); + } else { + // Docker for Desktop includes a host-gateway mapping on host.docker.internal but to simplify the config + // we use the Podman variant host.containers.internal in all environments. + params.push("--add-host"); + params.push("host.containers.internal:host-gateway"); + } + + // Provided we are not running in CI, add a `--rm` parameter. + // There is no need to remove containers in CI (since they are automatically removed anyway), and + // `--rm` means that if a container crashes this means its logs are wiped out. + if (!process.env.CI) params.unshift("--rm"); + const args = [ "run", "--name", `${opts.containerName}-${crypto.randomBytes(4).toString("hex")}`, "-d", - "--rm", ...params, opts.image, ]; if (opts.cmd) args.push(...opts.cmd); - this.id = await new Promise((resolve, reject) => { - childProcess.execFile("docker", args, (err, stdout) => { - if (err) reject(err); - resolve(stdout.trim()); - }); - }); + const { stdout } = await exec("docker", args); + this.id = stdout.trim(); return this.id; } - stop(): Promise { - return new Promise((resolve, reject) => { - childProcess.execFile("docker", ["stop", this.id], (err) => { - if (err) reject(err); - resolve(); - }); - }); - } - - exec(params: string[]): Promise { - return new Promise((resolve, reject) => { - childProcess.execFile( - "docker", - ["exec", this.id, ...params], - { encoding: "utf8" }, - (err, stdout, stderr) => { - if (err) { - console.log(stdout); - console.log(stderr); - reject(err); - return; - } - resolve(); - }, - ); - }); + async stop(): Promise { + try { + await exec("docker", ["stop", this.id]); + } catch (err) { + console.error(`Failed to stop docker container`, this.id, err); + } } - rm(): Promise { - return new Promise((resolve, reject) => { - childProcess.execFile("docker", ["rm", this.id], (err) => { - if (err) reject(err); - resolve(); - }); - }); + /** + * @param params - list of parameters to pass to `docker exec` + * @param suppressOutput - whether to suppress the stdout and stderr resulting from this command. + */ + async exec(params: string[], suppressOutput = true): Promise { + await exec("docker", ["exec", this.id, ...params], suppressOutput); } - getContainerIp(): Promise { - return new Promise((resolve, reject) => { - childProcess.execFile( - "docker", - ["inspect", "-f", "{{ .NetworkSettings.IPAddress }}", this.id], - (err, stdout) => { - if (err) reject(err); - else resolve(stdout.trim()); - }, - ); - }); + async getContainerIp(): Promise { + const { stdout } = await exec("docker", ["inspect", "-f", "{{ .NetworkSettings.IPAddress }}", this.id]); + return stdout.trim(); } async persistLogsToFile(args: { stdoutFile?: string; stderrFile?: string }): Promise { @@ -134,20 +148,8 @@ export class Docker { * Detects whether the docker command is actually podman. * To do this, it looks for "podman" in the output of "docker --help". */ - static isPodman(): Promise { - return new Promise((resolve, reject) => { - childProcess.execFile("docker", ["--help"], (err, stdout) => { - if (err) reject(err); - else resolve(stdout.toLowerCase().includes("podman")); - }); - }); - } - - /** - * Supply the right hostname to use to talk to the host machine. On Docker this - * is "host.docker.internal" and on Podman this is "host.containers.internal". - */ - static async hostnameOfHost(): Promise<"host.containers.internal" | "host.docker.internal"> { - return (await Docker.isPodman()) ? "host.containers.internal" : "host.docker.internal"; + static async isPodman(): Promise { + const { stdout } = await exec("docker", ["--help"], true); + return stdout.toLowerCase().includes("podman"); } } diff --git a/playwright/plugins/homeserver/dendrite/index.ts b/playwright/plugins/homeserver/dendrite/index.ts index 2ca54cc0d8f..603bd360a8c 100644 --- a/playwright/plugins/homeserver/dendrite/index.ts +++ b/playwright/plugins/homeserver/dendrite/index.ts @@ -46,7 +46,6 @@ export class Dendrite extends Synapse implements Homeserver, HomeserverInstance const dendriteId = await this.docker.run({ image: this.image, params: [ - "--rm", "-v", `${denCfg.configDir}:` + dockerConfigDir, "-p", @@ -140,7 +139,7 @@ async function cfgDirFromTemplate( const docker = new Docker(); await docker.run({ image: dendriteImage, - params: ["--rm", "--entrypoint=", "-v", `${tempDir}:/mnt`], + params: ["--entrypoint=", "-v", `${tempDir}:/mnt`], containerName: `react-sdk-playwright-dendrite-keygen`, cmd: ["/usr/bin/generate-keys", "-private-key", "/mnt/matrix_key.pem"], }); diff --git a/playwright/plugins/homeserver/synapse/index.ts b/playwright/plugins/homeserver/synapse/index.ts index 95165c14428..c11f937cf3e 100644 --- a/playwright/plugins/homeserver/synapse/index.ts +++ b/playwright/plugins/homeserver/synapse/index.ts @@ -57,20 +57,9 @@ async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise> | null = null; for (const key in opts.variables) { - let value = String(opts.variables[key]); - - if (value === "{{HOST_DOCKER_INTERNAL}}") { - if (!fetchedHostContainer) { - fetchedHostContainer = await Docker.hostnameOfHost(); - } - value = fetchedHostContainer; - } - - hsYaml = hsYaml.replace(new RegExp("%" + key + "%", "g"), value); + hsYaml = hsYaml.replace(new RegExp("%" + key + "%", "g"), String(opts.variables[key])); } } @@ -106,26 +95,13 @@ export class Synapse implements Homeserver, HomeserverInstance { * Start a synapse instance: the template must be the name of * one of the templates in the playwright/plugins/synapsedocker/templates * directory. - * - * Any value in `opts.variables` that is set to `{{HOST_DOCKER_INTERNAL}}' - * will be replaced with 'host.docker.internal' (if we are on Docker) or - * 'host.containers.internal' if we are on Podman. */ public async start(opts: StartHomeserverOpts): Promise { if (this.config) await this.stop(); const synCfg = await cfgDirFromTemplate(opts); console.log(`Starting synapse with config dir ${synCfg.configDir}...`); - const dockerSynapseParams = ["--rm", "-v", `${synCfg.configDir}:/data`, "-p", `${synCfg.port}:8008/tcp`]; - if (await Docker.isPodman()) { - // Make host.containers.internal work to allow Synapse to talk to the test OIDC server. - dockerSynapseParams.push("--network"); - dockerSynapseParams.push("slirp4netns:allow_host_loopback=true"); - } else { - // Make host.docker.internal work to allow Synapse to talk to the test OIDC server. - dockerSynapseParams.push("--add-host"); - dockerSynapseParams.push("host.docker.internal:host-gateway"); - } + const dockerSynapseParams = ["-v", `${synCfg.configDir}:/data`, "-p", `${synCfg.port}:8008/tcp`]; const synapseId = await this.docker.run({ image: "matrixdotorg/synapse:develop", containerName: `react-sdk-playwright-synapse`, @@ -158,7 +134,7 @@ export class Synapse implements Homeserver, HomeserverInstance { public async stop(): Promise { if (!this.config) throw new Error("Missing existing synapse instance, did you call stop() before start()?"); const id = this.config.serverId; - const synapseLogsPath = path.join("playwright", "synapselogs", id); + const synapseLogsPath = path.join("playwright", "logs", "synapse", id); await fse.ensureDir(synapseLogsPath); await this.docker.persistLogsToFile({ stdoutFile: path.join(synapseLogsPath, "stdout.log"), diff --git a/playwright/plugins/homeserver/synapse/templates/default/homeserver.yaml b/playwright/plugins/homeserver/synapse/templates/default/homeserver.yaml index 76cedb78f86..c5bea307b44 100644 --- a/playwright/plugins/homeserver/synapse/templates/default/homeserver.yaml +++ b/playwright/plugins/homeserver/synapse/templates/default/homeserver.yaml @@ -81,10 +81,8 @@ oidc_providers: issuer: "http://localhost:{{OAUTH_SERVER_PORT}}/oauth" authorization_endpoint: "http://localhost:{{OAUTH_SERVER_PORT}}/oauth/auth.html" # the token endpoint receives requests from synapse, rather than the webapp, so needs to escape the docker container. - # Hence, HOST_DOCKER_INTERNAL rather than localhost. This is set to - # host.docker.internal on Docker and host.containers.internal on Podman. - token_endpoint: "http://{{HOST_DOCKER_INTERNAL}}:{{OAUTH_SERVER_PORT}}/oauth/token" - userinfo_endpoint: "http://{{HOST_DOCKER_INTERNAL}}:{{OAUTH_SERVER_PORT}}/oauth/userinfo" + token_endpoint: "http://host.containers.internal:{{OAUTH_SERVER_PORT}}/oauth/token" + userinfo_endpoint: "http://host.containers.internal:{{OAUTH_SERVER_PORT}}/oauth/userinfo" client_id: "synapse" discover: false scopes: ["profile"] diff --git a/playwright/plugins/homeserver/synapse/templates/mas-oidc/README.md b/playwright/plugins/homeserver/synapse/templates/mas-oidc/README.md new file mode 100644 index 00000000000..223ff436a8d --- /dev/null +++ b/playwright/plugins/homeserver/synapse/templates/mas-oidc/README.md @@ -0,0 +1 @@ +A synapse configured with auth delegated to via matrix authentication service diff --git a/playwright/plugins/homeserver/synapse/templates/mas-oidc/homeserver.yaml b/playwright/plugins/homeserver/synapse/templates/mas-oidc/homeserver.yaml new file mode 100644 index 00000000000..802d97acade --- /dev/null +++ b/playwright/plugins/homeserver/synapse/templates/mas-oidc/homeserver.yaml @@ -0,0 +1,194 @@ +server_name: "localhost" +pid_file: /data/homeserver.pid +public_baseurl: "{{PUBLIC_BASEURL}}" +listeners: + - port: 8008 + tls: false + bind_addresses: ["::"] + type: http + x_forwarded: true + + resources: + - names: [client] + compress: false + +database: + name: "sqlite3" + args: + database: ":memory:" + +log_config: "/data/log.config" + +rc_messages_per_second: 10000 +rc_message_burst_count: 10000 +rc_registration: + per_second: 10000 + burst_count: 10000 +rc_joins: + local: + per_second: 9999 + burst_count: 9999 + remote: + per_second: 9999 + burst_count: 9999 +rc_joins_per_room: + per_second: 9999 + burst_count: 9999 +rc_3pid_validation: + per_second: 1000 + burst_count: 1000 + +rc_invites: + per_room: + per_second: 1000 + burst_count: 1000 + per_user: + per_second: 1000 + burst_count: 1000 + +rc_login: + address: + per_second: 10000 + burst_count: 10000 + account: + per_second: 10000 + burst_count: 10000 + failed_attempts: + per_second: 10000 + burst_count: 10000 + +media_store_path: "/data/media_store" +uploads_path: "/data/uploads" +registration_shared_secret: "{{REGISTRATION_SECRET}}" +report_stats: false +macaroon_secret_key: "{{MACAROON_SECRET_KEY}}" +form_secret: "{{FORM_SECRET}}" +signing_key_path: "/data/localhost.signing.key" + +trusted_key_servers: + - server_name: "matrix.org" +suppress_key_server_warning: true + +ui_auth: + session_timeout: "300s" + +# Inhibit background updates as this Synapse isn't long-lived +background_updates: + min_batch_size: 100000 + sleep_duration_ms: 100000 + +serve_server_wellknown: true +experimental_features: + msc3861: + enabled: true + + issuer: http://localhost:%MAS_PORT%/ + # We have to bake in the metadata here as we need to override `introspection_endpoint` + issuer_metadata: { + "issuer": "http://localhost:%MAS_PORT%/", + "authorization_endpoint": "http://localhost:%MAS_PORT%/authorize", + "token_endpoint": "http://localhost:%MAS_PORT%/oauth2/token", + "jwks_uri": "http://localhost:%MAS_PORT%/oauth2/keys.json", + "registration_endpoint": "http://localhost:%MAS_PORT%/oauth2/registration", + "scopes_supported": ["openid", "email"], + "response_types_supported": ["code", "id_token", "code id_token"], + "response_modes_supported": ["form_post", "query", "fragment"], + "grant_types_supported": + [ + "authorization_code", + "refresh_token", + "client_credentials", + "urn:ietf:params:oauth:grant-type:device_code", + ], + "token_endpoint_auth_methods_supported": + ["client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt", "none"], + "token_endpoint_auth_signing_alg_values_supported": + [ + "HS256", + "HS384", + "HS512", + "RS256", + "RS384", + "RS512", + "PS256", + "PS384", + "PS512", + "ES256", + "ES384", + "ES256K", + ], + "revocation_endpoint": "http://localhost:%MAS_PORT%/oauth2/revoke", + "revocation_endpoint_auth_methods_supported": + ["client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt", "none"], + "revocation_endpoint_auth_signing_alg_values_supported": + [ + "HS256", + "HS384", + "HS512", + "RS256", + "RS384", + "RS512", + "PS256", + "PS384", + "PS512", + "ES256", + "ES384", + "ES256K", + ], + # This is the only changed value + "introspection_endpoint": "http://host.containers.internal:%MAS_PORT%/oauth2/introspect", + "introspection_endpoint_auth_methods_supported": + ["client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt", "none"], + "introspection_endpoint_auth_signing_alg_values_supported": + [ + "HS256", + "HS384", + "HS512", + "RS256", + "RS384", + "RS512", + "PS256", + "PS384", + "PS512", + "ES256", + "ES384", + "ES256K", + ], + "code_challenge_methods_supported": ["plain", "S256"], + "userinfo_endpoint": "http://localhost:%MAS_PORT%/oauth2/userinfo", + "subject_types_supported": ["public"], + "id_token_signing_alg_values_supported": + ["RS256", "RS384", "RS512", "ES256", "ES384", "PS256", "PS384", "PS512", "ES256K"], + "userinfo_signing_alg_values_supported": + ["RS256", "RS384", "RS512", "ES256", "ES384", "PS256", "PS384", "PS512", "ES256K"], + "display_values_supported": ["page"], + "claim_types_supported": ["normal"], + "claims_supported": ["iss", "sub", "aud", "iat", "exp", "nonce", "auth_time", "at_hash", "c_hash"], + "claims_parameter_supported": false, + "request_parameter_supported": false, + "request_uri_parameter_supported": false, + "prompt_values_supported": ["none", "login", "create"], + "device_authorization_endpoint": "http://localhost:%MAS_PORT%/oauth2/device", + "org.matrix.matrix-authentication-service.graphql_endpoint": "http://localhost:%MAS_PORT%/graphql", + "account_management_uri": "http://localhost:%MAS_PORT%/account/", + "account_management_actions_supported": + [ + "org.matrix.profile", + "org.matrix.sessions_list", + "org.matrix.session_view", + "org.matrix.session_end", + ], + } + + # Matches the `client_id` in the auth service config + client_id: 0000000000000000000SYNAPSE + # Matches the `client_auth_method` in the auth service config + client_auth_method: client_secret_basic + # Matches the `client_secret` in the auth service config + client_secret: "SomeRandomSecret" + + # Matches the `matrix.secret` in the auth service config + admin_token: "AnotherRandomSecret" + + # URL to advertise to clients where users can self-manage their account + account_management_url: "http://localhost:%MAS_PORT%/account" diff --git a/playwright/plugins/homeserver/synapse/templates/mas-oidc/log.config b/playwright/plugins/homeserver/synapse/templates/mas-oidc/log.config new file mode 100644 index 00000000000..b9123d0f5b9 --- /dev/null +++ b/playwright/plugins/homeserver/synapse/templates/mas-oidc/log.config @@ -0,0 +1,50 @@ +# Log configuration for Synapse. +# +# This is a YAML file containing a standard Python logging configuration +# dictionary. See [1] for details on the valid settings. +# +# Synapse also supports structured logging for machine readable logs which can +# be ingested by ELK stacks. See [2] for details. +# +# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema +# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html + +version: 1 + +formatters: + precise: + format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' + +handlers: + # A handler that writes logs to stderr. Unused by default, but can be used + # instead of "buffer" and "file" in the logger handlers. + console: + class: logging.StreamHandler + formatter: precise + +loggers: + synapse.storage.SQL: + # beware: increasing this to DEBUG will make synapse log sensitive + # information such as access tokens. + level: DEBUG + + twisted: + # We send the twisted logging directly to the file handler, + # to work around https://github.com/matrix-org/synapse/issues/3471 + # when using "buffer" logger. Use "console" to log to stderr instead. + handlers: [console] + propagate: false + +root: + level: DEBUG + + # Write logs to the `buffer` handler, which will buffer them together in memory, + # then write them to a file. + # + # Replace "buffer" with "console" to log to stderr instead. (Note that you'll + # also need to update the configuration for the `twisted` logger above, in + # this case.) + # + handlers: [console] + +disable_existing_loggers: false diff --git a/playwright/plugins/mailhog/index.ts b/playwright/plugins/mailhog/index.ts index abcc4026b82..684aaee5ed8 100644 --- a/playwright/plugins/mailhog/index.ts +++ b/playwright/plugins/mailhog/index.ts @@ -38,7 +38,7 @@ export class MailHogServer { const containerId = await this.docker.run({ image: "mailhog/mailhog:latest", containerName: `react-sdk-playwright-mailhog`, - params: ["--rm", "-p", `${smtpPort}:1025/tcp`, "-p", `${httpPort}:8025/tcp`], + params: ["-p", `${smtpPort}:1025/tcp`, "-p", `${httpPort}:8025/tcp`], }); console.log(`Started mailhog on ports smtp=${smtpPort} http=${httpPort}.`); const host = await this.docker.getContainerIp(); diff --git a/playwright/plugins/matrix-authentication-service/config.yaml b/playwright/plugins/matrix-authentication-service/config.yaml new file mode 100644 index 00000000000..e7ab83e736e --- /dev/null +++ b/playwright/plugins/matrix-authentication-service/config.yaml @@ -0,0 +1,153 @@ +clients: + - client_id: 0000000000000000000SYNAPSE + client_auth_method: client_secret_basic + client_secret: "SomeRandomSecret" +http: + listeners: + - name: web + resources: + - name: discovery + - name: human + - name: oauth + - name: compat + - name: graphql + playground: true + - name: assets + path: /usr/local/share/mas-cli/assets/ + binds: + - address: "[::]:8080" + proxy_protocol: false + - name: internal + resources: + - name: health + binds: + - host: localhost + port: 8081 + proxy_protocol: false + trusted_proxies: + - 192.128.0.0/16 + - 172.16.0.0/12 + - 10.0.0.0/10 + - 127.0.0.1/8 + - fd00::/8 + - ::1/128 + public_base: "http://localhost:{{MAS_PORT}}/" + issuer: http://localhost:{{MAS_PORT}}/ +database: + host: "{{POSTGRES_HOST}}" + port: 5432 + database: postgres + username: postgres + password: "{{POSTGRES_PASSWORD}}" + max_connections: 10 + min_connections: 0 + connect_timeout: 30 + idle_timeout: 600 + max_lifetime: 1800 +telemetry: + tracing: + exporter: none + propagators: [] + metrics: + exporter: none + sentry: + dsn: null +templates: + path: /usr/local/share/mas-cli/templates/ + assets_manifest: /usr/local/share/mas-cli/manifest.json + translations_path: /usr/local/share/mas-cli/translations/ +email: + from: '"Authentication Service" ' + reply_to: '"Authentication Service" ' + transport: smtp + mode: plain + hostname: "host.containers.internal" + port: %{{SMTP_PORT}} + username: username + password: password + +secrets: + encryption: 984b18e207c55ad5fbb2a49b217481a722917ee87b2308d4cf314c83fed8e3b5 + keys: + - kid: YEAhzrKipJ + key: | + -----BEGIN RSA PRIVATE KEY----- + MIIEowIBAAKCAQEAuIV+AW5vx52I4CuumgSxp6yvKfIAnRdALeZZCoFkIGxUli1B + S79NJ3ls46oLh1pSD9RrhaMp6HTNoi4K3hnP9Q9v77pD7KwdFKG3UdG1zksIB0s/ + +/Ey/DmX4LPluwBBS7r/LkQ1jk745lENA++oiDqZf2D/uP8jCHlvaSNyVKTqi1ki + OXPd4T4xBUjzuas9ze5jQVSYtfOidgnv1EzUipbIxgvH1jNt4raRlmP8mOq7xEnW + R+cF5x6n/g17PdSEfrwO4kz6aKGZuMP5lVlDEEnMHKabFSQDBl7+Mpok6jXutbtA + uiBnsKEahF9eoj4na4fpbRNPdIVyoaN5eGvm5wIDAQABAoIBAApyFCYEmHNWaa83 + CdVSOrRhRDE9r+c0r79pcNT1ajOjrk4qFa4yEC4R46YntCtfY5Hd1pBkIjU0l4d8 + z8Su9WTMEOwjQUEepS7L0NLi6kXZXYT8L40VpGs+32grBvBFHW0qEtQNrHJ36gMv + x2rXoFTF7HaXiSJx3wvVxAbRqOE9tBXLsmNHaWaAdWQG5o77V9+zvMri3cAeEg2w + VkKokb0dza7es7xG3tqS26k69SrwGeeuKo7qCHPH2cfyWmY5Yhv8iOoA59JzzbiK + UdxyzCHskrPSpRKVkVVwmY3RBt282TmSRG7td7e5ESSj50P2e5BI5uu1Hp/dvU4F + vYjV7kECgYEA6WqYoUpVsgQiqhvJwJIc/8gRm0mUy8TenI36z4Iim01Nt7fibWH7 + XnsFqLGjXtYNVWvBcCrUl9doEnRbJeG2eRGbGKYAWVrOeFvwM4fYvw9GoOiJdDj4 + cgWDe7eHbHE+UTqR7Nnr/UBfipoNWDh6X68HRBuXowh0Q6tOfxsrRFECgYEAyl/V + 4b8bFp3pKZZCb+KPSYsQf793cRmrBexPcLWcDPYbMZQADEZ/VLjbrNrpTOWxUWJT + hr8MrWswnHO+l5AFu5CNO+QgV2dHLk+2w8qpdpFRPJCfXfo2D3wZ0c4cv3VCwv1V + 5y7f6XWVjDWZYV4wj6c3shxZJjZ+9Hbhf3/twbcCgYA6fuRRR3fCbRbi2qPtBrEN + yO3gpMgNaQEA6vP4HPzfPrhDWmn8T5nXS61XYW03zxz4U1De81zj0K/cMBzHmZFJ + NghQXQmpWwBzWVcREvJWr1Vb7erEnaJlsMwKrSvbGWYspSj82oAxr3hCG+lMOpsw + b4S6pM+TpAK/EqdRY1WsgQKBgQCGoMaaTRXqL9bC0bEU2XVVCWxKb8c3uEmrwQ7/ + /fD4NmjUzI5TnDps1CVfkqoNe+hAKddDFqmKXHqUOfOaxDbsFje+lf5l5tDVoDYH + fjTKKdYPIm7CiAeauYY7qpA5Vfq52Opixy4yEwUPp0CII67OggFtPaqY3zwJyWQt + +57hdQKBgGCXM/KKt7ceUDcNJxSGjvu0zD9D5Sv2ihYlEBT/JLaTCCJdvzREevaJ + 1d+mpUAt0Lq6A8NWOMq8HPaxAik3rMQ0WtM5iG+XgsUqvTSb7NcshArDLuWGnW3m + MC4rM0UBYAS4QweduUSH1imrwH/1Gu5+PxbiecceRMMggWpzu0Bq + -----END RSA PRIVATE KEY----- + - kid: 8J1AxrlNZT + key: | + -----BEGIN EC PRIVATE KEY----- + MHcCAQEEIF1cjfIOEdy3BXJ72x6fKpEB8WP1ddZAUJAaqqr/6CpToAoGCCqGSM49 + AwEHoUQDQgAEfHdNuI1Yeh3/uOq2PlnW2vymloOVpwBYebbw4VVsna9xhnutIdQW + dE8hkX8Yb0pIDasrDiwllVLzSvsWJAI0Kw== + -----END EC PRIVATE KEY----- + - kid: 3BW6un1EBi + key: | + -----BEGIN EC PRIVATE KEY----- + MIGkAgEBBDA+3ZV17r8TsiMdw1cpbTSNbyEd5SMy3VS1Mk/kz6O2Ev/3QZut8GE2 + q3eGtLBoVQigBwYFK4EEACKhZANiAASs8Wxjk/uRimRKXnPr2/wDaXkN9wMDjYQK + mZULb+0ZP1/cXmuXuri8hUGhQvIU8KWY9PkpV+LMPEdpE54mHPKSLjq5CDXoSZ/P + 9f7cdRaOZ000KQPZfIFR9ujJTtDN7Vs= + -----END EC PRIVATE KEY----- + - kid: pkZ0pTKK0X + key: | + -----BEGIN EC PRIVATE KEY----- + MHQCAQEEIHenfsXYPc5yzjZKUfvmydDR1YRwdsfZYvwHf/2wsYxooAcGBSuBBAAK + oUQDQgAEON1x7Vlu+nA0KvC5vYSOHhDUkfLYNZwYSLPFVT02h9E13yFFMIJegIBl + Aer+6PMZpPc8ycyeH9N+U9NAyliBhQ== + -----END EC PRIVATE KEY----- +passwords: + enabled: true + schemes: + - version: 1 + algorithm: argon2id +matrix: + homeserver: localhost + secret: AnotherRandomSecret + endpoint: "{{SYNAPSE_URL}}" +policy: + wasm_module: /usr/local/share/mas-cli/policy.wasm + client_registration_entrypoint: client_registration/violation + register_entrypoint: register/violation + authorization_grant_entrypoint: authorization_grant/violation + password_entrypoint: password/violation + email_entrypoint: email/violation + data: + client_registration: + allow_insecure_uris: true # allow non-SSL and localhost URIs + allow_missing_contacts: true # EW doesn't have contacts at this time +upstream_oauth2: + providers: [] +branding: + service_name: null + policy_uri: null + tos_uri: null + imprint: null + logo_uri: null +experimental: + access_token_ttl: 300 + compat_token_ttl: 300 diff --git a/playwright/plugins/matrix-authentication-service/index.ts b/playwright/plugins/matrix-authentication-service/index.ts new file mode 100644 index 00000000000..40649159efd --- /dev/null +++ b/playwright/plugins/matrix-authentication-service/index.ts @@ -0,0 +1,159 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import path, { basename } from "node:path"; +import os from "node:os"; +import * as fse from "fs-extra"; +import { BrowserContext, TestInfo } from "@playwright/test"; + +import { getFreePort } from "../utils/port"; +import { Docker } from "../docker"; +import { PG_PASSWORD, PostgresDocker } from "../postgres"; +import { HomeserverInstance } from "../homeserver"; +import { Instance as MailhogInstance } from "../mailhog"; + +// Docker tag to use for `ghcr.io/matrix-org/matrix-authentication-service` image. +// We use a debug tag so that we have a shell and can run all 3 necessary commands in one run. +const TAG = "0.8.0-debug"; + +export interface ProxyInstance { + containerId: string; + postgresId: string; + configDir: string; + port: number; +} + +async function cfgDirFromTemplate(opts: { + postgresHost: string; + synapseUrl: string; + masPort: string; + smtpPort: string; +}): Promise<{ + configDir: string; +}> { + const configPath = path.join(__dirname, "config.yaml"); + const tempDir = await fse.mkdtemp(path.join(os.tmpdir(), "react-sdk-mas-")); + + const outputHomeserver = path.join(tempDir, "config.yaml"); + console.log(`Gen ${configPath} -> ${outputHomeserver}`); + let config = await fse.readFile(configPath, "utf8"); + config = config.replace(/{{MAS_PORT}}/g, opts.masPort); + config = config.replace(/{{POSTGRES_HOST}}/g, opts.postgresHost); + config = config.replace(/{{POSTGRES_PASSWORD}}/g, PG_PASSWORD); + config = config.replace(/%{{SMTP_PORT}}/g, opts.smtpPort); + config = config.replace(/{{SYNAPSE_URL}}/g, opts.synapseUrl); + + await fse.writeFile(outputHomeserver, config); + + // Allow anyone to read, write and execute in the temp directory + // so that the DIND setup that we use to update the playwright screenshots work without any issues. + await fse.chmod(tempDir, 0o757); + + return { + configDir: tempDir, + }; +} + +export class MatrixAuthenticationService { + private readonly masDocker = new Docker(); + private readonly postgresDocker = new PostgresDocker("mas"); + private instance: ProxyInstance; + public port: number; + + constructor(private context: BrowserContext) {} + + async prepare(): Promise<{ port: number }> { + this.port = await getFreePort(); + return { port: this.port }; + } + + async start(homeserver: HomeserverInstance, mailhog: MailhogInstance): Promise { + console.log(new Date(), "Starting mas..."); + + if (!this.port) await this.prepare(); + const port = this.port; + const { containerId: postgresId, ipAddress: postgresIp } = await this.postgresDocker.start(); + const { configDir } = await cfgDirFromTemplate({ + masPort: port.toString(), + postgresHost: postgresIp, + synapseUrl: homeserver.config.dockerUrl, + smtpPort: mailhog.smtpPort.toString(), + }); + + console.log(new Date(), "starting mas container...", TAG); + const containerId = await this.masDocker.run({ + image: "ghcr.io/matrix-org/matrix-authentication-service:" + TAG, + containerName: "react-sdk-playwright-mas", + params: ["-p", `${port}:8080/tcp`, "-v", `${configDir}:/config`, "--entrypoint", "sh"], + cmd: [ + "-c", + "mas-cli database migrate --config /config/config.yaml && " + + "mas-cli config sync --config /config/config.yaml && " + + "mas-cli server --config /config/config.yaml", + ], + }); + console.log(new Date(), "started!"); + + // Set up redirects + const baseUrl = `http://localhost:${port}`; + for (const path of [ + "**/_matrix/client/*/login", + "**/_matrix/client/*/login/**", + "**/_matrix/client/*/logout", + "**/_matrix/client/*/refresh", + ]) { + await this.context.route(path, async (route) => { + await route.continue({ + url: new URL(route.request().url().split("/").slice(3).join("/"), baseUrl).href, + }); + }); + } + + this.instance = { containerId, postgresId, port, configDir }; + return this.instance; + } + + async stop(testInfo: TestInfo): Promise { + if (!this.instance) return; // nothing to stop + const id = this.instance.containerId; + const logPath = path.join("playwright", "logs", "matrix-authentication-service", id); + await fse.ensureDir(logPath); + await this.masDocker.persistLogsToFile({ + stdoutFile: path.join(logPath, "stdout.log"), + stderrFile: path.join(logPath, "stderr.log"), + }); + + await this.masDocker.stop(); + await this.postgresDocker.stop(); + + if (testInfo.status !== "passed") { + const logs = [path.join(logPath, "stdout.log"), path.join(logPath, "stderr.log")]; + for (const path of logs) { + await testInfo.attach(`mas-${basename(path)}`, { + path, + contentType: "text/plain", + }); + } + await testInfo.attach("mas-config.yaml", { + path: path.join(this.instance.configDir, "config.yaml"), + contentType: "text/plain", + }); + } + + await fse.remove(this.instance.configDir); + console.log(new Date(), "Stopped mas."); + } +} diff --git a/playwright/plugins/postgres/index.ts b/playwright/plugins/postgres/index.ts new file mode 100644 index 00000000000..2b67afefa39 --- /dev/null +++ b/playwright/plugins/postgres/index.ts @@ -0,0 +1,72 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { Docker } from "../docker"; + +export const PG_PASSWORD = "p4S5w0rD"; + +/** + * Class to manage a postgres database in docker + */ +export class PostgresDocker extends Docker { + /** + * @param key an opaque string to use when naming the docker containers instantiated by this class + */ + public constructor(private key: string) { + super(); + } + + private async waitForPostgresReady(): Promise { + const waitTimeMillis = 30000; + const startTime = new Date().getTime(); + let lastErr: Error | null = null; + while (new Date().getTime() - startTime < waitTimeMillis) { + try { + await this.exec(["pg_isready", "-U", "postgres"], true); + lastErr = null; + break; + } catch (err) { + console.log("pg_isready: failed"); + lastErr = err; + } + } + if (lastErr) { + console.log("rethrowing"); + throw lastErr; + } + } + + public async start(): Promise<{ + ipAddress: string; + containerId: string; + }> { + console.log(new Date(), "starting postgres container"); + const containerId = await this.run({ + image: "postgres", + containerName: `react-sdk-playwright-postgres-${this.key}`, + params: ["--tmpfs=/pgtmpfs", "-e", "PGDATA=/pgtmpfs", "-e", `POSTGRES_PASSWORD=${PG_PASSWORD}`], + // Optimise for testing - https://www.postgresql.org/docs/current/non-durability.html + cmd: ["-c", `fsync=off`, "-c", `synchronous_commit=off`, "-c", `full_page_writes=off`], + }); + + const ipAddress = await this.getContainerIp(); + console.log(new Date(), "postgres container up"); + + await this.waitForPostgresReady(); + console.log(new Date(), "postgres container ready"); + return { ipAddress, containerId }; + } +} diff --git a/playwright/plugins/sliding-sync-proxy/index.ts b/playwright/plugins/sliding-sync-proxy/index.ts index b8cc365ffb6..f7e07a8cb15 100644 --- a/playwright/plugins/sliding-sync-proxy/index.ts +++ b/playwright/plugins/sliding-sync-proxy/index.ts @@ -16,10 +16,10 @@ limitations under the License. import { getFreePort } from "../utils/port"; import { Docker } from "../docker"; +import { PG_PASSWORD, PostgresDocker } from "../postgres"; // Docker tag to use for `ghcr.io/matrix-org/sliding-sync` image. const SLIDING_SYNC_PROXY_TAG = "v0.99.3"; -const PG_PASSWORD = "p4S5w0rD"; export interface ProxyInstance { containerId: string; @@ -28,45 +28,16 @@ export interface ProxyInstance { } export class SlidingSyncProxy { - private readonly postgresDocker = new Docker(); private readonly proxyDocker = new Docker(); + private readonly postgresDocker = new PostgresDocker("sliding-sync"); private instance: ProxyInstance; constructor(private synapseIp: string) {} - private async waitForPostgresReady(): Promise { - const waitTimeMillis = 30000; - const startTime = new Date().getTime(); - let lastErr: Error | null = null; - while (new Date().getTime() - startTime < waitTimeMillis) { - try { - await this.postgresDocker.exec(["pg_isready", "-U", "postgres"]); - lastErr = null; - break; - } catch (err) { - console.log("pg_isready: failed"); - lastErr = err; - } - } - if (lastErr) { - console.log("rethrowing"); - throw lastErr; - } - } - async start(): Promise { console.log(new Date(), "Starting sliding sync proxy..."); - const postgresId = await this.postgresDocker.run({ - image: "postgres", - containerName: "react-sdk-playwright-sliding-sync-postgres", - params: ["--rm", "-e", `POSTGRES_PASSWORD=${PG_PASSWORD}`], - }); - - const postgresIp = await this.postgresDocker.getContainerIp(); - console.log(new Date(), "postgres container up"); - - await this.waitForPostgresReady(); + const { ipAddress: postgresIp, containerId: postgresId } = await this.postgresDocker.start(); const port = await getFreePort(); console.log(new Date(), "starting proxy container...", SLIDING_SYNC_PROXY_TAG); @@ -74,7 +45,6 @@ export class SlidingSyncProxy { image: "ghcr.io/matrix-org/sliding-sync:" + SLIDING_SYNC_PROXY_TAG, containerName: "react-sdk-playwright-sliding-sync-proxy", params: [ - "--rm", "-p", `${port}:8008/tcp`, "-e", diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--group-layout-linux.png index 9a0a24ae562..7ece0291884 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--group-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--group-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--group-layout-linux.png index 0b232ddcfd3..b6c259785e5 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--group-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--group-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--group-layout-linux.png index deedd2d703f..fb80e1645cc 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--group-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--group-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--group-layout-linux.png index 9fa8fcd0084..ca0fab0f789 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--group-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--group-layout-linux.png differ diff --git a/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png b/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png index 488132b30f3..d3c89613914 100644 Binary files a/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png and b/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png differ diff --git a/playwright/snapshots/editing/editing.spec.ts/message-edit-history-dialog-linux.png b/playwright/snapshots/editing/editing.spec.ts/message-edit-history-dialog-linux.png index 0135b0a66e8..9f46fce516b 100644 Binary files a/playwright/snapshots/editing/editing.spec.ts/message-edit-history-dialog-linux.png and b/playwright/snapshots/editing/editing.spec.ts/message-edit-history-dialog-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 aeecf52e6ab..7e8992dca10 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 663505b4c51..dfa6d4f0aa2 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 998d423690f..9cc13698cfd 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/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png index d06d9e81648..44d8129404d 100644 Binary files a/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png and b/playwright/snapshots/invite/invite-dialog.spec.ts/invite-dialog-room-without-user-linux.png differ diff --git a/playwright/snapshots/permalinks/permalinks.spec.ts/permalink-rendering-linux.png b/playwright/snapshots/permalinks/permalinks.spec.ts/permalink-rendering-linux.png index 67f9435c6de..db91140763a 100644 Binary files a/playwright/snapshots/permalinks/permalinks.spec.ts/permalink-rendering-linux.png and b/playwright/snapshots/permalinks/permalinks.spec.ts/permalink-rendering-linux.png differ diff --git a/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png b/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png index 73deb5b9293..2fc33b1f0b6 100644 Binary files a/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png and b/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-bubble-layout-linux.png differ diff --git a/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png b/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png index d5348395c80..a4c053e7a7f 100644 Binary files a/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png and b/playwright/snapshots/polls/polls.spec.ts/ThreadView-with-a-poll-on-group-layout-linux.png differ diff --git a/playwright/snapshots/register/register.spec.ts/email-prompt-linux.png b/playwright/snapshots/register/register.spec.ts/email-prompt-linux.png index 664e7ac85e7..3d4ea984a4e 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/registration-linux.png b/playwright/snapshots/register/register.spec.ts/registration-linux.png index d5462f30afd..ab9fdb2bf62 100644 Binary files a/playwright/snapshots/register/register.spec.ts/registration-linux.png and b/playwright/snapshots/register/register.spec.ts/registration-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 d54710f6f3b..94b5505b7a9 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/register/register.spec.ts/terms-prompt-linux.png b/playwright/snapshots/register/register.spec.ts/terms-prompt-linux.png index 347c86f69ee..30436d0abc6 100644 Binary files a/playwright/snapshots/register/register.spec.ts/terms-prompt-linux.png and b/playwright/snapshots/register/register.spec.ts/terms-prompt-linux.png 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 38592f9b677..e4f6313c97f 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/notification-panel.spec.ts/empty-linux.png b/playwright/snapshots/right-panel/notification-panel.spec.ts/empty-linux.png index ee3bd0348b1..7d8884dc4d0 100644 Binary files a/playwright/snapshots/right-panel/notification-panel.spec.ts/empty-linux.png and b/playwright/snapshots/right-panel/notification-panel.spec.ts/empty-linux.png differ diff --git a/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png b/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png index 7d4e21e240a..e53470df87b 100644 Binary files a/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png and b/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png differ diff --git a/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-no-results-linux.png b/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-no-results-linux.png index c827248e120..e0fc9cc5bdf 100644 Binary files a/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-no-results-linux.png and b/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-no-results-linux.png differ diff --git a/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-one-result-linux.png b/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-one-result-linux.png index ba6cd4c789b..a962c3cbade 100644 Binary files a/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-one-result-linux.png and b/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-one-result-linux.png differ diff --git a/playwright/snapshots/room/room-header.spec.ts/room-header-highlighted-linux.png b/playwright/snapshots/room/room-header.spec.ts/room-header-highlighted-linux.png index 6d6ea59ae83..5fa24a887f8 100644 Binary files a/playwright/snapshots/room/room-header.spec.ts/room-header-highlighted-linux.png and b/playwright/snapshots/room/room-header.spec.ts/room-header-highlighted-linux.png differ diff --git a/playwright/snapshots/room/room-header.spec.ts/room-header-with-apps-button-not-highlighted-linux.png b/playwright/snapshots/room/room-header.spec.ts/room-header-with-apps-button-not-highlighted-linux.png index ad2bf20e946..84eb8fcccca 100644 Binary files a/playwright/snapshots/room/room-header.spec.ts/room-header-with-apps-button-not-highlighted-linux.png and b/playwright/snapshots/room/room-header.spec.ts/room-header-with-apps-button-not-highlighted-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png index 921eae80494..e5680339f4b 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/window-12px-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/window-12px-linux.png new file mode 100644 index 00000000000..23b77fd7518 Binary files /dev/null and b/playwright/snapshots/settings/appearance-user-settings-tab.spec.ts/window-12px-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 6bc792df906..253b230419c 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/general-user-settings-tab.spec.ts/general-linux.png b/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-linux.png index fd27088c04e..f0b18bc950a 100644 Binary files a/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-linux.png and b/playwright/snapshots/settings/general-user-settings-tab.spec.ts/general-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 bdd8e9153a0..02ce908efa5 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/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 b853732b531..c59d60178d5 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/spaces/spaces.spec.ts/invite-teammates-dialog-linux.png b/playwright/snapshots/spaces/spaces.spec.ts/invite-teammates-dialog-linux.png index 59330ad5e1c..ef7536d455d 100644 Binary files a/playwright/snapshots/spaces/spaces.spec.ts/invite-teammates-dialog-linux.png and b/playwright/snapshots/spaces/spaces.spec.ts/invite-teammates-dialog-linux.png differ diff --git a/playwright/snapshots/spaces/spaces.spec.ts/space-create-menu-linux.png b/playwright/snapshots/spaces/spaces.spec.ts/space-create-menu-linux.png index bfe77a33119..6d2e83b23db 100644 Binary files a/playwright/snapshots/spaces/spaces.spec.ts/space-create-menu-linux.png and b/playwright/snapshots/spaces/spaces.spec.ts/space-create-menu-linux.png differ diff --git a/playwright/snapshots/spaces/spaces.spec.ts/space-panel-collapsed-linux.png b/playwright/snapshots/spaces/spaces.spec.ts/space-panel-collapsed-linux.png index 26d2ed4d8ec..32e664808e1 100644 Binary files a/playwright/snapshots/spaces/spaces.spec.ts/space-panel-collapsed-linux.png and b/playwright/snapshots/spaces/spaces.spec.ts/space-panel-collapsed-linux.png differ diff --git a/playwright/snapshots/spaces/spaces.spec.ts/space-panel-expanded-linux.png b/playwright/snapshots/spaces/spaces.spec.ts/space-panel-expanded-linux.png index b117b04d358..a2edd3d88fc 100644 Binary files a/playwright/snapshots/spaces/spaces.spec.ts/space-panel-expanded-linux.png and b/playwright/snapshots/spaces/spaces.spec.ts/space-panel-expanded-linux.png differ diff --git a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-button-expanded-linux.png b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-button-expanded-linux.png index f47fecf2d86..dcac67dc1f0 100644 Binary files a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-button-expanded-linux.png and b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-button-expanded-linux.png differ diff --git a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-expanded-linux.png b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-expanded-linux.png new file mode 100644 index 00000000000..37405cd821a Binary files /dev/null and b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-expanded-linux.png differ diff --git a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-linux.png b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-linux.png new file mode 100644 index 00000000000..26f5bfdfa98 Binary files /dev/null and b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-linux.png differ diff --git a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-mix-unread-linux.png b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-mix-unread-linux.png index f02a943d6bd..ec5a8193d25 100644 Binary files a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-mix-unread-linux.png and b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-mix-unread-linux.png differ diff --git a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-notification-unread-linux.png b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-notification-unread-linux.png index 1a94524d688..f0f6cee3e6a 100644 Binary files a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-notification-unread-linux.png and b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-notification-unread-linux.png differ diff --git a/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png b/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png index 498fb8f044b..7ef5b405438 100644 Binary files a/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png and b/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png index ad9e8c7a832..5703f384498 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-bubble-layout-linux.png index 35d4bfe8f45..0a3d265a601 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-bubble-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-bubble-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/configured-room-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/configured-room-irc-layout-linux.png index ec24ef90aa5..0453bf932a9 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/configured-room-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/configured-room-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-line-inline-start-margin-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-line-inline-start-margin-irc-layout-linux.png index 5ca3f3f50ff..077ecbf7176 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-line-inline-start-margin-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-line-inline-start-margin-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-bubble-layout-linux.png index ee983ee3a56..c0a01c99fb0 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-bubble-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-bubble-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-compact-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-compact-modern-layout-linux.png index fbee0bb1de3..87e65a86ae5 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-compact-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-compact-modern-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-layout-linux.png index 80359800e72..45c43f06fec 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-modern-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-modern-linux.png index cdab9fe0540..445d616ea40 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-modern-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-modern-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-bubble-layout-linux.png index 07e319522f1..194ecd07fbc 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-bubble-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-bubble-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-compact-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-compact-modern-layout-linux.png index 9c695fe077c..2b990bb3b86 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-compact-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-compact-modern-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-irc-layout-linux.png index 69d40e90160..294cd3ec7ab 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-modern-layout-linux.png index 899e7079afe..f0064c81e19 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-modern-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-and-messages-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-and-messages-irc-layout-linux.png index 0697e7813af..001ac64f2a6 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-and-messages-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-and-messages-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-bubble-layout-linux.png index 8b8d4d9fb54..f7a276d2f72 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-bubble-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-bubble-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-emote-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-emote-irc-layout-linux.png index 5188d731635..de056e0fa56 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-emote-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-emote-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-irc-layout-linux.png index 5ca3f3f50ff..077ecbf7176 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-modern-layout-linux.png index 9cdcc4a3b71..e1cd5ab19c3 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-modern-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-redaction-placeholder-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-redaction-placeholder-linux.png index e4d589c568a..4fb29024560 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-redaction-placeholder-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-redaction-placeholder-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-padding-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-padding-modern-layout-linux.png index 0bba20ad522..ceddab3312b 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-padding-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-padding-modern-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-zero-padding-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-zero-padding-irc-layout-linux.png index 670266b413d..5fba124a929 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-zero-padding-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-zero-padding-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-bubble-layout-linux.png index 5084f9b1222..800ceefc6e4 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-bubble-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-bubble-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-irc-layout-linux.png index 6e3529dddbf..9d2fcdf272d 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-modern-layout-linux.png index 70f15ccf8f3..f85715b0765 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-modern-layout-linux.png differ diff --git a/playwright/snapshots/user-onboarding/user-onboarding-new.spec.ts/User-Onboarding-new-user-app-download-dialog-1-linux.png b/playwright/snapshots/user-onboarding/user-onboarding-new.spec.ts/User-Onboarding-new-user-app-download-dialog-1-linux.png index 5be54c31582..589cb34cb4f 100644 Binary files a/playwright/snapshots/user-onboarding/user-onboarding-new.spec.ts/User-Onboarding-new-user-app-download-dialog-1-linux.png and b/playwright/snapshots/user-onboarding/user-onboarding-new.spec.ts/User-Onboarding-new-user-app-download-dialog-1-linux.png differ diff --git a/playwright/snapshots/user-onboarding/user-onboarding-new.spec.ts/User-Onboarding-new-user-page-is-shown-and-preference-exists-1-linux.png b/playwright/snapshots/user-onboarding/user-onboarding-new.spec.ts/User-Onboarding-new-user-page-is-shown-and-preference-exists-1-linux.png index db7b89c8411..ea1fa63bdec 100644 Binary files a/playwright/snapshots/user-onboarding/user-onboarding-new.spec.ts/User-Onboarding-new-user-page-is-shown-and-preference-exists-1-linux.png and b/playwright/snapshots/user-onboarding/user-onboarding-new.spec.ts/User-Onboarding-new-user-page-is-shown-and-preference-exists-1-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 ddb9667ba6d..9d9b431b0ca 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/playwright/tsconfig.json b/playwright/tsconfig.json index aea1bc543ae..55f979f3ce5 100644 --- a/playwright/tsconfig.json +++ b/playwright/tsconfig.json @@ -6,11 +6,12 @@ "resolveJsonModule": true, "esModuleInterop": true, "moduleResolution": "node", - "module": "es2022", + "module": "es2022" }, "include": [ "**/*.ts", + "../src/@types/matrix-js-sdk.d.ts", "../node_modules/matrix-js-sdk/src/@types/*.d.ts", - "../node_modules/matrix-js-sdk/node_modules/@matrix-org/olm/index.d.ts", - ], + "../node_modules/matrix-js-sdk/node_modules/@matrix-org/olm/index.d.ts" + ] } diff --git a/post-release.sh b/post-release.sh deleted file mode 100755 index 916d4b6f181..00000000000 --- a/post-release.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -./node_modules/matrix-js-sdk/post-release.sh "$@" diff --git a/release.sh b/release.sh deleted file mode 100755 index a9fb09e1fa6..00000000000 --- a/release.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -# -# Script to perform a release of matrix-react-sdk. - -set -e - -cd "$(dirname "$0")" - -# This link seems to get eaten by the release process, so ensure it exists. -yarn link matrix-js-sdk - -./node_modules/matrix-js-sdk/release.sh "$@" diff --git a/res/css/_common.pcss b/res/css/_common.pcss index f6ec9458dba..20ed9dfa392 100644 --- a/res/css/_common.pcss +++ b/res/css/_common.pcss @@ -329,16 +329,27 @@ legend { justify-content: center; } +.mx_Dialog_border { + z-index: var(--dialog-zIndex-standard); + position: relative; + max-height: calc(100% - var(--cpd-space-12x)); + display: flex; + flex-direction: column; + + .mx_Dialog_lightbox & { + /* The lightbox isn't so much of a dialog as a fullscreen overlay. We + don't want the glass border. */ + display: contents; + } +} + .mx_Dialog { background-color: $background; color: $light-fg-color; - z-index: var(--dialog-zIndex-standard); font-size: $font-15px; position: relative; - padding: 24px; - max-height: 80%; - box-shadow: 2px 15px 30px 0 $dialog-shadow-color; - border-radius: 8px; + padding: var(--cpd-space-8x) var(--cpd-space-10x); + box-sizing: border-box; overflow-y: auto; .mx_Dialog_staticWrapper & { @@ -439,7 +450,6 @@ legend { width: 100%; height: 100%; background-color: $dialog-backdrop-color; - opacity: 0.8; z-index: var(--dialog-zIndex-standard-background); &.mx_Dialog_staticBackground { @@ -483,42 +493,45 @@ legend { .mx_Dialog_header { position: relative; - padding: 3px 0; - margin-bottom: 10px; + padding: 0; + padding-inline-end: 20px; /* Reserve room for the close button */ + margin-bottom: var(--cpd-space-2x); &.mx_Dialog_headerWithButton > .mx_Dialog_title { text-align: center; } - - &.mx_Dialog_headerWithCancel { - padding-right: 20px; /* leave space for the 'X' cancel button */ - } - - &.mx_Dialog_headerWithCancelOnly { - padding: 0 20px 0 0; - margin: 0; - } } @define-mixin customisedCancelButton { - mask: url("$(res)/img/cancel.svg"); - mask-repeat: no-repeat; - mask-position: center; - mask-size: cover; - background-color: $dialog-close-fg-color; cursor: pointer; - position: unset; - width: unset; - height: unset; + position: relative; + width: 28px; + height: 28px; + border-radius: 14px; + background-color: var(--cpd-color-bg-subtle-secondary); + + &:hover { + background-color: var(--cpd-color-bg-subtle-primary); + } + + &::before { + content: ""; + width: 28px; + height: 28px; + position: absolute; + mask-image: url("@vector-im/compound-design-tokens/icons/close.svg"); + mask-repeat: no-repeat; + mask-position: center; + mask-size: 20px; + background-color: var(--cpd-color-icon-secondary); + } } .mx_Dialog_cancelButton { @mixin customisedCancelButton; - width: 18px; - height: 18px; position: absolute; - top: 10px; - right: 0; + top: var(--cpd-space-4x); + right: var(--cpd-space-4x); } .mx_Dialog_content { @@ -559,7 +572,7 @@ legend { /* align images in buttons (eg spinners) */ vertical-align: middle; border: 0px; - border-radius: 8px; + border-radius: 24px; font: var(--cpd-font-body-md-regular); color: $button-fg-color; background-color: var(--cpd-color-bg-action-primary-rest); @@ -633,7 +646,7 @@ legend { .mx_Dialog button.warning:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]), .mx_Dialog input[type="submit"].warning { - border: solid 1px var(--cpd-color-border-critical-primary); + border: solid 1px var(--cpd-color-border-critical-subtle); color: var(--cpd-color-text-critical-primary); } @@ -647,15 +660,23 @@ legend { } /* Spinner Dialog overide */ -.mx_Dialog_wrapper.mx_Dialog_spinner .mx_Dialog { - width: auto; - border-radius: 8px; - padding: 8px; - box-shadow: none; +.mx_Dialog_wrapper.mx_Dialog_spinner { + /* This is not a real dialog, so we shouldn't show a glass border */ + .mx_Dialog_border { + display: contents; + } - /* Don't show scroll-bars on spinner dialogs */ - overflow-x: hidden; - overflow-y: hidden; + .mx_Dialog { + inline-size: auto; + block-size: auto; + border-radius: 8px; + padding: 8px; + box-shadow: none; + + /* Don't show scroll-bars on spinner dialogs */ + overflow-x: hidden; + overflow-y: hidden; + } } /* TODO: Review mx_GeneralButton usage to see if it can use a different class */ diff --git a/res/css/_components.pcss b/res/css/_components.pcss index a29e7e98570..3ef26f8199d 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -209,7 +209,6 @@ @import "./views/elements/_SearchWarning.pcss"; @import "./views/elements/_ServerPicker.pcss"; @import "./views/elements/_SettingsFlag.pcss"; -@import "./views/elements/_Slider.pcss"; @import "./views/elements/_Spinner.pcss"; @import "./views/elements/_StyledCheckbox.pcss"; @import "./views/elements/_StyledRadioButton.pcss"; @@ -336,6 +335,7 @@ @import "./views/settings/_NotificationSettings2.pcss"; @import "./views/settings/_Notifications.pcss"; @import "./views/settings/_PhoneNumbers.pcss"; +@import "./views/settings/_PowerLevelSelector.pcss"; @import "./views/settings/_ProfileSettings.pcss"; @import "./views/settings/_SecureBackupPanel.pcss"; @import "./views/settings/_SetIdServer.pcss"; diff --git a/res/css/structures/_SpacePanel.pcss b/res/css/structures/_SpacePanel.pcss index d23f6f8c69f..0252da01b78 100644 --- a/res/css/structures/_SpacePanel.pcss +++ b/res/css/structures/_SpacePanel.pcss @@ -18,7 +18,7 @@ limitations under the License. --activeBackground-color: $panel-actions; --activeBorder-color: $primary-content; --activeBorder-transparent-gap: 1px; - --gutterSize: 16px; + --gutterSize: 14px; --height-nested: 24px; --height-topLevel: 32px; @@ -34,6 +34,10 @@ limitations under the License. display: flex; flex-direction: column; + &.collapsed { + width: 68px; + } + .mx_SpacePanel_toggleCollapse { position: absolute; width: 18px; @@ -149,6 +153,11 @@ limitations under the License. min-width: 0; } + &.mx_SpaceButton_narrow .mx_SpaceButton_selectionWrapper { + flex: initial; + width: 32px; + } + .mx_SpaceButton_name { flex: 1; margin-left: 8px; @@ -194,7 +203,8 @@ limitations under the License. &.mx_SpaceButton_home, &.mx_SpaceButton_favourites, &.mx_SpaceButton_people, - &.mx_SpaceButton_orphans { + &.mx_SpaceButton_orphans, + &.mx_SpaceButton_videoRooms { .mx_SpaceButton_icon { background-color: $panel-actions; @@ -220,6 +230,10 @@ limitations under the License. mask-image: url("$(res)/img/element-icons/roomlist/hash-circle.svg"); } + &.mx_SpaceButton_videoRooms .mx_SpaceButton_icon::before { + mask-image: url("@vector-im/compound-design-tokens/icons/video-call-solid.svg"); + } + &.mx_SpaceButton_new .mx_SpaceButton_icon { &::before { background-color: $primary-content; @@ -323,7 +337,8 @@ limitations under the License. /* root space buttons are bigger and not indented */ & > .mx_AutoHideScrollbar { flex: 1; - padding: 0 8px 16px 0; + padding: 0 0 16px 0; + scrollbar-gutter: stable; & > .mx_SpaceButton { height: var(--height-topLevel); @@ -338,23 +353,58 @@ limitations under the License. } &.mx_IndicatorScrollbar_topOverflow { - mask-image: linear-gradient(180deg, transparent, black 5%); + mask-image: linear-gradient(to bottom, transparent, black 16px); } &.mx_IndicatorScrollbar_bottomOverflow { - mask-image: linear-gradient(180deg, black, black 95%, transparent); + mask-image: linear-gradient( + to top, + transparent, + rgba(255, 255, 255, 30%) 4px, + rgba(255, 255, 255, 55%) 8px, + rgba(255, 255, 255, 75%) 12px, + black 16px + ); } &.mx_IndicatorScrollbar_topOverflow.mx_IndicatorScrollbar_bottomOverflow { - mask-image: linear-gradient(180deg, transparent, black 5%, black 95%, transparent); + /* This stacks two gradients on top of one another, which lets us + have a fixed pixel offset from both top and bottom for the colour stops. + Note the top fade is much smaller because the spaces start close to the top, + so otherwise a large gradient suddenly appears when you scroll down. + */ + mask-image: linear-gradient(to bottom, transparent, black 16px), + linear-gradient( + to top, + transparent, + rgba(255, 255, 255, 30%) 4px, + rgba(255, 255, 255, 55%) 8px, + rgba(255, 255, 255, 75%) 12px, + black 16px + ); + mask-position: + 0% 0%, + 0% 100%; + mask-size: + calc(100% - 10px) 50%, + calc(100% - 10px) 50%; + mask-repeat: no-repeat; } } .mx_UserMenu { - padding: 0 2px 8px; + padding-bottom: 12px; border-bottom: 1px solid $separator; margin: 12px 14px 4px 18px; + width: min-content; max-width: 226px; + + /* Display the container and img here as block elements so they don't take + * up extra vertical space. + */ + .mx_UserMenu_userAvatar_BaseAvatar { + display: block; + } } } diff --git a/res/css/structures/_TabbedView.pcss b/res/css/structures/_TabbedView.pcss index 847bea9cba3..756f6ab8647 100644 --- a/res/css/structures/_TabbedView.pcss +++ b/res/css/structures/_TabbedView.pcss @@ -18,7 +18,7 @@ limitations under the License. .mx_TabbedView { margin: 0; - padding: 0 0 0 16px; + padding: 0 0 0 var(--cpd-space-8x); display: flex; flex-direction: column; inset: 0; @@ -30,37 +30,42 @@ limitations under the License. position: absolute; .mx_TabbedView_tabLabels { - width: 170px; - max-width: 170px; + width: 220px; + max-width: 220px; position: fixed; margin: 0; /* Remove the default value */ padding: 0; /* Remove the default value */ } .mx_TabbedView_tabPanel { - margin-left: 240px; /* 170px sidebar + 70px padding */ + margin-left: 280px; /* 220px sidebar + 60px padding */ flex-direction: column; } + .mx_TabbedView_tabLabel:hover, .mx_TabbedView_tabLabel_active { - background-color: var(--cpd-color-bg-action-primary-rest); - color: var(--cpd-color-text-on-solid-primary); + color: $tab-label-active-fg-color; + + .mx_TabbedView_maskedIcon::before { + background-color: var(--cpd-color-icon-primary); + } } - .mx_TabbedView_tabLabel_active .mx_TabbedView_maskedIcon::before { - background-color: var(--cpd-color-icon-on-solid-primary); + .mx_TabbedView_tabLabel_active { + background-color: var(--cpd-color-bg-subtle-secondary); } .mx_TabbedView_maskedIcon { - width: 16px; - height: 16px; - margin-right: 16px; + width: 20px; + height: 20px; + margin-right: var(--cpd-space-3x); } .mx_TabbedView_maskedIcon::before { - mask-size: 16px; - width: 16px; - height: 16px; + mask-size: 20px; + width: 20px; + height: 20px; + transition: background-color 0.1s; } } @@ -120,10 +125,16 @@ limitations under the License. align-items: center; vertical-align: text-top; cursor: pointer; - padding: 8px; - border-radius: 8px; - font-size: $font-13px; + padding-block: var(--cpd-space-2x); + padding-inline: var(--cpd-space-3x) var(--cpd-space-4x); + box-sizing: border-box; + min-block-size: 40px; + border-radius: 24px; + font: var(--cpd-font-body-md-medium); position: relative; + transition: + color 0.1s, + background-color 0.1s; } .mx_TabbedView_maskedIcon { @@ -132,7 +143,7 @@ limitations under the License. .mx_TabbedView_maskedIcon::before { display: inline-block; - background-color: $icon-button-color; + background-color: var(--cpd-color-icon-secondary); mask-repeat: no-repeat; mask-position: center; content: ""; diff --git a/res/css/structures/_ThreadsActivityCentre.pcss b/res/css/structures/_ThreadsActivityCentre.pcss index ed30d1a6e7b..76b38d6c076 100644 --- a/res/css/structures/_ThreadsActivityCentre.pcss +++ b/res/css/structures/_ThreadsActivityCentre.pcss @@ -16,23 +16,42 @@ * / */ -.mx_ThreadsActivityCentreButton { - color: $secondary-content; - height: 32px; - min-width: 32px; +.mx_ThreadsActivityCentre_container { display: flex; - align-items: center; - justify-content: center; +} + +.mx_ThreadsActivityCentreButton { border-radius: 8px; - margin: auto; + margin: 18px auto auto auto; &.expanded { + /** + * override compound default background color when hovered + * should disappear when the space panel will be migrated to compound + */ + background-color: transparent !important; + /* align with settings icon */ - margin-left: 25px; + margin-left: 21px; - & > .mx_ThreadsActivityCentreButton_IndicatorIcon { + /** + * modify internal css of the compound component + * dirty but we need to add the `Threads` label into the indicator icon button + **/ + & > div { + display: flex; + align-items: center; + } + + & .mx_ThreadsActivityCentreButton_Icon { /* align with settings label */ margin-right: 14px; + /* required to set the icon width when into a flex container */ + min-width: 24px; + } + + & .mx_ThreadsActivityCentreButton_Text { + color: $secondary-content; } } @@ -49,12 +68,12 @@ } } -.mx_ThreadsActivity_rows { +.mx_ThreadsActivityCentre_rows { overflow-y: scroll; /* Let some space at the top and the bottom of the pop-up */ max-height: calc(100vh - 200px); - .mx_ThreadsActivityRow { + .mx_ThreadsActivityCentreRow { height: 48px; /* Make the label of the MenuItem stay on one line and truncate with ellipsis if needed */ @@ -63,7 +82,7 @@ overflow: hidden; text-overflow: ellipsis; /* Arbitrary size, keep the TAC as the wanted width */ - width: 194px; + width: 202px; } } } diff --git a/res/css/structures/auth/_CompleteSecurity.pcss b/res/css/structures/auth/_CompleteSecurity.pcss index 4c3602ac264..ef8c82cbc83 100644 --- a/res/css/structures/auth/_CompleteSecurity.pcss +++ b/res/css/structures/auth/_CompleteSecurity.pcss @@ -35,8 +35,6 @@ limitations under the License. .mx_CompleteSecurity_skip { @mixin customisedCancelButton; - width: 18px; - height: 18px; position: absolute; right: 24px; } diff --git a/res/css/views/auth/_LoginWithQR.pcss b/res/css/views/auth/_LoginWithQR.pcss index 665c351eb7b..6a112c7c82c 100644 --- a/res/css/views/auth/_LoginWithQR.pcss +++ b/res/css/views/auth/_LoginWithQR.pcss @@ -14,7 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_LoginWithQRSection .mx_AccessibleButton { +.mx_LoginWithQRSection p { + margin-top: 0; + margin-bottom: $spacing-16; +} + +.mx_LoginWithQRSection .mx_AccessibleButton svg { margin-right: $spacing-12; } @@ -69,7 +74,6 @@ limitations under the License. } .mx_QRCode { - padding: $spacing-12 $spacing-40; margin: $spacing-28 0; } @@ -89,7 +93,7 @@ limitations under the License. .mx_LoginWithQR_centreTitle { h1 { - text-align: centre; + text-align: center; } } @@ -141,14 +145,28 @@ limitations under the License. } } + .mx_LoginWithQR_heading { + display: flex; + gap: $spacing-12; + align-items: center; + } + .mx_LoginWithQR_BackButton { - height: $spacing-12; - margin-bottom: $spacing-24; + height: $spacing-28; + border-radius: $spacing-28; + padding: $spacing-4; + box-sizing: border-box; + background-color: var(--cpd-color-bg-subtle-secondary); svg { height: 100%; } } + .mx_LoginWithQR_breadcrumbs { + font-size: $font-13px; + color: var(--cpd-color-text-secondary); + } + .mx_LoginWithQR_main { display: flex; flex-direction: column; @@ -156,7 +174,6 @@ limitations under the License. } .mx_QRCode { - border: 1px solid $quinary-content; border-radius: $spacing-8; display: flex; justify-content: center; diff --git a/res/css/views/context_menus/_RoomGeneralContextMenu.pcss b/res/css/views/context_menus/_RoomGeneralContextMenu.pcss index b5162bb1bbb..4017a53f202 100644 --- a/res/css/views/context_menus/_RoomGeneralContextMenu.pcss +++ b/res/css/views/context_menus/_RoomGeneralContextMenu.pcss @@ -10,6 +10,10 @@ mask-image: url("$(res)/img/element-icons/roomlist/mark-as-read.svg"); } +.mx_RoomGeneralContextMenu_iconMarkAsUnread::before { + mask-image: url("$(res)/img/element-icons/roomlist/mark-as-unread.svg"); +} + .mx_RoomGeneralContextMenu_iconNotificationsDefault::before { mask-image: url("$(res)/img/element-icons/notifications.svg"); } diff --git a/res/css/views/dialogs/_CompoundDialog.pcss b/res/css/views/dialogs/_CompoundDialog.pcss index 70ba1f8c10c..addf2108637 100644 --- a/res/css/views/dialogs/_CompoundDialog.pcss +++ b/res/css/views/dialogs/_CompoundDialog.pcss @@ -21,7 +21,7 @@ limitations under the License. /* -------------------------------------------------------------------------------- */ /* Override legacy/default styles for dialogs */ -.mx_Dialog_wrapper.mx_CompoundDialog > .mx_Dialog { +.mx_Dialog_wrapper.mx_CompoundDialog .mx_Dialog { padding: 0; /* we'll manage it ourselves */ color: $primary-content; } @@ -41,17 +41,14 @@ limitations under the License. font-size: $font-24px; margin: 0; /* managed by header class */ } + } - .mx_CompoundDialog_cancelButton { - @mixin customisedCancelButton; - width: 20px; - height: 20px; - - /* Align with middle of title, 34px from right edge */ - position: absolute; - top: 34px; - right: 34px; - } + .mx_CompoundDialog_cancelButton { + @mixin customisedCancelButton; + /* Align with corner radius of dialog */ + position: absolute; + top: var(--cpd-space-4x); + right: var(--cpd-space-4x); } .mx_CompoundDialog_form { diff --git a/res/css/views/dialogs/_ForwardDialog.pcss b/res/css/views/dialogs/_ForwardDialog.pcss index e6c322a77c6..69c9bafc89c 100644 --- a/res/css/views/dialogs/_ForwardDialog.pcss +++ b/res/css/views/dialogs/_ForwardDialog.pcss @@ -96,7 +96,8 @@ limitations under the License. padding: 6px; border-radius: 8px; - &:hover { + &:hover, + &.mx_ForwardList_entry_active { background-color: $spacePanel-bg-color; } diff --git a/res/css/views/dialogs/_JoinRuleDropdown.pcss b/res/css/views/dialogs/_JoinRuleDropdown.pcss index 6df937816e1..da0796edead 100644 --- a/res/css/views/dialogs/_JoinRuleDropdown.pcss +++ b/res/css/views/dialogs/_JoinRuleDropdown.pcss @@ -19,10 +19,6 @@ limitations under the License. font: var(--cpd-font-body-md-regular); color: $primary-content; - .mx_Dropdown_input { - border: 1px solid $input-border-color; - } - .mx_Dropdown_option { font: var(--cpd-font-body-md-regular); line-height: $font-32px; diff --git a/res/css/views/dialogs/_LocationViewDialog.pcss b/res/css/views/dialogs/_LocationViewDialog.pcss index 600c3082657..8e04b5f4287 100644 --- a/res/css/views/dialogs/_LocationViewDialog.pcss +++ b/res/css/views/dialogs/_LocationViewDialog.pcss @@ -16,11 +16,6 @@ limitations under the License. .mx_LocationViewDialog_wrapper .mx_Dialog { padding: 0px; - - /* Unset contain and position to allow the close button - to appear outside the dialog */ - contain: unset; - position: unset; } .mx_LocationViewDialog { @@ -37,16 +32,13 @@ limitations under the License. .mx_Dialog_title { display: none; } + } - .mx_Dialog_cancelButton { - z-index: 4010; - position: absolute; - right: 5vw; - top: 5vh; - width: 20px; - height: 20px; - background-color: $dialog-close-external-color; - } + .mx_Dialog_cancelButton { + z-index: 4010; + position: absolute; + left: var(--cpd-space-4x); + top: var(--cpd-space-4x); } } diff --git a/res/css/views/dialogs/_SettingsDialog.pcss b/res/css/views/dialogs/_SettingsDialog.pcss index 71dedd3fe33..9b0205a3b49 100644 --- a/res/css/views/dialogs/_SettingsDialog.pcss +++ b/res/css/views/dialogs/_SettingsDialog.pcss @@ -20,12 +20,12 @@ limitations under the License. .mx_SpaceSettingsDialog, .mx_SpacePreferencesDialog { width: 90vw; - max-width: 1000px; + max-width: 980px; /* set the height too since tabbed view scrolls itself. */ height: 80vh; .mx_TabbedView { - top: 65px; + top: 90px; } .mx_TabbedView .mx_SettingsTab { diff --git a/res/css/views/dialogs/_SpotlightDialog.pcss b/res/css/views/dialogs/_SpotlightDialog.pcss index 78346e9b7ee..32c14bca9d0 100644 --- a/res/css/views/dialogs/_SpotlightDialog.pcss +++ b/res/css/views/dialogs/_SpotlightDialog.pcss @@ -14,38 +14,46 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_SpotlightDialog_wrapper .mx_Dialog { - border-radius: 8px; - overflow-y: initial; - position: relative; - height: 60%; - padding: 0; - contain: unset; /* needed for #mx_SpotlightDialog_keyboardPrompt to not be culled */ - - #mx_SpotlightDialog_keyboardPrompt { - position: absolute; - padding: $spacing-8; +.mx_SpotlightDialog_wrapper { + .mx_Dialog_border { + /* Disable the glass border as this dialog wasn't designed with it in mind */ + display: contents; + } + + .mx_Dialog { + width: fit-content; border-radius: 8px; - background-color: $background; - top: -60px; /* relative to the top of the modal */ - left: 50%; - transform: translateX(-50%); - font-size: $font-12px; - line-height: $font-15px; - color: $secondary-content; - - kbd { - display: inline-block; - padding: 2px $spacing-4; - margin: 0 $spacing-4; - border-radius: 6px; - background-color: $quinary-content; - vertical-align: middle; - color: $tertiary-content; - /* To avoid any styling inherent with elements */ - font-family: inherit; - font-weight: inherit; - font-size: inherit; + overflow-y: initial; + position: relative; + height: 60%; + padding: 0; + contain: unset; /* needed for #mx_SpotlightDialog_keyboardPrompt to not be culled */ + + #mx_SpotlightDialog_keyboardPrompt { + position: absolute; + padding: $spacing-8; + border-radius: 8px; + background-color: $background; + top: -60px; /* relative to the top of the modal */ + left: 50%; + transform: translateX(-50%); + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-content; + + kbd { + display: inline-block; + padding: 2px $spacing-4; + margin: 0 $spacing-4; + border-radius: 6px; + background-color: $quinary-content; + vertical-align: middle; + color: $tertiary-content; + /* To avoid any styling inherent with elements */ + font-family: inherit; + font-weight: inherit; + font-size: inherit; + } } } } diff --git a/res/css/views/dialogs/_TermsDialog.pcss b/res/css/views/dialogs/_TermsDialog.pcss index 99d7eb720b0..9b48cb8945e 100644 --- a/res/css/views/dialogs/_TermsDialog.pcss +++ b/res/css/views/dialogs/_TermsDialog.pcss @@ -19,7 +19,7 @@ limitations under the License. * terms dialog sizing when it will appear for the integration manager so that * it gets the same basic size as the IM's own modal. */ -.mx_TermsDialog_forIntegrationManager .mx_Dialog { +.mx_TermsDialog_forIntegrationManager .mx_Dialog_border { width: 60%; height: 70%; box-sizing: border-box; diff --git a/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss b/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss index 85e5c082585..82e490c7817 100644 --- a/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss +++ b/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss @@ -117,6 +117,8 @@ limitations under the License. .mx_AccessSecretStorageDialog_reset { position: relative; padding-inline-start: $spacingStart; + /* To avoid bold styling inherent with elements */ + font-weight: inherit; &::before { content: ""; diff --git a/res/css/views/elements/_AccessibleButton.pcss b/res/css/views/elements/_AccessibleButton.pcss index 1ce2ddc6795..236cd03abf4 100644 --- a/res/css/views/elements/_AccessibleButton.pcss +++ b/res/css/views/elements/_AccessibleButton.pcss @@ -38,7 +38,7 @@ limitations under the License. &.mx_AccessibleButton_hasKind { padding: 7px 18px; text-align: center; - border-radius: 8px; + border-radius: 24px; display: inline-flex; align-items: center; justify-content: center; diff --git a/res/css/views/elements/_Dropdown.pcss b/res/css/views/elements/_Dropdown.pcss index 28e96f0146f..4bc78608061 100644 --- a/res/css/views/elements/_Dropdown.pcss +++ b/res/css/views/elements/_Dropdown.pcss @@ -27,8 +27,8 @@ limitations under the License. display: flex; align-items: center; position: relative; - border-radius: 4px; - border: 1px solid $strong-input-border-color; + border-radius: 8px; + border: 1px solid var(--cpd-color-border-interactive-secondary); font: var(--cpd-font-body-sm-regular); user-select: none; } diff --git a/res/css/views/elements/_Field.pcss b/res/css/views/elements/_Field.pcss index 02b0e482b55..149ea054f6f 100644 --- a/res/css/views/elements/_Field.pcss +++ b/res/css/views/elements/_Field.pcss @@ -22,17 +22,17 @@ limitations under the License. min-width: 0; position: relative; margin: 1em 0; - border-radius: 4px; + border-radius: 8px; transition: border-color 0.25s; - border: 1px solid $input-border-color; + border: 1px solid var(--cpd-color-border-interactive-secondary); } .mx_Field_prefix { - border-right: 1px solid $input-border-color; + border-right: 1px solid var(--cpd-color-border-interactive-secondary); } .mx_Field_postfix { - border-left: 1px solid $input-border-color; + border-left: 1px solid var(--cpd-color-border-interactive-secondary); } .mx_Field input, @@ -42,7 +42,7 @@ limitations under the License. border: none; /* Even without a border here, we still need this avoid overlapping the rounded */ /* corners on the field above. */ - border-radius: 4px; + border-radius: 8px; padding: 8px 9px; color: $primary-content; background-color: $background; @@ -102,6 +102,7 @@ limitations under the License. background-color 0.25s ease-out 0.1s; background-color: transparent; font: var(--cpd-font-body-md-regular); + color: var(--cpd-color-text-secondary); transform: translateY(0); position: absolute; left: 0px; diff --git a/res/css/views/elements/_Pill.pcss b/res/css/views/elements/_Pill.pcss index c697b63de50..8363fc6641f 100644 --- a/res/css/views/elements/_Pill.pcss +++ b/res/css/views/elements/_Pill.pcss @@ -26,7 +26,7 @@ limitations under the License. overflow: hidden; cursor: pointer; - color: $accent-fg-color !important; /* To override .markdown-body */ + color: var(--cpd-color-text-on-solid-primary) !important; /* To override .markdown-body */ background-color: $pill-bg-color !important; /* To override .markdown-body */ > * { @@ -35,20 +35,26 @@ limitations under the License. &.mx_UserPill_me, &.mx_AtRoomPill { - background-color: $alert !important; /* To override .markdown-body */ + background-color: var(--cpd-color-bg-critical-primary) !important; /* To override .markdown-body */ } &:hover { background-color: $pill-hover-bg-color !important; /* To override .markdown-body */ } + &:active { + background-color: $pill-press-bg-color !important; /* To override .markdown-body */ + } + &.mx_UserPill_me:hover { - background-color: #ff6b75 !important; /* To override .markdown-body | same on both themes */ + background-color: var( + --cpd-color-bg-critical-hovered + ) !important; /* To override .markdown-body | same on both themes */ } /* We don't want to indicate clickability */ &.mx_AtRoomPill:hover { - background-color: $alert !important; /* To override .markdown-body */ + background-color: var(--cpd-color-bg-critical-primary) !important; /* To override .markdown-body */ cursor: unset; } diff --git a/res/css/views/elements/_Slider.pcss b/res/css/views/elements/_Slider.pcss deleted file mode 100644 index 2477d542c64..00000000000 --- a/res/css/views/elements/_Slider.pcss +++ /dev/null @@ -1,126 +0,0 @@ -/* -Copyright 2020 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_Slider { - position: relative; - margin: 0; - flex-grow: 1; - - input[type="range"] { - height: 2.4em; - appearance: none; - width: 100%; - background: none; - font-size: 1em; /* set base multiplier for em units applied later */ - - --active-color: var(--cpd-color-bg-action-primary-rest); - --selection-dot-size: 2.4em; - - &:disabled { - cursor: not-allowed; - - --active-color: $slider-background-color; - } - - &:focus:not(:focus-visible) { - outline: none; - } - - &::-webkit-slider-runnable-track { - width: 100%; - height: 0.4em; - background: $slider-background-color; - border-radius: 5px; - border: 0 solid #000000; - } - &::-webkit-slider-thumb { - border: 0 solid #000000; - width: var(--selection-dot-size); - height: var(--selection-dot-size); - background: var(--active-color); - border-radius: 50%; - -webkit-appearance: none; - margin-top: calc(2px + 1.2em - var(--selection-dot-size)); - } - &:focus::-webkit-slider-runnable-track { - background: $slider-background-color; - } - - &::-moz-range-track { - width: 100%; - height: 0.4em; - background: $slider-background-color; - border-radius: 5px; - border: 0 solid #000000; - } - &::-moz-range-progress { - height: 0.4em; - background: var(--active-color); - border-radius: 5px; - border: 0 solid #000000; - } - &::-moz-range-thumb { - border: 0 solid #000000; - width: var(--selection-dot-size); - height: var(--selection-dot-size); - background: var(--active-color); - border-radius: 50%; - } - - &::-ms-track { - width: 100%; - height: 0.4em; - background: transparent; - border-color: transparent; - color: transparent; - } - &::-ms-fill-lower, - &::-ms-fill-upper { - background: $slider-background-color; - border: 0 solid #000000; - border-radius: 10px; - } - &::-ms-thumb { - margin-top: 1px; - width: var(--selection-dot-size); - height: var(--selection-dot-size); - background: var(--active-color); - border-radius: 50%; - } - &:focus::-ms-fill-upper { - background: $slider-background-color; - } - &::-ms-fill-lower, - &:focus::-ms-fill-lower { - background: var(--active-color); - } - } - - output { - position: absolute; - left: 50%; - transform: translateX(-50%); - - font-size: 1em; /* set base multiplier for em units applied later */ - text-align: center; - top: 3em; - - .mx_Slider_selection_label { - color: $muted-fg-color; - font: var(--cpd-font-body-md-regular); - } - } -} diff --git a/res/css/views/elements/_StyledCheckbox.pcss b/res/css/views/elements/_StyledCheckbox.pcss index 4bda5ef217b..66cc13279bf 100644 --- a/res/css/views/elements/_StyledCheckbox.pcss +++ b/res/css/views/elements/_StyledCheckbox.pcss @@ -84,22 +84,23 @@ limitations under the License. } &:checked + label > .mx_Checkbox_background { - background: var(--cpd-color-bg-action-primary-rest); - border-color: var(--cpd-color-bg-action-primary-rest); + background: var(--cpd-color-bg-accent-rest); + border-color: var(--cpd-color-bg-accent-rest); } &:checked:disabled + label > .mx_Checkbox_background { - opacity: 0.5; + background: var(--cpd-color-bg-action-primary-disabled); + border-color: var(--cpd-color-bg-action-primary-disabled); } } .mx_Checkbox.mx_Checkbox_kind_outline input[type="checkbox"] { & + label > .mx_Checkbox_background .mx_Checkbox_checkmark { - background: var(--cpd-color-bg-action-primary-rest); + background: var(--cpd-color-bg-accent-rest); } &:checked + label > .mx_Checkbox_background { background: transparent; - border-color: var(--cpd-color-bg-action-primary-rest); + border-color: var(--cpd-color-bg-accent-rest); } } diff --git a/res/css/views/elements/_StyledRadioButton.pcss b/res/css/views/elements/_StyledRadioButton.pcss index e250c1d0156..360dc2f55da 100644 --- a/res/css/views/elements/_StyledRadioButton.pcss +++ b/res/css/views/elements/_StyledRadioButton.pcss @@ -21,7 +21,7 @@ limitations under the License. .mx_StyledRadioButton { $radio-circle-color: var(--cpd-color-border-interactive-primary); - $active-radio-circle-color: var(--cpd-color-bg-action-primary-rest); + $active-radio-circle-color: var(--cpd-color-bg-accent-rest); position: relative; display: flex; @@ -126,5 +126,5 @@ limitations under the License. } .mx_StyledRadioButton_checked { - border-color: var(--cpd-color-bg-action-primary-rest); + border-color: var(--cpd-color-bg-accent-rest); } diff --git a/res/css/views/elements/_ToggleSwitch.pcss b/res/css/views/elements/_ToggleSwitch.pcss index 099be7e8ef2..c18444c0b83 100644 --- a/res/css/views/elements/_ToggleSwitch.pcss +++ b/res/css/views/elements/_ToggleSwitch.pcss @@ -17,33 +17,42 @@ limitations under the License. .mx_ToggleSwitch { --ToggleSwitch-min-width: $font-44px; - transition: background-color 0.2s ease-out 0.1s; + transition: + background-color 0.2s ease-out 0.1s, + border-color 0.2s ease-out 0.1s; width: $font-44px; height: $font-20px; border-radius: 1.5rem; padding: 2px; - background-color: $background; - border: 1px solid $strong-input-border-color; - opacity: 0.5; + background-color: var(--cpd-color-bg-canvas-disabled); + border: 1px solid var(--cpd-color-border-disabled); + cursor: not-allowed; - &[aria-disabled="true"] { - cursor: not-allowed; - } -} + &.mx_ToggleSwitch_enabled { + cursor: pointer; + background-color: var(--cpd-color-bg-canvas-default); + border: var(--cpd-border-width-1) solid var(--cpd-color-border-interactive-primary); -.mx_ToggleSwitch_enabled { - cursor: pointer; - opacity: 1; -} + &.mx_ToggleSwitch_on { + background-color: var(--cpd-color-bg-accent-rest); + border-color: var(--cpd-color-bg-accent-rest); + } + + > .mx_ToggleSwitch_ball { + background-color: var(--cpd-color-icon-secondary); + } + } -.mx_ToggleSwitch.mx_ToggleSwitch_on { - background-color: $inverted-bg-color; + &.mx_ToggleSwitch_on { + background-color: var(--cpd-color-bg-action-primary-disabled); + border-color: var(--cpd-color-bg-action-primary-disabled); - > .mx_ToggleSwitch_ball { - left: calc(100% - $font-20px); - background-color: $background; + > .mx_ToggleSwitch_ball { + left: calc(100% - $font-20px); + background-color: var(--cpd-color-icon-on-solid-primary); + } } } @@ -52,7 +61,9 @@ limitations under the License. width: $font-20px; height: $font-20px; border-radius: $font-20px; - background-color: $togglesw-ball-color; - transition: left 0.15s ease-out 0.1s; + background-color: var(--cpd-color-bg-action-primary-disabled); + transition: + left 0.15s ease-out 0.1s, + background-color 0.15s ease-out 0.1s; left: 0; } diff --git a/res/css/views/elements/_Tooltip.pcss b/res/css/views/elements/_Tooltip.pcss index d35e55db095..54a16a0cbf0 100644 --- a/res/css/views/elements/_Tooltip.pcss +++ b/res/css/views/elements/_Tooltip.pcss @@ -71,8 +71,8 @@ limitations under the License. max-width: 300px; word-break: break-word; - background-color: #21262c; /* Same on both themes */ - color: $accent-fg-color; + background-color: var(--cpd-color-alpha-gray-1400); + color: var(--cpd-color-text-on-solid-primary); border: 0; text-align: center; diff --git a/res/css/views/messages/_DateSeparator.pcss b/res/css/views/messages/_DateSeparator.pcss index de0cd668329..39cffc0f332 100644 --- a/res/css/views/messages/_DateSeparator.pcss +++ b/res/css/views/messages/_DateSeparator.pcss @@ -39,5 +39,5 @@ limitations under the License. mask-size: contain; mask-repeat: no-repeat; mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); - background-color: $tertiary-content; + background-color: var(--cpd-color-icon-secondary); } diff --git a/res/css/views/messages/_TimelineSeparator.pcss b/res/css/views/messages/_TimelineSeparator.pcss index 40ca0967a66..d8550179a6e 100644 --- a/res/css/views/messages/_TimelineSeparator.pcss +++ b/res/css/views/messages/_TimelineSeparator.pcss @@ -20,12 +20,12 @@ limitations under the License. display: flex; align-items: center; font: var(--cpd-font-body-md-regular); - color: $roomtopic-color; + color: var(--cpd-color-text-secondary); } .mx_TimelineSeparator > hr { flex: 1 1 0; height: 0; border: none; - border-bottom: 1px solid $menu-selected-color; + border-bottom: 1px solid var(--cpd-color-gray-400); } diff --git a/res/css/views/right_panel/_BaseCard.pcss b/res/css/views/right_panel/_BaseCard.pcss index e8bbc2dc26f..6d17930fcea 100644 --- a/res/css/views/right_panel/_BaseCard.pcss +++ b/res/css/views/right_panel/_BaseCard.pcss @@ -179,6 +179,7 @@ limitations under the License. left: 0; mask-repeat: no-repeat; mask-position: center; + mask-size: 20px; background-color: var(--cpd-color-icon-secondary); } } @@ -195,7 +196,7 @@ limitations under the License. .mx_BaseCard_close { order: 999; /* always last */ &::before { - mask-image: url("$(res)/img/icons-close.svg"); + mask-image: url("@vector-im/compound-design-tokens/icons/close.svg"); } } diff --git a/res/css/views/right_panel/_ThreadPanel.pcss b/res/css/views/right_panel/_ThreadPanel.pcss index 9d14c993dfd..104430c190e 100644 --- a/res/css/views/right_panel/_ThreadPanel.pcss +++ b/res/css/views/right_panel/_ThreadPanel.pcss @@ -1,5 +1,5 @@ /* -Copyright 2021 The Matrix.org Foundation C.I.C. +Copyright 2021,2024 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,11 +20,22 @@ limitations under the License. .mx_BaseCard_header { .mx_BaseCard_header_title { + .mx_BaseCard_header_title_heading { + margin-right: auto; + } + .mx_AccessibleButton { font-size: 12px; color: $secondary-content; } + .mx_ThreadPanel_vertical_separator { + height: 16px; + margin-left: var(--cpd-space-3x); + margin-right: var(--cpd-space-1x); + border-left: 1px solid var(--cpd-color-gray-400); + } + .mx_ThreadPanel_dropdown { padding: 3px $spacing-4 3px $spacing-8; border-radius: 4px; diff --git a/res/css/views/right_panel/_VerificationPanel.pcss b/res/css/views/right_panel/_VerificationPanel.pcss index ee8dd6ecfc5..ff080e8cb66 100644 --- a/res/css/views/right_panel/_VerificationPanel.pcss +++ b/res/css/views/right_panel/_VerificationPanel.pcss @@ -49,9 +49,6 @@ limitations under the License. .mx_EncryptionPanel_cancel { @mixin customisedCancelButton; - width: 14px; - height: 14px; - background-color: $settings-subsection-fg-color; position: absolute; z-index: 100; top: 14px; diff --git a/res/css/views/rooms/_BasicMessageComposer.pcss b/res/css/views/rooms/_BasicMessageComposer.pcss index e09eaa5a04f..3add788b129 100644 --- a/res/css/views/rooms/_BasicMessageComposer.pcss +++ b/res/css/views/rooms/_BasicMessageComposer.pcss @@ -20,7 +20,7 @@ limitations under the License. .mx_BasicMessageComposer_inputEmpty > :first-child::before { content: var(--placeholder); - opacity: 0.333; + color: var(--cpd-color-text-secondary); width: 0; height: 0; overflow: visible; diff --git a/res/css/views/rooms/_E2EIcon.pcss b/res/css/views/rooms/_E2EIcon.pcss index 18ff3f28f92..d6c184f103c 100644 --- a/res/css/views/rooms/_E2EIcon.pcss +++ b/res/css/views/rooms/_E2EIcon.pcss @@ -37,15 +37,6 @@ limitations under the License. } } -/* white infill for the transparency */ -.mx_E2EIcon::before { - background-color: #ffffff; - mask-image: url("$(res)/img/e2e/normal.svg"); - mask-repeat: no-repeat; - mask-position: center; - mask-size: 80%; -} - /* transparent-looking border surrounding the shield for when overlain over avatars */ .mx_E2EIcon_bordered { mask-image: url("$(res)/img/e2e/normal.svg"); @@ -59,6 +50,7 @@ limitations under the License. /* shrink the infill of the badge */ &::before { mask-size: 60%; + background: var(--cpd-color-bg-canvas-default); } } @@ -69,7 +61,7 @@ limitations under the License. .mx_E2EIcon_normal::after { mask-image: url("$(res)/img/e2e/normal.svg"); - background-color: $header-panel-text-primary-color; + background-color: var(--cpd-color-icon-tertiary); } .mx_E2EIcon_verified::after { diff --git a/res/css/views/rooms/_EventTile.pcss b/res/css/views/rooms/_EventTile.pcss index 4e11f64e773..ad88a7c7863 100644 --- a/res/css/views/rooms/_EventTile.pcss +++ b/res/css/views/rooms/_EventTile.pcss @@ -53,7 +53,7 @@ $left-gutter: 64px; height: 16px; &::before { - background-color: $tertiary-content; + background-color: var(--cpd-color-icon-tertiary); mask-repeat: no-repeat; mask-position: center; mask-size: 16px; @@ -858,12 +858,12 @@ $left-gutter: 64px; &.mx_EventTile_e2eIcon_normal::after { mask-image: url("$(res)/img/e2e/normal.svg"); /* regular shield */ - background-color: $header-panel-text-primary-color; /* grey */ + background-color: var(--cpd-color-icon-tertiary); /* grey */ } &.mx_EventTile_e2eIcon_decryption_failure::after { mask-image: url("$(res)/img/e2e/decryption-failure.svg"); /* key in a circle */ - background-color: $secondary-content; + background-color: var(--cpd-color-icon-tertiary); } } diff --git a/res/css/views/rooms/_JumpToBottomButton.pcss b/res/css/views/rooms/_JumpToBottomButton.pcss index 0a760e2cd69..5bf2c0c0baf 100644 --- a/res/css/views/rooms/_JumpToBottomButton.pcss +++ b/res/css/views/rooms/_JumpToBottomButton.pcss @@ -39,13 +39,12 @@ limitations under the License. /* with text-align in parent */ display: inline-block; padding: 0 4px; - color: $accent-fg-color; - background-color: $muted-fg-color; + color: var(--cpd-color-text-on-solid-primary); + background-color: var(--cpd-color-icon-secondary); } .mx_JumpToBottomButton_highlight .mx_JumpToBottomButton_badge { - color: $secondary-accent-color; - background-color: $alert; + background-color: var(--cpd-color-icon-critical-primary); } .mx_JumpToBottomButton_scrollDown { @@ -55,7 +54,7 @@ limitations under the License. border-radius: 19px; box-sizing: border-box; background: $background; - border: 1.3px solid $muted-fg-color; + border: 1.3px solid var(--cpd-color-icon-tertiary); cursor: pointer; } @@ -68,5 +67,5 @@ limitations under the License. mask-size: 20px; mask-position: center 6px; transform: rotate(180deg); - background: $muted-fg-color; + background: var(--cpd-color-icon-tertiary); } diff --git a/res/css/views/rooms/_LegacyRoomHeader.pcss b/res/css/views/rooms/_LegacyRoomHeader.pcss index 9994f4223ea..ce088f7deba 100644 --- a/res/css/views/rooms/_LegacyRoomHeader.pcss +++ b/res/css/views/rooms/_LegacyRoomHeader.pcss @@ -185,10 +185,10 @@ limitations under the License. } &:hover { - background: $accent-300; + background: var(--cpd-color-bg-subtle-primary); &::before { - background-color: $accent; + background-color: var(--cpd-color-icon-primary); } } } @@ -213,18 +213,18 @@ limitations under the License. margin: 4px; &.mx_Indicator_highlight { - background: $alert; - box-shadow: $alert; + background: var(--cpd-color-icon-critical-primary); + box-shadow: var(--cpd-color-icon-critical-primary); } &.mx_Indicator_notification { - background: $room-icon-unread-color; - box-shadow: $room-icon-unread-color; + background: var(--cpd-color-icon-success-primary); + box-shadow: var(--cpd-color-icon-success-primary); } &.mx_Indicator_activity { - background: $primary-content; - box-shadow: $primary-content; + background: var(--cpd-color-icon-primary); + box-shadow: var(--cpd-color-icon-primary); } } @@ -234,10 +234,9 @@ limitations under the License. } } -.mx_LegacyRoomHeader_button--highlight, -.mx_LegacyRoomHeader_button:hover { +.mx_LegacyRoomHeader_button--highlight { &::before { - background-color: $accent !important; + background-color: var(--cpd-color-icon-primary) !important; } } diff --git a/res/css/views/rooms/_MessageComposer.pcss b/res/css/views/rooms/_MessageComposer.pcss index 0fd72575b01..2c8fe592c42 100644 --- a/res/css/views/rooms/_MessageComposer.pcss +++ b/res/css/views/rooms/_MessageComposer.pcss @@ -191,7 +191,7 @@ limitations under the License. } .mx_MessageComposer_button { - @mixin composerButton 50%, var(--cpd-color-icon-secondary), var(--cpd-color-bg-subtle-secondary); + @mixin composerButton 50%, var(--cpd-color-icon-primary), var(--cpd-color-bg-subtle-primary); &:last-child { margin-right: auto; diff --git a/res/css/views/rooms/_NotificationBadge.pcss b/res/css/views/rooms/_NotificationBadge.pcss index 41b1e0f530c..6ffe7d9da71 100644 --- a/res/css/views/rooms/_NotificationBadge.pcss +++ b/res/css/views/rooms/_NotificationBadge.pcss @@ -39,7 +39,7 @@ limitations under the License. width: 8px; height: 8px; border-radius: 8px; - background-color: var(--cpd-color-text-primary); + background-color: var(--cpd-color-icon-primary); .mx_NotificationBadge_count { display: none; @@ -86,7 +86,8 @@ limitations under the License. .mx_NotificationBadge_count { font-size: $font-10px; line-height: $font-14px; - color: #fff; /* TODO: Variable */ + font-weight: var(--cpd-font-weight-semibold); + color: var(--cpd-color-text-on-solid-primary); } } } diff --git a/res/css/views/rooms/_RoomSublist.pcss b/res/css/views/rooms/_RoomSublist.pcss index 24898b89251..0e9ad1a9bb4 100644 --- a/res/css/views/rooms/_RoomSublist.pcss +++ b/res/css/views/rooms/_RoomSublist.pcss @@ -118,7 +118,7 @@ limitations under the License. mask-position: center; mask-size: contain; mask-repeat: no-repeat; - background: $muted-fg-color; + background: var(--cpd-color-icon-secondary); } } @@ -167,7 +167,7 @@ limitations under the License. mask-position: center; mask-size: contain; mask-repeat: no-repeat; - background-color: $tertiary-content; + background-color: var(--cpd-color-icon-secondary); mask-image: url("$(res)/img/feather-customised/chevron-down.svg"); } diff --git a/res/css/views/rooms/_RoomTile.pcss b/res/css/views/rooms/_RoomTile.pcss index bf68e4035e4..decd1067b19 100644 --- a/res/css/views/rooms/_RoomTile.pcss +++ b/res/css/views/rooms/_RoomTile.pcss @@ -131,7 +131,7 @@ limitations under the License. mask-position: center; mask-size: contain; mask-repeat: no-repeat; - background: $primary-content; + background: var(--cpd-color-icon-primary); } } diff --git a/res/css/views/rooms/_TopUnreadMessagesBar.pcss b/res/css/views/rooms/_TopUnreadMessagesBar.pcss index 9aa97561d77..6c25ff8e749 100644 --- a/res/css/views/rooms/_TopUnreadMessagesBar.pcss +++ b/res/css/views/rooms/_TopUnreadMessagesBar.pcss @@ -31,7 +31,7 @@ limitations under the License. height: 4px; border-radius: 16px; background-color: var(--cpd-color-bg-canvas-default); - border: 6px solid var(--cpd-color-icon-primary); + border: 6px solid var(--cpd-color-icon-accent-tertiary); pointer-events: none; } @@ -40,7 +40,7 @@ limitations under the License. border-radius: 19px; box-sizing: border-box; background: $background; - border: 1.3px solid $muted-fg-color; + border: 1.3px solid var(--cpd-color-icon-tertiary); cursor: pointer; } @@ -53,7 +53,7 @@ limitations under the License. mask-repeat: no-repeat; mask-size: 20px; mask-position: center; - background: $muted-fg-color; + background: var(--cpd-color-icon-tertiary); } .mx_TopUnreadMessagesBar_markAsRead { @@ -61,7 +61,7 @@ limitations under the License. width: 18px; height: 18px; background: $background; - border: 1.3px solid $muted-fg-color; + border: 1.3px solid var(--cpd-color-icon-tertiary); border-radius: 10px; margin: 5px auto; } @@ -75,5 +75,5 @@ limitations under the License. mask-repeat: no-repeat; mask-size: 10px; mask-position: 4px 4px; - background: $muted-fg-color; + background: var(--cpd-color-icon-tertiary); } diff --git a/res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss b/res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss index d4194052fec..595a67b1250 100644 --- a/res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss +++ b/res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss @@ -115,7 +115,7 @@ limitations under the License. max-width: 100%; overflow: hidden; - color: $accent-fg-color; + color: var(--cpd-color-text-on-solid-primary); background-color: $pill-bg-color; /* ...with the overrides from _BasicMessageComposer.pcss */ diff --git a/res/css/views/settings/_FontScalingPanel.pcss b/res/css/views/settings/_FontScalingPanel.pcss index 3c013214873..974ca0fa0dd 100644 --- a/res/css/views/settings/_FontScalingPanel.pcss +++ b/res/css/views/settings/_FontScalingPanel.pcss @@ -36,26 +36,8 @@ limitations under the License. } } -.mx_FontScalingPanel_fontSlider { - display: flex; - align-items: center; - padding: 15px $spacing-20 35px; - background: $panels; - border-radius: 10px; - font-size: $font-10px; - - .mx_FontScalingPanel_fontSlider_smallText, - .mx_FontScalingPanel_fontSlider_largeText { - font-weight: 500; - } - - .mx_FontScalingPanel_fontSlider_smallText { - font-size: $font-15px; - padding-inline-end: $spacing-20; - } - - .mx_FontScalingPanel_fontSlider_largeText { - font-size: $font-18px; - padding-inline-start: $spacing-20; - } +.mx_FontScalingPanel_Dropdown { + width: 120px; + /* Override default mx_Field margin */ + margin-bottom: var(--cpd-space-2x) !important; } diff --git a/res/css/views/settings/_IntegrationManager.pcss b/res/css/views/settings/_IntegrationManager.pcss index 505ccf86c24..0576a072464 100644 --- a/res/css/views/settings/_IntegrationManager.pcss +++ b/res/css/views/settings/_IntegrationManager.pcss @@ -15,16 +15,19 @@ limitations under the License. */ .mx_IntegrationManager { - .mx_Dialog { + .mx_Dialog_border { box-sizing: border-box; - padding: 0; width: 60%; height: 70%; - overflow: hidden; max-width: initial; max-height: initial; } + .mx_Dialog { + padding: 0; + overflow: hidden; + } + iframe { background-color: #fff; border: 0; diff --git a/res/css/views/settings/_LayoutSwitcher.pcss b/res/css/views/settings/_LayoutSwitcher.pcss index 6ae91c02863..571b9a1cf1c 100644 --- a/res/css/views/settings/_LayoutSwitcher.pcss +++ b/res/css/views/settings/_LayoutSwitcher.pcss @@ -63,7 +63,7 @@ limitations under the License. } &.mx_LayoutSwitcher_RadioButton_selected { - border-color: var(--cpd-color-bg-action-primary-rest); + border-color: var(--cpd-color-bg-accent-rest); } } diff --git a/res/css/views/settings/_PowerLevelSelector.pcss b/res/css/views/settings/_PowerLevelSelector.pcss new file mode 100644 index 00000000000..50745d1cd89 --- /dev/null +++ b/res/css/views/settings/_PowerLevelSelector.pcss @@ -0,0 +1,21 @@ +/* + * + * Copyright 2024 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * / + */ + +.mx_PowerLevelSelector_Button { + align-self: flex-start; +} diff --git a/res/css/views/settings/_ProfileSettings.pcss b/res/css/views/settings/_ProfileSettings.pcss index 14409f55a22..5caff1f2c01 100644 --- a/res/css/views/settings/_ProfileSettings.pcss +++ b/res/css/views/settings/_ProfileSettings.pcss @@ -52,12 +52,9 @@ limitations under the License. } .mx_ProfileSettings_buttons { + display: flex; + gap: var(--cpd-space-4x); margin-top: 10px; /* 18px is already accounted for by the

above the buttons */ margin-bottom: $spacing-28; - - > .mx_AccessibleButton_kind_link { - font: var(--cpd-font-body-md-semibold); - margin-inline-end: 10px; - } } } diff --git a/res/css/views/voip/_DialPadContextMenu.pcss b/res/css/views/voip/_DialPadContextMenu.pcss index e6e5d227b9c..322fb633e55 100644 --- a/res/css/views/voip/_DialPadContextMenu.pcss +++ b/res/css/views/voip/_DialPadContextMenu.pcss @@ -37,8 +37,6 @@ limitations under the License. .mx_DialPadContextMenu_cancel { @mixin customisedCancelButton; float: right; - width: 14px; - height: 14px; } .mx_DialPadContextMenu_header:focus-within { diff --git a/res/css/views/voip/_DialPadModal.pcss b/res/css/views/voip/_DialPadModal.pcss index 949a9a2a2e0..93071c8c7c0 100644 --- a/res/css/views/voip/_DialPadModal.pcss +++ b/res/css/views/voip/_DialPadModal.pcss @@ -47,8 +47,6 @@ limitations under the License. .mx_DialPadModal_cancel { @mixin customisedCancelButton; float: right; - width: 14px; - height: 14px; margin-right: 16px; } diff --git a/res/img/element-icons/check-all.svg b/res/img/element-icons/check-all.svg new file mode 100644 index 00000000000..d81382504d3 --- /dev/null +++ b/res/img/element-icons/check-all.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/res/img/element-icons/roomlist/mark-as-unread.svg b/res/img/element-icons/roomlist/mark-as-unread.svg new file mode 100644 index 00000000000..a3ea89e3e93 --- /dev/null +++ b/res/img/element-icons/roomlist/mark-as-unread.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/themes/dark/css/_dark.pcss b/res/themes/dark/css/_dark.pcss index 93ebfa7cc2c..326debc0628 100644 --- a/res/themes/dark/css/_dark.pcss +++ b/res/themes/dark/css/_dark.pcss @@ -22,7 +22,7 @@ $separator: var(--cpd-color-alpha-gray-400); /* ******************** */ $roomlist-bg-color: rgba(38, 40, 45, 0.9); $roomsublist-skeleton-ui-bg: linear-gradient(180deg, $background 0%, #ffffff00 100%); -$roomtile-default-badge-bg-color: $muted-fg-color; +$roomtile-default-badge-bg-color: var(--cpd-color-icon-secondary); /* ******************** */ /** @@ -125,8 +125,9 @@ $roomheader-addroom-fg-color: $primary-content; /* Rich-text-editor */ /* ******************** */ -$pill-bg-color: $room-highlight-color; -$pill-hover-bg-color: #545a66; +$pill-bg-color: var(--cpd-color-bg-action-primary-rest); +$pill-hover-bg-color: var(--cpd-color-bg-action-primary-hovered); +$pill-press-bg-color: var(--cpd-color-bg-action-primary-pressed); /* ******************** */ /* Inputs */ @@ -141,7 +142,7 @@ $input-placeholder: var(--cpd-color-text-placeholder); /* Dialog */ /* ******************** */ $dialog-title-fg-color: $primary-content; -$dialog-backdrop-color: $menu-border-color; +$dialog-backdrop-color: #00000080; $dialog-close-fg-color: $icon-button-color; $dialog-close-external-color: $primary-content; /* ******************** */ @@ -150,12 +151,12 @@ $dialog-close-external-color: $primary-content; /* ******************** */ $system: var(--cpd-color-bg-subtle-secondary); $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%); -$roomtile-default-badge-bg-color: $input-darker-fg-color; +$roomtile-default-badge-bg-color: var(--cpd-color-icon-secondary); /* ******************** */ /* Tabbed views */ /* ******************** */ -$tab-label-fg-color: $primary-content; +$tab-label-fg-color: $secondary-content; $tab-label-active-fg-color: $primary-content; /* ******************** */ @@ -199,10 +200,9 @@ $voice-record-icon-color: $quaternary-content; /* Bubble tiles */ /* ******************** */ -$eventbubble-self-bg: #133a34; -$eventbubble-others-bg: #21262c; -$eventbubble-bg-hover: #1c2026; -$eventbubble-reply-color: #c1c6cd; +$eventbubble-self-bg: var(--cpd-color-green-300); +$eventbubble-others-bg: var(--cpd-color-gray-300); +$eventbubble-bg-hover: var(--cpd-color-bg-subtle-secondary); /* ******************** */ /* Lightbox */ @@ -251,8 +251,6 @@ $progressbar-bg-color: var(--cpd-color-gray-200); $kbd-border-color: $strong-input-border-color; $visual-bell-bg-color: #800; $event-timestamp-color: $text-secondary-color; -$slider-background-color: $quinary-content; -$appearance-tab-border-color: $room-highlight-color; $composer-shadow-color: rgba(0, 0, 0, 0.28); $breadcrumb-placeholder-bg-color: #272c35; $theme-button-bg-color: #e3e8f0; diff --git a/res/themes/legacy-dark/css/_legacy-dark.pcss b/res/themes/legacy-dark/css/_legacy-dark.pcss index d9db8465831..7e14e85f10a 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.pcss +++ b/res/themes/legacy-dark/css/_legacy-dark.pcss @@ -28,8 +28,9 @@ $light-fg-color: $header-panel-text-secondary-color; /* used for focusing form controls */ $focus-bg-color: $room-highlight-color; -$pill-bg-color: $room-highlight-color; -$pill-hover-bg-color: #545a66; +$pill-bg-color: var(--cpd-color-bg-action-primary-rest); +$pill-hover-bg-color: var(--cpd-color-bg-action-primary-hovered); +$pill-press-bg-color: var(--cpd-color-bg-action-primary-pressed); /* informational plinth */ $info-plinth-bg-color: $header-panel-bg-color; @@ -72,8 +73,7 @@ $h3-color: $primary-fg-color; $icon-button-color: var(--cpd-color-icon-tertiary); $dialog-title-fg-color: $base-text-color; -$dialog-backdrop-color: #000; -$dialog-shadow-color: rgba(0, 0, 0, 0.48); +$dialog-backdrop-color: #00000080; $dialog-close-fg-color: $icon-button-color; $dialog-close-external-color: $text-primary-color; @@ -208,9 +208,6 @@ $breadcrumb-placeholder-bg-color: #272c35; $voice-record-stop-border-color: #6f7882; $voice-record-icon-color: #6f7882; -/* Appearance tab colors */ -$appearance-tab-border-color: $room-highlight-color; - $composer-shadow-color: tranparent; $codeblock-background-color: #2a3039; @@ -221,7 +218,6 @@ $inlinecode-background-color: #2a3039; $eventbubble-self-bg: #14322e; $eventbubble-others-bg: $event-selected-color; $eventbubble-bg-hover: #1c2026; -$eventbubble-reply-color: #c1c6cd; /* Location sharing */ /* ******************** */ diff --git a/res/themes/legacy-light/css/_legacy-light.pcss b/res/themes/legacy-light/css/_legacy-light.pcss index 290027e5841..5f9b8fd4520 100644 --- a/res/themes/legacy-light/css/_legacy-light.pcss +++ b/res/themes/legacy-light/css/_legacy-light.pcss @@ -97,7 +97,6 @@ $icon-button-color: var(--cpd-color-icon-tertiary); $dialog-title-fg-color: #45474a; $dialog-backdrop-color: rgba(46, 48, 51, 0.38); -$dialog-shadow-color: rgba(0, 0, 0, 0.48); $dialog-close-fg-color: $icon-button-color; $dialog-close-external-color: $primary-bg-color; @@ -124,8 +123,9 @@ $rte-code-bg-color: rgba(0, 0, 0, 0.04); $header-panel-text-primary-color: #91a1c0; -$pill-bg-color: #aaa; -$pill-hover-bg-color: #ccc; +$pill-bg-color: var(--cpd-color-bg-action-primary-rest); +$pill-hover-bg-color: var(--cpd-color-bg-action-primary-hovered); +$pill-press-bg-color: var(--cpd-color-bg-action-primary-pressed); $topleftmenu-color: #212121; $roomheader-bg-color: $primary-bg-color; @@ -269,9 +269,6 @@ $visual-bell-bg-color: #faa; $togglesw-off-color: #c1c9d6; $togglesw-ball-color: var(--cpd-color-bg-action-primary-rest); -/* Slider */ -$slider-background-color: #c1c9d6; - $progressbar-bg-color: rgba(141, 151, 165, 0.2); $authpage-bg-color: #2e3649; @@ -305,9 +302,6 @@ $voice-record-live-circle-color: #ff4b55; $voice-record-stop-border-color: #e3e8f0; $voice-record-icon-color: $tertiary-fg-color; -/* FontSlider colors */ -$appearance-tab-border-color: $input-darker-bg-color; - $composer-shadow-color: tranparent; $codeblock-background-color: $header-panel-bg-color; @@ -318,7 +312,6 @@ $inlinecode-background-color: $header-panel-bg-color; $eventbubble-self-bg: #f0fbf8; $eventbubble-others-bg: $system; $eventbubble-bg-hover: #fafbfd; -$eventbubble-reply-color: #c1c6cd; /* pinned events indicator */ $pinned-color: $tertiary-content; diff --git a/res/themes/light-custom/css/_custom.pcss b/res/themes/light-custom/css/_custom.pcss index ed345a28ec6..7fadb2cd0ac 100644 --- a/res/themes/light-custom/css/_custom.pcss +++ b/res/themes/light-custom/css/_custom.pcss @@ -68,12 +68,10 @@ $roomlist-bg-color: var(--roomlist-background-color); $message-action-bar-fg-color: var(--timeline-text-color); $primary-content: var(--timeline-text-color); $roomtopic-color: var(--timeline-text-color-50pct); -$tab-label-fg-color: var(--timeline-text-color); /* was #212121 */ $topleftmenu-color: var(--timeline-text-color); /* was #45474a */ $dialog-title-fg-color: var(--timeline-text-color); -$tab-label-fg-color: var(--timeline-text-color); /* was #4e5054 */ $authpage-lang-color: var(--timeline-text-color); /* was #232f32 */ @@ -118,7 +116,6 @@ $settings-grey-fg-color: $primary-content; $eventbubble-self-bg: var(--eventbubble-self-bg, $eventbubble-self-bg); $eventbubble-others-bg: var(--eventbubble-others-bg, $eventbubble-others-bg); $eventbubble-bg-hover: var(--eventbubble-bg-hover, $eventbubble-bg-hover); -$eventbubble-reply-color: var(--eventbubble-reply-color, $eventbubble-reply-color); $reaction-row-button-selected-bg-color: var( --reaction-row-button-selected-bg-color, @@ -128,6 +125,7 @@ $reaction-row-button-selected-bg-color: var( $menu-selected-color: var(--menu-selected-color, $menu-selected-color); $pill-bg-color: var(--other-user-pill-bg-color, $pill-bg-color); $pill-hover-bg-color: var(--other-user-pill-bg-color, $pill-hover-bg-color); +$pill-press-bg-color: var(--other-user-pill-bg-color, $pill-press-bg-color); $icon-button-color: var(--icon-button-color, $icon-button-color); $strong-input-border-color: var(--strong-input-border-color, $strong-input-border-color); 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 396b4946996..2ba97c5a722 100644 --- a/res/themes/light-high-contrast/css/_light-high-contrast.pcss +++ b/res/themes/light-high-contrast/css/_light-high-contrast.pcss @@ -31,8 +31,6 @@ $button-secondary-bg-color: $accent-fg-color; $message-action-bar-fg-color: $primary-content; $voice-record-stop-border-color: $quinary-content; $voice-record-icon-color: $tertiary-content; -$appearance-tab-border-color: $input-darker-bg-color; -$eventbubble-reply-color: $quaternary-content; $roomtopic-color: $secondary-content; /** @@ -104,10 +102,6 @@ $accent-1400: var(--cpd-color-green-1400); background-color: $panel-actions !important; } -.mx_FontScalingPanel_fontSlider { - background-color: $panel-actions !important; -} - .mx_ThemeChoicePanel_themeSelectors > .mx_StyledRadioButton input[type="radio"]:disabled + div { border-color: $primary-content; } diff --git a/res/themes/light/css/_light.pcss b/res/themes/light/css/_light.pcss index 12cd95ab856..730c1155143 100644 --- a/res/themes/light/css/_light.pcss +++ b/res/themes/light/css/_light.pcss @@ -159,8 +159,9 @@ $roomheader-addroom-fg-color: #5c6470; /* Rich-text-editor */ /* ******************** */ -$pill-bg-color: #aaa; -$pill-hover-bg-color: #ccc; +$pill-bg-color: var(--cpd-color-bg-action-primary-rest); +$pill-hover-bg-color: var(--cpd-color-bg-action-primary-hovered); +$pill-press-bg-color: var(--cpd-color-bg-action-primary-pressed); $rte-bg-color: #e9e9e9; $rte-code-bg-color: rgba(0, 0, 0, 0.04); /* ******************** */ @@ -189,10 +190,9 @@ $input-placeholder: var(--cpd-color-text-placeholder); /* Dialog */ /* ******************** */ $dialog-title-fg-color: var(--cpd-color-text-primary); -$dialog-backdrop-color: rgba(46, 48, 51, 0.38); +$dialog-backdrop-color: #030c1b4d; $dialog-close-fg-color: $icon-button-color; $dialog-close-external-color: $background; -$dialog-shadow-color: rgba(0, 0, 0, 0.48); /* ******************** */ /* ImageBody */ @@ -206,21 +206,21 @@ $imagebody-giflabel-color: $accent-fg-color; /* ******************** */ $roomlist-bg-color: rgba(245, 245, 245, 0.9); $roomsublist-skeleton-ui-bg: linear-gradient(180deg, $background 0%, #ffffff00 100%); -$roomtile-default-badge-bg-color: $muted-fg-color; +$roomtile-default-badge-bg-color: var(--cpd-color-icon-secondary); /* ******************** */ /* e2e */ /* ******************** */ -$e2e-verified-color: var(--cpd-color-green-900); -$e2e-warning-color: var(--cpd-color-red-900); +$e2e-verified-color: var(--cpd-color-icon-success-primary); +$e2e-warning-color: var(--cpd-color-icon-critical-primary); $e2e-verified-color-light: var(--cpd-color-green-300); $e2e-warning-color-light: var(--cpd-color-red-300); /* ******************** */ /* Tabbed views */ /* ******************** */ -$tab-label-fg-color: $dialog-title-fg-color; -$tab-label-active-fg-color: $background; +$tab-label-fg-color: $secondary-content; +$tab-label-active-fg-color: $primary-content; /* ******************** */ /* Buttons */ @@ -275,10 +275,9 @@ $voice-record-icon-color: $tertiary-content; /* Bubble tiles */ /* ******************** */ -$eventbubble-self-bg: #e7f8f3; -$eventbubble-others-bg: #e8edf4; -$eventbubble-bg-hover: #f6f7f8; -$eventbubble-reply-color: $quaternary-content; +$eventbubble-self-bg: var(--cpd-color-green-300); +$eventbubble-others-bg: var(--cpd-color-gray-300); +$eventbubble-bg-hover: var(--cpd-color-bg-subtle-secondary); /* ******************** */ /* Lightbox */ @@ -316,8 +315,6 @@ $progressbar-bg-color: var(--cpd-color-gray-200); $kbd-border-color: $strong-input-border-color; $visual-bell-bg-color: #faa; $event-timestamp-color: #acacac; -$slider-background-color: $togglesw-off-color; -$appearance-tab-border-color: $input-darker-bg-color; $composer-shadow-color: rgba(0, 0, 0, 0.04); $breadcrumb-placeholder-bg-color: #e8eef5; $theme-button-bg-color: $quinary-content; diff --git a/src/@types/common.ts b/src/@types/common.ts index 4141418ac4e..50e02e6a8f4 100644 --- a/src/@types/common.ts +++ b/src/@types/common.ts @@ -16,12 +16,7 @@ limitations under the License. import { JSXElementConstructor } from "react"; -export type { NonEmptyArray } from "matrix-js-sdk/src/matrix"; - -// Based on https://stackoverflow.com/a/53229857/3532235 -export type Without = { [P in Exclude]?: never }; -export type XOR = T | U extends object ? (Without & U) | (Without & T) : T | U; -export type Writeable = { -readonly [P in keyof T]: T[P] }; +export type { NonEmptyArray, XOR, Writeable } from "matrix-js-sdk/src/matrix"; export type ComponentClass = keyof JSX.IntrinsicElements | JSXElementConstructor; diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index b1c8a4ec144..c62733c0f07 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -151,16 +151,10 @@ declare global { interface HTMLAudioElement { type?: string; - // sinkId & setSinkId are experimental and typescript doesn't know about them - sinkId: string; - setSinkId(outputId: string): void; } interface HTMLVideoElement { type?: string; - // sinkId & setSinkId are experimental and typescript doesn't know about them - sinkId: string; - setSinkId(outputId: string): void; } // Add Chrome-specific `instant` ScrollBehaviour diff --git a/src/@types/matrix-js-sdk.d.ts b/src/@types/matrix-js-sdk.d.ts new file mode 100644 index 00000000000..a58eea55bc9 --- /dev/null +++ b/src/@types/matrix-js-sdk.d.ts @@ -0,0 +1,85 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import type { IWidget } from "matrix-widget-api"; +import type { BLURHASH_FIELD } from "../utils/image-media"; +import type { JitsiCallMemberEventType, JitsiCallMemberContent } from "../call-types"; +import type { ILayoutStateEvent, WIDGET_LAYOUT_EVENT_TYPE } from "../stores/widgets/types"; +import type { VoiceBroadcastInfoEventContent, VoiceBroadcastInfoEventType } from "../voice-broadcast/types"; +import type { EncryptedFile } from "matrix-js-sdk/src/types"; + +// Extend Matrix JS SDK types via Typescript declaration merging to support unspecced event fields and types +declare module "matrix-js-sdk/src/types" { + export interface FileInfo { + /** + * @see https://github.com/matrix-org/matrix-spec-proposals/pull/2448 + */ + [BLURHASH_FIELD]?: string; + } + + export interface StateEvents { + // Jitsi-backed video room state events + [JitsiCallMemberEventType]: JitsiCallMemberContent; + + // Unstable widgets state events + "im.vector.modular.widgets": IWidget | {}; + [WIDGET_LAYOUT_EVENT_TYPE]: ILayoutStateEvent; + + // Unstable voice broadcast state events + [VoiceBroadcastInfoEventType]: VoiceBroadcastInfoEventContent; + + // Element custom state events + "im.vector.web.settings": Record; + "org.matrix.room.preview_urls": { disable: boolean }; + + // XXX unspecced usages of `m.room.*` events + "m.room.plumbing": { + status: string; + }; + "m.room.bot.options": unknown; + } + + export interface TimelineEvents { + "io.element.performance_metric": { + "io.element.performance_metrics": { + forEventId: string; + responseTs: number; + kind: "send_time"; + }; + }; + } + + export interface AudioContent { + // MSC1767 + Ideals of MSC2516 as MSC3245 + // https://github.com/matrix-org/matrix-doc/pull/3245 + "org.matrix.msc1767.text"?: string; + "org.matrix.msc1767.file"?: { + url?: string; + file?: EncryptedFile; + name: string; + mimetype: string; + size: number; + }; + "org.matrix.msc1767.audio"?: { + duration: number; + // https://github.com/matrix-org/matrix-doc/pull/3246 + waveform?: number[]; + }; + "org.matrix.msc3245.voice"?: {}; + + "io.element.voice_broadcast_chunk"?: { sequence: number }; + } +} diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts index b343fcab495..7150336e450 100644 --- a/src/BasePlatform.ts +++ b/src/BasePlatform.ts @@ -313,7 +313,11 @@ export default abstract class BasePlatform { return null; } - protected getSSOCallbackUrl(fragmentAfterLogin = ""): URL { + /** + * The URL to return to after a successful SSO/OIDC authentication + * @param fragmentAfterLogin optional fragment for specific view to return to + */ + public getSSOCallbackUrl(fragmentAfterLogin = ""): URL { const url = new URL(window.location.href); url.hash = fragmentAfterLogin; return url; @@ -478,4 +482,12 @@ export default abstract class BasePlatform { policyUri: config.privacy_policy_url, }; } + + /** + * Suffix to append to the `state` parameter of OIDC /auth calls. Will be round-tripped to the callback URI. + * Currently only required for ElectronPlatform for passing element-desktop-ssoid. + */ + public getOidcClientState(): string { + return ""; + } } diff --git a/src/ContentMessages.ts b/src/ContentMessages.ts index 9a8a1d7c4fb..79990956812 100644 --- a/src/ContentMessages.ts +++ b/src/ContentMessages.ts @@ -19,7 +19,6 @@ limitations under the License. import { MatrixClient, MsgType, - IImageInfo, HTTPError, IEventRelation, ISendEventResponse, @@ -28,19 +27,19 @@ import { UploadProgress, THREAD_RELATION_TYPE, } from "matrix-js-sdk/src/matrix"; +import { + ImageInfo, + AudioInfo, + VideoInfo, + EncryptedFile, + MediaEventContent, + MediaEventInfo, +} from "matrix-js-sdk/src/types"; import encrypt from "matrix-encrypt-attachment"; import extractPngChunks from "png-chunks-extract"; import { logger } from "matrix-js-sdk/src/logger"; import { removeElement } from "matrix-js-sdk/src/utils"; -import { - AudioInfo, - EncryptedFile, - ImageInfo, - IMediaEventContent, - IMediaEventInfo, - VideoInfo, -} from "./customisations/models/IMediaEventContent"; import dis from "./dispatcher/dispatcher"; import { _t } from "./languageHandler"; import Modal from "./Modal"; @@ -390,7 +389,7 @@ export default class ContentMessages { url: string, roomId: string, threadId: string | null, - info: IImageInfo, + info: ImageInfo, text: string, matrixClient: MatrixClient, ): Promise { @@ -537,7 +536,7 @@ export default class ContentMessages { promBefore?: Promise, ): Promise { const fileName = file.name || _t("common|attachment"); - const content: Omit & { info: Partial } = { + const content: Omit & { info: Partial } = { body: fileName, info: { size: file.size, @@ -623,7 +622,7 @@ export default class ContentMessages { if (upload.cancelled) throw new UploadCanceledError(); const threadId = relation?.rel_type === THREAD_RELATION_TYPE.name ? relation.event_id : null; - const response = await matrixClient.sendMessage(roomId, threadId ?? null, content); + const response = await matrixClient.sendMessage(roomId, threadId ?? null, content as MediaEventContent); if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) { sendRoundTripMetric(matrixClient, roomId, response.event_id); diff --git a/src/DecryptionFailureTracker.ts b/src/DecryptionFailureTracker.ts index 96526c034a9..f9afec0daa3 100644 --- a/src/DecryptionFailureTracker.ts +++ b/src/DecryptionFailureTracker.ts @@ -93,11 +93,11 @@ export class DecryptionFailureTracker { public static TRACK_INTERVAL_MS = 60000; // Call `checkFailures` every `CHECK_INTERVAL_MS`. - public static CHECK_INTERVAL_MS = 5000; + public static CHECK_INTERVAL_MS = 40000; // Give events a chance to be decrypted by waiting `GRACE_PERIOD_MS` before counting // the failure in `failureCounts`. - public static GRACE_PERIOD_MS = 4000; + public static GRACE_PERIOD_MS = 30000; /** * Create a new DecryptionFailureTracker. diff --git a/src/IConfigOptions.ts b/src/IConfigOptions.ts index 501d8a3bd64..4dc537aab07 100644 --- a/src/IConfigOptions.ts +++ b/src/IConfigOptions.ts @@ -119,6 +119,7 @@ export interface IConfigOptions { }; element_call: { url?: string; + guest_spa_url?: string; use_exclusively?: boolean; participant_limit?: number; brand?: string; diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 1d6577ca399..3e87541ddd6 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -19,11 +19,9 @@ limitations under the License. import { ReactNode } from "react"; import { createClient, MatrixClient, SSOAction, OidcTokenRefresher } from "matrix-js-sdk/src/matrix"; -import { InvalidStoreError } from "matrix-js-sdk/src/errors"; import { IEncryptedPayload } from "matrix-js-sdk/src/crypto/aes"; import { QueryDict } from "matrix-js-sdk/src/utils"; import { logger } from "matrix-js-sdk/src/logger"; -import { MINIMUM_MATRIX_VERSION, SUPPORTED_MATRIX_VERSIONS } from "matrix-js-sdk/src/version-support"; import { IMatrixClientCreds, MatrixClientPeg } from "./MatrixClientPeg"; import SecurityCustomisations from "./customisations/Security"; @@ -53,8 +51,6 @@ import LegacyCallHandler from "./LegacyCallHandler"; import LifecycleCustomisations from "./customisations/Lifecycle"; import ErrorDialog from "./components/views/dialogs/ErrorDialog"; import { _t } from "./languageHandler"; -import LazyLoadingResyncDialog from "./components/views/dialogs/LazyLoadingResyncDialog"; -import LazyLoadingDisabledDialog from "./components/views/dialogs/LazyLoadingDisabledDialog"; import SessionRestoreErrorDialog from "./components/views/dialogs/SessionRestoreErrorDialog"; import StorageEvictedDialog from "./components/views/dialogs/StorageEvictedDialog"; import { setSentryUser } from "./sentry"; @@ -74,7 +70,6 @@ import { getStoredOidcTokenIssuer, persistOidcAuthenticatedSettings, } from "./utils/oidc/persistOidcSettings"; -import GenericToast from "./components/views/toasts/GenericToast"; import { ACCESS_TOKEN_IV, ACCESS_TOKEN_STORAGE_KEY, @@ -97,8 +92,20 @@ dis.register((payload) => { onLoggedOut(); } else if (payload.action === Action.OverwriteLogin) { const typed = payload; - // noinspection JSIgnoredPromiseFromCall - we don't care if it fails - doSetLoggedIn(typed.credentials, true); + // Stop the current client before overwriting the login. + // If not done it might be impossible to clear the storage, as the + // rust crypto backend might be holding an open connection to the indexeddb store. + // We also use the `unsetClient` flag to false, because at this point we are + // already in the logged in flows of the `MatrixChat` component, and it will + // always expect to have a client (calls to `MatrixClientPeg.safeGet()`). + // If we unset the client and the component is updated, the render will fail and unmount everything. + // (The module dialog closes and fires a `aria_unhide_main_app` that will trigger a re-render) + stopMatrixClient(false); + doSetLoggedIn(typed.credentials, true).catch((e) => { + // XXX we might want to fire a new event here to let the app know that the login failed ? + // The module api could use it to display a message to the user. + logger.warn("Failed to overwrite login", e); + }); } }); @@ -429,39 +436,6 @@ async function onFailedDelegatedAuthLogin(description: string | ReactNode, tryAg }); } -export function handleInvalidStoreError(e: InvalidStoreError): Promise | void { - if (e.reason === InvalidStoreError.TOGGLED_LAZY_LOADING) { - return Promise.resolve() - .then(() => { - const lazyLoadEnabled = e.value; - if (lazyLoadEnabled) { - return new Promise((resolve) => { - Modal.createDialog(LazyLoadingResyncDialog, { - onFinished: resolve, - }); - }); - } else { - // show warning about simultaneous use - // between LL/non-LL version on same host. - // as disabling LL when previously enabled - // is a strong indicator of this (/develop & /app) - return new Promise((resolve) => { - Modal.createDialog(LazyLoadingDisabledDialog, { - onFinished: resolve, - host: window.location.host, - }); - }); - } - }) - .then(() => { - return MatrixClientPeg.safeGet().store.deleteAllData(); - }) - .then(() => { - PlatformPeg.get()?.reload(); - }); - } -} - function registerAsGuest(hsUrl: string, isUrl?: string, defaultDeviceDisplayName?: string): Promise { logger.log(`Doing guest login on ${hsUrl}`); @@ -635,7 +609,6 @@ export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): }, false, ); - await checkServerVersions(); return true; } else { logger.log("No previous session found."); @@ -643,37 +616,6 @@ export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): } } -async function checkServerVersions(): Promise { - const client = MatrixClientPeg.get(); - if (!client) return; - for (const version of SUPPORTED_MATRIX_VERSIONS) { - // Check if the server supports this spec version. (`isVersionSupported` caches the response, so this loop will - // only make a single HTTP request). - if (await client.isVersionSupported(version)) { - // we found a compatible spec version - return; - } - } - - const toastKey = "LEGACY_SERVER"; - ToastStore.sharedInstance().addOrReplaceToast({ - key: toastKey, - title: _t("unsupported_server_title"), - props: { - description: _t("unsupported_server_description", { - version: MINIMUM_MATRIX_VERSION, - brand: SdkConfig.get().brand, - }), - acceptLabel: _t("action|ok"), - onAccept: () => { - ToastStore.sharedInstance().dismissToast(toastKey); - }, - }, - component: GenericToast, - priority: 98, - }); -} - async function handleLoadSessionFailure(e: unknown): Promise { logger.error("Unable to load session", e); @@ -777,13 +719,13 @@ async function createOidcTokenRefresher(credentials: IMatrixClientCreds): Promis try { const clientId = getStoredOidcClientId(); const idTokenClaims = getStoredOidcIdTokenClaims(); - const redirectUri = window.location.origin; + const redirectUri = PlatformPeg.get()!.getSSOCallbackUrl().href; const deviceId = credentials.deviceId; if (!deviceId) { throw new Error("Expected deviceId in user credentials."); } const tokenRefresher = new TokenRefresher( - { issuer: tokenIssuer }, + tokenIssuer, clientId, redirectUri, deviceId, diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 47585173904..a42903f46f3 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -29,9 +29,8 @@ import { RoomNameType, TokenRefreshFunction, } from "matrix-js-sdk/src/matrix"; +import { VerificationMethod } from "matrix-js-sdk/src/types"; import * as utils from "matrix-js-sdk/src/utils"; -import { verificationMethods } from "matrix-js-sdk/src/crypto"; -import { SHOW_QR_CODE_METHOD } from "matrix-js-sdk/src/crypto/verification/QRCode"; import { logger } from "matrix-js-sdk/src/logger"; import createMatrixClient from "./utils/createMatrixClient"; @@ -433,9 +432,9 @@ class MatrixClientPegClass implements IMatrixClientPeg { // the call arrives. iceCandidatePoolSize: 20, verificationMethods: [ - verificationMethods.SAS, - SHOW_QR_CODE_METHOD, - verificationMethods.RECIPROCATE_QR_CODE, + VerificationMethod.Sas, + VerificationMethod.ShowQrCode, + VerificationMethod.Reciprocate, ], identityServer: new IdentityAuthClient(), // These are always installed regardless of the labs flag so that cross-signing features diff --git a/src/Modal.tsx b/src/Modal.tsx index f2835799fd3..aa4ba691dc3 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -20,7 +20,7 @@ import ReactDOM from "react-dom"; import classNames from "classnames"; import { defer, sleep } from "matrix-js-sdk/src/utils"; import { TypedEventEmitter } from "matrix-js-sdk/src/matrix"; -import { TooltipProvider } from "@vector-im/compound-web"; +import { Glass, TooltipProvider } from "@vector-im/compound-web"; import dis from "./dispatcher/dispatcher"; import AsyncWrapper from "./AsyncWrapper"; @@ -376,7 +376,9 @@ export class ModalManager extends TypedEventEmitter

-
{this.staticModal.elem}
+ +
{this.staticModal.elem}
+
-
{modal.elem}
+ +
{modal.elem}
+
0) { + const markedUnreadState = getMarkedUnreadState(room); + if (greyNotifs > 0 || markedUnreadState) { return { symbol: null, count: trueCount, level: NotificationLevel.Notification }; } diff --git a/src/ScalarMessaging.ts b/src/ScalarMessaging.ts index fb2801c9b64..0ee6921a24d 100644 --- a/src/ScalarMessaging.ts +++ b/src/ScalarMessaging.ts @@ -291,7 +291,8 @@ Response: */ -import { IContent, MatrixEvent, IEvent } from "matrix-js-sdk/src/matrix"; +import { IContent, MatrixEvent, IEvent, StateEvents } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { logger } from "matrix-js-sdk/src/logger"; import { MatrixClientPeg } from "./MatrixClientPeg"; @@ -357,7 +358,10 @@ function inviteUser(event: MessageEvent, roomId: string, userId: string): v if (room) { // if they are already invited or joined we can resolve immediately. const member = room.getMember(userId); - if (member && ["join", "invite"].includes(member.membership!)) { + if ( + member && + ([KnownMembership.Join, KnownMembership.Invite] as Array).includes(member.membership) + ) { sendResponse(event, { success: true, }); @@ -608,15 +612,7 @@ async function setBotPower( }); } } - await client.setPowerLevel( - roomId, - userId, - level, - new MatrixEvent({ - type: "m.room.power_levels", - content: powerLevels, - }), - ); + await client.setPowerLevel(roomId, userId, level); return sendResponse(event, { success: true, }); @@ -669,7 +665,7 @@ function canSendEvent(event: MessageEvent, roomId: string): void { sendError(event, _t("scalar|error_room_unknown")); return; } - if (room.getMyMembership() !== "join") { + if (room.getMyMembership() !== KnownMembership.Join) { sendError(event, _t("scalar|error_membership")); return; } @@ -721,7 +717,7 @@ async function getOpenIdToken(event: MessageEvent): Promise { async function sendEvent( event: MessageEvent<{ - type: string; + type: keyof StateEvents; state_key?: string; content?: IContent; }>, diff --git a/src/Searching.ts b/src/Searching.ts index 691600d5913..27348ebc591 100644 --- a/src/Searching.ts +++ b/src/Searching.ts @@ -667,6 +667,10 @@ export function searchPagination(client: MatrixClient, searchResult: ISearchResu else return eventIndexSearchPagination(client, searchResult); } +// ROSBERG START +/* this function, originally used in src\components\structures\RoomView.tsx:1736, +is replaced with our own custom search src\VerjiLocalSearch.ts, and will be commented out here. */ +/* eslint-disable */ export default function eventSearch( client: MatrixClient, term: string, @@ -681,3 +685,5 @@ export default function eventSearch( return eventIndexSearch(client, term, roomId, abortSignal); } } +/* eslint-enable */ +// ROSBERG END diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index ff8946614fd..bef9589ce41 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -14,8 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { DeviceVerificationStatus, ICryptoCallbacks, MatrixClient, encodeBase64 } from "matrix-js-sdk/src/matrix"; -import { ISecretStorageKeyInfo } from "matrix-js-sdk/src/crypto/api"; +import { + DeviceVerificationStatus, + ICryptoCallbacks, + MatrixClient, + encodeBase64, + SecretStorage, +} from "matrix-js-sdk/src/matrix"; import { deriveKey } from "matrix-js-sdk/src/crypto/key_passphrase"; import { decodeRecoveryKey } from "matrix-js-sdk/src/crypto/recoverykey"; import { logger } from "matrix-js-sdk/src/logger"; @@ -38,14 +43,14 @@ import InteractiveAuthDialog from "./components/views/dialogs/InteractiveAuthDia // single secret storage operation, as it will clear the cached keys once the // operation ends. let secretStorageKeys: Record = {}; -let secretStorageKeyInfo: Record = {}; +let secretStorageKeyInfo: Record = {}; let secretStorageBeingAccessed = false; let nonInteractive = false; let dehydrationCache: { key?: Uint8Array; - keyInfo?: ISecretStorageKeyInfo; + keyInfo?: SecretStorage.SecretStorageKeyDescription; } = {}; function isCachingAllowed(): boolean { @@ -80,7 +85,9 @@ async function confirmToDismiss(): Promise { return !sure; } -function makeInputToKey(keyInfo: ISecretStorageKeyInfo): (keyParams: KeyParams) => Promise { +function makeInputToKey( + keyInfo: SecretStorage.SecretStorageKeyDescription, +): (keyParams: KeyParams) => Promise { return async ({ passphrase, recoveryKey }): Promise => { if (passphrase) { return deriveKey(passphrase, keyInfo.passphrase.salt, keyInfo.passphrase.iterations); @@ -94,11 +101,11 @@ function makeInputToKey(keyInfo: ISecretStorageKeyInfo): (keyParams: KeyParams) async function getSecretStorageKey({ keys: keyInfos, }: { - keys: Record; + keys: Record; }): Promise<[string, Uint8Array]> { const cli = MatrixClientPeg.safeGet(); let keyId = await cli.getDefaultSecretStorageKeyId(); - let keyInfo!: ISecretStorageKeyInfo; + let keyInfo!: SecretStorage.SecretStorageKeyDescription; if (keyId) { // use the default SSSS key if set keyInfo = keyInfos[keyId]; @@ -177,7 +184,7 @@ async function getSecretStorageKey({ } export async function getDehydrationKey( - keyInfo: ISecretStorageKeyInfo, + keyInfo: SecretStorage.SecretStorageKeyDescription, checkFunc: (data: Uint8Array) => void, ): Promise { const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.(); @@ -226,7 +233,11 @@ export async function getDehydrationKey( return key; } -function cacheSecretStorageKey(keyId: string, keyInfo: ISecretStorageKeyInfo, key: Uint8Array): void { +function cacheSecretStorageKey( + keyId: string, + keyInfo: SecretStorage.SecretStorageKeyDescription, + key: Uint8Array, +): void { if (isCachingAllowed()) { secretStorageKeys[keyId] = key; secretStorageKeyInfo[keyId] = keyInfo; diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index f309ca4bc54..682145bd38c 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -18,8 +18,9 @@ limitations under the License. */ import * as React from "react"; -import { User, IContent, Direction, ContentHelpers, MRoomTopicEventContent } from "matrix-js-sdk/src/matrix"; +import { ContentHelpers, Direction, EventType, IContent, MRoomTopicEventContent, User } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; +import { KnownMembership, RoomMemberEventContent } from "matrix-js-sdk/src/types"; import dis from "./dispatcher/dispatcher"; import { _t, _td, UserFriendlyError } from "./languageHandler"; @@ -239,12 +240,12 @@ export const Commands = [ isEnabled: (cli) => !isCurrentLocalRoom(cli), runFn: function (cli, roomId, threadId, args) { if (args) { - const ev = cli.getRoom(roomId)?.currentState.getStateEvents("m.room.member", cli.getSafeUserId()); - const content = { - ...(ev ? ev.getContent() : { membership: "join" }), + const ev = cli.getRoom(roomId)?.currentState.getStateEvents(EventType.RoomMember, cli.getSafeUserId()); + const content: RoomMemberEventContent = { + ...(ev ? ev.getContent() : { membership: KnownMembership.Join }), displayname: args, }; - return success(cli.sendStateEvent(roomId, "m.room.member", content, cli.getSafeUserId())); + return success(cli.sendStateEvent(roomId, EventType.RoomMember, content, cli.getSafeUserId())); } return reject(this.getUsage()); }, @@ -265,7 +266,7 @@ export const Commands = [ return success( promise.then((url) => { if (!url) return; - return cli.sendStateEvent(roomId, "m.room.avatar", { url }, ""); + return cli.sendStateEvent(roomId, EventType.RoomAvatar, { url }, ""); }), ); }, @@ -289,12 +290,12 @@ export const Commands = [ return success( promise.then((url) => { if (!url) return; - const ev = room?.currentState.getStateEvents("m.room.member", userId); - const content = { - ...(ev ? ev.getContent() : { membership: "join" }), + const ev = room?.currentState.getStateEvents(EventType.RoomMember, userId); + const content: RoomMemberEventContent = { + ...(ev ? ev.getContent() : { membership: KnownMembership.Join }), avatar_url: url, }; - return cli.sendStateEvent(roomId, "m.room.member", content, userId); + return cli.sendStateEvent(roomId, EventType.RoomMember, content, userId); }), ); }, diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx index 3389e7d8385..96eb38f01b1 100644 --- a/src/TextForEvent.tsx +++ b/src/TextForEvent.tsx @@ -26,6 +26,7 @@ import { M_POLL_START, M_POLL_END, } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { logger } from "matrix-js-sdk/src/logger"; import { removeDirectionOverrideChars } from "matrix-js-sdk/src/utils"; import { PollStartEvent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent"; @@ -122,7 +123,7 @@ function textForMemberEvent( const reason = content.reason; switch (content.membership) { - case "invite": { + case KnownMembership.Invite: { const threePidContent = content.third_party_invite; if (threePidContent) { if (threePidContent.display_name) { @@ -138,13 +139,13 @@ function textForMemberEvent( return () => _t("timeline|m.room.member|invite", { senderName, targetName }); } } - case "ban": + case KnownMembership.Ban: return () => reason ? _t("timeline|m.room.member|ban_reason", { senderName, targetName, reason }) : _t("timeline|m.room.member|ban", { senderName, targetName }); - case "join": - if (prevContent && prevContent.membership === "join") { + case KnownMembership.Join: + if (prevContent && prevContent.membership === KnownMembership.Join) { const modDisplayname = getModification(prevContent.displayname, content.displayname); const modAvatarUrl = getModification(prevContent.avatar_url, content.avatar_url); @@ -194,9 +195,9 @@ function textForMemberEvent( if (!ev.target) logger.warn("Join message has no target! -- " + ev.getContent().state_key); return () => _t("timeline|m.room.member|join", { targetName }); } - case "leave": + case KnownMembership.Leave: if (ev.getSender() === ev.getStateKey()) { - if (prevContent.membership === "invite") { + if (prevContent.membership === KnownMembership.Invite) { return () => _t("timeline|m.room.member|reject_invite", { targetName }); } else { return () => @@ -204,9 +205,9 @@ function textForMemberEvent( ? _t("timeline|m.room.member|left_reason", { targetName, reason }) : _t("timeline|m.room.member|left", { targetName }); } - } else if (prevContent.membership === "ban") { + } else if (prevContent.membership === KnownMembership.Ban) { return () => _t("timeline|m.room.member|unban", { senderName, targetName }); - } else if (prevContent.membership === "invite") { + } else if (prevContent.membership === KnownMembership.Invite) { return () => reason ? _t("timeline|m.room.member|withdrew_invite_reason", { @@ -215,7 +216,7 @@ function textForMemberEvent( reason, }) : _t("timeline|m.room.member|withdrew_invite", { senderName, targetName }); - } else if (prevContent.membership === "join") { + } else if (prevContent.membership === KnownMembership.Join) { return () => reason ? _t("timeline|m.room.member|kick_reason", { diff --git a/src/VerjiLocalSearch.ts b/src/VerjiLocalSearch.ts new file mode 100644 index 00000000000..f40bebd4708 --- /dev/null +++ b/src/VerjiLocalSearch.ts @@ -0,0 +1,403 @@ +/* + Copyright 2024 Verji Tech AS. All rights reserved. + Unauthorized copying or distribution of this file, via any medium, is strictly prohibited. +*/ + +import { + EventTimeline, + MatrixClient, + MatrixEvent, + Room, + RoomMember, + SearchResult as ElementSearchResult, +} from "matrix-js-sdk/src/matrix"; +import { EventContext } from "matrix-js-sdk/src/models/event-context"; // eslint-disable-line + +import { MatrixClientPeg } from "./MatrixClientPeg"; + +interface WordHighlight { + word: string; + highlight: boolean; +} + +export interface SearchTerm { + searchTypeAdvanced: boolean; + searchTypeNormal: boolean; + searchExpression?: RegExp | null; + regExpHighlightMap?: { [key: string]: boolean }; + fullText?: string; + words: WordHighlight[]; + regExpHighlights: any[]; + isEmptySearch?: boolean; +} + +interface Member { + userId: string; +} + +interface MemberObj { + [key: string]: Member; +} + +interface SearchResultItem { + result: MatrixEvent; + context: EventContext; +} + +interface SearchResult { + _query: string; + results: any[]; + highlights: any[]; + count: number; +} + +/** + * Searches all events locally based on the provided search term and room ID. + * + * @param {string} term - The search term. + * @param {string | undefined} roomId - The ID of the room to search in. + * @returns {Promise} A promise that resolves to the search result. + * @throws {Error} If the Matrix client is not initialized or the room is not found. + */ +export default async function searchAllEventsLocally(term: string, roomId: string | undefined): Promise { + const searchResult: SearchResult = { + _query: term, + results: [], + highlights: [], + count: 0, + }; + + const client: MatrixClient | null = MatrixClientPeg.get(); + if (!client) { + throw new Error("Matrix client is not initialized"); + } + + const room: Room | null = client.getRoom(roomId); + if (!room) { + throw new Error("Room not found"); + } + + const members = room.getLiveTimeline().getState(EventTimeline.FORWARDS)?.getMembers(); + const termObj: SearchTerm = makeSearchTermObject(term.trim()); + + if (termObj.isEmptySearch) { + return searchResult; + } + + let matchingMembers: Member[] = []; + if (members && Array(members).length) { + matchingMembers = members.filter((member: RoomMember) => isMemberMatch(member, termObj)); + } + + const memberObj: MemberObj = {}; + for (let i = 0; i < matchingMembers.length; i++) { + memberObj[matchingMembers[i].userId] = matchingMembers[i]; + } + + await loadFullHistory(client, room); + const matches = await findAllMatches(termObj, room, memberObj); + + processSearchResults(searchResult, matches, termObj); + + // console.log("Search results 1: ", searchResult); + + // const results = searchResult.results; + + // results.forEach(result => { + // result.context.timeline = result.context.timeline.reverse(); + // }); + + // console.log("Search results 2: ", searchResult); + + return searchResult; +} + +/** + * Loads the full history of events for a given room. + * + * @param client - The Matrix client instance. + * @param room - The room for which to load the history. + * @returns A promise that resolves when the full history is loaded. + * @throws {Error} If the Matrix client is not initialized. + */ +async function loadFullHistory(client: MatrixClient | null, room: Room): Promise { + let hasMoreEvents = true; + do { + try { + // get the first neighbour of the live timeline on every iteration + // as each time we paginate, two timelines could have overlapped and connected, and the new + // pagination token ends up on the first one. + const timeline: EventTimeline | null = getFirstLiveTimelineNeighbour(room); + if (!timeline) { + throw new Error("Timeline not found"); + } + if (client && timeline) { + hasMoreEvents = await client.paginateEventTimeline(timeline, { limit: 100, backwards: true }); + } else { + throw new Error("Matrix client is not initialized"); + } + } catch (err: any) { + // deal with rate-limit error + if (err.name === "M_LIMIT_EXCEEDED") { + const waitTime = err.data.retry_after_ms; + await new Promise((r) => setTimeout(r, waitTime)); + } else { + throw err; + } + } + } while (hasMoreEvents); +} + +/** + * Retrieves the first live timeline neighbour of a given room. + * A live timeline neighbour is a timeline that is adjacent to the current timeline in the backwards direction. + * + * @param room - The room for which to retrieve the first live timeline neighbour. + * @returns The first live timeline neighbour if found, otherwise null. + */ +function getFirstLiveTimelineNeighbour(room: Room): EventTimeline | null { + const liveTimeline = room.getLiveTimeline(); + let timeline = liveTimeline; + while (timeline) { + const neighbour = timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS); + if (!neighbour) { + return timeline; + } + timeline = neighbour; + } + return null; +} + +/** + * Iterates over all events in a room and invokes a callback function for each event. + * The iteration starts from the most recent event and goes backwards in time. + * + * @param room - The room to iterate over. + * @param callback - The callback function to invoke for each event. + */ +function iterateAllEvents(room: Room, callback: (event: MatrixEvent) => void): void { + let timeline: EventTimeline | null = room.getLiveTimeline(); + while (timeline) { + const events = timeline.getEvents(); + for (let i = events.length - 1; i >= 0; i--) { + callback(events[i]); + } + timeline = timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS); + } +} + +/** + * Finds all matches in a room based on the given search term object and matching members. + * + * @param termObj - The search term object. + * @param room - The room to search in. + * @param matchingMembers - The matching members. + * @returns A promise that resolves to an array of search result items. + */ +export async function findAllMatches(termObj: SearchTerm, room: Room, matchingMembers: MemberObj): Promise { + return new Promise((resolve) => { + const matches: SearchResultItem[] = []; + let searchHit: SearchResultItem | null = null; + let mostRecentEvent: MatrixEvent | null = null; + const iterationCallback = (roomEvent: MatrixEvent): void => { + if (searchHit !== null) { + searchHit.context.addEvents([roomEvent], false); + } + searchHit = null; + + if (roomEvent.getType() === "m.room.message" && !roomEvent.isRedacted()) { + if (eventMatchesSearchTerms(termObj, roomEvent, matchingMembers)) { + const evCtx = new EventContext(roomEvent); + if (mostRecentEvent !== null) { + evCtx.addEvents([mostRecentEvent], true); + } + + const resObj: SearchResultItem = { result: roomEvent, context: evCtx }; + + matches.push(resObj); + searchHit = resObj; + return; + } + } + mostRecentEvent = roomEvent; + }; + + iterateAllEvents(room, iterationCallback); + resolve(matches); + }); +} + +/** + * Checks if a room member matches the given search term. + * @param member - The room member to check. + * @param termObj - The search term object. + * @returns True if the member matches the search term, false otherwise. + */ +export function isMemberMatch(member: RoomMember, termObj: SearchTerm): boolean { + const memberName = member.name.toLowerCase(); + if (termObj.searchTypeAdvanced === true) { + const expResults = termObj.searchExpression && memberName.match(termObj.searchExpression); + if (expResults && expResults.length > 0) { + for (let i = 0; i < expResults.length; i++) { + if (termObj.regExpHighlightMap && !termObj.regExpHighlightMap[expResults[i]]) { + termObj.regExpHighlightMap[expResults[i]] = true; + termObj.regExpHighlights.push(expResults[i]); + } + } + return true; + } + return false; + } + + if (termObj.fullText && memberName.indexOf(termObj.fullText) > -1) { + return true; + } + + for (let i = 0; i < termObj.words.length; i++) { + const word = termObj.words[i].word; + if (memberName.indexOf(word) === -1) { + return false; + } + } + + return true; +} + +/** + * Checks if an event matches the given search terms. + * @param searchTermObj - The search term object containing the search criteria. + * @param evt - The Matrix event to be checked. + * @param matchingMembers - The object containing matching members. + * @returns True if the event matches the search terms, false otherwise. + */ +export function eventMatchesSearchTerms( + searchTermObj: SearchTerm, + evt: MatrixEvent, + matchingMembers: MemberObj, +): boolean { + const content = evt.getContent(); + const sender = evt.getSender(); + const loweredEventContent = content.body.toLowerCase(); + + const evtDate = evt.getDate(); + const dateIso = evtDate && evtDate.toISOString(); + const dateLocale = evtDate && evtDate.toLocaleString(); + + // if (matchingMembers[sender?.userId] !== undefined) { + if (sender && matchingMembers[sender] !== undefined) { + return true; + } + + if (searchTermObj.searchTypeAdvanced === true) { + const expressionResults = loweredEventContent.match(searchTermObj.searchExpression); + if (expressionResults && expressionResults.length > 0) { + for (let i = 0; i < expressionResults.length; i++) { + if (searchTermObj.regExpHighlightMap && !searchTermObj.regExpHighlightMap[expressionResults[i]]) { + searchTermObj.regExpHighlightMap[expressionResults[i]] = true; + searchTermObj.regExpHighlights.push(expressionResults[i]); + } + } + return true; + } + + let dateIsoExprResults; + let dateLocaleExprResults; + if (dateIso && dateLocale && searchTermObj.searchExpression instanceof RegExp) { + dateIsoExprResults = dateIso.match(searchTermObj.searchExpression); + dateLocaleExprResults = dateLocale.match(searchTermObj.searchExpression); + } + + if ( + (dateIsoExprResults && dateIsoExprResults.length > 0) || + (dateLocaleExprResults && dateLocaleExprResults.length > 0) + ) { + return true; + } + + return false; + } + + if (loweredEventContent.indexOf(searchTermObj.fullText) > -1) { + return true; + } + + if ( + (dateIso && searchTermObj.fullText && dateIso.indexOf(searchTermObj.fullText) > -1) || + (dateLocale && searchTermObj.fullText && dateLocale.indexOf(searchTermObj.fullText) > -1) + ) { + return true; + } + + if (searchTermObj.words.length > 0) { + for (let i = 0; i < searchTermObj.words.length; i++) { + const word = searchTermObj.words[i]; + if (loweredEventContent.indexOf(word) === -1) { + return false; + } + } + return true; + } + + return false; +} + +/** + * Creates a search term object based on the provided search term. + * @param searchTerm - The search term to create the object from. + * @returns The created search term object. + */ +export function makeSearchTermObject(searchTerm: string): SearchTerm { + let term = searchTerm.toLowerCase(); + if (term.indexOf("rx:") === 0) { + term = searchTerm.substring(3).trim(); + return { + searchTypeAdvanced: true, + searchTypeNormal: false, + searchExpression: new RegExp(term), + words: [], + regExpHighlights: [], + regExpHighlightMap: {}, + isEmptySearch: term.length === 0, + }; + } + + const words = term + .split(" ") + .filter((w) => w) + .map(function (w) { + return { word: w, highlight: false }; + }); + + return { + searchTypeAdvanced: false, + searchTypeNormal: true, + fullText: term, + words: words, + regExpHighlights: [], + isEmptySearch: term.length === 0, + }; +} + +/** + * Processes the search results and updates the searchResults object. + * + * @param searchResults - The search results object to be updated. + * @param matches - An array of matches. + * @param termObj - The search term object. + * @returns The updated searchResults object. + */ +function processSearchResults(searchResults: SearchResult, matches: any[], termObj: SearchTerm): SearchResult { + for (let i = 0; i < matches.length; i++) { + const sr = new ElementSearchResult(1, matches[i].context); + // sr.context.timeline = sr.context.timeline.reverse(); + searchResults.results.push(sr); + } + + const highlights = termObj.words.filter((w) => w.highlight).map((w) => w.word); + searchResults.highlights = highlights; + for (let i = 0; i < termObj.regExpHighlights.length; i++) { + searchResults.highlights.push(termObj.regExpHighlights[i]); + } + searchResults.count = matches.length; + return searchResults; +} diff --git a/src/VoipUserMapper.ts b/src/VoipUserMapper.ts index d03a38b333b..09e7cbd1af7 100644 --- a/src/VoipUserMapper.ts +++ b/src/VoipUserMapper.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { Room, EventType } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { logger } from "matrix-js-sdk/src/logger"; import { ensureVirtualRoomExists } from "./createRoom"; @@ -95,7 +96,7 @@ export default class VoipUserMapper { if (!virtualRoomEvent || !virtualRoomEvent.getContent()) return null; const nativeRoomID = virtualRoomEvent.getContent()["native_room"]; const nativeRoom = cli.getRoom(nativeRoomID); - if (!nativeRoom || nativeRoom.getMyMembership() !== "join") return null; + if (!nativeRoom || nativeRoom.getMyMembership() !== KnownMembership.Join) return null; return nativeRoomID; } diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index 5f3901a3916..9a2a8552423 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -175,6 +175,8 @@ interface IProps { handleHomeEnd?: boolean; handleUpDown?: boolean; handleLeftRight?: boolean; + handleInputFields?: boolean; + scrollIntoView?: boolean | ScrollIntoViewOptions; children(renderProps: { onKeyDownHandler(ev: React.KeyboardEvent): void; onDragEndHandler(): void }): ReactNode; onKeyDown?(ev: React.KeyboardEvent, state: IState, dispatch: Dispatch): void; } @@ -212,6 +214,8 @@ export const RovingTabIndexProvider: React.FC = ({ handleUpDown, handleLeftRight, handleLoop, + handleInputFields, + scrollIntoView, onKeyDown, }) => { const [state, dispatch] = useReducer>(reducer, { @@ -234,7 +238,7 @@ export const RovingTabIndexProvider: React.FC = ({ let focusRef: RefObject | undefined; // Don't interfere with input default keydown behaviour // but allow people to move focus from it with Tab. - if (checkInputableElement(ev.target as HTMLElement)) { + if (!handleInputFields && checkInputableElement(ev.target as HTMLElement)) { switch (action) { case KeyBindingAction.Tab: handled = true; @@ -311,9 +315,21 @@ export const RovingTabIndexProvider: React.FC = ({ ref: focusRef, }, }); + if (scrollIntoView) { + focusRef.current?.scrollIntoView(scrollIntoView); + } } }, - [context, onKeyDown, handleHomeEnd, handleUpDown, handleLeftRight, handleLoop], + [ + context, + onKeyDown, + handleHomeEnd, + handleUpDown, + handleLeftRight, + handleLoop, + handleInputFields, + scrollIntoView, + ], ); const onDragEndHandler = useCallback(() => { diff --git a/src/accessibility/Toolbar.tsx b/src/accessibility/Toolbar.tsx index a606f9aae89..c5a81aeda34 100644 --- a/src/accessibility/Toolbar.tsx +++ b/src/accessibility/Toolbar.tsx @@ -53,8 +53,9 @@ const Toolbar = forwardRef(({ children, ...props }, ref) } }; + // We handle both up/down and left/right as is allowed in the above WAI ARIA best practices return ( - + {({ onKeyDownHandler }) => (
{children} diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx index 036fb5038b3..3fe64499caa 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx @@ -19,10 +19,9 @@ import React, { createRef } from "react"; import FileSaver from "file-saver"; import { logger } from "matrix-js-sdk/src/logger"; import { AuthDict, CrossSigningKeys, MatrixError, UIAFlow, UIAResponse } from "matrix-js-sdk/src/matrix"; -import { IRecoveryKey } from "matrix-js-sdk/src/crypto/api"; import { CryptoEvent } from "matrix-js-sdk/src/crypto"; import classNames from "classnames"; -import { BackupTrustInfo, KeyBackupInfo } from "matrix-js-sdk/src/crypto-api"; +import { BackupTrustInfo, GeneratedSecretStorageKey, KeyBackupInfo } from "matrix-js-sdk/src/crypto-api"; import { MatrixClientPeg } from "../../../../MatrixClientPeg"; import { _t, _td } from "../../../../languageHandler"; @@ -122,7 +121,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent(); private passphraseField = createRef(); diff --git a/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx b/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx index 95aab12e45c..0778879976c 100644 --- a/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx +++ b/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx @@ -63,7 +63,9 @@ export default class NewRecoveryMethodDialog extends React.PureComponent const newMethodDetected =

{_t("encryption|new_recovery_method_detected|description_1")}

; - const hackWarning =

{_t("encryption|new_recovery_method_detected|warning")}

; + const hackWarning = ( + {_t("encryption|new_recovery_method_detected|warning")} + ); let content: JSX.Element | undefined; if (MatrixClientPeg.safeGet().getKeyBackupEnabled()) { diff --git a/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.tsx b/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.tsx index 4dafcfa5182..c864eb1abba 100644 --- a/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.tsx +++ b/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.tsx @@ -55,7 +55,7 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent

{_t("encryption|recovery_method_removed|description_1")}

{_t("encryption|recovery_method_removed|description_2")}

-

{_t("encryption|recovery_method_removed|warning")}

+ {_t("encryption|recovery_method_removed|warning")} userId !== currentUserId); - this.users = this.users.concat(this.room.getMembersWithMembership("invite")); + this.users = this.users.concat(this.room.getMembersWithMembership(KnownMembership.Invite)); this.users = sortBy(this.users, (member) => 1e20 - lastSpoken[member.userId] || 1e20); diff --git a/src/call-types.ts b/src/call-types.ts index d042faaaf39..40cd006dcc9 100644 --- a/src/call-types.ts +++ b/src/call-types.ts @@ -17,3 +17,12 @@ limitations under the License. // Event type for room account data and room creation content used to mark rooms as virtual rooms // (and store the ID of their native room) export const VIRTUAL_ROOM_EVENT_TYPE = "im.vector.is_virtual_room"; + +export const JitsiCallMemberEventType = "io.element.video.member"; + +export interface JitsiCallMemberContent { + // Connected device IDs + devices: string[]; + // Time at which this state event should be considered stale + expires_ts: number; +} diff --git a/src/components/structures/InteractiveAuth.tsx b/src/components/structures/InteractiveAuth.tsx index 2547af77a1d..1ca2b6e5ce3 100644 --- a/src/components/structures/InteractiveAuth.tsx +++ b/src/components/structures/InteractiveAuth.tsx @@ -18,7 +18,7 @@ import React, { createRef } from "react"; import { AuthType, IAuthData, - IAuthDict, + AuthDict, IInputs, InteractiveAuth, IStageStatus, @@ -64,7 +64,7 @@ export interface InteractiveAuthProps { continueText?: string; continueKind?: ContinueKind; // callback - makeRequest(auth: IAuthDict | null): Promise; + makeRequest(auth: AuthDict | null): Promise; // callback called when the auth process has finished, // successfully or unsuccessfully. // @param {boolean} status True if the operation requiring @@ -213,7 +213,7 @@ export default class InteractiveAuthComponent extends React.Component => { + private requestCallback = (auth: AuthDict | null, background: boolean): Promise => { // This wrapper just exists because the js-sdk passes a second // 'busy' param for backwards compat. This throws the tests off // so discard it here. @@ -246,7 +246,7 @@ export default class InteractiveAuthComponent extends React.Component { + private submitAuthDict = (authData: AuthDict): void => { this.authLogic.submitAuthDict(authData); }; diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index b4d609850de..2cf41215a7d 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -26,8 +26,8 @@ import { RoomType, SyncStateData, SyncState, + TimelineEvents, } from "matrix-js-sdk/src/matrix"; -import { InvalidStoreError } from "matrix-js-sdk/src/errors"; import { defer, IDeferred, QueryDict } from "matrix-js-sdk/src/utils"; import { logger } from "matrix-js-sdk/src/logger"; import { throttle } from "lodash"; @@ -1176,10 +1176,10 @@ export default class MatrixChat extends React.PureComponent { const memberCount = roomToLeave?.currentState.getJoinedMemberCount(); if (memberCount === 1) { warnings.push( - + {" " /* Whitespace, otherwise the sentences get smashed together */} {_t("leave_room_dialog|last_person_warning")} - , + , ); return warnings; @@ -1190,15 +1190,44 @@ export default class MatrixChat extends React.PureComponent { const rule = joinRules.getContent().join_rule; if (rule !== "public") { warnings.push( - + {" " /* Whitespace, otherwise the sentences get smashed together */} {isSpace ? _t("leave_room_dialog|space_rejoin_warning") : _t("leave_room_dialog|room_rejoin_warning")} - , + , ); } } + + const client = MatrixClientPeg.get(); + if (client && roomToLeave) { + const plEvent = roomToLeave.currentState.getStateEvents(EventType.RoomPowerLevels, ""); + const plContent = plEvent ? plEvent.getContent() : {}; + const userLevels = plContent.users || {}; + const currentUserLevel = userLevels[client.getUserId()!]; + const userLevelValues = Object.values(userLevels); + if (userLevelValues.every((x) => typeof x === "number")) { + const maxUserLevel = Math.max(...(userLevelValues as number[])); + // If the user is the only user with highest power level + if ( + maxUserLevel === currentUserLevel && + userLevelValues.lastIndexOf(maxUserLevel) == userLevelValues.indexOf(maxUserLevel) + ) { + const warning = + maxUserLevel >= 100 + ? _t("leave_room_dialog|room_leave_admin_warning") + : _t("leave_room_dialog|room_leave_mod_warning"); + warnings.push( + + {" " /* Whitespace, otherwise the sentences get smashed together */} + {warning} + , + ); + } + } + } + return warnings; } @@ -1223,6 +1252,7 @@ export default class MatrixChat extends React.PureComponent { ), button: _t("action|leave"), + danger: warnings.length > 0, onFinished: async (shouldLeave) => { if (shouldLeave) { await leaveRoomBehaviour(cli, roomId); @@ -1484,9 +1514,6 @@ export default class MatrixChat extends React.PureComponent { cli.on(ClientEvent.Sync, (state: SyncState, prevState: SyncState | null, data?: SyncStateData) => { if (state === SyncState.Error || state === SyncState.Reconnecting) { - if (data?.error instanceof InvalidStoreError) { - Lifecycle.handleInvalidStoreError(data.error); - } this.setState({ syncError: data?.error ?? null }); } else if (this.state.syncError) { this.setState({ syncError: null }); @@ -1904,7 +1931,7 @@ export default class MatrixChat extends React.PureComponent { const cli = MatrixClientPeg.get(); if (!cli) return; - cli.sendEvent(roomId, event.getType(), event.getContent()).then(() => { + cli.sendEvent(roomId, event.getType() as keyof TimelineEvents, event.getContent()).then(() => { dis.dispatch({ action: "message_sent" }); }); } @@ -2018,14 +2045,10 @@ export default class MatrixChat extends React.PureComponent { /> ); } else if (this.state.view === Views.LOGGED_IN) { - // store errors stop the client syncing and require user intervention, so we'll - // be showing a dialog. Don't show anything else. - const isStoreError = this.state.syncError && this.state.syncError instanceof InvalidStoreError; - // `ready` and `view==LOGGED_IN` may be set before `page_type` (because the // latter is set via the dispatcher). If we don't yet have a `page_type`, // keep showing the spinner for now. - if (this.state.ready && this.state.page_type && !isStoreError) { + if (this.state.ready && this.state.page_type) { /* for now, we stuff the entirety of our props and state into the LoggedInView. * we should go through and figure out what we actually need to pass down, as well * as using something like redux to avoid having a billion bits of state kicking around. @@ -2042,12 +2065,11 @@ export default class MatrixChat extends React.PureComponent { ); } else { // we think we are logged in, but are still waiting for the /sync to complete - // Suppress `InvalidStoreError`s here, since they have their own error dialog. view = ( ); } diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index ff1d61a259d..407c09e5b5b 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -38,6 +38,7 @@ import { ISearchResults, THREAD_RELATION_TYPE, } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { logger } from "matrix-js-sdk/src/logger"; import { CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call"; import { throttle } from "lodash"; @@ -120,7 +121,6 @@ import { SDKContext } from "../../contexts/SDKContext"; import { CallStore, CallStoreEvent } from "../../stores/CallStore"; import { Call } from "../../models/Call"; import { RoomSearchView } from "./RoomSearchView"; -import eventSearch from "../../Searching"; import VoipUserMapper from "../../VoipUserMapper"; import { isCallEvent } from "./LegacyCallEventGrouper"; import { WidgetType } from "../../widgets/WidgetType"; @@ -132,6 +132,8 @@ import { CancelAskToJoinPayload } from "../../dispatcher/payloads/CancelAskToJoi import { SubmitAskToJoinPayload } from "../../dispatcher/payloads/SubmitAskToJoinPayload"; import RightPanelStore from "../../stores/right-panel/RightPanelStore"; import { onView3pidInvite } from "../../stores/right-panel/action-handlers"; +// import eventSearch from "../../Searching"; +import searchAllEventsLocally from "../../VerjiLocalSearch"; // ROSBERG const DEBUG = false; const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000; @@ -687,7 +689,7 @@ export class RoomView extends React.Component { newState.showRightPanel = false; } - const initialEventId = this.context.roomViewStore.getInitialEventId(); + const initialEventId = this.context.roomViewStore.getInitialEventId() ?? this.state.initialEventId; if (initialEventId) { let initialEvent = room?.findEventById(initialEventId); // The event does not exist in the current sync data @@ -1430,6 +1432,8 @@ export class RoomView extends React.Component { tombstone: this.getRoomTombstone(room), liveTimeline: room.getLiveTimeline(), }); + + dis.dispatch({ action: Action.RoomLoaded }); }; private onRoomTimelineReset = (room?: Room): void => { @@ -1452,7 +1456,7 @@ export class RoomView extends React.Component { private async loadMembersIfJoined(room: Room): Promise { // lazy load members if enabled if (this.context.client?.hasLazyLoadMembersEnabled()) { - if (room && room.getMyMembership() === "join") { + if (room && room.getMyMembership() === KnownMembership.Join) { try { await room.loadMembersIfNeeded(); if (!this.unmounted) { @@ -1586,7 +1590,8 @@ export class RoomView extends React.Component { if (room && this.context.client) { const me = this.context.client.getSafeUserId(); const canReact = - room.getMyMembership() === "join" && room.currentState.maySendEvent(EventType.Reaction, me); + room.getMyMembership() === KnownMembership.Join && + room.currentState.maySendEvent(EventType.Reaction, me); const canSendMessages = room.maySendMessage(); const canSelfRedact = room.currentState.maySendEvent(EventType.RoomRedaction, me); @@ -1620,10 +1625,10 @@ export class RoomView extends React.Component { private updateDMState(): void { const room = this.state.room; - if (room?.getMyMembership() !== "join") { + if (room?.getMyMembership() != KnownMembership.Join) { return; } - const dmInviter = room.getDMInviter(); + const dmInviter = room?.getDMInviter(); if (dmInviter) { Rooms.setDMRoom(room.client, room.roomId, dmInviter); } @@ -1660,7 +1665,8 @@ export class RoomView extends React.Component { action: Action.JoinRoom, roomId, opts: { inviteSignUrl: signUrl }, - metricsTrigger: this.state.room?.getMyMembership() === "invite" ? "Invite" : "RoomPreview", + metricsTrigger: + this.state.room?.getMyMembership() === KnownMembership.Invite ? "Invite" : "RoomPreview", canAskToJoin: this.state.canAskToJoin, }); } @@ -1725,7 +1731,11 @@ export class RoomView extends React.Component { const roomId = scope === SearchScope.Room ? this.getRoomId() : undefined; debuglog("sending search request"); const abortController = new AbortController(); - const promise = eventSearch(this.context.client!, term, roomId, abortController.signal); + + // ROSBERG START + // const promise = eventSearch(this.context.client!, term, roomId, abortController.signal); + const promise = searchAllEventsLocally(term, roomId); + // ROSBERG END this.setState({ search: { @@ -1735,7 +1745,10 @@ export class RoomView extends React.Component { roomId, term, scope, - promise, + // ROSBERG START + // promise, + promise: promise as any, + // ROSBERG END abortController, }, }); @@ -2181,7 +2194,7 @@ export class RoomView extends React.Component { const myMembership = this.state.room.getMyMembership(); if ( isVideoRoom(this.state.room) && - !(SettingsStore.getValue("feature_video_rooms") && myMembership === "join") + !(SettingsStore.getValue("feature_video_rooms") && myMembership === KnownMembership.Join) ) { return ( @@ -2198,7 +2211,7 @@ export class RoomView extends React.Component { } // SpaceRoomView handles invites itself - if (myMembership === "invite" && !this.state.room.isSpaceRoom()) { + if (myMembership === KnownMembership.Invite && !this.state.room.isSpaceRoom()) { if (this.state.joining || this.state.rejecting) { return ( @@ -2245,16 +2258,19 @@ export class RoomView extends React.Component { } } - if (this.state.canAskToJoin && ["knock", "leave"].includes(myMembership)) { + if ( + this.state.canAskToJoin && + ([KnownMembership.Knock, KnownMembership.Leave] as Array).includes(myMembership) + ) { return (
{ statusBar = ( { ); } else if (showRoomUpgradeBar) { aux = ; - } else if (myMembership !== "join") { + } else if (myMembership !== KnownMembership.Join) { // We do have a room object for this room, but we're not currently in it. // We may have a 3rd party invite to it. let inviterName: string | undefined; @@ -2404,7 +2420,7 @@ export class RoomView extends React.Component { let messageComposer; const showComposer = // joined and not showing search results - myMembership === "join" && !this.state.search; + myMembership === KnownMembership.Join && !this.state.search; if (showComposer) { messageComposer = ( { const myMember = this.state.room!.getMember(this.context.client!.getSafeUserId()); const showForgetButton = - !this.context.client.isGuest() && (["leave", "ban"].includes(myMembership) || myMember?.isKicked()); + !this.context.client.isGuest() && + (([KnownMembership.Leave, KnownMembership.Ban] as Array).includes(myMembership) || + myMember?.isKicked()); return ( @@ -2638,7 +2656,7 @@ export class RoomView extends React.Component { room={this.state.room} searchInfo={this.state.search} oobData={this.props.oobData} - inRoom={myMembership === "join"} + inRoom={myMembership === KnownMembership.Join} onSearchClick={onSearchClick} onInviteClick={onInviteClick} onForgetClick={showForgetButton ? onForgetClick : null} diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx index feeacb45818..dfd8ff5fce4 100644 --- a/src/components/structures/SpaceHierarchy.tsx +++ b/src/components/structures/SpaceHierarchy.tsx @@ -46,6 +46,7 @@ import { RoomHierarchy } from "matrix-js-sdk/src/room-hierarchy"; import classNames from "classnames"; import { sortBy, uniqBy } from "lodash"; import { logger } from "matrix-js-sdk/src/logger"; +import { KnownMembership, SpaceChildEventContent } from "matrix-js-sdk/src/types"; import defaultDispatcher from "../../dispatcher/dispatcher"; import { _t } from "../../languageHandler"; @@ -112,7 +113,7 @@ const Tile: React.FC = ({ const cli = useContext(MatrixClientContext); const joinedRoom = useTypedEventEmitterState(cli, ClientEvent.Room, () => { const cliRoom = cli?.getRoom(room.room_id); - return cliRoom?.getMyMembership() === "join" ? cliRoom : undefined; + return cliRoom?.getMyMembership() === KnownMembership.Join ? cliRoom : undefined; }); const joinedRoomName = useTypedEventEmitterState(joinedRoom, RoomEvent.Name, (room) => room?.name); const name = @@ -726,7 +727,7 @@ const ManageButtons: React.FC = ({ hierarchy, selected, set const existingContent = hierarchy.getRelation(parentId, childId)?.content; if (!existingContent || existingContent.suggested === suggested) continue; - const content = { + const content: SpaceChildEventContent = { ...existingContent, suggested: !selectionAllSuggested, }; @@ -828,7 +829,7 @@ const SpaceHierarchy: React.FC = ({ space, initialText = "", showRoom, a content = ; } else { const hasPermissions = - space?.getMyMembership() === "join" && + space?.getMyMembership() === KnownMembership.Join && space.currentState.maySendStateEvent(EventType.SpaceChild, cli.getSafeUserId()); const root = hierarchy.roomMap.get(space.roomId); @@ -846,7 +847,7 @@ const SpaceHierarchy: React.FC = ({ space, initialText = "", showRoom, a onViewRoomClick={(roomId, roomType) => showRoom(cli, hierarchy, roomId, roomType)} onJoinRoomClick={async (roomId, parents) => { for (const parent of parents) { - if (cli.getRoom(parent)?.getMyMembership() !== "join") { + if (cli.getRoom(parent)?.getMyMembership() !== KnownMembership.Join) { await joinRoom(cli, hierarchy, parent); } } diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index dc79a25489a..edc857edaf0 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -15,6 +15,7 @@ limitations under the License. */ import { EventType, RoomType, JoinRule, Preset, Room, RoomEvent } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { logger } from "matrix-js-sdk/src/logger"; import React, { useCallback, useContext, useRef, useState } from "react"; @@ -237,7 +238,7 @@ const SpaceLanding: React.FC<{ space: Room }> = ({ space }) => { } const hasAddRoomPermissions = - myMembership === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId); + myMembership === KnownMembership.Join && space.currentState.maySendStateEvent(EventType.SpaceChild, userId); let addRoomButton; if (hasAddRoomPermissions) { @@ -678,7 +679,7 @@ export default class SpaceRoomView extends React.PureComponent { private renderBody(): JSX.Element { switch (this.state.phase) { case Phase.Landing: - if (this.state.myMembership === "join") { + if (this.state.myMembership === KnownMembership.Join) { return ; } else { return ( diff --git a/src/components/structures/ThreadPanel.tsx b/src/components/structures/ThreadPanel.tsx index d43b4e25d16..e83eace4845 100644 --- a/src/components/structures/ThreadPanel.tsx +++ b/src/components/structures/ThreadPanel.tsx @@ -17,14 +17,17 @@ limitations under the License. import { Optional } from "matrix-events-sdk"; import React, { useContext, useEffect, useRef, useState } from "react"; import { EventTimelineSet, Room, Thread } from "matrix-js-sdk/src/matrix"; +import { IconButton, Tooltip } from "@vector-im/compound-web"; +import { logger } from "matrix-js-sdk/src/logger"; +import { Icon as MarkAllThreadsReadIcon } from "../../../res/img/element-icons/check-all.svg"; import BaseCard from "../views/right_panel/BaseCard"; import ResizeNotifier from "../../utils/ResizeNotifier"; -import MatrixClientContext from "../../contexts/MatrixClientContext"; +import MatrixClientContext, { useMatrixClientContext } from "../../contexts/MatrixClientContext"; import { _t } from "../../languageHandler"; import { ContextMenuButton } from "../../accessibility/context_menu/ContextMenuButton"; import ContextMenu, { ChevronFace, MenuItemRadio, useContextMenu } from "./ContextMenu"; -import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; +import RoomContext, { TimelineRenderingType, useRoomContext } from "../../contexts/RoomContext"; import TimelinePanel from "./TimelinePanel"; import { Layout } from "../../settings/enums/Layout"; import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; @@ -33,6 +36,7 @@ import PosthogTrackers from "../../PosthogTrackers"; import { ButtonEvent } from "../views/elements/AccessibleButton"; import Spinner from "../views/elements/Spinner"; import Heading from "../views/typography/Heading"; +import { clearRoomNotification } from "../../utils/notifications"; interface IProps { roomId: string; @@ -71,6 +75,8 @@ export const ThreadPanelHeader: React.FC<{ setFilterOption: (filterOption: ThreadFilterType) => void; empty: boolean; }> = ({ filterOption, setFilterOption, empty }) => { + const mxClient = useMatrixClientContext(); + const roomContext = useRoomContext(); const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); const options: readonly ThreadPanelHeaderOption[] = [ { @@ -109,6 +115,26 @@ export const ThreadPanelHeader: React.FC<{ {contextMenuOptions} ) : null; + + const onMarkAllThreadsReadClick = React.useCallback( + (e) => { + PosthogTrackers.trackInteraction("WebThreadsMarkAllReadButton", e); + if (!roomContext.room) { + logger.error("No room in context to mark all threads read"); + return; + } + // This actually clears all room notifications by sending an unthreaded read receipt. + // We'd have to loop over all unread threads (pagninating back to find any we don't + // know about yet) and send threaded receipts for all of them... or implement a + // specific API for it. In practice, the user will have to be viewing the room to + // see this button, so will have marked the room itself read anyway. + clearRoomNotification(roomContext.room, mxClient).catch((e) => { + logger.error("Failed to mark all threads read", e); + }); + }, + [roomContext.room, mxClient], + ); + return (
@@ -116,6 +142,16 @@ export const ThreadPanelHeader: React.FC<{ {!empty && ( <> + + + + + +
{ } if ( - !(await client.doesServerSupportUnstableFeature("org.matrix.msc2285.stable")) || + !(await client.doesServerSupportUnstableFeature("org.matrix.msc2285.stable")) && !(await client.isVersionSupported("v1.4")) ) { logger.warn( @@ -1824,7 +1825,7 @@ class TimelinePanel extends React.Component { // that the event belongs to, and traversing the timeline looking for // that event, while keeping track of the user's membership let i = events.length - 1; - let userMembership = "leave"; + let userMembership: Membership = KnownMembership.Leave; for (; i >= 0; i--) { const timeline = this.props.timelineSet.getTimelineForEvent(events[i].getId()!); if (!timeline) { @@ -1837,14 +1838,15 @@ class TimelinePanel extends React.Component { continue; } - userMembership = timeline.getState(EventTimeline.FORWARDS)?.getMember(userId)?.membership ?? "leave"; + userMembership = + timeline.getState(EventTimeline.FORWARDS)?.getMember(userId)?.membership ?? KnownMembership.Leave; const timelineEvents = timeline.getEvents(); for (let j = timelineEvents.length - 1; j >= 0; j--) { const event = timelineEvents[j]; if (event.getId() === events[i].getId()) { break; } else if (event.getStateKey() === userId && event.getType() === EventType.RoomMember) { - userMembership = event.getPrevContent().membership || "leave"; + userMembership = event.getPrevContent().membership || KnownMembership.Leave; } } break; @@ -1855,8 +1857,11 @@ class TimelinePanel extends React.Component { for (; i >= 0; i--) { const event = events[i]; if (event.getStateKey() === userId && event.getType() === EventType.RoomMember) { - userMembership = event.getPrevContent().membership || "leave"; - } else if (userMembership === "leave" && (event.isDecryptionFailure() || event.isBeingDecrypted())) { + userMembership = event.getPrevContent().membership || KnownMembership.Leave; + } else if ( + userMembership === KnownMembership.Leave && + (event.isDecryptionFailure() || event.isBeingDecrypted()) + ) { // reached an undecryptable message when the user wasn't in the room -- don't try to load any more // Note: for now, we assume that events that are being decrypted are // not decryptable - we will be called once more when it is decrypted. diff --git a/src/components/structures/auth/LoginSplashView.tsx b/src/components/structures/auth/LoginSplashView.tsx index 808b97ff419..2ae13ce1475 100644 --- a/src/components/structures/auth/LoginSplashView.tsx +++ b/src/components/structures/auth/LoginSplashView.tsx @@ -23,6 +23,7 @@ import ProgressBar from "../../views/elements/ProgressBar"; import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton"; import { _t } from "../../../languageHandler"; import { useTypedEventEmitterState } from "../../../hooks/useEventEmitter"; +import SdkConfig from "../../../SdkConfig"; interface Props { /** The matrix client which is logging in */ @@ -65,7 +66,7 @@ export function LoginSplashView(props: Props): React.JSX.Element { if (migrationState.totalSteps !== -1) { spinnerOrProgress = (
-

{_t("migrating_crypto")}

+

{_t("migrating_crypto", { brand: SdkConfig.get().brand })}

); diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index 4da7282660e..5ac49537c58 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -18,7 +18,7 @@ import { AuthType, createClient, IAuthData, - IAuthDict, + AuthDict, IInputs, MatrixError, IRegisterRequestParams, @@ -478,7 +478,7 @@ export default class Registration extends React.Component { }); }; - private makeRegisterRequest = (auth: IAuthDict | null): Promise => { + private makeRegisterRequest = (auth: AuthDict | null): Promise => { if (!this.state.matrixClient) throw new Error("Matrix client has not yet been loaded"); const registerParams: IRegisterRequestParams = { diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index 3ad4638306d..684a7b5af4e 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -15,10 +15,10 @@ limitations under the License. */ import React from "react"; -import { ISecretStorageKeyInfo } from "matrix-js-sdk/src/crypto/api"; import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup"; import { VerificationRequest } from "matrix-js-sdk/src/crypto-api"; import { logger } from "matrix-js-sdk/src/logger"; +import { SecretStorageKeyDescription } from "matrix-js-sdk/src/secret-storage"; import { _t } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; @@ -29,7 +29,7 @@ import EncryptionPanel from "../../views/right_panel/EncryptionPanel"; import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton"; import Spinner from "../../views/elements/Spinner"; -function keyHasPassphrase(keyInfo: ISecretStorageKeyInfo): boolean { +function keyHasPassphrase(keyInfo: SecretStorageKeyDescription): boolean { return Boolean(keyInfo.passphrase && keyInfo.passphrase.salt && keyInfo.passphrase.iterations); } diff --git a/src/components/structures/grouper/CreationGrouper.tsx b/src/components/structures/grouper/CreationGrouper.tsx index 0ceb6f58575..fa91a1bd90c 100644 --- a/src/components/structures/grouper/CreationGrouper.tsx +++ b/src/components/structures/grouper/CreationGrouper.tsx @@ -16,6 +16,7 @@ limitations under the License. import React, { ReactNode } from "react"; import { EventType, M_BEACON_INFO, MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { BaseGrouper } from "./BaseGrouper"; import MessagePanel, { WrappedEvent } from "../MessagePanel"; @@ -48,7 +49,8 @@ export class CreationGrouper extends BaseGrouper { const eventType = event.getType(); if ( eventType === EventType.RoomMember && - (event.getStateKey() !== createEvent.getSender() || event.getContent()["membership"] !== "join") + (event.getStateKey() !== createEvent.getSender() || + event.getContent()["membership"] !== KnownMembership.Join) ) { return false; } diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index 0a7ed19b2ab..e8969f12adf 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -16,7 +16,7 @@ limitations under the License. import classNames from "classnames"; import { MatrixClient } from "matrix-js-sdk/src/matrix"; -import { AuthType, IAuthDict, IInputs, IStageStatus } from "matrix-js-sdk/src/interactive-auth"; +import { AuthType, AuthDict, IInputs, IStageStatus } from "matrix-js-sdk/src/interactive-auth"; import { logger } from "matrix-js-sdk/src/logger"; import React, { ChangeEvent, createRef, FormEvent, Fragment } from "react"; @@ -89,7 +89,7 @@ interface IAuthEntryProps { // Is the auth logic currently waiting for something to happen? busy?: boolean; onPhaseChange: (phase: number) => void; - submitAuthDict: (auth: IAuthDict) => void; + submitAuthDict: (auth: AuthDict) => void; requestEmailToken?: () => Promise; fail: (error: Error) => void; clientSecret: string; diff --git a/src/components/views/auth/LoginWithQRFlow.tsx b/src/components/views/auth/LoginWithQRFlow.tsx index 526496246a9..05c8d95c423 100644 --- a/src/components/views/auth/LoginWithQRFlow.tsx +++ b/src/components/views/auth/LoginWithQRFlow.tsx @@ -16,16 +16,15 @@ limitations under the License. import React from "react"; import { RendezvousFailureReason } from "matrix-js-sdk/src/rendezvous"; +import { Icon as ChevronLeftIcon } from "@vector-im/compound-design-tokens/icons/chevron-left.svg"; import { _t } from "../../../languageHandler"; import AccessibleButton from "../elements/AccessibleButton"; import QRCode from "../elements/QRCode"; import Spinner from "../elements/Spinner"; -import { Icon as BackButtonIcon } from "../../../../res/img/element-icons/back.svg"; -import { Icon as DevicesIcon } from "../../../../res/img/element-icons/devices.svg"; -import { Icon as WarningBadge } from "../../../../res/img/element-icons/warning-badge.svg"; import { Icon as InfoIcon } from "../../../../res/img/element-icons/i.svg"; import { Click, FailureReason, LoginWithQRFailureReason, Phase } from "./LoginWithQR"; +import SdkConfig from "../../../SdkConfig"; interface IProps { phase: Phase; @@ -70,8 +69,6 @@ export default class LoginWithQRFlow extends React.Component { }; public render(): React.ReactNode { - let title = ""; - let titleIcon: JSX.Element | undefined; let main: JSX.Element | undefined; let buttons: JSX.Element | undefined; let backButton = true; @@ -115,9 +112,7 @@ export default class LoginWithQRFlow extends React.Component { cancellationMessage = _t("auth|qr_code_login|error_request_cancelled"); break; } - title = _t("timeline|m.call.invite|failed_connection"); centreTitle = true; - titleIcon = ; backButton = false; main =

{cancellationMessage}

; buttons = ( @@ -134,8 +129,6 @@ export default class LoginWithQRFlow extends React.Component { ); break; case Phase.Connected: - title = _t("auth|qr_code_login|devices_connected"); - titleIcon = ; backButton = false; main = ( <> @@ -170,7 +163,6 @@ export default class LoginWithQRFlow extends React.Component { ); break; case Phase.ShowingQR: - title = _t("settings|sessions|sign_in_with_qr"); if (this.props.code) { const code = (
@@ -182,17 +174,22 @@ export default class LoginWithQRFlow extends React.Component { ); main = ( <> -

{_t("auth|qr_code_login|scan_code_instruction")}

+

{_t("auth|qr_code_login|scan_code_instruction")}

+ {code}
    -
  1. {_t("auth|qr_code_login|start_at_sign_in_screen")}
  2. +
  3. + {_t("auth|qr_code_login|open_element_other_device", { + brand: SdkConfig.get().brand, + })} +
  4. {_t("auth|qr_code_login|select_qr_code", { - scanQRCode: _t("auth|qr_code_login|scan_qr_code"), + scanQRCode: {_t("auth|qr_code_login|scan_qr_code")}, })}
  5. -
  6. {_t("auth|qr_code_login|review_and_approve")}
  7. +
  8. {_t("auth|qr_code_login|point_the_camera")}
  9. +
  10. {_t("auth|qr_code_login|follow_remaining_instructions")}
- {code} ); } else { @@ -212,7 +209,6 @@ export default class LoginWithQRFlow extends React.Component { buttons = this.cancelButton(); break; case Phase.Verifying: - title = _t("common|success"); centreTitle = true; main = this.simpleSpinner(_t("auth|qr_code_login|completing_setup")); break; @@ -222,19 +218,20 @@ export default class LoginWithQRFlow extends React.Component {
{backButton ? ( - - - +
+ + + +
+ {_t("settings|sessions|title")} / {_t("settings|sessions|sign_in_with_qr")} +
+
) : null} -

- {titleIcon} - {title} -

{main}
{buttons}
diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx index 5bdc90a1f7c..5879dd3b1a7 100644 --- a/src/components/views/avatars/DecoratedRoomAvatar.tsx +++ b/src/components/views/avatars/DecoratedRoomAvatar.tsx @@ -29,12 +29,17 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { _t } from "../../../languageHandler"; import DMRoomMap from "../../../utils/DMRoomMap"; import { IOOBData } from "../../../stores/ThreepidInviteStore"; +import { getJoinedNonFunctionalMembers } from "../../../utils/room/getJoinedNonFunctionalMembers"; interface IProps { room: Room; size: string; displayBadge?: boolean; - forceCount?: boolean; + /** + * If true, show nothing if the notification would only cause a dot to be shown rather than + * a badge. That is: only display badges and not dots. Default: false. + */ + hideIfDot?: boolean; oobData?: IOOBData; viewAvatarOnClick?: boolean; tooltipProps?: { @@ -158,7 +163,7 @@ export default class DecoratedRoomAvatar extends React.PureComponent ); diff --git a/src/components/views/beacon/BeaconMarker.tsx b/src/components/views/beacon/BeaconMarker.tsx index 217be7351e7..6ac29c8a24e 100644 --- a/src/components/views/beacon/BeaconMarker.tsx +++ b/src/components/views/beacon/BeaconMarker.tsx @@ -20,7 +20,7 @@ import { Beacon, BeaconEvent, LocationAssetType } from "matrix-js-sdk/src/matrix import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { useEventEmitterState } from "../../../hooks/useEventEmitter"; -import SmartMarker from "../location/SmartMarker"; +import { SmartMarker } from "../location"; interface Props { map: maplibregl.Map; diff --git a/src/components/views/beacon/BeaconViewDialog.tsx b/src/components/views/beacon/BeaconViewDialog.tsx index a5d79a472f0..227ba221b7e 100644 --- a/src/components/views/beacon/BeaconViewDialog.tsx +++ b/src/components/views/beacon/BeaconViewDialog.tsx @@ -36,7 +36,7 @@ import MapFallback from "../location/MapFallback"; import { MapError } from "../location/MapError"; import { LocationShareError } from "../../../utils/location"; -interface IProps { +export interface IProps { roomId: Room["roomId"]; matrixClient: MatrixClient; // open the map centered on this beacon's location diff --git a/src/components/views/beacon/index.tsx b/src/components/views/beacon/index.tsx new file mode 100644 index 00000000000..77119edbdd4 --- /dev/null +++ b/src/components/views/beacon/index.tsx @@ -0,0 +1,31 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Exports beacon components which touch maplibre-gs wrapped in React Suspense to enable code splitting + +import React, { ComponentProps, lazy, Suspense } from "react"; + +import Spinner from "../elements/Spinner"; + +const BeaconViewDialogComponent = lazy(() => import("./BeaconViewDialog")); + +export function BeaconViewDialog(props: ComponentProps): JSX.Element { + return ( + }> + + + ); +} diff --git a/src/components/views/context_menus/RoomContextMenu.tsx b/src/components/views/context_menus/RoomContextMenu.tsx index 7527ef4feef..6ae926707d1 100644 --- a/src/components/views/context_menus/RoomContextMenu.tsx +++ b/src/components/views/context_menus/RoomContextMenu.tsx @@ -16,6 +16,7 @@ limitations under the License. import React, { useContext } from "react"; import { Room } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { IProps as IContextMenuProps } from "../../structures/ContextMenu"; import IconizedContextMenu, { @@ -144,7 +145,7 @@ const RoomContextMenu: React.FC = ({ room, onFinished, ...props }) => { let favouriteOption: JSX.Element | undefined; let lowPriorityOption: JSX.Element | undefined; let notificationOption: JSX.Element | undefined; - if (room.getMyMembership() === "join") { + if (room.getMyMembership() === KnownMembership.Join) { const isFavorite = roomTags.includes(DefaultTagID.Favourite); favouriteOption = ( void; + /** + * Called when the 'low priority' option is selected, after the menu has processed + * the mouse or keyboard event. + * @param event The event that caused the option to be selected. + */ onPostLowPriorityClick?: (event: ButtonEvent) => void; + /** + * Called when the 'invite' option is selected, after the menu has processed + * the mouse or keyboard event. + * @param event The event that caused the option to be selected. + */ onPostInviteClick?: (event: ButtonEvent) => void; + /** + * Called when the 'copy link' option is selected, after the menu has processed + * the mouse or keyboard event. + * @param event The event that caused the option to be selected. + */ onPostCopyLinkClick?: (event: ButtonEvent) => void; + /** + * Called when the 'settings' option is selected, after the menu has processed + * the mouse or keyboard event. + * @param event The event that caused the option to be selected. + */ onPostSettingsClick?: (event: ButtonEvent) => void; + /** + * Called when the 'forget room' option is selected, after the menu has processed + * the mouse or keyboard event. + * @param event The event that caused the option to be selected. + */ onPostForgetClick?: (event: ButtonEvent) => void; + /** + * Called when the 'leave' option is selected, after the menu has processed + * the mouse or keyboard event. + * @param event The event that caused the option to be selected. + */ onPostLeaveClick?: (event: ButtonEvent) => void; + /** + * Called when the 'mark as read' option is selected, after the menu has processed + * the mouse or keyboard event. + * @param event The event that caused the option to be selected. + */ + onPostMarkAsReadClick?: (event: ButtonEvent) => void; + /** + * Called when the 'mark as unread' option is selected, after the menu has processed + * the mouse or keyboard event. + * @param event The event that caused the option to be selected. + */ + onPostMarkAsUnreadClick?: (event: ButtonEvent) => void; } /** @@ -67,6 +114,8 @@ export const RoomGeneralContextMenu: React.FC = ({ onPostSettingsClick, onPostLeaveClick, onPostForgetClick, + onPostMarkAsReadClick, + onPostMarkAsUnreadClick, ...props }) => { const cli = useContext(MatrixClientContext); @@ -213,18 +262,33 @@ export const RoomGeneralContextMenu: React.FC = ({ } const { level } = useUnreadNotifications(room); - const markAsReadOption: JSX.Element | null = - level > NotificationLevel.None ? ( - { - clearRoomNotification(room, cli); - onFinished?.(); - }} - active={false} - label={_t("room|context_menu|mark_read")} - iconClassName="mx_RoomGeneralContextMenu_iconMarkAsRead" - /> - ) : null; + const markAsReadOption: JSX.Element | null = (() => { + if (level > NotificationLevel.None) { + return ( + { + clearRoomNotification(room, cli); + onFinished?.(); + }, onPostMarkAsReadClick)} + label={_t("room|context_menu|mark_read")} + iconClassName="mx_RoomGeneralContextMenu_iconMarkAsRead" + /> + ); + } else if (!roomTags.includes(DefaultTagID.Archived)) { + return ( + { + setMarkedUnreadState(room, cli, true); + onFinished?.(); + }, onPostMarkAsUnreadClick)} + label={_t("room|context_menu|mark_unread")} + iconClassName="mx_RoomGeneralContextMenu_iconMarkAsUnread" + /> + ); + } else { + return null; + } + })(); const developerModeEnabled = useSettingValue("developerMode"); const developerToolsOption = developerModeEnabled ? ( diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index 39a00f10347..3567dbecc75 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -17,6 +17,7 @@ limitations under the License. import React, { ReactElement, ReactNode, useContext, useMemo, useRef, useState } from "react"; import classNames from "classnames"; import { Room, EventType } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { sleep } from "matrix-js-sdk/src/utils"; import { logger } from "matrix-js-sdk/src/logger"; @@ -140,7 +141,9 @@ export const AddExistingToSpace: React.FC = ({ const msc3946ProcessDynamicPredecessor = useSettingValue("feature_dynamic_room_predecessors"); const visibleRooms = useMemo( () => - cli?.getVisibleRooms(msc3946ProcessDynamicPredecessor).filter((r) => r.getMyMembership() === "join") ?? [], + cli + ?.getVisibleRooms(msc3946ProcessDynamicPredecessor) + .filter((r) => r.getMyMembership() === KnownMembership.Join) ?? [], [cli, msc3946ProcessDynamicPredecessor], ); diff --git a/src/components/views/dialogs/BaseDialog.tsx b/src/components/views/dialogs/BaseDialog.tsx index 5e7c023e2d5..1b160150f76 100644 --- a/src/components/views/dialogs/BaseDialog.tsx +++ b/src/components/views/dialogs/BaseDialog.tsx @@ -155,9 +155,6 @@ export default class BaseDialog extends React.Component { lockProps["aria-labelledby"] = "mx_BaseDialog_title"; } - const isHeaderWithCancelOnly = - !!cancelButton && !this.props.title && !this.props.headerButton && !this.props.headerImage; - return ( {this.props.screenName && } @@ -172,8 +169,6 @@ export default class BaseDialog extends React.Component {
{!!(this.props.title || headerImage) && ( @@ -188,8 +183,8 @@ export default class BaseDialog extends React.Component { )} {this.props.headerButton} - {cancelButton}
+ {cancelButton} {this.props.children}
diff --git a/src/components/views/dialogs/EndPollDialog.tsx b/src/components/views/dialogs/EndPollDialog.tsx index cc68e80191d..6d1abc4a335 100644 --- a/src/components/views/dialogs/EndPollDialog.tsx +++ b/src/components/views/dialogs/EndPollDialog.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import React from "react"; -import { MatrixEvent, MatrixClient } from "matrix-js-sdk/src/matrix"; +import { MatrixEvent, MatrixClient, TimelineEvents } from "matrix-js-sdk/src/matrix"; import { PollEndEvent } from "matrix-js-sdk/src/extensible_events_v1/PollEndEvent"; import { _t } from "../../../languageHandler"; @@ -51,7 +51,11 @@ export default class EndPollDialog extends React.Component { const endEvent = PollEndEvent.from(this.props.event.getId()!, message).serialize(); - await this.props.matrixClient.sendEvent(this.props.event.getRoomId()!, endEvent.type, endEvent.content); + await this.props.matrixClient.sendEvent( + this.props.event.getRoomId()!, + endEvent.type as keyof TimelineEvents, + endEvent.content as TimelineEvents[keyof TimelineEvents], + ); } catch (e) { console.error("Failed to submit poll response event:", e); Modal.createDialog(ErrorDialog, { diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index a21acd7b717..d59e23fe4cb 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -28,7 +28,9 @@ import { LocationAssetType, M_TIMESTAMP, M_BEACON, + TimelineEvents, } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { _t } from "../../../languageHandler"; import dis from "../../../dispatcher/dispatcher"; @@ -57,6 +59,15 @@ import { isLocationEvent } from "../../../utils/EventUtils"; import { isSelfLocation, locationEventGeoUri } from "../../../utils/location"; import { RoomContextDetails } from "../rooms/RoomContextDetails"; import { filterBoolean } from "../../../utils/arrays"; +import { + IState, + RovingTabIndexContext, + RovingTabIndexProvider, + Type, + useRovingTabIndex, +} from "../../../accessibility/RovingTabIndex"; +import { getKeyBindingsManager } from "../../../KeyBindingsManager"; +import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; const AVATAR_SIZE = 30; @@ -70,10 +81,10 @@ interface IProps { onFinished(): void; } -interface IEntryProps { +interface IEntryProps { room: Room; - type: EventType | string; - content: IContent; + type: K; + content: TimelineEvents[K]; matrixClient: MatrixClient; onFinished(success: boolean): void; } @@ -85,8 +96,9 @@ enum SendState { Failed, } -const Entry: React.FC = ({ room, type, content, matrixClient: cli, onFinished }) => { +const Entry: React.FC> = ({ room, type, content, matrixClient: cli, onFinished }) => { const [sendState, setSendState] = useState(SendState.CanSend); + const [onFocus, isActive, ref] = useRovingTabIndex(); const jumpToRoom = (ev: ButtonEvent): void => { dis.dispatch({ @@ -134,16 +146,30 @@ const Entry: React.FC = ({ room, type, content, matrixClient: cli, icon = ; } + const id = `mx_ForwardDialog_entry_${room.roomId}`; return ( -
+
- - {room.name} + + + {room.name} + = ({ room, type, content, matrixClient: cli, disabled={disabled} title={title} alignment={Alignment.Top} + tabIndex={isActive ? 0 : -1} + id={`${id}_send`} >
{_t("forward|send_label")}
{icon} @@ -241,7 +269,7 @@ const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCr sortRooms( cli .getVisibleRooms(msc3946DynamicRoomPredecessors) - .filter((room) => room.getMyMembership() === "join" && !room.isSpaceRoom()), + .filter((room) => room.getMyMembership() === KnownMembership.Join && !room.isSpaceRoom()), ), [cli, msc3946DynamicRoomPredecessors], ); @@ -270,6 +298,26 @@ const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCr ); } + const onKeyDown = (ev: React.KeyboardEvent, state: IState): void => { + let handled = true; + + const action = getKeyBindingsManager().getAccessibilityAction(ev); + switch (action) { + case KeyBindingAction.Enter: { + state.activeRef?.current?.querySelector(".mx_ForwardList_sendButton")?.click(); + break; + } + + default: + handled = false; + } + + if (handled) { + ev.preventDefault(); + ev.stopPropagation(); + } + }; + return ( = ({ matrixClient: cli, event, permalinkCr />

-
- - - {rooms.length > 0 ? ( -
- - rooms - .slice(start, end) - .map((room) => ( - - )) - } - getChildCount={() => rooms.length} - /> -
- ) : ( - {_t("common|no_results")} - )} -
-
+ + {({ onKeyDownHandler }) => ( +
+ + {(context) => ( + { + setQuery(query); + setImmediate(() => { + const ref = context.state.refs[0]; + if (ref) { + context.dispatch({ + type: Type.SetFocus, + payload: { ref }, + }); + ref.current?.scrollIntoView?.({ + block: "nearest", + }); + } + }); + }} + autoFocus={true} + onKeyDown={onKeyDownHandler} + aria-activedescendant={context.state.activeRef?.current?.id} + aria-owns="mx_ForwardDialog_resultsList" + /> + )} + + + {rooms.length > 0 ? ( +
+ + rooms + .slice(start, end) + .map((room) => ( + + )) + } + getChildCount={() => rooms.length} + /> +
+ ) : ( + {_t("common|no_results")} + )} +
+
+ )} +
); }; diff --git a/src/components/views/dialogs/IncomingSasDialog.tsx b/src/components/views/dialogs/IncomingSasDialog.tsx index a562760b6a1..f1f09897f95 100644 --- a/src/components/views/dialogs/IncomingSasDialog.tsx +++ b/src/components/views/dialogs/IncomingSasDialog.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import React, { ReactNode } from "react"; -import { GeneratedSas, ShowSasCallbacks, Verifier, VerifierEvent } from "matrix-js-sdk/src/crypto-api/verification"; +import { GeneratedSas, ShowSasCallbacks, Verifier, VerifierEvent } from "matrix-js-sdk/src/crypto-api"; import { logger } from "matrix-js-sdk/src/logger"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index 08dbddc21d2..2b3c7af8dbd 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -17,6 +17,7 @@ limitations under the License. import React, { createRef, ReactNode, SyntheticEvent } from "react"; import classNames from "classnames"; import { RoomMember, Room, MatrixError, EventType } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { MatrixCall } from "matrix-js-sdk/src/webrtc/call"; import { logger } from "matrix-js-sdk/src/logger"; import { uniqBy } from "lodash"; @@ -372,10 +373,10 @@ export default class InviteDialog extends React.PureComponent excludedIds.add(m.userId)); - room.getMembersWithMembership("join").forEach((m) => excludedIds.add(m.userId)); + room.getMembersWithMembership(KnownMembership.Invite).forEach((m) => excludedIds.add(m.userId)); + room.getMembersWithMembership(KnownMembership.Join).forEach((m) => excludedIds.add(m.userId)); // add banned users, so we don't try to invite them - room.getMembersWithMembership("ban").forEach((m) => excludedIds.add(m.userId)); + room.getMembersWithMembership(KnownMembership.Ban).forEach((m) => excludedIds.add(m.userId)); if (isFederated === false) { // exclude users from external servers const homeserver = props.roomId.split(":")[1]; diff --git a/src/components/views/dialogs/LazyLoadingDisabledDialog.tsx b/src/components/views/dialogs/LazyLoadingDisabledDialog.tsx deleted file mode 100644 index 1e90b91b455..00000000000 --- a/src/components/views/dialogs/LazyLoadingDisabledDialog.tsx +++ /dev/null @@ -1,55 +0,0 @@ -/* -Copyright 2018 New Vector Ltd -Copyright 2020 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from "react"; - -import QuestionDialog from "./QuestionDialog"; -import { _t } from "../../../languageHandler"; -import SdkConfig from "../../../SdkConfig"; - -interface IProps { - host: string; - onFinished(): void; -} - -const LazyLoadingDisabledDialog: React.FC = (props) => { - const brand = SdkConfig.get().brand; - const description1 = _t("lazy_loading|disabled_description1", { - brand, - host: props.host, - }); - const description2 = _t("lazy_loading|disabled_description2", { - brand, - }); - - return ( - -

{description1}

-

{description2}

-
- } - button={_t("lazy_loading|disabled_action")} - onFinished={props.onFinished} - /> - ); -}; - -export default LazyLoadingDisabledDialog; diff --git a/src/components/views/dialogs/LazyLoadingResyncDialog.tsx b/src/components/views/dialogs/LazyLoadingResyncDialog.tsx deleted file mode 100644 index 54779bda4c6..00000000000 --- a/src/components/views/dialogs/LazyLoadingResyncDialog.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/* -Copyright 2018 New Vector Ltd -Copyright 2020 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from "react"; - -import QuestionDialog from "./QuestionDialog"; -import { _t } from "../../../languageHandler"; -import SdkConfig from "../../../SdkConfig"; - -interface IProps { - onFinished(): void; -} - -const LazyLoadingResyncDialog: React.FC = (props) => { - const brand = SdkConfig.get().brand; - const description = _t("lazy_loading|resync_description", { brand }); - - return ( - {description}
} - button={_t("action|ok")} - onFinished={props.onFinished} - /> - ); -}; - -export default LazyLoadingResyncDialog; diff --git a/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx b/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx index 0799ff88f12..9bc28ab4a0d 100644 --- a/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx +++ b/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx @@ -16,6 +16,7 @@ limitations under the License. import React, { useMemo, useState } from "react"; import { Room } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { _t } from "../../../languageHandler"; import BaseDialog from "./BaseDialog"; @@ -102,7 +103,7 @@ const ManageRestrictedJoinRuleDialog: React.FC = ({ room, selected = [], if (!room) { return { roomId, name: roomId } as Room; } - if (room.getMyMembership() !== "join" || !room.isSpaceRoom()) { + if (room.getMyMembership() !== KnownMembership.Join || !room.isSpaceRoom()) { return room; } }), diff --git a/src/components/views/dialogs/ReportEventDialog.tsx b/src/components/views/dialogs/ReportEventDialog.tsx index 52859c55f6a..0e0b231b3fe 100644 --- a/src/components/views/dialogs/ReportEventDialog.tsx +++ b/src/components/views/dialogs/ReportEventDialog.tsx @@ -32,6 +32,12 @@ import Field from "../elements/Field"; import Spinner from "../elements/Spinner"; import LabelledCheckbox from "../elements/LabelledCheckbox"; +declare module "matrix-js-sdk/src/types" { + interface TimelineEvents { + [ABUSE_EVENT_TYPE]: AbuseEventContent; + } +} + interface IProps { mxEvent: MatrixEvent; onFinished(report?: boolean): void; @@ -56,7 +62,16 @@ const MODERATED_BY_STATE_EVENT_TYPE = [ */ ]; -const ABUSE_EVENT_TYPE = "org.matrix.msc3215.abuse.report"; +export const ABUSE_EVENT_TYPE = "org.matrix.msc3215.abuse.report"; + +interface AbuseEventContent { + event_id: string; + room_id: string; + moderated_by_id: string; + nature?: ExtendedNature; + reporter: string; + comment: string; +} // Standard abuse natures. enum Nature { @@ -250,13 +265,13 @@ export default class ReportEventDialog extends React.Component { } await client.sendEvent(dmRoomId, ABUSE_EVENT_TYPE, { - event_id: ev.getId(), - room_id: ev.getRoomId(), + event_id: ev.getId()!, + room_id: ev.getRoomId()!, moderated_by_id: this.moderation.moderationRoomId, nature, - reporter: client.getUserId(), + reporter: client.getUserId()!, comment: this.state.reason.trim(), - }); + } satisfies AbuseEventContent); } else { // Report to homeserver admin through the dedicated Matrix API. await client.reportEvent(ev.getRoomId()!, ev.getId()!, -100, this.state.reason.trim()); diff --git a/src/components/views/dialogs/ScrollableBaseModal.tsx b/src/components/views/dialogs/ScrollableBaseModal.tsx index 8fa9fa3f645..a12632a05fe 100644 --- a/src/components/views/dialogs/ScrollableBaseModal.tsx +++ b/src/components/views/dialogs/ScrollableBaseModal.tsx @@ -94,12 +94,12 @@ export default abstract class ScrollableBaseModal< >

{this.state.title}

-
+
{this.renderContent()}
diff --git a/src/components/views/dialogs/ServerPickerDialog.tsx b/src/components/views/dialogs/ServerPickerDialog.tsx index ee8a3751daa..d0597f52848 100644 --- a/src/components/views/dialogs/ServerPickerDialog.tsx +++ b/src/components/views/dialogs/ServerPickerDialog.tsx @@ -80,9 +80,6 @@ export default class ServerPickerDialog extends React.PureComponent({ deriveData: async ({ value }): Promise<{ error?: string }> => { let hsUrl = (value ?? "").trim(); // trim to account for random whitespace @@ -91,7 +88,10 @@ export default class ServerPickerDialog extends React.PureComponentmatrix.to link will be generated out of it if it's not already a url. + */ + target: Room | User | RoomMember | URL; permalinkCreator?: RoomPermalinkCreator; } @@ -109,7 +126,9 @@ export default class ShareDialog extends React.PureComponent 0) { @@ -146,9 +167,9 @@ export default class ShareDialog extends React.PureComponent + {this.props.subtitle &&

{this.props.subtitle}

}
matrixToUrl}> diff --git a/src/components/views/dialogs/UserSettingsDialog.tsx b/src/components/views/dialogs/UserSettingsDialog.tsx index f0c9d1cd73a..820617ae962 100644 --- a/src/components/views/dialogs/UserSettingsDialog.tsx +++ b/src/components/views/dialogs/UserSettingsDialog.tsx @@ -84,6 +84,16 @@ export default class UserSettingsDialog extends React.Component "UserSettingsGeneral", ), ); + tabs.push( + new Tab( + UserTab.SessionManager, + _td("settings|sessions|title"), + "mx_UserSettingsDialog_sessionsIcon", + , + // don't track with posthog while under construction + undefined, + ), + ); tabs.push( new Tab( UserTab.Appearance, @@ -151,16 +161,6 @@ export default class UserSettingsDialog extends React.Component "UserSettingsSecurityPrivacy", ), ); - tabs.push( - new Tab( - UserTab.SessionManager, - _td("settings|sessions|title"), - "mx_UserSettingsDialog_sessionsIcon", - , - // don't track with posthog while under construction - undefined, - ), - ); // Show the Labs tab if enabled or if there are any active betas if (showLabsFlags() || SettingsStore.getFeatureSettingNames().some((k) => SettingsStore.getBetaInfo(k))) { tabs.push( diff --git a/src/components/views/dialogs/devtools/BaseTool.tsx b/src/components/views/dialogs/devtools/BaseTool.tsx index 49bcd76009d..e5f93ead3f9 100644 --- a/src/components/views/dialogs/devtools/BaseTool.tsx +++ b/src/components/views/dialogs/devtools/BaseTool.tsx @@ -60,7 +60,7 @@ const BaseTool: React.FC> = ({ let actionButton: ReactNode = null; if (message) { children = message; - } else if (onAction) { + } else if (onAction && actionLabel) { const onActionClick = (): void => { onAction().then((msg) => { if (typeof msg === "string") { diff --git a/src/components/views/dialogs/devtools/Event.tsx b/src/components/views/dialogs/devtools/Event.tsx index 4b85dbe3f6f..e1e0e469a36 100644 --- a/src/components/views/dialogs/devtools/Event.tsx +++ b/src/components/views/dialogs/devtools/Event.tsx @@ -16,7 +16,7 @@ limitations under the License. */ import React, { ChangeEvent, ReactNode, useContext, useMemo, useRef, useState } from "react"; -import { IContent, MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { IContent, MatrixEvent, TimelineEvents } from "matrix-js-sdk/src/matrix"; import { _t, _td, TranslationKey } from "../../../../languageHandler"; import Field from "../../elements/Field"; @@ -32,7 +32,7 @@ export const stringify = (object: object): string => { interface IEventEditorProps extends Pick { fieldDefs: IFieldDef[]; // immutable defaultContent?: string; - onSend(fields: string[], content?: IContent): Promise; + onSend(fields: string[], content: IContent): Promise; } interface IFieldDef { @@ -180,8 +180,8 @@ export const TimelineEventEditor: React.FC = ({ mxEvent, onBack }) const fields = useMemo(() => [eventTypeField(mxEvent?.getType())], [mxEvent]); - const onSend = ([eventType]: string[], content?: IContent): Promise => { - return cli.sendEvent(context.room.roomId, eventType, content || {}); + const onSend = ([eventType]: string[], content: TimelineEvents[keyof TimelineEvents]): Promise => { + return cli.sendEvent(context.room.roomId, eventType as keyof TimelineEvents, content); }; let defaultContent: string | undefined; diff --git a/src/components/views/dialogs/devtools/RoomState.tsx b/src/components/views/dialogs/devtools/RoomState.tsx index ba8e3c75d9d..51fdd1e8300 100644 --- a/src/components/views/dialogs/devtools/RoomState.tsx +++ b/src/components/views/dialogs/devtools/RoomState.tsx @@ -38,7 +38,7 @@ export const StateEventEditor: React.FC = ({ mxEvent, onBack }) => ); const onSend = async ([eventType, stateKey]: string[], content: IContent): Promise => { - await cli.sendStateEvent(context.room.roomId, eventType, content, stateKey); + await cli.sendStateEvent(context.room.roomId, eventType as any, content, stateKey); }; const defaultContent = mxEvent ? stringify(mxEvent.getContent()) : undefined; diff --git a/src/components/views/dialogs/devtools/ServersInRoom.tsx b/src/components/views/dialogs/devtools/ServersInRoom.tsx index 4096a226262..bf980163eac 100644 --- a/src/components/views/dialogs/devtools/ServersInRoom.tsx +++ b/src/components/views/dialogs/devtools/ServersInRoom.tsx @@ -17,6 +17,7 @@ limitations under the License. import React, { useContext, useMemo } from "react"; import { EventType } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import BaseTool, { DevtoolsContext, IDevtoolsProps } from "./BaseTool"; import { _t } from "../../../../languageHandler"; @@ -27,7 +28,7 @@ const ServersInRoom: React.FC = ({ onBack }) => { const servers = useMemo>(() => { const servers: Record = {}; context.room.currentState.getStateEvents(EventType.RoomMember).forEach((ev) => { - if (ev.getContent().membership !== "join") return; // only count joined users + if (ev.getContent().membership !== KnownMembership.Join) return; // only count joined users const server = ev.getSender()!.split(":")[1]; servers[server] = (servers[server] ?? 0) + 1; }); diff --git a/src/components/views/dialogs/devtools/VerificationExplorer.tsx b/src/components/views/dialogs/devtools/VerificationExplorer.tsx index dd9a4b0ee21..aad806d27f6 100644 --- a/src/components/views/dialogs/devtools/VerificationExplorer.tsx +++ b/src/components/views/dialogs/devtools/VerificationExplorer.tsx @@ -16,8 +16,11 @@ limitations under the License. */ import React, { useContext, useEffect, useState } from "react"; -import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; -import { VerificationPhase as Phase, VerificationRequestEvent } from "matrix-js-sdk/src/crypto-api"; +import { + VerificationPhase as Phase, + VerificationRequest, + VerificationRequestEvent, +} from "matrix-js-sdk/src/crypto-api"; import { CryptoEvent } from "matrix-js-sdk/src/crypto"; import { useTypedEventEmitter, useTypedEventEmitterState } from "../../../../hooks/useEventEmitter"; @@ -67,13 +70,11 @@ const VerificationRequestExplorer: React.FC<{
{_t("devtools|phase")}
{PHASE_MAP[request.phase] ? _t(PHASE_MAP[request.phase]) : request.phase}
{_t("devtools|timeout")}
-
{Math.floor(timeout / 1000)}
+
{timeout === null ? _t("devtools|timeout_none") : Math.floor(timeout / 1000)}
{_t("devtools|methods")}
{request.methods && request.methods.join(", ")}
-
{_t("devtools|requester")}
-
{request.requestingUserId}
-
{_t("devtools|observe_only")}
-
{JSON.stringify(request.observeOnly)}
+
{_t("devtools|other_user")}
+
{request.otherUserId}
); diff --git a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx index 3d114abc30e..4b24182f6f6 100644 --- a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx +++ b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx @@ -17,8 +17,8 @@ limitations under the License. import { debounce } from "lodash"; import classNames from "classnames"; import React, { ChangeEvent, FormEvent } from "react"; -import { ISecretStorageKeyInfo } from "matrix-js-sdk/src/crypto/api"; import { logger } from "matrix-js-sdk/src/logger"; +import { SecretStorage } from "matrix-js-sdk/src/matrix"; import { MatrixClientPeg } from "../../../../MatrixClientPeg"; import Field from "../../elements/Field"; @@ -42,7 +42,7 @@ const VALIDATION_THROTTLE_MS = 200; export type KeyParams = { passphrase?: string; recoveryKey?: string }; interface IProps { - keyInfo: ISecretStorageKeyInfo; + keyInfo: SecretStorage.SecretStorageKeyDescription; checkPrivateKey: (k: KeyParams) => Promise; onFinished(result?: false | KeyParams): void; } @@ -278,8 +278,8 @@ export default class AccessSecretStorageDialog extends React.PureComponent + const resetLine = ( + {_t("encryption|reset_all_button", undefined, { a: (sub) => ( ), })} -
+ ); let content; @@ -366,7 +366,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent
@@ -430,11 +430,11 @@ export default class AccessSecretStorageDialog extends React.PureComponent
diff --git a/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx b/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx index edc2befe118..c535245e61d 100644 --- a/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx +++ b/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx @@ -16,9 +16,8 @@ limitations under the License. */ import React, { ChangeEvent } from "react"; -import { MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix"; +import { MatrixClient, MatrixError, SecretStorage } from "matrix-js-sdk/src/matrix"; import { IKeyBackupInfo, IKeyBackupRestoreResult } from "matrix-js-sdk/src/crypto/keybackup"; -import { ISecretStorageKeyInfo } from "matrix-js-sdk/src/crypto/api"; import { logger } from "matrix-js-sdk/src/logger"; import { MatrixClientPeg } from "../../../../MatrixClientPeg"; @@ -53,7 +52,7 @@ interface IProps { interface IState { backupInfo: IKeyBackupInfo | null; - backupKeyStored: Record | null; + backupKeyStored: Record | null; loading: boolean; loadError: boolean | null; restoreError: unknown | null; diff --git a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx index 04df85e79ff..ee42a59221d 100644 --- a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx +++ b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx @@ -26,6 +26,7 @@ import { HierarchyRoom, JoinRule, } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { normalize } from "matrix-js-sdk/src/utils"; import React, { ChangeEvent, RefObject, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import sanitizeHtml from "sanitize-html"; @@ -244,7 +245,7 @@ const findVisibleRooms = (cli: MatrixClient, msc3946ProcessDynamicPredecessor: b if (isLocalRoom(room)) return false; // TODO we may want to put invites in their own list - return room.getMyMembership() === "join" || room.getMyMembership() == "invite"; + return room.getMyMembership() === KnownMembership.Join || room.getMyMembership() == KnownMembership.Invite; }); }; @@ -675,7 +676,7 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n // world readable, a modal will appear asking you to register first. If // it is readable, the preview appears as normal. const showViewButton = - clientRoom?.getMyMembership() === "join" || + clientRoom?.getMyMembership() === KnownMembership.Join || (result.publicRoom.world_readable && !canAskToJoin(joinRule)) || cli.isGuest(); diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx index 74317041bd2..7a1b641791a 100644 --- a/src/components/views/elements/AppTile.tsx +++ b/src/components/views/elements/AppTile.tsx @@ -21,6 +21,7 @@ import React, { ContextType, createRef, CSSProperties, MutableRefObject, ReactNo import classNames from "classnames"; import { IWidget, MatrixCapabilities } from "matrix-widget-api"; import { Room, RoomEvent } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { logger } from "matrix-js-sdk/src/logger"; import { ApprovalOpts, WidgetLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle"; @@ -217,7 +218,10 @@ export default class AppTile extends React.Component { } private onMyMembership = (room: Room, membership: string): void => { - if ((membership === "leave" || membership === "ban") && room.roomId === this.props.room?.roomId) { + if ( + (membership === KnownMembership.Leave || membership === KnownMembership.Ban) && + room.roomId === this.props.room?.roomId + ) { this.onUserLeftRoom(); } }; @@ -249,8 +253,9 @@ export default class AppTile extends React.Component { private getNewState(newProps: IProps): IState { return { initialising: true, // True while we are mangling the widget URL - // True while the iframe content is loading - loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this.persistKey), + // Don't show loading at all if the widget is ready once the IFrame is loaded (waitForIframeLoad = true). + // We only need the loading screen if the widget sends a contentLoaded event (waitForIframeLoad = false). + loading: !this.props.waitForIframeLoad && !PersistedElement.isMounted(this.persistKey), // Assume that widget has permission to load if we are the user who // added it to the room, or if explicitly granted by the user hasPermissionToLoad: this.hasPermissionToLoad(newProps), @@ -312,7 +317,6 @@ export default class AppTile extends React.Component { if (this.props.room) { this.context.on(RoomEvent.MyMembership, this.onMyMembership); } - this.allowedWidgetsWatchRef = SettingsStore.watchSetting("allowedWidgets", null, this.onAllowedWidgetsChange); // Widget action listeners this.dispatcherRef = dis.register(this.onAction); @@ -352,7 +356,7 @@ export default class AppTile extends React.Component { } private setupSgListeners(): void { - this.sgWidget?.on("preparing", this.onWidgetPreparing); + this.sgWidget?.on("ready", this.onWidgetReady); this.sgWidget?.on("error:preparing", this.updateRequiresClient); // emits when the capabilities have been set up or changed this.sgWidget?.on("capabilitiesNotified", this.updateRequiresClient); @@ -360,7 +364,7 @@ export default class AppTile extends React.Component { private stopSgListeners(): void { if (!this.sgWidget) return; - this.sgWidget.off("preparing", this.onWidgetPreparing); + this.sgWidget?.off("ready", this.onWidgetReady); this.sgWidget.off("error:preparing", this.updateRequiresClient); this.sgWidget.off("capabilitiesNotified", this.updateRequiresClient); } @@ -446,8 +450,7 @@ export default class AppTile extends React.Component { this.sgWidget?.stopMessaging({ forceDestroy: true }); } - - private onWidgetPreparing = (): void => { + private onWidgetReady = (): void => { this.setState({ loading: false }); }; diff --git a/src/components/views/elements/EventListSummary.tsx b/src/components/views/elements/EventListSummary.tsx index 4d9bbe35c91..a1270427ccd 100644 --- a/src/components/views/elements/EventListSummary.tsx +++ b/src/components/views/elements/EventListSummary.tsx @@ -18,6 +18,7 @@ limitations under the License. import React, { ComponentProps, ReactNode } from "react"; import { MatrixEvent, RoomMember, EventType } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { _t } from "../../../languageHandler"; import { formatList } from "../../../utils/FormattingUtils"; @@ -416,12 +417,12 @@ export default class EventListSummary extends React.Component< case EventType.RoomMember: switch (e.mxEvent.getContent().membership) { - case "invite": + case KnownMembership.Invite: return TransitionType.Invited; - case "ban": + case KnownMembership.Ban: return TransitionType.Banned; - case "join": - if (e.mxEvent.getPrevContent().membership === "join") { + case KnownMembership.Join: + if (e.mxEvent.getPrevContent().membership === KnownMembership.Join) { if (e.mxEvent.getContent().displayname !== e.mxEvent.getPrevContent().displayname) { return TransitionType.ChangedName; } else if (e.mxEvent.getContent().avatar_url !== e.mxEvent.getPrevContent().avatar_url) { @@ -431,17 +432,17 @@ export default class EventListSummary extends React.Component< } else { return TransitionType.Joined; } - case "leave": + case KnownMembership.Leave: if (e.mxEvent.getSender() === e.mxEvent.getStateKey()) { - if (e.mxEvent.getPrevContent().membership === "invite") { + if (e.mxEvent.getPrevContent().membership === KnownMembership.Invite) { return TransitionType.InviteReject; } return TransitionType.Left; } switch (e.mxEvent.getPrevContent().membership) { - case "invite": + case KnownMembership.Invite: return TransitionType.InviteWithdrawal; - case "ban": + case KnownMembership.Ban: return TransitionType.Unbanned; // sender is not target and made the target leave, if not from invite/ban then this is a kick default: diff --git a/src/components/views/elements/PollCreateDialog.tsx b/src/components/views/elements/PollCreateDialog.tsx index 5049a3b0162..0fd1b7c21e9 100644 --- a/src/components/views/elements/PollCreateDialog.tsx +++ b/src/components/views/elements/PollCreateDialog.tsx @@ -23,6 +23,7 @@ import { M_POLL_KIND_UNDISCLOSED, M_POLL_START, IPartialEvent, + TimelineEvents, } from "matrix-js-sdk/src/matrix"; import { PollStartEvent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent"; @@ -166,8 +167,8 @@ export default class PollCreateDialog extends ScrollableBaseModal { // The name to annotate the selector with label?: string; - onChange(value: number, powerLevelKey: K extends undefined ? void : K): void; + onChange(value: number, powerLevelKey: K extends undefined ? void : K): void | Promise; // Optional key to pass as the second argument to `onChange` powerLevelKey: K extends undefined ? void : K; @@ -60,6 +60,7 @@ export default class PowerSelector extends React.C maxValue: Infinity, usersDefault: 0, }; + private unmounted = false; public constructor(props: Props) { super(props); @@ -84,6 +85,10 @@ export default class PowerSelector extends React.C } } + public componentWillUnmount(): void { + this.unmounted = true; + } + private initStateFromProps(): void { // This needs to be done now because levelRoleMap has translated strings const levelRoleMap = Roles.levelRoleMap(this.props.usersDefault); @@ -106,14 +111,20 @@ export default class PowerSelector extends React.C }); } - private onSelectChange = (event: React.ChangeEvent): void => { + private onSelectChange = async (event: React.ChangeEvent): Promise => { const isCustom = event.target.value === CUSTOM_VALUE; if (isCustom) { this.setState({ custom: true }); } else { const powerLevel = parseInt(event.target.value); - this.props.onChange(powerLevel, this.props.powerLevelKey); this.setState({ selectValue: powerLevel }); + try { + await this.props.onChange(powerLevel, this.props.powerLevelKey); + } catch { + if (this.unmounted) return; + // If the request failed, roll back the state of the selector. + this.initStateFromProps(); + } } }; @@ -121,12 +132,18 @@ export default class PowerSelector extends React.C this.setState({ customValue: parseInt(event.target.value) }); }; - private onCustomBlur = (event: React.FocusEvent): void => { + private onCustomBlur = async (event: React.FocusEvent): Promise => { event.preventDefault(); event.stopPropagation(); if (Number.isFinite(this.state.customValue)) { - this.props.onChange(this.state.customValue, this.props.powerLevelKey); + try { + await this.props.onChange(this.state.customValue, this.props.powerLevelKey); + } catch { + if (this.unmounted) return; + // If the request failed, roll back the state of the selector. + this.initStateFromProps(); + } } else { this.initStateFromProps(); // reset, invalid input } diff --git a/src/components/views/elements/RoomFacePile.tsx b/src/components/views/elements/RoomFacePile.tsx index 6ec333c4e55..0d1491edba6 100644 --- a/src/components/views/elements/RoomFacePile.tsx +++ b/src/components/views/elements/RoomFacePile.tsx @@ -16,6 +16,7 @@ limitations under the License. import React, { FC, HTMLAttributes, useContext } from "react"; import { Room, RoomMember } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { sortBy } from "lodash"; import { _t } from "../../../languageHandler"; @@ -38,7 +39,7 @@ interface IProps extends HTMLAttributes { const RoomFacePile: FC = ({ room, onlyKnownUsers = true, numShown = DEFAULT_NUM_FACES, ...props }) => { const cli = useContext(MatrixClientContext); - const isJoined = room.getMyMembership() === "join"; + const isJoined = room.getMyMembership() === KnownMembership.Join; let members = useRoomMembers(room); const count = members.length; diff --git a/src/components/views/elements/RoomTopic.tsx b/src/components/views/elements/RoomTopic.tsx index 35eb72515fa..8f69f3c61e1 100644 --- a/src/components/views/elements/RoomTopic.tsx +++ b/src/components/views/elements/RoomTopic.tsx @@ -36,7 +36,7 @@ interface IProps extends React.HTMLProps { room: Room; } -export default function RoomTopic({ room, ...props }: IProps): JSX.Element { +export default function RoomTopic({ room, className, ...props }: IProps): JSX.Element { const client = useContext(MatrixClientContext); const ref = useRef(null); @@ -110,15 +110,13 @@ export default function RoomTopic({ room, ...props }: IProps): JSX.Element { } }); - const className = classNames(props.className, "mx_RoomTopic"); - return ( void; - - // The current value of the slider - value: number; - - // The min and max of the slider - min: number; - max: number; - // The step size of the slider, can be a number or "any" - step: number | "any"; - - // A function for formatting the values - displayFunc: (value: number) => string; - - // Whether the slider is disabled - disabled: boolean; - - label: string; -} - -const THUMB_SIZE = 2.4; // em - -export default class Slider extends React.Component { - private get position(): number { - const { min, max, value } = this.props; - return Number(((value - min) * 100) / (max - min)); - } - - private onChange = (ev: ChangeEvent): void => { - this.props.onChange(parseInt(ev.target.value, 10)); - }; - - public render(): React.ReactNode { - let selection: JSX.Element | undefined; - - if (!this.props.disabled) { - const position = this.position; - selection = ( - - {this.props.value} - - ); - } - - return ( -
- - {selection} -
- ); - } -} diff --git a/src/components/views/elements/SyntaxHighlight.tsx b/src/components/views/elements/SyntaxHighlight.tsx index 3262ce5d6c0..caa7ceb51c8 100644 --- a/src/components/views/elements/SyntaxHighlight.tsx +++ b/src/components/views/elements/SyntaxHighlight.tsx @@ -16,22 +16,23 @@ limitations under the License. */ import React from "react"; -import hljs from "highlight.js"; -interface IProps { +import { useAsyncMemo } from "../../../hooks/useAsyncMemo"; + +interface Props { language?: string; children: string; } -export default class SyntaxHighlight extends React.PureComponent { - public render(): React.ReactNode { - const { children: content, language } = this.props; - const highlighted = language ? hljs.highlight(content, { language }) : hljs.highlightAuto(content); - - return ( -
-                
-            
- ); - } +export default function SyntaxHighlight({ children, language }: Props): JSX.Element { + const highlighted = useAsyncMemo(async () => { + const { default: highlight } = await import("highlight.js"); + return language ? highlight.highlight(children, { language }) : highlight.highlightAuto(children); + }, [language, children]); + + return ( +
+            {highlighted ?  : children}
+        
+ ); } diff --git a/src/components/views/elements/TruncatedList.tsx b/src/components/views/elements/TruncatedList.tsx index 074df5bfb26..4c6979832d2 100644 --- a/src/components/views/elements/TruncatedList.tsx +++ b/src/components/views/elements/TruncatedList.tsx @@ -36,6 +36,7 @@ interface IProps { // This will be inserted after the children. createOverflowElement: (overflowCount: number, totalCount: number) => React.ReactNode; children?: ReactNode; + id?: string; } export default class TruncatedList extends React.Component { @@ -86,7 +87,7 @@ export default class TruncatedList extends React.Component { const childNodes = this.getChildren(0, upperBound); return ( -
+
{childNodes} {overflowNode}
diff --git a/src/components/views/emojipicker/ReactionPicker.tsx b/src/components/views/emojipicker/ReactionPicker.tsx index a1c07b63d78..075a6e6cee7 100644 --- a/src/components/views/emojipicker/ReactionPicker.tsx +++ b/src/components/views/emojipicker/ReactionPicker.tsx @@ -108,7 +108,7 @@ class ReactionPicker extends React.Component { MatrixClientPeg.safeGet().sendEvent(this.props.mxEvent.getRoomId()!, EventType.Reaction, { "m.relates_to": { rel_type: RelationType.Annotation, - event_id: this.props.mxEvent.getId(), + event_id: this.props.mxEvent.getId()!, key: reaction, }, }); diff --git a/src/components/views/location/LocationButton.tsx b/src/components/views/location/LocationButton.tsx index fe9cf056b4e..aab595e1f97 100644 --- a/src/components/views/location/LocationButton.tsx +++ b/src/components/views/location/LocationButton.tsx @@ -24,7 +24,7 @@ import { aboveLeftOf, useContextMenu, MenuProps } from "../../structures/Context import { OverflowMenuContext } from "../rooms/MessageComposerButtons"; import LocationShareMenu from "./LocationShareMenu"; -interface IProps { +export interface IProps { roomId: string; sender: RoomMember; menuPosition?: MenuProps; diff --git a/src/components/views/location/Map.tsx b/src/components/views/location/Map.tsx index 8eda492b302..7d726da494b 100644 --- a/src/components/views/location/Map.tsx +++ b/src/components/views/location/Map.tsx @@ -139,7 +139,7 @@ const onGeolocateError = (e: GeolocationPositionError): void => { }); }; -interface MapProps { +export interface MapProps { id: string; interactive?: boolean; /** diff --git a/src/components/views/location/SmartMarker.tsx b/src/components/views/location/SmartMarker.tsx index f082bce8d80..27f6eb5d032 100644 --- a/src/components/views/location/SmartMarker.tsx +++ b/src/components/views/location/SmartMarker.tsx @@ -18,7 +18,8 @@ import React, { ReactNode, useCallback, useEffect, useState } from "react"; import * as maplibregl from "maplibre-gl"; import { RoomMember } from "matrix-js-sdk/src/matrix"; -import { createMarker, parseGeoUri } from "../../../utils/location"; +import { parseGeoUri } from "../../../utils/location"; +import { createMarker } from "../../../utils/location/map"; import Marker from "./Marker"; const useMapMarker = ( @@ -66,7 +67,7 @@ const useMapMarker = ( }; }; -interface SmartMarkerProps { +export interface SmartMarkerProps { map: maplibregl.Map; geoUri: string; id?: string; diff --git a/src/components/views/location/index.tsx b/src/components/views/location/index.tsx new file mode 100644 index 00000000000..3df2289d64b --- /dev/null +++ b/src/components/views/location/index.tsx @@ -0,0 +1,71 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Exports location components which touch maplibre-gs wrapped in React Suspense to enable code splitting + +import React, { ComponentProps, lazy, Suspense } from "react"; + +import Spinner from "../elements/Spinner"; + +const MapComponent = lazy(() => import("./Map")); + +export function Map(props: ComponentProps): JSX.Element { + return ( + }> + + + ); +} + +const LocationPickerComponent = lazy(() => import("./LocationPicker")); + +export function LocationPicker(props: ComponentProps): JSX.Element { + return ( + }> + + + ); +} + +const SmartMarkerComponent = lazy(() => import("./SmartMarker")); + +export function SmartMarker(props: ComponentProps): JSX.Element { + return ( + }> + + + ); +} + +const LocationButtonComponent = lazy(() => import("./LocationButton")); + +export function LocationButton(props: ComponentProps): JSX.Element { + return ( + }> + + + ); +} + +const LocationViewDialogComponent = lazy(() => import("./LocationViewDialog")); + +export function LocationViewDialog(props: ComponentProps): JSX.Element { + return ( + }> + + + ); +} diff --git a/src/components/views/location/shareLocation.ts b/src/components/views/location/shareLocation.ts index cf44a77c739..42a8e25c5b9 100644 --- a/src/components/views/location/shareLocation.ts +++ b/src/components/views/location/shareLocation.ts @@ -16,13 +16,13 @@ limitations under the License. import { MatrixClient, - IContent, IEventRelation, MatrixError, THREAD_RELATION_TYPE, ContentHelpers, LocationAssetType, } from "matrix-js-sdk/src/matrix"; +import { RoomMessageEventContent } from "matrix-js-sdk/src/types"; import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "../../../languageHandler"; @@ -146,7 +146,7 @@ export const shareLocation = timestamp, undefined, assetType, - ) as IContent; + ) as RoomMessageEventContent; await doMaybeLocalRoomAction( roomId, (actualRoomId: string) => client.sendMessage(actualRoomId, threadId, content), diff --git a/src/components/views/messages/MAudioBody.tsx b/src/components/views/messages/MAudioBody.tsx index 52cd2334afd..de30b65f724 100644 --- a/src/components/views/messages/MAudioBody.tsx +++ b/src/components/views/messages/MAudioBody.tsx @@ -17,12 +17,12 @@ limitations under the License. import React from "react"; import { logger } from "matrix-js-sdk/src/logger"; import { IContent } from "matrix-js-sdk/src/matrix"; +import { MediaEventContent } from "matrix-js-sdk/src/types"; import { Playback } from "../../../audio/Playback"; import InlineSpinner from "../elements/InlineSpinner"; import { _t } from "../../../languageHandler"; import AudioPlayer from "../audio_messages/AudioPlayer"; -import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent"; import MFileBody from "./MFileBody"; import { IBodyProps } from "./IBodyProps"; import { PlaybackManager } from "../../../audio/PlaybackManager"; @@ -67,7 +67,7 @@ export default class MAudioBody extends React.PureComponent // We should have a buffer to work with now: let's set it up // Note: we don't actually need a waveform to render an audio event, but voice messages do. - const content = this.props.mxEvent.getContent(); + const content = this.props.mxEvent.getContent(); const waveform = content?.["org.matrix.msc1767.audio"]?.waveform?.map((p: number) => p / 1024); // We should have a buffer to work with now: let's set it up diff --git a/src/components/views/messages/MBeaconBody.tsx b/src/components/views/messages/MBeaconBody.tsx index d5a8609376f..16f85559d3f 100644 --- a/src/components/views/messages/MBeaconBody.tsx +++ b/src/components/views/messages/MBeaconBody.tsx @@ -38,12 +38,11 @@ import { isSelfLocation, LocationShareError } from "../../../utils/location"; import { BeaconDisplayStatus, getBeaconDisplayStatus } from "../beacon/displayStatus"; import BeaconStatus from "../beacon/BeaconStatus"; import OwnBeaconStatus from "../beacon/OwnBeaconStatus"; -import Map from "../location/Map"; +import { Map, SmartMarker } from "../location"; import { MapError } from "../location/MapError"; import MapFallback from "../location/MapFallback"; -import SmartMarker from "../location/SmartMarker"; import { GetRelationsForEvent } from "../rooms/EventTile"; -import BeaconViewDialog from "../beacon/BeaconViewDialog"; +import { BeaconViewDialog } from "../beacon"; import { IBodyProps } from "./IBodyProps"; const useBeaconState = ( diff --git a/src/components/views/messages/MFileBody.tsx b/src/components/views/messages/MFileBody.tsx index ab0c1f505ec..12d4c804168 100644 --- a/src/components/views/messages/MFileBody.tsx +++ b/src/components/views/messages/MFileBody.tsx @@ -16,6 +16,7 @@ limitations under the License. import React, { AllHTMLAttributes, createRef } from "react"; import { logger } from "matrix-js-sdk/src/logger"; +import { MediaEventContent } from "matrix-js-sdk/src/types"; import { _t } from "../../../languageHandler"; import Modal from "../../../Modal"; @@ -23,7 +24,6 @@ import AccessibleButton from "../elements/AccessibleButton"; import { mediaFromContent } from "../../../customisations/Media"; import ErrorDialog from "../dialogs/ErrorDialog"; import { fileSize, presentableTextForFile } from "../../../utils/FileUtils"; -import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent"; import { IBodyProps } from "./IBodyProps"; import { FileDownloader } from "../../../utils/FileDownloader"; import TextWithTooltip from "../elements/TextWithTooltip"; @@ -128,8 +128,8 @@ export default class MFileBody extends React.Component { const media = mediaFromContent(this.props.mxEvent.getContent()); return media.srcHttp; } - private get content(): IMediaEventContent { - return this.props.mxEvent.getContent(); + private get content(): MediaEventContent { + return this.props.mxEvent.getContent(); } private get fileName(): string { diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index ff4d573e059..36f3a851687 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -21,6 +21,7 @@ import classNames from "classnames"; import { CSSTransition, SwitchTransition } from "react-transition-group"; import { logger } from "matrix-js-sdk/src/logger"; import { ClientEvent, ClientEventHandlerMap } from "matrix-js-sdk/src/matrix"; +import { ImageContent } from "matrix-js-sdk/src/types"; import { Tooltip } from "@vector-im/compound-web"; import MFileBody from "./MFileBody"; @@ -30,7 +31,6 @@ import SettingsStore from "../../../settings/SettingsStore"; import Spinner from "../elements/Spinner"; import { Media, mediaFromContent } from "../../../customisations/Media"; import { BLURHASH_FIELD, createThumbnail } from "../../../utils/image-media"; -import { ImageContent } from "../../../customisations/models/IMediaEventContent"; import ImageView from "../elements/ImageView"; import { IBodyProps } from "./IBodyProps"; import { ImageSize, suggestedSize as suggestedImageSize } from "../../../settings/enums/ImageSize"; diff --git a/src/components/views/messages/MImageReplyBody.tsx b/src/components/views/messages/MImageReplyBody.tsx index c6d61a7ba5b..ded0d374a80 100644 --- a/src/components/views/messages/MImageReplyBody.tsx +++ b/src/components/views/messages/MImageReplyBody.tsx @@ -15,9 +15,9 @@ limitations under the License. */ import React from "react"; +import { ImageContent } from "matrix-js-sdk/src/types"; import MImageBody from "./MImageBody"; -import { ImageContent } from "../../../customisations/models/IMediaEventContent"; const FORCED_IMAGE_HEIGHT = 44; diff --git a/src/components/views/messages/MLocationBody.tsx b/src/components/views/messages/MLocationBody.tsx index 8b2dc2e95d2..29c1c97e1a5 100644 --- a/src/components/views/messages/MLocationBody.tsx +++ b/src/components/views/messages/MLocationBody.tsx @@ -29,9 +29,7 @@ import { import MatrixClientContext from "../../../contexts/MatrixClientContext"; import TooltipTarget from "../elements/TooltipTarget"; import { Alignment } from "../elements/Tooltip"; -import LocationViewDialog from "../location/LocationViewDialog"; -import Map from "../location/Map"; -import SmartMarker from "../location/SmartMarker"; +import { SmartMarker, Map, LocationViewDialog } from "../location"; import { IBodyProps } from "./IBodyProps"; import { createReconnectedListener } from "../../../utils/connection"; diff --git a/src/components/views/messages/MPollBody.tsx b/src/components/views/messages/MPollBody.tsx index fcf92c7f627..d777ed9d779 100644 --- a/src/components/views/messages/MPollBody.tsx +++ b/src/components/views/messages/MPollBody.tsx @@ -25,6 +25,7 @@ import { M_POLL_KIND_DISCLOSED, M_POLL_RESPONSE, M_POLL_START, + TimelineEvents, } from "matrix-js-sdk/src/matrix"; import { RelatedRelations } from "matrix-js-sdk/src/models/related-relations"; import { PollStartEvent, PollAnswerSubevent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent"; @@ -225,14 +226,20 @@ export default class MPollBody extends React.Component { const response = PollResponseEvent.from([answerId], this.props.mxEvent.getId()!).serialize(); - this.context.sendEvent(this.props.mxEvent.getRoomId()!, response.type, response.content).catch((e: any) => { - console.error("Failed to submit poll response event:", e); - - Modal.createDialog(ErrorDialog, { - title: _t("poll|error_voting_title"), - description: _t("poll|error_voting_description"), + this.context + .sendEvent( + this.props.mxEvent.getRoomId()!, + response.type as keyof TimelineEvents, + response.content as TimelineEvents[keyof TimelineEvents], + ) + .catch((e: any) => { + console.error("Failed to submit poll response event:", e); + + Modal.createDialog(ErrorDialog, { + title: _t("poll|error_voting_title"), + description: _t("poll|error_voting_description"), + }); }); - }); this.setState({ selected: answerId }); } diff --git a/src/components/views/messages/MStickerBody.tsx b/src/components/views/messages/MStickerBody.tsx index 26c33e89fca..a002670b52b 100644 --- a/src/components/views/messages/MStickerBody.tsx +++ b/src/components/views/messages/MStickerBody.tsx @@ -16,10 +16,10 @@ limitations under the License. import React, { ComponentProps, ReactNode } from "react"; import { Tooltip } from "@vector-im/compound-web"; +import { MediaEventContent } from "matrix-js-sdk/src/types"; import MImageBody from "./MImageBody"; import { BLURHASH_FIELD } from "../../../utils/image-media"; -import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent"; export default class MStickerBody extends MImageBody { // Mostly empty to prevent default behaviour of MImageBody @@ -80,7 +80,7 @@ export default class MStickerBody extends MImageBody { return null; } - protected getBanner(content: IMediaEventContent): ReactNode { + protected getBanner(content: MediaEventContent): ReactNode { return null; // we don't need a banner, we have a tooltip } } diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx index df3ab6abbf9..be6ae4442ce 100644 --- a/src/components/views/messages/MVideoBody.tsx +++ b/src/components/views/messages/MVideoBody.tsx @@ -16,6 +16,7 @@ limitations under the License. import React, { ReactNode } from "react"; import { decode } from "blurhash"; +import { MediaEventContent } from "matrix-js-sdk/src/types"; import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "../../../languageHandler"; @@ -23,7 +24,6 @@ import SettingsStore from "../../../settings/SettingsStore"; import InlineSpinner from "../elements/InlineSpinner"; import { mediaFromContent } from "../../../customisations/Media"; import { BLURHASH_FIELD } from "../../../utils/image-media"; -import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent"; import { IBodyProps } from "./IBodyProps"; import MFileBody from "./MFileBody"; import { ImageSize, suggestedSize as suggestedVideoSize } from "../../../settings/enums/ImageSize"; @@ -62,7 +62,7 @@ export default class MVideoBody extends React.PureComponent } private getContentUrl(): string | undefined { - const content = this.props.mxEvent.getContent(); + const content = this.props.mxEvent.getContent(); // During export, the content url will point to the MSC, which will later point to a local url if (this.props.forExport) return content.file?.url ?? content.url; const media = mediaFromContent(content); @@ -82,7 +82,7 @@ export default class MVideoBody extends React.PureComponent // there's no need of thumbnail when the content is local if (this.props.forExport) return null; - const content = this.props.mxEvent.getContent(); + const content = this.props.mxEvent.getContent(); const media = mediaFromContent(content); if (media.isEncrypted && this.state.decryptedThumbnailUrl) { @@ -121,7 +121,7 @@ export default class MVideoBody extends React.PureComponent posterLoading: true, }); - const content = this.props.mxEvent.getContent(); + const content = this.props.mxEvent.getContent(); const media = mediaFromContent(content); if (media.hasThumbnail) { const image = new Image(); @@ -157,7 +157,7 @@ export default class MVideoBody extends React.PureComponent this.props.onHeightChanged?.(); } else { logger.log("NOT preloading video"); - const content = this.props.mxEvent.getContent(); + const content = this.props.mxEvent.getContent(); let mimetype = content?.info?.mimetype; diff --git a/src/components/views/messages/ReactionsRowButton.tsx b/src/components/views/messages/ReactionsRowButton.tsx index 99a1a6088b6..2737212d33b 100644 --- a/src/components/views/messages/ReactionsRowButton.tsx +++ b/src/components/views/messages/ReactionsRowButton.tsx @@ -16,7 +16,7 @@ limitations under the License. import React from "react"; import classNames from "classnames"; -import { MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { EventType, MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix"; import { mediaFromMxc } from "../../../customisations/Media"; import { _t } from "../../../languageHandler"; @@ -26,6 +26,7 @@ import ReactionsRowButtonTooltip from "./ReactionsRowButtonTooltip"; import AccessibleButton from "../elements/AccessibleButton"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { REACTION_SHORTCODE_KEY } from "./ReactionsRow"; + export interface IProps { // The event we're displaying reactions for mxEvent: MatrixEvent; @@ -62,10 +63,10 @@ export default class ReactionsRowButton extends React.PureComponent { pre.append(document.createElement("span")); } - private highlightCode(code: HTMLElement): void { + private async highlightCode(code: HTMLElement): Promise { + const { default: highlight } = await import("highlight.js"); + if (code.textContent && code.textContent.length > MAX_HIGHLIGHT_LENGTH) { console.log( "Code block is bigger than highlight limit (" + diff --git a/src/components/views/right_panel/TimelineCard.tsx b/src/components/views/right_panel/TimelineCard.tsx index 7e3ce09e500..7dfcd633070 100644 --- a/src/components/views/right_panel/TimelineCard.tsx +++ b/src/components/views/right_panel/TimelineCard.tsx @@ -23,6 +23,7 @@ import { EventTimelineSet, Thread, } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import BaseCard from "./BaseCard"; import ResizeNotifier from "../../../utils/ResizeNotifier"; @@ -217,7 +218,7 @@ export default class TimelineCard extends React.Component { const isUploading = ContentMessages.sharedInstance().getCurrentUploads(this.props.composerRelation).length > 0; const myMembership = this.props.room.getMyMembership(); - const showComposer = myMembership === "join"; + const showComposer = myMembership === KnownMembership.Join; return ( ; + if (member.membership !== KnownMembership.Invite && member.membership !== KnownMembership.Join) return <>; const onKick = async (): Promise => { if (isUpdating) return; // only allow one operation at a time @@ -647,17 +648,17 @@ export const RoomKickButton = ({ const commonProps = { member, action: room.isSpaceRoom() - ? member.membership === "invite" + ? member.membership === KnownMembership.Invite ? _t("user_info|disinvite_button_space") : _t("user_info|kick_button_space") - : member.membership === "invite" + : member.membership === KnownMembership.Invite ? _t("user_info|disinvite_button_room") : _t("user_info|kick_button_room"), title: - member.membership === "invite" + member.membership === KnownMembership.Invite ? _t("user_info|disinvite_button_room_name", { roomName: room.name }) : _t("user_info|kick_button_room_name", { roomName: room.name }), - askReason: member.membership === "join", + askReason: member.membership === KnownMembership.Join, danger: true, }; @@ -718,10 +719,10 @@ export const RoomKickButton = ({ }; const kickLabel = room.isSpaceRoom() - ? member.membership === "invite" + ? member.membership === KnownMembership.Invite ? _t("user_info|disinvite_button_space") : _t("user_info|kick_button_space") - : member.membership === "invite" + : member.membership === KnownMembership.Invite ? _t("user_info|disinvite_button_room") : _t("user_info|kick_button_room"); @@ -771,7 +772,7 @@ export const BanToggleButton = ({ }: Omit): JSX.Element => { const cli = useContext(MatrixClientContext); - const isBanned = member.membership === "ban"; + const isBanned = member.membership === KnownMembership.Ban; const onBanOrUnban = async (): Promise => { if (isUpdating) return; // only allow one operation at a time startUpdating(); @@ -808,7 +809,7 @@ export const BanToggleButton = ({ return ( !!myMember && !!theirMember && - theirMember.membership === "ban" && + theirMember.membership === KnownMembership.Ban && myMember.powerLevel > theirMember.powerLevel && child.currentState.hasSufficientPowerLevelFor("ban", myMember.powerLevel) ); @@ -820,7 +821,7 @@ export const BanToggleButton = ({ return ( !!myMember && !!theirMember && - theirMember.membership !== "ban" && + theirMember.membership !== KnownMembership.Ban && myMember.powerLevel > theirMember.powerLevel && child.currentState.hasSufficientPowerLevelFor("ban", myMember.powerLevel) ); @@ -903,7 +904,7 @@ const MuteToggleButton: React.FC = ({ const cli = useContext(MatrixClientContext); // Don't show the mute/unmute option if the user is not in the room - if (member.membership !== "join") return null; + if (member.membership !== KnownMembership.Join) return null; const muted = isMuted(member, powerLevels); const onMuteToggle = async (): Promise => { @@ -931,7 +932,7 @@ const MuteToggleButton: React.FC = ({ return; } - cli.setPowerLevel(roomId, target, level, powerLevelEvent) + cli.setPowerLevel(roomId, target, level) .then( () => { // NO-OP; rely on the m.room.member event coming down else we could @@ -1158,13 +1159,8 @@ export const PowerLevelEditor: React.FC<{ async (powerLevel: number) => { setSelectedPowerLevel(powerLevel); - const applyPowerChange = ( - roomId: string, - target: string, - powerLevel: number, - powerLevelEvent: MatrixEvent, - ): Promise => { - return cli.setPowerLevel(roomId, target, powerLevel, powerLevelEvent).then( + const applyPowerChange = (roomId: string, target: string, powerLevel: number): Promise => { + return cli.setPowerLevel(roomId, target, powerLevel).then( function () { // NO-OP; rely on the m.room.member event coming down else we could // get out of sync if we force setState here! @@ -1212,7 +1208,7 @@ export const PowerLevelEditor: React.FC<{ } } - await applyPowerChange(roomId, target, powerLevel, powerLevelEvent); + await applyPowerChange(roomId, target, powerLevel); }, [user.roomId, user.userId, cli, room], ); diff --git a/src/components/views/right_panel/VerificationPanel.tsx b/src/components/views/right_panel/VerificationPanel.tsx index c0872192ec9..5e9a17a8c5f 100644 --- a/src/components/views/right_panel/VerificationPanel.tsx +++ b/src/components/views/right_panel/VerificationPanel.tsx @@ -15,16 +15,17 @@ limitations under the License. */ import React from "react"; -import { verificationMethods } from "matrix-js-sdk/src/crypto"; -import { SCAN_QR_CODE_METHOD } from "matrix-js-sdk/src/crypto/verification/QRCode"; import { + ShowQrCodeCallbacks, + ShowSasCallbacks, VerificationPhase as Phase, VerificationRequest, VerificationRequestEvent, + VerifierEvent, } from "matrix-js-sdk/src/crypto-api"; -import { RoomMember, Device, User } from "matrix-js-sdk/src/matrix"; +import { Device, RoomMember, User } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; -import { ShowQrCodeCallbacks, ShowSasCallbacks, VerifierEvent } from "matrix-js-sdk/src/crypto-api/verification"; +import { VerificationMethod } from "matrix-js-sdk/src/types"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import VerificationQRCode from "../elements/crypto/VerificationQRCode"; @@ -85,8 +86,8 @@ export default class VerificationPanel extends React.PureComponent => { this.setState({ emojiButtonClicked: true }); - await this.props.request.startVerification(verificationMethods.SAS); + await this.props.request.startVerification(VerificationMethod.Sas); }; private onSasMatchesClick = (): void => { diff --git a/src/components/views/room_settings/AliasSettings.tsx b/src/components/views/room_settings/AliasSettings.tsx index 7b1bc1be287..3b2f294e5c1 100644 --- a/src/components/views/room_settings/AliasSettings.tsx +++ b/src/components/views/room_settings/AliasSettings.tsx @@ -15,8 +15,9 @@ limitations under the License. */ import React, { ChangeEvent, ContextType, createRef, SyntheticEvent } from "react"; -import { IContent, MatrixEvent, EventType } from "matrix-js-sdk/src/matrix"; +import { MatrixEvent, EventType } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; +import { RoomCanonicalAliasEventContent } from "matrix-js-sdk/src/types"; import EditableItemList from "../elements/EditableItemList"; import { _t } from "../../../languageHandler"; @@ -169,7 +170,7 @@ export default class AliasSettings extends React.Component { updatingCanonicalAlias: true, }); - const eventContent: IContent = { + const eventContent: RoomCanonicalAliasEventContent = { alt_aliases: this.state.altAliases, }; @@ -197,7 +198,7 @@ export default class AliasSettings extends React.Component { updatingCanonicalAlias: true, }); - const eventContent: IContent = {}; + const eventContent: RoomCanonicalAliasEventContent = {}; if (this.state.canonicalAlias) { eventContent["alias"] = this.state.canonicalAlias; diff --git a/src/components/views/room_settings/RoomProfileSettings.tsx b/src/components/views/room_settings/RoomProfileSettings.tsx index 73eb11a8d1d..15d03a4c5d8 100644 --- a/src/components/views/room_settings/RoomProfileSettings.tsx +++ b/src/components/views/room_settings/RoomProfileSettings.tsx @@ -225,7 +225,11 @@ export default class RoomProfileSettings extends React.Component if (this.state.canSetName || this.state.canSetTopic || this.state.canSetAvatar) { profileSettingsButtons = (
- + {_t("action|cancel")} diff --git a/src/components/views/rooms/EditMessageComposer.tsx b/src/components/views/rooms/EditMessageComposer.tsx index 2839732c135..fabca13a1c7 100644 --- a/src/components/views/rooms/EditMessageComposer.tsx +++ b/src/components/views/rooms/EditMessageComposer.tsx @@ -16,9 +16,10 @@ limitations under the License. import React, { createRef, KeyboardEvent } from "react"; import classNames from "classnames"; -import { EventStatus, IContent, MatrixEvent, Room, MsgType } from "matrix-js-sdk/src/matrix"; +import { EventStatus, MatrixEvent, Room, MsgType } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { Composer as ComposerEvent } from "@matrix-org/analytics-events/types/typescript/Composer"; +import { ReplacementEvent, RoomMessageEventContent, RoomMessageTextEventContent } from "matrix-js-sdk/src/types"; import { _t } from "../../../languageHandler"; import dis from "../../../dispatcher/dispatcher"; @@ -70,7 +71,11 @@ function getTextReplyFallback(mxEvent: MatrixEvent): string { } // exported for tests -export function createEditContent(model: EditorModel, editedEvent: MatrixEvent, replyToEvent?: MatrixEvent): IContent { +export function createEditContent( + model: EditorModel, + editedEvent: MatrixEvent, + replyToEvent?: MatrixEvent, +): RoomMessageEventContent { const isEmote = containsEmote(model); if (isEmote) { model = stripEmoteCommand(model); @@ -86,11 +91,11 @@ export function createEditContent(model: EditorModel, editedEvent: MatrixEvent, const body = textSerialize(model); - const newContent: IContent = { + const newContent: RoomMessageEventContent = { msgtype: isEmote ? MsgType.Emote : MsgType.Text, body: body, }; - const contentBody: IContent = { + const contentBody: RoomMessageTextEventContent & Omit, "m.relates_to"> = { "msgtype": newContent.msgtype, "body": `${plainPrefix} * ${body}`, "m.new_content": newContent, @@ -111,7 +116,7 @@ export function createEditContent(model: EditorModel, editedEvent: MatrixEvent, attachMentions(editedEvent.sender!.userId, contentBody, model, replyToEvent, editedEvent.getContent()); attachRelation(contentBody, { rel_type: "m.replace", event_id: editedEvent.getId() }); - return contentBody; + return contentBody as RoomMessageEventContent; } interface IEditMessageComposerProps extends MatrixClientProps { @@ -142,7 +147,7 @@ class EditMessageComposer extends React.Component(); if ( oldContent["msgtype"] === newContent["msgtype"] && oldContent["body"] === newContent["body"] && - oldContent["format"] === newContent["format"] && - oldContent["formatted_body"] === newContent["formatted_body"] + (oldContent as RoomMessageTextEventContent)["format"] === + (newContent as RoomMessageTextEventContent)["format"] && + (oldContent as RoomMessageTextEventContent)["formatted_body"] === + (newContent as RoomMessageTextEventContent)["formatted_body"] ) { return false; } @@ -318,7 +325,7 @@ class EditMessageComposer extends React.Component
{isRenderingNotification && room ? ( diff --git a/src/components/views/rooms/ExtraTile.tsx b/src/components/views/rooms/ExtraTile.tsx index 157bfc4d562..3bb3a21525a 100644 --- a/src/components/views/rooms/ExtraTile.tsx +++ b/src/components/views/rooms/ExtraTile.tsx @@ -52,7 +52,7 @@ export default function ExtraTile({ let badge: JSX.Element | null = null; if (notificationState) { - badge = ; + badge = ; } let name = displayName; diff --git a/src/components/views/rooms/MemberList.tsx b/src/components/views/rooms/MemberList.tsx index 639e1493b26..828f9691dae 100644 --- a/src/components/views/rooms/MemberList.tsx +++ b/src/components/views/rooms/MemberList.tsx @@ -31,6 +31,7 @@ import { EventType, ClientEvent, } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { throttle } from "lodash"; import { Button, Tooltip } from "@vector-im/compound-web"; import { Icon as UserAddIcon } from "@vector-im/compound-design-tokens/icons/user-add-solid.svg"; @@ -171,7 +172,11 @@ export default class MemberList extends React.Component { }; private onMyMembership = (room: Room, membership: string, oldMembership?: string): void => { - if (room.roomId === this.props.roomId && membership === "join" && oldMembership !== "join") { + if ( + room.roomId === this.props.roomId && + membership === KnownMembership.Join && + oldMembership !== KnownMembership.Join + ) { // we just joined the room, load the member list this.updateListNow(true); } @@ -363,7 +368,7 @@ export default class MemberList extends React.Component { const room = cli.getRoom(this.props.roomId); let inviteButton: JSX.Element | undefined; - if (room?.getMyMembership() === "join" && shouldShowComponent(UIComponent.InviteUsers)) { + if (room?.getMyMembership() === KnownMembership.Join && shouldShowComponent(UIComponent.InviteUsers)) { const inviteButtonText = room.isSpaceRoom() ? _t("space|invite_this_space") : _t("room|invite_this_room"); const button = ( diff --git a/src/components/views/rooms/MessageComposerButtons.tsx b/src/components/views/rooms/MessageComposerButtons.tsx index d9d364a2110..7f23efbce2b 100644 --- a/src/components/views/rooms/MessageComposerButtons.tsx +++ b/src/components/views/rooms/MessageComposerButtons.tsx @@ -24,7 +24,7 @@ import { CollapsibleButton } from "./CollapsibleButton"; import { MenuProps } from "../../structures/ContextMenu"; import dis from "../../../dispatcher/dispatcher"; import ErrorDialog from "../dialogs/ErrorDialog"; -import LocationButton from "../location/LocationButton"; +import { LocationButton } from "../location"; import Modal from "../../../Modal"; import PollCreateDialog from "../elements/PollCreateDialog"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx index 4cc9028d487..70ea6c23a2a 100644 --- a/src/components/views/rooms/NewRoomIntro.tsx +++ b/src/components/views/rooms/NewRoomIntro.tsx @@ -16,6 +16,7 @@ limitations under the License. import React, { useContext } from "react"; import { EventType, Room, User, MatrixClient } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import RoomContext from "../../../contexts/RoomContext"; @@ -112,7 +113,7 @@ const NewRoomIntro: React.FC = () => { ); } else { - const inRoom = room && room.getMyMembership() === "join"; + const inRoom = room && room.getMyMembership() === KnownMembership.Join; const topic = room.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic; const canAddTopic = inRoom && room.currentState.maySendStateEvent(EventType.RoomTopic, cli.getSafeUserId()); diff --git a/src/components/views/rooms/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge.tsx index d152ab6a626..20ee53d95d6 100644 --- a/src/components/views/rooms/NotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge.tsx @@ -28,10 +28,10 @@ interface IProps { notification: NotificationState; /** - * If true, the badge will show a count if at all possible. This is typically - * used to override the user's preference for things like room sublists. + * If true, show nothing if the notification would only cause a dot to be shown rather than + * a badge. That is: only display badges and not dots. Default: false. */ - forceCount?: boolean; + hideIfDot?: boolean; /** * The room ID, if any, the badge represents. @@ -48,7 +48,7 @@ interface IClickableProps extends IProps, React.InputHTMLAttributes { } interface IState { - showCounts: boolean; // whether to show counts. Independent of props.forceCount + showCounts: boolean; // whether to show counts. } export default class NotificationBadge extends React.PureComponent, IState> { @@ -97,11 +97,12 @@ export default class NotificationBadge extends React.PureComponent = { diff --git a/src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx b/src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx index 69f756b3b7e..825192b82c3 100644 --- a/src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx @@ -28,7 +28,12 @@ interface Props { count: number; level: NotificationLevel; knocked?: boolean; - type?: "badge" | "dot"; + /** + * If true, where we would normally show a badge, we instead show a dot. No numeric count will + * be displayed (but may affect whether the the dot is displayed). See class doc + * for the difference between the two. + */ + forceDot?: boolean; } interface ClickableProps extends Props { @@ -39,8 +44,17 @@ interface ClickableProps extends Props { tabIndex?: number; } +/** + * A notification indicator that conveys what activity / notifications the user has in whatever + * context it is being used. + * + * Can either be a 'badge': a small circle with a number in it (the 'count'), or a 'dot': a smaller, empty circle. + * The two can be used to convey the same meaning but in different contexts, for example: for unread + * 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, type = "badge", ...props }, ref) => { + ({ symbol, count, level, knocked, forceDot = false, ...props }, ref) => { const hideBold = useSettingValue("feature_hidebold"); // Don't show a badge if we don't need to @@ -56,15 +70,29 @@ export const StatelessNotificationBadge = forwardRef= NotificationLevel.Highlight, - mx_NotificationBadge_dot: (isEmptyBadge && !knocked) || type === "dot", - mx_NotificationBadge_knocked: knocked, - mx_NotificationBadge_2char: type === "badge" && symbol && symbol.length > 0 && symbol.length < 3, - mx_NotificationBadge_3char: type === "badge" && symbol && symbol.length > 2, + "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) { diff --git a/src/components/views/rooms/NotificationBadge/UnreadNotificationBadge.tsx b/src/components/views/rooms/NotificationBadge/UnreadNotificationBadge.tsx index 5864a63be01..c3c8cf7df89 100644 --- a/src/components/views/rooms/NotificationBadge/UnreadNotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge/UnreadNotificationBadge.tsx @@ -23,11 +23,15 @@ import { StatelessNotificationBadge } from "./StatelessNotificationBadge"; interface Props { room?: Room; threadId?: string; - type?: "badge" | "dot"; + /** + * If true, where we would normally show a badge, we instead show a dot. No numeric count will + * be displayed. + */ + forceDot?: boolean; } -export function UnreadNotificationBadge({ room, threadId, type }: Props): JSX.Element { +export function UnreadNotificationBadge({ room, threadId, forceDot }: Props): JSX.Element { const { symbol, count, level } = useUnreadNotifications(room, threadId); - return ; + return ; } diff --git a/src/components/views/rooms/RoomBreadcrumbs.tsx b/src/components/views/rooms/RoomBreadcrumbs.tsx index eca8da46240..cd31dbd8e79 100644 --- a/src/components/views/rooms/RoomBreadcrumbs.tsx +++ b/src/components/views/rooms/RoomBreadcrumbs.tsx @@ -61,7 +61,7 @@ const RoomBreadcrumbTile: React.FC<{ room: Room; onClick: (ev: ButtonEvent) => v room={room} size="32px" displayBadge={true} - forceCount={true} + hideIfDot={true} tooltipProps={{ tabIndex: isActive ? 0 : -1 }} /> diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index 6d1ea9f8eb6..8ca26ebcdea 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -18,6 +18,7 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"; import { Body as BodyText, Button, IconButton, Menu, MenuItem, Tooltip } from "@vector-im/compound-web"; import { Icon as VideoCallIcon } from "@vector-im/compound-design-tokens/icons/video-call-solid.svg"; import { Icon as VoiceCallIcon } from "@vector-im/compound-design-tokens/icons/voice-call.svg"; +import { Icon as ExternalLinkIcon } from "@vector-im/compound-design-tokens/icons/link.svg"; import { Icon as CloseCallIcon } from "@vector-im/compound-design-tokens/icons/close.svg"; import { Icon as ThreadsIcon } from "@vector-im/compound-design-tokens/icons/threads-solid.svg"; import { Icon as NotificationsIcon } from "@vector-im/compound-design-tokens/icons/notifications-solid.svg"; @@ -26,6 +27,7 @@ import { Icon as ErrorIcon } from "@vector-im/compound-design-tokens/icons/error import { Icon as PublicIcon } from "@vector-im/compound-design-tokens/icons/public.svg"; import { EventType, JoinRule, type Room } from "matrix-js-sdk/src/matrix"; import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle"; +import { logger } from "matrix-js-sdk/src/logger"; import { useRoomName } from "../../../hooks/useRoomName"; import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases"; @@ -54,6 +56,8 @@ import { VideoRoomChatButton } from "./RoomHeader/VideoRoomChatButton"; import { RoomKnocksBar } from "./RoomKnocksBar"; import { isVideoRoom } from "../../../utils/video-rooms"; import { notificationLevelToIndicator } from "../../../utils/notifications"; +import Modal from "../../../Modal"; +import ShareDialog from "../dialogs/ShareDialog"; export default function RoomHeader({ room, @@ -78,6 +82,8 @@ export default function RoomHeader({ videoCallClick, toggleCallMaximized: toggleCall, isViewingCall, + generateCallLink, + canGenerateCallLink, isConnectedToCall, hasActiveCallSession, callOptions, @@ -118,6 +124,20 @@ export default function RoomHeader({ const videoClick = useCallback((ev) => videoCallClick(ev, callOptions[0]), [callOptions, videoCallClick]); + const shareClick = useCallback(() => { + try { + // generateCallLink throws if the permissions are not met + const target = generateCallLink(); + Modal.createDialog(ShareDialog, { + target, + customTitle: _t("share|share_call"), + subtitle: _t("share|share_call_subtitle"), + }); + } catch (e) { + logger.error("Could not generate call link.", e); + } + }, [generateCallLink]); + const toggleCallButton = ( @@ -125,7 +145,13 @@ export default function RoomHeader({ ); - + const createExternalLinkButton = ( + + + + + + ); const joinCallButton = ( + + ); +} + +/** + * Sort the users by power level, then by name + * @param userA + * @param userB + * @param userLevels + */ +function sortUser(userA: string, userB: string, userLevels: PowerLevelSelectorProps["userLevels"]): number { + const powerLevelDiff = userLevels[userA] - userLevels[userB]; + return powerLevelDiff !== 0 ? powerLevelDiff : compare(userA.toLocaleLowerCase(), userB.toLocaleLowerCase()); +} diff --git a/src/components/views/settings/ProfileSettings.tsx b/src/components/views/settings/ProfileSettings.tsx index 92024659242..a9a33ec2887 100644 --- a/src/components/views/settings/ProfileSettings.tsx +++ b/src/components/views/settings/ProfileSettings.tsx @@ -209,7 +209,7 @@ export default class ProfileSettings extends React.Component<{}, IState> {
{_t("action|cancel")} diff --git a/src/components/views/settings/SecureBackupPanel.tsx b/src/components/views/settings/SecureBackupPanel.tsx index 4a379508c76..520379cc7b0 100644 --- a/src/components/views/settings/SecureBackupPanel.tsx +++ b/src/components/views/settings/SecureBackupPanel.tsx @@ -328,14 +328,14 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { ); actions.push( - + {restoreButtonCaption} , ); if (!isSecureBackupRequired(MatrixClientPeg.safeGet())) { actions.push( - + {_t("settings|security|delete_backup")} , ); @@ -350,7 +350,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { ); actions.push( - + {_t("encryption|setup_secure_backup|title")} , ); @@ -358,7 +358,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { if (secretStorageKeyInAccount) { actions.push( - + {_t("action|reset")} , ); diff --git a/src/components/views/settings/SetIdServer.tsx b/src/components/views/settings/SetIdServer.tsx index bdf4ee837e2..bd75204e740 100644 --- a/src/components/views/settings/SetIdServer.tsx +++ b/src/components/views/settings/SetIdServer.tsx @@ -137,7 +137,7 @@ export default class SetIdServer extends React.Component {
); } else if (this.state.error) { - return {this.state.error}; + return {this.state.error}; } else { return null; } @@ -226,7 +226,7 @@ export default class SetIdServer extends React.Component { title: _t("terms|identity_server_no_terms_title"), description: (
- {_t("identity_server|no_terms")} + {_t("identity_server|no_terms")}  {_t("terms|identity_server_no_terms_description_2")}
), diff --git a/src/components/views/settings/UpdateCheckButton.tsx b/src/components/views/settings/UpdateCheckButton.tsx index d0654a743e6..9793999bd87 100644 --- a/src/components/views/settings/UpdateCheckButton.tsx +++ b/src/components/views/settings/UpdateCheckButton.tsx @@ -85,7 +85,7 @@ const UpdateCheckButton: React.FC = () => { return ( - + {_t("update|check_action")} {suffix} diff --git a/src/components/views/settings/account/PhoneNumbers.tsx b/src/components/views/settings/account/PhoneNumbers.tsx index 27c2bdf104c..b037643bc03 100644 --- a/src/components/views/settings/account/PhoneNumbers.tsx +++ b/src/components/views/settings/account/PhoneNumbers.tsx @@ -16,7 +16,7 @@ limitations under the License. */ import React from "react"; -import { ThreepidMedium } from "matrix-js-sdk/src/matrix"; +import { IAuthData, ThreepidMedium } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { _t, UserFriendlyError } from "../../../../languageHandler"; @@ -216,7 +216,7 @@ export default class PhoneNumbers extends React.Component { const address = this.state.verifyMsisdn; this.state.addTask ?.haveMsisdnToken(token) - .then(([finished] = []) => { + .then(([finished]: [success?: boolean, result?: IAuthData | Error | null] = []) => { let newPhoneNumber = this.state.newPhoneNumber; if (finished !== false) { const msisdns = [...this.props.msisdns, { address, medium: ThreepidMedium.Phone }]; diff --git a/src/components/views/settings/devices/LoginWithQRSection.tsx b/src/components/views/settings/devices/LoginWithQRSection.tsx index 036597cfe9c..c85b80c3a7a 100644 --- a/src/components/views/settings/devices/LoginWithQRSection.tsx +++ b/src/components/views/settings/devices/LoginWithQRSection.tsx @@ -22,6 +22,7 @@ import { Capabilities, IClientWellKnown, } from "matrix-js-sdk/src/matrix"; +import { Icon as QrCodeIcon } from "@vector-im/compound-design-tokens/icons/qr-code.svg"; import { _t } from "../../../../languageHandler"; import AccessibleButton from "../../elements/AccessibleButton"; @@ -62,6 +63,7 @@ export default class LoginWithQRSection extends React.Component { {_t("settings|sessions|sign_in_with_qr_description")}

+ {_t("settings|sessions|sign_in_with_qr_button")}
diff --git a/src/components/views/settings/devices/deleteDevices.tsx b/src/components/views/settings/devices/deleteDevices.tsx index c07757f7a9b..e42dbe10b9b 100644 --- a/src/components/views/settings/devices/deleteDevices.tsx +++ b/src/components/views/settings/devices/deleteDevices.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import { MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix"; -import { IAuthDict, IAuthData } from "matrix-js-sdk/src/interactive-auth"; +import { AuthDict, IAuthData } from "matrix-js-sdk/src/interactive-auth"; import { _t } from "../../../../languageHandler"; import Modal from "../../../../Modal"; @@ -25,7 +25,7 @@ import InteractiveAuthDialog from "../../dialogs/InteractiveAuthDialog"; const makeDeleteRequest = (matrixClient: MatrixClient, deviceIds: string[]) => - async (auth: IAuthDict | null): Promise => { + async (auth: AuthDict | null): Promise => { return matrixClient.deleteMultipleDevices(deviceIds, auth ?? undefined); }; diff --git a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.tsx index 8e990d7d33c..7b260e3a7e6 100644 --- a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.tsx @@ -16,6 +16,7 @@ limitations under the License. import React, { ContextType } from "react"; import { Room } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { _t } from "../../../../../languageHandler"; import RoomProfileSettings from "../../../room_settings/RoomProfileSettings"; @@ -73,7 +74,7 @@ export default class GeneralRoomSettingsTab extends React.Component diff --git a/src/components/views/settings/tabs/room/PeopleRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/PeopleRoomSettingsTab.tsx index 418addfaf96..22c2bc471f1 100644 --- a/src/components/views/settings/tabs/room/PeopleRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/PeopleRoomSettingsTab.tsx @@ -15,6 +15,7 @@ limitations under the License. */ import { EventTimeline, MatrixError, Room, RoomMember, RoomStateEvent } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import React, { useCallback, useState, VFC } from "react"; import { Icon as CheckIcon } from "../../../../../../res/img/feather-customised/check.svg"; @@ -145,7 +146,7 @@ export const PeopleRoomSettingsTab: VFC<{ room: Room }> = ({ room }) => { const knockMembers = useTypedEventEmitterState( room, RoomStateEvent.Update, - useCallback(() => room.getMembersWithMembership("knock"), [room]), + useCallback(() => room.getMembersWithMembership(KnownMembership.Knock), [room]), ); return ( diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx index 7bce2ccb17f..2197cad3dff 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx @@ -18,7 +18,7 @@ import React from "react"; import { EventType, RoomMember, RoomState, RoomStateEvent, Room, IContent } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { throttle, get } from "lodash"; -import { compare } from "matrix-js-sdk/src/utils"; +import { KnownMembership, RoomPowerLevelsEventContent } from "matrix-js-sdk/src/types"; import { _t, _td, TranslationKey } from "../../../../../languageHandler"; import AccessibleButton from "../../../elements/AccessibleButton"; @@ -34,6 +34,7 @@ import { AddPrivilegedUsers } from "../../AddPrivilegedUsers"; import SettingsTab from "../SettingsTab"; import { SettingsSection } from "../../shared/SettingsSection"; import MatrixClientContext from "../../../../../contexts/MatrixClientContext"; +import { PowerLevelSelector } from "../../PowerLevelSelector"; interface IEventShowOpts { isState?: boolean; @@ -174,11 +175,11 @@ export default class RolesRoomSettingsTab extends React.Component { } } - private onPowerLevelsChanged = (value: number, powerLevelKey: string): void => { + private onPowerLevelsChanged = async (value: number, powerLevelKey: string): Promise => { const client = this.context; const room = this.props.room; const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, ""); - let plContent = plEvent?.getContent() ?? {}; + let plContent = plEvent?.getContent() ?? {}; // Clone the power levels just in case plContent = Object.assign({}, plContent); @@ -192,7 +193,7 @@ export default class RolesRoomSettingsTab extends React.Component { } else { const keyPath = powerLevelKey.split("."); let parentObj: IContent = {}; - let currentObj = plContent; + let currentObj: IContent = plContent; for (const key of keyPath) { if (!currentObj[key]) { currentObj[key] = {}; @@ -203,21 +204,26 @@ export default class RolesRoomSettingsTab extends React.Component { parentObj[keyPath[keyPath.length - 1]] = value; } - client.sendStateEvent(this.props.room.roomId, EventType.RoomPowerLevels, plContent).catch((e) => { + try { + await client.sendStateEvent(this.props.room.roomId, EventType.RoomPowerLevels, plContent); + } catch (e) { logger.error(e); Modal.createDialog(ErrorDialog, { title: _t("room_settings|permissions|error_changing_pl_reqs_title"), description: _t("room_settings|permissions|error_changing_pl_reqs_description"), }); - }); + + // Rethrow so that the PowerSelector can roll back + throw e; + } }; - private onUserPowerLevelChanged = (value: number, powerLevelKey: string): void => { + private onUserPowerLevelChanged = async (value: number, powerLevelKey: string): Promise => { const client = this.context; const room = this.props.room; const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, ""); - let plContent = plEvent?.getContent() ?? {}; + let plContent = plEvent?.getContent() ?? {}; // Clone the power levels just in case plContent = Object.assign({}, plContent); @@ -226,14 +232,16 @@ export default class RolesRoomSettingsTab extends React.Component { if (!plContent["users"]) plContent["users"] = {}; plContent["users"][powerLevelKey] = value; - client.sendStateEvent(this.props.room.roomId, EventType.RoomPowerLevels, plContent).catch((e) => { + try { + await client.sendStateEvent(this.props.room.roomId, EventType.RoomPowerLevels, plContent); + } catch (e) { logger.error(e); Modal.createDialog(ErrorDialog, { title: _t("room_settings|permissions|error_changing_pl_title"), description: _t("room_settings|permissions|error_changing_pl_description"), }); - }); + } }; public render(): React.ReactNode { @@ -342,68 +350,32 @@ export default class RolesRoomSettingsTab extends React.Component { let privilegedUsersSection =
{_t("room_settings|permissions|no_privileged_users")}
; let mutedUsersSection; if (Object.keys(userLevels).length) { - const privilegedUsers: JSX.Element[] = []; - const mutedUsers: JSX.Element[] = []; - - Object.keys(userLevels).forEach((user) => { - if (!Number.isInteger(userLevels[user])) return; - const isMe = user === client.getUserId(); - const canChange = canChangeLevels && (userLevels[user] < currentUserLevel || isMe); - if (userLevels[user] > defaultUserLevel) { - // privileged - privilegedUsers.push( - , - ); - } else if (userLevels[user] < defaultUserLevel) { - // muted - mutedUsers.push( - , - ); - } - }); + privilegedUsersSection = ( + userLevels[user] > defaultUserLevel} + > +
{_t("room_settings|permissions|no_privileged_users")}
+
+ ); - // comparator for sorting PL users lexicographically on PL descending, MXID ascending. (case-insensitive) - const comparator = (a: JSX.Element, b: JSX.Element): number => { - const aKey = a.key as string; - const bKey = b.key as string; - const plDiff = userLevels[bKey] - userLevels[aKey]; - return plDiff !== 0 ? plDiff : compare(aKey.toLocaleLowerCase(), bKey.toLocaleLowerCase()); - }; - - privilegedUsers.sort(comparator); - mutedUsers.sort(comparator); - - if (privilegedUsers.length) { - privilegedUsersSection = ( - - {privilegedUsers} - - ); - } - if (mutedUsers.length) { - mutedUsersSection = ( - - {mutedUsers} - - ); - } + mutedUsersSection = ( + userLevels[user] < defaultUserLevel} + /> + ); } - const banned = room.getMembersWithMembership("ban"); + const banned = room.getMembersWithMembership(KnownMembership.Ban); let bannedUsersSection: JSX.Element | undefined; if (banned?.length) { const canBanUsers = currentUserLevel >= banLevel; diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx index 79eae267c22..3009c81a17f 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx @@ -175,6 +175,7 @@ export default class GeneralUserSettingsTab extends React.Component } > - + {_t("bug_reporting|submit_debug_logs")} @@ -316,7 +316,7 @@ export default class HelpUserSettingsTab extends React.Component - + {_t("setting|help_about|clear_cache_reload")} diff --git a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx index 0b345322180..7ec29d43662 100644 --- a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx @@ -256,7 +256,7 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState> - {_t("labs_mjolnir|advanced_warning")} + {_t("labs_mjolnir|advanced_warning")}

{_t("labs_mjolnir|explainer_1", { brand }, { code: (s) => {s} })}

{_t("labs_mjolnir|explainer_2")}

@@ -289,7 +289,7 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState> heading={_t("labs_mjolnir|lists_heading")} description={ <> - {_t("labs_mjolnir|lists_description_1")} + {_t("labs_mjolnir|lists_description_1")}   {_t("labs_mjolnir|lists_description_2")} diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx index 74511dfa4ab..81ff32e38ee 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx @@ -17,6 +17,7 @@ limitations under the License. import React, { ReactNode } from "react"; import { sleep } from "matrix-js-sdk/src/utils"; import { Room, RoomEvent } from "matrix-js-sdk/src/matrix"; +import { KnownMembership, Membership } from "matrix-js-sdk/src/types"; import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "../../../../../languageHandler"; @@ -121,12 +122,12 @@ export default class SecurityUserSettingsTab extends React.Component { + private onMyMembership = (room: Room, membership: Membership): void => { if (room.isSpaceRoom()) { return; } - if (membership === "invite") { + if (membership === KnownMembership.Invite) { this.addInvitedRoom(room); } else if (this.state.invitedRoomIds.has(room.roomId)) { // The user isn't invited anymore @@ -167,7 +168,7 @@ export default class SecurityUserSettingsTab extends React.Component { - return r.hasMembershipState(MatrixClientPeg.safeGet().getUserId()!, "invite"); + return r.hasMembershipState(MatrixClientPeg.safeGet().getUserId()!, KnownMembership.Invite); }); }; @@ -256,14 +257,14 @@ export default class SecurityUserSettingsTab extends React.Component {_t("settings|security|bulk_options_accept_all_invites", { invitedRooms: invitedRoomIds.size })} {_t("settings|security|bulk_options_reject_all_invites", { invitedRooms: invitedRoomIds.size })} diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index bc06103255c..fc215f069ae 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -173,7 +173,10 @@ const SessionManagerTab: React.FC = () => { * delegated auth provider. * See https://github.com/matrix-org/matrix-spec-proposals/pull/3824 */ - const delegatedAuthAccountUrl = sdkContext.oidcClientStore.accountManagementEndpoint; + const delegatedAuthAccountUrl = useAsyncMemo(async () => { + await sdkContext.oidcClientStore.readyPromise; // wait for the store to be ready + return sdkContext.oidcClientStore.accountManagementEndpoint; + }, [sdkContext.oidcClientStore]); const disableMultipleSignout = !!delegatedAuthAccountUrl; const userId = matrixClient?.getUserId(); diff --git a/src/components/views/settings/tabs/user/SidebarUserSettingsTab.tsx b/src/components/views/settings/tabs/user/SidebarUserSettingsTab.tsx index 4d6cf9a40f5..7211fa863fe 100644 --- a/src/components/views/settings/tabs/user/SidebarUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/SidebarUserSettingsTab.tsx @@ -14,7 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ChangeEvent } from "react"; +import React, { ChangeEvent, useMemo } from "react"; +import { Icon as CameraCircle } from "@vector-im/compound-design-tokens/icons/video-call-solid.svg"; import { Icon as HomeIcon } from "../../../../../../res/img/element-icons/home.svg"; import { Icon as FavoriteIcon } from "../../../../../../res/img/element-icons/roomlist/favorite.svg"; @@ -30,6 +31,7 @@ import PosthogTrackers from "../../../../../PosthogTrackers"; import SettingsTab from "../SettingsTab"; import { SettingsSection } from "../../shared/SettingsSection"; import SettingsSubsection, { SettingsSubsectionText } from "../../shared/SettingsSubsection"; +import SdkConfig from "../../../../../SdkConfig"; type InteractionName = "WebSettingsSidebarTabSpacesCheckbox" | "WebQuickSettingsPinToSidebarCheckbox"; @@ -44,7 +46,14 @@ export const onMetaSpaceChangeFactory = PosthogTrackers.trackInteraction( interactionName, e, - [MetaSpace.Home, null, MetaSpace.Favourites, MetaSpace.People, MetaSpace.Orphans].indexOf(metaSpace), + [ + MetaSpace.Home, + null, + MetaSpace.Favourites, + MetaSpace.People, + MetaSpace.Orphans, + MetaSpace.VideoRooms, + ].indexOf(metaSpace), ); }; @@ -54,8 +63,15 @@ const SidebarUserSettingsTab: React.FC = () => { [MetaSpace.Favourites]: favouritesEnabled, [MetaSpace.People]: peopleEnabled, [MetaSpace.Orphans]: orphansEnabled, + [MetaSpace.VideoRooms]: videoRoomsEnabled, } = useSettingValue>("Spaces.enabledMetaSpaces"); const allRoomsInHome = useSettingValue("Spaces.allRoomsInHome"); + const guestSpaUrl = useMemo(() => { + return SdkConfig.get("element_call").guest_spa_url; + }, []); + const conferenceSubsectionText = + _t("settings|sidebar|metaspaces_video_rooms_description") + + (guestSpaUrl ? " " + _t("settings|sidebar|metaspaces_video_rooms_description_invite_extension") : ""); const onAllRoomsInHomeToggle = async (event: ChangeEvent): Promise => { await SettingsStore.setValue("Spaces.allRoomsInHome", null, SettingLevel.ACCOUNT, event.target.checked); @@ -140,6 +156,22 @@ const SidebarUserSettingsTab: React.FC = () => { {_t("settings|sidebar|metaspaces_orphans_description")} + {SettingsStore.getValue("feature_video_rooms") && ( + + + + {_t("settings|sidebar|metaspaces_video_rooms")} + + {conferenceSubsectionText} + + )}
diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index 5b1756244b8..429a18e1344 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -209,6 +209,20 @@ const OrphansButton: React.FC = ({ selected, isPanelCollap ); }; +const VideoRoomsButton: React.FC = ({ selected, isPanelCollapsed }) => { + return ( + + ); +}; + const CreateSpaceButton: React.FC> = ({ isPanelCollapsed, setPanelCollapsed, @@ -263,6 +277,7 @@ const metaSpaceComponentMap: Record = { [MetaSpace.Favourites]: FavouritesButton, [MetaSpace.People]: PeopleButton, [MetaSpace.Orphans]: OrphansButton, + [MetaSpace.VideoRooms]: VideoRoomsButton, }; interface IInnerSpacePanelProps extends DroppableProvidedProps { @@ -279,10 +294,12 @@ const InnerSpacePanel = React.memo( const [invites, metaSpaces, actualSpaces, activeSpace] = useSpaces(); const activeSpaces = activeSpace ? [activeSpace] : []; - const metaSpacesSection = metaSpaces.map((key) => { - const Component = metaSpaceComponentMap[key]; - return ; - }); + const metaSpacesSection = metaSpaces + .filter((key) => !(key === MetaSpace.VideoRooms && !SettingsStore.getValue("feature_video_rooms"))) + .map((key) => { + const Component = metaSpaceComponentMap[key]; + return ; + }); return ( ({ let notifBadge; if (spaceKey && notificationState) { let ariaLabel = _t("a11y_jump_first_unread_room"); - if (space?.getMyMembership() === "invite") { + if (space?.getMyMembership() === KnownMembership.Invite) { ariaLabel = _t("a11y|jump_first_invite"); } @@ -113,7 +114,6 @@ export const SpaceButton = ({
{ hasSubSpaces: this.state.childSpaces?.length, }); - const isInvite = space.getMyMembership() === "invite"; + const isInvite = space.getMyMembership() === KnownMembership.Invite; const notificationState = isInvite ? StaticNotificationState.forSymbol("!", NotificationLevel.Highlight) @@ -379,7 +379,9 @@ export class SpaceItem extends React.PureComponent { isNarrow={isPanelCollapsed} size={isNested ? "24px" : "32px"} onKeyDown={this.onKeyDown} - ContextMenuComponent={this.props.space.getMyMembership() === "join" ? SpaceContextMenu : undefined} + ContextMenuComponent={ + this.props.space.getMyMembership() === KnownMembership.Join ? SpaceContextMenu : undefined + } > {toggleCollapseButton} diff --git a/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentre.tsx b/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentre.tsx index f6374ef32a9..ddb1dd98d3e 100644 --- a/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentre.tsx +++ b/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentre.tsx @@ -32,6 +32,8 @@ import { useUnreadThreadRooms } from "./useUnreadThreadRooms"; import { StatelessNotificationBadge } from "../../rooms/NotificationBadge/StatelessNotificationBadge"; import { NotificationLevel } from "../../../../stores/notifications/NotificationLevel"; import PosthogTrackers from "../../../../PosthogTrackers"; +import { getKeyBindingsManager } from "../../../../KeyBindingsManager"; +import { KeyBindingAction } from "../../../../accessibility/KeyboardShortcuts"; interface ThreadsActivityCentreProps { /** @@ -49,41 +51,56 @@ export function ThreadsActivityCentre({ displayButtonLabel }: ThreadsActivityCen const roomsAndNotifications = useUnreadThreadRooms(open); return ( - { - // Track only when the Threads Activity Centre is opened - if (newOpen) PosthogTrackers.trackInteraction("WebThreadsActivityCentreButton"); +
{ + // Do nothing if the TAC is closed + if (!open) return; - setOpen(newOpen); + const action = getKeyBindingsManager().getNavigationAction(evt); + + // Block spotlight opening + if (action === KeyBindingAction.FilterRooms) { + evt.stopPropagation(); + } }} - side="right" - title={_t("threads_activity_centre|header")} - trigger={ - - } > - {/* Make the content of the pop-up scrollable */} -
- {roomsAndNotifications.rooms.map(({ room, notificationLevel }) => ( - setOpen(false)} + { + // Track only when the Threads Activity Centre is opened + if (newOpen) PosthogTrackers.trackInteraction("WebThreadsActivityCentreButton"); + + setOpen(newOpen); + }} + side="right" + title={_t("threads_activity_centre|header")} + trigger={ + - ))} - {roomsAndNotifications.rooms.length === 0 && ( -
- {_t("threads_activity_centre|no_rooms_with_unreads_threads")} -
- )} -
-
+ } + > + {/* Make the content of the pop-up scrollable */} +
+ {roomsAndNotifications.rooms.map(({ room, notificationLevel }) => ( + setOpen(false)} + /> + ))} + {roomsAndNotifications.rooms.length === 0 && ( +
+ {_t("threads_activity_centre|no_rooms_with_unreads_threads")} +
+ )} +
+ +
); } @@ -105,10 +122,10 @@ interface ThreadsActivityRow { /** * Display a room with unread threads. */ -function ThreadsActivityRow({ room, onClick, notificationLevel }: ThreadsActivityRow): JSX.Element { +function ThreadsActivityCentreRow({ room, onClick, notificationLevel }: ThreadsActivityRow): JSX.Element { return ( { onClick(); @@ -130,7 +147,7 @@ function ThreadsActivityRow({ room, onClick, notificationLevel }: ThreadsActivit label={room.name} Icon={} > - + ); } diff --git a/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentreButton.tsx b/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentreButton.tsx index 13478c8c5bf..3f85de38fa3 100644 --- a/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentreButton.tsx +++ b/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentreButton.tsx @@ -16,17 +16,16 @@ * / */ -import React, { forwardRef, HTMLProps } from "react"; +import React, { ComponentProps, forwardRef } from "react"; import { Icon } from "@vector-im/compound-design-tokens/icons/threads-solid.svg"; import classNames from "classnames"; -import { IndicatorIcon } from "@vector-im/compound-web"; +import { IconButton, Text, Tooltip } from "@vector-im/compound-web"; import { _t } from "../../../../languageHandler"; -import AccessibleTooltipButton from "../../elements/AccessibleTooltipButton"; import { NotificationLevel } from "../../../../stores/notifications/NotificationLevel"; import { notificationLevelToIndicator } from "../../../../utils/notifications"; -interface ThreadsActivityCentreButtonProps extends HTMLProps { +interface ThreadsActivityCentreButtonProps extends ComponentProps { /** * Display the `Treads` label next to the icon. */ @@ -40,28 +39,36 @@ interface ThreadsActivityCentreButtonProps extends HTMLProps { /** * A button to open the thread activity centre. */ -export const ThreadsActivityCentreButton = forwardRef( +export const ThreadsActivityCentreButton = forwardRef( function ThreadsActivityCentreButton({ displayLabel, notificationLevel, ...props }, ref): React.JSX.Element { + // Disable tooltip when the label is displayed + const openTooltip = displayLabel ? false : undefined; + return ( - - + - - - {displayLabel && _t("common|threads")} - + <> + + {/* This is dirty, but we need to add the label to the indicator icon */} + {displayLabel && ( + + {_t("common|threads")} + + )} + + + ); }, ); diff --git a/src/components/views/spaces/threads-activity-centre/useUnreadThreadRooms.ts b/src/components/views/spaces/threads-activity-centre/useUnreadThreadRooms.ts index addaeb97b8e..72b5380fbd1 100644 --- a/src/components/views/spaces/threads-activity-centre/useUnreadThreadRooms.ts +++ b/src/components/views/spaces/threads-activity-centre/useUnreadThreadRooms.ts @@ -89,13 +89,19 @@ function computeUnreadThreadRooms(mxClient: MatrixClient, msc3946ProcessDynamicP const visibleRooms = mxClient.getVisibleRooms(msc3946ProcessDynamicPredecessor); let greatestNotificationLevel = NotificationLevel.None; - const rooms = []; + const rooms: Result["rooms"] = []; for (const room of visibleRooms) { // We only care about rooms with unread threads if (VisibilityProvider.instance.isRoomVisible(room) && doesRoomHaveUnreadThreads(room)) { - // Get the greatest notification level of all rooms + // Get the greatest notification level of all threads const notificationLevel = getThreadNotificationLevel(room); + + // If the room has an activity notification or less, we ignore it + if (notificationLevel <= NotificationLevel.Activity) { + continue; + } + if (notificationLevel > greatestNotificationLevel) { greatestNotificationLevel = notificationLevel; } @@ -104,20 +110,35 @@ function computeUnreadThreadRooms(mxClient: MatrixClient, msc3946ProcessDynamicP } } - const sortedRooms = rooms.sort((a, b) => sortRoom(a.notificationLevel, b.notificationLevel)); + const sortedRooms = rooms.sort((a, b) => sortRoom(a, b)); return { greatestNotificationLevel, rooms: sortedRooms }; } +/** + * Store the room and its thread notification level + */ +type RoomData = Result["rooms"][0]; + /** * Sort notification level by the most important notification level to the least important * Highlight > Notification > Activity - * @param notificationLevelA - notification level of room A - * @param notificationLevelB - notification level of room B + * If the notification level is the same, we sort by the most recent thread + * @param roomDataA - room and notification level of room A + * @param roomDataB - room and notification level of room B * @returns {number} */ -function sortRoom(notificationLevelA: NotificationLevel, notificationLevelB: NotificationLevel): number { +function sortRoom(roomDataA: RoomData, roomDataB: RoomData): number { + const { notificationLevel: notificationLevelA, room: roomA } = roomDataA; + const { notificationLevel: notificationLevelB, room: roomB } = roomDataB; + + const timestampA = roomA.getLastThread()?.events.at(-1)?.getTs(); + const timestampB = roomB.getLastThread()?.events.at(-1)?.getTs(); + // NotificationLevel is a numeric enum, so we can compare them directly if (notificationLevelA > notificationLevelB) return -1; else if (notificationLevelB > notificationLevelA) return 1; - else return 0; + // Display most recent first + else if (!timestampA) return 1; + else if (!timestampB) return -1; + else return timestampB - timestampA; } diff --git a/src/components/views/verification/VerificationShowSas.tsx b/src/components/views/verification/VerificationShowSas.tsx index 42cb7721597..de091091abc 100644 --- a/src/components/views/verification/VerificationShowSas.tsx +++ b/src/components/views/verification/VerificationShowSas.tsx @@ -16,7 +16,7 @@ limitations under the License. import React from "react"; import { Device } from "matrix-js-sdk/src/matrix"; -import { GeneratedSas, EmojiMapping } from "matrix-js-sdk/src/crypto-api/verification"; +import { GeneratedSas, EmojiMapping } from "matrix-js-sdk/src/crypto-api"; import SasEmoji from "@matrix-org/spec/sas-emoji.json"; import { _t, getNormalizedLanguageKeys, getUserLanguage } from "../../../languageHandler"; diff --git a/src/createRoom.ts b/src/createRoom.ts index 579eeab7f30..93ef63383f4 100644 --- a/src/createRoom.ts +++ b/src/createRoom.ts @@ -347,15 +347,13 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro await JitsiCall.create(await room); // Reset our power level back to admin so that the widget becomes immutable - const plEvent = (await room).currentState.getStateEvents(EventType.RoomPowerLevels, ""); - await client.setPowerLevel(roomId, client.getUserId()!, 100, plEvent); + await client.setPowerLevel(roomId, client.getUserId()!, 100); } else if (opts.roomType === RoomType.UnstableCall) { // Set up this video room with an Element call await ElementCall.create(await room); // Reset our power level back to admin so that the call becomes immutable - const plEvent = (await room).currentState.getStateEvents(EventType.RoomPowerLevels, ""); - await client.setPowerLevel(roomId, client.getUserId()!, 100, plEvent); + await client.setPowerLevel(roomId, client.getUserId()!, 100); } }) .then( diff --git a/src/customisations/Media.ts b/src/customisations/Media.ts index 05f91325dd7..25e8489658d 100644 --- a/src/customisations/Media.ts +++ b/src/customisations/Media.ts @@ -15,10 +15,11 @@ */ import { MatrixClient, ResizeMethod } from "matrix-js-sdk/src/matrix"; +import { MediaEventContent } from "matrix-js-sdk/src/types"; import { Optional } from "matrix-events-sdk"; import { MatrixClientPeg } from "../MatrixClientPeg"; -import { IMediaEventContent, IPreparedMedia, prepEventContentAsMedia } from "./models/IMediaEventContent"; +import { IPreparedMedia, prepEventContentAsMedia } from "./models/IMediaEventContent"; import { UserFriendlyError } from "../languageHandler"; // Populate this class with the details of your customisations when copying it. @@ -154,11 +155,11 @@ export class Media { /** * Creates a media object from event content. - * @param {IMediaEventContent} content The event content. + * @param {MediaEventContent} content The event content. * @param {MatrixClient} client? Optional client to use. * @returns {Media} The media object. */ -export function mediaFromContent(content: Partial, client?: MatrixClient): Media { +export function mediaFromContent(content: Partial, client?: MatrixClient): Media { return new Media(prepEventContentAsMedia(content), client); } diff --git a/src/customisations/models/IMediaEventContent.ts b/src/customisations/models/IMediaEventContent.ts index 81714000d9b..ad305239b15 100644 --- a/src/customisations/models/IMediaEventContent.ts +++ b/src/customisations/models/IMediaEventContent.ts @@ -14,220 +14,7 @@ * limitations under the License. */ -// TODO: These types should be elsewhere. - -import { MsgType } from "matrix-js-sdk/src/matrix"; - -import { BLURHASH_FIELD } from "../../utils/image-media"; - -/** - * @see https://spec.matrix.org/v1.7/client-server-api/#extensions-to-mroommessage-msgtypes - */ -export interface EncryptedFile { - /** - * The URL to the file. - */ - url: string; - /** - * A JSON Web Key object. - */ - key: { - alg: string; - key_ops: string[]; // eslint-disable-line camelcase - kty: string; - k: string; - ext: boolean; - }; - /** - * The 128-bit unique counter block used by AES-CTR, encoded as unpadded base64. - */ - iv: string; - /** - * A map from an algorithm name to a hash of the ciphertext, encoded as unpadded base64. - * Clients should support the SHA-256 hash, which uses the key sha256. - */ - hashes: { [alg: string]: string }; - /** - * Version of the encrypted attachment's protocol. Must be v2. - */ - v: string; -} - -interface ThumbnailInfo { - /** - * The mimetype of the image, e.g. image/jpeg. - */ - mimetype?: string; - /** - * The intended display width of the image in pixels. - * This may differ from the intrinsic dimensions of the image file. - */ - w?: number; - /** - * The intended display height of the image in pixels. - * This may differ from the intrinsic dimensions of the image file. - */ - h?: number; - /** - * Size of the image in bytes. - */ - size?: number; -} - -interface BaseInfo { - mimetype?: string; - size?: number; -} - -/** - * @see https://spec.matrix.org/v1.7/client-server-api/#mfile - */ -export interface FileInfo extends BaseInfo { - /** - * @see https://github.com/matrix-org/matrix-spec-proposals/pull/2448 - */ - [BLURHASH_FIELD]?: string; - /** - * Information on the encrypted thumbnail file, as specified in End-to-end encryption. - * Only present if the thumbnail is encrypted. - * @see https://spec.matrix.org/v1.7/client-server-api/#sending-encrypted-attachments - */ - thumbnail_file?: EncryptedFile; - /** - * Metadata about the image referred to in thumbnail_url. - */ - thumbnail_info?: ThumbnailInfo; - /** - * The URL to the thumbnail of the file. Only present if the thumbnail is unencrypted. - */ - thumbnail_url?: string; -} - -/** - * @see https://spec.matrix.org/v1.7/client-server-api/#mimage - * - */ -export interface ImageInfo extends FileInfo, ThumbnailInfo {} - -/** - * @see https://spec.matrix.org/v1.7/client-server-api/#mimage - */ -export interface AudioInfo extends BaseInfo { - /** - * The duration of the audio in milliseconds. - */ - duration?: number; -} - -/** - * @see https://spec.matrix.org/v1.7/client-server-api/#mvideo - */ -export interface VideoInfo extends AudioInfo, ImageInfo { - /** - * The duration of the video in milliseconds. - */ - duration?: number; -} - -export type IMediaEventInfo = FileInfo | ImageInfo | AudioInfo | VideoInfo; - -interface BaseContent { - /** - * Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption. - * @see https://spec.matrix.org/v1.7/client-server-api/#sending-encrypted-attachments - */ - file?: EncryptedFile; - /** - * Required if the file is unencrypted. The URL (typically mxc:// URI) to the file. - */ - url?: string; -} - -/** - * @see https://spec.matrix.org/v1.7/client-server-api/#mfile - */ -export interface FileContent extends BaseContent { - /** - * A human-readable description of the file. - * This is recommended to be the filename of the original upload. - */ - body: string; - /** - * The original filename of the uploaded file. - */ - filename?: string; - /** - * Information about the file referred to in url. - */ - info?: FileInfo; - /** - * One of: [m.file]. - */ - msgtype: MsgType.File; -} - -/** - * @see https://spec.matrix.org/v1.7/client-server-api/#mimage - */ -export interface ImageContent extends BaseContent { - /** - * A textual representation of the image. - * This could be the alt text of the image, the filename of the image, - * or some kind of content description for accessibility e.g. ‘image attachment’. - */ - body: string; - /** - * Metadata about the image referred to in url. - */ - info?: ImageInfo; - /** - * One of: [m.image]. - */ - msgtype: MsgType.Image; -} - -/** - * @see https://spec.matrix.org/v1.7/client-server-api/#maudio - */ -export interface AudioContent extends BaseContent { - /** - * A description of the audio e.g. ‘Bee Gees - Stayin’ Alive’, - * or some kind of content description for accessibility e.g. ‘audio attachment’. - */ - body: string; - /** - * Metadata for the audio clip referred to in url. - */ - info?: AudioInfo; - /** - * One of: [m.audio]. - */ - msgtype: MsgType.Audio; -} - -/** - * @see https://spec.matrix.org/v1.7/client-server-api/#mvideo - */ -export interface VideoContent extends BaseContent { - /** - * A description of the video e.g. ‘Gangnam style’, - * or some kind of content description for accessibility e.g. ‘video attachment’. - */ - body: string; - /** - * Metadata about the video clip referred to in url. - */ - info?: VideoInfo; - /** - * One of: [m.video]. - */ - msgtype: MsgType.Video; -} - -/** - * Type representing media event contents for `m.room.message` events listed in the Matrix specification - */ -export type IMediaEventContent = FileContent | ImageContent | AudioContent | VideoContent; +import { EncryptedFile, MediaEventContent } from "matrix-js-sdk/src/types"; export interface IPreparedMedia extends IMediaObject { thumbnail?: IMediaObject; @@ -241,11 +28,11 @@ export interface IMediaObject { /** * Parses an event content body into a prepared media object. This prepared media object * can be used with other functions to manipulate the media. - * @param {IMediaEventContent} content Unredacted media event content. See interface. + * @param {MediaEventContent} content Unredacted media event content. See interface. * @returns {IPreparedMedia} A prepared media object. * @throws Throws if the given content cannot be packaged into a prepared media object. */ -export function prepEventContentAsMedia(content: Partial): IPreparedMedia { +export function prepEventContentAsMedia(content: Partial): IPreparedMedia { let thumbnail: IMediaObject | undefined; if (typeof content?.info === "object" && "thumbnail_url" in content.info && content.info.thumbnail_url) { thumbnail = { diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index 5783e783315..f774508f54f 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -107,9 +107,11 @@ export enum Action { MigrateBaseFontSize = "migrate_base_font_size", /** - * Sets the apps root font size. Should be used with UpdateFontSizePayload + * Sets the apps root font size delta. Should be used with UpdateFontSizeDeltaPayload + * It will add the delta to the current font size. + * The delta should be between {@link FontWatcher.MIN_DELTA} and {@link FontWatcher.MAX_DELTA}. */ - UpdateFontSize = "update_font_size", + UpdateFontSizeDelta = "update_font_size_delta", /** * Sets a system font. Should be used with UpdateSystemFontPayload @@ -372,6 +374,11 @@ export enum Action { */ OpenSpotlight = "open_spotlight", + /** + * Fired when the room loaded. + */ + RoomLoaded = "room_loaded", + /** * Opens right panel with 3pid invite information */ diff --git a/src/dispatcher/payloads/UpdateFontSizePayload.ts b/src/dispatcher/payloads/UpdateFontSizeDeltaPayload.ts similarity index 70% rename from src/dispatcher/payloads/UpdateFontSizePayload.ts rename to src/dispatcher/payloads/UpdateFontSizeDeltaPayload.ts index 6577acd594f..63a36d736bc 100644 --- a/src/dispatcher/payloads/UpdateFontSizePayload.ts +++ b/src/dispatcher/payloads/UpdateFontSizeDeltaPayload.ts @@ -17,11 +17,12 @@ limitations under the License. import { ActionPayload } from "../payloads"; import { Action } from "../actions"; -export interface UpdateFontSizePayload extends ActionPayload { - action: Action.UpdateFontSize; +export interface UpdateFontSizeDeltaPayload extends ActionPayload { + action: Action.UpdateFontSizeDelta; /** - * The font size to set the root to + * The delta is added to the current font size. + * The delta should be between {@link FontWatcher.MIN_DELTA} and {@link FontWatcher.MAX_DELTA}. */ - size: number; + delta: number; } diff --git a/src/editor/commands.tsx b/src/editor/commands.tsx index 52ab881693b..27f64f32be4 100644 --- a/src/editor/commands.tsx +++ b/src/editor/commands.tsx @@ -16,7 +16,8 @@ limitations under the License. import React from "react"; import { logger } from "matrix-js-sdk/src/logger"; -import { IContent, MatrixClient } from "matrix-js-sdk/src/matrix"; +import { MatrixClient } from "matrix-js-sdk/src/matrix"; +import { RoomMessageEventContent } from "matrix-js-sdk/src/types"; import EditorModel from "./model"; import { Type } from "./parts"; @@ -63,9 +64,9 @@ export async function runSlashCommand( args: string | undefined, roomId: string, threadId: string | null, -): Promise<[content: IContent | null, success: boolean]> { +): Promise<[content: RoomMessageEventContent | null, success: boolean]> { const result = cmd.run(matrixClient, roomId, threadId, args); - let messageContent: IContent | null = null; + let messageContent: RoomMessageEventContent | null = null; let error: any = result.error; if (result.promise) { try { diff --git a/src/hooks/room/useRoomCall.ts b/src/hooks/room/useRoomCall.ts index 8d9045825c1..aed7fcbff43 100644 --- a/src/hooks/room/useRoomCall.ts +++ b/src/hooks/room/useRoomCall.ts @@ -14,9 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Room } from "matrix-js-sdk/src/matrix"; +import { JoinRule, Room } from "matrix-js-sdk/src/matrix"; import React, { useCallback, useEffect, useMemo, useState } from "react"; import { CallType } from "matrix-js-sdk/src/webrtc/call"; +import { logger } from "matrix-js-sdk/src/logger"; import { useFeatureEnabled } from "../useSettings"; import SdkConfig from "../../SdkConfig"; @@ -39,6 +40,8 @@ import defaultDispatcher from "../../dispatcher/dispatcher"; import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; import { Action } from "../../dispatcher/actions"; import { CallStore, CallStoreEvent } from "../../stores/CallStore"; +import { calculateRoomVia } from "../../utils/permalinks/Permalinks"; +import { isVideoRoom } from "../../utils/video-rooms"; export enum PlatformCallType { ElementCall, @@ -78,40 +81,53 @@ export const useRoomCall = ( videoCallClick(evt: React.MouseEvent | undefined, selectedType: PlatformCallType): void; toggleCallMaximized: () => void; isViewingCall: boolean; + generateCallLink: () => URL; + canGenerateCallLink: boolean; isConnectedToCall: boolean; hasActiveCallSession: boolean; callOptions: PlatformCallType[]; } => { + // settings const groupCallsEnabled = useFeatureEnabled("feature_group_calls"); const useElementCallExclusively = useMemo(() => { return SdkConfig.get("element_call").use_exclusively; }, []); + const guestSpaUrl = useMemo(() => { + return SdkConfig.get("element_call").guest_spa_url; + }, []); + const hasLegacyCall = useEventEmitterState( LegacyCallHandler.instance, LegacyCallHandlerEvent.CallsChanged, () => LegacyCallHandler.instance.getCallForRoom(room.roomId) !== null, ); - + // settings const widgets = useWidgets(room); const jitsiWidget = useMemo(() => widgets.find((widget) => WidgetType.JITSI.matches(widget.type)), [widgets]); const hasJitsiWidget = !!jitsiWidget; const managedHybridWidget = useMemo(() => widgets.find(isManagedHybridWidget), [widgets]); const hasManagedHybridWidget = !!managedHybridWidget; + // group call const groupCall = useCall(room.roomId); const isConnectedToCall = useConnectionState(groupCall) === ConnectionState.Connected; const hasGroupCall = groupCall !== null; const hasActiveCallSession = useParticipantCount(groupCall) > 0; - const isViewingCall = useEventEmitterState(SdkContextClass.instance.roomViewStore, UPDATE_EVENT, () => - SdkContextClass.instance.roomViewStore.isViewingCall(), + const isViewingCall = useEventEmitterState( + SdkContextClass.instance.roomViewStore, + UPDATE_EVENT, + () => SdkContextClass.instance.roomViewStore.isViewingCall() || isVideoRoom(room), ); + // room const memberCount = useRoomMemberCount(room); - const [mayEditWidgets, mayCreateElementCalls] = useRoomState(room, () => [ + const [mayEditWidgets, mayCreateElementCalls, canJoinWithoutInvite] = useRoomState(room, () => [ room.currentState.mayClientSendStateEvent("im.vector.modular.widgets", room.client), room.currentState.mayClientSendStateEvent(ElementCall.MEMBER_EVENT_TYPE.name, room.client), + room.getJoinRule() === "public" || room.getJoinRule() === JoinRule.Knock, + /*|| room.getJoinRule() === JoinRule.Restricted <- rule for joining via token?*/ ]); // The options provided to the RoomHeader. @@ -131,7 +147,7 @@ export const useRoomCall = ( return [PlatformCallType.ElementCall]; } if (hasGroupCall && WidgetType.CALL.matches(groupCall.widget.type)) { - // only allow joining joining the ongoing Element call if there is one. + // only allow joining the ongoing Element call if there is one. return [PlatformCallType.ElementCall]; } } @@ -173,6 +189,8 @@ export const useRoomCall = ( // We only want to prompt to pin the widget if it's not element call based. const isECWidget = WidgetType.CALL.matches(widget?.type ?? ""); const promptPinWidget = !isECWidget && canPinWidget && !widgetPinned; + const userId = room.client.getUserId(); + const canInviteToRoom = userId ? room.canInvite(userId) : false; const state = useMemo((): State => { if (activeCalls.find((call) => call.roomId != room.roomId)) { return State.Ongoing; @@ -183,8 +201,9 @@ export const useRoomCall = ( if (hasLegacyCall) { return State.Ongoing; } - - if (memberCount <= 1) { + const canCallAlone = + canInviteToRoom && (room.getJoinRule() === "public" || room.getJoinRule() === JoinRule.Knock); + if (!(memberCount > 1 || canCallAlone)) { return State.NoOneHere; } @@ -194,6 +213,7 @@ export const useRoomCall = ( return State.NoCall; }, [ activeCalls, + canInviteToRoom, hasGroupCall, hasJitsiWidget, hasLegacyCall, @@ -202,7 +222,7 @@ export const useRoomCall = ( mayEditWidgets, memberCount, promptPinWidget, - room.roomId, + room, ]); const voiceCallClick = useCallback( @@ -258,6 +278,26 @@ export const useRoomCall = ( }); }, [isViewingCall, room.roomId]); + const generateCallLink = useCallback(() => { + if (!canJoinWithoutInvite) + throw new Error("Cannot create link for room that users can not join without invite."); + if (!guestSpaUrl) throw new Error("No guest SPA url for external links provided."); + const url = new URL(guestSpaUrl); + url.pathname = "/room/"; + // Set params for the sharable url + url.searchParams.set("roomId", room.roomId); + if (room.hasEncryptionStateEvent()) url.searchParams.set("perParticipantE2EE", "true"); + for (const server of calculateRoomVia(room)) { + url.searchParams.set("viaServers", server); + } + + // Move params into hash + url.hash = "/" + room.name + url.search; + url.search = ""; + + logger.info("Generated element call external url:", url); + return url; + }, [canJoinWithoutInvite, guestSpaUrl, room]); /** * We've gone through all the steps */ @@ -268,6 +308,8 @@ export const useRoomCall = ( videoCallClick, toggleCallMaximized: toggleCallMaximized, isViewingCall: isViewingCall, + generateCallLink, + canGenerateCallLink: guestSpaUrl !== undefined && canJoinWithoutInvite, isConnectedToCall: isConnectedToCall, hasActiveCallSession: hasActiveCallSession, callOptions, diff --git a/src/hooks/useRoomMembers.ts b/src/hooks/useRoomMembers.ts index c0436562df7..09c9773842d 100644 --- a/src/hooks/useRoomMembers.ts +++ b/src/hooks/useRoomMembers.ts @@ -16,6 +16,7 @@ limitations under the License. import { useMemo, useState } from "react"; import { Room, RoomEvent, RoomMember, RoomStateEvent } from "matrix-js-sdk/src/matrix"; +import { Membership } from "matrix-js-sdk/src/types"; import { throttle } from "lodash"; import { useTypedEventEmitter } from "./useEventEmitter"; @@ -81,8 +82,8 @@ export const useRoomMemberCount = ( }; // Hook to simplify watching the local user's membership in a room -export const useMyRoomMembership = (room: Room): string => { - const [membership, setMembership] = useState(room.getMyMembership()); +export const useMyRoomMembership = (room: Room): Membership => { + const [membership, setMembership] = useState(room.getMyMembership()); useTypedEventEmitter(room, RoomEvent.MyMembership, () => { setMembership(room.getMyMembership()); }); diff --git a/src/hooks/useSpaceResults.ts b/src/hooks/useSpaceResults.ts index f21e0716105..d2dde1315c6 100644 --- a/src/hooks/useSpaceResults.ts +++ b/src/hooks/useSpaceResults.ts @@ -16,6 +16,7 @@ limitations under the License. import { useCallback, useEffect, useMemo, useState } from "react"; import { Room, RoomType, HierarchyRoom } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { RoomHierarchy } from "matrix-js-sdk/src/room-hierarchy"; import { normalize } from "matrix-js-sdk/src/utils"; @@ -57,7 +58,7 @@ export const useSpaceResults = (space: Room | undefined, query: string): [Hierar return rooms?.filter((r) => { return ( r.room_type !== RoomType.Space && - cli.getRoom(r.room_id)?.getMyMembership() !== "join" && + cli.getRoom(r.room_id)?.getMyMembership() !== KnownMembership.Join && (normalize(r.name || "").includes(normalizedQuery) || (r.canonical_alias || "").includes(lcQuery)) ); }); diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index e8a5263ba6e..eacf7b6762b 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -249,7 +249,6 @@ "completing_setup": "Dokončování nastavení nového zařízení", "confirm_code_match": "Zkontrolujte, zda se níže uvedený kód shoduje s vaším dalším zařízením:", "connecting": "Připojování…", - "devices_connected": "Zařízení byla propojena", "error_device_already_signed_in": "Druhé zařízení je již přihlášeno.", "error_device_not_signed_in": "Druhé zařízení není přihlášeno.", "error_device_unsupported": "Propojení s tímto zařízením není podporováno.", @@ -260,12 +259,10 @@ "error_request_cancelled": "Požadavek byl zrušen.", "error_request_declined": "Požadavek byl na druhém zařízení odmítnut.", "error_unexpected": "Došlo k neočekávané chybě.", - "review_and_approve": "Zkontrolovat a schválit přihlášení", "scan_code_instruction": "Níže uvedený QR kód naskenujte pomocí přihlašovaného zařízení.", "scan_qr_code": "Skenovat QR kód", "select_qr_code": "Vybrat '%(scanQRCode)s'", "sign_in_new_device": "Přihlásit nové zařízení", - "start_at_sign_in_screen": "Začněte na přihlašovací obrazovce", "waiting_for_device": "Čekání na přihlášení zařízení" }, "register_action": "Vytvořit účet", @@ -479,6 +476,7 @@ "legal": "Právní informace", "light": "Světlý", "loading": "Načítání…", + "lobby": "Předsálí", "location": "Poloha", "low_priority": "Nízká priorita", "matrix": "Matrix", @@ -741,7 +739,6 @@ "notification_state": "Stav oznámení je %(notificationState)s", "notifications_debug": "Ladění oznámení", "number_of_users": "Počet uživatelů", - "observe_only": "Pouze sledovat", "original_event_source": "Původní zdroj události", "phase": "Fáze", "phase_cancelled": "Zrušeno", @@ -749,7 +746,6 @@ "phase_requested": "Požadované", "phase_started": "Zahájeno", "phase_transaction": "Transakce", - "requester": "Žadatel", "room_encrypted": "Místnost je šifrovaná ✅", "room_id": "ID místnosti: %(roomId)s", "room_not_encrypted": "Místnost není šifrovaná 🚨", @@ -762,6 +758,8 @@ "room_notifications_type": "Typ: ", "room_status": "Stav místnosti", "room_unread_status_count": { + "one": "Stav nepřečtení místnosti: %(status)s, počet: %(count)s", + "few": "", "other": "Stav nepřečtení místnosti: %(status)s, počet: %(count)s" }, "save_setting_values": "Uložit hodnoty nastavení", @@ -1414,6 +1412,7 @@ "group_rooms": "Místnosti", "group_spaces": "Prostory", "group_themes": "Motivy vzhledu", + "group_threads": "Vlákna", "group_voip": "Zvuk a video", "group_widgets": "Widgety", "hidebold": "Skrýt tečku oznámení (zobrazit pouze odznaky čítačů)", @@ -1457,6 +1456,8 @@ "sliding_sync_server_no_support": "Váš server nemá nativní podporu", "sliding_sync_server_specify_proxy": "Váš server nemá nativní podporu, musíte zadat proxy server", "sliding_sync_server_support": "Váš server má nativní podporu", + "threads_activity_centre": "Centrum aktivit vláken (ve vývoji).", + "threads_activity_centre_description": "Upozornění: V aktivním vývoji; znovu načte %(brand)s.", "under_active_development": "V aktivním vývoji.", "unrealiable_e2e": "Nespolehlivé v šifrovaných místnostech", "video_rooms": "Video místnosti", @@ -1506,14 +1507,6 @@ "view_rules": "Zobrazit pravidla" }, "language_dropdown_label": "Menu jazyků", - "lazy_loading": { - "disabled_action": "Smazat paměť a sesynchronizovat", - "disabled_description1": "Na adrese %(host)s už jste použili %(brand)s se zapnutou volbou načítání členů místností až při prvním zobrazení. V této verzi je načítání členů až při prvním zobrazení vypnuté. Protože je s tímto nastavením lokální vyrovnávací paměť nekompatibilní, %(brand)s potřebuje znovu synchronizovat údaje z vašeho účtu.", - "disabled_description2": "Je-li jiná verze programu %(brand)s stále otevřená na jiné kartě, tak ji prosím zavřete, neboť užívání programu %(brand)s stejným hostitelem se zpožděným nahráváním současně povoleným i zakázaným bude působit problémy.", - "disabled_title": "Nekompatibilní lokální vyrovnávací paměť", - "resync_description": "%(brand)s teď používá 3-5× méně paměti, protože si informace o ostatních uživatelích načítá až když je potřebuje. Prosím počkejte na dokončení synchronizace se serverem!", - "resync_title": "Aktualizujeme %(brand)s" - }, "leave_room_dialog": { "last_person_warning": "Jste zde jediná osoba. Pokud odejdete, nikdo se v budoucnu nebude moci připojit, včetně vás.", "leave_room_question": "Opravdu chcete opustit místnost '%(roomName)s'?", @@ -1585,7 +1578,7 @@ }, "member_list_back_action_label": "Členové místnosti", "message_edit_dialog_title": "Úpravy zpráv", - "migrating_crypto": "Vydržte. Aktualizujeme Element, aby bylo šifrování rychlejší a spolehlivější.", + "migrating_crypto": "Vydržte. Aktualizujeme %(brand)s, aby bylo šifrování rychlejší a spolehlivější.", "mobile_guide": { "toast_accept": "Použijte aplikaci", "toast_description": "%(brand)s je experimentální v mobilním webovém prohlížeči. Chcete-li získat lepší zážitek a nejnovější funkce, použijte naši bezplatnou nativní aplikaci.", @@ -1887,6 +1880,7 @@ "forget": "Zapomenout místnost", "low_priority": "Nízká priorita", "mark_read": "Označit jako přečtené", + "mark_unread": "Označit jako nepřečtené", "mentions_only": "Pouze zmínky", "notifications_default": "Odpovídá výchozímu nastavení", "notifications_mute": "Ztlumit místnost", @@ -2419,9 +2413,7 @@ "custom_theme_success": "Motiv vzhledu přidán!", "custom_theme_url": "URL adresa vlastního vzhledu", "font_size": "Velikost písma", - "font_size_limit": "Vlastní velikost písma může být pouze mezi %(min)s pt a %(max)s pt", - "font_size_nan": "Velikost musí být číslo", - "font_size_valid": "Použijte velikost mezi %(min)s pt a %(max)s pt", + "font_size_default": "%(fontSize)s (výchozí)", "heading": "Přizpůsobte si vzhled aplikace", "image_size_default": "Výchozí", "image_size_large": "Velký", @@ -2895,6 +2887,9 @@ "link_title": "Odkaz na místnost", "permalink_message": "Odkaz na vybranou zprávu", "permalink_most_recent": "Odkaz na nejnovější zprávu", + "share_call": "Odkaz na pozvánku na konferenci", + "share_call_subtitle": "Odkaz pro externí uživatele, aby se připojili k hovoru bez Matrix účtu:", + "title_link": "Sdílet odkaz", "title_message": "Sdílet zprávu z místnosti", "title_room": "Sdílet místnost", "title_user": "Sdílet uživatele" @@ -3156,6 +3151,10 @@ "show_thread_filter": "Zobrazit:", "unable_to_decrypt": "Nepodařilo se dešifrovat zprávu" }, + "threads_activity_centre": { + "header": "Aktivita vláken", + "no_rooms_with_unreads_threads": "Zatím nemáte místnosti s nepřečtenými vlákny." + }, "time": { "about_day_ago": "před jedním dnem", "about_hour_ago": "asi před hodinou", @@ -3803,6 +3802,7 @@ "camera_enabled": "Vaše kamera je stále zapnutá", "cannot_call_yourself_description": "Nemůžete volat sami sobě.", "change_input_device": "Změnit vstupní zařízení", + "close_lobby": "Zavřít lobby", "connecting": "Spojování", "connection_lost": "Došlo ke ztrátě připojení k serveru", "connection_lost_description": "Bez připojení k serveru nelze uskutečňovat hovory.", @@ -3816,17 +3816,23 @@ "disabled_no_perms_start_video_call": "Nemáte oprávnění ke spuštění videohovorů", "disabled_no_perms_start_voice_call": "Nemáte oprávnění k zahájení hlasových hovorů", "disabled_ongoing_call": "Průběžný hovor", + "element_call": "Element Call", "enable_camera": "Zapnout kameru", "enable_microphone": "Zrušit ztlumení mikrofonu", "expand": "Návrat do hovoru", "failed_call_live_broadcast_description": "Nemůžete zahájit hovor, protože právě nahráváte živé vysílání. Ukončete prosím živé vysílání, abyste mohli zahájit hovor.", "failed_call_live_broadcast_title": "Nelze zahájit hovor", + "get_call_link": "Sdílet odkaz na hovor", "hangup": "Zavěsit", "hide_sidebar_button": "Skrýt postranní panel", "input_devices": "Vstupní zařízení", + "jitsi_call": "Jitsi konference", "join_button_tooltip_call_full": "Omlouváme se — tento hovor je v současné době plný", "join_button_tooltip_connecting": "Spojování", + "legacy_call": "Zastaralý způsob hovoru", "maximise": "Vyplnit obrazovku", + "maximise_call": "Maximalizovat hovor", + "minimise_call": "Minimalizovat hovor", "misconfigured_server": "Volání selhalo, protože je rozbitá konfigurace serveru", "misconfigured_server_description": "Požádejte správce svého domovského serveru (%(homeserverDomain)s) jestli by nemohl nakonfigurovat TURN server, aby volání fungovala spolehlivě.", "misconfigured_server_fallback": "Případně můžete zkusit použít veřejný server na adrese , ale ten nebude tak spolehlivý a bude sdílet vaši IP adresu s tímto serverem. Můžete to spravovat také v Nastavení.", @@ -3874,6 +3880,7 @@ "user_is_presenting": "%(sharerName)s prezentuje", "video_call": "Videohovor", "video_call_started": "Videohovor byl zahájen", + "video_call_using": "Videohovor pomocí:", "voice_call": "Hlasový hovor", "you_are_presenting": "Prezentujete" }, diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 471f01f7d53..3fc5e490351 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -1,5 +1,6 @@ { "a11y": { + "emoji_picker": "Emoji-Auswahl", "jump_first_invite": "Zur ersten Einladung springen.", "n_unread_messages": { "other": "%(count)s ungelesene Nachrichten.", @@ -244,7 +245,6 @@ "completing_setup": "Schließe Anmeldung deines neuen Gerätes ab", "confirm_code_match": "Überprüfe, dass der unten angezeigte Code mit deinem anderen Gerät übereinstimmt:", "connecting": "Verbinde …", - "devices_connected": "Geräte verbunden", "error_device_already_signed_in": "Das andere Gerät ist bereits angemeldet.", "error_device_not_signed_in": "Das andere Gerät ist nicht angemeldet.", "error_device_unsupported": "Die Verbindung mit diesem Gerät wird nicht unterstützt.", @@ -255,12 +255,10 @@ "error_request_cancelled": "Die Anfrage wurde abgebrochen.", "error_request_declined": "Die Anfrage wurde auf dem anderen Gerät abgelehnt.", "error_unexpected": "Ein unerwarteter Fehler ist aufgetreten.", - "review_and_approve": "Überprüfe und genehmige die Anmeldung", "scan_code_instruction": "Lese den folgenden QR-Code mit deinem nicht angemeldeten Gerät ein.", "scan_qr_code": "QR-Code einlesen", "select_qr_code": "Wähle „%(scanQRCode)s“", "sign_in_new_device": "Neues Gerät anmelden", - "start_at_sign_in_screen": "Beginne auf dem Anmeldebildschirm", "waiting_for_device": "Warte auf Anmeldung des Gerätes" }, "register_action": "Konto erstellen", @@ -530,6 +528,7 @@ "show_more": "Mehr zeigen", "someone": "Jemand", "space": "Raum", + "spaces": "Räume", "sticker": "Sticker", "stickerpack": "Sticker-Paket", "success": "Erfolg", @@ -735,7 +734,6 @@ "notification_state": "Benachrichtigungsstand ist %(notificationState)s", "notifications_debug": "Debug-Modus für Benachrichtigungen", "number_of_users": "Benutzeranzahl", - "observe_only": "Nur beobachten", "original_event_source": "Ursprüngliche Rohdaten", "phase": "Phase", "phase_cancelled": "Abgebrochen", @@ -743,7 +741,6 @@ "phase_requested": "Angefragt", "phase_started": "Gestartet", "phase_transaction": "Transaktion", - "requester": "Anforderer", "room_encrypted": "Raum ist verschlüsselt ✅", "room_id": "Raum-ID: %(roomId)s", "room_not_encrypted": "Raum ist nicht verschlüsselt 🚨", @@ -1006,7 +1003,7 @@ "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 un Freunde, die dich vorher verifiziert haben werden Sicherheitswarnungen bekommen, bis du dich erneut mit ihnen verifizierst.", + "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 fahre nur fort, wenn du sicher bist, dass du alle anderen Geräte und deinen Sicherheitsschlüssel verloren hast.", "verify_using_device": "Mit anderem Gerät verifizieren", "verify_using_key": "Mit Sicherheitsschlüssel verifizieren", @@ -1054,7 +1051,7 @@ "error_app_open_in_another_tab": "Wechsle zu einem anderen Tab um mit %(brand)s zu verbinden. Dieser Tab kann jetzt geschlossen werden.", "error_app_open_in_another_tab_title": "%(brand)s läuft bereits in einem anderen Tab.", "error_app_opened_in_another_window": "%(brand)s läuft bereit in einem anderen Fenster. Klicke \"%(label)s um %(brand)s hier zu nutzen und beende das andere Fenster.", - "error_database_closed_title": "Datenbank unerwartet geschlossen", + "error_database_closed_title": "%(brand)s funktioniert nicht mehr", "error_dialog": { "copy_room_link_failed": { "description": "Der Link zum Raum konnte nicht kopiert werden.", @@ -1326,6 +1323,8 @@ "control": "Strg", "dismiss_read_marker_and_jump_bottom": "Entferne Lesemarker und springe nach unten", "end": "Ende", + "enter": "Enter", + "escape": "Esc", "go_home_view": "Zur Startseite gehen", "home": "Startseite", "jump_first_message": "Zur ersten Nachricht springen", @@ -1356,7 +1355,7 @@ "send_sticker": "Sticker senden", "shift": "Umschalt", "space": "Leertaste", - "switch_to_space": "Mit Nummer zu Space springen", + "switch_to_space": "Mit Nummer zu Leerzeichen springen", "toggle_hidden_events": "Sichtbarkeit versteckter Ereignisse umschalten", "toggle_microphone_mute": "Mikrofon an-/ausschalten", "toggle_right_panel": "Rechtes Panel ein-/ausblenden", @@ -1402,6 +1401,7 @@ "group_rooms": "Räume", "group_spaces": "Räume", "group_themes": "Themen", + "group_threads": "Themen", "group_voip": "Anrufe", "group_widgets": "Widgets", "hidebold": "Benachrichtigungspunkt ausblenden (nur Zähler zeigen)", @@ -1490,14 +1490,6 @@ "view_rules": "Regeln öffnen" }, "language_dropdown_label": "Sprachauswahl", - "lazy_loading": { - "disabled_action": "Zwischenspeicher löschen und erneut synchronisieren", - "disabled_description1": "Du hast zuvor %(brand)s auf %(host)s ohne das verzögerte Laden von Mitgliedern genutzt. In dieser Version war das verzögerte Laden deaktiviert. Da die lokal zwischengespeicherten Daten zwischen diesen Einstellungen nicht kompatibel sind, muss %(brand)s dein Konto neu synchronisieren.", - "disabled_description2": "Wenn %(brand)s mit der alten Version in einem anderen Tab geöffnet ist, schließe dies bitte, da das parallele Nutzen von %(brand)s auf demselben Host mit aktivierten und deaktivierten verzögertem Laden, Probleme verursachen wird.", - "disabled_title": "Inkompatibler lokaler Zwischenspeicher", - "resync_description": "%(brand)s benutzt nun 3 bis 5 Mal weniger Arbeitsspeicher, indem Informationen über andere Nutzer erst bei Bedarf geladen werden. Bitte warte, während die Daten erneut mit dem Server abgeglichen werden!", - "resync_title": "Aktualisiere %(brand)s" - }, "leave_room_dialog": { "last_person_warning": "Du bist die einzige Person im Raum. Sobald du ihn verlässt, wird niemand mehr hineingelangen, auch du nicht.", "leave_room_question": "Bist du sicher, dass du den Raum „%(roomName)s“ verlassen möchtest?", @@ -2301,7 +2293,7 @@ "join_rule_upgrade_upgrading_room": "Raum wird aktualisiert", "public_without_alias_warning": "Um den Raum zu verlinken, füge bitte eine Adresse hinzu.", "publish_room": "Diesen Raum im Raumverzeichnis veröffentlichen.", - "publish_space": "Diesen Space im Raumerzeichnis veröffentlichen.", + "publish_space": "Diesen Space im Raumverzeichnis veröffentlichen.", "strict_encryption": "Niemals verschlüsselte Nachrichten von dieser Sitzung zu unverifizierten Sitzungen in diesem Raum senden", "title": "Sicherheit" }, @@ -2395,9 +2387,6 @@ "custom_theme_success": "Design hinzugefügt!", "custom_theme_url": "URL des selbstdefinierten Designs", "font_size": "Schriftgröße", - "font_size_limit": "Eigene Schriftgröße kann nur eine Zahl zwischen %(min)s pt und %(max)s pt sein", - "font_size_nan": "Schriftgröße muss eine Zahl sein", - "font_size_valid": "Verwende eine Zahl zwischen %(min)s pt und %(max)s pt", "heading": "Verändere das Erscheinungsbild", "image_size_default": "Standard", "image_size_large": "Groß", @@ -3750,6 +3739,7 @@ "failed_others_already_recording_description": "Jemand anderes nimmt bereits eine Sprachübertragung auf. Warte auf das Ende der Übertragung, bevor du eine neue startest.", "failed_others_already_recording_title": "Sprachübertragung kann nicht gestartet werden", "go_live": "Live schalten", + "live": "Live", "pause": "Sprachübertragung pausieren", "play": "Sprachübertragung wiedergeben", "resume": "Sprachübertragung fortsetzen" @@ -3995,6 +3985,7 @@ "extendedRepeat": "Wiederholungen wie \"abcabcabc\" sind fast so schnell zu erraten wie \"abc\"", "keyPattern": "Kurze Tastaturmuster sind einfach zu erraten", "namesByThemselves": "Namen und Familiennamen alleine sind einfach zu erraten", + "pwned": "Dein Passwort wurde durch eine Datenleck im Internet offengelegt.", "recentYears": "Kürzlich vergangene Jahre sind einfach zu raten", "sequences": "Sequenzen wie \"abc\" oder \"6543\" sind leicht zu raten", "similarToCommon": "Dies ist ähnlich zu einem oft genutzten Passwort", diff --git a/src/i18n/strings/el.json b/src/i18n/strings/el.json index 1779067bb6d..bd9f43db301 100644 --- a/src/i18n/strings/el.json +++ b/src/i18n/strings/el.json @@ -606,7 +606,6 @@ "methods": "Μέθοδοι", "no_verification_requests_found": "Δεν βρέθηκαν αιτήματα επαλήθευσης", "number_of_users": "Αριθμός χρηστών", - "observe_only": "Παρατηρήστε μόνο", "original_event_source": "Αρχική πηγή συμβάντος", "phase": "Φάση", "phase_cancelled": "Ακυρώθηκαν", @@ -614,7 +613,6 @@ "phase_requested": "Απαιτείται", "phase_started": "Ξεκίνησαν", "phase_transaction": "Συναλλαγή", - "requester": "Aιτών", "room_id": "ID δωματίου: %(roomId)s", "save_setting_values": "Αποθήκευση τιμών ρύθμισης", "send_custom_account_data_event": "Αποστολή προσαρμοσμένου συμβάντος δεδομένων λογαριασμού", @@ -1165,6 +1163,7 @@ "group_rooms": "Δωμάτια", "group_spaces": "Χώροι", "group_themes": "Θέματα", + "group_threads": "Νήματα", "group_voip": "Φωνή & Βίντεο", "group_widgets": "Μικροεφαρμογές", "join_beta": "Συμμετοχή στη beta", @@ -1214,14 +1213,6 @@ "view_rules": "Προβολή κανόνων" }, "language_dropdown_label": "Επιλογή Γλώσσας", - "lazy_loading": { - "disabled_action": "Εκκαθάριση προσωρινής μνήμης και επανασυγχρονισμός", - "disabled_description1": "Έχετε χρησιμοποιήσει στο παρελθόν %(brand)s στον %(host)s με ενεργοποιημένη την αργή φόρτωση μελών. Σε αυτήν την έκδοση η αργή φόρτωση είναι απενεργοποιημένη. Η τοπική κρυφή μνήμη δεν είναι συμβατή μεταξύ αυτών των δύο ρυθμίσεων και έτσι το %(brand)s πρέπει να συγχρονίσει ξανά τον λογαριασμό σας.", - "disabled_description2": "Εάν η άλλη έκδοση του %(brand)s εξακολουθεί να είναι ανοιχτή σε άλλη καρτέλα, κλείστε την, καθώς η χρήση του %(brand)s στον ίδιο κεντρικό υπολογιστή με ενεργοποιημένη και απενεργοποιημένη την αργή φόρτωση ταυτόχρονα , θα προκαλέσει προβλήματα.", - "disabled_title": "Μη συμβατή τοπική κρυφή μνήμη", - "resync_description": "Το %(brand)s χρησιμοποιεί πλέον 3-5 φορές λιγότερη μνήμη, φορτώνοντας πληροφορίες για άλλους χρήστες μόνο όταν χρειάζεται. Περιμένετε όσο γίνεται εκ νέου συγχρονισμός με τον διακομιστή!", - "resync_title": "Ενημέρωση %(brand)s" - }, "leave_room_dialog": { "last_person_warning": "Είστε το μόνο άτομο εδώ μέσα. Εάν φύγετε, κανείς δε θα μπορεί αργότερα να συμμετάσχει, συμπεριλαμβανομένου και εσάς.", "leave_room_question": "Είστε σίγουροι ότι θέλετε να αποχωρήσετε από το δωμάτιο '%(roomName)s';", @@ -1922,9 +1913,6 @@ "custom_theme_success": "Το θέμα προστέθηκε!", "custom_theme_url": "URL προσαρμοσμένου θέματος", "font_size": "Μέγεθος γραμματοσειράς", - "font_size_limit": "Το προσαρμοσμένο μέγεθος γραμματοσειράς μπορεί να είναι μόνο μεταξύ %(min)s pt και %(max)s pt", - "font_size_nan": "Το μέγεθος πρέπει να είναι ένας αριθμός", - "font_size_valid": "Χρήση μεταξύ %(min)s pt και %(max)s pt", "heading": "Προσαρμόστε την εμφάνισή σας", "image_size_default": "Προεπιλογή", "image_size_large": "Μεγάλο", diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 99035cf9d47..07a85767e17 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -249,7 +249,6 @@ "completing_setup": "Completing set up of your new device", "confirm_code_match": "Check that the code below matches with your other device:", "connecting": "Connecting…", - "devices_connected": "Devices connected", "error_device_already_signed_in": "The other device is already signed in.", "error_device_not_signed_in": "The other device isn't signed in.", "error_device_unsupported": "Linking with this device is not supported.", @@ -260,12 +259,13 @@ "error_request_cancelled": "The request was cancelled.", "error_request_declined": "The request was declined on the other device.", "error_unexpected": "An unexpected error occurred.", - "review_and_approve": "Review and approve the sign in", - "scan_code_instruction": "Scan the QR code below with your device that's signed out.", + "follow_remaining_instructions": "Follow the remaining instructions to verify your other device", + "open_element_other_device": "Open %(brand)s on your other device", + "point_the_camera": "Point the camera at the QR code shown here", + "scan_code_instruction": "Scan the QR code with another device", "scan_qr_code": "Scan QR code", - "select_qr_code": "Select '%(scanQRCode)s'", + "select_qr_code": "Select \"%(scanQRCode)s\"", "sign_in_new_device": "Sign in new device", - "start_at_sign_in_screen": "Start at the sign in screen", "waiting_for_device": "Waiting for device to sign in" }, "register_action": "Create Account", @@ -742,15 +742,14 @@ "notification_state": "Notification state is %(notificationState)s", "notifications_debug": "Notifications debug", "number_of_users": "Number of users", - "observe_only": "Observe only", "original_event_source": "Original event source", + "other_user": "Other user", "phase": "Phase", "phase_cancelled": "Cancelled", "phase_ready": "Ready", "phase_requested": "Requested", "phase_started": "Started", "phase_transaction": "Transaction", - "requester": "Requester", "room_encrypted": "Room is encrypted ✅", "room_id": "Room ID: %(roomId)s", "room_not_encrypted": "Room is not encrypted 🚨", @@ -789,6 +788,7 @@ "thread_root_id": "Thread Root ID: %(threadRootId)s", "threads_timeline": "Threads timeline", "timeout": "Timeout", + "timeout_none": "None", "title": "Developer tools", "toggle_event": "toggle event", "toolbox": "Toolbox", @@ -1461,7 +1461,7 @@ "sliding_sync_server_specify_proxy": "Your server lacks native support, you must specify a proxy", "sliding_sync_server_support": "Your server has native support", "threads_activity_centre": "Threads Activity Centre (in development)", - "threads_activity_centre_description": "Warning: Under active development; reloads Element.", + "threads_activity_centre_description": "Warning: Under active development; reloads %(brand)s.", "under_active_development": "Under active development.", "unrealiable_e2e": "Unreliable in encrypted rooms", "video_rooms": "Video rooms", @@ -1511,18 +1511,12 @@ "view_rules": "View rules" }, "language_dropdown_label": "Language Dropdown", - "lazy_loading": { - "disabled_action": "Clear cache and resync", - "disabled_description1": "You've previously used %(brand)s on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, %(brand)s needs to resync your account.", - "disabled_description2": "If the other version of %(brand)s is still open in another tab, please close it as using %(brand)s on the same host with both lazy loading enabled and disabled simultaneously will cause issues.", - "disabled_title": "Incompatible local cache", - "resync_description": "%(brand)s now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!", - "resync_title": "Updating %(brand)s" - }, "leave_room_dialog": { "last_person_warning": "You are the only person here. If you leave, no one will be able to join in the future, including you.", "leave_room_question": "Are you sure you want to leave the room '%(roomName)s'?", "leave_space_question": "Are you sure you want to leave the space '%(spaceName)s'?", + "room_leave_admin_warning": "You're the only administrator in this room. If you leave, nobody will be able to change room settings or take other important actions.", + "room_leave_mod_warning": "You're the only moderator in this room. If you leave, nobody will be able to change room settings or take other important actions.", "room_rejoin_warning": "This room is not public. You will not be able to rejoin without an invite.", "space_rejoin_warning": "This space is not public. You will not be able to rejoin without an invite." }, @@ -1590,7 +1584,7 @@ }, "member_list_back_action_label": "Room members", "message_edit_dialog_title": "Message edits", - "migrating_crypto": "Hang tight. We are updating Element to make encryption faster and more reliable.", + "migrating_crypto": "Hang tight. We are updating %(brand)s to make encryption faster and more reliable.", "mobile_guide": { "toast_accept": "Use app", "toast_description": "%(brand)s is experimental on a mobile web browser. For a better experience and the latest features, use our free native app.", @@ -1892,6 +1886,7 @@ "forget": "Forget Room", "low_priority": "Low Priority", "mark_read": "Mark as read", + "mark_unread": "Mark as unread", "mentions_only": "Mentions only", "notifications_default": "Match default setting", "notifications_mute": "Mute room", @@ -2422,9 +2417,7 @@ "custom_theme_success": "Theme added!", "custom_theme_url": "Custom theme URL", "font_size": "Font size", - "font_size_limit": "Custom font size can only be between %(min)s pt and %(max)s pt", - "font_size_nan": "Size must be a number", - "font_size_valid": "Use between %(min)s pt and %(max)s pt", + "font_size_default": "%(fontSize)s (default)", "heading": "Customise your appearance", "image_size_default": "Default", "image_size_large": "Large", @@ -2809,9 +2802,9 @@ "security_recommendations_description": "Improve your account security by following these recommendations.", "session_id": "Session ID", "show_details": "Show details", - "sign_in_with_qr": "Sign in with QR code", + "sign_in_with_qr": "Link new device", "sign_in_with_qr_button": "Show QR code", - "sign_in_with_qr_description": "You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.", + "sign_in_with_qr_description": "Use a QR code to sign in to another device and set up secure messaging.", "sign_out": "Sign out of this session", "sign_out_all_other_sessions": "Sign out of all other sessions (%(otherSessionsCount)s)", "sign_out_confirm_description": { @@ -2860,6 +2853,9 @@ "metaspaces_orphans_description": "Group all your rooms that aren't part of a space in one place.", "metaspaces_people_description": "Group all your people in one place.", "metaspaces_subsection": "Spaces to show", + "metaspaces_video_rooms": "Video rooms and conferences", + "metaspaces_video_rooms_description": "Group all private video rooms and conferences.", + "metaspaces_video_rooms_description_invite_extension": "In conferences you can invite people outside of matrix.", "spaces_explainer": "Spaces are ways to group rooms and people. Alongside the spaces you're in, you can use some pre-built ones too.", "title": "Sidebar" }, @@ -2898,6 +2894,9 @@ "link_title": "Link to room", "permalink_message": "Link to selected message", "permalink_most_recent": "Link to most recent message", + "share_call": "Conference invite link", + "share_call_subtitle": "Link for external users to join the call without a matrix account:", + "title_link": "Share Link", "title_message": "Share Room Message", "title_room": "Share Room", "title_user": "Share User" @@ -3152,6 +3151,7 @@ "empty_heading": "Keep discussions organised with threads", "empty_tip": "Tip: Use “%(replyInThread)s” when hovering over a message.", "error_start_thread_existing_relation": "Can't create a thread from an event with an existing relation", + "mark_all_read": "Mark all as read", "my_threads": "My threads", "my_threads_description": "Shows all threads you've participated in", "open_thread": "Open thread", @@ -3830,6 +3830,7 @@ "expand": "Return to call", "failed_call_live_broadcast_description": "You can’t start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call.", "failed_call_live_broadcast_title": "Can’t start a call", + "get_call_link": "Share call link", "hangup": "Hangup", "hide_sidebar_button": "Hide sidebar", "input_devices": "Input devices", @@ -3839,6 +3840,9 @@ "legacy_call": "Legacy Call", "maximise": "Fill screen", "maximise_call": "Maximise call", + "metaspace_video_rooms": { + "conference_room_section": "Conferences" + }, "minimise_call": "Minimise call", "misconfigured_server": "Call failed due to misconfigured server", "misconfigured_server_description": "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.", diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json index babd4dc1aa2..7845e4bfb64 100644 --- a/src/i18n/strings/eo.json +++ b/src/i18n/strings/eo.json @@ -1056,6 +1056,7 @@ "group_profile": "Profilo", "group_rooms": "Ĉambroj", "group_spaces": "Aroj", + "group_threads": "Fadenoj", "group_voip": "Voĉo kaj vido", "group_widgets": "Fenestraĵoj", "html_topic": "Montru HTML-prezenton de ĉambrotemoj", @@ -1108,14 +1109,6 @@ "view_rules": "Montri regulojn" }, "language_dropdown_label": "Lingva falmenuo", - "lazy_loading": { - "disabled_action": "Vakigi kaŝmemoron kaj respeguli", - "disabled_description1": "Vi antaŭe uzis %(brand)s-on je %(host)s kun ŝaltita malfrua enlegado de anoj. En ĉi tiu versio, malfrua enlegado estas malŝaltita. Ĉar la loka kaŝmemoro de ambaŭ versioj ne akordas, %(brand)s bezonas respeguli vian konton.", - "disabled_description2": "Se la alia versio de %(brand)s ankoraŭ estas malfermita en alia langeto, bonvolu tiun fermi, ĉar uzado de %(brand)s je la sama gastiganto, kun malfrua enlegado samtempe ŝaltita kaj malŝaltita, kaŭzos problemojn.", - "disabled_title": "Neakorda loka kaŝmemoro", - "resync_description": "%(brand)s nun uzas 3–5-oble malpli da memoro, ĉar ĝi enlegas informojn pri aliaj uzantoj nur tiam, kiam ĝi bezonas. Bonvolu atendi ĝis ni respegulos la servilon!", - "resync_title": "Ĝisdatigante %(brand)s" - }, "leave_room_dialog": { "last_person_warning": "Vi estas la nura persono tie ĉi. Se vi foriros, neniu alia plu povos aliĝi, inkluzive vin mem.", "leave_room_question": "Ĉu vi certe volas forlasi la ĉambron '%(roomName)s'?", @@ -1719,9 +1712,6 @@ "custom_theme_success": "Haŭto aldoniĝis!", "custom_theme_url": "Propra URL al haŭto", "font_size": "Grando de tiparo", - "font_size_limit": "Propra grando de tiparo povas interi nur %(min)s punktojn kaj %(max)s punktojn", - "font_size_nan": "Grando devas esti nombro", - "font_size_valid": "Uzi inter %(min)s punktoj kaj %(max)s punktoj", "heading": "Adaptu vian aspekton", "image_size_default": "Ordinara", "layout_bubbles": "Mesaĝaj vezikoj", diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json index ee584c07963..936ecebb6f5 100644 --- a/src/i18n/strings/es.json +++ b/src/i18n/strings/es.json @@ -233,7 +233,6 @@ "completing_setup": "Terminando de configurar tu nuevo dispositivo", "confirm_code_match": "Comprueba que el siguiente código también aparece en el otro dispositivo:", "connecting": "Conectando…", - "devices_connected": "Dispositivos conectados", "error_device_already_signed_in": "El otro dispositivo ya tiene una sesión iniciada.", "error_device_not_signed_in": "El otro dispositivo no tiene una sesión iniciada.", "error_homeserver_lacks_support": "Tu servidor base no es compatible con el inicio de sesión en otro dispositivo.", @@ -243,12 +242,10 @@ "error_request_cancelled": "La solicitud ha sido cancelada.", "error_request_declined": "El otro dispositivo ha rechazado la solicitud.", "error_unexpected": "Ha ocurrido un error inesperado.", - "review_and_approve": "Revisar y aprobar inicio de sesión", "scan_code_instruction": "Escanea el siguiente código QR con tu dispositivo.", "scan_qr_code": "Escanear código QR", "select_qr_code": "Selecciona «%(scanQRCode)s»", "sign_in_new_device": "Conectar nuevo dispositivo", - "start_at_sign_in_screen": "Ve a la pantalla de inicio de sesión", "waiting_for_device": "Esperando a que el dispositivo inicie sesión" }, "register_action": "Crear cuenta", @@ -692,7 +689,6 @@ "methods": "Métodos", "no_verification_requests_found": "Ninguna solicitud de verificación encontrada", "number_of_users": "Número de usuarios", - "observe_only": "Solo observar", "original_event_source": "Fuente original del evento", "phase": "Fase", "phase_cancelled": "Cancelado", @@ -700,7 +696,6 @@ "phase_requested": "Solicitado", "phase_started": "Empezado", "phase_transaction": "Transacción", - "requester": "Solicitante", "room_encrypted": "La sala está cifrada ✅", "room_id": "ID de la sala: %(roomId)s", "room_not_encrypted": "La sala no está cifrada 🚨", @@ -1306,6 +1301,7 @@ "group_rooms": "Salas", "group_spaces": "Espacios", "group_themes": "Temas", + "group_threads": "Hilos", "group_voip": "Voz y vídeo", "group_widgets": "Accesorios", "hidebold": "Ocultar el punto indicador de notificaciones (solo mostrar un indicador con número)", @@ -1382,14 +1378,6 @@ "view_rules": "Ver reglas" }, "language_dropdown_label": "Lista selección de idiomas", - "lazy_loading": { - "disabled_action": "Limpiar la caché y resincronizar", - "disabled_description1": "Has usado %(brand)s anteriormente en %(host)s con carga diferida de usuarios activada. En esta versión la carga diferida está desactivada. Como el caché local no es compatible entre estas dos configuraciones, %(brand)s tiene que resincronizar tu cuenta.", - "disabled_description2": "Si la otra versión de %(brand)s esta todavía abierta en otra pestaña, por favor, ciérrala, ya que usar %(brand)s en el mismo host con la opción de carga diferida activada y desactivada simultáneamente causará problemas.", - "disabled_title": "Caché local incompatible", - "resync_description": "%(brand)s ahora utiliza de 3 a 5 veces menos memoria, porque solo carga información sobre otros usuarios cuando es necesario. Por favor, ¡aguarda mientras volvemos a sincronizar con el servidor!", - "resync_title": "Actualizando %(brand)s" - }, "leave_room_dialog": { "last_person_warning": "Eres la única persona aquí. Si te vas, no podrá unirse nadie en el futuro, incluyéndote a ti.", "leave_room_question": "¿Salir de la sala «%(roomName)s»?", @@ -2217,9 +2205,6 @@ "custom_theme_success": "¡Se añadió el tema!", "custom_theme_url": "URL de tema personalizado", "font_size": "Tamaño del texto", - "font_size_limit": "El tamaño de la fuente solo puede estar entre los valores %(min)s y %(max)s", - "font_size_nan": "El tamaño debe ser un dígito", - "font_size_valid": "Utiliza un valor entre %(min)s y %(max)s", "heading": "Personaliza la apariencia", "image_size_default": "Por defecto", "image_size_large": "Grande", diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index 0fa0b5b18d9..cdfe16cc661 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -1,6 +1,8 @@ { "a11y": { + "emoji_picker": "Emojide valija", "jump_first_invite": "Siirdu esimese kutse juurde.", + "message_composer": "Sõnumikoostaja", "n_unread_messages": { "other": "%(count)s lugemata teadet.", "one": "1 lugemata teade." @@ -9,7 +11,10 @@ "one": "1 lugemata mainimine.", "other": "%(count)s lugemata sõnumit kaasa arvatud mainimised." }, + "recent_rooms": "Hiljuti kasutatud jututoad", "room_name": "Jututuba %(name)s", + "room_status_bar": "Jututoa olekuriba", + "seek_bar_label": "Heli kerimisriba", "unread_messages": "Lugemata sõnumid.", "user_menu": "Kasutajamenüü" }, @@ -30,7 +35,7 @@ "click": "Klõpsi", "click_to_copy": "Kopeerimiseks klõpsa", "close": "Sulge", - "collapse": "ahenda", + "collapse": "Ahenda", "complete": "Valmis", "confirm": "Kinnita", "continue": "Jätka", @@ -50,7 +55,7 @@ "enable": "Võta kasutusele", "enter_fullscreen": "Lülita täisekraanivaade sisse", "exit_fullscreeen": "Lülita täisekraanivaade välja", - "expand": "laienda", + "expand": "Laienda", "explore_public_rooms": "Sirvi avalikke jututubasid", "explore_rooms": "Tutvu jututubadega", "export": "Ekspordi", @@ -244,7 +249,6 @@ "completing_setup": "Lõpetame uue seadme seadistamise", "confirm_code_match": "Kontrolli, et järgnev kood klapib teises seadmes kuvatava koodiga:", "connecting": "Kõne on ühendamisel…", - "devices_connected": "Seadmed on ühendatud", "error_device_already_signed_in": "Teine seade on juba võrku loginud.", "error_device_not_signed_in": "Teine seade ei ole võrku loginud.", "error_device_unsupported": "Sidumine selle seadmega ei ole toetatud.", @@ -255,12 +259,10 @@ "error_request_cancelled": "Päring katkestati.", "error_request_declined": "Teine seade lükkas päringu tagasi.", "error_unexpected": "Tekkis teadmata viga.", - "review_and_approve": "Vaata üle ja kinnita sisselogimine Matrixi'i võrku", "scan_code_instruction": "Loe QR-koodi seadmega, kus sa oled Matrix'i võrgust välja loginud.", "scan_qr_code": "Loe QR-koodi", "select_qr_code": "Vali „%(scanQRCode)s“", "sign_in_new_device": "Logi sisse uus seade", - "start_at_sign_in_screen": "Alusta sisselogimisvaatest", "waiting_for_device": "Ootame, et teine seade logiks võrku" }, "register_action": "Loo konto", @@ -319,7 +321,7 @@ "set_email_prompt": "Kas sa soovid seadistada e-posti aadressi?", "sign_in_description": "Jätkamaks kasuta oma kontot.", "sign_in_instead": "Pigem logi sisse", - "sign_in_instead_prompt": "Pigem logi sisse", + "sign_in_instead_prompt": "Sul juba on kasutajakonto olemas?
Siis logi siin sisse", "sign_in_or_register": "Logi sisse või loo uus konto", "sign_in_or_register_description": "Jätkamaks kasuta oma kontot või loo uus konto.", "sign_in_prompt": "Sul on kasutajakonto olemas? Siis logi sisse", @@ -736,7 +738,6 @@ "notification_state": "Teavituste olek: %(notificationState)s", "notifications_debug": "Teavituste silumine", "number_of_users": "Kasutajate arv", - "observe_only": "Ainult vaatle", "original_event_source": "Sündmuse töötlemata lähtekood", "phase": "Faas", "phase_cancelled": "Katkestatud", @@ -744,7 +745,6 @@ "phase_requested": "Päring tehtud", "phase_started": "Alustatud", "phase_transaction": "Transaktsioon", - "requester": "Päringu tegija", "room_encrypted": "Jututuba on krüptitud ✅", "room_id": "Jututoa tunnus: %(roomId)s", "room_not_encrypted": "Jututuba on krüptimata 🚨", @@ -1052,9 +1052,13 @@ "unknown_error_code": "tundmatu veakood", "update_power_level": "Õiguste muutmine ei õnnestunud" }, - "error_app_open_in_another_tab": "%(brand)s on avatud teises vahekaardis.", + "error_app_open_in_another_tab": "%(brand)s'i kasutamiseks ava teine vahekaart. Selle vahekaardi võid kinni panna.", + "error_app_open_in_another_tab_title": "%(brand)s'i on kasutatav teisel vahekaardil", "error_app_opened_in_another_window": "%(brand)s on avatud teises aknas. Klõpsa \"%(label)s\", et kasutada siin %(brand)s ja katkestada teise akna ühendus.", - "error_database_closed_title": "Andmebaasiühendus sulgus ootamatult", + "error_database_closed_description": { + "for_desktop": "Andmekandja maht võib olla täis saanud. Palun tee ruumi juurde ja laadi leht uuesti." + }, + "error_database_closed_title": "%(brand)s lõpetas ootamatult töö", "error_dialog": { "copy_room_link_failed": { "description": "Jututoa lingi kopeerimine lõikelauale ei õnnestunud.", @@ -1299,6 +1303,7 @@ }, "keyboard": { "activate_button": "Aktiveeri valitud nupp", + "alt": "Alt", "autocomplete_cancel": "Lülita automaatne sõnalõpetus välja", "autocomplete_force": "Sunni lõpetama", "autocomplete_navigate_next": "Järgmine sisestussoovitus", @@ -1322,9 +1327,13 @@ "composer_toggle_link": "Lülita link sisse/välja", "composer_toggle_quote": "Lülita tsiteerimine sisse/välja", "composer_undo": "Võta muudatus tagasi", + "control": "Ctrl", "dismiss_read_marker_and_jump_bottom": "Ära arvesta loetud sõnumite järjehoidjat ning mine kõige lõppu", + "end": "End", + "enter": "Enter", + "escape": "Esc", "go_home_view": "Avalehele", - "home": "Avaleht", + "home": "Home", "jump_first_message": "Mine esimese sõnumi juurde", "jump_last_message": "Mine viimase sõnumi juurde", "jump_room_search": "Suundu jututoa otsingusse", @@ -1336,7 +1345,10 @@ "navigate_prev_message_edit": "Muutmiseks liigu eelmise sõnumi juurde", "next_room": "Järgmine otsevestlus või jututuba", "next_unread_room": "Järgmine lugemata otsevestlus või jututuba", + "number": "[number]", "open_user_settings": "Ava kasutaja seadistused", + "page_down": "Page Down", + "page_up": "Page Up", "prev_room": "Eelmine otsevestlus või jututuba", "prev_unread_room": "Eelmine lugemata otsevestlus või jututuba", "room_list_collapse_section": "Ahenda jututubade loendi valikut", @@ -1348,6 +1360,7 @@ "scroll_up_timeline": "Liigu ajajoonel üles", "search": "Otsing (peab olema lubatud)", "send_sticker": "Saada kleeps", + "shift": "Shift", "space": "Tühikuklahv", "switch_to_space": "Vaata kogukonnakeskust tema numbri alusel", "toggle_hidden_events": "Lülita peidetud sündmuste näitamine sisse/välja", @@ -1383,6 +1396,7 @@ "element_call_video_rooms": "Element Call videotoad", "experimental_description": "Soovid katsetada? Proovi meie uusimaid arendusmõtteid. Need funktsionaalsused pole üldsegi veel valmis, nad võivad toimida puudulikult, võivad muutuda või sootuks lõpetamata jääda. Lisateavet leiad siit.", "experimental_section": "Varased arendusjärgud", + "feature_disable_call_per_sender_encryption": "Lülita Element Call'i kasutamisel krüptimine kasutajakohaselt välja", "feature_wysiwyg_composer_description": "Sõnumite kirjutamisel kasuta Markdown'i asemel täisfunktsionaalset küljendust.", "group_calls": "Uus rühmakõnede lahendus", "group_developer": "Arendajad", @@ -1394,6 +1408,7 @@ "group_rooms": "Jututoad", "group_spaces": "Kogukonnakeskused", "group_themes": "Teemad", + "group_threads": "Jutulõngad", "group_voip": "Heli ja video", "group_widgets": "Vidinad", "hidebold": "Peida teavituse täpp (ja näita loendure)", @@ -1412,11 +1427,17 @@ "new_room_decoration_ui": "Uus jututoa päis ja infovaade on hetkel aktiivses arenduses", "notification_settings": "Uued teavituste seadistused", "notification_settings_beta_title": "Teavituste seadistused", - "oidc_native_flow": "Luba OIDC liidestus (aktiivselt arendamisel)", + "notifications": "Kasuta jututoa päises teavituste riba", + "oidc_native_flow": "OIDC-põhine autentimine", + "oidc_native_flow_description": "⚠ HOIATUS: Kasuta OIDC liidestust, kui server seda võimaldab (aktiivselt arendamisel).", "pinning": "Sõnumite esiletõstmine", "report_to_moderators": "Teata moderaatoritele", "report_to_moderators_description": "Kui jututoas on modereerimine kasutusel, siis nupust „Teata sisust“ avaneva vormi abil saad jututoa reegleid rikkuvast sisust teatada moderaatoritele.", "rust_crypto": "Rust'is teostatud krüptolahendus", + "rust_crypto_in_config": "Rust'i krüptograafiat ei saa selles %(brand)s'i paigalduses välja lülitada", + "rust_crypto_in_config_description": "Rust'i teekidel põhineva krüptograafia kasutusele võtmine eeldab andmete ümbertõstmist ja selleks võib kuluda õige mitu minutit. Hiljem ei saa seda funktsionaalsust enam välja lülitada. Palun ole kindel, et tead, mida teed!", + "rust_crypto_optin_warning": "Rust'i teekidel põhineva krüptograafia kasutusele võtmine eeldab andmete ümbertõstmist ja selleks võib kuluda õige mitu minutit. Selle funktsionaalsuse väljalülitamiseks pead võrgust välja logima ning seejärel tagasi logima. Palun ole kindel, et tead, mida teed!", + "rust_crypto_requires_logout": "Kui Rust'i põhised teegid on kasutusel, siis selle funktsionaalsuse väljalülitamiseks pead võrgust välja logima ning seejärel tagasi logima", "sliding_sync": "Järkjärgulise sünkroniseerimise režiim", "sliding_sync_checking": "Kontrollin…", "sliding_sync_configuration": "Sliding Sync konfiguratsioon", @@ -1475,14 +1496,6 @@ "view_rules": "Näita reegleid" }, "language_dropdown_label": "Keelevalik", - "lazy_loading": { - "disabled_action": "Tühjenda puhver ja sünkroniseeri andmed uuesti", - "disabled_description1": "Oled varem kasutanud %(brand)s serveriga %(host)s ja lubanud andmete laisa laadimise. Selles versioonis on laisk laadimine keelatud. Kuna kohalik vahemälu nende kahe seadistuse vahel ei ühildu, peab %(brand)s sinu konto uuesti sünkroonima.", - "disabled_description2": "Kui %(brand)s teine versioon on mõnel teisel vahekaardil endiselt avatud, palun sulge see. %(brand)s kasutamine samal serveril põhjustab vigu olukorras, kus laisk laadimine on samal ajal lubatud ja keelatud.", - "disabled_title": "Kohalikud andmepuhvrid ei ühildu", - "resync_description": "%(brand)s kasutab varasemaga võrreldes 3-5 korda vähem mälu, sest laadib teavet kasutajate kohta vaid siis, kui vaja. Palun oota hetke, kuni sünkroniseerime andmeid serveriga!", - "resync_title": "Uuendan rakendust %(brand)s" - }, "leave_room_dialog": { "last_person_warning": "Sa oled siin viimane osaleja. Kui sa nüüd lahkud, siis mitte keegi, kaasa arvatud sa ise, ei saa hiljem enam liituda.", "leave_room_question": "Kas oled kindel, et soovid lahkuda jututoast „%(roomName)s“?", @@ -1559,6 +1572,7 @@ "toast_description": "%(brand)s toimib nutiseadme veebibrauseris kastseliselt. Parima kasutajakogemuse ja uusima funktsionaalsuse jaoks kasuta meie rakendust.", "toast_title": "Rakendusega saad Matrix'is suhelda parimal viisil" }, + "name_and_id": "%(name)s (%(userId)s)", "no_more_results": "Rohkem otsingutulemusi pole", "notif_panel": { "empty_description": "Sul pole nähtavaid teavitusi.", @@ -2266,6 +2280,8 @@ }, "join_rule_upgrade_upgrading_room": "Uuendan jututoa versiooni", "public_without_alias_warning": "Sellele jututoale viitamiseks palun lisa talle aadress.", + "publish_room": "Tee see jututuba nähtavaks avalikus jututubade kataloogis.", + "publish_space": "Tee see kogukond nähtavaks avalikus jututubade kataloogis.", "strict_encryption": "Ära iialgi saada sellest sessioonist krüptitud sõnumeid verifitseerimata sessioonidesse selles jututoas", "title": "Turvalisus ja privaatsus" }, @@ -2358,9 +2374,6 @@ "custom_theme_success": "Teema sai lisatud!", "custom_theme_url": "Kohandatud teema URL", "font_size": "Fontide suurus", - "font_size_limit": "Kohandatud fondisuurus peab olema vahemikus %(min)s pt ja %(max)s pt", - "font_size_nan": "Suurus peab olema number", - "font_size_valid": "Kasuta suurust vahemikus %(min)s pt ja %(max)s pt", "heading": "Kohenda välimust", "image_size_default": "Tavaline", "image_size_large": "Suur", @@ -2368,7 +2381,7 @@ "layout_irc": "IRC (katseline)", "match_system_theme": "Kasuta süsteemset teemat", "subheading": "Välimuse kohendused kehtivad vaid selles %(brand)s'i sessioonis.", - "timeline_image_size": "Ajajoone piltide suurus", + "timeline_image_size": "Piltide suurus ajajoonel", "use_high_contrast": "Kasuta kontrastset välimust" }, "automatic_language_detection_syntax_highlight": "Kasuta süntaksi esiletõstmisel automaatset keeletuvastust", @@ -2701,6 +2714,7 @@ "device_verified_description": "See sessioon on valmis turvaliseks sõnumivahetuseks.", "device_verified_description_current": "Sinu praegune sessioon on valmis turvaliseks sõnumivahetuseks.", "error_pusher_state": "Tõuketeavituste teenuse oleku määramine ei õnnestunud", + "error_set_name": "Sessiooni nime määramine ei õnnestunud", "filter_all": "Kõik", "filter_inactive": "Pole pidevas kasutuses", "filter_inactive_description": "Pole olnud kasutusel %(inactiveAgeDays)s või enam päeva", @@ -3082,6 +3096,9 @@ "show_thread_filter": "Näita:", "unable_to_decrypt": "Sõnumi dekrüptimine ei õnnestunud" }, + "threads_activity_centre": { + "no_rooms_with_unreads_threads": "Sul veel pole lugemata jutulõngadega jututubasid." + }, "time": { "about_day_ago": "umbes päev tagasi", "about_hour_ago": "umbes tund aega tagasi", @@ -3926,6 +3943,7 @@ "l33t": "Ennustatavatest asendustest nagu '@' 'a' asemel pole eriti kasu", "longerKeyboardPattern": "Kasuta pikemaid klahvikombinatsioone, kus vajutatud klahvid pole kõrvuti ega kohakuti", "noNeed": "Sa ei pea sisestama erilisi tähemärke, numbreid ega suurtähti", + "pwned": "Kui sa kasutad seda salasõna mujalgi, siis palun muuda ta siin ära.", "recentYears": "Väldi hiljutisi aastaid", "repeated": "Väldi korduvaid sõnu ja tähemärke", "reverseWords": "Tagurpidi kirjutatud sõnu pole eriti keeruline ära arvata", @@ -3939,6 +3957,7 @@ "extendedRepeat": "Kordusi, nagu „abcabcabc“ on vaid natuke raskem ära arvata kui „abc“", "keyPattern": "Lühikesi klahvijärjestusi on lihtne ära arvata", "namesByThemselves": "Nimesid ja perenimesid on lihtne ära arvata", + "pwned": "Sinu salasõna on muutunud laiemas internetis toimunud ründe tõttu avalikuks.", "recentYears": "Hiljutisi aastaid on lihtne ära arvata", "sequences": "Jadasid nagu „abc“ või „6543“ on lihtne ära arvata", "similarToCommon": "See on sarnane tavaliselt kasutatavatele salasõnadele", @@ -3946,6 +3965,7 @@ "straightRow": "Klaviatuuril järjest paiknevaid klahvikombinatsioone on lihtne ära arvata", "topHundred": "See on saja levinuima salasõna seas", "topTen": "See on kümne levinuima salasõna seas", + "userInputs": "Siin ei tohiks olla ei isiklikku ega selle lehega seotud andmeid.", "wordByItself": "Üksikut sõna on lihtne ära arvata" } } diff --git a/src/i18n/strings/fa.json b/src/i18n/strings/fa.json index 2efdb62191c..aad57928ed4 100644 --- a/src/i18n/strings/fa.json +++ b/src/i18n/strings/fa.json @@ -422,12 +422,14 @@ "unmute": "صدادار", "unnamed_room": "اتاق بدون نام", "unnamed_space": "فضای کاری بدون نام", + "user": "کاربر", "user_avatar": "تصویر پروفایل", "username": "نام کاربری", "verification_cancelled": "تأیید هویت لغو شد", "video": "ویدئو", "view_message": "مشاهده پیام", - "warning": "هشدار" + "warning": "هشدار", + "welcome": "خوش آمدید" }, "composer": { "autocomplete": { @@ -1023,14 +1025,6 @@ "view_rules": "مشاهده قوانین" }, "language_dropdown_label": "منو زبان", - "lazy_loading": { - "disabled_action": "پاک کردن حافظه‌ی کش و همگام سازی مجدد", - "disabled_description1": "شما از %(brand)s بر روی %(host)s با قابلیت بارگیری اعضا به شکل تکه‌تکه استفاده می‌کنید. در این نسخه قابلیت بارگیری تکه‌تکه غیرفعال است. از آن‌جایی که حافظه‌ی کش مورد استفاده برای این دو پیکربندی با هم سازگار نیست، %(brand)s نیاز به همگام‌سازی مجدد حساب کاربری شما دارد.", - "disabled_description2": "اگر نسخه دیگری از %(brand)s هنوز در تب‌های دیگر باز است، لطفاً آن را ببندید زیرا استفاده از %(brand)s با قابلیت بارگیری تکه‌تکه‌ی فعال روی یکی و غیرفعال روی دیگری، باعث ایجاد مشکل می شود.", - "disabled_title": "حافظه‌ی محلی ناسازگار", - "resync_description": "هم‌اکنون %(brand)s از طریق بارگیری و نمایش اطلاعات کاربران تنها در زمان‌هایی که نیاز است، حدود ۳ تا ۵ مرتبه حافظه‌ی کمتری استفاده می‌کند. لطفا تا همگام‌سازی با سرور منتظر بمانید!", - "resync_title": "به‌روزرسانی %(brand)s" - }, "leave_room_dialog": { "last_person_warning": "شما در این‌جا تنها هستید. اگر اینجا را ترک کنید، دیگر هیچ‌کس حتی خودتان امکان پیوستن مجدد را نخواهید داشت.", "leave_room_question": "آیا مطمئن هستید که می خواهید از اتاق '2%(roomName)s' خارج شوید؟", @@ -1509,9 +1503,6 @@ "custom_theme_success": "پوسته اضافه شد!", "custom_theme_url": "آدرس پوسته دلخواه", "font_size": "اندازه فونت", - "font_size_limit": "اندازه فونت دلخواه تنها می‌تواند عددی بین %(min)s pt و %(max)s pt باشد", - "font_size_nan": "سایز باید یک عدد باشد", - "font_size_valid": "از عددی بین %(min)s pt و %(max)s pt استفاده کنید", "heading": "ظاهر پیام‌رسان خود را سفارشی‌سازی کنید", "image_size_default": "پیشفرض", "match_system_theme": "با پوسته‌ی سیستم تطبیق پیدا کن", diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index d45003b45f4..21535ba92b4 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -230,7 +230,6 @@ "phone_optional_label": "Puhelin (valinnainen)", "qr_code_login": { "connecting": "Yhdistetään…", - "devices_connected": "Yhdistetyt laitteet", "error_device_already_signed_in": "Toinen laite on jo sisäänkirjautunut.", "error_device_not_signed_in": "Toinen laite ei ole sisäänkirjautunut.", "error_device_unsupported": "Tämän laitteen kanssa linkittäminen ei ole tuettu.", @@ -240,7 +239,6 @@ "error_request_cancelled": "Pyyntö peruttiin.", "error_request_declined": "Pyyntö hylättiin toiselta laitteelta.", "error_unexpected": "Tapahtui odottamaton virhe.", - "review_and_approve": "Katselmoi ja hyväksy sisäänkirjautuminen", "sign_in_new_device": "Kirjaa sisään uusi laite", "waiting_for_device": "Odotetaan laitteen sisäänkirjautumista" }, @@ -679,7 +677,6 @@ "no_receipt_found": "Kuittausta ei löytynyt", "no_verification_requests_found": "Vahvistuspyyntöjä ei löytynyt", "number_of_users": "Käyttäjämäärä", - "observe_only": "Tarkkaile ainoastaan", "original_event_source": "Alkuperäinen tapahtumalähde", "phase": "Vaihe", "phase_cancelled": "Peruttu", @@ -687,7 +684,6 @@ "phase_requested": "Pyydetty", "phase_started": "Käynnistetty", "phase_transaction": "Transaktio", - "requester": "Pyytäjä", "room_id": "Huoneen ID-tunniste: %(roomId)s", "room_notifications_sender": "Lähettäjä: ", "save_setting_values": "Tallenna asetusarvot", @@ -1239,6 +1235,7 @@ "group_rooms": "Huoneet", "group_spaces": "Avaruudet", "group_themes": "Teemat", + "group_threads": "Ketjut", "group_voip": "Ääni ja video", "group_widgets": "Sovelmat", "html_topic": "Näytä huoneiden aiheiden HTML-esitys", @@ -1308,14 +1305,6 @@ "view_rules": "Näytä säännöt" }, "language_dropdown_label": "Kielipudotusvalikko", - "lazy_loading": { - "disabled_action": "Tyhjennä välimuisti ja hae tiedot uudelleen", - "disabled_description1": "Olet aikaisemmin käytttänyt %(brand)sia laitteella %(host)s, jossa oli jäsenten laiska lataus käytössä. Tässä versiossa laiska lataus on pois käytöstä. Koska paikallinen välimuisti ei ole yhteensopiva näiden kahden asetuksen välillä, %(brand)sin täytyy synkronoida tilisi tiedot uudelleen.", - "disabled_description2": "Jos sinulla on toinen %(brand)sin versio edelleen auki toisessa välilehdessä, suljethan sen, koska %(brand)sin käyttäminen samalla laitteella niin, että laiska lataus on toisessa välilehdessä käytössä ja toisessa ei, aiheuttaa ongelmia.", - "disabled_title": "Yhteensopimaton paikallinen välimuisti", - "resync_description": "%(brand)s käyttää nyt 3-5 kertaa vähemmän muistia, koska se lataa tietoa muista käyttäjistä vain tarvittaessa. Odotathan, kun haemme tarvittavat tiedot palvelimelta!", - "resync_title": "Päivitetään %(brand)s" - }, "leave_room_dialog": { "last_person_warning": "Olet ainoa henkilö täällä. Jos lähdet, kukaan ei voi liittyä tulevaisuudessa, et myöskään sinä.", "leave_room_question": "Oletko varma että haluat poistua huoneesta '%(roomName)s'?", @@ -2102,9 +2091,6 @@ "custom_theme_success": "Teema lisätty!", "custom_theme_url": "Mukautettu teeman osoite", "font_size": "Fontin koko", - "font_size_limit": "Mukautetun fonttikoon täytyy olla vähintään %(min)s pt ja enintään %(max)s pt", - "font_size_nan": "Koon täytyy olla luku", - "font_size_valid": "Käytä kokoa väliltä %(min)s pt ja %(max)s pt", "heading": "Mukauta ulkoasua", "image_size_default": "Oletus", "image_size_large": "Suuri", diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 1c33feb744d..30441221cbf 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -1,6 +1,8 @@ { "a11y": { + "emoji_picker": "Sélecteur d'emoji", "jump_first_invite": "Sauter à la première invitation.", + "message_composer": "Rédaction des messages", "n_unread_messages": { "other": "%(count)s messages non lus.", "one": "1 message non lu." @@ -9,7 +11,10 @@ "other": "%(count)s messages non lus y compris les mentions.", "one": "1 mention non lue." }, + "recent_rooms": "Salons récents", "room_name": "Salon %(name)s", + "room_status_bar": "Barre de statut du salon", + "seek_bar_label": "Barre de recherche audio", "unread_messages": "Messages non lus.", "user_menu": "Menu utilisateur" }, @@ -186,13 +191,13 @@ "create_account_prompt": "Nouveau ici ? Créez un compte", "create_account_title": "Créer un compte", "email_discovery_text": "Utiliser une adresse e-mail pour pouvoir être découvert par des contacts existants.", - "email_field_label": "E-mail", + "email_field_label": "Adresse e-mail", "email_field_label_invalid": "Cela ne ressemble pas a une adresse e-mail valide", "email_field_label_required": "Saisir l’adresse e-mail", "email_help_text": "Ajouter une adresse e-mail pour pouvoir réinitialiser votre mot de passe.", "email_phone_discovery_text": "Utiliser une adresse e-mail ou un numéro de téléphone pour pouvoir être découvert par des contacts existants.", "enter_email_explainer": "%(homeserver)s va vous envoyer un lien de vérification vous permettant de réinitialiser votre mot de passe.", - "enter_email_heading": "Entrez votre e-mail pour réinitialiser le mot de passe", + "enter_email_heading": "Entrez votre adresse e-mail pour réinitialiser le mot de passe", "failed_connect_identity_server": "Impossible de joindre le serveur d’identité", "failed_connect_identity_server_other": "Vous pouvez vous connecter, mais certaines fonctionnalités ne seront pas disponibles jusqu’au retour du serveur d’identité. Si vous continuez à voir cet avertissement, vérifiez votre configuration ou contactez un administrateur du serveur.", "failed_connect_identity_server_register": "Vous pouvez vous inscrire, mais certaines fonctionnalités ne seront pas disponibles jusqu’au retour du serveur d’identité. Si vous continuez à voir cet avertissement, vérifiez votre configuration ou contactez un administrateur du serveur.", @@ -202,7 +207,7 @@ "failed_soft_logout_auth": "Échec de la ré-authentification", "failed_soft_logout_homeserver": "Échec de la ré-authentification à cause d’un problème du serveur d’accueil", "footer_powered_by_matrix": "propulsé par Matrix", - "forgot_password_email_invalid": "L’adresse de courriel semble être invalide.", + "forgot_password_email_invalid": "L’adresse e-mail semble être invalide.", "forgot_password_email_required": "L’adresse e-mail liée à votre compte doit être renseignée.", "forgot_password_prompt": "Mot de passe oublié ?", "forgot_password_send_email": "Envoyer l’e-mail", @@ -244,7 +249,6 @@ "completing_setup": "Fin de la configuration de votre nouvel appareil", "confirm_code_match": "Vérifiez que le code ci-dessous correspond à celui sur votre autre appareil :", "connecting": "Connexion…", - "devices_connected": "Appareils connectés", "error_device_already_signed_in": "L’autre appareil est déjà connecté.", "error_device_not_signed_in": "L’autre appareil n’est pas connecté.", "error_device_unsupported": "L’appairage avec cet appareil n’est pas pris en charge.", @@ -255,19 +259,17 @@ "error_request_cancelled": "La demande a été annulée.", "error_request_declined": "La requête a été refusée sur l’autre appareil.", "error_unexpected": "Une erreur inattendue s’est produite.", - "review_and_approve": "Vérifier et autoriser la connexion", "scan_code_instruction": "Scannez le QR code ci-dessous avec l’appareil qui n’est pas connecté.", "scan_qr_code": "Scanner le QR code", "select_qr_code": "Sélectionnez « %(scanQRCode)s »", "sign_in_new_device": "Connecter le nouvel appareil", - "start_at_sign_in_screen": "Démarrez à l’écran de connexion", "waiting_for_device": "En attente de connexion de l’appareil" }, "register_action": "Créer un compte", "registration": { - "continue_without_email_description": "Juste une remarque, si vous n'ajoutez pas d’e-mail et que vous oubliez votre mot de passe, vous pourriez perdre définitivement l’accès à votre compte.", - "continue_without_email_field_label": "E-mail (facultatif)", - "continue_without_email_title": "Continuer sans e-mail" + "continue_without_email_description": "Juste une remarque, si vous n'ajoutez pas d’adresse e-mail et que vous oubliez votre mot de passe, vous pourriez perdre définitivement l’accès à votre compte.", + "continue_without_email_field_label": "Adresse e-mail (facultatif)", + "continue_without_email_title": "Continuer sans adresse e-mail" }, "registration_disabled": "L’inscription a été désactivée sur ce serveur d’accueil.", "registration_msisdn_field_required_invalid": "Saisir le numéro de téléphone (obligatoire sur ce serveur d’accueil)", @@ -294,7 +296,7 @@ "reset_password_email_field_required_invalid": "Saisir l’adresse e-mail (obligatoire sur ce serveur d’accueil)", "reset_password_email_not_associated": "Votre adresse e-mail ne semble pas être associée à un identifiant Matrix sur ce serveur d’accueil.", "reset_password_email_not_found_title": "Cette adresse e-mail n’a pas été trouvée", - "reset_password_title": "Réinitialise votre mot de passe", + "reset_password_title": "Réinitialisez votre mot de passe", "server_picker_custom": "Autre serveur d’accueil", "server_picker_description": "Vous pouvez utiliser l’option de serveur personnalisé pour vous connecter à d'autres serveurs Matrix en spécifiant une URL de serveur d'accueil différente. Cela vous permet d’utiliser %(brand)s avec un compte Matrix existant sur un serveur d’accueil différent.", "server_picker_description_matrix.org": "Rejoignez des millions d’utilisateurs gratuitement sur le plus grand serveur public", @@ -318,8 +320,8 @@ }, "set_email_prompt": "Souhaitez-vous configurer une adresse e-mail ?", "sign_in_description": "Utilisez votre compte pour continuer.", - "sign_in_instead": "Se connecter à la place", - "sign_in_instead_prompt": "Se connecter à la place", + "sign_in_instead": "Me connecter avec mon compte existant", + "sign_in_instead_prompt": "Vous avez déjà un compte ? Connectez-vous ici", "sign_in_or_register": "Se connecter ou créer un compte", "sign_in_or_register_description": "Utilisez votre compte ou créez en un pour continuer.", "sign_in_prompt": "Vous avez un compte ? Connectez-vous", @@ -474,6 +476,7 @@ "legal": "Légal", "light": "Clair", "loading": "Chargement…", + "lobby": "Salle d'attente", "location": "Position", "low_priority": "Priorité basse", "matrix": "Matrix", @@ -736,7 +739,6 @@ "notification_state": "L’état des notifications est %(notificationState)s", "notifications_debug": "Débogage des notifications", "number_of_users": "Nombre d’utilisateurs", - "observe_only": "Observer uniquement", "original_event_source": "Évènement source original", "phase": "Phase", "phase_cancelled": "Annulé", @@ -744,7 +746,6 @@ "phase_requested": "Envoyé", "phase_started": "Démarré", "phase_transaction": "Transaction", - "requester": "Demandeur", "room_encrypted": "Le salon est chiffré ✅", "room_id": "Identifiant du salon : %(roomId)s", "room_not_encrypted": "Le salon n’est pas chiffré 🚨", @@ -757,6 +758,7 @@ "room_notifications_type": "Type : ", "room_status": "Statut du salon", "room_unread_status_count": { + "one": "Statut non-lus du salon : %(status)s, total : %(count)s", "other": "Statut non-lus du salon : %(status)s, total : %(count)s" }, "save_setting_values": "Enregistrer les valeurs des paramètres", @@ -1010,7 +1012,7 @@ "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 Sécurité.", "verify_using_device": "Vérifier avec un autre appareil", - "verify_using_key": "Vérifié avec une clé de sécurité", + "verify_using_key": "Vérifier avec une clé de sécurité", "verify_using_key_or_phrase": "Vérifier avec une clé de sécurité ou une phrase", "waiting_for_user_accept": "En attente d’acceptation par %(displayName)s…", "waiting_other_device": "En attente de votre vérification sur votre autre appareil…", @@ -1052,7 +1054,14 @@ "unknown_error_code": "code d’erreur inconnu", "update_power_level": "Échec du changement de rang" }, - "error_database_closed_title": "La base de données s’est fermée de manière inattendue", + "error_app_open_in_another_tab": "Vous pouvez fermer cet onglet déconnecté, et aller à l'autre onglet %(brand)s.", + "error_app_open_in_another_tab_title": "%(brand)s est connecté dans un autre onglet", + "error_app_opened_in_another_window": "%(brand)s est ouvert dans une autre fenêtre. Cliquez \\\"%(label)s\\\" pour utiliser %(brand)s ici et déconnecter l'autre fenêtre.", + "error_database_closed_description": { + "for_desktop": "Votre disque dur est peut-être plein. Libérez de l'espace puis rechargez la page.", + "for_web": "Si vous avez effacé les données de navigation, alors ce message est normal. %(brand)s est peut-être aussi ouvert dans un autre onglet, ou votre disque dur est plein. Libérez de l'espace et rechargez la page" + }, + "error_database_closed_title": "%(brand)s ne fonctionne plus", "error_dialog": { "copy_room_link_failed": { "description": "Impossible de copier le lien du salon dans le presse-papier.", @@ -1086,6 +1095,7 @@ "user": "%(senderName)s a commencé un appel", "you": "Vous avez commencé un appel" }, + "m.emote": "* %(senderName)s %(emote)s", "m.reaction": { "user": "%(sender)s a réagi avec %(reaction)s à %(message)s", "you": "Vous avez réagi avec %(reaction)s à %(message)s" @@ -1261,7 +1271,7 @@ "failed_title": "Échec de l’invitation", "invalid_address": "Adresse non reconnue", "key_share_warning": "Les personnes invitées pourront lire les anciens messages.", - "name_email_mxid_share_room": "Invitez quelqu’un via son nom, e-mail ou pseudo (p. ex. ) ou partagez ce salon.", + "name_email_mxid_share_room": "Invitez quelqu’un via son nom, adresse e-mail ou pseudo (p. ex. ) ou partagez ce salon.", "name_email_mxid_share_space": "Invitez quelqu’un grâce à son nom, adresse e-mail, nom d’utilisateur (tel que ) ou partagez cet espace.", "name_mxid_share_room": "Invitez quelqu’un à partir de son nom, pseudo (comme ) ou partagez ce salon.", "name_mxid_share_space": "Invitez quelqu’un grâce à son nom, nom d’utilisateur (tel que ) ou partagez cet espace.", @@ -1270,7 +1280,7 @@ "room_failed_partial_title": "Certaines invitations n’ont pas pu être envoyées", "room_failed_title": "Impossible d’inviter les utilisateurs dans %(roomName)s", "send_link_prompt": "Ou envoyer le lien d’invitation", - "start_conversation_name_email_mxid_prompt": "Commencer une conversation privée avec quelqu’un via son nom, e-mail ou pseudo (comme par exemple ).", + "start_conversation_name_email_mxid_prompt": "Commencer une conversation privée avec quelqu’un via son nom, adresse e-mail ou pseudo (comme par exemple ).", "start_conversation_name_mxid_prompt": "Commencer une conversation privée avec quelqu’un en utilisant son nom ou son pseudo (comme ).", "suggestions_disclaimer": "Certaines suggestions pourraient être masquées pour votre confidentialité.", "suggestions_disclaimer_prompt": "Si vous ne trouvez pas la personne que vous cherchez, envoyez-lui le lien d’invitation ci-dessous.", @@ -1401,6 +1411,7 @@ "group_rooms": "Salons", "group_spaces": "Espaces", "group_themes": "Thèmes", + "group_threads": "Fils de discussion", "group_voip": "Audio et vidéo", "group_widgets": "Widgets", "hidebold": "Masquer le point de notification (affiche seulement les badges des compteurs)", @@ -1418,6 +1429,7 @@ "msc3531_hide_messages_pending_moderation": "Permettre aux modérateurs de cacher des messages en attente de modération.", "new_room_decoration_ui": "En cours de développement, nouvel en-tête de salon et interface des détails", "notification_settings": "Nouveaux paramètres de notification", + "notification_settings_beta_caption": "Introduit une manière plus simple de changer vos préférences de notifications. Customisez %(brand)s, comme ça vous convient.", "notification_settings_beta_title": "Paramètres de notification", "notifications": "Active le panneau de notifications dans l’en-tête du salon", "oidc_native_flow": "Authentification native OIDC", @@ -1428,6 +1440,10 @@ "report_to_moderators": "Signaler aux modérateurs", "report_to_moderators_description": "Dans les salons prenant en charge la modération, le bouton « Signaler » vous permet de signaler des abus aux modérateurs du salon.", "rust_crypto": "Implémentation cryptographique en Rust", + "rust_crypto_in_config": "La cryptographie Rust ne peut être désactivée sur ce déploiement de %(brand)s", + "rust_crypto_in_config_description": "Si vous passez à la cryptographie Rust, cela démarrera un processus de migration qui peut durer plusieurs minutes. Il ne peut être arrêté; à utiliser prudemment !", + "rust_crypto_optin_warning": "Si vous passez à la cryptographie Rust, cela démarrera un processus de migration qui peut durer plusieurs minutes. Pour la désactiver, vous devrez vous déconnecter et vous reconnecter; à utiliser prudemment !", + "rust_crypto_requires_logout": "Une fois activée, la cryptographie Rust ne peut être désactivée qu'en se déconnectant et se reconnectant", "sliding_sync": "Mode synchronisation progressive", "sliding_sync_checking": "Vérification…", "sliding_sync_configuration": "Configuration de la synchronisation progressive", @@ -1439,6 +1455,8 @@ "sliding_sync_server_no_support": "Votre serveur manque d’un support natif", "sliding_sync_server_specify_proxy": "Votre serveur manque d’un support natif, vous devez spécifier un serveur mandataire (proxy)", "sliding_sync_server_support": "Votre serveur a un support natif", + "threads_activity_centre": "Centre d'activité des fils de discussion (en développement)", + "threads_activity_centre_description": "Attention: en cours de développement actif. Recharge %(brand)s", "under_active_development": "En cours de développement.", "unrealiable_e2e": "Non fiable dans les salons chiffrés", "video_rooms": "Salons vidéo", @@ -1488,14 +1506,6 @@ "view_rules": "Voir les règles" }, "language_dropdown_label": "Sélection de la langue", - "lazy_loading": { - "disabled_action": "Vider le cache et resynchroniser", - "disabled_description1": "Vous avez utilisé auparavant %(brand)s sur %(host)s avec le chargement différé activé. Dans cette version le chargement différé est désactivé. Comme le cache local n’est pas compatible entre ces deux réglages, %(brand)s doit resynchroniser votre compte.", - "disabled_description2": "Si l’autre version de %(brand)s est encore ouverte dans un autre onglet, merci de le fermer car l’utilisation de %(brand)s sur le même hôte avec le chargement différé activé et désactivé à la fois causera des problèmes.", - "disabled_title": "Cache local incompatible", - "resync_description": "%(brand)s utilise maintenant 3 à 5 fois moins de mémoire, en ne chargeant les informations des autres utilisateurs que quand elles sont nécessaires. Veuillez patienter pendant que l’on se resynchronise avec le serveur !", - "resync_title": "Mise à jour de %(brand)s" - }, "leave_room_dialog": { "last_person_warning": "Vous êtes la seule personne ici. Si vous partez, plus personne ne pourra rejoindre cette conversation, y compris vous.", "leave_room_question": "Voulez-vous vraiment quitter le salon « %(roomName)s » ?", @@ -1567,11 +1577,13 @@ }, "member_list_back_action_label": "Membres du salon", "message_edit_dialog_title": "Modifications du message", + "migrating_crypto": "Accrochez-vous. Nous mettons à jour %(brand)s pour que le chiffrement soit plus rapide et plus fiable.", "mobile_guide": { "toast_accept": "Utiliser l’application", "toast_description": "%(brand)s est expérimental sur un navigateur mobile. Pour une meilleure expérience et bénéficier des dernières fonctionnalités, utilisez notre application native gratuite.", "toast_title": "Utilisez une application pour une meilleure expérience" }, + "name_and_id": "%(name)s (%(userId)s)", "no_more_results": "Fin des résultats", "notif_panel": { "empty_description": "Vous n’avez aucune notification visible.", @@ -1593,6 +1605,7 @@ "level_activity": "Activité", "level_muted": "Muet", "level_none": "Aucun", + "level_notification": "Notification", "level_unsent": "Non envoyé", "mark_all_read": "Tout marquer comme lu", "mentions_and_keywords": "@mentions et mots-clés", @@ -1763,6 +1776,7 @@ "nature": "Veuillez choisir la nature du rapport et décrire ce qui rend ce message abusif.", "nature_disagreement": "Ce que cet utilisateur écrit est déplacé.\nCeci sera signalé aux modérateurs du salon.", "nature_illegal": "Cet utilisateur fait preuve d’un comportement illicite, par exemple en publiant des informations personnelles d’autres ou en proférant des menaces.\nCeci sera signalé aux modérateurs du salon qui pourront l’escalader aux autorités.", + "nature_nonstandard_admin": "Le sujet de ce salon est illégal ou toxique, ou les modérateurs ne modèrent pas le contenu illégal ou toxique.\nCe fait sera signalé aux administrateurs de %(homeserver)s.", "nature_other": "Toute autre raison. Veuillez décrire le problème.\nCeci sera signalé aux modérateurs du salon.", "nature_spam": "Cet utilisateur inonde le salon de publicités ou liens vers des publicités, ou vers de la propagande.\nCeci sera signalé aux modérateurs du salon.", "nature_toxic": "Cet utilisateur fait preuve d’un comportement toxique, par exemple en insultant les autres ou en partageant du contenu pour adultes dans un salon familial, ou en violant les règles de ce salon.\nCeci sera signalé aux modérateurs du salon.", @@ -1981,7 +1995,7 @@ "leave_server_notices_description": "Ce salon est utilisé pour les messages importants du serveur d’accueil, vous ne pouvez donc pas en partir.", "leave_server_notices_title": "Impossible de quitter le salon des Annonces du Serveur", "leave_unexpected_error": "Erreur de serveur inattendue en essayant de quitter le salon", - "link_email_to_receive_3pid_invite": "Liez cet e-mail à votre compte dans les paramètres pour recevoir les invitations directement dans %(brand)s.", + "link_email_to_receive_3pid_invite": "Associez cette adresse e-mail à votre compte dans les paramètres pour recevoir les invitations directement dans %(brand)s.", "loading_preview": "Chargement de l’aperçu", "no_peek_join_prompt": "Vous ne pouvez pas avoir d’aperçu de %(roomName)s. Voulez-vous rejoindre le salon ?", "no_peek_no_name_join_prompt": "Il n’y a pas d’aperçu, voulez-vous rejoindre ?", @@ -2388,9 +2402,6 @@ "custom_theme_success": "Thème ajouté !", "custom_theme_url": "URL personnalisée pour le thème", "font_size": "Taille de la police", - "font_size_limit": "La taille de police personnalisée doit être comprise entre %(min)s pt et %(max)s pt", - "font_size_nan": "La taille doit être un nombre", - "font_size_valid": "Utiliser entre %(min)s pt et %(max)s pt", "heading": "Personnalisez l’apparence", "image_size_default": "Par défaut", "image_size_large": "Grande", @@ -2424,7 +2435,7 @@ "add_msisdn_instructions": "Un SMS a été envoyé à +%(msisdn)s. Saisissez le code de vérification qu’il contient.", "add_msisdn_misconfigured": "L’ajout / liaison avec le flux MSISDN est mal configuré", "confirm_adding_email_body": "Cliquez sur le bouton ci-dessous pour confirmer l’ajout de l’adresse e-mail.", - "confirm_adding_email_title": "Confirmer l’ajout de l’e-mail", + "confirm_adding_email_title": "Confirmer l’ajout de l’adresse e-mail", "deactivate_confirm_body": "Voulez-vous vraiment désactiver votre compte ? Ceci est irréversible.", "deactivate_confirm_body_password": "Pour continuer, saisissez votre mot de passe de connexion :", "deactivate_confirm_body_sso": "Confirmez la désactivation de votre compte en utilisant l’authentification unique pour prouver votre identité.", @@ -2604,6 +2615,7 @@ "voip": "Appels audio et vidéo" }, "preferences": { + "Electron.enableHardwareAcceleration": "Activer l’accélération matérielle (redémarrez %(appName)s pour l'activer)", "always_show_menu_bar": "Toujours afficher la barre de menu de la fenêtre", "autocomplete_delay": "Délai pour l’autocomplétion (ms)", "code_blocks_heading": "Blocs de code", @@ -2736,6 +2748,7 @@ "device_verified_description": "Cette session est prête pour l’envoi de messages sécurisés.", "device_verified_description_current": "Votre session actuelle est prête pour une messagerie sécurisée.", "error_pusher_state": "Échec lors de la définition de l’état push", + "error_set_name": "Impossible d'enregistrer le nom de la session", "filter_all": "Tout", "filter_inactive": "Inactive", "filter_inactive_description": "Inactive depuis au moins %(inactiveAgeDays)s jours", @@ -3142,7 +3155,13 @@ "n_hours_ago": "il y a %(num)s heures", "n_minutes_ago": "il y a %(num)s minutes", "seconds_left": "%(seconds)s secondes restantes", - "short_days_hours_minutes_seconds": "%(days)sj %(hours)sh %(minutes)sm %(seconds)ss" + "short_days": "%(value)sj", + "short_days_hours_minutes_seconds": "%(days)sj %(hours)sh %(minutes)sm %(seconds)ss", + "short_hours": "%(value)sh", + "short_hours_minutes_seconds": "%(hours)sh %(minutes)sm %(seconds)ss", + "short_minutes": "%(value)sm", + "short_minutes_seconds": "%(minutes)sm %(seconds)ss", + "short_seconds": "%(value)ss" }, "timeline": { "context_menu": { @@ -3158,6 +3177,7 @@ "creation_summary_dm": "%(creator)s a créé cette conversation privée.", "creation_summary_room": "%(creator)s a créé et configuré le salon.", "decryption_failure_blocked": "L’expéditeur a bloqué la réception de votre message", + "disambiguated_profile": "%(displayName)s (%(matrixId)s)", "download_action_decrypting": "Déchiffrement", "download_action_downloading": "Téléchargement en cours", "edits": { @@ -3237,6 +3257,12 @@ "location": "A partagé une position : ", "self_location": "Ont partagé leur position : " }, + "m.poll": { + "count_of_votes": { + "one": "%(count)s vote", + "other": "%(count)s votes" + } + }, "m.poll.end": { "ended": "Sondage terminé", "sender_ended": "%(senderName)s a terminé un sondage" @@ -3371,6 +3397,8 @@ "label": "Actions de message", "view_in_room": "Voir dans le salon" }, + "message_timestamp_received_at": "Reçu à : %(dateTime)s", + "message_timestamp_sent_at": "Envoyé à : %(dateTime)s", "mjolnir": { "changed_rule_glob": "%(senderName)s a mis à jour une règle de bannissement correspondant à %(oldGlob)s vers une règle correspondant à %(newGlob)s pour %(reason)s", "changed_rule_rooms": "%(senderName)s a changé une règle qui bannit les salons correspondant à %(oldGlob)s vers une règle correspondant à %(newGlob)s pour %(reason)s", @@ -3449,6 +3477,7 @@ "other": "%(severalUsers)s ont changé de nom %(count)s fois", "one": "%(severalUsers)s ont changé de nom" }, + "format": "%(nameList)s %(transitionList)s", "hidden_event": { "one": "%(oneUser)s a envoyé un message caché", "other": "%(oneUser)s ont envoyé %(count)s messages cachés" @@ -3772,6 +3801,8 @@ "join_button_tooltip_call_full": "Désolé — Cet appel est actuellement complet", "join_button_tooltip_connecting": "Connexion", "maximise": "Remplir l’écran", + "maximise_call": "Plein écran", + "minimise_call": "Quitter le mode plein écran", "misconfigured_server": "L’appel a échoué à cause d’un serveur mal configuré", "misconfigured_server_description": "Demandez à l’administrateur de votre serveur d’accueil (%(homeserverDomain)s) de configurer un serveur TURN afin que les appels fonctionnent de manière fiable.", "misconfigured_server_fallback": "Vous pouvez sinon essayer d’utiliser le serveur public , mais ça ne sera pas aussi fiable et votre adresse IP sera partagée avec ce serveur. Vous pouvez aussi gérer ce réglage dans les paramètres.", diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index 2c58a7ed38b..e58b0ea5adb 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -619,7 +619,6 @@ "methods": "Métodos", "no_verification_requests_found": "Non se atopan solicitudes de verificación", "number_of_users": "Número de usuarias", - "observe_only": "Só observar", "original_event_source": "Fonte orixinal do evento", "phase": "Fase", "phase_cancelled": "Cancelado", @@ -627,7 +626,6 @@ "phase_requested": "Solicitado", "phase_started": "Iniciado", "phase_transaction": "Transacción", - "requester": "Solicitante", "room_id": "ID da sala: %(roomId)s", "save_setting_values": "Gardar valores configurados", "send_custom_account_data_event": "Enviar evento de datos da conta personalizado", @@ -1202,6 +1200,7 @@ "group_rooms": "Salas", "group_spaces": "Espazos", "group_themes": "Decorados", + "group_threads": "Conversas", "group_voip": "Voz e Vídeo", "html_topic": "Mostrar representación HTML dos temas da sala", "join_beta": "Unirse á beta", @@ -1262,14 +1261,6 @@ "view_rules": "Ver regras" }, "language_dropdown_label": "Selector de idioma", - "lazy_loading": { - "disabled_action": "Baleirar caché e sincronizar", - "disabled_description1": "Anteriormente utilizaches %(brand)s en %(host)s con carga preguiceira de membros. Nesta versión a carga preguiceira está desactivada. Como a caché local non é compatible entre as dúas configuracións, %(brand)s precisa volver a sincronizar a conta.", - "disabled_description2": "Se a outra versión de %(brand)s aínda está aberta noutra lapela, péchaa xa que usar %(brand)s no mesmo servidor con carga preguiceira activada e desactivada ao mesmo tempo causará problemas.", - "disabled_title": "Caché local incompatible", - "resync_description": "%(brand)s utiliza agora entre 3 e 5 veces menos memoria, cargando só información sobre as usuarias cando é preciso. Agarda mentras se sincroniza co servidor!", - "resync_title": "Actualizando %(brand)s" - }, "leave_room_dialog": { "last_person_warning": "Es a única persoa aquí. Se saes, ninguén poderá unirse no futuro, incluíndote a ti.", "leave_room_question": "Seguro que desexa saír da sala '%(roomName)s'?", @@ -2042,9 +2033,6 @@ "custom_theme_success": "Decorado engadido!", "custom_theme_url": "URL do decorado personalizado", "font_size": "Tamaño da letra", - "font_size_limit": "O tamaño da fonte só pode estar entre %(min)s pt e %(max)s pt", - "font_size_nan": "O tamaño ten que ser un número", - "font_size_valid": "Usa entre %(min)s pt e %(max)s pt", "heading": "Personaliza o aspecto", "image_size_default": "Por defecto", "image_size_large": "Grande", diff --git a/src/i18n/strings/he.json b/src/i18n/strings/he.json index 0403e8f0683..3a3c2ad7360 100644 --- a/src/i18n/strings/he.json +++ b/src/i18n/strings/he.json @@ -1057,14 +1057,6 @@ "view_rules": "צפה בכללים" }, "language_dropdown_label": "תפריט שפות", - "lazy_loading": { - "disabled_action": "נקה מטמון וסנכרן מחדש", - "disabled_description1": "השתמשת בעבר ב- %(brand)s ב- %(host)s עם טעינה עצלה של חברים מופעלת. בגרסה זו טעינה עצלה מושבתת. מכיוון שהמטמון המקומי אינו תואם בין שתי ההגדרות הללו, %(brand)s צריך לסנכרן מחדש את חשבונך.", - "disabled_description2": "אם הגרסה האחרת של %(brand)s עדיין פתוחה בכרטיסייה אחרת, אנא סגור אותה כשימוש ב-%(brand)s באותו מארח כאשר טעינה עצלה מופעלת וגם מושבתת בו זמנית תגרום לבעיות.", - "disabled_title": "מטמון מקומי לא תואם", - "resync_description": "%(brand)s משתמש כעת בזכרון פחות פי 3-5, על ידי טעינת מידע רק על משתמשים אחרים בעת הצורך. אנא המתן בזמן שאנחנו מסתנכרנים מחדש עם השרת!", - "resync_title": "מעדכן %(brand)s" - }, "leave_room_dialog": { "leave_room_question": "האם אתה בטוח שברצונך לעזוב את החדר '%(roomName)s'?", "room_rejoin_warning": "חדר זה אינו ציבורי. לא תוכל להצטרף שוב ללא הזמנה." @@ -1633,9 +1625,6 @@ "custom_theme_success": "ערכת נושא התווספה בהצלחה!", "custom_theme_url": "כתובת ערכת נושא מותאמת אישית", "font_size": "גודל אותיות", - "font_size_limit": "גודל גופן מותאם אישית יכול להיות רק בין %(min)s ל %(max)s נקודות", - "font_size_nan": "הגדול חייב להיות מספר", - "font_size_valid": "השתמש בין %(min)s ל %(max)s נקודות", "heading": "התאם את התצוגה שלך", "image_size_default": "ברירת מחדל", "image_size_large": "גדול", diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 31da450d65b..69988db429f 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -243,7 +243,6 @@ "completing_setup": "Új eszköz beállításának elvégzése", "confirm_code_match": "Ellenőrizze, hogy az alábbi kód megegyezik a másik eszközödön lévővel:", "connecting": "Kapcsolás…", - "devices_connected": "Összekötött eszközök", "error_device_already_signed_in": "A másik eszköz már bejelentkezett.", "error_device_not_signed_in": "A másik eszköz még nincs bejelentkezve.", "error_device_unsupported": "Összekötés ezzel az eszközzel nem támogatott.", @@ -253,12 +252,10 @@ "error_request_cancelled": "A kérés megszakítva.", "error_request_declined": "A kérést elutasították a másik eszközön.", "error_unexpected": "Nemvárt hiba történt.", - "review_and_approve": "Belépés áttekintése és engedélyezés", "scan_code_instruction": "A kijelentkezett eszközzel olvasd be a QR kódot alább.", "scan_qr_code": "QR kód beolvasása", "select_qr_code": "Kiválasztás „%(scanQRCode)s”", "sign_in_new_device": "Új eszköz bejelentkeztetése", - "start_at_sign_in_screen": "Kezdje a bejelentkező képernyőn", "waiting_for_device": "Várakozás a másik eszköz bejelentkezésére" }, "register_action": "Fiók létrehozása", @@ -371,25 +368,25 @@ "verify_email_heading": "E-mail ellenőrzés a továbblépéshez" }, "bug_reporting": { - "additional_context": "Ha a hiba felderítésében további adat is segítséget adhat, mint az, hogy mit csináltál éppen, mi a szoba-, felhasználó azonosítója, stb... itt add meg.", - "before_submitting": "Mielőtt a naplót elküldöd, egy Github jegyet kell nyitni amiben leírod a problémádat.", + "additional_context": "Ha a hiba felderítésében további adatok is segíthetnek, mint az, hogy mit csinált épp, mik a szobák vagy felhasználók azonosítói, stb. Ezeket itt adja meg.", + "before_submitting": "Mielőtt elküldi a naplókat, hozzon létre egy jegyet a GitHubon, amelyben leírja a problémáját.", "collecting_information": "Alkalmazás verzióinformációinak összegyűjtése", "collecting_logs": "Naplók összegyűjtése", "create_new_issue": "Ahhoz hogy megvizsgálhassuk a hibát, hozzon létre egy új hibajegyet a GitHubon.", - "description": "A hibakeresési napló alkalmazáshasználati adatokat tartalmaz, amely tartalmazza a felhasználónevét, a felkeresett szobák azonosítóit vagy álneveit, az utolsó felhasználói felület elemét, amelyet használt, valamint a többi felhasználó neveit. A csevegési üzenetek szövegét nem tartalmazza.", - "download_logs": "Napló letöltése", + "description": "A hibakeresési naplók alkalmazáshasználati adatokat tartalmaznak, amelyek tartalmazzák a felkeresett szobák azonosítóit vagy álneveit, a legutóbb használt felületi elemeket, valamint a többi felhasználó neveit. A csevegőüzenetek szövegét nem tartalmazza.", + "download_logs": "Naplók letöltése", "downloading_logs": "Naplók letöltése folyamatban", "error_empty": "Kérlek mond el nekünk mi az ami nem működött, vagy még jobb, ha egy GitHub jegyben leírod a problémát.", "failed_send_logs": "Hiba a napló küldésénél: ", - "github_issue": "GitHub hibajegy", - "introduction": "Ha a GitHubon keresztül küldött be hibajegyet, akkor a hibakeresési napló segít nekünk felderíteni a problémát. ", + "github_issue": "GitHub-jegy", + "introduction": "Ha a GitHubon keresztül küldött be hibajegyet, akkor a hibakeresési naplók segítenek nekünk felderíteni a problémát. ", "log_request": "Segítsen abban, hogy ez később ne fordulhasson elő, küldje el nekünk a naplókat.", "logs_sent": "Napló elküldve", "matrix_security_issue": "A Matrixszal kapcsolatos biztonsági hibák jelentésével kapcsolatban olvassa el a Matrix.org biztonsági hibák közzétételi házirendjét.", "preparing_download": "Napló előkészítése feltöltéshez", "preparing_logs": "Előkészülés napló küldéshez", - "send_logs": "Naplófájlok elküldése", - "submit_debug_logs": "Hibakeresési napló elküldése", + "send_logs": "Naplók elküldése", + "submit_debug_logs": "Hibakeresési naplók elküldése", "textarea_label": "Megjegyzések", "thank_you": "Köszönjük!", "title": "Hibajelentés", @@ -459,10 +456,10 @@ "general": "Általános", "go_to_settings": "Irány a Beállítások", "guest": "Vendég", - "help": "Segítség", + "help": "Súgó", "historical": "Archív", "home": "Kezdőlap", - "homeserver": "Matrix kiszolgáló", + "homeserver": "Matrix-kiszolgáló", "identity_server": "Azonosítási kiszolgáló", "image": "Kép", "integration_manager": "Integrációkezelő", @@ -575,9 +572,9 @@ "close_sticker_picker": "Matricák elrejtése", "edit_composer_label": "Üzenet szerkesztése", "format_bold": "Félkövér", - "format_code_block": "Kód blokk", - "format_decrease_indent": "Behúzás csökkentés", - "format_increase_indent": "Behúzás növelés", + "format_code_block": "Kódblokk", + "format_decrease_indent": "Behúzás csökkentése", + "format_increase_indent": "Behúzás növelése", "format_inline_code": "Kód", "format_insert_link": "Link beillesztése", "format_italic": "Dőlt", @@ -620,7 +617,7 @@ "console_wait": "Várjon!", "create_room": { "action_create_room": "Szoba létrehozása", - "action_create_video_room": "Videó szoba készítése", + "action_create_video_room": "Videószoba létrehozása", "encrypted_video_room_warning": "Ezt később nem lehet kikapcsolni. A szoba titkosítva lesz de a hívások nem.", "encrypted_warning": "Ezt később nem lehet kikapcsolni. A hidak és a legtöbb bot nem fog működni egyenlőre.", "encryption_forced": "A szervered megköveteli, hogy a titkosítás be legyen kapcsolva a privát szobákban.", @@ -639,7 +636,7 @@ "room_visibility_label": "Szoba láthatóság", "title_private_room": "Privát szoba létrehozása", "title_public_room": "Nyilvános szoba létrehozása", - "title_video_room": "Videó szoba készítése", + "title_video_room": "Videószoba létrehozása", "topic_label": "Téma (nem kötelező)", "unfederated": "A szobába ne léphessenek be azok, akik nem ezen a szerveren vannak: %(serverName)s.", "unfederated_label_default_off": "Beállíthatod, ha a szobát csak egy belső csoport használja majd a matrix szervereden. Ezt később nem lehet megváltoztatni.", @@ -716,7 +713,7 @@ "event_type": "Esemény típusa", "explore_account_data": "Fiókadatok felderítése", "explore_room_account_data": "Szoba fiók adatok felderítése", - "explore_room_state": "Szoba állapot felderítése", + "explore_room_state": "Szobaállapot felderítése", "failed_to_find_widget": "Hiba történt a kisalkalmazás keresése során.", "failed_to_load": "Betöltés sikertelen.", "failed_to_save": "A beállítások elmentése nem sikerült.", @@ -733,7 +730,6 @@ "notification_state": "Értesítés állapot: %(notificationState)s", "notifications_debug": "Értesítések hibakeresése", "number_of_users": "Felhasználószám", - "observe_only": "Csak megfigyel", "original_event_source": "Eredeti esemény forráskód", "phase": "Fázis", "phase_cancelled": "Megszakítva", @@ -741,7 +737,6 @@ "phase_requested": "Kérve", "phase_started": "Elindult", "phase_transaction": "Tranzakció", - "requester": "Kérelmező", "room_encrypted": "A szoba titkosított ✅", "room_id": "Szoba azon.: %(roomId)s", "room_not_encrypted": "A szoba nincs titkosítva 🚨", @@ -761,7 +756,7 @@ "send_custom_account_data_event": "Egyedi fiókadat esemény küldése", "send_custom_room_account_data_event": "Egyedi szoba fiókadat esemény küldése", "send_custom_state_event": "Egyedi állapotesemény küldése", - "send_custom_timeline_event": "Egyedi idővonal esemény küldése", + "send_custom_timeline_event": "Egyéni idővonal-esemény küldése", "server_info": "Kiszolgálóinformációk", "server_versions": "Kiszolgálóverziók", "settable_global": "Általánosan beállítható", @@ -1381,7 +1376,7 @@ "dehydration": "Kapcsolat nélküli titkosított üzenetküldés tartósított eszközökkel", "dynamic_room_predecessors": "A dinamikus szoba előfutárai", "dynamic_room_predecessors_description": "MSC3946 engedélyezése (a későn érkező szobaarchívumok támogatáshoz)", - "element_call_video_rooms": "Element videóhívásos szobák", + "element_call_video_rooms": "Element Call videószobák", "experimental_description": "Kísérletező kedvében van? Próbálja ki a legújabb fejlesztési ötleteinket. Ezek nincsenek befejezve; lehet, hogy instabilak, megváltozhatnak vagy el is tűnhetnek. Tudjon meg többet.", "experimental_section": "Lehetőségek korai megjelenítése", "feature_wysiwyg_composer_description": "Szövegszerkesztő használata a Markdown formázás helyett az üzenet írásakor.", @@ -1474,14 +1469,6 @@ "view_rules": "Szabályok megtekintése" }, "language_dropdown_label": "Nyelvválasztó lenyíló menü", - "lazy_loading": { - "disabled_action": "Gyorsítótár törlése és újraszinkronizálás", - "disabled_description1": "Előzőleg a szoba tagság késleltetett betöltésének engedélyével itt használtad a %(brand)sot: %(host)s. Ebben a verzióban viszont a késleltetett betöltés nem engedélyezett. Mivel a két gyorsítótár nem kompatibilis egymással így %(brand)snak újra kell szinkronizálnia a fiókot.", - "disabled_description2": "Ha a másik %(brand)s verzió még fut egy másik fülön, akkor zárja be, mert ha egy gépen használja a %(brand)sot úgy, hogy az egyiken be van kapcsolva a késleltetett betöltés, a másikon pedig ki, akkor problémák adódhatnak.", - "disabled_title": "A helyi gyorsítótár nem kompatibilis ezzel a verzióval", - "resync_description": "Az %(brand)s harmad-ötöd annyi memóriát használ azáltal, hogy csak akkor tölti be a felhasználók információit, amikor az szükséges. Kis türelmet, amíg megtörténik az újbóli szinkronizálás a kiszolgálóval.", - "resync_title": "%(brand)s frissítése" - }, "leave_room_dialog": { "last_person_warning": "Csak ön van itt. Ha kilép, akkor a jövőben senki nem tud majd ide belépni, beleértve önt is.", "leave_room_question": "Biztos, hogy elhagyja a(z) „%(roomName)s” szobát?", @@ -2342,9 +2329,6 @@ "custom_theme_success": "Téma hozzáadva!", "custom_theme_url": "Egyéni téma webcíme", "font_size": "Betűméret", - "font_size_limit": "Az egyéni betűméret csak %(min)s pont és %(max)s pont közötti lehet", - "font_size_nan": "A méretnek számnak kell lennie", - "font_size_valid": "%(min)s pont és %(max)s pont közötti értéket használjon", "heading": "A megjelenés testreszabása", "image_size_default": "Alapértelmezett", "image_size_large": "Nagy", diff --git a/src/i18n/strings/id.json b/src/i18n/strings/id.json index ad298664608..4f942d89da7 100644 --- a/src/i18n/strings/id.json +++ b/src/i18n/strings/id.json @@ -163,6 +163,7 @@ "autodiscovery_invalid_is_base_url": "base_url tidak absah untuk m.identity_server", "autodiscovery_invalid_is_response": "Respons penemuan server identitas tidak absah", "autodiscovery_invalid_json": "JSON tidak absah", + "autodiscovery_no_well_known": "Tidak ditemukan berkas JSON .well-known", "autodiscovery_unexpected_error_hs": "Kesalahan tidak terduga saat menyelesaikan konfigurasi homeserver", "autodiscovery_unexpected_error_is": "Kesalahan tidak terduga saat menyelesaikan konfigurasi server identitas", "captcha_description": "Homeserver ini memastikan Anda bahwa Anda bukan sebuah robot.", @@ -185,6 +186,7 @@ "create_account_prompt": "Baru di sini? Buat sebuah akun", "create_account_title": "Buat akun", "email_discovery_text": "Gunakan email untuk dapat ditemukan oleh kontak yang sudah ada secara opsional.", + "email_field_label": "E-mail", "email_field_label_invalid": "Kelihatannya bukan sebuah alamat email yang absah", "email_field_label_required": "Masukkan alamat email", "email_help_text": "Tambahkan sebuah email untuk dapat mengatur ulang kata sandi Anda.", @@ -227,6 +229,7 @@ "no_hs_url_provided": "Tidak ada URL homeserver yang disediakan", "oidc": { "error_title": "Kami tidak dapat memasukkan Anda", + "logout_redirect_warning": "Anda akan diarahkan ke penyedia autentikasi server Anda untuk menyelesaikan proses keluar.", "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…", @@ -240,7 +243,6 @@ "completing_setup": "Menyelesaikan penyiapan perangkat baru Anda", "confirm_code_match": "Periksa bahwa kode di bawah cocok dengan perangkat Anda yang lain:", "connecting": "Menghubungkan…", - "devices_connected": "Perangkat terhubung", "error_device_already_signed_in": "Perangkat yang lain sudah masuk.", "error_device_not_signed_in": "Perangkat yang lain belum masuk.", "error_device_unsupported": "Penautan dengan perangkat ini tidak didukung.", @@ -250,12 +252,10 @@ "error_request_cancelled": "Permintaan dibatalkan.", "error_request_declined": "Permintaan ditolak di perangkat yang lain.", "error_unexpected": "Sebuah kesalahan terjadi secara tidak terduga.", - "review_and_approve": "Lihat dan perbolehkan pemasukan", "scan_code_instruction": "Pindai kode QR di bawah dengan perangkat Anda yang sudah keluar dari akun.", "scan_qr_code": "Pindai kode QR", "select_qr_code": "Pilih '%(scanQRCode)s'", "sign_in_new_device": "Masuk perangkat baru", - "start_at_sign_in_screen": "Mulai dari layar masuk", "waiting_for_device": "Menunggu perangkat untuk masuk" }, "register_action": "Buat Akun", @@ -284,6 +284,7 @@ "sign_out_other_devices": "Keluarkan semua perangkat" }, "reset_password_action": "Atur ulang kata sandi", + "reset_password_button": "Lupa kata sandi?", "reset_password_email_field_description": "Gunakan sebuah alamat email untuk memulihkan akun Anda", "reset_password_email_field_required_invalid": "Masukkan alamat email (diperlukan di homeserver ini)", "reset_password_email_not_associated": "Alamat email Anda terlihat tidak diasosiasikan dengan sebuah ID Matrix di homeserver ini.", @@ -330,6 +331,7 @@ "soft_logout_intro_unsupported_auth": "Anda tidak dapat masuk ke akun Anda. Mohon hubungi admin homeserver untuk informasi lanjut.", "soft_logout_subheading": "Hapus data personal", "soft_logout_warning": "Peringatan: Data personal Anda (termasuk kunci enkripsi) masih disimpan di sesi ini. Hapus jika Anda selesai menggunakan sesi ini, atau jika ingin masuk ke akun yang lain.", + "sso": "Sistem Masuk Tunggal", "sso_failed_missing_storage": "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.", "sso_or_username_password": "%(ssoButtons)s Atau %(usernamePassword)s", "sync_footer_subtitle": "Jika Anda bergabung dengan banyak ruangan, ini mungkin membutuhkan beberapa waktu", @@ -427,6 +429,7 @@ "are_you_sure": "Apakah Anda yakin?", "attachment": "Lampiran", "authentication": "Autentikasi", + "avatar": "Avatar", "beta": "Beta", "camera": "Kamera", "cameras": "Kamera", @@ -726,7 +729,6 @@ "notification_state": "Keadaan notifikasi adalah %(notificationState)s", "notifications_debug": "Pengawakutuan notifikasi", "number_of_users": "Jumlah pengguna", - "observe_only": "Lihat saja", "original_event_source": "Sumber peristiwa asli", "phase": "Masa", "phase_cancelled": "Dibatalkan", @@ -734,7 +736,6 @@ "phase_requested": "Diminta", "phase_started": "Dimulai", "phase_transaction": "Transaksi", - "requester": "Peminta", "room_encrypted": "Ruangan terenkripsi ✅", "room_id": "ID ruangan: %(roomId)s", "room_not_encrypted": "Ruangan tidak terenkripsi 🚨", @@ -747,7 +748,7 @@ "room_notifications_type": "Jenis: ", "room_status": "Keadaan ruangan", "room_unread_status_count": { - "other": "Keadaan belum dibaca ruangan: %(status)s, jumlah: %(count)s" + "other": "Keadaan ruangan belum dibaca: %(status)s, jumlah: %(count)s" }, "save_setting_values": "Simpan pengaturan nilai", "see_history": "Lihat riwayat", @@ -855,6 +856,9 @@ }, "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", "import_invalid_keyfile": "Bukan keyfile %(brand)s yang absah", "import_invalid_passphrase": "Pemeriksaan autentikasi gagal: kata sandi salah?", @@ -1039,7 +1043,7 @@ "unknown_error_code": "kode kesalahan tidak diketahui", "update_power_level": "Gagal untuk mengubah tingkat daya" }, - "error_database_closed_title": "Basis data ditutup secara tidak terduga", + "error_database_closed_title": "%(brand)s berhenti bekerja", "error_dialog": { "copy_room_link_failed": { "description": "Tidak dapat menyalin sebuah tautan ruangan ke papan klip.", @@ -1112,6 +1116,7 @@ }, "fetching_events": "Mendapatkan peristiwa…", "file_attached": "File Dilampirkan", + "format": "Format", "from_the_beginning": "Dari awal", "generating_zip": "Membuat sebuah ZIP", "html": "HTML", @@ -1131,6 +1136,7 @@ "select_option": "Pilih dari opsi di bawah untuk mengekspor obrolan dari lini masa Anda", "size_limit": "Batas Ukuran", "size_limit_min_max": "Ukuran harus sebuah angka antara %(min)s MB dan %(max)s MB", + "size_limit_postfix": "MB", "starting_export": "Memulai pengeksporan…", "successful": "Ekspor Berhasil", "successful_detail": "Ekspor Anda berhasil. Temukan di folder Unduhan Anda.", @@ -1307,6 +1313,7 @@ "control": "Ctrl", "dismiss_read_marker_and_jump_bottom": "Abaikan penanda baca dan pergi ke bawah", "end": "End", + "enter": "Masuk", "escape": "Esc", "go_home_view": "Pergi ke Tampilan Beranda", "home": "Beranda", @@ -1323,6 +1330,8 @@ "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_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", @@ -1398,9 +1407,13 @@ "msc3531_hide_messages_pending_moderation": "Memperbolehkan moderator untuk menyembunyikan pesan yang akan dimoderasikan.", "new_room_decoration_ui": "Dalam pengembangan aktif, tajuk ruangan & antarmuka detail 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", - "oidc_native_flow": "Aktifkan alur OIDC native baru (Dalam pengembangan aktif)", + "notifications": "Aktifkan panel notifikasi di tajuk ruangan", + "oidc_native_flow": "Autentikasi asli OIDC", "pinning": "Pin Pesan", + "render_reaction_images": "Render gambar khusus dalam reaksi", + "render_reaction_images_description": "Terkadang disebut sebagai \"emoji khusus\".", "report_to_moderators": "Laporkan ke moderator", "report_to_moderators_description": "Dalam ruangan yang mendukung moderasi, tombol “Laporkan” memungkinkan Anda untuk melaporkan penyalahgunaan ke moderator ruangan.", "rust_crypto": "Implementasi kriptografi Rust", @@ -1416,6 +1429,7 @@ "sliding_sync_server_specify_proxy": "Server Anda belum mendukungnya, Anda harus menetapkan sebuah proksi", "sliding_sync_server_support": "Server Anda mendukungnya", "under_active_development": "Dalam pengembangan aktif.", + "unrealiable_e2e": "Tidak dapat diandalkan di ruangan terenkripsi", "video_rooms": "Ruangan video", "video_rooms_a_new_way_to_chat": "Sebuah cara baru untuk mengobrol melalui suara dan video di %(brand)s.", "video_rooms_always_on_voip_channels": "Ruangan video adalah saluran VoIP yang selalu ada tersemat di dalam sebuah ruangan di %(brand)s.", @@ -1424,6 +1438,7 @@ "video_rooms_faq1_question": "Bagaimana caranya saya membuat sebuah ruangan video?", "video_rooms_faq2_answer": "Ya, lini masa obrolan akan ditampilkan di sebelah videonya.", "video_rooms_faq2_question": "Bisakah saya mengobrol dengan teks saat ada panggilan video?", + "video_rooms_feedbackSubheading": "Terima kasih telah mencoba fitur beta, mohon berikan masukan sedetail mungkin supaya kami dapat menyempurnakannya.", "voice_broadcast": "Siaran suara", "voice_broadcast_force_small_chunks": "Paksakan panjang bagian siaran suara 15d", "wysiwyg_composer": "Editor teks kaya" @@ -1462,14 +1477,6 @@ "view_rules": "Tampilkan aturan" }, "language_dropdown_label": "Dropdown Bahasa", - "lazy_loading": { - "disabled_action": "Hapus cache dan sinkron ulang", - "disabled_description1": "Anda sebelumnya menggunakan %(brand)s di %(host)s dengan pemuatan malas pengguna diaktifkan. Di versi ini pemuatan malas dinonaktifkan. Karena cache lokal tidak kompatibel antara dua pengaturan ini, %(brand)s harus mengsinkronisasi ulang akun Anda.", - "disabled_description2": "Jika versi %(brand)s yang lain masih terbuka di tab yang lain, mohon menutupnya karena menggunakan %(brand)s di host yang sama dengan pemuatan malas diaktifkan dan dinonaktifkan secara bersamaan akan mengakibatkan masalah.", - "disabled_title": "Cache lokal tidak kompatibel", - "resync_description": "%(brand)s sekarang menggunakan memori 3-5x kecil dari sebelumnya dengan hanya memuat informasi tentang pengguna lain jika dibutuhkan. Mohon tunggu selagi kita mengsinkronisasi ulang dengan servernya!", - "resync_title": "Memperbarui %(brand)s" - }, "leave_room_dialog": { "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'?", @@ -1546,6 +1553,7 @@ "toast_description": "%(brand)s bersifat eksperimental pada peramban web ponsel. Untuk pengalaman yang lebih baik dan fitur-fitur terkini, gunakan aplikasi natif gratis kami.", "toast_title": "Gunakan aplikasi untuk pengalaman yang lebih baik" }, + "name_and_id": "%(name)s (%(userId)s)", "no_more_results": "Tidak ada hasil lagi", "notif_panel": { "empty_description": "Anda tidak memiliki notifikasi.", @@ -1554,6 +1562,7 @@ "notifications": { "all_messages": "Semua pesan", "all_messages_description": "Dapatkan notifikasi untuk setiap pesan", + "class_global": "Global", "class_other": "Lainnya", "default": "Bawaan", "email_pusher_app_display_name": "Notifikasi Surel", @@ -1876,6 +1885,9 @@ "close_call_button": "Tutup panggilan", "forget_room_button": "Lupakan ruangan", "hide_widgets_button": "Sembunyikan Widget", + "n_people_asking_to_join": { + "other": "%(count)s orang meminta untuk bergabung" + }, "room_is_public": "Ruangan ini publik", "show_widgets_button": "Tampilkan Widget", "video_call_button_ec": "Panggilan video (%(brand)s)", @@ -1885,6 +1897,7 @@ "video_call_ec_layout_spotlight": "Sorotan", "video_room_view_chat_button": "Tampilkan lini masa obrolan" }, + "header_untrusted_label": "Tidak dipercaya", "inaccessible": "Ruangan atau space ini tidak dapat diakses pada saat ini.", "inaccessible_name": "%(roomName)s tidak dapat diakses sekarang.", "inaccessible_subtitle_1": "Coba ulang nanti, atau tanya kepada admin ruangan atau space untuk memeriksa jika Anda memiliki akses.", @@ -1933,6 +1946,8 @@ "kicked_by": "Anda telah dikeluarkan oleh %(memberName)s", "kicked_from_room_by": "Anda telah dikeluarkan dari %(roomName)s oleh %(memberName)s", "knock_cancel_action": "Batalkan permintaan", + "knock_denied_subtitle": "Karena Anda telah ditolak aksesnya, Anda tidak dapat bergabung kembali kecuali Anda diundang oleh admin atau moderator grup.", + "knock_denied_title": "Anda telah ditolak aksesnya", "knock_message_field_placeholder": "Pesan (opsional)", "knock_prompt": "Tanyakan untuk bergabung?", "knock_prompt_name": "Tanyakan untuk bergabung ke %(roomName)s?", @@ -2254,6 +2269,7 @@ }, "join_rule_upgrade_upgrading_room": "Meningkatkan ruangan", "public_without_alias_warning": "Untuk menautkan ruangan ini, mohon tambahkan sebuah alamat.", + "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", "title": "Keamanan & Privasi" }, @@ -2346,9 +2362,6 @@ "custom_theme_success": "Tema ditambahkan!", "custom_theme_url": "URL tema kustom", "font_size": "Ukuran font", - "font_size_limit": "Ukuran font kustom hanya bisa antara %(min)s pt dan %(max)s pt", - "font_size_nan": "Ukuran harus sebuah angka", - "font_size_valid": "Gunakan antara %(min)s pt dan %(max)s pt", "heading": "Ubah tampilan Anda", "image_size_default": "Bawaan", "image_size_large": "Besar", @@ -2503,6 +2516,9 @@ "phrase_must_match": "Frasa sandi harus cocok", "phrase_strong_enough": "Hebat! Frasa keamanan ini kelihatannya kuat" }, + "keyboard": { + "title": "Papan tik" + }, "notifications": { "default_setting_description": "Pengaturan ini akan diterapkan secara bawaan ke semua ruangan Anda.", "default_setting_section": "Saya ingin diberi tahu (Pengaturan Bawaan)", @@ -2559,6 +2575,7 @@ "voip": "Panggilan Audio dan Video" }, "preferences": { + "Electron.enableHardwareAcceleration": "Aktifkan akselerasi perangkat keras (mulai ulang %(appName)s untuk menerapkan)", "always_show_menu_bar": "Selalu tampilkan bilah menu window", "autocomplete_delay": "Delay penyelesaian otomatis (md)", "code_blocks_heading": "Blok kode", @@ -2585,6 +2602,7 @@ "security": { "4s_public_key_in_account_data": "di data akun", "4s_public_key_status": "Kunci publik penyimpanan rahasia:", + "analytics_description": "Bagikan data anonim untuk membantu kami mengenal masalah. Tidak ada yang pribadi. Tanpa pihak ketiga.", "backup_key_cached_status": "Cadangan kunci dicache:", "backup_key_stored_status": "Cadangan kunci disimpan:", "backup_key_unexpected_type": "tipe yang tidak terduga", @@ -2620,14 +2638,17 @@ "ignore_users_section": "Pengguna yang diabaikan", "import_megolm_keys": "Impor kunci enkripsi ujung ke ujung", "key_backup_active": "Sesi ini mencadangkan kunci Anda.", + "key_backup_active_version": "Versi cadangan aktif:", "key_backup_active_version_none": "Tidak Ada", "key_backup_algorithm": "Algoritma:", + "key_backup_can_be_restored": "Cadangan ini dapat dipulihkan di sesi ini", "key_backup_complete": "Semua kunci telah dicadangkan", "key_backup_connect": "Hubungkan sesi ini ke Pencadangan Kunci", "key_backup_connect_prompt": "Hubungkan sesi ini ke pencadangan kunci sebelum keluar untuk menghindari kehilangan kunci apa saja yang mungkin hanya ada di sesi ini.", "key_backup_in_progress": "Mencadangkan %(sessionsRemaining)s kunci…", "key_backup_inactive": "Sesi ini tidak mencadangkan kunci Anda, tetapi Anda memiliki cadangan yang ada yang dapat Anda pulihkan dan tambahkan untuk selanjutnya.", "key_backup_inactive_warning": "Kunci Anda tidak dicadangan dari sesi ini.", + "key_backup_latest_version": "Versi cadangan terbaru di server:", "manually_verify_all_sessions": "Verifikasi semua sesi jarak jauh secara manual", "message_search_disable_warning": "Jika dinonaktifkan, pesan dari ruangan terenkripsi tidak akan muncul di hasil pencarian.", "message_search_disabled": "Simpan pesan terenkripsi secara lokal dengan aman agar muncul di hasil pencarian.", @@ -2647,7 +2668,7 @@ "message_search_space_used": "Ruangan terpakai:", "message_search_unsupported": "%(brand)s tidak memiliki beberapa komponen yang diperlukan untuk menyimpan pesan terenkripsi secara lokal dengan aman. Jika Anda ingin bereksperimen dengan fitur ini, buat %(brand)s Desktop yang khusus dengan tambahan komponen penelusuran.", "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 muda dalam pengelola sesi", + "record_session_details": "Rekam nama, versi, dan URL klien untuk dapat mengenal sesi dengan lebih mudah dalam pengelola sesi", "restore_key_backup": "Pulihkan dari Cadangan", "secret_storage_not_ready": "belum siap", "secret_storage_ready": "siap", @@ -2661,6 +2682,7 @@ "send_read_receipts_unsupported": "Server Anda tidak mendukung penonaktifkan pengiriman laporan dibaca.", "send_typing_notifications": "Kirim notifikasi pengetikan", "sessions": { + "best_security_note": "Untuk keamanan yang terbaik, verifikasi sesi Anda dan keluarkan sesi yang Anda tidak kenal atau tidak digunakan lagi.", "browser": "Peramban", "confirm_sign_out": { "one": "Konfirmasi mengeluarkan perangkat ini", @@ -2686,6 +2708,7 @@ "device_verified_description": "Sesi ini siap untuk perpesanan yang aman.", "device_verified_description_current": "Sesi Anda saat ini siap untuk perpesanan aman.", "error_pusher_state": "Gagal menetapkan keadaan pendorong", + "error_set_name": "Gagal mengatur nama sesi", "filter_all": "Semua", "filter_inactive": "Tidak aktif", "filter_inactive_description": "Tidak aktif selama %(inactiveAgeDays)s hari atau lebih", @@ -2746,6 +2769,7 @@ "unverified_sessions_explainer_1": "Sesi yang belum diverifikasi adalah sesi yang telah masuk dengan kredensial Anda tetapi belum diverifikasi secara silang.", "unverified_sessions_explainer_2": "Anda seharusnya yakin bahwa Anda mengenal sesi ini karena mereka dapat berarti bahwa seseorang telah menggunakan akun Anda tanpa diketahui.", "unverified_sessions_list_description": "Verifikasi sesi Anda untuk perpesanan aman yang baik atau keluarkan sesi yang Anda tidak kenal atau tidak digunakan lagi.", + "url": "URL", "verified_session": "Sesi terverifikasi", "verified_sessions": "Sesi terverifikasi", "verified_sessions_explainer_1": "Sesi terverifikasi bisa dari menggunakan akun ini setelah memasukkan frasa sandi atau mengonfirmasi identitas Anda dengan sesi terverifikasi lain.", @@ -2773,6 +2797,7 @@ "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", + "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", @@ -2835,7 +2860,9 @@ "devtools": "Membuka dialog Peralatan Pengembang", "discardsession": "Memaksa sesi grup keluar saat ini di ruang terenkripsi untuk dibuang", "error_invalid_rendering_type": "Kesalahan perintah: Tidak dapat menemukan tipe render (%(renderingType)s)", + "error_invalid_room": "Perintah gagal: Tidak dapat menemukan ruangan (%(roomId)s)", "error_invalid_runfn": "Kesalahan perintah: Tidak dapat menangani perintah slash.", + "error_invalid_user_in_room": "Tidak dapat menemukan pengguna dalam ruangan", "help": "Menampilkan daftar perintah dengan penggunaan dan deskripsi", "help_dialog_title": "Bantuan Perintah", "holdcall": "Menunda panggilan di ruangan saat ini", @@ -3109,6 +3136,7 @@ "creation_summary_dm": "%(creator)s membuat pesan langsung ini.", "creation_summary_room": "%(creator)s membuat dan mengatur ruangan ini.", "decryption_failure_blocked": "Pengirim telah memblokir Anda supaya tidak menerima pesan ini", + "disambiguated_profile": "%(displayName)s (%(matrixId)s)", "download_action_decrypting": "Mendekripsi", "download_action_downloading": "Mengunduh", "edits": { @@ -3354,6 +3382,7 @@ "pending_moderation_reason": "Pesan akan dimoderasikan: %(reason)s", "reactions": { "add_reaction_prompt": "Tambahkan reaksi", + "custom_reaction_fallback_label": "Reaksi khusus", "label": "%(reactors)s berekasi dengan %(content)s", "tooltip": "bereaksi dengan %(shortName)s" }, @@ -3543,6 +3572,7 @@ "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": { + "changelog": "Catatan perubahan", "check_action": "Periksa untuk pembaruan", "checking": "Memeriksa pembaruan…", "downloading": "Mengunduh pembaruan…", @@ -3911,6 +3941,7 @@ "l33t": "Pergantian yang dapat diprediksi seperti '@' daripada 'a' tidak terlalu membantu", "longerKeyboardPattern": "Gunakan pola keyboard yang lebih panjang dengan lebih banyak putaran", "noNeed": "Tidak perlu untuk simbol, digit, atau huruf besar", + "pwned": "Jika Anda menggunakan kata sandi ini di tempat lain, Anda harus mengubahnya.", "recentYears": "Hindari tahun terkini", "repeated": "HIndari kata dan karakter yang diulang", "reverseWords": "Kata yang dibalik tidak terlalu susah untuk ditebak", @@ -3924,6 +3955,7 @@ "extendedRepeat": "Kata yang berulang seperti \"abcabcabc\" masih sedikit susah untuk ditebak daripada \"abc\"", "keyPattern": "Pola keyboard yang pendek mudah ditebak", "namesByThemselves": "Nama depan dan nama belakang sendiri mudah ditebak", + "pwned": "Kata sandi Anda telah terekspos oleh pelanggaran data di Internet.", "recentYears": "Tahun terkini masih mudah untuk ditebak", "sequences": "Urutan seperti abc atau 6543 masih mudah untuk ditebak", "similarToCommon": "Ini mirip dengan kata sandi yang biasa digunakan", @@ -3931,6 +3963,7 @@ "straightRow": "Deretan tombol keyboard yang lurus mudah ditebak", "topHundred": "Ini adalah 100 kata sandi umum teratas", "topTen": "Ini adalah 10 kata sandi umum teratas", + "userInputs": "Seharusnya tidak ada data pribadi atau halaman yang terkait.", "wordByItself": "Sebuah kata dengan sendirinya mudah ditebak" } } diff --git a/src/i18n/strings/is.json b/src/i18n/strings/is.json index d3890087733..8c8122b30ba 100644 --- a/src/i18n/strings/is.json +++ b/src/i18n/strings/is.json @@ -219,7 +219,6 @@ "phone_optional_label": "Sími (valfrjálst)", "qr_code_login": { "error_invalid_scanned_code": "Skannaði kóðinn er ógildur.", - "review_and_approve": "Yfirfarðu og samþykktu innskráninguna", "sign_in_new_device": "Skrá inn nýtt tæki", "waiting_for_device": "Bíð eftir að tækið skráist inn" }, @@ -630,14 +629,12 @@ "methods": "Aðferðir", "no_verification_requests_found": "Engar staðfestingarbeiðnir fundust", "number_of_users": "Fjöldi notenda", - "observe_only": "Aðeins fylgjast með", "phase": "Fasi", "phase_cancelled": "Hætt við", "phase_ready": "Tilbúið", "phase_requested": "Umbeðið", "phase_started": "Hafið", "phase_transaction": "Færsluaðgerð", - "requester": "Beiðandi", "room_id": "Auðkenni spjallrásar: %(roomId)s", "save_setting_values": "Vista gildi valkosta", "send_custom_state_event": "Senda sérsniðinn stöðuatburð", @@ -1154,6 +1151,7 @@ "group_rooms": "Spjallrásir", "group_spaces": "Svæði", "group_themes": "Þemu", + "group_threads": "Spjallþræðir", "group_voip": "Tal og myndmerki", "group_widgets": "Viðmótshlutar", "html_topic": "Birta HTML-framsetningu umfjöllunarefnis spjallrása", @@ -1212,11 +1210,6 @@ "view_rules": "Skoða reglur" }, "language_dropdown_label": "Fellilisti tungumála", - "lazy_loading": { - "disabled_action": "Hreinsa skyndiminni og endursamstilla", - "disabled_title": "Ósamhæft staðvært skyndiminni", - "resync_title": "Uppfæri %(brand)s" - }, "leave_room_dialog": { "last_person_warning": "Þú ert eini eintaklingurinn hérna. Ef þú ferð út, mun enginn framar geta tekið þátt, að þér meðtöldum.", "leave_room_question": "Ertu viss um að þú viljir yfirgefa spjallrásina '%(roomName)s'?", @@ -1936,9 +1929,6 @@ "custom_theme_success": "Þema bætt við!", "custom_theme_url": "Slóð á sérsniðið þema", "font_size": "Leturstærð", - "font_size_limit": "Sérsniðin stærð á letri getur aðeins verið á milli %(min)s pt og %(max)s pt", - "font_size_nan": "Stærð verður að vera tala", - "font_size_valid": "Nota á milli %(min)s pt og %(max)s pt", "heading": "Sérsníddu útlitið þitt", "image_size_default": "Sjálfgefið", "image_size_large": "Stórt", diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 013f258b27c..46085e10504 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -1,6 +1,8 @@ { "a11y": { + "emoji_picker": "Selettore di emoji", "jump_first_invite": "Salta al primo invito.", + "message_composer": "Compositore di messaggi", "n_unread_messages": { "other": "%(count)s messaggi non letti.", "one": "1 messaggio non letto." @@ -9,7 +11,10 @@ "other": "%(count)s messaggi non letti incluse le citazioni.", "one": "1 citazione non letta." }, + "recent_rooms": "Stanze recenti", "room_name": "Stanza %(name)s", + "room_status_bar": "Barra di stato della stanza", + "seek_bar_label": "Barra di ricerca audio", "unread_messages": "Messaggi non letti.", "user_menu": "Menu utente" }, @@ -27,6 +32,7 @@ "cancel": "Annulla", "change": "Cambia", "clear": "Svuota", + "click": "Clicca", "click_to_copy": "Clicca per copiare", "close": "Chiudi", "collapse": "Riduci", @@ -162,6 +168,7 @@ "autodiscovery_invalid_is_base_url": "Base_url per m.identity_server non valido", "autodiscovery_invalid_is_response": "Risposta non valida cercando server di identità", "autodiscovery_invalid_json": "JSON non valido", + "autodiscovery_no_well_known": "Nessun file JSON .well-known trovato", "autodiscovery_unexpected_error_hs": "Errore inaspettato nella risoluzione della configurazione homeserver", "autodiscovery_unexpected_error_is": "Errore inaspettato risolvendo la configurazione del server identità", "captcha_description": "Questo homeserver vorrebbe assicurarsi che non sei un robot.", @@ -227,6 +234,8 @@ "no_hs_url_provided": "Nessun URL homeserver fornito", "oidc": { "error_title": "Non abbiamo potuto farti accedere", + "generic_auth_error": "Qualcosa è andato storto durante l'autenticazione. Vai alla pagina di accesso e riprova.", + "logout_redirect_warning": "Verrai reindirizzato al fornitore di autenticazione del tuo server per completare la disconnessione.", "missing_or_invalid_stored_state": "Abbiamo chiesto al browser di ricordare quale homeserver usi per farti accedere, ma sfortunatamente l'ha dimenticato. Vai alla pagina di accesso e riprova." }, "password_field_keep_going_prompt": "Continua…", @@ -240,22 +249,20 @@ "completing_setup": "Completamento configurazione nuovo dispositivo", "confirm_code_match": "Controlla che il codice sottostante corrisponda nell'altro dispositivo:", "connecting": "In connessione…", - "devices_connected": "Dispositivo connesso", "error_device_already_signed_in": "L'altro dispositivo ha già fatto l'accesso.", "error_device_not_signed_in": "L'altro dispositivo non ha fatto l'accesso.", "error_device_unsupported": "Il collegamento con questo dispositivo non è supportato.", "error_homeserver_lacks_support": "L'homeserver non supporta l'accesso in un altro dispositivo.", "error_invalid_scanned_code": "Il codice scansionato non è valido.", "error_linking_incomplete": "Il collegamento non è stato completato nel tempo previsto.", + "error_rate_limited": "Troppi tentativi in poco tempo. Attendi un po' prima di riprovare.", "error_request_cancelled": "La richiesta è stata annullata.", "error_request_declined": "La richiesta è stata negata sull'altro dispositivo.", "error_unexpected": "Si è verificato un errore imprevisto.", - "review_and_approve": "Verifica e approva l'accesso", "scan_code_instruction": "Scansiona il codice QR sottostante con il dispositivo che è disconnesso.", "scan_qr_code": "Scansiona codice QR", "select_qr_code": "Seleziona '%(scanQRCode)s'", "sign_in_new_device": "Accedi nel nuovo dispositivo", - "start_at_sign_in_screen": "Inizia nella schermata di accesso", "waiting_for_device": "In attesa che il dispositivo acceda" }, "register_action": "Crea account", @@ -331,6 +338,7 @@ "soft_logout_intro_unsupported_auth": "Non puoi accedere al tuo account. Contatta l'admin del tuo homeserver per maggiori informazioni.", "soft_logout_subheading": "Elimina dati personali", "soft_logout_warning": "Attenzione: i tuoi dati personali (incluse le chiavi di crittografia) sono ancora memorizzati in questa sessione. Cancellali se hai finito di usare questa sessione o se vuoi accedere ad un altro account.", + "sso": "Single sign-on", "sso_failed_missing_storage": "Abbiamo chiesto al browser di ricordare quale homeserver usi per farti accedere, ma sfortunatamente l'ha dimenticato. Vai alla pagina di accesso e riprova.", "sso_or_username_password": "%(ssoButtons)s o %(usernamePassword)s", "sync_footer_subtitle": "Se sei dentro a molte stanze, potrebbe impiegarci un po'", @@ -428,6 +436,7 @@ "are_you_sure": "Sei sicuro?", "attachment": "Allegato", "authentication": "Autenticazione", + "avatar": "Avatar", "beta": "Beta", "camera": "Videocamera", "cameras": "Fotocamere", @@ -448,6 +457,7 @@ "error": "Errore", "faq": "Domande frequenti", "favourites": "Preferiti", + "feedback": "Commenti", "filter_results": "Filtra risultati", "forward_message": "Inoltra messaggio", "general": "Generale", @@ -466,6 +476,7 @@ "legal": "Informazioni legali", "light": "Chiaro", "loading": "Caricamento…", + "lobby": "Sala d’attesa", "location": "Posizione", "low_priority": "Bassa priorità", "matrix": "Matrix", @@ -728,7 +739,6 @@ "notification_state": "Lo stato di notifica è %(notificationState)s", "notifications_debug": "Debug notifiche", "number_of_users": "Numero di utenti", - "observe_only": "Osserva solo", "original_event_source": "Sorgente dell'evento originale", "phase": "Fase", "phase_cancelled": "Annullato", @@ -736,7 +746,6 @@ "phase_requested": "Richiesto", "phase_started": "Iniziato", "phase_transaction": "Transazione", - "requester": "Richiedente", "room_encrypted": "La stanza è crittografata ✅", "room_id": "ID stanza: %(roomId)s", "room_not_encrypted": "La stanza non è crittografata 🚨", @@ -749,6 +758,7 @@ "room_notifications_type": "Tipo: ", "room_status": "Stato della stanza", "room_unread_status_count": { + "one": "Stato \"non letto\" nella stanza: %(status)s, conteggio: %(count)s", "other": "Stato \"non letto\" nella stanza: %(status)s, conteggio: %(count)s" }, "save_setting_values": "Salva valori impostazione", @@ -1044,7 +1054,14 @@ "unknown_error_code": "codice errore sconosciuto", "update_power_level": "Cambio di livello poteri fallito" }, - "error_database_closed_title": "Database chiuso inaspettatamente", + "error_app_open_in_another_tab": "Passa all'altra scheda per connetterti a %(brand)s. Questa scheda può ora essere chiusa.", + "error_app_open_in_another_tab_title": "%(brand)s è connesso in un'altra scheda", + "error_app_opened_in_another_window": "%(brand)s è aperto in un'altra finestra. Clicca \"%(label)s\" per usare %(brand)s qui e scollegare l'altra finestra.", + "error_database_closed_description": { + "for_desktop": "Il disco potrebbe essere pieno. Libera spazio e ricarica.", + "for_web": "Se hai cancellato i dati di navigazione, questo messaggio è normale. %(brand)s potrebbe anche essere aperto in un'altra scheda o il disco è pieno. Libera spazio e ricarica" + }, + "error_database_closed_title": "%(brand)s ha smesso di funzionare", "error_dialog": { "copy_room_link_failed": { "description": "Impossibile copiare un collegamento alla stanza negli appunti.", @@ -1244,6 +1261,8 @@ "error_permissions_space": "Non hai l'autorizzazione di invitare persone in questo spazio.", "error_profile_undisclosed": "L'utente forse non esiste", "error_transfer_multiple_target": "Una chiamata può essere trasferita solo ad un singolo utente.", + "error_unfederated_room": "Questa stanza non è federata. Non puoi invitare persone da server esterni.", + "error_unfederated_space": "Questo spazio non è federato. Non puoi invitare persone da server esterni.", "error_unknown": "Errore sconosciuto del server", "error_user_not_found": "L'utente non esiste", "error_version_unsupported_room": "L'homeserver dell'utente non supporta la versione della stanza.", @@ -1380,6 +1399,7 @@ "element_call_video_rooms": "Stanze video di Element Call", "experimental_description": "Ti senti di sperimentare? Prova le nostre ultime idee in sviluppo. Queste funzioni non sono complete; potrebbero essere instabili, cambiare o essere scartate. Maggiori informazioni.", "experimental_section": "Anteprime", + "feature_disable_call_per_sender_encryption": "Disattiva la crittografia per mittente in Element Call", "feature_wysiwyg_composer_description": "Usa il rich text invece del Markdown nel compositore di messaggi.", "group_calls": "Nuova esperienza per chiamate di gruppo", "group_developer": "Sviluppatore", @@ -1391,6 +1411,7 @@ "group_rooms": "Stanze", "group_spaces": "Spazi", "group_themes": "Temi", + "group_threads": "Conversazioni", "group_voip": "Voce e video", "group_widgets": "Widget", "hidebold": "Nascondi il punto di notifica (mostra solo i contatori)", @@ -1408,12 +1429,21 @@ "msc3531_hide_messages_pending_moderation": "Lascia che i moderatori nascondano i messaggi in attesa di moderazione.", "new_room_decoration_ui": "In sviluppo attivo, nuova interfaccia per intestazione e dettagli della stanza", "notification_settings": "Nuove impostazioni di notifica", + "notification_settings_beta_caption": "Ti presentiamo un modo più semplice per modificare le impostazioni delle notifiche. Personalizza il tuo %(brand)s, proprio come piace a te.", "notification_settings_beta_title": "Impostazioni di notifica", - "oidc_native_flow": "Attiva i nuovi flussi OIDC nativi (in sviluppo attivo)", + "notifications": "Attiva il pannello delle notifiche nell'intestazione della stanza", + "oidc_native_flow": "Autenticazione nativa OIDC", + "oidc_native_flow_description": "⚠ ATTENZIONE: sperimentale. Usa l'autenticazione nativa OIDC se supportata dal server.", "pinning": "Ancoraggio messaggi", + "render_reaction_images": "Mostra immagini personalizzate nelle reazioni", + "render_reaction_images_description": "A volte chiamati \"emoji personalizzati\".", "report_to_moderators": "Segnala ai moderatori", "report_to_moderators_description": "Nelle stanze che supportano la moderazione, il pulsante \"Segnala\" ti permetterà di segnalare abusi ai moderatori della stanza.", "rust_crypto": "Implementazione crittografia Rust", + "rust_crypto_in_config": "La crittografia Rust non può essere disattivata in questa distribuzione di %(brand)s", + "rust_crypto_in_config_description": "Il passaggio alla crittografia Rust richiede un processo di migrazione che può impiegare diversi minuti. Non può essere disattivata, usala con cautela!", + "rust_crypto_optin_warning": "Il passaggio alla crittografia Rust richiede un processo di migrazione che può impiegare diversi minuti. Per disattivarla dovrai disconnetterti e poi riaccedere, usala con cautela!", + "rust_crypto_requires_logout": "Una volta attivata, la crittografia Rust può essere disattivata solo disconnettendoti e riaccedendo.", "sliding_sync": "Modalità di sincr. con slide", "sliding_sync_checking": "Controllo…", "sliding_sync_configuration": "Configurazione sincr. Sliding", @@ -1425,7 +1455,10 @@ "sliding_sync_server_no_support": "Il tuo server non ha il supporto nativo", "sliding_sync_server_specify_proxy": "Il tuo server non ha il supporto nativo, devi specificare un proxy", "sliding_sync_server_support": "Il tuo server ha il supporto nativo", + "threads_activity_centre": "Centro attività in discussioni (in sviluppo).", + "threads_activity_centre_description": "Attenzione: in fase di sviluppo attivo; ricarica %(brand)s.", "under_active_development": "In sviluppo attivo.", + "unrealiable_e2e": "Inaffidabile nelle stanze cifrate", "video_rooms": "Stanze video", "video_rooms_a_new_way_to_chat": "Un nuovo modo di fare chiamate audio e video in %(brand)s.", "video_rooms_always_on_voip_channels": "Le stanze video sono canali VoIP sempre attivi integrati all'interno della stanza in %(brand)s.", @@ -1473,14 +1506,6 @@ "view_rules": "Vedi regole" }, "language_dropdown_label": "Lingua a tendina", - "lazy_loading": { - "disabled_action": "Svuota cache e risincronizza", - "disabled_description1": "Hai usato %(brand)s precedentemente su %(host)s con il caricamento lento dei membri attivato. In questa versione il caricamento lento è disattivato. Dato che la cache locale non è compatibile tra queste due impostazioni, %(brand)s deve risincronizzare il tuo account.", - "disabled_description2": "Se l'altra versione di %(brand)s è ancora aperta in un'altra scheda, chiudila perché usare %(brand)s nello stesso host con il caricamento lento sia attivato che disattivato può causare errori.", - "disabled_title": "Cache locale non compatibile", - "resync_description": "%(brand)s ora usa da 3 a 5 volte meno memoria, caricando le informazioni degli altri utenti solo quando serve. Si prega di attendere mentre ci risincronizziamo con il server!", - "resync_title": "Aggiornamento di %(brand)s" - }, "leave_room_dialog": { "last_person_warning": "Sei l'unica persona qui. Se esci, nessuno potrà entrare in futuro, incluso te.", "leave_room_question": "Sei sicuro di volere uscire dalla stanza '%(roomName)s'?", @@ -1552,6 +1577,7 @@ }, "member_list_back_action_label": "Membri stanza", "message_edit_dialog_title": "Modifiche del messaggio", + "migrating_crypto": "Tieni duro. Stiamo aggiornando %(brand)s per rendere la crittografia più veloce e affidabile.", "mobile_guide": { "toast_accept": "Usa l'app", "toast_description": "%(brand)s è sperimentale su un browser web mobile. Per un'esperienza migliore e le ultime funzionalità, usa la nostra app nativa gratuita.", @@ -1576,6 +1602,12 @@ "error_change_title": "Cambia impostazioni di notifica", "keyword": "Parola chiave", "keyword_new": "Nuova parola chiave", + "level_activity": "Attività", + "level_highlight": "Evidenzia", + "level_muted": "Silenziato", + "level_none": "Nessuno", + "level_notification": "Notifica", + "level_unsent": "Non inviato", "mark_all_read": "Segna tutto come letto", "mentions_and_keywords": "@citazioni e parole chiave", "mentions_and_keywords_description": "Ricevi notifiche solo per citazioni e parole chiave come configurato nelle tue impostazioni", @@ -1708,7 +1740,8 @@ "online": "Online", "online_for": "Online per %(duration)s", "unknown": "Sconosciuto", - "unknown_for": "Sconosciuto per %(duration)s" + "unknown_for": "Sconosciuto per %(duration)s", + "unreachable": "Server dell'utente irraggiungibile" }, "quick_settings": { "all_settings": "Tutte le impostazioni", @@ -1736,6 +1769,7 @@ "report_content": { "description": "La segnalazione di questo messaggio invierà il suo 'ID evento' univoco all'amministratore del tuo homeserver. Se i messaggi della stanza sono cifrati, l'amministratore non potrà leggere il messaggio o vedere file e immagini.", "disagree": "Rifiuta", + "error_create_room_moderation_bot": "Impossibile creare una stanza con il bot di moderazione", "hide_messages_from_user": "Seleziona se vuoi nascondere tutti i messaggi attuali e futuri di questo utente.", "ignore_user": "Ignora utente", "illegal_content": "Contenuto illegale", @@ -1743,6 +1777,8 @@ "nature": "Scegli la natura del problema e descrivi cosa rende questo messaggio un abuso.", "nature_disagreement": "Questo utente sta scrivendo cose sbagliate.\nVerrà segnalato ai moderatori della stanza.", "nature_illegal": "Questo utente sta mostrando un comportamento illegale, ad esempio facendo doxing o minacciando violenza.\nVerrà segnalato ai moderatori della stanza che potrebbero portarlo in ambito legale.", + "nature_nonstandard_admin": "Questa stanza è dedicata a contenuti illegali o dannosi, oppure i moderatori non riescono a fermarli.\nVerrà segnalato agli amministratori di %(homeserver)s.", + "nature_nonstandard_admin_encrypted": "Questa stanza è dedicata a contenuti illegali o dannosi, oppure i moderatori non riescono a fermarli.\nVerrà segnalato agli amministratori di %(homeserver)s. Gli amministratori NON potranno leggere il contenuto criptato di questa stanza.", "nature_other": "Altri motivi. Si prega di descrivere il problema.\nVerrà segnalato ai moderatori della stanza.", "nature_spam": "Questo utente sta facendo spam nella stanza con pubblicità, collegamenti ad annunci o a propagande.\nVerrà segnalato ai moderatori della stanza.", "nature_toxic": "Questo utente sta mostrando un cattivo comportamento, ad esempio insultando altri utenti o condividendo contenuti per adulti in una stanza per tutti, oppure violando le regole della stessa.\nVerrà segnalato ai moderatori della stanza.", @@ -1889,15 +1925,22 @@ "close_call_button": "Chiudi chiamata", "forget_room_button": "Dimentica la stanza", "hide_widgets_button": "Nascondi i widget", + "n_people_asking_to_join": { + "one": "Richiesta di entrare", + "other": "%(count)s persone chiedono di entrare" + }, "room_is_public": "Questa stanza è pubblica", "show_widgets_button": "Mostra i widget", "video_call_button_ec": "Videochiamata (%(brand)s)", "video_call_button_jitsi": "Videochiamata (Jitsi)", + "video_call_button_legacy": "Videochiamata legacy", "video_call_ec_change_layout": "Cambia disposizione", "video_call_ec_layout_freedom": "Libertà", "video_call_ec_layout_spotlight": "Riflettore", "video_room_view_chat_button": "Vedi linea temporale chat" }, + "header_face_pile_tooltip": "Cambia l'elenco dei membri", + "header_untrusted_label": "Non fidato", "inaccessible": "Questa stanza o spazio non è al momento accessibile.", "inaccessible_name": "%(roomName)s non è al momento accessibile.", "inaccessible_subtitle_1": "Riprova più tardi, o chiedi ad un admin della stanza o spazio di controllare se hai l'accesso.", @@ -1946,6 +1989,8 @@ "kicked_by": "Sei stato rimosso da %(memberName)s", "kicked_from_room_by": "Sei stato rimosso da %(roomName)s da %(memberName)s", "knock_cancel_action": "Annulla richiesta", + "knock_denied_subtitle": "Poiché ti è stato negato l'accesso, non puoi rientrare a meno che tu non venga invitato dall'amministratore o dal moderatore del gruppo.", + "knock_denied_title": "Ti è stato negato l'accesso", "knock_message_field_placeholder": "Messaggio (facoltativo)", "knock_prompt": "Chiedi di entrare?", "knock_prompt_name": "Chiedi di entrare in %(roomName)s?", @@ -2340,6 +2385,7 @@ "access_token_detail": "Il tuo token di accesso ti dà l'accesso al tuo account. Non condividerlo con nessuno.", "brand_version": "versione %(brand)s:", "clear_cache_reload": "Svuota la cache e ricarica", + "crypto_version": "Versione crittografica:", "help_link": "Per aiuto su come usare %(brand)s, clicca qui.", "homeserver": "L'homeserver è %(homeserverUrl)s", "identity_server": "Il server d'identità è %(identityServerUrl)s", @@ -2352,6 +2398,7 @@ "all_rooms_home_description": "Tutte le stanze in cui sei appariranno nella pagina principale.", "always_show_message_timestamps": "Mostra sempre l'orario dei messaggi", "appearance": { + "bundled_emoji_font": "Usa i font emoji integrati", "custom_font": "Usa un carattere di sistema", "custom_font_description": "Imposta il nome di un font installato nel tuo sistema e %(brand)s proverà ad usarlo.", "custom_font_name": "Nome carattere di sistema", @@ -2362,9 +2409,7 @@ "custom_theme_success": "Tema aggiunto!", "custom_theme_url": "URL tema personalizzato", "font_size": "Dimensione carattere", - "font_size_limit": "La dimensione del carattere personalizzata può solo essere tra %(min)s pt e %(max)s pt", - "font_size_nan": "La dimensione deve essere un numero", - "font_size_valid": "Usa tra %(min)s pt e %(max)s pt", + "font_size_default": "%(fontSize)s (predefinito)", "heading": "Personalizza l'aspetto", "image_size_default": "Predefinito", "image_size_large": "Grande", @@ -2685,6 +2730,7 @@ "send_read_receipts_unsupported": "Il tuo server non supporta la disattivazione delle conferme di lettura.", "send_typing_notifications": "Invia notifiche di scrittura", "sessions": { + "best_security_note": "Per una maggiore sicurezza, verifica le tue sessioni ed esci da quelle che non riconosci o non usi più.", "browser": "Browser", "confirm_sign_out": { "one": "Conferma la disconnessione da questo dispositivo", @@ -2799,6 +2845,7 @@ "metaspaces_orphans_description": "Raggruppa tutte le tue stanze che non fanno parte di uno spazio in un unico posto.", "metaspaces_people_description": "Raggruppa tutte le tue persone in un unico posto.", "metaspaces_subsection": "Spazi da mostrare", + "spaces_explainer": "Gli spazi sono modi per raggruppare stanze e persone. Oltre agli spazi in cui ti trovi, puoi usarne anche altri già integrati.", "title": "Barra laterale" }, "start_automatically": "Esegui automaticamente all'avvio del sistema", @@ -2861,7 +2908,9 @@ "devtools": "Apre la finestra di strumenti per sviluppatori", "discardsession": "Forza l'eliminazione dell'attuale sessione di gruppo in uscita in una stanza criptata", "error_invalid_rendering_type": "Errore comando: impossibile trovare il tipo di rendering (%(renderingType)s)", + "error_invalid_room": "Comando fallito: impossibile trovare la stanza (%(roomId)s)", "error_invalid_runfn": "Errore comando: impossibile gestire il comando slash.", + "error_invalid_user_in_room": "Impossibile trovare l'utente nella stanza", "help": "Visualizza l'elenco dei comandi con usi e descrizioni", "help_dialog_title": "Aiuto comando", "holdcall": "Mette in pausa la chiamata nella stanza attuale", @@ -3030,6 +3079,7 @@ }, "create_new_room_button": "Crea una nuova stanza", "failed_querying_public_rooms": "Richiesta di stanze pubbliche fallita", + "failed_querying_public_spaces": "Interrogazione degli spazi pubblici fallita", "group_chat_section_title": "Altre opzioni", "heading_with_query": "Usa \"%(query)s\" per cercare", "heading_without_query": "Cerca", @@ -3094,6 +3144,10 @@ "show_thread_filter": "Mostra:", "unable_to_decrypt": "Impossibile decifrare il messaggio" }, + "threads_activity_centre": { + "header": "Attività delle conversazioni", + "no_rooms_with_unreads_threads": "Non hai ancora stanze con conversazioni non lette." + }, "time": { "about_day_ago": "circa un giorno fa", "about_hour_ago": "circa un'ora fa", @@ -3154,6 +3208,7 @@ "you": "Hai terminato una trasmissione vocale" }, "io.element.widgets.layout": "%(senderName)s ha aggiornato la disposizione della stanza", + "late_event_separator": "Inviato originariamente il %(dateTime)s", "load_error": { "no_permission": "Si è tentato di caricare un punto specifico nella cronologia della stanza, ma non hai l'autorizzazione per vedere il messaggio in questione.", "title": "Caricamento posizione cronologica fallito", @@ -3356,6 +3411,8 @@ "label": "Azioni messaggio", "view_in_room": "Vedi nella stanza" }, + "message_timestamp_received_at": "Ricevuto il: %(dateTime)s", + "message_timestamp_sent_at": "Inviato il: %(dateTime)s", "mjolnir": { "changed_rule_glob": "%(senderName)s ha modificato una regola di ban che corrispondeva a %(oldGlob)s per corrispondere a %(newGlob)s perchè %(reason)s", "changed_rule_rooms": "%(senderName)s ha modificato una regola che bandiva stanze corrispondenti a %(oldGlob)s per corrispondere a %(newGlob)s perchè %(reason)s", @@ -3738,6 +3795,7 @@ "camera_enabled": "La tua fotocamera è ancora attiva", "cannot_call_yourself_description": "Non puoi chiamare te stesso.", "change_input_device": "Cambia dispositivo di input", + "close_lobby": "Chiudi sala d'attesa", "connecting": "In connessione", "connection_lost": "La connessione al server è stata persa", "connection_lost_description": "Non puoi fare chiamate senza una connessione al server.", @@ -3751,6 +3809,7 @@ "disabled_no_perms_start_video_call": "Non hai il permesso di avviare videochiamate", "disabled_no_perms_start_voice_call": "Non hai il permesso di avviare chiamate", "disabled_ongoing_call": "Chiamata in corso", + "element_call": "Element Call", "enable_camera": "Accendi la fotocamera", "enable_microphone": "Riaccendi il microfono", "expand": "Torna alla chiamata", @@ -3759,9 +3818,13 @@ "hangup": "Riaggancia", "hide_sidebar_button": "Nascondi barra laterale", "input_devices": "Dispositivi di input", + "jitsi_call": "Conferenza Jitsi", "join_button_tooltip_call_full": "Spiacenti — questa chiamata è piena", "join_button_tooltip_connecting": "In connessione", + "legacy_call": "Chiamata legacy", "maximise": "Riempi schermo", + "maximise_call": "Massimizza la chiamata", + "minimise_call": "Minimizza la chiamata", "misconfigured_server": "Chiamata non riuscita a causa di un server non configurato correttamente", "misconfigured_server_description": "Chiedi all'amministratore del tuo homeserver(%(homeserverDomain)s) per configurare un server TURN affinché le chiamate funzionino in modo affidabile.", "misconfigured_server_fallback": "In alternativa puoi provare ad usare il server pubblico , ma non è molto affidabile e il tuo indirizzo IP verrà condiviso con tale server. Puoi gestire questa cosa nelle impostazioni.", @@ -3809,6 +3872,7 @@ "user_is_presenting": "%(sharerName)s sta presentando", "video_call": "Videochiamata", "video_call_started": "Videochiamata iniziata", + "video_call_using": "Videochiamata usando:", "voice_call": "Telefonata", "you_are_presenting": "Stai presentando" }, diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json index 839460be321..22a17aabdef 100644 --- a/src/i18n/strings/ja.json +++ b/src/i18n/strings/ja.json @@ -233,7 +233,6 @@ "completing_setup": "新しい端末の設定を完了しています", "confirm_code_match": "以下のコードが他の端末と一致していることを確認してください:", "connecting": "接続しています…", - "devices_connected": "接続中の端末", "error_device_already_signed_in": "もう一方のデバイスは既にサインインしています。", "error_device_not_signed_in": "もう一方の端末はサインインしていません。", "error_device_unsupported": "この端末とのリンクはサポートしていません。", @@ -244,12 +243,10 @@ "error_request_cancelled": "リクエストはキャンセルされました。", "error_request_declined": "リクエストはもう一方の端末で拒否されました。", "error_unexpected": "予期しないエラーが発生しました。", - "review_and_approve": "サインインを確認して承認", "scan_code_instruction": "サインアウトした端末で以下のQRコードをスキャンしてください。", "scan_qr_code": "QRコードをスキャン", "select_qr_code": "「%(scanQRCode)s」を選択", "sign_in_new_device": "新しい端末でサインイン", - "start_at_sign_in_screen": "サインインの画面で開始", "waiting_for_device": "端末のサインインを待機しています" }, "register_action": "アカウントを作成", @@ -706,7 +703,6 @@ "no_verification_requests_found": "認証リクエストがありません", "notifications_debug": "通知のデバッグ", "number_of_users": "ユーザー数", - "observe_only": "観察のみ", "original_event_source": "元のイベントのソースコード", "phase": "フェーズ", "phase_cancelled": "キャンセル済", @@ -714,7 +710,6 @@ "phase_requested": "要求済", "phase_started": "開始済", "phase_transaction": "トランザクション", - "requester": "リクエストしたユーザー", "room_id": "ルームID:%(roomId)s", "room_notifications_dot": "ドット: ", "room_notifications_highlight": "ハイライト: ", @@ -1323,6 +1318,7 @@ "group_rooms": "ルーム", "group_spaces": "スペース", "group_themes": "テーマ", + "group_threads": "スレッド", "group_voip": "音声とビデオ", "group_widgets": "ウィジェット", "hidebold": "通知のドットを非表示にする(カウンターのバッジのみを表示)", @@ -1398,14 +1394,6 @@ "view_rules": "ルールを表示" }, "language_dropdown_label": "言語一覧", - "lazy_loading": { - "disabled_action": "キャッシュをクリアして再同期", - "disabled_description1": "以前%(host)sにて、メンバーの遅延ロードを有効にした%(brand)sが使用されていました。このバージョンでは、遅延ロードは無効です。ローカルのキャッシュにはこれらの2つの設定の間での互換性がないため、%(brand)sはアカウントを再同期する必要があります。", - "disabled_description2": "他のバージョンの%(brand)sが別のタブで開いている場合は、それを閉じてください。同じホスト上で遅延ロードを有効と無効の両方に設定して%(brand)sを使用すると、問題が発生します。", - "disabled_title": "互換性のないローカルキャッシュ", - "resync_description": "%(brand)sは、必要なときだけに他のユーザーに関する情報を読み込むようにすることで、メモリの使用量を3〜5倍減らしました。サーバーと再同期するのを待ってください!", - "resync_title": "%(brand)sを更新しています" - }, "leave_room_dialog": { "last_person_warning": "このルームの参加者はあなただけです。退出すると、今後あなたを含めて誰もこのルームに参加できなくなります。", "leave_room_question": "このルーム「%(roomName)s」から退出してよろしいですか?", @@ -2213,9 +2201,6 @@ "custom_theme_success": "テーマが追加されました!", "custom_theme_url": "ユーザー定義のテーマのURL", "font_size": "フォントの大きさ", - "font_size_limit": "ユーザー定義のフォントの大きさは%(min)s~%(max)s(単位:point)の間で指定できます", - "font_size_nan": "サイズには数値を指定してください", - "font_size_valid": "%(min)s~%(max)s(pt)の間の数字を指定", "heading": "外観のカスタマイズ", "image_size_default": "既定値", "image_size_large": "大", diff --git a/src/i18n/strings/lo.json b/src/i18n/strings/lo.json index 03427ca6ca1..72f90e0f0a8 100644 --- a/src/i18n/strings/lo.json +++ b/src/i18n/strings/lo.json @@ -616,7 +616,6 @@ "methods": "ວິທີການ", "no_verification_requests_found": "ບໍ່ພົບການຮ້ອງຂໍການຢັ້ງຢືນ", "number_of_users": "ຈໍານວນຜູ້ໃຊ້", - "observe_only": "ສັງເກດເທົ່ານັ້ນ", "original_event_source": "ແຫຼ່ງຕົ້ນສະບັບ", "phase": "ໄລຍະ", "phase_cancelled": "ຍົກເລີກ", @@ -624,7 +623,6 @@ "phase_requested": "ຮ້ອງຂໍ", "phase_started": "ໄດ້ເລີ່ມແລ້ວ", "phase_transaction": "ທຸລະກໍາ", - "requester": "ຜູ້ຮ້ອງຂໍ", "room_id": "ID ຫ້ອງ: %(roomId)s", "save_setting_values": "ບັນທຶກຄ່າການຕັ້ງຄ່າ", "send_custom_account_data_event": "ສົ່ງຂໍ້ມູນບັນຊີແບບກຳນົດເອງທຸກເຫດການ", @@ -1184,6 +1182,7 @@ "group_rooms": "ຫ້ອງ", "group_spaces": "ພື້ນທີ່", "group_themes": "ຫົວຂໍ້", + "group_threads": "ກະທູ້", "group_voip": "ສຽງ & ວິດີໂອ", "group_widgets": "ວິດເຈັດ", "join_beta": "ເຂົ້າຮ່ວມເບຕ້າ", @@ -1226,14 +1225,6 @@ "view_rules": "ເບິ່ງກົດລະບຽບ" }, "language_dropdown_label": "ເລື່ອນພາສາລົງ", - "lazy_loading": { - "disabled_action": "ລຶບ cache ແລະ resync", - "disabled_description1": "ກ່ອນໜ້ານີ້ທ່ານເຄີຍໃຊ້ %(brand)sກັບ %(host)s ດ້ວຍການເປີດໂຫຼດສະມາຊິກ. ໃນເວີຊັ້ນນີ້ ໄດ້ປິດການໃຊ້ງານ. ເນື່ອງຈາກ cache ໃນເຄື່ອງບໍ່ເຂົ້າກັນລະຫວ່າງສອງການຕັ້ງຄ່ານີ້, %(brand)s ຕ້ອງການ sync ບັນຊີຂອງທ່ານຄືນໃໝ່.", - "disabled_description2": "ຖ້າເວີຊັ້ນອື່ນຂອງ %(brand)s ເປີດຢູ່ໃນແຖບອື່ນ, ກະລຸນາປິດການໃຊ້ %(brand)s ຢູ່ໃນໂຮດດຽວກັນທັງການໂຫຼດແບບ lazy ເປີດໃຊ້ງານ ແລະປິດໃຊ້ງານພ້ອມກັນຈະເຮັດໃຫ້ເກີດບັນຫາ.", - "disabled_title": "ແຄດໃນເຄື່ອງບໍ່ເຂົ້າກັນໄດ້", - "resync_description": "ຕອນນີ້ %(brand)s ໃຊ້ຄວາມຈຳໜ້ອຍກວ່າ 3-5x, ໂດຍການໂຫຼດຂໍ້ມູນກ່ຽວກັບຜູ້ໃຊ້ອື່ນເມື່ອຕ້ອງການເທົ່ານັ້ນ. ກະລຸນາລໍຖ້າໃນຂະນະທີ່ພວກເຮົາ synchronise ກັບເຊີບເວີ!", - "resync_title": "ກຳລັງອັບເດດ %(brand)s" - }, "leave_room_dialog": { "last_person_warning": "ເຈົ້າເປັນພຽງຄົນດຽວຢູ່ທີ່ນີ້. ຖ້າທ່ານອອກໄປ, ບໍ່ມີໃຜຈະສາມາດເຂົ້າຮ່ວມໃນອະນາຄົດໄດ້, ລວມທັງທ່ານ.", "leave_room_question": "ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການອອກຈາກຫ້ອງ '%(roomName)s'?", @@ -1949,9 +1940,6 @@ "custom_theme_success": "ເພີ່ມຫົວຂໍ້!", "custom_theme_url": "ການ ກຳນົດເອງຫົວຂໍ້ URL", "font_size": "ຂະໜາດຕົວອັກສອນ", - "font_size_limit": "ຂະໜາດຕົວອັກສອນທີ່ກຳນົດເອງສາມາດຢູ່ໃນລະຫວ່າງ %(min)s pt ແລະ %(max)s pt", - "font_size_nan": "ຂະໜາດຕ້ອງເປັນຕົວເລກ", - "font_size_valid": "ໃຊ້ລະຫວ່າງ %(min)s pt ແລະ %(max)s pt", "heading": "ປັບແຕ່ງຮູບລັກສະນະຂອງທ່ານ", "image_size_default": "ຄ່າເລີ່ມຕົ້ນ", "image_size_large": "ຂະຫນາດໃຫຍ່", diff --git a/src/i18n/strings/lt.json b/src/i18n/strings/lt.json index 1ebe6fe21a5..66deb29fe1e 100644 --- a/src/i18n/strings/lt.json +++ b/src/i18n/strings/lt.json @@ -928,14 +928,6 @@ "title": "Ignoruojami vartotojai", "view_rules": "Peržiūrėti taisykles" }, - "lazy_loading": { - "disabled_action": "Išvalyti talpyklą ir sinchronizuoti iš naujo", - "disabled_description1": "Jūs anksčiau naudojote %(brand)s ant %(host)s įjungę tingų narių įkėlimą. Šioje versijoje tingus įkėlimas yra išjungtas. Kadangi vietinė talpykla nesuderinama tarp šių dviejų nustatymų, %(brand)s reikia iš naujo sinchronizuoti jūsų paskyrą.", - "disabled_description2": "Jei kita %(brand)s versija vis dar yra atidaryta kitame skirtuke, uždarykite jį, nes %(brand)s naudojimas tame pačiame serveryje, tuo pačiu metu įjungus ir išjungus tingų įkėlimą, sukelks problemų.", - "disabled_title": "Nesuderinamas vietinis podėlis", - "resync_description": "%(brand)s dabar naudoja 3-5 kartus mažiau atminties, įkeliant vartotojų informaciją tik prireikus. Palaukite, kol mes iš naujo sinchronizuosime su serveriu!", - "resync_title": "Atnaujinama %(brand)s" - }, "leave_room_dialog": { "leave_room_question": "Ar tikrai norite išeiti iš kambario %(roomName)s?", "room_rejoin_warning": "Šis kambarys nėra viešas. Jūs negalėsite prisijungti iš naujo be pakvietimo." @@ -1530,9 +1522,6 @@ "custom_theme_success": "Tema pridėta!", "custom_theme_url": "Pasirinktinės temos URL", "font_size": "Šrifto dydis", - "font_size_limit": "Pasirinktinis šrifto dydis gali būti tik tarp %(min)s pt ir %(max)s pt", - "font_size_nan": "Dydis turi būti skaičius", - "font_size_valid": "Naudokite dydį tarp %(min)s pt ir %(max)s pt", "heading": "Tinkinti savo išvaizdą", "image_size_default": "Numatytas", "image_size_large": "Didelis", diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index 95b629c1c00..9352a3cf2fe 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -221,7 +221,6 @@ "approve_access_warning": "Door de toegang voor dit apparaat goed te keuren, heeft het volledige toegang tot jouw account.", "completing_setup": "De configuratie van je nieuwe apparaat voltooien", "confirm_code_match": "Controleer of de onderstaande code overeenkomt met je andere apparaat:", - "devices_connected": "Verbonden apparaten", "error_device_already_signed_in": "Het andere apparaat is al aangemeld.", "error_device_not_signed_in": "Het andere apparaat is niet ingelogd.", "error_device_unsupported": "Koppelen met dit apparaat wordt niet ondersteund.", @@ -231,10 +230,8 @@ "error_request_cancelled": "Het verzoek is geannuleerd.", "error_request_declined": "Het verzoek is afgewezen op het andere apparaat.", "error_unexpected": "Er is een onverwachte fout opgetreden.", - "review_and_approve": "Controleer en keur de aanmelding goed", "scan_code_instruction": "Scan de onderstaande QR-code met je apparaat dat is uitgelogd.", "sign_in_new_device": "Aanmelden nieuw apparaat", - "start_at_sign_in_screen": "Begin bij het inlogscherm", "waiting_for_device": "Wachten op apparaat om in te loggen" }, "register_action": "Registreren", @@ -630,7 +627,6 @@ "methods": "Methoden", "no_verification_requests_found": "Geen verificatieverzoeken gevonden", "number_of_users": "Aantal personen", - "observe_only": "Alleen observeren", "original_event_source": "Originele gebeurtenisbron", "phase": "Fase", "phase_cancelled": "Geannuleerd", @@ -638,7 +634,6 @@ "phase_requested": "Aangevraagd", "phase_started": "Begonnen", "phase_transaction": "Transactie", - "requester": "Aanvrager", "room_id": "Kamer ID: %(roomId)s", "save_setting_values": "Instelling waardes opslaan", "send_custom_account_data_event": "Aangepaste accountgegevens gebeurtenis versturen", @@ -1257,14 +1252,6 @@ "view_rules": "Bekijk regels" }, "language_dropdown_label": "Taalselectie", - "lazy_loading": { - "disabled_action": "Cache wissen en hersynchroniseren", - "disabled_description1": "Je hebt voorheen %(brand)s op %(host)s gebruikt met lui laden van leden ingeschakeld. In deze versie is lui laden uitgeschakeld. De lokale cache is niet compatibel tussen deze twee instellingen, zodat %(brand)s je account moet hersynchroniseren.", - "disabled_description2": "Indien de andere versie van %(brand)s nog open staat in een ander tabblad kan je dat beter sluiten, want het geeft problemen als %(brand)s op dezelfde host gelijktijdig met lui laden ingeschakeld en uitgeschakeld draait.", - "disabled_title": "Incompatibele lokale cache", - "resync_description": "%(brand)s verbruikt nu 3-5x minder geheugen, door informatie over andere personen enkel te laden wanneer nodig. Even geduld, we synchroniseren met de server!", - "resync_title": "%(brand)s wordt bijgewerkt" - }, "leave_room_dialog": { "last_person_warning": "Je bent de enige persoon hier. Als je weggaat, zal niemand in de toekomst kunnen toetreden, jij ook niet.", "leave_room_question": "Weet je zeker dat je de kamer ‘%(roomName)s’ wil verlaten?", @@ -2048,9 +2035,6 @@ "custom_theme_success": "Thema toegevoegd!", "custom_theme_url": "Aangepaste thema-URL", "font_size": "Lettergrootte", - "font_size_limit": "Aangepaste lettergrootte kan alleen een getal tussen %(min)s pt en %(max)s pt zijn", - "font_size_nan": "Grootte moet een getal zijn", - "font_size_valid": "Gebruik een getal tussen %(min)s pt en %(max)s pt", "heading": "Weergave aanpassen", "image_size_default": "Standaard", "image_size_large": "Groot", diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index 903e13f70ed..d562f7803af 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -249,7 +249,6 @@ "completing_setup": "Kończenie konfiguracji nowego urządzenia", "confirm_code_match": "Potwierdź, że kod poniżej pasuje z Twoim drugim urządzeniem:", "connecting": "Łączenie…", - "devices_connected": "Urządzenia połączone", "error_device_already_signed_in": "Drugie urządzenie jest już zalogowane.", "error_device_not_signed_in": "Drugie urządzenie nie jest zalogowane.", "error_device_unsupported": "Wiązanie z tym urządzeniem nie jest wspierane.", @@ -260,12 +259,10 @@ "error_request_cancelled": "Żądanie zostało anulowane.", "error_request_declined": "Żądanie zostało odrzucone przez drugie urządzenie.", "error_unexpected": "Wystąpił niespodziewany błąd.", - "review_and_approve": "Sprawdź i potwierdź logowanie", "scan_code_instruction": "Zeskanuj kod QR poniżej za pomocą urządzenia, które jest wylogowane.", "scan_qr_code": "Skanuj kod QR", "select_qr_code": "Wybierz '%(scanQRCode)s'", "sign_in_new_device": "Zaloguj nowe urządzenie", - "start_at_sign_in_screen": "Rozpocznij na ekranie logowania", "waiting_for_device": "Oczekiwanie na logowanie urządzenia" }, "register_action": "Utwórz konto", @@ -743,7 +740,6 @@ "notification_state": "Status powiadomień %(notificationState)s", "notifications_debug": "Debug powiadomień", "number_of_users": "Liczba użytkowników", - "observe_only": "Tylko obserwuj", "original_event_source": "Oryginalne źródło wydarzenia", "phase": "Etap", "phase_cancelled": "Anulowano", @@ -751,7 +747,6 @@ "phase_requested": "Żądane", "phase_started": "Rozpoczęto", "phase_transaction": "Transakcja", - "requester": "Żądający", "room_encrypted": "Pokój jest szyfrowany ✅", "room_id": "ID pokoju: %(roomId)s", "room_not_encrypted": "Pokój nie jest szyfrowany 🚨", @@ -1513,14 +1508,6 @@ "view_rules": "Zobacz zasady" }, "language_dropdown_label": "Rozwiń języki", - "lazy_loading": { - "disabled_action": "Wyczyść pamięć podręczną i zsynchronizuj ponownie", - "disabled_description1": "Ostatnia sesja %(brand)s na %(host)s miała włączone leniwe ładowanie członków. W tej wersji leniwe ładowanie jest wyłączone. Ponieważ lokalna pamięć podręczna nie jest kompatybilna pomiędzy tymi wersjami, %(brand)s musi zsynchronizować ponownie Twoje konto.", - "disabled_description2": "Jeśli inna wersja %(brand)s jest nadal otwarta w innej zakładce, proszę zamknij ją, ponieważ używanie %(brand)s na tym samym komputerze z włączonym i wyłączonym jednocześnie leniwym ładowaniem będzie powodować problemy.", - "disabled_title": "Niekompatybilna lokalna pamięć podręczna", - "resync_description": "%(brand)s używa teraz 3-5x mniej pamięci, ładując informacje o innych użytkownikach tylko wtedy, gdy jest to konieczne. Poczekaj, aż ponownie zsynchronizujemy się z serwerem!", - "resync_title": "Aktualizowanie %(brand)s" - }, "leave_room_dialog": { "last_person_warning": "Jesteś jedyną osoba tutaj. Jeśli wyjdziesz, nikt nie będzie w stanie dołączyć w przyszłości, włączając Ciebie.", "leave_room_question": "Czy na pewno chcesz opuścić pokój '%(roomName)s'?", @@ -2425,9 +2412,6 @@ "custom_theme_success": "Dodano motyw!", "custom_theme_url": "Niestandardowy adres URL motywu", "font_size": "Rozmiar czcionki", - "font_size_limit": "Niestandardowy rozmiar czcionki może być wyłącznie pomiędzy %(min)s pt i %(max)s pt", - "font_size_nan": "Rozmiar musi być liczbą", - "font_size_valid": "Użyj pomiędzy %(min)s pt i %(max)s pt", "heading": "Dostosuj wygląd", "image_size_default": "Zwykły", "image_size_large": "Duży", diff --git a/src/i18n/strings/pt_BR.json b/src/i18n/strings/pt_BR.json index 07993e44ab4..6d322e1f666 100644 --- a/src/i18n/strings/pt_BR.json +++ b/src/i18n/strings/pt_BR.json @@ -997,6 +997,7 @@ "group_rooms": "Salas", "group_spaces": "Espaços", "group_themes": "Temas", + "group_threads": "Tópicos", "group_voip": "Voz e vídeo", "latex_maths": "Renderizar fórmulas matemáticas LaTeX em mensagens", "pinning": "Fixar mensagem", @@ -1035,14 +1036,6 @@ "view_rules": "Ver regras" }, "language_dropdown_label": "Menu suspenso de idiomas", - "lazy_loading": { - "disabled_action": "Limpar cache e ressincronizar", - "disabled_description1": "Você já usou o %(brand)s em %(host)s com o carregamento Lazy de participantes ativado. Nesta versão, o carregamento Lazy está desativado. Como o cache local não é compatível entre essas duas configurações, o %(brand)s precisa ressincronizar sua conta.", - "disabled_description2": "Se a outra versão do %(brand)s ainda estiver aberta em outra aba, por favor, feche-a pois usar o %(brand)s no mesmo host com o carregamento Lazy ativado e desativado simultaneamente causará problemas.", - "disabled_title": "Cache local incompatível", - "resync_description": "%(brand)s agora usa de 3 a 5 vezes menos memória, pois carrega as informações dos outros usuários apenas quando for necessário. Por favor, aguarde enquanto ressincronizamos com o servidor!", - "resync_title": "Atualizando o %(brand)s" - }, "leave_room_dialog": { "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'?", @@ -1624,9 +1617,6 @@ "custom_theme_success": "Tema adicionado!", "custom_theme_url": "Link do tema personalizado", "font_size": "Tamanho da fonte", - "font_size_limit": "O tamanho da fonte personalizada só pode estar entre %(min)s pt e %(max)s pt", - "font_size_nan": "O tamanho deve ser um número", - "font_size_valid": "Use entre %(min)s pt e %(max)s pt", "heading": "Personalize sua aparência", "image_size_default": "Padrão", "image_size_large": "Grande", diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index 437a54bc385..f9a84fd00d4 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -244,7 +244,6 @@ "completing_setup": "Завершение настройки нового устройства", "confirm_code_match": "Проверьте, чтобы код ниже совпадал с тем, что показан на другом устройстве:", "connecting": "Подключение…", - "devices_connected": "Подключенные устройства", "error_device_already_signed_in": "Уже выполнен вход на другом устройстве.", "error_device_not_signed_in": "На другом устройстве вход не выполнен.", "error_device_unsupported": "Соединение с этим устройством не поддерживается.", @@ -254,12 +253,10 @@ "error_request_cancelled": "Запрос был отменён.", "error_request_declined": "Запрос был отклонен на другом устройстве.", "error_unexpected": "Произошла неожиданная ошибка.", - "review_and_approve": "Проверьте и подтвердите вход", "scan_code_instruction": "Отсканируйте приведенный ниже QR-код на устройстве, которое вышло из системы.", "scan_qr_code": "Сканировать QR-код", "select_qr_code": "Выберите '%(scanQRCode)s'", "sign_in_new_device": "Войдите в систему c нового устройства", - "start_at_sign_in_screen": "Начните с экрана входа в систему", "waiting_for_device": "Ожидание входа устройства в систему" }, "register_action": "Создать учётную запись", @@ -735,7 +732,6 @@ "notification_state": "Состояние уведомления %(notificationState)s", "notifications_debug": "Отладка уведомлений", "number_of_users": "Количество пользователей", - "observe_only": "Только наблюдать", "original_event_source": "Оригинальный исходный код", "phase": "Фаза", "phase_cancelled": "Отменено", @@ -743,7 +739,6 @@ "phase_requested": "Запрошено", "phase_started": "Начато", "phase_transaction": "Транзакция", - "requester": "Адресат", "room_encrypted": "Комната зашифрована ✅", "room_id": "ID комнаты: %(roomId)s", "room_not_encrypted": "Комната не имеет шифрования 🚨", @@ -1494,14 +1489,6 @@ "view_rules": "Посмотреть правила" }, "language_dropdown_label": "Список языков", - "lazy_loading": { - "disabled_action": "Очистить кэш и выполнить повторную синхронизацию", - "disabled_description1": "Ранее вы использовали %(brand)s на %(host)s с отложенной загрузкой участников. В этой версии отложенная загрузка отключена. Поскольку локальный кеш не совместим между этими двумя настройками, %(brand)s необходимо повторно синхронизировать вашу учётную запись.", - "disabled_description2": "Если другая версия %(brand)s все еще открыта на другой вкладке, закройте ее, так как использование %(brand)s на том же хосте с включенной и отключенной ленивой загрузкой одновременно вызовет проблемы.", - "disabled_title": "Несовместимый локальный кэш", - "resync_description": "%(brand)s теперь использует в 3-5 раз меньше памяти, загружая информацию о других пользователях только когда это необходимо. Пожалуйста, подождите, пока мы ресинхронизируемся с сервером!", - "resync_title": "Обновление %(brand)s" - }, "leave_room_dialog": { "last_person_warning": "Вы здесь единственный человек. Если вы уйдете, никто не сможет присоединиться в будущем, включая вас.", "leave_room_question": "Уверены, что хотите покинуть '%(roomName)s'?", @@ -2400,9 +2387,6 @@ "custom_theme_success": "Тема добавлена!", "custom_theme_url": "Ссылка на стороннюю тему", "font_size": "Размер шрифта", - "font_size_limit": "Пользовательский размер шрифта может быть только между %(min)s pt и %(max)s pt", - "font_size_nan": "Размер должен быть числом", - "font_size_valid": "Введите значение между %(min)s pt и %(max)s pt", "heading": "Настройка внешнего вида", "image_size_default": "По умолчанию", "image_size_large": "Большой", diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json index a4c1e27de22..ca7b24e4760 100644 --- a/src/i18n/strings/sk.json +++ b/src/i18n/strings/sk.json @@ -244,7 +244,6 @@ "completing_setup": "Dokončenie nastavenia nového zariadenia", "confirm_code_match": "Skontrolujte, či sa nižšie uvedený kód zhoduje s vaším druhým zariadením:", "connecting": "Pripájanie…", - "devices_connected": "Zariadenia pripojené", "error_device_already_signed_in": "Druhé zariadenie je už prihlásené.", "error_device_not_signed_in": "Druhé zariadenie nie je prihlásené.", "error_device_unsupported": "Prepojenie s týmto zariadením nie je podporované.", @@ -254,12 +253,10 @@ "error_request_cancelled": "Žiadosť bola zrušená.", "error_request_declined": "Žiadosť bola na druhom zariadení zamietnutá.", "error_unexpected": "Vyskytla sa neočakávaná chyba.", - "review_and_approve": "Skontrolujte a schváľte prihlásenie", "scan_code_instruction": "Naskenujte nižšie uvedený QR kód pomocou zariadenia, ktoré je odhlásené.", "scan_qr_code": "Skenovať QR kód", "select_qr_code": "Vyberte '%(scanQRCode)s'", "sign_in_new_device": "Prihlásiť nové zariadenie", - "start_at_sign_in_screen": "Začnite na prihlasovacej obrazovke", "waiting_for_device": "Čaká sa na prihlásenie zariadenia" }, "register_action": "Vytvoriť účet", @@ -735,7 +732,6 @@ "notification_state": "Stav oznámenia je %(notificationState)s", "notifications_debug": "Ladenie oznámení", "number_of_users": "Počet používateľov", - "observe_only": "Iba pozorovať", "original_event_source": "Pôvodný zdroj udalosti", "phase": "Fáza", "phase_cancelled": "Zrušené", @@ -743,7 +739,6 @@ "phase_requested": "Vyžiadané", "phase_started": "Spustené", "phase_transaction": "Transakcia", - "requester": "Žiadateľ", "room_encrypted": "Miestnosť je šifrovaná ✅", "room_id": "ID miestnosti: %(roomId)s", "room_not_encrypted": "Miestnosť nie je šifrovaná 🚨", @@ -1499,14 +1494,6 @@ "view_rules": "Zobraziť pravidlá" }, "language_dropdown_label": "Rozbaľovací zoznam jazykov", - "lazy_loading": { - "disabled_action": "Vymazať vyrovnávaciu pamäť a synchronizovať znovu", - "disabled_description1": "Použili ste aj %(brand)s na adrese %(host)s so zapnutou voľbou Načítanie zoznamu členov pri prvom zobrazení. V tejto verzii je Načítanie zoznamu členov pri prvom zobrazení vypnuté. Keď že lokálna vyrovnávacia pamäť nie je vzájomne kompatibilná s takýmito nastaveniami, %(brand)s potrebuje znovu synchronizovať údaje z vašeho účtu.", - "disabled_description2": "Ak máte %(brand)s s iným nastavením otvorený na ďalšej karte, prosím zatvorte ju, pretože použitie %(brand)s s rôznym nastavením na jednom zariadení vám spôsobí len problémy.", - "disabled_title": "Nekompatibilná lokálna vyrovnávacia pamäť", - "resync_description": "%(brand)s teraz vyžaduje 3-5× menej pamäte, pretože informácie o ostatných používateľoch načítava len podľa potreby. Prosím počkajte na dokončenie synchronizácie so serverom!", - "resync_title": "Prebieha aktualizácia %(brand)s" - }, "leave_room_dialog": { "last_person_warning": "Ste tu jediný človek. Ak odídete, nikto sa už v budúcnosti nebude môcť pripojiť do tejto miestnosti, vrátane vás.", "leave_room_question": "Ste si istí, že chcete opustiť miestnosť '%(roomName)s'?", @@ -2404,9 +2391,6 @@ "custom_theme_success": "Vzhľad pridaný!", "custom_theme_url": "URL adresa vlastného vzhľadu", "font_size": "Veľkosť písma", - "font_size_limit": "Vlastná veľkosť písma môže byť len v rozmedzí %(min)s pt až %(max)s pt", - "font_size_nan": "Veľkosť musí byť číslo", - "font_size_valid": "Použite veľkosť mezi %(min)s pt a %(max)s pt", "heading": "Upravte svoj vzhľad", "image_size_default": "Predvolené", "image_size_large": "Veľký", diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index c812e2e81c8..c75782696c6 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -235,7 +235,6 @@ "completing_setup": "Po plotësohet ujdisja e pajisjes tuaj të re", "confirm_code_match": "Kontrolloni se kodi më poshtë përkon me atë në pajisjen tuaj tjetër:", "connecting": "Po lidhet…", - "devices_connected": "Pajisje të lidhura", "error_device_already_signed_in": "Nga pajisja tjetër është bërë tashmë hyrja.", "error_device_not_signed_in": "Te pajisja tjetër s’është bërë hyrja.", "error_device_unsupported": "Lidhja me këtë pajisje nuk mbulohet.", @@ -246,12 +245,10 @@ "error_request_cancelled": "Kërkesa u anulua.", "error_request_declined": "Kërkesa u hodh poshtë në pajisjen tjetër.", "error_unexpected": "Ndodhi një gabim të papritur.", - "review_and_approve": "Shqyrtoni dhe miratojeni hyrjen", "scan_code_instruction": "Skanoni kodin QR më poshtë me pajisjen ku është bërë dalja.", "scan_qr_code": "Skanoni kodin QR", "select_qr_code": "Përzgjidhni “%(scanQRCode)s”", "sign_in_new_device": "Hyni në pajisje të re", - "start_at_sign_in_screen": "Filloja në skenën e hyrjes", "waiting_for_device": "Po pritet që të bëhet hyrja te pajisja" }, "register_action": "Krijoni Llogari", @@ -706,7 +703,6 @@ "notification_state": "Gjendje njoftimi është %(notificationState)s", "notifications_debug": "Diagnostikim njoftimesh", "number_of_users": "Numër përdoruesish", - "observe_only": "Vetëm vëzhgo", "original_event_source": "Burim i veprimtarisë origjinale", "phase": "Fazë", "phase_cancelled": "Anuluar", @@ -714,7 +710,6 @@ "phase_requested": "E kërkuar", "phase_started": "Nisur më", "phase_transaction": "Transaksion", - "requester": "Kërkues", "room_encrypted": "Dhoma është e fshehtëzuar ✅", "room_id": "ID Dhome: %(roomId)s", "room_not_encrypted": "Dhoma është e pafshehtëzuar 🚨", @@ -1333,6 +1328,7 @@ "group_rooms": "Dhoma", "group_spaces": "Hapësira", "group_themes": "Tema", + "group_threads": "Rrjedha", "group_voip": "Zë & Video", "group_widgets": "Widget-e", "hidebold": "Fshihe pikën e njoftimeve (shfaq vetëm stema numëratorësh)", @@ -1409,14 +1405,6 @@ "view_rules": "Shihni rregulla" }, "language_dropdown_label": "Menu Hapmbyll Gjuhësh", - "lazy_loading": { - "disabled_action": "Spastro fshehtinën dhe rinjëkohëso", - "disabled_description1": "Më parë përdornit %(brand)s në %(host)s me lazy loading anëtarësh të aktivizuar. Në këtë version lazy loading është çaktivizuar. Ngaqë fshehtina vendore s’është e përputhshme mes këtyre dy rregullimeve, %(brand)s-i lyp të rinjëkohësohet llogaria juaj.", - "disabled_description2": "Nëse versioni tjetër i %(brand)s-it është ende i hapur në një skedë tjetër, ju lutemi, mbylleni, ngaqë përdorimi njëkohësisht i %(brand)s-it në të njëjtën strehë, në njërën anë me lazy loading të aktivizuar dhe në anën tjetër të çaktivizuar do të shkaktojë probleme.", - "disabled_title": "Fshehtinë vendore e papërputhshme", - "resync_description": "%(brand)s-i tani përdor 3 deri 5 herë më pak kujtesë, duke ngarkuar të dhëna mbi përdorues të tjerë vetëm kur duhen. Ju lutemi, prisni, teksa njëkohësojmë të dhënat me shërbyesin!", - "resync_title": "%(brand)s-i po përditësohet" - }, "leave_room_dialog": { "last_person_warning": "Jeni i vetmi person këtu. Nëse e braktisni, askush s’do të jetë në gjendje të hyjë në të ardhmen, përfshi ju.", "leave_room_question": "Jeni i sigurt se doni të dilni nga dhoma '%(roomName)s'?", @@ -2268,9 +2256,6 @@ "custom_theme_success": "Tema u shtua!", "custom_theme_url": "URL teme vetjake", "font_size": "Madhësi shkronjash", - "font_size_limit": "Madhësia vetjake për shkronjat mund të jetë vetëm mes vlerave %(min)s pt dhe %(max)s pt", - "font_size_nan": "Madhësia duhet të jetë një numër", - "font_size_valid": "Përdor me %(min)s pt dhe %(max)s pt", "heading": "Përshtatni dukjen tuaj", "image_size_default": "Parazgjedhje", "image_size_large": "E madhe", diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json index ffc67e8e52d..722ef247afb 100644 --- a/src/i18n/strings/sv.json +++ b/src/i18n/strings/sv.json @@ -249,7 +249,6 @@ "completing_setup": "Slutför inställning av din nya enhet", "confirm_code_match": "Kolla att koden nedan matchar din andra enhet:", "connecting": "Kopplar upp …", - "devices_connected": "Enheter anslutna", "error_device_already_signed_in": "Den andra enheten är redan inloggad.", "error_device_not_signed_in": "Den andra enheten är inte inloggad.", "error_device_unsupported": "Länkning med den här enheten stöds inte.", @@ -260,12 +259,10 @@ "error_request_cancelled": "Förfrågan avbröts.", "error_request_declined": "Förfrågan nekades på den andra enheten.", "error_unexpected": "Ett oväntade fel inträffade.", - "review_and_approve": "Granska och godkänn inloggningen", "scan_code_instruction": "Skanna QR-koden nedan med din andra enhet som är utloggad.", "scan_qr_code": "Skanna QR-kod", "select_qr_code": "Välj '%(scanQRCode)s'", "sign_in_new_device": "Logga in ny enhet", - "start_at_sign_in_screen": "Börja på inloggningsskärmen", "waiting_for_device": "Väntar på att enheter loggar in" }, "register_action": "Skapa konto", @@ -479,6 +476,7 @@ "legal": "Juridiskt", "light": "Ljust", "loading": "Laddar …", + "lobby": "Lobby", "location": "Plats", "low_priority": "Låg prioritet", "matrix": "Matrix", @@ -741,7 +739,6 @@ "notification_state": "Aviseringsstatus är %(notificationState)s", "notifications_debug": "Aviseringsfelsökning", "number_of_users": "Antal användare", - "observe_only": "Bara kolla", "original_event_source": "Ursprunglig händelsekällkod", "phase": "Fas", "phase_cancelled": "Avbruten", @@ -749,7 +746,6 @@ "phase_requested": "Efterfrågad", "phase_started": "Påbörjad", "phase_transaction": "Transaktion", - "requester": "Den som skickat förfrågan", "room_encrypted": "Rummet är krypterat ✅", "room_id": "Rums-ID: %(roomId)s", "room_not_encrypted": "Rummet är inte krypterat 🚨", @@ -762,6 +758,7 @@ "room_notifications_type": "Typ: ", "room_status": "Rumsstatus", "room_unread_status_count": { + "one": "Rummets oläst-status: %(status)s, antal: %(count)s", "other": "Rummets oläst-status: %(status)s, antal: %(count)s" }, "save_setting_values": "Spara inställningsvärden", @@ -1414,6 +1411,7 @@ "group_rooms": "Rum", "group_spaces": "Utrymmen", "group_themes": "Teman", + "group_threads": "Trådar", "group_voip": "Röst & video", "group_widgets": "Widgets", "hidebold": "Dölj aviseringspunkt (visa bara räknarmärken)", @@ -1442,9 +1440,13 @@ "report_to_moderators": "Rapportera till moderatorer", "report_to_moderators_description": "I rum som stöder moderering så låter \"Rapportera\"-knappen dig rapportera trakasseri till rumsmoderatorer.", "rust_crypto": "Kryptografiimplementering i Rust", - "sliding_sync": "Glidande synkroniseringsläge", + "rust_crypto_in_config": "Rust-kryptografi kan inte inaktiveras på den här distributionen av %(brand)s", + "rust_crypto_in_config_description": "Byte till Rust-kryptografi kräver en migreringsprocess som kan ta flera minuter. Den kan inte inaktiveras; använd den med försiktighet!", + "rust_crypto_optin_warning": "Byte till Rust-kryptografi kräver en migreringsprocess som kan ta flera minuter. För att inaktivera måste du logga ut och in igen; använd med försiktighet!", + "rust_crypto_requires_logout": "När Rust-kryptografi har aktiverats kan den endast avaktiveras genom att logga ut och logga in igen", + "sliding_sync": "Sliding sync-läge", "sliding_sync_checking": "Kontrollerar …", - "sliding_sync_configuration": "Glidande synk-läge", + "sliding_sync_configuration": "Sliding sync-konfiguration", "sliding_sync_description": "Under aktiv utveckling, kan inte inaktiveras.", "sliding_sync_disable_warning": "För att inaktivera det här så behöver du logga ut och logga in igen, använd varsamt!", "sliding_sync_disabled_notice": "Logga ut och in igen för att inaktivera", @@ -1453,6 +1455,8 @@ "sliding_sync_server_no_support": "Din server saknar nativt stöd", "sliding_sync_server_specify_proxy": "Din server saknar nativt stöd, du måste ange en proxy", "sliding_sync_server_support": "Din server har nativt stöd", + "threads_activity_centre": "Aktivitetscenter för trådar (under utveckling). För närvarande tar detta bara bort antalet trådaviseringar från det totala antalet i rumslistan", + "threads_activity_centre_description": "Varning: Under aktiv utveckling; laddar om Element.", "under_active_development": "Under aktiv utveckling.", "unrealiable_e2e": "Otillförlitlig i krypterade rum", "video_rooms": "Videorum", @@ -1502,14 +1506,6 @@ "view_rules": "Visa regler" }, "language_dropdown_label": "Språkmeny", - "lazy_loading": { - "disabled_action": "Töm cache och synkronisera om", - "disabled_description1": "Du har tidigare använt %(brand)s på %(host)s med fördröjd inladdning av medlemmar aktiverat. I den här versionen är fördröjd inladdning inaktiverat. Eftersom den lokala cachen inte är kompatibel mellan dessa två inställningar behöver %(brand)s synkronisera om ditt konto.", - "disabled_description2": "Om den andra versionen av %(brand)s fortfarande är öppen i en annan flik, stäng den eftersom användning av %(brand)s på samma värd med fördröjd inladdning både aktiverad och inaktiverad samtidigt kommer att orsaka problem.", - "disabled_title": "Inkompatibel lokal cache", - "resync_description": "%(brand)s använder nu 3-5 gånger mindre minne, genom att bara ladda information om andra användare när det behövs. Vänta medan vi återsynkroniserar med servern!", - "resync_title": "Uppdaterar %(brand)s" - }, "leave_room_dialog": { "last_person_warning": "Du är den enda personen här. Om du lämnar så kommer ingen kunna gå med igen, inklusive du.", "leave_room_question": "Vill du lämna rummet '%(roomName)s'?", @@ -2413,9 +2409,6 @@ "custom_theme_success": "Tema tillagt!", "custom_theme_url": "Anpassad tema-URL", "font_size": "Teckenstorlek", - "font_size_limit": "Anpassad teckenstorlek kan bara vara mellan %(min)s pt och %(max)s pt", - "font_size_nan": "Storleken måste vara ett nummer", - "font_size_valid": "Använd mellan %(min)s pt och %(max)s pt", "heading": "Anpassa ditt utseende", "image_size_default": "Standard", "image_size_large": "Stor", @@ -3150,6 +3143,10 @@ "show_thread_filter": "Visa:", "unable_to_decrypt": "Kunde inte avkryptera meddelande" }, + "threads_activity_centre": { + "header": "Aktivitet för trådar", + "no_rooms_with_unreads_threads": "Du har inte rum med olästa trådar än." + }, "time": { "about_day_ago": "cirka en dag sedan", "about_hour_ago": "cirka en timme sedan", @@ -3797,6 +3794,7 @@ "camera_enabled": "Din kamera är fortfarande på", "cannot_call_yourself_description": "Du kan inte ringa till dig själv.", "change_input_device": "Byt ingångsenhet", + "close_lobby": "Stäng lobbyn", "connecting": "Ansluter", "connection_lost": "Anslutningen till servern har förlorats", "connection_lost_description": "Du kan inte ringa samtal utan en anslutning till servern.", @@ -3810,6 +3808,7 @@ "disabled_no_perms_start_video_call": "Du är inte behörig att starta videosamtal", "disabled_no_perms_start_voice_call": "Du är inte behörig att starta röstsamtal", "disabled_ongoing_call": "Pågående samtal", + "element_call": "Element Call", "enable_camera": "Sätt på kamera", "enable_microphone": "Slå på mikrofonen", "expand": "Återgå till samtal", @@ -3818,9 +3817,13 @@ "hangup": "Lägg på", "hide_sidebar_button": "Göm sidopanel", "input_devices": "Ingångsenheter", + "jitsi_call": "Jitsi-gruppsamtal", "join_button_tooltip_call_full": "Tyvärr - det här samtalet är för närvarande fullt", "join_button_tooltip_connecting": "Ansluter", + "legacy_call": "Gammal samtalsfunktion", "maximise": "Fyll skärmen", + "maximise_call": "Maximera samtal", + "minimise_call": "Minimera samtal", "misconfigured_server": "Anrop misslyckades på grund av felkonfigurerad server", "misconfigured_server_description": "Be administratören för din hemserver (%(homeserverDomain)s) att konfigurera en TURN-server för att samtal ska fungera pålitligt.", "misconfigured_server_fallback": "Alternativt kan du försöka använda den offentliga servern på , men det kommer inte att vara lika tillförlitligt och det kommer att dela din IP-adress med den servern. Du kan också hantera detta i Inställningar.", @@ -3868,6 +3871,7 @@ "user_is_presenting": "%(sharerName)s presenterar", "video_call": "Videosamtal", "video_call_started": "Videosamtal startat", + "video_call_using": "Videosamtal med hjälp av:", "voice_call": "Röstsamtal", "you_are_presenting": "Du presenterar" }, diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json index 496c5d292f8..b9b307889e6 100644 --- a/src/i18n/strings/uk.json +++ b/src/i18n/strings/uk.json @@ -241,7 +241,6 @@ "completing_setup": "Завершення налаштування нового пристрою", "confirm_code_match": "Перевірте, чи збігається наведений внизу код з кодом на вашому іншому пристрої:", "connecting": "З'єднання…", - "devices_connected": "Пристрої під'єднано", "error_device_already_signed_in": "На іншому пристрої вхід було виконано.", "error_device_not_signed_in": "На іншому пристрої вхід не виконано.", "error_device_unsupported": "Зв'язок з цим пристроєм не підтримується.", @@ -252,12 +251,10 @@ "error_request_cancelled": "Запит було скасовано.", "error_request_declined": "На іншому пристрої запит відхилено.", "error_unexpected": "Виникла непередбачувана помилка.", - "review_and_approve": "Розглянути та схвалити вхід", "scan_code_instruction": "Скануйте QR-код знизу своїм пристроєм, на якому ви вийшли.", "scan_qr_code": "Скануйте QR-код", "select_qr_code": "Виберіть «%(scanQRCode)s»", "sign_in_new_device": "Увійти на новому пристрої", - "start_at_sign_in_screen": "Почніть з екрана входу", "waiting_for_device": "Очікування входу з пристрою" }, "register_action": "Створити обліковий запис", @@ -728,7 +725,6 @@ "notification_state": "Стан сповіщень %(notificationState)s", "notifications_debug": "Сповіщення зневадження", "number_of_users": "Кількість користувачів", - "observe_only": "Лише спостерігати", "original_event_source": "Оригінальний початковий код", "phase": "Фаза", "phase_cancelled": "Скасовано", @@ -736,7 +732,6 @@ "phase_requested": "Подано запит", "phase_started": "Почато", "phase_transaction": "Транзакція", - "requester": "Адресант", "room_encrypted": "Кімната зашифрована ✅", "room_id": "ID кімнати: %(roomId)s", "room_not_encrypted": "Кімната не зашифрована 🚨", @@ -1376,6 +1371,7 @@ "group_rooms": "Кімнати", "group_spaces": "Простори", "group_themes": "Теми", + "group_threads": "Гілки", "group_voip": "Голос і відео", "group_widgets": "Віджети", "hidebold": "Сховати крапку сповіщення ( показувати тільки значки лічильників)", @@ -1457,14 +1453,6 @@ "view_rules": "Переглянути правила" }, "language_dropdown_label": "Спадне меню мов", - "lazy_loading": { - "disabled_action": "Очистити кеш і повторно синхронізувати", - "disabled_description1": "Ви використовували %(brand)s на %(host)s, ввімкнувши відкладене звантаження учасників. У цій версії відкладене звантаження вимкнене. Оскільки локальне кешування не підтримує переходу між цими опціями, %(brand)s мусить заново синхронізувати ваш обліковий запис.", - "disabled_description2": "Якщо інший примірник %(brand)s досі відкритий в іншій вкладці, просимо закрити її, бо використання %(brand)s із водночас увімкненим і вимкненим відкладеним звантаженням створюватиме проблеми.", - "disabled_title": "Несумісний локальний кеш", - "resync_description": "%(brand)s тепер використовує в 3-5 разів менше пам'яті, звантажуючи дані про інших користувачів лише за потреби. Зачекайте, поки ми синхронізуємося з сервером!", - "resync_title": "Оновлення %(brand)s" - }, "leave_room_dialog": { "last_person_warning": "Тут лише ви. Якщо ви вийдете, ніхто більше не зможе приєднатися, навіть ви самі.", "leave_room_question": "Ви впевнені, що хочете вийти з «%(roomName)s»?", @@ -2340,9 +2328,6 @@ "custom_theme_success": "Тему додано!", "custom_theme_url": "Посилання на власну тему", "font_size": "Розмір шрифту", - "font_size_limit": "Нетиповий розмір шрифту може бути лише в межах %(min)s пт і %(max)s пт", - "font_size_nan": "Розмір повинен бути числом", - "font_size_valid": "Введіть від %(min)s пт до %(max)s пт", "heading": "Налаштування вигляду", "image_size_default": "Типовий", "image_size_large": "Великі", diff --git a/src/i18n/strings/vi.json b/src/i18n/strings/vi.json index 50593db3cb0..7fc2c66760d 100644 --- a/src/i18n/strings/vi.json +++ b/src/i18n/strings/vi.json @@ -1266,6 +1266,7 @@ "group_rooms": "Phòng", "group_spaces": "Không gian", "group_themes": "Chủ đề", + "group_threads": "Chủ đề", "group_voip": "Âm thanh & Hình ảnh", "group_widgets": "Vật dụng", "hidebold": "Ẩn dấu chấm thông báo (chỉ hiển thị bộ đếm)", @@ -1340,14 +1341,6 @@ "view_rules": "Xem các quy tắc" }, "language_dropdown_label": "Danh sách ngôn ngữ", - "lazy_loading": { - "disabled_action": "Xóa bộ nhớ cache và đồng bộ hóa lại", - "disabled_description1": "Trước đây, bạn đã sử dụng %(brand)s trên %(host)s khi đã bật tính năng tải chậm các thành viên. Trong phiên bản này, tính năng tải lười biếng bị vô hiệu hóa. Vì bộ nhớ cache cục bộ không tương thích giữa hai cài đặt này, %(brand)s cần phải đồng bộ hóa lại tài khoản của bạn.", - "disabled_description2": "Nếu phiên bản khác của %(brand)s vẫn đang mở trong một tab khác, vui lòng đóng nó lại vì việc sử dụng %(brand)s trên cùng một máy chủ với cả hai chế độ tải chậm được bật và tắt đồng thời sẽ gây ra sự cố.", - "disabled_title": "Bộ nhớ cache cục bộ không tương thích", - "resync_description": "%(brand)s hiện sử dụng bộ nhớ ít hơn 3-5 lần, bằng cách chỉ tải thông tin về những người dùng khác khi cần thiết. Vui lòng đợi trong khi chúng tôi đồng bộ hóa lại với máy chủ!", - "resync_title": "Đang cập nhật %(brand)s" - }, "leave_room_dialog": { "last_person_warning": "Bạn là người duy nhất ở đây. Nếu bạn rời, không ai có thể tham gia trong tương lai, kể cả bạn.", "leave_room_question": "Bạn có chắc chắn muốn rời khỏi phòng '%(roomName)s' không?", @@ -2127,9 +2120,6 @@ "custom_theme_success": "Đã thêm chủ đề!", "custom_theme_url": "URL chủ đề tùy chỉnh", "font_size": "Cỡ chữ", - "font_size_limit": "Kích thước phông chữ tùy chỉnh chỉ có thể nằm trong khoảng từ %(min)s pt đến %(max)s pt", - "font_size_nan": "Kích thước phải là một số", - "font_size_valid": "Sử dụng từ khoảng %(min)s pt đến %(max)s pt", "heading": "Tùy chỉnh diện mạo của bạn", "image_size_default": "Mặc định", "image_size_large": "Lớn", diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json index 7950fe405a5..e03a592144d 100644 --- a/src/i18n/strings/zh_Hans.json +++ b/src/i18n/strings/zh_Hans.json @@ -206,7 +206,7 @@ "forgot_password_email_required": "必须输入和你账户关联的邮箱地址。", "forgot_password_prompt": "忘记你的密码了吗?", "forgot_password_send_email": "发送重置连接", - "identifier_label": "第三方登录", + "identifier_label": "登录方式", "incorrect_credentials": "用户名或密码错误。", "incorrect_credentials_detail": "请注意,你正在登录 %(hs)s,而非 matrix.org。", "incorrect_password": "密码错误", @@ -244,7 +244,6 @@ "completing_setup": "完成新设备的设置", "confirm_code_match": "检查以下代码是否与你的其他设备匹配:", "connecting": "正在连接……", - "devices_connected": "已连接的设备", "error_homeserver_lacks_support": "此服务器不支持多设备登录" }, "register_action": "创建账户", @@ -277,7 +276,7 @@ "server_picker_description": "你可以使用自定义服务器选项来指定不同的家服务器URL以登录其他Matrix服务器。这让你能把%(brand)s和不同家服务器上的已有Matrix账户搭配使用。", "server_picker_description_matrix.org": "免费加入最大的公共服务器,成为数百万用户中的一员", "server_picker_dialog_title": "决定账户托管位置", - "server_picker_explainer": "使用你偏好的Matrix家服务器,如果你有的话,或自己架设一个。", + "server_picker_explainer": "使用你的Matrix服务器,或自己架设一个。", "server_picker_failed_validate_homeserver": "无法验证家服务器", "server_picker_intro": "我们将您可以托管账户的地方称为“服务器组”。", "server_picker_invalid_url": "URL 无效", @@ -686,14 +685,12 @@ "methods": "方法", "no_verification_requests_found": "未找到验证请求", "number_of_users": "用户数", - "observe_only": "仅观察", "original_event_source": "原始事件源码", "phase": "阶段", "phase_cancelled": "已取消", "phase_requested": "已请求", "phase_started": "已开始", "phase_transaction": "交易", - "requester": "请求者", "room_id": "房间ID: %(roomId)s", "save_setting_values": "保存设置值", "send_custom_account_data_event": "发送自定义账户数据事件", @@ -1363,14 +1360,6 @@ "view_rules": "查看规则" }, "language_dropdown_label": "语言下拉菜单", - "lazy_loading": { - "disabled_action": "清除缓存并重新同步", - "disabled_description1": "你之前在 %(host)s 上开启了 %(brand)s 的成员列表延迟加载设置。目前版本中延迟加载功能已被停用。因为本地缓存在这两个设置项上不相容,%(brand)s 需要重新同步你的账户。", - "disabled_description2": "如果别的 %(brand)s 版本在别的标签页中仍然开启,请关闭它,因为在同一宿主上同时使用开启了延迟加载和关闭了延迟加载的 %(brand)s 会导致问题。", - "disabled_title": "本地缓存不兼容", - "resync_description": "通过仅在需要时加载其他用户的信息,%(brand)s 现在使用的内存减少到了原来的三分之一至五分之一。 请等待与服务器重新同步!", - "resync_title": "正在更新 %(brand)s" - }, "leave_room_dialog": { "last_person_warning": "你是这里唯一的人。如果你离开了,以后包括你在内任何人都将无法加入。", "leave_room_question": "你确定要离开房间 “%(roomName)s” 吗?", @@ -2168,9 +2157,6 @@ "custom_theme_success": "主题已添加!", "custom_theme_url": "自定义主题URL", "font_size": "字体大小", - "font_size_limit": "自定义字体大小只能介于 %(min)s pt 和 %(max)s pt 之间", - "font_size_nan": "大小必须是数字", - "font_size_valid": "请使用介于 %(min)s pt 和 %(max)s pt 之间的大小", "heading": "自定义你的外观", "image_size_default": "默认", "image_size_large": "大", diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index de847b63ac1..0f02cb8ed05 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -40,6 +40,7 @@ "create_a_room": "建立聊天室", "decline": "拒絕", "delete": "刪除", + "deny": "拒絕", "disable": "停用", "disconnect": "中斷連線", "dismiss": "關閉", @@ -240,7 +241,6 @@ "completing_setup": "完成您新裝置的設定", "confirm_code_match": "請確認下列代碼與您另一台裝置上的代碼相符:", "connecting": "連線中…", - "devices_connected": "裝置已連線", "error_device_already_signed_in": "其他裝置已登入。", "error_device_not_signed_in": "其他裝置未登入。", "error_device_unsupported": "不支援與此裝置連結。", @@ -251,12 +251,10 @@ "error_request_cancelled": "請求已取消。", "error_request_declined": "請求在另一台裝置上被拒絕。", "error_unexpected": "發生預料之外的錯誤。", - "review_and_approve": "審閱並批准登入", "scan_code_instruction": "請用您已登出的裝置掃描下列 QR Code。", "scan_qr_code": "掃描 QR Code", "select_qr_code": "選取「%(scanQRCode)s」", "sign_in_new_device": "登入新裝置", - "start_at_sign_in_screen": "從登入畫面開始", "waiting_for_device": "正在等待裝置登入" }, "register_action": "建立帳號", @@ -423,6 +421,7 @@ "other": "與另 %(count)s 個人…", "one": "與另 1 個人…" }, + "android": "安卓", "appearance": "外觀", "application": "應用程式", "are_you_sure": "您確定嗎?", @@ -461,6 +460,7 @@ "identity_server": "身分伺服器", "image": "圖片", "integration_manager": "整合管理員", + "ios": "iOS", "joined": "已加入", "labs": "實驗室", "legal": "法律", @@ -468,6 +468,7 @@ "loading": "載入中…", "location": "位置", "low_priority": "低優先度", + "matrix": "Matrix", "message": "訊息", "message_layout": "訊息佈局", "microphone": "麥克風", @@ -725,7 +726,6 @@ "notification_state": "通知狀態為 %(notificationState)s", "notifications_debug": "通知除錯", "number_of_users": "使用者數量", - "observe_only": "僅觀察", "original_event_source": "原始活動來源", "phase": "階段", "phase_cancelled": "已取消", @@ -733,7 +733,6 @@ "phase_requested": "已請求", "phase_started": "已開始", "phase_transaction": "交易", - "requester": "請求者", "room_encrypted": "聊天室已加密 ✅", "room_id": "聊天室 ID:%(roomId)s", "room_not_encrypted": "聊天室未加密 🚨", @@ -1301,6 +1300,7 @@ "composer_toggle_quote": "切換引用", "composer_undo": "復原編輯", "dismiss_read_marker_and_jump_bottom": "清除讀取標記並跳至底部", + "end": "", "go_home_view": "前往主畫面", "home": "首頁", "jump_first_message": "跳至第一則訊息", @@ -1315,6 +1315,8 @@ "next_room": "下一個聊天室或私人訊息", "next_unread_room": "下一個未讀的聊天室或私人訊息", "open_user_settings": "開啟使用者設定", + "page_down": "下頁", + "page_up": "上頁", "prev_room": "上一個聊天室或私人訊息", "prev_unread_room": "上一個未讀的聊天室或私人訊息", "room_list_collapse_section": "折疊聊天室清單段落", @@ -1372,6 +1374,7 @@ "group_rooms": "聊天室", "group_spaces": "聊天空間", "group_themes": "主題", + "group_threads": "討論串", "group_voip": "語音與視訊", "group_widgets": "小工具", "hidebold": "隱藏通知點(僅顯示計數器徽章)", @@ -1453,14 +1456,6 @@ "view_rules": "檢視規則" }, "language_dropdown_label": "語言下拉式選單", - "lazy_loading": { - "disabled_action": "清除快取並重新同步", - "disabled_description1": "您之前曾在 %(host)s 上使用 %(brand)s 並啟用成員列表的延遲載入。在此版本中延遲載入已停用。由於本機快取在這兩個設定間不相容,%(brand)s 必須重新同步您的帳號。", - "disabled_description2": "如果其他版本的 %(brand)s 仍在其他分頁中開啟,請關閉它,因為在同一主機上使用同時啟用與停用惰性載入的 %(brand)s 可能會造成問題。", - "disabled_title": "不相容的本機快取", - "resync_description": "%(brand)s 現在僅使用低於原本3-5倍的記憶體,僅在需要時才會載入其他使用者的資訊。請等待我們與伺服器重新同步!", - "resync_title": "正在更新 %(brand)s" - }, "leave_room_dialog": { "last_person_warning": "您是這裡唯一的人。如果您離開,包含您在內的任何人都無法加入。", "leave_room_question": "您確定要離開聊天室「%(roomName)s」嗎?", @@ -2335,9 +2330,6 @@ "custom_theme_success": "已新增佈景主題!", "custom_theme_url": "自訂佈景主題網址", "font_size": "字型大小", - "font_size_limit": "自訂字型大小僅能為 %(min)s 點至 %(max)s 點間", - "font_size_nan": "大小必須為數字", - "font_size_valid": "使用 %(min)s 點至 %(max)s 點間", "heading": "自訂您的外觀", "image_size_default": "預設", "image_size_large": "大", diff --git a/src/indexing/EventIndex.ts b/src/indexing/EventIndex.ts index 69cb0e12184..15419eecf6d 100644 --- a/src/indexing/EventIndex.ts +++ b/src/indexing/EventIndex.ts @@ -38,6 +38,7 @@ import { TimelineIndex, TimelineWindow, } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { sleep } from "matrix-js-sdk/src/utils"; import { logger } from "matrix-js-sdk/src/logger"; @@ -533,7 +534,7 @@ export default class EventIndex extends EventEmitter { const profiles: Record = {}; stateEvents.forEach((ev) => { - if (ev.getContent().membership === "join") { + if (ev.getContent().membership === KnownMembership.Join) { profiles[ev.getSender()!] = { displayname: ev.getContent().displayname, avatar_url: ev.getContent().avatar_url, @@ -754,7 +755,7 @@ export default class EventIndex extends EventEmitter { // This is sets the avatar URL. const memberEvent = eventMapper({ content: { - membership: "join", + membership: KnownMembership.Join, avatar_url: e.profile.avatar_url, displayname: e.profile.displayname, }, diff --git a/src/mjolnir/BanList.ts b/src/mjolnir/BanList.ts index 68c15282c19..b0b63c0817c 100644 --- a/src/mjolnir/BanList.ts +++ b/src/mjolnir/BanList.ts @@ -16,12 +16,14 @@ limitations under the License. // Inspiration largely taken from Mjolnir itself +import { EventType } from "matrix-js-sdk/src/matrix"; + import { ListRule, RECOMMENDATION_BAN, recommendationToStable } from "./ListRule"; import { MatrixClientPeg } from "../MatrixClientPeg"; -export const RULE_USER = "m.policy.rule.user"; -export const RULE_ROOM = "m.policy.rule.room"; -export const RULE_SERVER = "m.policy.rule.server"; +export const RULE_USER = EventType.PolicyRuleUser; +export const RULE_ROOM = EventType.PolicyRuleRoom; +export const RULE_SERVER = EventType.PolicyRuleServer; // m.room.* events are legacy from when MSC2313 changed to m.policy.* last minute. export const USER_RULE_TYPES = [RULE_USER, "m.room.rule.user", "org.matrix.mjolnir.rule.user"]; @@ -29,7 +31,9 @@ export const ROOM_RULE_TYPES = [RULE_ROOM, "m.room.rule.room", "org.matrix.mjoln export const SERVER_RULE_TYPES = [RULE_SERVER, "m.room.rule.server", "org.matrix.mjolnir.rule.server"]; export const ALL_RULE_TYPES = [...USER_RULE_TYPES, ...ROOM_RULE_TYPES, ...SERVER_RULE_TYPES]; -export function ruleTypeToStable(rule: string): string | null { +export function ruleTypeToStable( + rule: string, +): EventType.PolicyRuleUser | EventType.PolicyRuleRoom | EventType.PolicyRuleServer | null { if (USER_RULE_TYPES.includes(rule)) { return RULE_USER; } @@ -72,7 +76,7 @@ export class BanList { { entity: entity, reason: reason, - recommendation: recommendationToStable(RECOMMENDATION_BAN, true), + recommendation: recommendationToStable(RECOMMENDATION_BAN, true)!, }, "rule:" + entity, ); diff --git a/src/mjolnir/ListRule.ts b/src/mjolnir/ListRule.ts index 3fc1b007685..c6595b33b3d 100644 --- a/src/mjolnir/ListRule.ts +++ b/src/mjolnir/ListRule.ts @@ -14,14 +14,24 @@ See the License for the specific language governing permissions and limitations under the License. */ +// We are using experimental APIs here, so we need to disable the linter +// eslint-disable-next-line no-restricted-imports +import { PolicyRecommendation } from "matrix-js-sdk/src/models/invites-ignorer"; + import { MatrixGlob } from "../utils/MatrixGlob"; // Inspiration largely taken from Mjolnir itself -export const RECOMMENDATION_BAN = "m.ban"; -export const RECOMMENDATION_BAN_TYPES = [RECOMMENDATION_BAN, "org.matrix.mjolnir.ban"]; +export const RECOMMENDATION_BAN = PolicyRecommendation.Ban; +export const RECOMMENDATION_BAN_TYPES: PolicyRecommendation[] = [ + RECOMMENDATION_BAN, + "org.matrix.mjolnir.ban" as PolicyRecommendation, +]; -export function recommendationToStable(recommendation: string, unstable = true): string | null { +export function recommendationToStable( + recommendation: PolicyRecommendation, + unstable = true, +): PolicyRecommendation | null { if (RECOMMENDATION_BAN_TYPES.includes(recommendation)) { return unstable ? RECOMMENDATION_BAN_TYPES[RECOMMENDATION_BAN_TYPES.length - 1] : RECOMMENDATION_BAN; } @@ -35,7 +45,7 @@ export class ListRule { private readonly _reason: string; private readonly _kind: string; - public constructor(entity: string, action: string, reason: string, kind: string) { + public constructor(entity: string, action: PolicyRecommendation, reason: string, kind: string) { this._glob = new MatrixGlob(entity); this._entity = entity; this._action = recommendationToStable(action, false); diff --git a/src/models/Call.ts b/src/models/Call.ts index 10af51ec54f..c7d53284ccb 100644 --- a/src/models/Call.ts +++ b/src/models/Call.ts @@ -24,6 +24,7 @@ import { Room, RoomMember, } from "matrix-js-sdk/src/matrix"; +import { KnownMembership, Membership } from "matrix-js-sdk/src/types"; import { logger } from "matrix-js-sdk/src/logger"; import { randomString } from "matrix-js-sdk/src/randomstring"; import { CallType } from "matrix-js-sdk/src/webrtc/call"; @@ -52,11 +53,12 @@ import WidgetStore from "../stores/WidgetStore"; import { WidgetMessagingStore, WidgetMessagingStoreEvent } from "../stores/widgets/WidgetMessagingStore"; import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../stores/ActiveWidgetStore"; import { getCurrentLanguage } from "../languageHandler"; -import { FontWatcher } from "../settings/watchers/FontWatcher"; import { PosthogAnalytics } from "../PosthogAnalytics"; import { UPDATE_EVENT } from "../stores/AsyncStore"; import { getJoinedNonFunctionalMembers } from "../utils/room/getJoinedNonFunctionalMembers"; import { isVideoRoom } from "../utils/video-rooms"; +import { FontWatcher } from "../settings/watchers/FontWatcher"; +import { JitsiCallMemberContent, JitsiCallMemberEventType } from "../call-types"; const TIMEOUT_MS = 16000; @@ -307,8 +309,8 @@ export abstract class Call extends TypedEventEmitter => { - if (membership !== "join") this.setDisconnected(); + private onMyMembership = async (_room: Room, membership: Membership): Promise => { + if (membership !== KnownMembership.Join) this.setDisconnected(); }; private onStopMessaging = (uid: string): void => { @@ -321,18 +323,13 @@ export abstract class Call extends TypedEventEmitter this.setDisconnected(); } -export interface JitsiCallMemberContent { - // Connected device IDs - devices: string[]; - // Time at which this state event should be considered stale - expires_ts: number; -} +export type { JitsiCallMemberContent }; /** * A group call using Jitsi as a backend. */ export class JitsiCall extends Call { - public static readonly MEMBER_EVENT_TYPE = "io.element.video.member"; + public static readonly MEMBER_EVENT_TYPE = JitsiCallMemberEventType; public readonly STUCK_DEVICE_TIMEOUT_MS = 1000 * 60 * 60; // 1 hour private resendDevicesTimer: number | null = null; @@ -386,7 +383,7 @@ export class JitsiCall extends Call { devices = devices.filter((d) => d !== this.client.getDeviceId()); } // Must have a connected device and still be joined to the room - if (devices.length > 0 && member?.membership === "join") { + if (devices.length > 0 && member?.membership === KnownMembership.Join) { participants.set(member, new Set(devices)); if (expiresAt < allExpireAt) allExpireAt = expiresAt; } @@ -416,7 +413,7 @@ export class JitsiCall extends Call { * returns null, the update is skipped. */ private async updateDevices(fn: (devices: string[]) => string[] | null): Promise { - if (this.room.getMyMembership() !== "join") return; + if (this.room.getMyMembership() !== KnownMembership.Join) return; const event = this.room.currentState.getStateEvents(JitsiCall.MEMBER_EVENT_TYPE, this.client.getUserId()!); const content = event?.getContent(); @@ -687,7 +684,8 @@ export class ElementCall extends Call { roomId: roomId, baseUrl: client.baseUrl, lang: getCurrentLanguage().replace("_", "-"), - fontScale: `${(SettingsStore.getValue("baseFontSizeV2") ?? 16) / FontWatcher.DEFAULT_SIZE}`, + fontScale: (FontWatcher.getRootFontSize() / FontWatcher.getBrowserDefaultFontSize()).toString(), + theme: "$org.matrix.msc2873.client_theme", analyticsID, }); @@ -757,7 +755,7 @@ export class ElementCall extends Call { name: "Element Call", type: WidgetType.CALL.preferred, url: url.toString(), - // waitForIframeLoad: false, + waitForIframeLoad: false, data: ElementCall.getWidgetData( client, roomId, @@ -780,7 +778,10 @@ export class ElementCall extends Call { overwriteData: IWidgetData, ): IWidgetData { let perParticipantE2EE = false; - if (client.isRoomEncrypted(roomId) && !SettingsStore.getValue("feature_disable_call_per_sender_encryption")) + if ( + client.getRoom(roomId)?.hasEncryptionStateEvent() && + !SettingsStore.getValue("feature_disable_call_per_sender_encryption") + ) perParticipantE2EE = true; return { ...currentData, diff --git a/src/models/RoomUpload.ts b/src/models/RoomUpload.ts index d6e0be4ca97..cc1b35315f5 100644 --- a/src/models/RoomUpload.ts +++ b/src/models/RoomUpload.ts @@ -15,8 +15,7 @@ limitations under the License. */ import { IEventRelation, UploadProgress } from "matrix-js-sdk/src/matrix"; - -import { EncryptedFile } from "../customisations/models/IMediaEventContent"; +import { EncryptedFile } from "matrix-js-sdk/src/types"; export class RoomUpload { public readonly abortController = new AbortController(); diff --git a/src/modules/ProxiedModuleApi.ts b/src/modules/ProxiedModuleApi.ts index 0f85992d24d..697d09a4659 100644 --- a/src/modules/ProxiedModuleApi.ts +++ b/src/modules/ProxiedModuleApi.ts @@ -160,6 +160,12 @@ export class ProxiedModuleApi implements ModuleApi { * @override */ public async overwriteAccountAuth(accountInfo: AccountAuthInfo): Promise { + // We want to wait for the new login to complete before returning. + // See `Action.OnLoggedIn` in dispatcher. + const awaitNewLogin = new Promise((resolve) => { + this.overrideLoginResolve = resolve; + }); + dispatcher.dispatch( { action: Action.OverwriteLogin, @@ -172,9 +178,7 @@ export class ProxiedModuleApi implements ModuleApi { ); // require to be sync to match inherited interface behaviour // wait for login to complete - await new Promise((resolve) => { - this.overrideLoginResolve = resolve; - }); + await awaitNewLogin; } /** diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 877469251da..3dc842945ef 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -511,6 +511,9 @@ export const SETTINGS: { [setting: string]: ISetting } = { supportedLevels: [SettingLevel.CONFIG], default: 0, }, + /** + * @deprecated in favor of {@link fontSizeDelta} + */ "baseFontSize": { displayName: _td("settings|appearance|font_size"), supportedLevels: LEVELS_ACCOUNT_SETTINGS, @@ -530,12 +533,22 @@ export const SETTINGS: { [setting: string]: ISetting } = { * With the transition to Compound we are moving to a base font size * of 16px. We're taking the opportunity to move away from the `baseFontSize` * setting that had a 5px offset. - * + * @deprecated in favor {@link fontSizeDelta} */ "baseFontSizeV2": { displayName: _td("settings|appearance|font_size"), supportedLevels: [SettingLevel.DEVICE], - default: FontWatcher.DEFAULT_SIZE, + default: "", + controller: new FontSizeController(), + }, + /** + * This delta is added to the browser default font size + * Moving from `baseFontSizeV2` to `fontSizeDelta` to replace the default 16px to --cpd-font-size-root (browser default font size) + fontSizeDelta + */ + "fontSizeDelta": { + displayName: _td("settings|appearance|font_size"), + supportedLevels: [SettingLevel.DEVICE], + default: FontWatcher.DEFAULT_DELTA, controller: new FontSizeController(), }, "useCustomFontSize": { @@ -1128,7 +1141,7 @@ export const SETTINGS: { [setting: string]: ISetting } = { labsGroup: LabGroup.Threads, controller: new ReloadOnChangeController(), displayName: _td("labs|threads_activity_centre"), - description: _td("labs|threads_activity_centre_description"), + description: () => _t("labs|threads_activity_centre_description", { brand: SdkConfig.get().brand }), default: false, isFeature: true, }, diff --git a/src/settings/controllers/FontSizeController.ts b/src/settings/controllers/FontSizeController.ts index 849a2822fed..83a2db198f2 100644 --- a/src/settings/controllers/FontSizeController.ts +++ b/src/settings/controllers/FontSizeController.ts @@ -16,7 +16,7 @@ limitations under the License. import SettingController from "./SettingController"; import dis from "../../dispatcher/dispatcher"; -import { UpdateFontSizePayload } from "../../dispatcher/payloads/UpdateFontSizePayload"; +import { UpdateFontSizeDeltaPayload } from "../../dispatcher/payloads/UpdateFontSizeDeltaPayload"; import { Action } from "../../dispatcher/actions"; import { SettingLevel } from "../SettingLevel"; @@ -34,9 +34,9 @@ export default class FontSizeController extends SettingController { dis.fire(Action.MigrateBaseFontSize); } else if (newValue !== "") { // Dispatch font size change so that everything open responds to the change. - dis.dispatch({ - action: Action.UpdateFontSize, - size: newValue, + dis.dispatch({ + action: Action.UpdateFontSizeDelta, + delta: newValue, }); } } diff --git a/src/settings/handlers/RoomSettingsHandler.ts b/src/settings/handlers/RoomSettingsHandler.ts index 12c31243658..652c323a9eb 100644 --- a/src/settings/handlers/RoomSettingsHandler.ts +++ b/src/settings/handlers/RoomSettingsHandler.ts @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixClient, MatrixEvent, RoomState, RoomStateEvent } from "matrix-js-sdk/src/matrix"; +import { MatrixClient, MatrixEvent, RoomState, RoomStateEvent, StateEvents } from "matrix-js-sdk/src/matrix"; import { defer } from "matrix-js-sdk/src/utils"; import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler"; @@ -24,6 +24,9 @@ import { SettingLevel } from "../SettingLevel"; import { WatchManager } from "../WatchManager"; const DEFAULT_SETTINGS_EVENT_TYPE = "im.vector.web.settings"; +const PREVIEW_URLS_EVENT_TYPE = "org.matrix.room.preview_urls"; + +type RoomSettingsEventType = typeof DEFAULT_SETTINGS_EVENT_TYPE | typeof PREVIEW_URLS_EVENT_TYPE; /** * Gets and sets settings at the "room" level. @@ -88,7 +91,12 @@ export default class RoomSettingsHandler extends MatrixClientBackedSettingsHandl } // helper function to send state event then await it being echoed back - private async sendStateEvent(roomId: string, eventType: string, field: string, value: any): Promise { + private async sendStateEvent( + roomId: string, + eventType: K, + field: F, + value: StateEvents[K][F], + ): Promise { const content = this.getSettings(roomId, eventType) || {}; content[field] = value; diff --git a/src/settings/watchers/FontWatcher.ts b/src/settings/watchers/FontWatcher.ts index bb538d1809f..df1a7ba4997 100644 --- a/src/settings/watchers/FontWatcher.ts +++ b/src/settings/watchers/FontWatcher.ts @@ -22,20 +22,19 @@ import { Action } from "../../dispatcher/actions"; import { SettingLevel } from "../SettingLevel"; import { UpdateSystemFontPayload } from "../../dispatcher/payloads/UpdateSystemFontPayload"; import { ActionPayload } from "../../dispatcher/payloads"; -import { clamp } from "../../utils/numbers"; export class FontWatcher implements IWatcher { /** - * Value indirectly defined by Compound. - * All `rem` calculations are made from a `16px` values in the - * @vector-im/compound-design-tokens package - * - * We might want to move to using `100%` instead so we can inherit the user - * preference set in the browser regarding font sizes. + * This Compound value is using `100%` of the default browser font size. + * It allows EW to use the browser's default font size instead of a fixed value. + * All the Compound font size are using `rem`, they are relative to the root font size + * and therefore of the browser font size. */ - public static readonly DEFAULT_SIZE = 16; - public static readonly MIN_SIZE = FontWatcher.DEFAULT_SIZE - 5; - public static readonly MAX_SIZE = FontWatcher.DEFAULT_SIZE + 5; + private static readonly DEFAULT_SIZE = "var(--cpd-font-size-root)"; + /** + * Default delta added to the ${@link DEFAULT_SIZE} + */ + public static readonly DEFAULT_DELTA = 0; private dispatcherRef: string | null; @@ -54,28 +53,106 @@ export class FontWatcher implements IWatcher { } /** - * Migrating the old `baseFontSize` for Compound. - * Everything will becomes slightly larger, and getting rid of the `SIZE_DIFF` - * weirdness for locally persisted values + * Migrate the base font size from the V1 and V2 version to the V3 version + * @private */ private async migrateBaseFontSize(): Promise { - const legacyBaseFontSize = SettingsStore.getValue("baseFontSize"); - if (legacyBaseFontSize) { - console.log("Migrating base font size for Compound, current value", legacyBaseFontSize); - - // For some odd reason, the persisted value in user storage has an offset - // of 5 pixels for all values stored under `baseFontSize` - const LEGACY_SIZE_DIFF = 5; - // Compound uses a base font size of `16px`, whereas the old Element - // styles based their calculations off a `15px` root font size. - const ROOT_FONT_SIZE_INCREASE = 1; - - const baseFontSize = legacyBaseFontSize + ROOT_FONT_SIZE_INCREASE + LEGACY_SIZE_DIFF; - - await SettingsStore.setValue("baseFontSizeV2", null, SettingLevel.DEVICE, baseFontSize); - await SettingsStore.setValue("baseFontSize", null, SettingLevel.DEVICE, ""); - console.log("Migration complete, deleting legacy `baseFontSize`"); - } + await this.migrateBaseFontV1toFontSizeDelta(); + await this.migrateBaseFontV2toFontSizeDelta(); + } + + /** + * Migrating from the V1 version of the base font size to the new delta system. + * The delta system is using the default browser font size as a base + * Everything will become slightly larger, and getting rid of the `SIZE_DIFF` + * weirdness for locally persisted values + * @private + */ + private async migrateBaseFontV1toFontSizeDelta(): Promise { + const legacyBaseFontSize = SettingsStore.getValue("baseFontSize"); + // No baseFontV1 found, nothing to migrate + if (!legacyBaseFontSize) return; + + console.log( + "Migrating base font size -> base font size V2 -> font size delta for Compound, current value", + legacyBaseFontSize, + ); + + // Compute the V1 to V2 version before migrating to fontSizeDelta + const baseFontSizeV2 = this.computeBaseFontSizeV1toV2(legacyBaseFontSize); + + // Compute the difference between the V2 and the fontSizeDelta + const delta = this.computeFontSizeDeltaFromV2BaseFontSize(baseFontSizeV2); + + await SettingsStore.setValue("fontSizeDelta", null, SettingLevel.DEVICE, delta); + await SettingsStore.setValue("baseFontSize", null, SettingLevel.DEVICE, 0); + console.log("Migration complete, deleting legacy `baseFontSize`"); + } + + /** + * Migrating from the V2 version of the base font size to the new delta system + * @private + */ + private async migrateBaseFontV2toFontSizeDelta(): Promise { + const legacyBaseFontV2Size = SettingsStore.getValue("baseFontSizeV2"); + // No baseFontV2 found, nothing to migrate + if (!legacyBaseFontV2Size) return; + + console.log("Migrating base font size V2 for Compound, current value", legacyBaseFontV2Size); + + // Compute the difference between the V2 and the fontSizeDelta + const delta = this.computeFontSizeDeltaFromV2BaseFontSize(legacyBaseFontV2Size); + + await SettingsStore.setValue("fontSizeDelta", null, SettingLevel.DEVICE, delta); + await SettingsStore.setValue("baseFontSizeV2", null, SettingLevel.DEVICE, 0); + console.log("Migration complete, deleting legacy `baseFontSizeV2`"); + } + + /** + * Compute the V2 font size from the V1 font size + * @param legacyBaseFontSize + * @private + */ + private computeBaseFontSizeV1toV2(legacyBaseFontSize: number): number { + // For some odd reason, the persisted value in user storage has an offset + // of 5 pixels for all values stored under `baseFontSize` + const LEGACY_SIZE_DIFF = 5; + + // Compound uses a base font size of `16px`, whereas the old Element + // styles based their calculations off a `15px` root font size. + const ROOT_FONT_SIZE_INCREASE = 1; + + // Compute the font size of the V2 version before migrating to V3 + return legacyBaseFontSize + ROOT_FONT_SIZE_INCREASE + LEGACY_SIZE_DIFF; + } + + /** + * Compute the difference between the V2 font size and the default browser font size + * @param legacyBaseFontV2Size + * @private + */ + private computeFontSizeDeltaFromV2BaseFontSize(legacyBaseFontV2Size: number): number { + const browserDefaultFontSize = FontWatcher.getRootFontSize(); + + // Compute the difference between the V2 font size and the default browser font size + return legacyBaseFontV2Size - browserDefaultFontSize; + } + + /** + * Get the root font size of the document + * Fallback to 16px if the value is not found + * @returns {number} + */ + public static getRootFontSize(): number { + return parseInt(window.getComputedStyle(document.documentElement).getPropertyValue("font-size"), 10) || 16; + } + + /** + * Get the browser default font size + * @returns {number} the default font size of the browser + */ + public static getBrowserDefaultFontSize(): number { + return this.getRootFontSize() - SettingsStore.getValue("fontSizeDelta"); } public stop(): void { @@ -84,7 +161,7 @@ export class FontWatcher implements IWatcher { } private updateFont(): void { - this.setRootFontSize(SettingsStore.getValue("baseFontSizeV2")); + this.setRootFontSize(SettingsStore.getValue("fontSizeDelta")); this.setSystemFont({ useBundledEmojiFont: SettingsStore.getValue("useBundledEmojiFont"), useSystemFont: SettingsStore.getValue("useSystemFont"), @@ -95,13 +172,13 @@ export class FontWatcher implements IWatcher { private onAction = (payload: ActionPayload): void => { if (payload.action === Action.MigrateBaseFontSize) { this.migrateBaseFontSize(); - } else if (payload.action === Action.UpdateFontSize) { - this.setRootFontSize(payload.size); + } else if (payload.action === Action.UpdateFontSizeDelta) { + this.setRootFontSize(payload.delta); } else if (payload.action === Action.UpdateSystemFont) { this.setSystemFont(payload as UpdateSystemFontPayload); } else if (payload.action === Action.OnLoggedOut) { // Clear font overrides when logging out - this.setRootFontSize(FontWatcher.DEFAULT_SIZE); + this.setRootFontSize(FontWatcher.DEFAULT_DELTA); this.setSystemFont({ useBundledEmojiFont: false, useSystemFont: false, @@ -113,13 +190,14 @@ export class FontWatcher implements IWatcher { } }; - private setRootFontSize = async (size: number): Promise => { - const fontSize = clamp(size, FontWatcher.MIN_SIZE, FontWatcher.MAX_SIZE); - - if (fontSize !== size) { - await SettingsStore.setValue("baseFontSizeV2", null, SettingLevel.DEVICE, fontSize); - } - document.querySelector(":root")!.style.fontSize = toPx(fontSize); + /** + * Set the root font size of the document + * @param delta {number} the delta to add to the default font size + */ + private setRootFontSize = async (delta: number): Promise => { + // Add the delta to the browser default font size + document.querySelector(":root")!.style.fontSize = + `calc(${FontWatcher.DEFAULT_SIZE} + ${toPx(delta)})`; }; public static readonly FONT_FAMILY_CUSTOM_PROPERTY = "--cpd-font-family-sans"; diff --git a/src/shouldHideEvent.ts b/src/shouldHideEvent.ts index dcfc5d920c0..0ee16009e12 100644 --- a/src/shouldHideEvent.ts +++ b/src/shouldHideEvent.ts @@ -15,6 +15,7 @@ */ import { MatrixEvent, EventType, RelationType } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import SettingsStore from "./settings/SettingsStore"; import { IRoomState } from "./components/structures/RoomView"; @@ -39,10 +40,11 @@ function memberEventDiff(ev: MatrixEvent): IDiff { const prevContent = ev.getPrevContent(); const isMembershipChanged = content.membership !== prevContent.membership; - diff.isJoin = isMembershipChanged && content.membership === "join"; - diff.isPart = isMembershipChanged && content.membership === "leave" && ev.getStateKey() === ev.getSender(); + diff.isJoin = isMembershipChanged && content.membership === KnownMembership.Join; + diff.isPart = + isMembershipChanged && content.membership === KnownMembership.Leave && ev.getStateKey() === ev.getSender(); - const isJoinToJoin = !isMembershipChanged && content.membership === "join"; + const isJoinToJoin = !isMembershipChanged && content.membership === KnownMembership.Join; diff.isDisplaynameChange = isJoinToJoin && content.displayname !== prevContent.displayname; diff.isAvatarChange = isJoinToJoin && content.avatar_url !== prevContent.avatar_url; return diff; diff --git a/src/slash-commands/interface.ts b/src/slash-commands/interface.ts index 94e95126c04..8efc2b3ce9b 100644 --- a/src/slash-commands/interface.ts +++ b/src/slash-commands/interface.ts @@ -17,7 +17,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { IContent } from "matrix-js-sdk/src/matrix"; +import { RoomMessageEventContent } from "matrix-js-sdk/src/types"; import { _td } from "../languageHandler"; import { XOR } from "../@types/common"; @@ -31,4 +31,4 @@ export const CommandCategories = { other: _td("slash_command|category_other"), }; -export type RunResult = XOR<{ error: Error }, { promise: Promise }>; +export type RunResult = XOR<{ error: Error }, { promise: Promise }>; diff --git a/src/stores/AutoRageshakeStore.ts b/src/stores/AutoRageshakeStore.ts index 0a2c1a6d7d9..7ac00452087 100644 --- a/src/stores/AutoRageshakeStore.ts +++ b/src/stores/AutoRageshakeStore.ts @@ -14,8 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ClientEvent, MatrixEvent, MatrixEventEvent, SyncStateData, SyncState } from "matrix-js-sdk/src/matrix"; +import { + ClientEvent, + MatrixEvent, + MatrixEventEvent, + SyncStateData, + SyncState, + ToDeviceMessageId, +} from "matrix-js-sdk/src/matrix"; import { sleep } from "matrix-js-sdk/src/utils"; +import { v4 as uuidv4 } from "uuid"; +import { logger } from "matrix-js-sdk/src/logger"; import SdkConfig from "../SdkConfig"; import sendBugReport from "../rageshake/submit-rageshake"; @@ -108,20 +117,28 @@ export default class AutoRageshakeStore extends AsyncStoreWithClient { const now = new Date().getTime(); if (now - this.state.lastRageshakeTime < RAGESHAKE_INTERVAL) { + logger.info( + `Not sending recipient-side autorageshake for event ${ev.getId()}/session ${sessionId}: last rageshake was too recent`, + ); return; } await this.updateState({ lastRageshakeTime: now }); + const senderUserId = ev.getSender()!; const eventInfo = { event_id: ev.getId(), room_id: ev.getRoomId(), session_id: sessionId, device_id: wireContent.device_id, - user_id: ev.getSender()!, + user_id: senderUserId, sender_key: wireContent.sender_key, }; + logger.info(`Sending recipient-side autorageshake for event ${ev.getId()}/session ${sessionId}`); + // XXX: the rageshake server returns the URL for the github issue... which is typically absent for + // auto-uisis, because we've disabled creation of GH issues for them. So the `recipient_rageshake` + // field is broken. const rageshakeURL = await sendBugReport(SdkConfig.get().bug_report_endpoint_url, { userText: "Auto-reporting decryption error (recipient)", sendLogs: true, @@ -133,10 +150,11 @@ export default class AutoRageshakeStore extends AsyncStoreWithClient { const messageContent = { ...eventInfo, recipient_rageshake: rageshakeURL, + [ToDeviceMessageId]: uuidv4(), }; this.matrixClient?.sendToDevice( AUTO_RS_REQUEST, - new Map([["messageContent.user_id", new Map([[messageContent.device_id, messageContent]])]]), + new Map([[senderUserId, new Map([[messageContent.device_id, messageContent]])]]), ); } } @@ -158,6 +176,9 @@ export default class AutoRageshakeStore extends AsyncStoreWithClient { const now = new Date().getTime(); if (now - this.state.lastRageshakeTime > RAGESHAKE_INTERVAL) { await this.updateState({ lastRageshakeTime: now }); + logger.info( + `Sending sender-side autorageshake for event ${messageContent["event_id"]}/session ${messageContent["session_id"]}`, + ); await sendBugReport(SdkConfig.get().bug_report_endpoint_url, { userText: `Auto-reporting decryption error (sender)\nRecipient rageshake: ${recipientRageshake}`, sendLogs: true, @@ -168,6 +189,10 @@ export default class AutoRageshakeStore extends AsyncStoreWithClient { auto_uisi: JSON.stringify(messageContent), }, }); + } else { + logger.info( + `Not sending sender-side autorageshake for event ${messageContent["event_id"]}/session ${messageContent["session_id"]}: last rageshake was too recent`, + ); } } diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts index bacbfe97beb..36bb0a78b63 100644 --- a/src/stores/BreadcrumbsStore.ts +++ b/src/stores/BreadcrumbsStore.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { Room, RoomEvent, ClientEvent } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import SettingsStore from "../settings/SettingsStore"; @@ -91,7 +92,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { // The tests might not result in a valid room object. const room = this.matrixClient.getRoom(payload.room_id); const membership = room?.getMyMembership(); - if (room && membership === "join") await this.appendRoom(room); + if (room && membership === KnownMembership.Join) await this.appendRoom(room); } } else if (payload.action === Action.JoinRoom) { const room = this.matrixClient.getRoom(payload.roomId); diff --git a/src/stores/LifecycleStore.ts b/src/stores/LifecycleStore.ts index e4da9645534..decef118818 100644 --- a/src/stores/LifecycleStore.ts +++ b/src/stores/LifecycleStore.ts @@ -14,11 +14,20 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { SyncState } from "matrix-js-sdk/src/matrix"; +import { MINIMUM_MATRIX_VERSION, SUPPORTED_MATRIX_VERSIONS } from "matrix-js-sdk/src/version-support"; +import { logger } from "matrix-js-sdk/src/logger"; + import { Action } from "../dispatcher/actions"; import dis from "../dispatcher/dispatcher"; import { ActionPayload } from "../dispatcher/payloads"; import { DoAfterSyncPreparedPayload } from "../dispatcher/payloads/DoAfterSyncPreparedPayload"; import { AsyncStore } from "./AsyncStore"; +import { MatrixClientPeg } from "../MatrixClientPeg"; +import ToastStore from "./ToastStore"; +import { _t } from "../languageHandler"; +import SdkConfig from "../SdkConfig"; +import GenericToast from "../components/views/toasts/GenericToast"; interface IState { deferredAction: ActionPayload | null; @@ -51,6 +60,12 @@ class LifecycleStore extends AsyncStore { }); break; case "MatrixActions.sync": { + if (payload.state === SyncState.Syncing && payload.prevState !== SyncState.Syncing) { + // We've reconnected to the server: update server version support + // This is async but we don't care about the result, so just fire & forget. + checkServerVersions(); + } + if (payload.state !== "PREPARED") { break; } @@ -70,6 +85,48 @@ class LifecycleStore extends AsyncStore { } } +async function checkServerVersions(): Promise { + try { + const client = MatrixClientPeg.get(); + if (!client) return; + for (const version of SUPPORTED_MATRIX_VERSIONS) { + // Check if the server supports this spec version. (`isVersionSupported` caches the response, so this loop will + // only make a single HTTP request). + // Note that although we do this on a reconnect, we cache the server's versions in memory + // indefinitely, so it will only ever trigger the toast on the first connection after a fresh + // restart of the client. + if (await client.isVersionSupported(version)) { + // we found a compatible spec version + return; + } + } + + // This is retrospective doc having debated about the exactly what this toast is for, but + // our guess is that it's a nudge to update, or ask your HS admin to update your Homeserver + // after a new version of Element has come out, in a way that doesn't lock you out of all + // your messages. + const toastKey = "LEGACY_SERVER"; + ToastStore.sharedInstance().addOrReplaceToast({ + key: toastKey, + title: _t("unsupported_server_title"), + props: { + description: _t("unsupported_server_description", { + version: MINIMUM_MATRIX_VERSION, + brand: SdkConfig.get().brand, + }), + acceptLabel: _t("action|ok"), + onAccept: () => { + ToastStore.sharedInstance().dismissToast(toastKey); + }, + }, + component: GenericToast, + priority: 98, + }); + } catch (e) { + logger.warn("Failed to check server versions", e); + } +} + let singletonLifecycleStore: LifecycleStore | null = null; if (!singletonLifecycleStore) { singletonLifecycleStore = new LifecycleStore(); diff --git a/src/stores/MemberListStore.ts b/src/stores/MemberListStore.ts index d7c50e00787..f9e1df79a41 100644 --- a/src/stores/MemberListStore.ts +++ b/src/stores/MemberListStore.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { Room, RoomMember } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import SettingsStore from "../settings/SettingsStore"; import { SdkContextClass } from "../contexts/SDKContext"; @@ -90,7 +91,7 @@ export class MemberListStore { // pull straight from the server. Don't use a since token as we don't have earlier deltas // accumulated. room.currentState.markOutOfBandMembersStarted(); - const response = await this.stores.client!.members(roomId, undefined, "leave"); + const response = await this.stores.client!.members(roomId, undefined, KnownMembership.Leave); const memberEvents = response.chunk.map(this.stores.client!.getEventMapper()); room.currentState.setOutOfBandMembers(memberEvents); } else { @@ -167,7 +168,7 @@ export class MemberListStore { invited: [], }; members.forEach((m) => { - if (m.membership !== "join" && m.membership !== "invite") { + if (m.membership !== KnownMembership.Join && m.membership !== KnownMembership.Invite) { return; // bail early for left/banned users } if (query) { @@ -179,10 +180,10 @@ export class MemberListStore { } } switch (m.membership) { - case "join": + case KnownMembership.Join: result.joined.push(m); break; - case "invite": + case KnownMembership.Invite: result.invited.push(m); break; } diff --git a/src/stores/OwnBeaconStore.ts b/src/stores/OwnBeaconStore.ts index e2b4a0c07bd..55399ef264b 100644 --- a/src/stores/OwnBeaconStore.ts +++ b/src/stores/OwnBeaconStore.ts @@ -28,6 +28,7 @@ import { MBeaconInfoEventContent, M_BEACON, } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { logger } from "matrix-js-sdk/src/logger"; import defaultDispatcher from "../dispatcher/dispatcher"; @@ -313,7 +314,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient { // in PSF-797 // stop watching beacons in rooms where user is no longer a member - if (member.membership === "leave" || member.membership === "ban") { + if (member.membership === KnownMembership.Leave || member.membership === KnownMembership.Ban) { this.beaconsByRoomId.get(roomState.roomId)?.forEach(this.removeBeacon); this.beaconsByRoomId.delete(roomState.roomId); } diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx index 83c91fdab79..dfe910d57f1 100644 --- a/src/stores/RoomViewStore.tsx +++ b/src/stores/RoomViewStore.tsx @@ -19,6 +19,7 @@ limitations under the License. import React, { ReactNode } from "react"; import * as utils from "matrix-js-sdk/src/utils"; import { MatrixError, JoinRule, Room, MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { logger } from "matrix-js-sdk/src/logger"; import { ViewRoom as ViewRoomEvent } from "@matrix-org/analytics-events/types/typescript/ViewRoom"; import { JoinedRoom as JoinedRoomEvent } from "@matrix-org/analytics-events/types/typescript/JoinedRoom"; @@ -62,6 +63,7 @@ import { ActionPayload } from "../dispatcher/payloads"; import { CancelAskToJoinPayload } from "../dispatcher/payloads/CancelAskToJoinPayload"; import { SubmitAskToJoinPayload } from "../dispatcher/payloads/SubmitAskToJoinPayload"; import { ModuleRunner } from "../modules/ModuleRunner"; +import { setMarkedUnreadState } from "../utils/notifications"; const NUM_JOIN_RETRY = 5; @@ -382,6 +384,10 @@ export class RoomViewStore extends EventEmitter { this.cancelAskToJoin(payload as CancelAskToJoinPayload); break; } + case Action.RoomLoaded: { + this.setViewRoomOpts(); + break; + } } } @@ -446,10 +452,6 @@ export class RoomViewStore extends EventEmitter { return; } - const viewRoomOpts: ViewRoomOpts = { buttons: [] }; - // Allow modules to update the list of buttons for the room by updating `viewRoomOpts`. - ModuleRunner.instance.invoke(RoomViewLifecycle.ViewRoom, viewRoomOpts, this.getRoomId()); - const newState: Partial = { roomId: payload.room_id, roomAlias: payload.room_alias ?? null, @@ -472,7 +474,6 @@ export class RoomViewStore extends EventEmitter { (payload.room_id === this.state.roomId ? this.state.viewingCall : CallStore.instance.getActiveCall(payload.room_id) !== null), - viewRoomOpts, }; // Allow being given an event to be replied to when switching rooms but sanity check its for this room @@ -498,6 +499,8 @@ export class RoomViewStore extends EventEmitter { if (room) { pauseNonLiveBroadcastFromOtherRoom(room, this.stores.voiceBroadcastPlaybacksStore); this.doMaybeSetCurrentVoiceBroadcastPlayback(room); + + await setMarkedUnreadState(room, MatrixClientPeg.safeGet(), false); } } else if (payload.room_alias) { // Try the room alias to room ID navigation cache first to avoid @@ -608,7 +611,7 @@ export class RoomViewStore extends EventEmitter { private getInvitingUserId(roomId: string): string | undefined { const cli = MatrixClientPeg.safeGet(); const room = cli.getRoom(roomId); - if (room?.getMyMembership() === "invite") { + if (room?.getMyMembership() === KnownMembership.Invite) { const myMember = room.getMember(cli.getSafeUserId()); const inviteEvent = myMember ? myMember.events.member : null; return inviteEvent?.getSender(); @@ -837,4 +840,15 @@ export class RoomViewStore extends EventEmitter { public getViewRoomOpts(): ViewRoomOpts { return this.state.viewRoomOpts; } + + /** + * Invokes the view room lifecycle to set the view room options. + * + * @returns {void} + */ + private setViewRoomOpts(): void { + const viewRoomOpts: ViewRoomOpts = { buttons: [] }; + ModuleRunner.instance.invoke(RoomViewLifecycle.ViewRoom, viewRoomOpts, this.getRoomId()); + this.setState({ viewRoomOpts }); + } } diff --git a/src/stores/SetupEncryptionStore.ts b/src/stores/SetupEncryptionStore.ts index 72e463a9c76..79621322be2 100644 --- a/src/stores/SetupEncryptionStore.ts +++ b/src/stores/SetupEncryptionStore.ts @@ -17,10 +17,9 @@ limitations under the License. import EventEmitter from "events"; import { VerificationPhase, VerificationRequest, VerificationRequestEvent } from "matrix-js-sdk/src/crypto-api"; import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup"; -import { ISecretStorageKeyInfo } from "matrix-js-sdk/src/crypto/api"; import { logger } from "matrix-js-sdk/src/logger"; import { CryptoEvent } from "matrix-js-sdk/src/crypto"; -import { Device } from "matrix-js-sdk/src/matrix"; +import { Device, SecretStorage } from "matrix-js-sdk/src/matrix"; import { MatrixClientPeg } from "../MatrixClientPeg"; import { AccessCancelledError, accessSecretStorage } from "../SecurityManager"; @@ -48,7 +47,7 @@ export class SetupEncryptionStore extends EventEmitter { // ID of the key that the secrets we want are encrypted with public keyId: string | null = null; // Descriptor of the key that the secrets we want are encrypted with - public keyInfo: ISecretStorageKeyInfo | null = null; + public keyInfo: SecretStorage.SecretStorageKeyDescription | null = null; public hasDevicesToVerifyAgainst?: boolean; public static sharedInstance(): SetupEncryptionStore { diff --git a/src/stores/notifications/RoomNotificationState.ts b/src/stores/notifications/RoomNotificationState.ts index 0503485584d..80ffbd03f44 100644 --- a/src/stores/notifications/RoomNotificationState.ts +++ b/src/stores/notifications/RoomNotificationState.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { MatrixEventEvent, RoomEvent, ClientEvent } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import type { Room, MatrixEvent } from "matrix-js-sdk/src/matrix"; import type { IDestroyable } from "../../utils/IDestroyable"; @@ -23,6 +24,7 @@ import { readReceiptChangeIsFor } from "../../utils/read-receipts"; import * as RoomNotifs from "../../RoomNotifs"; import { NotificationState } from "./NotificationState"; import SettingsStore from "../../settings/SettingsStore"; +import { MARKED_UNREAD_TYPE_STABLE, MARKED_UNREAD_TYPE_UNSTABLE } from "../../utils/notifications"; export class RoomNotificationState extends NotificationState implements IDestroyable { public constructor( @@ -36,6 +38,7 @@ export class RoomNotificationState extends NotificationState implements IDestroy this.room.on(RoomEvent.LocalEchoUpdated, this.handleLocalEchoUpdated); this.room.on(RoomEvent.Timeline, this.handleRoomEventUpdate); this.room.on(RoomEvent.Redaction, this.handleRoomEventUpdate); + this.room.on(RoomEvent.AccountData, this.handleRoomAccountDataUpdate); this.room.on(RoomEvent.UnreadNotifications, this.handleNotificationCountUpdate); // for server-sent counts cli.on(MatrixEventEvent.Decrypted, this.onEventDecrypted); @@ -51,6 +54,7 @@ export class RoomNotificationState extends NotificationState implements IDestroy this.room.removeListener(RoomEvent.LocalEchoUpdated, this.handleLocalEchoUpdated); this.room.removeListener(RoomEvent.Timeline, this.handleRoomEventUpdate); this.room.removeListener(RoomEvent.Redaction, this.handleRoomEventUpdate); + this.room.removeListener(RoomEvent.AccountData, this.handleRoomAccountDataUpdate); cli.removeListener(MatrixEventEvent.Decrypted, this.onEventDecrypted); cli.removeListener(ClientEvent.AccountData, this.handleAccountDataUpdate); } @@ -90,13 +94,20 @@ export class RoomNotificationState extends NotificationState implements IDestroy } }; + private handleRoomAccountDataUpdate = (ev: MatrixEvent): void => { + if ([MARKED_UNREAD_TYPE_STABLE, MARKED_UNREAD_TYPE_UNSTABLE].includes(ev.getType())) { + this.updateNotificationState(); + } + }; + private updateNotificationState(): void { const snapshot = this.snapshot(); const { level, symbol, count } = RoomNotifs.determineUnreadState(this.room, undefined, this.includeThreads); const muted = RoomNotifs.getRoomNotifsState(this.room.client, this.room.roomId) === RoomNotifs.RoomNotifState.Mute; - const knocked = SettingsStore.getValue("feature_ask_to_join") && this.room.getMyMembership() === "knock"; + const knocked = + SettingsStore.getValue("feature_ask_to_join") && this.room.getMyMembership() === KnownMembership.Knock; this._level = level; this._symbol = symbol; this._count = count; diff --git a/src/stores/oidc/OidcClientStore.ts b/src/stores/oidc/OidcClientStore.ts index aafbbc6276d..57fe1adcd1f 100644 --- a/src/stores/oidc/OidcClientStore.ts +++ b/src/stores/oidc/OidcClientStore.ts @@ -14,32 +14,54 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { IDelegatedAuthConfig, MatrixClient, M_AUTHENTICATION } from "matrix-js-sdk/src/matrix"; -import { discoverAndValidateAuthenticationConfig } from "matrix-js-sdk/src/oidc/discovery"; +import { MatrixClient, discoverAndValidateOIDCIssuerWellKnown } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { OidcClient } from "oidc-client-ts"; import { getStoredOidcTokenIssuer, getStoredOidcClientId } from "../../utils/oidc/persistOidcSettings"; -import { getDelegatedAuthAccountUrl } from "../../utils/oidc/getDelegatedAuthAccountUrl"; +import PlatformPeg from "../../PlatformPeg"; /** * @experimental * Stores information about configured OIDC provider + * + * In OIDC Native mode the client is registered with OIDC directly and maintains an OIDC token. + * + * In OIDC Aware mode, the client is aware that the Server is using OIDC, but is using the standard Matrix APIs for most things. + * (Notable exceptions are account management, where a link to the account management endpoint will be provided instead.) + * + * Otherwise, the store is not operating. Auth is then in Legacy mode and everything uses normal Matrix APIs. */ export class OidcClientStore { private oidcClient?: OidcClient; private initialisingOidcClientPromise: Promise | undefined; - private authenticatedIssuer?: string; + private authenticatedIssuer?: string; // set only in OIDC-native mode private _accountManagementEndpoint?: string; + /** + * Promise which resolves once this store is read to use, which may mean there is no OIDC client if we're in legacy mode, + * or we just have the account management endpoint if running in OIDC-aware mode. + */ + public readonly readyPromise: Promise; public constructor(private readonly matrixClient: MatrixClient) { + this.readyPromise = this.init(); + } + + private async init(): Promise { this.authenticatedIssuer = getStoredOidcTokenIssuer(); if (this.authenticatedIssuer) { - this.getOidcClient(); + await this.getOidcClient(); } else { - matrixClient.waitForClientWellKnown().then((wellKnown) => { - this._accountManagementEndpoint = getDelegatedAuthAccountUrl(wellKnown); - }); + // We are not in OIDC Native mode, as we have no locally stored issuer. Check if the server delegates auth to OIDC. + try { + const authIssuer = await this.matrixClient.getAuthIssuer(); + const { accountManagementEndpoint, metadata } = await discoverAndValidateOIDCIssuerWellKnown( + authIssuer.issuer, + ); + this._accountManagementEndpoint = accountManagementEndpoint ?? metadata.issuer; + } catch (e) { + console.log("Auth issuer not found", e); + } } } @@ -118,28 +140,23 @@ export class OidcClientStore { * @returns promise that resolves when initialising OidcClient succeeds or fails */ private async initOidcClient(): Promise { - const wellKnown = await this.matrixClient.waitForClientWellKnown(); - if (!wellKnown && !this.authenticatedIssuer) { + if (!this.authenticatedIssuer) { logger.error("Cannot initialise OIDC client without issuer."); return; } - const delegatedAuthConfig = - (wellKnown && M_AUTHENTICATION.findIn(wellKnown)) ?? undefined; try { const clientId = getStoredOidcClientId(); - const { account, metadata, signingKeys } = await discoverAndValidateAuthenticationConfig( - // if HS has valid delegated auth config in .well-known, use it - // otherwise fallback to the known issuer - delegatedAuthConfig ?? { issuer: this.authenticatedIssuer! }, + const { accountManagementEndpoint, metadata, signingKeys } = await discoverAndValidateOIDCIssuerWellKnown( + this.authenticatedIssuer, ); // if no account endpoint is configured default to the issuer - this._accountManagementEndpoint = account ?? metadata.issuer; + this._accountManagementEndpoint = accountManagementEndpoint ?? metadata.issuer; this.oidcClient = new OidcClient({ ...metadata, authority: metadata.issuer, signingKeys, - redirect_uri: window.location.origin, + redirect_uri: PlatformPeg.get()!.getSSOCallbackUrl().href, client_id: clientId, }); } catch (error) { diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index b6e65fada75..16c4d3be5bc 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { MatrixClient, Room, RoomState, EventType } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { logger } from "matrix-js-sdk/src/logger"; import SettingsStore from "../../settings/SettingsStore"; @@ -233,7 +234,12 @@ export class RoomListStoreClass extends AsyncStoreWithClient implements return; } } - await this.handleRoomUpdate(updatedRoom, RoomUpdateCause.Timeline); + // If the join rule changes we need to update the tags for the room. + // A conference tag is determined by the room public join rule. + if (eventPayload.event.getType() === EventType.RoomJoinRules) + await this.handleRoomUpdate(updatedRoom, RoomUpdateCause.PossibleTagChange); + else await this.handleRoomUpdate(updatedRoom, RoomUpdateCause.Timeline); + this.updateFn.trigger(); }; if (!room) { @@ -350,7 +356,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient implements } private async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise { - if (cause === RoomUpdateCause.NewRoom && room.getMyMembership() === "invite") { + if (cause === RoomUpdateCause.NewRoom && room.getMyMembership() === KnownMembership.Invite) { // Let the visibility provider know that there is a new invited room. It would be nice // if this could just be an event that things listen for but the point of this is that // we delay doing anything about this room until the VoipUserMapper had had a chance diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index cc81d362497..267b9bd7420 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -14,7 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Room } from "matrix-js-sdk/src/matrix"; +import { JoinRule, Room } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import { EventEmitter } from "events"; import { logger } from "matrix-js-sdk/src/logger"; @@ -172,7 +173,7 @@ export class Algorithm extends EventEmitter { } private doUpdateStickyRoom(val: Room | null): void { - if (val?.isSpaceRoom() && val.getMyMembership() !== "invite") { + if (val?.isSpaceRoom() && val.getMyMembership() !== KnownMembership.Invite) { // no-op sticky rooms for spaces - they're effectively virtual rooms val = null; } @@ -498,6 +499,8 @@ export class Algorithm extends EventEmitter { newTags[DefaultTagID.Invite].push(room); } for (const room of memberships[EffectiveMembership.Leave]) { + // We may not have had an archived section previously, so make sure its there. + if (newTags[DefaultTagID.Archived] === undefined) newTags[DefaultTagID.Archived] = []; newTags[DefaultTagID.Archived].push(room); } @@ -574,6 +577,9 @@ export class Algorithm extends EventEmitter { tags = [DefaultTagID.DM]; } } + if (room.isCallRoom() && (room.getJoinRule() === JoinRule.Public || room.getJoinRule() === JoinRule.Knock)) { + tags.push(DefaultTagID.Conference); + } return tags; } diff --git a/src/stores/room-list/models.ts b/src/stores/room-list/models.ts index 1c19f824946..d8b04888423 100644 --- a/src/stores/room-list/models.ts +++ b/src/stores/room-list/models.ts @@ -21,6 +21,7 @@ export enum DefaultTagID { LowPriority = "m.lowpriority", Favourite = "m.favourite", DM = "im.vector.fake.direct", + Conference = "im.vector.fake.conferences", ServerNotice = "m.server_notice", Suggested = "im.vector.fake.suggested", } @@ -29,6 +30,7 @@ export const OrderedDefaultTagIDs = [ DefaultTagID.Invite, DefaultTagID.Favourite, DefaultTagID.DM, + DefaultTagID.Conference, DefaultTagID.Untagged, DefaultTagID.LowPriority, DefaultTagID.ServerNotice, diff --git a/src/stores/spaces/SpaceStore.ts b/src/stores/spaces/SpaceStore.ts index 0075e992314..966b564d687 100644 --- a/src/stores/spaces/SpaceStore.ts +++ b/src/stores/spaces/SpaceStore.ts @@ -26,6 +26,7 @@ import { ClientEvent, ISendEventResponse, } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { logger } from "matrix-js-sdk/src/logger"; import { AsyncStoreWithClient } from "../AsyncStoreWithClient"; @@ -74,7 +75,13 @@ interface IState {} const ACTIVE_SPACE_LS_KEY = "mx_active_space"; -const metaSpaceOrder: MetaSpace[] = [MetaSpace.Home, MetaSpace.Favourites, MetaSpace.People, MetaSpace.Orphans]; +const metaSpaceOrder: MetaSpace[] = [ + MetaSpace.Home, + MetaSpace.Favourites, + MetaSpace.People, + MetaSpace.Orphans, + MetaSpace.VideoRooms, +]; const MAX_SUGGESTED_ROOMS = 20; @@ -255,8 +262,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // else view space home or home depending on what is being clicked on if ( roomId && - cliSpace?.getMyMembership() !== "invite" && - this.matrixClient.getRoom(roomId)?.getMyMembership() === "join" && + cliSpace?.getMyMembership() !== KnownMembership.Invite && + this.matrixClient.getRoom(roomId)?.getMyMembership() === KnownMembership.Join && this.isRoomInSpace(space, roomId) ) { defaultDispatcher.dispatch({ @@ -325,7 +332,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { .filter((roomInfo) => { return ( roomInfo.room_type !== RoomType.Space && - this.matrixClient?.getRoom(roomInfo.room_id)?.getMyMembership() !== "join" + this.matrixClient?.getRoom(roomInfo.room_id)?.getMyMembership() !== KnownMembership.Join ); }) .map((roomInfo) => ({ @@ -368,7 +375,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient { return history[history.length - 1]; }) .filter((room) => { - return room?.getMyMembership() === "join" || room?.getMyMembership() === "invite"; + return ( + room?.getMyMembership() === KnownMembership.Join || + room?.getMyMembership() === KnownMembership.Invite + ); }) || [] ); } @@ -379,7 +389,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { public getChildSpaces(spaceId: string): Room[] { // don't show invited subspaces as they surface at the top level for better visibility - return this.getChildren(spaceId).filter((r) => r.isSpaceRoom() && r.getMyMembership() === "join"); + return this.getChildren(spaceId).filter((r) => r.isSpaceRoom() && r.getMyMembership() === KnownMembership.Join); } public getParents(roomId: string, canonicalOnly = false): Room[] { @@ -428,7 +438,9 @@ export class SpaceStoreClass extends AsyncStoreWithClient { if (space === MetaSpace.Home && this.allRoomsInHome) { return true; } - + if (space === MetaSpace.VideoRooms) { + return !!this.matrixClient?.getRoom(roomId)?.isCallRoom(); + } if (this.getSpaceFilteredRoomIds(space, includeDescendantSpaces)?.has(roomId)) { return true; } @@ -593,7 +605,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { private rebuildParentMap = (): void => { if (!this.matrixClient) return; const joinedSpaces = this.matrixClient.getVisibleRooms(this._msc3946ProcessDynamicPredecessor).filter((r) => { - return r.isSpaceRoom() && r.getMyMembership() === "join"; + return r.isSpaceRoom() && r.getMyMembership() === KnownMembership.Join; }); this.parentMap = new EnhancedMap>(); @@ -717,12 +729,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient { return ( !this.parentMap.get(room.roomId)?.size || // put all orphaned rooms in the Home Space !!DMRoomMap.shared().getUserIdForRoomId(room.roomId) || // put all DMs in the Home Space - room.getMyMembership() === "invite" + room.getMyMembership() === KnownMembership.Invite ); // put all invites in the Home Space }; private static isInSpace(member?: RoomMember | null): boolean { - return member?.membership === "join" || member?.membership === "invite"; + return member?.membership === KnownMembership.Join || member?.membership === KnownMembership.Invite; } // Method for resolving the impact of a single user's membership change in the given Space and its hierarchy @@ -766,7 +778,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const hiddenChildren = new EnhancedMap>(); visibleRooms.forEach((room) => { - if (!["join", "invite"].includes(room.getMyMembership())) return; + if (!([KnownMembership.Join, KnownMembership.Invite] as Array).includes(room.getMyMembership())) + return; this.getParents(room.roomId).forEach((parent) => { hiddenChildren.getOrCreate(parent.roomId, new Set()).add(room.roomId); }); @@ -796,7 +809,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { space ?.getMembers() .filter((m) => { - return m.membership === "join" || m.membership === "invite"; + return m.membership === KnownMembership.Join || m.membership === KnownMembership.Invite; }) .map((m) => m.userId), ); @@ -924,7 +937,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { if (!room.isSpaceRoom()) { this.onRoomsUpdate(); - if (membership === "join") { + if (membership === KnownMembership.Join) { // the user just joined a room, remove it from the suggested list if it was there const numSuggestedRooms = this._suggestedRooms.length; this._suggestedRooms = this._suggestedRooms.filter((r) => r.room_id !== room.roomId); @@ -935,7 +948,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } // if the room currently being viewed was just joined then switch to its related space - if (newMembership === "join" && room.roomId === SdkContextClass.instance.roomViewStore.getRoomId()) { + if ( + newMembership === KnownMembership.Join && + room.roomId === SdkContextClass.instance.roomViewStore.getRoomId() + ) { this.switchSpaceIfNeeded(room.roomId); } } @@ -943,13 +959,13 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } // Space - if (membership === "invite") { + if (membership === KnownMembership.Invite) { const len = this._invitedSpaces.size; this._invitedSpaces.add(room); if (len !== this._invitedSpaces.size) { this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces); } - } else if (oldMembership === "invite" && membership !== "join") { + } else if (oldMembership === KnownMembership.Invite && membership !== KnownMembership.Join) { if (this._invitedSpaces.delete(room)) { this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces); } @@ -962,10 +978,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.emit(room.roomId); } - if (membership === "join" && room.roomId === SdkContextClass.instance.roomViewStore.getRoomId()) { + if (membership === KnownMembership.Join && room.roomId === SdkContextClass.instance.roomViewStore.getRoomId()) { // if the user was looking at the space and then joined: select that space this.setActiveSpace(room.roomId, false); - } else if (membership === "leave" && room.roomId === this.activeSpace) { + } else if (membership === KnownMembership.Leave && room.roomId === this.activeSpace) { // user's active space has gone away, go back to home this.goToFirstSpace(true); } @@ -1000,7 +1016,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { if ( room.roomId === this.activeSpace && // current space - target?.getMyMembership() !== "join" && // target not joined + target?.getMyMembership() !== KnownMembership.Join && // target not joined ev.getPrevContent().suggested !== ev.getContent().suggested // suggested flag changed ) { this.loadSuggestedRooms(room); @@ -1025,6 +1041,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.onRoomsUpdate(); } break; + case EventType.RoomCreate: + // The room might become a video room. We need to tag it for that videoRooms space. + this.onRoomsUpdate(); + break; } }; diff --git a/src/stores/spaces/index.ts b/src/stores/spaces/index.ts index 30f6798bb6a..10963fbf02d 100644 --- a/src/stores/spaces/index.ts +++ b/src/stores/spaces/index.ts @@ -32,6 +32,7 @@ export enum MetaSpace { Favourites = "favourites-space", People = "people-space", Orphans = "orphans-space", + VideoRooms = "video-rooms-space", } export const getMetaSpaceName = (spaceKey: MetaSpace, allRoomsInHome = false): string => { @@ -44,6 +45,8 @@ export const getMetaSpaceName = (spaceKey: MetaSpace, allRoomsInHome = false): s return _t("common|people"); case MetaSpace.Orphans: return _t("common|orphan_rooms"); + case MetaSpace.VideoRooms: + return _t("voip|metaspace_video_rooms|conference_room_section"); } }; @@ -58,6 +61,7 @@ export function isMetaSpace(spaceKey?: SpaceKey): boolean { spaceKey === MetaSpace.Home || spaceKey === MetaSpace.Favourites || spaceKey === MetaSpace.People || - spaceKey === MetaSpace.Orphans + spaceKey === MetaSpace.Orphans || + spaceKey === MetaSpace.VideoRooms ); } diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index c131a7e7666..36ade5d5947 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -15,6 +15,7 @@ */ import { Room, MatrixEvent, MatrixEventEvent, MatrixClient, ClientEvent } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { ClientWidgetApi, IModalWidgetOpenRequest, @@ -495,6 +496,11 @@ export class StopGapWidget extends EventEmitter { // // This approach of "read up to" prevents widgets receiving decryption spam from startup or // receiving out-of-order events from backfill and such. + // + // Skip marker timeline check for events with relations to unknown parent because these + // events are not added to the timeline here and will be ignored otherwise: + // https://github.com/matrix-org/matrix-js-sdk/blob/d3dfcd924201d71b434af3d77343b5229b6ed75e/src/models/room.ts#L2207-L2213 + let isRelationToUnknown: boolean | undefined = undefined; const upToEventId = this.readUpToMap[ev.getRoomId()!]; if (upToEventId) { // Small optimization for exact match (prevent search) @@ -502,7 +508,8 @@ export class StopGapWidget extends EventEmitter { return; } - let isBeforeMark = true; + // should be true to forward the event to the widget + let shouldForward = false; const room = this.client.getRoom(ev.getRoomId()!); if (!room) return; @@ -515,14 +522,19 @@ export class StopGapWidget extends EventEmitter { if (timelineEvent.getId() === upToEventId) { break; } else if (timelineEvent.getId() === ev.getId()) { - isBeforeMark = false; + shouldForward = true; break; } } - if (isBeforeMark) { - // Ignore the event: it is before our interest. - return; + if (!shouldForward) { + // checks that the event has a relation to unknown event + isRelationToUnknown = + !ev.replyEventId && !!ev.relationEventId && !room.findEventById(ev.relationEventId); + if (!isRelationToUnknown) { + // Ignore the event: it is before our interest. + return; + } } } @@ -533,7 +545,7 @@ export class StopGapWidget extends EventEmitter { const evId = ev.getId(); if (evRoomId && evId) { const room = this.client.getRoom(evRoomId); - if (room && room.getMyMembership() === "join") { + if (room && room.getMyMembership() === KnownMembership.Join && !isRelationToUnknown) { this.readUpToMap[evRoomId] = evId; } } diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index c2453ebccf4..248c520ddd5 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -43,6 +43,8 @@ import { Room, Direction, THREAD_RELATION_TYPE, + StateEvents, + TimelineEvents, } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { @@ -139,6 +141,9 @@ export class StopGapWidgetDriver extends WidgetDriver { this.allowedCapabilities.add( WidgetEventCapability.forStateEvent(EventDirection.Receive, "org.matrix.msc3401.call").raw, ); + this.allowedCapabilities.add( + WidgetEventCapability.forStateEvent(EventDirection.Receive, EventType.RoomEncryption).raw, + ); this.allowedCapabilities.add( WidgetEventCapability.forStateEvent( EventDirection.Send, @@ -241,6 +246,18 @@ export class StopGapWidgetDriver extends WidgetDriver { return allAllowed; } + public async sendEvent( + eventType: K, + content: StateEvents[K], + stateKey?: string, + targetRoomId?: string, + ): Promise; + public async sendEvent( + eventType: K, + content: TimelineEvents[K], + stateKey: null, + targetRoomId?: string, + ): Promise; public async sendEvent( eventType: string, content: IContent, @@ -255,13 +272,22 @@ export class StopGapWidgetDriver extends WidgetDriver { let r: { event_id: string } | null; if (stateKey !== null) { // state event - r = await client.sendStateEvent(roomId, eventType, content, stateKey); + r = await client.sendStateEvent( + roomId, + eventType as keyof StateEvents, + content as StateEvents[keyof StateEvents], + stateKey, + ); } else if (eventType === EventType.RoomRedaction) { // special case: extract the `redacts` property and call redact r = await client.redactEvent(roomId, content["redacts"]); } else { // message event - r = await client.sendEvent(roomId, eventType, content); + r = await client.sendEvent( + roomId, + eventType as keyof TimelineEvents, + content as TimelineEvents[keyof TimelineEvents], + ); if (eventType === EventType.RoomMessage) { CHAT_EFFECTS.forEach((effect) => { diff --git a/src/stores/widgets/WidgetLayoutStore.ts b/src/stores/widgets/WidgetLayoutStore.ts index c965836c8bb..eef5d84d0de 100644 --- a/src/stores/widgets/WidgetLayoutStore.ts +++ b/src/stores/widgets/WidgetLayoutStore.ts @@ -28,55 +28,10 @@ import { ReadyWatchingStore } from "../ReadyWatchingStore"; import { SettingLevel } from "../../settings/SettingLevel"; import { arrayFastClone } from "../../utils/arrays"; import { UPDATE_EVENT } from "../AsyncStore"; +import { Container, IStoredLayout, ILayoutStateEvent, WIDGET_LAYOUT_EVENT_TYPE, IWidgetLayouts } from "./types"; -export const WIDGET_LAYOUT_EVENT_TYPE = "io.element.widgets.layout"; - -export enum Container { - // "Top" is the app drawer, and currently the only sensible value. - Top = "top", - - // "Right" is the right panel, and the default for widgets. Setting - // this as a container on a widget is essentially like saying "no - // changes needed", though this may change in the future. - Right = "right", - - Center = "center", -} - -export interface IStoredLayout { - // Where to store the widget. Required. - container: Container; - - // The index (order) to position the widgets in. Only applies for - // ordered containers (like the top container). Smaller numbers first, - // and conflicts resolved by comparing widget IDs. - index?: number; - - // Percentage (integer) for relative width of the container to consume. - // Clamped to 0-100 and may have minimums imposed upon it. Only applies - // to containers which support inner resizing (currently only the top - // container). - width?: number; - - // Percentage (integer) for relative height of the container. Note that - // this only applies to the top container currently, and that container - // will take the highest value among widgets in the container. Clamped - // to 0-100 and may have minimums imposed on it. - height?: number | null; - - // TODO: [Deferred] Maximizing (fullscreen) widgets by default. -} - -interface IWidgetLayouts { - [widgetId: string]: IStoredLayout; -} - -interface ILayoutStateEvent { - // TODO: [Deferred] Forced layout (fixed with no changes) - - // The widget layouts. - widgets: IWidgetLayouts; -} +export type { IStoredLayout, ILayoutStateEvent }; +export { Container, WIDGET_LAYOUT_EVENT_TYPE }; interface ILayoutSettings extends ILayoutStateEvent { overrides?: string; // event ID for layout state event, if present diff --git a/src/stores/widgets/types.ts b/src/stores/widgets/types.ts new file mode 100644 index 00000000000..36d5d864729 --- /dev/null +++ b/src/stores/widgets/types.ts @@ -0,0 +1,64 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export interface IStoredLayout { + // Where to store the widget. Required. + container: Container; + + // The index (order) to position the widgets in. Only applies for + // ordered containers (like the top container). Smaller numbers first, + // and conflicts resolved by comparing widget IDs. + index?: number; + + // Percentage (integer) for relative width of the container to consume. + // Clamped to 0-100 and may have minimums imposed upon it. Only applies + // to containers which support inner resizing (currently only the top + // container). + width?: number; + + // Percentage (integer) for relative height of the container. Note that + // this only applies to the top container currently, and that container + // will take the highest value among widgets in the container. Clamped + // to 0-100 and may have minimums imposed on it. + height?: number | null; + + // TODO: [Deferred] Maximizing (fullscreen) widgets by default. +} + +export interface IWidgetLayouts { + [widgetId: string]: IStoredLayout; +} + +export interface ILayoutStateEvent { + // TODO: [Deferred] Forced layout (fixed with no changes) + + // The widget layouts. + widgets: IWidgetLayouts; +} + +export const WIDGET_LAYOUT_EVENT_TYPE = "io.element.widgets.layout"; + +export enum Container { + // "Top" is the app drawer, and currently the only sensible value. + Top = "top", + + // "Right" is the right panel, and the default for widgets. Setting + // this as a container on a widget is essentially like saying "no + // changes needed", though this may change in the future. + Right = "right", + + Center = "center", +} diff --git a/src/utils/AutoDiscoveryUtils.tsx b/src/utils/AutoDiscoveryUtils.tsx index cb91c53b0f2..c1f2a7a7d08 100644 --- a/src/utils/AutoDiscoveryUtils.tsx +++ b/src/utils/AutoDiscoveryUtils.tsx @@ -19,9 +19,11 @@ import { AutoDiscovery, AutoDiscoveryError, ClientConfig, - OidcClientConfig, - M_AUTHENTICATION, + discoverAndValidateOIDCIssuerWellKnown, IClientWellKnown, + MatrixClient, + MatrixError, + OidcClientConfig, } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; @@ -217,12 +219,12 @@ export default class AutoDiscoveryUtils { * @param {boolean} isSynthetic If true, then the discoveryResult was synthesised locally. * @returns {Promise} Resolves to the validated configuration. */ - public static buildValidatedConfigFromDiscovery( + public static async buildValidatedConfigFromDiscovery( serverName?: string, discoveryResult?: ClientConfig, syntaxOnly = false, isSynthetic = false, - ): ValidatedServerConfig { + ): Promise { if (!discoveryResult?.["m.homeserver"]) { // This shouldn't happen without major misconfiguration, so we'll log a bit of information // in the log so we can find this bit of code but otherwise tell the user "it broke". @@ -293,26 +295,20 @@ export default class AutoDiscoveryUtils { throw new UserFriendlyError("auth|autodiscovery_unexpected_error_hs"); } + // This isn't inherently auto-discovery but used to be in an earlier incarnation of the MSC, + // and shuttling the data together makes a lot of sense let delegatedAuthentication: OidcClientConfig | undefined; - if (discoveryResult[M_AUTHENTICATION.stable!]?.state === AutoDiscovery.SUCCESS) { - const { - authorizationEndpoint, - registrationEndpoint, - tokenEndpoint, - account, - issuer, - metadata, - signingKeys, - } = discoveryResult[M_AUTHENTICATION.stable!] as OidcClientConfig; - delegatedAuthentication = Object.freeze({ - authorizationEndpoint, - registrationEndpoint, - tokenEndpoint, - account, - issuer, - metadata, - signingKeys, - }); + let delegatedAuthenticationError: Error | undefined; + try { + const tempClient = new MatrixClient({ baseUrl: preferredHomeserverUrl }); + const { issuer } = await tempClient.getAuthIssuer(); + delegatedAuthentication = await discoverAndValidateOIDCIssuerWellKnown(issuer); + } catch (e) { + if (e instanceof MatrixError && e.httpStatus === 404 && e.errcode === "M_UNRECOGNIZED") { + // 404 M_UNRECOGNIZED means the server does not support OIDC + } else { + delegatedAuthenticationError = e as Error; + } } return { @@ -321,7 +317,7 @@ export default class AutoDiscoveryUtils { hsNameIsDifferent: url.hostname !== preferredHomeserverName, isUrl: preferredIdentityUrl, isDefault: false, - warning: hsResult.error, + warning: hsResult.error ?? delegatedAuthenticationError ?? null, isNameResolvable: !isSynthetic, delegatedAuthentication, } as ValidatedServerConfig; diff --git a/src/utils/DMRoomMap.ts b/src/utils/DMRoomMap.ts index eafbe07d5d5..cdabb50ec09 100644 --- a/src/utils/DMRoomMap.ts +++ b/src/utils/DMRoomMap.ts @@ -16,6 +16,7 @@ limitations under the License. import { uniq } from "lodash"; import { Room, MatrixEvent, EventType, ClientEvent, MatrixClient } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { logger } from "matrix-js-sdk/src/logger"; import { Optional } from "matrix-events-sdk"; @@ -172,7 +173,7 @@ export default class DMRoomMap { const joinedRooms = commonRooms .map((r) => this.matrixClient.getRoom(r)) - .filter((r) => r && r.getMyMembership() === "join"); + .filter((r) => r && r.getMyMembership() === KnownMembership.Join); return joinedRooms[0]; } diff --git a/src/utils/DecryptFile.ts b/src/utils/DecryptFile.ts index 9b72c13bd39..c72f57589f7 100644 --- a/src/utils/DecryptFile.ts +++ b/src/utils/DecryptFile.ts @@ -17,9 +17,9 @@ limitations under the License. // Pull in the encryption lib so that we can decrypt attachments. import encrypt from "matrix-encrypt-attachment"; import { parseErrorResponse } from "matrix-js-sdk/src/matrix"; +import { EncryptedFile, MediaEventInfo } from "matrix-js-sdk/src/types"; import { mediaFromContent } from "../customisations/Media"; -import { EncryptedFile, IMediaEventInfo } from "../customisations/models/IMediaEventContent"; import { getBlobSafeMimeType } from "./blobs"; export class DownloadError extends Error { @@ -44,10 +44,10 @@ export class DecryptError extends Error { * This passed to [link]{@link https://github.com/matrix-org/matrix-encrypt-attachment} * as the encryption info object, so will also have the those keys in addition to * the keys below. - * @param {IMediaEventInfo} info The info parameter taken from the matrix event. + * @param {MediaEventInfo} info The info parameter taken from the matrix event. * @returns {Promise} Resolves to a Blob of the file. */ -export async function decryptFile(file?: EncryptedFile, info?: IMediaEventInfo): Promise { +export async function decryptFile(file?: EncryptedFile, info?: MediaEventInfo): Promise { // throws if file is falsy const media = mediaFromContent({ file }); diff --git a/src/utils/FileUtils.ts b/src/utils/FileUtils.ts index 8423a54ce4e..75511756f58 100644 --- a/src/utils/FileUtils.ts +++ b/src/utils/FileUtils.ts @@ -25,22 +25,22 @@ import { FileSizeReturnArray, FileSizeReturnObject, } from "filesize"; +import { MediaEventContent } from "matrix-js-sdk/src/types"; -import { IMediaEventContent } from "../customisations/models/IMediaEventContent"; import { _t } from "../languageHandler"; /** * Extracts a human-readable label for the file attachment to use as * link text. * - * @param {IMediaEventContent} content The "content" key of the matrix event. + * @param {MediaEventContent} content The "content" key of the matrix event. * @param {string} fallbackText The fallback text * @param {boolean} withSize Whether to include size information. Default true. * @param {boolean} shortened Ensure the extension of the file name is visible. Default false. * @return {string} the human-readable link text for the attachment. */ export function presentableTextForFile( - content: IMediaEventContent, + content: MediaEventContent, fallbackText = _t("common|attachment"), withSize = true, shortened = false, diff --git a/src/utils/MediaEventHelper.ts b/src/utils/MediaEventHelper.ts index ea14b175ae9..b9ceae9db5d 100644 --- a/src/utils/MediaEventHelper.ts +++ b/src/utils/MediaEventHelper.ts @@ -15,12 +15,12 @@ limitations under the License. */ import { MatrixEvent, EventType, MsgType } from "matrix-js-sdk/src/matrix"; +import { FileContent, ImageContent, MediaEventContent } from "matrix-js-sdk/src/types"; import { logger } from "matrix-js-sdk/src/logger"; import { LazyValue } from "./LazyValue"; import { Media, mediaFromContent } from "../customisations/Media"; import { decryptFile } from "./DecryptFile"; -import { FileContent, ImageContent, IMediaEventContent } from "../customisations/models/IMediaEventContent"; import { IDestroyable } from "./IDestroyable"; // TODO: We should consider caching the blobs. https://github.com/vector-im/element-web/issues/17192 @@ -48,7 +48,7 @@ export class MediaEventHelper implements IDestroyable { public get fileName(): string { return ( this.event.getContent().filename || - this.event.getContent().body || + this.event.getContent().body || "download" ); } @@ -81,7 +81,7 @@ export class MediaEventHelper implements IDestroyable { private fetchSource = (): Promise => { if (this.media.isEncrypted) { - const content = this.event.getContent(); + const content = this.event.getContent(); return decryptFile(content.file!, content.info); } return this.media.downloadSource().then((r) => r.blob()); diff --git a/src/utils/MultiInviter.ts b/src/utils/MultiInviter.ts index de8ea1d7bcd..3df4e747a9f 100644 --- a/src/utils/MultiInviter.ts +++ b/src/utils/MultiInviter.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { MatrixError, MatrixClient, EventType, HistoryVisibility } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { defer, IDeferred } from "matrix-js-sdk/src/utils"; import { logger } from "matrix-js-sdk/src/logger"; @@ -159,17 +160,17 @@ export default class MultiInviter { if (!room) throw new Error("Room not found"); const member = room.getMember(addr); - if (member?.membership === "join") { + if (member?.membership === KnownMembership.Join) { throw new MatrixError({ errcode: USER_ALREADY_JOINED, error: "Member already joined", }); - } else if (member?.membership === "invite") { + } else if (member?.membership === KnownMembership.Invite) { throw new MatrixError({ errcode: USER_ALREADY_INVITED, error: "Member already invited", }); - } else if (member?.membership === "ban") { + } else if (member?.membership === KnownMembership.Ban) { let proceed = false; // Check if we can unban the invitee. // See https://spec.matrix.org/v1.7/rooms/v10/#authorization-rules, particularly 4.5.3 and 4.5.4. diff --git a/src/utils/RoomUpgrade.ts b/src/utils/RoomUpgrade.ts index 88fc9045419..ea9f773e34b 100644 --- a/src/utils/RoomUpgrade.ts +++ b/src/utils/RoomUpgrade.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { Room, EventType, ClientEvent, MatrixClient } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { logger } from "matrix-js-sdk/src/logger"; import { inviteUsersToRoom } from "../RoomInvite"; @@ -67,7 +68,10 @@ export async function upgradeRoom( let toInvite: string[] = []; if (inviteUsers) { - toInvite = [...room.getMembersWithMembership("join"), ...room.getMembersWithMembership("invite")] + toInvite = [ + ...room.getMembersWithMembership(KnownMembership.Join), + ...room.getMembersWithMembership(KnownMembership.Invite), + ] .map((m) => m.userId) .filter((m) => m !== cli.getUserId()); } @@ -131,7 +135,7 @@ export async function upgradeRoom( EventType.SpaceChild, { ...(currentEv?.getContent() || {}), // copy existing attributes like suggested - via: [cli.getDomain()], + via: [cli.getDomain()!], }, newRoomId, ); diff --git a/src/utils/SortMembers.ts b/src/utils/SortMembers.ts index 534fe6a82e2..413f7971b2c 100644 --- a/src/utils/SortMembers.ts +++ b/src/utils/SortMembers.ts @@ -16,6 +16,7 @@ limitations under the License. import { groupBy, mapValues, maxBy, minBy, sumBy, takeRight } from "lodash"; import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { Member } from "./direct-messages"; import DMRoomMap from "./DMRoomMap"; @@ -52,7 +53,7 @@ function joinedRooms(cli: MatrixClient): Room[] { return ( cli .getRooms() - .filter((r) => r.getMyMembership() === "join") + .filter((r) => r.getMyMembership() === KnownMembership.Join) // Skip low priority rooms and DMs .filter((r) => !DMRoomMap.shared().getUserIdForRoomId(r.roomId)) .filter((r) => !Object.keys(r.tags).includes("m.lowpriority")) diff --git a/src/utils/ValidatedServerConfig.ts b/src/utils/ValidatedServerConfig.ts index 14a28b058d5..cf49ebaddaf 100644 --- a/src/utils/ValidatedServerConfig.ts +++ b/src/utils/ValidatedServerConfig.ts @@ -14,10 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { OidcClientConfig, IDelegatedAuthConfig } from "matrix-js-sdk/src/matrix"; -import { ValidatedIssuerConfig } from "matrix-js-sdk/src/oidc/validate"; - -export type ValidatedDelegatedAuthConfig = IDelegatedAuthConfig & ValidatedIssuerConfig; +import { OidcClientConfig } from "matrix-js-sdk/src/matrix"; export interface ValidatedServerConfig { hsUrl: string; @@ -34,9 +31,9 @@ export interface ValidatedServerConfig { /** * Config related to delegated authentication - * Included when delegated auth is configured and valid, otherwise undefined - * From homeserver .well-known m.authentication, and issuer's .well-known/openid-configuration - * Used for OIDC native flow authentication + * Included when delegated auth is configured and valid, otherwise undefined. + * From issuer's .well-known/openid-configuration. + * Used for OIDC native flow authentication. */ delegatedAuthentication?: OidcClientConfig; } diff --git a/src/utils/WidgetUtils.ts b/src/utils/WidgetUtils.ts index 11918d14589..3272a14e4e9 100644 --- a/src/utils/WidgetUtils.ts +++ b/src/utils/WidgetUtils.ts @@ -18,6 +18,7 @@ limitations under the License. import { base32 } from "rfc4648"; import { IWidget, IWidgetData } from "matrix-widget-api"; import { Room, ClientEvent, MatrixClient, RoomStateEvent, MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { logger } from "matrix-js-sdk/src/logger"; import { CallType } from "matrix-js-sdk/src/webrtc/call"; import { randomString, randomLowercaseString, randomUppercaseString } from "matrix-js-sdk/src/randomstring"; @@ -83,7 +84,7 @@ export default class WidgetUtils { return false; } - if (room.getMyMembership() !== "join") { + if (room.getMyMembership() !== KnownMembership.Join) { logger.warn(`User ${me} is not in room ${roomId}`); return false; } @@ -520,7 +521,7 @@ export default class WidgetUtils { // safe to send. // We'll end up using a local render URL when we see a Jitsi widget anyways, so this is // really just for backwards compatibility and to appease the spec. - baseUrl = "https://app.element.io/"; + baseUrl = PlatformPeg.get()!.baseUrl; } const url = new URL("jitsi.html#" + queryString, baseUrl); // this strips hash fragment from baseUrl return url.href; diff --git a/src/utils/colour.ts b/src/utils/colour.ts index 518b11f835a..8262718a399 100644 --- a/src/utils/colour.ts +++ b/src/utils/colour.ts @@ -27,13 +27,13 @@ export function textToHtmlRainbow(str: string): string { const [a, b] = generateAB(i * frequency, 1); const [red, green, blue] = labToRGB(75, a, b); return ( - '' + c + - "" + "" ); }) .join(""); diff --git a/src/utils/createVoiceMessageContent.ts b/src/utils/createVoiceMessageContent.ts index 28ba5befacc..06bd335389b 100644 --- a/src/utils/createVoiceMessageContent.ts +++ b/src/utils/createVoiceMessageContent.ts @@ -14,7 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { IContent, IEncryptedFile, MsgType } from "matrix-js-sdk/src/matrix"; +import { IEncryptedFile, MsgType } from "matrix-js-sdk/src/matrix"; +import { RoomMessageEventContent } from "matrix-js-sdk/src/types"; /** * @param {string} mxc MXC URL of the file @@ -31,7 +32,7 @@ export const createVoiceMessageContent = ( size: number, file?: IEncryptedFile, waveform?: number[], -): IContent => { +): RoomMessageEventContent => { return { "body": "Voice message", //"msgtype": "org.matrix.msc2516.voice", diff --git a/src/utils/dm/createDmLocalRoom.ts b/src/utils/dm/createDmLocalRoom.ts index ac14834303e..5ffa491bcfc 100644 --- a/src/utils/dm/createDmLocalRoom.ts +++ b/src/utils/dm/createDmLocalRoom.ts @@ -16,6 +16,7 @@ limitations under the License. import { MEGOLM_ALGORITHM } from "matrix-js-sdk/src/crypto/olmlib"; import { EventType, KNOWN_SAFE_ROOM_VERSION, MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../../src/models/LocalRoom"; import { determineCreateRoomEncryptionOption, Member } from "../../../src/utils/direct-messages"; @@ -76,7 +77,7 @@ export async function createDmLocalRoom(client: MatrixClient, targets: Member[]) type: EventType.RoomMember, content: { displayname: userId, - membership: "join", + membership: KnownMembership.Join, }, state_key: userId, user_id: userId, @@ -93,7 +94,7 @@ export async function createDmLocalRoom(client: MatrixClient, targets: Member[]) content: { displayname: target.name, avatar_url: target.getMxcAvatarUrl() ?? undefined, - membership: "invite", + membership: KnownMembership.Invite, isDirect: true, }, state_key: target.userId, @@ -108,7 +109,7 @@ export async function createDmLocalRoom(client: MatrixClient, targets: Member[]) content: { displayname: target.name, avatar_url: target.getMxcAvatarUrl() ?? undefined, - membership: "join", + membership: KnownMembership.Join, }, state_key: target.userId, sender: target.userId, @@ -118,7 +119,7 @@ export async function createDmLocalRoom(client: MatrixClient, targets: Member[]) }); localRoom.targets = targets; - localRoom.updateMyMembership("join"); + localRoom.updateMyMembership(KnownMembership.Join); localRoom.addLiveEvents(events); localRoom.currentState.setStateEvents(events); localRoom.name = localRoom.getDefaultRoomName(client.getUserId()!); diff --git a/src/utils/dm/findDMForUser.ts b/src/utils/dm/findDMForUser.ts index 92575d41be2..55b5f0093f2 100644 --- a/src/utils/dm/findDMForUser.ts +++ b/src/utils/dm/findDMForUser.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import DMRoomMap from "../DMRoomMap"; import { isLocalRoom } from "../localRoom/isLocalRoom"; @@ -42,7 +43,7 @@ function extractSuitableRoom(rooms: Room[], userId: string, findRoomWithThirdpar // a DM is a room of two people that contains those two people exactly. This does mean // that bots, assistants, etc will ruin a room's DM-ness, though this is a problem for // canonical DMs to solve. - if (r && r.getMyMembership() === "join") { + if (r && r.getMyMembership() === KnownMembership.Join) { if (isLocalRoom(r)) return false; const functionalUsers = getFunctionalMembers(r); diff --git a/src/utils/exportUtils/Exporter.ts b/src/utils/exportUtils/Exporter.ts index 9a4f3d42089..57b19e618ba 100644 --- a/src/utils/exportUtils/Exporter.ts +++ b/src/utils/exportUtils/Exporter.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { Direction, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; +import { MediaEventContent } from "matrix-js-sdk/src/types"; import { saveAs } from "file-saver"; import { logger } from "matrix-js-sdk/src/logger"; import sanitizeFilename from "sanitize-filename"; @@ -24,7 +25,6 @@ import { decryptFile } from "../DecryptFile"; import { mediaFromContent } from "../../customisations/Media"; import { formatFullDateNoDay, formatFullDateNoDayISO } from "../../DateUtils"; import { isVoiceMessage } from "../EventUtils"; -import { IMediaEventContent } from "../../customisations/models/IMediaEventContent"; import { _t } from "../../languageHandler"; import SdkConfig from "../../SdkConfig"; @@ -225,7 +225,7 @@ export default abstract class Exporter { let blob: Blob | undefined = undefined; try { const isEncrypted = event.isEncrypted(); - const content = event.getContent(); + const content = event.getContent(); const shouldDecrypt = isEncrypted && content.hasOwnProperty("file") && event.getType() !== "m.sticker"; if (shouldDecrypt) { blob = await decryptFile(content.file); diff --git a/src/utils/exportUtils/JSONExport.ts b/src/utils/exportUtils/JSONExport.ts index d0470576d23..8805f95d65f 100644 --- a/src/utils/exportUtils/JSONExport.ts +++ b/src/utils/exportUtils/JSONExport.ts @@ -74,9 +74,7 @@ export default class JSONExporter extends Exporter { logger.log("Error fetching file: " + err); } } - const jsonEvent: any = mxEv.toJSON(); - const clearEvent = mxEv.isEncrypted() ? jsonEvent.decrypted : jsonEvent; - return clearEvent; + return mxEv.getEffectiveEvent(); } protected async createOutput(events: MatrixEvent[]): Promise { diff --git a/src/utils/image-media.ts b/src/utils/image-media.ts index fa252ee1aee..096fddbcbe9 100644 --- a/src/utils/image-media.ts +++ b/src/utils/image-media.ts @@ -14,8 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { EncryptedFile } from "matrix-js-sdk/src/types"; + import { BlurhashEncoder } from "../BlurhashEncoder"; -import { EncryptedFile } from "../customisations/models/IMediaEventContent"; type ThumbnailableElement = HTMLImageElement | HTMLVideoElement; diff --git a/src/utils/location/index.ts b/src/utils/location/index.ts index 035fe526942..8a107451c6f 100644 --- a/src/utils/location/index.ts +++ b/src/utils/location/index.ts @@ -18,6 +18,6 @@ export * from "./findMapStyleUrl"; export * from "./isSelfLocation"; export * from "./locationEventGeoUri"; export * from "./LocationShareErrors"; -export * from "./map"; +export * from "./links"; export * from "./parseGeoUri"; export * from "./positionFailureMessage"; diff --git a/src/utils/location/links.ts b/src/utils/location/links.ts new file mode 100644 index 00000000000..cafae1ae1a9 --- /dev/null +++ b/src/utils/location/links.ts @@ -0,0 +1,47 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { MatrixEvent, M_LOCATION } from "matrix-js-sdk/src/matrix"; + +import { parseGeoUri } from "./parseGeoUri"; + +export const makeMapSiteLink = (coords: GeolocationCoordinates): string => { + return ( + "https://www.openstreetmap.org/" + + `?mlat=${coords.latitude}` + + `&mlon=${coords.longitude}` + + `#map=16/${coords.latitude}/${coords.longitude}` + ); +}; + +export const createMapSiteLinkFromEvent = (event: MatrixEvent): string | null => { + const content = event.getContent(); + const mLocation = content[M_LOCATION.name]; + if (mLocation !== undefined) { + const uri = mLocation["uri"]; + if (uri !== undefined) { + const geoCoords = parseGeoUri(uri); + return geoCoords ? makeMapSiteLink(geoCoords) : null; + } + } else { + const geoUri = content["geo_uri"]; + if (geoUri) { + const geoCoords = parseGeoUri(geoUri); + return geoCoords ? makeMapSiteLink(geoCoords) : null; + } + } + return null; +}; diff --git a/src/utils/location/map.ts b/src/utils/location/map.ts index 78f17c9868a..707d703bea5 100644 --- a/src/utils/location/map.ts +++ b/src/utils/location/map.ts @@ -15,11 +15,10 @@ limitations under the License. */ import * as maplibregl from "maplibre-gl"; -import { MatrixClient, MatrixEvent, M_LOCATION } from "matrix-js-sdk/src/matrix"; +import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "../../languageHandler"; -import { parseGeoUri } from "./parseGeoUri"; import { findMapStyleUrl } from "./findMapStyleUrl"; import { LocationShareError } from "./LocationShareErrors"; @@ -75,31 +74,3 @@ export const createMarker = (coords: GeolocationCoordinates, element: HTMLElemen }).setLngLat({ lon: coords.longitude, lat: coords.latitude }); return marker; }; - -export const makeMapSiteLink = (coords: GeolocationCoordinates): string => { - return ( - "https://www.openstreetmap.org/" + - `?mlat=${coords.latitude}` + - `&mlon=${coords.longitude}` + - `#map=16/${coords.latitude}/${coords.longitude}` - ); -}; - -export const createMapSiteLinkFromEvent = (event: MatrixEvent): string | null => { - const content = event.getContent(); - const mLocation = content[M_LOCATION.name]; - if (mLocation !== undefined) { - const uri = mLocation["uri"]; - if (uri !== undefined) { - const geoCoords = parseGeoUri(uri); - return geoCoords ? makeMapSiteLink(geoCoords) : null; - } - } else { - const geoUri = content["geo_uri"]; - if (geoUri) { - const geoCoords = parseGeoUri(geoUri); - return geoCoords ? makeMapSiteLink(geoCoords) : null; - } - } - return null; -}; diff --git a/src/utils/location/useMap.ts b/src/utils/location/useMap.ts index f6fc0aa62d0..98ec53ffde1 100644 --- a/src/utils/location/useMap.ts +++ b/src/utils/location/useMap.ts @@ -15,8 +15,8 @@ limitations under the License. */ import { useEffect, useState } from "react"; -import { Map as MapLibreMap } from "maplibre-gl"; +import type { Map as MapLibreMap } from "maplibre-gl"; import { createMap } from "./map"; import { useMatrixClientContext } from "../../contexts/MatrixClientContext"; diff --git a/src/utils/membership.ts b/src/utils/membership.ts index df012e442b6..2ff99fa3609 100644 --- a/src/utils/membership.ts +++ b/src/utils/membership.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { Room, RoomMember, RoomState, RoomStateEvent, MatrixEvent, MatrixClient } from "matrix-js-sdk/src/matrix"; +import { KnownMembership, Membership } from "matrix-js-sdk/src/types"; import { MatrixClientPeg } from "../MatrixClientPeg"; import SettingsStore from "../settings/SettingsStore"; @@ -65,10 +66,13 @@ export function splitRoomsByMembership(rooms: Room[]): MembershipSplit { return split; } -export function getEffectiveMembership(membership: string): EffectiveMembership { - if (membership === "invite") { +export function getEffectiveMembership(membership: Membership): EffectiveMembership { + if (membership === KnownMembership.Invite) { return EffectiveMembership.Invite; - } else if (membership === "join" || (SettingsStore.getValue("feature_ask_to_join") && membership === "knock")) { + } else if ( + membership === KnownMembership.Join || + (SettingsStore.getValue("feature_ask_to_join") && membership === KnownMembership.Knock) + ) { return EffectiveMembership.Join; } else { // Probably a leave, kick, or ban @@ -81,7 +85,7 @@ export function isKnockDenied(room: Room): boolean | undefined { const member = memberId ? room.getMember(memberId) : null; const previousMembership = member?.events.member?.getPrevContent().membership; - return member?.isKicked() && previousMembership === "knock"; + return member?.isKicked() && previousMembership === KnownMembership.Knock; } export function getEffectiveMembershipTag(room: Room, membership?: string): EffectiveMembership { @@ -90,7 +94,7 @@ export function getEffectiveMembershipTag(room: Room, membership?: string): Effe : getEffectiveMembership(membership ?? room.getMyMembership()); } -export function isJoinedOrNearlyJoined(membership: string): boolean { +export function isJoinedOrNearlyJoined(membership: Membership): boolean { const effective = getEffectiveMembership(membership); return effective === EffectiveMembership.Join || effective === EffectiveMembership.Invite; } diff --git a/src/utils/notifications.ts b/src/utils/notifications.ts index 1dd2dd7788b..46e61fc9841 100644 --- a/src/utils/notifications.ts +++ b/src/utils/notifications.ts @@ -21,6 +21,7 @@ import { Room, LocalNotificationSettings, ReceiptType, + IMarkedUnreadEvent, } from "matrix-js-sdk/src/matrix"; import { IndicatorIcon } from "@vector-im/compound-web"; @@ -28,6 +29,19 @@ import SettingsStore from "../settings/SettingsStore"; import { NotificationLevel } from "../stores/notifications/NotificationLevel"; import { doesRoomHaveUnreadMessages } from "../Unread"; +// MSC2867 is not yet spec at time of writing. We read from both stable +// and unstable prefixes and accept the risk that the format may change, +// since the stable prefix is not actually defined yet. + +/** + * Unstable identifier for the marked_unread event, per MSC2867 + */ +export const MARKED_UNREAD_TYPE_UNSTABLE = "com.famedly.marked_unread"; +/** + * Stable identifier for the marked_unread event + */ +export const MARKED_UNREAD_TYPE_STABLE = "m.marked_unread"; + export const deviceNotificationSettingsKeys = [ "notificationsEnabled", "notificationBodyEnabled", @@ -74,6 +88,8 @@ export function localNotificationsAreSilenced(cli: MatrixClient): boolean { export async function clearRoomNotification(room: Room, client: MatrixClient): Promise<{} | undefined> { const lastEvent = room.getLastLiveEvent(); + await setMarkedUnreadState(room, client, false); + try { if (lastEvent) { const receiptType = SettingsStore.getValue("sendReadReceipts", room.roomId) @@ -117,6 +133,39 @@ export function clearAllNotifications(client: MatrixClient): Promise()?.unread; + const currentStateUnstable = room + .getAccountData(MARKED_UNREAD_TYPE_UNSTABLE) + ?.getContent()?.unread; + return currentStateStable ?? currentStateUnstable; +} + +/** + * Sets the marked_unread state of the given room. This sets some room account data that indicates to + * clients that the user considers this room to be 'unread', but without any actual notifications. + * + * @param room The room to set + * @param client MatrixClient object to use + * @param unread The new marked_unread state of the room + */ +export async function setMarkedUnreadState(room: Room, client: MatrixClient, unread: boolean): Promise { + // if there's no event, treat this as false as we don't need to send the flag to clear it if the event isn't there + const currentState = getMarkedUnreadState(room); + + if (Boolean(currentState) !== unread) { + // Assuming MSC2867 passes FCP with no changes, we should update to start writing + // the flag to the stable prefix (or both) and then ultimately use only the + // stable prefix. + await client.setRoomAccountData(room.roomId, MARKED_UNREAD_TYPE_UNSTABLE, { unread }); + } +} + /** * A helper to transform a notification color to the what the Compound Icon Button * expects diff --git a/src/utils/oidc/TokenRefresher.ts b/src/utils/oidc/TokenRefresher.ts index a6a0be29be7..1297a2cb601 100644 --- a/src/utils/oidc/TokenRefresher.ts +++ b/src/utils/oidc/TokenRefresher.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { IDelegatedAuthConfig, OidcTokenRefresher, AccessTokens } from "matrix-js-sdk/src/matrix"; +import { OidcTokenRefresher, AccessTokens } from "matrix-js-sdk/src/matrix"; import { IdTokenClaims } from "oidc-client-ts"; import PlatformPeg from "../../PlatformPeg"; @@ -28,14 +28,14 @@ export class TokenRefresher extends OidcTokenRefresher { private readonly deviceId!: string; public constructor( - authConfig: IDelegatedAuthConfig, + issuer: string, clientId: string, redirectUri: string, deviceId: string, idTokenClaims: IdTokenClaims, private readonly userId: string, ) { - super(authConfig, clientId, deviceId, redirectUri, idTokenClaims); + super(issuer, clientId, deviceId, redirectUri, idTokenClaims); this.deviceId = deviceId; } diff --git a/src/utils/oidc/authorize.ts b/src/utils/oidc/authorize.ts index 154b07d9ed8..8bbdd9894ae 100644 --- a/src/utils/oidc/authorize.ts +++ b/src/utils/oidc/authorize.ts @@ -21,6 +21,7 @@ import { randomString } from "matrix-js-sdk/src/randomstring"; import { IdTokenClaims } from "oidc-client-ts"; import { OidcClientError } from "./error"; +import PlatformPeg from "../../PlatformPeg"; /** * Start OIDC authorization code flow @@ -39,7 +40,7 @@ export const startOidcLogin = async ( identityServerUrl?: string, isRegistration?: boolean, ): Promise => { - const redirectUri = window.location.origin; + const redirectUri = PlatformPeg.get()!.getSSOCallbackUrl().href; const nonce = randomString(10); @@ -53,6 +54,7 @@ export const startOidcLogin = async ( identityServerUrl, nonce, prompt, + urlState: PlatformPeg.get()?.getOidcClientState(), }); window.location.href = authorizationUrl; diff --git a/src/utils/oidc/getDelegatedAuthAccountUrl.ts b/src/utils/oidc/getDelegatedAuthAccountUrl.ts deleted file mode 100644 index cfb61cb4434..00000000000 --- a/src/utils/oidc/getDelegatedAuthAccountUrl.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* -Copyright 2023 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { IClientWellKnown, IDelegatedAuthConfig, M_AUTHENTICATION } from "matrix-js-sdk/src/matrix"; - -/** - * Get the delegated auth account management url if configured - * @param clientWellKnown from MatrixClient.getClientWellKnown - * @returns the account management url, or undefined - */ -export const getDelegatedAuthAccountUrl = (clientWellKnown: IClientWellKnown | undefined): string | undefined => { - const delegatedAuthConfig = M_AUTHENTICATION.findIn(clientWellKnown); - return delegatedAuthConfig?.account; -}; diff --git a/src/utils/oidc/registerClient.ts b/src/utils/oidc/registerClient.ts index 9f112293b69..f554e62e4ed 100644 --- a/src/utils/oidc/registerClient.ts +++ b/src/utils/oidc/registerClient.ts @@ -15,10 +15,9 @@ limitations under the License. */ import { logger } from "matrix-js-sdk/src/logger"; -import { registerOidcClient } from "matrix-js-sdk/src/oidc/register"; +import { registerOidcClient, OidcClientConfig } from "matrix-js-sdk/src/matrix"; import { IConfigOptions } from "../../IConfigOptions"; -import { ValidatedDelegatedAuthConfig } from "../ValidatedServerConfig"; import PlatformPeg from "../../PlatformPeg"; /** @@ -46,12 +45,12 @@ const getStaticOidcClientId = ( * @throws if no clientId is found */ export const getOidcClientId = async ( - delegatedAuthConfig: ValidatedDelegatedAuthConfig, + delegatedAuthConfig: OidcClientConfig, staticOidcClients?: IConfigOptions["oidc_static_clients"], ): Promise => { - const staticClientId = getStaticOidcClientId(delegatedAuthConfig.issuer, staticOidcClients); + const staticClientId = getStaticOidcClientId(delegatedAuthConfig.metadata.issuer, staticOidcClients); if (staticClientId) { - logger.debug(`Using static clientId for issuer ${delegatedAuthConfig.issuer}`); + logger.debug(`Using static clientId for issuer ${delegatedAuthConfig.metadata.issuer}`); return staticClientId; } return await registerOidcClient(delegatedAuthConfig, await PlatformPeg.get()!.getOidcClientMetadata()); diff --git a/src/utils/permalinks/Permalinks.ts b/src/utils/permalinks/Permalinks.ts index 9af7476a39d..537494b26b6 100644 --- a/src/utils/permalinks/Permalinks.ts +++ b/src/utils/permalinks/Permalinks.ts @@ -17,6 +17,7 @@ limitations under the License. import isIp from "is-ip"; import * as utils from "matrix-js-sdk/src/utils"; import { Room, MatrixClient, RoomStateEvent, EventType } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { logger } from "matrix-js-sdk/src/logger"; import MatrixToPermalinkConstructor, { @@ -178,7 +179,7 @@ export class RoomPermalinkCreator { const entries = Object.entries(users); const allowedEntries = entries.filter(([userId]) => { const member = this.room?.getMember(userId); - if (!member || member.membership !== "join") { + if (!member || member.membership !== KnownMembership.Join) { return false; } const serverName = getServerName(userId); diff --git a/src/utils/pillify.tsx b/src/utils/pillify.tsx index f46892949e9..22fcaec99ae 100644 --- a/src/utils/pillify.tsx +++ b/src/utils/pillify.tsx @@ -17,11 +17,11 @@ limitations under the License. import React from "react"; import ReactDOM from "react-dom"; import { PushProcessor } from "matrix-js-sdk/src/pushprocessor"; -import { MatrixEvent, MatrixClient } from "matrix-js-sdk/src/matrix"; +import { MatrixClient, MatrixEvent, RuleId } from "matrix-js-sdk/src/matrix"; import { TooltipProvider } from "@vector-im/compound-web"; import SettingsStore from "../settings/SettingsStore"; -import { Pill, PillType, pillRoomNotifLen, pillRoomNotifPos } from "../components/views/elements/Pill"; +import { Pill, pillRoomNotifLen, pillRoomNotifPos, PillType } from "../components/views/elements/Pill"; import { parsePermalink } from "./permalinks/Permalinks"; import { PermalinkParts } from "./permalinks/PermalinkConstructor"; @@ -127,7 +127,9 @@ export function pillifyLinks( if (roomNotifTextNodes.length > 0) { const pushProcessor = new PushProcessor(matrixClient); - const atRoomRule = pushProcessor.getPushRuleById(".m.rule.roomnotif"); + const atRoomRule = pushProcessor.getPushRuleById( + mxEvent.getContent()["m.mentions"] !== undefined ? RuleId.IsRoomMention : RuleId.AtRoomNotification, + ); if (atRoomRule && pushProcessor.ruleMatchesEvent(atRoomRule, mxEvent)) { // Now replace all those nodes with Pills for (const roomNotifTextNode of roomNotifTextNodes) { diff --git a/src/utils/room/canInviteTo.ts b/src/utils/room/canInviteTo.ts index 55265e6cc8a..bd306d930ee 100644 --- a/src/utils/room/canInviteTo.ts +++ b/src/utils/room/canInviteTo.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { JoinRule, Room } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { shouldShowComponent } from "../../customisations/helpers/UIComponents"; import { UIComponent } from "../../settings/UIFeature"; @@ -29,5 +30,5 @@ export function canInviteTo(room: Room): boolean { const canInvite = !!room.canInvite(client.getSafeUserId()) || !!(room.isSpaceRoom() && room.getJoinRule() === JoinRule.Public); - return canInvite && room.getMyMembership() === "join" && shouldShowComponent(UIComponent.InviteUsers); + return canInvite && room.getMyMembership() === KnownMembership.Join && shouldShowComponent(UIComponent.InviteUsers); } diff --git a/src/utils/space.tsx b/src/utils/space.tsx index d6cfc5a4fff..4222ab14f3a 100644 --- a/src/utils/space.tsx +++ b/src/utils/space.tsx @@ -16,6 +16,7 @@ limitations under the License. import React from "react"; import { Room, ICreateRoomStateEvent, RoomType, EventType, JoinRule } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { calculateRoomVia } from "./permalinks/Permalinks"; import Modal from "../Modal"; @@ -40,7 +41,7 @@ import { SdkContextClass } from "../contexts/SDKContext"; export const shouldShowSpaceSettings = (space: Room): boolean => { const userId = space.client.getUserId()!; return ( - space.getMyMembership() === "join" && + space.getMyMembership() === KnownMembership.Join && (space.currentState.maySendStateEvent(EventType.RoomAvatar, userId) || space.currentState.maySendStateEvent(EventType.RoomName, userId) || space.currentState.maySendStateEvent(EventType.RoomTopic, userId) || @@ -85,7 +86,7 @@ export const showCreateNewRoom = async (space: Room, type?: RoomType): Promise - ((space?.getMyMembership() === "join" && space.canInvite(space.client.getUserId()!)) || + ((space?.getMyMembership() === KnownMembership.Join && space.canInvite(space.client.getUserId()!)) || space.getJoinRule() === JoinRule.Public) && shouldShowComponent(UIComponent.InviteUsers); diff --git a/src/verification.ts b/src/verification.ts index 3b1938c5cac..64a9cf26186 100644 --- a/src/verification.ts +++ b/src/verification.ts @@ -15,7 +15,7 @@ limitations under the License. */ import { User, MatrixClient, RoomMember } from "matrix-js-sdk/src/matrix"; -import { verificationMethods as VerificationMethods } from "matrix-js-sdk/src/crypto"; +import { VerificationMethod } from "matrix-js-sdk/src/types"; import { CrossSigningKey, VerificationRequest } from "matrix-js-sdk/src/crypto-api"; import dis from "./dispatcher/dispatcher"; @@ -61,7 +61,7 @@ export async function verifyDevice(matrixClient: MatrixClient, user: User, devic const verificationRequestPromise = matrixClient.legacyDeviceVerification( user.userId, device.deviceId, - VerificationMethods.SAS, + VerificationMethod.Sas, ); setRightPanel({ member: user, verificationRequestPromise }); } else if (action === "legacy") { diff --git a/src/voice-broadcast/models/VoiceBroadcastRecording.ts b/src/voice-broadcast/models/VoiceBroadcastRecording.ts index 81e07646a9d..c36e3f75b3a 100644 --- a/src/voice-broadcast/models/VoiceBroadcastRecording.ts +++ b/src/voice-broadcast/models/VoiceBroadcastRecording.ts @@ -26,6 +26,7 @@ import { RelationType, TypedEventEmitter, } from "matrix-js-sdk/src/matrix"; +import { AudioContent, EncryptedFile } from "matrix-js-sdk/src/types"; import { ChunkRecordedPayload, @@ -38,7 +39,6 @@ import { VoiceBroadcastRecorderEvent, } from ".."; import { uploadFile } from "../../ContentMessages"; -import { EncryptedFile } from "../../customisations/models/IMediaEventContent"; import { createVoiceMessageContent } from "../../utils/createVoiceMessageContent"; import { IDestroyable } from "../../utils/IDestroyable"; import dis from "../../dispatcher/dispatcher"; @@ -387,7 +387,7 @@ export class VoiceBroadcastRecording rel_type: RelationType.Reference, event_id: this.infoEventId, }; - content["io.element.voice_broadcast_chunk"] = { + (content)["io.element.voice_broadcast_chunk"] = { sequence, }; diff --git a/test/ContentMessages-test.ts b/test/ContentMessages-test.ts index a54ad29ab78..231ddc1b20e 100644 --- a/test/ContentMessages-test.ts +++ b/test/ContentMessages-test.ts @@ -15,7 +15,8 @@ limitations under the License. */ import { mocked } from "jest-mock"; -import { IImageInfo, ISendEventResponse, MatrixClient, RelationType, UploadResponse } from "matrix-js-sdk/src/matrix"; +import { ISendEventResponse, MatrixClient, RelationType, UploadResponse } from "matrix-js-sdk/src/matrix"; +import { ImageInfo } from "matrix-js-sdk/src/types"; import { defer } from "matrix-js-sdk/src/utils"; import encrypt, { IEncryptedFile } from "matrix-encrypt-attachment"; @@ -43,7 +44,7 @@ const createElement = document.createElement.bind(document); describe("ContentMessages", () => { const stickerUrl = "https://example.com/sticker"; const roomId = "!room:example.com"; - const imageInfo = {} as unknown as IImageInfo; + const imageInfo = {} as unknown as ImageInfo; const text = "test sticker"; let client: MatrixClient; let contentMessages: ContentMessages; diff --git a/test/DecryptionFailureTracker-test.js b/test/DecryptionFailureTracker-test.ts similarity index 61% rename from test/DecryptionFailureTracker-test.js rename to test/DecryptionFailureTracker-test.ts index 63b0489ee4c..553d4f4d747 100644 --- a/test/DecryptionFailureTracker-test.js +++ b/test/DecryptionFailureTracker-test.ts @@ -14,36 +14,38 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { decryptExistingEvent, mkDecryptionFailureMatrixEvent } from "matrix-js-sdk/src/testing"; +import { DecryptionFailureCode } from "matrix-js-sdk/src/crypto-api"; import { DecryptionFailureTracker } from "../src/DecryptionFailureTracker"; class MockDecryptionError extends Error { - constructor(code) { + public readonly code: string; + + constructor(code?: string) { super(); this.code = code || "MOCK_DECRYPTION_ERROR"; } } -function createFailedDecryptionEvent() { - const event = new MatrixEvent({ - event_id: "event-id-" + Math.random().toString(16).slice(2), - content: { - algorithm: "m.megolm.v1.aes-sha2", - }, +async function createFailedDecryptionEvent() { + return await mkDecryptionFailureMatrixEvent({ + roomId: "!room:id", + sender: "@alice:example.com", + code: DecryptionFailureCode.UNKNOWN_ERROR, + msg: ":(", }); - event.setClearData(event.badEncryptedMessage(":(")); - return event; } describe("DecryptionFailureTracker", function () { - it("tracks a failed decryption for a visible event", function () { - const failedDecryptionEvent = createFailedDecryptionEvent(); + it("tracks a failed decryption for a visible event", async function () { + const failedDecryptionEvent = await createFailedDecryptionEvent(); let count = 0; + // @ts-ignore access to private constructor const tracker = new DecryptionFailureTracker( - (total) => (count += total), + (total: number) => (count += total), () => "UnknownError", ); @@ -58,16 +60,18 @@ describe("DecryptionFailureTracker", function () { // Immediately track the newest failures tracker.trackFailures(); - expect(count).not.toBe(0, "should track a failure for an event that failed decryption"); + // should track a failure for an event that failed decryption + expect(count).not.toBe(0); }); - it("tracks a failed decryption with expected raw error for a visible event", function () { - const failedDecryptionEvent = createFailedDecryptionEvent(); + it("tracks a failed decryption with expected raw error for a visible event", async function () { + const failedDecryptionEvent = await createFailedDecryptionEvent(); let count = 0; let reportedRawCode = ""; + // @ts-ignore access to private constructor const tracker = new DecryptionFailureTracker( - (total, errcode, rawCode) => { + (total: number, _errCode: string, rawCode: string) => { count += total; reportedRawCode = rawCode; }, @@ -85,16 +89,20 @@ describe("DecryptionFailureTracker", function () { // Immediately track the newest failures tracker.trackFailures(); - expect(count).not.toBe(0, "should track a failure for an event that failed decryption"); - expect(reportedRawCode).toBe("INBOUND_SESSION_MISMATCH_ROOM_ID", "Should add the rawCode to the event context"); + // should track a failure for an event that failed decryption + expect(count).not.toBe(0); + + // Should add the rawCode to the event context + expect(reportedRawCode).toBe("INBOUND_SESSION_MISMATCH_ROOM_ID"); }); - it("tracks a failed decryption for an event that becomes visible later", function () { - const failedDecryptionEvent = createFailedDecryptionEvent(); + it("tracks a failed decryption for an event that becomes visible later", async function () { + const failedDecryptionEvent = await createFailedDecryptionEvent(); let count = 0; + // @ts-ignore access to private constructor const tracker = new DecryptionFailureTracker( - (total) => (count += total), + (total: number) => (count += total), () => "UnknownError", ); @@ -109,15 +117,17 @@ describe("DecryptionFailureTracker", function () { // Immediately track the newest failures tracker.trackFailures(); - expect(count).not.toBe(0, "should track a failure for an event that failed decryption"); + // should track a failure for an event that failed decryption + expect(count).not.toBe(0); }); - it("does not track a failed decryption for an event that never becomes visible", function () { - const failedDecryptionEvent = createFailedDecryptionEvent(); + it("does not track a failed decryption for an event that never becomes visible", async function () { + const failedDecryptionEvent = await createFailedDecryptionEvent(); let count = 0; + // @ts-ignore access to private constructor const tracker = new DecryptionFailureTracker( - (total) => (count += total), + (total: number) => (count += total), () => "UnknownError", ); @@ -130,14 +140,17 @@ describe("DecryptionFailureTracker", function () { // Immediately track the newest failures tracker.trackFailures(); - expect(count).toBe(0, "should not track a failure for an event that never became visible"); + // should not track a failure for an event that never became visible + expect(count).toBe(0); }); - it("does not track a failed decryption where the event is subsequently successfully decrypted", () => { - const decryptedEvent = createFailedDecryptionEvent(); + it("does not track a failed decryption where the event is subsequently successfully decrypted", async () => { + const decryptedEvent = await createFailedDecryptionEvent(); + // @ts-ignore access to private constructor const tracker = new DecryptionFailureTracker( - (total) => { - expect(true).toBe(false, "should not track an event that has since been decrypted correctly"); + (_total: number) => { + // should not track an event that has since been decrypted correctly + expect(true).toBe(false); }, () => "UnknownError", ); @@ -147,8 +160,11 @@ describe("DecryptionFailureTracker", function () { const err = new MockDecryptionError(); tracker.eventDecrypted(decryptedEvent, err); - // Indicate successful decryption: clear data can be anything where the msgtype is not m.bad.encrypted - decryptedEvent.setClearData({}); + // Indicate successful decryption. + await decryptExistingEvent(decryptedEvent, { + plainType: "m.room.message", + plainContent: { body: "success" }, + }); tracker.eventDecrypted(decryptedEvent, null); // Pretend "now" is Infinity @@ -161,11 +177,13 @@ describe("DecryptionFailureTracker", function () { it( "does not track a failed decryption where the event is subsequently successfully decrypted " + "and later becomes visible", - () => { - const decryptedEvent = createFailedDecryptionEvent(); + async () => { + const decryptedEvent = await createFailedDecryptionEvent(); + // @ts-ignore access to private constructor const tracker = new DecryptionFailureTracker( - (total) => { - expect(true).toBe(false, "should not track an event that has since been decrypted correctly"); + (_total: number) => { + // should not track an event that has since been decrypted correctly + expect(true).toBe(false); }, () => "UnknownError", ); @@ -173,8 +191,11 @@ describe("DecryptionFailureTracker", function () { const err = new MockDecryptionError(); tracker.eventDecrypted(decryptedEvent, err); - // Indicate successful decryption: clear data can be anything where the msgtype is not m.bad.encrypted - decryptedEvent.setClearData({}); + // Indicate successful decryption. + await decryptExistingEvent(decryptedEvent, { + plainType: "m.room.message", + plainContent: { body: "success" }, + }); tracker.eventDecrypted(decryptedEvent, null); tracker.addVisibleEvent(decryptedEvent); @@ -187,13 +208,14 @@ describe("DecryptionFailureTracker", function () { }, ); - it("only tracks a single failure per event, despite multiple failed decryptions for multiple events", () => { - const decryptedEvent = createFailedDecryptionEvent(); - const decryptedEvent2 = createFailedDecryptionEvent(); + it("only tracks a single failure per event, despite multiple failed decryptions for multiple events", async () => { + const decryptedEvent = await createFailedDecryptionEvent(); + const decryptedEvent2 = await createFailedDecryptionEvent(); let count = 0; + // @ts-ignore access to private constructor const tracker = new DecryptionFailureTracker( - (total) => (count += total), + (total: number) => (count += total), () => "UnknownError", ); @@ -220,15 +242,17 @@ describe("DecryptionFailureTracker", function () { tracker.trackFailures(); tracker.trackFailures(); - expect(count).toBe(2, count + " failures tracked, should only track a single failure per event"); + // should only track a single failure per event + expect(count).toBe(2); }); - it("should not track a failure for an event that was tracked previously", () => { - const decryptedEvent = createFailedDecryptionEvent(); + it("should not track a failure for an event that was tracked previously", async () => { + const decryptedEvent = await createFailedDecryptionEvent(); let count = 0; + // @ts-ignore access to private constructor const tracker = new DecryptionFailureTracker( - (total) => (count += total), + (total: number) => (count += total), () => "UnknownError", ); @@ -248,18 +272,20 @@ describe("DecryptionFailureTracker", function () { tracker.trackFailures(); - expect(count).toBe(1, "should only track a single failure per event"); + // should only track a single failure per event + expect(count).toBe(1); }); - it.skip("should not track a failure for an event that was tracked in a previous session", () => { + it.skip("should not track a failure for an event that was tracked in a previous session", async () => { // This test uses localStorage, clear it beforehand localStorage.clear(); - const decryptedEvent = createFailedDecryptionEvent(); + const decryptedEvent = await createFailedDecryptionEvent(); let count = 0; + // @ts-ignore access to private constructor const tracker = new DecryptionFailureTracker( - (total) => (count += total), + (total: number) => (count += total), () => "UnknownError", ); @@ -276,8 +302,9 @@ describe("DecryptionFailureTracker", function () { tracker.trackFailures(); // Simulate the browser refreshing by destroying tracker and creating a new tracker + // @ts-ignore access to private constructor const secondTracker = new DecryptionFailureTracker( - (total) => (count += total), + (total: number) => (count += total), () => "UnknownError", ); @@ -289,19 +316,22 @@ describe("DecryptionFailureTracker", function () { secondTracker.checkFailures(Infinity); secondTracker.trackFailures(); - expect(count).toBe(1, count + " failures tracked, should only track a single failure per event"); + // should only track a single failure per event + expect(count).toBe(1); }); - it("should count different error codes separately for multiple failures with different error codes", () => { - const counts = {}; + it("should count different error codes separately for multiple failures with different error codes", async () => { + const counts: Record = {}; + + // @ts-ignore access to private constructor const tracker = new DecryptionFailureTracker( - (total, errorCode) => (counts[errorCode] = (counts[errorCode] || 0) + total), - (error) => (error === "UnknownError" ? "UnknownError" : "OlmKeysNotSentError"), + (total: number, errorCode: string) => (counts[errorCode] = (counts[errorCode] || 0) + total), + (error: string) => (error === "UnknownError" ? "UnknownError" : "OlmKeysNotSentError"), ); - const decryptedEvent1 = createFailedDecryptionEvent(); - const decryptedEvent2 = createFailedDecryptionEvent(); - const decryptedEvent3 = createFailedDecryptionEvent(); + const decryptedEvent1 = await createFailedDecryptionEvent(); + const decryptedEvent2 = await createFailedDecryptionEvent(); + const decryptedEvent3 = await createFailedDecryptionEvent(); const error1 = new MockDecryptionError("UnknownError"); const error2 = new MockDecryptionError("OlmKeysNotSentError"); @@ -322,19 +352,21 @@ describe("DecryptionFailureTracker", function () { tracker.trackFailures(); //expect(counts['UnknownError']).toBe(1, 'should track one UnknownError'); - expect(counts["OlmKeysNotSentError"]).toBe(2, "should track two OlmKeysNotSentError"); + expect(counts["OlmKeysNotSentError"]).toBe(2); }); - it("should aggregate error codes correctly", () => { - const counts = {}; + it("should aggregate error codes correctly", async () => { + const counts: Record = {}; + + // @ts-ignore access to private constructor const tracker = new DecryptionFailureTracker( - (total, errorCode) => (counts[errorCode] = (counts[errorCode] || 0) + total), - (errorCode) => "OlmUnspecifiedError", + (total: number, errorCode: string) => (counts[errorCode] = (counts[errorCode] || 0) + total), + (_errorCode: string) => "OlmUnspecifiedError", ); - const decryptedEvent1 = createFailedDecryptionEvent(); - const decryptedEvent2 = createFailedDecryptionEvent(); - const decryptedEvent3 = createFailedDecryptionEvent(); + const decryptedEvent1 = await createFailedDecryptionEvent(); + const decryptedEvent2 = await createFailedDecryptionEvent(); + const decryptedEvent3 = await createFailedDecryptionEvent(); const error1 = new MockDecryptionError("ERROR_CODE_1"); const error2 = new MockDecryptionError("ERROR_CODE_2"); @@ -353,20 +385,19 @@ describe("DecryptionFailureTracker", function () { tracker.trackFailures(); - expect(counts["OlmUnspecifiedError"]).toBe( - 3, - "should track three OlmUnspecifiedError, got " + counts["OlmUnspecifiedError"], - ); + expect(counts["OlmUnspecifiedError"]).toBe(3); }); - it("should remap error codes correctly", () => { - const counts = {}; + it("should remap error codes correctly", async () => { + const counts: Record = {}; + + // @ts-ignore access to private constructor const tracker = new DecryptionFailureTracker( - (total, errorCode) => (counts[errorCode] = (counts[errorCode] || 0) + total), - (errorCode) => Array.from(errorCode).reverse().join(""), + (total: number, errorCode: string) => (counts[errorCode] = (counts[errorCode] || 0) + total), + (errorCode: string) => Array.from(errorCode).reverse().join(""), ); - const decryptedEvent = createFailedDecryptionEvent(); + const decryptedEvent = await createFailedDecryptionEvent(); const error = new MockDecryptionError("ERROR_CODE_1"); @@ -379,6 +410,7 @@ describe("DecryptionFailureTracker", function () { tracker.trackFailures(); - expect(counts["1_EDOC_RORRE"]).toBe(1, "should track remapped error code"); + // should track remapped error code + expect(counts["1_EDOC_RORRE"]).toBe(1); }); }); diff --git a/test/LegacyCallHandler-test.ts b/test/LegacyCallHandler-test.ts index 9e31baad2a2..8e54ac04903 100644 --- a/test/LegacyCallHandler-test.ts +++ b/test/LegacyCallHandler-test.ts @@ -23,6 +23,7 @@ import { RuleId, TweakName, } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { CallEvent, CallState, CallType, MatrixCall } from "matrix-js-sdk/src/webrtc/call"; import EventEmitter from "events"; import { mocked } from "jest-mock"; @@ -102,7 +103,7 @@ function mkStubDM(roomId: string, userId: string) { name: "Member", rawDisplayName: "Member", roomId: roomId, - membership: "join", + membership: KnownMembership.Join, getAvatarUrl: () => "mxc://avatar.url/image.png", getMxcAvatarUrl: () => "mxc://avatar.url/image.png", }, @@ -111,7 +112,7 @@ function mkStubDM(roomId: string, userId: string) { name: "Member", rawDisplayName: "Member", roomId: roomId, - membership: "join", + membership: KnownMembership.Join, getAvatarUrl: () => "mxc://avatar.url/image.png", getMxcAvatarUrl: () => "mxc://avatar.url/image.png", }, @@ -120,7 +121,7 @@ function mkStubDM(roomId: string, userId: string) { name: "Bot user", rawDisplayName: "Bot user", roomId: roomId, - membership: "join", + membership: KnownMembership.Join, getAvatarUrl: () => "mxc://avatar.url/image.png", getMxcAvatarUrl: () => "mxc://avatar.url/image.png", }, diff --git a/test/Lifecycle-test.ts b/test/Lifecycle-test.ts index 90dd0c53353..fac59b235ae 100644 --- a/test/Lifecycle-test.ts +++ b/test/Lifecycle-test.ts @@ -28,10 +28,10 @@ import { MatrixClientPeg } from "../src/MatrixClientPeg"; import Modal from "../src/Modal"; import * as StorageManager from "../src/utils/StorageManager"; import { flushPromises, getMockClientWithEventEmitter, mockClientMethodsUser, mockPlatformPeg } from "./test-utils"; -import ToastStore from "../src/stores/ToastStore"; import { OidcClientStore } from "../src/stores/oidc/OidcClientStore"; import { makeDelegatedAuthConfig } from "./test-utils/oidc"; import { persistOidcAuthenticatedSettings } from "../src/utils/oidc/persistOidcSettings"; +import { Action } from "../src/dispatcher/actions"; const webCrypto = new Crypto(); @@ -451,17 +451,10 @@ describe("Lifecycle", () => { }); }); - it("should show a toast if the matrix server version is unsupported", async () => { - const toastSpy = jest.spyOn(ToastStore.sharedInstance(), "addOrReplaceToast"); - mockClient.isVersionSupported.mockImplementation(async (version) => version == "r0.6.0"); - initLocalStorageMock({ ...localStorageSession }); + it("should proceed if server is not accessible", async () => { + mockClient.isVersionSupported.mockRejectedValue(new Error("Oh, noes, the server is down!")); expect(await restoreFromLocalStorage()).toEqual(true); - expect(toastSpy).toHaveBeenCalledWith( - expect.objectContaining({ - title: "Your server is unsupported", - }), - ); }); }); }); @@ -682,10 +675,10 @@ describe("Lifecycle", () => { beforeAll(() => { fetchMock.get( - `${delegatedAuthConfig.issuer}.well-known/openid-configuration`, + `${delegatedAuthConfig.metadata.issuer}.well-known/openid-configuration`, delegatedAuthConfig.metadata, ); - fetchMock.get(`${delegatedAuthConfig.issuer}jwks`, { + fetchMock.get(`${delegatedAuthConfig.metadata.issuer}jwks`, { status: 200, headers: { "Content-Type": "application/json", @@ -695,12 +688,6 @@ describe("Lifecycle", () => { }); beforeEach(() => { - // mock oidc config for oidc client initialisation - mockClient.waitForClientWellKnown.mockResolvedValue({ - "m.authentication": { - issuer: issuer, - }, - }); initSessionStorageMock(); // set values in session storage as they would be after a successful oidc authentication persistOidcAuthenticatedSettings(clientId, issuer, idTokenClaims); @@ -710,7 +697,9 @@ describe("Lifecycle", () => { await setLoggedIn(credentials); // didn't try to initialise token refresher - expect(fetchMock).not.toHaveFetched(`${delegatedAuthConfig.issuer}.well-known/openid-configuration`); + expect(fetchMock).not.toHaveFetched( + `${delegatedAuthConfig.metadata.issuer}.well-known/openid-configuration`, + ); }); it("should not try to create a token refresher without a deviceId", async () => { @@ -721,7 +710,9 @@ describe("Lifecycle", () => { }); // didn't try to initialise token refresher - expect(fetchMock).not.toHaveFetched(`${delegatedAuthConfig.issuer}.well-known/openid-configuration`); + expect(fetchMock).not.toHaveFetched( + `${delegatedAuthConfig.metadata.issuer}.well-known/openid-configuration`, + ); }); it("should not try to create a token refresher without an issuer in session storage", async () => { @@ -737,7 +728,9 @@ describe("Lifecycle", () => { }); // didn't try to initialise token refresher - expect(fetchMock).not.toHaveFetched(`${delegatedAuthConfig.issuer}.well-known/openid-configuration`); + expect(fetchMock).not.toHaveFetched( + `${delegatedAuthConfig.metadata.issuer}.well-known/openid-configuration`, + ); }); it("should create a client with a tokenRefreshFunction", async () => { @@ -823,4 +816,75 @@ describe("Lifecycle", () => { expect(oidcClientStore.revokeTokens).toHaveBeenCalledWith(accessToken, refreshToken); }); }); + + describe("overwritelogin", () => { + beforeEach(async () => { + jest.spyOn(MatrixJs, "createClient").mockReturnValue(mockClient); + }); + + it("should replace the current login with a new one", async () => { + const stopSpy = jest.spyOn(mockClient, "stopClient").mockReturnValue(undefined); + const dis = window.mxDispatcher; + + const firstLoginEvent: Promise = new Promise((resolve) => { + dis.register(({ action }) => { + if (action === Action.OnLoggedIn) { + resolve(); + } + }); + }); + // set a logged in state + await setLoggedIn(credentials); + + await firstLoginEvent; + + expect(stopSpy).toHaveBeenCalledTimes(1); + // important the overwrite action should not call unset before replacing. + // So spy on it and make sure it's not called. + jest.spyOn(MatrixClientPeg, "unset").mockReturnValue(undefined); + + expect(MatrixClientPeg.replaceUsingCreds).toHaveBeenCalledWith( + expect.objectContaining({ + userId, + }), + undefined, + ); + + const otherCredentials = { + ...credentials, + userId: "@bob:server.org", + deviceId: "def456", + }; + + const secondLoginEvent: Promise = new Promise((resolve) => { + dis.register(({ action }) => { + if (action === Action.OnLoggedIn) { + resolve(); + } + }); + }); + + // Trigger the overwrite login action + dis.dispatch( + { + action: "overwrite_login", + credentials: otherCredentials, + }, + true, + ); + + await secondLoginEvent; + // the client should have been stopped + expect(stopSpy).toHaveBeenCalledTimes(2); + + expect(MatrixClientPeg.replaceUsingCreds).toHaveBeenCalledWith( + expect.objectContaining({ + userId: otherCredentials.userId, + }), + undefined, + ); + + expect(MatrixClientPeg.unset).not.toHaveBeenCalled(); + }); + }); }); diff --git a/test/Reply-test.ts b/test/Reply-test.ts index ac64610ec7a..b7ae3c9eefd 100644 --- a/test/Reply-test.ts +++ b/test/Reply-test.ts @@ -22,6 +22,7 @@ import { LocationAssetType, M_ASSET, M_POLL_END, + Room, } from "matrix-js-sdk/src/matrix"; import { @@ -31,7 +32,7 @@ import { stripHTMLReply, stripPlainReply, } from "../src/utils/Reply"; -import { makePollStartEvent, mkEvent } from "./test-utils"; +import { makePollStartEvent, mkEvent, stubClient } from "./test-utils"; import { RoomPermalinkCreator } from "../src/utils/permalinks/Permalinks"; function makeTestEvent(type: string, content: IContent): MatrixEvent { @@ -66,7 +67,7 @@ describe("Reply", () => { room: "!room1:server", content: {}, }); - event.makeRedacted(event); + event.makeRedacted(event, new Room(event.getRoomId()!, stubClient(), event.getSender()!)); expect(getParentEventId(event)).toBeUndefined(); }); @@ -182,7 +183,7 @@ But this is not room: "!room1:server", content: {}, }); - event.makeRedacted(event); + event.makeRedacted(event, new Room(event.getRoomId()!, stubClient(), event.getSender()!)); expect(shouldDisplayReply(event)).toBe(false); }); diff --git a/test/RoomNotifs-test.ts b/test/RoomNotifs-test.ts index 371ffea9104..3526acf4bf4 100644 --- a/test/RoomNotifs-test.ts +++ b/test/RoomNotifs-test.ts @@ -25,6 +25,7 @@ import { MatrixEvent, PendingEventOrdering, } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import type { MatrixClient } from "matrix-js-sdk/src/matrix"; import { mkEvent, mkRoom, mkRoomMember, muteRoom, stubClient, upsertRoomStateEvents } from "./test-utils"; @@ -277,7 +278,7 @@ describe("RoomNotifs test", () => { }); it("indicates the user has been invited to a channel", async () => { - room.updateMyMembership("invite"); + room.updateMyMembership(KnownMembership.Invite); const { level, symbol, count } = determineUnreadState(room); @@ -290,9 +291,15 @@ describe("RoomNotifs test", () => { jest.spyOn(SettingsStore, "getValue").mockImplementation((name) => { return name === "feature_ask_to_join"; }); - const roomMember = mkRoomMember(room.roomId, MatrixClientPeg.get()!.getSafeUserId(), "leave", true, { - membership: "knock", - }); + const roomMember = mkRoomMember( + room.roomId, + MatrixClientPeg.get()!.getSafeUserId(), + KnownMembership.Leave, + true, + { + membership: KnownMembership.Knock, + }, + ); jest.spyOn(room, "getMember").mockReturnValue(roomMember); const { level, symbol, count } = determineUnreadState(room); diff --git a/test/SlashCommands-test.tsx b/test/SlashCommands-test.tsx index ae90d2bbc42..6ef56aa7fd9 100644 --- a/test/SlashCommands-test.tsx +++ b/test/SlashCommands-test.tsx @@ -15,6 +15,7 @@ limitations under the License. */ import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { mocked } from "jest-mock"; import { Command, Commands, getCommand } from "../src/SlashCommands"; @@ -162,7 +163,7 @@ describe("SlashCommands", () => { it("should warn about self demotion", async () => { setCurrentRoom(); const member = new RoomMember(roomId, client.getSafeUserId()); - member.membership = "join"; + member.membership = KnownMembership.Join; member.powerLevel = 100; room.getMember = () => member; command.run(client, roomId, null, `${client.getUserId()} 0`); @@ -172,7 +173,7 @@ describe("SlashCommands", () => { it("should default to 50 if no powerlevel specified", async () => { setCurrentRoom(); const member = new RoomMember(roomId, "@user:server"); - member.membership = "join"; + member.membership = KnownMembership.Join; room.getMember = () => member; command.run(client, roomId, null, member.userId); expect(client.setPowerLevel).toHaveBeenCalledWith(roomId, member.userId, 50); @@ -191,7 +192,7 @@ describe("SlashCommands", () => { it("should warn about self demotion", async () => { setCurrentRoom(); const member = new RoomMember(roomId, client.getSafeUserId()); - member.membership = "join"; + member.membership = KnownMembership.Join; member.powerLevel = 100; room.getMember = () => member; command.run(client, roomId, null, client.getSafeUserId()); @@ -366,7 +367,7 @@ describe("SlashCommands", () => { describe("/join", () => { beforeEach(() => { jest.spyOn(dispatcher, "dispatch"); - command = findCommand("join")!; + command = findCommand(KnownMembership.Join)!; }); it("should return usage if no args", () => { diff --git a/test/TextForEvent-test.ts b/test/TextForEvent-test.ts index 4a8258879cd..ff1e0a06bc9 100644 --- a/test/TextForEvent-test.ts +++ b/test/TextForEvent-test.ts @@ -23,6 +23,7 @@ import { Room, RoomMember, } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { render } from "@testing-library/react"; import { ReactElement } from "react"; import { Mocked, mocked } from "jest-mock"; @@ -418,7 +419,7 @@ describe("TextForEvent", () => { }); it("returns correct message for redacted poll start", () => { - pollEvent.makeRedacted(pollEvent); + pollEvent.makeRedacted(pollEvent, new Room(pollEvent.getRoomId()!, mockClient, mockClient.getSafeUserId())); expect(textForEvent(pollEvent, mockClient)).toEqual("@a: Message deleted"); }); @@ -444,7 +445,10 @@ describe("TextForEvent", () => { }); it("returns correct message for redacted message", () => { - messageEvent.makeRedacted(messageEvent); + messageEvent.makeRedacted( + messageEvent, + new Room(messageEvent.getRoomId()!, mockClient, mockClient.getSafeUserId()), + ); expect(textForEvent(messageEvent, mockClient)).toEqual("@a: Message deleted"); }); @@ -504,12 +508,12 @@ describe("TextForEvent", () => { type: "m.room.member", sender: "@a:foo", content: { - membership: "join", + membership: KnownMembership.Join, avatar_url: "b", displayname: "Bob", }, prev_content: { - membership: "join", + membership: KnownMembership.Join, avatar_url: "a", displayname: "Andy", }, diff --git a/test/Unread-test.ts b/test/Unread-test.ts index 5caeeb7f346..8d4f319a398 100644 --- a/test/Unread-test.ts +++ b/test/Unread-test.ts @@ -63,7 +63,7 @@ describe("Unread", () => { type: EventType.RoomMessage, sender: aliceId, }); - redactedEvent.makeRedacted(redactedEvent); + redactedEvent.makeRedacted(redactedEvent, new Room(redactedEvent.getRoomId()!, client, aliceId)); beforeEach(() => { jest.clearAllMocks(); @@ -408,7 +408,7 @@ describe("Unread", () => { content: {}, }); console.log("Event Id", redactedEvent.getId()); - redactedEvent.makeRedacted(redactedEvent); + redactedEvent.makeRedacted(redactedEvent, room); console.log("Event Id", redactedEvent.getId()); // Only for timeline events. room.addLiveEvents([redactedEvent]); diff --git a/test/VerjiLocalSearch-test.ts b/test/VerjiLocalSearch-test.ts new file mode 100644 index 00000000000..e29f019414c --- /dev/null +++ b/test/VerjiLocalSearch-test.ts @@ -0,0 +1,217 @@ +/* + Copyright 2024 Verji Tech AS. All rights reserved. + Unauthorized copying or distribution of this file, via any medium, is strictly prohibited. +*/ + +// import { MatrixEvent } from "matrix-js-sdk"; +import { MatrixEvent } from "matrix-js-sdk/src/matrix"; + +import { + findAllMatches, + eventMatchesSearchTerms, + makeSearchTermObject, + isMemberMatch, + SearchTerm, +} from "../src/VerjiLocalSearch"; + +describe("LocalSearch", () => { + it("should return true for matches", async () => { + const testEvent = {} as MatrixEvent; + testEvent.getType = () => "m.room.message"; + testEvent.isRedacted = () => false; + testEvent.getContent = () => ({ body: "bodytext" }) as any; + testEvent.getSender = () => ({ userId: "testtestsson" }) as any; + testEvent.getDate = () => new Date(); + + const termObj: SearchTerm = { + searchTypeAdvanced: false, + searchTypeNormal: true, + fullText: "bodytext", + words: [{ word: "bodytext", highlight: false }], + regExpHighlights: [], + }; + + const isMatch = eventMatchesSearchTerms(termObj, testEvent as MatrixEvent, [] as any); + expect(isMatch).toBe(true); + }); + + it("finds only one match among several", async () => { + const testEvent = {} as MatrixEvent; + testEvent.getType = () => "m.room.message"; + testEvent.isRedacted = () => false; + testEvent.getContent = () => ({ body: "bodytext" }) as any; + testEvent.getSender = () => ({ userId: "testtestsson" }) as any; + testEvent.getDate = () => new Date(); + + const testEvent2 = {} as MatrixEvent; + testEvent2.getType = () => "m.room.message"; + testEvent2.isRedacted = () => false; + testEvent2.getContent = () => ({ body: "not that text at all" }) as any; + testEvent2.getSender = () => ({ userId: "testtestsson" }) as any; + testEvent2.getDate = () => new Date(); + + const testEvent3 = {} as MatrixEvent; + testEvent3.getType = () => "m.room.message"; + testEvent3.isRedacted = () => false; + testEvent3.getContent = () => ({ body: "some different text that doesn't match" }) as any; + testEvent3.getSender = () => ({ userId: "testtestsson" }) as any; + testEvent3.getDate = () => new Date(); + + const room = { + getLiveTimeline: () => { + const timeline = {} as any; + timeline.getEvents = () => [testEvent, testEvent2, testEvent3]; + timeline.getNeighbouringTimeline = () => null; + return timeline; + }, + currentState: { + getMembers: () => [{ name: "Name Namesson", userId: "testtestsson" }], + }, + }; + + const termObj = { + searchTypeAdvanced: false, + searchTypeNormal: true, + fullText: "bodytext", + words: [{ word: "bodytext", highlight: false }], + regExpHighlights: [], + }; + + const matches = await findAllMatches(termObj, room as any, [] as any); + expect(matches.length).toBe(1); + }); + + it("finds several different with advanced search", async () => { + const testEvent = {} as MatrixEvent; + testEvent.getType = () => "m.room.message"; + testEvent.isRedacted = () => false; + testEvent.getContent = () => ({ body: "body text" }) as any; + testEvent.getSender = () => ({ userId: "testtestsson" }) as any; + testEvent.getDate = () => new Date(); + + const testEvent2 = {} as MatrixEvent; + testEvent2.getType = () => "m.room.message"; + testEvent2.isRedacted = () => false; + testEvent2.getContent = () => ({ body: "not that text at all" }) as any; + testEvent2.getSender = () => ({ userId: "testtestsson" }) as any; + testEvent2.getDate = () => new Date(); + + const testEvent3 = {} as MatrixEvent; + testEvent3.getType = () => "m.room.message"; + testEvent3.isRedacted = () => false; + testEvent3.getContent = () => ({ body: "some different text that doesn't match" }) as any; + testEvent3.getSender = () => ({ userId: "testtestsson" }) as any; + testEvent3.getDate = () => new Date(); + + const testEvent4 = {} as MatrixEvent; + testEvent4.getType = () => "m.room.message"; + testEvent4.isRedacted = () => false; + testEvent4.getContent = () => ({ body: "a text that isn't found" }) as any; + testEvent4.getSender = () => ({ userId: "testtestsson" }) as any; + testEvent4.getDate = () => new Date(); + + const room = { + getLiveTimeline: () => { + const timeline = {} as any; + timeline.getEvents = () => [testEvent, testEvent2, testEvent3, testEvent4]; + timeline.getNeighbouringTimeline = () => null; + return timeline; + }, + }; + + const termObj = makeSearchTermObject("rx:(body|all|some)"); + expect(termObj.searchTypeAdvanced).toBe(true); + + const matches = await findAllMatches(termObj, room as any, [] as any); + expect(matches.length).toBe(3); + }); + + it("should be able to find messages sent by specific members", async () => { + const testEvent = {} as MatrixEvent; + testEvent.getType = () => "m.room.message"; + testEvent.isRedacted = () => false; + testEvent.getContent = () => ({ body: "body text Testsson" }) as any; + testEvent.getSender = () => ({ userId: "testtestsson" }) as any; + testEvent.getDate = () => new Date(); + + const testEvent2 = {} as MatrixEvent; + testEvent2.getType = () => "m.room.message"; + testEvent2.isRedacted = () => false; + testEvent2.getContent = () => ({ body: "not that text at all" }) as any; + testEvent2.getSender = () => ({ userId: "namersson" }) as any; + testEvent2.getDate = () => new Date(); + + const testEvent3 = {} as MatrixEvent; + testEvent3.getType = () => "m.room.message"; + testEvent3.isRedacted = () => false; + testEvent3.getContent = () => ({ body: "some different text, but not the one Testsson" }) as any; + testEvent3.getSender = () => ({ userId: "namersson" }) as any; + testEvent3.getDate = () => new Date(); + + const testEvent4 = {} as MatrixEvent; + testEvent4.getType = () => "m.room.message"; + testEvent4.isRedacted = () => false; + testEvent4.getContent = () => ({ body: "a text" }) as any; + testEvent4.getSender = () => ({ userId: "testtestsson" }) as any; + testEvent4.getDate = () => new Date(); + + const room = { + getLiveTimeline: () => { + const timeline = {} as any; + timeline.getEvents = () => [testEvent, testEvent2, testEvent3, testEvent4]; + timeline.getNeighbouringTimeline = () => null; + return timeline; + }, + }; + + const foundUsers = { + ["testtestsson"]: { name: "Test Testsson", userId: "testtestsson" }, + }; + + const termObj = makeSearchTermObject("Testsson"); + const matches = await findAllMatches(termObj, room as any, foundUsers); + console.log(matches.length); + + expect(matches.length).toBe(2); + expect(matches[0].result.getSender().userId).toBe("namersson"); + expect(matches[1].result.getSender().userId).toBe("testtestsson"); + }); + + it("can find by ISO date", async () => { + const testEvent = {} as MatrixEvent; + testEvent.getType = () => "m.room.message"; + testEvent.isRedacted = () => false; + testEvent.getContent = () => ({ body: "body text" }) as any; + testEvent.getSender = () => ({ userId: "testtestsson" }) as any; + testEvent.getDate = () => new Date(2020, 10, 2, 13, 30, 0); + + const testEvent2 = {} as MatrixEvent; + testEvent2.getType = () => "m.room.message"; + testEvent2.isRedacted = () => false; + testEvent2.getContent = () => ({ body: "not that text at all" }) as any; + testEvent2.getSender = () => ({ userId: "namersson" }) as any; + testEvent2.getDate = () => new Date(2020, 9, 28, 14, 0, 0); + + const room = { + getLiveTimeline: () => { + const timeline = { + getEvents: () => [testEvent, testEvent2], + getNeighbouringTimeline: () => null, + }; + return timeline; + }, + }; + + const foundUsers = {}; + const termObj = makeSearchTermObject("2020-10-28"); + const matches = await findAllMatches(termObj, room as any, foundUsers); + expect(matches.length).toBe(1); + expect(matches[0].result.getSender().userId).toBe("namersson"); + }); + + it("matches users", async () => { + const termObj = makeSearchTermObject("Namesson"); + const isMatch = isMemberMatch({ name: "Name Namesson", userId: "namenamesson" } as any, termObj); + expect(isMatch).toBe(true); + }); +}); diff --git a/test/__snapshots__/SlashCommands-test.tsx.snap b/test/__snapshots__/SlashCommands-test.tsx.snap index 08d3bdcc47e..fdffb74ac31 100644 --- a/test/__snapshots__/SlashCommands-test.tsx.snap +++ b/test/__snapshots__/SlashCommands-test.tsx.snap @@ -18,7 +18,7 @@ exports[`SlashCommands /rainbow should make things rainbowy 1`] = ` { "body": "this is a test message", "format": "org.matrix.custom.html", - "formatted_body": "this is a test message", + "formatted_body": "this is a test message", "msgtype": "m.text", } `; @@ -27,7 +27,7 @@ exports[`SlashCommands /rainbowme should make things rainbowy 1`] = ` { "body": "this is a test message", "format": "org.matrix.custom.html", - "formatted_body": "this is a test message", + "formatted_body": "this is a test message", "msgtype": "m.emote", } `; diff --git a/test/accessibility/RovingTabIndex-test.tsx b/test/accessibility/RovingTabIndex-test.tsx index 4a2e67fece6..c2d5fbf0a86 100644 --- a/test/accessibility/RovingTabIndex-test.tsx +++ b/test/accessibility/RovingTabIndex-test.tsx @@ -16,6 +16,7 @@ limitations under the License. import React, { HTMLAttributes } from "react"; import { render } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { IState, @@ -364,4 +365,61 @@ describe("RovingTabIndex", () => { }); }); }); + + describe("handles arrow keys", () => { + it("should handle up/down arrow keys work when handleUpDown=true", async () => { + const { container } = render( + + {({ onKeyDownHandler }) => ( +
+ {button1} + {button2} + {button3} +
+ )} +
, + ); + + container.querySelectorAll("button")[0].focus(); + checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]); + + await userEvent.keyboard("[ArrowDown]"); + checkTabIndexes(container.querySelectorAll("button"), [-1, 0, -1]); + + await userEvent.keyboard("[ArrowDown]"); + checkTabIndexes(container.querySelectorAll("button"), [-1, -1, 0]); + + await userEvent.keyboard("[ArrowUp]"); + checkTabIndexes(container.querySelectorAll("button"), [-1, 0, -1]); + + await userEvent.keyboard("[ArrowUp]"); + checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]); + + // Does not loop without + await userEvent.keyboard("[ArrowUp]"); + checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]); + }); + + it("should call scrollIntoView if specified", async () => { + const { container } = render( + + {({ onKeyDownHandler }) => ( +
+ {button1} + {button2} + {button3} +
+ )} +
, + ); + + container.querySelectorAll("button")[0].focus(); + checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]); + + const button = container.querySelectorAll("button")[1]; + const mock = jest.spyOn(button, "scrollIntoView"); + await userEvent.keyboard("[ArrowDown]"); + expect(mock).toHaveBeenCalled(); + }); + }); }); diff --git a/test/components/structures/MatrixChat-test.tsx b/test/components/structures/MatrixChat-test.tsx index 441de19df94..63eec01ae31 100644 --- a/test/components/structures/MatrixChat-test.tsx +++ b/test/components/structures/MatrixChat-test.tsx @@ -211,6 +211,11 @@ describe("", () => { unstable_features: {}, versions: SERVER_SUPPORTED_MATRIX_VERSIONS, }); + fetchMock.catch({ + status: 404, + body: '{"errcode": "M_UNRECOGNIZED", "error": "Unrecognized request"}', + headers: { "content-type": "application/json" }, + }); jest.spyOn(StorageManager, "idbLoad").mockReset(); jest.spyOn(StorageManager, "idbSave").mockResolvedValue(undefined); diff --git a/test/components/structures/MessagePanel-test.tsx b/test/components/structures/MessagePanel-test.tsx index 45fe3b4abec..3513cee91f7 100644 --- a/test/components/structures/MessagePanel-test.tsx +++ b/test/components/structures/MessagePanel-test.tsx @@ -18,6 +18,7 @@ limitations under the License. import React from "react"; import { EventEmitter } from "events"; import { MatrixEvent, Room, RoomMember, Thread, ReceiptType } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { render } from "@testing-library/react"; import { TooltipProvider } from "@vector-im/compound-web"; @@ -171,8 +172,8 @@ describe("MessagePanel", function () { user: "@user:id", target: bobMember, ts: ts0 + i * 1000, - mship: "join", - prevMship: "join", + mship: KnownMembership.Join, + prevMship: KnownMembership.Join, name: "A user", }), ); @@ -205,8 +206,8 @@ describe("MessagePanel", function () { user: "@user:id", target: bobMember, ts: ts0 + i * 1000, - mship: "join", - prevMship: "join", + mship: KnownMembership.Join, + prevMship: KnownMembership.Join, name: "A user", }), ); @@ -245,7 +246,7 @@ describe("MessagePanel", function () { user: alice, target: aliceMember, ts: ts0 + 1, - mship: "join", + mship: KnownMembership.Join, name: "Alice", }), mkEvent({ @@ -285,7 +286,7 @@ describe("MessagePanel", function () { skey: "@bob:example.org", target: bobMember, ts: ts0 + 5, - mship: "invite", + mship: KnownMembership.Invite, name: "Bob", }), ]; @@ -542,8 +543,8 @@ describe("MessagePanel", function () { user: "@user:id", target: bobMember, ts: Date.now(), - mship: "join", - prevMship: "join", + mship: KnownMembership.Join, + prevMship: KnownMembership.Join, name: "A user", }), ]; @@ -571,8 +572,8 @@ describe("MessagePanel", function () { user: "@user:id", target: bobMember, ts: Date.now(), - mship: "join", - prevMship: "join", + mship: KnownMembership.Join, + prevMship: KnownMembership.Join, name: "A user", }), ...events, @@ -695,8 +696,8 @@ describe("MessagePanel", function () { for (let i = 0; i < 100; i++) { events.push( TestUtilsMatrix.mkMembership({ - mship: "join", - prevMship: "join", + mship: KnownMembership.Join, + prevMship: KnownMembership.Join, room: "!room:id", user: "@user:id", event: true, @@ -716,8 +717,8 @@ describe("MessagePanel", function () { for (let i = 0; i < 100; i++) { events.push( TestUtilsMatrix.mkMembership({ - mship: "join", - prevMship: "join", + mship: KnownMembership.Join, + prevMship: KnownMembership.Join, room: "!room:id", user: "@user:id", event: true, diff --git a/test/components/structures/RoomView-test.tsx b/test/components/structures/RoomView-test.tsx index 066f8b38a20..d0d12d71052 100644 --- a/test/components/structures/RoomView-test.tsx +++ b/test/components/structures/RoomView-test.tsx @@ -29,6 +29,7 @@ import { SearchResult, IEvent, } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { MEGOLM_ALGORITHM } from "matrix-js-sdk/src/crypto/olmlib"; import { fireEvent, render, screen, RenderResult, waitForElementToBeRemoved, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; @@ -238,7 +239,7 @@ describe("RoomView", () => { }); it("updates url preview visibility on encryption state change", async () => { - room.getMyMembership = jest.fn().mockReturnValue("join"); + room.getMyMembership = jest.fn().mockReturnValue(KnownMembership.Join); // we should be starting unencrypted expect(cli.isCryptoEnabled()).toEqual(false); expect(cli.isRoomEncrypted(room.roomId)).toEqual(false); @@ -583,7 +584,7 @@ describe("RoomView", () => { it("allows to cancel a join request", async () => { jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(client); jest.spyOn(client, "leave").mockResolvedValue({}); - jest.spyOn(room, "getMyMembership").mockReturnValue("knock"); + jest.spyOn(room, "getMyMembership").mockReturnValue(KnownMembership.Knock); await mountRoomView(); fireEvent.click(screen.getByRole("button", { name: "Cancel request" })); @@ -594,7 +595,7 @@ describe("RoomView", () => { }); it("should close search results when edit is clicked", async () => { - room.getMyMembership = jest.fn().mockReturnValue("join"); + room.getMyMembership = jest.fn().mockReturnValue(KnownMembership.Join); const eventMapper = (obj: Partial) => new MatrixEvent(obj); @@ -655,7 +656,7 @@ describe("RoomView", () => { const room2 = new Room(`!${roomCount++}:example.org`, cli, "@alice:example.org"); rooms.set(room2.roomId, room2); - room.getMyMembership = jest.fn().mockReturnValue("join"); + room.getMyMembership = jest.fn().mockReturnValue(KnownMembership.Join); const eventMapper = (obj: Partial) => new MatrixEvent(obj); @@ -711,4 +712,10 @@ describe("RoomView", () => { await expect(prom).resolves.toEqual(expect.objectContaining({ room_id: room2.roomId })); }); + + it("fires Action.RoomLoaded", async () => { + jest.spyOn(dis, "dispatch"); + await mountRoomView(); + expect(dis.dispatch).toHaveBeenCalledWith({ action: Action.RoomLoaded }); + }); }); diff --git a/test/components/structures/SpaceHierarchy-test.tsx b/test/components/structures/SpaceHierarchy-test.tsx index 3b851c5a61a..0246329d8d6 100644 --- a/test/components/structures/SpaceHierarchy-test.tsx +++ b/test/components/structures/SpaceHierarchy-test.tsx @@ -18,6 +18,7 @@ import React from "react"; import { mocked } from "jest-mock"; import { fireEvent, render, screen, waitFor, waitForElementToBeRemoved } from "@testing-library/react"; import { MatrixClient, Room, HierarchyRoom } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import { RoomHierarchy } from "matrix-js-sdk/src/room-hierarchy"; import { TooltipProvider } from "@vector-im/compound-web"; @@ -181,7 +182,7 @@ describe("SpaceHierarchy", () => { mocked(client.getRoom).mockImplementation( (roomId) => client.getRooms().find((room) => room.roomId === roomId) ?? null, ); - [room1, room2, space1, room3].forEach((r) => mocked(r.getMyMembership).mockReturnValue("leave")); + [room1, room2, space1, room3].forEach((r) => mocked(r.getMyMembership).mockReturnValue(KnownMembership.Leave)); const hierarchyRoot: HierarchyRoom = { room_id: root.roomId, diff --git a/test/components/structures/ThreadPanel-test.tsx b/test/components/structures/ThreadPanel-test.tsx index 9c8bed0af6f..df939044fbc 100644 --- a/test/components/structures/ThreadPanel-test.tsx +++ b/test/components/structures/ThreadPanel-test.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import React from "react"; -import { render, screen, fireEvent, waitFor } from "@testing-library/react"; +import { render, screen, fireEvent, waitFor, getByRole } from "@testing-library/react"; import { mocked } from "jest-mock"; import { MatrixClient, @@ -34,8 +34,9 @@ import { _t } from "../../../src/languageHandler"; import { MatrixClientPeg } from "../../../src/MatrixClientPeg"; import { RoomPermalinkCreator } from "../../../src/utils/permalinks/Permalinks"; import ResizeNotifier from "../../../src/utils/ResizeNotifier"; -import { getRoomContext, mockPlatformPeg, stubClient } from "../../test-utils"; +import { createTestClient, getRoomContext, mkRoom, mockPlatformPeg, stubClient } from "../../test-utils"; import { mkThread } from "../../test-utils/threads"; +import { IRoomState } from "../../../src/components/structures/RoomView"; jest.mock("../../../src/utils/Feedback"); @@ -48,6 +49,7 @@ describe("ThreadPanel", () => { filterOption={ThreadFilterType.All} setFilterOption={() => undefined} />, + { wrapper: TooltipProvider }, ); expect(asFragment()).toMatchSnapshot(); }); @@ -64,6 +66,18 @@ describe("ThreadPanel", () => { expect(asFragment()).toMatchSnapshot(); }); + it("matches snapshot when no threads", () => { + const { asFragment } = render( + undefined} + />, + { wrapper: TooltipProvider }, + ); + expect(asFragment()).toMatchSnapshot(); + }); + it("expect that ThreadPanelHeader properly opens a context menu when clicked on the button", () => { const { container } = render( { ); expect(foundButton).toMatchSnapshot(); }); + + it("sends an unthreaded read receipt when the Mark All Threads Read button is clicked", async () => { + const mockClient = createTestClient(); + const mockEvent = {} as MatrixEvent; + const mockRoom = mkRoom(mockClient, "!roomId:example.org"); + mockRoom.getLastLiveEvent.mockReturnValue(mockEvent); + const roomContextObject = { + room: mockRoom, + } as unknown as IRoomState; + const { container } = render( + + + + undefined} + /> + + + , + ); + fireEvent.click(getByRole(container, "button", { name: "Mark all as read" })); + await waitFor(() => + expect(mockClient.sendReadReceipt).toHaveBeenCalledWith(mockEvent, expect.anything(), true), + ); + }); + + it("doesn't send a receipt if no room is in context", async () => { + const mockClient = createTestClient(); + const { container } = render( + + + undefined} + /> + + , + ); + fireEvent.click(getByRole(container, "button", { name: "Mark all as read" })); + await waitFor(() => expect(mockClient.sendReadReceipt).not.toHaveBeenCalled()); + }); }); describe("Filtering", () => { diff --git a/test/components/structures/TimelinePanel-test.tsx b/test/components/structures/TimelinePanel-test.tsx index a5312e43c55..afbe173940d 100644 --- a/test/components/structures/TimelinePanel-test.tsx +++ b/test/components/structures/TimelinePanel-test.tsx @@ -35,6 +35,7 @@ import { ThreadEvent, ThreadFilterType, } from "matrix-js-sdk/src/matrix"; +import { KnownMembership } from "matrix-js-sdk/src/types"; import React, { createRef } from "react"; import { Mocked, mocked } from "jest-mock"; import { forEachRight } from "lodash"; @@ -988,8 +989,8 @@ describe("TimelinePanel", () => { events.forEach((event) => timelineSet.getLiveTimeline().addEvent(event, { toStartOfTimeline: true })); const roomMembership = mkMembership({ - mship: "join", - prevMship: "join", + mship: KnownMembership.Join, + prevMship: KnownMembership.Join, user: authorId, room: room.roomId, event: true, @@ -999,7 +1000,7 @@ describe("TimelinePanel", () => { events.push(roomMembership); const member = new RoomMember(room.roomId, authorId); - member.membership = "join"; + member.membership = KnownMembership.Join; const roomState = new RoomState(room.roomId); jest.spyOn(roomState, "getMember").mockReturnValue(member); diff --git a/test/components/structures/ViewSource-test.tsx b/test/components/structures/ViewSource-test.tsx index 44c122e901d..8f2559dff90 100644 --- a/test/components/structures/ViewSource-test.tsx +++ b/test/components/structures/ViewSource-test.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import { render } from "@testing-library/react"; -import { EventType, MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { EventType, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; import React from "react"; import ViewSource from "../../../src/components/structures/ViewSource"; @@ -43,7 +43,7 @@ describe("ViewSource", () => { content: {}, state_key: undefined, }); - redactedMessageEvent.makeRedacted(redactionEvent); + redactedMessageEvent.makeRedacted(redactionEvent, new Room(ROOM_ID, stubClient(), SENDER)); }); beforeEach(stubClient); diff --git a/test/components/structures/__snapshots__/MatrixChat-test.tsx.snap b/test/components/structures/__snapshots__/MatrixChat-test.tsx.snap index 01a5c7818b6..bf03f84a6e1 100644 --- a/test/components/structures/__snapshots__/MatrixChat-test.tsx.snap +++ b/test/components/structures/__snapshots__/MatrixChat-test.tsx.snap @@ -325,7 +325,7 @@ exports[` with an existing session onAction() room actions leave_r role="dialog" >

with an existing session onAction() room actions leave_r > Leave room

-
+
with an existing session onAction() room actions leave_r role="dialog" >

with an existing session onAction() room actions leave_r > Leave space

-
+
unsent messages should render warning w class="mx_RoomStatusBar_unsentBadge" >
unsent messages should render warning w class="mx_RoomStatusBar_unsentBadge" >
renders 1`] = ` class="mx_SpaceHierarchy_roomTile_avatar" > renders 1`] = ` class="mx_SpaceHierarchy_roomTile_avatar" > renders 1`] = ` class="mx_SpaceHierarchy_roomTile_avatar" > renders 1`] = ` class="mx_SpaceHierarchy_roomTile_avatar" > Threads + +