From 5ee157b64a5148c956edf4d49f72f14f426c8925 Mon Sep 17 00:00:00 2001 From: Charly Nguyen Date: Wed, 25 Oct 2023 14:55:20 +0200 Subject: [PATCH 1/6] Allow adding extra icons to the room header Signed-off-by: Charly Nguyen --- package.json | 2 +- src/components/structures/RoomView.tsx | 13 +++++++- .../views/rooms/LegacyRoomHeader.tsx | 16 +++++++++ src/components/views/rooms/RoomHeader.tsx | 22 ++++++++++++- src/contexts/RoomContext.ts | 1 + src/dispatcher/actions.ts | 5 +++ src/stores/RoomViewStore.tsx | 29 ++++++++++++++++ test/components/structures/RoomView-test.tsx | 6 ++++ .../views/rooms/LegacyRoomHeader-test.tsx | 14 ++++++++ .../views/rooms/RoomHeader-test.tsx | 17 ++++++++++ .../views/rooms/SendMessageComposer-test.tsx | 1 + test/stores/RoomViewStore-test.ts | 33 +++++++++++++++++++ test/test-utils/room.ts | 1 + yarn.lock | 8 ++--- 14 files changed, 161 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 95b30f18346..c504eb92bc3 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "@matrix-org/analytics-events": "^0.8.0", "@matrix-org/emojibase-bindings": "^1.1.2", "@matrix-org/matrix-wysiwyg": "2.4.1", - "@matrix-org/react-sdk-module-api": "^2.1.1", + "@matrix-org/react-sdk-module-api": "^2.2.0", "@matrix-org/spec": "^1.7.0", "@sentry/browser": "^7.0.0", "@sentry/tracing": "^7.0.0", diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 8ca3cd42763..9887e9cc60d 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -42,6 +42,7 @@ import { logger } from "matrix-js-sdk/src/logger"; import { CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call"; import { throttle } from "lodash"; import { CryptoEvent } from "matrix-js-sdk/src/crypto"; +import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle"; import shouldHideEvent from "../../shouldHideEvent"; import { _t } from "../../languageHandler"; @@ -246,6 +247,8 @@ export interface IRoomState { canAskToJoin: boolean; promptAskToJoin: boolean; + + viewRoomOpts: ViewRoomOpts; } interface LocalRoomViewProps { @@ -458,6 +461,7 @@ export class RoomView extends React.Component { msc3946ProcessDynamicPredecessor: SettingsStore.getValue("feature_dynamic_room_predecessors"), canAskToJoin: this.askToJoinEnabled, promptAskToJoin: false, + viewRoomOpts: { buttons: [] }, }; this.dispatcherRef = dis.register(this.onAction); @@ -663,6 +667,7 @@ export class RoomView extends React.Component { : false, activeCall: roomId ? CallStore.instance.getActiveCall(roomId) : null, promptAskToJoin: this.context.roomViewStore.promptAskToJoin(), + viewRoomOpts: this.context.roomViewStore.getViewRoomOpts(), }; if ( @@ -1407,6 +1412,8 @@ export class RoomView extends React.Component { tombstone: this.getRoomTombstone(room), liveTimeline: room.getLiveTimeline(), }); + + dis.dispatch({ action: Action.RoomLoaded }); }; private onRoomTimelineReset = (room?: Room): void => { @@ -2601,7 +2608,10 @@ export class RoomView extends React.Component { data-layout={this.state.layout} > {SettingsStore.getValue("feature_new_room_decoration_ui") ? ( - + ) : ( { enableRoomOptionsMenu={!this.viewsLocalRoom} viewingCall={viewingCall} activeCall={this.state.activeCall} + additionalButtons={this.state.viewRoomOpts.buttons} /> )} {mainSplitBody} diff --git a/src/components/views/rooms/LegacyRoomHeader.tsx b/src/components/views/rooms/LegacyRoomHeader.tsx index 81ede27ed81..61159c17d2d 100644 --- a/src/components/views/rooms/LegacyRoomHeader.tsx +++ b/src/components/views/rooms/LegacyRoomHeader.tsx @@ -20,6 +20,8 @@ import classNames from "classnames"; import { throttle } from "lodash"; import { RoomStateEvent, ISearchResults } from "matrix-js-sdk/src/matrix"; import { CallType } from "matrix-js-sdk/src/webrtc/call"; +import { IconButton, Tooltip } from "@vector-im/compound-web"; +import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle"; import type { MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; import { _t } from "../../../languageHandler"; @@ -476,6 +478,7 @@ export interface IProps { enableRoomOptionsMenu?: boolean; viewingCall: boolean; activeCall: Call | null; + additionalButtons?: ViewRoomOpts["buttons"]; } interface IState { @@ -669,6 +672,19 @@ export default class RoomHeader extends React.Component { return ( <> + {this.props.additionalButtons?.map(({ icon, id, label, onClick }) => ( + + { + onClick(); + this.forceUpdate(); + }} + title={label()} + > + {icon} + + + ))} {startButtons} + {additionalButtons?.map(({ icon, id, label, onClick }) => ( + + { + event.stopPropagation(); + onClick(); + }} + > + {icon} + + + ))} {!useElementCallExclusively && ( void; @@ -370,6 +375,10 @@ export class RoomViewStore extends EventEmitter { this.cancelAskToJoin(payload as CancelAskToJoinPayload); break; } + case Action.RoomLoaded: { + this.setViewRoomOpts(); + break; + } } } @@ -805,4 +814,24 @@ export class RoomViewStore extends EventEmitter { }), ); } + + /** + * Gets the current state of the 'viewRoomOpts' property. + * + * @returns {ViewRoomOpts} The value of the 'viewRoomOpts' property. + */ + public getViewRoomOpts(): ViewRoomOpts { + return this.state.viewRoomOpts; + } + + /** + * Invokes the view room lifecycle to set the view room options. + * + * @returns {void} + */ + private setViewRoomOpts(): void { + const viewRoomOpts: ViewRoomOpts = { buttons: [] }; + ModuleRunner.instance.invoke(RoomViewLifecycle.ViewRoom, viewRoomOpts, this.getRoomId()); + this.setState({ viewRoomOpts }); + } } diff --git a/test/components/structures/RoomView-test.tsx b/test/components/structures/RoomView-test.tsx index 8f6034d2e15..29a0f51c032 100644 --- a/test/components/structures/RoomView-test.tsx +++ b/test/components/structures/RoomView-test.tsx @@ -585,4 +585,10 @@ describe("RoomView", () => { expect(dis.dispatch).toHaveBeenCalledWith({ action: "cancel_ask_to_join", roomId: room.roomId }); }); }); + + it("fires Action.RoomLoaded", async () => { + jest.spyOn(dis, "dispatch"); + await mountRoomView(); + expect(dis.dispatch).toHaveBeenCalledWith({ action: Action.RoomLoaded }); + }); }); diff --git a/test/components/views/rooms/LegacyRoomHeader-test.tsx b/test/components/views/rooms/LegacyRoomHeader-test.tsx index f4fc7b3603f..9fe315c0110 100644 --- a/test/components/views/rooms/LegacyRoomHeader-test.tsx +++ b/test/components/views/rooms/LegacyRoomHeader-test.tsx @@ -29,6 +29,7 @@ import { CallType } from "matrix-js-sdk/src/webrtc/call"; import { ClientWidgetApi, Widget } from "matrix-widget-api"; import EventEmitter from "events"; import { setupJestCanvasMock } from "jest-canvas-mock"; +import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle"; import type { MatrixClient, MatrixEvent, RoomMember } from "matrix-js-sdk/src/matrix"; import type { MatrixCall } from "matrix-js-sdk/src/webrtc/call"; @@ -738,6 +739,19 @@ describe("LegacyRoomHeader", () => { expect(wrapper.container.querySelector(".mx_LegacyRoomHeader_name.mx_AccessibleButton")).toBeFalsy(); }, ); + + it("renders additionalButtons", async () => { + const additionalButtons: ViewRoomOpts["buttons"] = [ + { + icon: <>test-icon, + id: "test-id", + label: () => "test-label", + onClick: () => {}, + }, + ]; + renderHeader({ additionalButtons }); + expect(screen.getByRole("button", { name: "test-icon" })).toBeInTheDocument(); + }); }); interface IRoomCreationInfo { diff --git a/test/components/views/rooms/RoomHeader-test.tsx b/test/components/views/rooms/RoomHeader-test.tsx index 293e857a43f..a7c059b82a5 100644 --- a/test/components/views/rooms/RoomHeader-test.tsx +++ b/test/components/views/rooms/RoomHeader-test.tsx @@ -27,6 +27,7 @@ import { screen, waitFor, } from "@testing-library/react"; +import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle"; import { filterConsole, mkEvent, stubClient, withClientContextRenderOptions } from "../../../test-utils"; import RoomHeader from "../../../../src/components/views/rooms/RoomHeader"; @@ -516,6 +517,22 @@ describe("RoomHeader", () => { await waitFor(() => expect(getByLabelText(container, expectedLabel)).toBeInTheDocument()); }); }); + + it("renders additionalButtons", async () => { + const additionalButtons: ViewRoomOpts["buttons"] = [ + { + icon: <>test-icon, + id: "test-id", + label: () => "test-label", + onClick: () => {}, + }, + ]; + render( + , + withClientContextRenderOptions(MatrixClientPeg.get()!), + ); + expect(screen.getByRole("button", { name: "test-label" })).toBeInTheDocument(); + }); }); /** diff --git a/test/components/views/rooms/SendMessageComposer-test.tsx b/test/components/views/rooms/SendMessageComposer-test.tsx index 7d47af6b65b..cc6eb9ec872 100644 --- a/test/components/views/rooms/SendMessageComposer-test.tsx +++ b/test/components/views/rooms/SendMessageComposer-test.tsx @@ -84,6 +84,7 @@ describe("", () => { msc3946ProcessDynamicPredecessor: false, canAskToJoin: false, promptAskToJoin: false, + viewRoomOpts: { buttons: [] }, }; describe("createMessageContent", () => { const permalinkCreator = jest.fn() as any; diff --git a/test/stores/RoomViewStore-test.ts b/test/stores/RoomViewStore-test.ts index 6322d9acc01..a9b63273fe8 100644 --- a/test/stores/RoomViewStore-test.ts +++ b/test/stores/RoomViewStore-test.ts @@ -17,6 +17,7 @@ limitations under the License. import { mocked } from "jest-mock"; import { MatrixError, Room } from "matrix-js-sdk/src/matrix"; import { sleep } from "matrix-js-sdk/src/utils"; +import { RoomViewLifecycle, ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle"; import { RoomViewStore } from "../../src/stores/RoomViewStore"; import { Action } from "../../src/dispatcher/actions"; @@ -43,6 +44,7 @@ import ErrorDialog from "../../src/components/views/dialogs/ErrorDialog"; import { CancelAskToJoinPayload } from "../../src/dispatcher/payloads/CancelAskToJoinPayload"; import { JoinRoomErrorPayload } from "../../src/dispatcher/payloads/JoinRoomErrorPayload"; import { SubmitAskToJoinPayload } from "../../src/dispatcher/payloads/SubmitAskToJoinPayload"; +import { ModuleRunner } from "../../src/modules/ModuleRunner"; jest.mock("../../src/Modal"); @@ -132,6 +134,11 @@ describe("RoomViewStore", function () { await untilDispatch(Action.CancelAskToJoin, dis); }; + const dispatchRoomLoaded = async () => { + dis.dispatch({ action: Action.RoomLoaded }); + await untilDispatch(Action.RoomLoaded, dis); + }; + let roomViewStore: RoomViewStore; let slidingSyncManager: SlidingSyncManager; let dis: MatrixDispatcher; @@ -569,4 +576,30 @@ describe("RoomViewStore", function () { }); }); }); + + describe("getViewRoomOpts", () => { + it("returns viewRoomOpts", () => { + expect(roomViewStore.getViewRoomOpts()).toEqual({ buttons: [] }); + }); + }); + + describe("Action.RoomLoaded", () => { + it("updates viewRoomOpts", async () => { + const buttons: ViewRoomOpts["buttons"] = [ + { + icon: "test-icon", + id: "test-id", + label: () => "test-label", + onClick: () => {}, + }, + ]; + jest.spyOn(ModuleRunner.instance, "invoke").mockImplementation((lifecycleEvent, opts) => { + if (lifecycleEvent === RoomViewLifecycle.ViewRoom) { + opts.buttons = buttons; + } + }); + await dispatchRoomLoaded(); + expect(roomViewStore.getViewRoomOpts()).toEqual({ buttons }); + }); + }); }); diff --git a/test/test-utils/room.ts b/test/test-utils/room.ts index 5c4142005ae..5d188fb0b64 100644 --- a/test/test-utils/room.ts +++ b/test/test-utils/room.ts @@ -90,6 +90,7 @@ export function getRoomContext(room: Room, override: Partial): IRoom msc3946ProcessDynamicPredecessor: false, canAskToJoin: false, promptAskToJoin: false, + viewRoomOpts: { buttons: [] }, ...override, }; diff --git a/yarn.lock b/yarn.lock index 13a73aae08e..f5b1adb9107 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1828,10 +1828,10 @@ resolved "https://registry.yarnpkg.com/@matrix-org/olm/-/olm-3.2.15.tgz#55f3c1b70a21bbee3f9195cecd6846b1083451ec" integrity sha512-S7lOrndAK9/8qOtaTq/WhttJC/o4GAzdfK0MUPpo8ApzsJEC0QjtwrkC3KBXdFP1cD1MXi/mlKR7aaoVMKgs6Q== -"@matrix-org/react-sdk-module-api@^2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@matrix-org/react-sdk-module-api/-/react-sdk-module-api-2.1.1.tgz#54e8617c15185010d608c0325ecaec8d1574d12b" - integrity sha512-dYPY3aXtNwPrg2aEmFeWddMdohus/Ha17XES2QH+WMCawt+hH+uq28jH1EmW1RUOOzxVcdY36lRGOwqRtAJbhA== +"@matrix-org/react-sdk-module-api@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@matrix-org/react-sdk-module-api/-/react-sdk-module-api-2.2.0.tgz#cb284601a82448dc23fac31949c466eb34ec64b4" + integrity sha512-HSicxLdagZRbQp35d3t2SeDFTiT4GmEQDQGih8dWSKRHXK4krVQjb6Kf1NkwweiFDAeU0qgbz2pP4RZqbv0XIg== dependencies: "@babel/runtime" "^7.17.9" From 7d32b65d5acf1b59f0f27e0c33af5bf32a742f7b Mon Sep 17 00:00:00 2001 From: Charly Nguyen Date: Wed, 1 Nov 2023 08:50:28 +0100 Subject: [PATCH 2/6] Apply PR feedback Signed-off-by: Charly Nguyen --- src/components/views/rooms/RoomHeader.tsx | 2 +- .../views/rooms/LegacyRoomHeader-test.tsx | 15 +++++++++++ .../views/rooms/RoomHeader-test.tsx | 26 +++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index 4997cad84cd..197ab142773 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -69,7 +69,7 @@ export default function RoomHeader({ additionalButtons, }: { room: Room; - additionalButtons?: ViewRoomOpts["buttons"]; + additionalButtons?: Readonly; }): JSX.Element { const client = useMatrixClientContext(); diff --git a/test/components/views/rooms/LegacyRoomHeader-test.tsx b/test/components/views/rooms/LegacyRoomHeader-test.tsx index 9fe315c0110..3038fa0d499 100644 --- a/test/components/views/rooms/LegacyRoomHeader-test.tsx +++ b/test/components/views/rooms/LegacyRoomHeader-test.tsx @@ -752,6 +752,21 @@ describe("LegacyRoomHeader", () => { renderHeader({ additionalButtons }); expect(screen.getByRole("button", { name: "test-icon" })).toBeInTheDocument(); }); + + it("calls onClick-callback on additionalButtons", () => { + const callback = jest.fn(); + const additionalButtons: ViewRoomOpts["buttons"] = [ + { + icon: <>test-icon, + id: "test-id", + label: () => "test-label", + onClick: callback, + }, + ]; + renderHeader({ additionalButtons }); + fireEvent.click(screen.getByRole("button", { name: "test-icon" })); + expect(callback).toHaveBeenCalled(); + }); }); interface IRoomCreationInfo { diff --git a/test/components/views/rooms/RoomHeader-test.tsx b/test/components/views/rooms/RoomHeader-test.tsx index a7c059b82a5..f354ae934a2 100644 --- a/test/components/views/rooms/RoomHeader-test.tsx +++ b/test/components/views/rooms/RoomHeader-test.tsx @@ -18,6 +18,7 @@ import React from "react"; import { CallType, MatrixCall } from "matrix-js-sdk/src/webrtc/call"; import { EventType, JoinRule, MatrixClient, MatrixEvent, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix"; import { + createEvent, fireEvent, getAllByLabelText, getByLabelText, @@ -533,6 +534,31 @@ describe("RoomHeader", () => { ); expect(screen.getByRole("button", { name: "test-label" })).toBeInTheDocument(); }); + + it("calls onClick-callback on additionalButtons", () => { + const callback = jest.fn(); + const additionalButtons: ViewRoomOpts["buttons"] = [ + { + icon: <>test-icon, + id: "test-id", + label: () => "test-label", + onClick: callback, + }, + ]; + + render( + , + withClientContextRenderOptions(MatrixClientPeg.get()!), + ); + + const button = screen.getByRole("button", { name: "test-label" }); + const event = createEvent.click(button); + event.stopPropagation = jest.fn(); + fireEvent(button, event); + + expect(callback).toHaveBeenCalled(); + expect(event.stopPropagation).toHaveBeenCalled(); + }); }); /** From 1c0a15760aaae2a63c16279c52f41ba78a89d794 Mon Sep 17 00:00:00 2001 From: Charly Nguyen Date: Wed, 1 Nov 2023 09:34:30 +0100 Subject: [PATCH 3/6] Apply PR feedback Signed-off-by: Charly Nguyen --- src/components/views/rooms/RoomHeader.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index 197ab142773..b37cf49ed0a 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -67,10 +67,10 @@ function notificationColorToIndicator(color: NotificationColor): React.Component export default function RoomHeader({ room, additionalButtons, -}: { +}: Readonly<{ room: Room; - additionalButtons?: Readonly; -}): JSX.Element { + additionalButtons: ViewRoomOpts["buttons"]; +}>): JSX.Element { const client = useMatrixClientContext(); const roomName = useRoomName(room); From 7314048921e37a0470d4848561e60d3282f3b44b Mon Sep 17 00:00:00 2001 From: Charly Nguyen Date: Wed, 1 Nov 2023 09:54:45 +0100 Subject: [PATCH 4/6] Apply PR feedback Signed-off-by: Charly Nguyen --- src/components/views/rooms/RoomHeader.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index b37cf49ed0a..e47ca66a965 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -67,10 +67,10 @@ function notificationColorToIndicator(color: NotificationColor): React.Component export default function RoomHeader({ room, additionalButtons, -}: Readonly<{ +}: { room: Room; additionalButtons: ViewRoomOpts["buttons"]; -}>): JSX.Element { +}): JSX.Element { const client = useMatrixClientContext(); const roomName = useRoomName(room); From 045dfa1cb0607ffecbfea386c366720ad308cc91 Mon Sep 17 00:00:00 2001 From: Charly Nguyen Date: Wed, 1 Nov 2023 10:40:12 +0100 Subject: [PATCH 5/6] Apply PR feedback Signed-off-by: Charly Nguyen --- src/components/views/rooms/RoomHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index e47ca66a965..4997cad84cd 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -69,7 +69,7 @@ export default function RoomHeader({ additionalButtons, }: { room: Room; - additionalButtons: ViewRoomOpts["buttons"]; + additionalButtons?: ViewRoomOpts["buttons"]; }): JSX.Element { const client = useMatrixClientContext(); From ce0a9993374ad1215bf4e0e2471e04c4ba499e76 Mon Sep 17 00:00:00 2001 From: Charly Nguyen Date: Wed, 1 Nov 2023 12:25:30 +0100 Subject: [PATCH 6/6] Apply PR feedback Signed-off-by: Charly Nguyen --- .../views/rooms/LegacyRoomHeader.tsx | 30 +++++++++++-------- src/components/views/rooms/RoomHeader.tsx | 30 +++++++++++-------- 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/src/components/views/rooms/LegacyRoomHeader.tsx b/src/components/views/rooms/LegacyRoomHeader.tsx index 61159c17d2d..ab910744690 100644 --- a/src/components/views/rooms/LegacyRoomHeader.tsx +++ b/src/components/views/rooms/LegacyRoomHeader.tsx @@ -672,19 +672,23 @@ export default class RoomHeader extends React.Component { return ( <> - {this.props.additionalButtons?.map(({ icon, id, label, onClick }) => ( - - { - onClick(); - this.forceUpdate(); - }} - title={label()} - > - {icon} - - - ))} + {this.props.additionalButtons?.map((props) => { + const label = props.label(); + + return ( + + { + props.onClick(); + this.forceUpdate(); + }} + title={label} + > + {props.icon} + + + ); + })} {startButtons} - {additionalButtons?.map(({ icon, id, label, onClick }) => ( - - { - event.stopPropagation(); - onClick(); - }} - > - {icon} - - - ))} + {additionalButtons?.map((props) => { + const label = props.label(); + + return ( + + { + event.stopPropagation(); + props.onClick(); + }} + > + {props.icon} + + + ); + })} {!useElementCallExclusively && (