Compare commits
8 Commits
v1.11.92
...
hs/customi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcf9854a4c | ||
|
|
b589757c34 | ||
|
|
0d576b217b | ||
|
|
80cc0a928f | ||
|
|
9cccbeb799 | ||
|
|
a8c170f8be | ||
|
|
f5402b4ec4 | ||
|
|
131b28ede8 |
@@ -7,4 +7,3 @@ test/end-to-end-tests/lib/
|
||||
src/component-index.js
|
||||
# Auto-generated file
|
||||
src/modules.ts
|
||||
src/modules.js
|
||||
|
||||
5
.github/CODEOWNERS
vendored
@@ -10,11 +10,10 @@
|
||||
/test/components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers
|
||||
/src/stores/SetupEncryptionStore.ts @element-hq/element-crypto-web-reviewers
|
||||
/test/stores/SetupEncryptionStore-test.ts @element-hq/element-crypto-web-reviewers
|
||||
/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx @element-hq/element-crypto-web-reviewers
|
||||
/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx @element-hq/element-crypto-web-reviewers
|
||||
/src/src/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers
|
||||
/test/unit-tests/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers
|
||||
/playwright/e2e/settings/encryption-user-tab/ @element-hq/element-crypto-web-reviewers
|
||||
/src/components/views/dialogs/devtools/Crypto.tsx @element-hq/element-crypto-web-reviewers
|
||||
/playwright/e2e/settings/encryption-user-tab/ @element-hq/element-crypto-web-reviewers
|
||||
|
||||
# Ignore translations as those will be updated by GHA for Localazy download
|
||||
/src/i18n/strings
|
||||
|
||||
6
.github/workflows/build_develop.yml
vendored
@@ -26,12 +26,6 @@ jobs:
|
||||
R2_URL: ${{ vars.CF_R2_S3_API }}
|
||||
R2_PUBLIC_URL: "https://element-web-develop.element.io"
|
||||
steps:
|
||||
# Workaround for https://www.cloudflarestatus.com/incidents/t5nrjmpxc1cj
|
||||
- uses: unfor19/install-aws-cli-action@v1
|
||||
with:
|
||||
version: 2.22.35
|
||||
verbose: false
|
||||
arch: amd64
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
|
||||
1
.github/workflows/deploy.yml
vendored
@@ -96,4 +96,3 @@ jobs:
|
||||
projectName: ${{ env.SITE == 'staging.element.io' && 'element-web-staging' || 'element-web' }}
|
||||
directory: _deploy
|
||||
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: main
|
||||
|
||||
2
.github/workflows/dockerhub.yaml
vendored
@@ -51,7 +51,7 @@ jobs:
|
||||
|
||||
- name: Build and push
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6
|
||||
uses: docker/build-push-action@b32b51a8eda65d6793cd0494a773d4f6bcef32dc # v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
|
||||
2
.github/workflows/tests.yml
vendored
@@ -104,7 +104,7 @@ jobs:
|
||||
|
||||
- name: Skip SonarCloud in merge queue
|
||||
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true'
|
||||
uses: guibranco/github-status-action-v2@ecd54a02cf761e85a8fb328fe937710fd4227cda
|
||||
uses: guibranco/github-status-action-v2@56cd38caf0615dd03f49d42ed301f1469911ac61
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: success
|
||||
|
||||
1
.gitignore
vendored
@@ -26,7 +26,6 @@ electron/pub
|
||||
/coverage
|
||||
# Auto-generated file
|
||||
/src/modules.ts
|
||||
/src/modules.js
|
||||
/build_config.yaml
|
||||
/book
|
||||
/index.html
|
||||
|
||||
@@ -17,7 +17,6 @@ electron/pub
|
||||
/coverage
|
||||
# Auto-generated file
|
||||
/src/modules.ts
|
||||
/src/modules.js
|
||||
/src/i18n/strings
|
||||
/build_config.yaml
|
||||
# Raises an error because it contains a template var breaking the script tag
|
||||
|
||||
@@ -33,15 +33,19 @@ module.exports = {
|
||||
"import-notation": null,
|
||||
"value-keyword-case": null,
|
||||
"declaration-block-no-redundant-longhand-properties": null,
|
||||
"declaration-block-no-duplicate-properties": [
|
||||
true,
|
||||
// useful for fallbacks
|
||||
{ ignore: ["consecutive-duplicates-with-different-values"] },
|
||||
],
|
||||
"shorthand-property-no-redundant-values": null,
|
||||
"property-no-vendor-prefix": null,
|
||||
"value-no-vendor-prefix": null,
|
||||
"selector-no-vendor-prefix": null,
|
||||
"media-feature-name-no-vendor-prefix": null,
|
||||
"number-max-precision": null,
|
||||
"no-invalid-double-slash-comments": true,
|
||||
"media-feature-range-notation": null,
|
||||
"declaration-property-value-no-unknown": null,
|
||||
"declaration-property-value-keyword-no-deprecated": null,
|
||||
"csstools/value-no-unknown-custom-properties": [
|
||||
true,
|
||||
{
|
||||
|
||||
51
CHANGELOG.md
@@ -1,54 +1,3 @@
|
||||
Changes in [1.11.92](https://github.com/element-hq/element-web/releases/tag/v1.11.92) (2025-02-11)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* [Backport staging] Log when we show, and hide, encryption setup toasts ([#29238](https://github.com/element-hq/element-web/pull/29238)). Contributed by @richvdh.
|
||||
* Make profile header section match the designs ([#29163](https://github.com/element-hq/element-web/pull/29163)). Contributed by @MidhunSureshR.
|
||||
* Always show back button in the right panel ([#29128](https://github.com/element-hq/element-web/pull/29128)). Contributed by @MidhunSureshR.
|
||||
* Schedule dehydration on reload if the dehydration key is already cached locally ([#29021](https://github.com/element-hq/element-web/pull/29021)). Contributed by @uhoreg.
|
||||
* update to twemoji 15.1.0 ([#29115](https://github.com/element-hq/element-web/pull/29115)). Contributed by @ara4n.
|
||||
* Update matrix-widget-api ([#29112](https://github.com/element-hq/element-web/pull/29112)). Contributed by @toger5.
|
||||
* Allow navigating through the memberlist using up/down keys ([#28949](https://github.com/element-hq/element-web/pull/28949)). Contributed by @MidhunSureshR.
|
||||
* Style room header icons and facepile for toggled state ([#28968](https://github.com/element-hq/element-web/pull/28968)). Contributed by @MidhunSureshR.
|
||||
* Move threads header below base card header ([#28969](https://github.com/element-hq/element-web/pull/28969)). Contributed by @MidhunSureshR.
|
||||
* Add `Advanced` section to the user settings encryption tab ([#28804](https://github.com/element-hq/element-web/pull/28804)). Contributed by @florianduros.
|
||||
* Fix outstanding UX issues with replies/mentions/keyword notifs ([#28270](https://github.com/element-hq/element-web/pull/28270)). Contributed by @taffyko.
|
||||
* Distinguish room state and timeline events when dealing with widgets ([#28681](https://github.com/element-hq/element-web/pull/28681)). Contributed by @robintown.
|
||||
* Switch OIDC primarily to new `/auth_metadata` API ([#29019](https://github.com/element-hq/element-web/pull/29019)). Contributed by @t3chguy.
|
||||
* More memberlist changes ([#29069](https://github.com/element-hq/element-web/pull/29069)). Contributed by @MidhunSureshR.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* [Backport staging] Wire up the "Forgot recovery key" button for the "Key storage out of sync" toast ([#29190](https://github.com/element-hq/element-web/pull/29190)). Contributed by @RiotRobot.
|
||||
* Encryption tab: hide `Advanced` section when the key storage is out of sync ([#29129](https://github.com/element-hq/element-web/pull/29129)). Contributed by @florianduros.
|
||||
* Fix share button in discovery settings being disabled incorrectly ([#29151](https://github.com/element-hq/element-web/pull/29151)). Contributed by @t3chguy.
|
||||
* Ensure switching rooms does not wrongly focus timeline search ([#29153](https://github.com/element-hq/element-web/pull/29153)). Contributed by @t3chguy.
|
||||
* Stop showing a dialog prompting the user to enter an old recovery key ([#29143](https://github.com/element-hq/element-web/pull/29143)). Contributed by @richvdh.
|
||||
* Make themed widgets reflect the effective theme ([#28342](https://github.com/element-hq/element-web/pull/28342)). Contributed by @robintown.
|
||||
* support non-VS16 emoji ligatures in TwemojiMozilla ([#29100](https://github.com/element-hq/element-web/pull/29100)). Contributed by @ara4n.
|
||||
* e2e test: Verify session with the encryption tab instead of the security \& privacy tab ([#29090](https://github.com/element-hq/element-web/pull/29090)). Contributed by @florianduros.
|
||||
* Work around cloudflare R2 / aws client incompatability ([#29086](https://github.com/element-hq/element-web/pull/29086)). Contributed by @dbkr.
|
||||
* Fix identity server settings visibility ([#29083](https://github.com/element-hq/element-web/pull/29083)). Contributed by @dbkr.
|
||||
|
||||
|
||||
Changes in [1.11.91](https://github.com/element-hq/element-web/releases/tag/v1.11.91) (2025-01-28)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Implement changes to memberlist from feedback ([#29029](https://github.com/element-hq/element-web/pull/29029)). Contributed by @MidhunSureshR.
|
||||
* Add toast for recovery keys being out of sync ([#28946](https://github.com/element-hq/element-web/pull/28946)). Contributed by @dbkr.
|
||||
* Refactor LegacyCallHandler event emitter to use TypedEventEmitter ([#29008](https://github.com/element-hq/element-web/pull/29008)). Contributed by @t3chguy.
|
||||
* Add `Recovery` section in the new user settings `Encryption` tab ([#28673](https://github.com/element-hq/element-web/pull/28673)). Contributed by @florianduros.
|
||||
* Retry loading chunks to make the app more resilient ([#29001](https://github.com/element-hq/element-web/pull/29001)). Contributed by @t3chguy.
|
||||
* Clear account idb table on logout ([#28996](https://github.com/element-hq/element-web/pull/28996)). Contributed by @t3chguy.
|
||||
* Implement new memberlist design with MVVM architecture ([#28874](https://github.com/element-hq/element-web/pull/28874)). Contributed by @MidhunSureshR.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* [Backport staging] Switch to secure random strings ([#29035](https://github.com/element-hq/element-web/pull/29035)). Contributed by @RiotRobot.
|
||||
* React to MatrixEvent sender/target being updated for rendering state events ([#28947](https://github.com/element-hq/element-web/pull/28947)). Contributed by @t3chguy.
|
||||
|
||||
|
||||
Changes in [1.11.90](https://github.com/element-hq/element-web/releases/tag/v1.11.90) (2025-01-14)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
@@ -8,13 +8,11 @@
|
||||
|
||||
#### develop
|
||||
|
||||
The develop branch holds the very latest and greatest code we have to offer, as such it may be less stable.
|
||||
It is auto-deployed on every commit to element-web or matrix-js-sdk to develop.element.io via GitHub Actions `build_develop.yml`.
|
||||
The develop branch holds the very latest and greatest code we have to offer, as such it may be less stable. It corresponds to the develop.element.io CD platform.
|
||||
|
||||
#### staging
|
||||
|
||||
The staging branch corresponds to the very latest release regardless of whether it is an RC or not. Deployed to staging.element.io manually.
|
||||
It is auto-deployed on every release of element-web to staging.element.io via GitHub Actions `deploy.yml`.
|
||||
|
||||
#### master
|
||||
|
||||
@@ -217,7 +215,7 @@ We ship Element Web to dockerhub, `*.element.io`, and packages.element.io.
|
||||
We ship Element Desktop to packages.element.io.
|
||||
|
||||
- [ ] Check that element-web has shipped to dockerhub
|
||||
- [ ] Check that the staging [deployment](https://github.com/element-hq/element-web/actions/workflows/deploy.yml) has completed successfully
|
||||
- [ ] Deploy staging.element.io. [See docs.](https://handbook.element.io/books/element-web-team/page/deploying-appstagingelementio)
|
||||
- [ ] Test staging.element.io
|
||||
|
||||
For final releases additionally do these steps:
|
||||
@@ -227,9 +225,6 @@ For final releases additionally do these steps:
|
||||
- [ ] Ensure Element Web package has shipped to packages.element.io
|
||||
- [ ] Ensure Element Desktop packages have shipped to packages.element.io
|
||||
|
||||
If you need to roll back a deployment to staging.element.io,
|
||||
you can run the `deploy.yml` automation choosing an older tag which you wish to deploy.
|
||||
|
||||
# Housekeeping
|
||||
|
||||
We have some manual housekeeping to do in order to prepare for the next release.
|
||||
|
||||
@@ -23,9 +23,10 @@ const MODULES_TS_HEADER = `
|
||||
* You are not a salmon.
|
||||
*/
|
||||
|
||||
import { RuntimeModule } from "@matrix-org/react-sdk-module-api/lib/RuntimeModule";
|
||||
`;
|
||||
const MODULES_TS_DEFINITIONS = `
|
||||
export const INSTALLED_MODULES = [];
|
||||
export const INSTALLED_MODULES: RuntimeModule[] = [];
|
||||
`;
|
||||
|
||||
export function installer(config: BuildConfig): void {
|
||||
@@ -77,8 +78,8 @@ export function installer(config: BuildConfig): void {
|
||||
return; // hit the finally{} block before exiting
|
||||
}
|
||||
|
||||
// If we reach here, everything seems fine. Write modules.js and log some output
|
||||
// Note: we compile modules.js in two parts for developer friendliness if they
|
||||
// If we reach here, everything seems fine. Write modules.ts and log some output
|
||||
// Note: we compile modules.ts in two parts for developer friendliness if they
|
||||
// happen to look at it.
|
||||
console.log("The following modules have been installed: ", installedModules);
|
||||
let modulesTsHeader = MODULES_TS_HEADER;
|
||||
@@ -192,5 +193,5 @@ function isModuleVersionCompatible(ourApiVersion: string, moduleApiVersion: stri
|
||||
}
|
||||
|
||||
function writeModulesTs(content: string): void {
|
||||
fs.writeFileSync("./src/modules.js", content, "utf-8");
|
||||
fs.writeFileSync("./src/modules.ts", content, "utf-8");
|
||||
}
|
||||
|
||||
22
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "element-web",
|
||||
"version": "1.11.92",
|
||||
"version": "1.11.90",
|
||||
"description": "A feature-rich client for Matrix.org",
|
||||
"author": "New Vector Ltd.",
|
||||
"repository": {
|
||||
@@ -74,7 +74,7 @@
|
||||
"@types/react-dom": "18.3.5",
|
||||
"oidc-client-ts": "3.1.0",
|
||||
"jwt-decode": "4.0.0",
|
||||
"caniuse-lite": "1.0.30001692",
|
||||
"caniuse-lite": "1.0.30001690",
|
||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
|
||||
"wrap-ansi": "npm:wrap-ansi@^7.0.0"
|
||||
},
|
||||
@@ -84,14 +84,14 @@
|
||||
"@fontsource/inter": "^5",
|
||||
"@formatjs/intl-segmenter": "^11.5.7",
|
||||
"@matrix-org/analytics-events": "^0.29.0",
|
||||
"@matrix-org/emojibase-bindings": "^1.3.4",
|
||||
"@matrix-org/emojibase-bindings": "^1.3.3",
|
||||
"@matrix-org/react-sdk-module-api": "^2.4.0",
|
||||
"@matrix-org/spec": "^1.7.0",
|
||||
"@sentry/browser": "^8.0.0",
|
||||
"@types/png-chunks-extract": "^1.0.2",
|
||||
"@types/react-virtualized": "^9.21.30",
|
||||
"@vector-im/compound-design-tokens": "^2.1.0",
|
||||
"@vector-im/compound-web": "^7.6.1",
|
||||
"@vector-im/compound-web": "^7.5.0",
|
||||
"@vector-im/matrix-wysiwyg": "2.38.0",
|
||||
"@zxcvbn-ts/core": "^3.0.4",
|
||||
"@zxcvbn-ts/language-common": "^3.0.4",
|
||||
@@ -127,7 +127,7 @@
|
||||
"maplibre-gl": "^5.0.0",
|
||||
"matrix-encrypt-attachment": "^1.0.3",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-js-sdk": "36.2.0",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||
"matrix-widget-api": "^1.10.0",
|
||||
"memoize-one": "^6.0.0",
|
||||
"mime": "^4.0.4",
|
||||
@@ -178,7 +178,7 @@
|
||||
"@peculiar/webcrypto": "^1.4.3",
|
||||
"@playwright/test": "^1.40.1",
|
||||
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
|
||||
"@sentry/webpack-plugin": "^3.0.0",
|
||||
"@sentry/webpack-plugin": "^2.7.1",
|
||||
"@stylistic/eslint-plugin": "^2.9.0",
|
||||
"@svgr/webpack": "^8.0.0",
|
||||
"@testcontainers/postgresql": "^10.16.0",
|
||||
@@ -230,7 +230,7 @@
|
||||
"dotenv": "^16.0.2",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-deprecate": "0.8.5",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-jest": "^28.0.0",
|
||||
@@ -256,7 +256,7 @@
|
||||
"jsqr": "^1.4.0",
|
||||
"knip": "^5.36.2",
|
||||
"lint-staged": "^15.0.2",
|
||||
"mailpit-api": "^1.0.5",
|
||||
"mailhog": "^4.16.0",
|
||||
"matrix-web-i18n": "^3.2.1",
|
||||
"mini-css-extract-plugin": "2.9.2",
|
||||
"minimist": "^1.2.6",
|
||||
@@ -280,14 +280,14 @@
|
||||
"semver": "^7.5.2",
|
||||
"source-map-loader": "^5.0.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"stylelint": "^16.13.0",
|
||||
"stylelint-config-standard": "^37.0.0",
|
||||
"stylelint": "^16.1.0",
|
||||
"stylelint-config-standard": "^36.0.0",
|
||||
"stylelint-scss": "^6.0.0",
|
||||
"stylelint-value-no-unknown-custom-properties": "^6.0.1",
|
||||
"terser-webpack-plugin": "^5.3.9",
|
||||
"testcontainers": "^10.16.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "5.7.3",
|
||||
"typescript": "5.7.2",
|
||||
"util": "^0.12.5",
|
||||
"web-streams-polyfill": "^4.0.0",
|
||||
"webpack": "^5.89.0",
|
||||
|
||||
43
playwright/e2e/branding/title.spec.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { expect, test } from "../../element-web-test";
|
||||
|
||||
/*
|
||||
* Tests for branding configuration
|
||||
**/
|
||||
|
||||
test.describe("Test without branding config", () => {
|
||||
test("Shows standard branding when showing the home page", async ({ pageWithCredentials: page }) => {
|
||||
await page.goto("/");
|
||||
await page.waitForSelector(".mx_MatrixChat", { timeout: 30000 });
|
||||
expect(page.title()).toEqual("Element *");
|
||||
});
|
||||
test("Shows standard branding when showing a room", async ({ app, pageWithCredentials: page }) => {
|
||||
await app.client.createRoom({ name: "Test Room" });
|
||||
await app.viewRoomByName("Test Room");
|
||||
expect(page.title()).toEqual("Element * | Test Room");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Test with custom branding", () => {
|
||||
test.use({
|
||||
config: {
|
||||
brand: "TestBrand",
|
||||
},
|
||||
});
|
||||
test("Shows custom branding when showing the home page", async ({ pageWithCredentials: page }) => {
|
||||
await page.goto("/");
|
||||
await page.waitForSelector(".mx_MatrixChat", { timeout: 30000 });
|
||||
expect(page.title()).toEqual("TestingApp TestBrand * $ignoredParameter");
|
||||
});
|
||||
test("Shows custom branding when showing a room", async ({ app, pageWithCredentials: page }) => {
|
||||
await app.client.createRoom({ name: "Test Room" });
|
||||
await app.viewRoomByName("Test Room");
|
||||
expect(page.title()).toEqual("TestingApp TestBrand * Test Room $ignoredParameter");
|
||||
});
|
||||
});
|
||||
@@ -19,19 +19,19 @@ test.use(masHomeserver);
|
||||
test.describe("Encryption state after registration", () => {
|
||||
test.skip(isDendrite, "does not yet support MAS");
|
||||
|
||||
test("Key backup is enabled by default", async ({ page, mailpitClient, app }, testInfo) => {
|
||||
test("Key backup is enabled by default", async ({ page, mailhogClient, app }, testInfo) => {
|
||||
await page.goto("/#/login");
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
await registerAccountMas(page, mailpitClient, `alice_${testInfo.testId}`, "alice@email.com", "Pa$sW0rD!");
|
||||
await registerAccountMas(page, mailhogClient, `alice_${testInfo.testId}`, "alice@email.com", "Pa$sW0rD!");
|
||||
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
await expect(page.getByText("This session is backing up your keys.")).toBeVisible();
|
||||
});
|
||||
|
||||
test("user is prompted to set up recovery", async ({ page, mailpitClient, app }, testInfo) => {
|
||||
test("user is prompted to set up recovery", async ({ page, mailhogClient, app }, testInfo) => {
|
||||
await page.goto("/#/login");
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
await registerAccountMas(page, mailpitClient, `alice_${testInfo.testId}`, "alice@email.com", "Pa$sW0rD!");
|
||||
await registerAccountMas(page, mailhogClient, `alice_${testInfo.testId}`, "alice@email.com", "Pa$sW0rD!");
|
||||
|
||||
await page.getByRole("button", { name: "Add room" }).click();
|
||||
await page.getByRole("menuitem", { name: "New room" }).click();
|
||||
@@ -47,7 +47,7 @@ test.describe("Key backup reset from elsewhere", () => {
|
||||
|
||||
test("Key backup is disabled when reset from elsewhere", async ({
|
||||
page,
|
||||
mailpitClient,
|
||||
mailhogClient,
|
||||
request,
|
||||
homeserver,
|
||||
}, testInfo) => {
|
||||
@@ -60,7 +60,7 @@ test.describe("Key backup reset from elsewhere", () => {
|
||||
|
||||
await page.goto("/#/login");
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
await registerAccountMas(page, mailpitClient, testUsername, "alice@email.com", testPassword);
|
||||
await registerAccountMas(page, mailhogClient, testUsername, "alice@email.com", testPassword);
|
||||
|
||||
await page.getByRole("button", { name: "Add room" }).click();
|
||||
await page.getByRole("menuitem", { name: "New room" }).click();
|
||||
|
||||
@@ -10,7 +10,6 @@ import { type Page } from "@playwright/test";
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
import { completeCreateSecretStorageDialog } from "./utils.ts";
|
||||
|
||||
async function expectBackupVersionToBe(page: Page, version: string) {
|
||||
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(5) td")).toHaveText(
|
||||
@@ -36,7 +35,19 @@ test.describe("Backups", () => {
|
||||
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
|
||||
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
|
||||
|
||||
const securityKey = await completeCreateSecretStorageDialog(page);
|
||||
const currentDialogLocator = page.locator(".mx_Dialog");
|
||||
|
||||
// It's the first time and secure storage is not set up, so it will create one
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Set up Secure Backup" })).toBeVisible();
|
||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Save your Security Key" })).toBeVisible();
|
||||
await currentDialogLocator.getByRole("button", { name: "Copy", exact: true }).click();
|
||||
// copy the recovery key to use it later
|
||||
const securityKey = await app.getClipboard();
|
||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Secure Backup successful" })).toBeVisible();
|
||||
await currentDialogLocator.getByRole("button", { name: "Done", exact: true }).click();
|
||||
|
||||
// Open the settings again
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
@@ -51,7 +62,6 @@ test.describe("Backups", () => {
|
||||
await expectBackupVersionToBe(page, "1");
|
||||
|
||||
await securityTab.getByRole("button", { name: "Delete Backup", exact: true }).click();
|
||||
const currentDialogLocator = page.locator(".mx_Dialog");
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Delete Backup" })).toBeVisible();
|
||||
// Delete it
|
||||
await currentDialogLocator.getByTestId("dialog-primary-button").click(); // Click "Delete Backup"
|
||||
|
||||
@@ -8,14 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import type { Page } from "@playwright/test";
|
||||
import { expect, test } from "../../element-web-test";
|
||||
import {
|
||||
autoJoin,
|
||||
completeCreateSecretStorageDialog,
|
||||
copyAndContinue,
|
||||
createSharedRoomWithUser,
|
||||
enableKeyBackup,
|
||||
verify,
|
||||
} from "./utils";
|
||||
import { autoJoin, copyAndContinue, createSharedRoomWithUser, enableKeyBackup, verify } from "./utils";
|
||||
import { Bot } from "../../pages/bot";
|
||||
import { ElementAppPage } from "../../pages/ElementAppPage";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
@@ -118,7 +111,18 @@ test.describe("Cryptography", function () {
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
await page.getByRole("button", { name: "Set up Secure Backup" }).click();
|
||||
|
||||
await completeCreateSecretStorageDialog(page);
|
||||
const dialog = page.locator(".mx_Dialog");
|
||||
// Recovery key is selected by default
|
||||
await dialog.getByRole("button", { name: "Continue" }).click();
|
||||
await copyAndContinue(page);
|
||||
|
||||
// If the device is unverified, there should be a "Setting up keys" step; however, it
|
||||
// can be quite quick, and playwright can miss it, so we can't test for it.
|
||||
|
||||
// Either way, we end up at a success dialog:
|
||||
await expect(dialog.getByText("Secure Backup successful")).toBeVisible();
|
||||
await dialog.getByRole("button", { name: "Done" }).click();
|
||||
await expect(dialog.getByText("Secure Backup successful")).not.toBeVisible();
|
||||
|
||||
// Verify that the SSSS keys are in the account data stored in the server
|
||||
await verifyKey(app, "master");
|
||||
|
||||
@@ -11,8 +11,6 @@ import { Locator, type Page } from "@playwright/test";
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { viewRoomSummaryByName } from "../right-panel/utils";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
import { completeCreateSecretStorageDialog, createBot, logIntoElement } from "./utils.ts";
|
||||
import { Client } from "../../pages/client.ts";
|
||||
|
||||
const ROOM_NAME = "Test room";
|
||||
const NAME = "Alice";
|
||||
@@ -46,7 +44,7 @@ test.use({
|
||||
test.describe("Dehydration", () => {
|
||||
test.skip(isDendrite, "does not yet support dehydration v2");
|
||||
|
||||
test("'Set up secure backup' creates dehydrated device", async ({ page, user, app }, workerInfo) => {
|
||||
test("Create dehydrated device", async ({ page, user, app }, workerInfo) => {
|
||||
// Create a backup (which will create SSSS, and dehydrated device)
|
||||
|
||||
const securityTab = await app.settings.openUserSettings("Security & Privacy");
|
||||
@@ -55,7 +53,17 @@ test.describe("Dehydration", () => {
|
||||
await expect(securityTab.getByText("Offline device enabled")).not.toBeVisible();
|
||||
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
|
||||
|
||||
await completeCreateSecretStorageDialog(page);
|
||||
const currentDialogLocator = page.locator(".mx_Dialog");
|
||||
|
||||
// It's the first time and secure storage is not set up, so it will create one
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Set up Secure Backup" })).toBeVisible();
|
||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Save your Security Key" })).toBeVisible();
|
||||
await currentDialogLocator.getByRole("button", { name: "Copy", exact: true }).click();
|
||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Secure Backup successful" })).toBeVisible();
|
||||
await currentDialogLocator.getByRole("button", { name: "Done", exact: true }).click();
|
||||
|
||||
// Open the settings again
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
@@ -88,49 +96,4 @@ test.describe("Dehydration", () => {
|
||||
await expect(page.locator(".mx_UserInfo_devices").getByText("Offline device enabled")).toBeVisible();
|
||||
await expect(page.locator(".mx_UserInfo_devices").getByText("Dehydrated device")).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("Reset recovery key during login re-creates dehydrated device", async ({
|
||||
page,
|
||||
homeserver,
|
||||
app,
|
||||
credentials,
|
||||
}) => {
|
||||
// Set up cross-signing and recovery
|
||||
const { botClient } = await createBot(page, homeserver, credentials);
|
||||
// ... and dehydration
|
||||
await botClient.evaluate(async (client) => await client.getCrypto().startDehydration());
|
||||
|
||||
const initialDehydratedDeviceIds = await getDehydratedDeviceIds(botClient);
|
||||
expect(initialDehydratedDeviceIds.length).toBe(1);
|
||||
|
||||
await botClient.evaluate(async (client) => client.stopClient());
|
||||
|
||||
// Log in our client
|
||||
await logIntoElement(page, credentials);
|
||||
|
||||
// Oh no, we forgot our recovery key
|
||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Reset all" }).click();
|
||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Proceed with reset" }).click();
|
||||
|
||||
await completeCreateSecretStorageDialog(page, { accountPassword: credentials.password });
|
||||
|
||||
// There should be a brand new dehydrated device
|
||||
const dehydratedDeviceIds = await getDehydratedDeviceIds(app.client);
|
||||
expect(dehydratedDeviceIds.length).toBe(1);
|
||||
expect(dehydratedDeviceIds[0]).not.toEqual(initialDehydratedDeviceIds[0]);
|
||||
});
|
||||
});
|
||||
|
||||
async function getDehydratedDeviceIds(client: Client): Promise<string[]> {
|
||||
return await client.evaluate(async (client) => {
|
||||
const userId = client.getUserId();
|
||||
const devices = await client.getCrypto().getUserDeviceInfo([userId]);
|
||||
return Array.from(
|
||||
devices
|
||||
.get(userId)
|
||||
.values()
|
||||
.filter((d) => d.dehydrated)
|
||||
.map((d) => d.deviceId),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -68,8 +68,8 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
|
||||
// Check that the current device is connected to key backup
|
||||
// For now we don't check that the backup key is in cache because it's a bit flaky,
|
||||
// as we need to wait for the secret gossiping to happen.
|
||||
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, false);
|
||||
// as we need to wait for the secret gossiping to happen and the settings dialog doesn't refresh automatically.
|
||||
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, false);
|
||||
});
|
||||
|
||||
test("Verify device with QR code during login", async ({ page, app, credentials, homeserver }) => {
|
||||
@@ -112,7 +112,9 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
await checkDeviceIsCrossSigned(app);
|
||||
|
||||
// Check that the current device is connected to key backup
|
||||
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
|
||||
// For now we don't check that the backup key is in cache because it's a bit flaky,
|
||||
// as we need to wait for the secret gossiping to happen and the settings dialog doesn't refresh automatically.
|
||||
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, false);
|
||||
});
|
||||
|
||||
test("Verify device with Security Phrase during login", async ({ page, app, credentials, homeserver }) => {
|
||||
@@ -133,7 +135,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
|
||||
// Check that the current device is connected to key backup
|
||||
// The backup decryption key should be in cache also, as we got it directly from the 4S
|
||||
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
|
||||
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, true);
|
||||
});
|
||||
|
||||
test("Verify device with Security Key during login", async ({ page, app, credentials, homeserver }) => {
|
||||
@@ -156,7 +158,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
|
||||
// Check that the current device is connected to key backup
|
||||
// The backup decryption key should be in cache also, as we got it directly from the 4S
|
||||
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
|
||||
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, true);
|
||||
});
|
||||
|
||||
test("Handle incoming verification request with SAS", async ({ page, credentials, homeserver, toasts }) => {
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { createBot, deleteCachedSecrets, logIntoElement } from "./utils";
|
||||
|
||||
test.describe("Key storage out of sync toast", () => {
|
||||
let recoveryKey: GeneratedSecretStorageKey;
|
||||
|
||||
test.beforeEach(async ({ page, homeserver, credentials }) => {
|
||||
const res = await createBot(page, homeserver, credentials);
|
||||
recoveryKey = res.recoveryKey;
|
||||
|
||||
await logIntoElement(page, credentials, recoveryKey.encodedPrivateKey);
|
||||
|
||||
await deleteCachedSecrets(page);
|
||||
|
||||
// We won't be prompted for crypto setup unless we have an e2e room, so make one
|
||||
await page.getByRole("button", { name: "Add room" }).click();
|
||||
await page.getByRole("menuitem", { name: "New room" }).click();
|
||||
await page.getByRole("textbox", { name: "Name" }).fill("Test room");
|
||||
await page.getByRole("button", { name: "Create room" }).click();
|
||||
});
|
||||
|
||||
test("should prompt for recovery key if 'enter recovery key' pressed", { tag: "@screenshot" }, async ({ page }) => {
|
||||
// Need to wait for 2 to appear since playwright only evaluates 'first()' initially, so the waiting won't work
|
||||
await expect(page.getByRole("alert")).toHaveCount(2);
|
||||
await expect(page.getByRole("alert").first()).toMatchScreenshot("key-storage-out-of-sync-toast.png");
|
||||
|
||||
await page.getByRole("button", { name: "Enter recovery key" }).click();
|
||||
await page.locator(".mx_Dialog").getByRole("button", { name: "use your Security Key" }).click();
|
||||
|
||||
await page.getByRole("textbox", { name: "Security key" }).fill(recoveryKey.encodedPrivateKey);
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
await expect(page.getByRole("button", { name: "Enter recovery key" })).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("should open settings to reset flow if 'forgot recovery key' pressed", async ({ page, app, credentials }) => {
|
||||
await expect(page.getByRole("button", { name: "Enter recovery key" })).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "Forgot recovery key?" }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Forgot your recovery key? You’ll need to reset your identity." }),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -139,14 +139,14 @@ export async function checkDeviceIsCrossSigned(app: ElementAppPage): Promise<voi
|
||||
* Check that the current device is connected to the expected key backup.
|
||||
* Also checks that the decryption key is known and cached locally.
|
||||
*
|
||||
* @param app -` ElementAppPage` wrapper for the playwright `Page`.
|
||||
* @param page - the page to check
|
||||
* @param expectedBackupVersion - the version of the backup we expect to be connected to.
|
||||
* @param checkBackupPrivateKeyInCache - whether to check that the backup decryption key is cached locally
|
||||
* @param checkBackupKeyInCache - whether to check that the backup key is cached locally.
|
||||
*/
|
||||
export async function checkDeviceIsConnectedKeyBackup(
|
||||
app: ElementAppPage,
|
||||
page: Page,
|
||||
expectedBackupVersion: string,
|
||||
checkBackupPrivateKeyInCache: boolean,
|
||||
checkBackupKeyInCache: boolean,
|
||||
): Promise<void> {
|
||||
// Sanity check the given backup version: if it's null, something went wrong earlier in the test.
|
||||
if (!expectedBackupVersion) {
|
||||
@@ -155,48 +155,23 @@ export async function checkDeviceIsConnectedKeyBackup(
|
||||
);
|
||||
}
|
||||
|
||||
const backupData = await app.client.evaluate(async (client: MatrixClient) => {
|
||||
const crypto = client.getCrypto();
|
||||
if (!crypto) return;
|
||||
await page.getByRole("button", { name: "User menu" }).click();
|
||||
await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Security & Privacy" }).click();
|
||||
await expect(page.locator(".mx_Dialog").getByRole("button", { name: "Restore from Backup" })).toBeVisible();
|
||||
|
||||
const backupInfo = await crypto.getKeyBackupInfo();
|
||||
const backupKeyIn4S = Boolean(await client.isKeyBackupKeyStored());
|
||||
const backupPrivateKeyFromCache = await crypto.getSessionBackupPrivateKey();
|
||||
const hasBackupPrivateKeyFromCache = Boolean(backupPrivateKeyFromCache);
|
||||
const backupPrivateKeyWellFormed = backupPrivateKeyFromCache instanceof Uint8Array;
|
||||
const activeBackupVersion = await crypto.getActiveSessionBackupVersion();
|
||||
// expand the advanced section to see the active version in the reports
|
||||
await page.locator(".mx_SecureBackupPanel_advanced").locator("..").click();
|
||||
|
||||
return {
|
||||
backupInfo,
|
||||
hasBackupPrivateKeyFromCache,
|
||||
backupPrivateKeyWellFormed,
|
||||
backupKeyIn4S,
|
||||
activeBackupVersion,
|
||||
};
|
||||
});
|
||||
|
||||
if (!backupData) {
|
||||
throw new Error("Crypto module is not available");
|
||||
if (checkBackupKeyInCache) {
|
||||
const cacheDecryptionKeyStatusElement = page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(2) td");
|
||||
await expect(cacheDecryptionKeyStatusElement).toHaveText("cached locally, well formed");
|
||||
}
|
||||
|
||||
const { backupInfo, backupKeyIn4S, hasBackupPrivateKeyFromCache, backupPrivateKeyWellFormed, activeBackupVersion } =
|
||||
backupData;
|
||||
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(5) td")).toHaveText(
|
||||
expectedBackupVersion + " (Algorithm: m.megolm_backup.v1.curve25519-aes-sha2)",
|
||||
);
|
||||
|
||||
// We have a key backup
|
||||
expect(backupInfo).toBeDefined();
|
||||
// The key backup version is as expected
|
||||
expect(backupInfo.version).toBe(expectedBackupVersion);
|
||||
// The active backup version is as expected
|
||||
expect(activeBackupVersion).toBe(expectedBackupVersion);
|
||||
// The backup key is stored in 4S
|
||||
expect(backupKeyIn4S).toBe(true);
|
||||
|
||||
if (checkBackupPrivateKeyInCache) {
|
||||
// The backup key is available locally
|
||||
expect(hasBackupPrivateKeyFromCache).toBe(true);
|
||||
// The backup key is well-formed
|
||||
expect(backupPrivateKeyWellFormed).toBe(true);
|
||||
}
|
||||
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(6) td")).toHaveText(expectedBackupVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -214,11 +189,6 @@ export async function logIntoElement(page: Page, credentials: Credentials, secur
|
||||
// if a securityKey was given, verify the new device
|
||||
if (securityKey !== undefined) {
|
||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Security Key" }).click();
|
||||
|
||||
const useSecurityKey = page.locator(".mx_Dialog").getByRole("button", { name: "use your Security Key" });
|
||||
if (await useSecurityKey.isVisible()) {
|
||||
await useSecurityKey.click();
|
||||
}
|
||||
// Fill in the security key
|
||||
await page.locator(".mx_Dialog").locator('input[type="password"]').fill(securityKey);
|
||||
await page.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click();
|
||||
@@ -246,19 +216,18 @@ export async function logOutOfElement(page: Page, discardKeys: boolean = false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the encryption settings, and verify the current session using the security key.
|
||||
* Open the security settings, and verify the current session using the security key.
|
||||
*
|
||||
* @param app - `ElementAppPage` wrapper for the playwright `Page`.
|
||||
* @param securityKey - The security key (i.e., 4S key), set up during a previous session.
|
||||
*/
|
||||
export async function verifySession(app: ElementAppPage, securityKey: string) {
|
||||
const settings = await app.settings.openUserSettings("Encryption");
|
||||
await settings.getByRole("button", { name: "Verify this device" }).click();
|
||||
const settings = await app.settings.openUserSettings("Security & Privacy");
|
||||
await settings.getByRole("button", { name: "Verify this session" }).click();
|
||||
await app.page.getByRole("button", { name: "Verify with Security Key" }).click();
|
||||
await app.page.locator(".mx_Dialog").locator('input[type="password"]').fill(securityKey);
|
||||
await app.page.getByRole("button", { name: "Continue", disabled: false }).click();
|
||||
await app.page.getByRole("button", { name: "Done" }).click();
|
||||
await app.settings.closeDialog();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -293,52 +262,19 @@ export async function doTwoWaySasVerification(page: Page, verifier: JSHandle<Ver
|
||||
export async function enableKeyBackup(app: ElementAppPage): Promise<string> {
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
await app.page.getByRole("button", { name: "Set up Secure Backup" }).click();
|
||||
const dialog = app.page.locator(".mx_Dialog");
|
||||
// Recovery key is selected by default
|
||||
await dialog.getByRole("button", { name: "Continue" }).click({ timeout: 60000 });
|
||||
|
||||
return await completeCreateSecretStorageDialog(app.page);
|
||||
}
|
||||
// copy the text ourselves
|
||||
const securityKey = await dialog.locator(".mx_CreateSecretStorageDialog_recoveryKey code").textContent();
|
||||
await copyAndContinue(app.page);
|
||||
|
||||
/**
|
||||
* Go through the "Set up Secure Backup" dialog (aka the `CreateSecretStorageDialog`).
|
||||
*
|
||||
* Assumes the dialog is already open for some reason (see also {@link enableKeyBackup}).
|
||||
*
|
||||
* @param page - The playwright `Page` fixture.
|
||||
* @param opts - Options object
|
||||
* @param opts.accountPassword - The user's account password. If we are also resetting cross-signing, then we will need
|
||||
* to upload the public cross-signing keys, which will cause the app to prompt for the password.
|
||||
*
|
||||
* @returns the new recovery key.
|
||||
*/
|
||||
export async function completeCreateSecretStorageDialog(
|
||||
page: Page,
|
||||
opts?: { accountPassword?: string },
|
||||
): Promise<string> {
|
||||
const currentDialogLocator = page.locator(".mx_Dialog");
|
||||
await expect(dialog.getByText("Secure Backup successful")).toBeVisible();
|
||||
await dialog.getByRole("button", { name: "Done" }).click();
|
||||
await expect(dialog.getByText("Secure Backup successful")).not.toBeVisible();
|
||||
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Set up Secure Backup" })).toBeVisible();
|
||||
// "Generate a Security Key" is selected by default
|
||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Save your Security Key" })).toBeVisible();
|
||||
await currentDialogLocator.getByRole("button", { name: "Copy", exact: true }).click();
|
||||
// copy the recovery key to use it later
|
||||
const recoveryKey = await page.evaluate(() => navigator.clipboard.readText());
|
||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
|
||||
// If the device is unverified, there should be a "Setting up keys" step.
|
||||
// If this is not the first time we are setting up cross-signing, the app will prompt for our password; otherwise
|
||||
// the step is quite quick, and playwright can miss it, so we can't test for it.
|
||||
if (opts && Object.hasOwn(opts, "accountPassword")) {
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Setting up keys" })).toBeVisible();
|
||||
await page.getByPlaceholder("Password").fill(opts!.accountPassword);
|
||||
await currentDialogLocator.getByRole("button", { name: "Continue" }).click();
|
||||
}
|
||||
|
||||
// Either way, we end up at a success dialog:
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Secure Backup successful" })).toBeVisible();
|
||||
await currentDialogLocator.getByRole("button", { name: "Done", exact: true }).click();
|
||||
await expect(currentDialogLocator.getByText("Secure Backup successful")).not.toBeVisible();
|
||||
|
||||
return recoveryKey;
|
||||
return securityKey;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,14 +6,14 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { MailpitClient } from "mailpit-api";
|
||||
import { API, Messages } from "mailhog";
|
||||
import { Page } from "@playwright/test";
|
||||
|
||||
import { expect } from "../../element-web-test";
|
||||
|
||||
export async function registerAccountMas(
|
||||
page: Page,
|
||||
mailpit: MailpitClient,
|
||||
mailhog: API,
|
||||
username: string,
|
||||
email: string,
|
||||
password: string,
|
||||
@@ -27,13 +27,13 @@ export async function registerAccountMas(
|
||||
await page.getByRole("textbox", { name: "Confirm Password" }).fill(password);
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
let code: string;
|
||||
let messages: Messages;
|
||||
await expect(async () => {
|
||||
const messages = await mailpit.listMessages();
|
||||
expect(messages.messages[0].To[0].Address).toEqual(email);
|
||||
const text = await mailpit.renderMessageText(messages.messages[0].ID);
|
||||
[, code] = text.match(/Your verification code to confirm this email address is: (\d{6})/);
|
||||
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(/Your verification code to confirm this email address is: (\d{6})/);
|
||||
|
||||
await page.getByRole("textbox", { name: "6-digit code" }).fill(code);
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
@@ -19,7 +19,7 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
context,
|
||||
page,
|
||||
homeserver,
|
||||
mailpitClient,
|
||||
mailhogClient,
|
||||
mas,
|
||||
}, testInfo) => {
|
||||
await page.clock.install();
|
||||
@@ -33,7 +33,7 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
const userId = `alice_${testInfo.testId}`;
|
||||
await registerAccountMas(page, mailpitClient, userId, "alice@email.com", "Pa$sW0rD!");
|
||||
await registerAccountMas(page, mailhogClient, userId, "alice@email.com", "Pa$sW0rD!");
|
||||
|
||||
// Eventually, we should end up at the home screen.
|
||||
await expect(page).toHaveURL(/\/#\/home$/, { timeout: 10000 });
|
||||
|
||||
@@ -526,10 +526,9 @@ class Helpers {
|
||||
await expect(threadPanel).toBeVisible();
|
||||
await threadPanel.evaluate(($panel) => {
|
||||
const $button = $panel.querySelector<HTMLElement>('[data-testid="base-card-back-button"]');
|
||||
const title = $panel.querySelector<HTMLElement>(".mx_BaseCard_header_title")?.textContent;
|
||||
// If the Threads back button is present then click it - the
|
||||
// threads button can open either threads list or thread panel
|
||||
if ($button && title !== "Threads") {
|
||||
if ($button) {
|
||||
$button.click();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -34,7 +34,7 @@ test.describe("Email Registration", async () => {
|
||||
test(
|
||||
"registers an account and lands on the home page",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, mailpitClient, request, checkA11y }) => {
|
||||
async ({ page, mailhogClient, request, checkA11y }) => {
|
||||
await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible();
|
||||
// Hide the server text as it contains the randomly allocated Homeserver port
|
||||
const screenshotOptions = { mask: [page.locator(".mx_ServerPicker_server")] };
|
||||
@@ -51,11 +51,10 @@ test.describe("Email Registration", async () => {
|
||||
|
||||
await expect(page.getByText("An error was encountered when sending the email")).not.toBeVisible();
|
||||
|
||||
const messages = await mailpitClient.listMessages();
|
||||
expect(messages.messages).toHaveLength(1);
|
||||
expect(messages.messages[0].To[0].Address).toEqual("alice@email.com");
|
||||
const text = await mailpitClient.renderMessageText(messages.messages[0].ID);
|
||||
const [emailLink] = text.match(/http.+/);
|
||||
const messages = await mailhogClient.messages();
|
||||
expect(messages.items).toHaveLength(1);
|
||||
expect(messages.items[0].to).toEqual("alice@email.com");
|
||||
const [emailLink] = messages.items[0].text.match(/http.+/);
|
||||
await request.get(emailLink); // "Click" the link in the email
|
||||
|
||||
await expect(page.getByText("Welcome alice")).toBeVisible();
|
||||
|
||||
@@ -42,7 +42,7 @@ test.describe("Memberlist", () => {
|
||||
await app.viewRoomByName(ROOM_NAME);
|
||||
const memberlist = await app.toggleMemberlistPanel();
|
||||
await expect(memberlist.locator(".mx_MemberTileView")).toHaveCount(4);
|
||||
await expect(memberlist.getByText("Invited")).toHaveCount(1);
|
||||
await expect(memberlist.getByText("(Invited)")).toHaveCount(1);
|
||||
await expect(page.locator(".mx_MemberListView")).toMatchScreenshot("with-four-members.png");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { test, expect } from "./index";
|
||||
import { checkDeviceIsCrossSigned } from "../../crypto/utils";
|
||||
import { bootstrapCrossSigningForClient } from "../../../pages/client";
|
||||
|
||||
test.describe("Advanced section in Encryption tab", () => {
|
||||
test.beforeEach(async ({ page, app, homeserver, credentials, util }) => {
|
||||
const clientHandle = await app.client.prepareClient();
|
||||
// Reset cross signing in order to have a verified session
|
||||
await bootstrapCrossSigningForClient(clientHandle, credentials, true);
|
||||
});
|
||||
|
||||
test("should show the encryption details", { tag: "@screenshot" }, async ({ page, app, util }) => {
|
||||
await util.openEncryptionTab();
|
||||
const section = util.getEncryptionDetailsSection();
|
||||
|
||||
const deviceId = await page.evaluate(() => window.mxMatrixClientPeg.get().getDeviceId());
|
||||
await expect(section.getByText(deviceId)).toBeVisible();
|
||||
|
||||
await expect(section).toMatchScreenshot("encryption-details.png", {
|
||||
mask: [section.getByTestId("deviceId"), section.getByTestId("sessionKey")],
|
||||
});
|
||||
});
|
||||
|
||||
test("should show the import room keys dialog", async ({ page, app, util }) => {
|
||||
await util.openEncryptionTab();
|
||||
const section = util.getEncryptionDetailsSection();
|
||||
|
||||
await section.getByRole("button", { name: "Import keys" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Import room keys" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("should show the export room keys dialog", async ({ page, app, util }) => {
|
||||
await util.openEncryptionTab();
|
||||
const section = util.getEncryptionDetailsSection();
|
||||
|
||||
await section.getByRole("button", { name: "Export keys" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Export room keys" })).toBeVisible();
|
||||
});
|
||||
|
||||
test(
|
||||
"should reset the cryptographic identity",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, credentials, util }) => {
|
||||
const tab = await util.openEncryptionTab();
|
||||
const section = util.getEncryptionDetailsSection();
|
||||
|
||||
await section.getByRole("button", { name: "Reset cryptographic identity" }).click();
|
||||
await expect(util.getEncryptionTabContent()).toMatchScreenshot("reset-cryptographic-identity.png");
|
||||
await tab.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// Fill password dialog and validate
|
||||
const dialog = page.locator(".mx_InteractiveAuthDialog");
|
||||
await dialog.getByRole("textbox", { name: "Password" }).fill(credentials.password);
|
||||
await dialog.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
await expect(section.getByRole("button", { name: "Reset cryptographic identity" })).toBeVisible();
|
||||
|
||||
// After resetting the identity, the user should set up a new recovery key
|
||||
await expect(
|
||||
util.getEncryptionRecoverySection().getByRole("button", { name: "Set up recovery" }),
|
||||
).toBeVisible();
|
||||
|
||||
await checkDeviceIsCrossSigned(app);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -1,96 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import { test, expect } from ".";
|
||||
import {
|
||||
checkDeviceIsConnectedKeyBackup,
|
||||
checkDeviceIsCrossSigned,
|
||||
createBot,
|
||||
deleteCachedSecrets,
|
||||
verifySession,
|
||||
} from "../../crypto/utils";
|
||||
|
||||
test.describe("Encryption tab", () => {
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
});
|
||||
|
||||
let recoveryKey: GeneratedSecretStorageKey;
|
||||
let expectedBackupVersion: string;
|
||||
|
||||
test.beforeEach(async ({ page, homeserver, credentials }) => {
|
||||
// The bot bootstraps cross-signing, creates a key backup and sets up a recovery key
|
||||
const res = await createBot(page, homeserver, credentials);
|
||||
recoveryKey = res.recoveryKey;
|
||||
expectedBackupVersion = res.expectedBackupVersion;
|
||||
});
|
||||
|
||||
test(
|
||||
"should show a 'Verify this device' button if the device is unverified",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, util }) => {
|
||||
const dialog = await util.openEncryptionTab();
|
||||
const content = util.getEncryptionTabContent();
|
||||
|
||||
// The user's device is in an unverified state, therefore the only option available to them here is to verify it
|
||||
const verifyButton = dialog.getByRole("button", { name: "Verify this device" });
|
||||
await expect(verifyButton).toBeVisible();
|
||||
await expect(content).toMatchScreenshot("verify-device-encryption-tab.png");
|
||||
await verifyButton.click();
|
||||
|
||||
await util.verifyDevice(recoveryKey);
|
||||
|
||||
await expect(content).toMatchScreenshot("default-tab.png", {
|
||||
mask: [content.getByTestId("deviceId"), content.getByTestId("sessionKey")],
|
||||
});
|
||||
|
||||
// Check that our device is now cross-signed
|
||||
await checkDeviceIsCrossSigned(app);
|
||||
|
||||
// Check that the current device is connected to key backup
|
||||
// The backup decryption key should be in cache also, as we got it directly from the 4S
|
||||
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
|
||||
},
|
||||
);
|
||||
|
||||
// Test what happens if the cross-signing secrets are in secret storage but are not cached in the local DB.
|
||||
//
|
||||
// This can happen if we verified another device and secret-gossiping failed, or the other device itself lacked the secrets.
|
||||
// We simulate this case by deleting the cached secrets in the indexedDB.
|
||||
test(
|
||||
"should prompt to enter the recovery key when the secrets are not cached locally",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, util }) => {
|
||||
await verifySession(app, "new passphrase");
|
||||
// We need to delete the cached secrets
|
||||
await deleteCachedSecrets(page);
|
||||
|
||||
await util.openEncryptionTab();
|
||||
// We ask the user to enter the recovery key
|
||||
const dialog = util.getEncryptionTabContent();
|
||||
const enterKeyButton = dialog.getByRole("button", { name: "Enter recovery key" });
|
||||
await expect(enterKeyButton).toBeVisible();
|
||||
await expect(dialog).toMatchScreenshot("out-of-sync-recovery.png");
|
||||
await enterKeyButton.click();
|
||||
|
||||
// Fill the recovery key
|
||||
await util.enterRecoveryKey(recoveryKey);
|
||||
await expect(dialog).toMatchScreenshot("default-tab.png", {
|
||||
mask: [dialog.getByTestId("deviceId"), dialog.getByTestId("sessionKey")],
|
||||
});
|
||||
|
||||
// Check that our device is now cross-signed
|
||||
await checkDeviceIsCrossSigned(app);
|
||||
|
||||
// Check that the current device is connected to key backup
|
||||
// The backup decryption key should be in cache also, as we got it directly from the 4S
|
||||
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -18,8 +18,6 @@ export { expect };
|
||||
export const test = base.extend<{
|
||||
util: Helpers;
|
||||
}>({
|
||||
displayName: "Alice",
|
||||
|
||||
util: async ({ page, app, bot }, use) => {
|
||||
await use(new Helpers(page, app));
|
||||
},
|
||||
@@ -69,20 +67,6 @@ class Helpers {
|
||||
return this.page.getByTestId("encryptionTab");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the recovery section
|
||||
*/
|
||||
getEncryptionRecoverySection() {
|
||||
return this.page.getByTestId("recoveryPanel");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the encryption details section
|
||||
*/
|
||||
getEncryptionDetailsSection() {
|
||||
return this.page.getByTestId("encryptionDetails");
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default key id of the secret storage to `null`
|
||||
*/
|
||||
@@ -108,6 +92,6 @@ class Helpers {
|
||||
const clipboardContent = await this.app.getClipboard();
|
||||
await dialog.getByRole("textbox").fill(clipboardContent);
|
||||
await dialog.getByRole("button", { name: confirmButtonLabel }).click();
|
||||
await expect(this.getEncryptionRecoverySection()).toMatchScreenshot("default-recovery.png");
|
||||
await expect(dialog).toMatchScreenshot("default-recovery.png");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,17 +5,50 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import { test, expect } from ".";
|
||||
import { checkDeviceIsConnectedKeyBackup, createBot, verifySession } from "../../crypto/utils";
|
||||
import {
|
||||
checkDeviceIsConnectedKeyBackup,
|
||||
checkDeviceIsCrossSigned,
|
||||
createBot,
|
||||
deleteCachedSecrets,
|
||||
verifySession,
|
||||
} from "../../crypto/utils";
|
||||
|
||||
test.describe("Recovery section in Encryption tab", () => {
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
});
|
||||
|
||||
let recoveryKey: GeneratedSecretStorageKey;
|
||||
let expectedBackupVersion: string;
|
||||
|
||||
test.beforeEach(async ({ page, homeserver, credentials }) => {
|
||||
// The bot bootstraps cross-signing, creates a key backup and sets up a recovery key
|
||||
await createBot(page, homeserver, credentials);
|
||||
const res = await createBot(page, homeserver, credentials);
|
||||
recoveryKey = res.recoveryKey;
|
||||
expectedBackupVersion = res.expectedBackupVersion;
|
||||
});
|
||||
|
||||
test("should verify the device", { tag: "@screenshot" }, async ({ page, app, util }) => {
|
||||
const dialog = await util.openEncryptionTab();
|
||||
|
||||
// The user's device is in an unverified state, therefore the only option available to them here is to verify it
|
||||
const verifyButton = dialog.getByRole("button", { name: "Verify this device" });
|
||||
await expect(verifyButton).toBeVisible();
|
||||
await expect(util.getEncryptionTabContent()).toMatchScreenshot("verify-device-encryption-tab.png");
|
||||
await verifyButton.click();
|
||||
|
||||
await util.verifyDevice(recoveryKey);
|
||||
await expect(util.getEncryptionTabContent()).toMatchScreenshot("default-recovery.png");
|
||||
|
||||
// Check that our device is now cross-signed
|
||||
await checkDeviceIsCrossSigned(app);
|
||||
|
||||
// Check that the current device is connected to key backup
|
||||
// The backup decryption key should be in cache also, as we got it directly from the 4S
|
||||
await app.closeDialog();
|
||||
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, true);
|
||||
});
|
||||
|
||||
test(
|
||||
@@ -28,7 +61,7 @@ test.describe("Recovery section in Encryption tab", () => {
|
||||
// The user can only change the recovery key
|
||||
const changeButton = dialog.getByRole("button", { name: "Change recovery key" });
|
||||
await expect(changeButton).toBeVisible();
|
||||
await expect(util.getEncryptionRecoverySection()).toMatchScreenshot("default-recovery.png");
|
||||
await expect(util.getEncryptionTabContent()).toMatchScreenshot("default-recovery.png");
|
||||
await changeButton.click();
|
||||
|
||||
// Display the new recovery key and click on the copy button
|
||||
@@ -56,7 +89,7 @@ test.describe("Recovery section in Encryption tab", () => {
|
||||
const dialog = await util.openEncryptionTab();
|
||||
const setupButton = dialog.getByRole("button", { name: "Set up recovery" });
|
||||
await expect(setupButton).toBeVisible();
|
||||
await expect(util.getEncryptionRecoverySection()).toMatchScreenshot("set-up-recovery.png");
|
||||
await expect(util.getEncryptionTabContent()).toMatchScreenshot("set-up-recovery.png");
|
||||
await setupButton.click();
|
||||
|
||||
// Display an informative panel about the recovery key
|
||||
@@ -82,7 +115,42 @@ test.describe("Recovery section in Encryption tab", () => {
|
||||
// The recovery key is now set up and the user can change it
|
||||
await expect(dialog.getByRole("button", { name: "Change recovery key" })).toBeVisible();
|
||||
|
||||
await app.closeDialog();
|
||||
// Check that the current device is connected to key backup and the backup version is the expected one
|
||||
await checkDeviceIsConnectedKeyBackup(app, "1", true);
|
||||
await checkDeviceIsConnectedKeyBackup(page, "1", true);
|
||||
});
|
||||
|
||||
// Test what happens if the cross-signing secrets are in secret storage but are not cached in the local DB.
|
||||
//
|
||||
// This can happen if we verified another device and secret-gossiping failed, or the other device itself lacked the secrets.
|
||||
// We simulate this case by deleting the cached secrets in the indexedDB.
|
||||
test(
|
||||
"should enter the recovery key when the secrets are not cached",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, util }) => {
|
||||
await verifySession(app, "new passphrase");
|
||||
// We need to delete the cached secrets
|
||||
await deleteCachedSecrets(page);
|
||||
|
||||
await util.openEncryptionTab();
|
||||
// We ask the user to enter the recovery key
|
||||
const dialog = util.getEncryptionTabContent();
|
||||
const enterKeyButton = dialog.getByRole("button", { name: "Enter recovery key" });
|
||||
await expect(enterKeyButton).toBeVisible();
|
||||
await expect(dialog).toMatchScreenshot("out-of-sync-recovery.png");
|
||||
await enterKeyButton.click();
|
||||
|
||||
// Fill the recovery key
|
||||
await util.enterRecoveryKey(recoveryKey);
|
||||
await expect(dialog).toMatchScreenshot("default-recovery.png");
|
||||
|
||||
// Check that our device is now cross-signed
|
||||
await checkDeviceIsCrossSigned(app);
|
||||
|
||||
// Check that the current device is connected to key backup
|
||||
// The backup decryption key should be in cache also, as we got it directly from the 4S
|
||||
await app.closeDialog();
|
||||
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, true);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -69,6 +69,11 @@ const test = base.extend<{
|
||||
});
|
||||
|
||||
test.describe("Sliding Sync", () => {
|
||||
test.skip(
|
||||
({ homeserverType }) => homeserverType === "pinecone",
|
||||
"due to a bug in Pinecone https://github.com/element-hq/dendrite/issues/3490",
|
||||
);
|
||||
|
||||
const checkOrder = async (wantOrder: string[], page: Page) => {
|
||||
await expect(page.getByRole("group", { name: "Rooms" }).locator(".mx_RoomTile_title")).toHaveText(wantOrder);
|
||||
};
|
||||
|
||||
@@ -192,6 +192,7 @@ export class Bot extends Client {
|
||||
|
||||
await clientHandle.evaluate(async (cli) => {
|
||||
await cli.initRustCrypto({ useIndexedDB: false });
|
||||
cli.setGlobalErrorOnUnknownDevices(false);
|
||||
await cli.startClient();
|
||||
});
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Fixtures } from "../../../element-web-test.ts";
|
||||
|
||||
export const consentHomeserver: Fixtures = {
|
||||
_homeserver: [
|
||||
async ({ _homeserver: container, mailpit }, use) => {
|
||||
async ({ _homeserver: container, mailhog }, use) => {
|
||||
container
|
||||
.withCopyDirectoriesToContainer([
|
||||
{ source: "playwright/plugins/homeserver/synapse/res", target: "/data/res" },
|
||||
@@ -18,7 +18,7 @@ export const consentHomeserver: Fixtures = {
|
||||
.withConfig({
|
||||
email: {
|
||||
enable_notifs: false,
|
||||
smtp_host: "mailpit",
|
||||
smtp_host: "mailhog",
|
||||
smtp_port: 1025,
|
||||
smtp_user: "username",
|
||||
smtp_pass: "password",
|
||||
|
||||
@@ -10,13 +10,13 @@ import { Fixtures } from "../../../element-web-test.ts";
|
||||
|
||||
export const emailHomeserver: Fixtures = {
|
||||
_homeserver: [
|
||||
async ({ _homeserver: container, mailpit }, use) => {
|
||||
async ({ _homeserver: container, mailhog }, use) => {
|
||||
container.withConfig({
|
||||
enable_registration_without_verification: undefined,
|
||||
disable_msisdn_registration: undefined,
|
||||
registrations_require_3pid: ["email"],
|
||||
email: {
|
||||
smtp_host: "mailpit",
|
||||
smtp_host: "mailhog",
|
||||
smtp_port: 1025,
|
||||
notif_from: "Your Friendly %(app)s homeserver <noreply@example.com>",
|
||||
app_name: "my_branded_matrix_server",
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Fixtures } from "../../../element-web-test.ts";
|
||||
|
||||
export const masHomeserver: Fixtures = {
|
||||
mas: [
|
||||
async ({ _homeserver: homeserver, logger, network, postgres, mailpit }, use) => {
|
||||
async ({ _homeserver: homeserver, logger, network, postgres, mailhog }, use) => {
|
||||
const config = {
|
||||
clients: [
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { test as base } from "@playwright/test";
|
||||
import { MailpitClient } from "mailpit-api";
|
||||
import mailhog from "mailhog";
|
||||
import { Network, StartedNetwork } from "testcontainers";
|
||||
import { PostgreSqlContainer, StartedPostgreSqlContainer } from "@testcontainers/postgresql";
|
||||
|
||||
@@ -14,13 +14,13 @@ import { SynapseConfig, SynapseContainer } from "./testcontainers/synapse.ts";
|
||||
import { Logger } from "./logger.ts";
|
||||
import { StartedMatrixAuthenticationServiceContainer } from "./testcontainers/mas.ts";
|
||||
import { HomeserverContainer, StartedHomeserverContainer } from "./testcontainers/HomeserverContainer.ts";
|
||||
import { MailhogContainer, StartedMailhogContainer } from "./testcontainers/mailpit.ts";
|
||||
import { MailhogContainer, StartedMailhogContainer } from "./testcontainers/mailhog.ts";
|
||||
import { OAuthServer } from "./plugins/oauth_server";
|
||||
import { DendriteContainer, PineconeContainer } from "./testcontainers/dendrite.ts";
|
||||
import { HomeserverType } from "./plugins/homeserver";
|
||||
|
||||
export interface TestFixtures {
|
||||
mailpitClient: MailpitClient;
|
||||
mailhogClient: mailhog.API;
|
||||
}
|
||||
|
||||
export interface Services {
|
||||
@@ -28,7 +28,7 @@ export interface Services {
|
||||
|
||||
network: StartedNetwork;
|
||||
postgres: StartedPostgreSqlContainer;
|
||||
mailpit: StartedMailhogContainer;
|
||||
mailhog: StartedMailhogContainer;
|
||||
|
||||
synapseConfig: SynapseConfig;
|
||||
_homeserver: HomeserverContainer<any>;
|
||||
@@ -90,20 +90,20 @@ export const test = base.extend<TestFixtures, Services & Options>({
|
||||
{ scope: "worker" },
|
||||
],
|
||||
|
||||
mailpit: [
|
||||
mailhog: [
|
||||
async ({ logger, network }, use) => {
|
||||
const container = await new MailhogContainer()
|
||||
.withNetwork(network)
|
||||
.withNetworkAliases("mailpit")
|
||||
.withLogConsumer(logger.getConsumer("mailpit"))
|
||||
.withNetworkAliases("mailhog")
|
||||
.withLogConsumer(logger.getConsumer("mailhog"))
|
||||
.start();
|
||||
await use(container);
|
||||
await container.stop();
|
||||
},
|
||||
{ scope: "worker" },
|
||||
],
|
||||
mailpitClient: async ({ mailpit: container }, use) => {
|
||||
await container.client.deleteMessages();
|
||||
mailhogClient: async ({ mailhog: container }, use) => {
|
||||
await container.client.deleteAll();
|
||||
await use(container.client);
|
||||
},
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
@@ -6,16 +6,13 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers";
|
||||
import { MailpitClient } from "mailpit-api";
|
||||
import mailhog from "mailhog";
|
||||
|
||||
export class MailhogContainer extends GenericContainer {
|
||||
constructor() {
|
||||
super("axllent/mailpit:latest");
|
||||
super("mailhog/mailhog:latest");
|
||||
|
||||
this.withExposedPorts(8025).withWaitStrategy(Wait.forListeningPorts()).withEnvironment({
|
||||
MP_SMTP_AUTH_ALLOW_INSECURE: "true",
|
||||
MP_SMTP_AUTH_ACCEPT_ANY: "true",
|
||||
});
|
||||
this.withExposedPorts(8025).withWaitStrategy(Wait.forListeningPorts());
|
||||
}
|
||||
|
||||
public override async start(): Promise<StartedMailhogContainer> {
|
||||
@@ -24,10 +21,10 @@ export class MailhogContainer extends GenericContainer {
|
||||
}
|
||||
|
||||
export class StartedMailhogContainer extends AbstractStartedContainer {
|
||||
public readonly client: MailpitClient;
|
||||
public readonly client: mailhog.API;
|
||||
|
||||
constructor(container: StartedTestContainer) {
|
||||
super(container);
|
||||
this.client = new MailpitClient(`http://${container.getHost()}:${container.getMappedPort(8025)}`);
|
||||
this.client = mailhog({ host: container.getHost(), port: container.getMappedPort(8025) });
|
||||
}
|
||||
}
|
||||
@@ -92,7 +92,7 @@ const DEFAULT_CONFIG = {
|
||||
reply_to: '"Authentication Service" <root@localhost>',
|
||||
transport: "smtp",
|
||||
mode: "plain",
|
||||
hostname: "mailpit",
|
||||
hostname: "mailhog",
|
||||
port: 1025,
|
||||
username: "username",
|
||||
password: "password",
|
||||
|
||||
@@ -19,7 +19,7 @@ import { HomeserverContainer, StartedHomeserverContainer } from "./HomeserverCon
|
||||
import { StartedMatrixAuthenticationServiceContainer } from "./mas.ts";
|
||||
import { Api, ClientServerApi, Verb } from "../plugins/utils/api.ts";
|
||||
|
||||
const TAG = "develop@sha256:098126c6be750dffaff5bd19db254609aadaf34f76c70f2dca9821cb12428613";
|
||||
const TAG = "develop@sha256:e48308d68dec00af6ce43a05785d475de21a37bc2afaabb440d3a575bcc3d57d";
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
server_name: "localhost",
|
||||
|
||||
@@ -134,7 +134,6 @@
|
||||
@import "./views/dialogs/_ConfirmUserActionDialog.pcss";
|
||||
@import "./views/dialogs/_CreateRoomDialog.pcss";
|
||||
@import "./views/dialogs/_CreateSubspaceDialog.pcss";
|
||||
@import "./views/dialogs/_Crypto.pcss";
|
||||
@import "./views/dialogs/_DeactivateAccountDialog.pcss";
|
||||
@import "./views/dialogs/_DevtoolsDialog.pcss";
|
||||
@import "./views/dialogs/_ExportDialog.pcss";
|
||||
@@ -284,7 +283,6 @@
|
||||
@import "./views/rooms/_EventTile.pcss";
|
||||
@import "./views/rooms/_HistoryTile.pcss";
|
||||
@import "./views/rooms/_IRCLayout.pcss";
|
||||
@import "./views/rooms/_InvitedIconView.pcss";
|
||||
@import "./views/rooms/_JumpToBottomButton.pcss";
|
||||
@import "./views/rooms/_LinkPreviewGroup.pcss";
|
||||
@import "./views/rooms/_LinkPreviewWidget.pcss";
|
||||
@@ -355,10 +353,8 @@
|
||||
@import "./views/settings/_ThemeChoicePanel.pcss";
|
||||
@import "./views/settings/_UpdateCheckButton.pcss";
|
||||
@import "./views/settings/_UserProfileSettings.pcss";
|
||||
@import "./views/settings/encryption/_AdvancedPanel.pcss";
|
||||
@import "./views/settings/encryption/_ChangeRecoveryKey.pcss";
|
||||
@import "./views/settings/encryption/_EncryptionCard.pcss";
|
||||
@import "./views/settings/encryption/_ResetIdentityPanel.pcss";
|
||||
@import "./views/settings/tabs/_SettingsBanner.pcss";
|
||||
@import "./views/settings/tabs/_SettingsIndent.pcss";
|
||||
@import "./views/settings/tabs/_SettingsSection.pcss";
|
||||
|
||||
@@ -37,6 +37,27 @@ Please see LICENSE files in the repository root for full details.
|
||||
line-height: $font-24px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.mx_UserMenu_dndBadge {
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
right: -7px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
position: absolute;
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
mask-repeat: no-repeat;
|
||||
background-color: $alert;
|
||||
mask-image: url("$(res)/img/element-icons/roomlist/dnd.svg");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_IconizedContextMenu {
|
||||
@@ -137,6 +158,14 @@ Please see LICENSE files in the repository root for full details.
|
||||
mask-image: url("@vector-im/compound-design-tokens/icons/home-solid.svg");
|
||||
}
|
||||
|
||||
.mx_UserMenu_iconDnd::before {
|
||||
mask-image: url("$(res)/img/element-icons/roomlist/dnd.svg");
|
||||
}
|
||||
|
||||
.mx_UserMenu_iconDndOff::before {
|
||||
mask-image: url("$(res)/img/element-icons/roomlist/dnd-cross.svg");
|
||||
}
|
||||
|
||||
.mx_UserMenu_iconBell::before {
|
||||
mask-image: url("$(res)/img/element-icons/notifications.svg");
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.mx_Crypto {
|
||||
table {
|
||||
margin: var(--cpd-space-4x) 0;
|
||||
text-align: left;
|
||||
border-spacing: var(--cpd-space-2x) 0;
|
||||
|
||||
thead {
|
||||
font: var(--cpd-font-heading-sm-semibold);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,8 +26,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
}
|
||||
|
||||
&.mx_UserPill_me,
|
||||
&.mx_AtRoomPill,
|
||||
&.mx_KeywordPill {
|
||||
&.mx_AtRoomPill {
|
||||
background-color: var(--cpd-color-bg-critical-primary) !important; /* To override .markdown-body */
|
||||
}
|
||||
|
||||
@@ -46,8 +45,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
}
|
||||
|
||||
/* We don't want to indicate clickability */
|
||||
&.mx_AtRoomPill:hover,
|
||||
&.mx_KeywordPill:hover {
|
||||
&.mx_AtRoomPill:hover {
|
||||
background-color: var(--cpd-color-bg-critical-primary) !important; /* To override .markdown-body */
|
||||
cursor: unset;
|
||||
}
|
||||
|
||||
@@ -35,8 +35,6 @@ Please see LICENSE files in the repository root for full details.
|
||||
.mx_DisambiguatedProfile_mxid {
|
||||
margin-inline-start: 0;
|
||||
font: var(--cpd-font-body-sm-regular);
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
span:not(.mx_DisambiguatedProfile_mxid) {
|
||||
|
||||
@@ -15,48 +15,41 @@ Please see LICENSE files in the repository root for full details.
|
||||
flex: unset;
|
||||
}
|
||||
|
||||
.mx_ThreadPanelHeader {
|
||||
height: 60px;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
padding: 16px;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--cpd-color-gray-400);
|
||||
|
||||
.mx_AccessibleButton {
|
||||
font-size: 12px;
|
||||
color: $secondary-content;
|
||||
}
|
||||
|
||||
.mx_ThreadPanel_vertical_separator {
|
||||
height: 28px;
|
||||
margin-left: var(--cpd-space-3x);
|
||||
margin-right: var(--cpd-space-2x);
|
||||
border-left: 1px solid var(--cpd-color-gray-400);
|
||||
}
|
||||
|
||||
.mx_ThreadPanel_dropdown {
|
||||
font: var(--cpd-font-body-sm-regular);
|
||||
padding: 3px $spacing-4 3px $spacing-8;
|
||||
border-radius: 4px;
|
||||
line-height: 1.5;
|
||||
user-select: none;
|
||||
|
||||
&:hover,
|
||||
&[aria-expanded="true"] {
|
||||
background: $quinary-content;
|
||||
.mx_BaseCard_header {
|
||||
.mx_BaseCard_header_title {
|
||||
.mx_AccessibleButton {
|
||||
font-size: 12px;
|
||||
color: $secondary-content;
|
||||
}
|
||||
|
||||
&::before {
|
||||
margin-left: 2px;
|
||||
content: "";
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: currentColor;
|
||||
mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg");
|
||||
mask-size: 100%;
|
||||
mask-repeat: no-repeat;
|
||||
float: right;
|
||||
.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;
|
||||
line-height: 1.5;
|
||||
user-select: none;
|
||||
|
||||
&:hover,
|
||||
&[aria-expanded="true"] {
|
||||
background: $quinary-content;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: currentColor;
|
||||
mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg");
|
||||
mask-size: 100%;
|
||||
mask-repeat: no-repeat;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
}
|
||||
|
||||
.mx_UserInfo_container {
|
||||
padding: var(--cpd-space-2x) 0 var(--cpd-space-4x);
|
||||
padding: var(--cpd-space-4x) 0;
|
||||
margin: 0 var(--cpd-space-4x);
|
||||
|
||||
.mx_UserInfo_container_verifyButton {
|
||||
@@ -65,7 +65,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
}
|
||||
|
||||
.mx_UserInfo_avatar {
|
||||
margin: var(--cpd-space-12x) var(--cpd-space-4x) 0 var(--cpd-space-4x);
|
||||
margin: $spacing-24 $spacing-32 0 $spacing-32;
|
||||
|
||||
.mx_UserInfo_avatar_transition {
|
||||
max-width: 120px;
|
||||
@@ -98,18 +98,8 @@ Please see LICENSE files in the repository root for full details.
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.mx_UserInfo_header {
|
||||
margin-bottom: var(--cpd-space-8x);
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.mx_UserInfo_profile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--cpd-space-1x);
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: $font-20px;
|
||||
line-height: $font-25px;
|
||||
|
||||
@@ -129,45 +119,8 @@ Please see LICENSE files in the repository root for full details.
|
||||
}
|
||||
}
|
||||
|
||||
.mx_UserInfo_profile_name {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.mx_UserInfo_profile_mxid {
|
||||
color: var(--cpd-color-text-secondary);
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.mx_UserInfo_profileStatus {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.mx_UserInfo_timezone {
|
||||
height: 20px;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/** Overrides for the copy to clipboard button **/
|
||||
.mx_CopyableText {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mx_CopyableText_copyButton {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: unset;
|
||||
padding-left: var(--cpd-space-2x);
|
||||
}
|
||||
|
||||
.mx_CopyableText_copyButton::before {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: var(--cpd-color-icon-secondary-alpha);
|
||||
margin: var(--cpd-space-1x) 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -135,6 +135,12 @@ $left-gutter: 64px;
|
||||
}
|
||||
}
|
||||
|
||||
&.mx_EventTile_highlight,
|
||||
&.mx_EventTile_highlight .markdown-body,
|
||||
&.mx_EventTile_highlight .mx_EventTile_edited {
|
||||
color: $alert;
|
||||
}
|
||||
|
||||
&.mx_EventTile_bubbleContainer {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 100px;
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.mx_InvitedIconView {
|
||||
color: var(--cpd-color-icon-tertiary);
|
||||
}
|
||||
@@ -14,10 +14,4 @@ Please see LICENSE files in the repository root for full details.
|
||||
.mx_MemberListView_container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mx_MemberListView_separator {
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-top: 2px solid var(--cpd-color-bg-subtle-primary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,11 +31,9 @@ Please see LICENSE files in the repository root for full details.
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mx_MemberTileView_userLabel {
|
||||
.mx_MemberTileView_user_label {
|
||||
font: var(--cpd-font-body-sm-regular);
|
||||
font-size: 13px;
|
||||
color: var(--cpd-color-text-secondary);
|
||||
margin-left: var(--cpd-space-4x);
|
||||
}
|
||||
|
||||
.mx_MemberTileView_avatar {
|
||||
@@ -43,4 +41,18 @@ Please see LICENSE files in the repository root for full details.
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.mx_E2EIconView {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mx_E2EIconView_warning {
|
||||
color: var(--cpd-color-icon-critical-primary);
|
||||
}
|
||||
|
||||
.mx_E2EIconView_verified {
|
||||
color: var(--cpd-color-icon-success-primary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,6 @@ Please see LICENSE files in the repository root for full details.
|
||||
padding: var(--cpd-space-1-5x);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
font: var(--cpd-font-body-sm-medium);
|
||||
|
||||
/* RoomAvatar doesn't pass classes down to avatar
|
||||
So set style here
|
||||
@@ -84,12 +83,6 @@ Please see LICENSE files in the repository root for full details.
|
||||
color: $primary-content;
|
||||
background: var(--cpd-color-bg-subtle-primary);
|
||||
}
|
||||
|
||||
&.mx_FacePile_toggled {
|
||||
background: var(--cpd-color-bg-success-subtle);
|
||||
color: var(--cpd-color-text-action-accent);
|
||||
font: var(--cpd-font-body-sm-semibold);
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomHeader .mx_BaseAvatar {
|
||||
@@ -100,7 +93,3 @@ Please see LICENSE files in the repository root for full details.
|
||||
/* Workaround for https://github.com/element-hq/compound/issues/331 */
|
||||
min-width: 240px;
|
||||
}
|
||||
|
||||
.mx_RoomHeader .mx_RoomHeader_toggled {
|
||||
color: var(--cpd-color-icon-accent-primary);
|
||||
}
|
||||
|
||||
@@ -20,14 +20,8 @@ Please see LICENSE files in the repository root for full details.
|
||||
margin-left: var(--cpd-space-6x);
|
||||
flex-grow: 1;
|
||||
}
|
||||
.mx_UserIdentityWarning_main.critical {
|
||||
color: var(--cpd-color-text-critical-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
.mx_UserIdentityWarning.critical {
|
||||
background: linear-gradient(180deg, var(--cpd-color-red-100) 0%, var(--cpd-color-theme-bg) 100%);
|
||||
}
|
||||
|
||||
.mx_MessageComposer.mx_MessageComposer--compact > .mx_UserIdentityWarning {
|
||||
margin-left: calc(-25px + var(--RoomView_MessageList-padding));
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.mx_EncryptionDetails,
|
||||
.mx_OtherSettings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--cpd-space-6x);
|
||||
width: 100%;
|
||||
align-items: start;
|
||||
|
||||
.mx_EncryptionDetails_session_title,
|
||||
.mx_OtherSettings_title {
|
||||
font: var(--cpd-font-body-lg-semibold);
|
||||
padding-bottom: var(--cpd-space-2x);
|
||||
border-bottom: 1px solid var(--cpd-color-gray-400);
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EncryptionDetails {
|
||||
.mx_EncryptionDetails_session {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--cpd-space-4x);
|
||||
width: 100%;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
|
||||
> span {
|
||||
width: 50%;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
> div:nth-child(odd) {
|
||||
background-color: var(--cpd-color-gray-200);
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EncryptionDetails_buttons {
|
||||
display: flex;
|
||||
gap: var(--cpd-space-4x);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.mx_ResetIdentityPanel {
|
||||
.mx_ResetIdentityPanel_content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--cpd-space-3x);
|
||||
|
||||
> span {
|
||||
font: var(--cpd-font-body-md-medium);
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_ResetIdentityPanel_footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--cpd-space-4x);
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
3
res/img/element-icons/roomlist/dnd-cross.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.3333 7.3335V8.66683H10.5533L13.56 11.6735C14.26 10.6202 14.6667 9.36016 14.6667 8.00016C14.6667 4.32016 11.68 1.3335 8.00001 1.3335C6.64001 1.3335 5.38001 1.74016 4.32668 2.44016L9.22001 7.3335H11.3333ZM0.926682 2.8135L2.44001 4.32683C1.74001 5.38016 1.33335 6.64016 1.33335 8.00016C1.33335 11.6802 4.32001 14.6668 8.00001 14.6668C9.36001 14.6668 10.62 14.2602 11.6733 13.5602L13.1867 15.0735L14.1267 14.1335L1.87335 1.8735L0.926682 2.8135ZM4.66668 7.3335H5.44668L6.78001 8.66683H4.66668V7.3335Z" fill="#737D8C"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 630 B |
3
res/img/element-icons/roomlist/dnd.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM17 13H7V11H17V13Z" fill="#17191C"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 241 B |
@@ -72,13 +72,6 @@ if [[ "$head" == *":"* ]]; then
|
||||
fi
|
||||
clone ${TRY_ORG} $defrepo ${TRY_BRANCH}
|
||||
|
||||
# For merge queue runs we need to extract the temporary branch name
|
||||
# the ref_name will look like `gh-readonly-queue/<branch>/pr-<number>-<sha>`
|
||||
if [[ "$GITHUB_EVENT_NAME" == "merge_group" ]]; then
|
||||
withoutPrefix=${GITHUB_REF_NAME#gh-readonly-queue/}
|
||||
clone $deforg $defrepo ${withoutPrefix%%/pr-*}
|
||||
fi
|
||||
|
||||
# Try the target branch of the push or PR.
|
||||
if [ -n "$GITHUB_BASE_REF" ]; then
|
||||
clone $deforg $defrepo $GITHUB_BASE_REF
|
||||
|
||||
@@ -31,50 +31,49 @@ export async function createCrossSigning(cli: MatrixClient): Promise<void> {
|
||||
throw new Error("No crypto API found!");
|
||||
}
|
||||
|
||||
const doBootstrapUIAuth = async (
|
||||
makeRequest: (authData: AuthDict) => Promise<UIAResponse<void>>,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
await makeRequest({});
|
||||
} catch (error) {
|
||||
if (!(error instanceof MatrixError) || !error.data || !error.data.flows) {
|
||||
// Not a UIA response
|
||||
throw error;
|
||||
}
|
||||
|
||||
const dialogAesthetics = {
|
||||
[SSOAuthEntry.PHASE_PREAUTH]: {
|
||||
title: _t("auth|uia|sso_title"),
|
||||
body: _t("auth|uia|sso_preauth_body"),
|
||||
continueText: _t("auth|sso"),
|
||||
continueKind: "primary",
|
||||
},
|
||||
[SSOAuthEntry.PHASE_POSTAUTH]: {
|
||||
title: _t("encryption|confirm_encryption_setup_title"),
|
||||
body: _t("encryption|confirm_encryption_setup_body"),
|
||||
continueText: _t("action|confirm"),
|
||||
continueKind: "primary",
|
||||
},
|
||||
};
|
||||
|
||||
const { finished } = Modal.createDialog(InteractiveAuthDialog, {
|
||||
title: _t("encryption|bootstrap_title"),
|
||||
matrixClient: cli,
|
||||
makeRequest,
|
||||
aestheticsForStagePhases: {
|
||||
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
|
||||
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
|
||||
},
|
||||
});
|
||||
const [confirmed] = await finished;
|
||||
if (!confirmed) {
|
||||
throw new Error("Cross-signing key upload auth canceled");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await cryptoApi.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: (makeRequest) => uiAuthCallback(cli, makeRequest),
|
||||
authUploadDeviceSigningKeys: doBootstrapUIAuth,
|
||||
});
|
||||
}
|
||||
|
||||
export async function uiAuthCallback(
|
||||
matrixClient: MatrixClient,
|
||||
makeRequest: (authData: AuthDict) => Promise<UIAResponse<void>>,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await makeRequest({});
|
||||
} catch (error) {
|
||||
if (!(error instanceof MatrixError) || !error.data || !error.data.flows) {
|
||||
// Not a UIA response
|
||||
throw error;
|
||||
}
|
||||
|
||||
const dialogAesthetics = {
|
||||
[SSOAuthEntry.PHASE_PREAUTH]: {
|
||||
title: _t("auth|uia|sso_title"),
|
||||
body: _t("auth|uia|sso_preauth_body"),
|
||||
continueText: _t("auth|sso"),
|
||||
continueKind: "primary",
|
||||
},
|
||||
[SSOAuthEntry.PHASE_POSTAUTH]: {
|
||||
title: _t("encryption|confirm_encryption_setup_title"),
|
||||
body: _t("encryption|confirm_encryption_setup_body"),
|
||||
continueText: _t("action|confirm"),
|
||||
continueKind: "primary",
|
||||
},
|
||||
};
|
||||
|
||||
const { finished } = Modal.createDialog(InteractiveAuthDialog, {
|
||||
title: _t("encryption|bootstrap_title"),
|
||||
matrixClient,
|
||||
makeRequest,
|
||||
aestheticsForStagePhases: {
|
||||
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
|
||||
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
|
||||
},
|
||||
});
|
||||
const [confirmed] = await finished;
|
||||
if (!confirmed) {
|
||||
throw new Error("Cross-signing key upload auth canceled");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
SyncState,
|
||||
ClientStoppedError,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { logger as baseLogger } from "matrix-js-sdk/src/logger";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { CryptoEvent, KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
||||
import { CryptoSessionStateChange } from "@matrix-org/analytics-events/types/typescript/CryptoSessionStateChange";
|
||||
|
||||
@@ -48,8 +48,6 @@ import { asyncSomeParallel } from "./utils/arrays.ts";
|
||||
|
||||
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
|
||||
|
||||
const logger = baseLogger.getChild("DeviceListener:");
|
||||
|
||||
export default class DeviceListener {
|
||||
private dispatcherRef?: string;
|
||||
// device IDs for which the user has dismissed the verify toast ('Later')
|
||||
@@ -133,7 +131,7 @@ export default class DeviceListener {
|
||||
* @param {String[]} deviceIds List of device IDs to dismiss notifications for
|
||||
*/
|
||||
public async dismissUnverifiedSessions(deviceIds: Iterable<string>): Promise<void> {
|
||||
logger.debug("Dismissing unverified sessions: " + Array.from(deviceIds).join(","));
|
||||
logger.log("Dismissing unverified sessions: " + Array.from(deviceIds).join(","));
|
||||
for (const d of deviceIds) {
|
||||
this.dismissed.add(d);
|
||||
}
|
||||
@@ -311,20 +309,16 @@ export default class DeviceListener {
|
||||
if (!crossSigningReady) {
|
||||
// This account is legacy and doesn't have cross-signing set up at all.
|
||||
// Prompt the user to set it up.
|
||||
logger.info("Cross-signing not ready: showing SET_UP_ENCRYPTION toast");
|
||||
showSetupEncryptionToast(SetupKind.SET_UP_ENCRYPTION);
|
||||
} else if (!isCurrentDeviceTrusted) {
|
||||
// cross signing is ready but the current device is not trusted: prompt the user to verify
|
||||
logger.info("Current device not verified: showing VERIFY_THIS_SESSION toast");
|
||||
showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION);
|
||||
} else if (!allCrossSigningSecretsCached) {
|
||||
// cross signing ready & device trusted, but we are missing secrets from our local cache.
|
||||
// prompt the user to enter their recovery key.
|
||||
logger.info("Some secrets not cached: showing KEY_STORAGE_OUT_OF_SYNC toast");
|
||||
showSetupEncryptionToast(SetupKind.KEY_STORAGE_OUT_OF_SYNC);
|
||||
} else if (defaultKeyId === null) {
|
||||
// the user just hasn't set up 4S yet: prompt them to do so
|
||||
logger.info("No default 4S key: showing SET_UP_RECOVERY toast");
|
||||
showSetupEncryptionToast(SetupKind.SET_UP_RECOVERY);
|
||||
} else {
|
||||
// some other condition... yikes! Show the 'set up encryption' toast: this is what we previously did
|
||||
|
||||
@@ -41,7 +41,6 @@ import PlatformPeg from "./PlatformPeg";
|
||||
import { formatList } from "./utils/FormattingUtils";
|
||||
import SdkConfig from "./SdkConfig";
|
||||
import { setDeviceIsolationMode } from "./settings/controllers/DeviceIsolationModeController.ts";
|
||||
import { initialiseDehydration } from "./utils/device/dehydration";
|
||||
|
||||
export interface IMatrixClientCreds {
|
||||
homeserverUrl: string;
|
||||
@@ -341,20 +340,7 @@ class MatrixClientPegClass implements IMatrixClientPeg {
|
||||
|
||||
setDeviceIsolationMode(this.matrixClient, SettingsStore.getValue("feature_exclude_insecure_devices"));
|
||||
|
||||
// Start dehydration. This code is only for the case where the client
|
||||
// gets restarted, so we only do this if we already have the dehydration
|
||||
// key cached, and we don't have to try to rehydrate a device. If this
|
||||
// is a new login, we will start dehydration after Secret Storage is
|
||||
// unlocked.
|
||||
try {
|
||||
await initialiseDehydration({ onlyIfKeyCached: true, rehydrate: false }, this.matrixClient);
|
||||
} catch (e) {
|
||||
// We may get an error dehydrating, such as if cross-signing and
|
||||
// SSSS are not set up yet. Just log the error and continue.
|
||||
// If SSSS gets set up later, we will re-try dehydration.
|
||||
console.log("Error starting device dehydration", e);
|
||||
}
|
||||
|
||||
// TODO: device dehydration and whathaveyou
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
import { lazy } from "react";
|
||||
import { SecretStorage } from "matrix-js-sdk/src/matrix";
|
||||
import { deriveRecoveryKeyFromPassphrase, decodeRecoveryKey, CryptoCallbacks } from "matrix-js-sdk/src/crypto-api";
|
||||
import { logger as rootLogger } from "matrix-js-sdk/src/logger";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import Modal from "./Modal";
|
||||
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||
@@ -29,8 +29,6 @@ let secretStorageKeys: Record<string, Uint8Array> = {};
|
||||
let secretStorageKeyInfo: Record<string, SecretStorage.SecretStorageKeyDescription> = {};
|
||||
let secretStorageBeingAccessed = false;
|
||||
|
||||
const logger = rootLogger.getChild("SecurityManager:");
|
||||
|
||||
/**
|
||||
* This can be used by other components to check if secret storage access is in
|
||||
* progress, so that we can e.g. avoid intermittently showing toasts during
|
||||
@@ -72,34 +70,33 @@ function makeInputToKey(
|
||||
};
|
||||
}
|
||||
|
||||
async function getSecretStorageKey(
|
||||
{
|
||||
keys: keyInfos,
|
||||
}: {
|
||||
keys: Record<string, SecretStorage.SecretStorageKeyDescription>;
|
||||
},
|
||||
secretName: string,
|
||||
): Promise<[string, Uint8Array]> {
|
||||
async function getSecretStorageKey({
|
||||
keys: keyInfos,
|
||||
}: {
|
||||
keys: Record<string, SecretStorage.SecretStorageKeyDescription>;
|
||||
}): Promise<[string, Uint8Array]> {
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
const defaultKeyId = await cli.secretStorage.getDefaultKeyId();
|
||||
|
||||
let keyId: string;
|
||||
// If the defaultKey is useful, use that
|
||||
if (defaultKeyId && keyInfos[defaultKeyId]) {
|
||||
keyId = defaultKeyId;
|
||||
} else {
|
||||
// Fall back to a heuristic of using the
|
||||
let keyId = await cli.secretStorage.getDefaultKeyId();
|
||||
let keyInfo!: SecretStorage.SecretStorageKeyDescription;
|
||||
if (keyId) {
|
||||
// use the default SSSS key if set
|
||||
keyInfo = keyInfos[keyId];
|
||||
if (!keyInfo) {
|
||||
// if the default key is not available, pretend the default key
|
||||
// isn't set
|
||||
keyId = null;
|
||||
}
|
||||
}
|
||||
if (!keyId) {
|
||||
// if no default SSSS key is set, fall back to a heuristic of using the
|
||||
// only available key, if only one key is set
|
||||
const usefulKeys = Object.keys(keyInfos);
|
||||
if (usefulKeys.length > 1) {
|
||||
const keyInfoEntries = Object.entries(keyInfos);
|
||||
if (keyInfoEntries.length > 1) {
|
||||
throw new Error("Multiple storage key requests not implemented");
|
||||
}
|
||||
keyId = usefulKeys[0];
|
||||
[keyId, keyInfo] = keyInfoEntries[0];
|
||||
}
|
||||
const keyInfo = keyInfos[keyId];
|
||||
logger.debug(
|
||||
`getSecretStorageKey: request for 4S keys [${Object.keys(keyInfos)}] for secret \`${secretName}\`: looking for key ${keyId}`,
|
||||
);
|
||||
logger.debug(`getSecretStorageKey: request for 4S keys [${Object.keys(keyInfos)}]: looking for key ${keyId}`);
|
||||
|
||||
// Check the in-memory cache
|
||||
if (secretStorageBeingAccessed && secretStorageKeys[keyId]) {
|
||||
@@ -109,18 +106,12 @@ async function getSecretStorageKey(
|
||||
|
||||
const keyFromCustomisations = ModuleRunner.instance.extensions.cryptoSetup.getSecretStorageKey();
|
||||
if (keyFromCustomisations) {
|
||||
logger.debug("getSecretStorageKey: Using secret storage key from CryptoSetupExtension");
|
||||
logger.log("getSecretStorageKey: Using secret storage key from CryptoSetupExtension");
|
||||
cacheSecretStorageKey(keyId, keyInfo, keyFromCustomisations);
|
||||
return [keyId, keyFromCustomisations];
|
||||
}
|
||||
|
||||
// We only prompt the user for the default key
|
||||
if (keyId !== defaultKeyId) {
|
||||
logger.debug(`getSecretStorageKey: request for non-default key ${keyId}: not prompting user`);
|
||||
throw new Error("Request for non-default 4S key");
|
||||
}
|
||||
|
||||
logger.debug(`getSecretStorageKey: prompting user for key ${keyId}`);
|
||||
logger.debug("getSecretStorageKey: prompting user for key");
|
||||
const inputToKey = makeInputToKey(keyInfo);
|
||||
const { finished } = Modal.createDialog(
|
||||
AccessSecretStorageDialog,
|
||||
@@ -148,7 +139,7 @@ async function getSecretStorageKey(
|
||||
if (!keyParams) {
|
||||
throw new AccessCancelledError();
|
||||
}
|
||||
logger.debug(`getSecretStorageKey: got key ${keyId} from user`);
|
||||
logger.debug("getSecretStorageKey: got key from user");
|
||||
const key = await inputToKey(keyParams);
|
||||
|
||||
// Save to cache to avoid future prompts in the current session
|
||||
@@ -163,7 +154,6 @@ function cacheSecretStorageKey(
|
||||
key: Uint8Array,
|
||||
): void {
|
||||
if (secretStorageBeingAccessed) {
|
||||
logger.debug(`Caching 4S key ${keyId}`);
|
||||
secretStorageKeys[keyId] = key;
|
||||
secretStorageKeyInfo[keyId] = keyInfo;
|
||||
}
|
||||
@@ -183,13 +173,13 @@ export const crossSigningCallbacks: CryptoCallbacks = {
|
||||
* @param func - The operation to be wrapped.
|
||||
*/
|
||||
export async function withSecretStorageKeyCache<T>(func: () => Promise<T>): Promise<T> {
|
||||
logger.debug("enabling 4S key cache");
|
||||
logger.debug("SecurityManager: enabling 4S key cache");
|
||||
secretStorageBeingAccessed = true;
|
||||
try {
|
||||
return await func();
|
||||
} finally {
|
||||
// Clear secret storage key cache now that work is complete
|
||||
logger.debug("disabling 4S key cache");
|
||||
logger.debug("SecurityManager: disabling 4S key cache");
|
||||
secretStorageBeingAccessed = false;
|
||||
secretStorageKeys = {};
|
||||
secretStorageKeyInfo = {};
|
||||
|
||||
33
src/Terms.ts
@@ -7,12 +7,11 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import classNames from "classnames";
|
||||
import { SERVICE_TYPES, MatrixClient, Terms, Policy, InternationalisedPolicy } from "matrix-js-sdk/src/matrix";
|
||||
import { SERVICE_TYPES, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import Modal from "./Modal";
|
||||
import TermsDialog from "./components/views/dialogs/TermsDialog";
|
||||
import { pickBestLanguage } from "./languageHandler.tsx";
|
||||
|
||||
export class TermsNotSignedError extends Error {}
|
||||
|
||||
@@ -33,8 +32,23 @@ export class Service {
|
||||
) {}
|
||||
}
|
||||
|
||||
export interface LocalisedPolicy {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface Policy {
|
||||
// @ts-ignore: No great way to express indexed types together with other keys
|
||||
version: string;
|
||||
[lang: string]: LocalisedPolicy;
|
||||
}
|
||||
|
||||
export type Policies = {
|
||||
[policy: string]: Policy;
|
||||
};
|
||||
|
||||
export type ServicePolicyPair = {
|
||||
policies: Terms["policies"];
|
||||
policies: Policies;
|
||||
service: Service;
|
||||
};
|
||||
|
||||
@@ -44,11 +58,6 @@ export type TermsInteractionCallback = (
|
||||
extraClassNames?: string,
|
||||
) => Promise<string[]>;
|
||||
|
||||
export function pickBestPolicyLanguage(policy: Policy): InternationalisedPolicy | undefined {
|
||||
const termsLang = pickBestLanguage(Object.keys(policy).filter((k) => k !== "version"));
|
||||
return <InternationalisedPolicy>policy[termsLang];
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a flow where the user is presented with terms & conditions for some services
|
||||
*
|
||||
@@ -87,7 +96,7 @@ export async function startTermsFlow(
|
||||
* }
|
||||
*/
|
||||
|
||||
const terms: Terms[] = await Promise.all(termsPromises);
|
||||
const terms: { policies: Policies }[] = await Promise.all(termsPromises);
|
||||
const policiesAndServicePairs = terms.map((t, i) => {
|
||||
return { service: services[i], policies: t.policies };
|
||||
});
|
||||
@@ -104,11 +113,11 @@ export async function startTermsFlow(
|
||||
// things they've not agreed to yet.
|
||||
const unagreedPoliciesAndServicePairs: ServicePolicyPair[] = [];
|
||||
for (const { service, policies } of policiesAndServicePairs) {
|
||||
const unagreedPolicies: Terms["policies"] = {};
|
||||
const unagreedPolicies: Policies = {};
|
||||
for (const [policyName, policy] of Object.entries(policies)) {
|
||||
let policyAgreed = false;
|
||||
for (const lang of Object.keys(policy)) {
|
||||
if (lang === "version" || typeof policy[lang] === "string") continue;
|
||||
if (lang === "version") continue;
|
||||
if (agreedUrlSet.has(policy[lang].url)) {
|
||||
policyAgreed = true;
|
||||
break;
|
||||
@@ -145,7 +154,7 @@ export async function startTermsFlow(
|
||||
const urlsForService = Array.from(agreedUrlSet).filter((url) => {
|
||||
for (const policy of Object.values(policiesAndService.policies)) {
|
||||
for (const lang of Object.keys(policy)) {
|
||||
if (lang === "version" || typeof policy[lang] === "string") continue;
|
||||
if (lang === "version") continue;
|
||||
if (policy[lang].url === url) return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,7 +332,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
||||
setupNewKeyBackup: !backupInfo,
|
||||
});
|
||||
}
|
||||
await initialiseDehydration({ createNewKey: true });
|
||||
await initialiseDehydration(true);
|
||||
|
||||
this.setState({
|
||||
phase: Phase.Stored,
|
||||
|
||||
@@ -50,7 +50,7 @@ import ThemeController from "../../settings/controllers/ThemeController";
|
||||
import { startAnyRegistrationFlow } from "../../Registration";
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils";
|
||||
import ThemeWatcher, { ThemeWatcherEvent } from "../../settings/watchers/ThemeWatcher";
|
||||
import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
|
||||
import { FontWatcher } from "../../settings/watchers/FontWatcher";
|
||||
import { storeRoomAliasInCache } from "../../RoomAliasCache";
|
||||
import ToastStore from "../../stores/ToastStore";
|
||||
@@ -131,7 +131,7 @@ import { ConfirmSessionLockTheftView } from "./auth/ConfirmSessionLockTheftView"
|
||||
import { LoginSplashView } from "./auth/LoginSplashView";
|
||||
import { cleanUpDraftsIfRequired } from "../../DraftCleaner";
|
||||
import { InitialCryptoSetupStore } from "../../stores/InitialCryptoSetupStore";
|
||||
import { setTheme } from "../../theme";
|
||||
import { AppTitleContext } from "@matrix-org/react-sdk-module-api/lib/lifecycles/BrandingExtensions";
|
||||
|
||||
// legacy export
|
||||
export { default as Views } from "../../Views";
|
||||
@@ -224,7 +224,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
private tokenLogin?: boolean;
|
||||
// What to focus on next component update, if anything
|
||||
private focusNext: FocusNextType;
|
||||
private subTitleStatus: string;
|
||||
private prevWindowWidth: number;
|
||||
|
||||
private readonly loggedInView = createRef<LoggedInViewType>();
|
||||
@@ -233,6 +232,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
private fontWatcher?: FontWatcher;
|
||||
private readonly stores: SdkContextClass;
|
||||
|
||||
private subtitleContext?: {unreadNotificationCount: number, userNotificationLevel: NotificationLevel, syncState: SyncState};
|
||||
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
this.stores = SdkContextClass.instance;
|
||||
@@ -276,10 +277,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
}
|
||||
|
||||
this.prevWindowWidth = UIStore.instance.windowWidth || 1000;
|
||||
|
||||
// object field used for tracking the status info appended to the title tag.
|
||||
// we don't do it as react state as i'm scared about triggering needless react refreshes.
|
||||
this.subTitleStatus = "";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -464,7 +461,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
this.themeWatcher = new ThemeWatcher();
|
||||
this.fontWatcher = new FontWatcher();
|
||||
this.themeWatcher.start();
|
||||
this.themeWatcher.on(ThemeWatcherEvent.Change, setTheme);
|
||||
this.fontWatcher.start();
|
||||
|
||||
initSentry(SdkConfig.get("sentry"));
|
||||
@@ -497,7 +493,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
public componentWillUnmount(): void {
|
||||
Lifecycle.stopMatrixClient();
|
||||
dis.unregister(this.dispatcherRef);
|
||||
this.themeWatcher?.off(ThemeWatcherEvent.Change, setTheme);
|
||||
this.themeWatcher?.stop();
|
||||
this.fontWatcher?.stop();
|
||||
UIStore.destroy();
|
||||
@@ -1477,7 +1472,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
collapseLhs: false,
|
||||
currentRoomId: null,
|
||||
});
|
||||
this.subTitleStatus = "";
|
||||
this.subtitleContext = undefined;
|
||||
this.setPageSubtitle();
|
||||
this.stores.onLoggedOut();
|
||||
}
|
||||
@@ -1493,7 +1488,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
collapseLhs: false,
|
||||
currentRoomId: null,
|
||||
});
|
||||
this.subTitleStatus = "";
|
||||
this.subtitleContext = undefined;
|
||||
this.setPageSubtitle();
|
||||
}
|
||||
|
||||
@@ -1698,6 +1693,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
if (crypto) {
|
||||
const blacklistEnabled = SettingsStore.getValueAt(SettingLevel.DEVICE, "blacklistUnverifiedDevices");
|
||||
crypto.globalBlacklistUnverifiedDevices = blacklistEnabled;
|
||||
|
||||
// With cross-signing enabled, we send to unknown devices
|
||||
// without prompting. Any bad-device status the user should
|
||||
// be aware of will be signalled through the room shield
|
||||
// changing colour. More advanced behaviour will come once
|
||||
// we implement more settings.
|
||||
cli.setGlobalErrorOnUnknownDevices(false);
|
||||
}
|
||||
|
||||
// Cannot be done in OnLoggedIn as at that point the AccountSettingsHandler doesn't yet have a client
|
||||
@@ -1937,15 +1939,51 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
});
|
||||
}
|
||||
|
||||
private setPageSubtitle(subtitle = ""): void {
|
||||
private setPageSubtitle(): void {
|
||||
const extraContext = this.subtitleContext;
|
||||
let context: AppTitleContext = {
|
||||
brand: SdkConfig.get().brand,
|
||||
syncError: extraContext?.syncState === SyncState.Error,
|
||||
notificationsMuted: extraContext && extraContext.userNotificationLevel < NotificationLevel.Activity,
|
||||
unreadNotificationCount: extraContext?.unreadNotificationCount,
|
||||
};
|
||||
|
||||
if (this.state.currentRoomId) {
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client?.getRoom(this.state.currentRoomId);
|
||||
if (room) {
|
||||
subtitle = `${this.subTitleStatus} | ${room.name} ${subtitle}`;
|
||||
context = {
|
||||
...context,
|
||||
roomId: this.state.currentRoomId,
|
||||
roomName: room?.name,
|
||||
};
|
||||
}
|
||||
|
||||
const moduleTitle = ModuleRunner.instance.extensions.branding?.getAppTitle(context);
|
||||
if (moduleTitle) {
|
||||
if (document.title !== moduleTitle) {
|
||||
document.title = moduleTitle;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Use application default.
|
||||
|
||||
let subtitle = "";
|
||||
if (context?.syncError) {
|
||||
subtitle += `[${_t("common|offline")}] `;
|
||||
}
|
||||
if (context.unreadNotificationCount !== undefined && context.unreadNotificationCount > 0) {
|
||||
subtitle += `[${context.unreadNotificationCount}]`;
|
||||
} else if (context.notificationsMuted !== undefined && !context.notificationsMuted) {
|
||||
subtitle += `*`;
|
||||
}
|
||||
|
||||
if ('roomId' in context && context.roomId) {
|
||||
if (context.roomName) {
|
||||
subtitle = `${subtitle} | ${context.roomName}`;
|
||||
}
|
||||
} else {
|
||||
subtitle = `${this.subTitleStatus} ${subtitle}`;
|
||||
subtitle = subtitle;
|
||||
}
|
||||
|
||||
const title = `${SdkConfig.get().brand} ${subtitle}`;
|
||||
@@ -1962,17 +2000,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
PlatformPeg.get()!.setErrorStatus(state === SyncState.Error);
|
||||
PlatformPeg.get()!.setNotificationCount(numUnreadRooms);
|
||||
}
|
||||
|
||||
this.subTitleStatus = "";
|
||||
if (state === SyncState.Error) {
|
||||
this.subTitleStatus += `[${_t("common|offline")}] `;
|
||||
}
|
||||
if (numUnreadRooms > 0) {
|
||||
this.subTitleStatus += `[${numUnreadRooms}]`;
|
||||
} else if (notificationState.level >= NotificationLevel.Activity) {
|
||||
this.subTitleStatus += `*`;
|
||||
}
|
||||
|
||||
this.subtitleContext = {
|
||||
syncState: state,
|
||||
userNotificationLevel: notificationState.level,
|
||||
unreadNotificationCount: numUnreadRooms,
|
||||
};
|
||||
this.setPageSubtitle();
|
||||
};
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
|
||||
import RoomPreviewCard from "../views/rooms/RoomPreviewCard";
|
||||
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
||||
import AuxPanel from "../views/rooms/AuxPanel";
|
||||
import RoomHeader from "../views/rooms/RoomHeader/RoomHeader";
|
||||
import RoomHeader from "../views/rooms/RoomHeader";
|
||||
import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||
import EffectsOverlay from "../views/elements/EffectsOverlay";
|
||||
import { containsEmoji } from "../../effects/utils";
|
||||
|
||||
@@ -129,10 +129,10 @@ export const ThreadPanelHeader: React.FC<{
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx_ThreadPanelHeader">
|
||||
<div className="mx_BaseCard_header_title">
|
||||
<Tooltip label={_t("threads|mark_all_read")}>
|
||||
<IconButton onClick={onMarkAllThreadsReadClick} size="28px">
|
||||
<MarkAllThreadsReadIcon height={20} width={20} />
|
||||
<IconButton onClick={onMarkAllThreadsReadClick} size="24px">
|
||||
<MarkAllThreadsReadIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<div className="mx_ThreadPanel_vertical_separator" />
|
||||
@@ -192,7 +192,9 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
|
||||
narrow={narrow}
|
||||
>
|
||||
<BaseCard
|
||||
header={_t("common|threads")}
|
||||
header={
|
||||
hasThreads && <ThreadPanelHeader filterOption={filterOption} setFilterOption={setFilterOption} />
|
||||
}
|
||||
id="thread-panel"
|
||||
className="mx_ThreadPanel"
|
||||
ariaLabelledBy="thread-panel-tab"
|
||||
@@ -202,7 +204,6 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
|
||||
ref={card}
|
||||
closeButtonRef={closeButonRef}
|
||||
>
|
||||
{hasThreads && <ThreadPanelHeader filterOption={filterOption} setFilterOption={setFilterOption} />}
|
||||
<Measured sensor={card} onMeasurement={setNarrow} />
|
||||
{timelineSet ? (
|
||||
<TimelinePanel
|
||||
|
||||
@@ -11,7 +11,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
||||
import RoomHeader from "../views/rooms/RoomHeader/RoomHeader.tsx";
|
||||
import RoomHeader from "../views/rooms/RoomHeader";
|
||||
import ScrollPanel from "./ScrollPanel";
|
||||
import EventTileBubble from "../views/messages/EventTileBubble";
|
||||
import NewRoomIntro from "../views/rooms/NewRoomIntro";
|
||||
|
||||
@@ -7,14 +7,14 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import classNames from "classnames";
|
||||
import React, { ComponentProps, JSXElementConstructor, useMemo } from "react";
|
||||
import React, { useMemo } from "react";
|
||||
|
||||
type FlexProps<T extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>> = {
|
||||
type FlexProps = {
|
||||
/**
|
||||
* The type of the HTML element
|
||||
* @default div
|
||||
*/
|
||||
as?: T;
|
||||
as?: string;
|
||||
/**
|
||||
* The CSS class name.
|
||||
*/
|
||||
@@ -30,7 +30,7 @@ type FlexProps<T extends keyof JSX.IntrinsicElements | JSXElementConstructor<any
|
||||
*/
|
||||
direction?: "row" | "column" | "row-reverse" | "column-reverse";
|
||||
/**
|
||||
* The alignment of the flex children
|
||||
* The alingment of the flex children
|
||||
* @default start
|
||||
*/
|
||||
align?: "start" | "center" | "end" | "baseline" | "stretch";
|
||||
@@ -48,12 +48,12 @@ type FlexProps<T extends keyof JSX.IntrinsicElements | JSXElementConstructor<any
|
||||
* the on click event callback
|
||||
*/
|
||||
onClick?: (e: React.MouseEvent) => void;
|
||||
} & ComponentProps<T>;
|
||||
};
|
||||
|
||||
/**
|
||||
* A flexbox container helper
|
||||
*/
|
||||
export function Flex<T extends keyof JSX.IntrinsicElements | JSXElementConstructor<any> = "div">({
|
||||
export function Flex({
|
||||
as = "div",
|
||||
display = "flex",
|
||||
direction = "row",
|
||||
@@ -63,7 +63,7 @@ export function Flex<T extends keyof JSX.IntrinsicElements | JSXElementConstruct
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.PropsWithChildren<FlexProps<T>>): JSX.Element {
|
||||
}: React.PropsWithChildren<FlexProps>): JSX.Element {
|
||||
const style = useMemo(
|
||||
() => ({
|
||||
"--mx-flex-display": display,
|
||||
|
||||
@@ -99,12 +99,8 @@ export function sdkRoomMemberToRoomMember(member: SdkRoomMember): Member {
|
||||
};
|
||||
}
|
||||
|
||||
export const SEPARATOR = "SEPARATOR";
|
||||
export type MemberWithSeparator = Member | typeof SEPARATOR;
|
||||
|
||||
export interface MemberListViewState {
|
||||
members: MemberWithSeparator[];
|
||||
memberCount: number;
|
||||
members: Member[];
|
||||
search: (searchQuery: string) => void;
|
||||
isPresenceEnabled: boolean;
|
||||
shouldShowInvite: boolean;
|
||||
@@ -122,16 +118,10 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
|
||||
}
|
||||
|
||||
const sdkContext = useContext(SDKContext);
|
||||
const [memberMap, setMemberMap] = useState<Map<string, MemberWithSeparator>>(new Map());
|
||||
const [memberMap, setMemberMap] = useState<Map<string, Member>>(new Map());
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
// This is the last known total number of members in this room.
|
||||
const [totalMemberCount, setTotalMemberCount] = useState(0);
|
||||
/**
|
||||
* This is the current number of members in the list.
|
||||
* This number will be less than the total number of members
|
||||
* in the room when the search functionality is used.
|
||||
*/
|
||||
const [memberCount, setMemberCount] = useState(0);
|
||||
|
||||
const loadMembers = useMemo(
|
||||
() =>
|
||||
@@ -141,34 +131,24 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
|
||||
roomId,
|
||||
searchQuery,
|
||||
);
|
||||
const threePidInvited = getPending3PidInvites(room, searchQuery);
|
||||
|
||||
const newMemberMap = new Map<string, MemberWithSeparator>();
|
||||
|
||||
// First add the joined room members
|
||||
for (const member of joinedSdk) {
|
||||
const roomMember = sdkRoomMemberToRoomMember(member);
|
||||
newMemberMap.set(member.userId, roomMember);
|
||||
}
|
||||
|
||||
// Then a separator if needed
|
||||
if (joinedSdk.length > 0 && (invitedSdk.length > 0 || threePidInvited.length > 0))
|
||||
newMemberMap.set(SEPARATOR, SEPARATOR);
|
||||
|
||||
// Then add the invited room members
|
||||
const newMemberMap = new Map<string, Member>();
|
||||
// First add the invited room members
|
||||
for (const member of invitedSdk) {
|
||||
const roomMember = sdkRoomMemberToRoomMember(member);
|
||||
newMemberMap.set(member.userId, roomMember);
|
||||
}
|
||||
|
||||
// Finally add the third party invites
|
||||
// Then add the third party invites
|
||||
const threePidInvited = getPending3PidInvites(room, searchQuery);
|
||||
for (const invited of threePidInvited) {
|
||||
const key = invited.threePidInvite!.event.getContent().display_name;
|
||||
newMemberMap.set(key, invited);
|
||||
}
|
||||
|
||||
// Finally add the joined room members
|
||||
for (const member of joinedSdk) {
|
||||
const roomMember = sdkRoomMemberToRoomMember(member);
|
||||
newMemberMap.set(member.userId, roomMember);
|
||||
}
|
||||
setMemberMap(newMemberMap);
|
||||
setMemberCount(joinedSdk.length + invitedSdk.length + threePidInvited.length);
|
||||
if (!searchQuery) {
|
||||
/**
|
||||
* Since searching for members only gives you the relevant
|
||||
@@ -261,7 +241,6 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
|
||||
|
||||
return {
|
||||
members: Array.from(memberMap.values()),
|
||||
memberCount,
|
||||
search: loadMembers,
|
||||
shouldShowInvite,
|
||||
isPresenceEnabled,
|
||||
|
||||
@@ -145,7 +145,7 @@ export function useMemberTileViewModel(props: MemberTileViewModelProps): MemberT
|
||||
userLabel = _t(PowerLabel[powerStatus]);
|
||||
}
|
||||
if (props.member.isInvite) {
|
||||
userLabel = _t("member_list|invited_label");
|
||||
userLabel = `(${_t("member_list|invited_label")})`;
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -8,7 +8,6 @@ Please see LICENSE files in the repository root for full details.
|
||||
import dis from "../../../../dispatcher/dispatcher";
|
||||
import { Action } from "../../../../dispatcher/actions";
|
||||
import { ThreePIDInvite } from "../../../../models/rooms/ThreePIDInvite";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
|
||||
interface ThreePidTileViewModelProps {
|
||||
threePidInvite: ThreePIDInvite;
|
||||
@@ -17,7 +16,6 @@ interface ThreePidTileViewModelProps {
|
||||
export interface ThreePidTileViewState {
|
||||
name: string;
|
||||
onClick: () => void;
|
||||
userLabel?: string;
|
||||
}
|
||||
|
||||
export function useThreePidTileViewModel(props: ThreePidTileViewModelProps): ThreePidTileViewState {
|
||||
@@ -30,11 +28,8 @@ export function useThreePidTileViewModel(props: ThreePidTileViewModelProps): Thr
|
||||
});
|
||||
};
|
||||
|
||||
const userLabel = _t("member_list|invited_label");
|
||||
|
||||
return {
|
||||
name,
|
||||
onClick,
|
||||
userLabel,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { EventType, MatrixEvent, Room, RoomMember, RoomStateEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { CryptoApi, CryptoEvent } from "matrix-js-sdk/src/crypto-api";
|
||||
import { throttle } from "lodash";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext.tsx";
|
||||
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter.ts";
|
||||
|
||||
export type ViolationType = "PinViolation" | "VerificationViolation";
|
||||
|
||||
/**
|
||||
* Represents a prompt to the user about a violation in the room.
|
||||
* The type of violation and the member it relates to are included.
|
||||
* If the type is "VerificationViolation", the warning is critical and should be reported with more urgency.
|
||||
*/
|
||||
export type ViolationPrompt = {
|
||||
member: RoomMember;
|
||||
type: ViolationType;
|
||||
};
|
||||
|
||||
/**
|
||||
* The state of the UserIdentityWarningViewModel.
|
||||
* This includes the current prompt to show to the user and a callback to handle button clicks.
|
||||
* If currentPrompt is undefined, there are no violations to show.
|
||||
*/
|
||||
export interface UserIdentityWarningState {
|
||||
currentPrompt?: ViolationPrompt;
|
||||
dispatchAction: (action: UserIdentityWarningViewModelAction) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* List of actions that can be dispatched to the UserIdentityWarningViewModel.
|
||||
*/
|
||||
export type UserIdentityWarningViewModelAction =
|
||||
| { type: "PinUserIdentity"; userId: string }
|
||||
| { type: "WithdrawVerification"; userId: string };
|
||||
|
||||
/**
|
||||
* Maps a list of room members to a list of violations.
|
||||
* Checks for all members in the room to see if they have any violations.
|
||||
* If no violations are found, an empty list is returned.
|
||||
*
|
||||
* @param cryptoApi
|
||||
* @param members - The list of room members to check for violations.
|
||||
*/
|
||||
async function mapToViolations(cryptoApi: CryptoApi, members: RoomMember[]): Promise<ViolationPrompt[]> {
|
||||
const violationList = new Array<ViolationPrompt>();
|
||||
for (const member of members) {
|
||||
const verificationStatus = await cryptoApi.getUserVerificationStatus(member.userId);
|
||||
if (verificationStatus.wasCrossSigningVerified() && !verificationStatus.isCrossSigningVerified()) {
|
||||
violationList.push({ member, type: "VerificationViolation" });
|
||||
} else if (verificationStatus.needsUserApproval) {
|
||||
violationList.push({ member, type: "PinViolation" });
|
||||
}
|
||||
}
|
||||
return violationList;
|
||||
}
|
||||
|
||||
export function useUserIdentityWarningViewModel(room: Room, key: string): UserIdentityWarningState {
|
||||
const cli = useMatrixClientContext();
|
||||
const crypto = cli.getCrypto();
|
||||
|
||||
const [members, setMembers] = useState<RoomMember[]>([]);
|
||||
const [currentPrompt, setCurrentPrompt] = useState<ViolationPrompt | undefined>(undefined);
|
||||
|
||||
const loadViolations = useMemo(
|
||||
() =>
|
||||
throttle(async (): Promise<void> => {
|
||||
const isEncrypted = crypto && (await crypto.isEncryptionEnabledInRoom(room.roomId));
|
||||
if (!isEncrypted) {
|
||||
setMembers([]);
|
||||
setCurrentPrompt(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const targetMembers = await room.getEncryptionTargetMembers();
|
||||
setMembers(targetMembers);
|
||||
const violations = await mapToViolations(crypto, targetMembers);
|
||||
|
||||
let candidatePrompt: ViolationPrompt | undefined;
|
||||
if (violations.length > 0) {
|
||||
// sort by user ID to ensure consistent ordering
|
||||
const sortedViolations = violations.sort((a, b) => a.member.userId.localeCompare(b.member.userId));
|
||||
candidatePrompt = sortedViolations[0];
|
||||
} else {
|
||||
candidatePrompt = undefined;
|
||||
}
|
||||
|
||||
// is the current prompt still valid?
|
||||
setCurrentPrompt((existingPrompt): ViolationPrompt | undefined => {
|
||||
if (existingPrompt && violations.includes(existingPrompt)) {
|
||||
return existingPrompt;
|
||||
} else if (candidatePrompt) {
|
||||
return candidatePrompt;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
}),
|
||||
[crypto, room],
|
||||
);
|
||||
|
||||
// We need to listen for changes to the members list
|
||||
useTypedEventEmitter(
|
||||
cli,
|
||||
RoomStateEvent.Events,
|
||||
useCallback(
|
||||
async (event: MatrixEvent): Promise<void> => {
|
||||
if (!crypto || event.getRoomId() !== room.roomId) {
|
||||
return;
|
||||
}
|
||||
let shouldRefresh = false;
|
||||
|
||||
const eventType = event.getType();
|
||||
|
||||
if (eventType === EventType.RoomEncryption && event.getStateKey() === "") {
|
||||
// Room is now encrypted, so we can initialise the component.
|
||||
shouldRefresh = true;
|
||||
} else if (eventType == EventType.RoomMember) {
|
||||
// We're processing an m.room.member event
|
||||
// Something has changed in membership, someone joined or someone left or
|
||||
// someone changed their display name. Anyhow let's refresh.
|
||||
const userId = event.getStateKey();
|
||||
shouldRefresh = !!userId;
|
||||
}
|
||||
|
||||
if (shouldRefresh) {
|
||||
loadViolations().catch((e) => {
|
||||
logger.error("Error refreshing UserIdentityWarningViewModel:", e);
|
||||
});
|
||||
}
|
||||
},
|
||||
[crypto, room, loadViolations],
|
||||
),
|
||||
);
|
||||
|
||||
// We need to listen for changes to the verification status of the members to refresh violations
|
||||
useTypedEventEmitter(
|
||||
cli,
|
||||
CryptoEvent.UserTrustStatusChanged,
|
||||
useCallback(
|
||||
(userId: string): void => {
|
||||
if (members.find((m) => m.userId == userId)) {
|
||||
// This member is tracked, we need to refresh.
|
||||
// refresh all for now?
|
||||
// As a later optimisation we could store the current violations and only update the relevant one.
|
||||
loadViolations().catch((e) => {
|
||||
logger.error("Error refreshing UserIdentityWarning:", e);
|
||||
});
|
||||
}
|
||||
},
|
||||
[loadViolations, members],
|
||||
),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
loadViolations().catch((e) => {
|
||||
logger.error("Error initialising UserIdentityWarning:", e);
|
||||
});
|
||||
}, [loadViolations]);
|
||||
|
||||
const dispatchAction = useCallback(
|
||||
(action: UserIdentityWarningViewModelAction): void => {
|
||||
if (!crypto) {
|
||||
return;
|
||||
}
|
||||
if (action.type === "PinUserIdentity") {
|
||||
crypto.pinCurrentUserIdentity(action.userId).catch((e) => {
|
||||
logger.error("Error pinning user identity:", e);
|
||||
});
|
||||
} else if (action.type === "WithdrawVerification") {
|
||||
crypto.withdrawVerificationRequirement(action.userId).catch((e) => {
|
||||
logger.error("Error withdrawing verification requirement:", e);
|
||||
});
|
||||
}
|
||||
},
|
||||
[crypto],
|
||||
);
|
||||
|
||||
return {
|
||||
currentPrompt,
|
||||
dispatchAction,
|
||||
};
|
||||
}
|
||||
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import classNames from "classnames";
|
||||
import { InternationalisedPolicy, Terms, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
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";
|
||||
@@ -16,13 +16,14 @@ import PopOutIcon from "@vector-im/compound-design-tokens/assets/web/icons/pop-o
|
||||
|
||||
import EmailPromptIcon from "../../../../res/img/element-icons/email-prompt.svg";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { LocalisedPolicy, Policies } from "../../../Terms";
|
||||
import { AuthHeaderModifier } from "../../structures/auth/header/AuthHeaderModifier";
|
||||
import AccessibleButton, { AccessibleButtonKind, ButtonEvent } from "../elements/AccessibleButton";
|
||||
import Field from "../elements/Field";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import CaptchaForm from "./CaptchaForm";
|
||||
import { Flex } from "../../utils/Flex";
|
||||
import { pickBestPolicyLanguage } from "../../../Terms.ts";
|
||||
|
||||
/* This file contains a collection of components which are used by the
|
||||
* InteractiveAuth to prompt the user to enter the information needed
|
||||
@@ -234,10 +235,12 @@ export class RecaptchaAuthEntry extends React.Component<IRecaptchaAuthEntryProps
|
||||
}
|
||||
|
||||
interface ITermsAuthEntryProps extends IAuthEntryProps {
|
||||
stageParams?: Partial<Terms>;
|
||||
stageParams?: {
|
||||
policies?: Policies;
|
||||
};
|
||||
}
|
||||
|
||||
interface LocalisedPolicyWithId extends InternationalisedPolicy {
|
||||
interface LocalisedPolicyWithId extends LocalisedPolicy {
|
||||
id: string;
|
||||
}
|
||||
|
||||
@@ -275,6 +278,7 @@ export class TermsAuthEntry extends React.Component<ITermsAuthEntryProps, ITerms
|
||||
// }
|
||||
|
||||
const allPolicies = this.props.stageParams?.policies || {};
|
||||
const prefLang = SettingsStore.getValue("language");
|
||||
const initToggles: Record<string, boolean> = {};
|
||||
const pickedPolicies: {
|
||||
id: string;
|
||||
@@ -283,7 +287,17 @@ export class TermsAuthEntry extends React.Component<ITermsAuthEntryProps, ITerms
|
||||
}[] = [];
|
||||
for (const policyId of Object.keys(allPolicies)) {
|
||||
const policy = allPolicies[policyId];
|
||||
const langPolicy = pickBestPolicyLanguage(policy);
|
||||
|
||||
// Pick a language based on the user's language, falling back to english,
|
||||
// and finally to the first language available. If there's still no policy
|
||||
// available then the homeserver isn't respecting the spec.
|
||||
let langPolicy: LocalisedPolicy | undefined = policy[prefLang];
|
||||
if (!langPolicy) langPolicy = policy["en"];
|
||||
if (!langPolicy) {
|
||||
// last resort
|
||||
const firstLang = Object.keys(policy).find((e) => e !== "version");
|
||||
langPolicy = firstLang ? policy[firstLang] : undefined;
|
||||
}
|
||||
if (!langPolicy) throw new Error("Failed to find a policy to show the user");
|
||||
|
||||
initToggles[policyId] = false;
|
||||
|
||||
@@ -24,7 +24,6 @@ import { SettingLevel } from "../../../settings/SettingLevel";
|
||||
import ServerInfo from "./devtools/ServerInfo";
|
||||
import CopyableText from "../elements/CopyableText";
|
||||
import RoomNotifications from "./devtools/RoomNotifications";
|
||||
import { Crypto } from "./devtools/Crypto";
|
||||
|
||||
enum Category {
|
||||
Room,
|
||||
@@ -50,7 +49,6 @@ const Tools: Record<Category, [label: TranslationKey, tool: Tool][]> = {
|
||||
[_td("devtools|explore_account_data"), AccountDataExplorer],
|
||||
[_td("devtools|settings_explorer"), SettingExplorer],
|
||||
[_td("devtools|server_info"), ServerInfo],
|
||||
[_td("devtools|crypto|title"), Crypto],
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ import { OwnProfileStore } from "../../../stores/OwnProfileStore";
|
||||
import { arrayFastClone } from "../../../utils/arrays";
|
||||
import { ElementWidget } from "../../../stores/widgets/StopGapWidget";
|
||||
import { ELEMENT_CLIENT_ID } from "../../../identifiers";
|
||||
import ThemeWatcher, { ThemeWatcherEvent } from "../../../settings/watchers/ThemeWatcher";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
interface IProps {
|
||||
widgetDefinition: IModalWidgetOpenRequestData;
|
||||
@@ -54,7 +54,6 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
|
||||
private readonly widget: Widget;
|
||||
private readonly possibleButtons: ModalButtonID[];
|
||||
private appFrame: React.RefObject<HTMLIFrameElement> = React.createRef();
|
||||
private readonly themeWatcher = new ThemeWatcher();
|
||||
|
||||
public state: IState = {
|
||||
disabledButtonIds: (this.props.widgetDefinition.buttons || []).filter((b) => b.disabled).map((b) => b.id),
|
||||
@@ -78,8 +77,6 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
this.themeWatcher.off(ThemeWatcherEvent.Change, this.onThemeChange);
|
||||
this.themeWatcher.stop();
|
||||
if (!this.state.messaging) return;
|
||||
this.state.messaging.off("ready", this.onReady);
|
||||
this.state.messaging.off(`action:${WidgetApiFromWidgetAction.CloseModalWidget}`, this.onWidgetClose);
|
||||
@@ -87,10 +84,6 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
|
||||
}
|
||||
|
||||
private onReady = (): void => {
|
||||
this.themeWatcher.start();
|
||||
this.themeWatcher.on(ThemeWatcherEvent.Change, this.onThemeChange);
|
||||
// Theme may have changed while messaging was starting
|
||||
this.onThemeChange(this.themeWatcher.getEffectiveTheme());
|
||||
this.state.messaging?.sendWidgetConfig(this.props.widgetDefinition);
|
||||
};
|
||||
|
||||
@@ -101,10 +94,6 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
|
||||
this.state.messaging.on(`action:${WidgetApiFromWidgetAction.SetModalButtonEnabled}`, this.onButtonEnableToggle);
|
||||
};
|
||||
|
||||
private onThemeChange = (theme: string): void => {
|
||||
this.state.messaging?.updateTheme({ name: theme });
|
||||
};
|
||||
|
||||
private onWidgetClose = (ev: CustomEvent<IModalWidgetCloseRequest>): void => {
|
||||
this.props.onFinished(true, ev.detail.data);
|
||||
};
|
||||
@@ -138,7 +127,7 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
|
||||
userDisplayName: OwnProfileStore.instance.displayName ?? undefined,
|
||||
userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl() ?? undefined,
|
||||
clientId: ELEMENT_CLIENT_ID,
|
||||
clientTheme: this.themeWatcher.getEffectiveTheme(),
|
||||
clientTheme: SettingsStore.getValue("theme"),
|
||||
clientLanguage: getUserLanguage(),
|
||||
baseUrl: MatrixClientPeg.safeGet().baseUrl,
|
||||
});
|
||||
|
||||
@@ -9,10 +9,10 @@ Please see LICENSE files in the repository root for full details.
|
||||
import React from "react";
|
||||
import { SERVICE_TYPES } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { _t, pickBestLanguage } from "../../../languageHandler";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import { pickBestPolicyLanguage, ServicePolicyPair } from "../../../Terms";
|
||||
import { ServicePolicyPair } from "../../../Terms";
|
||||
import ExternalLink from "../elements/ExternalLink";
|
||||
import { parseUrl } from "../../../utils/UrlUtils";
|
||||
|
||||
@@ -126,8 +126,8 @@ export default class TermsDialog extends React.PureComponent<ITermsDialogProps,
|
||||
|
||||
const policyValues = Object.values(policiesAndService.policies);
|
||||
for (let i = 0; i < policyValues.length; ++i) {
|
||||
const internationalisedPolicy = pickBestPolicyLanguage(policyValues[i]);
|
||||
if (!internationalisedPolicy) continue;
|
||||
const termDoc = policyValues[i];
|
||||
const termsLang = pickBestLanguage(Object.keys(termDoc).filter((k) => k !== "version"));
|
||||
let serviceName: JSX.Element | undefined;
|
||||
let summary: JSX.Element | undefined;
|
||||
if (i === 0) {
|
||||
@@ -136,19 +136,19 @@ export default class TermsDialog extends React.PureComponent<ITermsDialogProps,
|
||||
}
|
||||
|
||||
rows.push(
|
||||
<tr key={internationalisedPolicy.url}>
|
||||
<tr key={termDoc[termsLang].url}>
|
||||
<td className="mx_TermsDialog_service">{serviceName}</td>
|
||||
<td className="mx_TermsDialog_summary">{summary}</td>
|
||||
<td>
|
||||
<ExternalLink rel="noreferrer noopener" target="_blank" href={internationalisedPolicy.url}>
|
||||
{internationalisedPolicy.name}
|
||||
<ExternalLink rel="noreferrer noopener" target="_blank" href={termDoc[termsLang].url}>
|
||||
{termDoc[termsLang].name}
|
||||
</ExternalLink>
|
||||
</td>
|
||||
<td>
|
||||
<TermsCheckbox
|
||||
url={internationalisedPolicy.url}
|
||||
url={termDoc[termsLang].url}
|
||||
onChange={this.onTermsCheckboxChange}
|
||||
checked={Boolean(this.state.agreedUrls[internationalisedPolicy.url])}
|
||||
checked={Boolean(this.state.agreedUrls[termDoc[termsLang].url])}
|
||||
/>
|
||||
</td>
|
||||
</tr>,
|
||||
@@ -164,7 +164,7 @@ export default class TermsDialog extends React.PureComponent<ITermsDialogProps,
|
||||
for (const terms of Object.values(policiesAndService.policies)) {
|
||||
let docAgreed = false;
|
||||
for (const lang of Object.keys(terms)) {
|
||||
if (lang === "version" || typeof terms[lang] === "string") continue;
|
||||
if (lang === "version") continue;
|
||||
if (this.state.agreedUrls[terms[lang].url]) {
|
||||
docAgreed = true;
|
||||
break;
|
||||
|
||||
@@ -50,7 +50,6 @@ import { EncryptionUserSettingsTab } from "../settings/tabs/user/EncryptionUserS
|
||||
interface IProps {
|
||||
initialTabId?: UserTab;
|
||||
showMsc4108QrCode?: boolean;
|
||||
showResetIdentity?: boolean;
|
||||
sdkContext: SdkContextClass;
|
||||
onFinished(): void;
|
||||
}
|
||||
@@ -92,9 +91,8 @@ function titleForTabID(tabId: UserTab): React.ReactNode {
|
||||
export default function UserSettingsDialog(props: IProps): JSX.Element {
|
||||
const voipEnabled = useSettingValue(UIFeature.Voip);
|
||||
const mjolnirEnabled = useSettingValue("feature_mjolnir");
|
||||
// store these props in state as changing tabs back and forth should clear it
|
||||
// store this prop in state as changing tabs back and forth should clear it
|
||||
const [showMsc4108QrCode, setShowMsc4108QrCode] = useState(props.showMsc4108QrCode);
|
||||
const [showResetIdentity, setShowResetIdentity] = useState(props.showResetIdentity);
|
||||
|
||||
const getTabs = (): NonEmptyArray<Tab<UserTab>> => {
|
||||
const tabs: Tab<UserTab>[] = [];
|
||||
@@ -186,12 +184,7 @@ export default function UserSettingsDialog(props: IProps): JSX.Element {
|
||||
);
|
||||
|
||||
tabs.push(
|
||||
new Tab(
|
||||
UserTab.Encryption,
|
||||
_td("settings|encryption|title"),
|
||||
<KeyIcon />,
|
||||
<EncryptionUserSettingsTab initialState={showResetIdentity ? "reset_identity_forgot" : undefined} />,
|
||||
),
|
||||
new Tab(UserTab.Encryption, _td("settings|encryption|title"), <KeyIcon />, <EncryptionUserSettingsTab />),
|
||||
);
|
||||
|
||||
if (showLabsFlags() || SettingsStore.getFeatureSettingNames().some((k) => SettingsStore.getBetaInfo(k))) {
|
||||
@@ -226,9 +219,8 @@ export default function UserSettingsDialog(props: IProps): JSX.Element {
|
||||
const [activeTabId, _setActiveTabId] = useActiveTabWithDefault(getTabs(), UserTab.Account, props.initialTabId);
|
||||
const setActiveTabId = (tabId: UserTab): void => {
|
||||
_setActiveTabId(tabId);
|
||||
// Clear these so switching away from the tab and back to it will not show the QR code again
|
||||
// Clear this so switching away from the tab and back to it will not show the QR code again
|
||||
setShowMsc4108QrCode(false);
|
||||
setShowResetIdentity(false);
|
||||
};
|
||||
|
||||
const [activeToast, toastRack] = useActiveToast();
|
||||
|
||||
@@ -1,256 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { JSX } from "react";
|
||||
import { InlineSpinner } from "@vector-im/compound-web";
|
||||
|
||||
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
|
||||
import BaseTool from "./BaseTool";
|
||||
import { useAsyncMemo } from "../../../../hooks/useAsyncMemo";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
|
||||
interface KeyBackupProps {
|
||||
/**
|
||||
* Callback to invoke when the back button is clicked.
|
||||
*/
|
||||
onBack(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A component that displays information about the key storage and cross-signing.
|
||||
*/
|
||||
export function Crypto({ onBack }: KeyBackupProps): JSX.Element {
|
||||
const matrixClient = useMatrixClientContext();
|
||||
return (
|
||||
<BaseTool onBack={onBack} className="mx_Crypto">
|
||||
{matrixClient.getCrypto() ? (
|
||||
<>
|
||||
<KeyStorage />
|
||||
<CrossSigning />
|
||||
</>
|
||||
) : (
|
||||
<span>{_t("devtools|crypto|crypto_not_available")}</span>
|
||||
)}
|
||||
</BaseTool>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A component that displays information about the key storage.
|
||||
*/
|
||||
function KeyStorage(): JSX.Element {
|
||||
const matrixClient = useMatrixClientContext();
|
||||
const keyStorageData = useAsyncMemo(async () => {
|
||||
const crypto = matrixClient.getCrypto()!;
|
||||
|
||||
// Get all the key storage data that we will display
|
||||
const backupInfo = await crypto.getKeyBackupInfo();
|
||||
const backupKeyStored = Boolean(await matrixClient.isKeyBackupKeyStored());
|
||||
const backupKeyFromCache = await crypto.getSessionBackupPrivateKey();
|
||||
const backupKeyCached = Boolean(backupKeyFromCache);
|
||||
const backupKeyWellFormed = backupKeyFromCache instanceof Uint8Array;
|
||||
const activeBackupVersion = await crypto.getActiveSessionBackupVersion();
|
||||
const secretStorageKeyInAccount = await matrixClient.secretStorage.hasKey();
|
||||
const secretStorageReady = await crypto.isSecretStorageReady();
|
||||
|
||||
return {
|
||||
backupInfo,
|
||||
backupKeyStored,
|
||||
backupKeyCached,
|
||||
backupKeyWellFormed,
|
||||
activeBackupVersion,
|
||||
secretStorageKeyInAccount,
|
||||
secretStorageReady,
|
||||
};
|
||||
}, [matrixClient]);
|
||||
|
||||
// Show a spinner while loading
|
||||
if (keyStorageData === undefined) return <InlineSpinner aria-label={_t("common|loading")} />;
|
||||
|
||||
const {
|
||||
backupInfo,
|
||||
backupKeyStored,
|
||||
backupKeyCached,
|
||||
backupKeyWellFormed,
|
||||
activeBackupVersion,
|
||||
secretStorageKeyInAccount,
|
||||
secretStorageReady,
|
||||
} = keyStorageData;
|
||||
|
||||
return (
|
||||
<table aria-label={_t("devtools|crypto|key_storage")}>
|
||||
<thead>{_t("devtools|crypto|key_storage")}</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">{_t("devtools|crypto|key_backup_latest_version")}</th>
|
||||
<td>
|
||||
{backupInfo
|
||||
? `${backupInfo.version} (${_t("settings|security|key_backup_algorithm")} ${backupInfo.algorithm})`
|
||||
: _t("devtools|crypto|key_backup_inactive_warning")}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{_t("devtools|crypto|backup_key_stored_status")}</th>
|
||||
<td>
|
||||
{backupKeyStored
|
||||
? _t("devtools|crypto|backup_key_stored")
|
||||
: _t("devtools|crypto|backup_key_not_stored")}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{_t("devtools|crypto|key_backup_active_version")}</th>
|
||||
<td>
|
||||
{activeBackupVersion === null
|
||||
? _t("devtools|crypto|key_backup_active_version_none")
|
||||
: activeBackupVersion}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{_t("devtools|crypto|backup_key_cached_status")}</th>
|
||||
<td>
|
||||
{`${
|
||||
backupKeyCached
|
||||
? _t("devtools|crypto|backup_key_cached")
|
||||
: _t("devtools|crypto|not_found_locally")
|
||||
}, ${
|
||||
backupKeyWellFormed
|
||||
? _t("devtools|crypto|backup_key_well_formed")
|
||||
: _t("devtools|crypto|backup_key_unexpected_type")
|
||||
}`}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{_t("devtools|crypto|4s_public_key_status")}</th>
|
||||
<td>
|
||||
{secretStorageKeyInAccount
|
||||
? _t("devtools|crypto|4s_public_key_in_account_data")
|
||||
: _t("devtools|crypto|4s_public_key_not_in_account_data")}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{_t("devtools|crypto|secret_storage_status")}</th>
|
||||
<td>
|
||||
{secretStorageReady
|
||||
? _t("devtools|crypto|secret_storage_ready")
|
||||
: _t("devtools|crypto|secret_storage_not_ready")}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A component that displays information about cross-signing.
|
||||
*/
|
||||
function CrossSigning(): JSX.Element {
|
||||
const matrixClient = useMatrixClientContext();
|
||||
const crossSigningData = useAsyncMemo(async () => {
|
||||
const crypto = matrixClient.getCrypto()!;
|
||||
|
||||
// Get all the cross-signing data that we will display
|
||||
const crossSigningStatus = await crypto.getCrossSigningStatus();
|
||||
const crossSigningPublicKeysOnDevice = crossSigningStatus.publicKeysOnDevice;
|
||||
const crossSigningPrivateKeysInStorage = crossSigningStatus.privateKeysInSecretStorage;
|
||||
const masterPrivateKeyCached = crossSigningStatus.privateKeysCachedLocally.masterKey;
|
||||
const selfSigningPrivateKeyCached = crossSigningStatus.privateKeysCachedLocally.selfSigningKey;
|
||||
const userSigningPrivateKeyCached = crossSigningStatus.privateKeysCachedLocally.userSigningKey;
|
||||
const crossSigningReady = await crypto.isCrossSigningReady();
|
||||
|
||||
return {
|
||||
crossSigningPublicKeysOnDevice,
|
||||
crossSigningPrivateKeysInStorage,
|
||||
masterPrivateKeyCached,
|
||||
selfSigningPrivateKeyCached,
|
||||
userSigningPrivateKeyCached,
|
||||
crossSigningReady,
|
||||
};
|
||||
}, [matrixClient]);
|
||||
|
||||
// Show a spinner while loading
|
||||
if (crossSigningData === undefined) return <InlineSpinner aria-label={_t("common|loading")} />;
|
||||
|
||||
const {
|
||||
crossSigningPublicKeysOnDevice,
|
||||
crossSigningPrivateKeysInStorage,
|
||||
masterPrivateKeyCached,
|
||||
selfSigningPrivateKeyCached,
|
||||
userSigningPrivateKeyCached,
|
||||
crossSigningReady,
|
||||
} = crossSigningData;
|
||||
|
||||
return (
|
||||
<table aria-label={_t("devtools|crypto|cross_signing")}>
|
||||
<thead>{_t("devtools|crypto|cross_signing")}</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">{_t("devtools|crypto|cross_signing_status")}</th>
|
||||
<td>{getCrossSigningStatus(crossSigningReady, crossSigningPrivateKeysInStorage)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{_t("devtools|crypto|cross_signing_public_keys_on_device_status")}</th>
|
||||
<td>
|
||||
{crossSigningPublicKeysOnDevice
|
||||
? _t("devtools|crypto|cross_signing_public_keys_on_device")
|
||||
: _t("devtools|crypto|not_found")}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{_t("devtools|crypto|cross_signing_private_keys_in_storage_status")}</th>
|
||||
<td>
|
||||
{crossSigningPrivateKeysInStorage
|
||||
? _t("devtools|crypto|cross_signing_private_keys_in_storage")
|
||||
: _t("devtools|crypto|cross_signing_private_keys_not_in_storage")}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{_t("devtools|crypto|master_private_key_cached_status")}</th>
|
||||
<td>
|
||||
{masterPrivateKeyCached
|
||||
? _t("devtools|crypto|cross_signing_cached")
|
||||
: _t("devtools|crypto|not_found_locally")}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{_t("devtools|crypto|self_signing_private_key_cached_status")}</th>
|
||||
<td>
|
||||
{selfSigningPrivateKeyCached
|
||||
? _t("devtools|crypto|cross_signing_cached")
|
||||
: _t("devtools|crypto|not_found_locally")}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{_t("devtools|crypto|user_signing_private_key_cached_status")}</th>
|
||||
<td>
|
||||
{userSigningPrivateKeyCached
|
||||
? _t("devtools|crypto|cross_signing_cached")
|
||||
: _t("devtools|crypto|not_found_locally")}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cross-signing status.
|
||||
* @param crossSigningReady Whether cross-signing is ready.
|
||||
* @param crossSigningPrivateKeysInStorage Whether cross-signing private keys are in secret storage.
|
||||
*/
|
||||
function getCrossSigningStatus(crossSigningReady: boolean, crossSigningPrivateKeysInStorage: boolean): string {
|
||||
if (crossSigningReady) {
|
||||
return crossSigningPrivateKeysInStorage
|
||||
? _t("devtools|crypto|cross_signing_ready")
|
||||
: _t("devtools|crypto|cross_signing_untrusted");
|
||||
}
|
||||
|
||||
if (crossSigningPrivateKeysInStorage) {
|
||||
return _t("devtools|crypto|cross_signing_not_ready");
|
||||
}
|
||||
|
||||
return _t("devtools|crypto|cross_signing_not_ready");
|
||||
}
|
||||
@@ -9,12 +9,9 @@ Please see LICENSE files in the repository root for full details.
|
||||
import React, { FC, HTMLAttributes, ReactNode } from "react";
|
||||
import { RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { AvatarStack, Tooltip } from "@vector-im/compound-web";
|
||||
import classNames from "classnames";
|
||||
|
||||
import MemberAvatar from "../avatars/MemberAvatar";
|
||||
import AccessibleButton, { ButtonEvent } from "./AccessibleButton";
|
||||
import { useToggled } from "../rooms/RoomHeader/toggle/useToggled";
|
||||
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
||||
|
||||
interface IProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
|
||||
members: RoomMember[];
|
||||
@@ -60,14 +57,8 @@ const FacePile: FC<IProps> = ({
|
||||
</>
|
||||
);
|
||||
|
||||
const toggled = useToggled(RightPanelPhases.MemberList);
|
||||
const classes = classNames({
|
||||
mx_FacePile: true,
|
||||
mx_FacePile_toggled: toggled,
|
||||
});
|
||||
|
||||
const content = (
|
||||
<AccessibleButton {...props} className={classes} onClick={onClick ?? null}>
|
||||
<AccessibleButton {...props} className="mx_FacePile" onClick={onClick ?? null}>
|
||||
<AvatarStack>{pileContents}</AvatarStack>
|
||||
{children}
|
||||
</AccessibleButton>
|
||||
|
||||
@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
|
||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||
|
||||
import ToggleSwitch from "./ToggleSwitch";
|
||||
import { Caption } from "../typography/Caption";
|
||||
@@ -36,7 +36,7 @@ interface IProps {
|
||||
}
|
||||
|
||||
export default class LabelledToggleSwitch extends React.PureComponent<IProps> {
|
||||
private readonly id = `mx_LabelledToggleSwitch_${secureRandomString(12)}`;
|
||||
private readonly id = `mx_LabelledToggleSwitch_${randomString(12)}`;
|
||||
|
||||
public render(): React.ReactNode {
|
||||
// This is a minimal version of a SettingsFlag
|
||||
|
||||
@@ -25,7 +25,6 @@ export enum PillType {
|
||||
AtRoomMention = "TYPE_AT_ROOM_MENTION", // '@room' mention
|
||||
EventInSameRoom = "TYPE_EVENT_IN_SAME_ROOM",
|
||||
EventInOtherRoom = "TYPE_EVENT_IN_OTHER_ROOM",
|
||||
Keyword = "TYPE_KEYWORD", // Used to highlight keywords that triggered a notification rule
|
||||
}
|
||||
|
||||
export const pillRoomNotifPos = (text: string | null): number => {
|
||||
@@ -77,32 +76,14 @@ export interface PillProps {
|
||||
room?: Room;
|
||||
// Whether to include an avatar in the pill
|
||||
shouldShowPillAvatar?: boolean;
|
||||
// Explicitly-provided text to display in the pill
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export const Pill: React.FC<PillProps> = ({
|
||||
type: propType,
|
||||
url,
|
||||
inMessage,
|
||||
room,
|
||||
shouldShowPillAvatar = true,
|
||||
text: customPillText,
|
||||
}) => {
|
||||
const {
|
||||
event,
|
||||
member,
|
||||
onClick,
|
||||
resourceId,
|
||||
targetRoom,
|
||||
text: linkText,
|
||||
type,
|
||||
} = usePermalink({
|
||||
export const Pill: React.FC<PillProps> = ({ type: propType, url, inMessage, room, shouldShowPillAvatar = true }) => {
|
||||
const { event, member, onClick, resourceId, targetRoom, text, type } = usePermalink({
|
||||
room,
|
||||
type: propType,
|
||||
url,
|
||||
});
|
||||
const text = customPillText ?? linkText;
|
||||
|
||||
if (!type || !text) {
|
||||
return null;
|
||||
@@ -115,7 +96,6 @@ export const Pill: React.FC<PillProps> = ({
|
||||
mx_UserPill: type === PillType.UserMention,
|
||||
mx_UserPill_me: resourceId === MatrixClientPeg.safeGet().getUserId(),
|
||||
mx_EventPill: type === PillType.EventInOtherRoom || type === PillType.EventInSameRoom,
|
||||
mx_KeywordPill: type === PillType.Keyword,
|
||||
});
|
||||
|
||||
let avatar: ReactElement | null = null;
|
||||
@@ -151,8 +131,6 @@ export const Pill: React.FC<PillProps> = ({
|
||||
case PillType.UserMention:
|
||||
avatar = <PillMemberAvatar shouldShowPillAvatar={shouldShowPillAvatar} member={member} />;
|
||||
break;
|
||||
case PillType.Keyword:
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
|
||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { _t } from "../../../languageHandler";
|
||||
@@ -35,7 +35,7 @@ interface IState {
|
||||
}
|
||||
|
||||
export default class SettingsFlag extends React.Component<IProps, IState> {
|
||||
private readonly id = `mx_SettingsFlag_${secureRandomString(12)}`;
|
||||
private readonly id = `mx_SettingsFlag_${randomString(12)}`;
|
||||
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { Ref } from "react";
|
||||
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
|
||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||
import classnames from "classnames";
|
||||
|
||||
export enum CheckboxStyle {
|
||||
@@ -33,7 +33,7 @@ export default class StyledCheckbox extends React.PureComponent<IProps, IState>
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
// 56^10 so unlikely chance of collision.
|
||||
this.id = this.props.id || "checkbox_" + secureRandomString(10);
|
||||
this.id = this.props.id || "checkbox_" + randomString(10);
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
ContentHelpers,
|
||||
M_BEACON,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
|
||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||
import classNames from "classnames";
|
||||
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
@@ -81,10 +81,10 @@ const useBeaconState = (
|
||||
// eg thread and main timeline, reply
|
||||
// maplibregl needs a unique id to attach the map instance to
|
||||
const useUniqueId = (eventId: string): string => {
|
||||
const [id, setId] = useState(`${eventId}_${secureRandomString(8)}`);
|
||||
const [id, setId] = useState(`${eventId}_${randomString(8)}`);
|
||||
|
||||
useEffect(() => {
|
||||
setId(`${eventId}_${secureRandomString(8)}`);
|
||||
setId(`${eventId}_${randomString(8)}`);
|
||||
}, [eventId]);
|
||||
|
||||
return id;
|
||||
|
||||
@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import React from "react";
|
||||
import { MatrixEvent, ClientEvent, ClientEventHandlerMap } from "matrix-js-sdk/src/matrix";
|
||||
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
|
||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||
import { Tooltip } from "@vector-im/compound-web";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
@@ -41,7 +41,7 @@ export default class MLocationBody extends React.Component<IBodyProps, IState> {
|
||||
|
||||
// multiple instances of same map might be in document
|
||||
// eg thread and main timeline, reply
|
||||
const idSuffix = `${props.mxEvent.getId()}_${secureRandomString(8)}`;
|
||||
const idSuffix = `${props.mxEvent.getId()}_${randomString(8)}`;
|
||||
this.mapId = `mx_MLocationBody_${idSuffix}`;
|
||||
|
||||
this.reconnectedListener = createReconnectedListener(this.clearError);
|
||||
|
||||
@@ -7,9 +7,8 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { createRef, SyntheticEvent, MouseEvent, StrictMode } from "react";
|
||||
import { MsgType, PushRuleKind } from "matrix-js-sdk/src/matrix";
|
||||
import { MsgType } from "matrix-js-sdk/src/matrix";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
import { globToRegexp } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import * as HtmlUtils from "../../../HtmlUtils";
|
||||
import { formatDate } from "../../../DateUtils";
|
||||
@@ -36,7 +35,6 @@ import { EditWysiwygComposer } from "../rooms/wysiwyg_composer";
|
||||
import { IEventTileOps } from "../rooms/EventTile";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import CodeBlock from "./CodeBlock";
|
||||
import { Pill, PillType } from "../elements/Pill";
|
||||
import { ReactRootManager } from "../../../utils/react";
|
||||
|
||||
interface IState {
|
||||
@@ -102,16 +100,6 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Highlight notification keywords using pills
|
||||
const pushDetails = this.props.mxEvent.getPushDetails();
|
||||
if (
|
||||
pushDetails.rule?.enabled &&
|
||||
pushDetails.rule.kind === PushRuleKind.ContentSpecific &&
|
||||
pushDetails.rule.pattern
|
||||
) {
|
||||
this.pillifyNotificationKeywords([content], this.regExpForKeywordPattern(pushDetails.rule.pattern));
|
||||
}
|
||||
}
|
||||
|
||||
private addCodeElement(pre: HTMLPreElement): void {
|
||||
@@ -222,55 +210,6 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the text that activated a push-notification keyword pattern.
|
||||
*/
|
||||
private pillifyNotificationKeywords(nodes: ArrayLike<Element>, exp: RegExp): void {
|
||||
let node: Node | null = nodes[0];
|
||||
while (node) {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
const text = node.nodeValue;
|
||||
if (!text) {
|
||||
node = node.nextSibling;
|
||||
continue;
|
||||
}
|
||||
const match = text.match(exp);
|
||||
if (!match || match.length < 3) {
|
||||
node = node.nextSibling;
|
||||
continue;
|
||||
}
|
||||
const keywordText = match[2];
|
||||
const idx = match.index! + match[1].length;
|
||||
const before = text.substring(0, idx);
|
||||
const after = text.substring(idx + keywordText.length);
|
||||
|
||||
const container = document.createElement("span");
|
||||
const newContent = (
|
||||
<>
|
||||
{before}
|
||||
<TooltipProvider>
|
||||
<Pill text={keywordText} type={PillType.Keyword} />
|
||||
</TooltipProvider>
|
||||
{after}
|
||||
</>
|
||||
);
|
||||
this.reactRoots.render(newContent, container, node);
|
||||
|
||||
node.parentNode?.replaceChild(container, node);
|
||||
} else if (node.childNodes && node.childNodes.length) {
|
||||
this.pillifyNotificationKeywords(node.childNodes as NodeListOf<Element>, exp);
|
||||
}
|
||||
|
||||
node = node.nextSibling;
|
||||
}
|
||||
}
|
||||
|
||||
private regExpForKeywordPattern(pattern: string): RegExp {
|
||||
// Reflects the push notification pattern-matching implementation at
|
||||
// https://github.com/matrix-org/matrix-js-sdk/blob/dbd7d26968b94700827bac525c39afff2c198e61/src/pushprocessor.ts#L570
|
||||
return new RegExp("(^|\\W)(" + globToRegexp(pattern) + ")(\\W|$)", "i");
|
||||
}
|
||||
|
||||
private findLinks(nodes: ArrayLike<Element>): string[] {
|
||||
let links: string[] = [];
|
||||
|
||||
|
||||
@@ -85,7 +85,6 @@ import { asyncSome } from "../../../utils/arrays";
|
||||
import { Flex } from "../../utils/Flex";
|
||||
import CopyableText from "../elements/CopyableText";
|
||||
import { useUserTimezone } from "../../../hooks/useUserTimezone";
|
||||
|
||||
export interface IDevice extends Device {
|
||||
ambiguous?: boolean;
|
||||
}
|
||||
@@ -581,10 +580,8 @@ export const warnSelfDemote = async (isSpace: boolean): Promise<boolean> => {
|
||||
|
||||
const Container: React.FC<{
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}> = ({ children, className }) => {
|
||||
const classes = classNames("mx_UserInfo_container", className);
|
||||
return <div className={classes}>{children}</div>;
|
||||
}> = ({ children }) => {
|
||||
return <div className="mx_UserInfo_container">{children}</div>;
|
||||
};
|
||||
|
||||
interface IPowerLevelsContent {
|
||||
@@ -1710,10 +1707,10 @@ export const UserInfoHeader: React.FC<{
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Container className="mx_UserInfo_header">
|
||||
<Container>
|
||||
<Flex direction="column" align="center" className="mx_UserInfo_profile">
|
||||
<Heading size="sm" weight="semibold" as="h1" dir="auto">
|
||||
<Flex className="mx_UserInfo_profile_name" direction="row-reverse" align="center">
|
||||
<Flex direction="row-reverse" align="center">
|
||||
{displayName}
|
||||
{e2eIcon}
|
||||
</Flex>
|
||||
@@ -1721,11 +1718,11 @@ export const UserInfoHeader: React.FC<{
|
||||
{presenceLabel}
|
||||
{timezoneInfo && (
|
||||
<Tooltip label={timezoneInfo?.timezone ?? ""}>
|
||||
<Flex align="center" className="mx_UserInfo_timezone">
|
||||
<span className="mx_UserInfo_timezone">
|
||||
<Text size="sm" weight="regular">
|
||||
{timezoneInfo?.friendly ?? ""}
|
||||
</Text>
|
||||
</Flex>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Text size="sm" weight="semibold" className="mx_UserInfo_profile_mxid">
|
||||
|
||||
@@ -88,10 +88,12 @@ function getHeaderLabelJSX(vm: MemberListViewState): React.ReactNode {
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
if (vm.memberCount === 0) {
|
||||
|
||||
const filteredMemberCount = vm.members.length;
|
||||
if (filteredMemberCount === 0) {
|
||||
return _t("member_list|no_matches");
|
||||
}
|
||||
return _t("member_list|count", { count: vm.memberCount });
|
||||
return _t("member_list|count", { count: filteredMemberCount });
|
||||
}
|
||||
|
||||
export const MemberListHeaderView: React.FC<Props> = (props: Props) => {
|
||||
|
||||
@@ -11,17 +11,12 @@ import { List, ListRowProps } from "react-virtualized/dist/commonjs/List";
|
||||
import { AutoSizer } from "react-virtualized";
|
||||
|
||||
import { Flex } from "../../../utils/Flex";
|
||||
import {
|
||||
MemberWithSeparator,
|
||||
SEPARATOR,
|
||||
useMemberListViewModel,
|
||||
} from "../../../viewmodels/memberlist/MemberListViewModel";
|
||||
import { useMemberListViewModel } from "../../../viewmodels/memberlist/MemberListViewModel";
|
||||
import { RoomMemberTileView } from "./tiles/RoomMemberTileView";
|
||||
import { ThreePidInviteTileView } from "./tiles/ThreePidInviteTileView";
|
||||
import { MemberListHeaderView } from "./MemberListHeaderView";
|
||||
import BaseCard from "../../right_panel/BaseCard";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { RovingTabIndexProvider } from "../../../../accessibility/RovingTabIndex";
|
||||
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
@@ -31,41 +26,10 @@ interface IProps {
|
||||
const MemberListView: React.FC<IProps> = (props: IProps) => {
|
||||
const vm = useMemberListViewModel(props.roomId);
|
||||
|
||||
const totalRows = vm.members.length;
|
||||
|
||||
const getRowComponent = (item: MemberWithSeparator): React.JSX.Element => {
|
||||
if (item === SEPARATOR) {
|
||||
return <hr className="mx_MemberListView_separator" />;
|
||||
} else if (item.member) {
|
||||
return <RoomMemberTileView member={item.member} showPresence={vm.isPresenceEnabled} />;
|
||||
} else {
|
||||
return <ThreePidInviteTileView threePidInvite={item.threePidInvite} />;
|
||||
}
|
||||
};
|
||||
|
||||
const getRowHeight = ({ index }: { index: number }): number => {
|
||||
if (vm.members[index] === SEPARATOR) {
|
||||
/**
|
||||
* This is a separator of 2px height rendered between
|
||||
* joined and invited members.
|
||||
*/
|
||||
return 2;
|
||||
} else if (totalRows && index === totalRows) {
|
||||
/**
|
||||
* The empty spacer div rendered at the bottom should
|
||||
* have a height of 32px.
|
||||
*/
|
||||
return 32;
|
||||
} else {
|
||||
/**
|
||||
* The actual member tiles have a height of 56px.
|
||||
*/
|
||||
return 56;
|
||||
}
|
||||
};
|
||||
const memberCount = vm.members.length;
|
||||
|
||||
const rowRenderer = ({ key, index, style }: ListRowProps): React.JSX.Element => {
|
||||
if (index === totalRows) {
|
||||
if (index === memberCount) {
|
||||
// We've rendered all the members,
|
||||
// now we render an empty div to add some space to the end of the list.
|
||||
return <div key={key} style={style} />;
|
||||
@@ -73,7 +37,11 @@ const MemberListView: React.FC<IProps> = (props: IProps) => {
|
||||
const item = vm.members[index];
|
||||
return (
|
||||
<div key={key} style={style}>
|
||||
{getRowComponent(item)}
|
||||
{item.member ? (
|
||||
<RoomMemberTileView member={item.member} showPresence={vm.isPresenceEnabled} />
|
||||
) : (
|
||||
<ThreePidInviteTileView threePidInvite={item.threePidInvite} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -87,33 +55,26 @@ const MemberListView: React.FC<IProps> = (props: IProps) => {
|
||||
header={_t("common|people")}
|
||||
onClose={props.onClose}
|
||||
>
|
||||
<RovingTabIndexProvider handleUpDown scrollIntoView>
|
||||
{({ onKeyDownHandler }) => (
|
||||
<Flex
|
||||
align="stretch"
|
||||
direction="column"
|
||||
className="mx_MemberListView_container"
|
||||
onKeyDown={onKeyDownHandler}
|
||||
>
|
||||
<Form.Root>
|
||||
<MemberListHeaderView vm={vm} />
|
||||
</Form.Root>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<List
|
||||
rowRenderer={rowRenderer}
|
||||
rowHeight={getRowHeight}
|
||||
// The +1 refers to the additional empty div that we render at the end of the list.
|
||||
rowCount={totalRows + 1}
|
||||
// Subtract the height of MemberlistHeaderView so that the parent div does not overflow.
|
||||
height={height - 113}
|
||||
width={width}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</Flex>
|
||||
)}
|
||||
</RovingTabIndexProvider>
|
||||
<Flex align="stretch" direction="column" className="mx_MemberListView_container">
|
||||
<Form.Root>
|
||||
<MemberListHeaderView vm={vm} />
|
||||
</Form.Root>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<List
|
||||
rowRenderer={rowRenderer}
|
||||
// All the member tiles will have a height of 56px.
|
||||
// The additional empty div at the end of the list should have a height of 32px.
|
||||
rowHeight={({ index }) => (index === memberCount ? 32 : 56)}
|
||||
// The +1 refers to the additional empty div that we render at the end of the list.
|
||||
rowCount={memberCount + 1}
|
||||
// Subtract the height of MemberlistHeaderView so that the parent div does not overflow.
|
||||
height={height - 113}
|
||||
width={width}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</Flex>
|
||||
</BaseCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,8 +14,7 @@ import { E2EIconView } from "./common/E2EIconView";
|
||||
import AvatarPresenceIconView from "./common/PresenceIconView";
|
||||
import BaseAvatar from "../../../avatars/BaseAvatar";
|
||||
import { _t } from "../../../../../languageHandler";
|
||||
import { MemberTileView } from "./common/MemberTileView";
|
||||
import { InvitedIconView } from "./common/InvitedIconView";
|
||||
import { MemberTileLayout } from "./common/MemberTileLayout";
|
||||
|
||||
interface IProps {
|
||||
member: RoomMember;
|
||||
@@ -44,23 +43,25 @@ export function RoomMemberTileView(props: IProps): JSX.Element {
|
||||
presenceJSX = <AvatarPresenceIconView presenceState={presenceState} />;
|
||||
}
|
||||
|
||||
let iconJsx;
|
||||
if (vm.e2eStatus) {
|
||||
iconJsx = <E2EIconView status={vm.e2eStatus} />;
|
||||
let userLabelJSX;
|
||||
if (vm.userLabel) {
|
||||
userLabelJSX = <div className="mx_MemberTileView_user_label">{vm.userLabel}</div>;
|
||||
}
|
||||
if (member.isInvite) {
|
||||
iconJsx = <InvitedIconView isThreePid={false} />;
|
||||
|
||||
let e2eIcon;
|
||||
if (vm.e2eStatus) {
|
||||
e2eIcon = <E2EIconView status={vm.e2eStatus} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<MemberTileView
|
||||
<MemberTileLayout
|
||||
title={vm.title}
|
||||
onClick={vm.onClick}
|
||||
avatarJsx={av}
|
||||
presenceJsx={presenceJSX}
|
||||
nameJsx={nameJSX}
|
||||
userLabel={vm.userLabel}
|
||||
iconJsx={iconJsx}
|
||||
userLabelJsx={userLabelJSX}
|
||||
e2eIconJsx={e2eIcon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||