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

Commit 01e7e01

Browse files
authored
Allow adding extra icons to the room header (#11799)
* Allow adding extra icons to the room header Signed-off-by: Charly Nguyen <charly.nguyen@nordeck.net> * Apply PR feedback Signed-off-by: Charly Nguyen <charly.nguyen@nordeck.net> * Apply PR feedback Signed-off-by: Charly Nguyen <charly.nguyen@nordeck.net> * Apply PR feedback Signed-off-by: Charly Nguyen <charly.nguyen@nordeck.net> * Apply PR feedback Signed-off-by: Charly Nguyen <charly.nguyen@nordeck.net> * Apply PR feedback Signed-off-by: Charly Nguyen <charly.nguyen@nordeck.net> --------- Signed-off-by: Charly Nguyen <charly.nguyen@nordeck.net>
1 parent 9496097 commit 01e7e01

File tree

14 files changed

+210
-7
lines changed

14 files changed

+210
-7
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
"@matrix-org/analytics-events": "^0.8.0",
6565
"@matrix-org/emojibase-bindings": "^1.1.2",
6666
"@matrix-org/matrix-wysiwyg": "2.4.1",
67-
"@matrix-org/react-sdk-module-api": "^2.1.1",
67+
"@matrix-org/react-sdk-module-api": "^2.2.0",
6868
"@matrix-org/spec": "^1.7.0",
6969
"@sentry/browser": "^7.0.0",
7070
"@sentry/tracing": "^7.0.0",

src/components/structures/RoomView.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import { logger } from "matrix-js-sdk/src/logger";
4242
import { CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
4343
import { throttle } from "lodash";
4444
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
45+
import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
4546

4647
import shouldHideEvent from "../../shouldHideEvent";
4748
import { _t } from "../../languageHandler";
@@ -246,6 +247,8 @@ export interface IRoomState {
246247

247248
canAskToJoin: boolean;
248249
promptAskToJoin: boolean;
250+
251+
viewRoomOpts: ViewRoomOpts;
249252
}
250253

251254
interface LocalRoomViewProps {
@@ -458,6 +461,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
458461
msc3946ProcessDynamicPredecessor: SettingsStore.getValue("feature_dynamic_room_predecessors"),
459462
canAskToJoin: this.askToJoinEnabled,
460463
promptAskToJoin: false,
464+
viewRoomOpts: { buttons: [] },
461465
};
462466

463467
this.dispatcherRef = dis.register(this.onAction);
@@ -663,6 +667,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
663667
: false,
664668
activeCall: roomId ? CallStore.instance.getActiveCall(roomId) : null,
665669
promptAskToJoin: this.context.roomViewStore.promptAskToJoin(),
670+
viewRoomOpts: this.context.roomViewStore.getViewRoomOpts(),
666671
};
667672

668673
if (
@@ -1407,6 +1412,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
14071412
tombstone: this.getRoomTombstone(room),
14081413
liveTimeline: room.getLiveTimeline(),
14091414
});
1415+
1416+
dis.dispatch<ActionPayload>({ action: Action.RoomLoaded });
14101417
};
14111418

