diff --git a/playwright/e2e/crypto/decryption-failure-messages.spec.ts b/playwright/e2e/crypto/decryption-failure-messages.spec.ts index ce7ca34d8e..b2a1209a70 100644 --- a/playwright/e2e/crypto/decryption-failure-messages.spec.ts +++ b/playwright/e2e/crypto/decryption-failure-messages.spec.ts @@ -67,6 +67,9 @@ test.describe("Cryptography", function () { await page.locator(".mx_AuthPage").getByRole("button", { name: "I'll verify later" }).click(); await app.viewRoomByName("Test room"); + // In this case, the call to cryptoApi.isEncryptionEnabledInRoom is taking a long time to resolve + await page.waitForTimeout(1000); + // There should be two historical events in the timeline const tiles = await page.locator(".mx_EventTile").all(); expect(tiles.length).toBeGreaterThanOrEqual(2); diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 470b73de7c..58e37606b2 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -9,7 +9,16 @@ 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, { ChangeEvent, ComponentProps, createRef, ReactElement, ReactNode, RefObject, useContext } from "react"; +import React, { + ChangeEvent, + ComponentProps, + createRef, + ReactElement, + ReactNode, + RefObject, + useContext, + JSX, +} from "react"; import classNames from "classnames"; import { IRecommendedVersion, @@ -29,6 +38,7 @@ import { MatrixError, ISearchResults, THREAD_RELATION_TYPE, + MatrixClient, } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; import { logger } from "matrix-js-sdk/src/logger"; @@ -233,6 +243,11 @@ export interface IRoomState { liveTimeline?: EventTimeline; narrow: boolean; msc3946ProcessDynamicPredecessor: boolean; + /** + * Whether the room is encrypted or not. + * If null, we are still determining the encryption status. + */ + isRoomEncrypted: boolean | null; canAskToJoin: boolean; promptAskToJoin: boolean; @@ -417,6 +432,7 @@ export class RoomView extends React.Component { canAskToJoin: this.askToJoinEnabled, promptAskToJoin: false, viewRoomOpts: { buttons: [] }, + isRoomEncrypted: null, }; } @@ -847,7 +863,7 @@ export class RoomView extends React.Component { return isManuallyShown && widgets.length > 0; } - public componentDidMount(): void { + public async componentDidMount(): Promise { this.unmounted = false; this.dispatcherRef = defaultDispatcher.register(this.onAction); @@ -1342,13 +1358,12 @@ export class RoomView extends React.Component { this.context.widgetLayoutStore.on(WidgetLayoutStore.emissionForRoom(room), this.onWidgetLayoutChange); this.calculatePeekRules(room); - this.updatePreviewUrlVisibility(room); this.loadMembersIfJoined(room); this.calculateRecommendedVersion(room); - this.updateE2EStatus(room); this.updatePermissions(room); this.checkWidgets(room); this.loadVirtualRoom(room); + this.updateRoomEncrypted(room); if ( this.getMainSplitContentType(room) !== MainSplitContentType.Timeline && @@ -1377,6 +1392,13 @@ export class RoomView extends React.Component { return room?.currentState.getStateEvents(EventType.RoomTombstone, "") ?? undefined; } + private async getIsRoomEncrypted(roomId = this.state.roomId): Promise { + const crypto = this.context.client?.getCrypto(); + if (!crypto || !roomId) return false; + + return await crypto.isEncryptionEnabledInRoom(roomId); + } + private async calculateRecommendedVersion(room: Room): Promise { const upgradeRecommendation = await room.getRecommendedVersion(); if (this.unmounted) return; @@ -1409,12 +1431,15 @@ export class RoomView extends React.Component { }); } - private updatePreviewUrlVisibility({ roomId }: Room): void { - // URL Previews in E2EE rooms can be a privacy leak so use a different setting which is per-room explicit - const key = this.context.client?.isRoomEncrypted(roomId) ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled"; - this.setState({ - showUrlPreview: SettingsStore.getValue(key, roomId), - }); + private updatePreviewUrlVisibility(room: Room): void { + this.setState(({ isRoomEncrypted }) => ({ + showUrlPreview: this.getPreviewUrlVisibility(room, isRoomEncrypted), + })); + } + + private getPreviewUrlVisibility({ roomId }: Room, isRoomEncrypted: boolean | null): boolean { + const key = isRoomEncrypted ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled"; + return SettingsStore.getValue(key, roomId); } private onRoom = (room: Room): void => { @@ -1456,7 +1481,7 @@ export class RoomView extends React.Component { }; private async updateE2EStatus(room: Room): Promise { - if (!this.context.client?.isRoomEncrypted(room.roomId)) return; + if (!this.context.client || !this.state.isRoomEncrypted) return; // If crypto is not currently enabled, we aren't tracking devices at all, // so we don't know what the answer is. Let's error on the safe side and show @@ -1467,33 +1492,54 @@ export class RoomView extends React.Component { if (this.context.client.getCrypto()) { /* At this point, the user has encryption on and cross-signing on */ - e2eStatus = await shieldStatusForRoom(this.context.client, room); - RoomView.e2eStatusCache.set(room.roomId, e2eStatus); + e2eStatus = await this.cacheAndGetE2EStatus(room, this.context.client); if (this.unmounted) return; this.setState({ e2eStatus }); } } + private async cacheAndGetE2EStatus(room: Room, client: MatrixClient): Promise { + const e2eStatus = await shieldStatusForRoom(client, room); + RoomView.e2eStatusCache.set(room.roomId, e2eStatus); + return e2eStatus; + } + private onUrlPreviewsEnabledChange = (): void => { if (this.state.room) { this.updatePreviewUrlVisibility(this.state.room); } }; - private onRoomStateEvents = (ev: MatrixEvent, state: RoomState): void => { + private onRoomStateEvents = async (ev: MatrixEvent, state: RoomState): Promise => { // ignore if we don't have a room yet - if (!this.state.room || this.state.room.roomId !== state.roomId) return; + if (!this.state.room || this.state.room.roomId !== state.roomId || !this.context.client) return; switch (ev.getType()) { case EventType.RoomTombstone: this.setState({ tombstone: this.getRoomTombstone() }); break; - + case EventType.RoomEncryption: { + await this.updateRoomEncrypted(); + break; + } default: this.updatePermissions(this.state.room); } }; + private async updateRoomEncrypted(room = this.state.room): Promise { + if (!room || !this.context.client) return; + + const isRoomEncrypted = await this.getIsRoomEncrypted(room.roomId); + const newE2EStatus = isRoomEncrypted ? await this.cacheAndGetE2EStatus(room, this.context.client) : null; + + this.setState({ + isRoomEncrypted, + showUrlPreview: this.getPreviewUrlVisibility(room, isRoomEncrypted), + ...(newE2EStatus && { e2eStatus: newE2EStatus }), + }); + } + private onRoomStateUpdate = (state: RoomState): void => { // ignore members in other rooms if (state.roomId !== this.state.room?.roomId) { @@ -2027,6 +2073,8 @@ export class RoomView extends React.Component { public render(): ReactNode { if (!this.context.client) return null; + const { isRoomEncrypted } = this.state; + const isRoomEncryptionLoading = isRoomEncrypted === null; if (this.state.room instanceof LocalRoom) { if (this.state.room.state === LocalRoomState.CREATING) { @@ -2242,14 +2290,16 @@ export class RoomView extends React.Component { let aux: JSX.Element | undefined; let previewBar; if (this.state.timelineRenderingType === TimelineRenderingType.Search) { - aux = ( - - ); + if (!isRoomEncryptionLoading) { + aux = ( + + ); + } } else if (showRoomUpgradeBar) { aux = ; } else if (myMembership !== KnownMembership.Join) { @@ -2325,8 +2375,10 @@ export class RoomView extends React.Component { let messageComposer; const showComposer = + !isRoomEncryptionLoading && // joined and not showing search results - myMembership === KnownMembership.Join && !this.state.search; + myMembership === KnownMembership.Join && + !this.state.search; if (showComposer) { messageComposer = ( { highlightedEventId = this.state.initialEventId; } - const messagePanel = ( -