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.
This commit is contained in:
David Baker 2018-11-21 16:56:44 +00:00
parent 93cd3b4206
commit 985966f8be
8 changed files with 66 additions and 47 deletions

View file

@ -1,4 +1,4 @@
{ {
"presets": ["react", "es2015", "es2016"], "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"]
} }

View file

@ -53,6 +53,7 @@
"test-multi": "karma start" "test-multi": "karma start"
}, },
"dependencies": { "dependencies": {
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"babel-runtime": "^6.26.0", "babel-runtime": "^6.26.0",
"bluebird": "^3.5.0", "bluebird": "^3.5.0",
"blueimp-canvas-to-blob": "^3.5.0", "blueimp-canvas-to-blob": "^3.5.0",

View file

@ -23,6 +23,7 @@ import PropTypes from 'prop-types';
import Analytics from './Analytics'; import Analytics from './Analytics';
import sdk from './index'; import sdk from './index';
import dis from './dispatcher'; import dis from './dispatcher';
import { _t } from './languageHandler';
const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
@ -32,15 +33,15 @@ const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
*/ */
const AsyncWrapper = React.createClass({ const AsyncWrapper = React.createClass({
propTypes: { propTypes: {
/** A function which takes a 'callback' argument which it will call /** A promise which resolves with the real component
* with the real component once it loads.
*/ */
loader: PropTypes.func.isRequired, prom: PropTypes.object.isRequired,
}, },
getInitialState: function() { getInitialState: function() {
return { return {
component: null, component: null,
error: null,
}; };
}, },
@ -49,14 +50,20 @@ const AsyncWrapper = React.createClass({
// XXX: temporary logging to try to diagnose // XXX: temporary logging to try to diagnose
// https://github.com/vector-im/riot-web/issues/3148 // https://github.com/vector-im/riot-web/issues/3148
console.log('Starting load of AsyncWrapper for modal'); console.log('Starting load of AsyncWrapper for modal');
this.props.loader((e) => { this.props.prom.then((result) => {
// XXX: temporary logging to try to diagnose // XXX: temporary logging to try to diagnose
// https://github.com/vector-im/riot-web/issues/3148 // 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) { if (this._unmounted) {
return; 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; this._unmounted = true;
}, },
_onWrapperCancelClick: function() {
this.props.onFinished(false);
},
render: function() { render: function() {
const {loader, ...otherProps} = this.props; const {loader, ...otherProps} = this.props;
if (this.state.component) { if (this.state.component) {
const Component = this.state.component; const Component = this.state.component;
return <Component {...otherProps} />; return <Component {...otherProps} />;
} else if (this.state.error) {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <BaseDialog onFinished={this.props.onFinished}
title={_t("Error")}
>
{_t("Unable to load! Check your network connectivity and try again.")}
<DialogButtons primaryButton={_t("Dismiss")}
onPrimaryButtonClick={this._onWrapperCancelClick}
hasCancel={false}
/>
</BaseDialog>;
} else { } else {
// show a spinner until the component is loaded. // show a spinner until the component is loaded.
const Spinner = sdk.getComponent("elements.Spinner"); const Spinner = sdk.getComponent("elements.Spinner");
@ -115,7 +138,7 @@ class ModalManager {
} }
createDialog(Element, ...rest) { createDialog(Element, ...rest) {
return this.createDialogAsync((cb) => {cb(Element);}, ...rest); return this.createDialogAsync(new Promise(resolve => resolve(Element)), ...rest);
} }
createTrackedDialogAsync(analyticsAction, analyticsInfo, ...rest) { createTrackedDialogAsync(analyticsAction, analyticsInfo, ...rest) {
@ -133,9 +156,8 @@ class ModalManager {
* require(['<module>'], cb); * require(['<module>'], cb);
* } * }
* *
* @param {Function} loader a function which takes a 'callback' argument, * @param {Promise} prom a promise which resolves with a React component
* which it should call with a React component which will be displayed as * which will be displayed as the modal view.
* the modal view.
* *
* @param {Object} props properties to pass to the displayed * @param {Object} props properties to pass to the displayed
* component. (We will also pass an 'onFinished' property.) * component. (We will also pass an 'onFinished' property.)
@ -147,7 +169,7 @@ class ModalManager {
* Also, when closed, all modals will be removed * Also, when closed, all modals will be removed
* from the stack. * from the stack.
*/ */
createDialogAsync(loader, props, className, isPriorityModal) { createDialogAsync(prom, props, className, isPriorityModal) {
const self = this; const self = this;
const modal = {}; const modal = {};
@ -178,7 +200,7 @@ class ModalManager {
// FIXME: If a dialog uses getDefaultProps it clobbers the onFinished // FIXME: If a dialog uses getDefaultProps it clobbers the onFinished
// property set here so you can't close the dialog from a button click! // property set here so you can't close the dialog from a button click!
modal.elem = ( modal.elem = (
<AsyncWrapper key={modalCount} loader={loader} {...props} <AsyncWrapper key={modalCount} prom={prom} {...props}
onFinished={closeDialog} /> onFinished={closeDialog} />
); );
modal.onFinished = props ? props.onFinished : null; modal.onFinished = props ? props.onFinished : null;

View file

@ -589,23 +589,21 @@ module.exports = React.createClass({
}, },
_onExportE2eKeysClicked: function() { _onExportE2eKeysClicked: function() {
Modal.createTrackedDialogAsync('Export E2E Keys', '', (cb) => { Modal.createTrackedDialogAsync('Export E2E Keys', '',
require.ensure(['../../async-components/views/dialogs/ExportE2eKeysDialog'], () => { import('../../async-components/views/dialogs/ExportE2eKeysDialog'),
cb(require('../../async-components/views/dialogs/ExportE2eKeysDialog')); {
}, "e2e-export"); matrixClient: MatrixClientPeg.get(),
}, { },
matrixClient: MatrixClientPeg.get(), );
});
}, },
_onImportE2eKeysClicked: function() { _onImportE2eKeysClicked: function() {
Modal.createTrackedDialogAsync('Import E2E Keys', '', (cb) => { Modal.createTrackedDialogAsync('Import E2E Keys', '',
require.ensure(['../../async-components/views/dialogs/ImportE2eKeysDialog'], () => { import('../../async-components/views/dialogs/ImportE2eKeysDialog'),
cb(require('../../async-components/views/dialogs/ImportE2eKeysDialog')); {
}, "e2e-export"); matrixClient: MatrixClientPeg.get(),
}, { },
matrixClient: MatrixClientPeg.get(), );
});
}, },
_renderGroupSettings: function() { _renderGroupSettings: function() {

View file

@ -121,13 +121,12 @@ module.exports = React.createClass({
}, },
_onExportE2eKeysClicked: function() { _onExportE2eKeysClicked: function() {
Modal.createTrackedDialogAsync('Export E2E Keys', 'Forgot Password', (cb) => { Modal.createTrackedDialogAsync('Export E2E Keys', 'Forgot Password',
require.ensure(['../../../async-components/views/dialogs/ExportE2eKeysDialog'], () => { import('../../../async-components/views/dialogs/ExportE2eKeysDialog'),
cb(require('../../../async-components/views/dialogs/ExportE2eKeysDialog')); {
}, "e2e-export"); matrixClient: MatrixClientPeg.get(),
}, { },
matrixClient: MatrixClientPeg.get(), );
});
}, },
onInputChanged: function(stateKey, ev) { onInputChanged: function(stateKey, ev) {

View file

@ -416,11 +416,10 @@ module.exports = withMatrixClient(React.createClass({
onCryptoClicked: function(e) { onCryptoClicked: function(e) {
const event = this.props.mxEvent; const event = this.props.mxEvent;
Modal.createTrackedDialogAsync('Encrypted Event Dialog', '', (cb) => { Modal.createTrackedDialogAsync('Encrypted Event Dialog', '',
require(['../../../async-components/views/dialogs/EncryptedEventDialog'], cb); import('../../../async-components/views/dialogs/EncryptedEventDialog'),
}, { {event},
event: event, );
});
}, },
onRequestKeysClick: function() { onRequestKeysClick: function() {

View file

@ -179,13 +179,12 @@ module.exports = React.createClass({
}, },
_onExportE2eKeysClicked: function() { _onExportE2eKeysClicked: function() {
Modal.createTrackedDialogAsync('Export E2E Keys', 'Change Password', (cb) => { Modal.createTrackedDialogAsync('Export E2E Keys', 'Change Password',
require.ensure(['../../../async-components/views/dialogs/ExportE2eKeysDialog'], () => { import('../../../async-components/views/dialogs/ExportE2eKeysDialog'),
cb(require('../../../async-components/views/dialogs/ExportE2eKeysDialog')); {
}, "e2e-export"); matrixClient: MatrixClientPeg.get(),
}, { },
matrixClient: MatrixClientPeg.get(), );
});
}, },
onClickChange: function(ev) { onClickChange: function(ev) {

View file

@ -82,6 +82,8 @@
"Failed to invite users to community": "Failed to invite users to community", "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 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:", "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 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", "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", "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.", "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.", "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.", "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.", "To continue, please enter your password.": "To continue, please enter your password.",
"Password:": "Password:", "Password:": "Password:",
"Please review and accept all of the homeserver's policies": "Please review and accept all of the homeserver's policies", "Please review and accept all of the homeserver's policies": "Please review and accept all of the homeserver's policies",