From e67142d589038497b965d1cd774d3eb702a23b42 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Fri, 18 Oct 2024 16:20:04 +0200 Subject: [PATCH 01/12] Replace `MatrixClient.isRoomEncrypted` by `MatrixClient.CryptoApi.isEncryptionEnabledInRoom` in `DeviceListener.ts` --- src/DeviceListener.ts | 10 +++++++--- test/unit-tests/DeviceListener-test.ts | 6 +++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index ea812d7379f..4f56fc2a92d 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -46,6 +46,7 @@ import SettingsStore, { CallbackFn } from "./settings/SettingsStore"; import { UIFeature } from "./settings/UIFeature"; import { isBulkUnverifiedDeviceReminderSnoozed } from "./utils/device/snoozeBulkUnverifiedDeviceReminder"; import { getUserDeviceIds } from "./utils/crypto/deviceInfo"; +import { asyncSome } from "./utils/arrays.ts"; const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000; @@ -244,13 +245,16 @@ export default class DeviceListener { return this.keyBackupInfo; } - private shouldShowSetupEncryptionToast(): boolean { + private shouldShowSetupEncryptionToast(): Promise | boolean { // If we're in the middle of a secret storage operation, we're likely // modifying the state involved here, so don't add new toasts to setup. if (isSecretStorageBeingAccessed()) return false; // Show setup toasts once the user is in at least one encrypted room. const cli = this.client; - return cli?.getRooms().some((r) => cli.isRoomEncrypted(r.roomId)) ?? false; + const cryptoApi = cli?.getCrypto(); + if (!cli || !cryptoApi) return false; + + return asyncSome(cli.getRooms(), ({ roomId }) => cryptoApi.isEncryptionEnabledInRoom(roomId)); } private recheck(): void { @@ -287,7 +291,7 @@ export default class DeviceListener { hideSetupEncryptionToast(); this.checkKeyBackupStatus(); - } else if (this.shouldShowSetupEncryptionToast()) { + } else if (await this.shouldShowSetupEncryptionToast()) { // make sure our keys are finished downloading await crypto.getUserDeviceInfo([cli.getSafeUserId()]); diff --git a/test/unit-tests/DeviceListener-test.ts b/test/unit-tests/DeviceListener-test.ts index 64761d7da10..5960f5e94d7 100644 --- a/test/unit-tests/DeviceListener-test.ts +++ b/test/unit-tests/DeviceListener-test.ts @@ -94,6 +94,7 @@ describe("DeviceListener", () => { }, }), getSessionBackupPrivateKey: jest.fn(), + isEncryptionEnabledInRoom: jest.fn(), } as unknown as Mocked; mockClient = getMockClientWithEventEmitter({ isGuest: jest.fn(), @@ -104,7 +105,6 @@ describe("DeviceListener", () => { isVersionSupported: jest.fn().mockResolvedValue(true), isInitialSyncComplete: jest.fn().mockReturnValue(true), waitForClientWellKnown: jest.fn(), - isRoomEncrypted: jest.fn(), getClientWellKnown: jest.fn(), getDeviceId: jest.fn().mockReturnValue(deviceId), setAccountData: jest.fn(), @@ -291,7 +291,7 @@ describe("DeviceListener", () => { mockCrypto!.isCrossSigningReady.mockResolvedValue(false); mockCrypto!.isSecretStorageReady.mockResolvedValue(false); mockClient!.getRooms.mockReturnValue(rooms); - mockClient!.isRoomEncrypted.mockReturnValue(true); + jest.spyOn(mockClient.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true); }); it("hides setup encryption toast when cross signing and secret storage are ready", async () => { @@ -316,7 +316,7 @@ describe("DeviceListener", () => { }); it("does not show any toasts when no rooms are encrypted", async () => { - mockClient!.isRoomEncrypted.mockReturnValue(false); + jest.spyOn(mockClient.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(false); await createAndStart(); expect(SetupEncryptionToast.showToast).not.toHaveBeenCalled(); From 445aa21d5196e1cf45068e2c2510cd90fa88eeea Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Fri, 18 Oct 2024 16:39:34 +0200 Subject: [PATCH 02/12] Replace `MatrixClient.isRoomEncrypted` by `MatrixClient.CryptoApi.isEncryptionEnabledInRoom` in `Searching.ts` --- src/Searching.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Searching.ts b/src/Searching.ts index 85483eb23ca..b14078fc83d 100644 --- a/src/Searching.ts +++ b/src/Searching.ts @@ -605,7 +605,7 @@ function eventIndexSearch( let searchPromise: Promise; if (roomId !== undefined) { - if (client.isRoomEncrypted(roomId)) { + if (client.getCrypto()?.isEncryptionEnabledInRoom(roomId)) { // The search is for a single encrypted room, use our local // search method. searchPromise = localSearchProcess(client, term, roomId); From 4f07a3029aa1772a3bf09c9fe81ad1b9a56042e1 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Fri, 18 Oct 2024 17:05:13 +0200 Subject: [PATCH 03/12] Replace `MatrixClient.isRoomEncrypted` by `MatrixClient.CryptoApi.isEncryptionEnabledInRoom` in `SlidingSyncManager.ts` --- src/SlidingSyncManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SlidingSyncManager.ts b/src/SlidingSyncManager.ts index eeac10b6034..8c0cb3e4434 100644 --- a/src/SlidingSyncManager.ts +++ b/src/SlidingSyncManager.ts @@ -233,7 +233,7 @@ export class SlidingSyncManager { subscriptions.delete(roomId); } const room = this.client?.getRoom(roomId); - let shouldLazyLoad = !this.client?.isRoomEncrypted(roomId); + let shouldLazyLoad = !(await this.client?.getCrypto()?.isEncryptionEnabledInRoom(roomId)); if (!room) { // default to safety: request all state if we can't work it out. This can happen if you // refresh the app whilst viewing a room: we call setRoomVisible before we know anything From a57f9e2e776430b8f267e6a7a1a2b0d1c1471f0d Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 13 Nov 2024 16:36:35 +0100 Subject: [PATCH 04/12] Replace `MatrixClient.isRoomEncrypted` by `MatrixClient.CryptoApi.isEncryptionEnabledInRoom` in `EncryptionEvent.tsx` --- .../views/messages/EncryptionEvent.tsx | 10 ++--- .../components/structures/RoomView-test.tsx | 11 +++++- .../views/messages/EncryptionEvent-test.tsx | 37 +++++++++++-------- 3 files changed, 36 insertions(+), 22 deletions(-) diff --git a/src/components/views/messages/EncryptionEvent.tsx b/src/components/views/messages/EncryptionEvent.tsx index e721662cb5d..bc6680d3000 100644 --- a/src/components/views/messages/EncryptionEvent.tsx +++ b/src/components/views/messages/EncryptionEvent.tsx @@ -6,18 +6,18 @@ 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, { forwardRef, useContext } from "react"; +import React, { forwardRef } from "react"; import { MatrixEvent } from "matrix-js-sdk/src/matrix"; import type { RoomEncryptionEventContent } from "matrix-js-sdk/src/types"; import { _t } from "../../../languageHandler"; -import { MatrixClientPeg } from "../../../MatrixClientPeg"; import EventTileBubble from "./EventTileBubble"; -import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; import DMRoomMap from "../../../utils/DMRoomMap"; import { objectHasDiff } from "../../../utils/objects"; import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom"; import { MEGOLM_ENCRYPTION_ALGORITHM } from "../../../utils/crypto"; +import { useIsEncrypted } from "../../../hooks/useIsEncrypted.ts"; interface IProps { mxEvent: MatrixEvent; @@ -25,9 +25,9 @@ interface IProps { } const EncryptionEvent = forwardRef(({ mxEvent, timestamp }, ref) => { - const cli = useContext(MatrixClientContext); + const cli = useMatrixClientContext(); const roomId = mxEvent.getRoomId()!; - const isRoomEncrypted = MatrixClientPeg.safeGet().isRoomEncrypted(roomId); + const isRoomEncrypted = useIsEncrypted(cli, cli.getRoom(roomId) || undefined); const prevContent = mxEvent.getPrevContent() as RoomEncryptionEventContent; const content = mxEvent.getContent(); diff --git a/test/unit-tests/components/structures/RoomView-test.tsx b/test/unit-tests/components/structures/RoomView-test.tsx index 02bed8cf4fc..f30db3d80e6 100644 --- a/test/unit-tests/components/structures/RoomView-test.tsx +++ b/test/unit-tests/components/structures/RoomView-test.tsx @@ -21,6 +21,7 @@ import { SearchResult, IEvent, } from "matrix-js-sdk/src/matrix"; +import { CryptoApi, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api"; import { KnownMembership } from "matrix-js-sdk/src/types"; import { fireEvent, render, screen, RenderResult, waitForElementToBeRemoved, waitFor } from "jest-matrix-react"; import userEvent from "@testing-library/user-event"; @@ -72,6 +73,7 @@ describe("RoomView", () => { let rooms: Map; let roomCount = 0; let stores: SdkContextClass; + let crypto: CryptoApi; // mute some noise filterConsole("RVS update", "does not have an m.room.create event", "Current version: 1", "Version capability"); @@ -97,6 +99,7 @@ describe("RoomView", () => { stores.rightPanelStore.useUnitTestClient(cli); jest.spyOn(VoipUserMapper.sharedInstance(), "getVirtualRoomForRoom").mockResolvedValue(undefined); + crypto = cli.getCrypto()!; jest.spyOn(cli, "getCrypto").mockReturnValue(undefined); }); @@ -341,7 +344,13 @@ describe("RoomView", () => { describe("that is encrypted", () => { beforeEach(() => { + // Not all the calls to cli.isRoomEncrypted are migrated, so we need to mock both. mocked(cli.isRoomEncrypted).mockReturnValue(true); + jest.spyOn(cli, "getCrypto").mockReturnValue(crypto); + jest.spyOn(cli.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true); + jest.spyOn(cli.getCrypto()!, "getUserVerificationStatus").mockResolvedValue( + new UserVerificationStatus(false, true, false), + ); localRoom.encrypted = true; localRoom.currentState.setStateEvents([ new MatrixEvent({ @@ -360,7 +369,7 @@ describe("RoomView", () => { it("should match the snapshot", async () => { const { container } = await renderRoomView(); - expect(container).toMatchSnapshot(); + await waitFor(() => expect(container).toMatchSnapshot()); }); }); }); diff --git a/test/unit-tests/components/views/messages/EncryptionEvent-test.tsx b/test/unit-tests/components/views/messages/EncryptionEvent-test.tsx index 3a78ef55e80..ca5f3d04b9b 100644 --- a/test/unit-tests/components/views/messages/EncryptionEvent-test.tsx +++ b/test/unit-tests/components/views/messages/EncryptionEvent-test.tsx @@ -10,6 +10,7 @@ import React from "react"; import { mocked } from "jest-mock"; import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; import { render, screen } from "jest-matrix-react"; +import { waitFor } from "@testing-library/dom"; import EncryptionEvent from "../../../../../src/components/views/messages/EncryptionEvent"; import { createTestClient, mkMessage } from "../../../../test-utils"; @@ -55,17 +56,19 @@ describe("EncryptionEvent", () => { describe("for an encrypted room", () => { beforeEach(() => { event.event.content!.algorithm = algorithm; - mocked(client.isRoomEncrypted).mockReturnValue(true); + jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true); const room = new Room(roomId, client, client.getUserId()!); mocked(client.getRoom).mockReturnValue(room); }); - it("should show the expected texts", () => { + it("should show the expected texts", async () => { renderEncryptionEvent(client, event); - checkTexts( - "Encryption enabled", - "Messages in this room are end-to-end encrypted. " + - "When people join, you can verify them in their profile, just tap on their profile picture.", + await waitFor(() => + checkTexts( + "Encryption enabled", + "Messages in this room are end-to-end encrypted. " + + "When people join, you can verify them in their profile, just tap on their profile picture.", + ), ); }); @@ -76,9 +79,9 @@ describe("EncryptionEvent", () => { }); }); - it("should show the expected texts", () => { + it("should show the expected texts", async () => { renderEncryptionEvent(client, event); - checkTexts("Encryption enabled", "Some encryption parameters have been changed."); + await waitFor(() => checkTexts("Encryption enabled", "Some encryption parameters have been changed.")); }); }); @@ -87,36 +90,38 @@ describe("EncryptionEvent", () => { event.event.content!.algorithm = "unknown"; }); - it("should show the expected texts", () => { + it("should show the expected texts", async () => { renderEncryptionEvent(client, event); - checkTexts("Encryption enabled", "Ignored attempt to disable encryption"); + await waitFor(() => checkTexts("Encryption enabled", "Ignored attempt to disable encryption")); }); }); }); describe("for an unencrypted room", () => { beforeEach(() => { - mocked(client.isRoomEncrypted).mockReturnValue(false); + jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(false); renderEncryptionEvent(client, event); }); - it("should show the expected texts", () => { - expect(client.isRoomEncrypted).toHaveBeenCalledWith(roomId); - checkTexts("Encryption not enabled", "The encryption used by this room isn't supported."); + it("should show the expected texts", async () => { + expect(client.getCrypto()!.isEncryptionEnabledInRoom).toHaveBeenCalledWith(roomId); + await waitFor(() => + checkTexts("Encryption not enabled", "The encryption used by this room isn't supported."), + ); }); }); describe("for an encrypted local room", () => { beforeEach(() => { event.event.content!.algorithm = algorithm; - mocked(client.isRoomEncrypted).mockReturnValue(true); + jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true); const localRoom = new LocalRoom(roomId, client, client.getUserId()!); mocked(client.getRoom).mockReturnValue(localRoom); renderEncryptionEvent(client, event); }); it("should show the expected texts", () => { - expect(client.isRoomEncrypted).toHaveBeenCalledWith(roomId); + expect(client.getCrypto()!.isEncryptionEnabledInRoom).toHaveBeenCalledWith(roomId); checkTexts("Encryption enabled", "Messages in this chat will be end-to-end encrypted."); }); }); From df0508546dde67b58256a479dc89c5becda132f3 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 13 Nov 2024 16:36:56 +0100 Subject: [PATCH 05/12] Replace `MatrixClient.isRoomEncrypted` by `MatrixClient.CryptoApi.isEncryptionEnabledInRoom` in `ReportEventDialog.tsx` --- .../views/dialogs/ReportEventDialog.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/ReportEventDialog.tsx b/src/components/views/dialogs/ReportEventDialog.tsx index 75b9977dc4f..3234c2be35b 100644 --- a/src/components/views/dialogs/ReportEventDialog.tsx +++ b/src/components/views/dialogs/ReportEventDialog.tsx @@ -43,6 +43,10 @@ interface IState { // If we know it, the nature of the abuse, as specified by MSC3215. nature?: ExtendedNature; ignoreUserToo: boolean; // if true, user will be ignored/blocked on submit + /* + * Whether the room is encrypted. + */ + isRoomEncrypted: boolean; } const MODERATED_BY_STATE_EVENT_TYPE = [ @@ -188,9 +192,20 @@ export default class ReportEventDialog extends React.Component { // If specified, the nature of the abuse, as specified by MSC3215. nature: undefined, ignoreUserToo: false, // default false, for now. Could easily be argued as default true + isRoomEncrypted: false, // async, will be set later }; } + public componentDidMount = async (): Promise => { + const crypto = MatrixClientPeg.safeGet().getCrypto(); + const roomId = this.props.mxEvent.getRoomId(); + if (!crypto || !roomId) return; + + this.setState({ + isRoomEncrypted: await crypto.isEncryptionEnabledInRoom(roomId), + }); + }; + private onIgnoreUserTooChanged = (newVal: boolean): void => { this.setState({ ignoreUserToo: newVal }); }; @@ -319,7 +334,6 @@ export default class ReportEventDialog extends React.Component { if (this.moderation) { // Display report-to-moderator dialog. // We let the user pick a nature. - const client = MatrixClientPeg.safeGet(); const homeServerName = SdkConfig.get("validated_server_config")!.hsName; let subtitle: string; switch (this.state.nature) { @@ -336,7 +350,7 @@ export default class ReportEventDialog extends React.Component { subtitle = _t("report_content|nature_spam"); break; case NonStandardValue.Admin: - if (client.isRoomEncrypted(this.props.mxEvent.getRoomId()!)) { + if (this.state.isRoomEncrypted) { subtitle = _t("report_content|nature_nonstandard_admin_encrypted", { homeserver: homeServerName, }); From be1837e257a9619fdf966c9da61818e74445c066 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 13 Nov 2024 16:37:12 +0100 Subject: [PATCH 06/12] Replace `MatrixClient.isRoomEncrypted` by `MatrixClient.CryptoApi.isEncryptionEnabledInRoom` in `RoomNotifications.tsx` --- src/components/views/dialogs/devtools/RoomNotifications.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/dialogs/devtools/RoomNotifications.tsx b/src/components/views/dialogs/devtools/RoomNotifications.tsx index c54e6950062..1bcff784879 100644 --- a/src/components/views/dialogs/devtools/RoomNotifications.tsx +++ b/src/components/views/dialogs/devtools/RoomNotifications.tsx @@ -17,6 +17,7 @@ import { determineUnreadState } from "../../../../RoomNotifs"; import { humanReadableNotificationLevel } from "../../../../stores/notifications/NotificationLevel"; import { doesRoomOrThreadHaveUnreadMessages } from "../../../../Unread"; import BaseTool, { DevtoolsContext, IDevtoolsProps } from "./BaseTool"; +import { useIsEncrypted } from "../../../../hooks/useIsEncrypted.ts"; function UserReadUpTo({ target }: { target: ReadReceipt }): JSX.Element { const cli = useContext(MatrixClientContext); @@ -59,6 +60,7 @@ function UserReadUpTo({ target }: { target: ReadReceipt }): JSX.Elemen export default function RoomNotifications({ onBack }: IDevtoolsProps): JSX.Element { const { room } = useContext(DevtoolsContext); const cli = useContext(MatrixClientContext); + const isRoomEncrypted = useIsEncrypted(cli, room); const { level, count } = determineUnreadState(room, undefined, false); const [notificationState] = useNotificationState(room); @@ -93,9 +95,7 @@ export default function RoomNotifications({ onBack }: IDevtoolsProps): JSX.Eleme
  • {_t( - cli.isRoomEncrypted(room.roomId!) - ? _td("devtools|room_encrypted") - : _td("devtools|room_not_encrypted"), + isRoomEncrypted ? _td("devtools|room_encrypted") : _td("devtools|room_not_encrypted"), {}, { strong: (sub) => {sub}, From 6aa6d615ebabde477f9ac0d63541ea63e88c18db Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 13 Nov 2024 17:06:02 +0100 Subject: [PATCH 07/12] Fix MessagePanel-test.tsx --- test/test-utils/client.ts | 1 + test/unit-tests/components/structures/MessagePanel-test.tsx | 2 ++ 2 files changed, 3 insertions(+) diff --git a/test/test-utils/client.ts b/test/test-utils/client.ts index 0a5798d8a1a..7842afbfe51 100644 --- a/test/test-utils/client.ts +++ b/test/test-utils/client.ts @@ -162,6 +162,7 @@ export const mockClientMethodsCrypto = (): Partial< getVersion: jest.fn().mockReturnValue("Version 0"), getOwnDeviceKeys: jest.fn().mockReturnValue(new Promise(() => {})), getCrossSigningKeyId: jest.fn(), + isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(false), }), }); diff --git a/test/unit-tests/components/structures/MessagePanel-test.tsx b/test/unit-tests/components/structures/MessagePanel-test.tsx index 037a57bb061..cf44716ba9e 100644 --- a/test/unit-tests/components/structures/MessagePanel-test.tsx +++ b/test/unit-tests/components/structures/MessagePanel-test.tsx @@ -23,6 +23,7 @@ import { createTestClient, getMockClientWithEventEmitter, makeBeaconInfoEvent, + mockClientMethodsCrypto, mockClientMethodsEvents, mockClientMethodsUser, } from "../../../test-utils"; @@ -42,6 +43,7 @@ describe("MessagePanel", function () { const client = getMockClientWithEventEmitter({ ...mockClientMethodsUser(userId), ...mockClientMethodsEvents(), + ...mockClientMethodsCrypto(), getAccountData: jest.fn(), isUserIgnored: jest.fn().mockReturnValue(false), isRoomEncrypted: jest.fn().mockReturnValue(false), From f074aceb5e4eeca3430b0aa0cafc9f6ca86decf5 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 13 Nov 2024 17:22:18 +0100 Subject: [PATCH 08/12] ReplaceReplace `MatrixCient..isRoomEncrypted` by `MatrixClient.CryptoApi.isEncryptionEnabledInRoom` in `shouldSkipSetupEncryption.ts` --- src/components/structures/MatrixChat.tsx | 2 +- src/utils/crypto/shouldSkipSetupEncryption.ts | 11 +++++++++-- .../components/structures/MatrixChat-test.tsx | 10 ++++++---- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index afd444c9524..e51dd966475 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -427,7 +427,7 @@ export default class MatrixChat extends React.PureComponent { } } else if ( (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) && - !shouldSkipSetupEncryption(cli) + !(await shouldSkipSetupEncryption(cli)) ) { // if cross-signing is not yet set up, do so now if possible. this.setStateForNewView({ view: Views.E2E_SETUP }); diff --git a/src/utils/crypto/shouldSkipSetupEncryption.ts b/src/utils/crypto/shouldSkipSetupEncryption.ts index 51d7a9303c1..3d5eef90348 100644 --- a/src/utils/crypto/shouldSkipSetupEncryption.ts +++ b/src/utils/crypto/shouldSkipSetupEncryption.ts @@ -9,6 +9,7 @@ Please see LICENSE files in the repository root for full details. import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { shouldForceDisableEncryption } from "./shouldForceDisableEncryption"; +import { asyncSome } from "../arrays.ts"; /** * If encryption is force disabled AND the user is not in any encrypted rooms @@ -16,7 +17,13 @@ import { shouldForceDisableEncryption } from "./shouldForceDisableEncryption"; * @param client * @returns {boolean} true when we can skip settings up encryption */ -export const shouldSkipSetupEncryption = (client: MatrixClient): boolean => { +export const shouldSkipSetupEncryption = async (client: MatrixClient): Promise => { const isEncryptionForceDisabled = shouldForceDisableEncryption(client); - return isEncryptionForceDisabled && !client.getRooms().some((r) => client.isRoomEncrypted(r.roomId)); + const crypto = client.getCrypto(); + if (!crypto) return true; + + return ( + isEncryptionForceDisabled && + !(await asyncSome(client.getRooms(), ({ roomId }) => crypto.isEncryptionEnabledInRoom(roomId))) + ); }; diff --git a/test/unit-tests/components/structures/MatrixChat-test.tsx b/test/unit-tests/components/structures/MatrixChat-test.tsx index 16106ee0d22..b3766bfc896 100644 --- a/test/unit-tests/components/structures/MatrixChat-test.tsx +++ b/test/unit-tests/components/structures/MatrixChat-test.tsx @@ -146,7 +146,6 @@ describe("", () => { matrixRTC: createStubMatrixRTC(), getDehydratedDevice: jest.fn(), whoami: jest.fn(), - isRoomEncrypted: jest.fn(), logout: jest.fn(), getDeviceId: jest.fn(), getKeyBackupVersion: jest.fn().mockResolvedValue(null), @@ -1011,6 +1010,7 @@ describe("", () => { 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), + isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(false), }; loginClient.getCrypto.mockReturnValue(mockCrypto as any); }); @@ -1058,9 +1058,11 @@ describe("", () => { }, }); - loginClient.isRoomEncrypted.mockImplementation((roomId) => { - return roomId === encryptedRoom.roomId; - }); + jest.spyOn(loginClient.getCrypto()!, "isEncryptionEnabledInRoom").mockImplementation( + async (roomId) => { + return roomId === encryptedRoom.roomId; + }, + ); }); it("should go straight to logged in view when user is not in any encrypted rooms", async () => { From 5b1b10e55b7ce39b942e78267e353cbf57699ec8 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Thu, 14 Nov 2024 10:24:31 +0100 Subject: [PATCH 09/12] Add missing `await` --- src/Searching.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Searching.ts b/src/Searching.ts index b14078fc83d..252d4378ade 100644 --- a/src/Searching.ts +++ b/src/Searching.ts @@ -596,7 +596,7 @@ async function combinedPagination( return result; } -function eventIndexSearch( +async function eventIndexSearch( client: MatrixClient, term: string, roomId?: string, @@ -605,7 +605,7 @@ function eventIndexSearch( let searchPromise: Promise; if (roomId !== undefined) { - if (client.getCrypto()?.isEncryptionEnabledInRoom(roomId)) { + if (await client.getCrypto()?.isEncryptionEnabledInRoom(roomId)) { // The search is for a single encrypted room, use our local // search method. searchPromise = localSearchProcess(client, term, roomId); From 7ec5450fa3911270ac2c5288fbabee454abe2c95 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Thu, 14 Nov 2024 11:25:00 +0100 Subject: [PATCH 10/12] Use `Promise.any` instead of `asyncSome` --- src/DeviceListener.ts | 13 ++++++++++--- src/utils/crypto/shouldSkipSetupEncryption.ts | 11 +++++++++-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index 5568e3f0274..cc8fa3a41b3 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -46,7 +46,6 @@ import SettingsStore, { CallbackFn } from "./settings/SettingsStore"; import { UIFeature } from "./settings/UIFeature"; import { isBulkUnverifiedDeviceReminderSnoozed } from "./utils/device/snoozeBulkUnverifiedDeviceReminder"; import { getUserDeviceIds } from "./utils/crypto/deviceInfo"; -import { asyncSome } from "./utils/arrays.ts"; const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000; @@ -241,7 +240,7 @@ export default class DeviceListener { return this.keyBackupInfo; } - private shouldShowSetupEncryptionToast(): Promise | boolean { + private async shouldShowSetupEncryptionToast(): Promise { // If we're in the middle of a secret storage operation, we're likely // modifying the state involved here, so don't add new toasts to setup. if (isSecretStorageBeingAccessed()) return false; @@ -250,7 +249,15 @@ export default class DeviceListener { const cryptoApi = cli?.getCrypto(); if (!cli || !cryptoApi) return false; - return asyncSome(cli.getRooms(), ({ roomId }) => cryptoApi.isEncryptionEnabledInRoom(roomId)); + return await Promise.any( + cli + .getRooms() + .map(({ roomId }) => + cryptoApi + .isEncryptionEnabledInRoom(roomId) + .then((encrypted) => (encrypted ? Promise.resolve(true) : Promise.reject(false))), + ), + ); } private recheck(): void { diff --git a/src/utils/crypto/shouldSkipSetupEncryption.ts b/src/utils/crypto/shouldSkipSetupEncryption.ts index 3d5eef90348..e2e36629027 100644 --- a/src/utils/crypto/shouldSkipSetupEncryption.ts +++ b/src/utils/crypto/shouldSkipSetupEncryption.ts @@ -9,7 +9,6 @@ Please see LICENSE files in the repository root for full details. import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { shouldForceDisableEncryption } from "./shouldForceDisableEncryption"; -import { asyncSome } from "../arrays.ts"; /** * If encryption is force disabled AND the user is not in any encrypted rooms @@ -24,6 +23,14 @@ export const shouldSkipSetupEncryption = async (client: MatrixClient): Promise crypto.isEncryptionEnabledInRoom(roomId))) + !(await Promise.any( + client + .getRooms() + .map(({ roomId }) => + crypto + .isEncryptionEnabledInRoom(roomId) + .then((encrypted) => (encrypted ? Promise.resolve(true) : Promise.reject(false))), + ), + )) ); }; From 662fbd99e353f24cb813afcaa1a007128afb22bf Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Thu, 14 Nov 2024 12:19:57 +0100 Subject: [PATCH 11/12] Add `asyncSomeParallel` --- src/utils/arrays.ts | 22 ++++++++++++++++++++++ test/unit-tests/utils/arrays-test.ts | 20 ++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/utils/arrays.ts b/src/utils/arrays.ts index 99c69b98912..d1a35f29501 100644 --- a/src/utils/arrays.ts +++ b/src/utils/arrays.ts @@ -328,6 +328,28 @@ export async function asyncSome(values: Iterable, predicate: (value: T) => return false; } +/** + * Async version of Array.some that runs all promises in parallel. + * @param values + * @param predicate + */ +export async function asyncSomeParallel( + values: Array, + predicate: (value: T) => Promise, +): Promise { + try { + return await Promise.any( + values.map((value) => + predicate(value).then((result) => (result ? Promise.resolve(true) : Promise.reject(false))), + ), + ); + } catch (e) { + // If the array is empty or all the promises are false, Promise.any will reject an AggregateError + if (e instanceof AggregateError) return false; + throw e; + } +} + export function filterBoolean(values: Array): T[] { return values.filter(Boolean) as T[]; } diff --git a/test/unit-tests/utils/arrays-test.ts b/test/unit-tests/utils/arrays-test.ts index 53baed8be3e..52b0053147b 100644 --- a/test/unit-tests/utils/arrays-test.ts +++ b/test/unit-tests/utils/arrays-test.ts @@ -23,6 +23,7 @@ import { concat, asyncEvery, asyncSome, + asyncSomeParallel, } from "../../../src/utils/arrays"; type TestParams = { input: number[]; output: number[] }; @@ -460,4 +461,23 @@ describe("arrays", () => { expect(predicate).toHaveBeenCalledWith(2); }); }); + + describe("asyncSomeParallel", () => { + it("when called with an empty array, it should return false", async () => { + expect(await asyncSomeParallel([], jest.fn().mockResolvedValue(true))).toBe(false); + }); + + it("when all the predicates return false", async () => { + expect(await asyncSomeParallel([1, 2, 3], jest.fn().mockResolvedValue(false))).toBe(false); + }); + + it("when all the predicates return true", async () => { + expect(await asyncSomeParallel([1, 2, 3], jest.fn().mockResolvedValue(true))).toBe(true); + }); + + it("when one of the predicate return true", async () => { + const predicate = jest.fn().mockImplementation((value) => Promise.resolve(value === 2)); + expect(await asyncSomeParallel([1, 2, 3], predicate)).toBe(true); + }); + }); }); From c0cca69836f57b71d2147ee042ebc899d7096e1b Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Thu, 14 Nov 2024 12:20:16 +0100 Subject: [PATCH 12/12] Use `asyncSomeParallel` instead of `asyncSome` --- src/DeviceListener.ts | 11 ++--------- src/utils/crypto/shouldSkipSetupEncryption.ts | 11 ++--------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index cc8fa3a41b3..4f47cd7eac4 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -46,6 +46,7 @@ import SettingsStore, { CallbackFn } from "./settings/SettingsStore"; import { UIFeature } from "./settings/UIFeature"; import { isBulkUnverifiedDeviceReminderSnoozed } from "./utils/device/snoozeBulkUnverifiedDeviceReminder"; import { getUserDeviceIds } from "./utils/crypto/deviceInfo"; +import { asyncSomeParallel } from "./utils/arrays.ts"; const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000; @@ -249,15 +250,7 @@ export default class DeviceListener { const cryptoApi = cli?.getCrypto(); if (!cli || !cryptoApi) return false; - return await Promise.any( - cli - .getRooms() - .map(({ roomId }) => - cryptoApi - .isEncryptionEnabledInRoom(roomId) - .then((encrypted) => (encrypted ? Promise.resolve(true) : Promise.reject(false))), - ), - ); + return await asyncSomeParallel(cli.getRooms(), ({ roomId }) => cryptoApi.isEncryptionEnabledInRoom(roomId)); } private recheck(): void { diff --git a/src/utils/crypto/shouldSkipSetupEncryption.ts b/src/utils/crypto/shouldSkipSetupEncryption.ts index e2e36629027..d4dbb27d1b2 100644 --- a/src/utils/crypto/shouldSkipSetupEncryption.ts +++ b/src/utils/crypto/shouldSkipSetupEncryption.ts @@ -9,6 +9,7 @@ Please see LICENSE files in the repository root for full details. import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { shouldForceDisableEncryption } from "./shouldForceDisableEncryption"; +import { asyncSomeParallel } from "../arrays.ts"; /** * If encryption is force disabled AND the user is not in any encrypted rooms @@ -23,14 +24,6 @@ export const shouldSkipSetupEncryption = async (client: MatrixClient): Promise - crypto - .isEncryptionEnabledInRoom(roomId) - .then((encrypted) => (encrypted ? Promise.resolve(true) : Promise.reject(false))), - ), - )) + !(await asyncSomeParallel(client.getRooms(), ({ roomId }) => crypto.isEncryptionEnabledInRoom(roomId))) ); };