diff --git a/src/CallHandler.js b/src/CallHandler.js index c459d12e31..5bd2d20ae8 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -181,11 +181,11 @@ function _onAction(payload) { console.error("Unknown conf call type: %s", payload.type); } } - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); switch (payload.action) { case 'place_call': if (module.exports.getAnyActiveCall()) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Existing Call", description: "You are already in a call." @@ -195,6 +195,7 @@ function _onAction(payload) { // if the runtime env doesn't do VoIP, whine. if (!MatrixClientPeg.get().supportsVoip()) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "VoIP is unsupported", description: "You cannot place VoIP calls in this browser." @@ -210,7 +211,7 @@ function _onAction(payload) { var members = room.getJoinedMembers(); if (members.length <= 1) { - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { description: "You cannot place a call with yourself." }); @@ -236,11 +237,13 @@ function _onAction(payload) { case 'place_conference_call': console.log("Place conference call in %s", payload.room_id); if (!ConferenceHandler) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { description: "Conference calls are not supported in this client" }); } else if (!MatrixClientPeg.get().supportsVoip()) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "VoIP is unsupported", description: "You cannot place VoIP calls in this browser." diff --git a/src/Lifecycle.js b/src/Lifecycle.js new file mode 100644 index 0000000000..163e6e9463 --- /dev/null +++ b/src/Lifecycle.js @@ -0,0 +1,118 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import MatrixClientPeg from './MatrixClientPeg'; +import Notifier from './Notifier' +import UserActivity from './UserActivity'; +import Presence from './Presence'; +import dis from './dispatcher'; + +/** + * Transitions to a logged-in state using the given credentials + * @param {string} credentials.homeserverUrl The base HS URL + * @param {string} credentials.identityServerUrl The base IS URL + * @param {string} credentials.userId The full Matrix User ID + * @param {string} credentials.accessToken The session access token + * @param {boolean} credentials.guest True if the session is a guest session + */ +function setLoggedIn(credentials) { + credentials.guest = Boolean(credentials.guest); + console.log("onLoggedIn => %s (guest=%s)", credentials.userId, credentials.guest); + MatrixClientPeg.replaceUsingCreds(credentials); + + dis.dispatch({action: 'on_logged_in'}); + + startMatrixClient(); +} + +/** + * Logs the current session out and transitions to the logged-out state + */ +function logout() { + if (MatrixClientPeg.get().isGuest()) { + // logout doesn't work for guest sessions + // Also we sometimes want to re-log in a guest session + // if we abort the login + _onLoggedOut(); + return; + } + + return MatrixClientPeg.get().logout().then(_onLoggedOut, + (err) => { + // Just throwing an error here is going to be very unhelpful + // if you're trying to log out because your server's down and + // you want to log into a different server, so just forget the + // access token. It's annoying that this will leave the access + // token still valid, but we should fix this by having access + // tokens expire (and if you really think you've been compromised, + // change your password). + console.log("Failed to call logout API: token will not be invalidated"); + _onLoggedOut(); + } + ); +} + +/** + * Starts the matrix client and all other react-sdk services that + * listen for events while a session is logged in. + */ +function startMatrixClient() { + // dispatch this before starting the matrix client: it's used + // to add listeners for the 'sync' event so otherwise we'd have + // a race condition (and we need to dispatch synchronously for this + // to work). + dis.dispatch({action: 'will_start_client'}, true); + + Notifier.start(); + UserActivity.start(); + Presence.start(); + + // the react sdk doesn't work without this, so don't allow + // it to be overridden (and modify the global object so at + // at least the app can see we've changed it) + MatrixClientPeg.opts.pendingEventOrdering = "detached"; + MatrixClientPeg.get().startClient(MatrixClientPeg.opts); +} + +function _onLoggedOut() { + if (window.localStorage) { + const hsUrl = window.localStorage.getItem("mx_hs_url"); + const isUrl = window.localStorage.getItem("mx_is_url"); + window.localStorage.clear(); + // preserve our HS & IS URLs for convenience + // N.B. we cache them in hsUrl/isUrl and can't really inline them + // as getCurrentHsUrl() may call through to localStorage. + if (hsUrl) window.localStorage.setItem("mx_hs_url", hsUrl); + if (isUrl) window.localStorage.setItem("mx_is_url", isUrl); + } + _stopMatrixClient(); + + dis.dispatch({action: 'on_logged_out'}); +} + +// stop all the background processes related to the current client +function _stopMatrixClient() { + Notifier.stop(); + UserActivity.stop(); + Presence.stop(); + MatrixClientPeg.get().stopClient(); + MatrixClientPeg.get().removeAllListeners(); + MatrixClientPeg.unset(); +} + +module.exports = { + setLoggedIn, logout, startMatrixClient +}; diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index ce4b5ba743..c8b015f99f 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -31,6 +31,14 @@ function deviceId() { return id; } +interface MatrixClientCreds { + homeserverUrl: string, + identityServerUrl: string, + userId: string, + accessToken: string, + guest: boolean, +} + /** * Wrapper object for handling the js-sdk Matrix Client object in the react-sdk * Handles the creation/initialisation of client objects. @@ -40,6 +48,14 @@ function deviceId() { class MatrixClientPeg { constructor() { this.matrixClient = null; + + // These are the default options used when Lifecycle.js + // starts the client. These can be altered at any + // time up to after the 'will_start_client' event is + // finished processing. + this.opts = { + initialSyncLimit: 20, + }; } get(): MatrixClient { @@ -62,8 +78,14 @@ class MatrixClientPeg { * Replace this MatrixClientPeg's client with a client instance that has * Home Server / Identity Server URLs and active credentials */ - replaceUsingAccessToken(hs_url, is_url, user_id, access_token, isGuest) { - this._replaceClient(hs_url, is_url, user_id, access_token, isGuest); + replaceUsingCreds(creds: MatrixClientCreds) { + this._replaceClient( + creds.homeserverUrl, + creds.identityServerUrl, + creds.userId, + creds.accessToken, + creds.guest, + ); } _replaceClient(hs_url, is_url, user_id, access_token, isGuest) { @@ -95,14 +117,14 @@ class MatrixClientPeg { } } - getCredentials() { - return [ - this.matrixClient.baseUrl, - this.matrixClient.idBaseUrl, - this.matrixClient.credentials.userId, - this.matrixClient.getAccessToken(), - this.matrixClient.isGuest(), - ]; + getCredentials(): MatrixClientCreds { + return { + homeserverUrl: this.matrixClient.baseUrl, + identityServerUrl: this.matrixClient.idBaseUrl, + userId: this.matrixClient.credentials.userId, + accessToken: this.matrixClient.getAccessToken(), + guest: this.matrixClient.isGuest(), + }; } tryRestore() { diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index dc9ca08e94..b712445dc2 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -36,6 +36,7 @@ var sdk = require('../../index'); var MatrixTools = require('../../MatrixTools'); var linkifyMatrix = require("../../linkify-matrix"); var KeyCode = require('../../KeyCode'); +var Lifecycle = require('../../Lifecycle'); var createRoom = require("../../createRoom"); @@ -140,9 +141,20 @@ module.exports = React.createClass({ componentWillMount: function() { this.favicon = new Favico({animation: 'none'}); + + // Stashed guest credentials if the user logs out + // whilst logged in as a guest user (so they can change + // their mind & log back in) + this.guestCreds = null; + + if (this.props.config.sync_timeline_limit) { + MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit; + } }, componentDidMount: function() { + let clientStarted = false; + this._autoRegisterAsGuest = false; if (this.props.enableGuest) { if (!this.getCurrentHsUrl()) { @@ -156,13 +168,14 @@ module.exports = React.createClass({ this.props.startingQueryParams.guest_access_token) { this._autoRegisterAsGuest = false; - this.onLoggedIn({ + Lifecycle.setLoggedIn({ userId: this.props.startingQueryParams.guest_user_id, accessToken: this.props.startingQueryParams.guest_access_token, homeserverUrl: this.getDefaultHsUrl(), identityServerUrl: this.getDefaultIsUrl(), guest: true }); + clientStarted = true; } else { this._autoRegisterAsGuest = true; @@ -174,7 +187,9 @@ module.exports = React.createClass({ // Don't auto-register as a guest. This applies if you refresh the page on a // logged in client THEN hit the Sign Out button. this._autoRegisterAsGuest = false; - this.startMatrixClient(); + if (!clientStarted) { + Lifecycle.startMatrixClient(); + } } this.focusComposer = false; // scrollStateMap is a map from room id to the scroll state returned by @@ -229,7 +244,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().registerGuest().done(function(creds) { console.log("Registered as guest: %s", creds.user_id); self._setAutoRegisterAsGuest(false); - self.onLoggedIn({ + Lifecycle.setLoggedIn({ userId: creds.user_id, accessToken: creds.access_token, homeserverUrl: hsUrl, @@ -260,34 +275,10 @@ module.exports = React.createClass({ var self = this; switch (payload.action) { case 'logout': - var guestCreds; if (MatrixClientPeg.get().isGuest()) { - guestCreds = { // stash our guest creds so we can backout if needed - userId: MatrixClientPeg.get().credentials.userId, - accessToken: MatrixClientPeg.get().getAccessToken(), - homeserverUrl: MatrixClientPeg.get().getHomeserverUrl(), - identityServerUrl: MatrixClientPeg.get().getIdentityServerUrl(), - guest: true - } + this.guestCreds = MatrixClientPeg.getCredentials(); } - - if (window.localStorage) { - var hsUrl = this.getCurrentHsUrl(); - var isUrl = this.getCurrentIsUrl(); - window.localStorage.clear(); - // preserve our HS & IS URLs for convenience - // N.B. we cache them in hsUrl/isUrl and can't really inline them - // as getCurrentHsUrl() may call through to localStorage. - window.localStorage.setItem("mx_hs_url", hsUrl); - window.localStorage.setItem("mx_is_url", isUrl); - } - this._stopMatrixClient(); - this.notifyNewScreen('login'); - this.replaceState({ - logged_in: false, - ready: false, - guestCreds: guestCreds, - }); + Lifecycle.logout(); break; case 'start_registration': var newState = payload.params || {}; @@ -313,7 +304,6 @@ module.exports = React.createClass({ if (this.state.logged_in) return; this.replaceState({ screen: 'login', - guestCreds: this.state.guestCreds, }); this.notifyNewScreen('login'); break; @@ -323,17 +313,12 @@ module.exports = React.createClass({ }); break; case 'start_upgrade_registration': + // stash our guest creds so we can backout if needed + this.guestCreds = MatrixClientPeg.getCredentials(); this.replaceState({ screen: "register", upgradeUsername: MatrixClientPeg.get().getUserIdLocalpart(), guestAccessToken: MatrixClientPeg.get().getAccessToken(), - guestCreds: { // stash our guest creds so we can backout if needed - userId: MatrixClientPeg.get().credentials.userId, - accessToken: MatrixClientPeg.get().getAccessToken(), - homeserverUrl: MatrixClientPeg.get().getHomeserverUrl(), - identityServerUrl: MatrixClientPeg.get().getIdentityServerUrl(), - guest: true - } }); this.notifyNewScreen('register'); break; @@ -355,10 +340,13 @@ module.exports = React.createClass({ var client = MatrixClientPeg.get(); client.loginWithToken(payload.params.loginToken).done(function(data) { - MatrixClientPeg.replaceUsingAccessToken( - client.getHomeserverUrl(), client.getIdentityServerUrl(), - data.user_id, data.access_token - ); + MatrixClientPeg.replaceUsingCreds({ + homeserverUrl: client.getHomeserverUrl(), + identityServerUrl: client.getIdentityServerUrl(), + userId: data.user_id, + accessToken: data.access_token, + guest: false, + }); self.setState({ screen: undefined, logged_in: true @@ -482,6 +470,15 @@ module.exports = React.createClass({ middleOpacity: payload.middleOpacity, }); break; + case 'on_logged_in': + this._onLoggedIn(); + break; + case 'on_logged_out': + this._onLoggedOut(); + break; + case 'will_start_client': + this._onWillStartClient(); + break; } }, @@ -592,23 +589,36 @@ module.exports = React.createClass({ this.scrollStateMap[roomId] = state; }, - onLoggedIn: function(credentials) { - credentials.guest = Boolean(credentials.guest); - console.log("onLoggedIn => %s (guest=%s)", credentials.userId, credentials.guest); - MatrixClientPeg.replaceUsingAccessToken( - credentials.homeserverUrl, credentials.identityServerUrl, - credentials.userId, credentials.accessToken, credentials.guest - ); + /** + * Called when a new logged in session has started + */ + _onLoggedIn: function(credentials) { + this.guestCreds = null; + this.notifyNewScreen(''); this.setState({ screen: undefined, - logged_in: true + logged_in: true, }); - this.startMatrixClient(); - this.notifyNewScreen(''); }, - startMatrixClient: function() { + /** + * Called when the session is logged out + */ + _onLoggedOut: function() { + this.notifyNewScreen('login'); + this.replaceState({ + logged_in: false, + ready: false, + }); + }, + + /** + * Called just before the matrix client is started + * (useful for setting listeners) + */ + _onWillStartClient() { var cli = MatrixClientPeg.get(); + var self = this; cli.on('sync', function(state, prevState) { self.updateFavicon(state, prevState); @@ -675,13 +685,6 @@ module.exports = React.createClass({ action: 'logout' }); }); - Notifier.start(); - UserActivity.start(); - Presence.start(); - cli.startClient({ - pendingEventOrdering: "detached", - initialSyncLimit: this.props.config.sync_timeline_limit || 20, - }); }, // stop all the background processes related to the current client @@ -919,12 +922,14 @@ module.exports = React.createClass({ onReturnToGuestClick: function() { // reanimate our guest login - this.onLoggedIn(this.state.guestCreds); - this.setState({ guestCreds: null }); + if (this.guestCreds) { + Lifecycle.setLoggedIn(this.guestCreds); + this.guestCreds = null; + } }, onRegistered: function(credentials) { - this.onLoggedIn(credentials); + Lifecycle.setLoggedIn(credentials); // do post-registration stuff // This now goes straight to user settings // We use _setPage since if we wait for @@ -1130,7 +1135,7 @@ module.exports = React.createClass({ onLoggedIn={this.onRegistered} onLoginClick={this.onLoginClick} onRegisterClick={this.onRegisterClick} - onCancelClick={ this.state.guestCreds ? this.onReturnToGuestClick : null } + onCancelClick={this.guestCreds ? this.onReturnToGuestClick : null} /> ); } else if (this.state.screen == 'forgot_password') { @@ -1146,7 +1151,7 @@ module.exports = React.createClass({ } else { return ( ); } diff --git a/test/test-utils.js b/test/test-utils.js index fc3aaace9f..e2ff5e8c10 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -51,7 +51,7 @@ module.exports.stubClient = function() { // 'sandbox.restore()' doesn't work correctly on inherited methods, // so we do this for each method var methods = ['get', 'unset', 'replaceUsingUrls', - 'replaceUsingAccessToken']; + 'replaceUsingCreds']; for (var i = 0; i < methods.length; i++) { sandbox.stub(peg, methods[i]); }