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

Commit 6e1cf6c

Browse files
authored
Merge pull request #1548 from matrix-org/rxl881/widgetrendering
Improve widget rendering on prop updates
2 parents 74c6ebc + ba8a9f2 commit 6e1cf6c

File tree

2 files changed

+102
-43
lines changed

2 files changed

+102
-43
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
"matrix-js-sdk": "0.8.5",
7575
"optimist": "^0.6.1",
7676
"prop-types": "^15.5.8",
77+
"querystring": "^0.2.0",
7778
"react": "^15.4.0",
7879
"react-addons-css-transition-group": "15.3.2",
7980
"react-dom": "^15.4.0",

src/components/views/elements/AppTile.js

Lines changed: 101 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
'use strict';
1818

1919
import url from 'url';
20+
import qs from 'querystring';
2021
import React from 'react';
2122
import MatrixClientPeg from '../../../MatrixClientPeg';
2223
import PlatformPeg from '../../../PlatformPeg';
@@ -51,42 +52,63 @@ export default React.createClass({
5152
creatorUserId: React.PropTypes.string,
5253
},
5354

54-
getDefaultProps: function() {
55+
getDefaultProps() {
5556
return {
5657
url: "",
5758
};
5859
},
5960

60-
getInitialState: function() {
61-
const widgetPermissionId = [this.props.room.roomId, encodeURIComponent(this.props.url)].join('_');
61+
/**
62+
* Set initial component state when the App wUrl (widget URL) is being updated.
63+
* Component props *must* be passed (rather than relying on this.props).
64+
* @param {Object} newProps The new properties of the component
65+
* @return {Object} Updated component state to be set with setState
66+
*/
67+
_getNewState(newProps) {
68+
const widgetPermissionId = [newProps.room.roomId, encodeURIComponent(newProps.url)].join('_');
6269
const hasPermissionToLoad = localStorage.getItem(widgetPermissionId);
6370
return {
64-
loading: false,
65-
widgetUrl: this.props.url,
71+
initialising: true, // True while we are mangling the widget URL
72+
loading: true, // True while the iframe content is loading
73+
widgetUrl: newProps.url,
6674
widgetPermissionId: widgetPermissionId,
67-
// Assume that widget has permission to load if we are the user who added it to the room, or if explicitly granted by the user
68-
hasPermissionToLoad: hasPermissionToLoad === 'true' || this.props.userId === this.props.creatorUserId,
75+
// Assume that widget has permission to load if we are the user who
76+
// added it to the room, or if explicitly granted by the user
77+
hasPermissionToLoad: hasPermissionToLoad === 'true' || newProps.userId === newProps.creatorUserId,
6978
error: null,
7079
deleting: false,
7180
};
7281
},
7382

74-
// Returns true if props.url is a scalar URL, typically https://scalar.vector.im/api
75-
isScalarUrl: function() {
83+
getInitialState() {
84+
return this._getNewState(this.props);
85+
},
86+
87+
/**
88+
* Returns true if specified url is a scalar URL, typically https://scalar.vector.im/api
89+
* @param {[type]} url URL to check
90+
* @return {Boolean} True if specified URL is a scalar URL
91+
*/
92+
isScalarUrl(url) {
93+
if (!url) {
94+
console.error('Scalar URL check failed. No URL specified');
95+
return false;
96+
}
97+
7698
let scalarUrls = SdkConfig.get().integrations_widgets_urls;
7799
if (!scalarUrls || scalarUrls.length == 0) {
78100
scalarUrls = [SdkConfig.get().integrations_rest_url];
79101
}
80102

81103
for (let i = 0; i < scalarUrls.length; i++) {
82-
if (this.props.url.startsWith(scalarUrls[i])) {
104+
if (url.startsWith(scalarUrls[i])) {
83105
return true;
84106
}
85107
}
86108
return false;
87109
},
88110

89-
isMixedContent: function() {
111+
isMixedContent() {
90112
const parentContentProtocol = window.location.protocol;
91113
const u = url.parse(this.props.url);
92114
const childContentProtocol = u.protocol;
@@ -98,43 +120,73 @@ export default React.createClass({
98120
return false;
99121
},
100122

101-
componentWillMount: function() {
102-
if (!this.isScalarUrl()) {
123+
componentWillMount() {
124+
window.addEventListener('message', this._onMessage, false);
125+
this.setScalarToken();
126+
},
127+
128+
/**
129+
* Adds a scalar token to the widget URL, if required
130+
* Component initialisation is only complete when this function has resolved
131+
*/
132+
setScalarToken() {
133+
this.setState({initialising: true});
134+
135+
if (!this.isScalarUrl(this.props.url)) {
136+
console.warn('Non-scalar widget, not setting scalar token!', url);
137+
this.setState({
138+
error: null,
139+
widgetUrl: this.props.url,
140+
initialising: false,
141+
});
103142
return;
104143
}
105-
// Fetch the token before loading the iframe as we need to mangle the URL
106-
this.setState({
107-
loading: true,
108-
});
109-
this._scalarClient = new ScalarAuthClient();
144+
145+
// Fetch the token before loading the iframe as we need it to mangle the URL
146+
if (!this._scalarClient) {
147+
this._scalarClient = new ScalarAuthClient();
148+
}
110149
this._scalarClient.getScalarToken().done((token) => {
111-
// Append scalar_token as a query param
150+
// Append scalar_token as a query param if not already present
112151
this._scalarClient.scalarToken = token;
113152
const u = url.parse(this.props.url);
114-
if (!u.search) {
115-
u.search = "?scalar_token=" + encodeURIComponent(token);
116-
} else {
117-
u.search += "&scalar_token=" + encodeURIComponent(token);
153+
const params = qs.parse(u.query);
154+
if (!params.scalar_token) {
155+
params.scalar_token = encodeURIComponent(token);
156+
// u.search must be set to undefined, so that u.format() uses query paramerters - https://nodejs.org/docs/latest/api/url.html#url_url_format_url_options
157+
u.search = undefined;
158+
u.query = params;
118159
}
119160

120161
this.setState({
121162
error: null,
122163
widgetUrl: u.format(),
123-
loading: false,
164+
initialising: false,
124165
});
125166
}, (err) => {
167+
console.error("Failed to get scalar_token", err);
126168
this.setState({
127169
error: err.message,
128-
loading: false,
170+
initialising: false,
129171
});
130172
});
131-
window.addEventListener('message', this._onMessage, false);
132173
},
133174

134175
componentWillUnmount() {
135176
window.removeEventListener('message', this._onMessage);
136177
},
137178

179+
componentWillReceiveProps(nextProps) {
180+
if (nextProps.url !== this.props.url) {
181+
this._getNewState(nextProps);
182+
this.setScalarToken();
183+
} else if (nextProps.show && !this.props.show) {
184+
this.setState({
185+
loading: true,
186+
});
187+
}
188+
},
189+
138190
_onMessage(event) {
139191
if (this.props.type !== 'jitsi') {
140192
return;
@@ -154,11 +206,11 @@ export default React.createClass({
154206
}
155207
},
156208

157-
_canUserModify: function() {
209+
_canUserModify() {
158210
return WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
159211
},
160212

161-
_onEditClick: function(e) {
213+
_onEditClick(e) {
162214
console.log("Edit widget ID ", this.props.id);
163215
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
164216
const src = this._scalarClient.getScalarInterfaceUrlForRoom(
@@ -168,9 +220,10 @@ export default React.createClass({
168220
}, "mx_IntegrationsManager");
169221
},
170222

171-
/* If user has permission to modify widgets, delete the widget, otherwise revoke access for the widget to load in the user's browser
223+
/* If user has permission to modify widgets, delete the widget,
224+
* otherwise revoke access for the widget to load in the user's browser
172225
*/
173-
_onDeleteClick: function() {
226+
_onDeleteClick() {
174227
if (this._canUserModify()) {
175228
// Show delete confirmation dialog
176229
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
@@ -202,6 +255,10 @@ export default React.createClass({
202255
}
203256
},
204257

258+
_onLoaded() {
259+
this.setState({loading: false});
260+
},
261+
205262
// Widget labels to render, depending upon user permissions
206263
// These strings are translated at the point that they are inserted in to the DOM, in the render method
207264
_deleteWidgetLabel() {
@@ -224,15 +281,15 @@ export default React.createClass({
224281
this.setState({hasPermissionToLoad: false});
225282
},
226283

227-
formatAppTileName: function() {
284+
formatAppTileName() {
228285
let appTileName = "No name";
229286
if(this.props.name && this.props.name.trim()) {
230287
appTileName = this.props.name.trim();
231288
}
232289
return appTileName;
233290
},
234291

235-
onClickMenuBar: function(ev) {
292+
onClickMenuBar(ev) {
236293
ev.preventDefault();
237294

238295
// Ignore clicks on menu bar children
@@ -247,7 +304,7 @@ export default React.createClass({
247304
});
248305
},
249306

250-
render: function() {
307+
render() {
251308
let appTileBody;
252309

253310
// Don't render widget if it is in the process of being deleted
@@ -269,29 +326,30 @@ export default React.createClass({
269326
}
270327

271328
if (this.props.show) {
272-
if (this.state.loading) {
273-
appTileBody = (
274-
<div className='mx_AppTileBody mx_AppLoading'>
275-
<MessageSpinner msg='Loading...' />
276-
</div>
277-
);
329+
const loadingElement = (
330+
<div className='mx_AppTileBody mx_AppLoading'>
331+
<MessageSpinner msg='Loading...' />
332+
</div>
333+
);
334+
if (this.state.initialising) {
335+
appTileBody = loadingElement;
278336
} else if (this.state.hasPermissionToLoad == true) {
279337
if (this.isMixedContent()) {
280338
appTileBody = (
281339
<div className="mx_AppTileBody">
282-
<AppWarning
283-
errorMsg="Error - Mixed content"
284-
/>
340+
<AppWarning errorMsg="Error - Mixed content" />
285341
</div>
286342
);
287343
} else {
288344
appTileBody = (
289-
<div className="mx_AppTileBody">
345+
<div className={this.state.loading ? 'mx_AppTileBody mx_AppLoading' : 'mx_AppTileBody'}>
346+
{ this.state.loading && loadingElement }
290347
<iframe
291348
ref="appFrame"
292349
src={safeWidgetUrl}
293350
allowFullScreen="true"
294351
sandbox={sandboxFlags}
352+
onLoad={this._onLoaded}
295353
></iframe>
296354
</div>
297355
);

0 commit comments

Comments
 (0)