Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace MatrixClient.isRoomEncrypted by MatrixClient.CryptoApi.isEncryptionEnabledInRoom in RoomView #28278

Merged
3 changes: 3 additions & 0 deletions playwright/e2e/crypto/decryption-failure-messages.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(4000);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 seconds? That seems terrifying, given how few rooms this client is in this sounds like a blocker to using the new API? 4 seconds to open a room is awful?

Copy link
Member Author

@florianduros florianduros Nov 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To open a room in a regular case, it's not taking 4 seconds. I tested manually and the other tests are running fine.

This test is going through already multiple issues (waiting 10sec for keys to be backed up) and I assume this is a special case


// There should be two historical events in the timeline
const tiles = await page.locator(".mx_EventTile").all();
expect(tiles.length).toBeGreaterThanOrEqual(2);
Expand Down
163 changes: 109 additions & 54 deletions src/components/structures/RoomView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -417,6 +432,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
canAskToJoin: this.askToJoinEnabled,
promptAskToJoin: false,
viewRoomOpts: { buttons: [] },
isRoomEncrypted: null,
};
}

Expand Down Expand Up @@ -847,7 +863,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
return isManuallyShown && widgets.length > 0;
}

public componentDidMount(): void {
public async componentDidMount(): Promise<void> {
this.unmounted = false;

this.dispatcherRef = defaultDispatcher.register(this.onAction);
Expand Down Expand Up @@ -1342,13 +1358,12 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
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 &&
Expand Down Expand Up @@ -1377,6 +1392,13 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
return room?.currentState.getStateEvents(EventType.RoomTombstone, "") ?? undefined;
}

private async getIsRoomEncrypted(roomId = this.state.roomId): Promise<boolean> {
const crypto = this.context.client?.getCrypto();
if (!crypto || !roomId) return false;

return await crypto.isEncryptionEnabledInRoom(roomId);
}

private async calculateRecommendedVersion(room: Room): Promise<void> {
const upgradeRecommendation = await room.getRecommendedVersion();
if (this.unmounted) return;
Expand Down Expand Up @@ -1409,12 +1431,15 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
});
}

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 => {
Expand Down Expand Up @@ -1456,7 +1481,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
};

