2019-08-15 01:15:49 +03:00
/ *
2017-06-28 14:23:33 +03:00
Copyright 2017 Vector Creations Ltd
2018-07-11 20:07:32 +03:00
Copyright 2018 New Vector Ltd
2019-08-15 01:15:49 +03:00
Copyright 2019 Michael Telatynski < 7 t3chguy @ gmail . com >
2020-04-01 12:00:33 +03:00
Copyright 2020 The Matrix . org Foundation C . I . C .
2017-05-22 14:34:27 +03:00
Licensed under the Apache License , Version 2.0 ( the "License" ) ;
you may not use this file except in compliance with the License .
You may obtain a copy of the License at
http : //www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing , software
distributed under the License is distributed on an "AS IS" BASIS ,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
See the License for the specific language governing permissions and
limitations under the License .
* /
2017-07-12 16:16:47 +03:00
import url from 'url' ;
2020-01-18 05:01:45 +03:00
import qs from 'qs' ;
2019-11-28 21:16:59 +03:00
import React , { createRef } from 'react' ;
2017-12-26 04:03:18 +03:00
import PropTypes from 'prop-types' ;
2019-12-21 00:13:46 +03:00
import { MatrixClientPeg } from '../../../MatrixClientPeg' ;
2017-11-30 01:16:22 +03:00
import WidgetMessaging from '../../../WidgetMessaging' ;
2019-02-01 13:02:02 +03:00
import AccessibleButton from './AccessibleButton' ;
2017-07-14 13:17:59 +03:00
import Modal from '../../../Modal' ;
2019-02-01 13:02:02 +03:00
import { _t } from '../../../languageHandler' ;
2019-12-20 04:19:56 +03:00
import * as sdk from '../../../index' ;
2017-07-26 13:28:43 +03:00
import AppPermission from './AppPermission' ;
2017-08-01 19:29:29 +03:00
import AppWarning from './AppWarning' ;
2017-07-27 18:41:52 +03:00
import MessageSpinner from './MessageSpinner' ;
2018-06-26 13:59:16 +03:00
import WidgetUtils from '../../../utils/WidgetUtils' ;
2020-05-14 05:41:41 +03:00
import dis from '../../../dispatcher/dispatcher' ;
2018-07-11 20:07:32 +03:00
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore' ;
2019-02-01 13:02:02 +03:00
import classNames from 'classnames' ;
2019-08-10 01:05:05 +03:00
import { IntegrationManagers } from "../../../integrations/IntegrationManagers" ;
2019-11-19 03:56:33 +03:00
import SettingsStore , { SettingLevel } from "../../../settings/SettingsStore" ;
2019-12-03 02:23:11 +03:00
import { aboveLeftOf , ContextMenu , ContextMenuButton } from "../../structures/ContextMenu" ;
2019-11-27 22:54:31 +03:00
import PersistedElement from "./PersistedElement" ;
2020-04-10 00:11:57 +03:00
import { WidgetType } from "../../../widgets/WidgetType" ;
2020-04-28 02:18:43 +03:00
import { Capability } from "../../../widgets/WidgetApi" ;
2020-04-18 16:57:19 +03:00
import { sleep } from "../../../utils/promise" ;
2017-05-22 14:34:27 +03:00
2017-07-12 16:16:47 +03:00
const ALLOWED _APP _URL _SCHEMES = [ 'https:' , 'http:' ] ;
2018-03-16 13:20:14 +03:00
const ENABLE _REACT _PERF = false ;
2017-07-06 11:28:48 +03:00
2020-04-01 12:00:33 +03:00
/ * *
* Does template substitution on a URL ( or any string ) . Variables will be
* passed through encodeURIComponent .
* @ param { string } uriTemplate The path with template variables e . g . '/foo/$bar' .
* @ param { Object } variables The key / value pairs to replace the template
* variables with . E . g . { '$bar' : 'baz' } .
* @ return { string } The result of replacing all template variables e . g . '/foo/baz' .
* /
function uriFromTemplate ( uriTemplate , variables ) {
let out = uriTemplate ;
for ( const [ key , val ] of Object . entries ( variables ) ) {
out = out . replace (
'$' + key , encodeURIComponent ( val ) ,
) ;
}
return out ;
}
2018-01-11 14:49:46 +03:00
export default class AppTile extends React . Component {
2018-01-11 15:33:02 +03:00
constructor ( props ) {
super ( props ) ;
2018-07-11 20:07:32 +03:00
// The key used for PersistedElement
2020-04-01 12:00:33 +03:00
this . _persistKey = 'widget_' + this . props . app . id ;
2018-07-11 20:07:32 +03:00
2018-01-11 16:20:58 +03:00
this . state = this . _getNewState ( props ) ;
2018-01-11 15:33:02 +03:00
2018-07-11 20:07:32 +03:00
this . _onAction = this . _onAction . bind ( this ) ;
2018-01-11 15:33:02 +03:00
this . _onLoaded = this . _onLoaded . bind ( this ) ;
this . _onEditClick = this . _onEditClick . bind ( this ) ;
this . _onDeleteClick = this . _onDeleteClick . bind ( this ) ;
2019-11-21 05:17:42 +03:00
this . _onRevokeClicked = this . _onRevokeClicked . bind ( this ) ;
2018-01-11 15:33:02 +03:00
this . _onSnapshotClick = this . _onSnapshotClick . bind ( this ) ;
this . onClickMenuBar = this . onClickMenuBar . bind ( this ) ;
2018-03-08 20:20:42 +03:00
this . _onMinimiseClick = this . _onMinimiseClick . bind ( this ) ;
2018-04-05 01:45:47 +03:00
this . _grantWidgetPermission = this . _grantWidgetPermission . bind ( this ) ;
this . _revokeWidgetPermission = this . _revokeWidgetPermission . bind ( this ) ;
2018-04-25 14:49:30 +03:00
this . _onPopoutWidgetClick = this . _onPopoutWidgetClick . bind ( this ) ;
2018-05-22 21:14:54 +03:00
this . _onReloadWidgetClick = this . _onReloadWidgetClick . bind ( this ) ;
2019-11-28 21:16:59 +03:00
this . _contextMenuButton = createRef ( ) ;
2019-12-08 15:16:17 +03:00
this . _appFrame = createRef ( ) ;
this . _menu _bar = createRef ( ) ;
2018-01-11 15:33:02 +03:00
}
2017-10-31 19:31:46 +03:00
/ * *
2017-11-10 14:50:14 +03: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 20:44:54 +03:00
* @ param { Object } newProps The new properties of the component
2017-10-31 19:31:46 +03:00
* @ return { Object } Updated component state to be set with setState
* /
2017-11-10 13:17:55 +03:00
_getNewState ( newProps ) {
2019-11-19 03:56:33 +03:00
// This is a function to make the impact of calling SettingsStore slightly less
const hasPermissionToLoad = ( ) => {
const currentlyAllowedWidgets = SettingsStore . getValue ( "allowedWidgets" , newProps . room . roomId ) ;
2020-04-01 12:00:33 +03:00
return ! ! currentlyAllowedWidgets [ newProps . app . eventId ] ;
2019-11-19 03:56:33 +03:00
} ;
2018-07-11 20:07:32 +03:00
const PersistedElement = sdk . getComponent ( "elements.PersistedElement" ) ;
2017-11-02 21:33:11 +03:00
return {
2017-12-29 01:27:12 +03:00
initialising : true , // True while we are mangling the widget URL
2018-07-11 20:07:32 +03:00
// True while the iframe content is loading
loading : this . props . waitForIframeLoad && ! PersistedElement . isMounted ( this . _persistKey ) ,
2020-04-01 12:00:33 +03:00
widgetUrl : this . _addWurlParams ( newProps . app . url ) ,
2017-11-10 14:50:14 +03: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
2019-11-19 03:56:33 +03:00
hasPermissionToLoad : newProps . userId === newProps . creatorUserId || hasPermissionToLoad ( ) ,
2017-11-02 21:33:11 +03:00
error : null ,
deleting : false ,
2017-12-08 18:12:48 +03:00
widgetPageTitle : newProps . widgetPageTitle ,
2019-11-28 21:16:59 +03:00
menuDisplayed : false ,
2017-11-02 21:33:11 +03:00
} ;
2018-01-11 14:49:46 +03:00
}
2017-10-27 19:49:14 +03:00
2017-12-16 12:16:24 +03:00
/ * *
* Does the widget support a given capability
2018-05-12 23:29:37 +03:00
* @ param { string } capability Capability to check for
2017-12-16 12:16:24 +03:00
* @ return { Boolean } True if capability supported
* /
_hasCapability ( capability ) {
2020-04-01 12:00:33 +03:00
return ActiveWidgetStore . widgetHasCapability ( this . props . app . id , capability ) ;
2018-01-11 14:49:46 +03:00
}
2017-12-16 12:16:24 +03:00
2017-11-30 01:16:22 +03:00
/ * *
* Add widget instance specific parameters to pass in wUrl
* Properties passed to widget instance :
* - widgetId
* - origin / parent URL
* @ param { string } urlString Url string to modify
* @ return { string }
* Url string with parameters appended .
* If url can not be parsed , it is returned unmodified .
* /
_addWurlParams ( urlString ) {
2020-04-08 21:58:52 +03:00
try {
const parsed = new URL ( urlString ) ;
// TODO: Replace these with proper widget params
// See https://github.com/matrix-org/matrix-doc/pull/1958/files#r405714833
parsed . searchParams . set ( 'widgetId' , this . props . app . id ) ;
parsed . searchParams . set ( 'parentUrl' , window . location . href . split ( '#' , 2 ) [ 0 ] ) ;
// Replace the encoded dollar signs back to dollar signs. They have no special meaning
// in HTTP, but URL parsers encode them anyways.
return parsed . toString ( ) . replace ( /%24/g , '$' ) ;
} catch ( e ) {
console . error ( "Failed to add widget URL params:" , e ) ;
return urlString ;
2017-11-30 01:16:22 +03:00
}
2018-01-11 14:49:46 +03:00
}
2017-11-30 01:16:22 +03:00
2017-10-27 19:49:14 +03:00
isMixedContent ( ) {
2017-08-01 19:29:29 +03:00
const parentContentProtocol = window . location . protocol ;
2020-04-01 12:00:33 +03:00
const u = url . parse ( this . props . app . url ) ;
2017-08-01 19:29:29 +03:00
const childContentProtocol = u . protocol ;
2017-08-01 19:49:41 +03:00
if ( parentContentProtocol === 'https:' && childContentProtocol !== 'https:' ) {
2017-08-02 19:05:46 +03:00
console . warn ( "Refusing to load mixed-content app:" ,
2020-04-01 12:00:33 +03:00
parentContentProtocol , childContentProtocol , window . location , this . props . app . url ) ;
2017-08-01 19:29:29 +03:00
return true ;
}
return false ;
2018-01-11 14:49:46 +03:00
}
2017-08-01 19:29:29 +03:00
2020-03-31 23:14:17 +03:00
componentDidMount ( ) {
2019-07-19 21:04:48 +03:00
// Only fetch IM token on mount if we're showing and have permission to load
if ( this . props . show && this . state . hasPermissionToLoad ) {
this . setScalarToken ( ) ;
}
2017-10-27 15:47:51 +03:00
2018-01-18 15:02:45 +03:00
// Widget action listeners
2018-07-11 20:07:32 +03:00
this . dispatcherRef = dis . register ( this . _onAction ) ;
2018-01-11 14:49:46 +03:00
}
2017-12-15 19:56:02 +03:00
2018-01-18 15:02:45 +03:00
componentWillUnmount ( ) {
// Widget action listeners
2020-03-31 23:05:56 +03:00
if ( this . dispatcherRef ) dis . unregister ( this . dispatcherRef ) ;
2018-01-18 15:02:45 +03:00
2018-07-11 20:07:32 +03:00
// if it's not remaining on screen, get rid of the PersistedElement container
2020-04-01 12:00:33 +03:00
if ( ! ActiveWidgetStore . getWidgetPersistence ( this . props . app . id ) ) {
ActiveWidgetStore . destroyPersistentWidget ( this . props . app . id ) ;
2018-08-01 17:01:11 +03:00
const PersistedElement = sdk . getComponent ( "elements.PersistedElement" ) ;
PersistedElement . destroyElement ( this . _persistKey ) ;
2018-07-11 20:07:32 +03:00
}
2018-01-18 15:02:45 +03:00
}
2020-04-22 09:34:08 +03:00
// TODO: Generify the name of this function. It's not just scalar tokens.
2017-11-02 21:33:11 +03:00
/ * *
* Adds a scalar token to the widget URL , if required
* Component initialisation is only complete when this function has resolved
* /
2017-10-27 15:47:51 +03:00
setScalarToken ( ) {
2020-04-01 12:00:33 +03:00
if ( ! WidgetUtils . isScalarUrl ( this . props . app . url ) ) {
2020-04-22 09:34:08 +03:00
console . warn ( 'Widget does not match integration manager, refusing to set auth token' , url ) ;
2017-11-02 21:33:11 +03:00
this . setState ( {
error : null ,
2020-04-01 12:00:33 +03:00
widgetUrl : this . _addWurlParams ( this . props . app . url ) ,
2017-11-02 21:33:11 +03:00
initialising : false ,
} ) ;
2017-07-06 11:28:48 +03:00
return ;
}
2017-11-02 21:33:11 +03:00
2019-08-10 01:05:05 +03:00
const managers = IntegrationManagers . sharedInstance ( ) ;
if ( ! managers . hasManager ( ) ) {
console . warn ( "No integration manager - not setting scalar token" , url ) ;
this . setState ( {
error : null ,
2020-04-01 12:00:33 +03:00
widgetUrl : this . _addWurlParams ( this . props . app . url ) ,
2019-08-10 01:05:05 +03:00
initialising : false ,
} ) ;
return ;
}
// TODO: Pick the right manager for the widget
2019-10-29 20:49:15 +03:00
const defaultManager = managers . getPrimaryManager ( ) ;
if ( ! WidgetUtils . isScalarUrl ( defaultManager . apiUrl ) ) {
2020-04-22 09:34:08 +03:00
console . warn ( 'Unknown integration manager, refusing to set auth token' , url ) ;
2019-10-29 20:49:15 +03:00
this . setState ( {
error : null ,
2020-04-01 12:00:33 +03:00
widgetUrl : this . _addWurlParams ( this . props . app . url ) ,
2019-10-29 20:49:15 +03:00
initialising : false ,
} ) ;
return ;
}
2017-11-02 21:33:11 +03:00
// Fetch the token before loading the iframe as we need it to mangle the URL
2017-10-31 13:04:37 +03:00
if ( ! this . _scalarClient ) {
2019-10-29 20:49:15 +03:00
this . _scalarClient = defaultManager . getScalarClient ( ) ;
2017-10-27 15:47:51 +03:00
}
2019-11-18 13:03:05 +03:00
this . _scalarClient . getScalarToken ( ) . then ( ( token ) => {
2017-10-27 19:49:14 +03:00
// Append scalar_token as a query param if not already present
2017-08-01 17:53:42 +03:00
this . _scalarClient . scalarToken = token ;
2020-04-01 12:00:33 +03:00
const u = url . parse ( this . _addWurlParams ( this . props . app . url ) ) ;
2017-11-07 15:33:38 +03:00
const params = qs . parse ( u . query ) ;
2017-10-31 13:37:40 +03:00
if ( ! params . scalar _token ) {
params . scalar _token = encodeURIComponent ( token ) ;
2019-08-10 01:05:05 +03:00
// u.search must be set to undefined, so that u.format() uses query parameters - https://nodejs.org/docs/latest/api/url.html#url_url_format_url_options
2017-11-09 17:07:29 +03:00
u . search = undefined ;
u . query = params ;
2017-07-06 11:28:48 +03:00
}
this . setState ( {
error : null ,
widgetUrl : u . format ( ) ,
2017-11-02 21:33:11 +03:00
initialising : false ,
2017-07-06 11:28:48 +03:00
} ) ;
2017-12-06 00:41:44 +03:00
2017-12-08 18:12:48 +03:00
// Fetch page title from remote content if not already set
2017-12-08 21:47:00 +03:00
if ( ! this . state . widgetPageTitle && params . url ) {
2017-12-08 18:12:48 +03:00
this . _fetchWidgetTitle ( params . url ) ;
}
2017-07-06 11:28:48 +03:00
} , ( err ) => {
2017-11-09 17:07:29 +03:00
console . error ( "Failed to get scalar_token" , err ) ;
2017-07-06 11:28:48 +03:00
this . setState ( {
error : err . message ,
2017-11-02 21:33:11 +03:00
initialising : false ,
2017-07-06 11:28:48 +03:00
} ) ;
} ) ;
2018-01-11 14:49:46 +03:00
}
2017-09-04 10:31:25 +03:00
2020-04-01 23:35:39 +03:00
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
2020-04-01 23:45:54 +03:00
UNSAFE _componentWillReceiveProps ( nextProps ) { // eslint-disable-line camelcase
2020-04-01 12:00:33 +03:00
if ( nextProps . app . url !== this . props . app . url ) {
2017-11-10 13:17:55 +03:00
this . _getNewState ( nextProps ) ;
2019-07-19 21:04:48 +03:00
// Fetch IM token for new URL if we're showing and have permission to load
if ( this . props . show && this . state . hasPermissionToLoad ) {
this . setScalarToken ( ) ;
}
2020-04-09 23:31:46 +03:00
}
if ( nextProps . show && ! this . props . show ) {
2019-11-27 22:54:31 +03:00
// We assume that persisted widgets are loaded and don't need a spinner.
if ( this . props . waitForIframeLoad && ! PersistedElement . isMounted ( this . _persistKey ) ) {
2019-07-19 21:04:48 +03:00
this . setState ( {
loading : true ,
} ) ;
}
// Fetch IM token now that we're showing if we already have permission to load
if ( this . state . hasPermissionToLoad ) {
this . setScalarToken ( ) ;
}
2020-04-09 23:31:46 +03:00
}
if ( nextProps . widgetPageTitle !== this . props . widgetPageTitle ) {
2017-12-13 13:14:26 +03:00
this . setState ( {
widgetPageTitle : nextProps . widgetPageTitle ,
} ) ;
2017-10-27 19:49:14 +03:00
}
2018-01-11 14:49:46 +03:00
}
2017-10-27 15:47:51 +03:00
2017-10-27 19:49:14 +03:00
_canUserModify ( ) {
2018-05-09 00:44:49 +03:00
// User widgets should always be modifiable by their creator
if ( this . props . userWidget && MatrixClientPeg . get ( ) . credentials . userId === this . props . creatorUserId ) {
return true ;
}
// Check if the current user can modify widgets in the current room
2017-08-01 13:39:17 +03:00
return WidgetUtils . canUserModifyWidgets ( this . props . room . roomId ) ;
2018-01-11 14:49:46 +03:00
}
2017-07-27 20:10:28 +03:00
2019-11-21 05:17:42 +03:00
_onEditClick ( ) {
2020-04-01 12:00:33 +03:00
console . log ( "Edit widget ID " , this . props . app . id ) ;
2018-02-07 17:44:01 +03:00
if ( this . props . onEditClick ) {
this . props . onEditClick ( ) ;
} else {
2019-08-10 01:05:05 +03:00
// TODO: Open the right manager for the widget
2019-08-23 18:12:40 +03:00
if ( SettingsStore . isFeatureEnabled ( "feature_many_integration_managers" ) ) {
IntegrationManagers . sharedInstance ( ) . openAll (
this . props . room ,
'type_' + this . props . type ,
2020-04-01 12:00:33 +03:00
this . props . app . id ,
2019-08-23 18:12:40 +03:00
) ;
} else {
IntegrationManagers . sharedInstance ( ) . getPrimaryManager ( ) . open (
this . props . room ,
'type_' + this . props . type ,
2020-04-01 12:00:33 +03:00
this . props . app . id ,
2019-08-23 18:12:40 +03:00
) ;
}
2018-02-07 17:44:01 +03:00
}
2018-01-11 14:49:46 +03:00
}
2017-05-22 20:00:17 +03:00
2019-11-21 05:17:42 +03:00
_onSnapshotClick ( ) {
2020-04-01 22:59:48 +03:00
console . log ( "Requesting widget snapshot" ) ;
2020-04-01 12:00:33 +03:00
ActiveWidgetStore . getWidgetMessaging ( this . props . app . id ) . getScreenshot ( )
2018-03-28 14:22:06 +03:00
. catch ( ( err ) => {
console . error ( "Failed to get screenshot" , err ) ;
} )
. then ( ( screenshot ) => {
dis . dispatch ( {
action : 'picture_snapshot' ,
file : screenshot ,
} , true ) ;
} ) ;
2018-01-11 14:49:46 +03:00
}
2017-12-03 14:25:15 +03:00
2020-04-09 23:31:46 +03: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 23:31:46 +03:00
* /
_endWidgetActions ( ) {
2020-04-28 02:18:43 +03:00
let terminationPromise ;
2020-04-18 14:53:48 +03:00
2020-04-28 02:18:43 +03:00
if ( this . _hasCapability ( Capability . ReceiveTerminate ) ) {
2020-04-18 17:04:56 +03:00
// Wait for widget to terminate within a timeout
const timeout = 2000 ;
const messaging = ActiveWidgetStore . getWidgetMessaging ( this . props . app . id ) ;
2020-04-28 02:18:43 +03:00
terminationPromise = Promise . race ( [ messaging . terminate ( ) , sleep ( timeout ) ] ) ;
2020-04-18 17:04:56 +03:00
} else {
2020-04-28 02:18:43 +03:00
terminationPromise = Promise . resolve ( ) ;
2020-04-09 23:31:46 +03:00
}
2020-04-28 02:18:43 +03:00
return terminationPromise . finally ( ( ) => {
2020-04-18 14:53:48 +03: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/riot-web/issues/7351
if ( this . _appFrame . current ) {
// 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 Riot instance is located.
this . _appFrame . current . src = 'about:blank' ;
}
2020-04-09 23:31:46 +03:00
2020-04-18 14:53:48 +03:00
// Delete the widget from the persisted store for good measure.
PersistedElement . destroyElement ( this . _persistKey ) ;
} ) ;
2020-04-09 23:31:46 +03:00
}
2017-11-10 14:50:14 +03:00
/ * I f u s e r h a s p e r m i s s i o n t o m o d i f y w i d g e t s , d e l e t e t h e w i d g e t ,
* otherwise revoke access for the widget to load in the user ' s browser
2017-07-27 20:10:28 +03:00
* /
2017-10-27 19:49:14 +03:00
_onDeleteClick ( ) {
2018-02-07 17:44:01 +03:00
if ( this . props . onDeleteClick ) {
this . props . onDeleteClick ( ) ;
2019-02-01 13:02:02 +03:00
} else if ( this . _canUserModify ( ) ) {
// Show delete confirmation dialog
const QuestionDialog = sdk . getComponent ( "dialogs.QuestionDialog" ) ;
Modal . createTrackedDialog ( 'Delete Widget' , '' , QuestionDialog , {
title : _t ( "Delete Widget" ) ,
description : _t (
"Deleting a widget removes it for all users in this room." +
" Are you sure you want to delete this widget?" ) ,
button : _t ( "Delete widget" ) ,
onFinished : ( confirmed ) => {
if ( ! confirmed ) {
return ;
}
this . setState ( { deleting : true } ) ;
2020-04-18 14:53:48 +03:00
this . _endWidgetActions ( ) . then ( ( ) => {
2020-04-22 19:20:28 +03:00
return WidgetUtils . setRoomWidget (
2020-04-18 14:53:48 +03:00
this . props . room . roomId ,
this . props . app . id ,
2020-04-22 19:20:28 +03:00
) ;
} ) . catch ( ( e ) => {
2019-02-01 13:02:02 +03:00
console . error ( 'Failed to delete widget' , e ) ;
const ErrorDialog = sdk . getComponent ( "dialogs.ErrorDialog" ) ;
Modal . createTrackedDialog ( 'Failed to remove widget' , '' , ErrorDialog , {
title : _t ( 'Failed to remove widget' ) ,
description : _t ( 'An error ocurred whilst trying to remove the widget from the room' ) ,
2018-02-07 17:44:01 +03:00
} ) ;
2019-02-01 13:02:02 +03:00
} ) . finally ( ( ) => {
this . setState ( { deleting : false } ) ;
} ) ;
} ,
} ) ;
}
}
2019-11-21 05:17:42 +03:00
_onRevokeClicked ( ) {
2020-04-01 12:00:33 +03:00
console . info ( "Revoke widget permissions - %s" , this . props . app . id ) ;
2019-11-21 05:17:42 +03:00
this . _revokeWidgetPermission ( ) ;
2018-01-11 14:49:46 +03:00
}
2017-07-27 20:10:28 +03:00
2017-11-30 01:16:22 +03:00
/ * *
* Called when widget iframe has finished loading
* /
2017-10-27 19:49:14 +03:00
_onLoaded ( ) {
2019-03-16 07:24:27 +03:00
// Destroy the old widget messaging before starting it back up again. Some widgets
// have startup routines that run when they are loaded, so we just need to reinitialize
// the messaging for them.
2020-04-01 12:00:33 +03:00
ActiveWidgetStore . delWidgetMessaging ( this . props . app . id ) ;
2019-03-16 07:24:27 +03:00
this . _setupWidgetMessaging ( ) ;
2020-04-01 12:00:33 +03:00
ActiveWidgetStore . setRoomId ( this . props . app . id , this . props . room . roomId ) ;
2018-05-14 13:42:38 +03:00
this . setState ( { loading : false } ) ;
2018-03-28 14:22:06 +03:00
}
2018-07-11 20:07:32 +03:00
_setupWidgetMessaging ( ) {
// FIXME: There's probably no reason to do this here: it should probably be done entirely
// in ActiveWidgetStore.
2019-03-24 08:31:19 +03:00
const widgetMessaging = new WidgetMessaging (
2020-05-13 18:10:40 +03:00
this . props . app . id ,
this . props . app . url ,
this . _getRenderedUrl ( ) ,
this . props . userWidget ,
this . _appFrame . current . contentWindow ,
) ;
2020-04-01 12:00:33 +03:00
ActiveWidgetStore . setWidgetMessaging ( this . props . app . id , widgetMessaging ) ;
2018-07-11 20:07:32 +03:00
widgetMessaging . getCapabilities ( ) . then ( ( requestedCapabilities ) => {
2020-04-01 12:00:33 +03:00
console . log ( ` Widget ${ this . props . app . id } requested capabilities: ` + requestedCapabilities ) ;
2018-03-12 16:56:02 +03:00
requestedCapabilities = requestedCapabilities || [ ] ;
// Allow whitelisted capabilities
2018-03-13 14:59:15 +03:00
let requestedWhitelistCapabilies = [ ] ;
if ( this . props . whitelistCapabilities && this . props . whitelistCapabilities . length > 0 ) {
requestedWhitelistCapabilies = requestedCapabilities . filter ( function ( e ) {
2018-03-12 16:56:02 +03:00
return this . indexOf ( e ) >= 0 ;
} , this . props . whitelistCapabilities ) ;
2018-03-13 14:59:15 +03:00
if ( requestedWhitelistCapabilies . length > 0 ) {
2020-04-01 22:59:48 +03:00
console . log ( ` Widget ${ this . props . app . id } allowing requested, whitelisted properties: ` +
2018-07-11 20:07:32 +03:00
requestedWhitelistCapabilies ,
) ;
2018-03-13 14:59:15 +03:00
}
}
2018-03-12 16:56:02 +03:00
// TODO -- Add UI to warn about and optionally allow requested capabilities
2018-07-11 20:07:32 +03:00
2020-04-01 12:00:33 +03:00
ActiveWidgetStore . setWidgetCapabilities ( this . props . app . id , requestedWhitelistCapabilies ) ;
2018-03-12 16:56:02 +03:00
if ( this . props . onCapabilityRequest ) {
this . props . onCapabilityRequest ( requestedCapabilities ) ;
}
2020-03-24 18:55:54 +03:00
// We only tell Jitsi widgets that we're ready because they're realistically the only ones
// using this custom extension to the widget API.
2020-04-10 00:11:57 +03:00
if ( WidgetType . JITSI . matches ( this . props . app . type ) ) {
2020-03-24 18:55:54 +03:00
widgetMessaging . flagReadyToContinue ( ) ;
}
2017-12-16 12:16:24 +03:00
} ) . catch ( ( err ) => {
2020-04-01 12:00:33 +03:00
console . log ( ` Failed to get capabilities for widget type ${ this . props . app . type } ` , this . props . app . id , err ) ;
2017-12-16 12:16:24 +03:00
} ) ;
2018-01-11 14:49:46 +03:00
}
2017-10-27 19:49:14 +03:00
2018-07-11 20:07:32 +03:00
_onAction ( payload ) {
2020-04-01 12:00:33 +03:00
if ( payload . widgetId === this . props . app . id ) {
2018-01-04 21:58:55 +03:00
switch ( payload . action ) {
2018-03-12 16:56:02 +03:00
case 'm.sticker' :
if ( this . _hasCapability ( 'm.sticker' ) ) {
2018-01-04 21:58:55 +03:00
dis . dispatch ( { action : 'post_sticker_message' , data : payload . data } ) ;
} else {
console . warn ( 'Ignoring sticker message. Invalid capability' ) ;
}
break ;
}
}
2018-01-11 14:49:46 +03:00
}
2018-01-04 21:41:47 +03:00
2017-11-30 01:16:22 +03:00
/ * *
2017-11-30 17:50:30 +03:00
* Set remote content title on AppTile
2017-12-06 00:41:44 +03:00
* @ param { string } url Url to check for title
2017-11-30 01:16:22 +03:00
* /
2017-12-08 18:12:48 +03:00
_fetchWidgetTitle ( url ) {
2017-12-06 00:41:44 +03:00
this . _scalarClient . getScalarPageTitle ( url ) . then ( ( widgetPageTitle ) => {
if ( widgetPageTitle ) {
this . setState ( { widgetPageTitle : widgetPageTitle } ) ;
}
} , ( err ) => {
console . error ( "Failed to get page title" , err ) ;
} ) ;
2018-01-11 14:49:46 +03:00
}
2017-10-27 19:49:14 +03:00
2017-07-26 13:28:43 +03:00
_grantWidgetPermission ( ) {
2019-11-19 03:56:33 +03:00
const roomId = this . props . room . roomId ;
2020-04-01 12:00:33 +03:00
console . info ( "Granting permission for widget to load: " + this . props . app . eventId ) ;
2019-11-19 03:56:33 +03:00
const current = SettingsStore . getValue ( "allowedWidgets" , roomId ) ;
2020-04-01 12:00:33 +03:00
current [ this . props . app . eventId ] = true ;
2019-11-19 03:56:33 +03:00
SettingsStore . setValue ( "allowedWidgets" , roomId , SettingLevel . ROOM _ACCOUNT , current ) . then ( ( ) => {
this . setState ( { hasPermissionToLoad : true } ) ;
// Fetch a token for the integration manager, now that we're allowed to
this . setScalarToken ( ) ;
} ) . catch ( err => {
console . error ( err ) ;
// We don't really need to do anything about this - the user will just hit the button again.
} ) ;
2018-01-11 14:49:46 +03:00
}
2017-07-26 13:28:43 +03:00
2017-07-27 20:10:28 +03:00
_revokeWidgetPermission ( ) {
2019-11-19 03:56:33 +03:00
const roomId = this . props . room . roomId ;
2020-04-01 12:00:33 +03:00
console . info ( "Revoking permission for widget to load: " + this . props . app . eventId ) ;
2019-11-19 03:56:33 +03:00
const current = SettingsStore . getValue ( "allowedWidgets" , roomId ) ;
2020-04-01 12:00:33 +03:00
current [ this . props . app . eventId ] = false ;
2019-11-19 03:56:33 +03:00
SettingsStore . setValue ( "allowedWidgets" , roomId , SettingLevel . ROOM _ACCOUNT , current ) . then ( ( ) => {
this . setState ( { hasPermissionToLoad : false } ) ;
// Force the widget to be non-persistent (able to be deleted/forgotten)
2020-04-01 12:00:33 +03:00
ActiveWidgetStore . destroyPersistentWidget ( this . props . app . id ) ;
2019-11-19 03:56:33 +03:00
const PersistedElement = sdk . getComponent ( "elements.PersistedElement" ) ;
PersistedElement . destroyElement ( this . _persistKey ) ;
} ) . catch ( err => {
console . error ( err ) ;
// We don't really need to do anything about this - the user will just hit the button again.
} ) ;
2018-01-11 14:49:46 +03:00
}
2017-05-22 20:00:17 +03:00
2017-10-27 19:49:14 +03:00
formatAppTileName ( ) {
2017-06-28 12:27:06 +03:00
let appTileName = "No name" ;
2020-04-01 12:00:33 +03:00
if ( this . props . app . name && this . props . app . name . trim ( ) ) {
appTileName = this . props . app . name . trim ( ) ;
2017-06-28 12:27:06 +03:00
}
return appTileName ;
2018-01-11 14:49:46 +03:00
}
2017-06-28 12:27:06 +03:00
2017-10-27 19:49:14 +03:00
onClickMenuBar ( ev ) {
2017-08-16 20:19:12 +03:00
ev . preventDefault ( ) ;
// Ignore clicks on menu bar children
2019-12-08 15:16:17 +03:00
if ( ev . target !== this . _menu _bar . current ) {
2017-08-16 20:19:12 +03:00
return ;
}
// Toggle the view state of the apps drawer
2019-03-24 08:50:06 +03:00
if ( this . props . userWidget ) {
this . _onMinimiseClick ( ) ;
} else {
2020-04-09 23:31:46 +03:00
if ( this . props . show ) {
// if we were being shown, end the widget as we're about to be minimized.
this . _endWidgetActions ( ) ;
}
2019-03-24 08:50:06 +03:00
dis . dispatch ( {
action : 'appsDrawer' ,
show : ! this . props . show ,
} ) ;
}
2018-01-11 14:49:46 +03:00
}
2017-08-16 20:19:12 +03:00
2020-04-01 12:00:33 +03:00
/ * *
* Replace the widget template variables in a url with their values
*
2020-04-01 13:28:02 +03:00
* @ param { string } u The URL with template variables
2020-04-24 08:25:53 +03:00
* @ param { string } widgetType The widget ' s type
2020-04-01 13:28:02 +03:00
*
2020-04-01 12:00:33 +03:00
* @ returns { string } url with temlate variables replaced
* /
2020-04-24 00:24:20 +03:00
_templatedUrl ( u , widgetType : string ) {
const targetData = { } ;
if ( WidgetType . JITSI . matches ( widgetType ) ) {
targetData [ 'domain' ] = 'jitsi.riot.im' ; // v1 jitsi widgets have this hardcoded
}
2020-04-01 12:00:33 +03:00
const myUserId = MatrixClientPeg . get ( ) . credentials . userId ;
const myUser = MatrixClientPeg . get ( ) . getUser ( myUserId ) ;
2020-04-24 00:24:20 +03:00
const vars = Object . assign ( targetData , this . props . app . data , {
2020-04-01 12:00:33 +03:00
'matrix_user_id' : myUserId ,
'matrix_room_id' : this . props . room . roomId ,
'matrix_display_name' : myUser ? myUser . displayName : myUserId ,
'matrix_avatar_url' : myUser ? MatrixClientPeg . get ( ) . mxcUrlToHttp ( myUser . avatarUrl ) : '' ,
// TODO: Namespace themes through some standard
'theme' : SettingsStore . getValue ( "theme" ) ,
} ) ;
if ( vars . conferenceId === undefined ) {
// we'll need to parse the conference ID out of the URL for v1 Jitsi widgets
const parsedUrl = new URL ( this . props . app . url ) ;
vars . conferenceId = parsedUrl . searchParams . get ( "confId" ) ;
}
return uriFromTemplate ( u , vars ) ;
}
/ * *
* Get the URL used in the iframe
* In cases where we supply our own UI for a widget , this is an internal
* URL different to the one used if the widget is popped out to a separate
* tab / browser
*
* @ returns { string } url
* /
_getRenderedUrl ( ) {
let url ;
2020-04-10 00:11:57 +03:00
if ( WidgetType . JITSI . matches ( this . props . app . type ) ) {
2020-04-01 12:00:33 +03:00
console . log ( "Replacing Jitsi widget URL with local wrapper" ) ;
url = WidgetUtils . getLocalJitsiWrapperUrl ( { forLocalRender : true } ) ;
url = this . _addWurlParams ( url ) ;
} else {
2020-04-01 13:18:45 +03:00
url = this . _getSafeUrl ( this . state . widgetUrl ) ;
2020-04-01 12:00:33 +03:00
}
2020-04-24 00:24:20 +03:00
return this . _templatedUrl ( url , this . props . app . type ) ;
2020-04-01 12:00:33 +03:00
}
_getPopoutUrl ( ) {
2020-04-10 00:11:57 +03:00
if ( WidgetType . JITSI . matches ( this . props . app . type ) ) {
2020-04-01 16:42:17 +03:00
return this . _templatedUrl (
WidgetUtils . getLocalJitsiWrapperUrl ( { forLocalRender : false } ) ,
2020-04-24 08:25:53 +03:00
this . props . app . type ,
2020-04-01 16:42:17 +03:00
) ;
2020-04-01 15:58:44 +03:00
} else {
// use app.url, not state.widgetUrl, because we want the one without
// the wURL params for the popped-out version.
2020-04-24 00:24:20 +03:00
return this . _templatedUrl ( this . _getSafeUrl ( this . props . app . url ) , this . props . app . type ) ;
2020-04-01 15:58:44 +03:00
}
2020-04-01 12:00:33 +03:00
}
2020-04-01 13:18:45 +03:00
_getSafeUrl ( u ) {
const parsedWidgetUrl = url . parse ( u , true ) ;
2018-03-16 13:20:14 +03:00
if ( ENABLE _REACT _PERF ) {
parsedWidgetUrl . search = null ;
parsedWidgetUrl . query . react _perf = true ;
}
2017-11-30 01:16:22 +03:00
let safeWidgetUrl = '' ;
2020-04-01 12:00:33 +03:00
if ( ALLOWED _APP _URL _SCHEMES . includes ( parsedWidgetUrl . protocol ) ) {
2017-11-30 01:16:22 +03:00
safeWidgetUrl = url . format ( parsedWidgetUrl ) ;
}
2020-04-24 00:22:54 +03:00
// Replace all the dollar signs back to dollar signs as they don't affect HTTP at all.
// We also need the dollar signs in-tact for variable substitution.
return safeWidgetUrl . replace ( /%24/g , '$' ) ;
2018-01-11 14:49:46 +03:00
}
2017-11-30 01:16:22 +03:00
2018-02-07 17:44:01 +03:00
_getTileTitle ( ) {
const name = this . formatAppTileName ( ) ;
const titleSpacer = < span > & nbsp ; - & nbsp ; < / s p a n > ;
let title = '' ;
if ( this . state . widgetPageTitle && this . state . widgetPageTitle != this . formatAppTileName ( ) ) {
title = this . state . widgetPageTitle ;
}
return (
< span >
< b > { name } < / b >
< span > { title ? titleSpacer : '' } { title } < / s p a n >
< / s p a n >
) ;
}
2018-03-08 20:20:42 +03:00
_onMinimiseClick ( e ) {
if ( this . props . onMinimiseClick ) {
this . props . onMinimiseClick ( ) ;
}
}
2019-11-21 05:17:42 +03:00
_onPopoutWidgetClick ( ) {
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).
if ( WidgetType . JITSI . matches ( this . props . app . type ) && this . props . show ) {
this . _endWidgetActions ( ) . then ( ( ) => {
if ( this . _appFrame . current ) {
// Reload iframe
this . _appFrame . current . src = this . _getRenderedUrl ( ) ;
this . setState ( { } ) ;
}
} ) ;
}
2018-04-25 14:49:30 +03:00
// Using Object.assign workaround as the following opens in a new window instead of a new tab.
2020-04-01 12:00:33 +03:00
// window.open(this._getPopoutUrl(), '_blank', 'noopener=yes');
2018-04-25 14:49:30 +03:00
Object . assign ( document . createElement ( 'a' ) ,
2020-04-01 12:00:33 +03:00
{ target : '_blank' , href : this . _getPopoutUrl ( ) , rel : 'noreferrer noopener' } ) . click ( ) ;
2018-04-25 14:49:30 +03:00
}
2019-11-21 05:17:42 +03:00
_onReloadWidgetClick ( ) {
2018-05-22 21:14:54 +03:00
// Reload iframe in this way to avoid cross-origin restrictions
2019-12-08 15:16:17 +03:00
this . _appFrame . current . src = this . _appFrame . current . src ;
2018-05-22 21:14:54 +03:00
}
2019-11-28 21:16:59 +03:00
_onContextMenuClick = ( ) => {
this . setState ( { menuDisplayed : true } ) ;
} ;
2019-11-21 05:17:42 +03:00
2019-11-28 21:16:59 +03:00
_closeContextMenu = ( ) => {
this . setState ( { menuDisplayed : false } ) ;
2019-11-21 05:17:42 +03:00
} ;
2017-10-27 19:49:14 +03:00
render ( ) {
2017-07-06 11:28:48 +03:00
let appTileBody ;
2017-07-13 02:27:03 +03:00
// Don't render widget if it is in the process of being deleted
if ( this . state . deleting ) {
2019-11-28 21:16:59 +03:00
return < div / > ;
2017-07-13 02:27:03 +03:00
}
2017-07-26 13:28:43 +03:00
// Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin
2019-11-21 05:17:42 +03:00
// because that would allow the iframe to programmatically remove the sandbox attribute, but
2017-07-26 13:28:43 +03:00
// this would only be for content hosted on the same origin as the riot client: anything
// hosted on the same origin as the client will get the same access as if you clicked
// a link to it.
const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox " +
2017-07-28 13:14:04 +03:00
"allow-same-origin allow-scripts allow-presentation" ;
2017-07-26 13:28:43 +03:00
2018-02-22 02:10:08 +03:00
// Additional iframe feature pemissions
// (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/)
2018-12-14 16:26:35 +03:00
const iframeFeatures = "microphone; camera; encrypted-media; autoplay;" ;
2018-02-22 02:10:08 +03:00
2018-07-12 20:43:49 +03:00
const appTileBodyClass = 'mx_AppTileBody' + ( this . props . miniMode ? '_mini ' : ' ' ) ;
2017-08-18 20:33:56 +03:00
if ( this . props . show ) {
2017-11-08 23:38:31 +03:00
const loadingElement = (
2018-05-09 18:41:45 +03:00
< div className = "mx_AppLoading_spinner_fadeIn" >
2017-11-08 23:38:31 +03:00
< MessageSpinner msg = 'Loading...' / >
< / d i v >
) ;
2019-07-19 21:04:48 +03:00
if ( ! this . state . hasPermissionToLoad ) {
2019-11-21 02:26:06 +03:00
const isEncrypted = MatrixClientPeg . get ( ) . isRoomEncrypted ( this . props . room . roomId ) ;
2019-07-19 21:04:48 +03:00
appTileBody = (
< div className = { appTileBodyClass } >
< AppPermission
2019-11-16 00:25:53 +03:00
roomId = { this . props . room . roomId }
creatorUserId = { this . props . creatorUserId }
2019-07-19 21:04:48 +03:00
url = { this . state . widgetUrl }
2019-11-21 02:26:06 +03:00
isRoomEncrypted = { isEncrypted }
2019-07-19 21:04:48 +03:00
onPermissionGranted = { this . _grantWidgetPermission }
/ >
< / d i v >
) ;
} else if ( this . state . initialising ) {
2018-05-09 17:48:53 +03:00
appTileBody = (
2018-07-12 20:43:49 +03:00
< div className = { appTileBodyClass + ( this . state . loading ? 'mx_AppLoading' : '' ) } >
2018-05-09 17:48:53 +03:00
{ loadingElement }
< / d i v >
) ;
2019-07-19 21:04:48 +03:00
} else {
2017-08-18 20:33:56 +03:00
if ( this . isMixedContent ( ) ) {
2017-11-08 23:38:31 +03:00
appTileBody = (
2018-07-12 20:43:49 +03:00
< div className = { appTileBodyClass } >
2017-11-08 23:38:31 +03:00
< AppWarning errorMsg = "Error - Mixed content" / >
< / d i v >
) ;
2017-08-18 20:33:56 +03:00
} else {
appTileBody = (
2018-07-12 20:43:49 +03:00
< div className = { appTileBodyClass + ( this . state . loading ? 'mx_AppLoading' : '' ) } >
2017-11-08 23:38:54 +03:00
{ this . state . loading && loadingElement }
2017-08-18 20:33:56 +03:00
< iframe
2018-02-22 02:10:08 +03:00
allow = { iframeFeatures }
2019-12-08 15:16:17 +03:00
ref = { this . _appFrame }
2020-04-01 12:00:33 +03:00
src = { this . _getRenderedUrl ( ) }
2019-10-08 14:10:37 +03:00
allowFullScreen = { true }
2017-08-18 20:33:56 +03:00
sandbox = { sandboxFlags }
2019-08-15 01:15:49 +03:00
onLoad = { this . _onLoaded } / >
2017-08-18 20:33:56 +03:00
< / d i v >
) ;
2019-08-15 01:15:49 +03:00
// if the widget would be allowed to remain on screen, we must put it in
2018-07-11 20:07:32 +03:00
// a PersistedElement from the get-go, otherwise the iframe will be
// re-mounted later when we do.
if ( this . props . whitelistCapabilities . includes ( 'm.always_on_screen' ) ) {
const PersistedElement = sdk . getComponent ( "elements.PersistedElement" ) ;
2018-07-18 13:52:57 +03:00
// Also wrap the PersistedElement in a div to fix the height, otherwise
// AppTile's border is in the wrong place
appTileBody = < div className = "mx_AppTile_persistedWrapper" >
< PersistedElement persistKey = { this . _persistKey } >
{ appTileBody }
< / P e r s i s t e d E l e m e n t >
< / d i v > ;
2018-07-11 20:07:32 +03:00
}
2017-08-18 20:33:56 +03:00
}
2017-07-12 16:16:47 +03:00
}
2017-07-06 11:28:48 +03:00
}
2017-07-14 13:17:59 +03:00
2019-02-01 13:02:02 +03:00
const showMinimiseButton = this . props . showMinimise && this . props . show ;
const showMaximiseButton = this . props . showMinimise && ! this . props . show ;
2017-12-03 14:25:15 +03:00
2018-07-23 17:08:17 +03:00
let appTileClass ;
if ( this . props . miniMode ) {
2018-07-23 17:58:07 +03:00
appTileClass = 'mx_AppTile_mini' ;
2018-07-23 17:08:17 +03:00
} else if ( this . props . fullWidth ) {
appTileClass = 'mx_AppTileFullWidth' ;
} else {
appTileClass = 'mx_AppTile' ;
}
2019-02-01 13:02:02 +03:00
const menuBarClasses = classNames ( {
mx _AppTileMenuBar : true ,
mx _AppTileMenuBar _expanded : this . props . show ,
2019-02-02 09:46:52 +03:00
} ) ;
2019-02-01 13:02:02 +03:00
2019-11-28 21:16:59 +03:00
let contextMenu ;
if ( this . state . menuDisplayed ) {
const elementRect = this . _contextMenuButton . current . getBoundingClientRect ( ) ;
const canUserModify = this . _canUserModify ( ) ;
const showEditButton = Boolean ( this . _scalarClient && canUserModify ) ;
const showDeleteButton = ( this . props . showDelete === undefined || this . props . showDelete ) && canUserModify ;
const showPictureSnapshotButton = this . _hasCapability ( 'm.capability.screenshot' ) && this . props . show ;
const WidgetContextMenu = sdk . getComponent ( 'views.context_menus.WidgetContextMenu' ) ;
contextMenu = (
2019-12-03 02:23:11 +03:00
< ContextMenu { ... aboveLeftOf ( elementRect , null ) } onFinished = { this . _closeContextMenu } >
2019-11-28 21:16:59 +03:00
< WidgetContextMenu
onRevokeClicked = { this . _onRevokeClicked }
2019-11-28 23:26:09 +03:00
onEditClicked = { showEditButton ? this . _onEditClick : undefined }
onDeleteClicked = { showDeleteButton ? this . _onDeleteClick : undefined }
onSnapshotClicked = { showPictureSnapshotButton ? this . _onSnapshotClick : undefined }
onReloadClicked = { this . props . showReload ? this . _onReloadWidgetClick : undefined }
2019-11-28 21:16:59 +03:00
onFinished = { this . _closeContextMenu }
/ >
< / C o n t e x t M e n u >
) ;
}
return < React . Fragment >
2020-04-01 12:00:33 +03:00
< div className = { appTileClass } id = { this . props . app . id } >
2018-01-11 16:20:58 +03:00
{ this . props . showMenubar &&
2019-12-08 15:16:17 +03:00
< div ref = { this . _menu _bar } className = { menuBarClasses } onClick = { this . onClickMenuBar } >
2018-03-08 20:20:42 +03:00
< span className = "mx_AppTileMenuBarTitle" style = { { pointerEvents : ( this . props . handleMinimisePointerEvents ? 'all' : false ) } } >
2019-02-01 13:02:02 +03:00
{ /* Minimise widget */ }
{ showMinimiseButton && < AccessibleButton
className = "mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_minimise"
title = { _t ( 'Minimize apps' ) }
onClick = { this . _onMinimiseClick }
/ > }
{ /* Maximise widget */ }
{ showMaximiseButton && < AccessibleButton
className = "mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_maximise"
2019-03-24 08:50:06 +03:00
title = { _t ( 'Maximize apps' ) }
2018-03-08 20:20:42 +03:00
onClick = { this . _onMinimiseClick }
2018-02-07 17:44:01 +03:00
/ > }
2019-02-01 13:02:02 +03:00
{ /* Title */ }
2018-02-07 17:44:01 +03:00
{ this . props . showTitle && this . _getTileTitle ( ) }
2017-12-05 21:18:51 +03:00
< / s p a n >
2017-05-22 14:34:27 +03:00
< span className = "mx_AppTileMenuBarWidgets" >
2018-04-25 14:49:30 +03:00
{ /* Popout widget */ }
2019-02-01 13:02:02 +03:00
{ this . props . showPopout && < AccessibleButton
className = "mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_popout"
2018-04-25 14:49:30 +03:00
title = { _t ( 'Popout widget' ) }
onClick = { this . _onPopoutWidgetClick }
2018-04-25 18:28:27 +03:00
/ > }
2019-11-21 05:17:42 +03:00
{ /* Context menu */ }
2019-11-28 21:16:59 +03:00
{ < ContextMenuButton
2019-11-21 05:17:42 +03:00
className = "mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_menu"
2019-11-28 21:16:59 +03:00
label = { _t ( 'More options' ) }
isExpanded = { this . state . menuDisplayed }
inputRef = { this . _contextMenuButton }
2019-11-21 05:17:42 +03:00
onClick = { this . _onContextMenuClick }
2018-03-08 20:20:42 +03:00
/ > }
2017-05-22 14:34:27 +03:00
< / s p a n >
2018-01-11 16:20:58 +03:00
< / d i v > }
2017-09-28 13:21:06 +03:00
{ appTileBody }
2017-05-22 14:34:27 +03:00
< / d i v >
2019-11-28 21:16:59 +03:00
{ contextMenu }
< / R e a c t . F r a g m e n t > ;
2018-01-11 14:49:46 +03:00
}
}
2019-11-28 21:16:59 +03:00
AppTile . displayName = 'AppTile' ;
2018-02-07 17:48:43 +03:00
2018-01-11 14:49:46 +03:00
AppTile . propTypes = {
2020-04-01 12:00:33 +03:00
app : PropTypes . object . isRequired ,
2018-02-26 01:36:59 +03:00
room : PropTypes . object . isRequired ,
2018-01-11 14:49:46 +03:00
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
2018-02-26 01:36:59 +03:00
fullWidth : PropTypes . bool ,
2018-07-12 20:43:49 +03:00
// Optional. If set, renders a smaller view of the widget
miniMode : PropTypes . bool ,
2018-01-11 14:49:46 +03:00
// UserId of the current user
2018-02-26 01:36:59 +03:00
userId : PropTypes . string . isRequired ,
2018-01-11 14:49:46 +03:00
// UserId of the entity that added / modified the widget
2018-02-26 01:36:59 +03:00
creatorUserId : PropTypes . string ,
waitForIframeLoad : PropTypes . bool ,
showMenubar : PropTypes . bool ,
2018-01-11 16:20:58 +03:00
// Should the AppTile render itself
2018-02-26 01:36:59 +03:00
show : PropTypes . bool ,
2018-02-07 17:44:01 +03:00
// Optional onEditClickHandler (overrides default behaviour)
2018-02-26 01:36:59 +03:00
onEditClick : PropTypes . func ,
2018-02-07 17:44:01 +03:00
// Optional onDeleteClickHandler (overrides default behaviour)
2018-02-26 01:36:59 +03:00
onDeleteClick : PropTypes . func ,
2018-03-08 20:20:42 +03:00
// Optional onMinimiseClickHandler
onMinimiseClick : PropTypes . func ,
2018-02-07 17:44:01 +03:00
// Optionally hide the tile title
2018-02-26 01:36:59 +03:00
showTitle : PropTypes . bool ,
2018-02-07 17:44:01 +03:00
// Optionally hide the tile minimise icon
2018-02-26 01:36:59 +03:00
showMinimise : PropTypes . bool ,
2018-03-08 20:20:42 +03:00
// Optionally handle minimise button pointer events (default false)
handleMinimisePointerEvents : PropTypes . bool ,
// Optionally hide the delete icon
showDelete : PropTypes . bool ,
2018-04-25 18:28:27 +03:00
// Optionally hide the popout widget icon
showPopout : PropTypes . bool ,
2018-05-22 21:14:54 +03:00
// Optionally show the reload widget icon
// This is not currently intended for use with production widgets. However
// it can be useful when developing persistent widgets in order to avoid
// having to reload all of riot to get new widget content.
showReload : PropTypes . bool ,
2018-05-12 23:29:37 +03:00
// Widget capabilities to allow by default (without user confirmation)
2018-03-12 16:56:02 +03:00
// NOTE -- Use with caution. This is intended to aid better integration / UX
// basic widget capabilities, e.g. injecting sticker message events.
whitelistCapabilities : PropTypes . array ,
// Optional function to be called on widget capability request
// Called with an array of the requested capabilities
onCapabilityRequest : PropTypes . func ,
2018-05-09 00:44:49 +03:00
// Is this an instance of a user widget
userWidget : PropTypes . bool ,
2018-01-11 14:49:46 +03:00
} ;
AppTile . defaultProps = {
waitForIframeLoad : true ,
2018-01-11 16:20:58 +03:00
showMenubar : true ,
2018-02-07 17:44:01 +03:00
showTitle : true ,
showMinimise : true ,
2018-03-08 20:20:42 +03:00
showDelete : true ,
2018-04-25 18:28:27 +03:00
showPopout : true ,
2018-05-22 21:14:54 +03:00
showReload : false ,
2018-03-08 20:20:42 +03:00
handleMinimisePointerEvents : false ,
2018-03-12 16:56:02 +03:00
whitelistCapabilities : [ ] ,
2018-05-09 00:44:49 +03:00
userWidget : false ,
2018-07-12 20:43:49 +03:00
miniMode : false ,
2018-01-11 14:49:46 +03:00
} ;