From e56feea9ec235c4d00afc82389a47ec3559c875f Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 12 Jul 2018 18:43:49 +0100 Subject: [PATCH] Put always-on-screen widgets in top left always-on-screen widgets now appear in the top-left where the call preview normally is if you're not in the room that they're in. Fixes https://github.com/vector-im/riot-web/issues/7007 Based off https://github.com/matrix-org/matrix-react-sdk/pull/2053 --- package.json | 1 + res/css/structures/_LeftPanel.scss | 4 + res/css/views/rooms/_AppsDrawer.scss | 6 ++ src/components/views/elements/AppTile.js | 15 +++- .../views/elements/PersistedElement.js | 22 ++++- .../views/elements/PersistentApp.js | 88 +++++++++++++++++++ src/components/views/rooms/AppsDrawer.js | 60 +------------ src/components/views/voip/CallPreview.js | 5 +- src/stores/ActiveWidgetStore.js | 19 ++++ src/utils/WidgetUtils.js | 64 ++++++++++++++ 10 files changed, 217 insertions(+), 67 deletions(-) create mode 100644 src/components/views/elements/PersistentApp.js diff --git a/package.json b/package.json index 8c0ec922f2..570f57abb5 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,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", "sanitize-html": "^1.14.1", "text-encoding-utf-8": "^1.0.1", "url": "^0.11.0", diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index 96ed5878ac..bedec0e363 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -54,6 +54,10 @@ limitations under the License. } +.mx_LeftPanel .mx_AppTileFullWidth { + height: 132px; +} + .mx_LeftPanel .mx_RoomList_scrollbar { order: 1; diff --git a/res/css/views/rooms/_AppsDrawer.scss b/res/css/views/rooms/_AppsDrawer.scss index 28d432686d..6431853672 100644 --- a/res/css/views/rooms/_AppsDrawer.scss +++ b/res/css/views/rooms/_AppsDrawer.scss @@ -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; diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index cf69727b15..e287abd07a 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -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 = (
@@ -530,20 +534,20 @@ export default class AppTile extends React.Component { ); if (this.state.initialising) { appTileBody = ( -
+
{ loadingElement }
); } else if (this.state.hasPermissionToLoad == true) { if (this.isMixedContent()) { appTileBody = ( -
+
); } else { appTileBody = ( -
+
{ 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 = ( -
+
{ + 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 ; + } + } + return null; + }, +}); + diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index da8c558cb5..aa086f8260 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -107,55 +107,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 +116,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 +164,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 ( ); } - return null; + const PersistentApp = sdk.getComponent('elements.PersistentApp'); + return ; }, }); diff --git a/src/stores/ActiveWidgetStore.js b/src/stores/ActiveWidgetStore.js index 7c179aef84..01d5f15601 100644 --- a/src/stores/ActiveWidgetStore.js +++ b/src/stores/ActiveWidgetStore.js @@ -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) { diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js index ab5b5b0130..98239b3cec 100644 --- a/src/utils/WidgetUtils.js +++ b/src/utils/WidgetUtils.js @@ -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; + } }