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

Commit

Permalink
Put always-on-screen widgets in top left
Browse files Browse the repository at this point in the history
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 element-hq/element-web#7007
Based off #2053
  • Loading branch information
dbkr committed Jul 12, 2018
1 parent 5a5e967 commit e56feea
Show file tree
Hide file tree
Showing 10 changed files with 217 additions and 67 deletions.
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

0 comments on commit e56feea

Please sign in to comment.