Merge branch 'develop' into florianduros/encryption-tab
|
@ -16,6 +16,11 @@ on:
|
|||
options:
|
||||
- staging.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' }}
|
||||
permissions: {}
|
||||
jobs:
|
||||
|
@ -75,6 +80,7 @@ jobs:
|
|||
|
||||
- name: Wait for other steps to succeed
|
||||
uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork
|
||||
if: inputs.skip-checks != true
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
running-workflow-name: "Deploy to Cloudflare Pages"
|
||||
|
|
10
CHANGELOG.md
|
@ -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)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "element-web",
|
||||
"version": "1.11.88",
|
||||
"version": "1.11.89",
|
||||
"description": "A feature-rich client for Matrix.org",
|
||||
"author": "New Vector Ltd.",
|
||||
"repository": {
|
||||
|
@ -282,7 +282,7 @@
|
|||
"terser-webpack-plugin": "^5.3.9",
|
||||
"ts-node": "^10.9.1",
|
||||
"ts-prune": "^0.10.3",
|
||||
"typescript": "5.6.3",
|
||||
"typescript": "5.7.2",
|
||||
"util": "^0.12.5",
|
||||
"web-streams-polyfill": "^4.0.0",
|
||||
"webpack": "^5.89.0",
|
||||
|
|
|
@ -9,6 +9,8 @@ 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 { test as masTest, registerAccountMas } from "../oidc";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
async function expectBackupVersionToBe(page: Page, version: string) {
|
||||
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);
|
||||
}
|
||||
|
||||
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.use({
|
||||
displayName: "Hanako",
|
||||
|
|
|
@ -8,11 +8,11 @@ Please see LICENSE files in the repository root for full details.
|
|||
|
||||
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 { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
const test = base.extend({
|
||||
const test = base.extend<Fixtures>({
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
startHomeserverOpts: async ({}, use) => {
|
||||
await use("dehydration");
|
||||
|
|
|
@ -9,9 +9,9 @@ Please see LICENSE files in the repository root for full details.
|
|||
import path from "path";
|
||||
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.
|
||||
user: async ({ context, pageWithCredentials: page, credentials }, use) => {
|
||||
await page.route(`/test_indexeddb_cryptostore_dump/*`, async (route, request) => {
|
||||
|
|
|
@ -71,7 +71,9 @@ test.describe("Room Header", () => {
|
|||
|
||||
// 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
|
||||
const buttons = header.locator(".mx_Flex").getByRole("button");
|
||||
const buttons = header.getByRole("button").filter({
|
||||
has: page.locator("svg"),
|
||||
});
|
||||
await expect(buttons).toHaveCount(5);
|
||||
|
||||
for (const button of await buttons.all()) {
|
||||
|
|
|
@ -60,7 +60,7 @@ interface CredentialsWithDisplayName extends Credentials {
|
|||
displayName: string;
|
||||
}
|
||||
|
||||
export const test = base.extend<{
|
||||
export interface Fixtures {
|
||||
axe: AxeBuilder;
|
||||
checkA11y: () => Promise<void>;
|
||||
|
||||
|
@ -124,7 +124,9 @@ export const test = base.extend<{
|
|||
slidingSyncProxy: ProxyInstance;
|
||||
labsFlags: string[];
|
||||
webserver: Webserver;
|
||||
}>({
|
||||
}
|
||||
|
||||
export const test = base.extend<Fixtures>({
|
||||
config: CONFIG_JSON,
|
||||
page: async ({ context, page, config, labsFlags }, use) => {
|
||||
await context.route(`http://localhost:8080/config.json*`, async (route) => {
|
||||
|
|
|
@ -20,7 +20,7 @@ import { randB64Bytes } from "../../utils/rand";
|
|||
// 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.
|
||||
// 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">> {
|
||||
const templateDir = path.join(__dirname, "templates", opts.template);
|
||||
|
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.0 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
@ -16,18 +16,19 @@ import { _t } from "./languageHandler";
|
|||
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
|
||||
* @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> {
|
||||
try {
|
||||
await cli.uploadDeviceSigningKeys(undefined, {} as CrossSigningKeys);
|
||||
// We should never get here: the server should always require
|
||||
// UI auth to upload device signing keys. If we do, we upload
|
||||
// no keys which would be a no-op.
|
||||
logger.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!");
|
||||
return false;
|
||||
// If we get here, it's because the server is allowing us to upload keys without
|
||||
// auth the first time due to MSC3967. Therefore, yes, we can upload keys
|
||||
// (with or without password, technically, but that's fine).
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (!(error instanceof MatrixError) || !error.data || !error.data.flows) {
|
||||
logger.log("uploadDeviceSigningKeys advertised no flows!");
|
||||
|
|
|
@ -295,21 +295,29 @@ export default class DeviceListener {
|
|||
await crypto.getUserDeviceInfo([cli.getSafeUserId()]);
|
||||
|
||||
// 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())) {
|
||||
// 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);
|
||||
this.checkKeyBackupStatus();
|
||||
} else {
|
||||
// No cross-signing or key backup on account (set up encryption)
|
||||
await cli.waitForClientWellKnown();
|
||||
if (isSecureBackupRequired(cli) && isLoggedIn()) {
|
||||
// If we're meant to set up, and Secure Backup is required,
|
||||
// trigger the flow directly without a toast once logged in.
|
||||
hideSetupEncryptionToast();
|
||||
accessSecretStorage();
|
||||
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 {
|
||||
showSetupEncryptionToast(SetupKind.SET_UP_ENCRYPTION);
|
||||
// Toast 3: No cross-signing or key backup on account (set up encryption)
|
||||
await cli.waitForClientWellKnown();
|
||||
if (isSecureBackupRequired(cli) && isLoggedIn()) {
|
||||
// If we're meant to set up, and Secure Backup is required,
|
||||
// trigger the flow directly without a toast once logged in.
|
||||
hideSetupEncryptionToast();
|
||||
accessSecretStorage();
|
||||
} else {
|
||||
showSetupEncryptionToast(SetupKind.SET_UP_ENCRYPTION);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,6 +38,9 @@ enum BackupStatus {
|
|||
/** there is a backup on the server but we are not backing up to it */
|
||||
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 */
|
||||
NO_BACKUP,
|
||||
|
||||
|
@ -104,7 +107,11 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
if ((await crypto.getActiveSessionBackupVersion()) !== null) {
|
||||
this.setState({ backupStatus: BackupStatus.BACKUP_ACTIVE });
|
||||
if (await crypto.isSecretStorageReady()) {
|
||||
this.setState({ backupStatus: BackupStatus.BACKUP_ACTIVE });
|
||||
} else {
|
||||
this.setState({ backupStatus: BackupStatus.BACKUP_NO_RECOVERY });
|
||||
}
|
||||
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
|
||||
* we are not connected to it ({@link BackupStatus.SERVER_BACKUP_BUT_DISABLED}), or we were unable to pull the
|
||||
* backup data ({@link BackupStatus.ERROR}). In all three cases, we should prompt the user to set up key backup.
|
||||
* Either:
|
||||
* * There is no backup at all ({@link BackupStatus.NO_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 = (
|
||||
<div>
|
||||
<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.SERVER_BACKUP_BUT_DISABLED:
|
||||
case BackupStatus.ERROR:
|
||||
return this.renderSetupBackupDialog();
|
||||
case BackupStatus.BACKUP_NO_RECOVERY:
|
||||
return this.renderSetupRecoveryMethod();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -128,7 +128,8 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
|||
if (!this.props.editState) {
|
||||
const stoppedEditing = prevProps.editState && !this.props.editState;
|
||||
const messageWasEdited = prevProps.replacingEventId !== this.props.replacingEventId;
|
||||
if (messageWasEdited || stoppedEditing) {
|
||||
const urlPreviewChanged = prevProps.showUrlPreview !== this.props.showUrlPreview;
|
||||
if (messageWasEdited || stoppedEditing || urlPreviewChanged) {
|
||||
this.applyFormatting();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -310,78 +310,78 @@ export default function RoomHeader({
|
|||
</BodyText>
|
||||
</Box>
|
||||
</button>
|
||||
<Flex align="center" gap="var(--cpd-space-2x)">
|
||||
{additionalButtons?.map((props) => {
|
||||
const label = props.label();
|
||||
|
||||
return (
|
||||
<Tooltip label={label} key={props.id}>
|
||||
<IconButton
|
||||
aria-label={label}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
props.onClick();
|
||||
}}
|
||||
>
|
||||
{typeof props.icon === "function" ? props.icon() : props.icon}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
{additionalButtons?.map((props) => {
|
||||
const label = props.label();
|
||||
|
||||
{isViewingCall && <CallGuestLinkButton room={room} />}
|
||||
|
||||
{hasActiveCallSession && !isConnectedToCall && !isViewingCall ? (
|
||||
joinCallButton
|
||||
) : (
|
||||
<>
|
||||
{!isVideoRoom && videoCallButton}
|
||||
{!useElementCallExclusively && !isVideoRoom && voiceCallButton}
|
||||
</>
|
||||
)}
|
||||
|
||||
<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} />}
|
||||
|
||||
<Tooltip label={_t("common|threads")}>
|
||||
<IconButton
|
||||
indicator={notificationLevelToIndicator(threadNotifications)}
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.ThreadPanel);
|
||||
PosthogTrackers.trackInteraction("WebRoomHeaderButtonsThreadsButton", evt);
|
||||
}}
|
||||
aria-label={_t("common|threads")}
|
||||
>
|
||||
<ThreadsIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{notificationsEnabled && (
|
||||
<Tooltip label={_t("notifications|enable_prompt_toast_title")}>
|
||||
return (
|
||||
<Tooltip label={label} key={props.id}>
|
||||
<IconButton
|
||||
indicator={notificationLevelToIndicator(globalNotificationState.level)}
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.NotificationPanel);
|
||||
aria-label={label}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
props.onClick();
|
||||
}}
|
||||
aria-label={_t("notifications|enable_prompt_toast_title")}
|
||||
>
|
||||
<NotificationsIcon />
|
||||
{typeof props.icon === "function" ? props.icon() : props.icon}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
|
||||
{isViewingCall && <CallGuestLinkButton room={room} />}
|
||||
|
||||
{hasActiveCallSession && !isConnectedToCall && !isViewingCall ? (
|
||||
joinCallButton
|
||||
) : (
|
||||
<>
|
||||
{!isVideoRoom && videoCallButton}
|
||||
{!useElementCallExclusively && !isVideoRoom && voiceCallButton}
|
||||
</>
|
||||
)}
|
||||
|
||||
{showChatButton && <VideoRoomChatButton room={room} />}
|
||||
|
||||
<Tooltip label={_t("common|threads")}>
|
||||
<IconButton
|
||||
indicator={notificationLevelToIndicator(threadNotifications)}
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.ThreadPanel);
|
||||
PosthogTrackers.trackInteraction("WebRoomHeaderButtonsThreadsButton", evt);
|
||||
}}
|
||||
aria-label={_t("common|threads")}
|
||||
>
|
||||
<ThreadsIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{notificationsEnabled && (
|
||||
<Tooltip label={_t("notifications|enable_prompt_toast_title")}>
|
||||
<IconButton
|
||||
indicator={notificationLevelToIndicator(globalNotificationState.level)}
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.NotificationPanel);
|
||||
}}
|
||||
aria-label={_t("notifications|enable_prompt_toast_title")}
|
||||
>
|
||||
<NotificationsIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<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 && (
|
||||
<BodyText as="div" size="sm" weight="medium">
|
||||
<FacePile
|
||||
|
|
|
@ -214,7 +214,7 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
|
|||
<SettingsSubsectionText>
|
||||
{this.state.enabling ? <InlineSpinner /> : _t("settings|security|message_search_failed")}
|
||||
</SettingsSubsectionText>
|
||||
{EventIndexPeg.error && (
|
||||
{EventIndexPeg.error ? (
|
||||
<SettingsSubsectionText>
|
||||
<details>
|
||||
<summary>{_t("common|advanced")}</summary>
|
||||
|
@ -230,7 +230,7 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
|
|||
</p>
|
||||
</details>
|
||||
</SettingsSubsectionText>
|
||||
)}
|
||||
) : undefined}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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."
|
||||
},
|
||||
"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_title": "Set up Secure Backup",
|
||||
"setup_secure_backup": {
|
||||
|
|
|
@ -2115,7 +2115,8 @@
|
|||
"show_less": "Pokaż mniej",
|
||||
"show_n_more": {
|
||||
"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",
|
||||
"sort_by": "Sortuj według",
|
||||
|
@ -3689,7 +3690,8 @@
|
|||
"close": "Zamknij podgląd",
|
||||
"show_n_more": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -114,8 +114,15 @@ export class InitialCryptoSetupStore extends EventEmitter {
|
|||
this.emit("update");
|
||||
|
||||
try {
|
||||
// Create the user's cross-signing keys
|
||||
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.status = "complete";
|
||||
|
|
|
@ -23,15 +23,19 @@ const getTitle = (kind: Kind): string => {
|
|||
switch (kind) {
|
||||
case Kind.SET_UP_ENCRYPTION:
|
||||
return _t("encryption|set_up_toast_title");
|
||||
case Kind.SET_UP_RECOVERY:
|
||||
return _t("encryption|set_up_recovery");
|
||||
case Kind.VERIFY_THIS_SESSION:
|
||||
return _t("encryption|verify_toast_title");
|
||||
}
|
||||
};
|
||||
|
||||
const getIcon = (kind: Kind): string => {
|
||||
const getIcon = (kind: Kind): string | undefined => {
|
||||
switch (kind) {
|
||||
case Kind.SET_UP_ENCRYPTION:
|
||||
return "secure_backup";
|
||||
case Kind.SET_UP_RECOVERY:
|
||||
return undefined;
|
||||
case Kind.VERIFY_THIS_SESSION:
|
||||
return "verification_warning";
|
||||
}
|
||||
|
@ -41,22 +45,49 @@ const getSetupCaption = (kind: Kind): string => {
|
|||
switch (kind) {
|
||||
case Kind.SET_UP_ENCRYPTION:
|
||||
return _t("action|continue");
|
||||
case Kind.SET_UP_RECOVERY:
|
||||
return _t("action|continue");
|
||||
case Kind.VERIFY_THIS_SESSION:
|
||||
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 => {
|
||||
switch (kind) {
|
||||
case Kind.SET_UP_ENCRYPTION:
|
||||
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:
|
||||
return _t("encryption|verify_toast_description");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The kind of toast to show.
|
||||
*/
|
||||
export enum Kind {
|
||||
/**
|
||||
* Prompt the user to 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",
|
||||
}
|
||||
|
||||
|
@ -64,6 +95,11 @@ const onReject = (): void => {
|
|||
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 => {
|
||||
if (
|
||||
ModuleRunner.instance.extensions.cryptoSetup.setupEncryptionNeeded({
|
||||
|
@ -101,15 +137,17 @@ export const showToast = (kind: Kind): void => {
|
|||
description: getDescription(kind),
|
||||
primaryLabel: getSetupCaption(kind),
|
||||
onPrimaryClick: onAccept,
|
||||
secondaryLabel: _t("encryption|verification|unverified_sessions_toast_reject"),
|
||||
secondaryLabel: getSecondaryButtonLabel(kind),
|
||||
onSecondaryClick: onReject,
|
||||
destructive: "secondary",
|
||||
},
|
||||
component: GenericToast,
|
||||
priority: kind === Kind.VERIFY_THIS_SESSION ? 95 : 40,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hide the encryption setup toast if it is currently being shown.
|
||||
*/
|
||||
export const hideToast = (): void => {
|
||||
ToastStore.sharedInstance().dismissToast(TOAST_KEY);
|
||||
};
|
||||
|
|
|
@ -137,6 +137,7 @@ export function createTestClient(): MatrixClient {
|
|||
restoreKeyBackupWithPassphrase: jest.fn(),
|
||||
loadSessionBackupPrivateKeyFromSecretStorage: jest.fn(),
|
||||
storeSessionBackupPrivateKey: jest.fn(),
|
||||
checkKeyBackupAndEnable: jest.fn().mockResolvedValue(null),
|
||||
getKeyBackupInfo: jest.fn().mockResolvedValue(null),
|
||||
getEncryptionInfoForEvent: jest.fn().mockResolvedValue(null),
|
||||
getCrossSigningStatus: jest.fn().mockResolvedValue({
|
||||
|
|
|
@ -352,13 +352,13 @@ describe("DeviceListener", () => {
|
|||
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
|
||||
mockCrypto.getKeyBackupInfo.mockResolvedValue({} as unknown as KeyBackupInfo);
|
||||
await createAndStart();
|
||||
|
||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith(
|
||||
SetupEncryptionToast.Kind.SET_UP_ENCRYPTION,
|
||||
SetupEncryptionToast.Kind.SET_UP_RECOVERY,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1003,7 +1003,9 @@ describe("<MatrixChat />", () => {
|
|||
userHasCrossSigningKeys: jest.fn().mockResolvedValue(false),
|
||||
// This needs to not finish immediately because we need to test the screen appears
|
||||
bootstrapCrossSigning: jest.fn().mockImplementation(() => bootstrapDeferred.promise),
|
||||
resetKeyBackup: jest.fn(),
|
||||
isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(false),
|
||||
checkKeyBackupAndEnable: jest.fn().mockResolvedValue(null),
|
||||
};
|
||||
loginClient.getCrypto.mockReturnValue(mockCrypto as any);
|
||||
});
|
||||
|
|
|
@ -42,12 +42,20 @@ describe("LogoutDialog", () => {
|
|||
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.isSecretStorageReady.mockResolvedValue(true);
|
||||
const rendered = renderComponent();
|
||||
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 () => {
|
||||
mockCrypto.getKeyBackupInfo.mockResolvedValue({} as KeyBackupInfo);
|
||||
const rendered = renderComponent();
|
||||
|
|
|
@ -375,55 +375,73 @@ describe("<TextualBody />", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("renders url previews correctly", () => {
|
||||
languageHandler.setMissingEntryGenerator((key) => key.split("|", 2)[1]);
|
||||
describe("url preview", () => {
|
||||
let matrixClient: MatrixClient;
|
||||
|
||||
const matrixClient = getMockClientWithEventEmitter({
|
||||
getRoom: () => mkStubRoom("room_id", "room name", undefined),
|
||||
getAccountData: (): MatrixClient | undefined => undefined,
|
||||
getUrlPreview: (url: string) => new Promise(() => {}),
|
||||
isGuest: () => false,
|
||||
mxcUrlToHttp: (s: string) => s,
|
||||
beforeEach(() => {
|
||||
languageHandler.setMissingEntryGenerator((key) => key.split("|", 2)[1]);
|
||||
matrixClient = getMockClientWithEventEmitter({
|
||||
getRoom: () => mkStubRoom("room_id", "room name", undefined),
|
||||
getAccountData: (): MatrixClient | undefined => undefined,
|
||||
getUrlPreview: (url: string) => new Promise(() => {}),
|
||||
isGuest: () => false,
|
||||
mxcUrlToHttp: (s: string) => s,
|
||||
});
|
||||
DMRoomMap.makeShared(defaultMatrixClient);
|
||||
});
|
||||
DMRoomMap.makeShared(defaultMatrixClient);
|
||||
|
||||
const ev = mkRoomTextMessage("Visit https://matrix.org/");
|
||||
const { container, rerender } = getComponent(
|
||||
{ mxEvent: ev, showUrlPreview: true, onHeightChanged: jest.fn() },
|
||||
matrixClient,
|
||||
);
|
||||
it("renders url previews correctly", () => {
|
||||
const ev = mkRoomTextMessage("Visit https://matrix.org/");
|
||||
const { container, rerender } = getComponent(
|
||||
{ mxEvent: ev, showUrlPreview: true, onHeightChanged: jest.fn() },
|
||||
matrixClient,
|
||||
);
|
||||
|
||||
expect(container).toHaveTextContent(ev.getContent().body);
|
||||
expect(container.querySelector("a")).toHaveAttribute("href", "https://matrix.org/");
|
||||
expect(container).toHaveTextContent(ev.getContent().body);
|
||||
expect(container.querySelector("a")).toHaveAttribute("href", "https://matrix.org/");
|
||||
|
||||
// simulate an event edit and check the transition from the old URL preview to the new one
|
||||
const ev2 = mkEvent({
|
||||
type: "m.room.message",
|
||||
room: "room_id",
|
||||
user: "sender",
|
||||
content: {
|
||||
"m.new_content": {
|
||||
body: "Visit https://vector.im/ and https://riot.im/",
|
||||
msgtype: "m.text",
|
||||
// simulate an event edit and check the transition from the old URL preview to the new one
|
||||
const ev2 = mkEvent({
|
||||
type: "m.room.message",
|
||||
room: "room_id",
|
||||
user: "sender",
|
||||
content: {
|
||||
"m.new_content": {
|
||||
body: "Visit https://vector.im/ and https://riot.im/",
|
||||
msgtype: "m.text",
|
||||
},
|
||||
},
|
||||
},
|
||||
event: true,
|
||||
event: true,
|
||||
});
|
||||
jest.spyOn(ev, "replacingEventDate").mockReturnValue(new Date(1993, 7, 3));
|
||||
ev.makeReplaced(ev2);
|
||||
|
||||
getComponent(
|
||||
{ mxEvent: ev, showUrlPreview: true, onHeightChanged: jest.fn(), replacingEventId: ev.getId() },
|
||||
matrixClient,
|
||||
rerender,
|
||||
);
|
||||
|
||||
expect(container).toHaveTextContent(ev2.getContent()["m.new_content"].body + "(edited)");
|
||||
|
||||
const links = ["https://vector.im/", "https://riot.im/"];
|
||||
const anchorNodes = container.querySelectorAll("a");
|
||||
Array.from(anchorNodes).forEach((node, index) => {
|
||||
expect(node).toHaveAttribute("href", links[index]);
|
||||
});
|
||||
});
|
||||
jest.spyOn(ev, "replacingEventDate").mockReturnValue(new Date(1993, 7, 3));
|
||||
ev.makeReplaced(ev2);
|
||||
|
||||
getComponent(
|
||||
{ mxEvent: ev, showUrlPreview: true, onHeightChanged: jest.fn(), replacingEventId: ev.getId() },
|
||||
matrixClient,
|
||||
rerender,
|
||||
);
|
||||
it("should listen to showUrlPreview change", () => {
|
||||
const ev = mkRoomTextMessage("Visit https://matrix.org/");
|
||||
|
||||
expect(container).toHaveTextContent(ev2.getContent()["m.new_content"].body + "(edited)");
|
||||
const { container, rerender } = getComponent(
|
||||
{ mxEvent: ev, showUrlPreview: false, onHeightChanged: jest.fn() },
|
||||
matrixClient,
|
||||
);
|
||||
expect(container.querySelector(".mx_LinkPreviewGroup")).toBeNull();
|
||||
|
||||
const links = ["https://vector.im/", "https://riot.im/"];
|
||||
const anchorNodes = container.querySelectorAll("a");
|
||||
Array.from(anchorNodes).forEach((node, index) => {
|
||||
expect(node).toHaveAttribute("href", links[index]);
|
||||
getComponent({ mxEvent: ev, showUrlPreview: true, onHeightChanged: jest.fn() }, matrixClient, rerender);
|
||||
expect(container.querySelector(".mx_LinkPreviewGroup")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -42,111 +42,106 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
|
|||
</div>
|
||||
</div>
|
||||
</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-labelledby=":r16c:"
|
||||
class="_icon-button_bh2qc_17"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<button
|
||||
aria-labelledby=":r16c:"
|
||||
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%;"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-disabled="true"
|
||||
aria-label="There's no one here to call"
|
||||
aria-labelledby=":r16h:"
|
||||
class="_icon-button_bh2qc_17"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
tabindex="0"
|
||||
<path
|
||||
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-disabled="true"
|
||||
aria-label="There's no one here to call"
|
||||
aria-labelledby=":r16h:"
|
||||
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%; --cpd-color-icon-tertiary: var(--cpd-color-icon-disabled);"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_133tf_26"
|
||||
style="--cpd-icon-button-size: 100%; --cpd-color-icon-tertiary: var(--cpd-color-icon-disabled);"
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m20.958 16.374.039 3.527c0 .285-.11.537-.33.756-.22.22-.472.33-.756.33a15.97 15.97 0 0 1-6.57-1.105 16.223 16.223 0 0 1-5.563-3.663 16.084 16.084 0 0 1-3.653-5.573 16.313 16.313 0 0 1-1.115-6.56c0-.285.11-.537.33-.757.22-.22.471-.329.755-.329l3.528.039a1.069 1.069 0 0 1 1.085.93l.543 3.954c.026.181.013.349-.039.504a1.088 1.088 0 0 1-.271.426l-1.64 1.64c.337.672.721 1.308 1.154 1.909.433.6 1.444 1.696 1.444 1.696s1.095 1.01 1.696 1.444c.6.433 1.237.817 1.909 1.153l1.64-1.64a1.08 1.08 0 0 1 .426-.27c.155-.052.323-.065.504-.04l3.954.543a1.069 1.069 0 0 1 .93 1.085Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Room info"
|
||||
aria-labelledby=":r16m:"
|
||||
class="_icon-button_bh2qc_17"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
tabindex="0"
|
||||
<path
|
||||
d="m20.958 16.374.039 3.527c0 .285-.11.537-.33.756-.22.22-.472.33-.756.33a15.97 15.97 0 0 1-6.57-1.105 16.223 16.223 0 0 1-5.563-3.663 16.084 16.084 0 0 1-3.653-5.573 16.313 16.313 0 0 1-1.115-6.56c0-.285.11-.537.33-.757.22-.22.471-.329.755-.329l3.528.039a1.069 1.069 0 0 1 1.085.93l.543 3.954c.026.181.013.349-.039.504a1.088 1.088 0 0 1-.271.426l-1.64 1.64c.337.672.721 1.308 1.154 1.909.433.6 1.444 1.696 1.444 1.696s1.095 1.01 1.696 1.444c.6.433 1.237.817 1.909 1.153l1.64-1.64a1.08 1.08 0 0 1 .426-.27c.155-.052.323-.065.504-.04l3.954.543a1.069 1.069 0 0 1 .93 1.085Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Threads"
|
||||
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%;"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<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
|
||||
aria-label="Threads"
|
||||
aria-labelledby=":r16r:"
|
||||
class="_icon-button_bh2qc_17"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
tabindex="0"
|
||||
<path
|
||||
d="M4 3h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6l-2.293 2.293c-.63.63-1.707.184-1.707-.707V5a2 2 0 0 1 2-2Zm3 7h10a.97.97 0 0 0 .712-.287A.967.967 0 0 0 18 9a.967.967 0 0 0-.288-.713A.968.968 0 0 0 17 8H7a.968.968 0 0 0-.713.287A.968.968 0 0 0 6 9c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 4h6c.283 0 .52-.096.713-.287A.968.968 0 0 0 14 13a.968.968 0 0 0-.287-.713A.968.968 0 0 0 13 12H7a.967.967 0 0 0-.713.287A.968.968 0 0 0 6 13c0 .283.096.52.287.713.192.191.43.287.713.287Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</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%;"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M4 3h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6l-2.293 2.293c-.63.63-1.707.184-1.707-.707V5a2 2 0 0 1 2-2Zm3 7h10a.97.97 0 0 0 .712-.287A.967.967 0 0 0 18 9a.967.967 0 0 0-.288-.713A.968.968 0 0 0 17 8H7a.968.968 0 0 0-.713.287A.968.968 0 0 0 6 9c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 4h6c.283 0 .52-.096.713-.287A.968.968 0 0 0 14 13a.968.968 0 0 0-.287-.713A.968.968 0 0 0 13 12H7a.967.967 0 0 0-.713.287A.968.968 0 0 0 6 13c0 .283.096.52.287.713.192.191.43.287.713.287Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<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>
|
||||
</header>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
|
|
@ -11,7 +11,6 @@ import { fireEvent, render, screen, waitFor, within } from "jest-matrix-react";
|
|||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import { SDKContext, SdkContextClass } from "../../../../../src/contexts/SDKContext";
|
||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||
import { UIFeature } from "../../../../../src/settings/UIFeature";
|
||||
import {
|
||||
|
@ -35,13 +34,9 @@ describe("SetIntegrationManager", () => {
|
|||
deleteThreePid: jest.fn(),
|
||||
});
|
||||
|
||||
let stores!: SdkContextClass;
|
||||
|
||||
const getComponent = () => (
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<SDKContext.Provider value={stores}>
|
||||
<SetIntegrationManager />
|
||||
</SDKContext.Provider>
|
||||
<SetIntegrationManager />
|
||||
</MatrixClientContext.Provider>
|
||||
);
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
14
yarn.lock
|
@ -8595,9 +8595,9 @@ murmurhash-js@^1.0.0:
|
|||
integrity sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==
|
||||
|
||||
nanoid@^3.3.7:
|
||||
version "3.3.7"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
|
||||
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
|
||||
version "3.3.8"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
|
||||
integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==
|
||||
|
||||
natural-compare@^1.4.0:
|
||||
version "1.4.0"
|
||||
|
@ -11539,10 +11539,10 @@ typed-array-length@^1.0.6:
|
|||
possible-typed-array-names "^1.0.0"
|
||||
reflect.getprototypeof "^1.0.6"
|
||||
|
||||
typescript@5.6.3:
|
||||
version "5.6.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.3.tgz#5f3449e31c9d94febb17de03cc081dd56d81db5b"
|
||||
integrity sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==
|
||||
typescript@5.7.2:
|
||||
version "5.7.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6"
|
||||
integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==
|
||||
|
||||
ua-parser-js@^1.0.2:
|
||||
version "1.0.39"
|
||||
|
|