14121419
private onRoomTimelineReset = (room?: Room): void => {
@@ -2601,7 +2608,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
26012608
data-layout={this.state.layout}
26022609
>
26032610
{SettingsStore.getValue("feature_new_room_decoration_ui") ? (
2604-
<RoomHeader room={this.state.room} />
2611+
<RoomHeader
2612+
room={this.state.room}
2613+
additionalButtons={this.state.viewRoomOpts.buttons}
2614+
/>
26052615
) : (
26062616
<LegacyRoomHeader
26072617
room={this.state.room}
@@ -2619,6 +2629,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
26192629
enableRoomOptionsMenu={!this.viewsLocalRoom}
26202630
viewingCall={viewingCall}
26212631
activeCall={this.state.activeCall}
2632+
additionalButtons={this.state.viewRoomOpts.buttons}
26222633
/>
26232634
)}
26242635
{mainSplitBody}

src/components/views/rooms/LegacyRoomHeader.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import classNames from "classnames";
2020
import { throttle } from "lodash";
2121
import { RoomStateEvent, ISearchResults } from "matrix-js-sdk/src/matrix";
2222
import { CallType } from "matrix-js-sdk/src/webrtc/call";
23+
import { IconButton, Tooltip } from "@vector-im/compound-web";
24+
import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
2325

2426
import type { MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
2527
import { _t } from "../../../languageHandler";
@@ -476,6 +478,7 @@ export interface IProps {
476478
enableRoomOptionsMenu?: boolean;
477479
viewingCall: boolean;
478480
activeCall: Call | null;
481+
additionalButtons?: ViewRoomOpts["buttons"];
479482
}
480483

481484
interface IState {
@@ -669,6 +672,23 @@ export default class RoomHeader extends React.Component<IProps, IState> {
669672

670673
return (
671674
<>
675+
{this.props.additionalButtons?.map((props) => {
676+
const label = props.label();
677+
678+
return (
679+
<Tooltip label={label} key={props.id}>
680+
<IconButton
681+
onClick={() => {
682+
props.onClick();
683+
this.forceUpdate();
684+
}}
685+
title={label}
686+
>
687+
{props.icon}
688+
</IconButton>
689+
</Tooltip>
690+
);
691+
})}
672692
{startButtons}
673693
<RoomHeaderButtons
674694
room={this.props.room}

src/components/views/rooms/RoomHeader.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { Icon as VerifiedIcon } from "@vector-im/compound-design-tokens/icons/ve
2424
import { Icon as ErrorIcon } from "@vector-im/compound-design-tokens/icons/error.svg";
2525
import { Icon as PublicIcon } from "@vector-im/compound-design-tokens/icons/public.svg";
2626
import { EventType, JoinRule, type Room } from "matrix-js-sdk/src/matrix";
27+
import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
2728

2829
import { useRoomName } from "../../../hooks/useRoomName";
2930
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
@@ -63,7 +64,13 @@ function notificationColorToIndicator(color: NotificationColor): React.Component
6364
}
6465
}
6566

66-
export default function RoomHeader({ room }: { room: Room }): JSX.Element {
67+
export default function RoomHeader({
68+
room,
69+
additionalButtons,
70+
}: {
71+
room: Room;
72+
additionalButtons?: ViewRoomOpts["buttons"];
73+
}): JSX.Element {
6774
const client = useMatrixClientContext();
6875

6976
const roomName = useRoomName(room);
@@ -169,6 +176,23 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
169176
)}
170177
</Box>
171178
<Flex as="nav" align="center" gap="var(--cpd-space-2x)">
179+
{additionalButtons?.map((props) => {
180+
const label = props.label();
181+
182+
return (
183+
<Tooltip label={label} key={props.id}>
184+
<IconButton
185+
aria-label={label}
186+
onClick={(event) => {
187+
event.stopPropagation();
188+
props.onClick();
189+
}}
190+
>
191+
{props.icon}
192+
</IconButton>
193+
</Tooltip>
194+
);
195+
})}
172196
{!useElementCallExclusively && (
173197
<Tooltip label={!voiceCallDisabledReason ? _t("voip|voice_call") : voiceCallDisabledReason!}>
174198
<IconButton

src/contexts/RoomContext.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ const RoomContext = createContext<
7373
msc3946ProcessDynamicPredecessor: false,
7474
canAskToJoin: false,
7575
promptAskToJoin: false,
76+
viewRoomOpts: { buttons: [] },
7677
});
7778
RoomContext.displayName = "RoomContext";
7879
export default RoomContext;

src/dispatcher/actions.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,4 +371,9 @@ export enum Action {
371371
* Fired when we want to open spotlight search. Use with a OpenSpotlightPayload.
372372
*/
373373
OpenSpotlight = "open_spotlight",
374+
375+
/**
376+
* Fired when the room loaded.
377+
*/
378+
RoomLoaded = "room_loaded",
374379
}

src/stores/RoomViewStore.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { ViewRoom as ViewRoomEvent } from "@matrix-org/analytics-events/types/ty
2424
import { JoinedRoom as JoinedRoomEvent } from "@matrix-org/analytics-events/types/typescript/JoinedRoom";
2525
import { Optional } from "matrix-events-sdk";
2626
import EventEmitter from "events";
27+
import { RoomViewLifecycle, ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
2728

2829
import { MatrixDispatcher } from "../dispatcher/dispatcher";
2930
import { MatrixClientPeg } from "../MatrixClientPeg";
@@ -60,6 +61,7 @@ import { pauseNonLiveBroadcastFromOtherRoom } from "../voice-broadcast/utils/pau
6061
import { ActionPayload } from "../dispatcher/payloads";
6162
import { CancelAskToJoinPayload } from "../dispatcher/payloads/CancelAskToJoinPayload";
6263
import { SubmitAskToJoinPayload } from "../dispatcher/payloads/SubmitAskToJoinPayload";
64+
import { ModuleRunner } from "../modules/ModuleRunner";
6365

6466
const NUM_JOIN_RETRY = 5;
6567

@@ -119,6 +121,8 @@ interface State {
119121
viewingCall: boolean;
120122

121123
promptAskToJoin: boolean;
124+
125+
viewRoomOpts: ViewRoomOpts;
122126
}
123127

124128
const INITIAL_STATE: State = {
@@ -140,6 +144,7 @@ const INITIAL_STATE: State = {
140144
wasContextSwitch: false,
141145
viewingCall: false,
142146
promptAskToJoin: false,
147+
viewRoomOpts: { buttons: [] },
143148
};
144149

145150
type Listener = (isActive: boolean) => void;
@@ -370,6 +375,10 @@ export class RoomViewStore extends EventEmitter {
370375
this.cancelAskToJoin(payload as CancelAskToJoinPayload);
371376
break;
372377
}
378+
case Action.RoomLoaded: {
379+
this.setViewRoomOpts();
380+
break;
381+
}
373382
}
374383
}
375384

@@ -805,4 +814,24 @@ export class RoomViewStore extends EventEmitter {
805814
}),
806815
);
807816
}
817+
818+
/**
819+
* Gets the current state of the 'viewRoomOpts' property.
820+
*
821+
* @returns {ViewRoomOpts} The value of the 'viewRoomOpts' property.
822+
*/
823+
public getViewRoomOpts(): ViewRoomOpts {
824+
return this.state.viewRoomOpts;
825+
}
826+
827+
/**
828+
* Invokes the view room lifecycle to set the view room options.
829+
*
830+
* @returns {void}
831+
*/
832+
private setViewRoomOpts(): void {
833+
const viewRoomOpts: ViewRoomOpts = { buttons: [] };
834+
ModuleRunner.instance.invoke(RoomViewLifecycle.ViewRoom, viewRoomOpts, this.getRoomId());
835+
this.setState({ viewRoomOpts });
836+
}
808837
}

