Skip to content

Commit 09bbf79

Browse files
dbkrflorianduros
andauthored
Add support for Widget & Room Header Buttons module APIs (#32734)
* Add support for Widget & Room Header Buttons module APIs To support element-hq/element-modules#217 * Update for new api * Test addRoomHeaderButtonCallback * Extra mock api * Test for widgetapi * Convert enum * Convert other enum usage * Add tests for widget context menu move buttons Which have just changed because of the enum * Add tests for moving the widgets * Fix copyright Co-authored-by: Florian Duros <florianduros@element.io> * Update module API * A little import/export --------- Co-authored-by: Florian Duros <florianduros@element.io>
1 parent 86692ce commit 09bbf79

30 files changed

Lines changed: 553 additions & 225 deletions

apps/web/src/LegacyCallHandler.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import { Action } from "./dispatcher/actions";
4242
import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from "./widgets/ManagedHybrid";
4343
import SdkConfig from "./SdkConfig";
4444
import { ensureDMExists } from "./createRoom";
45-
import { Container, WidgetLayoutStore } from "./stores/widgets/WidgetLayoutStore";
45+
import { WidgetLayoutStore } from "./stores/widgets/WidgetLayoutStore";
4646
import IncomingLegacyCallToast, { getIncomingLegacyCallToastKey } from "./toasts/IncomingLegacyCallToast";
4747
import ToastStore from "./stores/ToastStore";
4848
import { type ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload";
@@ -1027,7 +1027,7 @@ export default class LegacyCallHandler extends TypedEventEmitter<LegacyCallHandl
10271027
// If there already is a Jitsi widget, pin it
10281028
const room = client.getRoom(roomId);
10291029
if (isNotNull(room)) {
1030-
WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Top);
1030+
WidgetLayoutStore.instance.moveToContainer(room, widget, "top");
10311031
}
10321032
return;
10331033
}

apps/web/src/components/structures/RoomView.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ import { CallView } from "../views/voip/CallView";
9090
import { UPDATE_EVENT } from "../../stores/AsyncStore";
9191
import Notifier from "../../Notifier";
9292
import { showToast as showNotificationsToast } from "../../toasts/DesktopNotificationsToast";
93-
import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
93+
import { WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
9494
import { getKeyBindingsManager } from "../../KeyBindingsManager";
9595
import { objectHasDiff } from "../../utils/objects";
9696
import SpaceRoomView from "./SpaceRoomView";
@@ -141,6 +141,7 @@ import { isRoomEncrypted } from "../../hooks/useIsEncrypted";
141141
import { type RoomViewStore } from "../../stores/RoomViewStore.tsx";
142142
import { RoomStatusBarViewModel } from "../../viewmodels/room/RoomStatusBar.ts";
143143
import { EncryptionEventViewModel } from "../../viewmodels/event-tiles/EncryptionEventViewModel.ts";
144+
import { ModuleApi } from "../../modules/Api.ts";
144145

145146
const DEBUG = false;
146147
const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000;
@@ -953,7 +954,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
953954
// Otherwise (in case the user set hideWidgetDrawer by clicking the button) follow the parameter.
954955
const isManuallyShown = hideWidgetDrawer ? hideWidgetDrawer === "false" : true;
955956

956-
const widgets = this.context.widgetLayoutStore.getContainerWidgets(room, Container.Top);
957+
const widgets = this.context.widgetLayoutStore.getContainerWidgets(room, "top");
957958
return isManuallyShown && widgets.length > 0;
958959
}
959960

@@ -2727,6 +2728,12 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
27272728
this.state.mainSplitContentType === MainSplitContentType.Call ? "video_room" : "maximised_widget";
27282729
}
27292730

