Merge branch 'develop' into florianduros/encryption-tab

florianduros/encryption-tab
Florian Duros 2024-12-19 13:13:30 +01:00
commit c805cd8260
No known key found for this signature in database
GPG Key ID: A5BBB4041B493F15
46 changed files with 1047 additions and 919 deletions

View File

@ -16,6 +16,11 @@ on:
options: options:
- staging.element.io - staging.element.io
- app.element.io - app.element.io
skip-checks:
description: Skip CI on the tagged commit
required: true
default: false
type: boolean
concurrency: ${{ inputs.site || 'staging.element.io' }} concurrency: ${{ inputs.site || 'staging.element.io' }}
permissions: {} permissions: {}
jobs: jobs:
@ -75,6 +80,7 @@ jobs:
- name: Wait for other steps to succeed - name: Wait for other steps to succeed
uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork
if: inputs.skip-checks != true
with: with:
ref: ${{ github.sha }} ref: ${{ github.sha }}
running-workflow-name: "Deploy to Cloudflare Pages" running-workflow-name: "Deploy to Cloudflare Pages"

View File

@ -1,3 +1,13 @@
Changes in [1.11.89](https://github.com/element-hq/element-web/releases/tag/v1.11.89) (2024-12-18)
==================================================================================================
This is a patch release to fix a bug which could prevent loading stored crypto state from storage, and also to fix URL previews when switching back to a room.
## 🐛 Bug Fixes
* Upgrade matrix-sdk-crypto-wasm to 1.11.0 (https://github.com/matrix-org/matrix-js-sdk/pull/4593)
* Fix url preview display ([#28766](https://github.com/element-hq/element-web/pull/28766)).
Changes in [1.11.88](https://github.com/element-hq/element-web/releases/tag/v1.11.88) (2024-12-17) Changes in [1.11.88](https://github.com/element-hq/element-web/releases/tag/v1.11.88) (2024-12-17)
================================================================================================== ==================================================================================================
## ✨ Features ## ✨ Features

View File

@ -1,6 +1,6 @@
{ {
"name": "element-web", "name": "element-web",
"version": "1.11.88", "version": "1.11.89",
"description": "A feature-rich client for Matrix.org", "description": "A feature-rich client for Matrix.org",
"author": "New Vector Ltd.", "author": "New Vector Ltd.",
"repository": { "repository": {
@ -282,7 +282,7 @@
"terser-webpack-plugin": "^5.3.9", "terser-webpack-plugin": "^5.3.9",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"ts-prune": "^0.10.3", "ts-prune": "^0.10.3",
"typescript": "5.6.3", "typescript": "5.7.2",
"util": "^0.12.5", "util": "^0.12.5",
"web-streams-polyfill": "^4.0.0", "web-streams-polyfill": "^4.0.0",
"webpack": "^5.89.0", "webpack": "^5.89.0",

View File

@ -9,6 +9,8 @@ Please see LICENSE files in the repository root for full details.
import { type Page } from "@playwright/test"; import { type Page } from "@playwright/test";
import { test, expect } from "../../element-web-test"; import { test, expect } from "../../element-web-test";
import { test as masTest, registerAccountMas } from "../oidc";
import { isDendrite } from "../../plugins/homeserver/dendrite";
async function expectBackupVersionToBe(page: Page, version: string) { async function expectBackupVersionToBe(page: Page, version: string) {
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(5) td")).toHaveText( await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(5) td")).toHaveText(
@ -18,6 +20,32 @@ async function expectBackupVersionToBe(page: Page, version: string) {
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(6) td")).toHaveText(version); await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(6) td")).toHaveText(version);
} }
masTest.describe("Encryption state after registration", () => {
masTest.skip(isDendrite, "does not yet support MAS");
masTest("Key backup is enabled by default", async ({ page, mailhog, app }) => {
await page.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click();
await registerAccountMas(page, mailhog.api, "alice", "alice@email.com", "Pa$sW0rD!");
await app.settings.openUserSettings("Security & Privacy");
expect(page.getByText("This session is backing up your keys.")).toBeVisible();
});
masTest("user is prompted to set up recovery", async ({ page, mailhog, app }) => {
await page.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click();
await registerAccountMas(page, mailhog.api, "alice", "alice@email.com", "Pa$sW0rD!");
await page.getByRole("button", { name: "Add room" }).click();
await page.getByRole("menuitem", { name: "New room" }).click();
await page.getByRole("textbox", { name: "Name" }).fill("test room");
await page.getByRole("button", { name: "Create room" }).click();
await expect(page.getByRole("heading", { name: "Set up recovery" })).toBeVisible();
});
});
test.describe("Backups", () => { test.describe("Backups", () => {
test.use({ test.use({
displayName: "Hanako", displayName: "Hanako",

View File

@ -8,11 +8,11 @@ Please see LICENSE files in the repository root for full details.
import { Locator, type Page } from "@playwright/test"; import { Locator, type Page } from "@playwright/test";
import { test as base, expect } from "../../element-web-test"; import { test as base, expect, Fixtures } from "../../element-web-test";
import { viewRoomSummaryByName } from "../right-panel/utils"; import { viewRoomSummaryByName } from "../right-panel/utils";
import { isDendrite } from "../../plugins/homeserver/dendrite"; import { isDendrite } from "../../plugins/homeserver/dendrite";
const test = base.extend({ const test = base.extend<Fixtures>({
// eslint-disable-next-line no-empty-pattern // eslint-disable-next-line no-empty-pattern
startHomeserverOpts: async ({}, use) => { startHomeserverOpts: async ({}, use) => {
await use("dehydration"); await use("dehydration");

View File

@ -9,9 +9,9 @@ Please see LICENSE files in the repository root for full details.
import path from "path"; import path from "path";
import { readFile } from "node:fs/promises"; import { readFile } from "node:fs/promises";
import { expect, test as base } from "../../element-web-test"; import { expect, Fixtures, test as base } from "../../element-web-test";
const test = base.extend({ const test = base.extend<Fixtures>({
// Replace the `user` fixture with one which populates the indexeddb data before starting the app. // Replace the `user` fixture with one which populates the indexeddb data before starting the app.
user: async ({ context, pageWithCredentials: page, credentials }, use) => { user: async ({ context, pageWithCredentials: page, credentials }, use) => {
await page.route(`/test_indexeddb_cryptostore_dump/*`, async (route, request) => { await page.route(`/test_indexeddb_cryptostore_dump/*`, async (route, request) => {

View File

@ -71,7 +71,9 @@ test.describe("Room Header", () => {
// Assert the size of buttons on RoomHeader are specified and the buttons are not compressed // Assert the size of buttons on RoomHeader are specified and the buttons are not compressed
// Note these assertions do not check the size of mx_LegacyRoomHeader_name button // Note these assertions do not check the size of mx_LegacyRoomHeader_name button
const buttons = header.locator(".mx_Flex").getByRole("button"); const buttons = header.getByRole("button").filter({
has: page.locator("svg"),
});
await expect(buttons).toHaveCount(5); await expect(buttons).toHaveCount(5);
for (const button of await buttons.all()) { for (const button of await buttons.all()) {

View File

@ -60,7 +60,7 @@ interface CredentialsWithDisplayName extends Credentials {
displayName: string; displayName: string;
} }
export const test = base.extend<{ export interface Fixtures {
axe: AxeBuilder; axe: AxeBuilder;
checkA11y: () => Promise<void>; checkA11y: () => Promise<void>;
@ -124,7 +124,9 @@ export const test = base.extend<{
slidingSyncProxy: ProxyInstance; slidingSyncProxy: ProxyInstance;
labsFlags: string[]; labsFlags: string[];
webserver: Webserver; webserver: Webserver;
}>({ }
export const test = base.extend<Fixtures>({
config: CONFIG_JSON, config: CONFIG_JSON,
page: async ({ context, page, config, labsFlags }, use) => { page: async ({ context, page, config, labsFlags }, use) => {
await context.route(`http://localhost:8080/config.json*`, async (route) => { await context.route(`http://localhost:8080/config.json*`, async (route) => {

View File

@ -20,7 +20,7 @@ import { randB64Bytes } from "../../utils/rand";
// Docker tag to use for synapse docker image. // Docker tag to use for synapse docker image.
// We target a specific digest as every now and then a Synapse update will break our CI. // We target a specific digest as every now and then a Synapse update will break our CI.
// This digest is updated by the playwright-image-updates.yaml workflow periodically. // This digest is updated by the playwright-image-updates.yaml workflow periodically.
const DOCKER_TAG = "develop@sha256:ef3d491214fa380918c736d9aa720992fb58829ce5c06fa3ca36d357fa1df75d"; const DOCKER_TAG = "develop@sha256:c965896a4865479ab2628807ebf6d9c742586f3b6185a56f10077a408f1c7c3b";
async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise<Omit<HomeserverConfig, "dockerUrl">> { async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise<Omit<HomeserverConfig, "dockerUrl">> {
const templateDir = path.join(__dirname, "templates", opts.template); const templateDir = path.join(__dirname, "templates", opts.template);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

View File

@ -16,18 +16,19 @@ import { _t } from "./languageHandler";
import InteractiveAuthDialog from "./components/views/dialogs/InteractiveAuthDialog"; import InteractiveAuthDialog from "./components/views/dialogs/InteractiveAuthDialog";
/** /**
* Determine if the homeserver allows uploading device keys with only password auth. * Determine if the homeserver allows uploading device keys with only password auth, or with no auth at
* all (ie. if the homeserver supports MSC3967).
* @param cli The Matrix Client to use * @param cli The Matrix Client to use
* @returns True if the homeserver allows uploading device keys with only password auth, otherwise false * @returns True if the homeserver allows uploading device keys with only password auth or with no auth
* at all, otherwise false
*/ */
async function canUploadKeysWithPasswordOnly(cli: MatrixClient): Promise<boolean> { async function canUploadKeysWithPasswordOnly(cli: MatrixClient): Promise<boolean> {
try { try {
await cli.uploadDeviceSigningKeys(undefined, {} as CrossSigningKeys); await cli.uploadDeviceSigningKeys(undefined, {} as CrossSigningKeys);
// We should never get here: the server should always require // If we get here, it's because the server is allowing us to upload keys without
// UI auth to upload device signing keys. If we do, we upload // auth the first time due to MSC3967. Therefore, yes, we can upload keys
// no keys which would be a no-op. // (with or without password, technically, but that's fine).
logger.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!"); return true;
return false;
} catch (error) { } catch (error) {
if (!(error instanceof MatrixError) || !error.data || !error.data.flows) { if (!(error instanceof MatrixError) || !error.data || !error.data.flows) {
logger.log("uploadDeviceSigningKeys advertised no flows!"); logger.log("uploadDeviceSigningKeys advertised no flows!");

View File

@ -295,13 +295,20 @@ export default class DeviceListener {
await crypto.getUserDeviceInfo([cli.getSafeUserId()]); await crypto.getUserDeviceInfo([cli.getSafeUserId()]);
// cross signing isn't enabled - nag to enable it // cross signing isn't enabled - nag to enable it
// There are 2 different toasts for: // There are 3 different toasts for:
if (!(await crypto.getCrossSigningKeyId()) && (await crypto.userHasCrossSigningKeys())) { if (!(await crypto.getCrossSigningKeyId()) && (await crypto.userHasCrossSigningKeys())) {
// Cross-signing on account but this device doesn't trust the master key (verify this session) // Toast 1. Cross-signing on account but this device doesn't trust the master key (verify this session)
showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION); showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION);
this.checkKeyBackupStatus(); this.checkKeyBackupStatus();
} else { } else {
// No cross-signing or key backup on account (set up encryption) const backupInfo = await this.getKeyBackupInfo();
if (backupInfo) {
// Toast 2: Key backup is enabled but recovery (4S) is not set up: prompt user to set up recovery.
// Since we now enable key backup at registration time, this will be the common case for
// new users.
showSetupEncryptionToast(SetupKind.SET_UP_RECOVERY);
} else {
// Toast 3: No cross-signing or key backup on account (set up encryption)
await cli.waitForClientWellKnown(); await cli.waitForClientWellKnown();
if (isSecureBackupRequired(cli) && isLoggedIn()) { if (isSecureBackupRequired(cli) && isLoggedIn()) {
// If we're meant to set up, and Secure Backup is required, // If we're meant to set up, and Secure Backup is required,
@ -313,6 +320,7 @@ export default class DeviceListener {
} }
} }
} }
}
// This needs to be done after awaiting on getUserDeviceInfo() above, so // This needs to be done after awaiting on getUserDeviceInfo() above, so
// we make sure we get the devices after the fetch is done. // we make sure we get the devices after the fetch is done.

View File

@ -38,6 +38,9 @@ enum BackupStatus {
/** there is a backup on the server but we are not backing up to it */ /** there is a backup on the server but we are not backing up to it */
SERVER_BACKUP_BUT_DISABLED, SERVER_BACKUP_BUT_DISABLED,
/** Key backup is set up but recovery (4s) is not */
BACKUP_NO_RECOVERY,
/** backup is not set up locally and there is no backup on the server */ /** backup is not set up locally and there is no backup on the server */
NO_BACKUP, NO_BACKUP,
@ -104,7 +107,11 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
} }
if ((await crypto.getActiveSessionBackupVersion()) !== null) { if ((await crypto.getActiveSessionBackupVersion()) !== null) {
if (await crypto.isSecretStorageReady()) {
this.setState({ backupStatus: BackupStatus.BACKUP_ACTIVE }); this.setState({ backupStatus: BackupStatus.BACKUP_ACTIVE });
} else {
this.setState({ backupStatus: BackupStatus.BACKUP_NO_RECOVERY });
}
return; return;
} }
@ -164,13 +171,17 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
}; };
/** /**
* Show a dialog prompting the user to set up key backup. * Show a dialog prompting the user to set up their recovery method.
* *
* Either there is no backup at all ({@link BackupStatus.NO_BACKUP}), there is a backup on the server but * Either:
* we are not connected to it ({@link BackupStatus.SERVER_BACKUP_BUT_DISABLED}), or we were unable to pull the * * There is no backup at all ({@link BackupStatus.NO_BACKUP})
* backup data ({@link BackupStatus.ERROR}). In all three cases, we should prompt the user to set up key backup. * * There is a backup set up but recovery (4s) is not ({@link BackupStatus.BACKUP_NO_RECOVERY})
* * There is a backup on the server but we are not connected to it ({@link BackupStatus.SERVER_BACKUP_BUT_DISABLED})
* * We were unable to pull the backup data ({@link BackupStatus.ERROR}).
*
* In all four cases, we should prompt the user to set up a method of recovery.
*/ */
private renderSetupBackupDialog(): React.ReactNode { private renderSetupRecoveryMethod(): React.ReactNode {
const description = ( const description = (
<div> <div>
<p>{_t("auth|logout_dialog|setup_secure_backup_description_1")}</p> <p>{_t("auth|logout_dialog|setup_secure_backup_description_1")}</p>
@ -254,7 +265,8 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
case BackupStatus.NO_BACKUP: case BackupStatus.NO_BACKUP:
case BackupStatus.SERVER_BACKUP_BUT_DISABLED: case BackupStatus.SERVER_BACKUP_BUT_DISABLED:
case BackupStatus.ERROR: case BackupStatus.ERROR:
return this.renderSetupBackupDialog(); case BackupStatus.BACKUP_NO_RECOVERY:
return this.renderSetupRecoveryMethod();
} }
} }
} }

View File

@ -128,7 +128,8 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
if (!this.props.editState) { if (!this.props.editState) {
const stoppedEditing = prevProps.editState && !this.props.editState; const stoppedEditing = prevProps.editState && !this.props.editState;
const messageWasEdited = prevProps.replacingEventId !== this.props.replacingEventId; const messageWasEdited = prevProps.replacingEventId !== this.props.replacingEventId;
if (messageWasEdited || stoppedEditing) { const urlPreviewChanged = prevProps.showUrlPreview !== this.props.showUrlPreview;
if (messageWasEdited || stoppedEditing || urlPreviewChanged) {
this.applyFormatting(); this.applyFormatting();
} }
} }

View File

@ -310,7 +310,7 @@ export default function RoomHeader({
</BodyText> </BodyText>
</Box> </Box>
</button> </button>
<Flex align="center" gap="var(--cpd-space-2x)">
{additionalButtons?.map((props) => { {additionalButtons?.map((props) => {
const label = props.label(); const label = props.label();
@ -340,18 +340,6 @@ export default function RoomHeader({
</> </>
)} )}
<Tooltip label={_t("right_panel|room_summary_card|title")}>
<IconButton
onClick={(evt) => {
evt.stopPropagation();
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomSummary);
}}
aria-label={_t("right_panel|room_summary_card|title")}
>
<RoomInfoIcon />
</IconButton>
</Tooltip>
{showChatButton && <VideoRoomChatButton room={room} />} {showChatButton && <VideoRoomChatButton room={room} />}
<Tooltip label={_t("common|threads")}> <Tooltip label={_t("common|threads")}>
@ -381,7 +369,19 @@ export default function RoomHeader({
</IconButton> </IconButton>
</Tooltip> </Tooltip>
)} )}
</Flex>
<Tooltip label={_t("right_panel|room_summary_card|title")}>
<IconButton
onClick={(evt) => {
evt.stopPropagation();
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomSummary);
}}
aria-label={_t("right_panel|room_summary_card|title")}
>
<RoomInfoIcon />
</IconButton>
</Tooltip>
{!isDirectMessage && ( {!isDirectMessage && (
<BodyText as="div" size="sm" weight="medium"> <BodyText as="div" size="sm" weight="medium">
<FacePile <FacePile

View File

@ -214,7 +214,7 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
<SettingsSubsectionText> <SettingsSubsectionText>
{this.state.enabling ? <InlineSpinner /> : _t("settings|security|message_search_failed")} {this.state.enabling ? <InlineSpinner /> : _t("settings|security|message_search_failed")}
</SettingsSubsectionText> </SettingsSubsectionText>
{EventIndexPeg.error && ( {EventIndexPeg.error ? (
<SettingsSubsectionText> <SettingsSubsectionText>
<details> <details>
<summary>{_t("common|advanced")}</summary> <summary>{_t("common|advanced")}</summary>
@ -230,7 +230,7 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
</p> </p>
</details> </details>
</SettingsSubsectionText> </SettingsSubsectionText>
)} ) : undefined}
</> </>
); );
} }

View File

@ -915,6 +915,9 @@
"warning": "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings." "warning": "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings."
}, },
"reset_all_button": "Forgotten or lost all recovery methods? <a>Reset all</a>", "reset_all_button": "Forgotten or lost all recovery methods? <a>Reset all</a>",
"set_up_recovery": "Set up recovery",
"set_up_recovery_later": "Not now",
"set_up_recovery_toast_description": "Generate a recovery key that can be used to restore your encrypted message history in case you lose access to your devices.",
"set_up_toast_description": "Safeguard against losing access to encrypted messages & data", "set_up_toast_description": "Safeguard against losing access to encrypted messages & data",
"set_up_toast_title": "Set up Secure Backup", "set_up_toast_title": "Set up Secure Backup",
"setup_secure_backup": { "setup_secure_backup": {

View File

@ -2115,7 +2115,8 @@
"show_less": "Pokaż mniej", "show_less": "Pokaż mniej",
"show_n_more": { "show_n_more": {
"one": "Pokaż %(count)s więcej", "one": "Pokaż %(count)s więcej",
"other": "Pokaż %(count)s więcej" "few": "Pokaż %(count)s więcej",
"many": "Pokaż %(count)s więcej"
}, },
"show_previews": "Pokazuj podgląd wiadomości", "show_previews": "Pokazuj podgląd wiadomości",
"sort_by": "Sortuj według", "sort_by": "Sortuj według",
@ -3689,7 +3690,8 @@
"close": "Zamknij podgląd", "close": "Zamknij podgląd",
"show_n_more": { "show_n_more": {
"one": "Pokaż %(count)s inny podgląd", "one": "Pokaż %(count)s inny podgląd",
"other": "Pokaż %(count)s innych podglądów" "few": "Pokaż %(count)s inne podglądy",
"many": "Pokaż %(count)s innych podglądów"
} }
} }
}, },

View File

@ -114,8 +114,15 @@ export class InitialCryptoSetupStore extends EventEmitter {
this.emit("update"); this.emit("update");
try { try {
// Create the user's cross-signing keys
await createCrossSigning(this.client, this.isTokenLogin, this.stores.accountPasswordStore.getPassword()); await createCrossSigning(this.client, this.isTokenLogin, this.stores.accountPasswordStore.getPassword());
// Check for any existing backup and enable key backup if there isn't one
const currentKeyBackup = await cryptoApi.checkKeyBackupAndEnable();
if (currentKeyBackup === null) {
await cryptoApi.resetKeyBackup();
}
this.reset(); this.reset();
this.status = "complete"; this.status = "complete";

View File

@ -23,15 +23,19 @@ const getTitle = (kind: Kind): string => {
switch (kind) { switch (kind) {
case Kind.SET_UP_ENCRYPTION: case Kind.SET_UP_ENCRYPTION:
return _t("encryption|set_up_toast_title"); return _t("encryption|set_up_toast_title");
case Kind.SET_UP_RECOVERY:
return _t("encryption|set_up_recovery");
case Kind.VERIFY_THIS_SESSION: case Kind.VERIFY_THIS_SESSION:
return _t("encryption|verify_toast_title"); return _t("encryption|verify_toast_title");
} }
}; };
const getIcon = (kind: Kind): string => { const getIcon = (kind: Kind): string | undefined => {
switch (kind) { switch (kind) {
case Kind.SET_UP_ENCRYPTION: case Kind.SET_UP_ENCRYPTION:
return "secure_backup"; return "secure_backup";
case Kind.SET_UP_RECOVERY:
return undefined;
case Kind.VERIFY_THIS_SESSION: case Kind.VERIFY_THIS_SESSION:
return "verification_warning"; return "verification_warning";
} }
@ -41,22 +45,49 @@ const getSetupCaption = (kind: Kind): string => {
switch (kind) { switch (kind) {
case Kind.SET_UP_ENCRYPTION: case Kind.SET_UP_ENCRYPTION:
return _t("action|continue"); return _t("action|continue");
case Kind.SET_UP_RECOVERY:
return _t("action|continue");
case Kind.VERIFY_THIS_SESSION: case Kind.VERIFY_THIS_SESSION:
return _t("action|verify"); return _t("action|verify");
} }
}; };
const getSecondaryButtonLabel = (kind: Kind): string => {
switch (kind) {
case Kind.SET_UP_RECOVERY:
return _t("encryption|set_up_recovery_later");
case Kind.SET_UP_ENCRYPTION:
case Kind.VERIFY_THIS_SESSION:
return _t("encryption|verification|unverified_sessions_toast_reject");
}
};
const getDescription = (kind: Kind): string => { const getDescription = (kind: Kind): string => {
switch (kind) { switch (kind) {
case Kind.SET_UP_ENCRYPTION: case Kind.SET_UP_ENCRYPTION:
return _t("encryption|set_up_toast_description"); return _t("encryption|set_up_toast_description");
case Kind.SET_UP_RECOVERY:
return _t("encryption|set_up_recovery_toast_description");
case Kind.VERIFY_THIS_SESSION: case Kind.VERIFY_THIS_SESSION:
return _t("encryption|verify_toast_description"); return _t("encryption|verify_toast_description");
} }
}; };
/**
* The kind of toast to show.
*/
export enum Kind { export enum Kind {
/**
* Prompt the user to set up encryption
*/
SET_UP_ENCRYPTION = "set_up_encryption", SET_UP_ENCRYPTION = "set_up_encryption",
/**
* Prompt the user to set up a recovery key
*/
SET_UP_RECOVERY = "set_up_recovery",
/**
* Prompt the user to verify this session
*/
VERIFY_THIS_SESSION = "verify_this_session", VERIFY_THIS_SESSION = "verify_this_session",
} }
@ -64,6 +95,11 @@ const onReject = (): void => {
DeviceListener.sharedInstance().dismissEncryptionSetup(); DeviceListener.sharedInstance().dismissEncryptionSetup();
}; };
/**
* Show a toast prompting the user for some action related to setting up their encryption.
*
* @param kind The kind of toast to show
*/
export const showToast = (kind: Kind): void => { export const showToast = (kind: Kind): void => {
if ( if (
ModuleRunner.instance.extensions.cryptoSetup.setupEncryptionNeeded({ ModuleRunner.instance.extensions.cryptoSetup.setupEncryptionNeeded({
@ -101,15 +137,17 @@ export const showToast = (kind: Kind): void => {
description: getDescription(kind), description: getDescription(kind),
primaryLabel: getSetupCaption(kind), primaryLabel: getSetupCaption(kind),
onPrimaryClick: onAccept, onPrimaryClick: onAccept,
secondaryLabel: _t("encryption|verification|unverified_sessions_toast_reject"), secondaryLabel: getSecondaryButtonLabel(kind),
onSecondaryClick: onReject, onSecondaryClick: onReject,
destructive: "secondary",
}, },
component: GenericToast, component: GenericToast,
priority: kind === Kind.VERIFY_THIS_SESSION ? 95 : 40, priority: kind === Kind.VERIFY_THIS_SESSION ? 95 : 40,
}); });
}; };
/**
* Hide the encryption setup toast if it is currently being shown.
*/
export const hideToast = (): void => { export const hideToast = (): void => {
ToastStore.sharedInstance().dismissToast(TOAST_KEY); ToastStore.sharedInstance().dismissToast(TOAST_KEY);
}; };

View File

@ -137,6 +137,7 @@ export function createTestClient(): MatrixClient {
restoreKeyBackupWithPassphrase: jest.fn(), restoreKeyBackupWithPassphrase: jest.fn(),
loadSessionBackupPrivateKeyFromSecretStorage: jest.fn(), loadSessionBackupPrivateKeyFromSecretStorage: jest.fn(),
storeSessionBackupPrivateKey: jest.fn(), storeSessionBackupPrivateKey: jest.fn(),
checkKeyBackupAndEnable: jest.fn().mockResolvedValue(null),
getKeyBackupInfo: jest.fn().mockResolvedValue(null), getKeyBackupInfo: jest.fn().mockResolvedValue(null),
getEncryptionInfoForEvent: jest.fn().mockResolvedValue(null), getEncryptionInfoForEvent: jest.fn().mockResolvedValue(null),
getCrossSigningStatus: jest.fn().mockResolvedValue({ getCrossSigningStatus: jest.fn().mockResolvedValue({

View File

@ -352,13 +352,13 @@ describe("DeviceListener", () => {
mockCrypto!.getCrossSigningKeyId.mockResolvedValue("abc"); mockCrypto!.getCrossSigningKeyId.mockResolvedValue("abc");
}); });
it("shows set up encryption toast when user has a key backup available", async () => { it("shows set up recovery toast when user has a key backup available", async () => {
// non falsy response // non falsy response
mockCrypto.getKeyBackupInfo.mockResolvedValue({} as unknown as KeyBackupInfo); mockCrypto.getKeyBackupInfo.mockResolvedValue({} as unknown as KeyBackupInfo);
await createAndStart(); await createAndStart();
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith( expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith(
SetupEncryptionToast.Kind.SET_UP_ENCRYPTION, SetupEncryptionToast.Kind.SET_UP_RECOVERY,
); );
}); });
}); });

View File

@ -1003,7 +1003,9 @@ describe("<MatrixChat />", () => {
userHasCrossSigningKeys: jest.fn().mockResolvedValue(false), userHasCrossSigningKeys: jest.fn().mockResolvedValue(false),
// This needs to not finish immediately because we need to test the screen appears // This needs to not finish immediately because we need to test the screen appears
bootstrapCrossSigning: jest.fn().mockImplementation(() => bootstrapDeferred.promise), bootstrapCrossSigning: jest.fn().mockImplementation(() => bootstrapDeferred.promise),
resetKeyBackup: jest.fn(),
isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(false), isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(false),
checkKeyBackupAndEnable: jest.fn().mockResolvedValue(null),
}; };
loginClient.getCrypto.mockReturnValue(mockCrypto as any); loginClient.getCrypto.mockReturnValue(mockCrypto as any);
}); });

View File

@ -45,10 +45,6 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
</div> </div>
</div> </div>
</button> </button>
<div
class="mx_Flex"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x);"
>
<button <button
aria-disabled="false" aria-disabled="false"
aria-label="Video call" aria-label="Video call"
@ -101,34 +97,9 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
</svg> </svg>
</div> </div>
</button> </button>
<button
aria-label="Room info"
aria-labelledby=":rge:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%;"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16v-4a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 11a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 12v4c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-8c.283 0 .52-.096.713-.287A.967.967 0 0 0 13 8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 13a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
/>
</svg>
</div>
</button>
<button <button
aria-label="Threads" aria-label="Threads"
aria-labelledby=":rgj:" aria-labelledby=":rge:"
class="_icon-button_bh2qc_17" class="_icon-button_bh2qc_17"
role="button" role="button"
style="--cpd-icon-button-size: 32px;" style="--cpd-icon-button-size: 32px;"
@ -151,7 +122,31 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
</svg> </svg>
</div> </div>
</button> </button>
<button
aria-label="Room info"
aria-labelledby=":rgj:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%;"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16v-4a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 11a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 12v4c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-8c.283 0 .52-.096.713-.287A.967.967 0 0 0 13 8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 13a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
/>
</svg>
</div> </div>
</button>
<div <div
class="_typography_yh5dq_162 _font-body-sm-medium_yh5dq_50" class="_typography_yh5dq_162 _font-body-sm-medium_yh5dq_50"
> >
@ -263,10 +258,6 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
</div> </div>
</div> </div>
</button> </button>
<div
class="mx_Flex"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x);"
>
<button <button
aria-disabled="false" aria-disabled="false"
aria-label="Video call" aria-label="Video call"
@ -319,34 +310,9 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
</svg> </svg>
</div> </div>
</button> </button>
<button
aria-label="Room info"
aria-labelledby=":rhc:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%;"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16v-4a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 11a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 12v4c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-8c.283 0 .52-.096.713-.287A.967.967 0 0 0 13 8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 13a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
/>
</svg>
</div>
</button>
<button <button
aria-label="Threads" aria-label="Threads"
aria-labelledby=":rhh:" aria-labelledby=":rhc:"
class="_icon-button_bh2qc_17" class="_icon-button_bh2qc_17"
role="button" role="button"
style="--cpd-icon-button-size: 32px;" style="--cpd-icon-button-size: 32px;"
@ -369,7 +335,31 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
</svg> </svg>
</div> </div>
</button> </button>
<button
aria-label="Room info"
aria-labelledby=":rhh:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%;"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16v-4a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 11a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 12v4c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-8c.283 0 .52-.096.713-.287A.967.967 0 0 0 13 8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 13a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
/>
</svg>
</div> </div>
</button>
<div <div
class="_typography_yh5dq_162 _font-body-sm-medium_yh5dq_50" class="_typography_yh5dq_162 _font-body-sm-medium_yh5dq_50"
> >
@ -566,10 +556,6 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
</div> </div>
</div> </div>
</button> </button>
<div
class="mx_Flex"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x);"
>
<button <button
aria-disabled="false" aria-disabled="false"
aria-label="Video call" aria-label="Video call"
@ -622,34 +608,9 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
</svg> </svg>
</div> </div>
</button> </button>
<button
aria-label="Room info"
aria-labelledby=":rc2:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%;"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16v-4a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 11a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 12v4c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-8c.283 0 .52-.096.713-.287A.967.967 0 0 0 13 8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 13a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
/>
</svg>
</div>
</button>
<button <button
aria-label="Threads" aria-label="Threads"
aria-labelledby=":rc7:" aria-labelledby=":rc2:"
class="_icon-button_bh2qc_17" class="_icon-button_bh2qc_17"
role="button" role="button"
style="--cpd-icon-button-size: 32px;" style="--cpd-icon-button-size: 32px;"
@ -672,7 +633,31 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
</svg> </svg>
</div> </div>
</button> </button>
<button
aria-label="Room info"
aria-labelledby=":rc7:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%;"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16v-4a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 11a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 12v4c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-8c.283 0 .52-.096.713-.287A.967.967 0 0 0 13 8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 13a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
/>
</svg>
</div> </div>
</button>
<div <div
class="_typography_yh5dq_162 _font-body-sm-medium_yh5dq_50" class="_typography_yh5dq_162 _font-body-sm-medium_yh5dq_50"
> >
@ -946,10 +931,6 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
</div> </div>
</div> </div>
</button> </button>
<div
class="mx_Flex"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x);"
>
<button <button
aria-disabled="false" aria-disabled="false"
aria-label="Video call" aria-label="Video call"
@ -1002,34 +983,9 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
</svg> </svg>
</div> </div>
</button> </button>
<button
aria-label="Room info"
aria-labelledby=":re8:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%;"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16v-4a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 11a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 12v4c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-8c.283 0 .52-.096.713-.287A.967.967 0 0 0 13 8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 13a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
/>
</svg>
</div>
</button>
<button <button
aria-label="Threads" aria-label="Threads"
aria-labelledby=":red:" aria-labelledby=":re8:"
class="_icon-button_bh2qc_17" class="_icon-button_bh2qc_17"
role="button" role="button"
style="--cpd-icon-button-size: 32px;" style="--cpd-icon-button-size: 32px;"
@ -1052,7 +1008,31 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
</svg> </svg>
</div> </div>
</button> </button>
<button
aria-label="Room info"
aria-labelledby=":red:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%;"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16v-4a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 11a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 12v4c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-8c.283 0 .52-.096.713-.287A.967.967 0 0 0 13 8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 13a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
/>
</svg>
</div> </div>
</button>
<div <div
class="_typography_yh5dq_162 _font-body-sm-medium_yh5dq_50" class="_typography_yh5dq_162 _font-body-sm-medium_yh5dq_50"
> >
@ -1334,10 +1314,6 @@ exports[`RoomView should not display the timeline when the room encryption is lo
</div> </div>
</div> </div>
</button> </button>
<div
class="mx_Flex"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x);"
>
<button <button
aria-disabled="true" aria-disabled="true"
aria-label="There's no one here to call" aria-label="There's no one here to call"
@ -1390,34 +1366,9 @@ exports[`RoomView should not display the timeline when the room encryption is lo
</svg> </svg>
</div> </div>
</button> </button>
<button
aria-label="Room info"
aria-labelledby=":r2m:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%;"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16v-4a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 11a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 12v4c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-8c.283 0 .52-.096.713-.287A.967.967 0 0 0 13 8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 13a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
/>
</svg>
</div>
</button>
<button <button
aria-label="Threads" aria-label="Threads"
aria-labelledby=":r2r:" aria-labelledby=":r2m:"
class="_icon-button_bh2qc_17" class="_icon-button_bh2qc_17"
role="button" role="button"
style="--cpd-icon-button-size: 32px;" style="--cpd-icon-button-size: 32px;"
@ -1440,7 +1391,31 @@ exports[`RoomView should not display the timeline when the room encryption is lo
</svg> </svg>
</div> </div>
</button> </button>
<button
aria-label="Room info"
aria-labelledby=":r2r:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%;"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16v-4a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 11a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 12v4c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-8c.283 0 .52-.096.713-.287A.967.967 0 0 0 13 8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 13a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
/>
</svg>
</div> </div>
</button>
<div <div
class="_typography_yh5dq_162 _font-body-sm-medium_yh5dq_50" class="_typography_yh5dq_162 _font-body-sm-medium_yh5dq_50"
> >
@ -1545,10 +1520,6 @@ exports[`RoomView should not display the timeline when the room encryption is lo
</div> </div>
</div> </div>
</button> </button>
<div
class="mx_Flex"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x);"
>
<button <button
aria-disabled="true" aria-disabled="true"
aria-label="There's no one here to call" aria-label="There's no one here to call"
@ -1601,34 +1572,9 @@ exports[`RoomView should not display the timeline when the room encryption is lo
</svg> </svg>
</div> </div>
</button> </button>
<button
aria-label="Room info"
aria-labelledby=":r2m:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%;"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16v-4a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 11a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 12v4c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-8c.283 0 .52-.096.713-.287A.967.967 0 0 0 13 8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 13a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
/>
</svg>
</div>
</button>
<button <button
aria-label="Threads" aria-label="Threads"
aria-labelledby=":r2r:" aria-labelledby=":r2m:"
class="_icon-button_bh2qc_17" class="_icon-button_bh2qc_17"
role="button" role="button"
style="--cpd-icon-button-size: 32px;" style="--cpd-icon-button-size: 32px;"
@ -1651,7 +1597,31 @@ exports[`RoomView should not display the timeline when the room encryption is lo
</svg> </svg>
</div> </div>
</button> </button>
<button
aria-label="Room info"
aria-labelledby=":r2r:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%;"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16v-4a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 11a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 12v4c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-8c.283 0 .52-.096.713-.287A.967.967 0 0 0 13 8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 13a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
/>
</svg>
</div> </div>
</button>
<div <div
class="_typography_yh5dq_162 _font-body-sm-medium_yh5dq_50" class="_typography_yh5dq_162 _font-body-sm-medium_yh5dq_50"
> >
@ -1929,38 +1899,9 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
</div> </div>
</div> </div>
</button> </button>
<div
class="mx_Flex"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x);"
>
<button
aria-label="Room info"
aria-labelledby=":r7c:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%;"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16v-4a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 11a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 12v4c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-8c.283 0 .52-.096.713-.287A.967.967 0 0 0 13 8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 13a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
/>
</svg>
</div>
</button>
<button <button
aria-label="Chat" aria-label="Chat"
aria-labelledby=":r7h:" aria-labelledby=":r7c:"
class="_icon-button_bh2qc_17" class="_icon-button_bh2qc_17"
role="button" role="button"
style="--cpd-icon-button-size: 32px;" style="--cpd-icon-button-size: 32px;"
@ -1985,7 +1926,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
</button> </button>
<button <button
aria-label="Threads" aria-label="Threads"
aria-labelledby=":r7m:" aria-labelledby=":r7h:"
class="_icon-button_bh2qc_17" class="_icon-button_bh2qc_17"
role="button" role="button"
style="--cpd-icon-button-size: 32px;" style="--cpd-icon-button-size: 32px;"
@ -2008,7 +1949,31 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
</svg> </svg>
</div> </div>
</button> </button>
<button
aria-label="Room info"
aria-labelledby=":r7m:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%;"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16v-4a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 11a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 12v4c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-8c.283 0 .52-.096.713-.287A.967.967 0 0 0 13 8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 13a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
/>
</svg>
</div> </div>
</button>
<div <div
class="_typography_yh5dq_162 _font-body-sm-medium_yh5dq_50" class="_typography_yh5dq_162 _font-body-sm-medium_yh5dq_50"
> >

View File

@ -42,12 +42,20 @@ describe("LogoutDialog", () => {
expect(rendered.container).toMatchSnapshot(); expect(rendered.container).toMatchSnapshot();
}); });
it("shows a regular dialog if backups are working", async () => { it("shows a regular dialog if backups and recovery are working", async () => {
mockCrypto.getActiveSessionBackupVersion.mockResolvedValue("1"); mockCrypto.getActiveSessionBackupVersion.mockResolvedValue("1");
mockCrypto.isSecretStorageReady.mockResolvedValue(true);
const rendered = renderComponent(); const rendered = renderComponent();
await rendered.findByText("Are you sure you want to sign out?"); await rendered.findByText("Are you sure you want to sign out?");
}); });
it("prompts user to set up recovery if backups are enabled but recovery isn't", async () => {
mockCrypto.getActiveSessionBackupVersion.mockResolvedValue("1");
mockCrypto.isSecretStorageReady.mockResolvedValue(false);
const rendered = renderComponent();
await rendered.findByText("You'll lose access to your encrypted messages");
});
it("Prompts user to connect backup if there is a backup on the server", async () => { it("Prompts user to connect backup if there is a backup on the server", async () => {
mockCrypto.getKeyBackupInfo.mockResolvedValue({} as KeyBackupInfo); mockCrypto.getKeyBackupInfo.mockResolvedValue({} as KeyBackupInfo);
const rendered = renderComponent(); const rendered = renderComponent();

View File

@ -375,10 +375,12 @@ describe("<TextualBody />", () => {
}); });
}); });
it("renders url previews correctly", () => { describe("url preview", () => {
languageHandler.setMissingEntryGenerator((key) => key.split("|", 2)[1]); let matrixClient: MatrixClient;
const matrixClient = getMockClientWithEventEmitter({ beforeEach(() => {
languageHandler.setMissingEntryGenerator((key) => key.split("|", 2)[1]);
matrixClient = getMockClientWithEventEmitter({
getRoom: () => mkStubRoom("room_id", "room name", undefined), getRoom: () => mkStubRoom("room_id", "room name", undefined),
getAccountData: (): MatrixClient | undefined => undefined, getAccountData: (): MatrixClient | undefined => undefined,
getUrlPreview: (url: string) => new Promise(() => {}), getUrlPreview: (url: string) => new Promise(() => {}),
@ -386,7 +388,9 @@ describe("<TextualBody />", () => {
mxcUrlToHttp: (s: string) => s, mxcUrlToHttp: (s: string) => s,
}); });
DMRoomMap.makeShared(defaultMatrixClient); DMRoomMap.makeShared(defaultMatrixClient);
});
it("renders url previews correctly", () => {
const ev = mkRoomTextMessage("Visit https://matrix.org/"); const ev = mkRoomTextMessage("Visit https://matrix.org/");
const { container, rerender } = getComponent( const { container, rerender } = getComponent(
{ mxEvent: ev, showUrlPreview: true, onHeightChanged: jest.fn() }, { mxEvent: ev, showUrlPreview: true, onHeightChanged: jest.fn() },
@ -426,4 +430,18 @@ describe("<TextualBody />", () => {
expect(node).toHaveAttribute("href", links[index]); expect(node).toHaveAttribute("href", links[index]);
}); });
}); });
it("should listen to showUrlPreview change", () => {
const ev = mkRoomTextMessage("Visit https://matrix.org/");
const { container, rerender } = getComponent(
{ mxEvent: ev, showUrlPreview: false, onHeightChanged: jest.fn() },
matrixClient,
);
expect(container.querySelector(".mx_LinkPreviewGroup")).toBeNull();
getComponent({ mxEvent: ev, showUrlPreview: true, onHeightChanged: jest.fn() }, matrixClient, rerender);
expect(container.querySelector(".mx_LinkPreviewGroup")).toBeTruthy();
});
});
}); });

View File

@ -42,10 +42,6 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
</div> </div>
</div> </div>
</button> </button>
<div
class="mx_Flex"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x);"
>
<button <button
aria-labelledby=":r16c:" aria-labelledby=":r16c:"
class="_icon-button_bh2qc_17" class="_icon-button_bh2qc_17"
@ -96,34 +92,9 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
</svg> </svg>
</div> </div>
</button> </button>
<button
aria-label="Room info"
aria-labelledby=":r16m:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%;"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16v-4a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 11a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 12v4c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-8c.283 0 .52-.096.713-.287A.967.967 0 0 0 13 8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 13a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
/>
</svg>
</div>
</button>
<button <button
aria-label="Threads" aria-label="Threads"
aria-labelledby=":r16r:" aria-labelledby=":r16m:"
class="_icon-button_bh2qc_17" class="_icon-button_bh2qc_17"
role="button" role="button"
style="--cpd-icon-button-size: 32px;" style="--cpd-icon-button-size: 32px;"
@ -146,7 +117,31 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
</svg> </svg>
</div> </div>
</button> </button>
<button
aria-label="Room info"
aria-labelledby=":r16r:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%;"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16v-4a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 11a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 12v4c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-8c.283 0 .52-.096.713-.287A.967.967 0 0 0 13 8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 13a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
/>
</svg>
</div> </div>
</button>
</header> </header>
</DocumentFragment> </DocumentFragment>
`; `;

View File

@ -11,7 +11,6 @@ import { fireEvent, render, screen, waitFor, within } from "jest-matrix-react";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import { SDKContext, SdkContextClass } from "../../../../../src/contexts/SDKContext";
import SettingsStore from "../../../../../src/settings/SettingsStore"; import SettingsStore from "../../../../../src/settings/SettingsStore";
import { UIFeature } from "../../../../../src/settings/UIFeature"; import { UIFeature } from "../../../../../src/settings/UIFeature";
import { import {
@ -35,13 +34,9 @@ describe("SetIntegrationManager", () => {
deleteThreePid: jest.fn(), deleteThreePid: jest.fn(),
}); });
let stores!: SdkContextClass;
const getComponent = () => ( const getComponent = () => (
<MatrixClientContext.Provider value={mockClient}> <MatrixClientContext.Provider value={mockClient}>
<SDKContext.Provider value={stores}>
<SetIntegrationManager /> <SetIntegrationManager />
</SDKContext.Provider>
</MatrixClientContext.Provider> </MatrixClientContext.Provider>
); );

View File

@ -0,0 +1,24 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render, screen } from "jest-matrix-react";
import ToastContainer from "../../../src/components/structures/ToastContainer";
import { Kind, showToast } from "../../../src/toasts/SetupEncryptionToast";
describe("SetupEncryptionToast", () => {
beforeEach(() => {
render(<ToastContainer />);
});
it("should render the se up recovery toast", async () => {
showToast(Kind.SET_UP_RECOVERY);
await expect(screen.findByText("Set up recovery")).resolves.toBeInTheDocument();
});
});

View File

@ -8595,9 +8595,9 @@ murmurhash-js@^1.0.0:
integrity sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw== integrity sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==
nanoid@^3.3.7: nanoid@^3.3.7:
version "3.3.7" version "3.3.8"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==
natural-compare@^1.4.0: natural-compare@^1.4.0:
version "1.4.0" version "1.4.0"
@ -11539,10 +11539,10 @@ typed-array-length@^1.0.6:
possible-typed-array-names "^1.0.0" possible-typed-array-names "^1.0.0"
reflect.getprototypeof "^1.0.6" reflect.getprototypeof "^1.0.6"
typescript@5.6.3: typescript@5.7.2:
version "5.6.3" version "5.7.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.3.tgz#5f3449e31c9d94febb17de03cc081dd56d81db5b" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6"
integrity sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw== integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==
ua-parser-js@^1.0.2: ua-parser-js@^1.0.2:
version "1.0.39" version "1.0.39"