From 5cdcf44b6fb3818dc60d9168fe877e3920e4e6dd Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 20 Nov 2024 15:27:09 +0100 Subject: [PATCH] Second batch: Replace `MatrixClient.isRoomEncrypted` by `MatrixClient.CryptoApi.isEncryptionEnabledInRoom` (#28466) * Add `asyncFilter` * Replace `MatrixClient.isRoomEncrypted` by `MatrixClient.CryptoApi.isEncryptionEnabledInRoom` in `MemberListStore.tsx` * Replace `MatrixClient.isRoomEncrypted` by `MatrixClient.CryptoApi.isEncryptionEnabledInRoom` in `EventIndex.tsx` * Replace `MatrixClient.isRoomEncrypted` by `MatrixClient.CryptoApi.isEncryptionEnabledInRoom` in `SendMessageComposer.tsx` * Replace `MatrixClient.isRoomEncrypted` by `MatrixClient.CryptoApi.isEncryptionEnabledInRoom` in `ScalarMessaging.ts` * Replace `MatrixClient.isRoomEncrypted` by `MatrixClient.CryptoApi.isEncryptionEnabledInRoom` in `RolesRoomSettingsTab.tsx` * Add reject doc to `asyncFilter` * Reverse `MemberListStore.loadMembers` condition * Remove async for `ScalarMessaging.ts` * Display permission section only after `isEncrypted` is computed * Display composer only after `isEncrypted` is computed * Revert "Display composer only after `isEncrypted` is computed" This reverts commit 4d4e0373919ad4b0e6fa53de63786c880ed6845a. * Revert "Replace `MatrixClient.isRoomEncrypted` by `MatrixClient.CryptoApi.isEncryptionEnabledInRoom` in `SendMessageComposer.tsx`" This reverts commit 6bf06da02ca2c0cf72f0e12a418d07f05a86da3a. --- src/ScalarMessaging.ts | 4 +- .../tabs/room/RolesRoomSettingsTab.tsx | 48 ++++++++++++------ src/indexing/EventIndex.ts | 9 ++-- src/stores/MemberListStore.ts | 6 +-- src/utils/arrays.ts | 11 +++++ .../tabs/room/RolesRoomSettingsTab-test.tsx | 49 ++++++++++--------- .../unit-tests/stores/MemberListStore-test.ts | 3 +- test/unit-tests/utils/arrays-test.ts | 12 +++++ 8 files changed, 93 insertions(+), 49 deletions(-) diff --git a/src/ScalarMessaging.ts b/src/ScalarMessaging.ts index cac5b561a0..82846b9abf 100644 --- a/src/ScalarMessaging.ts +++ b/src/ScalarMessaging.ts @@ -514,7 +514,7 @@ function getWidgets(event: MessageEvent, roomId: string | null): void { sendResponse(event, widgetStateEvents); } -function getRoomEncState(event: MessageEvent, roomId: string): void { +async function getRoomEncState(event: MessageEvent, roomId: string): Promise { const client = MatrixClientPeg.get(); if (!client) { sendError(event, _t("widget|error_need_to_be_logged_in")); @@ -525,7 +525,7 @@ function getRoomEncState(event: MessageEvent, roomId: string): void { sendError(event, _t("scalar|error_room_unknown")); return; } - const roomIsEncrypted = MatrixClientPeg.safeGet().isRoomEncrypted(roomId); + const roomIsEncrypted = Boolean(await client.getCrypto()?.isEncryptionEnabledInRoom(roomId)); sendResponse(event, roomIsEncrypted); } diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx index ec8a2b8718..8261bfd3eb 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx @@ -127,12 +127,30 @@ interface IProps { room: Room; } -export default class RolesRoomSettingsTab extends React.Component { +interface RolesRoomSettingsTabState { + isRoomEncrypted: boolean; + isReady: boolean; +} + +export default class RolesRoomSettingsTab extends React.Component { public static contextType = MatrixClientContext; public declare context: React.ContextType; - public componentDidMount(): void { + public constructor(props: IProps) { + super(props); + this.state = { + isReady: false, + isRoomEncrypted: false, + }; + } + + public async componentDidMount(): Promise { this.context.on(RoomStateEvent.Update, this.onRoomStateUpdate); + this.setState({ + isRoomEncrypted: + (await this.context.getCrypto()?.isEncryptionEnabledInRoom(this.props.room.roomId)) || false, + isReady: true, + }); } public componentWillUnmount(): void { @@ -416,7 +434,7 @@ export default class RolesRoomSettingsTab extends React.Component { .filter(Boolean); // hide the power level selector for enabling E2EE if it the room is already encrypted - if (client.isRoomEncrypted(this.props.room.roomId)) { + if (this.state.isRoomEncrypted) { delete eventsLevels[EventType.RoomEncryption]; } @@ -458,17 +476,19 @@ export default class RolesRoomSettingsTab extends React.Component { {canChangeLevels && } {mutedUsersSection} {bannedUsersSection} - - {powerSelectors} - {eventPowerSelectors} - + {this.state.isReady && ( + + {powerSelectors} + {eventPowerSelectors} + + )} ); diff --git a/src/indexing/EventIndex.ts b/src/indexing/EventIndex.ts index ec3935cd68..fc1be4eba5 100644 --- a/src/indexing/EventIndex.ts +++ b/src/indexing/EventIndex.ts @@ -39,6 +39,7 @@ import { MatrixClientPeg } from "../MatrixClientPeg"; import SettingsStore from "../settings/SettingsStore"; import { SettingLevel } from "../settings/SettingLevel"; import { ICrawlerCheckpoint, IEventAndProfile, IIndexStats, ILoadArgs, ISearchArgs } from "./BaseEventIndexManager"; +import { asyncFilter } from "../utils/arrays.ts"; // The time in ms that the crawler will wait loop iterations if there // have not been any checkpoints to consume in the last iteration. @@ -103,13 +104,11 @@ export default class EventIndex extends EventEmitter { const client = MatrixClientPeg.safeGet(); const rooms = client.getRooms(); - const isRoomEncrypted = (room: Room): boolean => { - return client.isRoomEncrypted(room.roomId); - }; - // We only care to crawl the encrypted rooms, non-encrypted // rooms can use the search provided by the homeserver. - const encryptedRooms = rooms.filter(isRoomEncrypted); + const encryptedRooms = await asyncFilter(rooms, async (room) => + Boolean(await client.getCrypto()?.isEncryptionEnabledInRoom(room.roomId)), + ); logger.log("EventIndex: Adding initial crawler checkpoints"); diff --git a/src/stores/MemberListStore.ts b/src/stores/MemberListStore.ts index 1455a4526f..e500dec84c 100644 --- a/src/stores/MemberListStore.ts +++ b/src/stores/MemberListStore.ts @@ -70,7 +70,7 @@ export class MemberListStore { return []; } - if (!this.isLazyLoadingEnabled(roomId) || this.loadedRooms.has(roomId)) { + if (this.loadedRooms.has(roomId) || !(await this.isLazyLoadingEnabled(roomId))) { // nice and easy, we must already have all the members so just return them. return this.loadMembersInRoom(room); } @@ -121,10 +121,10 @@ export class MemberListStore { * @param roomId The room to check if lazy loading is enabled * @returns True if enabled */ - private isLazyLoadingEnabled(roomId: string): boolean { + private async isLazyLoadingEnabled(roomId: string): Promise { if (SettingsStore.getValue("feature_sliding_sync")) { // only unencrypted rooms use lazy loading - return !this.stores.client!.isRoomEncrypted(roomId); + return !(await this.stores.client?.getCrypto()?.isEncryptionEnabledInRoom(roomId)); } return this.stores.client!.hasLazyLoadMembersEnabled(); } diff --git a/src/utils/arrays.ts b/src/utils/arrays.ts index d1a35f2950..da8157adce 100644 --- a/src/utils/arrays.ts +++ b/src/utils/arrays.ts @@ -350,6 +350,17 @@ export async function asyncSomeParallel( } } +/** + * Async version of Array.filter. + * If one of the promises rejects, the whole operation will reject. + * @param values + * @param predicate + */ +export async function asyncFilter(values: Array, predicate: (value: T) => Promise): Promise> { + const results = await Promise.all(values.map(predicate)); + return values.filter((_, i) => results[i]); +} + export function filterBoolean(values: Array): T[] { return values.filter(Boolean) as T[]; } diff --git a/test/unit-tests/components/views/settings/tabs/room/RolesRoomSettingsTab-test.tsx b/test/unit-tests/components/views/settings/tabs/room/RolesRoomSettingsTab-test.tsx index 45855a0e25..000c38c771 100644 --- a/test/unit-tests/components/views/settings/tabs/room/RolesRoomSettingsTab-test.tsx +++ b/test/unit-tests/components/views/settings/tabs/room/RolesRoomSettingsTab-test.tsx @@ -27,16 +27,19 @@ describe("RolesRoomSettingsTab", () => { let cli: MatrixClient; let room: Room; - const renderTab = (propRoom: Room = room): RenderResult => { - return render(, withClientContextRenderOptions(cli)); + const renderTab = async (propRoom: Room = room): Promise => { + const renderResult = render(, withClientContextRenderOptions(cli)); + // Wait for the tab to be ready + await waitFor(() => expect(screen.getByText("Permissions")).toBeInTheDocument()); + return renderResult; }; - const getVoiceBroadcastsSelect = (): HTMLElement => { - return renderTab().container.querySelector("select[label='Voice broadcasts']")!; + const getVoiceBroadcastsSelect = async (): Promise => { + return (await renderTab()).container.querySelector("select[label='Voice broadcasts']")!; }; - const getVoiceBroadcastsSelectedOption = (): HTMLElement => { - return renderTab().container.querySelector("select[label='Voice broadcasts'] option:checked")!; + const getVoiceBroadcastsSelectedOption = async (): Promise => { + return (await renderTab()).container.querySelector("select[label='Voice broadcasts'] option:checked")!; }; beforeEach(() => { @@ -45,7 +48,7 @@ describe("RolesRoomSettingsTab", () => { room = mkStubRoom(roomId, "test room", cli); }); - it("should allow an Admin to demote themselves but not others", () => { + it("should allow an Admin to demote themselves but not others", async () => { mocked(cli.getRoom).mockReturnValue(room); // @ts-ignore - mocked doesn't support overloads properly mocked(room.currentState.getStateEvents).mockImplementation((type, key) => { @@ -67,19 +70,19 @@ describe("RolesRoomSettingsTab", () => { return null; }); mocked(room.currentState.mayClientSendStateEvent).mockReturnValue(true); - const { container } = renderTab(); + const { container } = await renderTab(); expect(container.querySelector(`[placeholder="${cli.getUserId()}"]`)).not.toBeDisabled(); expect(container.querySelector(`[placeholder="@admin:server"]`)).toBeDisabled(); }); - it("should initially show »Moderator« permission for »Voice broadcasts«", () => { - expect(getVoiceBroadcastsSelectedOption().textContent).toBe("Moderator"); + it("should initially show »Moderator« permission for »Voice broadcasts«", async () => { + expect((await getVoiceBroadcastsSelectedOption()).textContent).toBe("Moderator"); }); describe("when setting »Default« permission for »Voice broadcasts«", () => { - beforeEach(() => { - fireEvent.change(getVoiceBroadcastsSelect(), { + beforeEach(async () => { + fireEvent.change(await getVoiceBroadcastsSelect(), { target: { value: 0 }, }); }); @@ -122,12 +125,12 @@ describe("RolesRoomSettingsTab", () => { }); describe("Join Element calls", () => { - it("defaults to moderator for joining calls", () => { - expect(getJoinCallSelectedOption(renderTab())?.textContent).toBe("Moderator"); + it("defaults to moderator for joining calls", async () => { + expect(getJoinCallSelectedOption(await renderTab())?.textContent).toBe("Moderator"); }); - it("can change joining calls power level", () => { - const tab = renderTab(); + it("can change joining calls power level", async () => { + const tab = await renderTab(); fireEvent.change(getJoinCallSelect(tab), { target: { value: 0 }, @@ -143,12 +146,12 @@ describe("RolesRoomSettingsTab", () => { }); describe("Start Element calls", () => { - it("defaults to moderator for starting calls", () => { - expect(getStartCallSelectedOption(renderTab())?.textContent).toBe("Moderator"); + it("defaults to moderator for starting calls", async () => { + expect(getStartCallSelectedOption(await renderTab())?.textContent).toBe("Moderator"); }); - it("can change starting calls power level", () => { - const tab = renderTab(); + it("can change starting calls power level", async () => { + const tab = await renderTab(); fireEvent.change(getStartCallSelect(tab), { target: { value: 0 }, @@ -164,10 +167,10 @@ describe("RolesRoomSettingsTab", () => { }); }); - it("hides when group calls disabled", () => { + it("hides when group calls disabled", async () => { setGroupCallsEnabled(false); - const tab = renderTab(); + const tab = await renderTab(); expect(getStartCallSelect(tab)).toBeFalsy(); expect(getStartCallSelectedOption(tab)).toBeFalsy(); @@ -250,7 +253,7 @@ describe("RolesRoomSettingsTab", () => { return null; }); mocked(room.currentState.mayClientSendStateEvent).mockReturnValue(true); - const { container } = renderTab(); + const { container } = await renderTab(); const selector = container.querySelector(`[placeholder="${cli.getUserId()}"]`)!; fireEvent.change(selector, { target: { value: "50" } }); diff --git a/test/unit-tests/stores/MemberListStore-test.ts b/test/unit-tests/stores/MemberListStore-test.ts index 889a9d3505..815dea8758 100644 --- a/test/unit-tests/stores/MemberListStore-test.ts +++ b/test/unit-tests/stores/MemberListStore-test.ts @@ -189,8 +189,7 @@ describe("MemberListStore", () => { }); it("does not use lazy loading on encrypted rooms", async () => { - client.isRoomEncrypted = jest.fn(); - mocked(client.isRoomEncrypted).mockReturnValue(true); + jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true); const { joined } = await store.loadMemberList(roomId); expect(joined).toEqual([room.getMember(alice)]); diff --git a/test/unit-tests/utils/arrays-test.ts b/test/unit-tests/utils/arrays-test.ts index 52b0053147..7e440d3ee5 100644 --- a/test/unit-tests/utils/arrays-test.ts +++ b/test/unit-tests/utils/arrays-test.ts @@ -24,6 +24,7 @@ import { asyncEvery, asyncSome, asyncSomeParallel, + asyncFilter, } from "../../../src/utils/arrays"; type TestParams = { input: number[]; output: number[] }; @@ -480,4 +481,15 @@ describe("arrays", () => { expect(await asyncSomeParallel([1, 2, 3], predicate)).toBe(true); }); }); + + describe("asyncFilter", () => { + it("when called with an empty array, it should return an empty array", async () => { + expect(await asyncFilter([], jest.fn().mockResolvedValue(true))).toEqual([]); + }); + + it("should filter the content", async () => { + const predicate = jest.fn().mockImplementation((value) => Promise.resolve(value === 2)); + expect(await asyncFilter([1, 2, 3], predicate)).toEqual([2]); + }); + }); });