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
@@ -1,4 +1,4 @@
/*

Check failure on line 1 in playwright/e2e/crypto/decryption-failure-messages.spec.ts

View workflow job for this annotation

GitHub Actions / Run Tests 1/6

crypto/decryption-failure-messages.spec.ts:31:13 › Cryptography › decryption failure messages › should handle device-relative historical messages

1) crypto/decryption-failure-messages.spec.ts:31:13 › Cryptography › decryption failure messages › should handle device-relative historical messages Test timeout of 60000ms exceeded.
Copyright 2024 New Vector Ltd.
Copyright 2022-2024 The Matrix.org Foundation C.I.C.

Expand Down Expand Up @@ -63,10 +63,13 @@
// Finally, log out again, and back in, skipping verification for now, and see what we see.
await logOutOfElement(page);
await logIntoElement(page, homeserver, credentials);
await page.locator(".mx_AuthPage").getByRole("button", { name: "Skip verification for now" }).click();

Check failure on line 66 in playwright/e2e/crypto/decryption-failure-messages.spec.ts

View workflow job for this annotation

GitHub Actions / Run Tests 1/6

crypto/decryption-failure-messages.spec.ts:31:13 › Cryptography › decryption failure messages › should handle device-relative historical messages

1) crypto/decryption-failure-messages.spec.ts:31:13 › Cryptography › decryption failure messages › should handle device-relative historical messages Error: locator.click: Test timeout of 60000ms exceeded. Call log: - waiting for locator('.mx_AuthPage').getByRole('button', { name: 'Skip verification for now' }) 64 | await logOutOfElement(page); 65 | await logIntoElement(page, homeserver, credentials); > 66 | await page.locator(".mx_AuthPage").getByRole("button", { name: "Skip verification for now" }).click(); | ^ 67 | await page.locator(".mx_AuthPage").getByRole("button", { name: "I'll verify later" }).click(); 68 | await app.viewRoomByName("Test room"); 69 | at /home/runner/work/element-web/element-web/playwright/e2e/crypto/decryption-failure-messages.spec.ts:66:107
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);
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