mirror of
https://github.com/vector-im/element-web.git
synced 2026-03-28 08:41:24 +01: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:
parent
86692ce0a7
commit
09bbf796dc
@ -42,7 +42,7 @@ import { Action } from "./dispatcher/actions";
|
||||
import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from "./widgets/ManagedHybrid";
|
||||
import SdkConfig from "./SdkConfig";
|
||||
import { ensureDMExists } from "./createRoom";
|
||||
import { Container, WidgetLayoutStore } from "./stores/widgets/WidgetLayoutStore";
|
||||
import { WidgetLayoutStore } from "./stores/widgets/WidgetLayoutStore";
|
||||
import IncomingLegacyCallToast, { getIncomingLegacyCallToastKey } from "./toasts/IncomingLegacyCallToast";
|
||||
import ToastStore from "./stores/ToastStore";
|
||||
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
|
||||
const room = client.getRoom(roomId);
|
||||
if (isNotNull(room)) {
|
||||
WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Top);
|
||||
WidgetLayoutStore.instance.moveToContainer(room, widget, "top");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@ -90,7 +90,7 @@ import { CallView } from "../views/voip/CallView";
|
||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||
import Notifier from "../../Notifier";
|
||||
import { showToast as showNotificationsToast } from "../../toasts/DesktopNotificationsToast";
|
||||
import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
|
||||
import { WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
|
||||
import { getKeyBindingsManager } from "../../KeyBindingsManager";
|
||||
import { objectHasDiff } from "../../utils/objects";
|
||||
import SpaceRoomView from "./SpaceRoomView";
|
||||
@ -141,6 +141,7 @@ import { isRoomEncrypted } from "../../hooks/useIsEncrypted";
|
||||
import { type RoomViewStore } from "../../stores/RoomViewStore.tsx";
|
||||
import { RoomStatusBarViewModel } from "../../viewmodels/room/RoomStatusBar.ts";
|
||||
import { EncryptionEventViewModel } from "../../viewmodels/event-tiles/EncryptionEventViewModel.ts";
|
||||
import { ModuleApi } from "../../modules/Api.ts";
|
||||
|
||||
const DEBUG = false;
|
||||
const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000;
|
||||
@ -953,7 +954,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
// Otherwise (in case the user set hideWidgetDrawer by clicking the button) follow the parameter.
|
||||
const isManuallyShown = hideWidgetDrawer ? hideWidgetDrawer === "false" : true;
|
||||
|
||||
const widgets = this.context.widgetLayoutStore.getContainerWidgets(room, Container.Top);
|
||||
const widgets = this.context.widgetLayoutStore.getContainerWidgets(room, "top");
|
||||
return isManuallyShown && widgets.length > 0;
|
||||
}
|
||||
|
||||
@ -2727,6 +2728,12 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
this.state.mainSplitContentType === MainSplitContentType.Call ? "video_room" : "maximised_widget";
|
||||
}
|
||||
|
||||
const extraButtons: JSX.Element[] = [];
|
||||
for (const cb of ModuleApi.instance.extras.roomHeaderButtonsCallbacks) {
|
||||
const b = cb(this.state.room.roomId);
|
||||
if (b) extraButtons.push(b);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScopedRoomContextProvider {...this.state} roomViewStore={this.roomViewStore}>
|
||||
<div
|
||||
@ -2754,7 +2761,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
{!this.props.hideHeader && (
|
||||
<RoomHeader
|
||||
room={this.state.room}
|
||||
additionalButtons={this.state.viewRoomOpts.buttons}
|
||||
legacyAdditionalButtons={this.state.viewRoomOpts.buttons}
|
||||
extraButtons={<>{extraButtons}</>}
|
||||
/>
|
||||
)}
|
||||
{mainSplitBody}
|
||||
|
||||
@ -25,7 +25,7 @@ import QuestionDialog from "../dialogs/QuestionDialog";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import { WidgetType } from "../../../widgets/WidgetType";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import { getConfigLivestreamUrl, startJitsiAudioLivestream } from "../../../Livestream";
|
||||
import { ModuleRunner } from "../../../modules/ModuleRunner";
|
||||
import { ElementWidget, type WidgetMessaging } from "../../../stores/widgets/WidgetMessaging";
|
||||
@ -79,7 +79,7 @@ const showSnapshotButton = (widgetMessaging: WidgetMessaging | undefined): boole
|
||||
const showMoveButtons = (app: IWidget, room: Room | undefined, showUnpin: boolean | undefined): [boolean, boolean] => {
|
||||
if (!showUnpin) return [false, false];
|
||||
|
||||
const pinnedWidgets = room ? WidgetLayoutStore.instance.getContainerWidgets(room, Container.Top) : [];
|
||||
const pinnedWidgets = room ? WidgetLayoutStore.instance.getContainerWidgets(room, "top") : [];
|
||||
const widgetIndex = pinnedWidgets.findIndex((widget) => widget.id === app.id);
|
||||
return [widgetIndex > 0, widgetIndex < pinnedWidgets.length - 1];
|
||||
};
|
||||
@ -221,7 +221,7 @@ export const WidgetContextMenu: React.FC<IProps> = ({
|
||||
if (showMoveLeftButton) {
|
||||
const onClick = (): void => {
|
||||
if (!room) throw new Error("room must be defined");
|
||||
WidgetLayoutStore.instance.moveWithinContainer(room, Container.Top, app, -1);
|
||||
WidgetLayoutStore.instance.moveWithinContainer(room, "top", app, -1);
|
||||
onFinished();
|
||||
};
|
||||
|
||||
@ -232,7 +232,7 @@ export const WidgetContextMenu: React.FC<IProps> = ({
|
||||
if (showMoveRightButton) {
|
||||
const onClick = (): void => {
|
||||
if (!room) throw new Error("room must be defined");
|
||||
WidgetLayoutStore.instance.moveWithinContainer(room, Container.Top, app, 1);
|
||||
WidgetLayoutStore.instance.moveWithinContainer(room, "top", app, 1);
|
||||
onFinished();
|
||||
};
|
||||
|
||||
|
||||
@ -47,7 +47,7 @@ import { ElementWidget, WidgetMessaging, WidgetMessagingEvent } from "../../../s
|
||||
import WidgetAvatar from "../avatars/WidgetAvatar";
|
||||
import LegacyCallHandler from "../../../LegacyCallHandler";
|
||||
import { type IApp, isAppWidget } from "../../../stores/WidgetStore";
|
||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import { OwnProfileStore } from "../../../stores/OwnProfileStore";
|
||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||
import WidgetUtils from "../../../utils/WidgetUtils";
|
||||
@ -682,21 +682,17 @@ export default class AppTile extends React.Component<IProps, IState> {
|
||||
|
||||
private onToggleMaximisedClick = (): void => {
|
||||
if (!this.props.room) return; // ignore action - it shouldn't even be visible
|
||||
const targetContainer = WidgetLayoutStore.instance.isInContainer(
|
||||
this.props.room,
|
||||
this.props.app,
|
||||
Container.Center,
|
||||
)
|
||||
? Container.Top
|
||||
: Container.Center;
|
||||
const targetContainer = WidgetLayoutStore.instance.isInContainer(this.props.room, this.props.app, "center")
|
||||
? "top"
|
||||
: "center";
|
||||
WidgetLayoutStore.instance.moveToContainer(this.props.room, this.props.app, targetContainer);
|
||||
|
||||
if (targetContainer === Container.Top) this.closeChatCardIfNeeded();
|
||||
if (targetContainer === "top") this.closeChatCardIfNeeded();
|
||||
};
|
||||
|
||||
private onMinimiseClicked = (): void => {
|
||||
if (!this.props.room) return; // ignore action - it shouldn't even be visible
|
||||
WidgetLayoutStore.instance.moveToContainer(this.props.room, this.props.app, Container.Right);
|
||||
WidgetLayoutStore.instance.moveToContainer(this.props.room, this.props.app, "right");
|
||||
this.closeChatCardIfNeeded();
|
||||
};
|
||||
|
||||
@ -822,8 +818,7 @@ export default class AppTile extends React.Component<IProps, IState> {
|
||||
const layoutButtons: ReactNode[] = [];
|
||||
if (this.props.showLayoutButtons) {
|
||||
const isMaximised =
|
||||
this.props.room &&
|
||||
WidgetLayoutStore.instance.isInContainer(this.props.room, this.props.app, Container.Center);
|
||||
this.props.room && WidgetLayoutStore.instance.isInContainer(this.props.room, this.props.app, "center");
|
||||
|
||||
layoutButtons.push(
|
||||
<AccessibleButton
|
||||
|
||||
@ -14,7 +14,7 @@ import { EventTileBubble } from "@element-hq/web-shared-components";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import WidgetStore from "../../../stores/WidgetStore";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
@ -32,7 +32,7 @@ export default class MJitsiWidgetEvent extends React.PureComponent<IProps> {
|
||||
const widget = WidgetStore.instance.getRoom(room.roomId, true).widgets.find((w) => w.id === widgetId);
|
||||
|
||||
let joinCopy: string | null = _t("timeline|m.widget|jitsi_join_top_prompt");
|
||||
if (widget && WidgetLayoutStore.instance.isInContainer(room, widget, Container.Right)) {
|
||||
if (widget && WidgetLayoutStore.instance.isInContainer(room, widget, "right")) {
|
||||
joinCopy = _t("timeline|m.widget|jitsi_join_right_prompt");
|
||||
} else if (!widget) {
|
||||
joinCopy = null;
|
||||
|
||||
@ -24,7 +24,7 @@ import { useContextMenu } from "../../structures/ContextMenu";
|
||||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||
import { type IApp } from "../../../stores/WidgetStore";
|
||||
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
||||
import { Container, MAX_PINNED, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import { MAX_PINNED, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import WidgetAvatar from "../avatars/WidgetAvatar";
|
||||
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
|
||||
@ -58,18 +58,18 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const isPinned = WidgetLayoutStore.instance.isInContainer(room, app, Container.Top);
|
||||
const isPinned = WidgetLayoutStore.instance.isInContainer(room, app, "top");
|
||||
const togglePin = isPinned
|
||||
? () => {
|
||||
WidgetLayoutStore.instance.moveToContainer(room, app, Container.Right);
|
||||
WidgetLayoutStore.instance.moveToContainer(room, app, "right");
|
||||
}
|
||||
: () => {
|
||||
WidgetLayoutStore.instance.moveToContainer(room, app, Container.Top);
|
||||
WidgetLayoutStore.instance.moveToContainer(room, app, "top");
|
||||
};
|
||||
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
|
||||
|
||||
const cannotPin = !isPinned && !WidgetLayoutStore.instance.canAddToContainer(room, Container.Top);
|
||||
const cannotPin = !isPinned && !WidgetLayoutStore.instance.canAddToContainer(room, "top");
|
||||
|
||||
let pinTitle: string;
|
||||
if (cannotPin) {
|
||||
@ -78,7 +78,7 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
|
||||
pinTitle = isPinned ? _t("action|unpin") : _t("action|pin");
|
||||
}
|
||||
|
||||
const isMaximised = WidgetLayoutStore.instance.isInContainer(room, app, Container.Center);
|
||||
const isMaximised = WidgetLayoutStore.instance.isInContainer(room, app, "center");
|
||||
|
||||
let openTitle = "";
|
||||
if (isPinned) {
|
||||
|
||||
@ -15,7 +15,7 @@ import WidgetUtils, { useWidgets } from "../../../utils/WidgetUtils";
|
||||
import AppTile from "../elements/AppTile";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { ContextMenuButton, useContextMenu } from "../../structures/ContextMenu";
|
||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||
import Heading from "../typography/Heading";
|
||||
import { WidgetContextMenu } from "../../../viewmodels/right-panel/WidgetContextMenuViewModel";
|
||||
@ -31,7 +31,7 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
|
||||
|
||||
const apps = useWidgets(room);
|
||||
const app = apps.find((a) => a.id === widgetId);
|
||||
const isRight = app && WidgetLayoutStore.instance.isInContainer(room, app, Container.Right);
|
||||
const isRight = app && WidgetLayoutStore.instance.isInContainer(room, app, "right");
|
||||
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
|
||||
|
||||
|
||||
@ -22,7 +22,7 @@ import type ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||
import ResizeHandle from "../elements/ResizeHandle";
|
||||
import Resizer, { type IConfig } from "../../../resizer/resizer";
|
||||
import PercentageDistributor from "../../../resizer/distributors/percentage";
|
||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
import { type ActionPayload } from "../../../dispatcher/payloads";
|
||||
import Spinner from "../elements/Spinner";
|
||||
@ -39,9 +39,9 @@ interface IProps {
|
||||
|
||||
interface IState {
|
||||
apps: {
|
||||
[Container.Top]: IWidget[];
|
||||
[Container.Center]: IWidget[];
|
||||
[Container.Right]?: IWidget[];
|
||||
["top"]: IWidget[];
|
||||
["center"]: IWidget[];
|
||||
["right"]?: IWidget[];
|
||||
};
|
||||
resizingVertical: boolean; // true when changing the height of the apps drawer
|
||||
resizingHorizontal: boolean; // true when changing the distribution of the width between widgets
|
||||
@ -119,7 +119,7 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
|
||||
this.resizeContainer?.classList.remove("mx_AppsDrawer--resizing");
|
||||
WidgetLayoutStore.instance.setResizerDistributions(
|
||||
this.props.room,
|
||||
Container.Top,
|
||||
"top",
|
||||
this.topApps()
|
||||
.slice(1)
|
||||
.map((_, i) => this.resizer.forHandleAt(i)!.size),
|
||||
@ -152,7 +152,7 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
|
||||
if (prevProps.userId !== this.props.userId || prevProps.room !== this.props.room) {
|
||||
// Room has changed, update apps
|
||||
this.updateApps();
|
||||
} else if (this.getAppsHash(this.topApps()) !== this.getAppsHash(prevState.apps[Container.Top])) {
|
||||
} else if (this.getAppsHash(this.topApps()) !== this.getAppsHash(prevState.apps["top"])) {
|
||||
this.loadResizerPreferences();
|
||||
}
|
||||
}
|
||||
@ -166,7 +166,7 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
|
||||
};
|
||||
|
||||
private loadResizerPreferences = (): void => {
|
||||
const distributions = WidgetLayoutStore.instance.getResizerDistributions(this.props.room, Container.Top);
|
||||
const distributions = WidgetLayoutStore.instance.getResizerDistributions(this.props.room, "top");
|
||||
if (this.state.apps && this.topApps().length - 1 === distributions.length) {
|
||||
distributions.forEach((size, i) => {
|
||||
const distributor = this.resizer.forHandleAt(i);
|
||||
@ -206,11 +206,11 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
|
||||
};
|
||||
|
||||
private getApps = (): IState["apps"] => ({
|
||||
[Container.Top]: WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Top),
|
||||
[Container.Center]: WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Center),
|
||||
["top"]: WidgetLayoutStore.instance.getContainerWidgets(this.props.room, "top"),
|
||||
["center"]: WidgetLayoutStore.instance.getContainerWidgets(this.props.room, "center"),
|
||||
});
|
||||
private topApps = (): IWidget[] => this.state.apps[Container.Top];
|
||||
private centerApps = (): IWidget[] => this.state.apps[Container.Center];
|
||||
private topApps = (): IWidget[] => this.state.apps["top"];
|
||||
private centerApps = (): IWidget[] => this.state.apps["center"];
|
||||
|
||||
private updateApps = (): void => {
|
||||
if (this.unmounted) return;
|
||||
@ -321,7 +321,7 @@ const PersistentVResizer: React.FC<IPersistentResizerProps> = ({
|
||||
resizeNotifier,
|
||||
children,
|
||||
}) => {
|
||||
let defaultHeight = WidgetLayoutStore.instance.getContainerHeight(room, Container.Top);
|
||||
let defaultHeight = WidgetLayoutStore.instance.getContainerHeight(room, "top");
|
||||
|
||||
// Arbitrary defaults to avoid NaN problems. 100 px or 3/4 of the visible window.
|
||||
if (!minHeight) minHeight = 100;
|
||||
@ -352,7 +352,7 @@ const PersistentVResizer: React.FC<IPersistentResizerProps> = ({
|
||||
let newHeight = defaultHeight! + d.height;
|
||||
newHeight = percentageOf(newHeight, minHeight, maxHeight) * 100;
|
||||
|
||||
WidgetLayoutStore.instance.setContainerHeight(room, Container.Top, newHeight);
|
||||
WidgetLayoutStore.instance.setContainerHeight(room, "top", newHeight);
|
||||
|
||||
resizeNotifier.stopResizing();
|
||||
}}
|
||||
|
||||
@ -60,10 +60,12 @@ import { useIsEncrypted } from "../../../../hooks/useIsEncrypted.ts";
|
||||
|
||||
function RoomHeaderButtons({
|
||||
room,
|
||||
additionalButtons,
|
||||
legacyAdditionalButtons,
|
||||
extraButtons,
|
||||
}: {
|
||||
room: Room;
|
||||
additionalButtons?: ViewRoomOpts["buttons"];
|
||||
legacyAdditionalButtons?: ViewRoomOpts["buttons"];
|
||||
extraButtons?: JSX.Element;
|
||||
}): JSX.Element {
|
||||
const members = useRoomMembers(room, 2500);
|
||||
const memberCount = useRoomMemberCount(room, { throttleWait: 2500, includeInvited: true });
|
||||
@ -297,7 +299,9 @@ function RoomHeaderButtons({
|
||||
roomContext.mainSplitContentType === MainSplitContentType.Call;
|
||||
return (
|
||||
<>
|
||||
{additionalButtons?.map((props) => {
|
||||
{extraButtons}
|
||||
|
||||
{legacyAdditionalButtons?.map((props) => {
|
||||
const label = props.label();
|
||||
|
||||
return (
|
||||
@ -427,11 +431,15 @@ function historyVisibilityIcon(historyVisibility: HistoryVisibility): JSX.Elemen
|
||||
|
||||
export default function RoomHeader({
|
||||
room,
|
||||
additionalButtons,
|
||||
extraButtons,
|
||||
legacyAdditionalButtons,
|
||||
oobData,
|
||||
}: {
|
||||
room: Room | LocalRoom;
|
||||
additionalButtons?: ViewRoomOpts["buttons"];
|
||||
// Extra buttons added by a new element web module API module
|
||||
extraButtons?: JSX.Element;
|
||||
// DEPRECATED: Buttons added by a legacy react-sdk module API module.
|
||||
legacyAdditionalButtons?: ViewRoomOpts["buttons"];
|
||||
oobData?: IOOBData;
|
||||
}): JSX.Element {
|
||||
const client = useMatrixClientContext();
|
||||
@ -530,7 +538,11 @@ export default function RoomHeader({
|
||||
</button>
|
||||
{/* If the room is local-only then we don't want to show any additional buttons, as it won't work */}
|
||||
{room instanceof LocalRoom === false && (
|
||||
<RoomHeaderButtons room={room} additionalButtons={additionalButtons} />
|
||||
<RoomHeaderButtons
|
||||
room={room}
|
||||
legacyAdditionalButtons={legacyAdditionalButtons}
|
||||
extraButtons={extraButtons}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
{askToJoinEnabled && <RoomKnocksBar room={room} />}
|
||||
|
||||
@ -22,7 +22,7 @@ import { useCall, useConnectionState, useParticipantCount } from "../useCall";
|
||||
import { useRoomMemberCount } from "../useRoomMembers";
|
||||
import { ConnectionState } from "../../models/Call";
|
||||
import { placeCall } from "../../utils/room/placeCall";
|
||||
import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
|
||||
import { WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
|
||||
import { useRoomState } from "../useRoomState";
|
||||
import { _t } from "../../languageHandler";
|
||||
import { isManagedHybridWidget, isManagedHybridWidgetEnabled } from "../../widgets/ManagedHybrid";
|
||||
@ -214,8 +214,8 @@ export const useRoomCall = (
|
||||
widget = groupCall?.widget ?? jitsiWidget;
|
||||
}
|
||||
const updateWidgetState = useCallback((): void => {
|
||||
setCanPinWidget(WidgetLayoutStore.instance.canAddToContainer(room, Container.Top));
|
||||
setWidgetPinned(!!widget && WidgetLayoutStore.instance.isInContainer(room, widget, Container.Top));
|
||||
setCanPinWidget(WidgetLayoutStore.instance.canAddToContainer(room, "top"));
|
||||
setWidgetPinned(!!widget && WidgetLayoutStore.instance.isInContainer(room, widget, "top"));
|
||||
}, [room, widget]);
|
||||
useEventEmitter(WidgetLayoutStore.instance, WidgetLayoutStore.emissionForRoom(room), updateWidgetState);
|
||||
useEffect(() => {
|
||||
@ -266,7 +266,7 @@ export const useRoomCall = (
|
||||
(evt: React.MouseEvent | undefined, callPlatformType: PlatformCallType): void => {
|
||||
evt?.stopPropagation();
|
||||
if (widget && promptPinWidget) {
|
||||
WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Top);
|
||||
WidgetLayoutStore.instance.moveToContainer(room, widget, "top");
|
||||
} else {
|
||||
placeCall(room, CallType.Voice, callPlatformType, evt?.shiftKey || undefined, true);
|
||||
}
|
||||
@ -277,7 +277,7 @@ export const useRoomCall = (
|
||||
(evt: React.MouseEvent | undefined, callPlatformType: PlatformCallType): void => {
|
||||
evt?.stopPropagation();
|
||||
if (widget && promptPinWidget) {
|
||||
WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Top);
|
||||
WidgetLayoutStore.instance.moveToContainer(room, widget, "top");
|
||||
} else {
|
||||
// If we have pressed shift then always skip the lobby, otherwise `undefined` will defer
|
||||
// to the defaults of the call implementation.
|
||||
|
||||
@ -31,6 +31,7 @@ import { ElementWebBuiltinsApi } from "./BuiltinsApi.tsx";
|
||||
import { ClientApi } from "./ClientApi.ts";
|
||||
import { StoresApi } from "./StoresApi.ts";
|
||||
import { WidgetLifecycleApi } from "./WidgetLifecycleApi.ts";
|
||||
import { WidgetApi } from "./WidgetApi.ts";
|
||||
import { CustomisationsApi } from "./customisationsApi.ts";
|
||||
|
||||
const legacyCustomisationsFactory = <T extends object>(baseCustomisations: T) => {
|
||||
@ -89,6 +90,7 @@ export class ModuleApi implements Api {
|
||||
public readonly extras = new ElementWebExtrasApi();
|
||||
public readonly builtins = new ElementWebBuiltinsApi();
|
||||
public readonly widgetLifecycle = new WidgetLifecycleApi();
|
||||
public readonly widget = new WidgetApi();
|
||||
public readonly rootNode = document.getElementById("matrixchat")!;
|
||||
public readonly client = new ClientApi();
|
||||
public readonly stores = new StoresApi();
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
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 { 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 { useTypedEventEmitter } from "../hooks/useEventEmitter";
|
||||
@ -26,6 +31,7 @@ interface EmittedEvents {
|
||||
export class ElementWebExtrasApi extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents> implements ExtrasApi {
|
||||
public spacePanelItems = new Map<string, SpacePanelItemProps>();
|
||||
public visibleRoomBySpaceKey = new Map<string, () => string[]>();
|
||||
public roomHeaderButtonsCallbacks: RoomHeaderButtonsCallback[] = [];
|
||||
|
||||
public setSpacePanelItem(spacekey: string, item: SpacePanelItemProps): void {
|
||||
this.spacePanelItems.set(spacekey, item);
|
||||
@ -35,6 +41,10 @@ export class ElementWebExtrasApi extends TypedEventEmitter<keyof EmittedEvents,
|
||||
public getVisibleRoomBySpaceKey(spaceKey: string, cb: () => string[]): void {
|
||||
this.visibleRoomBySpaceKey.set(spaceKey, cb);
|
||||
}
|
||||
|
||||
public addRoomHeaderButtonCallback(cb: RoomHeaderButtonsCallback): void {
|
||||
this.roomHeaderButtonsCallbacks.push(cb);
|
||||
}
|
||||
}
|
||||
|
||||
export function useModuleSpacePanelItems(api: ElementWebExtrasApi): ModuleSpacePanelItem[] {
|
||||
|
||||
47
apps/web/src/modules/WidgetApi.ts
Normal file
47
apps/web/src/modules/WidgetApi.ts
Normal file
@ -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 { type IWidget } from "matrix-widget-api";
|
||||
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 WidgetStore, { type IApp } from "../WidgetStore";
|
||||
@ -19,16 +20,10 @@ import { ReadyWatchingStore } from "../ReadyWatchingStore";
|
||||
import { SettingLevel } from "../../settings/SettingLevel";
|
||||
import { arrayFastClone } from "../../utils/arrays";
|
||||
import { UPDATE_EVENT } from "../AsyncStore";
|
||||
import {
|
||||
Container,
|
||||
type IStoredLayout,
|
||||
type ILayoutStateEvent,
|
||||
WIDGET_LAYOUT_EVENT_TYPE,
|
||||
type IWidgetLayouts,
|
||||
} from "./types";
|
||||
import { type IStoredLayout, type ILayoutStateEvent, WIDGET_LAYOUT_EVENT_TYPE, type IWidgetLayouts } from "./types";
|
||||
|
||||
export type { IStoredLayout, ILayoutStateEvent };
|
||||
export { Container, WIDGET_LAYOUT_EVENT_TYPE };
|
||||
export { type Container, WIDGET_LAYOUT_EVENT_TYPE };
|
||||
|
||||
export type ILayoutSettings = Partial<ILayoutStateEvent> & {
|
||||
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 manualContainer = userLayout?.widgets?.[widget.id]?.container;
|
||||
const isLegacyPinned = !!legacyPinned?.[widget.id];
|
||||
const defaultContainer = WidgetType.JITSI.matches(widget.type) ? Container.Top : Container.Right;
|
||||
if (manualContainer ? manualContainer === Container.Center : stateContainer === Container.Center) {
|
||||
const defaultContainer = WidgetType.JITSI.matches(widget.type) ? "top" : "right";
|
||||
if (manualContainer ? manualContainer === "center" : stateContainer === "center") {
|
||||
if (centerWidgets.length) {
|
||||
console.error("Tried to push a second widget into the center container");
|
||||
} else {
|
||||
@ -188,9 +183,9 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
|
||||
targetContainer = manualContainer ?? stateContainer!;
|
||||
} else if (isLegacyPinned && !stateContainer) {
|
||||
// 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
|
||||
@ -291,19 +286,19 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
|
||||
const newRoomContainers = new Map();
|
||||
this.byRoom.set(room.roomId, newRoomContainers);
|
||||
if (topWidgets.length) {
|
||||
newRoomContainers.set(Container.Top, {
|
||||
newRoomContainers.set("top", {
|
||||
ordered: topWidgets,
|
||||
distributions: widths,
|
||||
height: maxHeight,
|
||||
});
|
||||
}
|
||||
if (rightWidgets.length) {
|
||||
newRoomContainers.set(Container.Right, {
|
||||
newRoomContainers.set("right", {
|
||||
ordered: rightWidgets,
|
||||
});
|
||||
}
|
||||
if (centerWidgets.length) {
|
||||
newRoomContainers.set(Container.Center, {
|
||||
newRoomContainers.set("center", {
|
||||
ordered: centerWidgets,
|
||||
});
|
||||
}
|
||||
@ -325,11 +320,11 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
|
||||
|
||||
public canAddToContainer(room: Room, container: Container): boolean {
|
||||
switch (container) {
|
||||
case Container.Top:
|
||||
case "top":
|
||||
return this.getContainerWidgets(room, container).length < MAX_PINNED;
|
||||
case Container.Right:
|
||||
case "right":
|
||||
return this.getContainerWidgets(room, container).length < MAX_PINNED;
|
||||
case Container.Center:
|
||||
case "center":
|
||||
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 {
|
||||
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 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)
|
||||
const newLayout: Record<string, IStoredLayout> = {};
|
||||
switch (toContainer) {
|
||||
case Container.Right:
|
||||
case "right":
|
||||
// new "right" widget
|
||||
break;
|
||||
case Container.Center:
|
||||
case "center":
|
||||
// new "center" widget => all other widgets go into "right"
|
||||
for (const w of this.getContainerWidgets(room, Container.Top)) {
|
||||
newLayout[w.id] = { container: Container.Right };
|
||||
for (const w of this.getContainerWidgets(room, "top")) {
|
||||
newLayout[w.id] = { container: "right" };
|
||||
}
|
||||
for (const w of this.getContainerWidgets(room, Container.Center)) {
|
||||
newLayout[w.id] = { container: Container.Right };
|
||||
for (const w of this.getContainerWidgets(room, "center")) {
|
||||
newLayout[w.id] = { container: "right" };
|
||||
}
|
||||
break;
|
||||
case Container.Top:
|
||||
case "top":
|
||||
// new "top" widget => the center widget moves into "right"
|
||||
if (this.hasMaximisedWidget(room)) {
|
||||
const centerWidget = this.getContainerWidgets(room, Container.Center)[0];
|
||||
newLayout[centerWidget.id] = { container: Container.Right };
|
||||
const centerWidget = this.getContainerWidgets(room, "center")[0];
|
||||
newLayout[centerWidget.id] = { container: "right" };
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -447,11 +442,11 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
|
||||
}
|
||||
|
||||
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 {
|
||||
return this.getContainerWidgets(room, Container.Top).length > 0;
|
||||
return this.getContainerWidgets(room, "top").length > 0;
|
||||
}
|
||||
|
||||
public canCopyLayoutToRoom(room: Room): boolean {
|
||||
@ -464,7 +459,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
|
||||
const evContent: ILayoutStateEvent = { widgets: {} };
|
||||
for (const [widget, container] of allWidgets) {
|
||||
evContent.widgets[widget.id] = { container };
|
||||
if (container === Container.Top) {
|
||||
if (container === "top") {
|
||||
const containerWidgets = this.getContainerWidgets(room, container);
|
||||
const idx = containerWidgets.findIndex((w) => w.id === widget.id);
|
||||
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.
|
||||
*/
|
||||
|
||||
import { type Container } from "@element-hq/element-web-module-api";
|
||||
|
||||
export interface IStoredLayout {
|
||||
// Where to store the widget. Required.
|
||||
container: Container;
|
||||
@ -42,15 +44,3 @@ export interface ILayoutStateEvent {
|
||||
}
|
||||
|
||||
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 Modal from "../../Modal";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import { Container } from "../../stores/widgets/types";
|
||||
import { WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
|
||||
import { WidgetMessagingStore } from "../../stores/widgets/WidgetMessagingStore";
|
||||
import { isAppWidget } from "../../stores/WidgetStore";
|
||||
@ -109,7 +108,7 @@ export class WidgetContextMenuViewModel
|
||||
|
||||
let showMoveButtons: [boolean, boolean] = [false, false];
|
||||
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);
|
||||
showMoveButtons = [widgetIndex > 0, widgetIndex < pinnedWidgets.length - 1];
|
||||
}
|
||||
@ -225,7 +224,7 @@ export class WidgetContextMenuViewModel
|
||||
public get onMoveButton(): (direction: number) => void {
|
||||
return (direction: number) => {
|
||||
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!();
|
||||
};
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@ import { CallStore, CallStoreEvent } from "../../stores/CallStore";
|
||||
import { type Call } from "../../models/Call";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
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";
|
||||
|
||||
export interface Props {
|
||||
@ -91,7 +91,7 @@ export class WidgetPipViewModel
|
||||
metricsTrigger: "WebFloatingCallWindow",
|
||||
});
|
||||
} else if (this.viewingRoom) {
|
||||
WidgetLayoutStore.instance.moveToContainer(this.props.room, this.widget, Container.Center);
|
||||
WidgetLayoutStore.instance.moveToContainer(this.props.room, this.widget, "center");
|
||||
} else {
|
||||
defaultDispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
|
||||
@ -680,11 +680,13 @@ export function mkStubRoom(
|
||||
maySendStateEvent: jest.fn().mockReturnValue(true),
|
||||
maySendRedactionForEvent: jest.fn().mockReturnValue(true),
|
||||
maySendEvent: jest.fn().mockReturnValue(true),
|
||||
maySendMessage: jest.fn().mockReturnValue(true),
|
||||
members: {},
|
||||
getHistoryVisibility: jest.fn().mockReturnValue(HistoryVisibility.Shared),
|
||||
getJoinRule: jest.fn().mockReturnValue(JoinRule.Invite),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
removeListener: jest.fn(),
|
||||
} as unknown as RoomState,
|
||||
eventShouldLiveIn: jest.fn().mockReturnValue({ shouldLiveInRoom: true, shouldLiveInThread: false }),
|
||||
fetchRoomThreads: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
@ -713,6 +715,7 @@ export function mkStubRoom(
|
||||
isKicked: () => false,
|
||||
}),
|
||||
getMembers: jest.fn().mockReturnValue([]),
|
||||
getEncryptionTargetMembers: jest.fn().mockReturnValue([]),
|
||||
getMembersWithMembership: jest.fn().mockReturnValue([]),
|
||||
getMxcAvatarUrl: () => "mxc://avatar.url/room.png",
|
||||
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 { TestSdkContext } from "../../TestSdkContext";
|
||||
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 { WidgetType } from "../../../../src/widgets/WidgetType";
|
||||
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
|
||||
@ -234,7 +234,7 @@ describe("PipContainer", () => {
|
||||
// The return button should maximize the widget
|
||||
const moveSpy = jest.spyOn(WidgetLayoutStore.instance, "moveToContainer");
|
||||
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();
|
||||
});
|
||||
|
||||
@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
import React, { type JSX, type ComponentProps } from "react";
|
||||
import { screen, render } from "jest-matrix-react";
|
||||
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 {
|
||||
type ApprovalOpts,
|
||||
@ -24,6 +24,10 @@ import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext
|
||||
import WidgetUtils from "../../../../../src/utils/WidgetUtils";
|
||||
import { ModuleRunner } from "../../../../../src/modules/ModuleRunner";
|
||||
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 />", () => {
|
||||
const widgetId = "w1";
|
||||
@ -44,8 +48,12 @@ describe("<WidgetContextMenu />", () => {
|
||||
|
||||
let mockClient: MatrixClient;
|
||||
|
||||
let room: Room;
|
||||
|
||||
let onFinished: () => void;
|
||||
|
||||
let roomContext: RoomContextType;
|
||||
|
||||
beforeEach(() => {
|
||||
onFinished = jest.fn();
|
||||
jest.spyOn(WidgetUtils, "canUserModifyWidgets").mockReturnValue(true);
|
||||
@ -53,6 +61,13 @@ describe("<WidgetContextMenu />", () => {
|
||||
mockClient = {
|
||||
getUserId: jest.fn().mockReturnValue(userId),
|
||||
} as unknown as MatrixClient;
|
||||
|
||||
room = mkStubRoom(roomId, "Test Room", mockClient);
|
||||
|
||||
roomContext = {
|
||||
room,
|
||||
roomId,
|
||||
} as unknown as RoomContextType;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -62,7 +77,9 @@ describe("<WidgetContextMenu />", () => {
|
||||
function getComponent(props: Partial<ComponentProps<typeof WidgetContextMenu>> = {}): JSX.Element {
|
||||
return (
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<WidgetContextMenu app={app} onFinished={onFinished} {...props} />
|
||||
<ScopedRoomContextProvider {...roomContext}>
|
||||
<WidgetContextMenu app={app} onFinished={onFinished} {...props} />
|
||||
</ScopedRoomContextProvider>
|
||||
</MatrixClientContext.Provider>
|
||||
);
|
||||
}
|
||||
@ -89,4 +106,69 @@ describe("<WidgetContextMenu />", () => {
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
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 ActiveWidgetStore from "../../../../../src/stores/ActiveWidgetStore";
|
||||
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 { ElementWidgetCapabilities } from "../../../../../src/stores/widgets/ElementWidgetCapabilities";
|
||||
import { ElementWidget, type WidgetMessaging } from "../../../../../src/stores/widgets/WidgetMessaging";
|
||||
@ -303,7 +303,7 @@ describe("AppTile", () => {
|
||||
return {
|
||||
widgets: {
|
||||
1: {
|
||||
container: Container.Top,
|
||||
container: "top",
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -334,7 +334,7 @@ describe("AppTile", () => {
|
||||
mockSettings.mockRestore();
|
||||
act(() => {
|
||||
// Move widget to center
|
||||
WidgetLayoutStore.instance.moveToContainer(r1, app1, Container.Center);
|
||||
WidgetLayoutStore.instance.moveToContainer(r1, app1, "center");
|
||||
});
|
||||
|
||||
expect(renderResult.getByText("Example 1")).toBeInTheDocument();
|
||||
@ -377,7 +377,7 @@ describe("AppTile", () => {
|
||||
);
|
||||
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
|
||||
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 () => {
|
||||
@ -388,7 +388,7 @@ describe("AppTile", () => {
|
||||
);
|
||||
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
|
||||
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 () => {
|
||||
@ -455,7 +455,7 @@ describe("AppTile", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockImplementation(
|
||||
(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 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 { type Call } from "../../../../../../src/models/Call";
|
||||
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 { _t } from "../../../../../../src/languageHandler";
|
||||
import WidgetStore, { type IApp } from "../../../../../../src/stores/WidgetStore";
|
||||
@ -504,7 +504,7 @@ describe("RoomHeader", () => {
|
||||
const videoButton = screen.getByRole("button", { name: "Video call" });
|
||||
expect(videoButton).not.toHaveAttribute("aria-disabled", "true");
|
||||
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", () => {
|
||||
@ -869,7 +869,7 @@ describe("RoomHeader", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("renders additionalButtons", async () => {
|
||||
it("renders legacy additionalButtons", async () => {
|
||||
const additionalButtons: ViewRoomOpts["buttons"] = [
|
||||
{
|
||||
icon: () => <>test-icon</>,
|
||||
@ -878,11 +878,11 @@ describe("RoomHeader", () => {
|
||||
onClick: () => {},
|
||||
},
|
||||
];
|
||||
render(<RoomHeader room={room} additionalButtons={additionalButtons} />, getWrapper());
|
||||
render(<RoomHeader room={room} legacyAdditionalButtons={additionalButtons} />, getWrapper());
|
||||
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 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 event = createEvent.click(button);
|
||||
|
||||
77
apps/web/test/unit-tests/modules/ExtrasApi-test.tsx
Normal file
77
apps/web/test/unit-tests/modules/ExtrasApi-test.tsx
Normal file
@ -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 { Action } from "../../../src/dispatcher/actions";
|
||||
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";
|
||||
|
||||
describe("ProxiedApiModule", () => {
|
||||
@ -319,18 +319,18 @@ describe("ProxiedApiModule", () => {
|
||||
it("should return false if there is no room", () => {
|
||||
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();
|
||||
});
|
||||
|
||||
it("should return false if the app is not in the container", () => {
|
||||
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", () => {
|
||||
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", () => {
|
||||
client.getRoom = jest.fn().mockReturnValue(null);
|
||||
api.moveAppToContainer(app, Container.Top, roomId);
|
||||
api.moveAppToContainer(app, "top", roomId);
|
||||
expect(WidgetLayoutStore.instance.moveToContainer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@ -358,8 +358,8 @@ describe("ProxiedApiModule", () => {
|
||||
const room = mkRoom(client, roomId);
|
||||
client.getRoom = jest.fn().mockReturnValue(room);
|
||||
|
||||
api.moveAppToContainer(app, Container.Top, roomId);
|
||||
expect(WidgetLayoutStore.instance.moveToContainer).toHaveBeenCalledWith(room, app, Container.Top);
|
||||
api.moveAppToContainer(app, "top", roomId);
|
||||
expect(WidgetLayoutStore.instance.moveToContainer).toHaveBeenCalledWith(room, app, "top");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
121
apps/web/test/unit-tests/modules/WidgetApi-test.ts
Normal file
121
apps/web/test/unit-tests/modules/WidgetApi-test.ts
Normal file
@ -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 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 defaultDispatcher from "../../../src/dispatcher/dispatcher";
|
||||
import SettingsStore from "../../../src/settings/SettingsStore";
|
||||
@ -74,14 +74,14 @@ describe("WidgetLayoutStore", () => {
|
||||
|
||||
it("all widgets should be in the right container by default", () => {
|
||||
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 () => {
|
||||
store.recalculateRoom(mockRoom);
|
||||
store.moveToContainer(mockRoom, mockApps[0], Container.Top);
|
||||
expect(store.getContainerWidgets(mockRoom, Container.Top)).toStrictEqual([mockApps[0]]);
|
||||
expect(store.getContainerHeight(mockRoom, Container.Top)).toBeNull();
|
||||
store.moveToContainer(mockRoom, mockApps[0], "top");
|
||||
expect(store.getContainerWidgets(mockRoom, "top")).toStrictEqual([mockApps[0]]);
|
||||
expect(store.getContainerHeight(mockRoom, "top")).toBeNull();
|
||||
});
|
||||
|
||||
it("ordering of top container widgets should be consistent even if no index specified", async () => {
|
||||
@ -97,71 +97,71 @@ describe("WidgetLayoutStore", () => {
|
||||
};
|
||||
|
||||
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 () => {
|
||||
store.recalculateRoom(mockRoom);
|
||||
store.moveToContainer(mockRoom, mockApps[0], Container.Top);
|
||||
store.moveToContainer(mockRoom, mockApps[1], Container.Top);
|
||||
store.moveToContainer(mockRoom, mockApps[2], Container.Top);
|
||||
expect(new Set(store.getContainerWidgets(mockRoom, Container.Top))).toEqual(
|
||||
store.moveToContainer(mockRoom, mockApps[0], "top");
|
||||
store.moveToContainer(mockRoom, mockApps[1], "top");
|
||||
store.moveToContainer(mockRoom, mockApps[2], "top");
|
||||
expect(new Set(store.getContainerWidgets(mockRoom, "top"))).toEqual(
|
||||
new Set([mockApps[0], mockApps[1], mockApps[2]]),
|
||||
);
|
||||
});
|
||||
|
||||
it("cannot add more than three widgets to top container", async () => {
|
||||
store.recalculateRoom(mockRoom);
|
||||
store.moveToContainer(mockRoom, mockApps[0], Container.Top);
|
||||
store.moveToContainer(mockRoom, mockApps[1], Container.Top);
|
||||
store.moveToContainer(mockRoom, mockApps[2], Container.Top);
|
||||
expect(store.canAddToContainer(mockRoom, Container.Top)).toEqual(false);
|
||||
store.moveToContainer(mockRoom, mockApps[0], "top");
|
||||
store.moveToContainer(mockRoom, mockApps[1], "top");
|
||||
store.moveToContainer(mockRoom, mockApps[2], "top");
|
||||
expect(store.canAddToContainer(mockRoom, "top")).toEqual(false);
|
||||
});
|
||||
|
||||
it("remove pins when maximising (other widget)", async () => {
|
||||
store.recalculateRoom(mockRoom);
|
||||
store.moveToContainer(mockRoom, mockApps[0], Container.Top);
|
||||
store.moveToContainer(mockRoom, mockApps[1], Container.Top);
|
||||
store.moveToContainer(mockRoom, mockApps[2], Container.Top);
|
||||
store.moveToContainer(mockRoom, mockApps[3], Container.Center);
|
||||
expect(store.getContainerWidgets(mockRoom, Container.Top)).toEqual([]);
|
||||
expect(new Set(store.getContainerWidgets(mockRoom, Container.Right))).toEqual(
|
||||
store.moveToContainer(mockRoom, mockApps[0], "top");
|
||||
store.moveToContainer(mockRoom, mockApps[1], "top");
|
||||
store.moveToContainer(mockRoom, mockApps[2], "top");
|
||||
store.moveToContainer(mockRoom, mockApps[3], "center");
|
||||
expect(store.getContainerWidgets(mockRoom, "top")).toEqual([]);
|
||||
expect(new Set(store.getContainerWidgets(mockRoom, "right"))).toEqual(
|
||||
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 () => {
|
||||
store.recalculateRoom(mockRoom);
|
||||
store.moveToContainer(mockRoom, mockApps[0], Container.Top);
|
||||
store.moveToContainer(mockRoom, mockApps[1], Container.Top);
|
||||
store.moveToContainer(mockRoom, mockApps[2], Container.Top);
|
||||
store.moveToContainer(mockRoom, mockApps[0], Container.Center);
|
||||
expect(store.getContainerWidgets(mockRoom, Container.Top)).toEqual([]);
|
||||
expect(store.getContainerWidgets(mockRoom, Container.Center)).toEqual([mockApps[0]]);
|
||||
expect(new Set(store.getContainerWidgets(mockRoom, Container.Right))).toEqual(
|
||||
store.moveToContainer(mockRoom, mockApps[0], "top");
|
||||
store.moveToContainer(mockRoom, mockApps[1], "top");
|
||||
store.moveToContainer(mockRoom, mockApps[2], "top");
|
||||
store.moveToContainer(mockRoom, mockApps[0], "center");
|
||||
expect(store.getContainerWidgets(mockRoom, "top")).toEqual([]);
|
||||
expect(store.getContainerWidgets(mockRoom, "center")).toEqual([mockApps[0]]);
|
||||
expect(new Set(store.getContainerWidgets(mockRoom, "right"))).toEqual(
|
||||
new Set([mockApps[1], mockApps[2], mockApps[3]]),
|
||||
);
|
||||
});
|
||||
|
||||
it("remove maximised when pinning (other widget)", async () => {
|
||||
store.recalculateRoom(mockRoom);
|
||||
store.moveToContainer(mockRoom, mockApps[0], Container.Center);
|
||||
store.moveToContainer(mockRoom, mockApps[1], Container.Top);
|
||||
expect(store.getContainerWidgets(mockRoom, Container.Top)).toEqual([mockApps[1]]);
|
||||
expect(store.getContainerWidgets(mockRoom, Container.Center)).toEqual([]);
|
||||
expect(new Set(store.getContainerWidgets(mockRoom, Container.Right))).toEqual(
|
||||
store.moveToContainer(mockRoom, mockApps[0], "center");
|
||||
store.moveToContainer(mockRoom, mockApps[1], "top");
|
||||
expect(store.getContainerWidgets(mockRoom, "top")).toEqual([mockApps[1]]);
|
||||
expect(store.getContainerWidgets(mockRoom, "center")).toEqual([]);
|
||||
expect(new Set(store.getContainerWidgets(mockRoom, "right"))).toEqual(
|
||||
new Set([mockApps[2], mockApps[3], mockApps[0]]),
|
||||
);
|
||||
});
|
||||
|
||||
it("remove maximised when pinning (same widget)", async () => {
|
||||
store.recalculateRoom(mockRoom);
|
||||
store.moveToContainer(mockRoom, mockApps[0], Container.Center);
|
||||
store.moveToContainer(mockRoom, mockApps[0], Container.Top);
|
||||
expect(store.getContainerWidgets(mockRoom, Container.Top)).toEqual([mockApps[0]]);
|
||||
expect(store.getContainerWidgets(mockRoom, Container.Center)).toEqual([]);
|
||||
expect(new Set(store.getContainerWidgets(mockRoom, Container.Right))).toEqual(
|
||||
store.moveToContainer(mockRoom, mockApps[0], "center");
|
||||
store.moveToContainer(mockRoom, mockApps[0], "top");
|
||||
expect(store.getContainerWidgets(mockRoom, "top")).toEqual([mockApps[0]]);
|
||||
expect(store.getContainerWidgets(mockRoom, "center")).toEqual([]);
|
||||
expect(new Set(store.getContainerWidgets(mockRoom, "right"))).toEqual(
|
||||
new Set([mockApps[2], mockApps[3], mockApps[1]]),
|
||||
);
|
||||
});
|
||||
@ -171,9 +171,9 @@ describe("WidgetLayoutStore", () => {
|
||||
await store.start();
|
||||
|
||||
expect(roomUpdateListener).toHaveBeenCalled();
|
||||
expect(store.getContainerWidgets(mockRoom, Container.Top)).toEqual([]);
|
||||
expect(store.getContainerWidgets(mockRoom, Container.Center)).toEqual([]);
|
||||
expect(store.getContainerWidgets(mockRoom, Container.Right)).toEqual([
|
||||
expect(store.getContainerWidgets(mockRoom, "top")).toEqual([]);
|
||||
expect(store.getContainerWidgets(mockRoom, "center")).toEqual([]);
|
||||
expect(store.getContainerWidgets(mockRoom, "right")).toEqual([
|
||||
mockApps[0],
|
||||
mockApps[1],
|
||||
mockApps[2],
|
||||
@ -190,58 +190,50 @@ describe("WidgetLayoutStore", () => {
|
||||
));
|
||||
store.recalculateRoom(mockRoom);
|
||||
expect(roomUpdateListener).toHaveBeenCalled();
|
||||
expect(store.getContainerWidgets(mockRoom, Container.Top)).toEqual([]);
|
||||
expect(store.getContainerWidgets(mockRoom, Container.Center)).toEqual([]);
|
||||
expect(store.getContainerWidgets(mockRoom, Container.Right)).toEqual([]);
|
||||
expect(store.getContainerWidgets(mockRoom, "top")).toEqual([]);
|
||||
expect(store.getContainerWidgets(mockRoom, "center")).toEqual([]);
|
||||
expect(store.getContainerWidgets(mockRoom, "right")).toEqual([]);
|
||||
});
|
||||
|
||||
it("should clear the layout if the client is not viable", () => {
|
||||
store.recalculateRoom(mockRoom);
|
||||
defaultDispatcher.dispatch({ action: Action.ClientNotViable }, true);
|
||||
|
||||
expect(store.getContainerWidgets(mockRoom, Container.Top)).toEqual([]);
|
||||
expect(store.getContainerWidgets(mockRoom, Container.Center)).toEqual([]);
|
||||
expect(store.getContainerWidgets(mockRoom, Container.Right)).toEqual([]);
|
||||
expect(store.getContainerWidgets(mockRoom, "top")).toEqual([]);
|
||||
expect(store.getContainerWidgets(mockRoom, "center")).toEqual([]);
|
||||
expect(store.getContainerWidgets(mockRoom, "right")).toEqual([]);
|
||||
});
|
||||
|
||||
it("should return the expected resizer distributions", () => {
|
||||
// this only works for top widgets
|
||||
store.recalculateRoom(mockRoom);
|
||||
store.moveToContainer(mockRoom, mockApps[0], Container.Top);
|
||||
store.moveToContainer(mockRoom, mockApps[1], Container.Top);
|
||||
expect(store.getResizerDistributions(mockRoom, Container.Top)).toEqual(["50.0%"]);
|
||||
store.moveToContainer(mockRoom, mockApps[0], "top");
|
||||
store.moveToContainer(mockRoom, mockApps[1], "top");
|
||||
expect(store.getResizerDistributions(mockRoom, "top")).toEqual(["50.0%"]);
|
||||
});
|
||||
|
||||
it("should set and return container height", () => {
|
||||
store.recalculateRoom(mockRoom);
|
||||
store.moveToContainer(mockRoom, mockApps[0], Container.Top);
|
||||
store.moveToContainer(mockRoom, mockApps[1], Container.Top);
|
||||
store.setContainerHeight(mockRoom, Container.Top, 23);
|
||||
expect(store.getContainerHeight(mockRoom, Container.Top)).toBe(23);
|
||||
store.moveToContainer(mockRoom, mockApps[0], "top");
|
||||
store.moveToContainer(mockRoom, mockApps[1], "top");
|
||||
store.setContainerHeight(mockRoom, "top", 23);
|
||||
expect(store.getContainerHeight(mockRoom, "top")).toBe(23);
|
||||
});
|
||||
|
||||
it("should move a widget within a container", () => {
|
||||
store.recalculateRoom(mockRoom);
|
||||
store.moveToContainer(mockRoom, mockApps[0], Container.Top);
|
||||
store.moveToContainer(mockRoom, mockApps[1], Container.Top);
|
||||
store.moveToContainer(mockRoom, mockApps[2], Container.Top);
|
||||
expect(store.getContainerWidgets(mockRoom, Container.Top)).toStrictEqual([
|
||||
mockApps[0],
|
||||
mockApps[1],
|
||||
mockApps[2],
|
||||
]);
|
||||
store.moveWithinContainer(mockRoom, Container.Top, mockApps[0], 1);
|
||||
expect(store.getContainerWidgets(mockRoom, Container.Top)).toStrictEqual([
|
||||
mockApps[1],
|
||||
mockApps[0],
|
||||
mockApps[2],
|
||||
]);
|
||||
store.moveToContainer(mockRoom, mockApps[0], "top");
|
||||
store.moveToContainer(mockRoom, mockApps[1], "top");
|
||||
store.moveToContainer(mockRoom, mockApps[2], "top");
|
||||
expect(store.getContainerWidgets(mockRoom, "top")).toStrictEqual([mockApps[0], mockApps[1], mockApps[2]]);
|
||||
store.moveWithinContainer(mockRoom, "top", mockApps[0], 1);
|
||||
expect(store.getContainerWidgets(mockRoom, "top")).toStrictEqual([mockApps[1], mockApps[0], mockApps[2]]);
|
||||
});
|
||||
|
||||
it("should copy the layout to the room", async () => {
|
||||
await store.start();
|
||||
store.recalculateRoom(mockRoom);
|
||||
store.moveToContainer(mockRoom, mockApps[0], Container.Top);
|
||||
store.moveToContainer(mockRoom, mockApps[0], "top");
|
||||
store.copyLayoutToRoom(mockRoom);
|
||||
|
||||
expect(mocked(client.sendStateEvent).mock.calls).toMatchInlineSnapshot(`
|
||||
|
||||
@ -16,7 +16,7 @@ import {
|
||||
import { stubClient } from "../../test-utils";
|
||||
import WidgetUtils from "../../../src/utils/WidgetUtils";
|
||||
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 Modal from "../../../src/Modal";
|
||||
import SettingsStore from "../../../src/settings/SettingsStore";
|
||||
@ -138,12 +138,7 @@ describe("WidgetContextMenuViewModel", () => {
|
||||
const vm = new WidgetContextMenuViewModel(props);
|
||||
vm.onMoveButton(1);
|
||||
|
||||
expect(WidgetLayoutStore.instance.moveWithinContainer).toHaveBeenCalledWith(
|
||||
props.room,
|
||||
Container.Top,
|
||||
props.app,
|
||||
1,
|
||||
);
|
||||
expect(WidgetLayoutStore.instance.moveWithinContainer).toHaveBeenCalledWith(props.room, "top", props.app, 1);
|
||||
expect(props.onFinished).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ import { WidgetPipViewModel } from "../../../src/viewmodels/room/WidgetPipViewMo
|
||||
import WidgetStore, { type IApp } from "../../../src/stores/WidgetStore";
|
||||
import defaultDispatcher from "../../../src/dispatcher/dispatcher";
|
||||
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 { type Call } from "../../../src/models/Call";
|
||||
|
||||
@ -96,7 +96,7 @@ describe("WidgetPipViewModel", () => {
|
||||
|
||||
vm.setViewingRoom(true);
|
||||
vm.onBackClick(createBackClickEvent());
|
||||
expect(moveSpy).toHaveBeenCalledWith(room, widget, Container.Center);
|
||||
expect(moveSpy).toHaveBeenCalledWith(room, widget, "center");
|
||||
|
||||
moveSpy.mockClear();
|
||||
vm.setViewingRoom(false);
|
||||
|
||||
36
pnpm-lock.yaml
generated
36
pnpm-lock.yaml
generated
@ -7,8 +7,8 @@ settings:
|
||||
catalogs:
|
||||
default:
|
||||
'@element-hq/element-web-module-api':
|
||||
specifier: 1.11.0
|
||||
version: 1.11.0
|
||||
specifier: 1.12.0
|
||||
version: 1.12.0
|
||||
'@element-hq/element-web-playwright-common':
|
||||
specifier: 2.2.7
|
||||
version: 2.2.7
|
||||
@ -149,7 +149,7 @@ importers:
|
||||
version: 7.28.6
|
||||
'@element-hq/element-web-module-api':
|
||||
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':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/shared-components
|
||||
@ -278,7 +278,7 @@ importers:
|
||||
version: 1.0.3
|
||||
matrix-js-sdk:
|
||||
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:
|
||||
specifier: ^1.17.0
|
||||
version: 1.17.0
|
||||
@ -411,7 +411,7 @@ importers:
|
||||
version: 0.16.3
|
||||
'@element-hq/element-web-playwright-common':
|
||||
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':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/playwright-common
|
||||
@ -750,7 +750,7 @@ importers:
|
||||
dependencies:
|
||||
'@element-hq/element-web-module-api':
|
||||
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':
|
||||
specifier: ^1.7.0
|
||||
version: 1.16.0
|
||||
@ -2016,8 +2016,8 @@ packages:
|
||||
'@element-hq/element-call-embedded@0.16.3':
|
||||
resolution: {integrity: sha512-OViKJonDaDNVBUW9WdV9mk78/Ruh34C7XsEgt3O8D9z+64C39elbIgllHSoH5S12IRlv9RYrrV37FZLo6QWsDQ==}
|
||||
|
||||
'@element-hq/element-web-module-api@1.11.0':
|
||||
resolution: {integrity: sha512-RBkvt+Z32CGLkiPtYcQTryQBjG01ZrSVV98CS1cPz/kTeBtxEbVBpqDoOLdGvpmVe0dWo4DLaFcldw2iK39TPA==}
|
||||
'@element-hq/element-web-module-api@1.12.0':
|
||||
resolution: {integrity: sha512-fLhHFiL1UbRjolpgera3osHHxhSzfnDGTRhaDEv1UsrHRHwMu3hb/IcyXNqGhLXkJiuX8XoOH0aetaAUqQ0YQA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
peerDependencies:
|
||||
'@matrix-org/react-sdk-module-api': '*'
|
||||
@ -2669,8 +2669,8 @@ packages:
|
||||
'@matrix-org/emojibase-bindings@1.5.0':
|
||||
resolution: {integrity: sha512-+W9/ow2Z3iQa7ZOF698PBhwNcgGkn36B5Sr8VDPx8N8CH7+Uw+7TrtbtKPZVdgf4m/THmgmfX40jS5YDBsLaYg==}
|
||||
|
||||
'@matrix-org/matrix-sdk-crypto-wasm@17.1.0':
|
||||
resolution: {integrity: sha512-yKPqBvKlHSqkt/UJh+Z+zLKQP8bd19OxokXYXh3VkKbW0+C44nPHsidSwd3SH+RxT+Ck2PDRwVcVXEnUft+/2g==}
|
||||
'@matrix-org/matrix-sdk-crypto-wasm@18.0.0':
|
||||
resolution: {integrity: sha512-88+n+dvxLI1cjS10UIlKXVYK7TGWbpAnnaDC9fow7ch/hCvdu3dFhJ3tS3/13N9s9+1QFXB4FFuommj+tHJPhQ==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
'@matrix-org/react-sdk-module-api@2.5.0':
|
||||
@ -7819,8 +7819,8 @@ packages:
|
||||
matrix-events-sdk@0.0.1:
|
||||
resolution: {integrity: sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==}
|
||||
|
||||
matrix-js-sdk@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/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/d99363d288b8471e38892ec11fd9b37c062b85fe}
|
||||
version: 41.1.0
|
||||
engines: {node: '>=22.0.0'}
|
||||
|
||||
@ -11980,7 +11980,7 @@ snapshots:
|
||||
|
||||
'@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:
|
||||
'@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-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:
|
||||
'@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
|
||||
'@testcontainers/postgresql': 11.11.0
|
||||
glob: 13.0.6
|
||||
@ -12712,7 +12712,7 @@ snapshots:
|
||||
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)':
|
||||
dependencies:
|
||||
@ -18712,10 +18712,10 @@ snapshots:
|
||||
|
||||
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:
|
||||
'@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
|
||||
bs58: 6.0.0
|
||||
content-type: 1.0.5
|
||||
|
||||
@ -17,7 +17,7 @@ catalog:
|
||||
"@playwright/test": 1.58.2
|
||||
"playwright-core": 1.58.2
|
||||
# Module API
|
||||
"@element-hq/element-web-module-api": 1.11.0
|
||||
"@element-hq/element-web-module-api": 1.12.0
|
||||
# Compound
|
||||
"@vector-im/compound-design-tokens": 6.10.1
|
||||
"@vector-im/compound-web": 8.4.0
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user