mirror of
https://github.com/Novaloop-AG/healthchat-web.git
synced 2026-11-06 06:28:27 +00:00
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:
@@ -42,7 +42,7 @@ import { Action } from "./dispatcher/actions";
|
|||||||
import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from "./widgets/ManagedHybrid";
|
import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from "./widgets/ManagedHybrid";
|
||||||
import SdkConfig from "./SdkConfig";
|
import SdkConfig from "./SdkConfig";
|
||||||
import { ensureDMExists } from "./createRoom";
|
import { ensureDMExists } from "./createRoom";
|
||||||
import { Container, WidgetLayoutStore } from "./stores/widgets/WidgetLayoutStore";
|
import { WidgetLayoutStore } from "./stores/widgets/WidgetLayoutStore";
|
||||||
import IncomingLegacyCallToast, { getIncomingLegacyCallToastKey } from "./toasts/IncomingLegacyCallToast";
|
import IncomingLegacyCallToast, { getIncomingLegacyCallToastKey } from "./toasts/IncomingLegacyCallToast";
|
||||||
import ToastStore from "./stores/ToastStore";
|
import ToastStore from "./stores/ToastStore";
|
||||||
import { type ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload";
|
import { type ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload";
|
||||||
@@ -1027,7 +1027,7 @@ export default class LegacyCallHandler extends TypedEventEmitter<LegacyCallHandl
|
|||||||
// If there already is a Jitsi widget, pin it
|
// If there already is a Jitsi widget, pin it
|
||||||
const room = client.getRoom(roomId);
|
const room = client.getRoom(roomId);
|
||||||
if (isNotNull(room)) {
|
if (isNotNull(room)) {
|
||||||
WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Top);
|
WidgetLayoutStore.instance.moveToContainer(room, widget, "top");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ import { CallView } from "../views/voip/CallView";
|
|||||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||||
import Notifier from "../../Notifier";
|
import Notifier from "../../Notifier";
|
||||||
import { showToast as showNotificationsToast } from "../../toasts/DesktopNotificationsToast";
|
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 { getKeyBindingsManager } from "../../KeyBindingsManager";
|
||||||
import { objectHasDiff } from "../../utils/objects";
|
import { objectHasDiff } from "../../utils/objects";
|
||||||
import SpaceRoomView from "./SpaceRoomView";
|
import SpaceRoomView from "./SpaceRoomView";
|
||||||
@@ -141,6 +141,7 @@ import { isRoomEncrypted } from "../../hooks/useIsEncrypted";
|
|||||||
import { type RoomViewStore } from "../../stores/RoomViewStore.tsx";
|
import { type RoomViewStore } from "../../stores/RoomViewStore.tsx";
|
||||||
import { RoomStatusBarViewModel } from "../../viewmodels/room/RoomStatusBar.ts";
|
import { RoomStatusBarViewModel } from "../../viewmodels/room/RoomStatusBar.ts";
|
||||||
import { EncryptionEventViewModel } from "../../viewmodels/event-tiles/EncryptionEventViewModel.ts";
|
import { EncryptionEventViewModel } from "../../viewmodels/event-tiles/EncryptionEventViewModel.ts";
|
||||||
|
import { ModuleApi } from "../../modules/Api.ts";
|
||||||
|
|
||||||
const DEBUG = false;
|
const DEBUG = false;
|
||||||
const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000;
|
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.
|
// Otherwise (in case the user set hideWidgetDrawer by clicking the button) follow the parameter.
|
||||||
const isManuallyShown = hideWidgetDrawer ? hideWidgetDrawer === "false" : true;
|
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;
|
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";
|
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 (
|
return (
|
||||||
<ScopedRoomContextProvider {...this.state} roomViewStore={this.roomViewStore}>
|
<ScopedRoomContextProvider {...this.state} roomViewStore={this.roomViewStore}>
|
||||||
<div
|
<div
|
||||||
@@ -2754,7 +2761,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||||||
{!this.props.hideHeader && (
|
{!this.props.hideHeader && (
|
||||||
<RoomHeader
|
<RoomHeader
|
||||||
room={this.state.room}
|
room={this.state.room}
|
||||||
additionalButtons={this.state.viewRoomOpts.buttons}
|
legacyAdditionalButtons={this.state.viewRoomOpts.buttons}
|
||||||
|
extraButtons={<>{extraButtons}</>}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{mainSplitBody}
|
{mainSplitBody}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import QuestionDialog from "../dialogs/QuestionDialog";
|
|||||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||||
import { WidgetType } from "../../../widgets/WidgetType";
|
import { WidgetType } from "../../../widgets/WidgetType";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||||
import { getConfigLivestreamUrl, startJitsiAudioLivestream } from "../../../Livestream";
|
import { getConfigLivestreamUrl, startJitsiAudioLivestream } from "../../../Livestream";
|
||||||
import { ModuleRunner } from "../../../modules/ModuleRunner";
|
import { ModuleRunner } from "../../../modules/ModuleRunner";
|
||||||
import { ElementWidget, type WidgetMessaging } from "../../../stores/widgets/WidgetMessaging";
|
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] => {
|
const showMoveButtons = (app: IWidget, room: Room | undefined, showUnpin: boolean | undefined): [boolean, boolean] => {
|
||||||
if (!showUnpin) return [false, false];
|
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);
|
const widgetIndex = pinnedWidgets.findIndex((widget) => widget.id === app.id);
|
||||||
return [widgetIndex > 0, widgetIndex < pinnedWidgets.length - 1];
|
return [widgetIndex > 0, widgetIndex < pinnedWidgets.length - 1];
|
||||||
};
|
};
|
||||||
@@ -221,7 +221,7 @@ export const WidgetContextMenu: React.FC<IProps> = ({
|
|||||||
if (showMoveLeftButton) {
|
if (showMoveLeftButton) {
|
||||||
const onClick = (): void => {
|
const onClick = (): void => {
|
||||||
if (!room) throw new Error("room must be defined");
|
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();
|
onFinished();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -232,7 +232,7 @@ export const WidgetContextMenu: React.FC<IProps> = ({
|
|||||||
if (showMoveRightButton) {
|
if (showMoveRightButton) {
|
||||||
const onClick = (): void => {
|
const onClick = (): void => {
|
||||||
if (!room) throw new Error("room must be defined");
|
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();
|
onFinished();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ import { ElementWidget, WidgetMessaging, WidgetMessagingEvent } from "../../../s
|
|||||||
import WidgetAvatar from "../avatars/WidgetAvatar";
|
import WidgetAvatar from "../avatars/WidgetAvatar";
|
||||||
import LegacyCallHandler from "../../../LegacyCallHandler";
|
import LegacyCallHandler from "../../../LegacyCallHandler";
|
||||||
import { type IApp, isAppWidget } from "../../../stores/WidgetStore";
|
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 { OwnProfileStore } from "../../../stores/OwnProfileStore";
|
||||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||||
import WidgetUtils from "../../../utils/WidgetUtils";
|
import WidgetUtils from "../../../utils/WidgetUtils";
|
||||||
@@ -682,21 +682,17 @@ export default class AppTile extends React.Component<IProps, IState> {
|
|||||||
|
|
||||||
private onToggleMaximisedClick = (): void => {
|
private onToggleMaximisedClick = (): void => {
|
||||||
if (!this.props.room) return; // ignore action - it shouldn't even be visible
|
if (!this.props.room) return; // ignore action - it shouldn't even be visible
|
||||||
const targetContainer = WidgetLayoutStore.instance.isInContainer(
|
const targetContainer = WidgetLayoutStore.instance.isInContainer(this.props.room, this.props.app, "center")
|
||||||
this.props.room,
|
? "top"
|
||||||
this.props.app,
|
: "center";
|
||||||
Container.Center,
|
|
||||||
)
|
|
||||||
? Container.Top
|
|
||||||
: Container.Center;
|
|
||||||
WidgetLayoutStore.instance.moveToContainer(this.props.room, this.props.app, targetContainer);
|
WidgetLayoutStore.instance.moveToContainer(this.props.room, this.props.app, targetContainer);
|
||||||
|
|
||||||
if (targetContainer === Container.Top) this.closeChatCardIfNeeded();
|
if (targetContainer === "top") this.closeChatCardIfNeeded();
|
||||||
};
|
};
|
||||||
|
|
||||||
private onMinimiseClicked = (): void => {
|
private onMinimiseClicked = (): void => {
|
||||||
if (!this.props.room) return; // ignore action - it shouldn't even be visible
|
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();
|
this.closeChatCardIfNeeded();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -822,8 +818,7 @@ export default class AppTile extends React.Component<IProps, IState> {
|
|||||||
const layoutButtons: ReactNode[] = [];
|
const layoutButtons: ReactNode[] = [];
|
||||||
if (this.props.showLayoutButtons) {
|
if (this.props.showLayoutButtons) {
|
||||||
const isMaximised =
|
const isMaximised =
|
||||||
this.props.room &&
|
this.props.room && WidgetLayoutStore.instance.isInContainer(this.props.room, this.props.app, "center");
|
||||||
WidgetLayoutStore.instance.isInContainer(this.props.room, this.props.app, Container.Center);
|
|
||||||
|
|
||||||
layoutButtons.push(
|
layoutButtons.push(
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { EventTileBubble } from "@element-hq/web-shared-components";
|
|||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import WidgetStore from "../../../stores/WidgetStore";
|
import WidgetStore from "../../../stores/WidgetStore";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
mxEvent: MatrixEvent;
|
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);
|
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");
|
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");
|
joinCopy = _t("timeline|m.widget|jitsi_join_right_prompt");
|
||||||
} else if (!widget) {
|
} else if (!widget) {
|
||||||
joinCopy = null;
|
joinCopy = null;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { useContextMenu } from "../../structures/ContextMenu";
|
|||||||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||||
import { type IApp } from "../../../stores/WidgetStore";
|
import { type IApp } from "../../../stores/WidgetStore";
|
||||||
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
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 AccessibleButton from "../elements/AccessibleButton";
|
||||||
import WidgetAvatar from "../avatars/WidgetAvatar";
|
import WidgetAvatar from "../avatars/WidgetAvatar";
|
||||||
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
|
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
|
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 [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;
|
let pinTitle: string;
|
||||||
if (cannotPin) {
|
if (cannotPin) {
|
||||||
@@ -78,7 +78,7 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
|
|||||||
pinTitle = isPinned ? _t("action|unpin") : _t("action|pin");
|
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 = "";
|
let openTitle = "";
|
||||||
if (isPinned) {
|
if (isPinned) {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import WidgetUtils, { useWidgets } from "../../../utils/WidgetUtils";
|
|||||||
import AppTile from "../elements/AppTile";
|
import AppTile from "../elements/AppTile";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { ContextMenuButton, useContextMenu } from "../../structures/ContextMenu";
|
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 RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||||
import Heading from "../typography/Heading";
|
import Heading from "../typography/Heading";
|
||||||
import { WidgetContextMenu } from "../../../viewmodels/right-panel/WidgetContextMenuViewModel";
|
import { WidgetContextMenu } from "../../../viewmodels/right-panel/WidgetContextMenuViewModel";
|
||||||
@@ -31,7 +31,7 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
|
|||||||
|
|
||||||
const apps = useWidgets(room);
|
const apps = useWidgets(room);
|
||||||
const app = apps.find((a) => a.id === widgetId);
|
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();
|
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import type ResizeNotifier from "../../../utils/ResizeNotifier";
|
|||||||
import ResizeHandle from "../elements/ResizeHandle";
|
import ResizeHandle from "../elements/ResizeHandle";
|
||||||
import Resizer, { type IConfig } from "../../../resizer/resizer";
|
import Resizer, { type IConfig } from "../../../resizer/resizer";
|
||||||
import PercentageDistributor from "../../../resizer/distributors/percentage";
|
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 UIStore from "../../../stores/UIStore";
|
||||||
import { type ActionPayload } from "../../../dispatcher/payloads";
|
import { type ActionPayload } from "../../../dispatcher/payloads";
|
||||||
import Spinner from "../elements/Spinner";
|
import Spinner from "../elements/Spinner";
|
||||||
@@ -39,9 +39,9 @@ interface IProps {
|
|||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
apps: {
|
apps: {
|
||||||
[Container.Top]: IWidget[];
|
["top"]: IWidget[];
|
||||||
[Container.Center]: IWidget[];
|
["center"]: IWidget[];
|
||||||
[Container.Right]?: IWidget[];
|
["right"]?: IWidget[];
|
||||||
};
|
};
|
||||||
resizingVertical: boolean; // true when changing the height of the apps drawer
|
resizingVertical: boolean; // true when changing the height of the apps drawer
|
||||||
resizingHorizontal: boolean; // true when changing the distribution of the width between widgets
|
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");
|
this.resizeContainer?.classList.remove("mx_AppsDrawer--resizing");
|
||||||
WidgetLayoutStore.instance.setResizerDistributions(
|
WidgetLayoutStore.instance.setResizerDistributions(
|
||||||
this.props.room,
|
this.props.room,
|
||||||
Container.Top,
|
"top",
|
||||||
this.topApps()
|
this.topApps()
|
||||||
.slice(1)
|
.slice(1)
|
||||||
.map((_, i) => this.resizer.forHandleAt(i)!.size),
|
.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) {
|
if (prevProps.userId !== this.props.userId || prevProps.room !== this.props.room) {
|
||||||
// Room has changed, update apps
|
// Room has changed, update apps
|
||||||
this.updateApps();
|
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();
|
this.loadResizerPreferences();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,7 +166,7 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private loadResizerPreferences = (): void => {
|
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) {
|
if (this.state.apps && this.topApps().length - 1 === distributions.length) {
|
||||||
distributions.forEach((size, i) => {
|
distributions.forEach((size, i) => {
|
||||||
const distributor = this.resizer.forHandleAt(i);
|
const distributor = this.resizer.forHandleAt(i);
|
||||||
@@ -206,11 +206,11 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private getApps = (): IState["apps"] => ({
|
private getApps = (): IState["apps"] => ({
|
||||||
[Container.Top]: WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Top),
|
["top"]: WidgetLayoutStore.instance.getContainerWidgets(this.props.room, "top"),
|
||||||
[Container.Center]: WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Center),
|
["center"]: WidgetLayoutStore.instance.getContainerWidgets(this.props.room, "center"),
|
||||||
});
|
});
|
||||||
private topApps = (): IWidget[] => this.state.apps[Container.Top];
|
private topApps = (): IWidget[] => this.state.apps["top"];
|
||||||
private centerApps = (): IWidget[] => this.state.apps[Container.Center];
|
private centerApps = (): IWidget[] => this.state.apps["center"];
|
||||||
|
|
||||||
private updateApps = (): void => {
|
private updateApps = (): void => {
|
||||||
if (this.unmounted) return;
|
if (this.unmounted) return;
|
||||||
@@ -321,7 +321,7 @@ const PersistentVResizer: React.FC<IPersistentResizerProps> = ({
|
|||||||
resizeNotifier,
|
resizeNotifier,
|
||||||
children,
|
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.
|
// Arbitrary defaults to avoid NaN problems. 100 px or 3/4 of the visible window.
|
||||||
if (!minHeight) minHeight = 100;
|
if (!minHeight) minHeight = 100;
|
||||||
@@ -352,7 +352,7 @@ const PersistentVResizer: React.FC<IPersistentResizerProps> = ({
|
|||||||
let newHeight = defaultHeight! + d.height;
|
let newHeight = defaultHeight! + d.height;
|
||||||
newHeight = percentageOf(newHeight, minHeight, maxHeight) * 100;
|
newHeight = percentageOf(newHeight, minHeight, maxHeight) * 100;
|
||||||
|
|
||||||
WidgetLayoutStore.instance.setContainerHeight(room, Container.Top, newHeight);
|
WidgetLayoutStore.instance.setContainerHeight(room, "top", newHeight);
|
||||||
|
|
||||||
resizeNotifier.stopResizing();
|
resizeNotifier.stopResizing();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -60,10 +60,12 @@ import { useIsEncrypted } from "../../../../hooks/useIsEncrypted.ts";
|
|||||||
|
|
||||||
function RoomHeaderButtons({
|
function RoomHeaderButtons({
|
||||||
room,
|
room,
|
||||||
additionalButtons,
|
legacyAdditionalButtons,
|
||||||
|
extraButtons,
|
||||||
}: {
|
}: {
|
||||||
room: Room;
|
room: Room;
|
||||||
additionalButtons?: ViewRoomOpts["buttons"];
|
legacyAdditionalButtons?: ViewRoomOpts["buttons"];
|
||||||
|
extraButtons?: JSX.Element;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const members = useRoomMembers(room, 2500);
|
const members = useRoomMembers(room, 2500);
|
||||||
const memberCount = useRoomMemberCount(room, { throttleWait: 2500, includeInvited: true });
|
const memberCount = useRoomMemberCount(room, { throttleWait: 2500, includeInvited: true });
|
||||||
@@ -297,7 +299,9 @@ function RoomHeaderButtons({
|
|||||||
roomContext.mainSplitContentType === MainSplitContentType.Call;
|
roomContext.mainSplitContentType === MainSplitContentType.Call;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{additionalButtons?.map((props) => {
|
{extraButtons}
|
||||||
|
|
||||||
|
{legacyAdditionalButtons?.map((props) => {
|
||||||
const label = props.label();
|
const label = props.label();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -427,11 +431,15 @@ function historyVisibilityIcon(historyVisibility: HistoryVisibility): JSX.Elemen
|
|||||||
|
|
||||||
export default function RoomHeader({
|
export default function RoomHeader({
|
||||||
room,
|
room,
|
||||||
additionalButtons,
|
extraButtons,
|
||||||
|
legacyAdditionalButtons,
|
||||||
oobData,
|
oobData,
|
||||||
}: {
|
}: {
|
||||||
room: Room | LocalRoom;
|
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;
|
oobData?: IOOBData;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const client = useMatrixClientContext();
|
const client = useMatrixClientContext();
|
||||||
@@ -530,7 +538,11 @@ export default function RoomHeader({
|
|||||||
</button>
|
</button>
|
||||||
{/* If the room is local-only then we don't want to show any additional buttons, as it won't work */}
|
{/* 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 && (
|
{room instanceof LocalRoom === false && (
|
||||||
<RoomHeaderButtons room={room} additionalButtons={additionalButtons} />
|
<RoomHeaderButtons
|
||||||
|
room={room}
|
||||||
|
legacyAdditionalButtons={legacyAdditionalButtons}
|
||||||
|
extraButtons={extraButtons}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
{askToJoinEnabled && <RoomKnocksBar room={room} />}
|
{askToJoinEnabled && <RoomKnocksBar room={room} />}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { useCall, useConnectionState, useParticipantCount } from "../useCall";
|
|||||||
import { useRoomMemberCount } from "../useRoomMembers";
|
import { useRoomMemberCount } from "../useRoomMembers";
|
||||||
import { ConnectionState } from "../../models/Call";
|
import { ConnectionState } from "../../models/Call";
|
||||||
import { placeCall } from "../../utils/room/placeCall";
|
import { placeCall } from "../../utils/room/placeCall";
|
||||||
import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
|
import { WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
|
||||||
import { useRoomState } from "../useRoomState";
|
import { useRoomState } from "../useRoomState";
|
||||||
import { _t } from "../../languageHandler";
|
import { _t } from "../../languageHandler";
|
||||||
import { isManagedHybridWidget, isManagedHybridWidgetEnabled } from "../../widgets/ManagedHybrid";
|
import { isManagedHybridWidget, isManagedHybridWidgetEnabled } from "../../widgets/ManagedHybrid";
|
||||||
@@ -214,8 +214,8 @@ export const useRoomCall = (
|
|||||||
widget = groupCall?.widget ?? jitsiWidget;
|
widget = groupCall?.widget ?? jitsiWidget;
|
||||||
}
|
}
|
||||||
const updateWidgetState = useCallback((): void => {
|
const updateWidgetState = useCallback((): void => {
|
||||||
setCanPinWidget(WidgetLayoutStore.instance.canAddToContainer(room, Container.Top));
|
setCanPinWidget(WidgetLayoutStore.instance.canAddToContainer(room, "top"));
|
||||||
setWidgetPinned(!!widget && WidgetLayoutStore.instance.isInContainer(room, widget, Container.Top));
|
setWidgetPinned(!!widget && WidgetLayoutStore.instance.isInContainer(room, widget, "top"));
|
||||||
}, [room, widget]);
|
}, [room, widget]);
|
||||||
useEventEmitter(WidgetLayoutStore.instance, WidgetLayoutStore.emissionForRoom(room), updateWidgetState);
|
useEventEmitter(WidgetLayoutStore.instance, WidgetLayoutStore.emissionForRoom(room), updateWidgetState);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -266,7 +266,7 @@ export const useRoomCall = (
|
|||||||
(evt: React.MouseEvent | undefined, callPlatformType: PlatformCallType): void => {
|
(evt: React.MouseEvent | undefined, callPlatformType: PlatformCallType): void => {
|
||||||
evt?.stopPropagation();
|
evt?.stopPropagation();
|
||||||
if (widget && promptPinWidget) {
|
if (widget && promptPinWidget) {
|
||||||
WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Top);
|
WidgetLayoutStore.instance.moveToContainer(room, widget, "top");
|
||||||
} else {
|
} else {
|
||||||
placeCall(room, CallType.Voice, callPlatformType, evt?.shiftKey || undefined, true);
|
placeCall(room, CallType.Voice, callPlatformType, evt?.shiftKey || undefined, true);
|
||||||
}
|
}
|
||||||
@@ -277,7 +277,7 @@ export const useRoomCall = (
|
|||||||
(evt: React.MouseEvent | undefined, callPlatformType: PlatformCallType): void => {
|
(evt: React.MouseEvent | undefined, callPlatformType: PlatformCallType): void => {
|
||||||
evt?.stopPropagation();
|
evt?.stopPropagation();
|
||||||
if (widget && promptPinWidget) {
|
if (widget && promptPinWidget) {
|
||||||
WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Top);
|
WidgetLayoutStore.instance.moveToContainer(room, widget, "top");
|
||||||
} else {
|
} else {
|
||||||
// If we have pressed shift then always skip the lobby, otherwise `undefined` will defer
|
// If we have pressed shift then always skip the lobby, otherwise `undefined` will defer
|
||||||
// to the defaults of the call implementation.
|
// to the defaults of the call implementation.
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { ElementWebBuiltinsApi } from "./BuiltinsApi.tsx";
|
|||||||
import { ClientApi } from "./ClientApi.ts";
|
import { ClientApi } from "./ClientApi.ts";
|
||||||
import { StoresApi } from "./StoresApi.ts";
|
import { StoresApi } from "./StoresApi.ts";
|
||||||
import { WidgetLifecycleApi } from "./WidgetLifecycleApi.ts";
|
import { WidgetLifecycleApi } from "./WidgetLifecycleApi.ts";
|
||||||
|
import { WidgetApi } from "./WidgetApi.ts";
|
||||||
import { CustomisationsApi } from "./customisationsApi.ts";
|
import { CustomisationsApi } from "./customisationsApi.ts";
|
||||||
|
|
||||||
const legacyCustomisationsFactory = <T extends object>(baseCustomisations: T) => {
|
const legacyCustomisationsFactory = <T extends object>(baseCustomisations: T) => {
|
||||||
@@ -89,6 +90,7 @@ export class ModuleApi implements Api {
|
|||||||
public readonly extras = new ElementWebExtrasApi();
|
public readonly extras = new ElementWebExtrasApi();
|
||||||
public readonly builtins = new ElementWebBuiltinsApi();
|
public readonly builtins = new ElementWebBuiltinsApi();
|
||||||
public readonly widgetLifecycle = new WidgetLifecycleApi();
|
public readonly widgetLifecycle = new WidgetLifecycleApi();
|
||||||
|
public readonly widget = new WidgetApi();
|
||||||
public readonly rootNode = document.getElementById("matrixchat")!;
|
public readonly rootNode = document.getElementById("matrixchat")!;
|
||||||
public readonly client = new ClientApi();
|
public readonly client = new ClientApi();
|
||||||
public readonly stores = new StoresApi();
|
public readonly stores = new StoresApi();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
|
Copyright 2026 Element Creations Ltd.
|
||||||
Copyright 2025 New Vector Ltd.
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
@@ -6,7 +7,11 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { type SpacePanelItemProps, type ExtrasApi } from "@element-hq/element-web-module-api";
|
import {
|
||||||
|
type SpacePanelItemProps,
|
||||||
|
type ExtrasApi,
|
||||||
|
type RoomHeaderButtonsCallback,
|
||||||
|
} from "@element-hq/element-web-module-api";
|
||||||
import { TypedEventEmitter } from "matrix-js-sdk/src/matrix";
|
import { TypedEventEmitter } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import { useTypedEventEmitter } from "../hooks/useEventEmitter";
|
import { useTypedEventEmitter } from "../hooks/useEventEmitter";
|
||||||
@@ -26,6 +31,7 @@ interface EmittedEvents {
|
|||||||
export class ElementWebExtrasApi extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents> implements ExtrasApi {
|
export class ElementWebExtrasApi extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents> implements ExtrasApi {
|
||||||
public spacePanelItems = new Map<string, SpacePanelItemProps>();
|
public spacePanelItems = new Map<string, SpacePanelItemProps>();
|
||||||
public visibleRoomBySpaceKey = new Map<string, () => string[]>();
|
public visibleRoomBySpaceKey = new Map<string, () => string[]>();
|
||||||
|
public roomHeaderButtonsCallbacks: RoomHeaderButtonsCallback[] = [];
|
||||||
|
|
||||||
public setSpacePanelItem(spacekey: string, item: SpacePanelItemProps): void {
|
public setSpacePanelItem(spacekey: string, item: SpacePanelItemProps): void {
|
||||||
this.spacePanelItems.set(spacekey, item);
|
this.spacePanelItems.set(spacekey, item);
|
||||||
@@ -35,6 +41,10 @@ export class ElementWebExtrasApi extends TypedEventEmitter<keyof EmittedEvents,
|
|||||||
public getVisibleRoomBySpaceKey(spaceKey: string, cb: () => string[]): void {
|
public getVisibleRoomBySpaceKey(spaceKey: string, cb: () => string[]): void {
|
||||||
this.visibleRoomBySpaceKey.set(spaceKey, cb);
|
this.visibleRoomBySpaceKey.set(spaceKey, cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public addRoomHeaderButtonCallback(cb: RoomHeaderButtonsCallback): void {
|
||||||
|
this.roomHeaderButtonsCallbacks.push(cb);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useModuleSpacePanelItems(api: ElementWebExtrasApi): ModuleSpacePanelItem[] {
|
export function useModuleSpacePanelItems(api: ElementWebExtrasApi): ModuleSpacePanelItem[] {
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 Element Creations Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { type Container, type WidgetApi as WidgetApiInterface } from "@element-hq/element-web-module-api";
|
||||||
|
import { getHttpUriForMxc } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import type { IWidget } from "matrix-widget-api";
|
||||||
|
import WidgetStore, { isAppWidget } from "../stores/WidgetStore";
|
||||||
|
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||||
|
import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Host-side implementation of the widget API.
|
||||||
|
* Allows modules to interact with widgets, including listing widgets in rooms.
|
||||||
|
*/
|
||||||
|
export class WidgetApi implements WidgetApiInterface {
|
||||||
|
public getWidgetsInRoom(roomId: string): IWidget[] {
|
||||||
|
return WidgetStore.instance.getApps(roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAppAvatarUrl(app: IWidget, width?: number, height?: number, resizeMethod?: string): string | null {
|
||||||
|
if (!isAppWidget(app) || !app.avatar_url) return null;
|
||||||
|
return getHttpUriForMxc(
|
||||||
|
MatrixClientPeg.safeGet().getHomeserverUrl(),
|
||||||
|
app.avatar_url,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
resizeMethod,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isAppInContainer(app: IWidget, container: Container, roomId: string): boolean {
|
||||||
|
const room = MatrixClientPeg.safeGet().getRoom(roomId);
|
||||||
|
if (!room) return false;
|
||||||
|
return WidgetLayoutStore.instance.isInContainer(room, app, container);
|
||||||
|
}
|
||||||
|
|
||||||
|
public moveAppToContainer(app: IWidget, container: Container, roomId: string): void {
|
||||||
|
const room = MatrixClientPeg.safeGet().getRoom(roomId);
|
||||||
|
if (!room) return;
|
||||||
|
WidgetLayoutStore.instance.moveToContainer(room, app, container);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import { type Room, RoomStateEvent, type MatrixEvent } from "matrix-js-sdk/src/m
|
|||||||
import { MapWithDefault, recursiveMapToObject } from "matrix-js-sdk/src/utils";
|
import { MapWithDefault, recursiveMapToObject } from "matrix-js-sdk/src/utils";
|
||||||
import { type IWidget } from "matrix-widget-api";
|
import { type IWidget } from "matrix-widget-api";
|
||||||
import { clamp, defaultNumber, sum } from "@element-hq/web-shared-components";
|
import { clamp, defaultNumber, sum } from "@element-hq/web-shared-components";
|
||||||
|
import { type Container } from "@element-hq/element-web-module-api";
|
||||||
|
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
import WidgetStore, { type IApp } from "../WidgetStore";
|
import WidgetStore, { type IApp } from "../WidgetStore";
|
||||||
@@ -19,16 +20,10 @@ import { ReadyWatchingStore } from "../ReadyWatchingStore";
|
|||||||
import { SettingLevel } from "../../settings/SettingLevel";
|
import { SettingLevel } from "../../settings/SettingLevel";
|
||||||
import { arrayFastClone } from "../../utils/arrays";
|
import { arrayFastClone } from "../../utils/arrays";
|
||||||
import { UPDATE_EVENT } from "../AsyncStore";
|
import { UPDATE_EVENT } from "../AsyncStore";
|
||||||
import {
|
import { type IStoredLayout, type ILayoutStateEvent, WIDGET_LAYOUT_EVENT_TYPE, type IWidgetLayouts } from "./types";
|
||||||
Container,
|
|
||||||
type IStoredLayout,
|
|
||||||
type ILayoutStateEvent,
|
|
||||||
WIDGET_LAYOUT_EVENT_TYPE,
|
|
||||||
type IWidgetLayouts,
|
|
||||||
} from "./types";
|
|
||||||
|
|
||||||
export type { IStoredLayout, ILayoutStateEvent };
|
export type { IStoredLayout, ILayoutStateEvent };
|
||||||
export { Container, WIDGET_LAYOUT_EVENT_TYPE };
|
export { type Container, WIDGET_LAYOUT_EVENT_TYPE };
|
||||||
|
|
||||||
export type ILayoutSettings = Partial<ILayoutStateEvent> & {
|
export type ILayoutSettings = Partial<ILayoutStateEvent> & {
|
||||||
overrides?: string; // event ID for layout state event, if present
|
overrides?: string; // event ID for layout state event, if present
|
||||||
@@ -173,8 +168,8 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
|
|||||||
const stateContainer = roomLayout?.widgets?.[widget.id]?.container;
|
const stateContainer = roomLayout?.widgets?.[widget.id]?.container;
|
||||||
const manualContainer = userLayout?.widgets?.[widget.id]?.container;
|
const manualContainer = userLayout?.widgets?.[widget.id]?.container;
|
||||||
const isLegacyPinned = !!legacyPinned?.[widget.id];
|
const isLegacyPinned = !!legacyPinned?.[widget.id];
|
||||||
const defaultContainer = WidgetType.JITSI.matches(widget.type) ? Container.Top : Container.Right;
|
const defaultContainer = WidgetType.JITSI.matches(widget.type) ? "top" : "right";
|
||||||
if (manualContainer ? manualContainer === Container.Center : stateContainer === Container.Center) {
|
if (manualContainer ? manualContainer === "center" : stateContainer === "center") {
|
||||||
if (centerWidgets.length) {
|
if (centerWidgets.length) {
|
||||||
console.error("Tried to push a second widget into the center container");
|
console.error("Tried to push a second widget into the center container");
|
||||||
} else {
|
} else {
|
||||||
@@ -188,9 +183,9 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
|
|||||||
targetContainer = manualContainer ?? stateContainer!;
|
targetContainer = manualContainer ?? stateContainer!;
|
||||||
} else if (isLegacyPinned && !stateContainer) {
|
} else if (isLegacyPinned && !stateContainer) {
|
||||||
// Special legacy case
|
// Special legacy case
|
||||||
targetContainer = Container.Top;
|
targetContainer = "top";
|
||||||
}
|
}
|
||||||
(targetContainer === Container.Top ? topWidgets : rightWidgets).push(widget);
|
(targetContainer === "top" ? topWidgets : rightWidgets).push(widget);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trim to MAX_PINNED
|
// Trim to MAX_PINNED
|
||||||
@@ -291,19 +286,19 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
|
|||||||
const newRoomContainers = new Map();
|
const newRoomContainers = new Map();
|
||||||
this.byRoom.set(room.roomId, newRoomContainers);
|
this.byRoom.set(room.roomId, newRoomContainers);
|
||||||
if (topWidgets.length) {
|
if (topWidgets.length) {
|
||||||
newRoomContainers.set(Container.Top, {
|
newRoomContainers.set("top", {
|
||||||
ordered: topWidgets,
|
ordered: topWidgets,
|
||||||
distributions: widths,
|
distributions: widths,
|
||||||
height: maxHeight,
|
height: maxHeight,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (rightWidgets.length) {
|
if (rightWidgets.length) {
|
||||||
newRoomContainers.set(Container.Right, {
|
newRoomContainers.set("right", {
|
||||||
ordered: rightWidgets,
|
ordered: rightWidgets,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (centerWidgets.length) {
|
if (centerWidgets.length) {
|
||||||
newRoomContainers.set(Container.Center, {
|
newRoomContainers.set("center", {
|
||||||
ordered: centerWidgets,
|
ordered: centerWidgets,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -325,11 +320,11 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
|
|||||||
|
|
||||||
public canAddToContainer(room: Room, container: Container): boolean {
|
public canAddToContainer(room: Room, container: Container): boolean {
|
||||||
switch (container) {
|
switch (container) {
|
||||||
case Container.Top:
|
case "top":
|
||||||
return this.getContainerWidgets(room, container).length < MAX_PINNED;
|
return this.getContainerWidgets(room, container).length < MAX_PINNED;
|
||||||
case Container.Right:
|
case "right":
|
||||||
return this.getContainerWidgets(room, container).length < MAX_PINNED;
|
return this.getContainerWidgets(room, container).length < MAX_PINNED;
|
||||||
case Container.Center:
|
case "center":
|
||||||
return this.getContainerWidgets(room, container).length < 1;
|
return this.getContainerWidgets(room, container).length < 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -349,7 +344,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public setResizerDistributions(room: Room, container: Container, distributions: string[]): void {
|
public setResizerDistributions(room: Room, container: Container, distributions: string[]): void {
|
||||||
if (container !== Container.Top) return; // ignore - not relevant
|
if (container !== "top") return; // ignore - not relevant
|
||||||
|
|
||||||
const numbers = distributions.map((d) => Number(Number(d.substring(0, d.length - 1)).toFixed(1)));
|
const numbers = distributions.map((d) => Number(Number(d.substring(0, d.length - 1)).toFixed(1)));
|
||||||
const widgets = this.getContainerWidgets(room, container);
|
const widgets = this.getContainerWidgets(room, container);
|
||||||
@@ -419,23 +414,23 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
|
|||||||
// Prepare other containers (potentially move widgets to obey the following rules)
|
// Prepare other containers (potentially move widgets to obey the following rules)
|
||||||
const newLayout: Record<string, IStoredLayout> = {};
|
const newLayout: Record<string, IStoredLayout> = {};
|
||||||
switch (toContainer) {
|
switch (toContainer) {
|
||||||
case Container.Right:
|
case "right":
|
||||||
// new "right" widget
|
// new "right" widget
|
||||||
break;
|
break;
|
||||||
case Container.Center:
|
case "center":
|
||||||
// new "center" widget => all other widgets go into "right"
|
// new "center" widget => all other widgets go into "right"
|
||||||
for (const w of this.getContainerWidgets(room, Container.Top)) {
|
for (const w of this.getContainerWidgets(room, "top")) {
|
||||||
newLayout[w.id] = { container: Container.Right };
|
newLayout[w.id] = { container: "right" };
|
||||||
}
|
}
|
||||||
for (const w of this.getContainerWidgets(room, Container.Center)) {
|
for (const w of this.getContainerWidgets(room, "center")) {
|
||||||
newLayout[w.id] = { container: Container.Right };
|
newLayout[w.id] = { container: "right" };
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Container.Top:
|
case "top":
|
||||||
// new "top" widget => the center widget moves into "right"
|
// new "top" widget => the center widget moves into "right"
|
||||||
if (this.hasMaximisedWidget(room)) {
|
if (this.hasMaximisedWidget(room)) {
|
||||||
const centerWidget = this.getContainerWidgets(room, Container.Center)[0];
|
const centerWidget = this.getContainerWidgets(room, "center")[0];
|
||||||
newLayout[centerWidget.id] = { container: Container.Right };
|
newLayout[centerWidget.id] = { container: "right" };
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -447,11 +442,11 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public hasMaximisedWidget(room: Room): boolean {
|
public hasMaximisedWidget(room: Room): boolean {
|
||||||
return this.getContainerWidgets(room, Container.Center).length > 0;
|
return this.getContainerWidgets(room, "center").length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public hasPinnedWidgets(room: Room): boolean {
|
public hasPinnedWidgets(room: Room): boolean {
|
||||||
return this.getContainerWidgets(room, Container.Top).length > 0;
|
return this.getContainerWidgets(room, "top").length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public canCopyLayoutToRoom(room: Room): boolean {
|
public canCopyLayoutToRoom(room: Room): boolean {
|
||||||
@@ -464,7 +459,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
|
|||||||
const evContent: ILayoutStateEvent = { widgets: {} };
|
const evContent: ILayoutStateEvent = { widgets: {} };
|
||||||
for (const [widget, container] of allWidgets) {
|
for (const [widget, container] of allWidgets) {
|
||||||
evContent.widgets[widget.id] = { container };
|
evContent.widgets[widget.id] = { container };
|
||||||
if (container === Container.Top) {
|
if (container === "top") {
|
||||||
const containerWidgets = this.getContainerWidgets(room, container);
|
const containerWidgets = this.getContainerWidgets(room, container);
|
||||||
const idx = containerWidgets.findIndex((w) => w.id === widget.id);
|
const idx = containerWidgets.findIndex((w) => w.id === widget.id);
|
||||||
const widths = this.byRoom.get(room.roomId)?.get(container)?.distributions;
|
const widths = this.byRoom.get(room.roomId)?.get(container)?.distributions;
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { type Container } from "@element-hq/element-web-module-api";
|
||||||
|
|
||||||
export interface IStoredLayout {
|
export interface IStoredLayout {
|
||||||
// Where to store the widget. Required.
|
// Where to store the widget. Required.
|
||||||
container: Container;
|
container: Container;
|
||||||
@@ -42,15 +44,3 @@ export interface ILayoutStateEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const WIDGET_LAYOUT_EVENT_TYPE = "io.element.widgets.layout";
|
export const WIDGET_LAYOUT_EVENT_TYPE = "io.element.widgets.layout";
|
||||||
|
|
||||||
export enum Container {
|
|
||||||
// "Top" is the app drawer, and currently the only sensible value.
|
|
||||||
Top = "top",
|
|
||||||
|
|
||||||
// "Right" is the right panel, and the default for widgets. Setting
|
|
||||||
// this as a container on a widget is essentially like saying "no
|
|
||||||
// changes needed", though this may change in the future.
|
|
||||||
Right = "right",
|
|
||||||
|
|
||||||
Center = "center",
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import { _t } from "../../languageHandler";
|
|||||||
import { getConfigLivestreamUrl, startJitsiAudioLivestream } from "../../Livestream";
|
import { getConfigLivestreamUrl, startJitsiAudioLivestream } from "../../Livestream";
|
||||||
import Modal from "../../Modal";
|
import Modal from "../../Modal";
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
import { Container } from "../../stores/widgets/types";
|
|
||||||
import { WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
|
import { WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
|
||||||
import { WidgetMessagingStore } from "../../stores/widgets/WidgetMessagingStore";
|
import { WidgetMessagingStore } from "../../stores/widgets/WidgetMessagingStore";
|
||||||
import { isAppWidget } from "../../stores/WidgetStore";
|
import { isAppWidget } from "../../stores/WidgetStore";
|
||||||
@@ -109,7 +108,7 @@ export class WidgetContextMenuViewModel
|
|||||||
|
|
||||||
let showMoveButtons: [boolean, boolean] = [false, false];
|
let showMoveButtons: [boolean, boolean] = [false, false];
|
||||||
if (showUnpin) {
|
if (showUnpin) {
|
||||||
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);
|
const widgetIndex = pinnedWidgets.findIndex((widget) => widget.id === app.id);
|
||||||
showMoveButtons = [widgetIndex > 0, widgetIndex < pinnedWidgets.length - 1];
|
showMoveButtons = [widgetIndex > 0, widgetIndex < pinnedWidgets.length - 1];
|
||||||
}
|
}
|
||||||
@@ -225,7 +224,7 @@ export class WidgetContextMenuViewModel
|
|||||||
public get onMoveButton(): (direction: number) => void {
|
public get onMoveButton(): (direction: number) => void {
|
||||||
return (direction: number) => {
|
return (direction: number) => {
|
||||||
if (!this._room) throw new Error("room must be defined");
|
if (!this._room) throw new Error("room must be defined");
|
||||||
WidgetLayoutStore.instance.moveWithinContainer(this._room, Container.Top, this._app, direction);
|
WidgetLayoutStore.instance.moveWithinContainer(this._room, "top", this._app, direction);
|
||||||
this.props.onFinished!();
|
this.props.onFinished!();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { CallStore, CallStoreEvent } from "../../stores/CallStore";
|
|||||||
import { type Call } from "../../models/Call";
|
import { type Call } from "../../models/Call";
|
||||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
import { type ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
|
import { type ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
|
||||||
import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
|
import { WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
|
||||||
import PersistentApp from "../../components/views/elements/PersistentApp";
|
import PersistentApp from "../../components/views/elements/PersistentApp";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
@@ -91,7 +91,7 @@ export class WidgetPipViewModel
|
|||||||
metricsTrigger: "WebFloatingCallWindow",
|
metricsTrigger: "WebFloatingCallWindow",
|
||||||
});
|
});
|
||||||
} else if (this.viewingRoom) {
|
} else if (this.viewingRoom) {
|
||||||
WidgetLayoutStore.instance.moveToContainer(this.props.room, this.widget, Container.Center);
|
WidgetLayoutStore.instance.moveToContainer(this.props.room, this.widget, "center");
|
||||||
} else {
|
} else {
|
||||||
defaultDispatcher.dispatch<ViewRoomPayload>({
|
defaultDispatcher.dispatch<ViewRoomPayload>({
|
||||||
action: Action.ViewRoom,
|
action: Action.ViewRoom,
|
||||||
|
|||||||
@@ -680,11 +680,13 @@ export function mkStubRoom(
|
|||||||
maySendStateEvent: jest.fn().mockReturnValue(true),
|
maySendStateEvent: jest.fn().mockReturnValue(true),
|
||||||
maySendRedactionForEvent: jest.fn().mockReturnValue(true),
|
maySendRedactionForEvent: jest.fn().mockReturnValue(true),
|
||||||
maySendEvent: jest.fn().mockReturnValue(true),
|
maySendEvent: jest.fn().mockReturnValue(true),
|
||||||
|
maySendMessage: jest.fn().mockReturnValue(true),
|
||||||
members: {},
|
members: {},
|
||||||
getHistoryVisibility: jest.fn().mockReturnValue(HistoryVisibility.Shared),
|
getHistoryVisibility: jest.fn().mockReturnValue(HistoryVisibility.Shared),
|
||||||
getJoinRule: jest.fn().mockReturnValue(JoinRule.Invite),
|
getJoinRule: jest.fn().mockReturnValue(JoinRule.Invite),
|
||||||
on: jest.fn(),
|
on: jest.fn(),
|
||||||
off: jest.fn(),
|
off: jest.fn(),
|
||||||
|
removeListener: jest.fn(),
|
||||||
} as unknown as RoomState,
|
} as unknown as RoomState,
|
||||||
eventShouldLiveIn: jest.fn().mockReturnValue({ shouldLiveInRoom: true, shouldLiveInThread: false }),
|
eventShouldLiveIn: jest.fn().mockReturnValue({ shouldLiveInRoom: true, shouldLiveInThread: false }),
|
||||||
fetchRoomThreads: jest.fn().mockReturnValue(Promise.resolve()),
|
fetchRoomThreads: jest.fn().mockReturnValue(Promise.resolve()),
|
||||||
@@ -713,6 +715,7 @@ export function mkStubRoom(
|
|||||||
isKicked: () => false,
|
isKicked: () => false,
|
||||||
}),
|
}),
|
||||||
getMembers: jest.fn().mockReturnValue([]),
|
getMembers: jest.fn().mockReturnValue([]),
|
||||||
|
getEncryptionTargetMembers: jest.fn().mockReturnValue([]),
|
||||||
getMembersWithMembership: jest.fn().mockReturnValue([]),
|
getMembersWithMembership: jest.fn().mockReturnValue([]),
|
||||||
getMxcAvatarUrl: () => "mxc://avatar.url/room.png",
|
getMxcAvatarUrl: () => "mxc://avatar.url/room.png",
|
||||||
getMyMembership: jest.fn().mockReturnValue(KnownMembership.Join),
|
getMyMembership: jest.fn().mockReturnValue(KnownMembership.Join),
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ import { Action } from "../../../../src/dispatcher/actions";
|
|||||||
import { type ViewRoomPayload } from "../../../../src/dispatcher/payloads/ViewRoomPayload";
|
import { type ViewRoomPayload } from "../../../../src/dispatcher/payloads/ViewRoomPayload";
|
||||||
import { TestSdkContext } from "../../TestSdkContext";
|
import { TestSdkContext } from "../../TestSdkContext";
|
||||||
import { RoomViewStore } from "../../../../src/stores/RoomViewStore";
|
import { RoomViewStore } from "../../../../src/stores/RoomViewStore";
|
||||||
import { Container, WidgetLayoutStore } from "../../../../src/stores/widgets/WidgetLayoutStore";
|
import { WidgetLayoutStore } from "../../../../src/stores/widgets/WidgetLayoutStore";
|
||||||
import WidgetStore from "../../../../src/stores/WidgetStore";
|
import WidgetStore from "../../../../src/stores/WidgetStore";
|
||||||
import { WidgetType } from "../../../../src/widgets/WidgetType";
|
import { WidgetType } from "../../../../src/widgets/WidgetType";
|
||||||
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
|
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
|
||||||
@@ -234,7 +234,7 @@ describe("PipContainer", () => {
|
|||||||
// The return button should maximize the widget
|
// The return button should maximize the widget
|
||||||
const moveSpy = jest.spyOn(WidgetLayoutStore.instance, "moveToContainer");
|
const moveSpy = jest.spyOn(WidgetLayoutStore.instance, "moveToContainer");
|
||||||
await user.click(await screen.findByRole("button", { name: "Back" }));
|
await user.click(await screen.findByRole("button", { name: "Back" }));
|
||||||
expect(moveSpy).toHaveBeenCalledWith(room, widget, Container.Center);
|
expect(moveSpy).toHaveBeenCalledWith(room, widget, "center");
|
||||||
|
|
||||||
expect(screen.queryByRole("button", { name: "Leave" })).toBeNull();
|
expect(screen.queryByRole("button", { name: "Leave" })).toBeNull();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
import React, { type JSX, type ComponentProps } from "react";
|
import React, { type JSX, type ComponentProps } from "react";
|
||||||
import { screen, render } from "jest-matrix-react";
|
import { screen, render } from "jest-matrix-react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
import { type Room, type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
import { MatrixWidgetType } from "matrix-widget-api";
|
import { MatrixWidgetType } from "matrix-widget-api";
|
||||||
import {
|
import {
|
||||||
type ApprovalOpts,
|
type ApprovalOpts,
|
||||||
@@ -24,6 +24,10 @@ import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext
|
|||||||
import WidgetUtils from "../../../../../src/utils/WidgetUtils";
|
import WidgetUtils from "../../../../../src/utils/WidgetUtils";
|
||||||
import { ModuleRunner } from "../../../../../src/modules/ModuleRunner";
|
import { ModuleRunner } from "../../../../../src/modules/ModuleRunner";
|
||||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||||
|
import { WidgetLayoutStore } from "../../../../../src/stores/widgets/WidgetLayoutStore";
|
||||||
|
import { mkStubRoom } from "../../../../test-utils/test-utils.ts";
|
||||||
|
import { type RoomContextType } from "../../../../../src/contexts/RoomContext.ts";
|
||||||
|
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
|
||||||
|
|
||||||
describe("<WidgetContextMenu />", () => {
|
describe("<WidgetContextMenu />", () => {
|
||||||
const widgetId = "w1";
|
const widgetId = "w1";
|
||||||
@@ -44,8 +48,12 @@ describe("<WidgetContextMenu />", () => {
|
|||||||
|
|
||||||
let mockClient: MatrixClient;
|
let mockClient: MatrixClient;
|
||||||
|
|
||||||
|
let room: Room;
|
||||||
|
|
||||||
let onFinished: () => void;
|
let onFinished: () => void;
|
||||||
|
|
||||||
|
let roomContext: RoomContextType;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
onFinished = jest.fn();
|
onFinished = jest.fn();
|
||||||
jest.spyOn(WidgetUtils, "canUserModifyWidgets").mockReturnValue(true);
|
jest.spyOn(WidgetUtils, "canUserModifyWidgets").mockReturnValue(true);
|
||||||
@@ -53,6 +61,13 @@ describe("<WidgetContextMenu />", () => {
|
|||||||
mockClient = {
|
mockClient = {
|
||||||
getUserId: jest.fn().mockReturnValue(userId),
|
getUserId: jest.fn().mockReturnValue(userId),
|
||||||
} as unknown as MatrixClient;
|
} as unknown as MatrixClient;
|
||||||
|
|
||||||
|
room = mkStubRoom(roomId, "Test Room", mockClient);
|
||||||
|
|
||||||
|
roomContext = {
|
||||||
|
room,
|
||||||
|
roomId,
|
||||||
|
} as unknown as RoomContextType;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -62,7 +77,9 @@ describe("<WidgetContextMenu />", () => {
|
|||||||
function getComponent(props: Partial<ComponentProps<typeof WidgetContextMenu>> = {}): JSX.Element {
|
function getComponent(props: Partial<ComponentProps<typeof WidgetContextMenu>> = {}): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<MatrixClientContext.Provider value={mockClient}>
|
<MatrixClientContext.Provider value={mockClient}>
|
||||||
<WidgetContextMenu app={app} onFinished={onFinished} {...props} />
|
<ScopedRoomContextProvider {...roomContext}>
|
||||||
|
<WidgetContextMenu app={app} onFinished={onFinished} {...props} />
|
||||||
|
</ScopedRoomContextProvider>
|
||||||
</MatrixClientContext.Provider>
|
</MatrixClientContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -89,4 +106,69 @@ describe("<WidgetContextMenu />", () => {
|
|||||||
expect(onFinished).toHaveBeenCalled();
|
expect(onFinished).toHaveBeenCalled();
|
||||||
expect(SettingsStore.getValue("allowedWidgets", roomId)[eventId]).toBe(false);
|
expect(SettingsStore.getValue("allowedWidgets", roomId)[eventId]).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows the move left button when the widget can be moved left", () => {
|
||||||
|
// Place our widget second so it can move left but not right.
|
||||||
|
jest.spyOn(WidgetLayoutStore.instance, "getContainerWidgets").mockReturnValue([
|
||||||
|
{ id: "someOtherWidget", type: "m.custom", creatorUserId: userId, url: "" },
|
||||||
|
{ id: widgetId, type: "m.custom", creatorUserId: userId, url: "" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
render(getComponent({ showUnpin: true }));
|
||||||
|
|
||||||
|
expect(screen.getByLabelText("Move left")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByLabelText("Move right")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows the move right button when the widget can be moved right", () => {
|
||||||
|
// Place our widget first so it can move right but not left.
|
||||||
|
jest.spyOn(WidgetLayoutStore.instance, "getContainerWidgets").mockReturnValue([
|
||||||
|
{ id: widgetId, type: "m.custom", creatorUserId: userId, url: "" },
|
||||||
|
{ id: "someOtherWidget", type: "m.custom", creatorUserId: userId, url: "" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
render(getComponent({ showUnpin: true }));
|
||||||
|
|
||||||
|
expect(screen.getByLabelText("Move right")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByLabelText("Move left")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("moves widget left when move left button is clicked", async () => {
|
||||||
|
// Place our widget second so move left is visible.
|
||||||
|
jest.spyOn(WidgetLayoutStore.instance, "getContainerWidgets").mockReturnValue([
|
||||||
|
{ id: "someOtherWidget", type: "m.custom", creatorUserId: userId, url: "" },
|
||||||
|
{ id: widgetId, type: "m.custom", creatorUserId: userId, url: "" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Mock moveWithinContainer to verify it's called with the correct arguments.
|
||||||
|
const moveWithinContainerSpy = jest
|
||||||
|
.spyOn(WidgetLayoutStore.instance, "moveWithinContainer")
|
||||||
|
.mockImplementation();
|
||||||
|
|
||||||
|
render(getComponent({ showUnpin: true }));
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByLabelText("Move left"));
|
||||||
|
|
||||||
|
expect(moveWithinContainerSpy).toHaveBeenCalledWith(room, "top", app, -1);
|
||||||
|
expect(onFinished).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("moves widget right when move right button is clicked", async () => {
|
||||||
|
// Place our widget first so move right is visible.
|
||||||
|
jest.spyOn(WidgetLayoutStore.instance, "getContainerWidgets").mockReturnValue([
|
||||||
|
{ id: widgetId, type: "m.custom", creatorUserId: userId, url: "" },
|
||||||
|
{ id: "someOtherWidget", type: "m.custom", creatorUserId: userId, url: "" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Mock moveWithinContainer to verify it's called with the correct arguments.
|
||||||
|
const moveWithinContainerSpy = jest
|
||||||
|
.spyOn(WidgetLayoutStore.instance, "moveWithinContainer")
|
||||||
|
.mockImplementation();
|
||||||
|
|
||||||
|
render(getComponent({ showUnpin: true }));
|
||||||
|
await userEvent.click(screen.getByLabelText("Move right"));
|
||||||
|
|
||||||
|
expect(moveWithinContainerSpy).toHaveBeenCalledWith(room, "top", app, 1);
|
||||||
|
expect(onFinished).toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import RightPanelStore from "../../../../../src/stores/right-panel/RightPanelSto
|
|||||||
import WidgetStore, { type IApp } from "../../../../../src/stores/WidgetStore";
|
import WidgetStore, { type IApp } from "../../../../../src/stores/WidgetStore";
|
||||||
import ActiveWidgetStore from "../../../../../src/stores/ActiveWidgetStore";
|
import ActiveWidgetStore from "../../../../../src/stores/ActiveWidgetStore";
|
||||||
import AppTile from "../../../../../src/components/views/elements/AppTile";
|
import AppTile from "../../../../../src/components/views/elements/AppTile";
|
||||||
import { Container, WidgetLayoutStore } from "../../../../../src/stores/widgets/WidgetLayoutStore";
|
import { type Container, WidgetLayoutStore } from "../../../../../src/stores/widgets/WidgetLayoutStore";
|
||||||
import AppsDrawer from "../../../../../src/components/views/rooms/AppsDrawer";
|
import AppsDrawer from "../../../../../src/components/views/rooms/AppsDrawer";
|
||||||
import { ElementWidgetCapabilities } from "../../../../../src/stores/widgets/ElementWidgetCapabilities";
|
import { ElementWidgetCapabilities } from "../../../../../src/stores/widgets/ElementWidgetCapabilities";
|
||||||
import { ElementWidget, type WidgetMessaging } from "../../../../../src/stores/widgets/WidgetMessaging";
|
import { ElementWidget, type WidgetMessaging } from "../../../../../src/stores/widgets/WidgetMessaging";
|
||||||
@@ -303,7 +303,7 @@ describe("AppTile", () => {
|
|||||||
return {
|
return {
|
||||||
widgets: {
|
widgets: {
|
||||||
1: {
|
1: {
|
||||||
container: Container.Top,
|
container: "top",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -334,7 +334,7 @@ describe("AppTile", () => {
|
|||||||
mockSettings.mockRestore();
|
mockSettings.mockRestore();
|
||||||
act(() => {
|
act(() => {
|
||||||
// Move widget to center
|
// Move widget to center
|
||||||
WidgetLayoutStore.instance.moveToContainer(r1, app1, Container.Center);
|
WidgetLayoutStore.instance.moveToContainer(r1, app1, "center");
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(renderResult.getByText("Example 1")).toBeInTheDocument();
|
expect(renderResult.getByText("Example 1")).toBeInTheDocument();
|
||||||
@@ -377,7 +377,7 @@ describe("AppTile", () => {
|
|||||||
);
|
);
|
||||||
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
|
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
|
||||||
await userEvent.click(renderResult.getByLabelText("Minimise"));
|
await userEvent.click(renderResult.getByLabelText("Minimise"));
|
||||||
expect(moveToContainerSpy).toHaveBeenCalledWith(r1, app1, Container.Right);
|
expect(moveToContainerSpy).toHaveBeenCalledWith(r1, app1, "right");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("clicking 'maximise' should send the widget to the center", async () => {
|
it("clicking 'maximise' should send the widget to the center", async () => {
|
||||||
@@ -388,7 +388,7 @@ describe("AppTile", () => {
|
|||||||
);
|
);
|
||||||
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
|
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
|
||||||
await userEvent.click(renderResult.getByLabelText("Maximise"));
|
await userEvent.click(renderResult.getByLabelText("Maximise"));
|
||||||
expect(moveToContainerSpy).toHaveBeenCalledWith(r1, app1, Container.Center);
|
expect(moveToContainerSpy).toHaveBeenCalledWith(r1, app1, "center");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render permission request", async () => {
|
it("should render permission request", async () => {
|
||||||
@@ -455,7 +455,7 @@ describe("AppTile", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockImplementation(
|
jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockImplementation(
|
||||||
(room: Room | null, widget: IWidget, container: Container) => {
|
(room: Room | null, widget: IWidget, container: Container) => {
|
||||||
return room === r1 && widget === app1 && container === Container.Center;
|
return room === r1 && widget === app1 && container === "center";
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -472,7 +472,7 @@ describe("AppTile", () => {
|
|||||||
);
|
);
|
||||||
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
|
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
|
||||||
await userEvent.click(renderResult.getByLabelText("Un-maximise"));
|
await userEvent.click(renderResult.getByLabelText("Un-maximise"));
|
||||||
expect(moveToContainerSpy).toHaveBeenCalledWith(r1, app1, Container.Top);
|
expect(moveToContainerSpy).toHaveBeenCalledWith(r1, app1, "top");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ import dispatcher from "../../../../../../src/dispatcher/dispatcher";
|
|||||||
import { CallStore } from "../../../../../../src/stores/CallStore";
|
import { CallStore } from "../../../../../../src/stores/CallStore";
|
||||||
import { type Call } from "../../../../../../src/models/Call";
|
import { type Call } from "../../../../../../src/models/Call";
|
||||||
import * as ShieldUtils from "../../../../../../src/utils/ShieldUtils";
|
import * as ShieldUtils from "../../../../../../src/utils/ShieldUtils";
|
||||||
import { Container, WidgetLayoutStore } from "../../../../../../src/stores/widgets/WidgetLayoutStore";
|
import { WidgetLayoutStore } from "../../../../../../src/stores/widgets/WidgetLayoutStore";
|
||||||
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
|
||||||
import { _t } from "../../../../../../src/languageHandler";
|
import { _t } from "../../../../../../src/languageHandler";
|
||||||
import WidgetStore, { type IApp } from "../../../../../../src/stores/WidgetStore";
|
import WidgetStore, { type IApp } from "../../../../../../src/stores/WidgetStore";
|
||||||
@@ -504,7 +504,7 @@ describe("RoomHeader", () => {
|
|||||||
const videoButton = screen.getByRole("button", { name: "Video call" });
|
const videoButton = screen.getByRole("button", { name: "Video call" });
|
||||||
expect(videoButton).not.toHaveAttribute("aria-disabled", "true");
|
expect(videoButton).not.toHaveAttribute("aria-disabled", "true");
|
||||||
await user.click(videoButton);
|
await user.click(videoButton);
|
||||||
expect(spy).toHaveBeenCalledWith(room, widget, Container.Top);
|
expect(spy).toHaveBeenCalledWith(room, widget, "top");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("disables calling if there's a jitsi call", () => {
|
it("disables calling if there's a jitsi call", () => {
|
||||||
@@ -869,7 +869,7 @@ describe("RoomHeader", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders additionalButtons", async () => {
|
it("renders legacy additionalButtons", async () => {
|
||||||
const additionalButtons: ViewRoomOpts["buttons"] = [
|
const additionalButtons: ViewRoomOpts["buttons"] = [
|
||||||
{
|
{
|
||||||
icon: () => <>test-icon</>,
|
icon: () => <>test-icon</>,
|
||||||
@@ -878,11 +878,11 @@ describe("RoomHeader", () => {
|
|||||||
onClick: () => {},
|
onClick: () => {},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
render(<RoomHeader room={room} additionalButtons={additionalButtons} />, getWrapper());
|
render(<RoomHeader room={room} legacyAdditionalButtons={additionalButtons} />, getWrapper());
|
||||||
expect(screen.getByRole("button", { name: "test-label" })).toBeInTheDocument();
|
expect(screen.getByRole("button", { name: "test-label" })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls onClick-callback on additionalButtons", () => {
|
it("calls onClick-callback on legacyAdditionalButtons", () => {
|
||||||
const callback = jest.fn();
|
const callback = jest.fn();
|
||||||
const additionalButtons: ViewRoomOpts["buttons"] = [
|
const additionalButtons: ViewRoomOpts["buttons"] = [
|
||||||
{
|
{
|
||||||
@@ -893,7 +893,7 @@ describe("RoomHeader", () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
render(<RoomHeader room={room} additionalButtons={additionalButtons} />, getWrapper());
|
render(<RoomHeader room={room} legacyAdditionalButtons={additionalButtons} />, getWrapper());
|
||||||
|
|
||||||
const button = screen.getByRole("button", { name: "test-label" });
|
const button = screen.getByRole("button", { name: "test-label" });
|
||||||
const event = createEvent.click(button);
|
const event = createEvent.click(button);
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { act } from "react";
|
||||||
|
import { render, type RenderOptions } from "jest-matrix-react";
|
||||||
|
import { type MatrixClient, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { EventEmitter } from "events";
|
||||||
|
|
||||||
|
import { stubClient } from "../../test-utils";
|
||||||
|
import DMRoomMap from "../../../src/utils/DMRoomMap";
|
||||||
|
import { SDKContext, SdkContextClass } from "../../../src/contexts/SDKContext";
|
||||||
|
import { ScopedRoomContextProvider } from "../../../src/contexts/ScopedRoomContext";
|
||||||
|
import RoomContext, { type RoomContextType } from "../../../src/contexts/RoomContext";
|
||||||
|
import MatrixClientContext from "../../../src/contexts/MatrixClientContext";
|
||||||
|
import { RoomView } from "../../../src/components/structures/RoomView";
|
||||||
|
import { ModuleApi } from "../../../src/modules/Api";
|
||||||
|
|
||||||
|
describe("ExtrasApi", () => {
|
||||||
|
let client: MatrixClient;
|
||||||
|
let sdkContext: SdkContextClass;
|
||||||
|
let room: Room;
|
||||||
|
let roomContext: RoomContextType;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
client = stubClient();
|
||||||
|
room = new Room("!test:room", client, "@alice:example.org", {
|
||||||
|
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||||
|
});
|
||||||
|
sdkContext = new SdkContextClass();
|
||||||
|
sdkContext.client = client;
|
||||||
|
jest.spyOn(sdkContext.roomViewStore, "getRoomId").mockReturnValue(room.roomId);
|
||||||
|
|
||||||
|
const mockRoomViewStore = new (class extends EventEmitter {
|
||||||
|
isViewingCall = jest.fn().mockReturnValue(false);
|
||||||
|
})();
|
||||||
|
|
||||||
|
roomContext = {
|
||||||
|
...RoomContext,
|
||||||
|
roomId: "!test:room",
|
||||||
|
roomViewStore: mockRoomViewStore,
|
||||||
|
} as unknown as RoomContextType;
|
||||||
|
|
||||||
|
DMRoomMap.setShared({
|
||||||
|
getUserIdForRoomId: jest.fn(),
|
||||||
|
getRoomIds: jest.fn().mockReturnValue(new Set()),
|
||||||
|
} as unknown as DMRoomMap);
|
||||||
|
});
|
||||||
|
|
||||||
|
function getWrapper(): RenderOptions {
|
||||||
|
return {
|
||||||
|
wrapper: ({ children }) => (
|
||||||
|
<SDKContext.Provider value={sdkContext}>
|
||||||
|
<ScopedRoomContextProvider {...roomContext}>
|
||||||
|
<MatrixClientContext.Provider value={client}>{children}</MatrixClientContext.Provider>
|
||||||
|
</ScopedRoomContextProvider>
|
||||||
|
</SDKContext.Provider>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
it("addRoomHeaderButtonCallback stores and uses the provided callback", () => {
|
||||||
|
const callback = jest.fn();
|
||||||
|
ModuleApi.instance.extras.addRoomHeaderButtonCallback(callback);
|
||||||
|
|
||||||
|
render(<RoomView />, getWrapper());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
sdkContext.roomViewStore.emit("update");
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -23,7 +23,7 @@ import { registerMockModule } from "./MockModule";
|
|||||||
import defaultDispatcher from "../../../src/dispatcher/dispatcher";
|
import defaultDispatcher from "../../../src/dispatcher/dispatcher";
|
||||||
import { Action } from "../../../src/dispatcher/actions";
|
import { Action } from "../../../src/dispatcher/actions";
|
||||||
import WidgetStore, { type IApp } from "../../../src/stores/WidgetStore";
|
import WidgetStore, { type IApp } from "../../../src/stores/WidgetStore";
|
||||||
import { Container, WidgetLayoutStore } from "../../../src/stores/widgets/WidgetLayoutStore";
|
import { WidgetLayoutStore } from "../../../src/stores/widgets/WidgetLayoutStore";
|
||||||
import * as navigator from "../../../src/utils/permalinks/navigator.ts";
|
import * as navigator from "../../../src/utils/permalinks/navigator.ts";
|
||||||
|
|
||||||
describe("ProxiedApiModule", () => {
|
describe("ProxiedApiModule", () => {
|
||||||
@@ -319,18 +319,18 @@ describe("ProxiedApiModule", () => {
|
|||||||
it("should return false if there is no room", () => {
|
it("should return false if there is no room", () => {
|
||||||
client.getRoom = jest.fn().mockReturnValue(null);
|
client.getRoom = jest.fn().mockReturnValue(null);
|
||||||
|
|
||||||
expect(api.isAppInContainer(app, Container.Top, roomId)).toBe(false);
|
expect(api.isAppInContainer(app, "top", roomId)).toBe(false);
|
||||||
expect(WidgetLayoutStore.instance.isInContainer).not.toHaveBeenCalled();
|
expect(WidgetLayoutStore.instance.isInContainer).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return false if the app is not in the container", () => {
|
it("should return false if the app is not in the container", () => {
|
||||||
jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockReturnValue(false);
|
jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockReturnValue(false);
|
||||||
expect(api.isAppInContainer(app, Container.Top, roomId)).toBe(false);
|
expect(api.isAppInContainer(app, "top", roomId)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return true if the app is in the container", () => {
|
it("should return true if the app is in the container", () => {
|
||||||
jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockReturnValue(true);
|
jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockReturnValue(true);
|
||||||
expect(api.isAppInContainer(app, Container.Top, roomId)).toBe(true);
|
expect(api.isAppInContainer(app, "top", roomId)).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -350,7 +350,7 @@ describe("ProxiedApiModule", () => {
|
|||||||
|
|
||||||
it("should not move if there is no room", () => {
|
it("should not move if there is no room", () => {
|
||||||
client.getRoom = jest.fn().mockReturnValue(null);
|
client.getRoom = jest.fn().mockReturnValue(null);
|
||||||
api.moveAppToContainer(app, Container.Top, roomId);
|
api.moveAppToContainer(app, "top", roomId);
|
||||||
expect(WidgetLayoutStore.instance.moveToContainer).not.toHaveBeenCalled();
|
expect(WidgetLayoutStore.instance.moveToContainer).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -358,8 +358,8 @@ describe("ProxiedApiModule", () => {
|
|||||||
const room = mkRoom(client, roomId);
|
const room = mkRoom(client, roomId);
|
||||||
client.getRoom = jest.fn().mockReturnValue(room);
|
client.getRoom = jest.fn().mockReturnValue(room);
|
||||||
|
|
||||||
api.moveAppToContainer(app, Container.Top, roomId);
|
api.moveAppToContainer(app, "top", roomId);
|
||||||
expect(WidgetLayoutStore.instance.moveToContainer).toHaveBeenCalledWith(room, app, Container.Top);
|
expect(WidgetLayoutStore.instance.moveToContainer).toHaveBeenCalledWith(room, app, "top");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 Element Creations Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { mocked } from "jest-mock";
|
||||||
|
|
||||||
|
import type { IWidget } from "matrix-widget-api";
|
||||||
|
import type { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { WidgetApi } from "../../../src/modules/WidgetApi";
|
||||||
|
import WidgetStore from "../../../src/stores/WidgetStore";
|
||||||
|
import { WidgetLayoutStore } from "../../../src/stores/widgets/WidgetLayoutStore";
|
||||||
|
import { stubClient } from "../../test-utils";
|
||||||
|
|
||||||
|
describe("WidgetApi", () => {
|
||||||
|
let client: MatrixClient;
|
||||||
|
let api: WidgetApi;
|
||||||
|
|
||||||
|
const mkWidget = (overrides: Partial<IWidget> = {}): IWidget => ({
|
||||||
|
id: "widget-id",
|
||||||
|
creatorUserId: "@alice:example.org",
|
||||||
|
type: "m.custom",
|
||||||
|
url: "https://example.org/widget",
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
client = stubClient();
|
||||||
|
|
||||||
|
api = new WidgetApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getWidgetsInRoom returns widgets from WidgetStore", () => {
|
||||||
|
const widgets = [{ id: "w1" }, { id: "w2" }] as unknown as IWidget[];
|
||||||
|
const getAppsSpy = jest.spyOn(WidgetStore.instance, "getApps").mockReturnValue(widgets as any);
|
||||||
|
|
||||||
|
expect(api.getWidgetsInRoom("!room:example.org")).toBe(widgets);
|
||||||
|
expect(getAppsSpy).toHaveBeenCalledWith("!room:example.org");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getAppAvatarUrl returns the http avatar URL for a widget if it has one", () => {
|
||||||
|
const app = {
|
||||||
|
...mkWidget(),
|
||||||
|
roomId: "!room:example.org",
|
||||||
|
avatar_url: "mxc://example.org/avatar",
|
||||||
|
} as unknown as IWidget;
|
||||||
|
|
||||||
|
mocked(client.getHomeserverUrl).mockReturnValue("https://hs.example.org");
|
||||||
|
const avatarUrl = api.getAppAvatarUrl(app, 32, 32, "scale");
|
||||||
|
|
||||||
|
expect(avatarUrl).toContain("https://hs.example.org/_matrix/media/");
|
||||||
|
expect(avatarUrl).toContain("/thumbnail/example.org/avatar");
|
||||||
|
expect(avatarUrl).toContain("width=32");
|
||||||
|
expect(avatarUrl).toContain("height=32");
|
||||||
|
expect(avatarUrl).toContain("method=scale");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getAppAvatarUrl returns null when app is not an app widget", () => {
|
||||||
|
const nonAppWidget = {
|
||||||
|
...mkWidget(),
|
||||||
|
avatar_url: "mxc://example.org/avatar",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(api.getAppAvatarUrl(nonAppWidget)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getAppAvatarUrl returns null when app has no avatar URL", () => {
|
||||||
|
const appWithoutAvatar = {
|
||||||
|
...mkWidget(),
|
||||||
|
roomId: "!room:example.org",
|
||||||
|
} as unknown as IWidget;
|
||||||
|
|
||||||
|
expect(api.getAppAvatarUrl(appWithoutAvatar)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("isAppInContainer returns false when room is not found", () => {
|
||||||
|
const isInContainerSpy = jest.spyOn(WidgetLayoutStore.instance, "isInContainer");
|
||||||
|
const app = mkWidget();
|
||||||
|
|
||||||
|
mocked(client.getRoom).mockReturnValue(null);
|
||||||
|
expect(api.isAppInContainer(app, "top", "!missing:example.org")).toBe(false);
|
||||||
|
expect(isInContainerSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("isAppInContainer delegates to WidgetLayoutStore when room exists", () => {
|
||||||
|
const room = { roomId: "!room:example.org" } as Room;
|
||||||
|
mocked(client.getRoom).mockReturnValue(room);
|
||||||
|
const isInContainerSpy = jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockReturnValue(true);
|
||||||
|
const app = mkWidget();
|
||||||
|
|
||||||
|
expect(api.isAppInContainer(app, "top", room.roomId)).toBe(true);
|
||||||
|
expect(isInContainerSpy).toHaveBeenCalledWith(room, app, "top");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("moveAppToContainer does nothing when room is not found", () => {
|
||||||
|
const moveToContainerSpy = jest.spyOn(WidgetLayoutStore.instance, "moveToContainer");
|
||||||
|
const app = mkWidget();
|
||||||
|
|
||||||
|
mocked(client.getRoom).mockReturnValue(null);
|
||||||
|
api.moveAppToContainer(app, "right", "!missing:example.org");
|
||||||
|
|
||||||
|
expect(moveToContainerSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("moveAppToContainer delegates to WidgetLayoutStore when room exists", () => {
|
||||||
|
const room = { roomId: "!room:example.org" } as Room;
|
||||||
|
mocked(client.getRoom).mockReturnValue(room);
|
||||||
|
const moveToContainerSpy = jest.spyOn(WidgetLayoutStore.instance, "moveToContainer").mockImplementation();
|
||||||
|
const app = mkWidget();
|
||||||
|
|
||||||
|
api.moveAppToContainer(app, "right", room.roomId);
|
||||||
|
|
||||||
|
expect(moveToContainerSpy).toHaveBeenCalledWith(room, app, "right");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -10,7 +10,7 @@ import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
|
|||||||
import { mocked } from "jest-mock";
|
import { mocked } from "jest-mock";
|
||||||
|
|
||||||
import WidgetStore, { type IApp } from "../../../src/stores/WidgetStore";
|
import WidgetStore, { type IApp } from "../../../src/stores/WidgetStore";
|
||||||
import { Container, WidgetLayoutStore } from "../../../src/stores/widgets/WidgetLayoutStore";
|
import { WidgetLayoutStore } from "../../../src/stores/widgets/WidgetLayoutStore";
|
||||||
import { stubClient } from "../../test-utils";
|
import { stubClient } from "../../test-utils";
|
||||||
import defaultDispatcher from "../../../src/dispatcher/dispatcher";
|
import defaultDispatcher from "../../../src/dispatcher/dispatcher";
|
||||||
import SettingsStore from "../../../src/settings/SettingsStore";
|
import SettingsStore from "../../../src/settings/SettingsStore";
|
||||||
@@ -74,14 +74,14 @@ describe("WidgetLayoutStore", () => {
|
|||||||
|
|
||||||
it("all widgets should be in the right container by default", () => {
|
it("all widgets should be in the right container by default", () => {
|
||||||
store.recalculateRoom(mockRoom);
|
store.recalculateRoom(mockRoom);
|
||||||
expect(store.getContainerWidgets(mockRoom, Container.Right).length).toStrictEqual(mockApps.length);
|
expect(store.getContainerWidgets(mockRoom, "right").length).toStrictEqual(mockApps.length);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("add widget to top container", async () => {
|
it("add widget to top container", async () => {
|
||||||
store.recalculateRoom(mockRoom);
|
store.recalculateRoom(mockRoom);
|
||||||
store.moveToContainer(mockRoom, mockApps[0], Container.Top);
|
store.moveToContainer(mockRoom, mockApps[0], "top");
|
||||||
expect(store.getContainerWidgets(mockRoom, Container.Top)).toStrictEqual([mockApps[0]]);
|
expect(store.getContainerWidgets(mockRoom, "top")).toStrictEqual([mockApps[0]]);
|
||||||
expect(store.getContainerHeight(mockRoom, Container.Top)).toBeNull();
|
expect(store.getContainerHeight(mockRoom, "top")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ordering of top container widgets should be consistent even if no index specified", async () => {
|
it("ordering of top container widgets should be consistent even if no index specified", async () => {
|
||||||
@@ -97,71 +97,71 @@ describe("WidgetLayoutStore", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
store.recalculateRoom(mockRoom);
|
store.recalculateRoom(mockRoom);
|
||||||
expect(store.getContainerWidgets(mockRoom, Container.Top)).toStrictEqual([mockApps[0], mockApps[1]]);
|
expect(store.getContainerWidgets(mockRoom, "top")).toStrictEqual([mockApps[0], mockApps[1]]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("add three widgets to top container", async () => {
|
it("add three widgets to top container", async () => {
|
||||||
store.recalculateRoom(mockRoom);
|
store.recalculateRoom(mockRoom);
|
||||||
store.moveToContainer(mockRoom, mockApps[0], Container.Top);
|
store.moveToContainer(mockRoom, mockApps[0], "top");
|
||||||
store.moveToContainer(mockRoom, mockApps[1], Container.Top);
|
store.moveToContainer(mockRoom, mockApps[1], "top");
|
||||||
store.moveToContainer(mockRoom, mockApps[2], Container.Top);
|
store.moveToContainer(mockRoom, mockApps[2], "top");
|
||||||
expect(new Set(store.getContainerWidgets(mockRoom, Container.Top))).toEqual(
|
expect(new Set(store.getContainerWidgets(mockRoom, "top"))).toEqual(
|
||||||
new Set([mockApps[0], mockApps[1], mockApps[2]]),
|
new Set([mockApps[0], mockApps[1], mockApps[2]]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("cannot add more than three widgets to top container", async () => {
|
it("cannot add more than three widgets to top container", async () => {
|
||||||
store.recalculateRoom(mockRoom);
|
store.recalculateRoom(mockRoom);
|
||||||
store.moveToContainer(mockRoom, mockApps[0], Container.Top);
|
store.moveToContainer(mockRoom, mockApps[0], "top");
|
||||||
store.moveToContainer(mockRoom, mockApps[1], Container.Top);
|
store.moveToContainer(mockRoom, mockApps[1], "top");
|
||||||
store.moveToContainer(mockRoom, mockApps[2], Container.Top);
|
store.moveToContainer(mockRoom, mockApps[2], "top");
|
||||||
expect(store.canAddToContainer(mockRoom, Container.Top)).toEqual(false);
|
expect(store.canAddToContainer(mockRoom, "top")).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("remove pins when maximising (other widget)", async () => {
|
it("remove pins when maximising (other widget)", async () => {
|
||||||
store.recalculateRoom(mockRoom);
|
store.recalculateRoom(mockRoom);
|
||||||
store.moveToContainer(mockRoom, mockApps[0], Container.Top);
|
store.moveToContainer(mockRoom, mockApps[0], "top");
|
||||||
store.moveToContainer(mockRoom, mockApps[1], Container.Top);
|
store.moveToContainer(mockRoom, mockApps[1], "top");
|
||||||
store.moveToContainer(mockRoom, mockApps[2], Container.Top);
|
store.moveToContainer(mockRoom, mockApps[2], "top");
|
||||||
store.moveToContainer(mockRoom, mockApps[3], Container.Center);
|
store.moveToContainer(mockRoom, mockApps[3], "center");
|
||||||
expect(store.getContainerWidgets(mockRoom, Container.Top)).toEqual([]);
|
expect(store.getContainerWidgets(mockRoom, "top")).toEqual([]);
|
||||||
expect(new Set(store.getContainerWidgets(mockRoom, Container.Right))).toEqual(
|
expect(new Set(store.getContainerWidgets(mockRoom, "right"))).toEqual(
|
||||||
new Set([mockApps[0], mockApps[1], mockApps[2]]),
|
new Set([mockApps[0], mockApps[1], mockApps[2]]),
|
||||||
);
|
);
|
||||||
expect(store.getContainerWidgets(mockRoom, Container.Center)).toEqual([mockApps[3]]);
|
expect(store.getContainerWidgets(mockRoom, "center")).toEqual([mockApps[3]]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("remove pins when maximising (one of the pinned widgets)", async () => {
|
it("remove pins when maximising (one of the pinned widgets)", async () => {
|
||||||
store.recalculateRoom(mockRoom);
|
store.recalculateRoom(mockRoom);
|
||||||
store.moveToContainer(mockRoom, mockApps[0], Container.Top);
|
store.moveToContainer(mockRoom, mockApps[0], "top");
|
||||||
store.moveToContainer(mockRoom, mockApps[1], Container.Top);
|
store.moveToContainer(mockRoom, mockApps[1], "top");
|
||||||
store.moveToContainer(mockRoom, mockApps[2], Container.Top);
|
store.moveToContainer(mockRoom, mockApps[2], "top");
|
||||||
store.moveToContainer(mockRoom, mockApps[0], Container.Center);
|
store.moveToContainer(mockRoom, mockApps[0], "center");
|
||||||
expect(store.getContainerWidgets(mockRoom, Container.Top)).toEqual([]);
|
expect(store.getContainerWidgets(mockRoom, "top")).toEqual([]);
|
||||||
expect(store.getContainerWidgets(mockRoom, Container.Center)).toEqual([mockApps[0]]);
|
expect(store.getContainerWidgets(mockRoom, "center")).toEqual([mockApps[0]]);
|
||||||
expect(new Set(store.getContainerWidgets(mockRoom, Container.Right))).toEqual(
|
expect(new Set(store.getContainerWidgets(mockRoom, "right"))).toEqual(
|
||||||
new Set([mockApps[1], mockApps[2], mockApps[3]]),
|
new Set([mockApps[1], mockApps[2], mockApps[3]]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("remove maximised when pinning (other widget)", async () => {
|
it("remove maximised when pinning (other widget)", async () => {
|
||||||
store.recalculateRoom(mockRoom);
|
store.recalculateRoom(mockRoom);
|
||||||
store.moveToContainer(mockRoom, mockApps[0], Container.Center);
|
store.moveToContainer(mockRoom, mockApps[0], "center");
|
||||||
store.moveToContainer(mockRoom, mockApps[1], Container.Top);
|
store.moveToContainer(mockRoom, mockApps[1], "top");
|
||||||
expect(store.getContainerWidgets(mockRoom, Container.Top)).toEqual([mockApps[1]]);
|
expect(store.getContainerWidgets(mockRoom, "top")).toEqual([mockApps[1]]);
|
||||||
expect(store.getContainerWidgets(mockRoom, Container.Center)).toEqual([]);
|
expect(store.getContainerWidgets(mockRoom, "center")).toEqual([]);
|
||||||
expect(new Set(store.getContainerWidgets(mockRoom, Container.Right))).toEqual(
|
expect(new Set(store.getContainerWidgets(mockRoom, "right"))).toEqual(
|
||||||
new Set([mockApps[2], mockApps[3], mockApps[0]]),
|
new Set([mockApps[2], mockApps[3], mockApps[0]]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("remove maximised when pinning (same widget)", async () => {
|
it("remove maximised when pinning (same widget)", async () => {
|
||||||
store.recalculateRoom(mockRoom);
|
store.recalculateRoom(mockRoom);
|
||||||
store.moveToContainer(mockRoom, mockApps[0], Container.Center);
|
store.moveToContainer(mockRoom, mockApps[0], "center");
|
||||||
store.moveToContainer(mockRoom, mockApps[0], Container.Top);
|
store.moveToContainer(mockRoom, mockApps[0], "top");
|
||||||
expect(store.getContainerWidgets(mockRoom, Container.Top)).toEqual([mockApps[0]]);
|
expect(store.getContainerWidgets(mockRoom, "top")).toEqual([mockApps[0]]);
|
||||||
expect(store.getContainerWidgets(mockRoom, Container.Center)).toEqual([]);
|
expect(store.getContainerWidgets(mockRoom, "center")).toEqual([]);
|
||||||
expect(new Set(store.getContainerWidgets(mockRoom, Container.Right))).toEqual(
|
expect(new Set(store.getContainerWidgets(mockRoom, "right"))).toEqual(
|
||||||
new Set([mockApps[2], mockApps[3], mockApps[1]]),
|
new Set([mockApps[2], mockApps[3], mockApps[1]]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -171,9 +171,9 @@ describe("WidgetLayoutStore", () => {
|
|||||||
await store.start();
|
await store.start();
|
||||||
|
|
||||||
expect(roomUpdateListener).toHaveBeenCalled();
|
expect(roomUpdateListener).toHaveBeenCalled();
|
||||||
expect(store.getContainerWidgets(mockRoom, Container.Top)).toEqual([]);
|
expect(store.getContainerWidgets(mockRoom, "top")).toEqual([]);
|
||||||
expect(store.getContainerWidgets(mockRoom, Container.Center)).toEqual([]);
|
expect(store.getContainerWidgets(mockRoom, "center")).toEqual([]);
|
||||||
expect(store.getContainerWidgets(mockRoom, Container.Right)).toEqual([
|
expect(store.getContainerWidgets(mockRoom, "right")).toEqual([
|
||||||
mockApps[0],
|
mockApps[0],
|
||||||
mockApps[1],
|
mockApps[1],
|
||||||
mockApps[2],
|
mockApps[2],
|
||||||
@@ -190,58 +190,50 @@ describe("WidgetLayoutStore", () => {
|
|||||||
));
|
));
|
||||||
store.recalculateRoom(mockRoom);
|
store.recalculateRoom(mockRoom);
|
||||||
expect(roomUpdateListener).toHaveBeenCalled();
|
expect(roomUpdateListener).toHaveBeenCalled();
|
||||||
expect(store.getContainerWidgets(mockRoom, Container.Top)).toEqual([]);
|
expect(store.getContainerWidgets(mockRoom, "top")).toEqual([]);
|
||||||
expect(store.getContainerWidgets(mockRoom, Container.Center)).toEqual([]);
|
expect(store.getContainerWidgets(mockRoom, "center")).toEqual([]);
|
||||||
expect(store.getContainerWidgets(mockRoom, Container.Right)).toEqual([]);
|
expect(store.getContainerWidgets(mockRoom, "right")).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should clear the layout if the client is not viable", () => {
|
it("should clear the layout if the client is not viable", () => {
|
||||||
store.recalculateRoom(mockRoom);
|
store.recalculateRoom(mockRoom);
|
||||||
defaultDispatcher.dispatch({ action: Action.ClientNotViable }, true);
|
defaultDispatcher.dispatch({ action: Action.ClientNotViable }, true);
|
||||||
|
|
||||||
expect(store.getContainerWidgets(mockRoom, Container.Top)).toEqual([]);
|
expect(store.getContainerWidgets(mockRoom, "top")).toEqual([]);
|
||||||
expect(store.getContainerWidgets(mockRoom, Container.Center)).toEqual([]);
|
expect(store.getContainerWidgets(mockRoom, "center")).toEqual([]);
|
||||||
expect(store.getContainerWidgets(mockRoom, Container.Right)).toEqual([]);
|
expect(store.getContainerWidgets(mockRoom, "right")).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return the expected resizer distributions", () => {
|
it("should return the expected resizer distributions", () => {
|
||||||
// this only works for top widgets
|
// this only works for top widgets
|
||||||
store.recalculateRoom(mockRoom);
|
store.recalculateRoom(mockRoom);
|
||||||
store.moveToContainer(mockRoom, mockApps[0], Container.Top);
|
store.moveToContainer(mockRoom, mockApps[0], "top");
|
||||||
store.moveToContainer(mockRoom, mockApps[1], Container.Top);
|
store.moveToContainer(mockRoom, mockApps[1], "top");
|
||||||
expect(store.getResizerDistributions(mockRoom, Container.Top)).toEqual(["50.0%"]);
|
expect(store.getResizerDistributions(mockRoom, "top")).toEqual(["50.0%"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should set and return container height", () => {
|
it("should set and return container height", () => {
|
||||||
store.recalculateRoom(mockRoom);
|
store.recalculateRoom(mockRoom);
|
||||||
store.moveToContainer(mockRoom, mockApps[0], Container.Top);
|
store.moveToContainer(mockRoom, mockApps[0], "top");
|
||||||
store.moveToContainer(mockRoom, mockApps[1], Container.Top);
|
store.moveToContainer(mockRoom, mockApps[1], "top");
|
||||||
store.setContainerHeight(mockRoom, Container.Top, 23);
|
store.setContainerHeight(mockRoom, "top", 23);
|
||||||
expect(store.getContainerHeight(mockRoom, Container.Top)).toBe(23);
|
expect(store.getContainerHeight(mockRoom, "top")).toBe(23);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should move a widget within a container", () => {
|
it("should move a widget within a container", () => {
|
||||||
store.recalculateRoom(mockRoom);
|
store.recalculateRoom(mockRoom);
|
||||||
store.moveToContainer(mockRoom, mockApps[0], Container.Top);
|
store.moveToContainer(mockRoom, mockApps[0], "top");
|
||||||
store.moveToContainer(mockRoom, mockApps[1], Container.Top);
|
store.moveToContainer(mockRoom, mockApps[1], "top");
|
||||||
store.moveToContainer(mockRoom, mockApps[2], Container.Top);
|
store.moveToContainer(mockRoom, mockApps[2], "top");
|
||||||
expect(store.getContainerWidgets(mockRoom, Container.Top)).toStrictEqual([
|
expect(store.getContainerWidgets(mockRoom, "top")).toStrictEqual([mockApps[0], mockApps[1], mockApps[2]]);
|
||||||
mockApps[0],
|
store.moveWithinContainer(mockRoom, "top", mockApps[0], 1);
|
||||||
mockApps[1],
|
expect(store.getContainerWidgets(mockRoom, "top")).toStrictEqual([mockApps[1], mockApps[0], mockApps[2]]);
|
||||||
mockApps[2],
|
|
||||||
]);
|
|
||||||
store.moveWithinContainer(mockRoom, Container.Top, mockApps[0], 1);
|
|
||||||
expect(store.getContainerWidgets(mockRoom, Container.Top)).toStrictEqual([
|
|
||||||
mockApps[1],
|
|
||||||
mockApps[0],
|
|
||||||
mockApps[2],
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should copy the layout to the room", async () => {
|
it("should copy the layout to the room", async () => {
|
||||||
await store.start();
|
await store.start();
|
||||||
store.recalculateRoom(mockRoom);
|
store.recalculateRoom(mockRoom);
|
||||||
store.moveToContainer(mockRoom, mockApps[0], Container.Top);
|
store.moveToContainer(mockRoom, mockApps[0], "top");
|
||||||
store.copyLayoutToRoom(mockRoom);
|
store.copyLayoutToRoom(mockRoom);
|
||||||
|
|
||||||
expect(mocked(client.sendStateEvent).mock.calls).toMatchInlineSnapshot(`
|
expect(mocked(client.sendStateEvent).mock.calls).toMatchInlineSnapshot(`
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
import { stubClient } from "../../test-utils";
|
import { stubClient } from "../../test-utils";
|
||||||
import WidgetUtils from "../../../src/utils/WidgetUtils";
|
import WidgetUtils from "../../../src/utils/WidgetUtils";
|
||||||
import { type IApp } from "../../../src/utils/WidgetUtils-types";
|
import { type IApp } from "../../../src/utils/WidgetUtils-types";
|
||||||
import { Container, WidgetLayoutStore } from "../../../src/stores/widgets/WidgetLayoutStore";
|
import { WidgetLayoutStore } from "../../../src/stores/widgets/WidgetLayoutStore";
|
||||||
import * as livestream from "../../../src/Livestream";
|
import * as livestream from "../../../src/Livestream";
|
||||||
import Modal from "../../../src/Modal";
|
import Modal from "../../../src/Modal";
|
||||||
import SettingsStore from "../../../src/settings/SettingsStore";
|
import SettingsStore from "../../../src/settings/SettingsStore";
|
||||||
@@ -138,12 +138,7 @@ describe("WidgetContextMenuViewModel", () => {
|
|||||||
const vm = new WidgetContextMenuViewModel(props);
|
const vm = new WidgetContextMenuViewModel(props);
|
||||||
vm.onMoveButton(1);
|
vm.onMoveButton(1);
|
||||||
|
|
||||||
expect(WidgetLayoutStore.instance.moveWithinContainer).toHaveBeenCalledWith(
|
expect(WidgetLayoutStore.instance.moveWithinContainer).toHaveBeenCalledWith(props.room, "top", props.app, 1);
|
||||||
props.room,
|
|
||||||
Container.Top,
|
|
||||||
props.app,
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
expect(props.onFinished).toHaveBeenCalled();
|
expect(props.onFinished).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { WidgetPipViewModel } from "../../../src/viewmodels/room/WidgetPipViewMo
|
|||||||
import WidgetStore, { type IApp } from "../../../src/stores/WidgetStore";
|
import WidgetStore, { type IApp } from "../../../src/stores/WidgetStore";
|
||||||
import defaultDispatcher from "../../../src/dispatcher/dispatcher";
|
import defaultDispatcher from "../../../src/dispatcher/dispatcher";
|
||||||
import { Action } from "../../../src/dispatcher/actions";
|
import { Action } from "../../../src/dispatcher/actions";
|
||||||
import { Container, WidgetLayoutStore } from "../../../src/stores/widgets/WidgetLayoutStore";
|
import { WidgetLayoutStore } from "../../../src/stores/widgets/WidgetLayoutStore";
|
||||||
import { CallStore, CallStoreEvent } from "../../../src/stores/CallStore";
|
import { CallStore, CallStoreEvent } from "../../../src/stores/CallStore";
|
||||||
import { type Call } from "../../../src/models/Call";
|
import { type Call } from "../../../src/models/Call";
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ describe("WidgetPipViewModel", () => {
|
|||||||
|
|
||||||
vm.setViewingRoom(true);
|
vm.setViewingRoom(true);
|
||||||
vm.onBackClick(createBackClickEvent());
|
vm.onBackClick(createBackClickEvent());
|
||||||
expect(moveSpy).toHaveBeenCalledWith(room, widget, Container.Center);
|
expect(moveSpy).toHaveBeenCalledWith(room, widget, "center");
|
||||||
|
|
||||||
moveSpy.mockClear();
|
moveSpy.mockClear();
|
||||||
vm.setViewingRoom(false);
|
vm.setViewingRoom(false);
|
||||||
|
|||||||
Generated
+18
-18
@@ -7,8 +7,8 @@ settings:
|
|||||||
catalogs:
|
catalogs:
|
||||||
default:
|
default:
|
||||||
'@element-hq/element-web-module-api':
|
'@element-hq/element-web-module-api':
|
||||||
specifier: 1.11.0
|
specifier: 1.12.0
|
||||||
version: 1.11.0
|
version: 1.12.0
|
||||||
'@element-hq/element-web-playwright-common':
|
'@element-hq/element-web-playwright-common':
|
||||||
specifier: 2.2.7
|
specifier: 2.2.7
|
||||||
version: 2.2.7
|
version: 2.2.7
|
||||||
@@ -149,7 +149,7 @@ importers:
|
|||||||
version: 7.28.6
|
version: 7.28.6
|
||||||
'@element-hq/element-web-module-api':
|
'@element-hq/element-web-module-api':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 1.11.0(@matrix-org/react-sdk-module-api@2.5.0(patch_hash=016146c9cc96e6363609d2b2ac0896ccef567882eb1d73b75a77b8a30929de96)(react@19.2.4))(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(matrix-web-i18n@3.6.0)(react@19.2.4)
|
version: 1.12.0(@matrix-org/react-sdk-module-api@2.5.0(patch_hash=016146c9cc96e6363609d2b2ac0896ccef567882eb1d73b75a77b8a30929de96)(react@19.2.4))(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(matrix-web-i18n@3.6.0)(react@19.2.4)
|
||||||
'@element-hq/web-shared-components':
|
'@element-hq/web-shared-components':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/shared-components
|
version: link:../../packages/shared-components
|
||||||
@@ -278,7 +278,7 @@ importers:
|
|||||||
version: 1.0.3
|
version: 1.0.3
|
||||||
matrix-js-sdk:
|
matrix-js-sdk:
|
||||||
specifier: github:matrix-org/matrix-js-sdk#develop
|
specifier: github:matrix-org/matrix-js-sdk#develop
|
||||||
version: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/219eb617dc6d5f48c9424eae38b9863fd177f99c
|
version: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/d99363d288b8471e38892ec11fd9b37c062b85fe
|
||||||
matrix-widget-api:
|
matrix-widget-api:
|
||||||
specifier: ^1.17.0
|
specifier: ^1.17.0
|
||||||
version: 1.17.0
|
version: 1.17.0
|
||||||
@@ -411,7 +411,7 @@ importers:
|
|||||||
version: 0.16.3
|
version: 0.16.3
|
||||||
'@element-hq/element-web-playwright-common':
|
'@element-hq/element-web-playwright-common':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 2.2.7(@element-hq/element-web-module-api@1.11.0(@matrix-org/react-sdk-module-api@2.5.0(patch_hash=016146c9cc96e6363609d2b2ac0896ccef567882eb1d73b75a77b8a30929de96)(react@19.2.4))(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(matrix-web-i18n@3.6.0)(react@19.2.4))(@playwright/test@1.58.2)(playwright-core@1.58.2)
|
version: 2.2.7(@element-hq/element-web-module-api@1.12.0(@matrix-org/react-sdk-module-api@2.5.0(patch_hash=016146c9cc96e6363609d2b2ac0896ccef567882eb1d73b75a77b8a30929de96)(react@19.2.4))(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(matrix-web-i18n@3.6.0)(react@19.2.4))(@playwright/test@1.58.2)(playwright-core@1.58.2)
|
||||||
'@element-hq/element-web-playwright-common-local':
|
'@element-hq/element-web-playwright-common-local':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/playwright-common
|
version: link:../../packages/playwright-common
|
||||||
@@ -750,7 +750,7 @@ importers:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@element-hq/element-web-module-api':
|
'@element-hq/element-web-module-api':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 1.11.0(@matrix-org/react-sdk-module-api@2.5.0(patch_hash=016146c9cc96e6363609d2b2ac0896ccef567882eb1d73b75a77b8a30929de96)(react@19.2.4))(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(matrix-web-i18n@3.6.0)(react@19.2.4)
|
version: 1.12.0(@matrix-org/react-sdk-module-api@2.5.0(patch_hash=016146c9cc96e6363609d2b2ac0896ccef567882eb1d73b75a77b8a30929de96)(react@19.2.4))(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(matrix-web-i18n@3.6.0)(react@19.2.4)
|
||||||
'@matrix-org/spec':
|
'@matrix-org/spec':
|
||||||
specifier: ^1.7.0
|
specifier: ^1.7.0
|
||||||
version: 1.16.0
|
version: 1.16.0
|
||||||
@@ -2016,8 +2016,8 @@ packages:
|
|||||||
'@element-hq/element-call-embedded@0.16.3':
|
'@element-hq/element-call-embedded@0.16.3':
|
||||||
resolution: {integrity: sha512-OViKJonDaDNVBUW9WdV9mk78/Ruh34C7XsEgt3O8D9z+64C39elbIgllHSoH5S12IRlv9RYrrV37FZLo6QWsDQ==}
|
resolution: {integrity: sha512-OViKJonDaDNVBUW9WdV9mk78/Ruh34C7XsEgt3O8D9z+64C39elbIgllHSoH5S12IRlv9RYrrV37FZLo6QWsDQ==}
|
||||||
|
|
||||||
'@element-hq/element-web-module-api@1.11.0':
|
'@element-hq/element-web-module-api@1.12.0':
|
||||||
resolution: {integrity: sha512-RBkvt+Z32CGLkiPtYcQTryQBjG01ZrSVV98CS1cPz/kTeBtxEbVBpqDoOLdGvpmVe0dWo4DLaFcldw2iK39TPA==}
|
resolution: {integrity: sha512-fLhHFiL1UbRjolpgera3osHHxhSzfnDGTRhaDEv1UsrHRHwMu3hb/IcyXNqGhLXkJiuX8XoOH0aetaAUqQ0YQA==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@matrix-org/react-sdk-module-api': '*'
|
'@matrix-org/react-sdk-module-api': '*'
|
||||||
@@ -2669,8 +2669,8 @@ packages:
|
|||||||
'@matrix-org/emojibase-bindings@1.5.0':
|
'@matrix-org/emojibase-bindings@1.5.0':
|
||||||
resolution: {integrity: sha512-+W9/ow2Z3iQa7ZOF698PBhwNcgGkn36B5Sr8VDPx8N8CH7+Uw+7TrtbtKPZVdgf4m/THmgmfX40jS5YDBsLaYg==}
|
resolution: {integrity: sha512-+W9/ow2Z3iQa7ZOF698PBhwNcgGkn36B5Sr8VDPx8N8CH7+Uw+7TrtbtKPZVdgf4m/THmgmfX40jS5YDBsLaYg==}
|
||||||
|
|
||||||
'@matrix-org/matrix-sdk-crypto-wasm@17.1.0':
|
'@matrix-org/matrix-sdk-crypto-wasm@18.0.0':
|
||||||
resolution: {integrity: sha512-yKPqBvKlHSqkt/UJh+Z+zLKQP8bd19OxokXYXh3VkKbW0+C44nPHsidSwd3SH+RxT+Ck2PDRwVcVXEnUft+/2g==}
|
resolution: {integrity: sha512-88+n+dvxLI1cjS10UIlKXVYK7TGWbpAnnaDC9fow7ch/hCvdu3dFhJ3tS3/13N9s9+1QFXB4FFuommj+tHJPhQ==}
|
||||||
engines: {node: '>= 18'}
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
'@matrix-org/react-sdk-module-api@2.5.0':
|
'@matrix-org/react-sdk-module-api@2.5.0':
|
||||||
@@ -7819,8 +7819,8 @@ packages:
|
|||||||
matrix-events-sdk@0.0.1:
|
matrix-events-sdk@0.0.1:
|
||||||
resolution: {integrity: sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==}
|
resolution: {integrity: sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==}
|
||||||
|
|
||||||
matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/219eb617dc6d5f48c9424eae38b9863fd177f99c:
|
matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/d99363d288b8471e38892ec11fd9b37c062b85fe:
|
||||||
resolution: {tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/219eb617dc6d5f48c9424eae38b9863fd177f99c}
|
resolution: {tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/d99363d288b8471e38892ec11fd9b37c062b85fe}
|
||||||
version: 41.1.0
|
version: 41.1.0
|
||||||
engines: {node: '>=22.0.0'}
|
engines: {node: '>=22.0.0'}
|
||||||
|
|
||||||
@@ -11980,7 +11980,7 @@ snapshots:
|
|||||||
|
|
||||||
'@element-hq/element-call-embedded@0.16.3': {}
|
'@element-hq/element-call-embedded@0.16.3': {}
|
||||||
|
|
||||||
'@element-hq/element-web-module-api@1.11.0(@matrix-org/react-sdk-module-api@2.5.0(patch_hash=016146c9cc96e6363609d2b2ac0896ccef567882eb1d73b75a77b8a30929de96)(react@19.2.4))(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(matrix-web-i18n@3.6.0)(react@19.2.4)':
|
'@element-hq/element-web-module-api@1.12.0(@matrix-org/react-sdk-module-api@2.5.0(patch_hash=016146c9cc96e6363609d2b2ac0896ccef567882eb1d73b75a77b8a30929de96)(react@19.2.4))(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(matrix-web-i18n@3.6.0)(react@19.2.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/react': 19.2.10
|
'@types/react': 19.2.10
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.10)
|
'@types/react-dom': 19.2.3(@types/react@19.2.10)
|
||||||
@@ -11989,10 +11989,10 @@ snapshots:
|
|||||||
'@matrix-org/react-sdk-module-api': 2.5.0(patch_hash=016146c9cc96e6363609d2b2ac0896ccef567882eb1d73b75a77b8a30929de96)(react@19.2.4)
|
'@matrix-org/react-sdk-module-api': 2.5.0(patch_hash=016146c9cc96e6363609d2b2ac0896ccef567882eb1d73b75a77b8a30929de96)(react@19.2.4)
|
||||||
matrix-web-i18n: 3.6.0
|
matrix-web-i18n: 3.6.0
|
||||||
|
|
||||||
'@element-hq/element-web-playwright-common@2.2.7(@element-hq/element-web-module-api@1.11.0(@matrix-org/react-sdk-module-api@2.5.0(patch_hash=016146c9cc96e6363609d2b2ac0896ccef567882eb1d73b75a77b8a30929de96)(react@19.2.4))(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(matrix-web-i18n@3.6.0)(react@19.2.4))(@playwright/test@1.58.2)(playwright-core@1.58.2)':
|
'@element-hq/element-web-playwright-common@2.2.7(@element-hq/element-web-module-api@1.12.0(@matrix-org/react-sdk-module-api@2.5.0(patch_hash=016146c9cc96e6363609d2b2ac0896ccef567882eb1d73b75a77b8a30929de96)(react@19.2.4))(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(matrix-web-i18n@3.6.0)(react@19.2.4))(@playwright/test@1.58.2)(playwright-core@1.58.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@axe-core/playwright': 4.11.1(playwright-core@1.58.2)
|
'@axe-core/playwright': 4.11.1(playwright-core@1.58.2)
|
||||||
'@element-hq/element-web-module-api': 1.11.0(@matrix-org/react-sdk-module-api@2.5.0(patch_hash=016146c9cc96e6363609d2b2ac0896ccef567882eb1d73b75a77b8a30929de96)(react@19.2.4))(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(matrix-web-i18n@3.6.0)(react@19.2.4)
|
'@element-hq/element-web-module-api': 1.12.0(@matrix-org/react-sdk-module-api@2.5.0(patch_hash=016146c9cc96e6363609d2b2ac0896ccef567882eb1d73b75a77b8a30929de96)(react@19.2.4))(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(matrix-web-i18n@3.6.0)(react@19.2.4)
|
||||||
'@playwright/test': 1.58.2
|
'@playwright/test': 1.58.2
|
||||||
'@testcontainers/postgresql': 11.11.0
|
'@testcontainers/postgresql': 11.11.0
|
||||||
glob: 13.0.6
|
glob: 13.0.6
|
||||||
@@ -12712,7 +12712,7 @@ snapshots:
|
|||||||
emojibase: 17.0.0
|
emojibase: 17.0.0
|
||||||
emojibase-data: 17.0.0(emojibase@17.0.0)
|
emojibase-data: 17.0.0(emojibase@17.0.0)
|
||||||
|
|
||||||
'@matrix-org/matrix-sdk-crypto-wasm@17.1.0': {}
|
'@matrix-org/matrix-sdk-crypto-wasm@18.0.0': {}
|
||||||
|
|
||||||
'@matrix-org/react-sdk-module-api@2.5.0(patch_hash=016146c9cc96e6363609d2b2ac0896ccef567882eb1d73b75a77b8a30929de96)(react@19.2.4)':
|
'@matrix-org/react-sdk-module-api@2.5.0(patch_hash=016146c9cc96e6363609d2b2ac0896ccef567882eb1d73b75a77b8a30929de96)(react@19.2.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -18712,10 +18712,10 @@ snapshots:
|
|||||||
|
|
||||||
matrix-events-sdk@0.0.1: {}
|
matrix-events-sdk@0.0.1: {}
|
||||||
|
|
||||||
matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/219eb617dc6d5f48c9424eae38b9863fd177f99c:
|
matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/d99363d288b8471e38892ec11fd9b37c062b85fe:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.28.6
|
'@babel/runtime': 7.28.6
|
||||||
'@matrix-org/matrix-sdk-crypto-wasm': 17.1.0
|
'@matrix-org/matrix-sdk-crypto-wasm': 18.0.0
|
||||||
another-json: 0.2.0
|
another-json: 0.2.0
|
||||||
bs58: 6.0.0
|
bs58: 6.0.0
|
||||||
content-type: 1.0.5
|
content-type: 1.0.5
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@ catalog:
|
|||||||
"@playwright/test": 1.58.2
|
"@playwright/test": 1.58.2
|
||||||
"playwright-core": 1.58.2
|
"playwright-core": 1.58.2
|
||||||
# Module API
|
# Module API
|
||||||
"@element-hq/element-web-module-api": 1.11.0
|
"@element-hq/element-web-module-api": 1.12.0
|
||||||
# Compound
|
# Compound
|
||||||
"@vector-im/compound-design-tokens": 6.10.1
|
"@vector-im/compound-design-tokens": 6.10.1
|
||||||
"@vector-im/compound-web": 8.4.0
|
"@vector-im/compound-web": 8.4.0
|
||||||
|
|||||||
Reference in New Issue
Block a user