From 985966f8beada6d3d0c47d173ab871c2d4e6a377 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 21 Nov 2018 16:56:44 +0000 Subject: [PATCH 1/2] Update async dialog interface to use promises Hopefully makes the syntax a bit nicer. Also uses ES6 async import rather than require.ensure which is now deprecated. Also also displays an error if the component fails to load rather than falling over in a heap, which is nice. --- .babelrc | 2 +- package.json | 1 + src/Modal.js | 46 ++++++++++++++----- src/components/structures/UserSettings.js | 26 +++++------ .../structures/login/ForgotPassword.js | 13 +++--- src/components/views/rooms/EventTile.js | 9 ++-- .../views/settings/ChangePassword.js | 13 +++--- src/i18n/strings/en_EN.json | 3 +- 8 files changed, 66 insertions(+), 47 deletions(-) diff --git a/.babelrc b/.babelrc index 6ba0e0dae0..fc5bd1788f 100644 --- a/.babelrc +++ b/.babelrc @@ -1,4 +1,4 @@ { "presets": ["react", "es2015", "es2016"], - "plugins": ["transform-class-properties", "transform-object-rest-spread", "transform-async-to-bluebird", "transform-runtime", "add-module-exports"] + "plugins": ["transform-class-properties", "transform-object-rest-spread", "transform-async-to-bluebird", "transform-runtime", "add-module-exports", "syntax-dynamic-import"] } diff --git a/package.json b/package.json index d2e54a2621..b0b3f57746 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "test-multi": "karma start" }, "dependencies": { + "babel-plugin-syntax-dynamic-import": "^6.18.0", "babel-runtime": "^6.26.0", "bluebird": "^3.5.0", "blueimp-canvas-to-blob": "^3.5.0", diff --git a/src/Modal.js b/src/Modal.js index 06a96824a7..f7b10041a6 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -23,6 +23,7 @@ import PropTypes from 'prop-types'; import Analytics from './Analytics'; import sdk from './index'; import dis from './dispatcher'; +import { _t } from './languageHandler'; const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; @@ -32,15 +33,15 @@ const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; */ const AsyncWrapper = React.createClass({ propTypes: { - /** A function which takes a 'callback' argument which it will call - * with the real component once it loads. + /** A promise which resolves with the real component */ - loader: PropTypes.func.isRequired, + prom: PropTypes.object.isRequired, }, getInitialState: function() { return { component: null, + error: null, }; }, @@ -49,14 +50,20 @@ const AsyncWrapper = React.createClass({ // XXX: temporary logging to try to diagnose // https://github.com/vector-im/riot-web/issues/3148 console.log('Starting load of AsyncWrapper for modal'); - this.props.loader((e) => { + this.props.prom.then((result) => { // XXX: temporary logging to try to diagnose // https://github.com/vector-im/riot-web/issues/3148 - console.log('AsyncWrapper load completed with '+e.displayName); + console.log('AsyncWrapper load completed with '+result.displayName); if (this._unmounted) { return; } - this.setState({component: e}); + // Take the 'default' member if it's there, then we support + // passing in just an import()ed module, since ES6 async import + // always returns a module *namespace*. + const component = result.default ? result.default : result; + this.setState({component}); + }).catch((e) => { + this.setState({error: e}); }); }, @@ -64,11 +71,27 @@ const AsyncWrapper = React.createClass({ this._unmounted = true; }, + _onWrapperCancelClick: function() { + this.props.onFinished(false); + }, + render: function() { const {loader, ...otherProps} = this.props; if (this.state.component) { const Component = this.state.component; return ; + } else if (this.state.error) { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return + {_t("Unable to load! Check your network connectivity and try again.")} + + ; } else { // show a spinner until the component is loaded. const Spinner = sdk.getComponent("elements.Spinner"); @@ -115,7 +138,7 @@ class ModalManager { } createDialog(Element, ...rest) { - return this.createDialogAsync((cb) => {cb(Element);}, ...rest); + return this.createDialogAsync(new Promise(resolve => resolve(Element)), ...rest); } createTrackedDialogAsync(analyticsAction, analyticsInfo, ...rest) { @@ -133,9 +156,8 @@ class ModalManager { * require([''], cb); * } * - * @param {Function} loader a function which takes a 'callback' argument, - * which it should call with a React component which will be displayed as - * the modal view. + * @param {Promise} prom a promise which resolves with a React component + * which will be displayed as the modal view. * * @param {Object} props properties to pass to the displayed * component. (We will also pass an 'onFinished' property.) @@ -147,7 +169,7 @@ class ModalManager { * Also, when closed, all modals will be removed * from the stack. */ - createDialogAsync(loader, props, className, isPriorityModal) { + createDialogAsync(prom, props, className, isPriorityModal) { const self = this; const modal = {}; @@ -178,7 +200,7 @@ class ModalManager { // FIXME: If a dialog uses getDefaultProps it clobbers the onFinished // property set here so you can't close the dialog from a button click! modal.elem = ( - ); modal.onFinished = props ? props.onFinished : null; diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index db12515285..c02e482746 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -589,23 +589,21 @@ module.exports = React.createClass({ }, _onExportE2eKeysClicked: function() { - Modal.createTrackedDialogAsync('Export E2E Keys', '', (cb) => { - require.ensure(['../../async-components/views/dialogs/ExportE2eKeysDialog'], () => { - cb(require('../../async-components/views/dialogs/ExportE2eKeysDialog')); - }, "e2e-export"); - }, { - matrixClient: MatrixClientPeg.get(), - }); + Modal.createTrackedDialogAsync('Export E2E Keys', '', + import('../../async-components/views/dialogs/ExportE2eKeysDialog'), + { + matrixClient: MatrixClientPeg.get(), + }, + ); }, _onImportE2eKeysClicked: function() { - Modal.createTrackedDialogAsync('Import E2E Keys', '', (cb) => { - require.ensure(['../../async-components/views/dialogs/ImportE2eKeysDialog'], () => { - cb(require('../../async-components/views/dialogs/ImportE2eKeysDialog')); - }, "e2e-export"); - }, { - matrixClient: MatrixClientPeg.get(), - }); + Modal.createTrackedDialogAsync('Import E2E Keys', '', + import('../../async-components/views/dialogs/ImportE2eKeysDialog'), + { + matrixClient: MatrixClientPeg.get(), + }, + ); }, _renderGroupSettings: function() { diff --git a/src/components/structures/login/ForgotPassword.js b/src/components/structures/login/ForgotPassword.js index 7e0cd5da8e..444f391258 100644 --- a/src/components/structures/login/ForgotPassword.js +++ b/src/components/structures/login/ForgotPassword.js @@ -121,13 +121,12 @@ module.exports = React.createClass({ }, _onExportE2eKeysClicked: function() { - Modal.createTrackedDialogAsync('Export E2E Keys', 'Forgot Password', (cb) => { - require.ensure(['../../../async-components/views/dialogs/ExportE2eKeysDialog'], () => { - cb(require('../../../async-components/views/dialogs/ExportE2eKeysDialog')); - }, "e2e-export"); - }, { - matrixClient: MatrixClientPeg.get(), - }); + Modal.createTrackedDialogAsync('Export E2E Keys', 'Forgot Password', + import('../../../async-components/views/dialogs/ExportE2eKeysDialog'), + { + matrixClient: MatrixClientPeg.get(), + }, + ); }, onInputChanged: function(stateKey, ev) { diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 53c73c8f84..e978bf438a 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -416,11 +416,10 @@ module.exports = withMatrixClient(React.createClass({ onCryptoClicked: function(e) { const event = this.props.mxEvent; - Modal.createTrackedDialogAsync('Encrypted Event Dialog', '', (cb) => { - require(['../../../async-components/views/dialogs/EncryptedEventDialog'], cb); - }, { - event: event, - }); + Modal.createTrackedDialogAsync('Encrypted Event Dialog', '', + import('../../../async-components/views/dialogs/EncryptedEventDialog'), + {event}, + ); }, onRequestKeysClick: function() { diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index b2ffe531b5..77c7810436 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -179,13 +179,12 @@ module.exports = React.createClass({ }, _onExportE2eKeysClicked: function() { - Modal.createTrackedDialogAsync('Export E2E Keys', 'Change Password', (cb) => { - require.ensure(['../../../async-components/views/dialogs/ExportE2eKeysDialog'], () => { - cb(require('../../../async-components/views/dialogs/ExportE2eKeysDialog')); - }, "e2e-export"); - }, { - matrixClient: MatrixClientPeg.get(), - }); + Modal.createTrackedDialogAsync('Export E2E Keys', 'Change Password', + import('../../../async-components/views/dialogs/ExportE2eKeysDialog'), + { + matrixClient: MatrixClientPeg.get(), + }, + ); }, onClickChange: function(ev) { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3dc0bb6b0a..01c4926467 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -82,6 +82,8 @@ "Failed to invite users to community": "Failed to invite users to community", "Failed to invite users to %(groupId)s": "Failed to invite users to %(groupId)s", "Failed to add the following rooms to %(groupId)s:": "Failed to add the following rooms to %(groupId)s:", + "Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.", + "Dismiss": "Dismiss", "Riot does not have permission to send you notifications - please check your browser settings": "Riot does not have permission to send you notifications - please check your browser settings", "Riot was not given permission to send notifications - please try again": "Riot was not given permission to send notifications - please try again", "Unable to enable Notifications": "Unable to enable Notifications", @@ -644,7 +646,6 @@ "You can use the custom server options to sign into other Matrix servers by specifying a different Home server URL.": "You can use the custom server options to sign into other Matrix servers by specifying a different Home server URL.", "This allows you to use this app with an existing Matrix account on a different home server.": "This allows you to use this app with an existing Matrix account on a different home server.", "You can also set a custom identity server but this will typically prevent interaction with users based on email address.": "You can also set a custom identity server but this will typically prevent interaction with users based on email address.", - "Dismiss": "Dismiss", "To continue, please enter your password.": "To continue, please enter your password.", "Password:": "Password:", "Please review and accept all of the homeserver's policies": "Please review and accept all of the homeserver's policies", From 2ba4d8a2d912ea407ca2dbaf8b92a4d5d82bc27d Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 21 Nov 2018 17:35:28 +0000 Subject: [PATCH 2/2] Remove outdated logging & log on failure --- src/Modal.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Modal.js b/src/Modal.js index f7b10041a6..960e0e5c30 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -51,9 +51,6 @@ const AsyncWrapper = React.createClass({ // https://github.com/vector-im/riot-web/issues/3148 console.log('Starting load of AsyncWrapper for modal'); this.props.prom.then((result) => { - // XXX: temporary logging to try to diagnose - // https://github.com/vector-im/riot-web/issues/3148 - console.log('AsyncWrapper load completed with '+result.displayName); if (this._unmounted) { return; } @@ -63,6 +60,7 @@ const AsyncWrapper = React.createClass({ const component = result.default ? result.default : result; this.setState({component}); }).catch((e) => { + console.warn('AsyncWrapper promise failed', e); this.setState({error: e}); }); },