-
-
Notifications
You must be signed in to change notification settings - Fork 827
Improve widget rendering on prop updates #1548
Changes from all commits
7662b5a
0a7273b
3756ce6
648b295
1cb878b
355d69b
6012b35
35b3326
758df29
17c0405
a52bb9d
0e854ee
853ada0
70c4100
eb8c150
d3784b4
96de72a
ca1ffdf
be0a76d
b2b07d9
56581ef
8016fb8
f796bc7
da8b1ff
98ac3dd
bd6b5c4
d2070a0
ba8a9f2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,7 @@ limitations under the License. | |
'use strict'; | ||
|
||
import url from 'url'; | ||
import qs from 'querystring'; | ||
import React from 'react'; | ||
import MatrixClientPeg from '../../../MatrixClientPeg'; | ||
import PlatformPeg from '../../../PlatformPeg'; | ||
|
@@ -51,42 +52,63 @@ export default React.createClass({ | |
creatorUserId: React.PropTypes.string, | ||
}, | ||
|
||
getDefaultProps: function() { | ||
getDefaultProps() { | ||
return { | ||
url: "", | ||
}; | ||
}, | ||
|
||
getInitialState: function() { | ||
const widgetPermissionId = [this.props.room.roomId, encodeURIComponent(this.props.url)].join('_'); | ||
/** | ||
* Set initial component state when the App wUrl (widget URL) is being updated. | ||
* Component props *must* be passed (rather than relying on this.props). | ||
* @param {Object} newProps The new properties of the component | ||
* @return {Object} Updated component state to be set with setState | ||
*/ | ||
_getNewState(newProps) { | ||
const widgetPermissionId = [newProps.room.roomId, encodeURIComponent(newProps.url)].join('_'); | ||
const hasPermissionToLoad = localStorage.getItem(widgetPermissionId); | ||
return { | ||
loading: false, | ||
widgetUrl: this.props.url, | ||
initialising: true, // True while we are mangling the widget URL | ||
loading: true, // True while the iframe content is loading | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. likewise for this one There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As discussed, leaving this for the time being. However, may need a refactor for improved clarity in future. |
||
widgetUrl: newProps.url, | ||
widgetPermissionId: widgetPermissionId, | ||
// 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 | ||
hasPermissionToLoad: hasPermissionToLoad === 'true' || this.props.userId === this.props.creatorUserId, | ||
// 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 | ||
hasPermissionToLoad: hasPermissionToLoad === 'true' || newProps.userId === newProps.creatorUserId, | ||
error: null, | ||
deleting: false, | ||
}; | ||
}, | ||
|
||
// Returns true if props.url is a scalar URL, typically https://scalar.vector.im/api | ||
isScalarUrl: function() { | ||
getInitialState() { | ||
return this._getNewState(this.props); | ||
}, | ||
|
||
/** | ||
* Returns true if specified url is a scalar URL, typically https://scalar.vector.im/api | ||
* @param {[type]} url URL to check | ||
* @return {Boolean} True if specified URL is a scalar URL | ||
*/ | ||
isScalarUrl(url) { | ||
if (!url) { | ||
console.error('Scalar URL check failed. No URL specified'); | ||
return false; | ||
} | ||
|
||
let scalarUrls = SdkConfig.get().integrations_widgets_urls; | ||
if (!scalarUrls || scalarUrls.length == 0) { | ||
scalarUrls = [SdkConfig.get().integrations_rest_url]; | ||
} | ||
|
||
for (let i = 0; i < scalarUrls.length; i++) { | ||
if (this.props.url.startsWith(scalarUrls[i])) { | ||
if (url.startsWith(scalarUrls[i])) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
}, | ||
|
||
isMixedContent: function() { | ||
isMixedContent() { | ||
const parentContentProtocol = window.location.protocol; | ||
const u = url.parse(this.props.url); | ||
const childContentProtocol = u.protocol; | ||
|
@@ -98,43 +120,73 @@ export default React.createClass({ | |
return false; | ||
}, | ||
|
||
componentWillMount: function() { | ||
if (!this.isScalarUrl()) { | ||
componentWillMount() { | ||
window.addEventListener('message', this._onMessage, false); | ||
this.setScalarToken(); | ||
}, | ||
|
||
/** | ||
* Adds a scalar token to the widget URL, if required | ||
* Component initialisation is only complete when this function has resolved | ||
*/ | ||
setScalarToken() { | ||
this.setState({initialising: true}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could do this immediately preceding the getScalarToken? Then you don't have to do the initialising: false on line 138. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You do still need to always set initialising to false, as it is always set to true in the initialiser. Leaving this as is. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, that's true. |
||
|
||
if (!this.isScalarUrl(this.props.url)) { | ||
console.warn('Non-scalar widget, not setting scalar token!', url); | ||
this.setState({ | ||
error: null, | ||
widgetUrl: this.props.url, | ||
initialising: false, | ||
}); | ||
return; | ||
} | ||
// Fetch the token before loading the iframe as we need to mangle the URL | ||
this.setState({ | ||
loading: true, | ||
}); | ||
this._scalarClient = new ScalarAuthClient(); | ||
|
||
// Fetch the token before loading the iframe as we need it to mangle the URL | ||
if (!this._scalarClient) { | ||
this._scalarClient = new ScalarAuthClient(); | ||
} | ||
this._scalarClient.getScalarToken().done((token) => { | ||
// Append scalar_token as a query param | ||
// Append scalar_token as a query param if not already present | ||
this._scalarClient.scalarToken = token; | ||
const u = url.parse(this.props.url); | ||
if (!u.search) { | ||
u.search = "?scalar_token=" + encodeURIComponent(token); | ||
} else { | ||
u.search += "&scalar_token=" + encodeURIComponent(token); | ||
const params = qs.parse(u.query); | ||
if (!params.scalar_token) { | ||
params.scalar_token = encodeURIComponent(token); | ||
// 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 | ||
u.search = undefined; | ||
u.query = params; | ||
} | ||
|
||
this.setState({ | ||
error: null, | ||
widgetUrl: u.format(), | ||
loading: false, | ||
initialising: false, | ||
}); | ||
}, (err) => { | ||
console.error("Failed to get scalar_token", err); | ||
this.setState({ | ||
error: err.message, | ||
loading: false, | ||
initialising: false, | ||
}); | ||
}); | ||
window.addEventListener('message', this._onMessage, false); | ||
}, | ||
|
||
componentWillUnmount() { | ||
window.removeEventListener('message', this._onMessage); | ||
}, | ||
|
||
componentWillReceiveProps(nextProps) { | ||
if (nextProps.url !== this.props.url) { | ||
this._getNewState(nextProps); | ||
this.setScalarToken(); | ||
} else if (nextProps.show && !this.props.show) { | ||
this.setState({ | ||
loading: true, | ||
}); | ||
} | ||
}, | ||
|
||
_onMessage(event) { | ||
if (this.props.type !== 'jitsi') { | ||
return; | ||
|
@@ -154,11 +206,11 @@ export default React.createClass({ | |
} | ||
}, | ||
|
||
_canUserModify: function() { | ||
_canUserModify() { | ||
return WidgetUtils.canUserModifyWidgets(this.props.room.roomId); | ||
}, | ||
|
||
_onEditClick: function(e) { | ||
_onEditClick(e) { | ||
console.log("Edit widget ID ", this.props.id); | ||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); | ||
const src = this._scalarClient.getScalarInterfaceUrlForRoom( | ||
|
@@ -168,9 +220,10 @@ export default React.createClass({ | |
}, "mx_IntegrationsManager"); | ||
}, | ||
|
||
/* If user has permission to modify widgets, delete the widget, otherwise revoke access for the widget to load in the user's browser | ||
/* If user has permission to modify widgets, delete the widget, | ||
* otherwise revoke access for the widget to load in the user's browser | ||
*/ | ||
_onDeleteClick: function() { | ||
_onDeleteClick() { | ||
if (this._canUserModify()) { | ||
// Show delete confirmation dialog | ||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); | ||
|
@@ -202,6 +255,10 @@ export default React.createClass({ | |
} | ||
}, | ||
|
||
_onLoaded() { | ||
this.setState({loading: false}); | ||
}, | ||
|
||
// Widget labels to render, depending upon user permissions | ||
// These strings are translated at the point that they are inserted in to the DOM, in the render method | ||
_deleteWidgetLabel() { | ||
|
@@ -224,15 +281,15 @@ export default React.createClass({ | |
this.setState({hasPermissionToLoad: false}); | ||
}, | ||
|
||
formatAppTileName: function() { | ||
formatAppTileName() { | ||
let appTileName = "No name"; | ||
if(this.props.name && this.props.name.trim()) { | ||
appTileName = this.props.name.trim(); | ||
} | ||
return appTileName; | ||
}, | ||
|
||
onClickMenuBar: function(ev) { | ||
onClickMenuBar(ev) { | ||
ev.preventDefault(); | ||
|
||
// Ignore clicks on menu bar children | ||
|
@@ -247,7 +304,7 @@ export default React.createClass({ | |
}); | ||
}, | ||
|
||
render: function() { | ||
render() { | ||
let appTileBody; | ||
|
||
// Don't render widget if it is in the process of being deleted | ||
|
@@ -269,29 +326,30 @@ export default React.createClass({ | |
} | ||
|
||
if (this.props.show) { | ||
if (this.state.loading) { | ||
appTileBody = ( | ||
<div className='mx_AppTileBody mx_AppLoading'> | ||
<MessageSpinner msg='Loading...' /> | ||
</div> | ||
); | ||
const loadingElement = ( | ||
<div className='mx_AppTileBody mx_AppLoading'> | ||
<MessageSpinner msg='Loading...' /> | ||
</div> | ||
); | ||
if (this.state.initialising) { | ||
appTileBody = loadingElement; | ||
} else if (this.state.hasPermissionToLoad == true) { | ||
if (this.isMixedContent()) { | ||
appTileBody = ( | ||
<div className="mx_AppTileBody"> | ||
<AppWarning | ||
errorMsg="Error - Mixed content" | ||
/> | ||
<AppWarning errorMsg="Error - Mixed content" /> | ||
</div> | ||
); | ||
} else { | ||
appTileBody = ( | ||
<div className="mx_AppTileBody"> | ||
<div className={this.state.loading ? 'mx_AppTileBody mx_AppLoading' : 'mx_AppTileBody'}> | ||
{ this.state.loading && loadingElement } | ||
<iframe | ||
ref="appFrame" | ||
src={safeWidgetUrl} | ||
allowFullScreen="true" | ||
sandbox={sandboxFlags} | ||
onLoad={this._onLoaded} | ||
></iframe> | ||
</div> | ||
); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ideally, this would be set to true in the function that also sets it to false, then the explanation isn't needed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both the initialising and loading flags need to be set as initial state (before other functions are called). Normally this would be set in getInitialState. However, as this code is reused to reset state at other points in the lifecycle (on URL change), I've pulled it out in to a separate function that is called from getInitialState. Unless you have major concerns, I'll leave this as is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's just nicer to read, instead of wondering "oh why did
loading
get set to false here?... where did it get to true... ah way over here"There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, that makes sense. However, given the reason for doing it like this (as described in the comment above), do you still think that changes are needed? If so, I'm not understanding the change that you are suggesting (sorry for the confusion).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We've discussed this out of band and (hopefully) cleared up the confusion on this one. I have changed it to explicitly set the state to 'initialising: true' in the setScalarToken function.