2731+
const extraButtons: JSX.Element[] = [];
2732+
for (const cb of ModuleApi.instance.extras.roomHeaderButtonsCallbacks) {
2733+
const b = cb(this.state.room.roomId);
2734+
if (b) extraButtons.push(b);
2735+
}
2736+
27302737
return (
27312738
<ScopedRoomContextProvider {...this.state} roomViewStore={this.roomViewStore}>
27322739
<div
@@ -2754,7 +2761,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
27542761
{!this.props.hideHeader && (
27552762
<RoomHeader
27562763
room={this.state.room}
2757-
additionalButtons={this.state.viewRoomOpts.buttons}
2764+
legacyAdditionalButtons={this.state.viewRoomOpts.buttons}
2765+
extraButtons={<>{extraButtons}</>}
27582766
/>
27592767
)}
27602768
{mainSplitBody}

apps/web/src/components/views/context_menus/WidgetContextMenu.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import QuestionDialog from "../dialogs/QuestionDialog";
2525
import ErrorDialog from "../dialogs/ErrorDialog";
2626
import { WidgetType } from "../../../widgets/WidgetType";
2727
import MatrixClientContext from "../../../contexts/MatrixClientContext";
28-
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
28+
import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
2929
import { getConfigLivestreamUrl, startJitsiAudioLivestream } from "../../../Livestream";
3030
import { ModuleRunner } from "../../../modules/ModuleRunner";
3131
import { ElementWidget, type WidgetMessaging } from "../../../stores/widgets/WidgetMessaging";
@@ -79,7 +79,7 @@ const showSnapshotButton = (widgetMessaging: WidgetMessaging | undefined): boole
7979
const showMoveButtons = (app: IWidget, room: Room | undefined, showUnpin: boolean | undefined): [boolean, boolean] => {
8080
if (!showUnpin) return [false, false];
8181

82-
const pinnedWidgets = room ? WidgetLayoutStore.instance.getContainerWidgets(room, Container.Top) : [];
82+
const pinnedWidgets = room ? WidgetLayoutStore.instance.getContainerWidgets(room, "top") : [];
8383
const widgetIndex = pinnedWidgets.findIndex((widget) => widget.id === app.id);
8484
return [widgetIndex > 0, widgetIndex < pinnedWidgets.length - 1];
8585
};
@@ -221,7 +221,7 @@ export const WidgetContextMenu: React.FC<IProps> = ({
221221
if (showMoveLeftButton) {
222222
const onClick = (): void => {
223223
if (!room) throw new Error("room must be defined");
224-
WidgetLayoutStore.instance.moveWithinContainer(room, Container.Top, app, -1);
224+
WidgetLayoutStore.instance.moveWithinContainer(room, "top", app, -1);
225225
onFinished();
226226
};
227227

@@ -232,7 +232,7 @@ export const WidgetContextMenu: React.FC<IProps> = ({
232232
if (showMoveRightButton) {
233233
const onClick = (): void => {
234234
if (!room) throw new Error("room must be defined");
235-
WidgetLayoutStore.instance.moveWithinContainer(room, Container.Top, app, 1);
235+
WidgetLayoutStore.instance.moveWithinContainer(room, "top", app, 1);
236236
onFinished();
237237
};
238238

apps/web/src/components/views/elements/AppTile.tsx

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ import { ElementWidget, WidgetMessaging, WidgetMessagingEvent } from "../../../s
4747
import WidgetAvatar from "../avatars/WidgetAvatar";
4848
import LegacyCallHandler from "../../../LegacyCallHandler";
4949
import { type IApp, isAppWidget } from "../../../stores/WidgetStore";
50-
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
50+
import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
5151
import { OwnProfileStore } from "../../../stores/OwnProfileStore";
5252
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
5353
import WidgetUtils from "../../../utils/WidgetUtils";
@@ -682,21 +682,17 @@ export default class AppTile extends React.Component<IProps, IState> {
682682

683683
private onToggleMaximisedClick = (): void => {
684684
if (!this.props.room) return; // ignore action - it shouldn't even be visible
685-
const targetContainer = WidgetLayoutStore.instance.isInContainer(
686-
this.props.room,
687-
this.props.app,
688-
Container.Center,
689-
)
690-
? Container.Top
691-
: Container.Center;
685+
const targetContainer = WidgetLayoutStore.instance.isInContainer(this.props.room, this.props.app, "center")
686+
? "top"
687+
: "center";
692688
WidgetLayoutStore.instance.moveToContainer(this.props.room, this.props.app, targetContainer);
693689

694-
if (targetContainer === Container.Top) this.closeChatCardIfNeeded();
690+
if (targetContainer === "top") this.closeChatCardIfNeeded();
695691
};
696692

697693
private onMinimiseClicked = (): void => {
698694
if (!this.props.room) return; // ignore action - it shouldn't even be visible
699-
WidgetLayoutStore.instance.moveToContainer(this.props.room, this.props.app, Container.Right);
695+
WidgetLayoutStore.instance.moveToContainer(this.props.room, this.props.app, "right");
700696
this.closeChatCardIfNeeded();
701697
};
702698

@@ -822,8 +818,7 @@ export default class AppTile extends React.Component<IProps, IState> {
822818
const layoutButtons: ReactNode[] = [];
823819
if (this.props.showLayoutButtons) {
824820
const isMaximised =
825-
this.props.room &&
826-
WidgetLayoutStore.instance.isInContainer(this.props.room, this.props.app, Container.Center);
821+
this.props.room && WidgetLayoutStore.instance.isInContainer(this.props.room, this.props.app, "center");
827822

828823
layoutButtons.push(
829824
<AccessibleButton

apps/web/src/components/views/messages/MJitsiWidgetEvent.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { EventTileBubble } from "@element-hq/web-shared-components";
1414
import { _t } from "../../../languageHandler";
1515
import WidgetStore from "../../../stores/WidgetStore";
1616
import { MatrixClientPeg } from "../../../MatrixClientPeg";
17-
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
17+
import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
1818

1919
interface IProps {
2020
mxEvent: MatrixEvent;
@@ -32,7 +32,7 @@ export default class MJitsiWidgetEvent extends React.PureComponent<IProps> {
3232
const widget = WidgetStore.instance.getRoom(room.roomId, true).widgets.find((w) => w.id === widgetId);
3333

3434
let joinCopy: string | null = _t("timeline|m.widget|jitsi_join_top_prompt");
35-
if (widget && WidgetLayoutStore.instance.isInContainer(room, widget, Container.Right)) {
35+
if (widget && WidgetLayoutStore.instance.isInContainer(room, widget, "right")) {
3636
joinCopy = _t("timeline|m.widget|jitsi_join_right_prompt");
3737
} else if (!widget) {
3838
joinCopy = null;

apps/web/src/components/views/right_panel/ExtensionsCard.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { useContextMenu } from "../../structures/ContextMenu";
2424
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
2525
import { type IApp } from "../../../stores/WidgetStore";
2626
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
27-
import { Container, MAX_PINNED, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
27+
import { MAX_PINNED, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
2828
import AccessibleButton from "../elements/AccessibleButton";
2929
import WidgetAvatar from "../avatars/WidgetAvatar";
3030
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
@@ -58,18 +58,18 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
5858
});
5959
};
6060

61-
const isPinned = WidgetLayoutStore.instance.isInContainer(room, app, Container.Top);
61+
const isPinned = WidgetLayoutStore.instance.isInContainer(room, app, "top");
6262
const togglePin = isPinned
6363
? () => {
64-
WidgetLayoutStore.instance.moveToContainer(room, app, Container.Right);
64+
WidgetLayoutStore.instance.moveToContainer(room, app, "right");
6565
}
6666
: () => {
67-
WidgetLayoutStore.instance.moveToContainer(room, app, Container.Top);
67+
WidgetLayoutStore.instance.moveToContainer(room, app, "top");
6868
};
6969

7070
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
7171

72-
const cannotPin = !isPinned && !WidgetLayoutStore.instance.canAddToContainer(room, Container.Top);
72+
const cannotPin = !isPinned && !WidgetLayoutStore.instance.canAddToContainer(room, "top");
7373

7474
let pinTitle: string;
7575
if (cannotPin) {
@@ -78,7 +78,7 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
7878
pinTitle = isPinned ? _t("action|unpin") : _t("action|pin");
7979
}
8080

81-
const isMaximised = WidgetLayoutStore.instance.isInContainer(room, app, Container.Center);
81+
const isMaximised = WidgetLayoutStore.instance.isInContainer(room, app, "center");
8282

8383
let openTitle = "";
8484
if (isPinned) {

apps/web/src/components/views/right_panel/WidgetCard.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import WidgetUtils, { useWidgets } from "../../../utils/WidgetUtils";
1515
import AppTile from "../elements/AppTile";
1616
import { _t } from "../../../languageHandler";
1717
import { ContextMenuButton, useContextMenu } from "../../structures/ContextMenu";
18-
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
18+
import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
1919
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
2020
import Heading from "../typography/Heading";
2121
import { WidgetContextMenu } from "../../../viewmodels/right-panel/WidgetContextMenuViewModel";
@@ -31,7 +31,7 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
3131

3232
const apps = useWidgets(room);
3333
const app = apps.find((a) => a.id === widgetId);
34-
const isRight = app && WidgetLayoutStore.instance.isInContainer(room, app, Container.Right);
34+
const isRight = app && WidgetLayoutStore.instance.isInContainer(room, app, "right");
3535

3636
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
3737

apps/web/src/components/views/rooms/AppsDrawer.tsx

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import type ResizeNotifier from "../../../utils/ResizeNotifier";
2222
import ResizeHandle from "../elements/ResizeHandle";
2323
import Resizer, { type IConfig } from "../../../resizer/resizer";
2424
import PercentageDistributor from "../../../resizer/distributors/percentage";
25-
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
25+
import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
2626
import UIStore from "../../../stores/UIStore";
2727
import { type ActionPayload } from "../../../dispatcher/payloads";
2828
import Spinner from "../elements/Spinner";
@@ -39,9 +39,9 @@ interface IProps {
3939

4040
interface IState {
4141
apps: {
42-
[Container.Top]: IWidget[];
43-
[Container.Center]: IWidget[];
44-
[Container.Right]?: IWidget[];
42+
["top"]: IWidget[];
43+
["center"]: IWidget[];
44+
["right"]?: IWidget[];
4545
};
4646
resizingVertical: boolean; // true when changing the height of the apps drawer
4747
resizingHorizontal: boolean; // true when changing the distribution of the width between widgets
@@ -119,7 +119,7 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
119119
this.resizeContainer?.classList.remove("mx_AppsDrawer--resizing");
120120
WidgetLayoutStore.instance.setResizerDistributions(
121121
this.props.room,
122-
Container.Top,
122+
"top",
123123
this.topApps()
124124
.slice(1)
125125
.map((_, i) => this.resizer.forHandleAt(i)!.size),
@@ -152,7 +152,7 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
152152
if (prevProps.userId !== this.props.userId || prevProps.room !== this.props.room) {
153153
// Room has changed, update apps
154154
this.updateApps();
155-
} else if (this.getAppsHash(this.topApps()) !== this.getAppsHash(prevState.apps[Container.Top])) {
155+
} else if (this.getAppsHash(this.topApps()) !== this.getAppsHash(prevState.apps["top"])) {
156156
this.loadResizerPreferences();
157157
}
158158
}
@@ -166,7 +166,7 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
166166
};
167167

168168
private loadResizerPreferences = (): void => {
169-
const distributions = WidgetLayoutStore.instance.getResizerDistributions(this.props.room, Container.Top);
169+
const distributions = WidgetLayoutStore.instance.getResizerDistributions(this.props.room, "top");
170170
if (this.state.apps && this.topApps().length - 1 === distributions.length) {
171171
distributions.forEach((size, i) => {
172172
const distributor = this.resizer.forHandleAt(i);
@@ -206,11 +206,11 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
206206
};
207207

208208
private getApps = (): IState["apps"] => ({
209-
[Container.Top]: WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Top),
210-
[Container.Center]: WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Center),
209+
["top"]: WidgetLayoutStore.instance.getContainerWidgets(this.props.room, "top"),
210+
["center"]: WidgetLayoutStore.instance.getContainerWidgets(this.props.room, "center"),
211211
});
212-
private topApps = (): IWidget[] => this.state.apps[Container.Top];
213-
private centerApps = (): IWidget[] => this.state.apps[Container.Center];
212+
private topApps = (): IWidget[] => this.state.apps["top"];
213+
private centerApps = (): IWidget[] => this.state.apps["center"];
214214

215215
private updateApps = (): void => {
216216
if (this.unmounted) return;
@@ -321,7 +321,7 @@ const PersistentVResizer: React.FC<IPersistentResizerProps> = ({
321321
resizeNotifier,
322322
children,
323323
}) => {
324-
let defaultHeight = WidgetLayoutStore.instance.getContainerHeight(room, Container.Top);
324+
let defaultHeight = WidgetLayoutStore.instance.getContainerHeight(room, "top");
325325

326326
// Arbitrary defaults to avoid NaN problems. 100 px or 3/4 of the visible window.
327327
if (!minHeight) minHeight = 100;
@@ -352,7 +352,7 @@ const PersistentVResizer: React.FC<IPersistentResizerProps> = ({
352352
let newHeight = defaultHeight! + d.height;
353353
newHeight = percentageOf(newHeight, minHeight, maxHeight) * 100;
354354

355-
WidgetLayoutStore.instance.setContainerHeight(room, Container.Top, newHeight);
355+
WidgetLayoutStore.instance.setContainerHeight(room, "top", newHeight);
356356

357357
resizeNotifier.stopResizing();
358358
}}

apps/web/src/components/views/rooms/RoomHeader/RoomHeader.tsx

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,12 @@ import { useIsEncrypted } from "../../../../hooks/useIsEncrypted.ts";
6060

6161
function RoomHeaderButtons({
6262
room,
63-
additionalButtons,
63+
legacyAdditionalButtons,
64+
extraButtons,
6465
}: {
6566
room: Room;
66-
additionalButtons?: ViewRoomOpts["buttons"];
67+
legacyAdditionalButtons?: ViewRoomOpts["buttons"];
68+
extraButtons?: JSX.Element;
6769
}): JSX.Element {
6870
const members = useRoomMembers(room, 2500);
6971
const memberCount = useRoomMemberCount(room, { throttleWait: 2500, includeInvited: true });
@@ -297,7 +299,9 @@ function RoomHeaderButtons({
297299
roomContext.mainSplitContentType === MainSplitContentType.Call;
298300
return (
299301
<>
300-
{additionalButtons?.map((props) => {
302+
{extraButtons}
303+
304+
{legacyAdditionalButtons?.map((props) => {
301305
const label = props.label();
302306

303307
return (
@@ -427,11 +431,15 @@ function historyVisibilityIcon(historyVisibility: HistoryVisibility): JSX.Elemen
427431

428432
export default function RoomHeader({
429433
room,
430-
additionalButtons,
434+
extraButtons,
435+
legacyAdditionalButtons,
431436
oobData,
432437
}: {
433438
room: Room | LocalRoom;
434-
additionalButtons?: ViewRoomOpts["buttons"];
439+
// Extra buttons added by a new element web module API module
440+
extraButtons?: JSX.Element;
441+
// DEPRECATED: Buttons added by a legacy react-sdk module API module.
442+
legacyAdditionalButtons?: ViewRoomOpts["buttons"];
435443
oobData?: IOOBData;
436444
}): JSX.Element {
437445
const client = useMatrixClientContext();
@@ -530,7 +538,11 @@ export default function RoomHeader({
530538
</button>
531539
{/* If the room is local-only then we don't want to show any additional buttons, as it won't work */}
532540
{room instanceof LocalRoom === false && (
533-
<RoomHeaderButtons room={room} additionalButtons={additionalButtons} />
541+
<RoomHeaderButtons
542+
room={room}
543+
legacyAdditionalButtons={legacyAdditionalButtons}
544+
extraButtons={extraButtons}
545+
/>
534546
)}
535547
</Flex>
536548
{askToJoinEnabled && <RoomKnocksBar room={room} />}

0 commit comments

Comments
 (0)