Add support for Widget & Room Header Buttons module APIs (#32734)
* Add support for Widget & Room Header Buttons module APIs To support https://github.com/element-hq/element-modules/pull/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>
This commit is contained in:
co-authored by
Florian Duros
parent
86692ce0a7
commit
09bbf796dc
@@ -90,7 +90,7 @@ import { CallView } from "../views/voip/CallView";
|
||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||
import Notifier from "../../Notifier";
|
||||
import { showToast as showNotificationsToast } from "../../toasts/DesktopNotificationsToast";
|
||||
import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
|
||||
import { WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
|
||||
import { getKeyBindingsManager } from "../../KeyBindingsManager";
|
||||
import { objectHasDiff } from "../../utils/objects";
|
||||
import SpaceRoomView from "./SpaceRoomView";
|
||||
@@ -141,6 +141,7 @@ import { isRoomEncrypted } from "../../hooks/useIsEncrypted";
|
||||
import { type RoomViewStore } from "../../stores/RoomViewStore.tsx";
|
||||
import { RoomStatusBarViewModel } from "../../viewmodels/room/RoomStatusBar.ts";
|
||||
import { EncryptionEventViewModel } from "../../viewmodels/event-tiles/EncryptionEventViewModel.ts";
|
||||
import { ModuleApi } from "../../modules/Api.ts";
|
||||
|
||||
const DEBUG = false;
|
||||
const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000;
|
||||
@@ -953,7 +954,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
// Otherwise (in case the user set hideWidgetDrawer by clicking the button) follow the parameter.
|
||||
const isManuallyShown = hideWidgetDrawer ? hideWidgetDrawer === "false" : true;
|
||||
|
||||
const widgets = this.context.widgetLayoutStore.getContainerWidgets(room, Container.Top);
|
||||
const widgets = this.context.widgetLayoutStore.getContainerWidgets(room, "top");
|
||||
return isManuallyShown && widgets.length > 0;
|
||||
}
|
||||
|
||||
@@ -2727,6 +2728,12 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
this.state.mainSplitContentType === MainSplitContentType.Call ? "video_room" : "maximised_widget";
|
||||
}
|
||||
|
||||
const extraButtons: JSX.Element[] = [];
|
||||
for (const cb of ModuleApi.instance.extras.roomHeaderButtonsCallbacks) {
|
||||
const b = cb(this.state.room.roomId);
|
||||
if (b) extraButtons.push(b);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScopedRoomContextProvider {...this.state} roomViewStore={this.roomViewStore}>
|
||||
<div
|
||||
@@ -2754,7 +2761,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
{!this.props.hideHeader && (
|
||||
<RoomHeader
|
||||
room={this.state.room}
|
||||
additionalButtons={this.state.viewRoomOpts.buttons}
|
||||
legacyAdditionalButtons={this.state.viewRoomOpts.buttons}
|
||||
extraButtons={<>{extraButtons}</>}
|
||||
/>
|
||||
)}
|
||||
{mainSplitBody}
|
||||
|
||||
@@ -25,7 +25,7 @@ import QuestionDialog from "../dialogs/QuestionDialog";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import { WidgetType } from "../../../widgets/WidgetType";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import { getConfigLivestreamUrl, startJitsiAudioLivestream } from "../../../Livestream";
|
||||
import { ModuleRunner } from "../../../modules/ModuleRunner";
|
||||
import { ElementWidget, type WidgetMessaging } from "../../../stores/widgets/WidgetMessaging";
|
||||
@@ -79,7 +79,7 @@ const showSnapshotButton = (widgetMessaging: WidgetMessaging | undefined): boole
|
||||
const showMoveButtons = (app: IWidget, room: Room | undefined, showUnpin: boolean | undefined): [boolean, boolean] => {
|
||||
if (!showUnpin) return [false, false];
|
||||
|
||||
const pinnedWidgets = room ? WidgetLayoutStore.instance.getContainerWidgets(room, Container.Top) : [];
|
||||
const pinnedWidgets = room ? WidgetLayoutStore.instance.getContainerWidgets(room, "top") : [];
|
||||
const widgetIndex = pinnedWidgets.findIndex((widget) => widget.id === app.id);
|
||||
return [widgetIndex > 0, widgetIndex < pinnedWidgets.length - 1];
|
||||
};
|
||||
@@ -221,7 +221,7 @@ export const WidgetContextMenu: React.FC<IProps> = ({
|
||||
if (showMoveLeftButton) {
|
||||
const onClick = (): void => {
|
||||
if (!room) throw new Error("room must be defined");
|
||||
WidgetLayoutStore.instance.moveWithinContainer(room, Container.Top, app, -1);
|
||||
WidgetLayoutStore.instance.moveWithinContainer(room, "top", app, -1);
|
||||
onFinished();
|
||||
};
|
||||
|
||||
@@ -232,7 +232,7 @@ export const WidgetContextMenu: React.FC<IProps> = ({
|
||||
if (showMoveRightButton) {
|
||||
const onClick = (): void => {
|
||||
if (!room) throw new Error("room must be defined");
|
||||
WidgetLayoutStore.instance.moveWithinContainer(room, Container.Top, app, 1);
|
||||
WidgetLayoutStore.instance.moveWithinContainer(room, "top", app, 1);
|
||||
onFinished();
|
||||
};
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ import { ElementWidget, WidgetMessaging, WidgetMessagingEvent } from "../../../s
|
||||
import WidgetAvatar from "../avatars/WidgetAvatar";
|
||||
import LegacyCallHandler from "../../../LegacyCallHandler";
|
||||
import { type IApp, isAppWidget } from "../../../stores/WidgetStore";
|
||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import { OwnProfileStore } from "../../../stores/OwnProfileStore";
|
||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||
import WidgetUtils from "../../../utils/WidgetUtils";
|
||||
@@ -682,21 +682,17 @@ export default class AppTile extends React.Component<IProps, IState> {
|
||||
|
||||
private onToggleMaximisedClick = (): void => {
|
||||
if (!this.props.room) return; // ignore action - it shouldn't even be visible
|
||||
const targetContainer = WidgetLayoutStore.instance.isInContainer(
|
||||
this.props.room,
|
||||
this.props.app,
|
||||
Container.Center,
|
||||
)
|
||||
? Container.Top
|
||||
: Container.Center;
|
||||
const targetContainer = WidgetLayoutStore.instance.isInContainer(this.props.room, this.props.app, "center")
|
||||
? "top"
|
||||
: "center";
|
||||
WidgetLayoutStore.instance.moveToContainer(this.props.room, this.props.app, targetContainer);
|
||||
|
||||
if (targetContainer === Container.Top) this.closeChatCardIfNeeded();
|
||||
if (targetContainer === "top") this.closeChatCardIfNeeded();
|
||||
};
|
||||
|
||||
private onMinimiseClicked = (): void => {
|
||||
if (!this.props.room) return; // ignore action - it shouldn't even be visible
|
||||
WidgetLayoutStore.instance.moveToContainer(this.props.room, this.props.app, Container.Right);
|
||||
WidgetLayoutStore.instance.moveToContainer(this.props.room, this.props.app, "right");
|
||||
this.closeChatCardIfNeeded();
|
||||
};
|
||||
|
||||
@@ -822,8 +818,7 @@ export default class AppTile extends React.Component<IProps, IState> {
|
||||
const layoutButtons: ReactNode[] = [];
|
||||
if (this.props.showLayoutButtons) {
|
||||
const isMaximised =
|
||||
this.props.room &&
|
||||
WidgetLayoutStore.instance.isInContainer(this.props.room, this.props.app, Container.Center);
|
||||
this.props.room && WidgetLayoutStore.instance.isInContainer(this.props.room, this.props.app, "center");
|
||||
|
||||
layoutButtons.push(
|
||||
<AccessibleButton
|
||||
|
||||
@@ -14,7 +14,7 @@ import { EventTileBubble } from "@element-hq/web-shared-components";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import WidgetStore from "../../../stores/WidgetStore";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
@@ -32,7 +32,7 @@ export default class MJitsiWidgetEvent extends React.PureComponent<IProps> {
|
||||
const widget = WidgetStore.instance.getRoom(room.roomId, true).widgets.find((w) => w.id === widgetId);
|
||||
|
||||
let joinCopy: string | null = _t("timeline|m.widget|jitsi_join_top_prompt");
|
||||
if (widget && WidgetLayoutStore.instance.isInContainer(room, widget, Container.Right)) {
|
||||
if (widget && WidgetLayoutStore.instance.isInContainer(room, widget, "right")) {
|
||||
joinCopy = _t("timeline|m.widget|jitsi_join_right_prompt");
|
||||
} else if (!widget) {
|
||||
joinCopy = null;
|
||||
|
||||
@@ -24,7 +24,7 @@ import { useContextMenu } from "../../structures/ContextMenu";
|
||||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||
import { type IApp } from "../../../stores/WidgetStore";
|
||||
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
||||
import { Container, MAX_PINNED, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import { MAX_PINNED, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import WidgetAvatar from "../avatars/WidgetAvatar";
|
||||
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
|
||||
@@ -58,18 +58,18 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const isPinned = WidgetLayoutStore.instance.isInContainer(room, app, Container.Top);
|
||||
const isPinned = WidgetLayoutStore.instance.isInContainer(room, app, "top");
|
||||
const togglePin = isPinned
|
||||
? () => {
|
||||
WidgetLayoutStore.instance.moveToContainer(room, app, Container.Right);
|
||||
WidgetLayoutStore.instance.moveToContainer(room, app, "right");
|
||||
}
|
||||
: () => {
|
||||
WidgetLayoutStore.instance.moveToContainer(room, app, Container.Top);
|
||||
WidgetLayoutStore.instance.moveToContainer(room, app, "top");
|
||||
};
|
||||
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
|
||||
|
||||
const cannotPin = !isPinned && !WidgetLayoutStore.instance.canAddToContainer(room, Container.Top);
|
||||
const cannotPin = !isPinned && !WidgetLayoutStore.instance.canAddToContainer(room, "top");
|
||||
|
||||
let pinTitle: string;
|
||||
if (cannotPin) {
|
||||
@@ -78,7 +78,7 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
|
||||
pinTitle = isPinned ? _t("action|unpin") : _t("action|pin");
|
||||
}
|
||||
|
||||
const isMaximised = WidgetLayoutStore.instance.isInContainer(room, app, Container.Center);
|
||||
const isMaximised = WidgetLayoutStore.instance.isInContainer(room, app, "center");
|
||||
|
||||
let openTitle = "";
|
||||
if (isPinned) {
|
||||
|
||||
@@ -15,7 +15,7 @@ import WidgetUtils, { useWidgets } from "../../../utils/WidgetUtils";
|
||||
import AppTile from "../elements/AppTile";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { ContextMenuButton, useContextMenu } from "../../structures/ContextMenu";
|
||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||
import Heading from "../typography/Heading";
|
||||
import { WidgetContextMenu } from "../../../viewmodels/right-panel/WidgetContextMenuViewModel";
|
||||
@@ -31,7 +31,7 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
|
||||
|
||||
const apps = useWidgets(room);
|
||||
const app = apps.find((a) => a.id === widgetId);
|
||||
const isRight = app && WidgetLayoutStore.instance.isInContainer(room, app, Container.Right);
|
||||
const isRight = app && WidgetLayoutStore.instance.isInContainer(room, app, "right");
|
||||
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import type ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||
import ResizeHandle from "../elements/ResizeHandle";
|
||||
import Resizer, { type IConfig } from "../../../resizer/resizer";
|
||||
import PercentageDistributor from "../../../resizer/distributors/percentage";
|
||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
import { type ActionPayload } from "../../../dispatcher/payloads";
|
||||
import Spinner from "../elements/Spinner";
|
||||
@@ -39,9 +39,9 @@ interface IProps {
|
||||
|
||||
interface IState {
|
||||
apps: {
|
||||
[Container.Top]: IWidget[];
|
||||
[Container.Center]: IWidget[];
|
||||
[Container.Right]?: IWidget[];
|
||||
["top"]: IWidget[];
|
||||
["center"]: IWidget[];
|
||||
["right"]?: IWidget[];
|
||||
};
|
||||
resizingVertical: boolean; // true when changing the height of the apps drawer
|
||||
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> {
|
||||
this.resizeContainer?.classList.remove("mx_AppsDrawer--resizing");
|
||||
WidgetLayoutStore.instance.setResizerDistributions(
|
||||
this.props.room,
|
||||
Container.Top,
|
||||
"top",
|
||||
this.topApps()
|
||||
.slice(1)
|
||||
.map((_, i) => this.resizer.forHandleAt(i)!.size),
|
||||
@@ -152,7 +152,7 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
|
||||
if (prevProps.userId !== this.props.userId || prevProps.room !== this.props.room) {
|
||||
// Room has changed, update apps
|
||||
this.updateApps();
|
||||
} else if (this.getAppsHash(this.topApps()) !== this.getAppsHash(prevState.apps[Container.Top])) {
|
||||
} else if (this.getAppsHash(this.topApps()) !== this.getAppsHash(prevState.apps["top"])) {
|
||||
this.loadResizerPreferences();
|
||||
}
|
||||
}
|
||||
@@ -166,7 +166,7 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
|
||||
};
|
||||
|
||||
private loadResizerPreferences = (): void => {
|
||||
const distributions = WidgetLayoutStore.instance.getResizerDistributions(this.props.room, Container.Top);
|
||||
const distributions = WidgetLayoutStore.instance.getResizerDistributions(this.props.room, "top");
|
||||
if (this.state.apps && this.topApps().length - 1 === distributions.length) {
|
||||
distributions.forEach((size, i) => {
|
||||
const distributor = this.resizer.forHandleAt(i);
|
||||
@@ -206,11 +206,11 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
|
||||
};
|
||||
|
||||
private getApps = (): IState["apps"] => ({
|
||||
[Container.Top]: WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Top),
|
||||
[Container.Center]: WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Center),
|
||||
["top"]: WidgetLayoutStore.instance.getContainerWidgets(this.props.room, "top"),
|
||||
["center"]: WidgetLayoutStore.instance.getContainerWidgets(this.props.room, "center"),
|
||||
});
|
||||
private topApps = (): IWidget[] => this.state.apps[Container.Top];
|
||||
private centerApps = (): IWidget[] => this.state.apps[Container.Center];
|
||||
private topApps = (): IWidget[] => this.state.apps["top"];
|
||||
private centerApps = (): IWidget[] => this.state.apps["center"];
|
||||
|
||||
private updateApps = (): void => {
|
||||
if (this.unmounted) return;
|
||||
@@ -321,7 +321,7 @@ const PersistentVResizer: React.FC<IPersistentResizerProps> = ({
|
||||
resizeNotifier,
|
||||
children,
|
||||
}) => {
|
||||
let defaultHeight = WidgetLayoutStore.instance.getContainerHeight(room, Container.Top);
|
||||
let defaultHeight = WidgetLayoutStore.instance.getContainerHeight(room, "top");
|
||||
|
||||
// Arbitrary defaults to avoid NaN problems. 100 px or 3/4 of the visible window.
|
||||
if (!minHeight) minHeight = 100;
|
||||
@@ -352,7 +352,7 @@ const PersistentVResizer: React.FC<IPersistentResizerProps> = ({
|
||||
let newHeight = defaultHeight! + d.height;
|
||||
newHeight = percentageOf(newHeight, minHeight, maxHeight) * 100;
|
||||
|
||||
WidgetLayoutStore.instance.setContainerHeight(room, Container.Top, newHeight);
|
||||
WidgetLayoutStore.instance.setContainerHeight(room, "top", newHeight);
|
||||
|
||||
resizeNotifier.stopResizing();
|
||||
}}
|
||||
|
||||
@@ -60,10 +60,12 @@ import { useIsEncrypted } from "../../../../hooks/useIsEncrypted.ts";
|
||||
|
||||
function RoomHeaderButtons({
|
||||
room,
|
||||
additionalButtons,
|
||||
legacyAdditionalButtons,
|
||||
extraButtons,
|
||||
}: {
|
||||
room: Room;
|
||||
additionalButtons?: ViewRoomOpts["buttons"];
|
||||
legacyAdditionalButtons?: ViewRoomOpts["buttons"];
|
||||
extraButtons?: JSX.Element;
|
||||
}): JSX.Element {
|
||||
const members = useRoomMembers(room, 2500);
|
||||
const memberCount = useRoomMemberCount(room, { throttleWait: 2500, includeInvited: true });
|
||||
@@ -297,7 +299,9 @@ function RoomHeaderButtons({
|
||||
roomContext.mainSplitContentType === MainSplitContentType.Call;
|
||||
return (
|
||||
<>
|
||||
{additionalButtons?.map((props) => {
|
||||
{extraButtons}
|
||||
|
||||
{legacyAdditionalButtons?.map((props) => {
|
||||
const label = props.label();
|
||||
|
||||
return (
|
||||
@@ -427,11 +431,15 @@ function historyVisibilityIcon(historyVisibility: HistoryVisibility): JSX.Elemen
|
||||
|
||||
export default function RoomHeader({
|
||||
room,
|
||||
additionalButtons,
|
||||
extraButtons,
|
||||
legacyAdditionalButtons,
|
||||
oobData,
|
||||
}: {
|
||||
room: Room | LocalRoom;
|
||||
additionalButtons?: ViewRoomOpts["buttons"];
|
||||
// Extra buttons added by a new element web module API module
|
||||
extraButtons?: JSX.Element;
|
||||
// DEPRECATED: Buttons added by a legacy react-sdk module API module.
|
||||
legacyAdditionalButtons?: ViewRoomOpts["buttons"];
|
||||
oobData?: IOOBData;
|
||||
}): JSX.Element {
|
||||
const client = useMatrixClientContext();
|
||||
@@ -530,7 +538,11 @@ export default function RoomHeader({
|
||||
</button>
|
||||
{/* If the room is local-only then we don't want to show any additional buttons, as it won't work */}
|
||||
{room instanceof LocalRoom === false && (
|
||||
<RoomHeaderButtons room={room} additionalButtons={additionalButtons} />
|
||||
<RoomHeaderButtons
|
||||
room={room}
|
||||
legacyAdditionalButtons={legacyAdditionalButtons}
|
||||
extraButtons={extraButtons}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
{askToJoinEnabled && <RoomKnocksBar room={room} />}
|
||||
|
||||
Reference in New Issue
Block a user