Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Implement always-on-screen capability for widgets #2056

Merged
merged 3 commits into from
Jul 16, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions res/css/structures/_LeftPanel.scss
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ limitations under the License.

}

.mx_LeftPanel .mx_AppTileFullWidth {
height: 132px;
}

.mx_LeftPanel .mx_RoomList_scrollbar {
order: 1;

Expand Down
6 changes: 6 additions & 0 deletions res/css/views/rooms/_AppsDrawer.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
15 changes: 11 additions & 4 deletions src/components/views/elements/AppTile.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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});
}

Expand Down Expand Up @@ -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">
Expand All @@ -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
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -738,4 +744,5 @@ AppTile.defaultProps = {
handleMinimisePointerEvents: false,
whitelistCapabilities: [],
userWidget: false,
miniMode: false,
};
22 changes: 19 additions & 3 deletions src/components/views/elements/PersistedElement.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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) {
Expand All @@ -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() {
Expand Down
88 changes: 88 additions & 0 deletions src/components/views/elements/PersistentApp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
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 PropTypes from 'prop-types';
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;
},
});

60 changes: 2 additions & 58 deletions src/components/views/rooms/AppsDrawer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
});
},

Expand Down Expand Up @@ -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 (<AppTile
key={app.id}
Expand Down
5 changes: 3 additions & 2 deletions src/components/views/voip/CallPreview.js
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -92,7 +92,8 @@ module.exports = React.createClass({
/>
);
}
return null;
const PersistentApp = sdk.getComponent('elements.PersistentApp');
return <PersistentApp />;
},
});

19 changes: 19 additions & 0 deletions src/stores/ActiveWidgetStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -46,6 +49,10 @@ class ActiveWidgetStore {
return this._persistentWidgetId === widgetId;
}

getPersistentWidgetId() {
return this._persistentWidgetId;
}

setWidgetCapabilities(widgetId, caps) {
this._capsByWidgetId[widgetId] = caps;
}
Expand Down Expand Up @@ -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) {
Expand Down
Loading