Merge remote-tracking branch 'origin/t3chguy/types-account-data' into t3chguy/types-account-data

This commit is contained in:
Michael Telatynski 2024-12-19 16:30:28 +00:00
commit 2806dbf88e
No known key found for this signature in database
GPG Key ID: A2B008A5F49F5D0D
62 changed files with 1145 additions and 1188 deletions

View File

@ -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"

View File

@ -1,3 +1,38 @@
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
* Allow trusted Element Call widget to send and receive media encryption key to-device messages ([#28316](https://github.com/element-hq/element-web/pull/28316)). Contributed by @hughns.
* increase ringing timeout from 10 seconds to 90 seconds ([#28630](https://github.com/element-hq/element-web/pull/28630)). Contributed by @fkwp.
* Add `Close` tooltip to dialog ([#28617](https://github.com/element-hq/element-web/pull/28617)). Contributed by @florianduros.
* New UX for Share dialog ([#28598](https://github.com/element-hq/element-web/pull/28598)). Contributed by @florianduros.
* Improve performance of RoomContext in RoomHeader ([#28574](https://github.com/element-hq/element-web/pull/28574)). Contributed by @t3chguy.
* Remove `Features.RustCrypto` flag ([#28582](https://github.com/element-hq/element-web/pull/28582)). Contributed by @florianduros.
* Add Modernizr warning when running in non-secure context ([#28581](https://github.com/element-hq/element-web/pull/28581)). Contributed by @t3chguy.
## 🐛 Bug Fixes
* Fix jumpy timeline when the pinned message banner is displayed ([#28654](https://github.com/element-hq/element-web/pull/28654)). Contributed by @florianduros.
* Fix font \& spaces in settings subsection ([#28631](https://github.com/element-hq/element-web/pull/28631)). Contributed by @florianduros.
* Remove manual device verification which is not supported by the new cryptography stack ([#28588](https://github.com/element-hq/element-web/pull/28588)). Contributed by @florianduros.
* Fix code block highlighting not working reliably with many code blocks ([#28613](https://github.com/element-hq/element-web/pull/28613)). Contributed by @t3chguy.
* Remove remaining reply fallbacks code ([#28610](https://github.com/element-hq/element-web/pull/28610)). Contributed by @t3chguy.
* Provide a way to activate GIFs via the keyboard for a11y ([#28611](https://github.com/element-hq/element-web/pull/28611)). Contributed by @t3chguy.
* Fix format bar position ([#28591](https://github.com/element-hq/element-web/pull/28591)). Contributed by @florianduros.
* Fix room taking long time to load ([#28579](https://github.com/element-hq/element-web/pull/28579)). Contributed by @florianduros.
* Show the correct shield status in tooltip for more conditions ([#28476](https://github.com/element-hq/element-web/pull/28476)). Contributed by @uhoreg.
Changes in [1.11.87](https://github.com/element-hq/element-web/releases/tag/v1.11.87) (2024-12-03)
==================================================================================================
## ✨ Features

View File

@ -1,6 +1,6 @@
{
"name": "element-web",
"version": "1.11.87",
"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",

View File

@ -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",

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 { 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");
@ -50,8 +50,6 @@ test.describe("Dehydration", () => {
});
test("Create dehydrated device", async ({ page, user, app }, workerInfo) => {
test.skip(workerInfo.project.name === "Legacy Crypto", "This test only works with Rust crypto.");
// Create a backup (which will create SSSS, and dehydrated device)
const securityTab = await app.settings.openUserSettings("Security & Privacy");

View File

@ -133,8 +133,7 @@ test.describe("Cryptography", function () {
"Encrypted by a device not verified by its owner.",
);
/* In legacy crypto: should show a grey padlock for a message from a deleted device.
* In rust crypto: should show a red padlock for a message from an unverified device.
/* Should show a red padlock for a message from an unverified device.
* Rust crypto remembers the verification state of the sending device, so it will know that the device was
* unverified, even if it gets deleted. */
// bob deletes his second device
@ -168,9 +167,7 @@ test.describe("Cryptography", function () {
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
await lastE2eIcon.focus();
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
workerInfo.project.name === "Legacy Crypto"
? "Encrypted by an unknown or deleted device."
: "Encrypted by a device not verified by its owner.",
"Encrypted by a device not verified by its owner.",
);
});

View File

@ -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) => {
@ -29,7 +29,6 @@ test.describe("migration", function () {
test.use({ displayName: "Alice" });
test("Should support migration from legacy crypto", async ({ context, user, page }, workerInfo) => {
test.skip(workerInfo.project.name === "Legacy Crypto", "This test only works with Rust crypto.");
test.slow();
// We should see a migration progress bar

View File

@ -220,11 +220,7 @@ export async function doTwoWaySasVerification(page: Page, verifier: JSHandle<Ver
for (let i = 0; i < emojis.length; i++) {
const emoji = emojis[i];
const emojiBlock = emojiBlocks.nth(i);
const textContent = await emojiBlock.textContent();
// VerificationShowSas munges the case of the emoji descriptions returned by the js-sdk before
// displaying them. Once we drop support for legacy crypto, that code can go away, and so can the
// case-munging here.
expect(textContent.toLowerCase()).toEqual(emoji[0] + emoji[1].toLowerCase());
await expect(emojiBlock).toHaveText(emoji[0] + emoji[1]);
}
}

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
// 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()) {

View File

@ -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) => {

View File

@ -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);

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

@ -7,59 +7,25 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { logger } from "matrix-js-sdk/src/logger";
import { AuthDict, CrossSigningKeys, MatrixClient, MatrixError, UIAFlow, UIAResponse } from "matrix-js-sdk/src/matrix";
import { AuthDict, MatrixClient, MatrixError, UIAResponse } from "matrix-js-sdk/src/matrix";
import { SSOAuthEntry } from "./components/views/auth/InteractiveAuthEntryComponents";
import Modal from "./Modal";
import { _t } from "./languageHandler";
import InteractiveAuthDialog from "./components/views/dialogs/InteractiveAuthDialog";
/**
* Determine if the homeserver allows uploading device keys with only password auth.
* @param cli The Matrix Client to use
* @returns True if the homeserver allows uploading device keys with only password auth, 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;
} catch (error) {
if (!(error instanceof MatrixError) || !error.data || !error.data.flows) {
logger.log("uploadDeviceSigningKeys advertised no flows!");
return false;
}
const canUploadKeysWithPasswordOnly = error.data.flows.some((f: UIAFlow) => {
return f.stages.length === 1 && f.stages[0] === "m.login.password";
});
return canUploadKeysWithPasswordOnly;
}
}
/**
* Ensures that cross signing keys are created and uploaded for the user.
* The homeserver may require user-interactive auth to upload the keys, in
* which case the user will be prompted to authenticate. If the homeserver
* allows uploading keys with just an account password and one is provided,
* the keys will be uploaded without user interaction.
* which case the user will be prompted to authenticate.
*
* This function does not set up backups of the created cross-signing keys
* (or message keys): the cross-signing keys are stored locally and will be
* lost requiring a crypto reset, if the user logs out or loses their session.
*
* @param cli The Matrix Client to use
* @param isTokenLogin True if the user logged in via a token login, otherwise false
* @param accountPassword The password that the user logged in with
*/
export async function createCrossSigning(
cli: MatrixClient,
isTokenLogin: boolean,
accountPassword?: string,
): Promise<void> {
export async function createCrossSigning(cli: MatrixClient): Promise<void> {
const cryptoApi = cli.getCrypto();
if (!cryptoApi) {
throw new Error("No crypto API found!");
@ -68,19 +34,14 @@ export async function createCrossSigning(
const doBootstrapUIAuth = async (
makeRequest: (authData: AuthDict) => Promise<UIAResponse<void>>,
): Promise<void> => {
if (accountPassword && (await canUploadKeysWithPasswordOnly(cli))) {
await makeRequest({
type: "m.login.password",
identifier: {
type: "m.id.user",
user: cli.getUserId(),
},
password: accountPassword,
});
} else if (isTokenLogin) {
// We are hoping the grace period is active
try {
await makeRequest({});
} else {
} catch (error) {
if (!(error instanceof MatrixError) || !error.data || !error.data.flows) {
// Not a UIA response
throw error;
}
const dialogAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: {
title: _t("auth|uia|sso_title"),

View File

@ -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);
}
}
}
}

View File

@ -191,8 +191,6 @@ export interface AccessSecretStorageOpts {
forceReset?: boolean;
/** Create new cross-signing keys. Only applicable if `forceReset` is `true`. */
resetCrossSigning?: boolean;
/** The cached account password, if available. */
accountPassword?: string;
}
/**

View File

@ -431,8 +431,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// if cross-signing is not yet set up, do so now if possible.
InitialCryptoSetupStore.sharedInstance().startInitialCryptoSetup(
cli,
Boolean(this.tokenLogin),
this.stores,
this.onCompleteSecurityE2eSetupFinished,
);
this.setStateForNewView({ view: Views.E2E_SETUP });
@ -504,8 +502,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
UIStore.destroy();
this.state.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize);
window.removeEventListener("resize", this.onWindowResized);
this.stores.accountPasswordStore.clearPassword();
}
private onWindowResized = (): void => {
@ -1935,8 +1931,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.showScreen("forgot_password");
};
private onRegisterFlowComplete = (credentials: IMatrixClientCreds, password: string): Promise<void> => {
return this.onUserCompletedLoginFlow(credentials, password);
private onRegisterFlowComplete = (credentials: IMatrixClientCreds): Promise<void> => {
return this.onUserCompletedLoginFlow(credentials);
};
// returns a promise which resolves to the new MatrixClient
@ -2003,9 +1999,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
* Note: SSO users (and any others using token login) currently do not pass through
* this, as they instead jump straight into the app after `attemptTokenLogin`.
*/
private onUserCompletedLoginFlow = async (credentials: IMatrixClientCreds, password: string): Promise<void> => {
this.stores.accountPasswordStore.setPassword(password);
private onUserCompletedLoginFlow = async (credentials: IMatrixClientCreds): Promise<void> => {
// Create and start the client
await Lifecycle.setLoggedIn(credentials);
await this.postLoginSetup();

View File

@ -48,10 +48,7 @@ interface IProps {
// Called when the user has logged in. Params:
// - The object returned by the login API
// - The user's password, if applicable, (may be cached in memory for a
// short time so the user is not required to re-enter their password
// for operations like uploading cross-signing keys).
onLoggedIn(data: IMatrixClientCreds, password: string): void;
onLoggedIn(data: IMatrixClientCreds): void;
// login shouldn't know or care how registration, password recovery, etc is done.
onRegisterClick(): void;
@ -199,7 +196,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
this.loginLogic.loginViaPassword(username, phoneCountry, phoneNumber, password).then(
(data) => {
this.setState({ serverIsAlive: true }); // it must be, we logged in.
this.props.onLoggedIn(data, password);
this.props.onLoggedIn(data);
},
(error) => {
if (this.unmounted) return;

View File

@ -72,10 +72,7 @@ interface IProps {
mobileRegister?: boolean;
// Called when the user has logged in. Params:
// - object with userId, deviceId, homeserverUrl, identityServerUrl, accessToken
// - The user's password, if available and applicable (may be cached in memory
// for a short time so the user is not required to re-enter their password
// for operations like uploading cross-signing keys).
onLoggedIn(params: IMatrixClientCreds, password: string): Promise<void>;
onLoggedIn(params: IMatrixClientCreds): Promise<void>;
// registration shouldn't know or care how login is done.
onLoginClick(): void;
onServerConfigChange(config: ValidatedServerConfig): void;
@ -431,16 +428,13 @@ export default class Registration extends React.Component<IProps, IState> {
newState.busy = false;
newState.completedNoSignin = true;
} else {
await this.props.onLoggedIn(
{
userId,
deviceId: (response as RegisterResponse).device_id!,
homeserverUrl: this.state.matrixClient.getHomeserverUrl(),
identityServerUrl: this.state.matrixClient.getIdentityServerUrl(),
accessToken,
},
this.state.formVals.password!,
);
await this.props.onLoggedIn({
userId,
deviceId: (response as RegisterResponse).device_id!,
homeserverUrl: this.state.matrixClient.getHomeserverUrl(),
identityServerUrl: this.state.matrixClient.getIdentityServerUrl(),
accessToken,
});
this.setupPushers();
}

View File

@ -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();
}
}
}

View File

@ -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();
}
}

View File

@ -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

View File

@ -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}
</>
);
}

View File

@ -13,7 +13,6 @@ import defaultDispatcher from "../dispatcher/dispatcher";
import LegacyCallHandler from "../LegacyCallHandler";
import { PosthogAnalytics } from "../PosthogAnalytics";
import { SlidingSyncManager } from "../SlidingSyncManager";
import { AccountPasswordStore } from "../stores/AccountPasswordStore";
import { MemberListStore } from "../stores/MemberListStore";
import { RoomNotificationStateStore } from "../stores/notifications/RoomNotificationStateStore";
import RightPanelStore from "../stores/right-panel/RightPanelStore";
@ -63,7 +62,6 @@ export class SdkContextClass {
protected _SpaceStore?: SpaceStoreClass;
protected _LegacyCallHandler?: LegacyCallHandler;
protected _TypingStore?: TypingStore;
protected _AccountPasswordStore?: AccountPasswordStore;
protected _UserProfilesStore?: UserProfilesStore;
protected _OidcClientStore?: OidcClientStore;
@ -149,13 +147,6 @@ export class SdkContextClass {
return this._TypingStore;
}
public get accountPasswordStore(): AccountPasswordStore {
if (!this._AccountPasswordStore) {
this._AccountPasswordStore = new AccountPasswordStore();
}
return this._AccountPasswordStore;
}
public get userProfilesStore(): UserProfilesStore {
if (!this.client) {
throw new Error("Unable to create UserProfilesStore without a client");

View File

@ -914,6 +914,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": {

View File

@ -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"
}
}
},

View File

@ -1,35 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
const PASSWORD_TIMEOUT = 5 * 60 * 1000; // five minutes
/**
* Store for the account password.
* This password can be used for a short time after login
* to avoid requestin the password all the time for instance during e2ee setup.
*/
export class AccountPasswordStore {
private password?: string;
private passwordTimeoutId?: ReturnType<typeof setTimeout>;
public setPassword(password: string): void {
this.password = password;
clearTimeout(this.passwordTimeoutId);
this.passwordTimeoutId = setTimeout(this.clearPassword, PASSWORD_TIMEOUT);
}
public getPassword(): string | undefined {
return this.password;
}
public clearPassword = (): void => {
clearTimeout(this.passwordTimeoutId);
this.passwordTimeoutId = undefined;
this.password = undefined;
};
}

View File

@ -11,7 +11,6 @@ import { logger } from "matrix-js-sdk/src/logger";
import { useEffect, useState } from "react";
import { createCrossSigning } from "../CreateCrossSigning";
import { SdkContextClass } from "../contexts/SDKContext";
type Status = "in_progress" | "complete" | "error" | undefined;
@ -45,8 +44,6 @@ export class InitialCryptoSetupStore extends EventEmitter {
private status: Status = undefined;
private client?: MatrixClient;
private isTokenLogin?: boolean;
private stores?: SdkContextClass;
private onFinished?: (success: boolean) => void;
public static sharedInstance(): InitialCryptoSetupStore {
@ -62,18 +59,9 @@ export class InitialCryptoSetupStore extends EventEmitter {
* Start the initial crypto setup process.
*
* @param {MatrixClient} client The client to use for the setup
* @param {boolean} isTokenLogin True if the user logged in via a token login, otherwise false
* @param {SdkContextClass} stores The stores to use for the setup
*/
public startInitialCryptoSetup(
client: MatrixClient,
isTokenLogin: boolean,
stores: SdkContextClass,
onFinished: (success: boolean) => void,
): void {
public startInitialCryptoSetup(client: MatrixClient, onFinished: (success: boolean) => void): void {
this.client = client;
this.isTokenLogin = isTokenLogin;
this.stores = stores;
this.onFinished = onFinished;
// We just start this process: it's progress is tracked by the events rather
@ -89,7 +77,7 @@ export class InitialCryptoSetupStore extends EventEmitter {
* @returns {boolean} True if a retry was initiated, otherwise false
*/
public retry(): boolean {
if (this.client === undefined || this.isTokenLogin === undefined || this.stores == undefined) return false;
if (this.client === undefined) return false;
this.doSetup().catch(() => logger.error("Initial crypto setup failed"));
@ -98,12 +86,10 @@ export class InitialCryptoSetupStore extends EventEmitter {
private reset(): void {
this.client = undefined;
this.isTokenLogin = undefined;
this.stores = undefined;
}
private async doSetup(): Promise<void> {
if (this.client === undefined || this.isTokenLogin === undefined || this.stores == undefined) {
if (this.client === undefined) {
throw new Error("No setup is in progress");
}
@ -114,7 +100,14 @@ export class InitialCryptoSetupStore extends EventEmitter {
this.emit("update");
try {
await createCrossSigning(this.client, this.isTokenLogin, this.stores.accountPasswordStore.getPassword());
// Create the user's cross-signing keys
await createCrossSigning(this.client);
// 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();
@ -122,16 +115,6 @@ export class InitialCryptoSetupStore extends EventEmitter {
this.emit("update");
this.onFinished?.(true);
} catch (e) {
if (this.isTokenLogin) {
// ignore any failures, we are relying on grace period here
this.reset();
this.status = "complete";
this.emit("update");
this.onFinished?.(true);
return;
}
logger.error("Error bootstrapping cross-signing", e);
this.status = "error";
this.emit("update");

View File

@ -19,7 +19,6 @@ import { Device, SecretStorage } from "matrix-js-sdk/src/matrix";
import { MatrixClientPeg } from "../MatrixClientPeg";
import { AccessCancelledError, accessSecretStorage } from "../SecurityManager";
import { SdkContextClass } from "../contexts/SDKContext";
import { asyncSome } from "../utils/arrays";
import { initialiseDehydration } from "../utils/device/dehydration";
@ -239,7 +238,6 @@ export class SetupEncryptionStore extends EventEmitter {
{
forceReset: true,
resetCrossSigning: true,
accountPassword: SdkContextClass.instance.accountPasswordStore.getPassword(),
},
);
} catch (e) {

View File

@ -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);
};

View File

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix";
import { HTTPError, MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix";
import { mocked } from "jest-mock";
import { createCrossSigning } from "../src/CreateCrossSigning";
@ -21,14 +21,14 @@ describe("CreateCrossSigning", () => {
});
it("should call bootstrapCrossSigning with an authUploadDeviceSigningKeys function", async () => {
await createCrossSigning(client, false, "password");
await createCrossSigning(client);
expect(client.getCrypto()?.bootstrapCrossSigning).toHaveBeenCalledWith({
authUploadDeviceSigningKeys: expect.any(Function),
});
});
it("should upload with password auth if possible", async () => {
it("should upload", async () => {
client.uploadDeviceSigningKeys = jest.fn().mockRejectedValueOnce(
new MatrixError({
flows: [
@ -39,24 +39,7 @@ describe("CreateCrossSigning", () => {
}),
);
await createCrossSigning(client, false, "password");
const { authUploadDeviceSigningKeys } = mocked(client.getCrypto()!).bootstrapCrossSigning.mock.calls[0][0];
const makeRequest = jest.fn();
await authUploadDeviceSigningKeys!(makeRequest);
expect(makeRequest).toHaveBeenCalledWith({
type: "m.login.password",
identifier: {
type: "m.id.user",
user: client.getUserId(),
},
password: "password",
});
});
it("should attempt to upload keys without auth if using token login", async () => {
await createCrossSigning(client, true, undefined);
await createCrossSigning(client);
const { authUploadDeviceSigningKeys } = mocked(client.getCrypto()!).bootstrapCrossSigning.mock.calls[0][0];
@ -65,7 +48,7 @@ describe("CreateCrossSigning", () => {
expect(makeRequest).toHaveBeenCalledWith({});
});
it("should prompt user if password upload not possible", async () => {
it("should prompt user if upload failed with UIA", async () => {
const createDialog = jest.spyOn(Modal, "createDialog").mockReturnValue({
finished: Promise.resolve([true]),
close: jest.fn(),
@ -81,13 +64,32 @@ describe("CreateCrossSigning", () => {
}),
);
await createCrossSigning(client, false, "password");
await createCrossSigning(client);
const { authUploadDeviceSigningKeys } = mocked(client.getCrypto()!).bootstrapCrossSigning.mock.calls[0][0];
const makeRequest = jest.fn();
const makeRequest = jest.fn().mockRejectedValue(
new MatrixError({
flows: [
{
stages: ["dummy.mystery_flow_nobody_knows"],
},
],
}),
);
await authUploadDeviceSigningKeys!(makeRequest);
expect(makeRequest).not.toHaveBeenCalledWith();
expect(createDialog).toHaveBeenCalled();
});
it("should throw error if server fails with something other than UIA", async () => {
await createCrossSigning(client);
const { authUploadDeviceSigningKeys } = mocked(client.getCrypto()!).bootstrapCrossSigning.mock.calls[0][0];
const error = new HTTPError("Internal Server Error", 500);
const makeRequest = jest.fn().mockRejectedValue(error);
await expect(authUploadDeviceSigningKeys!(makeRequest)).rejects.toThrow(error);
expect(makeRequest).not.toHaveBeenCalledWith();
});
});

View File

@ -134,6 +134,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),
}),

View File

@ -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,
);
});
});

View File

@ -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);
});

View File

@ -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();

View File

@ -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();
});
});
});

View File

@ -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>
`;

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 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>
);

View File

@ -1,53 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { AccountPasswordStore } from "../../../src/stores/AccountPasswordStore";
jest.useFakeTimers();
describe("AccountPasswordStore", () => {
let accountPasswordStore: AccountPasswordStore;
beforeEach(() => {
accountPasswordStore = new AccountPasswordStore();
});
it("should not have a password by default", () => {
expect(accountPasswordStore.getPassword()).toBeUndefined();
});
describe("when setting a password", () => {
beforeEach(() => {
accountPasswordStore.setPassword("pass1");
});
it("should return the password", () => {
expect(accountPasswordStore.getPassword()).toBe("pass1");
});
describe("and the password timeout exceed", () => {
beforeEach(() => {
jest.advanceTimersToNextTimer();
});
it("should clear the password", () => {
expect(accountPasswordStore.getPassword()).toBeUndefined();
});
});
describe("and setting another password", () => {
beforeEach(() => {
accountPasswordStore.setPassword("pass2");
});
it("should return the other password", () => {
expect(accountPasswordStore.getPassword()).toBe("pass2");
});
});
});
});

View File

@ -8,12 +8,11 @@ Please see LICENSE files in the repository root for full details.
import { mocked } from "jest-mock";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { waitFor } from "jest-matrix-react";
import { sleep } from "matrix-js-sdk/src/utils";
import { createCrossSigning } from "../../../src/CreateCrossSigning";
import { InitialCryptoSetupStore } from "../../../src/stores/InitialCryptoSetupStore";
import { SdkContextClass } from "../../../src/contexts/SDKContext";
import { createTestClient } from "../../test-utils";
import { AccountPasswordStore } from "../../../src/stores/AccountPasswordStore";
jest.mock("../../../src/CreateCrossSigning", () => ({
createCrossSigning: jest.fn(),
@ -22,7 +21,6 @@ jest.mock("../../../src/CreateCrossSigning", () => ({
describe("InitialCryptoSetupStore", () => {
let testStore: InitialCryptoSetupStore;
let client: MatrixClient;
let stores: SdkContextClass;
let createCrossSigningResolve: () => void;
let createCrossSigningReject: (e: Error) => void;
@ -30,11 +28,6 @@ describe("InitialCryptoSetupStore", () => {
beforeEach(() => {
testStore = new InitialCryptoSetupStore();
client = createTestClient();
stores = {
accountPasswordStore: {
getPassword: jest.fn(),
} as unknown as AccountPasswordStore,
} as unknown as SdkContextClass;
mocked(createCrossSigning).mockImplementation(() => {
return new Promise<void>((resolve, reject) => {
@ -45,7 +38,7 @@ describe("InitialCryptoSetupStore", () => {
});
it("should call createCrossSigning when startInitialCryptoSetup is called", async () => {
testStore.startInitialCryptoSetup(client, false, stores, jest.fn());
testStore.startInitialCryptoSetup(client, jest.fn());
await waitFor(() => expect(createCrossSigning).toHaveBeenCalled());
});
@ -54,7 +47,7 @@ describe("InitialCryptoSetupStore", () => {
const updateSpy = jest.fn();
testStore.on("update", updateSpy);
testStore.startInitialCryptoSetup(client, false, stores, jest.fn());
testStore.startInitialCryptoSetup(client, jest.fn());
createCrossSigningResolve();
await waitFor(() => expect(updateSpy).toHaveBeenCalled());
@ -65,21 +58,28 @@ describe("InitialCryptoSetupStore", () => {
const updateSpy = jest.fn();
testStore.on("update", updateSpy);
testStore.startInitialCryptoSetup(client, false, stores, jest.fn());
testStore.startInitialCryptoSetup(client, jest.fn());
createCrossSigningReject(new Error("Test error"));
await waitFor(() => expect(updateSpy).toHaveBeenCalled());
expect(testStore.getStatus()).toBe("error");
});
it("should ignore failures if tokenLogin is true", async () => {
const updateSpy = jest.fn();
testStore.on("update", updateSpy);
it("should fail to retry once complete", async () => {
testStore.startInitialCryptoSetup(client, jest.fn());
testStore.startInitialCryptoSetup(client, true, stores, jest.fn());
await waitFor(() => expect(createCrossSigning).toHaveBeenCalled());
createCrossSigningResolve();
await sleep(0); // await the next tick
expect(testStore.retry()).toBeFalsy();
});
it("should retry if initial attempt failed", async () => {
testStore.startInitialCryptoSetup(client, jest.fn());
await waitFor(() => expect(createCrossSigning).toHaveBeenCalled());
createCrossSigningReject(new Error("Test error"));
await waitFor(() => expect(updateSpy).toHaveBeenCalled());
expect(testStore.getStatus()).toBe("complete");
await sleep(0); // await the next tick
expect(testStore.retry()).toBeTruthy();
});
});

View File

@ -11,7 +11,6 @@ import { MatrixClient, Device } from "matrix-js-sdk/src/matrix";
import { SecretStorageKeyDescriptionAesV1, ServerSideSecretStorage } from "matrix-js-sdk/src/secret-storage";
import { BootstrapCrossSigningOpts, CryptoApi, DeviceVerificationStatus } from "matrix-js-sdk/src/crypto-api";
import { SdkContextClass } from "../../../src/contexts/SDKContext";
import { accessSecretStorage } from "../../../src/SecurityManager";
import { SetupEncryptionStore } from "../../../src/stores/SetupEncryptionStore";
import { emitPromise, stubClient } from "../../test-utils";
@ -21,7 +20,6 @@ jest.mock("../../../src/SecurityManager", () => ({
}));
describe("SetupEncryptionStore", () => {
const cachedPassword = "p4assword";
let client: Mocked<MatrixClient>;
let mockCrypto: Mocked<CryptoApi>;
let mockSecretStorage: Mocked<ServerSideSecretStorage>;
@ -47,11 +45,6 @@ describe("SetupEncryptionStore", () => {
Object.defineProperty(client, "secretStorage", { value: mockSecretStorage });
setupEncryptionStore = new SetupEncryptionStore();
SdkContextClass.instance.accountPasswordStore.setPassword(cachedPassword);
});
afterEach(() => {
SdkContextClass.instance.accountPasswordStore.clearPassword();
});
describe("start", () => {
@ -172,7 +165,6 @@ describe("SetupEncryptionStore", () => {
await setupEncryptionStore.resetConfirm();
expect(mocked(accessSecretStorage)).toHaveBeenCalledWith(expect.any(Function), {
accountPassword: cachedPassword,
forceReset: true,
resetCrossSigning: true,
});

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

@ -2019,10 +2019,10 @@
emojibase "^15.3.1"
emojibase-data "^15.3.1"
"@matrix-org/matrix-sdk-crypto-wasm@^9.0.0":
version "9.1.0"
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-9.1.0.tgz#f889653eb4fafaad2a963654d586bd34de62acd5"
integrity sha512-CtPoNcoRW6ehwxpRQAksG3tR+NJ7k4DV02nMFYTDwQtie1V4R8OTY77BjEIs97NOblhtS26jU8m1lWsOBEz0Og==
"@matrix-org/matrix-sdk-crypto-wasm@^12.0.0":
version "12.0.0"
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-12.0.0.tgz#e3a5150ccbb21d5e98ee3882e7057b9f17fb962a"
integrity sha512-nkkXAxUIk9UTso4TbU6Bgqsv/rJShXQXRx0ti/W+AWXHJ2HoH4sL5LsXkc7a8yYGn8tyXqxGPsYA1UeHqLwm0Q==
"@matrix-org/olm@3.2.15":
version "3.2.15"
@ -8308,11 +8308,11 @@ matrix-events-sdk@0.0.1:
integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
version "34.13.0"
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/c4ea57d42dcf8bd04c40feaa2c686487dbcab338"
version "35.0.0"
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/349a86c119e6ac53d8d0677f6b6db5944c3ddcd1"
dependencies:
"@babel/runtime" "^7.12.5"
"@matrix-org/matrix-sdk-crypto-wasm" "^9.0.0"
"@matrix-org/matrix-sdk-crypto-wasm" "^12.0.0"
"@matrix-org/olm" "3.2.15"
another-json "^0.2.0"
bs58 "^6.0.0"
@ -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"