test/components/structures/RoomView-test.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,4 +585,10 @@ describe("RoomView", () => {
585585
expect(dis.dispatch).toHaveBeenCalledWith({ action: "cancel_ask_to_join", roomId: room.roomId });
586586
});
587587
});
588+
589+
it("fires Action.RoomLoaded", async () => {
590+
jest.spyOn(dis, "dispatch");
591+
await mountRoomView();
592+
expect(dis.dispatch).toHaveBeenCalledWith({ action: Action.RoomLoaded });
593+
});
588594
});

test/components/views/rooms/LegacyRoomHeader-test.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { CallType } from "matrix-js-sdk/src/webrtc/call";
2929
import { ClientWidgetApi, Widget } from "matrix-widget-api";
3030
import EventEmitter from "events";
3131
import { setupJestCanvasMock } from "jest-canvas-mock";
32+
import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
3233

3334
import type { MatrixClient, MatrixEvent, RoomMember } from "matrix-js-sdk/src/matrix";
3435
import type { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
@@ -738,6 +739,34 @@ describe("LegacyRoomHeader", () => {
738739
expect(wrapper.container.querySelector(".mx_LegacyRoomHeader_name.mx_AccessibleButton")).toBeFalsy();
739740
},
740741
);
742+
743+
it("renders additionalButtons", async () => {
744+
const additionalButtons: ViewRoomOpts["buttons"] = [
745+
{
746+
icon: <>test-icon</>,
747+
id: "test-id",
748+
label: () => "test-label",
749+
onClick: () => {},
750+
},
751+
];
752+
renderHeader({ additionalButtons });
753+
expect(screen.getByRole("button", { name: "test-icon" })).toBeInTheDocument();
754+
});
755+
756+
it("calls onClick-callback on additionalButtons", () => {
757+
const callback = jest.fn();
758+
const additionalButtons: ViewRoomOpts["buttons"] = [
759+
{
760+
icon: <>test-icon</>,
761+
id: "test-id",
762+
label: () => "test-label",
763+
onClick: callback,
764+
},
765+
];
766+
renderHeader({ additionalButtons });
767+
fireEvent.click(screen.getByRole("button", { name: "test-icon" }));
768+
expect(callback).toHaveBeenCalled();
769+
});
741770
});
742771

