Hide widget menu button if it there are no options available (#11257)
* Hide widget menu button if it there are no options available * Update snapshots
This commit is contained in:
parent
5d4153fa64
commit
8b8ca425d7
3 changed files with 94 additions and 68 deletions
|
@ -15,9 +15,10 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { ComponentProps, useContext } from "react";
|
import React, { ComponentProps, useContext } from "react";
|
||||||
import { IWidget, MatrixCapabilities } from "matrix-widget-api";
|
import { ClientWidgetApi, IWidget, MatrixCapabilities } from "matrix-widget-api";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { ApprovalOpts, WidgetLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle";
|
import { ApprovalOpts, WidgetLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle";
|
||||||
|
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu";
|
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu";
|
||||||
import { ChevronFace } from "../../structures/ContextMenu";
|
import { ChevronFace } from "../../structures/ContextMenu";
|
||||||
|
@ -48,6 +49,69 @@ interface IProps extends Omit<ComponentProps<typeof IconizedContextMenu>, "child
|
||||||
onEditClick?(): void;
|
onEditClick?(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showStreamAudioStreamButton = (app: IWidget): boolean => {
|
||||||
|
return !!getConfigLivestreamUrl() && WidgetType.JITSI.matches(app.type);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showEditButton = (app: IWidget, canModify: boolean): boolean => {
|
||||||
|
return canModify && WidgetUtils.isManagedByManager(app);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showRevokeButton = (
|
||||||
|
cli: MatrixClient,
|
||||||
|
roomId: string | undefined,
|
||||||
|
app: IWidget,
|
||||||
|
userWidget: boolean | undefined,
|
||||||
|
): boolean => {
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
const showDeleteButton = (canModify: boolean, onDeleteClick: undefined | (() => void)): boolean => {
|
||||||
|
return !!onDeleteClick || canModify;
|
||||||
|
};
|
||||||
|
|
||||||
|
const showSnapshotButton = (widgetMessaging: ClientWidgetApi | undefined): boolean => {
|
||||||
|
return (
|
||||||
|
SettingsStore.getValue<boolean>("enableWidgetScreenshots") &&
|
||||||
|
!!widgetMessaging?.hasCapability(MatrixCapabilities.Screenshots)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 widgetIndex = pinnedWidgets.findIndex((widget) => widget.id === app.id);
|
||||||
|
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> = ({
|
export const WidgetContextMenu: React.FC<IProps> = ({
|
||||||
onFinished,
|
onFinished,
|
||||||
app,
|
app,
|
||||||
|
@ -64,7 +128,7 @@ export const WidgetContextMenu: React.FC<IProps> = ({
|
||||||
const canModify = userWidget || WidgetUtils.canUserModifyWidgets(cli, roomId);
|
const canModify = userWidget || WidgetUtils.canUserModifyWidgets(cli, roomId);
|
||||||
|
|
||||||
let streamAudioStreamButton: JSX.Element | undefined;
|
let streamAudioStreamButton: JSX.Element | undefined;
|
||||||
if (roomId && getConfigLivestreamUrl() && WidgetType.JITSI.matches(app.type)) {
|
if (roomId && showStreamAudioStreamButton(app)) {
|
||||||
const onStreamAudioClick = async (): Promise<void> => {
|
const onStreamAudioClick = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
await startJitsiAudioLivestream(cli, widgetMessaging!, roomId);
|
await startJitsiAudioLivestream(cli, widgetMessaging!, roomId);
|
||||||
|
@ -84,11 +148,8 @@ export const WidgetContextMenu: React.FC<IProps> = ({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pinnedWidgets = room ? WidgetLayoutStore.instance.getContainerWidgets(room, Container.Top) : [];
|
|
||||||
const widgetIndex = pinnedWidgets.findIndex((widget) => widget.id === app.id);
|
|
||||||
|
|
||||||
let editButton: JSX.Element | undefined;
|
let editButton: JSX.Element | undefined;
|
||||||
if (canModify && WidgetUtils.isManagedByManager(app)) {
|
if (showEditButton(app, canModify)) {
|
||||||
const _onEditClick = (): void => {
|
const _onEditClick = (): void => {
|
||||||
if (onEditClick) {
|
if (onEditClick) {
|
||||||
onEditClick();
|
onEditClick();
|
||||||
|
@ -102,8 +163,7 @@ export const WidgetContextMenu: React.FC<IProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
let snapshotButton: JSX.Element | undefined;
|
let snapshotButton: JSX.Element | undefined;
|
||||||
const screenshotsEnabled = SettingsStore.getValue("enableWidgetScreenshots");
|
if (showSnapshotButton(widgetMessaging)) {
|
||||||
if (screenshotsEnabled && widgetMessaging?.hasCapability(MatrixCapabilities.Screenshots)) {
|
|
||||||
const onSnapshotClick = (): void => {
|
const onSnapshotClick = (): void => {
|
||||||
widgetMessaging
|
widgetMessaging
|
||||||
?.takeScreenshot()
|
?.takeScreenshot()
|
||||||
|
@ -123,7 +183,7 @@ export const WidgetContextMenu: React.FC<IProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
let deleteButton: JSX.Element | undefined;
|
let deleteButton: JSX.Element | undefined;
|
||||||
if (onDeleteClick || canModify) {
|
if (showDeleteButton(canModify, onDeleteClick)) {
|
||||||
const _onDeleteClick = (): void => {
|
const _onDeleteClick = (): void => {
|
||||||
if (onDeleteClick) {
|
if (onDeleteClick) {
|
||||||
onDeleteClick();
|
onDeleteClick();
|
||||||
|
@ -154,15 +214,8 @@ export const WidgetContextMenu: React.FC<IProps> = ({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
|
||||||
let revokeButton: JSX.Element | undefined;
|
let revokeButton: JSX.Element | undefined;
|
||||||
if (!userWidget && !isLocalWidget && isAllowedWidget) {
|
if (showRevokeButton(cli, roomId, app, userWidget)) {
|
||||||
const opts: ApprovalOpts = { approved: undefined };
|
const opts: ApprovalOpts = { approved: undefined };
|
||||||
ModuleRunner.instance.invoke(WidgetLifecycle.PreLoadRequest, opts, new ElementWidget(app));
|
ModuleRunner.instance.invoke(WidgetLifecycle.PreLoadRequest, opts, new ElementWidget(app));
|
||||||
|
|
||||||
|
@ -185,8 +238,9 @@ export const WidgetContextMenu: React.FC<IProps> = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [showMoveLeftButton, showMoveRightButton] = showMoveButtons(app, room, showUnpin);
|
||||||
let moveLeftButton: JSX.Element | undefined;
|
let moveLeftButton: JSX.Element | undefined;
|
||||||
if (showUnpin && widgetIndex > 0) {
|
if (showMoveLeftButton) {
|
||||||
const onClick = (): void => {
|
const onClick = (): void => {
|
||||||
if (!room) throw new Error("room must be defined");
|
if (!room) throw new Error("room must be defined");
|
||||||
WidgetLayoutStore.instance.moveWithinContainer(room, Container.Top, app, -1);
|
WidgetLayoutStore.instance.moveWithinContainer(room, Container.Top, app, -1);
|
||||||
|
@ -197,7 +251,7 @@ export const WidgetContextMenu: React.FC<IProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
let moveRightButton: JSX.Element | undefined;
|
let moveRightButton: JSX.Element | undefined;
|
||||||
if (showUnpin && widgetIndex < pinnedWidgets.length - 1) {
|
if (showMoveRightButton) {
|
||||||
const onClick = (): void => {
|
const onClick = (): void => {
|
||||||
if (!room) throw new Error("room must be defined");
|
if (!room) throw new Error("room must be defined");
|
||||||
WidgetLayoutStore.instance.moveWithinContainer(room, Container.Top, app, 1);
|
WidgetLayoutStore.instance.moveWithinContainer(room, Container.Top, app, 1);
|
||||||
|
|
|
@ -36,7 +36,7 @@ import { aboveLeftOf, ContextMenuButton } from "../../structures/ContextMenu";
|
||||||
import PersistedElement, { getPersistKey } from "./PersistedElement";
|
import PersistedElement, { getPersistKey } from "./PersistedElement";
|
||||||
import { WidgetType } from "../../../widgets/WidgetType";
|
import { WidgetType } from "../../../widgets/WidgetType";
|
||||||
import { ElementWidget, StopGapWidget } from "../../../stores/widgets/StopGapWidget";
|
import { ElementWidget, StopGapWidget } from "../../../stores/widgets/StopGapWidget";
|
||||||
import { WidgetContextMenu } from "../context_menus/WidgetContextMenu";
|
import { showContextMenu, WidgetContextMenu } from "../context_menus/WidgetContextMenu";
|
||||||
import WidgetAvatar from "../avatars/WidgetAvatar";
|
import WidgetAvatar from "../avatars/WidgetAvatar";
|
||||||
import LegacyCallHandler from "../../../LegacyCallHandler";
|
import LegacyCallHandler from "../../../LegacyCallHandler";
|
||||||
import { IApp, isAppWidget } from "../../../stores/WidgetStore";
|
import { IApp, isAppWidget } from "../../../stores/WidgetStore";
|
||||||
|
@ -107,6 +107,7 @@ interface IState {
|
||||||
error: Error | null;
|
error: Error | null;
|
||||||
menuDisplayed: boolean;
|
menuDisplayed: boolean;
|
||||||
requiresClient: boolean;
|
requiresClient: boolean;
|
||||||
|
hasContextMenuOptions: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class AppTile extends React.Component<IProps, IState> {
|
export default class AppTile extends React.Component<IProps, IState> {
|
||||||
|
@ -249,6 +250,14 @@ export default class AppTile extends React.Component<IProps, IState> {
|
||||||
error: null,
|
error: null,
|
||||||
menuDisplayed: false,
|
menuDisplayed: false,
|
||||||
requiresClient: this.determineInitialRequiresClientState(),
|
requiresClient: this.determineInitialRequiresClientState(),
|
||||||
|
hasContextMenuOptions: showContextMenu(
|
||||||
|
this.context,
|
||||||
|
this.props.room,
|
||||||
|
newProps.app,
|
||||||
|
newProps.userWidget,
|
||||||
|
!newProps.userWidget,
|
||||||
|
newProps.onDeleteClick,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -770,15 +779,17 @@ export default class AppTile extends React.Component<IProps, IState> {
|
||||||
<PopoutIcon className="mx_Icon mx_Icon_12 mx_Icon--stroke" />
|
<PopoutIcon className="mx_Icon mx_Icon_12 mx_Icon--stroke" />
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
)}
|
)}
|
||||||
<ContextMenuButton
|
{this.state.hasContextMenuOptions && (
|
||||||
className="mx_AppTileMenuBar_widgets_button"
|
<ContextMenuButton
|
||||||
label={_t("Options")}
|
className="mx_AppTileMenuBar_widgets_button"
|
||||||
isExpanded={this.state.menuDisplayed}
|
label={_t("Options")}
|
||||||
inputRef={this.contextMenuButton}
|
isExpanded={this.state.menuDisplayed}
|
||||||
onClick={this.onContextMenuClick}
|
inputRef={this.contextMenuButton}
|
||||||
>
|
onClick={this.onContextMenuClick}
|
||||||
<MenuIcon className="mx_Icon mx_Icon_12" />
|
>
|
||||||
</ContextMenuButton>
|
<MenuIcon className="mx_Icon mx_Icon_12" />
|
||||||
|
</ContextMenuButton>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -139,19 +139,6 @@ exports[`AppTile for a pinned widget should render 1`] = `
|
||||||
class="mx_Icon mx_Icon_12"
|
class="mx_Icon mx_Icon_12"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
aria-expanded="false"
|
|
||||||
aria-haspopup="true"
|
|
||||||
aria-label="Options"
|
|
||||||
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
title="Options"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_Icon mx_Icon_12"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
@ -214,19 +201,6 @@ exports[`AppTile for a pinned widget should render permission request 1`] = `
|
||||||
class="mx_Icon mx_Icon_12"
|
class="mx_Icon mx_Icon_12"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
aria-expanded="false"
|
|
||||||
aria-haspopup="true"
|
|
||||||
aria-label="Options"
|
|
||||||
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
title="Options"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_Icon mx_Icon_12"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
@ -367,19 +341,6 @@ exports[`AppTile preserves non-persisted widget on container move 1`] = `
|
||||||
class="mx_Icon mx_Icon_12"
|
class="mx_Icon mx_Icon_12"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
aria-expanded="false"
|
|
||||||
aria-haspopup="true"
|
|
||||||
aria-label="Options"
|
|
||||||
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
title="Options"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_Icon mx_Icon_12"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|
Loading…
Reference in a new issue