Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Move subscriptions out of RoomView constructor #9973

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/components/structures/MessagePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import React, { createRef, KeyboardEvent, ReactNode, TransitionEvent } from "react";
import React, { createRef, ReactNode, TransitionEvent } from "react";
import ReactDOM from "react-dom";
import classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room";
Expand Down Expand Up @@ -428,7 +428,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
*
* @param {KeyboardEvent} ev: the keyboard event to handle
*/
public handleScrollKey(ev: KeyboardEvent): void {
public handleScrollKey(ev: KeyboardEvent | React.KeyboardEvent): void {
if (this.scrollPanel.current) {
this.scrollPanel.current.handleScrollKey(ev);
}
Expand Down
542 changes: 274 additions & 268 deletions src/components/structures/RoomView.tsx

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/components/structures/ScrollPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import React, { createRef, CSSProperties, ReactNode, KeyboardEvent } from "react";
import React, { createRef, CSSProperties, ReactNode } from "react";
import { logger } from "matrix-js-sdk/src/logger";

import SettingsStore from "../../settings/SettingsStore";
Expand Down Expand Up @@ -587,7 +587,7 @@ export default class ScrollPanel extends React.Component<IProps> {
* Scroll up/down in response to a scroll key
* @param {object} ev the keyboard event
*/
public handleScrollKey = (ev: KeyboardEvent): void => {
public handleScrollKey = (ev: KeyboardEvent | React.KeyboardEvent): void => {
const roomAction = getKeyBindingsManager().getRoomAction(ev);
switch (roomAction) {
case KeyBindingAction.ScrollUp:
Expand Down
4 changes: 2 additions & 2 deletions src/components/structures/TimelinePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1264,7 +1264,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
*
* returns null if we are not mounted.
*/
public getScrollState = (): IScrollState => {
public getScrollState = (): IScrollState | null => {
if (!this.messagePanel.current) {
return null;
}
Expand Down Expand Up @@ -1316,7 +1316,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
*
* We pass it down to the scroll panel.
*/
public handleScrollKey = (ev: React.KeyboardEvent): void => {
public handleScrollKey = (ev: KeyboardEvent | React.KeyboardEvent): void => {
if (!this.messagePanel.current) return;

// jump to the live timeline on ctrl-end, rather than the end of the
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/elements/Measured.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import React from "react";
import UIStore, { UI_EVENTS } from "../../../stores/UIStore";

interface IProps {
sensor: Element;
sensor?: Element;
breakpoint: number;
onMeasurement(narrow: boolean): void;
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/rooms/RoomHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,7 @@ export interface IProps {
onInviteClick: (() => void) | null;
onForgetClick: (() => void) | null;
onAppsClick: (() => void) | null;
e2eStatus: E2EStatus;
e2eStatus?: E2EStatus;
appsShown: boolean;
searchInfo?: ISearchInfo;
excludedRightPanelPhaseButtons?: Array<RightPanelPhases>;
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/rooms/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import SearchWarning, { WarningKind } from "../elements/SearchWarning";

interface IProps {
onCancelClick: () => void;
onSearch: (query: string, scope: string) => void;
onSearch: (query: string, scope: SearchScope) => void;
searchInProgress?: boolean;
isRoomEncrypted?: boolean;
}
Expand Down
7 changes: 4 additions & 3 deletions src/stores/RoomScrollStateStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,13 @@ export class RoomScrollStateStore {
// from the focussedEvent.
private scrollStateMap = new Map<string, ScrollState>();

public getScrollState(roomId: string): ScrollState {
public getScrollState(roomId: string): ScrollState | undefined {
return this.scrollStateMap.get(roomId);
}

public setScrollState(roomId: string, scrollState: ScrollState): void {
this.scrollStateMap.set(roomId, scrollState);
public setScrollState(roomId: string, scrollState: ScrollState | undefined): void {
if (scrollState === undefined) this.scrollStateMap.delete(roomId);
else this.scrollStateMap.set(roomId, scrollState);
}
}

Expand Down
166 changes: 78 additions & 88 deletions test/components/structures/RoomView-test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand All @@ -15,35 +15,28 @@ limitations under the License.
*/

import React from "react";
// eslint-disable-next-line deprecate/import
import { mount, ReactWrapper } from "enzyme";
import { mocked, MockedObject } from "jest-mock";
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
import { ClientEvent } from "matrix-js-sdk/src/client";
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { EventType } from "matrix-js-sdk/src/matrix";
import { EventType, MsgType } from "matrix-js-sdk/src/matrix";
import { MEGOLM_ALGORITHM } from "matrix-js-sdk/src/crypto/olmlib";
import { fireEvent, render } from "@testing-library/react";
import { act } from "react-dom/test-utils";

import {
stubClient,
mockPlatformPeg,
unmockPlatformPeg,
wrapInMatrixClientContext,
flushPromises,
} from "../../test-utils";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
import type { MatrixClient } from "matrix-js-sdk/src/client";
import { stubClient, mockPlatformPeg, wrapInMatrixClientContext, flushPromises, filterConsole } from "../../test-utils";
import { Action } from "../../../src/dispatcher/actions";
import { defaultDispatcher } from "../../../src/dispatcher/dispatcher";
import { ViewRoomPayload } from "../../../src/dispatcher/payloads/ViewRoomPayload";
import type { ViewRoomPayload } from "../../../src/dispatcher/payloads/ViewRoomPayload";
import { RoomView as _RoomView } from "../../../src/components/structures/RoomView";
import ResizeNotifier from "../../../src/utils/ResizeNotifier";
import SettingsStore from "../../../src/settings/SettingsStore";
import { SettingLevel } from "../../../src/settings/SettingLevel";
import DMRoomMap from "../../../src/utils/DMRoomMap";
import { NotificationState } from "../../../src/stores/notifications/NotificationState";
import { RightPanelPhases } from "../../../src/stores/right-panel/RightPanelStorePhases";
import { LocalRoom, LocalRoomState } from "../../../src/models/LocalRoom";
import type { LocalRoom } from "../../../src/models/LocalRoom";
import { LocalRoomState } from "../../../src/models/LocalRoom";
import { DirectoryMember } from "../../../src/utils/direct-messages";
import { createDmLocalRoom } from "../../../src/utils/dm/createDmLocalRoom";
import { UPDATE_EVENT } from "../../../src/stores/AsyncStore";
Expand All @@ -53,15 +46,21 @@ import VoipUserMapper from "../../../src/VoipUserMapper";
const RoomView = wrapInMatrixClientContext(_RoomView);

describe("RoomView", () => {
let cli: MockedObject<MatrixClient>;
let cli: jest.Mocked<MatrixClient>;
let room: Room;
let roomCount = 0;
let stores: SdkContextClass;

filterConsole(
"does not have an m.room.create event",
"Current version: ",
"Version capability: ",
"checkForPreJoinUISI",
);

beforeEach(async () => {
mockPlatformPeg({ reload: () => {} });
stubClient();
cli = mocked(MatrixClientPeg.get());
cli = stubClient() as jest.Mocked<MatrixClient>;

room = new Room(`!${roomCount++}:example.org`, cli, "@alice:example.org");
room.getPendingEvents = () => [];
Expand All @@ -76,49 +75,10 @@ describe("RoomView", () => {
stores.rightPanelStore.useUnitTestClient(cli);

jest.spyOn(VoipUserMapper.sharedInstance(), "getVirtualRoomForRoom").mockResolvedValue(null);
});

afterEach(async () => {
unmockPlatformPeg();
jest.restoreAllMocks();
room.updateMyMembership("join");
});

const mountRoomView = async (): Promise<ReactWrapper> => {
if (stores.roomViewStore.getRoomId() !== room.roomId) {
const switchedRoom = new Promise<void>((resolve) => {
const subFn = () => {
if (stores.roomViewStore.getRoomId()) {
stores.roomViewStore.off(UPDATE_EVENT, subFn);
resolve();
}
};
stores.roomViewStore.on(UPDATE_EVENT, subFn);
});

defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: room.roomId,
metricsTrigger: undefined,
});

await switchedRoom;
}

const roomView = mount(
<SDKContext.Provider value={stores}>
<RoomView
// threepidInvite should be optional on RoomView props
// it is treated as optional in RoomView
threepidInvite={undefined as any}
resizeNotifier={new ResizeNotifier()}
forceTimeline={false}
/>
</SDKContext.Provider>,
);
await flushPromises();
return roomView;
};

const renderRoomView = async (): Promise<ReturnType<typeof render>> => {
if (stores.roomViewStore.getRoomId() !== room.roomId) {
const switchedRoom = new Promise<void>((resolve) => {
Expand Down Expand Up @@ -152,18 +112,38 @@ describe("RoomView", () => {
/>
</SDKContext.Provider>,
);
await flushPromises();
await act(async () => {
await flushPromises();
});

// Want to make sure we aren't just rendering an error screen
expect(roomView.container.querySelector(".mx_ErrorBoundary")).toBeNull();
expect(roomView.container.innerHTML).not.toContain("Can't load this message");

return roomView;
};
const getRoomViewInstance = async (): Promise<_RoomView> =>
(await mountRoomView()).find(_RoomView).instance() as _RoomView;

it("updates url preview visibility on encryption state change", async () => {
room.addLiveEvents([
new MatrixEvent({
type: EventType.RoomMessage,
event_id: "$link-msg",
sender: `@link.sender:example.com`,
room_id: room.roomId,
origin_server_ts: 999_999,
content: {
body: "go to https://www.example.org/",
msgtype: MsgType.Text,
formatted_body: `go to <a href="https://www.example.org/">https://www.example.org/</a>`,
},
}),
]);

// we should be starting unencrypted
expect(cli.isCryptoEnabled()).toEqual(false);
expect(cli.isRoomEncrypted(room.roomId)).toEqual(false);

const roomViewInstance = await getRoomViewInstance();
const { container } = await renderRoomView();

// in a default (non-encrypted room, it should start out with url previews enabled)
// This is a white-box test in that we're asserting things about the state, which
Expand All @@ -173,33 +153,35 @@ describe("RoomView", () => {
// This also relies on the default settings being URL previews on normally and
// off for e2e rooms because 1) it's probably useful to assert this and
// 2) SettingsStore is a static class and so very hard to mock out.
expect(roomViewInstance.state.showUrlPreview).toBe(true);

// now enable encryption
cli.isCryptoEnabled.mockReturnValue(true);
cli.isRoomEncrypted.mockReturnValue(true);

// and fake an encryption event into the room to prompt it to re-check
room.addLiveEvents([
new MatrixEvent({
type: "m.room.encryption",
sender: cli.getUserId()!,
content: {},
event_id: "someid",
room_id: room.roomId,
}),
]);
expect(container.querySelector(".mx_LinkPreviewGroup")).not.toBeNull();

act(() => {
// now enable encryption
cli.isCryptoEnabled.mockReturnValue(true);
cli.isRoomEncrypted.mockReturnValue(true);

// and fake an encryption event into the room to prompt it to re-check
room.addLiveEvents([
new MatrixEvent({
type: "m.room.encryption",
sender: cli.getUserId()!,
content: {},
event_id: "someid",
room_id: room.roomId,
}),
]);
});

// URL previews should now be disabled
expect(roomViewInstance.state.showUrlPreview).toBe(false);
expect(container.querySelector(".mx_LinkPreviewGroup")).toBeNull();
});

it("updates live timeline when a timeline reset happens", async () => {
const roomViewInstance = await getRoomViewInstance();
const oldTimeline = roomViewInstance.state.liveTimeline;
const { container } = await renderRoomView();
const oldTimeline = container.innerHTML;

room.getUnfilteredTimelineSet().resetLiveTimeline();
expect(roomViewInstance.state.liveTimeline).not.toEqual(oldTimeline);
expect(container.innerHTML).not.toEqual(oldTimeline);
});

describe("with virtual rooms", () => {
Expand All @@ -212,13 +194,15 @@ describe("RoomView", () => {
});

it("checks for a virtual room on room event", async () => {
const mock = VoipUserMapper.sharedInstance().getVirtualRoomForRoom as unknown as jest.SpyInstance;
await renderRoomView();
expect(VoipUserMapper.sharedInstance().getVirtualRoomForRoom).toHaveBeenCalledWith(room.roomId);
expect(mock).toHaveBeenCalledWith(room.roomId);

mock.mockReset();
cli.emit(ClientEvent.Room, room);

// called again after room event
expect(VoipUserMapper.sharedInstance().getVirtualRoomForRoom).toHaveBeenCalledTimes(2);
expect(mock).toHaveBeenCalledTimes(1);
});
});

Expand All @@ -231,13 +215,13 @@ describe("RoomView", () => {

it("normally doesn't open the chat panel", async () => {
jest.spyOn(NotificationState.prototype, "isUnread", "get").mockReturnValue(false);
await mountRoomView();
await renderRoomView();
expect(stores.rightPanelStore.isOpen).toEqual(false);
});

it("opens the chat panel if there are unread messages", async () => {
jest.spyOn(NotificationState.prototype, "isUnread", "get").mockReturnValue(true);
await mountRoomView();
await renderRoomView();
expect(stores.rightPanelStore.isOpen).toEqual(true);
expect(stores.rightPanelStore.currentCard.phase).toEqual(RightPanelPhases.Timeline);
});
Expand All @@ -247,7 +231,13 @@ describe("RoomView", () => {
let localRoom: LocalRoom;

beforeEach(async () => {
cli.getClientWellKnown.mockReturnValue({
"io.element.e2ee": {
default: false,
},
});
localRoom = room = await createDmLocalRoom(cli, [new DirectoryMember({ user_id: "@user:example.com" })]);
expect(localRoom.encrypted).toBe(false);
cli.store.storeRoom(room);
});

Expand All @@ -265,7 +255,7 @@ describe("RoomView", () => {

describe("that is encrypted", () => {
beforeEach(() => {
mocked(cli.isRoomEncrypted).mockReturnValue(true);
cli.isRoomEncrypted.mockReturnValue(true);
localRoom.encrypted = true;
localRoom.currentState.setStateEvents([
new MatrixEvent({
Expand Down
Loading