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

Commit e56feea

Browse files
committed
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 element-hq/element-web#7007 Based off #2053
1 parent 5a5e967 commit e56feea

File tree

10 files changed

+217
-67
lines changed

10 files changed

+217
-67
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
"react-beautiful-dnd": "^4.0.1",
8888
"react-dom": "^15.6.0",
8989
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
90+
"resize-observer-polyfill": "^1.5.0",
9091
"sanitize-html": "^1.14.1",
9192
"text-encoding-utf-8": "^1.0.1",
9293
"url": "^0.11.0",

res/css/structures/_LeftPanel.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ limitations under the License.
5454

5555
}
5656

57+
.mx_LeftPanel .mx_AppTileFullWidth {
58+
height: 132px;
59+
}
60+
5761
.mx_LeftPanel .mx_RoomList_scrollbar {
5862
order: 1;
5963

res/css/views/rooms/_AppsDrawer.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,12 @@ limitations under the License.
126126
overflow: hidden;
127127
}
128128

129+
.mx_AppTileBody_mini {
130+
height: 132px;
131+
width: 100%;
132+
overflow: hidden;
133+
}
134+
129135
.mx_AppTileBody iframe {
130136
width: 100%;
131137
height: 280px;

src/components/views/elements/AppTile.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ export default class AppTile extends React.Component {
164164
PersistedElement.destroyElement(this._persistKey);
165165
ActiveWidgetStore.delWidgetMessaging(this.props.id);
166166
ActiveWidgetStore.delWidgetCapabilities(this.props.id);
167+
ActiveWidgetStore.delRoomId(this.props.id);
167168
}
168169
}
169170

@@ -343,6 +344,7 @@ export default class AppTile extends React.Component {
343344
if (!ActiveWidgetStore.getWidgetMessaging(this.props.id)) {
344345
this._setupWidgetMessaging();
345346
}
347+
ActiveWidgetStore.setRoomId(this.props.id, this.props.room.roomId);
346348
this.setState({loading: false});
347349
}
348350

