Merge pull request #1102 from matrix-org/rav/refactor_matrixclient_states

Refactor the state machine in MatrixChat
pull/21833/head
Richard van der Hoff 2017-06-19 08:53:50 +01:00 committed by GitHub
commit 115a3deed9
2 changed files with 122 additions and 80 deletions

View File

@ -62,6 +62,8 @@ import { _t } from './languageHandler';
* true; defines the IS to use. * true; defines the IS to use.
* *
* @returns {Promise} a promise which resolves when the above process completes. * @returns {Promise} a promise which resolves when the above process completes.
* Resolves to `true` if we ended up starting a session, or `false` if we
* failed.
*/ */
export function loadSession(opts) { export function loadSession(opts) {
const fragmentQueryParams = opts.fragmentQueryParams || {}; const fragmentQueryParams = opts.fragmentQueryParams || {};
@ -86,12 +88,12 @@ export function loadSession(opts) {
homeserverUrl: guestHsUrl, homeserverUrl: guestHsUrl,
identityServerUrl: guestIsUrl, identityServerUrl: guestIsUrl,
guest: true, guest: true,
}, true); }, true).then(() => true);
} }
return _restoreFromLocalStorage().then((success) => { return _restoreFromLocalStorage().then((success) => {
if (success) { if (success) {
return; return true;
} }
if (enableGuest) { if (enableGuest) {
@ -99,6 +101,7 @@ export function loadSession(opts) {
} }
// fall back to login screen // fall back to login screen
return false;
}); });
} }
@ -176,9 +179,10 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
homeserverUrl: hsUrl, homeserverUrl: hsUrl,
identityServerUrl: isUrl, identityServerUrl: isUrl,
guest: true, guest: true,
}, true); }, true).then(() => true);
}, (err) => { }, (err) => {
console.error("Failed to register as guest: " + err + " " + err.data); console.error("Failed to register as guest: " + err + " " + err.data);
return false;
}); });
} }

View File

