2019-08-14 23:15:49 +01:00
/*
2024-09-09 14:57:16 +01:00
Copyright 2024 New Vector Ltd.
Copyright 2020-2022 The Matrix.org Foundation C.I.C.
2019-08-14 23:15:49 +01:00
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
2024-09-09 14:57:16 +01:00
Copyright 2018 New Vector Ltd
Copyright 2017 Vector Creations Ltd
2017-05-22 12:34:27 +01:00
2024-09-09 14:57:16 +01:00
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
2017-05-22 12:34:27 +01:00
*/
2023-02-13 11:39:16 +00:00
import React , { ContextType , createRef , CSSProperties , MutableRefObject , ReactNode } from "react" ;
2021-12-09 09:10:23 +00:00
import classNames from "classnames" ;
2023-05-11 09:56:56 +01:00
import { IWidget , MatrixCapabilities } from "matrix-widget-api" ;
2024-03-18 14:40:52 +00:00
import { Room , RoomEvent } from "matrix-js-sdk/src/matrix" ;
import { KnownMembership } from "matrix-js-sdk/src/types" ;
2021-12-09 09:10:23 +00:00
import { logger } from "matrix-js-sdk/src/logger" ;
2023-02-23 17:53:44 +03:00
import { ApprovalOpts , WidgetLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle" ;
2024-10-21 11:36:31 +01:00
import {
OverflowHorizontalIcon ,
MinusIcon ,
ExpandIcon ,
CollapseIcon ,
} from "@vector-im/compound-design-tokens/assets/web/icons" ;
2021-12-09 09:10:23 +00:00
2019-02-01 11:02:02 +01:00
import AccessibleButton from "./AccessibleButton" ;
import { _t } from "../../../languageHandler" ;
2017-07-26 11:28:43 +01:00
import AppPermission from "./AppPermission" ;
2017-08-01 17:29:29 +01:00
import AppWarning from "./AppWarning" ;
2020-04-24 22:14:41 +01:00
import Spinner from "./Spinner" ;
2020-05-13 20:41:41 -06:00
import dis from "../../../dispatcher/dispatcher" ;
2018-07-11 18:07:32 +01:00
import ActiveWidgetStore from "../../../stores/ActiveWidgetStore" ;
2020-07-28 11:53:43 -06:00
import SettingsStore from "../../../settings/SettingsStore" ;
2021-06-29 13:11:58 +01:00
import { aboveLeftOf , ContextMenuButton } from "../../structures/ContextMenu" ;
import PersistedElement , { getPersistKey } from "./PersistedElement" ;
import { WidgetType } from "../../../widgets/WidgetType" ;
2022-06-06 12:00:18 +02:00
import { ElementWidget , StopGapWidget } from "../../../stores/widgets/StopGapWidget" ;
2023-07-17 11:37:07 +01:00
import { showContextMenu , WidgetContextMenu } from "../context_menus/WidgetContextMenu" ;
2020-10-12 13:16:55 +01:00
import WidgetAvatar from "../avatars/WidgetAvatar" ;
2022-08-30 15:13:39 -04:00
import LegacyCallHandler from "../../../LegacyCallHandler" ;
2023-05-11 09:56:56 +01:00
import { IApp , isAppWidget } from "../../../stores/WidgetStore" ;
2024-10-21 11:36:31 +01:00
import { Icon as PopoutIcon } from "../../../../res/img/external-link.svg" ;
2022-02-22 12:18:08 +00:00
import { Container , WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore" ;
2022-01-17 14:47:07 +01:00
import { OwnProfileStore } from "../../../stores/OwnProfileStore" ;
import { UPDATE_EVENT } from "../../../stores/AsyncStore" ;
2022-01-24 15:07:54 +01:00
import WidgetUtils from "../../../utils/WidgetUtils" ;
2022-02-07 14:40:22 +00:00
import MatrixClientContext from "../../../contexts/MatrixClientContext" ;
import { ActionPayload } from "../../../dispatcher/payloads" ;
2022-02-22 11:04:27 +01:00
import { Action } from "../../../dispatcher/actions" ;
2022-04-14 23:25:53 +02:00
import { ElementWidgetCapabilities } from "../../../stores/widgets/ElementWidgetCapabilities" ;
2022-06-06 12:00:18 +02:00
import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore" ;
2022-10-19 13:07:03 +01:00
import { SdkContextClass } from "../../../contexts/SDKContext" ;
2023-02-23 17:53:44 +03:00
import { ModuleRunner } from "../../../modules/ModuleRunner" ;
2023-05-17 12:50:00 +01:00
import { parseUrl } from "../../../utils/UrlUtils" ;
2024-09-27 15:10:33 +01:00
import RightPanelStore from "../../../stores/right-panel/RightPanelStore.ts" ;
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases.ts" ;
2021-12-09 09:10:23 +00:00
2021-09-16 20:05:57 +02:00
interface IProps {
2023-05-11 09:56:56 +01:00
app : IWidget | IApp ;
2021-09-16 20:05:57 +02:00
// If room is not specified then it is an account level widget
// which bypasses permission prompts as it was added explicitly by that user
2022-02-07 14:40:22 +00:00
room? : Room ;
2021-12-03 08:22:13 +00:00
threadId? : string | null ;
2022-05-09 16:52:05 -06:00
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer container.
2021-09-16 20:05:57 +02:00
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
fullWidth? : boolean ;
// Optional. If set, renders a smaller view of the widget
miniMode? : boolean ;
// UserId of the current user
userId : string ;
// UserId of the entity that added / modified the widget
creatorUserId : string ;
waitForIframeLoad : boolean ;
showMenubar? : boolean ;
// Optional onEditClickHandler (overrides default behaviour)
onEditClick ?: () => void ;
// Optional onDeleteClickHandler (overrides default behaviour)
onDeleteClick ?: () => void ;
// Optionally hide the tile title
showTitle? : boolean ;
// Optionally handle minimise button pointer events (default false)
handleMinimisePointerEvents? : boolean ;
// Optionally hide the popout widget icon
showPopout? : boolean ;
// Is this an instance of a user widget
userWidget : boolean ;
// sets the pointer-events property on the iframe
2023-02-13 11:39:16 +00:00
pointerEvents? : CSSProperties [ "pointerEvents" ];
2021-09-16 20:05:57 +02:00
widgetPageTitle? : string ;
2022-02-09 10:47:41 +00:00
showLayoutButtons? : boolean ;
2022-06-02 14:11:28 -04:00
// Handle to manually notify the PersistedElement that it needs to move
2023-01-03 23:44:38 -05:00
movePersistedElement? : MutableRefObject < (() => void ) | undefined > ;
2024-01-08 12:11:44 +00:00
// An element to render after the iframe as an overlay
overlay? : ReactNode ;
2024-01-29 17:06:12 +01:00
// If defined this async method will be called when the widget requests to become sticky.
// It will only become sticky once the returned promise resolves.
// This is useful because: Widget B is sticky. Making widget A sticky will kill widget B immediately.
// This promise allows to do Widget B related cleanup before Widget A becomes sticky. (e.g. hangup a Voip call)
stickyPromise ?: () => Promise < void >;
2021-09-16 20:05:57 +02:00
}
interface IState {
initialising : boolean ; // True while we are mangling the widget URL
// True while the iframe content is loading
loading : boolean ;
// Assume that widget has permission to load if we are the user who
// added it to the room, or if explicitly granted by the user
hasPermissionToLoad : boolean ;
2022-01-17 14:47:07 +01:00
// Wait for user profile load to display correct name
isUserProfileReady : boolean ;
2023-03-08 11:48:58 +00:00
error : Error | null ;
2021-09-16 20:05:57 +02:00
menuDisplayed : boolean ;
2021-10-28 14:17:04 +02:00
requiresClient : boolean ;
2023-07-17 11:37:07 +01:00
hasContextMenuOptions : boolean ;
2021-09-16 20:05:57 +02:00
}
2020-04-01 10:00:33 +01:00
2021-09-16 20:05:57 +02:00
export default class AppTile extends React . Component < IProps , IState > {
2022-02-07 14:40:22 +00:00
public static contextType = MatrixClientContext ;
2024-08-01 17:14:28 +01:00
public declare context : ContextType < typeof MatrixClientContext >;
2022-02-07 14:40:22 +00:00
2021-09-16 20:05:57 +02:00
public static defaultProps : Partial < IProps > = {
waitForIframeLoad : true ,
showMenubar : true ,
showTitle : true ,
showPopout : true ,
handleMinimisePointerEvents : false ,
userWidget : false ,
miniMode : false ,
2021-12-03 08:22:13 +00:00
threadId : null ,
2022-02-09 10:47:41 +00:00
showLayoutButtons : true ,
2021-09-16 20:05:57 +02:00
};
private contextMenuButton = createRef < any >();
2023-05-10 08:41:55 +01:00
private iframe? : HTMLIFrameElement ; // ref to the iframe (callback style)
private allowedWidgetsWatchRef? : string ;
2021-09-16 20:05:57 +02:00
private persistKey : string ;
2023-03-08 11:48:58 +00:00
private sgWidget : StopGapWidget | null ;
2023-05-10 08:41:55 +01:00
private dispatcherRef? : string ;
2023-05-16 14:25:43 +01:00
private unmounted = false ;
2024-03-28 12:04:14 +00:00
2023-08-03 10:23:20 +01:00
public constructor ( props : IProps , context : ContextType < typeof MatrixClientContext >) {
2024-08-01 13:01:05 +01:00
super ( props , context );
2018-07-11 18:07:32 +01:00
2022-04-25 08:29:54 -04:00
// Tiles in miniMode are floating, and therefore not docked
if ( ! this . props . miniMode ) {
2023-05-11 09:56:56 +01:00
ActiveWidgetStore . instance . dockWidget (
this . props . app . id ,
isAppWidget ( this . props . app ) ? this . props.app.roomId : null ,
);
2022-04-25 08:29:54 -04:00
}
2022-02-17 16:30:36 +00:00
2018-07-11 18:07:32 +01:00
// The key used for PersistedElement
2022-03-15 08:15:26 -04:00
this . persistKey = getPersistKey ( WidgetUtils . getWidgetUid ( this . props . app ));
2021-06-08 12:40:38 +01:00
try {
2021-09-16 20:05:57 +02:00
this . sgWidget = new StopGapWidget ( this . props );
2022-02-07 14:40:22 +00:00
this . setupSgListeners ();
2021-06-08 12:40:38 +01:00
} catch ( e ) {
2021-09-21 17:48:09 +02:00
logger . log ( "Failed to construct widget" , e );
2021-09-16 20:05:57 +02:00
this . sgWidget = null ;
2021-06-08 12:40:38 +01:00
}
2018-07-11 18:07:32 +01:00
2021-09-16 20:05:57 +02:00
this . state = this . getNewState ( props );
2018-01-11 12:33:02 +00:00
}
2023-01-12 13:25:14 +00:00
private watchUserReady = () : void => {
2022-01-17 14:47:07 +01:00
if ( OwnProfileStore . instance . isProfileInfoFetched ) {
return ;
}
OwnProfileStore . instance . once ( UPDATE_EVENT , this . onUserReady );
};
private onUserReady = () : void => {
this . setState ({ isUserProfileReady : true });
};
2020-10-09 08:42:21 +01:00
// This is a function to make the impact of calling SettingsStore slightly less
2021-09-16 20:05:57 +02:00
private hasPermissionToLoad = ( props : IProps ) : boolean => {
if ( this . usingLocalWidget ()) return true ;
2020-10-21 12:48:51 +01:00
if ( ! props . room ) return true ; // user widgets always have permissions
2023-02-23 17:53:44 +03:00
const opts : ApprovalOpts = { approved : undefined };
ModuleRunner . instance . invoke ( WidgetLifecycle . PreLoadRequest , opts , new ElementWidget ( this . props . app ));
if ( opts . approved ) return true ;
2020-10-09 08:42:21 +01:00
const currentlyAllowedWidgets = SettingsStore . getValue ( "allowedWidgets" , props . room . roomId );
2023-05-11 09:56:56 +01:00
const allowed =
isAppWidget ( props . app ) &&
props . app . eventId !== undefined &&
( currentlyAllowedWidgets [ props . app . eventId ] ?? false );
2022-09-16 11:12:27 -04:00
return allowed || props . userId === props . creatorUserId ;
2020-10-09 08:42:21 +01:00
};
2023-01-12 13:25:14 +00:00
private onUserLeftRoom () : void {
2022-03-15 08:15:26 -04:00
const isActiveWidget = ActiveWidgetStore . instance . getWidgetPersistence (
this . props . app . id ,
2023-05-11 09:56:56 +01:00
isAppWidget ( this . props . app ) ? this . props.app.roomId : null ,
2022-03-15 08:15:26 -04:00
);
2022-02-07 14:40:22 +00:00
if ( isActiveWidget ) {
// We just left the room that the active widget was from.
2022-10-19 13:07:03 +01:00
if ( this . props . room && SdkContextClass . instance . roomViewStore . getRoomId () !== this . props . room . roomId ) {
2022-02-07 14:40:22 +00:00
// If we are not actively looking at the room then destroy this widget entirely.
this . endWidgetActions ();
} else if ( WidgetType . JITSI . matches ( this . props . app . type )) {
// If this was a Jitsi then reload to end call.
this . reload ();
} else {
// Otherwise just cancel its persistence.
2023-05-11 09:56:56 +01:00
ActiveWidgetStore . instance . destroyPersistentWidget (
this . props . app . id ,
isAppWidget ( this . props . app ) ? this . props.app.roomId : null ,
);
2022-02-07 14:40:22 +00:00
}
}
}
private onMyMembership = ( room : Room , membership : string ) : void => {
2024-03-11 17:16:53 +00:00
if (
2024-03-12 14:52:54 +00:00
( membership === KnownMembership . Leave || membership === KnownMembership . Ban ) &&
2024-03-11 17:16:53 +00:00
room . roomId === this . props . room ? . roomId
) {
2022-02-07 14:40:22 +00:00
this . onUserLeftRoom ();
2022-01-24 10:24:30 -05:00
}
};
2022-06-06 12:00:18 +02:00
private determineInitialRequiresClientState () : boolean {
try {
const mockWidget = new ElementWidget ( this . props . app );
2023-03-08 11:48:58 +00:00
const widgetApi = WidgetMessagingStore . instance . getMessaging ( mockWidget , this . props . room ? . roomId );
2022-06-06 12:00:18 +02:00
if ( widgetApi ) {
// Load value from existing API to prevent resetting the requiresClient value on layout changes.
return widgetApi . hasCapability ( ElementWidgetCapabilities . RequiresClient );
}
} catch {
// fallback to true
}
// requiresClient is initially set to true. This avoids the broken state of the popout
// button being visible (for an instance) and then disappearing when the widget is loaded.
// requiresClient <-> hide the popout button
return true ;
}
2017-10-31 16:31:46 +00:00
/**
2017-11-10 11:50:14 +00:00
* Set initial component state when the App wUrl (widget URL) is being updated.
* Component props *must* be passed (rather than relying on this.props).
2017-11-08 17:44:54 +00:00
* @param {Object} newProps The new properties of the component
2017-10-31 16:31:46 +00:00
* @return {Object} Updated component state to be set with setState
*/
2021-09-16 20:05:57 +02:00
private getNewState ( newProps : IProps ) : IState {
2017-11-02 18:33:11 +00:00
return {
2017-12-28 22:27:12 +00:00
initialising : true , // True while we are mangling the widget URL
2024-03-12 17:55:01 +01:00
// Don't show loading at all if the widget is ready once the IFrame is loaded (waitForIframeLoad = true).
// We only need the loading screen if the widget sends a contentLoaded event (waitForIframeLoad = false).
loading : ! this . props . waitForIframeLoad && ! PersistedElement . isMounted ( this . persistKey ),
2017-11-10 11:50:14 +00:00
// Assume that widget has permission to load if we are the user who
// added it to the room, or if explicitly granted by the user
2020-10-12 09:51:49 +01:00
hasPermissionToLoad : this.hasPermissionToLoad ( newProps ),
2022-01-17 14:47:07 +01:00
isUserProfileReady : OwnProfileStore.instance.isProfileInfoFetched ,
2017-11-02 18:33:11 +00:00
error : null ,
2019-11-28 18:16:59 +00:00
menuDisplayed : false ,
2022-06-06 12:00:18 +02:00
requiresClient : this.determineInitialRequiresClientState (),
2023-07-17 11:37:07 +01:00
hasContextMenuOptions : showContextMenu (
this . context ,
this . props . room ,
newProps . app ,
newProps . userWidget ,
! newProps . userWidget ,
newProps . onDeleteClick ,
),
2017-11-02 18:33:11 +00:00
};
2018-01-11 11:49:46 +00:00
}
2017-10-27 17:49:14 +01:00
2021-09-16 20:05:57 +02:00
private onAllowedWidgetsChange = () : void => {
2020-10-12 09:51:49 +01:00
const hasPermissionToLoad = this . hasPermissionToLoad ( this . props );
2020-10-09 08:42:21 +01:00
if ( this . state . hasPermissionToLoad && ! hasPermissionToLoad ) {
// Force the widget to be non-persistent (able to be deleted/forgotten)
2023-05-11 09:56:56 +01:00
ActiveWidgetStore . instance . destroyPersistentWidget (
this . props . app . id ,
isAppWidget ( this . props . app ) ? this . props.app.roomId : null ,
);
2021-09-16 20:05:57 +02:00
PersistedElement . destroyElement ( this . persistKey );
2022-01-24 10:24:30 -05:00
this . sgWidget ? . stopMessaging ();
2020-10-09 08:42:21 +01:00
}
2020-10-09 08:45:11 +01:00
this . setState ({ hasPermissionToLoad });
2020-10-09 08:42:21 +01:00
};
2021-09-16 20:05:57 +02:00
private isMixedContent () : boolean {
2017-08-01 17:29:29 +01:00
const parentContentProtocol = window . location . protocol ;
2023-05-17 12:50:00 +01:00
const u = parseUrl ( this . props . app . url );
2017-08-01 17:29:29 +01:00
const childContentProtocol = u . protocol ;
2017-08-01 17:49:41 +01:00
if ( parentContentProtocol === "https:" && childContentProtocol !== "https:" ) {
2021-10-15 16:31:29 +02:00
logger . warn (
"Refusing to load mixed-content app:" ,
2021-04-27 17:23:27 +02:00
parentContentProtocol ,
childContentProtocol ,
window . location ,
this . props . app . url ,
);
2017-08-01 17:29:29 +01:00
return true ;
}
return false ;
2018-01-11 11:49:46 +00:00
}
2017-08-01 17:29:29 +01:00
2021-09-16 20:05:57 +02:00
public componentDidMount () : void {
2019-07-19 19:04:48 +01:00
// Only fetch IM token on mount if we're showing and have permission to load
2021-09-16 20:05:57 +02:00
if ( this . sgWidget && this . state . hasPermissionToLoad ) {
this . startWidget ();
2019-07-19 19:04:48 +01:00
}
2022-01-24 10:24:30 -05:00
this . watchUserReady ();
2017-10-27 13:47:51 +01:00
2022-01-28 08:52:46 -07:00
if ( this . props . room ) {
2022-02-22 12:18:08 +00:00
this . context . on ( RoomEvent . MyMembership , this . onMyMembership );
2022-01-28 08:52:46 -07:00
}
2022-01-24 10:24:30 -05:00
this . allowedWidgetsWatchRef = SettingsStore . watchSetting ( "allowedWidgets" , null , this . onAllowedWidgetsChange );
2018-01-18 12:02:45 +00:00
// Widget action listeners
2021-09-16 20:05:57 +02:00
this . dispatcherRef = dis . register ( this . onAction );
2018-01-11 11:49:46 +00:00
}
2017-12-15 16:56:02 +00:00
2021-09-16 20:05:57 +02:00
public componentWillUnmount () : void {
2022-02-17 16:30:36 +00:00
this . unmounted = true ;
2022-04-25 08:29:54 -04:00
if ( ! this . props . miniMode ) {
2023-05-11 09:56:56 +01:00
ActiveWidgetStore . instance . undockWidget (
this . props . app . id ,
isAppWidget ( this . props . app ) ? this . props.app.roomId : null ,
);
2022-04-25 08:29:54 -04:00
}
2022-02-17 16:30:36 +00:00
2022-04-25 08:29:54 -04:00
// Only tear down the widget if no other component is keeping it alive,
// because we support moving widgets between containers, in which case
// another component will keep it loaded throughout the transition
2023-05-11 09:56:56 +01:00
if (
! ActiveWidgetStore . instance . isLive (
this . props . app . id ,
isAppWidget ( this . props . app ) ? this . props.app.roomId : null ,
)
) {
2022-02-17 16:30:36 +00:00
this . endWidgetActions ();
}
2018-01-18 12:02:45 +00:00
// Widget action listeners
2020-03-31 14:05:56 -06:00
if ( this . dispatcherRef ) dis . unregister ( this . dispatcherRef );
2018-01-18 12:02:45 +00:00
2022-01-28 08:52:46 -07:00
if ( this . props . room ) {
2022-02-22 12:18:08 +00:00
this . context . off ( RoomEvent . MyMembership , this . onMyMembership );
2022-01-28 08:52:46 -07:00
}
2023-05-10 08:41:55 +01:00
if ( this . allowedWidgetsWatchRef ) SettingsStore . unwatchSetting ( this . allowedWidgetsWatchRef );
2022-01-17 14:47:07 +01:00
OwnProfileStore . instance . removeListener ( UPDATE_EVENT , this . onUserReady );
2018-01-18 12:02:45 +00:00
}
2023-01-12 13:25:14 +00:00
private setupSgListeners () : void {
2024-03-12 17:55:01 +01:00
this . sgWidget ? . on ( "ready" , this . onWidgetReady );
2023-08-03 11:14:15 +01:00
this . sgWidget ? . on ( "error:preparing" , this . updateRequiresClient );
2022-05-09 16:52:05 -06:00
// emits when the capabilities have been set up or changed
2023-08-03 11:14:15 +01:00
this . sgWidget ? . on ( "capabilitiesNotified" , this . updateRequiresClient );
2022-02-07 14:40:22 +00:00
}
2023-01-12 13:25:14 +00:00
private stopSgListeners () : void {
2022-02-07 14:40:22 +00:00
if ( ! this . sgWidget ) return ;
2024-03-12 17:55:01 +01:00
this . sgWidget ? . off ( "ready" , this . onWidgetReady );
2023-08-03 11:14:15 +01:00
this . sgWidget . off ( "error:preparing" , this . updateRequiresClient );
this . sgWidget . off ( "capabilitiesNotified" , this . updateRequiresClient );
2022-02-07 14:40:22 +00:00
}
2021-09-16 20:05:57 +02:00
private resetWidget ( newProps : IProps ) : void {
2022-01-24 10:24:30 -05:00
this . sgWidget ? . stopMessaging ();
2022-02-07 14:40:22 +00:00
this . stopSgListeners ();
2021-06-08 12:40:38 +01:00
try {
2021-09-16 20:05:57 +02:00
this . sgWidget = new StopGapWidget ( newProps );
2022-02-07 14:40:22 +00:00
this . setupSgListeners ();
2021-09-16 20:05:57 +02:00
this . startWidget ();
2021-06-08 12:40:38 +01:00
} catch ( e ) {
2021-11-04 11:36:20 +01:00
logger . error ( "Failed to construct widget" , e );
2021-09-16 20:05:57 +02:00
this . sgWidget = null ;
2021-06-08 12:40:38 +01:00
}
2020-09-29 14:14:51 -06:00
}
2017-11-02 18:33:11 +00:00
2021-09-16 20:05:57 +02:00
private startWidget () : void {
2023-03-08 11:48:58 +00:00
this . sgWidget ? . prepare (). then (() => {
2022-02-17 16:30:36 +00:00
if ( this . unmounted ) return ;
2021-06-29 13:11:58 +01:00
this . setState ({ initialising : false });
2017-07-06 09:28:48 +01:00
});
2018-01-11 11:49:46 +00:00
}
2017-09-04 09:31:25 +02:00
2023-01-12 13:25:14 +00:00
private startMessaging () : void {
2022-02-07 14:40:22 +00:00
try {
2023-05-10 08:41:55 +01:00
this . sgWidget ? . startMessaging ( this . iframe ! );
2022-02-07 14:40:22 +00:00
} catch ( e ) {
logger . error ( "Failed to start widget" , e );
}
}
2021-09-16 20:05:57 +02:00
private iframeRefChange = ( ref : HTMLIFrameElement ) : void => {
2020-10-01 10:03:52 -06:00
this . iframe = ref ;
2022-02-17 16:30:36 +00:00
if ( this . unmounted ) return ;
2020-09-29 14:33:46 -06:00
if ( ref ) {
2022-02-07 14:40:22 +00:00
this . startMessaging ();
2020-09-29 14:33:46 -06:00
} else {
2021-09-16 20:05:57 +02:00
this . resetWidget ( this . props );
2020-09-29 14:33:46 -06:00
}
};
2017-09-04 09:31:25 +02:00
2022-11-18 09:22:43 +00:00
public componentDidUpdate ( prevProps : IProps ) : void {
if ( prevProps . app . url !== this . props . app . url ) {
this . getNewState ( this . props );
2019-07-19 19:04:48 +01:00
if ( this . state . hasPermissionToLoad ) {
2022-11-18 09:22:43 +00:00
this . resetWidget ( this . props );
2019-07-19 19:04:48 +01:00
}
2020-04-09 14:31:46 -06:00
}
2018-01-11 11:49:46 +00:00
}
2017-10-27 13:47:51 +01:00
2020-04-09 14:31:46 -06:00
/**
* Ends all widget interaction, such as cancelling calls and disabling webcams.
* @private
2020-04-18 14:53:48 +03:00
* @returns {Promise<*>} Resolves when the widget is terminated, or timeout passed.
2020-04-09 14:31:46 -06:00
*/
2021-09-16 20:05:57 +02:00
private async endWidgetActions () : Promise < void > {
// widget migration dev note: async to maintain signature
2020-09-29 14:14:51 -06:00
// HACK: This is a really dirty way to ensure that Jitsi cleans up
// its hold on the webcam. Without this, the widget holds a media
// stream open, even after death. See https://github.com/vector-im/element-web/issues/7351
2020-10-01 10:03:52 -06:00
if ( this . iframe ) {
2020-09-29 14:14:51 -06:00
// In practice we could just do `+= ''` to trick the browser
// into thinking the URL changed, however I can foresee this
// being optimized out by a browser. Instead, we'll just point
// the iframe at a page that is reasonably safe to use in the
// event the iframe doesn't wink away.
// This is relative to where the Element instance is located.
2020-10-01 10:03:52 -06:00
this . iframe . src = "about:blank" ;
2020-04-09 14:31:46 -06:00
}
2022-02-16 13:26:18 -07:00
if ( WidgetType . JITSI . matches ( this . props . app . type ) && this . props . room ) {
2022-08-30 15:13:39 -04:00
LegacyCallHandler . instance . hangupCallApp ( this . props . room . roomId );
2020-10-13 14:55:44 -06:00
}
2020-04-18 14:53:48 +03:00
2020-09-29 14:14:51 -06:00
// Delete the widget from the persisted store for good measure.
2021-09-16 20:05:57 +02:00
PersistedElement . destroyElement ( this . persistKey );
2023-05-11 09:56:56 +01:00
ActiveWidgetStore . instance . destroyPersistentWidget (
this . props . app . id ,
isAppWidget ( this . props . app ) ? this . props.app.roomId : null ,
);
2020-04-18 14:53:48 +03:00
2022-01-24 10:24:30 -05:00
this . sgWidget ? . stopMessaging ({ forceDestroy : true });
2020-04-09 14:31:46 -06:00
}
2024-03-12 17:55:01 +01:00
private onWidgetReady = () : void => {
2021-06-29 13:11:58 +01:00
this . setState ({ loading : false });
2020-10-08 15:35:22 -06:00
};
2018-03-28 12:22:06 +01:00
2023-08-03 11:14:15 +01:00
private updateRequiresClient = () : void => {
2021-10-28 14:17:04 +02:00
this . setState ({
2023-03-08 11:48:58 +00:00
requiresClient : !! this . sgWidget ? . widgetApi ? . hasCapability ( ElementWidgetCapabilities . RequiresClient ),
2021-10-28 14:17:04 +02:00
});
};
2022-02-07 14:40:22 +00:00
private onAction = ( payload : ActionPayload ) : void => {
switch ( payload . action ) {
case "m.sticker" :
if (
payload . widgetId === this . props . app . id &&
2023-03-08 11:48:58 +00:00
this . sgWidget ? . widgetApi ? . hasCapability ( MatrixCapabilities . StickerSending )
2022-02-07 14:40:22 +00:00
) {
dis . dispatch ({
action : "post_sticker_message" ,
data : {
... payload . data ,
threadId : this.props.threadId ,
},
});
dis . dispatch ({ action : "stickerpicker_close" });
} else {
logger . warn ( "Ignoring sticker message. Invalid capability" );
}
break ;
2022-02-22 11:04:27 +01:00
case Action.AfterLeaveRoom :
2022-02-07 14:40:22 +00:00
if ( payload . room_id === this . props . room ? . roomId ) {
// call this before we get it echoed down /sync, so it doesn't hang around as long and look jarring
this . onUserLeftRoom ();
}
break ;
2018-01-04 18:58:55 +00:00
}
2020-10-09 08:42:21 +01:00
};
2018-01-04 18:41:47 +00:00
2021-09-16 20:05:57 +02:00
private grantWidgetPermission = () : void => {
2022-02-16 13:26:18 -07:00
const roomId = this . props . room ? . roomId ;
2023-05-11 09:56:56 +01:00
const eventId = isAppWidget ( this . props . app ) ? this . props.app.eventId : undefined ;
logger . info ( "Granting permission for widget to load: " + eventId );
2019-11-18 17:56:33 -07:00
const current = SettingsStore . getValue ( "allowedWidgets" , roomId );
2023-05-11 09:56:56 +01:00
if ( eventId !== undefined ) current [ eventId ] = true ;
2023-03-08 11:48:58 +00:00
const level = SettingsStore . firstSupportedLevel ( "allowedWidgets" ) ! ;
SettingsStore . setValue ( "allowedWidgets" , roomId ?? null , level , current )
2021-01-14 17:30:25 +00:00
. then (() => {
2021-06-29 13:11:58 +01:00
this . setState ({ hasPermissionToLoad : true });
2019-11-18 17:56:33 -07:00
// Fetch a token for the integration manager, now that we're allowed to
2021-09-16 20:05:57 +02:00
this . startWidget ();
2019-11-18 17:56:33 -07:00
})
. catch (( err ) => {
2021-10-15 16:30:53 +02:00
logger . error ( err );
2019-11-18 17:56:33 -07:00
// We don't really need to do anything about this - the user will just hit the button again.
});
2020-10-09 08:42:21 +01:00
};
2017-05-22 18:00:17 +01:00
2021-09-16 20:05:57 +02:00
private formatAppTileName () : string {
2017-06-28 10:27:06 +01:00
let appTileName = "No name" ;
2020-04-01 10:00:33 +01:00
if ( this . props . app . name && this . props . app . name . trim ()) {
appTileName = this . props . app . name . trim ();
2017-06-28 10:27:06 +01:00
}
return appTileName ;
2018-01-11 11:49:46 +00:00
}
2017-06-28 10:27:06 +01:00
2020-09-09 15:35:05 +01:00
/**
* Whether we're using a local version of the widget rather than loading the
* actual widget URL
2020-09-09 15:42:25 +01:00
* @returns {bool} true If using a local version of the widget
2020-09-09 15:35:05 +01:00
*/
2021-09-16 20:05:57 +02:00
private usingLocalWidget () : boolean {
2020-09-09 15:35:05 +01:00
return WidgetType . JITSI . matches ( this . props . app . type );
}
2021-09-16 20:05:57 +02:00
private getTileTitle () : JSX . Element {
2018-02-07 14:44:01 +00:00
const name = this . formatAppTileName ();
const titleSpacer = < span > & nbsp ; -& nbsp ;</ span >;
let title = "" ;
2022-11-18 09:22:43 +00:00
if ( this . props . widgetPageTitle && this . props . widgetPageTitle !== this . formatAppTileName ()) {
title = this . props . widgetPageTitle ;
2018-02-07 14:44:01 +00:00
}
return (
< span >
2023-08-24 04:48:35 +01:00
< WidgetAvatar app = { this . props . app } size = "20px" />
2024-05-08 19:27:17 +05:30
< h3 >{ name }</ h3 >
2018-02-07 14:44:01 +00:00
< span >
{ title ? titleSpacer : "" }
{ title }
</ span >
</ span >
);
}
2023-01-12 13:25:14 +00:00
private reload () : void {
2022-02-07 14:40:22 +00:00
this . endWidgetActions (). then (() => {
// reset messaging
this . resetWidget ( this . props );
this . startMessaging ();
2024-03-28 12:04:14 +00:00
if ( this . iframe && this . sgWidget ) {
2022-02-07 14:40:22 +00:00
// Reload iframe
2024-03-28 12:04:14 +00:00
this . iframe . src = this . sgWidget . embedUrl ;
2022-02-07 14:40:22 +00:00
}
});
}
2020-10-09 08:42:21 +01:00
// TODO replace with full screen interactions
2021-09-16 20:05:57 +02:00
private onPopoutWidgetClick = () : void => {
2020-04-11 15:23:32 +03:00
// Ensure Jitsi conferences are closed on pop-out, to not confuse the user to join them
// twice from the same computer, which Jitsi can have problems with (audio echo/gain-loop).
2020-10-09 08:42:21 +01:00
if ( WidgetType . JITSI . matches ( this . props . app . type )) {
2022-02-07 14:40:22 +00:00
this . reload ();
2020-04-11 15:23:32 +03:00
}
2018-04-25 12:49:30 +01:00
// Using Object.assign workaround as the following opens in a new window instead of a new tab.
2020-04-01 10:00:33 +01:00
// window.open(this._getPopoutUrl(), '_blank', 'noopener=yes');
2018-04-25 12:49:30 +01:00
Object . assign ( document . createElement ( "a" ), {
2021-09-16 20:05:57 +02:00
target : "_blank" ,
2023-03-08 11:48:58 +00:00
href : this.sgWidget?.popoutUrl ,
2021-09-16 20:05:57 +02:00
rel : "noreferrer noopener" ,
}). click ();
2020-10-09 08:42:21 +01:00
};
2018-05-22 19:14:54 +01:00
2022-02-09 10:47:41 +00:00
private onToggleMaximisedClick = () : void => {
2022-02-16 13:26:18 -07:00
if ( ! this . props . room ) return ; // ignore action - it shouldn't even be visible
2021-11-29 17:06:15 +01:00
const targetContainer = WidgetLayoutStore . instance . isInContainer (
this . props . room ,
this . props . app ,
Container . Center ,
)
2022-06-03 12:00:16 +02:00
? Container.Top
2021-11-29 17:06:15 +01:00
: Container.Center ;
2021-11-16 15:43:18 +01:00
WidgetLayoutStore . instance . moveToContainer ( this . props . room , this . props . app , targetContainer );
2024-09-27 15:10:33 +01:00
2024-10-21 10:06:18 +01:00
if ( targetContainer === Container . Top ) this . closeChatCardIfNeeded ();
2021-11-16 15:43:18 +01:00
};
2022-06-03 12:00:16 +02:00
private onMinimiseClicked = () : void => {
2022-02-16 13:26:18 -07:00
if ( ! this . props . room ) return ; // ignore action - it shouldn't even be visible
2022-06-03 12:00:16 +02:00
WidgetLayoutStore . instance . moveToContainer ( this . props . room , this . props . app , Container . Right );
2024-10-21 10:06:18 +01:00
this . closeChatCardIfNeeded ();
};
private closeChatCardIfNeeded = () : void => {
if ( ! this . props . room ) return ; // ignore action - it shouldn't even be visible
// If the right panel has a timeline, but we're about to show the timeline in the main view, pop the right panel
if ( RightPanelStore . instance . currentCardForRoom ( this . props . room . roomId ). phase === RightPanelPhases . Timeline ) {
RightPanelStore . instance . popCard ( this . props . room . roomId );
}
2022-02-09 10:47:41 +00:00
};
2021-09-16 20:05:57 +02:00
private onContextMenuClick = () : void => {
2019-11-28 18:16:59 +00:00
this . setState ({ menuDisplayed : true });
};
2019-11-20 19:17:42 -07:00
2021-09-16 20:05:57 +02:00
private closeContextMenu = () : void => {
2019-11-28 18:16:59 +00:00
this . setState ({ menuDisplayed : false });
2019-11-20 19:17:42 -07:00
};
2023-02-13 17:01:43 +00:00
public render () : React . ReactNode {
2023-05-11 09:56:56 +01:00
let appTileBody : JSX.Element ;
2017-07-13 00:27:03 +01:00
2017-07-26 11:28:43 +01:00
// Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin
2019-11-20 19:17:42 -07:00
// because that would allow the iframe to programmatically remove the sandbox attribute, but
2020-08-03 16:02:26 +01:00
// this would only be for content hosted on the same origin as the element client: anything
2017-07-26 11:28:43 +01:00
// hosted on the same origin as the client will get the same access as if you clicked
// a link to it.
2021-08-19 06:42:10 +02:00
const sandboxFlags =
"allow-forms allow-popups allow-popups-to-escape-sandbox " +
2022-01-21 00:52:06 +01:00
"allow-same-origin allow-scripts allow-presentation allow-downloads" ;
2017-07-26 11:28:43 +01:00
2022-05-09 16:52:05 -06:00
// Additional iframe feature permissions
2018-02-21 23:10:08 +00:00
// (see - https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-permissions-in-cross-origin-iframes and https://wicg.github.io/feature-policy/)
2022-09-07 06:07:52 +02:00
const iframeFeatures =
"microphone; camera; encrypted-media; autoplay; display-capture; clipboard-write; " + "clipboard-read;" ;
2018-02-21 23:10:08 +00:00
2023-05-19 15:18:18 +00:00
const appTileBodyClass = classNames ({
2023-06-16 18:24:10 +00:00
"mx_AppTileBody" : true ,
"mx_AppTileBody--large" : ! this . props . miniMode ,
"mx_AppTileBody--mini" : this . props . miniMode ,
"mx_AppTileBody--loading" : this . state . loading ,
2024-01-29 17:06:12 +01:00
// We don't want mx_AppTileBody (rounded corners) for call widgets
2024-03-28 12:04:14 +00:00
"mx_AppTileBody--call" : this . props . app . type === WidgetType . CALL . preferred ,
2023-05-19 15:18:18 +00:00
});
2023-02-13 11:39:16 +00:00
const appTileBodyStyles : CSSProperties = {};
2021-03-05 17:52:51 +00:00
if ( this . props . pointerEvents ) {
2023-02-13 11:39:16 +00:00
appTileBodyStyles . pointerEvents = this . props . pointerEvents ;
2021-03-05 17:52:51 +00:00
}
2018-07-12 18:43:49 +01:00
2020-10-09 08:42:21 +01:00
const loadingElement = (
2023-06-01 15:17:28 +00:00
< div className = "mx_AppTileBody_fadeInSpinner" >
2023-08-22 18:47:33 +01:00
< Spinner message = { _t ( "common|loading" )} />
2020-10-09 08:42:21 +01:00
</ div >
);
2022-01-24 15:07:54 +01:00
const widgetTitle = WidgetUtils . getWidgetName ( this . props . app );
2021-09-16 20:05:57 +02:00
if ( this . sgWidget === null ) {
2021-06-08 12:40:38 +01:00
appTileBody = (
< div className = { appTileBodyClass } style = { appTileBodyStyles }>
2023-09-25 18:12:41 +01:00
< AppWarning errorMsg = { _t ( "widget|error_loading" )} />
2021-06-08 12:40:38 +01:00
</ div >
);
2023-03-29 08:23:54 +01:00
} else if ( ! this . state . hasPermissionToLoad && this . props . room ) {
2020-10-21 12:48:51 +01:00
// only possible for room widgets, can assert this.props.room here
2022-02-07 14:40:22 +00:00
const isEncrypted = this . context . isRoomEncrypted ( this . props . room . roomId );
2020-10-09 08:42:21 +01:00
appTileBody = (
2021-03-05 17:52:51 +00:00
< div className = { appTileBodyClass } style = { appTileBodyStyles }>
2020-10-09 08:42:21 +01:00
< AppPermission
roomId = { this . props . room . roomId }
creatorUserId = { this . props . creatorUserId }
2024-03-28 12:04:14 +00:00
url = { this . sgWidget . embedUrl }
2020-10-09 08:42:21 +01:00
isRoomEncrypted = { isEncrypted }
2021-09-16 20:05:57 +02:00
onPermissionGranted = { this . grantWidgetPermission }
2020-10-09 08:42:21 +01:00
/>
2017-11-08 20:38:31 +00:00
</ div >
);
2022-01-17 14:47:07 +01:00
} else if ( this . state . initialising || ! this . state . isUserProfileReady ) {
2020-10-09 08:42:21 +01:00
appTileBody = (
2023-05-19 15:18:18 +00:00
< div className = { appTileBodyClass } style = { appTileBodyStyles }>
2020-10-09 08:42:21 +01:00
{ loadingElement }
</ div >
);
} else {
if ( this . isMixedContent ()) {
2019-07-19 19:04:48 +01:00
appTileBody = (
2021-03-05 17:52:51 +00:00
< div className = { appTileBodyClass } style = { appTileBodyStyles }>
2023-09-25 18:12:41 +01:00
< AppWarning errorMsg = { _t ( "widget|error_mixed_content" )} />
2018-05-09 15:48:53 +01:00
</ div >
);
2019-07-19 19:04:48 +01:00
} else {
2020-10-09 08:42:21 +01:00
appTileBody = (
2024-01-08 12:11:44 +00:00
<>
< div className = { appTileBodyClass } style = { appTileBodyStyles }>
{ this . state . loading && loadingElement }
< iframe
title = { widgetTitle }
allow = { iframeFeatures }
ref = { this . iframeRefChange }
2024-03-28 12:04:14 +00:00
src = { this . sgWidget . embedUrl }
2024-01-08 12:11:44 +00:00
allowFullScreen = { true }
sandbox = { sandboxFlags }
/>
</ div >
{ this . props . overlay }
</>
2020-10-09 08:42:21 +01:00
);
2020-11-20 16:53:15 -07:00
2020-11-26 15:09:08 -07:00
if ( ! this . props . userWidget ) {
// All room widgets can theoretically be allowed to remain on screen, so we
// wrap them all in a PersistedElement from the get-go. If we wait, the iframe
// will be re-mounted later, which means the widget has to start over, which is
// bad.
2020-11-20 16:53:15 -07:00
2020-11-26 15:09:08 -07:00
// Also wrap the PersistedElement in a div to fix the height, otherwise
// AppTile's border is in the wrong place
2022-01-13 12:10:41 +01:00
2022-01-24 10:24:30 -05:00
// For persisted apps in PiP we want the zIndex to be higher then for other persisted apps (100)
2022-01-13 12:10:41 +01:00
// otherwise there are issues that the PiP view is drawn UNDER another widget (Persistent app) when dragged around.
const zIndexAboveOtherPersistentElements = 101 ;
2020-11-26 15:09:08 -07:00
appTileBody = (
2024-03-28 12:04:14 +00:00
< div className = "mx_AppTile_persistedWrapper" >
2022-06-02 14:11:28 -04:00
< PersistedElement
zIndex = { this . props . miniMode ? zIndexAboveOtherPersistentElements : 9 }
persistKey = { this . persistKey }
moveRef = { this . props . movePersistedElement }
>
2021-07-19 22:43:11 +01:00
{ appTileBody }
2020-11-26 15:09:08 -07:00
</ PersistedElement >
</ div >
);
}
2017-07-12 14:16:47 +01:00
}
2017-07-06 09:28:48 +01:00
}
2017-07-14 11:17:59 +01:00
2020-04-20 23:05:50 +03:00
let appTileClasses ;
2018-07-23 15:08:17 +01:00
if ( this . props . miniMode ) {
2021-06-29 13:11:58 +01:00
appTileClasses = { mx_AppTile_mini : true };
2018-07-23 15:08:17 +01:00
} else if ( this . props . fullWidth ) {
2021-06-29 13:11:58 +01:00
appTileClasses = { mx_AppTileFullWidth : true };
2018-07-23 15:08:17 +01:00
} else {
2021-06-29 13:11:58 +01:00
appTileClasses = { mx_AppTile : true };
2018-07-23 15:08:17 +01:00
}
2020-04-20 23:05:50 +03:00
appTileClasses = classNames ( appTileClasses );
2018-07-23 15:08:17 +01:00
2019-11-28 18:16:59 +00:00
let contextMenu ;
if ( this . state . menuDisplayed ) {
contextMenu = (
2022-01-27 17:08:09 +00:00
< WidgetContextMenu
2023-03-08 11:48:58 +00:00
{ ...aboveLeftOf ( this.contextMenuButton.current.getBoundingClientRect ()) }
2020-10-09 08:42:21 +01:00
app = { this . props . app }
2021-09-16 20:05:57 +02:00
onFinished = { this . closeContextMenu }
2020-10-09 08:42:21 +01:00
showUnpin = { ! this . props . userWidget }
2020-10-12 09:51:49 +01:00
userWidget = { this . props . userWidget }
2021-06-07 09:22:47 +01:00
onEditClick = { this . props . onEditClick }
onDeleteClick = { this . props . onDeleteClick }
2020-10-09 08:42:21 +01:00
/>
2019-11-28 18:16:59 +00:00
);
}
2022-01-24 10:24:30 -05:00
2022-09-12 11:58:05 +01:00
const layoutButtons : ReactNode [] = [];
2022-02-09 10:47:41 +00:00
if ( this . props . showLayoutButtons ) {
2023-03-29 08:23:54 +01:00
const isMaximised =
this . props . room &&
WidgetLayoutStore . instance . isInContainer ( this . props . room , this . props . app , Container . Center );
2023-05-22 07:48:40 +00:00
2022-02-09 10:47:41 +00:00
layoutButtons . push (
< AccessibleButton
2022-02-17 16:30:36 +00:00
key = "toggleMaximised"
2023-05-22 07:48:40 +00:00
className = "mx_AppTileMenuBar_widgets_button"
2023-09-25 18:12:41 +01:00
title = { isMaximised ? _t ( "widget|unmaximise" ) : _t ( "action|maximise" )}
2022-02-09 10:47:41 +00:00
onClick = { this . onToggleMaximisedClick }
2023-05-22 07:48:40 +00:00
>
{ isMaximised ? (
< CollapseIcon className = "mx_Icon mx_Icon_12" />
) : (
2024-10-21 11:36:31 +01:00
< ExpandIcon className = "mx_Icon mx_Icon_12" />
2023-05-22 07:48:40 +00:00
)}
</ AccessibleButton >,
2022-02-09 10:47:41 +00:00
);
layoutButtons . push (
< AccessibleButton
2022-06-03 12:00:16 +02:00
key = "minimise"
2023-05-22 07:48:40 +00:00
className = "mx_AppTileMenuBar_widgets_button"
2023-09-01 08:26:48 +01:00
title = { _t ( "action|minimise" )}
2022-06-03 12:00:16 +02:00
onClick = { this . onMinimiseClicked }
2023-05-22 07:48:40 +00:00
>
2024-10-21 11:36:31 +01:00
< MinusIcon className = "mx_Icon mx_Icon_16" />
2023-05-22 07:48:40 +00:00
</ AccessibleButton >,
2022-02-09 10:47:41 +00:00
);
2021-11-16 15:43:18 +01:00
}
2019-11-28 18:16:59 +00:00
return (
< React.Fragment >
2020-04-20 23:05:50 +03:00
< div className = { appTileClasses } id = { this . props . app . id }>
2018-01-11 13:20:58 +00:00
{ this . props . showMenubar && (
2021-08-19 06:42:10 +02:00
< div className = "mx_AppTileMenuBar" >
2021-09-16 20:05:57 +02:00
< span
2023-05-09 21:07:12 +00:00
className = "mx_AppTileMenuBar_title"
2021-09-16 20:05:57 +02:00
style = {{ pointerEvents : this.props.handleMinimisePointerEvents ? "all" : "none" }}
>
{ this . props . showTitle && this . getTileTitle ()}
2021-08-19 06:42:10 +02:00
</ span >
2023-05-09 21:07:12 +00:00
< span className = "mx_AppTileMenuBar_widgets" >
2022-02-09 10:47:41 +00:00
{ layoutButtons }
2021-10-28 14:17:04 +02:00
{ this . props . showPopout && ! this . state . requiresClient && (
< AccessibleButton
2023-05-22 07:48:40 +00:00
className = "mx_AppTileMenuBar_widgets_button"
2023-09-25 18:12:41 +01:00
title = { _t ( "widget|popout" )}
2021-09-16 20:05:57 +02:00
onClick = { this . onPopoutWidgetClick }
2023-05-22 07:48:40 +00:00
>
< PopoutIcon className = "mx_Icon mx_Icon_12 mx_Icon--stroke" />
</ AccessibleButton >
2021-08-19 06:42:10 +02:00
)}
2023-07-17 11:37:07 +01:00
{ this . state . hasContextMenuOptions && (
< ContextMenuButton
className = "mx_AppTileMenuBar_widgets_button"
2023-08-23 10:25:33 +01:00
label = { _t ( "common|options" )}
2023-07-17 11:37:07 +01:00
isExpanded = { this . state . menuDisplayed }
2023-12-21 08:50:42 +00:00
ref = { this . contextMenuButton }
2023-07-17 11:37:07 +01:00
onClick = { this . onContextMenuClick }
>
2024-10-21 11:36:31 +01:00
< OverflowHorizontalIcon className = "mx_Icon mx_Icon_12" />
2023-07-17 11:37:07 +01:00
</ ContextMenuButton >
)}
2021-08-19 06:42:10 +02:00
</ span >
</ div >
)}
2017-09-28 11:21:06 +01:00
{ appTileBody }
2017-05-22 12:34:27 +01:00
</ div >
2019-11-28 18:16:59 +00:00
{ contextMenu }
</ React.Fragment >
);
2018-01-11 11:49:46 +00:00
}
}