@@ -522,6 +524,8 @@ export default class AppTile extends React.Component {
522524
// (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/)
523525
const iframeFeatures = "microphone; camera; encrypted-media;";
524526

527+
const appTileBodyClass = 'mx_AppTileBody' + (this.props.miniMode ? '_mini ' : ' ');
528+
525529
if (this.props.show) {
526530
const loadingElement = (
527531
<div className="mx_AppLoading_spinner_fadeIn">
@@ -530,20 +534,20 @@ export default class AppTile extends React.Component {
530534
);
531535
if (this.state.initialising) {
532536
appTileBody = (
533-
<div className={'mx_AppTileBody ' + (this.state.loading ? 'mx_AppLoading' : '')}>
537+
<div className={appTileBodyClass + (this.state.loading ? 'mx_AppLoading' : '')}>
534538
{ loadingElement }
535539
</div>
536540
);
537541
} else if (this.state.hasPermissionToLoad == true) {
538542
if (this.isMixedContent()) {
539543
appTileBody = (
540-
<div className="mx_AppTileBody">
544+
<div className={appTileBodyClass}>
541545
<AppWarning errorMsg="Error - Mixed content" />
542546
</div>
543547
);
544548
} else {
545549
appTileBody = (
546-
<div className={'mx_AppTileBody ' + (this.state.loading ? 'mx_AppLoading' : '')}>
550+
<div className={appTileBodyClass + (this.state.loading ? 'mx_AppLoading' : '')}>
547551
{ this.state.loading && loadingElement }
548552
{ /*
549553
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 {
573577
} else {
574578
const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
575579
appTileBody = (
576-
<div className="mx_AppTileBody">
580+
<div className={appTileBodyClass}>
577581
<AppPermission
578582
isRoomEncrypted={isRoomEncrypted}
579583
url={this.state.widgetUrl}
@@ -686,6 +690,8 @@ AppTile.propTypes = {
686690
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
687691
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
688692
fullWidth: PropTypes.bool,
693+
// Optional. If set, renders a smaller view of the widget
694+
miniMode: PropTypes.bool,
689695
// UserId of the current user
690696
userId: PropTypes.string.isRequired,
691697
// UserId of the entity that added / modified the widget
@@ -738,4 +744,5 @@ AppTile.defaultProps = {
738744
handleMinimisePointerEvents: false,
739745
whitelistCapabilities: [],
740746
userWidget: false,
747+
miniMode: false,
741748
};

src/components/views/elements/PersistedElement.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
const React = require('react');
18-
const ReactDOM = require('react-dom');
19-
const PropTypes = require('prop-types');
17+
import React from 'react';
18+
import ReactDOM from 'react-dom';
19+
import PropTypes from 'prop-types';
20+
21+
import ResizeObserver from 'resize-observer-polyfill';
2022

2123
// Shamelessly ripped off Modal.js. There's probably a better way
2224
// of doing reusable widgets like dialog boxes & menus where we go and
@@ -62,6 +64,9 @@ export default class PersistedElement extends React.Component {
6264
super();
6365
this.collectChildContainer = this.collectChildContainer.bind(this);
6466
this.collectChild = this.collectChild.bind(this);
67+
this._onContainerResize = this._onContainerResize.bind(this);
68+
69+
this.resizeObserver = new ResizeObserver(this._onContainerResize);
6570
}
6671

6772
/**
@@ -83,7 +88,13 @@ export default class PersistedElement extends React.Component {
8388
}
8489

8590
collectChildContainer(ref) {
91+
if (this.childContainer) {
92+
this.resizeObserver.unobserve(this.childContainer);
93+
}
8694
this.childContainer = ref;
95+
if (ref) {
96+
this.resizeObserver.observe(ref);
97+
}
8798
}
8899

89100
collectChild(ref) {
@@ -101,6 +112,11 @@ export default class PersistedElement extends React.Component {
101112

102113
componentWillUnmount() {
103114
this.updateChildVisibility(this.child, false);
115+
this.resizeObserver.disconnect();
116+
}
117+
118+
_onContainerResize() {
119+
this.updateChildPosition(this.child, this.childContainer);
104120
}
105121

106122
updateChild() {
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
Copyright 2018 New Vector Ltd
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import React from 'react';
18+
import PropTypes from 'prop-types';
19+
import RoomViewStore from '../../../stores/RoomViewStore';
20+
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
21+
import WidgetUtils from '../../../utils/WidgetUtils';
22+
import sdk from '../../../index';
23+
import MatrixClientPeg from '../../../MatrixClientPeg';
24+
25+
module.exports = React.createClass({
26+
displayName: 'PersistentApp',
27+
28+
getInitialState: function() {
29+
return {
30+
roomId: RoomViewStore.getRoomId(),
31+
};
32+
},
33+
34+
componentWillMount: function() {
35+
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
36+
},
37+
38+
componentWillUnmount: function() {
39+
if (this._roomStoreToken) {
40+
this._roomStoreToken.remove();
41+
}
42+
},
43+
44+
_onRoomViewStoreUpdate: function(payload) {
45+
if (RoomViewStore.getRoomId() === this.state.roomId) return;
46+
this.setState({
47+
roomId: RoomViewStore.getRoomId(),
48+
});
49+
},
50+
51+
render: function() {
52+
if (ActiveWidgetStore.getPersistentWidgetId()) {
53+
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(ActiveWidgetStore.getPersistentWidgetId());
54+
if (this.state.roomId !== persistentWidgetInRoomId) {
55+
const persistentWidgetInRoom = MatrixClientPeg.get().getRoom(persistentWidgetInRoomId);
56+
// get the widget data
57+
const appEvent = WidgetUtils.getRoomWidgets(persistentWidgetInRoom).find((ev) => {
58+
return ev.getStateKey() === ActiveWidgetStore.getPersistentWidgetId();
59+
});
60+
const app = WidgetUtils.makeAppConfig(
61+
appEvent.getStateKey(), appEvent.getContent(), appEvent.sender, persistentWidgetInRoomId,
62+
);
63+
const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, persistentWidgetInRoomId);
64+
const AppTile = sdk.getComponent('elements.AppTile');
65+
return <AppTile
66+
key={app.id}
67+
id={app.id}
68+
url={app.url}
69+
name={app.name}
70+
type={app.type}
71+
fullWidth={true}
72+
room={persistentWidgetInRoom}
73+
userId={MatrixClientPeg.get().credentials.userId}
74+
show={true}
75+
creatorUserId={app.creatorUserId}
76+
widgetPageTitle={(app.data && app.data.title) ? app.data.title : ''}
77+
waitForIframeLoad={app.waitForIframeLoad}
78+
whitelistCapabilities={capWhitelist}
79+
showDelete={false}
80+
showMinimise={false}
81+
miniMode={true}
82+
/>;
83+
}
84+
}
85+
return null;
86+
},
87+
});
88+

src/components/views/rooms/AppsDrawer.js

Lines changed: 2 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -107,55 +107,6 @@ module.exports = React.createClass({
107107
}
108108
},
109109

110-
/**
111-
* Encodes a URI according to a set of template variables. Variables will be
112-
* passed through encodeURIComponent.
113-
* @param {string} pathTemplate The path with template variables e.g. '/foo/$bar'.
114-
* @param {Object} variables The key/value pairs to replace the template
115-
* variables with. E.g. { '$bar': 'baz' }.
116-
* @return {string} The result of replacing all template variables e.g. '/foo/baz'.
117-
*/
118-
encodeUri: function(pathTemplate, variables) {
119-
for (const key in variables) {
120-
if (!variables.hasOwnProperty(key)) {
121-
continue;
122-
}
123-
pathTemplate = pathTemplate.replace(
124-
key, encodeURIComponent(variables[key]),
125-
);
126-
}
127-
return pathTemplate;
128-
},
129-
130-
_initAppConfig: function(appId, app, sender) {
131-
const user = MatrixClientPeg.get().getUser(this.props.userId);
132-
const params = {
133-
'$matrix_user_id': this.props.userId,
134-
'$matrix_room_id': this.props.room.roomId,
135-
'$matrix_display_name': user ? user.displayName : this.props.userId,
136-
'$matrix_avatar_url': user ? MatrixClientPeg.get().mxcUrlToHttp(user.avatarUrl) : '',
137-
138-
// TODO: Namespace themes through some standard
139-
'$theme': SettingsStore.getValue("theme"),
140-
};
141-
142-
app.id = appId;
143-
app.name = app.name || app.type;
144-
145-
if (app.data) {
146-
Object.keys(app.data).forEach((key) => {
147-
params['$' + key] = app.data[key];
148-
});
149-
150-
app.waitForIframeLoad = (app.data.waitForIframeLoad === 'false' ? false : true);
151-
}
152-
153-
app.url = this.encodeUri(app.url, params);
154-
app.creatorUserId = (sender && sender.userId) ? sender.userId : null;
155-
156-
return app;
157-
},
158-
159110
onRoomStateEvents: function(ev, state) {
160111
if (ev.getRoomId() !== this.props.room.roomId || ev.getType() !== 'im.vector.modular.widgets') {
161112
return;
@@ -165,7 +116,7 @@ module.exports = React.createClass({
165116

166117
_getApps: function() {
167118
return WidgetUtils.getRoomWidgets(this.props.room).map((ev) => {
168-
return this._initAppConfig(ev.getStateKey(), ev.getContent(), ev.sender);
119+
return WidgetUtils.makeAppConfig(ev.getStateKey(), ev.getContent(), ev.sender, this.props.room.roomId);
169120
});
170121
},
171122

@@ -213,15 +164,8 @@ module.exports = React.createClass({
213164
},
214165

215166
render: function() {
216-
const enableScreenshots = SettingsStore.getValue("enableWidgetScreenshots", this.props.room.room_id);
217-
218167
const apps = this.state.apps.map((app, index, arr) => {
219-
const capWhitelist = enableScreenshots ? ["m.capability.screenshot"] : [];
220-
221-
// Obviously anyone that can add a widget can claim it's a jitsi widget,
222-
// so this doesn't really offer much over the set of domains we load
223-
// widgets from at all, but it probably makes sense for sanity.
224-
if (app.type == 'jitsi') capWhitelist.push("m.always_on_screen");
168+
const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, this.props.room.roomId);
225169

226170
return (<AppTile
227171
key={app.id}

src/components/views/voip/CallPreview.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2017 New Vector Ltd
2+
Copyright 2017, 2018 New Vector Ltd
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -92,7 +92,8 @@ module.exports = React.createClass({
9292
/>
9393
);
9494
}
95-
return null;
95+
const PersistentApp = sdk.getComponent('elements.PersistentApp');
96+
return <PersistentApp />;
9697
},
9798
});
9899

src/stores/ActiveWidgetStore.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ class ActiveWidgetStore {
3232

3333
// A WidgetMessaging instance for each widget ID
3434
this._widgetMessagingByWidgetId = {};
35+
36+
// What room ID each widget is associated with (if it's a room widget)
37+
this._roomIdByWidgetId = {};
3538
}
3639

3740
setWidgetPersistence(widgetId, val) {
@@ -46,6 +49,10 @@ class ActiveWidgetStore {
4649
return this._persistentWidgetId === widgetId;
4750
}
4851

52+
getPersistentWidgetId() {
53+
return this._persistentWidgetId;
54+
}
55+
4956
setWidgetCapabilities(widgetId, caps) {
5057
this._capsByWidgetId[widgetId] = caps;
5158
}
@@ -76,6 +83,18 @@ class ActiveWidgetStore {
7683
delete this._widgetMessagingByWidgetId[widgetId];
7784
}
7885
}
86+
87+
getRoomId(widgetId) {
88+
return this._roomIdByWidgetId[widgetId];
89+
}
90+
91+
setRoomId(widgetId, roomId) {
92+
this._roomIdByWidgetId[widgetId] = roomId;
93+
}
94+
95+
delRoomId(widgetId) {
96+
delete this._roomIdByWidgetId[widgetId];
97+
}
7998
}
8099

81100
if (global.singletonActiveWidgetStore === undefined) {

0 commit comments

Comments
 (0)