From 8774100508cc36b5b0b96a4d78001543cdbe2f9f Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 28 Apr 2017 13:22:55 +0100 Subject: [PATCH 1/7] Initial implementation: SetDisplayName -> SetMxIdDialog - Replaces SetDisplayNameDialog with SetMxIdDialog. This new dialog will use InteractiveAuth to authenticate a user with their chosen mxid. De-scoped: - style tweaks for the InteractiveAuth in the dialog (capcha) and error message. - checking for mxid availability --- src/component-index.js | 4 +- src/components/structures/LoggedInView.js | 5 + src/components/structures/MatrixChat.js | 1 + src/components/structures/RoomView.js | 53 +++--- .../views/dialogs/SetDisplayNameDialog.js | 87 ---------- src/components/views/dialogs/SetMxIdDialog.js | 153 ++++++++++++++++++ 6 files changed, 182 insertions(+), 121 deletions(-) delete mode 100644 src/components/views/dialogs/SetDisplayNameDialog.js create mode 100644 src/components/views/dialogs/SetMxIdDialog.js diff --git a/src/component-index.js b/src/component-index.js index d6873c6dfd..2e55ad14cc 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -95,8 +95,8 @@ import views$dialogs$QuestionDialog from './components/views/dialogs/QuestionDia views$dialogs$QuestionDialog && (module.exports.components['views.dialogs.QuestionDialog'] = views$dialogs$QuestionDialog); import views$dialogs$SessionRestoreErrorDialog from './components/views/dialogs/SessionRestoreErrorDialog'; views$dialogs$SessionRestoreErrorDialog && (module.exports.components['views.dialogs.SessionRestoreErrorDialog'] = views$dialogs$SessionRestoreErrorDialog); -import views$dialogs$SetDisplayNameDialog from './components/views/dialogs/SetDisplayNameDialog'; -views$dialogs$SetDisplayNameDialog && (module.exports.components['views.dialogs.SetDisplayNameDialog'] = views$dialogs$SetDisplayNameDialog); +import views$dialogs$SetMxIdDialog from './components/views/dialogs/SetMxIdDialog'; +views$dialogs$SetMxIdDialog && (module.exports.components['views.dialogs.SetMxIdDialog'] = views$dialogs$SetMxIdDialog); import views$dialogs$TextInputDialog from './components/views/dialogs/TextInputDialog'; views$dialogs$TextInputDialog && (module.exports.components['views.dialogs.TextInputDialog'] = views$dialogs$TextInputDialog); import views$dialogs$UnknownDeviceDialog from './components/views/dialogs/UnknownDeviceDialog'; diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index c4eeb03d5f..6514b32f79 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -43,6 +43,10 @@ export default React.createClass({ onRoomCreated: React.PropTypes.func, onUserSettingsClose: React.PropTypes.func, + // Called with the credentials of a registered user (if they were a ROU that + // transitioned to PWLU) + onRegistered: React.PropTypes.func, + teamToken: React.PropTypes.string, // and lots and lots of other stuff. @@ -184,6 +188,7 @@ export default React.createClass({ roomAddress={this.props.currentRoomAlias || this.props.currentRoomId} autoJoin={this.props.autoJoin} onRoomIdResolved={this.props.onRoomIdResolved} + onRegistered={this.props.onRegistered} eventId={this.props.initialEventId} thirdPartyInvite={this.props.thirdPartyInvite} oobData={this.props.roomOobData} diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 9b8aa3426a..0fe2d6a52f 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1166,6 +1166,7 @@ module.exports = React.createClass({ onRoomIdResolved={this.onRoomIdResolved} onRoomCreated={this.onRoomCreated} onUserSettingsClose={this.onUserSettingsClose} + onRegistered={this.onRegistered} teamToken={this._teamToken} {...this.props} {...this.state} diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index a0c36374b6..83b446597c 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -69,6 +69,10 @@ module.exports = React.createClass({ // once it has been resolved. onRoomIdResolved: React.PropTypes.func, + // Called with the credentials of a registered user (if they were a ROU that + // transitioned to PWLU) + onRegistered: React.PropTypes.func, + // An object representing a third party invite to join this room // Fields: // * inviteSignUrl (string) The URL used to join this room from an email invite @@ -764,38 +768,27 @@ module.exports = React.createClass({ var self = this; var cli = MatrixClientPeg.get(); - var display_name_promise = q(); - // if this is the first room we're joining, check the user has a display name - // and if they don't, prompt them to set one. - // NB. This unfortunately does not re-use the ChangeDisplayName component because - // it doesn't behave quite as desired here (we want an input field here rather than - // content-editable, and we want a default). - if (cli.getRooms().filter((r) => { - return r.hasMembershipState(cli.credentials.userId, "join"); - })) { - display_name_promise = cli.getProfileInfo(cli.credentials.userId).then((result) => { - if (!result.displayname) { - var SetDisplayNameDialog = sdk.getComponent('views.dialogs.SetDisplayNameDialog'); - var dialog_defer = q.defer(); - Modal.createDialog(SetDisplayNameDialog, { - currentDisplayName: result.displayname, - onFinished: (submitted, newDisplayName) => { - if (submitted) { - cli.setDisplayName(newDisplayName).done(() => { - dialog_defer.resolve(); - }); - } - else { - dialog_defer.reject(); - } - } - }); - return dialog_defer.promise; + var mxIdPromise = q(); + + // If the user is a ROU, allow them to transition to a PWLU + if (cli.isGuest()) { + const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog'); + mxIdPromise = q.defer(); + Modal.createDialog(SetMxIdDialog, { + onFinished: (submitted, credentials) => { + if (!submitted) { + mxIdPromise.reject(); + } + this.props.onRegistered(credentials); + mxIdPromise.resolve(); } }); } - display_name_promise.then(() => { + mxIdPromise.then(() => { + this.setState({ + joining: true + }); // if this is an invite and has the 'direct' hint set, mark it as a DM room now. if (this.state.room) { const me = this.state.room.getMember(MatrixClientPeg.get().credentials.userId); @@ -870,10 +863,6 @@ module.exports = React.createClass({ }); } }).done(); - - this.setState({ - joining: true - }); }, onMessageListScroll: function(ev) { diff --git a/src/components/views/dialogs/SetDisplayNameDialog.js b/src/components/views/dialogs/SetDisplayNameDialog.js deleted file mode 100644 index 1047e05c26..0000000000 --- a/src/components/views/dialogs/SetDisplayNameDialog.js +++ /dev/null @@ -1,87 +0,0 @@ -/* -Copyright 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 React from 'react'; -import sdk from '../../../index'; -import MatrixClientPeg from '../../../MatrixClientPeg'; - -/** - * Prompt the user to set a display name. - * - * On success, `onFinished(true, newDisplayName)` is called. - */ -export default React.createClass({ - displayName: 'SetDisplayNameDialog', - propTypes: { - onFinished: React.PropTypes.func.isRequired, - currentDisplayName: React.PropTypes.string, - }, - - getInitialState: function() { - if (this.props.currentDisplayName) { - return { value: this.props.currentDisplayName }; - } - - if (MatrixClientPeg.get().isGuest()) { - return { value : "Guest " + MatrixClientPeg.get().getUserIdLocalpart() }; - } - else { - return { value : MatrixClientPeg.get().getUserIdLocalpart() }; - } - }, - - componentDidMount: function() { - this.refs.input_value.select(); - }, - - onValueChange: function(ev) { - this.setState({ - value: ev.target.value - }); - }, - - onFormSubmit: function(ev) { - ev.preventDefault(); - this.props.onFinished(true, this.state.value); - return false; - }, - - render: function() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - return ( - -
- Your display name is how you'll appear to others when you speak in rooms.
- What would you like it to be? -
-
-
- -
-
- -
-
-
- ); - }, -}); diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js new file mode 100644 index 0000000000..78737e4ac4 --- /dev/null +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -0,0 +1,153 @@ +/* +Copyright 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 q from 'q'; +import React from 'react'; +import sdk from '../../../index'; +import MatrixClientPeg from '../../../MatrixClientPeg'; + +/** + * Prompt the user to set a display name. + * + * On success, `onFinished(true, newDisplayName)` is called. + */ +export default React.createClass({ + displayName: 'SetMxIdDialog', + propTypes: { + onFinished: React.PropTypes.func.isRequired, + }, + + getInitialState: function() { + return { + username : '', + doingUIAuth: false, + } + }, + + componentDidMount: function() { + this.refs.input_value.select(); + + this._matrixClient = MatrixClientPeg.get(); + }, + + onValueChange: function(ev) { + this.setState({ + username: ev.target.value + }); + }, + + onSubmit: function(ev) { + this.setState({ + doingUIAuth: true, + }); + }, + + _generateAndCachePassword: function() { + const pass = Math.random().toString(36).slice(2); + if (localStorage) { + localStorage.setItem('mx_pass', pass); + } + return pass; + }, + + _makeRegisterRequest: function(auth) { + // Not upgrading - changing mxids + const guestAccessToken = null; + + return this._matrixClient.register( + this.state.username, + this._generateAndCachePassword(), + undefined, // session id: included in the auth dict already + auth, + {}, + guestAccessToken, + ); + }, + + _onUIAuthFinished: function(success, response) { + this.setState({ + doingUIAuth: false, + }); + console.info('Auth Finsihed', arguments); + + if (!success) { + this.setState({ errorText : response.message }); + return; + } + + // XXX Implement RTS /register here + const teamToken = null; + + this.props.onFinished(true, { + userId: response.user_id, + deviceId: response.device_id, + homeserverUrl: this._matrixClient.getHomeserverUrl(), + identityServerUrl: this._matrixClient.getIdentityServerUrl(), + accessToken: response.access_token, + teamToken: teamToken, + }); + }, + + render: function() { + 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 = ; + } + return ( + +
+

+ Beyond this point you're going to need to pick a username - your + unique identifire 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 } +
+
+
+ +
+
+ ); + }, +}); From 6dff4a44157bd8006a4ee6ccc97efa534bbf5b36 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 28 Apr 2017 13:28:34 +0100 Subject: [PATCH 2/7] Return early after cancelled mxid dialog --- src/components/structures/RoomView.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 83b446597c..d06c2c2226 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -778,6 +778,7 @@ module.exports = React.createClass({ onFinished: (submitted, credentials) => { if (!submitted) { mxIdPromise.reject(); + return; } this.props.onRegistered(credentials); mxIdPromise.resolve(); From d12b1903f2b528426931271c6256aa016082c00d Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 28 Apr 2017 13:29:30 +0100 Subject: [PATCH 3/7] Fix defer promise logic --- src/components/structures/RoomView.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index d06c2c2226..569c4e46f0 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -773,15 +773,16 @@ module.exports = React.createClass({ // If the user is a ROU, allow them to transition to a PWLU if (cli.isGuest()) { const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog'); - mxIdPromise = q.defer(); + const defered = q.defer(); + mxIdPromise = defered.promise; Modal.createDialog(SetMxIdDialog, { onFinished: (submitted, credentials) => { if (!submitted) { - mxIdPromise.reject(); + defered.reject(); return; } this.props.onRegistered(credentials); - mxIdPromise.resolve(); + defered.resolve(); } }); } From 5a5768a4ecc9033f64e98e146ad8628a61cf079c Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 28 Apr 2017 13:38:35 +0100 Subject: [PATCH 4/7] Try to fix tests --- src/components/structures/RoomView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 569c4e46f0..848a8cc7ba 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -771,7 +771,7 @@ module.exports = React.createClass({ var mxIdPromise = q(); // If the user is a ROU, allow them to transition to a PWLU - if (cli.isGuest()) { + if (cli && cli.isGuest()) { const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog'); const defered = q.defer(); mxIdPromise = defered.promise; From a887af9f92c8f9c9b58bcff8190eb42f1e7f6bb3 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 2 May 2017 09:56:14 +0100 Subject: [PATCH 5/7] copyright --- src/component-index.js | 1 + src/components/views/dialogs/SetMxIdDialog.js | 1 + 2 files changed, 2 insertions(+) diff --git a/src/component-index.js b/src/component-index.js index 2e55ad14cc..aa65cabbec 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index 78737e4ac4..c8083371de 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -1,5 +1,6 @@ /* Copyright 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 4f71f4c331bb36d19ccdd92d2d92425bc7cf3138 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 2 May 2017 10:07:06 +0100 Subject: [PATCH 6/7] Store mx_pass at the same point as access_token So that we don't overwrite the existing one every time we try to register. --- src/Lifecycle.js | 6 ++++++ src/components/views/dialogs/SetMxIdDialog.js | 12 +++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index f20716cae6..7fba0a0298 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -303,6 +303,12 @@ export function setLoggedIn(credentials) { localStorage.setItem("mx_device_id", credentials.deviceId); } + // 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); + } + console.log("Session persisted for %s", credentials.userId); } catch (e) { console.warn("Error using local storage: can't persist session!", e); diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index c8083371de..db88c3304b 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -56,21 +56,18 @@ export default React.createClass({ }); }, - _generateAndCachePassword: function() { - const pass = Math.random().toString(36).slice(2); - if (localStorage) { - localStorage.setItem('mx_pass', pass); - } - return pass; + _generatePassword: function() { + return Math.random().toString(36).slice(2); }, _makeRegisterRequest: function(auth) { // Not upgrading - changing mxids const guestAccessToken = null; + this._generatedPassword = this._generatePassword(); return this._matrixClient.register( this.state.username, - this._generateAndCachePassword(), + this._generatedPassword, undefined, // session id: included in the auth dict already auth, {}, @@ -98,6 +95,7 @@ export default React.createClass({ homeserverUrl: this._matrixClient.getHomeserverUrl(), identityServerUrl: this._matrixClient.getIdentityServerUrl(), accessToken: response.access_token, + password: this._generatedPassword, teamToken: teamToken, }); }, From 13d37e43ff85605ba74a06c146a0ab11086421d8 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 2 May 2017 10:14:54 +0100 Subject: [PATCH 7/7] Mock isGuest --- test/test-utils.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test-utils.js b/test/test-utils.js index 5209465362..9f404f98eb 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -133,6 +133,7 @@ export function createTestClient() { sendHtmlMessage: () => q({}), getSyncState: () => "SYNCING", generateClientSecret: () => "t35tcl1Ent5ECr3T", + isGuest: () => false, }; }