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

127 changes: 83 additions & 44 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 Down Expand Up @@ -233,6 +242,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 +431,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
canAskToJoin: this.askToJoinEnabled,
promptAskToJoin: false,
viewRoomOpts: { buttons: [] },
isRoomEncrypted: false,
};
}

Expand Down Expand Up @@ -847,7 +862,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 @@ -914,6 +929,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
const callState = call?.state;
this.setState({
callState,
isRoomEncrypted: await this.getIsRoomEncrypted(),
florianduros marked this conversation as resolved.
Show resolved Hide resolved
});

this.context.legacyCallHandler.on(LegacyCallHandlerEvent.CallState, this.onCallState);
Expand Down Expand Up @@ -1377,6 +1393,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 @@ -1411,7 +1434,7 @@ 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";
const key = this.state.isRoomEncrypted ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled";
this.setState({
showUrlPreview: SettingsStore.getValue(key, roomId),
});
Expand Down Expand Up @@ -1456,7 +1479,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 @@ -1480,15 +1503,22 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
}
};

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;

switch (ev.getType()) {
case EventType.RoomTombstone:
this.setState({ tombstone: this.getRoomTombstone() });
break;

case EventType.RoomEncryption:
this.setState({ isRoomEncrypted: await this.getIsRoomEncrypted() }, () => {
if (this.state.room) {
this.updatePreviewUrlVisibility(this.state.room);
this.updateE2EStatus(this.state.room);
}
});
break;
default:
this.updatePermissions(this.state.room);
}
Expand Down Expand Up @@ -2027,6 +2057,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 +2274,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 +2359,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 +2403,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 +2454,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 @@ -117,7 +117,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
46 changes: 24 additions & 22 deletions test/unit-tests/components/structures/RoomView-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,17 @@ import React, { createRef, RefObject } from "react";
import { mocked, MockedObject } from "jest-mock";
import {
ClientEvent,
MatrixClient,
Room,
RoomEvent,
EventTimeline,
EventType,
IEvent,
JoinRule,
MatrixClient,
MatrixError,
RoomStateEvent,
MatrixEvent,
Room,
RoomEvent,
RoomStateEvent,
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";
Expand Down Expand Up @@ -87,8 +88,7 @@ describe("RoomView", () => {

beforeEach(() => {
mockPlatformPeg({ reload: () => {} });
stubClient();
cli = mocked(MatrixClientPeg.safeGet());
cli = mocked(stubClient());

room = new Room(`!${roomCount++}:example.org`, cli, "@alice:example.org");
jest.spyOn(room, "findPredecessor");
Expand Down Expand Up @@ -247,8 +247,9 @@ describe("RoomView", () => {

it("updates url preview visibility on encryption state change", async () => {
room.getMyMembership = jest.fn().mockReturnValue(KnownMembership.Join);
jest.spyOn(cli, "getCrypto").mockReturnValue(crypto);
// we should be starting unencrypted
expect(cli.isRoomEncrypted(room.roomId)).toEqual(false);
expect(await cli.getCrypto()?.isEncryptionEnabledInRoom(room.roomId)).toEqual(false);

const roomViewInstance = await getRoomViewInstance();

Expand All @@ -263,23 +264,23 @@ describe("RoomView", () => {
expect(roomViewInstance.state.showUrlPreview).toBe(true);

// now enable encryption
cli.isRoomEncrypted.mockReturnValue(true);
jest.spyOn(cli.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);

// and fake an encryption event into the room to prompt it to re-check
await act(() =>
room.addLiveEvents([
new MatrixEvent({
type: "m.room.encryption",
sender: cli.getUserId()!,
content: {},
event_id: "someid",
room_id: room.roomId,
}),
]),
);
act(() => {
const encryptionEvent = new MatrixEvent({
type: EventType.RoomEncryption,
sender: cli.getUserId()!,
content: {},
event_id: "someid",
room_id: room.roomId,
});
const roomState = room.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
cli.emit(RoomStateEvent.Events, encryptionEvent, roomState, null);
});

// URL previews should now be disabled
expect(roomViewInstance.state.showUrlPreview).toBe(false);
await waitFor(() => expect(roomViewInstance.state.showUrlPreview).toBe(false));
});

it("updates live timeline when a timeline reset happens", async () => {
Expand Down Expand Up @@ -427,7 +428,8 @@ describe("RoomView", () => {
]);
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(cli.getSafeUserId());
jest.spyOn(DMRoomMap.shared(), "getRoomIds").mockReturnValue(new Set([room.roomId]));
mocked(cli).isRoomEncrypted.mockReturnValue(true);
jest.spyOn(cli, "getCrypto").mockReturnValue(crypto);
jest.spyOn(cli.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
await renderRoomView();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ describe("<SendMessageComposer/>", () => {
canAskToJoin: false,
promptAskToJoin: false,
viewRoomOpts: { buttons: [] },
isRoomEncrypted: false,
};
describe("createMessageContent", () => {
it("sends plaintext messages correctly", () => {
Expand Down
Loading