private async updateE2EStatus(room: Room): Promise<void> {
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
Expand All @@ -1467,33 +1492,54 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {

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<E2EStatus> {
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<void> => {
// 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<void> {
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) {
Expand Down Expand Up @@ -2027,6 +2073,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {

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) {
Expand Down Expand Up @@ -2242,14 +2290,16 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
let aux: JSX.Element | undefined;
let previewBar;
if (this.state.timelineRenderingType === TimelineRenderingType.Search) {
aux = (
<RoomSearchAuxPanel
searchInfo={this.state.search}
onCancelClick={this.onCancelSearchClick}
onSearchScopeChange={this.onSearchScopeChange}
isRoomEncrypted={this.context.client.isRoomEncrypted(this.state.room.roomId)}
/>
);
if (!isRoomEncryptionLoading) {
aux = (
<RoomSearchAuxPanel
searchInfo={this.state.search}
onCancelClick={this.onCancelSearchClick}
onSearchScopeChange={this.onSearchScopeChange}
isRoomEncrypted={isRoomEncrypted}
/>
);
}
} else if (showRoomUpgradeBar) {
aux = <RoomUpgradeWarningBar room={this.state.room} />;
} else if (myMembership !== KnownMembership.Join) {
Expand Down Expand Up @@ -2325,8 +2375,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {

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 = (
<MessageComposer
Expand Down Expand Up @@ -2367,34 +2419,37 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
highlightedEventId = this.state.initialEventId;
}

const messagePanel = (
<TimelinePanel
ref={this.gatherTimelinePanelRef}
timelineSet={this.state.room.getUnfilteredTimelineSet()}
overlayTimelineSet={this.state.virtualRoom?.getUnfilteredTimelineSet()}
overlayTimelineSetFilter={isCallEvent}
showReadReceipts={this.state.showReadReceipts}
manageReadReceipts={!this.state.isPeeking}
sendReadReceiptOnLoad={!this.state.wasContextSwitch}
manageReadMarkers={!this.state.isPeeking}
hidden={hideMessagePanel}
highlightedEventId={highlightedEventId}
eventId={this.state.initialEventId}
eventScrollIntoView={this.state.initialEventScrollIntoView}
eventPixelOffset={this.state.initialEventPixelOffset}
onScroll={this.onMessageListScroll}
onEventScrolledIntoView={this.resetJumpToEvent}
onReadMarkerUpdated={this.updateTopUnreadMessagesBar}
showUrlPreview={this.state.showUrlPreview}
className={this.messagePanelClassNames}
membersLoaded={this.state.membersLoaded}
permalinkCreator={this.permalinkCreator}
resizeNotifier={this.props.resizeNotifier}
showReactions={true}
layout={this.state.layout}
editState={this.state.editState}
/>
);
let messagePanel: JSX.Element | undefined;
if (!isRoomEncryptionLoading) {
florianduros marked this conversation as resolved.
Show resolved Hide resolved
messagePanel = (
<TimelinePanel
ref={this.gatherTimelinePanelRef}
timelineSet={this.state.room.getUnfilteredTimelineSet()}
overlayTimelineSet={this.state.virtualRoom?.getUnfilteredTimelineSet()}
overlayTimelineSetFilter={isCallEvent}
showReadReceipts={this.state.showReadReceipts}
manageReadReceipts={!this.state.isPeeking}
sendReadReceiptOnLoad={!this.state.wasContextSwitch}
manageReadMarkers={!this.state.isPeeking}
hidden={hideMessagePanel}
highlightedEventId={highlightedEventId}
eventId={this.state.initialEventId}
eventScrollIntoView={this.state.initialEventScrollIntoView}
eventPixelOffset={this.state.initialEventPixelOffset}
onScroll={this.onMessageListScroll}
onEventScrolledIntoView={this.resetJumpToEvent}
onReadMarkerUpdated={this.updateTopUnreadMessagesBar}
showUrlPreview={this.state.showUrlPreview}
className={this.messagePanelClassNames}
membersLoaded={this.state.membersLoaded}
permalinkCreator={this.permalinkCreator}
resizeNotifier={this.props.resizeNotifier}
showReactions={true}
layout={this.state.layout}
editState={this.state.editState}
/>
);
}

let topUnreadMessagesBar: JSX.Element | undefined;
// Do not show TopUnreadMessagesBar if we have search results showing, it makes no sense
Expand All @@ -2415,7 +2470,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
);
}

const showRightPanel = this.state.room && this.state.showRightPanel;
const showRightPanel = !isRoomEncryptionLoading && this.state.room && this.state.showRightPanel;

const rightPanel = showRightPanel ? (
<RightPanel
Expand Down
1 change: 1 addition & 0 deletions src/contexts/RoomContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ const RoomContext = createContext<
canAskToJoin: false,
promptAskToJoin: false,
viewRoomOpts: { buttons: [] },
isRoomEncrypted: null,
});
RoomContext.displayName = "RoomContext";
export default RoomContext;
Expand Down
2 changes: 1 addition & 1 deletion test/test-utils/room.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export function getRoomContext(room: Room, override: Partial<IRoomState>): IRoom
canAskToJoin: false,
promptAskToJoin: false,
viewRoomOpts: { buttons: [] },

isRoomEncrypted: false,
...override,
};
}
Expand Down
2 changes: 1 addition & 1 deletion test/test-utils/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export function createTestClient(): MatrixClient {

getCrypto: jest.fn().mockReturnValue({
getOwnDeviceKeys: jest.fn(),
getUserDeviceInfo: jest.fn(),
getUserDeviceInfo: jest.fn().mockResolvedValue(new Map()),
getUserVerificationStatus: jest.fn(),
getDeviceVerificationStatus: jest.fn(),
resetKeyBackup: jest.fn(),
Expand Down
Loading
Loading