diff --git a/src/Lifecycle.js b/src/Lifecycle.js index a7a06401da..20d5836dae 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -185,6 +185,14 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) { // returns a promise which resolves to true if a session is found in // localstorage +// +// N.B. Lifecycle.js should not maintain any further localStorage state, we +// are moving towards using SessionStore to keep track of state related +// to the current session (which is typically backed by localStorage). +// +// The plan is to gradually move the localStorage access done here into +// SessionStore to avoid bugs where the view becomes out-of-sync with +// localStorage (e.g. teamToken, isGuest etc.) function _restoreFromLocalStorage() { if (!localStorage) { return q(false); @@ -289,7 +297,6 @@ export function setLoggedIn(credentials) { // Resolves by default let teamPromise = Promise.resolve(null); - let isPasswordStored = false; // persist the session if (localStorage) { @@ -312,8 +319,11 @@ export function setLoggedIn(credentials) { // The user registered as a PWLU (PassWord-Less User), the generated password // is cached here such that the user can change it at a later time. if (credentials.password) { - localStorage.setItem("mx_pass", credentials.password); - isPasswordStored = true; + // Update SessionStore + dis.dispatch({ + action: 'cached_password', + cachedPassword: credentials.password, + }); } console.log("Session persisted for %s", credentials.userId); @@ -339,10 +349,10 @@ export function setLoggedIn(credentials) { MatrixClientPeg.replaceUsingCreds(credentials); teamPromise.then((teamToken) => { - dis.dispatch({action: 'on_logged_in', teamToken: teamToken, isPasswordStored}); + dis.dispatch({action: 'on_logged_in', teamToken: teamToken}); }, (err) => { console.warn("Failed to get team token on login", err); - dis.dispatch({action: 'on_logged_in', teamToken: null, isPasswordStored}); + dis.dispatch({action: 'on_logged_in', teamToken: null}); }); startMatrixClient(); diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 4001227355..a64ae0a25c 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -23,6 +23,7 @@ import Notifier from '../../Notifier'; import PageTypes from '../../PageTypes'; import sdk from '../../index'; import dis from '../../dispatcher'; +import sessionStore from '../../stores/SessionStore'; /** * This is what our MatrixChat shows when we are logged in. The precise view is @@ -49,10 +50,6 @@ export default React.createClass({ teamToken: React.PropTypes.string, - // Has the user generated a password that is stored in local storage? - // (are they a PWLU?) - userHasGeneratedPassword: React.PropTypes.boolean, - // and lots and lots of other stuff. }, @@ -80,10 +77,19 @@ export default React.createClass({ this._scrollStateMap = {}; document.addEventListener('keydown', this._onKeyDown); + + this._sessionStore = sessionStore; + this._sessionStoreToken = this._sessionStore.addListener( + this._setStateFromSessionStore, + ); + this._setStateFromSessionStore(); }, componentWillUnmount: function() { document.removeEventListener('keydown', this._onKeyDown); + if (this._sessionStoreToken) { + this._sessionStoreToken.remove(); + } }, getScrollStateForRoom: function(roomId) { @@ -97,6 +103,12 @@ export default React.createClass({ return this.refs.roomView.canResetTimeline(); }, + _setStateFromSessionStore() { + this.setState({ + userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()), + }); + }, + _onKeyDown: function(ev) { /* // Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers @@ -257,7 +269,7 @@ export default React.createClass({ />; } else if (this.props.matrixClient.isGuest()) { topBar = ; - } else if (this.props.userHasGeneratedPassword) { + } else if (this.state.userHasGeneratedPassword) { topBar = ; } else if (Notifier.supportsDesktopNotifications() && !Notifier.isEnabled() && !Notifier.isToolbarHidden()) { topBar = ; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index ee3c601146..ee7236bb15 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -138,9 +138,6 @@ module.exports = React.createClass({ register_hs_url: null, register_is_url: null, register_id_sid: null, - - // Initially, use localStorage as source of truth - userHasGeneratedPassword: localStorage && localStorage.getItem('mx_pass'), }; return s; }, @@ -785,15 +782,11 @@ module.exports = React.createClass({ /** * Called when a new logged in session has started */ - _onLoggedIn: function(teamToken, isPasswordStored) { + _onLoggedIn: function(teamToken) { this.setState({ guestCreds: null, loggedIn: true, loggingIn: false, - // isPasswordStored only true when ROU sets a username and becomes PWLU. - // (the password was randomly generated and stored in localStorage). - userHasGeneratedPassword: - this.state.userHasGeneratedPassword || isPasswordStored, }); if (teamToken) { @@ -1206,7 +1199,6 @@ module.exports = React.createClass({ onUserSettingsClose={this.onUserSettingsClose} onRegistered={this.onRegistered} teamToken={this._teamToken} - userHasGeneratedPassword={this.state.userHasGeneratedPassword} {...this.props} {...this.state} /> diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 96c60d7cd8..998199b598 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -327,6 +327,7 @@ module.exports = React.createClass({ receive push notifications on other devices until you log back in to them.`, }); + dis.dispatch({action: 'password_changed'}); }, onUpgradeClicked: function() { diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index 20ce45e5dd..e3845390de 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -22,6 +22,8 @@ var Modal = require("../../../Modal"); var sdk = require("../../../index"); import AccessibleButton from '../elements/AccessibleButton'; +import sessionStore from '../../../stores/SessionStore'; + module.exports = React.createClass({ displayName: 'ChangePassword', propTypes: { @@ -31,7 +33,7 @@ module.exports = React.createClass({ rowClassName: React.PropTypes.string, rowLabelClassName: React.PropTypes.string, rowInputClassName: React.PropTypes.string, - buttonClassName: React.PropTypes.string + buttonClassName: React.PropTypes.string, }, Phases: { @@ -60,10 +62,32 @@ module.exports = React.createClass({ getInitialState: function() { return { - phase: this.Phases.Edit + phase: this.Phases.Edit, + cachedPassword: null, }; }, + componentWillMount: function() { + this._sessionStore = sessionStore; + this._sessionStoreToken = this._sessionStore.addListener( + this._setStateFromSessionStore, + ); + + this._setStateFromSessionStore(); + }, + + componentWillUnmount: function() { + if (this._sessionStoreToken) { + this._sessionStoreToken.remove(); + } + }, + + _setStateFromSessionStore: function() { + this.setState({ + cachedPassword: this._sessionStore.getCachedPassword(), + }); + }, + changePassword: function(old_password, new_password) { var cli = MatrixClientPeg.get(); @@ -121,10 +145,10 @@ module.exports = React.createClass({ matrixClient: MatrixClientPeg.get(), } ); - }, + }, onClickChange: function() { - var old_password = this.refs.old_input.value; + var old_password = this.state.cachedPassword || this.refs.old_input.value; var new_password = this.refs.new_input.value; var confirm_password = this.refs.confirm_input.value; var err = this.props.onCheckPassword( @@ -139,23 +163,28 @@ module.exports = React.createClass({ }, render: function() { - var rowClassName = this.props.rowClassName; - var rowLabelClassName = this.props.rowLabelClassName; - var rowInputClassName = this.props.rowInputClassName; - var buttonClassName = this.props.buttonClassName; + const rowClassName = this.props.rowClassName; + const rowLabelClassName = this.props.rowLabelClassName; + const rowInputClassName = this.props.rowInputClassName; + const buttonClassName = this.props.buttonClassName; + + let currentPassword = null; + if (!this.state.cachedPassword) { + currentPassword =
+
+ +
+
+ +
+
; + } switch (this.state.phase) { case this.Phases.Edit: return (
-
-
- -
-
- -
-
+ { currentPassword }
diff --git a/src/stores/SessionStore.js b/src/stores/SessionStore.js new file mode 100644 index 0000000000..1570f58688 --- /dev/null +++ b/src/stores/SessionStore.js @@ -0,0 +1,66 @@ +import dis from '../dispatcher'; +import {Store} from 'flux/utils'; + +/** + * A class for storing application state to do with the session. This is a simple flux + * store that listens for actions and updates its state accordingly, informing any + * listeners (views) of state changes. + * + * Usage: + * ``` + * sessionStore.addListener(() => { + * this.setState({ cachedPassword: sessionStore.getCachedPassword() }) + * }) + * ``` + */ +class SessionStore extends Store { + constructor() { + super(dis); + + // Initialise state + this._state = { + cachedPassword: localStorage.getItem('mx_pass'), + }; + } + + _update() { + // Persist state to localStorage + if (this._state.cachedPassword) { + localStorage.setItem('mx_pass', this._state.cachedPassword); + } else { + localStorage.removeItem('mx_pass', this._state.cachedPassword); + } + + this.__emitChange(); + } + + _setState(newState) { + this._state = Object.assign(this._state, newState); + this._update(); + } + + __onDispatch(payload) { + switch (payload.action) { + case 'cached_password': + this._setState({ + cachedPassword: payload.cachedPassword, + }); + break; + case 'password_changed': + this._setState({ + cachedPassword: null, + }); + break; + } + } + + getCachedPassword() { + return this._state.cachedPassword; + } +} + +let singletonSessionStore = null; +if (!singletonSessionStore) { + singletonSessionStore = new SessionStore(); +} +module.exports = singletonSessionStore;