@ -43,7 +43,41 @@ import createRoom from "../../createRoom";
import * as UDEHandler from '../../UnknownDeviceErrorHandler'; import * as UDEHandler from '../../UnknownDeviceErrorHandler';
import { _t, getCurrentLanguage } from '../../languageHandler'; import { _t, getCurrentLanguage } from '../../languageHandler';
/** constants for MatrixChat.state.view */
const VIEWS = {
// a special initial state which is only used at startup, while we are
// trying to re-animate a matrix client or register as a guest.
LOADING: 0,
// we are showing the login view
LOGIN: 1,
// we are showing the registration view
REGISTER: 2,
// completeing the registration flow
POST_REGISTRATION: 3,
// showing the 'forgot password' view
FORGOT_PASSWORD: 4,
// we have valid matrix credentials (either via an explicit login, via the
// initial re-animation/guest registration, or via a registration), and are
// now setting up a matrixclient to talk to it. This isn't an instant
// process because (a) we need to clear out indexeddb, and (b) we need to
// talk to the team server; while it is going on we show a big spinner.
LOGGING_IN: 5,
// we are logged in with an active matrix client.
LOGGED_IN: 6,
};
module.exports = React.createClass({ module.exports = React.createClass({
// we export this so that the integration tests can use it :-S
statics: {
VIEWS: VIEWS,
},
displayName: 'MatrixChat', displayName: 'MatrixChat',
propTypes: { propTypes: {
@ -93,8 +127,10 @@ module.exports = React.createClass({
getInitialState: function() { getInitialState: function() {
const s = { const s = {
loading: true, // the master view we are showing.
screen: undefined, view: VIEWS.LOADING,
// a thing to call showScreen with once login completes.
screenAfterLogin: this.props.initialScreenAfterLogin, screenAfterLogin: this.props.initialScreenAfterLogin,
// Stashed guest credentials if the user logs out // Stashed guest credentials if the user logs out
@ -113,8 +149,6 @@ module.exports = React.createClass({
// If we're trying to just view a user ID (i.e. /user URL), this is it // If we're trying to just view a user ID (i.e. /user URL), this is it
viewUserId: null, viewUserId: null,
loggedIn: false,
loggingIn: false,
collapse_lhs: false, collapse_lhs: false,
collapse_rhs: false, collapse_rhs: false,
ready: false, ready: false,
@ -301,10 +335,12 @@ module.exports = React.createClass({
}); });
}).catch((e) => { }).catch((e) => {
console.error("Unable to load session", e); console.error("Unable to load session", e);
}).then(()=>{ return false;
// stuff this through the dispatcher so that it happens }).then((loadedSession) => {
// after the on_logged_in action. if (!loadedSession) {
dis.dispatch({action: 'load_completed'}); // fall back to showing the login screen
dis.dispatch({action: "start_login"});
}
}); });
}).done(); }).done();
}, },
@ -325,18 +361,19 @@ module.exports = React.createClass({
} }
}, },
setStateForNewScreen: function(state) { setStateForNewView: function(state) {
if (state.view === undefined) {
throw new Error("setStateForNewView with no view!");
}
const newState = { const newState = {
screen: undefined,
viewUserId: null, viewUserId: null,
loggedIn: false,
ready: false,
}; };
Object.assign(newState, state); Object.assign(newState, state);
this.setState(newState); this.setState(newState);
}, },
onAction: function(payload) { onAction: function(payload) {
// console.log(`MatrixClientPeg.onAction: ${payload.action}`);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
@ -355,19 +392,19 @@ module.exports = React.createClass({
guestCreds: MatrixClientPeg.getCredentials(), guestCreds: MatrixClientPeg.getCredentials(),
}); });
} }
this.setStateForNewScreen({ this.setStateForNewView({
screen: 'login', view: VIEWS.LOGIN,
}); });
this.notifyNewScreen('login'); this.notifyNewScreen('login');
break; break;
case 'start_post_registration': case 'start_post_registration':
this.setState({ // don't clobber loggedIn status this.setState({
screen: 'post_registration', view: VIEWS.POST_REGISTRATION,
}); });
break; break;
case 'start_password_recovery': case 'start_password_recovery':
this.setStateForNewScreen({ this.setStateForNewView({
screen: 'forgot_password', view: VIEWS.FORGOT_PASSWORD,
}); });
this.notifyNewScreen('forgot_password'); this.notifyNewScreen('forgot_password');
break; break;
@ -511,7 +548,10 @@ module.exports = React.createClass({
// and also that we're not ready (we'll be marked as logged // and also that we're not ready (we'll be marked as logged
// in once the login completes, then ready once the sync // in once the login completes, then ready once the sync
// completes). // completes).
this.setState({loggingIn: true, ready: false}); this.setStateForNewView({
view: VIEWS.LOGGING_IN,
ready: false,
});
break; break;
case 'on_logged_in': case 'on_logged_in':
this._onLoggedIn(payload.teamToken); this._onLoggedIn(payload.teamToken);
@ -522,9 +562,6 @@ module.exports = React.createClass({
case 'will_start_client': case 'will_start_client':
this._onWillStartClient(); this._onWillStartClient();
break; break;
case 'load_completed':
this._onLoadCompleted();
break;
case 'new_version': case 'new_version':
this.onVersion( this.onVersion(
payload.currentVersion, payload.newVersion, payload.currentVersion, payload.newVersion,
@ -548,8 +585,8 @@ module.exports = React.createClass({
}, },
_startRegistration: function(params) { _startRegistration: function(params) {
this.setStateForNewScreen({ this.setStateForNewView({
screen: 'register', view: VIEWS.REGISTER,
// these params may be undefined, but if they are, // these params may be undefined, but if they are,
// unset them from our state: we don't want to // unset them from our state: we don't want to
// resume a previous registration session if the // resume a previous registration session if the
@ -857,13 +894,6 @@ module.exports = React.createClass({
}); });
}, },
/**
* Called when the sessionloader has finished
*/
_onLoadCompleted: function() {
this.setState({loading: false});
},
/** /**
* Called whenever someone changes the theme * Called whenever someone changes the theme
* *
@ -916,9 +946,8 @@ module.exports = React.createClass({
*/ */
_onLoggedIn: function(teamToken) { _onLoggedIn: function(teamToken) {
this.setState({ this.setState({
view: VIEWS.LOGGED_IN,
guestCreds: null, guestCreds: null,
loggedIn: true,
loggingIn: false,
}); });
if (teamToken) { if (teamToken) {
@ -979,8 +1008,8 @@ module.exports = React.createClass({
*/ */
_onLoggedOut: function() { _onLoggedOut: function() {
this.notifyNewScreen('login'); this.notifyNewScreen('login');
this.setStateForNewScreen({ this.setStateForNewView({
loggedIn: false, view: VIEWS.LOGIN,
ready: false, ready: false,
collapse_lhs: false, collapse_lhs: false,
collapse_rhs: false, collapse_rhs: false,
@ -1143,7 +1172,7 @@ module.exports = React.createClass({
// we can't view a room unless we're logged in // we can't view a room unless we're logged in
// (a guest account is fine) // (a guest account is fine)
if (this.state.loggedIn) { if (this.state.view === VIEWS.LOGGED_IN) {
dis.dispatch(payload); dis.dispatch(payload);
} }
} else if (screen.indexOf('user/') == 0) { } else if (screen.indexOf('user/') == 0) {
@ -1263,7 +1292,7 @@ module.exports = React.createClass({
onFinishPostRegistration: function() { onFinishPostRegistration: function() {
// Don't confuse this with "PageType" which is the middle window to show // Don't confuse this with "PageType" which is the middle window to show
this.setState({ this.setState({
screen: undefined, view: VIEWS.LOGGED_IN,
}); });
this.showScreen("settings"); this.showScreen("settings");
}, },
@ -1352,11 +1381,9 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
// `loading` might be set to false before `loggedIn = true`, causing the default // console.log(`Rendering MatrixChat with view ${this.state.view}`);
// (`<Login>`) to be visible for a few MS (say, whilst a request is in-flight to
// the RTS). So in the meantime, use `loggingIn`, which is true between if (this.state.view === VIEWS.LOADING || this.state.view === VIEWS.LOGGING_IN) {
// actions `on_logging_in` and `on_logged_in`.
if (this.state.loading || this.state.loggingIn) {
const Spinner = sdk.getComponent('elements.Spinner'); const Spinner = sdk.getComponent('elements.Spinner');
return ( return (
<div className="mx_MatrixChat_splash"> <div className="mx_MatrixChat_splash">
@ -1366,7 +1393,7 @@ module.exports = React.createClass({
} }
// needs to be before normal PageTypes as you are logged in technically // needs to be before normal PageTypes as you are logged in technically
if (this.state.screen == 'post_registration') { if (this.state.view === VIEWS.POST_REGISTRATION) {
const PostRegistration = sdk.getComponent('structures.login.PostRegistration'); const PostRegistration = sdk.getComponent('structures.login.PostRegistration');
return ( return (
<PostRegistration <PostRegistration
@ -1374,38 +1401,42 @@ module.exports = React.createClass({
); );
} }
// `ready` and `loggedIn` may be set before `page_type` (because the if (this.state.view === VIEWS.LOGGED_IN) {
// latter is set via the dispatcher). If we don't yet have a `page_type`, // `ready` and `view==LOGGED_IN` may be set before `page_type` (because the
// keep showing the spinner for now. // latter is set via the dispatcher). If we don't yet have a `page_type`,
if (this.state.loggedIn && this.state.ready && this.state.page_type) { // keep showing the spinner for now.
/* for now, we stuff the entirety of our props and state into the LoggedInView. if (this.state.ready && this.state.page_type) {
* we should go through and figure out what we actually need to pass down, as well /* for now, we stuff the entirety of our props and state into the LoggedInView.
* as using something like redux to avoid having a billion bits of state kicking around. * we should go through and figure out what we actually need to pass down, as well
*/ * as using something like redux to avoid having a billion bits of state kicking around.
const LoggedInView = sdk.getComponent('structures.LoggedInView'); */
return ( const LoggedInView = sdk.getComponent('structures.LoggedInView');
<LoggedInView ref="loggedInView" matrixClient={MatrixClientPeg.get()} return (
onRoomCreated={this.onRoomCreated} <LoggedInView ref="loggedInView" matrixClient={MatrixClientPeg.get()}
onUserSettingsClose={this.onUserSettingsClose} onRoomCreated={this.onRoomCreated}
onRegistered={this.onRegistered} onUserSettingsClose={this.onUserSettingsClose}
currentRoomId={this.state.currentRoomId} onRegistered={this.onRegistered}
teamToken={this._teamToken} currentRoomId={this.state.currentRoomId}
{...this.props} teamToken={this._teamToken}
{...this.state} {...this.props}
/> {...this.state}
); />
} else if (this.state.loggedIn) { );
// we think we are logged in, but are still waiting for the /sync to complete } else {
const Spinner = sdk.getComponent('elements.Spinner'); // we think we are logged in, but are still waiting for the /sync to complete
return ( const Spinner = sdk.getComponent('elements.Spinner');
<div className="mx_MatrixChat_splash"> return (
<Spinner /> <div className="mx_MatrixChat_splash">
<a href="#" className="mx_MatrixChat_splashButtons" onClick={ this.onLogoutClick }> <Spinner />
{ _t('Logout') } <a href="#" className="mx_MatrixChat_splashButtons" onClick={ this.onLogoutClick }>
</a> { _t('Logout') }
</div> </a>
); </div>
} else if (this.state.screen == 'register') { );
}
}
if (this.state.view === VIEWS.REGISTER) {
const Registration = sdk.getComponent('structures.login.Registration'); const Registration = sdk.getComponent('structures.login.Registration');
return ( return (
<Registration <Registration
@ -1428,7 +1459,10 @@ module.exports = React.createClass({
onCancelClick={this.state.guestCreds ? this.onReturnToGuestClick : null} onCancelClick={this.state.guestCreds ? this.onReturnToGuestClick : null}
/> />
); );
} else if (this.state.screen == 'forgot_password') { }
if (this.state.view === VIEWS.FORGOT_PASSWORD) {
const ForgotPassword = sdk.getComponent('structures.login.ForgotPassword'); const ForgotPassword = sdk.getComponent('structures.login.ForgotPassword');
return ( return (
<ForgotPassword <ForgotPassword
@ -1440,7 +1474,9 @@ module.exports = React.createClass({
onRegisterClick={this.onRegisterClick} onRegisterClick={this.onRegisterClick}
onLoginClick={this.onLoginClick} /> onLoginClick={this.onLoginClick} />
); );
} else { }
if (this.state.view === VIEWS.LOGIN) {
const Login = sdk.getComponent('structures.login.Login'); const Login = sdk.getComponent('structures.login.Login');
return ( return (
<Login <Login
@ -1458,5 +1494,7 @@ module.exports = React.createClass({
/> />
); );
} }
console.error(`Unknown view ${this.state.view}`);
}, },
}); });