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

Commit 8f10ee0

Browse files
committed
Implement always-on-screen capability for widgets
As per https://github.com/matrix-org/matrix-doc/issues/1354 This is whitelisted to only jitsi widgets for now as per comment, mostly because any widget that we may make always-on-screen we need to preemptively put in a PersistedElement container, which is unnecessary for any other widget. Apologies that this does a bunch of refactoring which could have been split out separately: I only discovered what needed to be refactored in the process of doing this. Fixes element-hq/element-web#6984
1 parent b482a4c commit 8f10ee0

File tree

8 files changed

+194
-74
lines changed

8 files changed

+194
-74
lines changed

res/css/views/rooms/_EventTile.scss

-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ limitations under the License.
3131
top: 14px;
3232
left: 8px;
3333
cursor: pointer;
34-
z-index: 2;
3534
}
3635

3736
.mx_EventTile.mx_EventTile_info .mx_EventTile_avatar {

src/CallHandler.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,8 @@ function _startCallApp(roomId, type) {
444444
'email=$matrix_user_id',
445445
].join('&');
446446
const widgetUrl = (
447-
'https://scalar.vector.im/api/widgets' +
447+
//'https://scalar.vector.im/api/widgets' +
448+
'http://localhost:8620' +
448449
'/jitsi.html?' +
449450
queryString
450451
);

src/FromWidgetPostMessageApi.js

+9
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import URL from 'url';
1818
import dis from './dispatcher';
1919
import IntegrationManager from './IntegrationManager';
2020
import WidgetMessagingEndpoint from './WidgetMessagingEndpoint';
21+
import ActiveWidgetStore from './stores/ActiveWidgetStore';
2122

2223
const WIDGET_API_VERSION = '0.0.1'; // Current API version
2324
const SUPPORTED_WIDGET_API_VERSIONS = [
@@ -155,6 +156,14 @@ export default class FromWidgetPostMessageApi {
155156
const integType = (data && data.integType) ? data.integType : null;
156157
const integId = (data && data.integId) ? data.integId : null;
157158
IntegrationManager.open(integType, integId);
159+
} else if (action === 'set_always_on_screen') {
160+
// This is a new message: there is no reason to support the deprecated widgetData here
161+
const data = event.data.data;
162+
const val = data.value;
163+
164+
if (ActiveWidgetStore.widgetHasCapability(widgetId, 'm.always_on_screen')) {
165+
ActiveWidgetStore.setWidgetPersistence(widgetId, val);
166+
}
158167
} else {
159168
console.warn('Widget postMessage event unhandled');
160169
this.sendError(event, {message: 'The postMessage was unhandled'});

src/components/views/elements/AppTile.js

+47-40
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/**
22
Copyright 2017 Vector Creations Ltd
3+
Copyright 2018 New Vector Ltd
34
45
Licensed under the Apache License, Version 2.0 (the "License");
56
you may not use this file except in compliance with the License.
@@ -33,24 +34,28 @@ import AppWarning from './AppWarning';
3334
import MessageSpinner from './MessageSpinner';
3435
import WidgetUtils from '../../../utils/WidgetUtils';
3536
import dis from '../../../dispatcher';
37+
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
3638

3739
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
3840
const ENABLE_REACT_PERF = false;
3941

4042
export default class AppTile extends React.Component {
4143
constructor(props) {
4244
super(props);
45+
46+
// The key used for PersistedElement
47+
this._persistKey = 'widget_' + this.props.id;
48+
4349
this.state = this._getNewState(props);
4450

45-
this._onWidgetAction = this._onWidgetAction.bind(this);
51+
this._onAction = this._onAction.bind(this);
4652
this._onMessage = this._onMessage.bind(this);
4753
this._onLoaded = this._onLoaded.bind(this);
4854
this._onEditClick = this._onEditClick.bind(this);
4955
this._onDeleteClick = this._onDeleteClick.bind(this);
5056
this._onSnapshotClick = this._onSnapshotClick.bind(this);
5157
this.onClickMenuBar = this.onClickMenuBar.bind(this);
5258
this._onMinimiseClick = this._onMinimiseClick.bind(this);
53-
this._onInitialLoad = this._onInitialLoad.bind(this);
5459
this._grantWidgetPermission = this._grantWidgetPermission.bind(this);
5560
this._revokeWidgetPermission = this._revokeWidgetPermission.bind(this);
5661
this._onPopoutWidgetClick = this._onPopoutWidgetClick.bind(this);
@@ -66,9 +71,12 @@ export default class AppTile extends React.Component {
6671
_getNewState(newProps) {
6772
const widgetPermissionId = [newProps.room.roomId, encodeURIComponent(newProps.url)].join('_');
6873
const hasPermissionToLoad = localStorage.getItem(widgetPermissionId);
74+
75+
const PersistedElement = sdk.getComponent("elements.PersistedElement");
6976
return {
7077
initialising: true, // True while we are mangling the widget URL
71-
loading: this.props.waitForIframeLoad, // True while the iframe content is loading
78+
// True while the iframe content is loading
79+
loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey),
7280
widgetUrl: this._addWurlParams(newProps.url),
7381
widgetPermissionId: widgetPermissionId,
7482
// Assume that widget has permission to load if we are the user who
@@ -77,9 +85,6 @@ export default class AppTile extends React.Component {
7785
error: null,
7886
deleting: false,
7987
widgetPageTitle: newProps.widgetPageTitle,
80-
allowedCapabilities: (this.props.whitelistCapabilities && this.props.whitelistCapabilities.length > 0) ?
81-
this.props.whitelistCapabilities : [],
82-
requestedCapabilities: [],
8388
};
8489
}
8590

@@ -89,7 +94,7 @@ export default class AppTile extends React.Component {
8994
* @return {Boolean} True if capability supported
9095
*/
9196
_hasCapability(capability) {
92-
return this.state.allowedCapabilities.some((c) => {return c === capability;});
97+
return ActiveWidgetStore.widgetHasCapability(this.props.id, capability);
9398
}
9499

95100
/**
@@ -142,30 +147,24 @@ export default class AppTile extends React.Component {
142147
window.addEventListener('message', this._onMessage, false);
143148

144149
// Widget action listeners
145-
this.dispatcherRef = dis.register(this._onWidgetAction);
146-
}
147-
148-
componentDidUpdate() {
149-
// Allow parents to access widget messaging
150-
if (this.props.collectWidgetMessaging) {
151-
this.props.collectWidgetMessaging(this.widgetMessaging);
152-
}
150+
this.dispatcherRef = dis.register(this._onAction);
153151
}
154152

155153
componentWillUnmount() {
156154
// Widget action listeners
157155
dis.unregister(this.dispatcherRef);
158156

159-
// Widget postMessage listeners
160-
try {
161-
if (this.widgetMessaging) {
162-
this.widgetMessaging.stop();
163-
}
164-
} catch (e) {
165-
console.error('Failed to stop listening for widgetMessaging events', e.message);
166-
}
167157
// Jitsi listener
168158
window.removeEventListener('message', this._onMessage);
159+
160+
// if it's not remaining on screen, get rid of the PersistedElement container
161+
if (!ActiveWidgetStore.getWidgetPersistence(this.props.id)) {
162+
// FIXME: ActiveWidgetStore should probably worry about this?
163+
const PersistedElement = sdk.getComponent("elements.PersistedElement");
164+
PersistedElement.destroyElement(this._persistKey);
165+
ActiveWidgetStore.delWidgetMessaging(this.props.id);
166+
ActiveWidgetStore.delWidgetCapabilities(this.props.id);
167+
}
169168
}
170169

171170
/**
@@ -286,7 +285,7 @@ export default class AppTile extends React.Component {
286285

287286
_onSnapshotClick(e) {
288287
console.warn("Requesting widget snapshot");
289-
this.widgetMessaging.getScreenshot()
288+
ActiveWidgetStore.getWidgetMessaging(this.props.id).getScreenshot()
290289
.catch((err) => {
291290
console.error("Failed to get screenshot", err);
292291
})
@@ -341,19 +340,19 @@ export default class AppTile extends React.Component {
341340
* Called when widget iframe has finished loading
342341
*/
343342
_onLoaded() {
344-
if (!this.widgetMessaging) {
345-
this._onInitialLoad();
343+
if (!ActiveWidgetStore.getWidgetMessaging(this.props.id)) {
344+
this._setupWidgetMessaging();
346345
}
347346
this.setState({loading: false});
348347
}
349348

350-
/**
351-
* Called on initial load of the widget iframe
352-
*/
353-
_onInitialLoad() {
354-
this.widgetMessaging = new WidgetMessaging(this.props.id, this.props.url, this.refs.appFrame.contentWindow);
355-
this.widgetMessaging.getCapabilities().then((requestedCapabilities) => {
356-
console.log(`Widget ${this.props.id} requested capabilities:`, requestedCapabilities);
349+
_setupWidgetMessaging() {
350+
// FIXME: There's probably no reason to do this here: it should probably be done entirely
351+
// in ActiveWidgetStore.
352+
const widgetMessaging = new WidgetMessaging(this.props.id, this.props.url, this.refs.appFrame.contentWindow);
353+
ActiveWidgetStore.setWidgetMessaging(this.props.id, widgetMessaging);
354+
widgetMessaging.getCapabilities().then((requestedCapabilities) => {
355+
console.log(`Widget ${this.props.id} requested capabilities: ` + requestedCapabilities);
357356
requestedCapabilities = requestedCapabilities || [];
358357

359358
// Allow whitelisted capabilities
@@ -365,16 +364,15 @@ export default class AppTile extends React.Component {
365364
}, this.props.whitelistCapabilities);
366365

367366
if (requestedWhitelistCapabilies.length > 0 ) {
368-
console.warn(`Widget ${this.props.id} allowing requested, whitelisted properties:`,
369-
requestedWhitelistCapabilies);
367+
console.warn(`Widget ${this.props.id} allowing requested, whitelisted properties: ` +
368+
requestedWhitelistCapabilies,
369+
);
370370
}
371371
}
372372

373373
// TODO -- Add UI to warn about and optionally allow requested capabilities
374-
this.setState({
375-
requestedCapabilities,
376-
allowedCapabilities: this.state.allowedCapabilities.concat(requestedWhitelistCapabilies),
377-
});
374+
375+
ActiveWidgetStore.setWidgetCapabilities(this.props.id, requestedWhitelistCapabilies);
378376

379377
if (this.props.onCapabilityRequest) {
380378
this.props.onCapabilityRequest(requestedCapabilities);
@@ -384,7 +382,7 @@ export default class AppTile extends React.Component {
384382
});
385383
}
386384

387-
_onWidgetAction(payload) {
385+
_onAction(payload) {
388386
if (payload.widgetId === this.props.id) {
389387
switch (payload.action) {
390388
case 'm.sticker':
@@ -562,6 +560,15 @@ export default class AppTile extends React.Component {
562560
></iframe>
563561
</div>
564562
);
563+
// if the widget would be allowed to remian on screen, we must put it in
564+
// a PersistedElement from the get-go, otherwise the iframe will be
565+
// re-mounted later when we do.
566+
if (this.props.whitelistCapabilities.includes('m.always_on_screen')) {
567+
const PersistedElement = sdk.getComponent("elements.PersistedElement");
568+
appTileBody = <PersistedElement persistKey={this._persistKey}>
569+
{appTileBody}
570+
</PersistedElement>;
571+
}
565572
}
566573
} else {
567574
const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);

src/components/views/elements/PersistedElement.js

+23-1
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,12 @@ const PropTypes = require('prop-types');
2222
// of doing reusable widgets like dialog boxes & menus where we go and
2323
// pass in a custom control as the actual body.
2424

25+
function getContainer(containerId) {
26+
return document.getElementById(containerId);
27+
}
28+
2529
function getOrCreateContainer(containerId) {
26-
let container = document.getElementById(containerId);
30+
let container = getContainer(containerId);
2731

2832
if (!container) {
2933
container = document.createElement("div");
@@ -60,6 +64,24 @@ export default class PersistedElement extends React.Component {
6064
this.collectChild = this.collectChild.bind(this);
6165
}
6266

67+
/**
68+
* Removes the DOM elements created when a PersistedElement with the given
69+
* persistKey was mounted. The DOM elements will be re-added if another
70+
* PeristedElement is mounted in the future.
71+
*
72+
* @param {string} persistKey Key used to uniquely identify this PersistedElement
73+
*/
74+
static destroyElement(persistKey) {
75+
const container = getContainer('mx_persistedElement_' + persistKey);
76+
if (container) {
77+
container.remove();
78+
}
79+
}
80+
81+
static isMounted(persistKey) {
82+
return Boolean(getContainer('mx_persistedElement_' + persistKey));
83+
}
84+
6385
collectChildContainer(ref) {
6486
this.childContainer = ref;
6587
}

src/components/views/rooms/AppsDrawer.js

+25-18
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/*
22
Copyright 2017 Vector Creations Ltd
3+
Copyright 2018 New Vector Ltd
34
45
Licensed under the Apache License, Version 2.0 (the "License");
56
you may not use this file except in compliance with the License.
@@ -214,24 +215,30 @@ module.exports = React.createClass({
214215
render: function() {
215216
const enableScreenshots = SettingsStore.getValue("enableWidgetScreenshots", this.props.room.room_id);
216217

217-
const apps = this.state.apps.map(
218-
(app, index, arr) => {
219-
return (<AppTile
220-
key={app.id}
221-
id={app.id}
222-
url={app.url}
223-
name={app.name}
224-
type={app.type}
225-
fullWidth={arr.length<2 ? true : false}
226-
room={this.props.room}
227-
userId={this.props.userId}
228-
show={this.props.showApps}
229-
creatorUserId={app.creatorUserId}
230-
widgetPageTitle={(app.data && app.data.title) ? app.data.title : ''}
231-
waitForIframeLoad={app.waitForIframeLoad}
232-
whitelistCapabilities={enableScreenshots ? ["m.capability.screenshot"] : []}
233-
/>);
234-
});
218+
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");
225+
226+
return (<AppTile
227+
key={app.id}
228+
id={app.id}
229+
url={app.url}
230+
name={app.name}
231+
type={app.type}
232+
fullWidth={arr.length<2 ? true : false}
233+
room={this.props.room}
234+
userId={this.props.userId}
235+
show={this.props.showApps}
236+
creatorUserId={app.creatorUserId}
237+
widgetPageTitle={(app.data && app.data.title) ? app.data.title : ''}
238+
waitForIframeLoad={app.waitForIframeLoad}
239+
whitelistCapabilities={capWhitelist}
240+
/>);
241+
});
235242

236243
let addWidget;
237244
if (this.props.showApps &&

src/components/views/rooms/Stickerpicker.js

+4-13
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import ScalarAuthClient from '../../../ScalarAuthClient';
2424
import dis from '../../../dispatcher';
2525
import AccessibleButton from '../elements/AccessibleButton';
2626
import WidgetUtils from '../../../utils/WidgetUtils';
27+
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
2728

2829
const widgetType = 'm.stickerpicker';
2930

@@ -43,8 +44,6 @@ export default class Stickerpicker extends React.Component {
4344
this._onResize = this._onResize.bind(this);
4445
this._onFinished = this._onFinished.bind(this);
4546

46-
this._collectWidgetMessaging = this._collectWidgetMessaging.bind(this);
47-
4847
this.popoverWidth = 300;
4948
this.popoverHeight = 300;
5049

@@ -166,17 +165,10 @@ export default class Stickerpicker extends React.Component {
166165
);
167166
}
168167

169-
_collectWidgetMessaging(widgetMessaging) {
170-
this._appWidgetMessaging = widgetMessaging;
171-
172-
// Do this now instead of in componentDidMount because we might not have had the
173-
// reference to widgetMessaging when mounting
174-
this._sendVisibilityToWidget(true);
175-
}
176-
177168
_sendVisibilityToWidget(visible) {
178-
if (this._appWidgetMessaging && visible !== this._prevSentVisibility) {
179-
this._appWidgetMessaging.sendVisibility(visible);
169+
const widgetMessaging = ActiveWidgetStore.getWidgetMessaging(this.state.stickerpickerWidget.id);
170+
if (widgetMessaging && visible !== this._prevSentVisibility) {
171+
widgetMessaging.sendVisibility(visible);
180172
this._prevSentVisibility = visible;
181173
}
182174
}
@@ -217,7 +209,6 @@ export default class Stickerpicker extends React.Component {
217209
>
218210
<PersistedElement containerId="mx_persisted_stickerPicker" style={{zIndex: STICKERPICKER_Z_INDEX}}>
219211
<AppTile
220-
collectWidgetMessaging={this._collectWidgetMessaging}
221212
id={stickerpickerWidget.id}
222213
url={stickerpickerWidget.content.url}
223214
name={stickerpickerWidget.content.name}

0 commit comments

Comments
 (0)