Merge pull request #2056 from matrix-org/dbkr/tiny_jitsi_follows_you_between_rooms
Implement always-on-screen capability for widgetspull/21833/head
						commit
						149a935594
					
				|  | @ -84,6 +84,7 @@ | |||
|     "react-beautiful-dnd": "^4.0.1", | ||||
|     "react-dom": "^15.6.0", | ||||
|     "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef", | ||||
|     "resize-observer-polyfill": "^1.5.0", | ||||
|     "slate": "0.33.4", | ||||
|     "slate-react": "^0.12.4", | ||||
|     "slate-html-serializer": "^0.6.1", | ||||
|  |  | |||
|  | @ -54,6 +54,10 @@ limitations under the License. | |||
| 
 | ||||
| } | ||||
| 
 | ||||
| .mx_LeftPanel .mx_AppTileFullWidth  { | ||||
|     height: 132px; | ||||
| } | ||||
| 
 | ||||
| .mx_LeftPanel .mx_RoomList_scrollbar { | ||||
|     order: 1; | ||||
| 
 | ||||
|  |  | |||
|  | @ -126,6 +126,12 @@ limitations under the License. | |||
|     overflow: hidden; | ||||
| } | ||||
| 
 | ||||
| .mx_AppTileBody_mini { | ||||
|     height: 132px; | ||||
|     width: 100%; | ||||
|     overflow: hidden; | ||||
| } | ||||
| 
 | ||||
