Merge branch 'develop' into travis/react-warnings/3-componentCannotDidMount

This commit is contained in:
Travis Ralston 2020-04-01 11:14:19 -06:00 committed by GitHub
commit 07030a9466
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
102 changed files with 640 additions and 294 deletions

View file

@ -2,6 +2,7 @@
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -41,12 +42,30 @@ import PersistedElement from "./PersistedElement";
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
const ENABLE_REACT_PERF = false;
/**
* Does template substitution on a URL (or any string). Variables will be
* passed through encodeURIComponent.
* @param {string} uriTemplate 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'.
*/
function uriFromTemplate(uriTemplate, variables) {
let out = uriTemplate;
for (const [key, val] of Object.entries(variables)) {
out = out.replace(
'$' + key, encodeURIComponent(val),
);
}
return out;
}
export default class AppTile extends React.Component {
constructor(props) {
super(props);
// The key used for PersistedElement
this._persistKey = 'widget_' + this.props.id;
this._persistKey = 'widget_' + this.props.app.id;
this.state = this._getNewState(props);
@ -78,7 +97,7 @@ export default class AppTile extends React.Component {
// This is a function to make the impact of calling SettingsStore slightly less
const hasPermissionToLoad = () => {
const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", newProps.room.roomId);
return !!currentlyAllowedWidgets[newProps.eventId];
return !!currentlyAllowedWidgets[newProps.app.eventId];
};
const PersistedElement = sdk.getComponent("elements.PersistedElement");
@ -86,7 +105,7 @@ export default class AppTile extends React.Component {
initialising: true, // True while we are mangling the widget URL
// True while the iframe content is loading
loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey),
widgetUrl: this._addWurlParams(newProps.url),
widgetUrl: this._addWurlParams(newProps.app.url),
// 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: newProps.userId === newProps.creatorUserId || hasPermissionToLoad(),
@ -103,7 +122,7 @@ export default class AppTile extends React.Component {
* @return {Boolean} True if capability supported
*/
_hasCapability(capability) {
return ActiveWidgetStore.widgetHasCapability(this.props.id, capability);
return ActiveWidgetStore.widgetHasCapability(this.props.app.id, capability);
}
/**
@ -125,7 +144,7 @@ export default class AppTile extends React.Component {
const params = qs.parse(u.query);
// Append widget ID to query parameters
params.widgetId = this.props.id;
params.widgetId = this.props.app.id;
// Append current / parent URL, minus the hash because that will change when
// we view a different room (ie. may change for persistent widgets)
params.parentUrl = window.location.href.split('#', 2)[0];
@ -137,35 +156,33 @@ export default class AppTile extends React.Component {
isMixedContent() {
const parentContentProtocol = window.location.protocol;
const u = url.parse(this.props.url);
const u = url.parse(this.props.app.url);
const childContentProtocol = u.protocol;
if (parentContentProtocol === 'https:' && childContentProtocol !== 'https:') {
console.warn("Refusing to load mixed-content app:",
parentContentProtocol, childContentProtocol, window.location, this.props.url);
parentContentProtocol, childContentProtocol, window.location, this.props.app.url);
return true;
}
return false;
}
componentWillMount() {
componentDidMount() {
// Only fetch IM token on mount if we're showing and have permission to load
if (this.props.show && this.state.hasPermissionToLoad) {
this.setScalarToken();
}
}
componentDidMount() {
// Widget action listeners
this.dispatcherRef = dis.register(this._onAction);
}
componentWillUnmount() {
// Widget action listeners
dis.unregister(this.dispatcherRef);
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
// if it's not remaining on screen, get rid of the PersistedElement container
if (!ActiveWidgetStore.getWidgetPersistence(this.props.id)) {
ActiveWidgetStore.destroyPersistentWidget(this.props.id);
if (!ActiveWidgetStore.getWidgetPersistence(this.props.app.id)) {
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
const PersistedElement = sdk.getComponent("elements.PersistedElement");
PersistedElement.destroyElement(this._persistKey);
}
@ -176,11 +193,11 @@ export default class AppTile extends React.Component {
* Component initialisation is only complete when this function has resolved
*/
setScalarToken() {
if (!WidgetUtils.isScalarUrl(this.props.url)) {
if (!WidgetUtils.isScalarUrl(this.props.app.url)) {
console.warn('Non-scalar widget, not setting scalar token!', url);
this.setState({
error: null,
widgetUrl: this._addWurlParams(this.props.url),
widgetUrl: this._addWurlParams(this.props.app.url),
initialising: false,
});
return;
@ -191,7 +208,7 @@ export default class AppTile extends React.Component {
console.warn("No integration manager - not setting scalar token", url);
this.setState({
error: null,
widgetUrl: this._addWurlParams(this.props.url),
widgetUrl: this._addWurlParams(this.props.app.url),
initialising: false,
});
return;
@ -204,7 +221,7 @@ export default class AppTile extends React.Component {
console.warn('Non-scalar manager, not setting scalar token!', url);
this.setState({
error: null,
widgetUrl: this._addWurlParams(this.props.url),
widgetUrl: this._addWurlParams(this.props.app.url),
initialising: false,
});
return;
@ -217,7 +234,7 @@ export default class AppTile extends React.Component {
this._scalarClient.getScalarToken().then((token) => {
// Append scalar_token as a query param if not already present
this._scalarClient.scalarToken = token;
const u = url.parse(this._addWurlParams(this.props.url));
const u = url.parse(this._addWurlParams(this.props.app.url));
const params = qs.parse(u.query);
if (!params.scalar_token) {
params.scalar_token = encodeURIComponent(token);
@ -246,7 +263,7 @@ export default class AppTile extends React.Component {
}
componentWillReceiveProps(nextProps) {
if (nextProps.url !== this.props.url) {
if (nextProps.app.url !== this.props.app.url) {
this._getNewState(nextProps);
// Fetch IM token for new URL if we're showing and have permission to load
if (this.props.show && this.state.hasPermissionToLoad) {
@ -280,7 +297,7 @@ export default class AppTile extends React.Component {
}
_onEditClick() {
console.log("Edit widget ID ", this.props.id);
console.log("Edit widget ID ", this.props.app.id);
if (this.props.onEditClick) {
this.props.onEditClick();
} else {
@ -289,13 +306,13 @@ export default class AppTile extends React.Component {
IntegrationManagers.sharedInstance().openAll(
this.props.room,
'type_' + this.props.type,
this.props.id,
this.props.app.id,
);
} else {
IntegrationManagers.sharedInstance().getPrimaryManager().open(
this.props.room,
'type_' + this.props.type,
this.props.id,
this.props.app.id,
);
}
}
@ -303,7 +320,7 @@ export default class AppTile extends React.Component {
_onSnapshotClick() {
console.warn("Requesting widget snapshot");
ActiveWidgetStore.getWidgetMessaging(this.props.id).getScreenshot()
ActiveWidgetStore.getWidgetMessaging(this.props.app.id).getScreenshot()
.catch((err) => {
console.error("Failed to get screenshot", err);
})
@ -351,7 +368,7 @@ export default class AppTile extends React.Component {
WidgetUtils.setRoomWidget(
this.props.room.roomId,
this.props.id,
this.props.app.id,
).catch((e) => {
console.error('Failed to delete widget', e);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@ -369,7 +386,7 @@ export default class AppTile extends React.Component {
}
_onRevokeClicked() {
console.info("Revoke widget permissions - %s", this.props.id);
console.info("Revoke widget permissions - %s", this.props.app.id);
this._revokeWidgetPermission();
}
@ -380,10 +397,10 @@ export default class AppTile extends React.Component {
// Destroy the old widget messaging before starting it back up again. Some widgets
// have startup routines that run when they are loaded, so we just need to reinitialize
// the messaging for them.
ActiveWidgetStore.delWidgetMessaging(this.props.id);
ActiveWidgetStore.delWidgetMessaging(this.props.app.id);
this._setupWidgetMessaging();
ActiveWidgetStore.setRoomId(this.props.id, this.props.room.roomId);
ActiveWidgetStore.setRoomId(this.props.app.id, this.props.room.roomId);
this.setState({loading: false});
}
@ -391,10 +408,10 @@ export default class AppTile extends React.Component {
// FIXME: There's probably no reason to do this here: it should probably be done entirely
// in ActiveWidgetStore.
const widgetMessaging = new WidgetMessaging(
this.props.id, this.props.url, this.props.userWidget, this._appFrame.current.contentWindow);
ActiveWidgetStore.setWidgetMessaging(this.props.id, widgetMessaging);
this.props.app.id, this._getRenderedUrl(), this.props.userWidget, this._appFrame.current.contentWindow);
ActiveWidgetStore.setWidgetMessaging(this.props.app.id, widgetMessaging);
widgetMessaging.getCapabilities().then((requestedCapabilities) => {
console.log(`Widget ${this.props.id} requested capabilities: ` + requestedCapabilities);
console.log(`Widget ${this.props.app.id} requested capabilities: ` + requestedCapabilities);
requestedCapabilities = requestedCapabilities || [];
// Allow whitelisted capabilities
@ -406,7 +423,7 @@ export default class AppTile extends React.Component {
}, this.props.whitelistCapabilities);
if (requestedWhitelistCapabilies.length > 0 ) {
console.warn(`Widget ${this.props.id} allowing requested, whitelisted properties: ` +
console.warn(`Widget ${this.props.app.id} allowing requested, whitelisted properties: ` +
requestedWhitelistCapabilies,
);
}
@ -414,7 +431,7 @@ export default class AppTile extends React.Component {
// TODO -- Add UI to warn about and optionally allow requested capabilities
ActiveWidgetStore.setWidgetCapabilities(this.props.id, requestedWhitelistCapabilies);
ActiveWidgetStore.setWidgetCapabilities(this.props.app.id, requestedWhitelistCapabilies);
if (this.props.onCapabilityRequest) {
this.props.onCapabilityRequest(requestedCapabilities);
@ -422,16 +439,16 @@ export default class AppTile extends React.Component {
// We only tell Jitsi widgets that we're ready because they're realistically the only ones
// using this custom extension to the widget API.
if (this.props.type === 'jitsi') {
if (this.props.app.type === 'jitsi') {
widgetMessaging.flagReadyToContinue();
}
}).catch((err) => {
console.log(`Failed to get capabilities for widget type ${this.props.type}`, this.props.id, err);
console.log(`Failed to get capabilities for widget type ${this.props.app.type}`, this.props.app.id, err);
});
}
_onAction(payload) {
if (payload.widgetId === this.props.id) {
if (payload.widgetId === this.props.app.id) {
switch (payload.action) {
case 'm.sticker':
if (this._hasCapability('m.sticker')) {
@ -460,9 +477,9 @@ export default class AppTile extends React.Component {
_grantWidgetPermission() {
const roomId = this.props.room.roomId;
console.info("Granting permission for widget to load: " + this.props.eventId);
console.info("Granting permission for widget to load: " + this.props.app.eventId);
const current = SettingsStore.getValue("allowedWidgets", roomId);
current[this.props.eventId] = true;
current[this.props.app.eventId] = true;
SettingsStore.setValue("allowedWidgets", roomId, SettingLevel.ROOM_ACCOUNT, current).then(() => {
this.setState({hasPermissionToLoad: true});
@ -476,14 +493,14 @@ export default class AppTile extends React.Component {
_revokeWidgetPermission() {
const roomId = this.props.room.roomId;
console.info("Revoking permission for widget to load: " + this.props.eventId);
console.info("Revoking permission for widget to load: " + this.props.app.eventId);
const current = SettingsStore.getValue("allowedWidgets", roomId);
current[this.props.eventId] = false;
current[this.props.app.eventId] = false;
SettingsStore.setValue("allowedWidgets", roomId, SettingLevel.ROOM_ACCOUNT, current).then(() => {
this.setState({hasPermissionToLoad: false});
// Force the widget to be non-persistent (able to be deleted/forgotten)
ActiveWidgetStore.destroyPersistentWidget(this.props.id);
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
const PersistedElement = sdk.getComponent("elements.PersistedElement");
PersistedElement.destroyElement(this._persistKey);
}).catch(err => {
@ -494,8 +511,8 @@ export default class AppTile extends React.Component {
formatAppTileName() {
let appTileName = "No name";
if (this.props.name && this.props.name.trim()) {
appTileName = this.props.name.trim();
if (this.props.app.name && this.props.app.name.trim()) {
appTileName = this.props.app.name.trim();
}
return appTileName;
}
@ -519,20 +536,78 @@ export default class AppTile extends React.Component {
}
}
_getSafeUrl() {
const parsedWidgetUrl = url.parse(this.state.widgetUrl, true);
/**
* Replace the widget template variables in a url with their values
*
* @param {string} u The URL with template variables
*
* @returns {string} url with temlate variables replaced
*/
_templatedUrl(u) {
const myUserId = MatrixClientPeg.get().credentials.userId;
const myUser = MatrixClientPeg.get().getUser(myUserId);
const vars = Object.assign({
domain: "jitsi.riot.im", // v1 widgets have this hardcoded
}, this.props.app.data, {
'matrix_user_id': myUserId,
'matrix_room_id': this.props.room.roomId,
'matrix_display_name': myUser ? myUser.displayName : myUserId,
'matrix_avatar_url': myUser ? MatrixClientPeg.get().mxcUrlToHttp(myUser.avatarUrl) : '',
// TODO: Namespace themes through some standard
'theme': SettingsStore.getValue("theme"),
});
if (vars.conferenceId === undefined) {
// we'll need to parse the conference ID out of the URL for v1 Jitsi widgets
const parsedUrl = new URL(this.props.app.url);
vars.conferenceId = parsedUrl.searchParams.get("confId");
}
return uriFromTemplate(u, vars);
}
/**
* Get the URL used in the iframe
* In cases where we supply our own UI for a widget, this is an internal
* URL different to the one used if the widget is popped out to a separate
* tab / browser
*
* @returns {string} url
*/
_getRenderedUrl() {
let url;
if (this.props.app.type === 'jitsi') {
console.log("Replacing Jitsi widget URL with local wrapper");
url = WidgetUtils.getLocalJitsiWrapperUrl({forLocalRender: true});
url = this._addWurlParams(url);
} else {
url = this._getSafeUrl(this.state.widgetUrl);
}
return this._templatedUrl(url);
}
_getPopoutUrl() {
if (this.props.app.type === 'jitsi') {
return this._templatedUrl(
WidgetUtils.getLocalJitsiWrapperUrl({forLocalRender: false}),
);
} else {
// use app.url, not state.widgetUrl, because we want the one without
// the wURL params for the popped-out version.
return this._templatedUrl(this._getSafeUrl(this.props.app.url));
}
}
_getSafeUrl(u) {
const parsedWidgetUrl = url.parse(u, true);
if (ENABLE_REACT_PERF) {
parsedWidgetUrl.search = null;
parsedWidgetUrl.query.react_perf = true;
}
let safeWidgetUrl = '';
if (ALLOWED_APP_URL_SCHEMES.includes(parsedWidgetUrl.protocol) || (
// Check if the widget URL is a Jitsi widget in Electron
parsedWidgetUrl.protocol === 'vector:'
&& parsedWidgetUrl.host === 'vector'
&& parsedWidgetUrl.pathname === '/webapp/jitsi.html'
&& this.props.type === 'jitsi'
)) {
if (ALLOWED_APP_URL_SCHEMES.includes(parsedWidgetUrl.protocol)) {
safeWidgetUrl = url.format(parsedWidgetUrl);
}
return safeWidgetUrl;
@ -562,9 +637,9 @@ export default class AppTile extends React.Component {
_onPopoutWidgetClick() {
// Using Object.assign workaround as the following opens in a new window instead of a new tab.
// window.open(this._getSafeUrl(), '_blank', 'noopener=yes');
// window.open(this._getPopoutUrl(), '_blank', 'noopener=yes');
Object.assign(document.createElement('a'),
{ target: '_blank', href: this._getSafeUrl(), rel: 'noreferrer noopener'}).click();
{ target: '_blank', href: this._getPopoutUrl(), rel: 'noreferrer noopener'}).click();
}
_onReloadWidgetClick() {
@ -641,7 +716,7 @@ export default class AppTile extends React.Component {
<iframe
allow={iframeFeatures}
ref={this._appFrame}
src={this._getSafeUrl()}
src={this._getRenderedUrl()}
allowFullScreen={true}
sandbox={sandboxFlags}
onLoad={this._onLoaded} />
@ -706,7 +781,7 @@ export default class AppTile extends React.Component {
}
return <React.Fragment>
<div className={appTileClass} id={this.props.id}>
<div className={appTileClass} id={this.props.app.id}>
{ this.props.showMenubar &&
<div ref={this._menu_bar} className={menuBarClasses} onClick={this.onClickMenuBar}>
<span className="mx_AppTileMenuBarTitle" style={{pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false)}}>
@ -753,12 +828,8 @@ export default class AppTile extends React.Component {
AppTile.displayName = 'AppTile';
AppTile.propTypes = {
id: PropTypes.string.isRequired,
eventId: PropTypes.string, // required for room widgets
url: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
app: PropTypes.object.isRequired,
room: PropTypes.object.isRequired,
type: PropTypes.string.isRequired,
// 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,
@ -805,7 +876,6 @@ AppTile.propTypes = {
};
AppTile.defaultProps = {
url: "",
waitForIframeLoad: true,
showMenubar: true,
showTitle: true,

View file

@ -38,7 +38,7 @@ export default createReactClass({
};
},
componentWillMount: function() {
componentDidMount: function() {
const cli = MatrixClientPeg.get();
cli.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
},

View file

@ -42,7 +42,7 @@ export default class EditableTextContainer extends React.Component {
this._onValueChanged = this._onValueChanged.bind(this);
}
componentWillMount() {
componentDidMount() {
if (this.props.getInitialValue === undefined) {
// use whatever was given in the initialValue property.
return;

View file

@ -24,8 +24,8 @@ import SettingsStore from "../../../settings/SettingsStore";
import { _t } from "../../../languageHandler";
function languageMatchesSearchQuery(query, language) {
if (language.label.toUpperCase().indexOf(query.toUpperCase()) == 0) return true;
if (language.value.toUpperCase() == query.toUpperCase()) return true;
if (language.label.toUpperCase().includes(query.toUpperCase())) return true;
if (language.value.toUpperCase() === query.toUpperCase()) return true;
return false;
}
@ -40,7 +40,7 @@ export default class LanguageDropdown extends React.Component {
};
}
componentWillMount() {
componentDidMount() {
languageHandler.getAllLanguagesFromJson().then((langs) => {
langs.sort(function(a, b) {
if (a.label < b.label) return -1;

View file

@ -1,6 +1,6 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -33,7 +33,7 @@ export default createReactClass({
};
},
componentWillMount: function() {
componentDidMount: function() {
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
ActiveWidgetStore.on('update', this._onActiveWidgetStoreUpdate);
},
@ -75,11 +75,7 @@ export default createReactClass({
const AppTile = sdk.getComponent('elements.AppTile');
return <AppTile
key={app.id}
id={app.id}
eventId={app.eventId}
url={app.url}
name={app.name}
type={app.type}
app={app}
fullWidth={true}
room={persistentWidgetInRoom}
userId={MatrixClientPeg.get().credentials.userId}

View file

@ -155,10 +155,10 @@ const Pill = createReactClass({
this.setState({resourceId, pillType, member, group, room});
},
componentWillMount() {
componentDidMount() {
this._unmounted = false;
this._matrixClient = MatrixClientPeg.get();
this.componentWillReceiveProps(this.props);
this.componentWillReceiveProps(this.props); // HACK: We shouldn't be calling lifecycle functions ourselves.
},
componentWillUnmount() {

View file

@ -62,7 +62,7 @@ export default createReactClass({
};
},
componentWillMount: function() {
componentDidMount: function() {
// TODO: [REACT-WARNING] Move this to class constructor
this._initStateFromProps(this.props);
},

View file

@ -184,7 +184,7 @@ export default class ReplyThread extends React.Component {
ref={ref} permalinkCreator={permalinkCreator} />;
}
componentWillMount() {
componentDidMount() {
this.unmounted = false;
this.room = this.context.getRoom(this.props.parentEv.getRoomId());
this.room.on("Room.redaction", this.onRoomRedaction);

View file

@ -36,11 +36,9 @@ const TintableSvg = createReactClass({
idSequence: 0,
},
componentWillMount: function() {
this.fixups = [];
},
componentDidMount: function() {
this.fixups = [];
this.id = TintableSvg.idSequence++;
TintableSvg.mounts[this.id] = this;
},