From d84509d8d335d96cb865773f45a29924c923fccf Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 27 Jan 2023 14:07:05 +0000 Subject: [PATCH 1/2] Implement MSC3946 for AdvancedRoomSettingsTab (#9995) --- .../tabs/room/AdvancedRoomSettingsTab.tsx | 20 +++++---- src/i18n/strings/en_EN.json | 2 + src/settings/Settings.tsx | 9 ++++ .../room/AdvancedRoomSettingsTab-test.tsx | 42 +++++++++++++++++++ 4 files changed, 64 insertions(+), 9 deletions(-) diff --git a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx index 809da069bafa..d9acba8524a4 100644 --- a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx @@ -19,13 +19,14 @@ import { EventType } from "matrix-js-sdk/src/@types/event"; import { _t } from "../../../../../languageHandler"; import { MatrixClientPeg } from "../../../../../MatrixClientPeg"; -import AccessibleButton from "../../../elements/AccessibleButton"; +import AccessibleButton, { ButtonEvent } from "../../../elements/AccessibleButton"; import RoomUpgradeDialog from "../../../dialogs/RoomUpgradeDialog"; import Modal from "../../../../../Modal"; import dis from "../../../../../dispatcher/dispatcher"; import { Action } from "../../../../../dispatcher/actions"; import CopyableText from "../../../elements/CopyableText"; import { ViewRoomPayload } from "../../../../../dispatcher/payloads/ViewRoomPayload"; +import SettingsStore from "../../../../../settings/SettingsStore"; interface IProps { roomId: string; @@ -46,9 +47,11 @@ interface IState { } export default class AdvancedRoomSettingsTab extends React.Component { - public constructor(props, context) { + public constructor(props: IProps, context: any) { super(props, context); + const msc3946ProcessDynamicPredecessor = SettingsStore.getValue("feature_dynamic_room_predecessors"); + this.state = { // This is eventually set to the value of room.getRecommendedVersion() upgradeRecommendation: null, @@ -60,11 +63,10 @@ export default class AdvancedRoomSettingsTab extends React.Component = {}; - const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, ""); - const predecessor = createEvent ? createEvent.getContent().predecessor : null; - if (predecessor && predecessor.room_id) { - additionalStateChanges.oldRoomId = predecessor.room_id; - additionalStateChanges.oldEventId = predecessor.event_id; + const predecessor = room.findPredecessor(msc3946ProcessDynamicPredecessor); + if (predecessor) { + additionalStateChanges.oldRoomId = predecessor.roomId; + additionalStateChanges.oldEventId = predecessor.eventId; } this.setState({ @@ -75,12 +77,12 @@ export default class AdvancedRoomSettingsTab extends React.Component { + private upgradeRoom = (): void => { const room = MatrixClientPeg.get().getRoom(this.props.roomId); Modal.createDialog(RoomUpgradeDialog, { room }); }; - private onOldRoomClicked = (e): void => { + private onOldRoomClicked = (e: ButtonEvent): void => { e.preventDefault(); e.stopPropagation(); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 45db7cb23f2b..6d315bf38d22 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -955,6 +955,8 @@ "New group call experience": "New group call experience", "Live Location Sharing": "Live Location Sharing", "Temporary implementation. Locations persist in room history.": "Temporary implementation. Locations persist in room history.", + "Dynamic room predecessors": "Dynamic room predecessors", + "Enable MSC3946 (to support late-arriving room archives)": "Enable MSC3946 (to support late-arriving room archives)", "Favourite Messages": "Favourite Messages", "Under active development.": "Under active development.", "Force 15s voice broadcast chunk length": "Force 15s voice broadcast chunk length", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index bfd40e96ce1c..fdd3a857c12d 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -439,6 +439,15 @@ export const SETTINGS: { [setting: string]: ISetting } = { shouldWarn: true, default: false, }, + "feature_dynamic_room_predecessors": { + isFeature: true, + labsGroup: LabGroup.Rooms, + supportedLevels: LEVELS_FEATURE, + displayName: _td("Dynamic room predecessors"), + description: _td("Enable MSC3946 (to support late-arriving room archives)"), + shouldWarn: true, + default: false, + }, "feature_favourite_messages": { isFeature: true, labsGroup: LabGroup.Messaging, diff --git a/test/components/views/settings/tabs/room/AdvancedRoomSettingsTab-test.tsx b/test/components/views/settings/tabs/room/AdvancedRoomSettingsTab-test.tsx index dafd555966ef..cb4b373e4fbf 100644 --- a/test/components/views/settings/tabs/room/AdvancedRoomSettingsTab-test.tsx +++ b/test/components/views/settings/tabs/room/AdvancedRoomSettingsTab-test.tsx @@ -26,6 +26,7 @@ import { mkEvent, mkStubRoom, stubClient } from "../../../../../test-utils"; import dis from "../../../../../../src/dispatcher/dispatcher"; import { Action } from "../../../../../../src/dispatcher/actions"; import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg"; +import SettingsStore from "../../../../../../src/settings/SettingsStore"; jest.mock("../../../../../../src/dispatcher/dispatcher"); @@ -43,6 +44,12 @@ describe("AdvancedRoomSettingsTab", () => { cli = MatrixClientPeg.get(); room = mkStubRoom(roomId, "test room", cli); mocked(cli.getRoom).mockReturnValue(room); + mocked(dis.dispatch).mockReset(); + mocked(room.findPredecessor).mockImplementation((msc3946: boolean) => + msc3946 + ? { roomId: "old_room_id_via_predecessor", eventId: null } + : { roomId: "old_room_id", eventId: "tombstone_event_id" }, + ); }); it("should render as expected", () => { @@ -71,6 +78,17 @@ describe("AdvancedRoomSettingsTab", () => { room: room.roomId, }); + // Because we're mocking Room.findPredecessor, it may not be necessary + // to provide the actual event here, but we do need the create event, + // and in future this may be needed, so included for symmetry. + const predecessorEvent = mkEvent({ + event: true, + user: "@a:b.com", + type: EventType.RoomPredecessor, + content: { predecessor_room_id: "old_room_id_via_predecessor" }, + room: room.roomId, + }); + type GetStateEvents2Args = (eventType: EventType | string, stateKey: string) => MatrixEvent | null; const getStateEvents = jest.spyOn( @@ -82,6 +100,8 @@ describe("AdvancedRoomSettingsTab", () => { switch (eventType) { case EventType.RoomCreate: return createEvent; + case EventType.RoomPredecessor: + return predecessorEvent; default: return null; } @@ -101,4 +121,26 @@ describe("AdvancedRoomSettingsTab", () => { metricsViaKeyboard: false, }); }); + + describe("When MSC3946 support is enabled", () => { + beforeEach(() => { + jest.spyOn(SettingsStore, "getValue") + .mockReset() + .mockImplementation((settingName) => settingName === "feature_dynamic_room_predecessors"); + }); + + it("should link to predecessor room via MSC3946 if enabled", async () => { + mockStateEvents(room); + const tab = renderTab(); + const link = await tab.findByText("View older messages in test room."); + fireEvent.click(link); + expect(dis.dispatch).toHaveBeenCalledWith({ + action: Action.ViewRoom, + event_id: null, + room_id: "old_room_id_via_predecessor", + metricsTrigger: "WebPredecessorSettings", + metricsViaKeyboard: false, + }); + }); + }); }); From 364c453907afa169c54ec9c8e3bfc42d07f71406 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 27 Jan 2023 15:23:23 +0000 Subject: [PATCH 2/2] Tests for RoomCreate (#9997) * Tests for RoomCreate tile * Prefer screen instead of holding the return from render * use userEvent instead of fireEvent --- src/components/views/messages/RoomCreate.tsx | 6 +- .../views/messages/RoomCreate-test.tsx | 87 +++++++++++++++++++ .../__snapshots__/RoomCreate-test.tsx.snap | 24 +++++ 3 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 test/components/views/messages/RoomCreate-test.tsx create mode 100644 test/components/views/messages/__snapshots__/RoomCreate-test.tsx.snap diff --git a/src/components/views/messages/RoomCreate.tsx b/src/components/views/messages/RoomCreate.tsx index a9035ca03cb8..8bff5dfdcc50 100644 --- a/src/components/views/messages/RoomCreate.tsx +++ b/src/components/views/messages/RoomCreate.tsx @@ -27,11 +27,15 @@ import EventTileBubble from "./EventTileBubble"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; interface IProps { - /* the MatrixEvent to show */ + /** The m.room.create MatrixEvent that this tile represents */ mxEvent: MatrixEvent; timestamp?: JSX.Element; } +/** + * A message tile showing that this room was created as an upgrade of a previous + * room. + */ export default class RoomCreate extends React.Component { private onLinkClicked = (e: React.MouseEvent): void => { e.preventDefault(); diff --git a/test/components/views/messages/RoomCreate-test.tsx b/test/components/views/messages/RoomCreate-test.tsx new file mode 100644 index 000000000000..31763f9ae895 --- /dev/null +++ b/test/components/views/messages/RoomCreate-test.tsx @@ -0,0 +1,87 @@ +/* +Copyright 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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import { act, render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { mocked } from "jest-mock"; +import { EventType, MatrixEvent } from "matrix-js-sdk/src/matrix"; + +import dis from "../../../../src/dispatcher/dispatcher"; +import SettingsStore from "../../../../src/settings/SettingsStore"; +import RoomCreate from "../../../../src/components/views/messages/RoomCreate"; +import { stubClient } from "../../../test-utils/test-utils"; +import { Action } from "../../../../src/dispatcher/actions"; + +jest.mock("../../../../src/dispatcher/dispatcher"); + +describe("", () => { + const userId = "@alice:server.org"; + const roomId = "!room:server.org"; + const createEvent = new MatrixEvent({ + type: EventType.RoomCreate, + sender: userId, + room_id: roomId, + content: { + predecessor: { room_id: "old_room_id", event_id: "tombstone_event_id" }, + }, + event_id: "$create", + }); + + beforeEach(() => { + jest.clearAllMocks(); + mocked(dis.dispatch).mockReset(); + jest.spyOn(SettingsStore, "getValue").mockReturnValue(false); + jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined); + stubClient(); + }); + + afterAll(() => { + jest.spyOn(SettingsStore, "getValue").mockRestore(); + jest.spyOn(SettingsStore, "setValue").mockRestore(); + }); + + it("Renders as expected", () => { + const roomCreate = render(); + expect(roomCreate.asFragment()).toMatchSnapshot(); + }); + + it("Links to the old version of the room", () => { + render(); + expect(screen.getByText("Click here to see older messages.")).toHaveAttribute( + "href", + "https://matrix.to/#/old_room_id/tombstone_event_id", + ); + }); + + it("Opens the old room on click", async () => { + render(); + const link = screen.getByText("Click here to see older messages."); + + await act(() => userEvent.click(link)); + + await waitFor(() => + expect(dis.dispatch).toHaveBeenCalledWith({ + action: Action.ViewRoom, + event_id: "tombstone_event_id", + highlighted: true, + room_id: "old_room_id", + metricsTrigger: "Predecessor", + metricsViaKeyboard: false, + }), + ); + }); +}); diff --git a/test/components/views/messages/__snapshots__/RoomCreate-test.tsx.snap b/test/components/views/messages/__snapshots__/RoomCreate-test.tsx.snap new file mode 100644 index 000000000000..97c1cee66f6e --- /dev/null +++ b/test/components/views/messages/__snapshots__/RoomCreate-test.tsx.snap @@ -0,0 +1,24 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` Renders as expected 1`] = ` + +
+
+ This room is a continuation of another conversation. +
+ +
+
+`;