From ad2ed129800bbcc79544c66426f2fd70a5708dfa Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 10 May 2017 14:22:17 +0100 Subject: [PATCH] Redesign mxID chooser, add availability checking Requires https://github.com/matrix-org/matrix-js-sdk/pull/432 for availability checking. Changes: - Redesign the dialog to look more like https://github.com/vector-im/riot-web/issues/3604#issuecomment-299226875 - Attempt to fix wrong password being stored by generating one per SetMxIdDialog (there's no issue tracking this for now, I shall open one if it persists) - Backwards compatible with servers that don't support register/availability - a spinner will appear the first time a username is checked because server support can only be determined after a request. - Rate-limited by a 2s debounce - General style improvements --- src/components/structures/RoomView.js | 11 +- src/components/views/dialogs/SetMxIdDialog.js | 154 +++++++++++++++--- 2 files changed, 137 insertions(+), 28 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 848a8cc7ba..710f333322 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -775,7 +775,8 @@ module.exports = React.createClass({ const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog'); const defered = q.defer(); mxIdPromise = defered.promise; - Modal.createDialog(SetMxIdDialog, { + const close = Modal.createDialog(SetMxIdDialog, { + homeserverUrl: cli.getHomeserverUrl(), onFinished: (submitted, credentials) => { if (!submitted) { defered.reject(); @@ -783,8 +784,12 @@ module.exports = React.createClass({ } this.props.onRegistered(credentials); defered.resolve(); - } - }); + }, + onDifferentServerClicked: (ev) => { + dis.dispatch({action: 'start_registration'}); + close(); + }, + }).close; } mxIdPromise.then(() => { diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index d139a4ae3b..445b7eb77f 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -19,6 +19,11 @@ import q from 'q'; import React from 'react'; import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; +import classnames from 'classnames'; + +// The amount of time to wait for further changes to the input username before +// sending a request to the server +const USERNAME_CHECK_DEBOUNCE_MS = 2000; /** * Prompt the user to set a display name. @@ -33,9 +38,20 @@ export default React.createClass({ getInitialState: function() { return { - username : '', + // The entered username + username: '', + // Indicate ongoing work on the username + usernameBusy: false, + // Indicate error with username + usernameError: '', + // Assume the homeserver supports username checking until "M_UNRECOGNIZED" + usernameCheckSupport: true, + + // Whether the auth UI is currently being used doingUIAuth: false, - } + // Indicate error with auth + authError: '', + }; }, componentDidMount: function() { @@ -46,7 +62,28 @@ export default React.createClass({ onValueChange: function(ev) { this.setState({ - username: ev.target.value + username: ev.target.value, + usernameBusy: true, + usernameError: '', + }, () => { + if (!this.state.username || !this.state.usernameCheckSupport) { + this.setState({ + usernameBusy: false, + }); + return; + } + + // Debounce the username check to limit number of requests sent + if (this._usernameCheckTimeout) { + clearTimeout(this._usernameCheckTimeout); + } + this._usernameCheckTimeout = setTimeout(() => { + this._doUsernameCheck().finally(() => { + this.setState({ + usernameBusy: false, + }); + }); + }, USERNAME_CHECK_DEBOUNCE_MS); }); }, @@ -56,6 +93,40 @@ export default React.createClass({ }); }, + _doUsernameCheck: function() { + // Check if username is available + return this._matrixClient.isUsernameAvailable(this.state.username).then( + (isAvailable) => { + if (isAvailable) { + this.setState({usernameError: ''}); + } + }, + (err) => { + // Indicate whether the homeserver supports username checking + const newState = { + usernameCheckSupport: err.errcode !== "M_UNRECOGNIZED", + }; + switch (err.errcode) { + case "M_USER_IN_USE": + newState.usernameError = 'Username not available'; + break; + case "M_INVALID_USERNAME": + newState.usernameError = 'Username invalid: ' + err.message; + break; + case "M_UNRECOGNIZED": + // This homeserver doesn't support username checking, assume it's + // fine and rely on the error appearing in registration step. + newState.usernameError = ''; + break; + default: + newState.usernameError = 'An error occurred' + err.message; + break; + } + this.setState(newState); + }, + ); + }, + _generatePassword: function() { return Math.random().toString(36).slice(2); }, @@ -63,8 +134,9 @@ export default React.createClass({ _makeRegisterRequest: function(auth) { // Not upgrading - changing mxids const guestAccessToken = null; - this._generatedPassword = this._generatePassword(); - + if (!this._generatedPassword) { + this._generatedPassword = this._generatePassword(); + } return this._matrixClient.register( this.state.username, this._generatedPassword, @@ -79,10 +151,9 @@ export default React.createClass({ this.setState({ doingUIAuth: false, }); - console.info('Auth Finsihed', arguments); if (!success) { - this.setState({ errorText : response.message }); + this.setState({ authError: response.message }); return; } @@ -104,6 +175,7 @@ export default React.createClass({ const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth'); const Spinner = sdk.getComponent('elements.Spinner'); + let auth; if (this.state.doingUIAuth) { auth = ; } + const inputClasses = classnames({ + "mx_SetMxIdDialog_input": true, + "error": Boolean(this.state.usernameError), + }); + + let usernameIndicator = null; + let usernameBusyIndicator = null; + if (this.state.usernameBusy) { + usernameBusyIndicator = ; + } else { + const usernameAvailable = this.state.username && + this.state.usernameCheckSupport && !this.state.usernameError; + const usernameIndicatorClasses = classnames({ + "error": Boolean(this.state.usernameError), + "success": usernameAvailable, + }); + usernameIndicator =
+ { usernameAvailable ? 'Username available' : this.state.usernameError } +
; + } + + let authErrorIndicator = null; + if (this.state.authError) { + authErrorIndicator =
+ { this.state.authError } +
; + } + const canContinue = this.state.username && + !this.state.usernameError && + !this.state.usernameBusy; + return (
-

- Beyond this point you're going to need to pick a username - your - unique identifier in Riot. -

-

- - You can't change your username, but you can always choose how you - appear to other people in Riot by changing your display name. - -

- - { auth } -
- { this.state.errorText } +
+ + { usernameBusyIndicator }
+ { usernameIndicator } +

+ This will be your account name on + the {this.props.homeserverUrl} homeserver, + or you can pick a  + + different server + . +

+ { auth } + { authErrorIndicator }