| .mx_AppTileBody iframe { | ||||
|     width: 100%; | ||||
|     height: 280px; | ||||
|  |  | |||
|  | @ -164,6 +164,7 @@ export default class AppTile extends React.Component { | |||
|             PersistedElement.destroyElement(this._persistKey); | ||||
|             ActiveWidgetStore.delWidgetMessaging(this.props.id); | ||||
|             ActiveWidgetStore.delWidgetCapabilities(this.props.id); | ||||
|             ActiveWidgetStore.delRoomId(this.props.id); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -343,6 +344,7 @@ export default class AppTile extends React.Component { | |||
|         if (!ActiveWidgetStore.getWidgetMessaging(this.props.id)) { | ||||
|             this._setupWidgetMessaging(); | ||||
|         } | ||||
|         ActiveWidgetStore.setRoomId(this.props.id, this.props.room.roomId); | ||||
|         this.setState({loading: false}); | ||||
|     } | ||||
| 
 | ||||
|  | @ -522,6 +524,8 @@ export default class AppTile extends React.Component { | |||
|         // (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/)
 | ||||
|         const iframeFeatures = "microphone; camera; encrypted-media;"; | ||||
| 
 | ||||
|         const appTileBodyClass = 'mx_AppTileBody' + (this.props.miniMode ? '_mini  ' : ' '); | ||||
| 
 | ||||
|         if (this.props.show) { | ||||
|             const loadingElement = ( | ||||
|                 <div className="mx_AppLoading_spinner_fadeIn"> | ||||
|  | @ -530,20 +534,20 @@ export default class AppTile extends React.Component { | |||
|             ); | ||||
|             if (this.state.initialising) { | ||||
|                 appTileBody = ( | ||||
|                     <div className={'mx_AppTileBody ' + (this.state.loading ? 'mx_AppLoading' : '')}> | ||||
|                     <div className={appTileBodyClass + (this.state.loading ? 'mx_AppLoading' : '')}> | ||||
|                         { loadingElement } | ||||
|                     </div> | ||||
|                 ); | ||||
|             } else if (this.state.hasPermissionToLoad == true) { | ||||
|                 if (this.isMixedContent()) { | ||||
|                     appTileBody = ( | ||||
|                         <div className="mx_AppTileBody"> | ||||
|                         <div className={appTileBodyClass}> | ||||
|                             <AppWarning errorMsg="Error - Mixed content" /> | ||||
|                         </div> | ||||
|                     ); | ||||
|                 } else { | ||||
|                     appTileBody = ( | ||||
|                         <div className={'mx_AppTileBody ' + (this.state.loading ? 'mx_AppLoading' : '')}> | ||||
|                         <div className={appTileBodyClass + (this.state.loading ? 'mx_AppLoading' : '')}> | ||||
|                             { this.state.loading && loadingElement } | ||||
|                             { /* | ||||
|                                 The "is" attribute in the following iframe tag is needed in order to enable rendering of the | ||||
|  | @ -573,7 +577,7 @@ export default class AppTile extends React.Component { | |||
|             } else { | ||||
|                 const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId); | ||||
|                 appTileBody = ( | ||||
|                     <div className="mx_AppTileBody"> | ||||
|                     <div className={appTileBodyClass}> | ||||
|                         <AppPermission | ||||
|                             isRoomEncrypted={isRoomEncrypted} | ||||
|                             url={this.state.widgetUrl} | ||||
|  | @ -686,6 +690,8 @@ AppTile.propTypes = { | |||
|     // 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.
 | ||||
|     fullWidth: PropTypes.bool, | ||||
|     // Optional. If set, renders a smaller view of the widget
 | ||||
|     miniMode: PropTypes.bool, | ||||
|     // UserId of the current user
 | ||||
|     userId: PropTypes.string.isRequired, | ||||
|     // UserId of the entity that added / modified the widget
 | ||||
|  | @ -738,4 +744,5 @@ AppTile.defaultProps = { | |||
|     handleMinimisePointerEvents: false, | ||||
|     whitelistCapabilities: [], | ||||
|     userWidget: false, | ||||
|     miniMode: false, | ||||
| }; | ||||
|  |  | |||
|  | @ -14,9 +14,11 @@ See the License for the specific language governing permissions and | |||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| const React = require('react'); | ||||
| const ReactDOM = require('react-dom'); | ||||
| const PropTypes = require('prop-types'); | ||||
| import React from 'react'; | ||||
| import ReactDOM from 'react-dom'; | ||||
| import PropTypes from 'prop-types'; | ||||
| 
 | ||||
| import ResizeObserver from 'resize-observer-polyfill'; | ||||
| 
 | ||||
| // Shamelessly ripped off Modal.js.  There's probably a better way
 | ||||
| // of doing reusable widgets like dialog boxes & menus where we go and
 | ||||
|  | @ -62,6 +64,9 @@ export default class PersistedElement extends React.Component { | |||
|         super(); | ||||
|         this.collectChildContainer = this.collectChildContainer.bind(this); | ||||
|         this.collectChild = this.collectChild.bind(this); | ||||
|         this._onContainerResize = this._onContainerResize.bind(this); | ||||
| 
 | ||||
|         this.resizeObserver = new ResizeObserver(this._onContainerResize); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -83,7 +88,13 @@ export default class PersistedElement extends React.Component { | |||
|     } | ||||
| 
 | ||||
|     collectChildContainer(ref) { | ||||
|         if (this.childContainer) { | ||||
|             this.resizeObserver.unobserve(this.childContainer); | ||||
|         } | ||||
|         this.childContainer = ref; | ||||
|         if (ref) { | ||||
|             this.resizeObserver.observe(ref); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     collectChild(ref) { | ||||
|  | @ -101,6 +112,11 @@ export default class PersistedElement extends React.Component { | |||
| 
 | ||||
|     componentWillUnmount() { | ||||
|         this.updateChildVisibility(this.child, false); | ||||
|         this.resizeObserver.disconnect(); | ||||
|     } | ||||
| 
 | ||||
|     _onContainerResize() { | ||||
|         this.updateChildPosition(this.child, this.childContainer); | ||||
|     } | ||||
| 
 | ||||
|     updateChild() { | ||||
|  |  | |||
|  | @ -0,0 +1,87 @@ | |||
| /* | ||||
| Copyright 2018 New Vector Ltd | ||||
| 
 | ||||
| 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. | ||||
| */ | ||||
| 
 | ||||
| import React from 'react'; | ||||
| import RoomViewStore from '../../../stores/RoomViewStore'; | ||||
| import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; | ||||
| import WidgetUtils from '../../../utils/WidgetUtils'; | ||||
| import sdk from '../../../index'; | ||||
| import MatrixClientPeg from '../../../MatrixClientPeg'; | ||||
| 
 | ||||
| module.exports = React.createClass({ | ||||
|     displayName: 'PersistentApp', | ||||
| 
 | ||||
|     getInitialState: function() { | ||||
|         return { | ||||
|             roomId: RoomViewStore.getRoomId(), | ||||
|         }; | ||||
|     }, | ||||
| 
 | ||||
|     componentWillMount: function() { | ||||
|         this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); | ||||
|     }, | ||||
| 
 | ||||
|     componentWillUnmount: function() { | ||||
|         if (this._roomStoreToken) { | ||||
|             this._roomStoreToken.remove(); | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     _onRoomViewStoreUpdate: function(payload) { | ||||
|         if (RoomViewStore.getRoomId() === this.state.roomId) return; | ||||
|         this.setState({ | ||||
|             roomId: RoomViewStore.getRoomId(), | ||||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|     render: function() { | ||||
|         if (ActiveWidgetStore.getPersistentWidgetId()) { | ||||
|             const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(ActiveWidgetStore.getPersistentWidgetId()); | ||||
|             if (this.state.roomId !== persistentWidgetInRoomId) { | ||||
|                 const persistentWidgetInRoom = MatrixClientPeg.get().getRoom(persistentWidgetInRoomId); | ||||
|                 // get the widget data
 | ||||
|                 const appEvent = WidgetUtils.getRoomWidgets(persistentWidgetInRoom).find((ev) => { | ||||
|                     return ev.getStateKey() === ActiveWidgetStore.getPersistentWidgetId(); | ||||
|                 }); | ||||
|                 const app = WidgetUtils.makeAppConfig( | ||||
|                     appEvent.getStateKey(), appEvent.getContent(), appEvent.sender, persistentWidgetInRoomId, | ||||
|                 ); | ||||
|                 const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, persistentWidgetInRoomId); | ||||
|                 const AppTile = sdk.getComponent('elements.AppTile'); | ||||
|                 return <AppTile | ||||
|                     key={app.id} | ||||
|                     id={app.id} | ||||
|                     url={app.url} | ||||
|                     name={app.name} | ||||
|                     type={app.type} | ||||
|                     fullWidth={true} | ||||
|                     room={persistentWidgetInRoom} | ||||
|                     userId={MatrixClientPeg.get().credentials.userId} | ||||
|                     show={true} | ||||
|                     creatorUserId={app.creatorUserId} | ||||
|                     widgetPageTitle={(app.data && app.data.title) ? app.data.title : ''} | ||||
|                     waitForIframeLoad={app.waitForIframeLoad} | ||||
|                     whitelistCapabilities={capWhitelist} | ||||
|                     showDelete={false} | ||||
|                     showMinimise={false} | ||||
|                     miniMode={true} | ||||
|                 />; | ||||
|             } | ||||
|         } | ||||
|         return null; | ||||
|     }, | ||||
| }); | ||||
| 
 | ||||
|  | @ -29,7 +29,6 @@ import ScalarAuthClient from '../../../ScalarAuthClient'; | |||
| import ScalarMessaging from '../../../ScalarMessaging'; | ||||
| import { _t } from '../../../languageHandler'; | ||||
| import WidgetUtils from '../../../utils/WidgetUtils'; | ||||
| import SettingsStore from "../../../settings/SettingsStore"; | ||||
| 
 | ||||
| // The maximum number of widgets that can be added in a room
 | ||||
| const MAX_WIDGETS = 2; | ||||
|  | @ -107,55 +106,6 @@ module.exports = React.createClass({ | |||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     /** | ||||
|      * Encodes a URI according to a set of template variables. Variables will be | ||||
|      * passed through encodeURIComponent. | ||||
|      * @param {string} pathTemplate 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'. | ||||
|      */ | ||||
|     encodeUri: function(pathTemplate, variables) { | ||||
|         for (const key in variables) { | ||||
|             if (!variables.hasOwnProperty(key)) { | ||||
|                 continue; | ||||
|             } | ||||
|             pathTemplate = pathTemplate.replace( | ||||
|                 key, encodeURIComponent(variables[key]), | ||||
|             ); | ||||
|         } | ||||
|         return pathTemplate; | ||||
|     }, | ||||
| 
 | ||||
|     _initAppConfig: function(appId, app, sender) { | ||||
|         const user = MatrixClientPeg.get().getUser(this.props.userId); | ||||
|         const params = { | ||||
|             '$matrix_user_id': this.props.userId, | ||||
|             '$matrix_room_id': this.props.room.roomId, | ||||
|             '$matrix_display_name': user ? user.displayName : this.props.userId, | ||||
|             '$matrix_avatar_url': user ? MatrixClientPeg.get().mxcUrlToHttp(user.avatarUrl) : '', | ||||
| 
 | ||||
|             // TODO: Namespace themes through some standard
 | ||||
|             '$theme': SettingsStore.getValue("theme"), | ||||
|         }; | ||||
| 
 | ||||
|         app.id = appId; | ||||
|         app.name = app.name || app.type; | ||||
| 
 | ||||
|         if (app.data) { | ||||
|             Object.keys(app.data).forEach((key) => { | ||||
|                 params['$' + key] = app.data[key]; | ||||
|             }); | ||||
| 
 | ||||
|             app.waitForIframeLoad = (app.data.waitForIframeLoad === 'false' ? false : true); | ||||
|         } | ||||
| 
 | ||||
|         app.url = this.encodeUri(app.url, params); | ||||
|         app.creatorUserId = (sender && sender.userId) ? sender.userId : null; | ||||
| 
 | ||||
|         return app; | ||||
|     }, | ||||
| 
 | ||||
|     onRoomStateEvents: function(ev, state) { | ||||
|         if (ev.getRoomId() !== this.props.room.roomId || ev.getType() !== 'im.vector.modular.widgets') { | ||||
|             return; | ||||
|  | @ -165,7 +115,7 @@ module.exports = React.createClass({ | |||
| 
 | ||||
|     _getApps: function() { | ||||
|         return WidgetUtils.getRoomWidgets(this.props.room).map((ev) => { | ||||
|             return this._initAppConfig(ev.getStateKey(), ev.getContent(), ev.sender); | ||||
|             return WidgetUtils.makeAppConfig(ev.getStateKey(), ev.getContent(), ev.sender, this.props.room.roomId); | ||||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|  | @ -213,15 +163,8 @@ module.exports = React.createClass({ | |||
|     }, | ||||
| 
 | ||||
|     render: function() { | ||||
|         const enableScreenshots = SettingsStore.getValue("enableWidgetScreenshots", this.props.room.room_id); | ||||
| 
 | ||||
|         const apps = this.state.apps.map((app, index, arr) => { | ||||
|             const capWhitelist = enableScreenshots ? ["m.capability.screenshot"] : []; | ||||
| 
 | ||||
|             // Obviously anyone that can add a widget can claim it's a jitsi widget,
 | ||||
|             // so this doesn't really offer much over the set of domains we load
 | ||||
|             // widgets from at all, but it probably makes sense for sanity.
 | ||||
|             if (app.type == 'jitsi') capWhitelist.push("m.always_on_screen"); | ||||
|             const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, this.props.room.roomId); | ||||
| 
 | ||||
|             return (<AppTile | ||||
|                 key={app.id} | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| /* | ||||
| Copyright 2017 New Vector Ltd | ||||
| Copyright 2017, 2018 New Vector Ltd | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
|  | @ -92,7 +92,8 @@ module.exports = React.createClass({ | |||
|                 /> | ||||
|             ); | ||||
|         } | ||||
|         return null; | ||||
|         const PersistentApp = sdk.getComponent('elements.PersistentApp'); | ||||
|         return <PersistentApp />; | ||||
|     }, | ||||
| }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -32,6 +32,9 @@ class ActiveWidgetStore { | |||
| 
 | ||||
|         // A WidgetMessaging instance for each widget ID
 | ||||
|         this._widgetMessagingByWidgetId = {}; | ||||
| 
 | ||||
|         // What room ID each widget is associated with (if it's a room widget)
 | ||||
|         this._roomIdByWidgetId = {}; | ||||
|     } | ||||
| 
 | ||||
|     setWidgetPersistence(widgetId, val) { | ||||
|  | @ -46,6 +49,10 @@ class ActiveWidgetStore { | |||
|         return this._persistentWidgetId === widgetId; | ||||
|     } | ||||
| 
 | ||||
|     getPersistentWidgetId() { | ||||
|         return this._persistentWidgetId; | ||||
|     } | ||||
| 
 | ||||
|     setWidgetCapabilities(widgetId, caps) { | ||||
|         this._capsByWidgetId[widgetId] = caps; | ||||
|     } | ||||
|  | @ -76,6 +83,18 @@ class ActiveWidgetStore { | |||
|             delete this._widgetMessagingByWidgetId[widgetId]; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     getRoomId(widgetId) { | ||||
|         return this._roomIdByWidgetId[widgetId]; | ||||
|     } | ||||
| 
 | ||||
|     setRoomId(widgetId, roomId) { | ||||
|         this._roomIdByWidgetId[widgetId] = roomId; | ||||
|     } | ||||
| 
 | ||||
|     delRoomId(widgetId) { | ||||
|         delete this._roomIdByWidgetId[widgetId]; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| if (global.singletonActiveWidgetStore === undefined) { | ||||
|  |  | |||
|  | @ -19,6 +19,27 @@ import MatrixClientPeg from '../MatrixClientPeg'; | |||
| import SdkConfig from "../SdkConfig"; | ||||
| import dis from '../dispatcher'; | ||||
| import * as url from "url"; | ||||
| import SettingsStore from "../settings/SettingsStore"; | ||||
| 
 | ||||
| /** | ||||
|  * Encodes a URI according to a set of template variables. Variables will be | ||||
|  * passed through encodeURIComponent. | ||||
|  * @param {string} pathTemplate 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 encodeUri(pathTemplate, variables) { | ||||
|     for (const key in variables) { | ||||
|         if (!variables.hasOwnProperty(key)) { | ||||
|             continue; | ||||
|         } | ||||
|         pathTemplate = pathTemplate.replace( | ||||
|             key, encodeURIComponent(variables[key]), | ||||
|         ); | ||||
|     } | ||||
|     return pathTemplate; | ||||
| } | ||||
| 
 | ||||
| export default class WidgetUtils { | ||||
|     /* Returns true if user is able to send state events to modify widgets in this room | ||||
|  | @ -324,4 +345,47 @@ export default class WidgetUtils { | |||
|         }); | ||||
|         return client.setAccountData('m.widgets', userWidgets); | ||||
|     } | ||||
| 
 | ||||
|     static makeAppConfig(appId, app, sender, roomId) { | ||||
|         const myUserId = MatrixClientPeg.get().credentials.userId; | ||||
|         const user = MatrixClientPeg.get().getUser(myUserId); | ||||
|         const params = { | ||||
|             '$matrix_user_id': myUserId, | ||||
|             '$matrix_room_id': roomId, | ||||
|             '$matrix_display_name': user ? user.displayName : myUserId, | ||||
|             '$matrix_avatar_url': user ? MatrixClientPeg.get().mxcUrlToHttp(user.avatarUrl) : '', | ||||
| 
 | ||||
|             // TODO: Namespace themes through some standard
 | ||||
|             '$theme': SettingsStore.getValue("theme"), | ||||
|         }; | ||||
| 
 | ||||
|         app.id = appId; | ||||
|         app.name = app.name || app.type; | ||||
| 
 | ||||
|         if (app.data) { | ||||
|             Object.keys(app.data).forEach((key) => { | ||||
|                 params['$' + key] = app.data[key]; | ||||
|             }); | ||||
| 
 | ||||
|             app.waitForIframeLoad = (app.data.waitForIframeLoad === 'false' ? false : true); | ||||
|         } | ||||
| 
 | ||||
|         app.url = encodeUri(app.url, params); | ||||
|         app.creatorUserId = (sender && sender.userId) ? sender.userId : null; | ||||
| 
 | ||||
|         return app; | ||||
|     } | ||||
| 
 | ||||
|     static getCapWhitelistForAppTypeInRoomId(appType, roomId) { | ||||
|         const enableScreenshots = SettingsStore.getValue("enableWidgetScreenshots", roomId); | ||||
| 
 | ||||
|         const capWhitelist = enableScreenshots ? ["m.capability.screenshot"] : []; | ||||
| 
 | ||||
|         // Obviously anyone that can add a widget can claim it's a jitsi widget,
 | ||||
|         // so this doesn't really offer much over the set of domains we load
 | ||||
|         // widgets from at all, but it probably makes sense for sanity.
 | ||||
|         if (appType == 'jitsi') capWhitelist.push("m.always_on_screen"); | ||||
| 
 | ||||
|         return capWhitelist; | ||||
|     } | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 David Baker
						David Baker