diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ac1856386..01c67c34c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,41 @@ +Changes in [1.11.97](https://github.com/element-hq/element-web/releases/tag/v1.11.97) (2025-04-08) +================================================================================================== +## ✨ Features + +* New room list: reduce padding between avatar and room list border ([#29634](https://github.com/element-hq/element-web/pull/29634)). Contributed by @florianduros. +* Bundle Element Call with Element Web packages ([#29309](https://github.com/element-hq/element-web/pull/29309)). Contributed by @t3chguy. +* Hide an event notification if it is redacted ([#29605](https://github.com/element-hq/element-web/pull/29605)). Contributed by @Half-Shot. +* Docker: Use nginx-unprivileged as base image ([#29353](https://github.com/element-hq/element-web/pull/29353)). Contributed by @AndrewFerr. +* Switch away from nesting React trees and mangling the DOM ([#29586](https://github.com/element-hq/element-web/pull/29586)). Contributed by @t3chguy. +* New room list: add notification decoration ([#29552](https://github.com/element-hq/element-web/pull/29552)). Contributed by @florianduros. +* RoomListStore: Unread filter should match rooms that were marked as unread ([#29580](https://github.com/element-hq/element-web/pull/29580)). Contributed by @MidhunSureshR. +* Add support for hiding videos ([#29496](https://github.com/element-hq/element-web/pull/29496)). Contributed by @Half-Shot. +* Use an outline icon for the report room button ([#29573](https://github.com/element-hq/element-web/pull/29573)). Contributed by @robintown. +* Generate/load pickle key on SSO ([#29568](https://github.com/element-hq/element-web/pull/29568)). Contributed by @Jujure. +* Add report room dialog button/dialog. ([#29513](https://github.com/element-hq/element-web/pull/29513)). Contributed by @Half-Shot. +* RoomListViewModel: Make the active room sticky in the list ([#29551](https://github.com/element-hq/element-web/pull/29551)). Contributed by @MidhunSureshR. +* Replace checkboxes with Compound checkboxes, and appropriately label each checkbox. ([#29363](https://github.com/element-hq/element-web/pull/29363)). Contributed by @Half-Shot. +* New room list: add selection decoration ([#29531](https://github.com/element-hq/element-web/pull/29531)). Contributed by @florianduros. +* Simplified Sliding Sync ([#28515](https://github.com/element-hq/element-web/pull/28515)). Contributed by @dbkr. +* Add ability to hide images after clicking "show image" ([#29467](https://github.com/element-hq/element-web/pull/29467)). Contributed by @Half-Shot. + +## 🐛 Bug Fixes + +* Fix scroll issues in memberlist ([#29392](https://github.com/element-hq/element-web/pull/29392)). Contributed by @MidhunSureshR. +* Ensure clicks on spoilers do not get handled by the hidden content ([#29618](https://github.com/element-hq/element-web/pull/29618)). Contributed by @t3chguy. +* New room list: add cursor pointer on room list item ([#29627](https://github.com/element-hq/element-web/pull/29627)). Contributed by @florianduros. +* Fix missing ambiguous url tooltips on Element Desktop ([#29619](https://github.com/element-hq/element-web/pull/29619)). Contributed by @t3chguy. +* New room list: fix spacing and padding ([#29607](https://github.com/element-hq/element-web/pull/29607)). Contributed by @florianduros. +* Make fetchdep check out matching branch name ([#29601](https://github.com/element-hq/element-web/pull/29601)). Contributed by @dbkr. +* Fix MFileBody fileName not considering `filename` ([#29589](https://github.com/element-hq/element-web/pull/29589)). Contributed by @t3chguy. +* Fix token expiry racing with login causing wrong error to be shown ([#29566](https://github.com/element-hq/element-web/pull/29566)). Contributed by @t3chguy. +* Fix bug which caused startup to hang if the clock was wound back since a previous session ([#29558](https://github.com/element-hq/element-web/pull/29558)). Contributed by @richvdh. +* RoomListViewModel: Reset any primary filter on secondary filter change ([#29562](https://github.com/element-hq/element-web/pull/29562)). Contributed by @MidhunSureshR. +* RoomListStore: Unread filter should only filter rooms having unread counts ([#29555](https://github.com/element-hq/element-web/pull/29555)). Contributed by @MidhunSureshR. +* In force-verify mode, prevent bypassing by cancelling device verification ([#29487](https://github.com/element-hq/element-web/pull/29487)). Contributed by @andybalaam. +* Add title attribute to user identifier ([#29547](https://github.com/element-hq/element-web/pull/29547)). Contributed by @arpitbatra123. + + Changes in [1.11.96](https://github.com/element-hq/element-web/releases/tag/v1.11.96) (2025-03-25) ================================================================================================== ## ✨ Features diff --git a/package.json b/package.json index 2e5bca3573..1463cc9948 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "element-web", - "version": "1.11.96", + "version": "1.11.97", "description": "Element: the future of secure communication", "author": "New Vector Ltd.", "repository": { diff --git a/playwright/e2e/crypto/backups.spec.ts b/playwright/e2e/crypto/backups.spec.ts deleted file mode 100644 index e0cf1cc60b..0000000000 --- a/playwright/e2e/crypto/backups.spec.ts +++ /dev/null @@ -1,108 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import { 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( - version + " (Algorithm: m.megolm_backup.v1.curve25519-aes-sha2)", - ); - - await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(6) td")).toHaveText(version); -} - -test.describe("Backups", () => { - test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here"); - test.use({ - displayName: "Hanako", - }); - - test( - "Create, delete and recreate a keys backup", - { tag: "@no-webkit" }, - async ({ page, user, app }, workerInfo) => { - // Create a backup - const securityTab = await app.settings.openUserSettings("Security & Privacy"); - - await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible(); - await securityTab.getByRole("button", { name: "Set up", exact: true }).click(); - - const securityKey = await completeCreateSecretStorageDialog(page); - - // Open the settings again - await app.settings.openUserSettings("Security & Privacy"); - await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible(); - - // expand the advanced section to see the active version in the reports - await page - .locator(".mx_Dialog .mx_SettingsSubsection_content details .mx_SecureBackupPanel_advanced") - .locator("..") - .click(); - - 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" - - // Create another - await securityTab.getByRole("button", { name: "Set up", exact: true }).click(); - await expect(currentDialogLocator.getByRole("heading", { name: "Recovery Key" })).toBeVisible(); - await currentDialogLocator.getByLabel("Recovery Key").fill(securityKey); - await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click(); - - // Should be successful - await expect(currentDialogLocator.getByRole("heading", { name: "Success!" })).toBeVisible(); - await currentDialogLocator.getByRole("button", { name: "OK", exact: true }).click(); - - // Open the settings again - await app.settings.openUserSettings("Security & Privacy"); - await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible(); - - // expand the advanced section to see the active version in the reports - await page - .locator(".mx_Dialog .mx_SettingsSubsection_content details .mx_SecureBackupPanel_advanced") - .locator("..") - .click(); - - await expectBackupVersionToBe(page, "2"); - - // == - // Ensure that if you don't have the secret storage passphrase the backup won't be created - // == - - // First delete version 2 - await securityTab.getByRole("button", { name: "Delete Backup", exact: true }).click(); - await expect(currentDialogLocator.getByRole("heading", { name: "Delete Backup" })).toBeVisible(); - // Click "Delete Backup" - await currentDialogLocator.getByTestId("dialog-primary-button").click(); - - // Try to create another - await securityTab.getByRole("button", { name: "Set up", exact: true }).click(); - await expect(currentDialogLocator.getByRole("heading", { name: "Recovery Key" })).toBeVisible(); - // But cancel the recovery key dialog, to simulate not having the secret storage passphrase - await currentDialogLocator.getByTestId("dialog-cancel-button").click(); - - await expect(currentDialogLocator.getByRole("heading", { name: "Starting backup…" })).toBeVisible(); - // check that it failed - await expect(currentDialogLocator.getByText("Unable to create key backup")).toBeVisible(); - // cancel - await currentDialogLocator.getByTestId("dialog-cancel-button").click(); - - // go back to the settings to check that no backup was created (the setup button should still be there) - await app.settings.openUserSettings("Security & Privacy"); - await expect(securityTab.getByRole("button", { name: "Set up", exact: true })).toBeVisible(); - }, - ); -}); diff --git a/playwright/e2e/crypto/crypto.spec.ts b/playwright/e2e/crypto/crypto.spec.ts index 6adef5be60..bad9072f0c 100644 --- a/playwright/e2e/crypto/crypto.spec.ts +++ b/playwright/e2e/crypto/crypto.spec.ts @@ -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, createSharedRoomWithUser, enableKeyBackup, verify } from "./utils"; import { type Bot } from "../../pages/bot"; import { type ElementAppPage } from "../../pages/ElementAppPage"; import { isDendrite } from "../../plugins/homeserver/dendrite"; @@ -84,86 +77,43 @@ test.describe("Cryptography", function () { }, }); - for (const isDeviceVerified of [true, false]) { - test.describe(`setting up secure key backup should work isDeviceVerified=${isDeviceVerified}`, () => { - /** - * Verify that the `m.cross_signing.${keyType}` key is available on the account data on the server - * @param keyType - */ - async function verifyKey(app: ElementAppPage, keyType: "master" | "self_signing" | "user_signing") { - const accountData: { encrypted: Record> } = await app.client.evaluate( - (cli, keyType) => cli.getAccountDataFromServer(`m.cross_signing.${keyType}`), - keyType, - ); - expect(accountData.encrypted).toBeDefined(); - const keys = Object.keys(accountData.encrypted); - const key = accountData.encrypted[keys[0]]; - expect(key.ciphertext).toBeDefined(); - expect(key.iv).toBeDefined(); - expect(key.mac).toBeDefined(); - } + /** + * Verify that the `m.cross_signing.${keyType}` key is available on the account data on the server + * @param keyType + */ + async function verifyKey(app: ElementAppPage, keyType: "master" | "self_signing" | "user_signing") { + const accountData: { encrypted: Record> } = await app.client.evaluate( + (cli, keyType) => cli.getAccountDataFromServer(`m.cross_signing.${keyType}`), + keyType, + ); - test("by recovery code", async ({ page, app, user: aliceCredentials }) => { - // Verified the device - if (isDeviceVerified) { - await app.client.bootstrapCrossSigning(aliceCredentials); - } - - await page.route("**/_matrix/client/v3/keys/signatures/upload", async (route) => { - // We delay this API otherwise the `Setting up keys` may happen too quickly and cause flakiness - await new Promise((resolve) => setTimeout(resolve, 500)); - await route.continue(); - }); - - await app.settings.openUserSettings("Security & Privacy"); - await page.getByRole("button", { name: "Set up Secure Backup" }).click(); - - await completeCreateSecretStorageDialog(page); - - // Verify that the SSSS keys are in the account data stored in the server - await verifyKey(app, "master"); - await verifyKey(app, "self_signing"); - await verifyKey(app, "user_signing"); - }); - - test("by passphrase", async ({ page, app, user: aliceCredentials }) => { - // Verified the device - if (isDeviceVerified) { - await app.client.bootstrapCrossSigning(aliceCredentials); - } - - await app.settings.openUserSettings("Security & Privacy"); - await page.getByRole("button", { name: "Set up Secure Backup" }).click(); - - const dialog = page.locator(".mx_Dialog"); - // Select passphrase option - await dialog.getByText("Enter a Security Phrase").click(); - await dialog.getByRole("button", { name: "Continue" }).click(); - - // Fill passphrase input - await dialog.locator("input").fill("new passphrase for setting up a secure key backup"); - await dialog.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click(); - // Confirm passphrase - await dialog.locator("input").fill("new passphrase for setting up a secure key backup"); - await dialog.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click(); - - await copyAndContinue(page); - - 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"); - await verifyKey(app, "self_signing"); - await verifyKey(app, "user_signing"); - }); - }); + expect(accountData.encrypted).toBeDefined(); + const keys = Object.keys(accountData.encrypted); + const key = accountData.encrypted[keys[0]]; + expect(key.ciphertext).toBeDefined(); + expect(key.iv).toBeDefined(); + expect(key.mac).toBeDefined(); } + test("Setting up key backup by recovery key", async ({ page, app, user: aliceCredentials }) => { + await app.client.bootstrapCrossSigning(aliceCredentials); + + await enableKeyBackup(app); + + // Wait for the cross signing keys to be uploaded + // Waiting for "Change the recovery key" button ensure that all the secrets are uploaded and cached locally + const encryptionTab = await app.settings.openUserSettings("Encryption"); + await expect(encryptionTab.getByRole("button", { name: "Change recovery key" })).toBeVisible(); + + // Verify that the SSSS keys are in the account data stored in the server + await verifyKey(app, "master"); + await verifyKey(app, "self_signing"); + await verifyKey(app, "user_signing"); + }); + test("Can reset cross-signing keys", async ({ page, app, user: aliceCredentials }) => { await app.client.bootstrapCrossSigning(aliceCredentials); - const secretStorageKey = await enableKeyBackup(app); + await enableKeyBackup(app); // Fetch the current cross-signing keys async function fetchMasterKey() { @@ -177,18 +127,15 @@ test.describe("Cryptography", function () { return k; }); } + const masterKey1 = await fetchMasterKey(); - // Find the "reset cross signing" button, and click it - await app.settings.openUserSettings("Security & Privacy"); - await page.locator("div.mx_CrossSigningPanel_buttonRow").getByRole("button", { name: "Reset" }).click(); + // Find "the Reset cryptographic identity" button + const encryptionTab = await app.settings.openUserSettings("Encryption"); + await encryptionTab.getByRole("button", { name: "Reset cryptographic identity" }).click(); // Confirm - await page.getByRole("button", { name: "Clear cross-signing keys" }).click(); - - // Enter the 4S key - await page.getByPlaceholder("Recovery Key").fill(secretStorageKey); - await page.getByRole("button", { name: "Continue" }).click(); + await encryptionTab.getByRole("button", { name: "Continue" }).click(); // Enter the password await page.getByPlaceholder("Password").fill(aliceCredentials.password); @@ -198,9 +145,6 @@ test.describe("Cryptography", function () { const masterKey2 = await fetchMasterKey(); expect(masterKey1).not.toEqual(masterKey2); }).toPass(); - - // The dialog should have gone away - await expect(page.locator(".mx_Dialog")).toHaveCount(1); }); test( diff --git a/playwright/e2e/room/invites.spec.ts b/playwright/e2e/room/invites.spec.ts new file mode 100644 index 0000000000..d81fb13de1 --- /dev/null +++ b/playwright/e2e/room/invites.spec.ts @@ -0,0 +1,67 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { test, expect } from "../../element-web-test"; + +test.describe("Invites", () => { + test.use({ + displayName: "Alice", + botCreateOpts: { + displayName: "Bob", + }, + }); + + test("should render an invite view", { tag: "@screenshot" }, async ({ page, homeserver, user, bot, app }) => { + const roomId = await bot.createRoom({ is_direct: true }); + await bot.inviteUser(roomId, user.userId); + await app.viewRoomByName("Bob"); + await expect(page.locator(".mx_RoomView")).toMatchScreenshot("Invites_room_view.png"); + }); + + test("should be able to decline an invite", async ({ page, homeserver, user, bot, app }) => { + const roomId = await bot.createRoom({ is_direct: true }); + await bot.inviteUser(roomId, user.userId); + await app.viewRoomByName("Bob"); + await page.getByRole("button", { name: "Decline", exact: true }).click(); + await expect(page.getByRole("heading", { name: "Welcome Alice", exact: true })).toBeVisible(); + await expect( + page.getByRole("tree", { name: "Rooms" }).getByRole("treeitem", { name: "Bob", exact: true }), + ).not.toBeVisible(); + }); + + test( + "should be able to decline an invite, report the room and ignore the user", + { tag: "@screenshot" }, + async ({ page, homeserver, user, bot, app }) => { + const roomId = await bot.createRoom({ is_direct: true }); + await bot.inviteUser(roomId, user.userId); + await app.viewRoomByName("Bob"); + await page.getByRole("button", { name: "Decline and block" }).click(); + await page.getByLabel("Ignore user").click(); + await page.getByLabel("Report room").click(); + await page.getByLabel("Reason").fill("Do not want the room"); + const roomReported = page.waitForRequest( + (req) => + req.url().endsWith(`/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/report`) && + req.method() === "POST", + ); + await expect(page.getByRole("dialog", { name: "Decline invitation" })).toMatchScreenshot( + "Invites_reject_dialog.png", + ); + await page.getByRole("button", { name: "Decline invite" }).click(); + + // Check room was reported. + await roomReported; + + // Check user is ignored. + await app.settings.openUserSettings("Security & Privacy"); + const ignoredUsersList = page.getByRole("list", { name: "Ignored users" }); + await ignoredUsersList.scrollIntoViewIfNeeded(); + await expect(ignoredUsersList.getByRole("listitem", { name: bot.credentials.userId })).toBeVisible(); + }, + ); +}); diff --git a/playwright/e2e/sliding-sync/sliding-sync.spec.ts b/playwright/e2e/sliding-sync/sliding-sync.spec.ts index 118bd4585e..b540cd11d5 100644 --- a/playwright/e2e/sliding-sync/sliding-sync.spec.ts +++ b/playwright/e2e/sliding-sync/sliding-sync.spec.ts @@ -255,8 +255,8 @@ test.describe("Sliding Sync", () => { // Select the room to reject await page.getByRole("treeitem", { name: "Room to Reject" }).click(); - // Reject the invite - await page.locator(".mx_RoomView").getByRole("button", { name: "Reject", exact: true }).click(); + // Decline the invite + await page.locator(".mx_RoomView").getByRole("button", { name: "Decline", exact: true }).click(); await expect( page.getByRole("group", { name: "Invites" }).locator(".mx_RoomSublist_tiles").getByRole("treeitem"), diff --git a/playwright/snapshots/right-panel/right-panel.spec.ts/room-report-dialog-linux.png b/playwright/snapshots/right-panel/right-panel.spec.ts/room-report-dialog-linux.png index bfaf0e909e..3beae421d4 100644 Binary files a/playwright/snapshots/right-panel/right-panel.spec.ts/room-report-dialog-linux.png and b/playwright/snapshots/right-panel/right-panel.spec.ts/room-report-dialog-linux.png differ diff --git a/playwright/snapshots/room/invites.spec.ts/Invites-reject-dialog-linux.png b/playwright/snapshots/room/invites.spec.ts/Invites-reject-dialog-linux.png new file mode 100644 index 0000000000..71f3e420ab Binary files /dev/null and b/playwright/snapshots/room/invites.spec.ts/Invites-reject-dialog-linux.png differ diff --git a/playwright/snapshots/room/invites.spec.ts/Invites-room-view-linux.png b/playwright/snapshots/room/invites.spec.ts/Invites-room-view-linux.png new file mode 100644 index 0000000000..8466ecab00 Binary files /dev/null and b/playwright/snapshots/room/invites.spec.ts/Invites-room-view-linux.png differ diff --git a/res/css/_components.pcss b/res/css/_components.pcss index d622f69b84..4691d71e25 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -340,8 +340,6 @@ @import "./views/rooms/wysiwyg_composer/components/_FormattingButtons.pcss"; @import "./views/rooms/wysiwyg_composer/components/_LinkModal.pcss"; @import "./views/settings/_AvatarSetting.pcss"; -@import "./views/settings/_CrossSigningPanel.pcss"; -@import "./views/settings/_CryptographyPanel.pcss"; @import "./views/settings/_FontScalingPanel.pcss"; @import "./views/settings/_ImageSizePanel.pcss"; @import "./views/settings/_IntegrationManager.pcss"; @@ -354,7 +352,6 @@ @import "./views/settings/_PhoneNumbers.pcss"; @import "./views/settings/_PowerLevelSelector.pcss"; @import "./views/settings/_RoomProfileSettings.pcss"; -@import "./views/settings/_SecureBackupPanel.pcss"; @import "./views/settings/_SetIntegrationManager.pcss"; @import "./views/settings/_SettingsFieldset.pcss"; @import "./views/settings/_SettingsHeader.pcss"; diff --git a/res/css/views/dialogs/_ReportRoomDialog.pcss b/res/css/views/dialogs/_ReportRoomDialog.pcss index fc9d087de1..b656638d9c 100644 --- a/res/css/views/dialogs/_ReportRoomDialog.pcss +++ b/res/css/views/dialogs/_ReportRoomDialog.pcss @@ -5,7 +5,8 @@ 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. */ -.mx_ReportRoomDialog { +.mx_ReportRoomDialog, +.mx_DeclineAndBlockInviteDialog { textarea { font: var(--cpd-font-body-md-regular); border: 1px solid var(--cpd-color-border-interactive-primary); @@ -14,7 +15,26 @@ Please see LICENSE files in the repository root for full details. padding: var(--cpd-space-3x) var(--cpd-space-4x); } - label { + /* + Workaround to fix labels appearing with the wrong color. + + .mx_Dialog (in res/css/_common.pcss) redefines the body color + as $light-fg-color rather than the standard primary color. + + This forces the colour to match the Compound style, but + in the future the Dialogs should not force a color. + */ + form label { + color: var(--cpd-color-text-primary); + } +} + +.mx_DeclineAndBlockInviteDialog { + div[aria-disabled="true"] > label { + color: var(--cpd-color-text-secondary); + } + + .mx_SettingsFlag_label { color: var(--cpd-color-text-primary); font-weight: var(--cpd-font-weight-semibold); } diff --git a/res/css/views/settings/_CrossSigningPanel.pcss b/res/css/views/settings/_CrossSigningPanel.pcss deleted file mode 100644 index e9b2aa0c29..0000000000 --- a/res/css/views/settings/_CrossSigningPanel.pcss +++ /dev/null @@ -1,36 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2019 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -.mx_CrossSigningPanel_statusList { - border-spacing: 0; - - th { - text-align: start; - } - - td, - th { - padding: 0; - - &:first-of-type { - padding-inline-end: 1em; - } - } -} - -.mx_CrossSigningPanel_buttonRow { - margin: 1em 0; - - :nth-child(n + 1) { - margin-inline-end: 10px; - } -} - -.mx_CrossSigningPanel_advanced { - width: fit-content; -} diff --git a/res/css/views/settings/_CryptographyPanel.pcss b/res/css/views/settings/_CryptographyPanel.pcss deleted file mode 100644 index 9c174ceaab..0000000000 --- a/res/css/views/settings/_CryptographyPanel.pcss +++ /dev/null @@ -1,32 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -.mx_CryptographyPanel_sessionInfo { - padding: 0em; - border-spacing: 0px; -} -.mx_CryptographyPanel_sessionInfo > tr { - vertical-align: baseline; - padding: 0em; - - th { - text-align: start; - } - - td, - th { - padding: 0 1em 0 0; - } -} - -.mx_CryptographyPanel_importExportButtons { - display: inline-flex; - flex-flow: wrap; - row-gap: $spacing-8; - column-gap: $spacing-8; -} diff --git a/res/css/views/settings/_SecureBackupPanel.pcss b/res/css/views/settings/_SecureBackupPanel.pcss deleted file mode 100644 index 6e571af339..0000000000 --- a/res/css/views/settings/_SecureBackupPanel.pcss +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. -Copyright 2018 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_SecureBackupPanel_deviceName { - font-style: italic; -} - -.mx_SecureBackupPanel_buttonRow { - margin: 1em 0; - display: inline-flex; - flex-flow: wrap; - row-gap: 10px; - - :nth-child(n + 1) { - margin-inline-end: 10px; - } -} - -.mx_SecureBackupPanel_statusList { - border-spacing: 0; - - th { - text-align: start; - } - - td, - th { - padding: 0; - - &:first-of-type { - padding-inline-end: 1em; - } - } -} - -.mx_SecureBackupPanel_advanced { - width: fit-content; -} diff --git a/res/css/views/settings/tabs/user/_SecurityUserSettingsTab.pcss b/res/css/views/settings/tabs/user/_SecurityUserSettingsTab.pcss index cb5d1fbc94..82f839042b 100644 --- a/res/css/views/settings/tabs/user/_SecurityUserSettingsTab.pcss +++ b/res/css/views/settings/tabs/user/_SecurityUserSettingsTab.pcss @@ -11,6 +11,12 @@ Please see LICENSE files in the repository root for full details. column-gap: $spacing-8; } +.mx_SecurityUserSettingsTab_ignoredUsers { + padding-left: 0; + margin: 0; + list-style: none; +} + .mx_SecurityUserSettingsTab_ignoredUser { margin-bottom: $spacing-4; } diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 0f722ae53d..3c7941bd4c 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -711,36 +711,6 @@ export default class MatrixChat extends React.PureComponent { case "copy_room": this.copyRoom(payload.room_id); break; - case "reject_invite": - Modal.createDialog(QuestionDialog, { - title: _t("reject_invitation_dialog|title"), - description: _t("reject_invitation_dialog|confirmation"), - onFinished: (confirm) => { - if (confirm) { - // FIXME: controller shouldn't be loading a view :( - const modal = Modal.createDialog(Spinner, undefined, "mx_Dialog_spinner"); - - MatrixClientPeg.safeGet() - .leave(payload.room_id) - .then( - () => { - modal.close(); - if (this.state.currentRoomId === payload.room_id) { - dis.dispatch({ action: Action.ViewHomePage }); - } - }, - (err) => { - modal.close(); - Modal.createDialog(ErrorDialog, { - title: _t("reject_invitation_dialog|failed"), - description: err.toString(), - }); - }, - ); - } - }, - }); - break; case "view_user_info": this.viewUser(payload.userId, payload.subAction); break; diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index b86e7937e8..3426ef7e97 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -134,6 +134,7 @@ import { onView3pidInvite } from "../../stores/right-panel/action-handlers"; import RoomSearchAuxPanel from "../views/rooms/RoomSearchAuxPanel"; import { PinnedMessageBanner } from "../views/rooms/PinnedMessageBanner"; import { ScopedRoomContextProvider, useScopedRoomContext } from "../../contexts/ScopedRoomContext"; +import { DeclineAndBlockInviteDialog } from "../views/dialogs/DeclineAndBlockInviteDialog"; const DEBUG = false; const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000; @@ -1732,48 +1733,61 @@ export class RoomView extends React.Component { }); }; - private onRejectButtonClicked = (): void => { - const roomId = this.getRoomId(); - if (!roomId) return; + private onDeclineAndBlockButtonClicked = async (): Promise => { + if (!this.state.room || !this.context.client) return; + const [shouldReject, ignoreUser, reportRoom] = await Modal.createDialog(DeclineAndBlockInviteDialog, { + roomName: this.state.room.name, + }).finished; + if (!shouldReject) { + return; + } + this.setState({ rejecting: true, }); - this.context.client?.leave(roomId).then( - () => { - defaultDispatcher.dispatch({ action: Action.ViewHomePage }); - this.setState({ - rejecting: false, - }); - }, - (error) => { - logger.error(`Failed to reject invite: ${error}`); - const msg = error.message ? error.message : JSON.stringify(error); - Modal.createDialog(ErrorDialog, { - title: _t("room|failed_reject_invite"), - description: msg, - }); + const actions: Promise[] = []; - this.setState({ - rejecting: false, - }); - }, - ); + if (ignoreUser) { + const myMember = this.state.room.getMember(this.context.client!.getSafeUserId()); + const inviteEvent = myMember!.events.member; + const ignoredUsers = this.context.client.getIgnoredUsers(); + ignoredUsers.push(inviteEvent!.getSender()!); // de-duped internally in the js-sdk + actions.push(this.context.client.setIgnoredUsers(ignoredUsers)); + } + + if (reportRoom !== false) { + actions.push(this.context.client.reportRoom(this.state.room.roomId, reportRoom)); + } + + actions.push(this.context.client.leave(this.state.room.roomId)); + try { + await Promise.all(actions); + defaultDispatcher.dispatch({ action: Action.ViewHomePage }); + this.setState({ + rejecting: false, + }); + } catch (error) { + logger.error(`Failed to reject invite: ${error}`); + + const msg = error instanceof Error ? error.message : JSON.stringify(error); + Modal.createDialog(ErrorDialog, { + title: _t("room|failed_reject_invite"), + description: msg, + }); + + this.setState({ + rejecting: false, + }); + } }; - private onRejectAndIgnoreClick = async (): Promise => { - this.setState({ - rejecting: true, - }); - + private onDeclineButtonClicked = async (): Promise => { + if (!this.state.room || !this.context.client) { + return; + } try { - const myMember = this.state.room!.getMember(this.context.client!.getSafeUserId()); - const inviteEvent = myMember!.events.member; - const ignoredUsers = this.context.client!.getIgnoredUsers(); - ignoredUsers.push(inviteEvent!.getSender()!); // de-duped internally in the js-sdk - await this.context.client!.setIgnoredUsers(ignoredUsers); - - await this.context.client!.leave(this.state.roomId!); + await this.context.client.leave(this.state.room.roomId); defaultDispatcher.dispatch({ action: Action.ViewHomePage }); this.setState({ rejecting: false, @@ -2126,7 +2140,7 @@ export class RoomView extends React.Component { { ; @@ -2196,8 +2210,9 @@ export class RoomView extends React.Component { { { onRejectButtonClicked={ this.props.threepidInvite ? this.onRejectThreepidInviteButtonClicked - : this.onRejectButtonClicked + : this.onDeclineButtonClicked } /> ); diff --git a/src/components/viewmodels/roomlist/useFilteredRooms.tsx b/src/components/viewmodels/roomlist/useFilteredRooms.tsx index 5e1554fcdc..4b4b9c0ec8 100644 --- a/src/components/viewmodels/roomlist/useFilteredRooms.tsx +++ b/src/components/viewmodels/roomlist/useFilteredRooms.tsx @@ -13,6 +13,8 @@ import { _t, _td, type TranslationKey } from "../../../languageHandler"; import RoomListStoreV3 from "../../../stores/room-list-v3/RoomListStoreV3"; import { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore"; import { useEventEmitter } from "../../../hooks/useEventEmitter"; +import SpaceStore from "../../../stores/spaces/SpaceStore"; +import { UPDATE_SELECTED_SPACE } from "../../../stores/spaces"; /** * Provides information about a primary filter. @@ -119,6 +121,12 @@ export function useFilteredRooms(): FilteredRooms { setRooms(newRooms); }, []); + // Reset filters when active space changes + useEventEmitter(SpaceStore.instance, UPDATE_SELECTED_SPACE, () => { + setPrimaryFilter(undefined); + activateSecondaryFilter(SecondaryFilters.AllActivity); + }); + const filterUndefined = (array: (FilterKey | undefined)[]): FilterKey[] => array.filter((f) => f !== undefined) as FilterKey[]; diff --git a/src/components/views/dialogs/DeclineAndBlockInviteDialog.tsx b/src/components/views/dialogs/DeclineAndBlockInviteDialog.tsx new file mode 100644 index 0000000000..a10dfbe42b --- /dev/null +++ b/src/components/views/dialogs/DeclineAndBlockInviteDialog.tsx @@ -0,0 +1,82 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import React, { type ChangeEventHandler, useCallback, useState } from "react"; +import { Field, Label, Root } from "@vector-im/compound-web"; + +import { _t } from "../../../languageHandler"; +import BaseDialog from "./BaseDialog"; +import DialogButtons from "../elements/DialogButtons"; +import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; + +interface IProps { + onFinished: (shouldReject: boolean, ignoreUser: boolean, reportRoom: false | string) => void; + roomName: string; +} + +export const DeclineAndBlockInviteDialog: React.FunctionComponent = ({ onFinished, roomName }) => { + const [shouldReport, setShouldReport] = useState(false); + const [ignoreUser, setIgnoreUser] = useState(false); + + const [reportReason, setReportReason] = useState(""); + const reportReasonChanged = useCallback>( + (e) => setReportReason(e.target.value), + [setReportReason], + ); + + const onCancel = useCallback(() => onFinished(false, false, false), [onFinished]); + const onOk = useCallback( + () => onFinished(true, ignoreUser, shouldReport ? reportReason : false), + [onFinished, ignoreUser, shouldReport, reportReason], + ); + + return ( + + +

{_t("decline_invitation_dialog|confirm", { roomName })}

+ + + + + + +
+ + + + +
+ +
+
+
+
+`; diff --git a/test/unit-tests/components/views/dialogs/__snapshots__/ReportRoomDialog-test.tsx.snap b/test/unit-tests/components/views/dialogs/__snapshots__/ReportRoomDialog-test.tsx.snap index 63f6f3ee10..44edfa9130 100644 --- a/test/unit-tests/components/views/dialogs/__snapshots__/ReportRoomDialog-test.tsx.snap +++ b/test/unit-tests/components/views/dialogs/__snapshots__/ReportRoomDialog-test.tsx.snap @@ -43,7 +43,7 @@ exports[`ReportRoomDialog displays admin message 1`] = ` /> Report this room to your account provider. If the messages are encrypted, your admin will not be able to read them. @@ -71,7 +71,7 @@ exports[`ReportRoomDialog displays admin message 1`] = ` class="mx_SettingsFlag_label" >
Leave room
@@ -79,7 +79,7 @@ exports[`ReportRoomDialog displays admin message 1`] = `
with live location disabled goes to labs flag scr class="mx_SettingsFlag_label" >
Enable live location sharing
@@ -34,7 +34,7 @@ exports[` with live location disabled goes to labs flag scr
", () => { }); it("renders join and reject action buttons correctly", () => { - const component = getComponent({ inviterName, room, onJoinClick, onRejectClick }); - expect(getActions(component)).toMatchSnapshot(); - }); - - it("renders reject and ignore action buttons when handler is provided", () => { - const onRejectAndIgnoreClick = jest.fn(); - const component = getComponent({ - inviterName, - room, - onJoinClick, - onRejectClick, - onRejectAndIgnoreClick, - }); + const component = getComponent({ inviterName, room, onJoinClick, onDeclineClick: onRejectClick }); expect(getActions(component)).toMatchSnapshot(); }); it("renders join and reject action buttons in reverse order when room can previewed", () => { // when room is previewed action buttons are rendered left to right, with primary on the right - const component = getComponent({ inviterName, room, onJoinClick, onRejectClick, canPreview: true }); + const component = getComponent({ + inviterName, + room, + onJoinClick, + onDeclineClick: onRejectClick, + canPreview: true, + }); expect(getActions(component)).toMatchSnapshot(); }); it("joins room on primary button click", () => { - const component = getComponent({ inviterName, room, onJoinClick, onRejectClick }); + const component = getComponent({ inviterName, room, onJoinClick, onDeclineClick: onRejectClick }); fireEvent.click(getPrimaryActionButton(component)!); expect(onJoinClick).toHaveBeenCalled(); }); it("rejects invite on secondary button click", () => { - const component = getComponent({ inviterName, room, onJoinClick, onRejectClick }); + const component = getComponent({ inviterName, room, onJoinClick, onDeclineClick: onRejectClick }); fireEvent.click(getSecondaryActionButton(component)!); expect(onRejectClick).toHaveBeenCalled(); @@ -337,18 +331,6 @@ describe("", () => { const component = getComponent({ inviterName, room }); expect(getMessage(component)).toMatchSnapshot(); }); - - it("renders join and reject action buttons with correct labels", () => { - const onRejectAndIgnoreClick = jest.fn(); - const component = getComponent({ - inviterName, - room, - onJoinClick, - onRejectAndIgnoreClick, - onRejectClick, - }); - expect(getActions(component)).toMatchSnapshot(); - }); }); }); @@ -364,7 +346,7 @@ describe("", () => { async () => { const onJoinClick = jest.fn(); const onRejectClick = jest.fn(); - const component = getComponent({ ...props, onJoinClick, onRejectClick }); + const component = getComponent({ ...props, onJoinClick, onDeclineClick: onRejectClick }); await waitFor(() => expect(getPrimaryActionButton(component)).toBeTruthy()); if (expectSecondaryButton) expect(getSecondaryActionButton(component)).toBeFalsy(); fireEvent.click(getPrimaryActionButton(component)!); diff --git a/test/unit-tests/components/views/rooms/__snapshots__/RoomPreviewBar-test.tsx.snap b/test/unit-tests/components/views/rooms/__snapshots__/RoomPreviewBar-test.tsx.snap index e89552dd85..dc44db8d21 100644 --- a/test/unit-tests/components/views/rooms/__snapshots__/RoomPreviewBar-test.tsx.snap +++ b/test/unit-tests/components/views/rooms/__snapshots__/RoomPreviewBar-test.tsx.snap @@ -339,34 +339,6 @@ exports[` with an invite without an invited email for a dm roo
`; -exports[` with an invite without an invited email for a dm room renders join and reject action buttons with correct labels 1`] = ` -
-
- Start chatting -
-
- Reject & Ignore user -
-
- Reject -
-
-`; - exports[` with an invite without an invited email for a non-dm room renders invite message 1`] = `
with an invite without an invited email for a non-dm role="button" tabindex="0" > - Reject + Decline
`; @@ -435,7 +407,7 @@ exports[` with an invite without an invited email for a non-dm role="button" tabindex="0" > - Reject + Decline
with an invite without an invited email for a non-dm
`; - -exports[` with an invite without an invited email for a non-dm room renders reject and ignore action buttons when handler is provided 1`] = ` -
-
- Accept -
-
- Reject & Ignore user -
-
- Reject -
-
-`; diff --git a/test/unit-tests/components/views/settings/CrossSigningPanel-test.tsx b/test/unit-tests/components/views/settings/CrossSigningPanel-test.tsx deleted file mode 100644 index 7c1f6b4d1f..0000000000 --- a/test/unit-tests/components/views/settings/CrossSigningPanel-test.tsx +++ /dev/null @@ -1,133 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { render, screen } from "jest-matrix-react"; -import { type Mocked, mocked } from "jest-mock"; -import { type MatrixClient } from "matrix-js-sdk/src/matrix"; - -import CrossSigningPanel from "../../../../../src/components/views/settings/CrossSigningPanel"; -import { - flushPromises, - getMockClientWithEventEmitter, - mockClientMethodsCrypto, - mockClientMethodsUser, -} from "../../../../test-utils"; -import Modal from "../../../../../src/Modal"; -import ConfirmDestroyCrossSigningDialog from "../../../../../src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog"; - -describe("", () => { - const userId = "@alice:server.org"; - let mockClient: Mocked; - const getComponent = () => render(); - - beforeEach(() => { - mockClient = getMockClientWithEventEmitter({ - ...mockClientMethodsUser(userId), - ...mockClientMethodsCrypto(), - doesServerSupportUnstableFeature: jest.fn(), - }); - - mockClient.doesServerSupportUnstableFeature.mockResolvedValue(true); - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - it("should render a spinner while loading", () => { - getComponent(); - - expect(screen.getByRole("progressbar")).toBeInTheDocument(); - }); - - it("should render when homeserver does not support cross-signing", async () => { - mockClient.doesServerSupportUnstableFeature.mockResolvedValue(false); - - getComponent(); - await flushPromises(); - - expect(screen.getByText("Your homeserver does not support cross-signing.")).toBeInTheDocument(); - }); - - describe("when cross signing is ready", () => { - it("should render when keys are not backed up", async () => { - getComponent(); - await flushPromises(); - - expect(screen.getByTestId("summarised-status").innerHTML).toEqual( - "⚠️ Cross-signing is ready but keys are not backed up.", - ); - expect(screen.getByText("Cross-signing private keys:").parentElement!).toMatchSnapshot(); - }); - - it("should render when keys are backed up", async () => { - mocked(mockClient.getCrypto()!.getCrossSigningStatus).mockResolvedValue({ - publicKeysOnDevice: true, - privateKeysInSecretStorage: true, - privateKeysCachedLocally: { - masterKey: true, - selfSigningKey: true, - userSigningKey: true, - }, - }); - getComponent(); - await flushPromises(); - - expect(screen.getByTestId("summarised-status").innerHTML).toEqual("✅ Cross-signing is ready for use."); - expect(screen.getByText("Cross-signing private keys:").parentElement!).toMatchSnapshot(); - }); - - it("should allow reset of cross-signing", async () => { - mockClient.getCrypto()!.bootstrapCrossSigning = jest.fn().mockResolvedValue(undefined); - getComponent(); - await flushPromises(); - - const modalSpy = jest.spyOn(Modal, "createDialog"); - - screen.getByRole("button", { name: "Reset" }).click(); - expect(modalSpy).toHaveBeenCalledWith(ConfirmDestroyCrossSigningDialog, expect.any(Object)); - modalSpy.mock.lastCall![1]!.onFinished(true); - expect(mockClient.getCrypto()!.bootstrapCrossSigning).toHaveBeenCalledWith( - expect.objectContaining({ setupNewCrossSigning: true }), - ); - }); - }); - - describe("when cross signing is not ready", () => { - beforeEach(() => { - mocked(mockClient.getCrypto()!.isCrossSigningReady).mockResolvedValue(false); - }); - - it("should render when keys are not backed up", async () => { - getComponent(); - await flushPromises(); - - expect(screen.getByTestId("summarised-status").innerHTML).toEqual("Cross-signing is not set up."); - }); - - it("should render when keys are backed up", async () => { - mocked(mockClient.getCrypto()!.getCrossSigningStatus).mockResolvedValue({ - publicKeysOnDevice: true, - privateKeysInSecretStorage: true, - privateKeysCachedLocally: { - masterKey: true, - selfSigningKey: true, - userSigningKey: true, - }, - }); - getComponent(); - await flushPromises(); - - expect(screen.getByTestId("summarised-status").innerHTML).toEqual( - "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.", - ); - expect(screen.getByText("Cross-signing private keys:").parentElement!).toMatchSnapshot(); - }); - }); -}); diff --git a/test/unit-tests/components/views/settings/CryptographyPanel-test.tsx b/test/unit-tests/components/views/settings/CryptographyPanel-test.tsx deleted file mode 100644 index 1ee64afe0f..0000000000 --- a/test/unit-tests/components/views/settings/CryptographyPanel-test.tsx +++ /dev/null @@ -1,97 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { render, waitFor, screen, fireEvent } from "jest-matrix-react"; -import { type MatrixClient } from "matrix-js-sdk/src/matrix"; -import { mocked } from "jest-mock"; - -import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; -import * as TestUtils from "../../../../test-utils"; -import CryptographyPanel from "../../../../../src/components/views/settings/CryptographyPanel"; -import { withClientContextRenderOptions } from "../../../../test-utils"; - -describe("CryptographyPanel", () => { - it("shows the session ID and key", async () => { - const sessionId = "ABCDEFGHIJ"; - const sessionKey = "AbCDeFghIJK7L/m4nOPqRSTUVW4xyzaBCDef6gHIJkl"; - const sessionKeyFormatted = "AbCD eFgh IJK7 L/m4 nOPq RSTU VW4x yzaB CDef 6gHI Jkl"; - - TestUtils.stubClient(); - const client: MatrixClient = MatrixClientPeg.safeGet(); - client.deviceId = sessionId; - - mocked(client.getCrypto()!.getOwnDeviceKeys).mockResolvedValue({ ed25519: sessionKey, curve25519: "1234" }); - - // When we render the CryptographyPanel - const rendered = render(, withClientContextRenderOptions(client)); - - // Then it displays info about the user's session - const codes = rendered.container.querySelectorAll("code"); - expect(codes.length).toEqual(2); - expect(codes[0].innerHTML).toEqual(sessionId); - - // Initially a placeholder - expect(codes[1].innerHTML).toEqual("..."); - - // Then the actual key - await waitFor(() => expect(codes[1].innerHTML).toEqual(sessionKeyFormatted)); - }); - - it("handles errors fetching session key", async () => { - const sessionId = "ABCDEFGHIJ"; - - TestUtils.stubClient(); - const client: MatrixClient = MatrixClientPeg.safeGet(); - client.deviceId = sessionId; - - mocked(client.getCrypto()!.getOwnDeviceKeys).mockRejectedValue(new Error("bleh")); - - // When we render the CryptographyPanel - const rendered = render(, withClientContextRenderOptions(client)); - - // Then it displays info about the user's session - const codes = rendered.container.querySelectorAll("code"); - - // Initially a placeholder - expect(codes[1].innerHTML).toEqual("..."); - - // Then "not supported key - await waitFor(() => expect(codes[1].innerHTML).toEqual("<not supported>")); - }); - - it("should open the export e2e keys dialog on click", async () => { - const sessionId = "ABCDEFGHIJ"; - const sessionKey = "AbCDeFghIJK7L/m4nOPqRSTUVW4xyzaBCDef6gHIJkl"; - - TestUtils.stubClient(); - const client: MatrixClient = MatrixClientPeg.safeGet(); - client.deviceId = sessionId; - - mocked(client.getCrypto()!.getOwnDeviceKeys).mockResolvedValue({ ed25519: sessionKey, curve25519: "1234" }); - - render(, withClientContextRenderOptions(client)); - fireEvent.click(await screen.findByRole("button", { name: "Export E2E room keys" })); - await expect(screen.findByRole("heading", { name: "Export room keys" })).resolves.toBeInTheDocument(); - }); - - it("should open the import e2e keys dialog on click", async () => { - const sessionId = "ABCDEFGHIJ"; - const sessionKey = "AbCDeFghIJK7L/m4nOPqRSTUVW4xyzaBCDef6gHIJkl"; - - TestUtils.stubClient(); - const client: MatrixClient = MatrixClientPeg.safeGet(); - client.deviceId = sessionId; - - mocked(client.getCrypto()!.getOwnDeviceKeys).mockResolvedValue({ ed25519: sessionKey, curve25519: "1234" }); - - render(, withClientContextRenderOptions(client)); - fireEvent.click(await screen.findByRole("button", { name: "Import E2E room keys" })); - await expect(screen.findByRole("heading", { name: "Import room keys" })).resolves.toBeInTheDocument(); - }); -}); diff --git a/test/unit-tests/components/views/settings/SecureBackupPanel-test.tsx b/test/unit-tests/components/views/settings/SecureBackupPanel-test.tsx deleted file mode 100644 index 78d59c07f1..0000000000 --- a/test/unit-tests/components/views/settings/SecureBackupPanel-test.tsx +++ /dev/null @@ -1,171 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { fireEvent, render, screen, within } from "jest-matrix-react"; -import { mocked } from "jest-mock"; - -import { - flushPromises, - getMockClientWithEventEmitter, - mockClientMethodsCrypto, - mockClientMethodsUser, -} from "../../../../test-utils"; -import SecureBackupPanel from "../../../../../src/components/views/settings/SecureBackupPanel"; -import { accessSecretStorage } from "../../../../../src/SecurityManager"; - -jest.mock("../../../../../src/SecurityManager", () => ({ - accessSecretStorage: jest.fn(), -})); - -describe("", () => { - const userId = "@alice:server.org"; - const client = getMockClientWithEventEmitter({ - ...mockClientMethodsUser(userId), - ...mockClientMethodsCrypto(), - getClientWellKnown: jest.fn(), - }); - - const getComponent = () => render(); - - beforeEach(() => { - jest.spyOn(client.getCrypto()!, "getKeyBackupInfo").mockResolvedValue({ - version: "1", - algorithm: "test", - auth_data: { - public_key: "1234", - }, - }); - Object.assign(client.getCrypto()!, { - isKeyBackupTrusted: jest.fn().mockResolvedValue({ - trusted: false, - matchesDecryptionKey: false, - }), - getActiveSessionBackupVersion: jest.fn().mockResolvedValue(null), - deleteKeyBackupVersion: jest.fn().mockResolvedValue(undefined), - }); - - mocked(client.secretStorage.hasKey).mockClear().mockResolvedValue(false); - - mocked(accessSecretStorage).mockClear().mockResolvedValue(); - }); - - it("displays a loader while checking keybackup", async () => { - getComponent(); - expect(screen.getByRole("progressbar")).toBeInTheDocument(); - await flushPromises(); - expect(screen.queryByRole("progressbar")).not.toBeInTheDocument(); - }); - - it("handles error fetching backup", async () => { - // getKeyBackupInfo can fail for various reasons - jest.spyOn(client.getCrypto()!, "getKeyBackupInfo").mockImplementation(async () => { - throw new Error("beep beep"); - }); - const renderResult = getComponent(); - await renderResult.findByText("Unable to load key backup status"); - expect(renderResult.container).toMatchSnapshot(); - }); - - it("handles absence of backup", async () => { - jest.spyOn(client.getCrypto()!, "getKeyBackupInfo").mockResolvedValue(null); - getComponent(); - // flush getKeyBackupInfo promise - await flushPromises(); - expect(screen.getByText("Back up your keys before signing out to avoid losing them.")).toBeInTheDocument(); - }); - - it("suggests connecting session to key backup when backup exists", async () => { - const { container } = getComponent(); - // flush checkKeyBackup promise - await flushPromises(); - - expect(container).toMatchSnapshot(); - }); - - it("displays when session is connected to key backup", async () => { - mocked(client.getCrypto()!).getActiveSessionBackupVersion.mockResolvedValue("1"); - getComponent(); - // flush checkKeyBackup promise - await flushPromises(); - - expect(screen.getByText("✅ This session is backing up your keys.")).toBeInTheDocument(); - }); - - it("asks for confirmation before deleting a backup", async () => { - getComponent(); - // flush checkKeyBackup promise - await flushPromises(); - - fireEvent.click(screen.getByText("Delete Backup")); - - const dialog = await screen.findByRole("dialog"); - - expect( - within(dialog).getByText( - "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.", - ), - ).toBeInTheDocument(); - - fireEvent.click(within(dialog).getByText("Cancel")); - - expect(client.getCrypto()!.deleteKeyBackupVersion).not.toHaveBeenCalled(); - }); - - it("deletes backup after confirmation", async () => { - jest.spyOn(client.getCrypto()!, "getKeyBackupInfo") - .mockResolvedValueOnce({ - version: "1", - algorithm: "test", - auth_data: { - public_key: "1234", - }, - }) - .mockResolvedValue(null); - getComponent(); - - fireEvent.click(await screen.findByText("Delete Backup")); - - const dialog = await screen.findByRole("dialog"); - - expect( - within(dialog).getByText( - "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.", - ), - ).toBeInTheDocument(); - - fireEvent.click(within(dialog).getByTestId("dialog-primary-button")); - - expect(client.getCrypto()!.deleteKeyBackupVersion).toHaveBeenCalledWith("1"); - - // delete request - await flushPromises(); - // refresh backup info - await flushPromises(); - }); - - it("resets secret storage", async () => { - mocked(client.secretStorage.hasKey).mockClear().mockResolvedValue(true); - getComponent(); - // flush checkKeyBackup promise - await flushPromises(); - - jest.spyOn(client.getCrypto()!, "getKeyBackupInfo").mockClear(); - mocked(client.getCrypto()!).isKeyBackupTrusted.mockClear(); - - fireEvent.click(screen.getByText("Reset")); - - // enter loading state - expect(accessSecretStorage).toHaveBeenCalled(); - await flushPromises(); - - // backup status refreshed - expect(client.getCrypto()!.getKeyBackupInfo).toHaveBeenCalled(); - expect(client.getCrypto()!.isKeyBackupTrusted).toHaveBeenCalled(); - }); -}); diff --git a/test/unit-tests/components/views/settings/__snapshots__/CrossSigningPanel-test.tsx.snap b/test/unit-tests/components/views/settings/__snapshots__/CrossSigningPanel-test.tsx.snap deleted file mode 100644 index d484ba4a3b..0000000000 --- a/test/unit-tests/components/views/settings/__snapshots__/CrossSigningPanel-test.tsx.snap +++ /dev/null @@ -1,40 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` when cross signing is not ready should render when keys are backed up 1`] = ` - - - Cross-signing private keys: - - - in secret storage - - -`; - -exports[` when cross signing is ready should render when keys are backed up 1`] = ` - - - Cross-signing private keys: - - - in secret storage - - -`; - -exports[` when cross signing is ready should render when keys are not backed up 1`] = ` - - - Cross-signing private keys: - - - not found in storage - - -`; diff --git a/test/unit-tests/components/views/settings/__snapshots__/Notifications-test.tsx.snap b/test/unit-tests/components/views/settings/__snapshots__/Notifications-test.tsx.snap index 25d06ffc23..f6fcc63262 100644 --- a/test/unit-tests/components/views/settings/__snapshots__/Notifications-test.tsx.snap +++ b/test/unit-tests/components/views/settings/__snapshots__/Notifications-test.tsx.snap @@ -10,22 +10,22 @@ exports[` main notification switches renders only enable notifi class="mx_SettingsFlag_label" >
Enable notifications for this account
Turn off to disable notifications on all your devices and sessions
main notification switches renders only enable notifi >