MVVM WidgetContextMenu component to shared component (#31190)
* Create WidgetContextMenu component in shared-components * Modify WidgetMenuContext call (apptile, extensioncard, widgetcard), test and stories * Correctly use new widgetcontextmenu component * WidgetContextMenuViewModel unit test * Lint and add comments * Finalize widgetcontextmenuviewmodel test * fix lint errors * Fix test error * Update playwright screenshots * add userWidget in widgetcontexstmenu props * Fix some a11y issues on playwright * fix linter error widget card * Use new i18n way for share component widget context menu * Add i18n context provider for widget context menu * chore: lint and update snapshot widgetcontextmenu
This commit is contained in:
@@ -84,26 +84,6 @@ const showMoveButtons = (app: IWidget, room: Room | undefined, showUnpin: boolea
|
||||
return [widgetIndex > 0, widgetIndex < pinnedWidgets.length - 1];
|
||||
};
|
||||
|
||||
export const showContextMenu = (
|
||||
cli: MatrixClient,
|
||||
room: Room | undefined,
|
||||
app: IWidget,
|
||||
userWidget: boolean,
|
||||
showUnpin: boolean,
|
||||
onDeleteClick: (() => void) | undefined,
|
||||
): boolean => {
|
||||
const canModify = userWidget || WidgetUtils.canUserModifyWidgets(cli, room?.roomId);
|
||||
const widgetMessaging = WidgetMessagingStore.instance.getMessagingForUid(WidgetUtils.getWidgetUid(app));
|
||||
return (
|
||||
showStreamAudioStreamButton(app) ||
|
||||
showEditButton(app, canModify) ||
|
||||
showRevokeButton(cli, room?.roomId, app, userWidget) ||
|
||||
showDeleteButton(canModify, onDeleteClick) ||
|
||||
showSnapshotButton(widgetMessaging) ||
|
||||
showMoveButtons(app, room, showUnpin).some(Boolean)
|
||||
);
|
||||
};
|
||||
|
||||
export const WidgetContextMenu: React.FC<IProps> = ({
|
||||
onFinished,
|
||||
app,
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
CollapseIcon,
|
||||
PopOutIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import { I18nContext } from "@element-hq/web-shared-components";
|
||||
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import { _t } from "../../../languageHandler";
|
||||
@@ -39,11 +40,10 @@ import Spinner from "./Spinner";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import ActiveWidgetStore from "../../../stores/ActiveWidgetStore";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { aboveLeftOf, ContextMenuButton } from "../../structures/ContextMenu";
|
||||
import { ContextMenuButton } from "../../structures/ContextMenu";
|
||||
import PersistedElement, { getPersistKey } from "./PersistedElement";
|
||||
import { WidgetType } from "../../../widgets/WidgetType";
|
||||
import { ElementWidget, WidgetMessaging, WidgetMessagingEvent } from "../../../stores/widgets/WidgetMessaging";
|
||||
import { showContextMenu, WidgetContextMenu } from "../context_menus/WidgetContextMenu";
|
||||
import WidgetAvatar from "../avatars/WidgetAvatar";
|
||||
import LegacyCallHandler from "../../../LegacyCallHandler";
|
||||
import { type IApp, isAppWidget } from "../../../stores/WidgetStore";
|
||||
@@ -61,6 +61,7 @@ import { ModuleRunner } from "../../../modules/ModuleRunner";
|
||||
import { parseUrl } from "../../../utils/UrlUtils";
|
||||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore.ts";
|
||||
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases.ts";
|
||||
import { WidgetContextMenu } from "../../../viewmodels/right-panel/WidgetContextMenuViewModel.tsx";
|
||||
|
||||
// Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin
|
||||
// because that would allow the iframe to programmatically remove the sandbox attribute, but
|
||||
@@ -132,7 +133,6 @@ interface IState {
|
||||
error: Error | null;
|
||||
menuDisplayed: boolean;
|
||||
requiresClient: boolean;
|
||||
hasContextMenuOptions: boolean;
|
||||
}
|
||||
|
||||
export default class AppTile extends React.Component<IProps, IState> {
|
||||
@@ -276,14 +276,6 @@ export default class AppTile extends React.Component<IProps, IState> {
|
||||
error: null,
|
||||
menuDisplayed: false,
|
||||
requiresClient: this.determineInitialRequiresClientState(),
|
||||
hasContextMenuOptions: showContextMenu(
|
||||
this.context,
|
||||
this.props.room,
|
||||
newProps.app,
|
||||
newProps.userWidget,
|
||||
!newProps.userWidget,
|
||||
newProps.onDeleteClick,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -768,21 +760,6 @@ export default class AppTile extends React.Component<IProps, IState> {
|
||||
}
|
||||
appTileClasses = classNames(appTileClasses);
|
||||
|
||||
let contextMenu;
|
||||
if (this.state.menuDisplayed) {
|
||||
contextMenu = (
|
||||
<WidgetContextMenu
|
||||
{...aboveLeftOf(this.contextMenuButton.current.getBoundingClientRect())}
|
||||
app={this.props.app}
|
||||
onFinished={this.closeContextMenu}
|
||||
showUnpin={!this.props.userWidget}
|
||||
userWidget={this.props.userWidget}
|
||||
onEditClick={this.props.onEditClick}
|
||||
onDeleteClick={this.props.onDeleteClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const layoutButtons: ReactNode[] = [];
|
||||
if (this.props.showLayoutButtons) {
|
||||
const isMaximised =
|
||||
@@ -838,24 +815,33 @@ export default class AppTile extends React.Component<IProps, IState> {
|
||||
<PopOutIcon className="mx_Icon mx_Icon_12" />
|
||||
</AccessibleButton>
|
||||
)}
|
||||
{this.state.hasContextMenuOptions && (
|
||||
<ContextMenuButton
|
||||
className="mx_AppTileMenuBar_widgets_button"
|
||||
label={_t("common|options")}
|
||||
isExpanded={this.state.menuDisplayed}
|
||||
ref={this.contextMenuButton}
|
||||
onClick={this.onContextMenuClick}
|
||||
>
|
||||
<OverflowHorizontalIcon className="mx_Icon mx_Icon_12" />
|
||||
</ContextMenuButton>
|
||||
)}
|
||||
<I18nContext.Provider value={window.mxModuleApi.i18n}>
|
||||
<WidgetContextMenu
|
||||
trigger={
|
||||
<ContextMenuButton
|
||||
className="mx_AppTileMenuBar_widgets_button"
|
||||
label={_t("common|options")}
|
||||
isExpanded={this.state.menuDisplayed}
|
||||
ref={this.contextMenuButton}
|
||||
onClick={this.onContextMenuClick}
|
||||
>
|
||||
<OverflowHorizontalIcon className="mx_Icon mx_Icon_12" />
|
||||
</ContextMenuButton>
|
||||
}
|
||||
app={this.props.app}
|
||||
onFinished={this.closeContextMenu}
|
||||
showUnpin={!this.props.userWidget}
|
||||
userWidget={this.props.userWidget}
|
||||
onEditClick={this.props.onEditClick}
|
||||
onDeleteClick={this.props.onDeleteClick}
|
||||
menuDisplayed={this.state.menuDisplayed}
|
||||
/>
|
||||
</I18nContext.Provider>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{appTileBody}
|
||||
</div>
|
||||
|
||||
{contextMenu}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,9 +20,7 @@ import {
|
||||
import BaseCard from "./BaseCard";
|
||||
import WidgetUtils, { useWidgets } from "../../../utils/WidgetUtils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { ChevronFace, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
|
||||
import { WidgetContextMenu } from "../context_menus/WidgetContextMenu";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
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";
|
||||
@@ -33,6 +31,7 @@ import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
|
||||
import EmptyState from "./EmptyState";
|
||||
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents.ts";
|
||||
import { UIComponent } from "../../../settings/UIFeature.ts";
|
||||
import { WidgetContextMenu } from "../../../viewmodels/right-panel/WidgetContextMenuViewModel.tsx";
|
||||
|
||||
interface Props {
|
||||
room: Room;
|
||||
@@ -69,21 +68,6 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
|
||||
};
|
||||
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
|
||||
let contextMenu;
|
||||
if (menuDisplayed) {
|
||||
const rect = handle.current?.getBoundingClientRect();
|
||||
const rightMargin = rect?.right ?? 0;
|
||||
const topMargin = rect?.top ?? 0;
|
||||
contextMenu = (
|
||||
<WidgetContextMenu
|
||||
chevronFace={ChevronFace.None}
|
||||
right={UIStore.instance.windowWidth - rightMargin}
|
||||
bottom={UIStore.instance.windowHeight - topMargin}
|
||||
onFinished={closeMenu}
|
||||
app={app}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const cannotPin = !isPinned && !WidgetLayoutStore.instance.canAddToContainer(room, Container.Top);
|
||||
|
||||
@@ -108,7 +92,7 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={classes} ref={handle}>
|
||||
<div className={classes}>
|
||||
<AccessibleButton
|
||||
className="mx_ExtensionsCard_icon_app"
|
||||
onClick={onOpenWidgetClick}
|
||||
@@ -123,14 +107,21 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
|
||||
</AccessibleButton>
|
||||
|
||||
{canModifyWidget && (
|
||||
<ContextMenuTooltipButton
|
||||
className="mx_ExtensionsCard_app_options"
|
||||
isExpanded={menuDisplayed}
|
||||
onClick={openMenu}
|
||||
title={_t("common|options")}
|
||||
>
|
||||
<OverflowHorizontalIcon />
|
||||
</ContextMenuTooltipButton>
|
||||
<WidgetContextMenu
|
||||
app={app}
|
||||
onFinished={closeMenu}
|
||||
menuDisplayed={menuDisplayed}
|
||||
trigger={
|
||||
<AccessibleButton
|
||||
ref={handle}
|
||||
className="mx_ExtensionsCard_app_options"
|
||||
onClick={openMenu}
|
||||
title={_t("common|options")}
|
||||
>
|
||||
<OverflowHorizontalIcon />
|
||||
</AccessibleButton>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<AccessibleButton
|
||||
@@ -141,8 +132,6 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
|
||||
>
|
||||
<PinSolidIcon />
|
||||
</AccessibleButton>
|
||||
|
||||
{contextMenu}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,19 +8,17 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import React, { type JSX, useContext, useEffect } from "react";
|
||||
import { type Room } from "matrix-js-sdk/src/matrix";
|
||||
import { OverflowHorizontalIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import BaseCard from "./BaseCard";
|
||||
import WidgetUtils, { useWidgets } from "../../../utils/WidgetUtils";
|
||||
import AppTile from "../elements/AppTile";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { ChevronFace, ContextMenuButton, useContextMenu } from "../../structures/ContextMenu";
|
||||
import { WidgetContextMenu } from "../context_menus/WidgetContextMenu";
|
||||
import { ContextMenuButton, useContextMenu } from "../../structures/ContextMenu";
|
||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||
import Heading from "../typography/Heading";
|
||||
import { WidgetContextMenu } from "../../../viewmodels/right-panel/WidgetContextMenuViewModel";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
@@ -47,36 +45,28 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
|
||||
// Don't render anything as we are about to transition
|
||||
if (!app || !isRight) return null;
|
||||
|
||||
let contextMenu: JSX.Element | undefined;
|
||||
if (menuDisplayed) {
|
||||
const rect = handle.current?.getBoundingClientRect();
|
||||
const rightMargin = rect ? rect.right : 0;
|
||||
const bottomMargin = rect ? rect.bottom : 0;
|
||||
contextMenu = (
|
||||
<WidgetContextMenu
|
||||
chevronFace={ChevronFace.None}
|
||||
right={UIStore.instance.windowWidth - rightMargin - 12}
|
||||
top={bottomMargin + 12}
|
||||
onFinished={closeMenu}
|
||||
app={app}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const contextMenu: JSX.Element = (
|
||||
<WidgetContextMenu
|
||||
trigger={
|
||||
<ContextMenuButton
|
||||
className="mx_BaseCard_header_title_button--option"
|
||||
ref={handle}
|
||||
onClick={openMenu}
|
||||
isExpanded={menuDisplayed}
|
||||
label={_t("common|options")}
|
||||
/>
|
||||
}
|
||||
onFinished={closeMenu}
|
||||
app={app}
|
||||
menuDisplayed={menuDisplayed}
|
||||
/>
|
||||
);
|
||||
|
||||
const header = (
|
||||
<div className="mx_BaseCard_header_title">
|
||||
<Heading size="4" className="mx_BaseCard_header_title_heading" as="h1">
|
||||
{WidgetUtils.getWidgetName(app)}
|
||||
</Heading>
|
||||
<ContextMenuButton
|
||||
className="mx_BaseCard_header_title_button--option"
|
||||
ref={handle}
|
||||
onClick={openMenu}
|
||||
isExpanded={menuDisplayed}
|
||||
label={_t("common|options")}
|
||||
>
|
||||
<OverflowHorizontalIcon />
|
||||
</ContextMenuButton>
|
||||
{contextMenu}
|
||||
</div>
|
||||
);
|
||||
|
||||
300
src/viewmodels/right-panel/WidgetContextMenuViewModel.tsx
Normal file
300
src/viewmodels/right-panel/WidgetContextMenuViewModel.tsx
Normal file
@@ -0,0 +1,300 @@
|
||||
/*
|
||||
* Copyright 2025 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, { useContext, useMemo, useEffect, type ReactElement, type ReactNode } from "react";
|
||||
import { logger } from "@sentry/browser";
|
||||
import { type Room, type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { type IWidget, MatrixCapabilities } from "matrix-widget-api";
|
||||
import {
|
||||
BaseViewModel,
|
||||
type WidgetContextMenuSnapshot,
|
||||
WidgetContextMenuView,
|
||||
type WidgetContextMenuViewModel as WidgetContextMenuViewModelInterface,
|
||||
} from "@element-hq/web-shared-components";
|
||||
import { type ApprovalOpts, WidgetLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle";
|
||||
|
||||
import ErrorDialog from "../../components/views/dialogs/ErrorDialog";
|
||||
import QuestionDialog from "../../components/views/dialogs/QuestionDialog";
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import { useScopedRoomContext } from "../../contexts/ScopedRoomContext";
|
||||
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";
|
||||
import WidgetUtils from "../../utils/WidgetUtils";
|
||||
import { WidgetType } from "../../widgets/WidgetType";
|
||||
import { ModuleRunner } from "../../modules/ModuleRunner";
|
||||
import { ElementWidget, type WidgetMessaging } from "../../stores/widgets/WidgetMessaging";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
|
||||
const checkRevokeButtonState = (
|
||||
cli: MatrixClient,
|
||||
roomId: string | undefined,
|
||||
app: IWidget,
|
||||
userWidget: boolean | undefined,
|
||||
): boolean => {
|
||||
const opts: ApprovalOpts = { approved: undefined };
|
||||
ModuleRunner.instance.invoke(WidgetLifecycle.PreLoadRequest, opts, new ElementWidget(app));
|
||||
if (!opts.approved) {
|
||||
const isAllowedWidget =
|
||||
(isAppWidget(app) &&
|
||||
app.eventId !== undefined &&
|
||||
(SettingsStore.getValue("allowedWidgets", roomId)[app.eventId] ?? false)) ||
|
||||
app.creatorUserId === cli?.getUserId();
|
||||
|
||||
const isLocalWidget = WidgetType.JITSI.matches(app.type);
|
||||
return !userWidget && !isLocalWidget && isAllowedWidget;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export class WidgetContextMenuViewModel
|
||||
extends BaseViewModel<WidgetContextMenuSnapshot, WidgetContextMenuViewModelProps>
|
||||
implements WidgetContextMenuViewModelInterface
|
||||
{
|
||||
private _app: IWidget;
|
||||
private _roomId: string | undefined;
|
||||
private _room: Room | undefined;
|
||||
private _cli: MatrixClient;
|
||||
private _widgetMessaging: WidgetMessaging | undefined;
|
||||
|
||||
public constructor(props: WidgetContextMenuViewModelProps) {
|
||||
const { app, cli, room, roomId, userWidget, showUnpin, menuDisplayed, trigger, onDeleteClick } = props;
|
||||
super(
|
||||
props,
|
||||
WidgetContextMenuViewModel.computeSnapshot(
|
||||
app,
|
||||
cli,
|
||||
room,
|
||||
userWidget,
|
||||
showUnpin,
|
||||
menuDisplayed,
|
||||
trigger,
|
||||
onDeleteClick,
|
||||
),
|
||||
);
|
||||
this._app = app;
|
||||
this._roomId = roomId;
|
||||
this._room = room;
|
||||
this._cli = cli;
|
||||
this._widgetMessaging = WidgetMessagingStore.instance.getMessagingForUid(WidgetUtils.getWidgetUid(props.app));
|
||||
}
|
||||
|
||||
private static readonly computeSnapshot = (
|
||||
app: IWidget,
|
||||
cli: MatrixClient,
|
||||
room: Room | undefined,
|
||||
userWidget: boolean | undefined,
|
||||
showUnpin: boolean | undefined,
|
||||
menuDisplayed: boolean,
|
||||
trigger: ReactNode,
|
||||
onDeleteClick?: () => void,
|
||||
): WidgetContextMenuSnapshot => {
|
||||
const showStreamAudioStreamButton = !!getConfigLivestreamUrl() && WidgetType.JITSI.matches(app.type);
|
||||
const canModify = userWidget || WidgetUtils.canUserModifyWidgets(cli, room?.roomId);
|
||||
const widgetMessaging = WidgetMessagingStore.instance.getMessagingForUid(WidgetUtils.getWidgetUid(app));
|
||||
const showDeleteButton = !!onDeleteClick || canModify;
|
||||
|
||||
const showSnapshotButton =
|
||||
SettingsStore.getValue("enableWidgetScreenshots") &&
|
||||
!!widgetMessaging?.widgetApi?.hasCapability(MatrixCapabilities.Screenshots);
|
||||
|
||||
let showMoveButtons: [boolean, boolean] = [false, false];
|
||||
if (showUnpin) {
|
||||
const pinnedWidgets = room ? WidgetLayoutStore.instance.getContainerWidgets(room, Container.Top) : [];
|
||||
const widgetIndex = pinnedWidgets.findIndex((widget) => widget.id === app.id);
|
||||
showMoveButtons = [widgetIndex > 0, widgetIndex < pinnedWidgets.length - 1];
|
||||
}
|
||||
|
||||
const showEditButton = canModify && WidgetUtils.isManagedByManager(app);
|
||||
|
||||
const showRevokeButton = checkRevokeButtonState(cli, room?.roomId, app, userWidget);
|
||||
|
||||
return {
|
||||
showStreamAudioStreamButton,
|
||||
showEditButton,
|
||||
showRevokeButton,
|
||||
showDeleteButton,
|
||||
showSnapshotButton,
|
||||
showMoveButtons,
|
||||
canModify,
|
||||
userWidget: !!userWidget,
|
||||
isMenuOpened: menuDisplayed,
|
||||
trigger,
|
||||
};
|
||||
};
|
||||
|
||||
public get onFinished(): () => void {
|
||||
return () => this.props.onFinished!();
|
||||
}
|
||||
|
||||
public get onRevokeClick(): () => void {
|
||||
return () => {
|
||||
const eventId = isAppWidget(this._app) ? this._app.eventId : undefined;
|
||||
logger.info("Revoking permission for widget to load: " + eventId);
|
||||
const current = SettingsStore.getValue("allowedWidgets", this._roomId);
|
||||
if (eventId !== undefined) current[eventId] = false;
|
||||
const level = SettingsStore.firstSupportedLevel("allowedWidgets");
|
||||
if (!level) throw new Error("level must be defined");
|
||||
SettingsStore.setValue("allowedWidgets", this._roomId ?? null, level, current).catch((err) => {
|
||||
logger.error(err);
|
||||
// We don't really need to do anything about this - the user will just hit the button again.
|
||||
});
|
||||
this.props.onFinished!();
|
||||
};
|
||||
}
|
||||
|
||||
public get onDeleteClick(): () => void {
|
||||
return () => {
|
||||
if (this.props.onDeleteClick) {
|
||||
this.props.onDeleteClick();
|
||||
} else if (this._roomId) {
|
||||
// Show delete confirmation dialog
|
||||
const { finished } = Modal.createDialog(QuestionDialog, {
|
||||
title: _t("widget|context_menu|delete"),
|
||||
description: _t("widget|context_menu|delete_warning"),
|
||||
button: _t("widget|context_menu|delete"),
|
||||
});
|
||||
|
||||
finished.then(([confirmed]) => {
|
||||
if (!confirmed) return;
|
||||
WidgetUtils.setRoomWidget(this._cli, this._roomId!, this._app.id);
|
||||
});
|
||||
}
|
||||
|
||||
this.props.onFinished!();
|
||||
};
|
||||
}
|
||||
|
||||
public get onSnapshotClick(): () => void {
|
||||
return () => {
|
||||
this._widgetMessaging?.widgetApi
|
||||
?.takeScreenshot()
|
||||
.then((data) => {
|
||||
dis.dispatch({
|
||||
action: "picture_snapshot",
|
||||
file: data.screenshot,
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error("Failed to take screenshot: ", err);
|
||||
});
|
||||
this.props.onFinished!();
|
||||
};
|
||||
}
|
||||
|
||||
public get onStreamAudioClick(): () => Promise<void> {
|
||||
return async () => {
|
||||
try {
|
||||
if (this._roomId) {
|
||||
await startJitsiAudioLivestream(this._cli, this._widgetMessaging!.widgetApi!, this._roomId!);
|
||||
}
|
||||
} catch (err: any) {
|
||||
logger.error("Failed to start livestream", err);
|
||||
// XXX: won't i18n well, but looks like widget api only support 'message'?
|
||||
const message =
|
||||
err instanceof Error ? err.message : _t("widget|error_unable_start_audio_stream_description");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: _t("widget|error_unable_start_audio_stream_title"),
|
||||
description: message,
|
||||
});
|
||||
}
|
||||
this.props.onFinished!();
|
||||
};
|
||||
}
|
||||
|
||||
public get onEditClick(): () => void {
|
||||
return () => {
|
||||
if (this.props.onEditClick) {
|
||||
this.props.onEditClick();
|
||||
} else if (this._room) {
|
||||
WidgetUtils.editWidget(this._room, this._app);
|
||||
}
|
||||
this.props.onFinished!();
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
this.props.onFinished!();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface WidgetContextMenuProps {
|
||||
app: IWidget;
|
||||
userWidget?: boolean;
|
||||
showUnpin?: boolean;
|
||||
menuDisplayed: boolean;
|
||||
trigger: ReactNode;
|
||||
// override delete handler
|
||||
onDeleteClick?(): void;
|
||||
// override edit handler
|
||||
onEditClick?(): void;
|
||||
onFinished(): void;
|
||||
}
|
||||
|
||||
export type WidgetContextMenuViewModelProps = WidgetContextMenuProps & {
|
||||
cli: MatrixClient;
|
||||
room: Room | undefined;
|
||||
roomId: string | undefined;
|
||||
};
|
||||
|
||||
export function WidgetContextMenu(props: WidgetContextMenuProps): ReactElement {
|
||||
const { app, userWidget, showUnpin, menuDisplayed, trigger, onEditClick, onDeleteClick, onFinished } = props;
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const { room, roomId } = useScopedRoomContext("room", "roomId");
|
||||
|
||||
const vm = useMemo(
|
||||
() =>
|
||||
new WidgetContextMenuViewModel({
|
||||
menuDisplayed,
|
||||
room,
|
||||
roomId,
|
||||
cli,
|
||||
app,
|
||||
showUnpin,
|
||||
userWidget,
|
||||
trigger,
|
||||
onEditClick,
|
||||
onDeleteClick,
|
||||
onFinished,
|
||||
}),
|
||||
[app, room, roomId, userWidget, showUnpin, menuDisplayed, cli, trigger, onEditClick, onDeleteClick, onFinished],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
vm.dispose();
|
||||
};
|
||||
}, [vm]);
|
||||
|
||||
const {
|
||||
showStreamAudioStreamButton,
|
||||
showEditButton,
|
||||
showRevokeButton,
|
||||
showDeleteButton,
|
||||
showSnapshotButton,
|
||||
showMoveButtons,
|
||||
} = vm.getSnapshot();
|
||||
|
||||
const hasContextMenuOptions =
|
||||
showStreamAudioStreamButton ||
|
||||
showEditButton ||
|
||||
showRevokeButton ||
|
||||
showDeleteButton ||
|
||||
showSnapshotButton ||
|
||||
showMoveButtons.some(Boolean);
|
||||
|
||||
return hasContextMenuOptions ? <WidgetContextMenuView vm={vm} /> : <></>;
|
||||
}
|
||||
Reference in New Issue
Block a user