743772
interface IRoomCreationInfo {

test/components/views/rooms/RoomHeader-test.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import React from "react";
1818
import { CallType, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
1919
import { EventType, JoinRule, MatrixClient, MatrixEvent, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix";
2020
import {
21+
createEvent,
2122
fireEvent,
2223
getAllByLabelText,
2324
getByLabelText,
@@ -27,6 +28,7 @@ import {
2728
screen,
2829
waitFor,
2930
} from "@testing-library/react";
31+
import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
3032

3133
import { filterConsole, mkEvent, stubClient, withClientContextRenderOptions } from "../../../test-utils";
3234
import RoomHeader from "../../../../src/components/views/rooms/RoomHeader";
@@ -516,6 +518,47 @@ describe("RoomHeader", () => {
516518
await waitFor(() => expect(getByLabelText(container, expectedLabel)).toBeInTheDocument());
517519
});
518520
});
521+
522+
it("renders additionalButtons", async () => {
523+
const additionalButtons: ViewRoomOpts["buttons"] = [
524+
{
525+
icon: <>test-icon</>,
526+
id: "test-id",
527+
label: () => "test-label",
528+
onClick: () => {},
529+
},
530+
];
531+
render(
532+
<RoomHeader room={room} additionalButtons={additionalButtons} />,
533+
withClientContextRenderOptions(MatrixClientPeg.get()!),
534+
);
535+
expect(screen.getByRole("button", { name: "test-label" })).toBeInTheDocument();
536+
});
537+
538+
it("calls onClick-callback on additionalButtons", () => {
539+
const callback = jest.fn();
540+
const additionalButtons: ViewRoomOpts["buttons"] = [
541+
{
542+
icon: <>test-icon</>,
543+
id: "test-id",
544+
label: () => "test-label",
545+
onClick: callback,
546+
},
547+
];
548+
549+
render(
550+
<RoomHeader room={room} additionalButtons={additionalButtons} />,
551+
withClientContextRenderOptions(MatrixClientPeg.get()!),
552+
);
553+
554+
const button = screen.getByRole("button", { name: "test-label" });
555+
const event = createEvent.click(button);
556+
event.stopPropagation = jest.fn();
557+
fireEvent(button, event);
558+
559+
expect(callback).toHaveBeenCalled();
560+
expect(event.stopPropagation).toHaveBeenCalled();
561+
});
519562
});
520563

521564
/**

0 commit comments

Comments
 (0)