From 747c0c44a68be5d40dfcad34b26af15e6d6092b0 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 9 Feb 2017 09:24:46 +0000 Subject: [PATCH 01/30] Use new method of getting team icon This was necessary because the team token may not be known when registering, but domain is. Storing the icon under the "common" directory is the chosen solution to this. --- src/components/structures/login/Registration.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index efe7dae723..78e65519ff 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -426,7 +426,12 @@ module.exports = React.createClass({ return (
- + {this._getRegisterContentJsx()}
From 69add8fd642a584281aecf0c775702144d2073fb Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 9 Feb 2017 13:16:46 +0000 Subject: [PATCH 02/30] Actually use the RTS URL --- src/components/structures/login/Registration.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 78e65519ff..7887f3e7de 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -428,7 +428,7 @@ module.exports = React.createClass({
From bdd031eac2b4ea70d43c03c0c11b26a4760405ea Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 10 Feb 2017 15:09:45 +0000 Subject: [PATCH 03/30] Enable branded URLs again by parsing the path client-side Use the first path segment to key off config.teamTokenMap, which contains a mapping to teamTokens. The client then behaves as before, keeping the path in the address bar constant with no redirects required. --- src/components/structures/MatrixChat.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 8fdcf15e1b..75ae51e9e5 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -191,6 +191,17 @@ module.exports = React.createClass({ MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit; } + // To enable things like riot.im/geektime in a nicer way than rewriting the URL + // and appending a team token query parameter, use the first path segment to + // indicate a team, with "public" team tokens stored in the config teamTokenMap. + let routedTeamToken = null; + if (this.props.config.teamTokenMap) { + const teamName = window.location.pathname.split('/')[1]; + if (this.props.config.teamTokenMap.hasOwnProperty(teamName)) { + routedTeamToken = this.props.config.teamTokenMap[teamName]; + } + } + // Persist the team token across refreshes using sessionStorage. A new window or // tab will not persist sessionStorage, but refreshes will. if (this.props.startingFragmentQueryParams.team_token) { @@ -202,8 +213,13 @@ module.exports = React.createClass({ // Use the locally-stored team token first, then as a fall-back, check to see if // a referral link was used, which will contain a query parameter `team_token`. - this._teamToken = window.localStorage.getItem('mx_team_token') || + this._teamToken = routedTeamToken || + window.localStorage.getItem('mx_team_token') || window.sessionStorage.getItem('mx_team_token'); + + if (this._teamToken) { + console.info(`Team token set to ${this._teamToken}`); + } }, componentDidMount: function() { From 29f5e88f6aae7bb8a4d827f9a607b5b713a48bce Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 10 Feb 2017 16:50:25 +0000 Subject: [PATCH 04/30] Instead of sending userId, userEmail, send sid, client_secret This has the benefit of being possible from the _second_ riot instance, which may not actually have the email of the user registering. With these parameters, the RTS can get the email and user ID itself. (see https://github.com/matrix-org/riot-team-server/pull/15) --- src/RtsClient.js | 10 +++++----- src/components/structures/login/Registration.js | 11 +++++++---- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/RtsClient.js b/src/RtsClient.js index 5cf2e811ad..8c3ce54b37 100644 --- a/src/RtsClient.js +++ b/src/RtsClient.js @@ -50,18 +50,18 @@ export default class RtsClient { * Track a referral with the Riot Team Server. This should be called once a referred * user has been successfully registered. * @param {string} referrer the user ID of one who referred the user to Riot. - * @param {string} userId the user ID of the user being referred. - * @param {string} userEmail the email address linked to `userId`. + * @param {string} sid the sign-up identity server session ID . + * @param {string} clientSecret the sign-up client secret. * @returns {Promise} a promise that resolves to { team_token: 'sometoken' } upon * success. */ - trackReferral(referrer, userId, userEmail) { + trackReferral(referrer, sid, clientSecret) { return request(this._url + '/register', { body: { referrer: referrer, - user_id: userId, - user_email: userEmail, + session_id: sid, + client_secret: clientSecret, }, method: 'POST', } diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index efe7dae723..ddd67921a8 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -213,16 +213,19 @@ module.exports = React.createClass({ accessToken: response.access_token }); + // Done regardless of `teamSelected`. People registering with non-team emails + // will just nop. The point of this being we might not have the email address + // that the user registered with at this stage (depending on whether this + // is the client they initiated registration). if ( self._rtsClient && - self.props.referrer && - self.state.teamSelected + self.props.referrer ) { // Track referral, get team_token in order to retrieve team config self._rtsClient.trackReferral( self.props.referrer, - response.user_id, - self.state.formVals.email + self.registerLogic.params.idSid, + self.registerLogic.params.clientSecret ).then((data) => { const teamToken = data.team_token; // Store for use /w welcome pages From 4ac769168aa2a9035e83e6616bd747ba7ebed41c Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 10 Feb 2017 17:17:58 +0000 Subject: [PATCH 05/30] View /home on registered /w team --- src/components/structures/login/Registration.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index efe7dae723..bd4dad5d5f 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -227,6 +227,9 @@ module.exports = React.createClass({ const teamToken = data.team_token; // Store for use /w welcome pages window.localStorage.setItem('mx_team_token', teamToken); + // Set the team token and view homepage + window.matrixChat._teamToken = teamToken; + window.mxDispatcher.dispatch({action: 'view_home_page'}); self._rtsClient.getTeam(teamToken).then((team) => { console.log( From 75deb5584495f07247772eb65b42ef68b983b4eb Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 13 Feb 2017 11:48:03 +0000 Subject: [PATCH 06/30] Null check on teamName --- src/components/structures/MatrixChat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 75ae51e9e5..8d08c19001 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -197,7 +197,7 @@ module.exports = React.createClass({ let routedTeamToken = null; if (this.props.config.teamTokenMap) { const teamName = window.location.pathname.split('/')[1]; - if (this.props.config.teamTokenMap.hasOwnProperty(teamName)) { + if (teamName && this.props.config.teamTokenMap.hasOwnProperty(teamName)) { routedTeamToken = this.props.config.teamTokenMap[teamName]; } } From 16e3365240b5d4c737d8626ceece0a918cefd5e9 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 13 Feb 2017 14:36:03 +0000 Subject: [PATCH 07/30] Use a callback prop instead of `window.` --- src/components/structures/MatrixChat.js | 6 ++++++ src/components/structures/login/Registration.js | 5 ++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 8fdcf15e1b..a2a7e34669 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -975,6 +975,11 @@ module.exports = React.createClass({ this._setPage(PageTypes.UserSettings); }, + onTeamMemberRegistered: function(teamToken) { + this._teamToken = teamToken; + this._setPage(PageTypes.HomePage); + }, + onFinishPostRegistration: function() { // Don't confuse this with "PageType" which is the middle window to show this.setState({ @@ -1103,6 +1108,7 @@ module.exports = React.createClass({ customIsUrl={this.getCurrentIsUrl()} registrationUrl={this.props.registrationUrl} defaultDeviceDisplayName={this.props.defaultDeviceDisplayName} + onTeamMemberRegistered={this.onTeamMemberRegistered} onLoggedIn={this.onRegistered} onLoginClick={this.onLoginClick} onRegisterClick={this.onRegisterClick} diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index bd4dad5d5f..8af208de10 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -58,6 +58,7 @@ module.exports = React.createClass({ teamServerURL: React.PropTypes.string.isRequired, }), teamSelected: React.PropTypes.object, + onTeamMemberRegistered: React.PropTypes.func.isRequired, defaultDeviceDisplayName: React.PropTypes.string, @@ -227,9 +228,7 @@ module.exports = React.createClass({ const teamToken = data.team_token; // Store for use /w welcome pages window.localStorage.setItem('mx_team_token', teamToken); - // Set the team token and view homepage - window.matrixChat._teamToken = teamToken; - window.mxDispatcher.dispatch({action: 'view_home_page'}); + self.props.onTeamMemberRegistered(teamToken); self._rtsClient.getTeam(teamToken).then((team) => { console.log( From 79d9deb339bdcde60e235b084e51a542ba3a1557 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 13 Feb 2017 16:03:21 +0000 Subject: [PATCH 08/30] Split out InterActiveAuthDialog Into a component that does Interactive Auth and a dialog that wraps it, so we can do interactive auth not necessarily in a dialog. As a side effect: * Put the buttons for each auth stage in the stage itself. Some stages don't have submit buttons (and it's very possible other stages may have other buttons entirely, like 'resend') so it makes more sense for the buttons to live in the stage components themselves. Plus it saves the slightly evil calling-functions-on-react-children thing we were doing (and indeed extending that to show the submit button at all). * Give all BaseDialogs a cross in the top right to cancel. They were all dismissable by clicking outside or pressing esc, so this adds a more visually obvious way of dismissing them. Plus, it means our InteractiveAuthDialog can have a way of canceling the whole operation separate from buttons for the individual stages. --- src/component-index.js | 2 + src/components/structures/InteractiveAuth.js | 153 +++++++++++++++++ src/components/views/dialogs/BaseDialog.js | 15 ++ .../views/dialogs/InteractiveAuthDialog.js | 154 +----------------- .../login/InteractiveAuthEntryComponents.js | 42 ++--- 5 files changed, 201 insertions(+), 165 deletions(-) create mode 100644 src/components/structures/InteractiveAuth.js diff --git a/src/component-index.js b/src/component-index.js index 5b28be0627..b357472ad7 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -31,6 +31,8 @@ import structures$CreateRoom from './components/structures/CreateRoom'; structures$CreateRoom && (module.exports.components['structures.CreateRoom'] = structures$CreateRoom); import structures$FilePanel from './components/structures/FilePanel'; structures$FilePanel && (module.exports.components['structures.FilePanel'] = structures$FilePanel); +import structures$InteractiveAuth from './components/structures/InteractiveAuth'; +structures$InteractiveAuth && (module.exports.components['structures.InteractiveAuth'] = structures$InteractiveAuth); import structures$LoggedInView from './components/structures/LoggedInView'; structures$LoggedInView && (module.exports.components['structures.LoggedInView'] = structures$LoggedInView); import structures$MatrixChat from './components/structures/MatrixChat'; diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js new file mode 100644 index 0000000000..205d2ca1b8 --- /dev/null +++ b/src/components/structures/InteractiveAuth.js @@ -0,0 +1,153 @@ +/* +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. +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 Matrix from 'matrix-js-sdk'; +const InteractiveAuth = Matrix.InteractiveAuth; + +import React from 'react'; + +import sdk from '../../index'; + +import {getEntryComponentForLoginType} from '../views/login/InteractiveAuthEntryComponents'; + +export default React.createClass({ + displayName: 'InteractiveAuth', + + propTypes: { + // response from initial request. If not supplied, will do a request on + // mount. + authData: React.PropTypes.shape({ + flows: React.PropTypes.array, + params: React.PropTypes.object, + session: React.PropTypes.string, + }), + + // callback + makeRequest: React.PropTypes.func.isRequired, + + onFinished: React.PropTypes.func.isRequired, + }, + + getInitialState: function() { + return { + authStage: null, + busy: false, + errorText: null, + stageErrorText: null, + submitButtonEnabled: false, + }; + }, + + componentWillMount: function() { + this._unmounted = false; + this._authLogic = new InteractiveAuth({ + authData: this.props.authData, + doRequest: this._requestCallback, + startAuthStage: this._startAuthStage, + }); + + this._authLogic.attemptAuth().then((result) => { + this.props.onFinished(true, result); + }).catch((error) => { + console.error("Error during user-interactive auth:", error); + if (this._unmounted) { + return; + } + + const msg = error.message || error.toString(); + this.setState({ + errorText: msg + }); + }).done(); + }, + + componentWillUnmount: function() { + this._unmounted = true; + }, + + _startAuthStage: function(stageType, error) { + this.setState({ + authStage: stageType, + errorText: error ? error.error : null, + }, this._setFocus); + }, + + _requestCallback: function(auth) { + this.setState({ + busy: true, + errorText: null, + stageErrorText: null, + }); + return this.props.makeRequest(auth).finally(() => { + if (this._unmounted) { + return; + } + this.setState({ + busy: false, + }); + }); + }, + + _setFocus: function() { + if (this.refs.stageComponent && this.refs.stageComponent.focus) { + this.refs.stageComponent.focus(); + } + }, + + _onCancel: function() { + this.props.onFinished(false); + }, + + _submitAuthDict: function(authData) { + this._authLogic.submitAuthDict(authData); + }, + + _renderCurrentStage: function() { + const stage = this.state.authStage; + var StageComponent = getEntryComponentForLoginType(stage); + return ( + + ); + }, + + render: function() { + const Loader = sdk.getComponent("elements.Spinner"); + + let error = null; + if (this.state.errorText) { + error = ( +
+ {this.state.errorText} +
+ ); + } + + return ( +
+
+ {this._renderCurrentStage()} + {error} +
+
+ ); + }, +}); diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index 2b3980c536..01a16e86ac 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -17,6 +17,7 @@ limitations under the License. import React from 'react'; import * as KeyCode from '../../../KeyCode'; +import AccessibleButton from '../elements/AccessibleButton'; /** * Basic container for modal dialogs. @@ -59,9 +60,23 @@ export default React.createClass({ } }, + _onCancelClick: function(e) { + e.stopPropagation(); + e.preventDefault(); + this.props.onFinished(); + }, + render: function() { return (
+ + Cancel +
{ this.props.title }
diff --git a/src/components/views/dialogs/InteractiveAuthDialog.js b/src/components/views/dialogs/InteractiveAuthDialog.js index a4abbb17d9..1c9c872070 100644 --- a/src/components/views/dialogs/InteractiveAuthDialog.js +++ b/src/components/views/dialogs/InteractiveAuthDialog.js @@ -15,13 +15,12 @@ limitations under the License. */ import Matrix from 'matrix-js-sdk'; -const InteractiveAuth = Matrix.InteractiveAuth; import React from 'react'; import sdk from '../../../index'; -import {getEntryComponentForLoginType} from '../login/InteractiveAuthEntryComponents'; +import AccessibleButton from '../elements/AccessibleButton'; export default React.createClass({ displayName: 'InteractiveAuthDialog', @@ -41,168 +40,33 @@ export default React.createClass({ onFinished: React.PropTypes.func.isRequired, title: React.PropTypes.string, - submitButtonLabel: React.PropTypes.string, }, getDefaultProps: function() { return { title: "Authentication", - submitButtonLabel: "Submit", }; }, - getInitialState: function() { - return { - authStage: null, - busy: false, - errorText: null, - stageErrorText: null, - submitButtonEnabled: false, - }; - }, - - componentWillMount: function() { - this._unmounted = false; - this._authLogic = new InteractiveAuth({ - authData: this.props.authData, - doRequest: this._requestCallback, - startAuthStage: this._startAuthStage, - }); - - this._authLogic.attemptAuth().then((result) => { - this.props.onFinished(true, result); - }).catch((error) => { - console.error("Error during user-interactive auth:", error); - if (this._unmounted) { - return; - } - - const msg = error.message || error.toString(); - this.setState({ - errorText: msg - }); - }).done(); - }, - - componentWillUnmount: function() { - this._unmounted = true; - }, - - _startAuthStage: function(stageType, error) { - this.setState({ - authStage: stageType, - errorText: error ? error.error : null, - }, this._setFocus); - }, - - _requestCallback: function(auth) { - this.setState({ - busy: true, - errorText: null, - stageErrorText: null, - }); - return this.props.makeRequest(auth).finally(() => { - if (this._unmounted) { - return; - } - this.setState({ - busy: false, - }); - }); - }, - - _onEnterPressed: function(e) { - if (this.state.submitButtonEnabled && !this.state.busy) { - this._onSubmit(); - } - }, - - _onSubmit: function() { - if (this.refs.stageComponent && this.refs.stageComponent.onSubmitClick) { - this.refs.stageComponent.onSubmitClick(); - } - }, - - _setFocus: function() { - if (this.refs.stageComponent && this.refs.stageComponent.focus) { - this.refs.stageComponent.focus(); - } - }, - - _onCancel: function() { + _onCancelClick: function() { this.props.onFinished(false); }, - _setSubmitButtonEnabled: function(enabled) { - this.setState({ - submitButtonEnabled: enabled, - }); - }, - - _submitAuthDict: function(authData) { - this._authLogic.submitAuthDict(authData); - }, - - _renderCurrentStage: function() { - const stage = this.state.authStage; - var StageComponent = getEntryComponentForLoginType(stage); - return ( - - ); - }, - render: function() { - const Loader = sdk.getComponent("elements.Spinner"); + const InteractiveAuth = sdk.getComponent("structures.InteractiveAuth"); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - let error = null; - if (this.state.errorText) { - error = ( -
- {this.state.errorText} -
- ); - } - - const submitLabel = this.state.busy ? : this.props.submitButtonLabel; - const submitEnabled = this.state.submitButtonEnabled && !this.state.busy; - - const submitButton = ( - - ); - - const cancelButton = ( - - ); - return ( -
-

This operation requires additional authentication.

- {this._renderCurrentStage()} - {error} -
-
- {submitButton} - {cancelButton} +
+
); diff --git a/src/components/views/login/InteractiveAuthEntryComponents.js b/src/components/views/login/InteractiveAuthEntryComponents.js index ec184ca09f..a8c58ea76f 100644 --- a/src/components/views/login/InteractiveAuthEntryComponents.js +++ b/src/components/views/login/InteractiveAuthEntryComponents.js @@ -32,7 +32,6 @@ import MatrixClientPeg from '../../../MatrixClientPeg'; * stageParams: params from the server for the stage being attempted * errorText: error message from a previous attempt to authenticate * submitAuthDict: a function which will be called with the new auth dict - * setSubmitButtonEnabled: a function which will enable/disable the 'submit' button * * Each component may also provide the following functions (beyond the standard React ones): * onSubmitClick: handle a 'submit' button click @@ -48,12 +47,13 @@ export const PasswordAuthEntry = React.createClass({ propTypes: { submitAuthDict: React.PropTypes.func.isRequired, - setSubmitButtonEnabled: React.PropTypes.func.isRequired, errorText: React.PropTypes.string, }, - componentWillMount: function() { - this.props.setSubmitButtonEnabled(false); + getInitialState: function() { + return { + enableSubmit: false, + }; }, focus: function() { @@ -62,7 +62,7 @@ export const PasswordAuthEntry = React.createClass({ } }, - onSubmitClick: function() { + _onSubmit: function() { this.props.submitAuthDict({ type: PasswordAuthEntry.LOGIN_TYPE, user: MatrixClientPeg.get().credentials.userId, @@ -72,7 +72,9 @@ export const PasswordAuthEntry = React.createClass({ _onPasswordFieldChange: function(ev) { // enable the submit button iff the password is non-empty - this.props.setSubmitButtonEnabled(Boolean(ev.target.value)); + this.setState({ + enableSubmit: Boolean(this.refs.passwordField.value), + }); }, render: function() { @@ -86,12 +88,20 @@ export const PasswordAuthEntry = React.createClass({

To continue, please enter your password.

Password:

- +
+ +
+ +
+
{this.props.errorText}
@@ -110,14 +120,9 @@ export const RecaptchaAuthEntry = React.createClass({ propTypes: { submitAuthDict: React.PropTypes.func.isRequired, stageParams: React.PropTypes.object.isRequired, - setSubmitButtonEnabled: React.PropTypes.func.isRequired, errorText: React.PropTypes.string, }, - componentWillMount: function() { - this.props.setSubmitButtonEnabled(false); - }, - _onCaptchaResponse: function(response) { this.props.submitAuthDict({ type: RecaptchaAuthEntry.LOGIN_TYPE, @@ -148,7 +153,6 @@ export const FallbackAuthEntry = React.createClass({ authSessionId: React.PropTypes.string.isRequired, loginType: React.PropTypes.string.isRequired, submitAuthDict: React.PropTypes.func.isRequired, - setSubmitButtonEnabled: React.PropTypes.func.isRequired, errorText: React.PropTypes.string, }, @@ -156,7 +160,6 @@ export const FallbackAuthEntry = React.createClass({ // we have to make the user click a button, as browsers will block // the popup if we open it immediately. this._popupWindow = null; - this.props.setSubmitButtonEnabled(true); window.addEventListener("message", this._onReceiveMessage); }, @@ -173,7 +176,6 @@ export const FallbackAuthEntry = React.createClass({ this.props.authSessionId ); this._popupWindow = window.open(url); - this.props.setSubmitButtonEnabled(false); }, _onReceiveMessage: function(event) { From 77b226631a13c6c893c08943a781a2f0a1c0a874 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 13 Feb 2017 16:15:00 +0000 Subject: [PATCH 09/30] Copyright --- src/components/views/dialogs/InteractiveAuthDialog.js | 1 + src/components/views/login/InteractiveAuthEntryComponents.js | 1 + 2 files changed, 2 insertions(+) diff --git a/src/components/views/dialogs/InteractiveAuthDialog.js b/src/components/views/dialogs/InteractiveAuthDialog.js index 1c9c872070..ecca00358f 100644 --- a/src/components/views/dialogs/InteractiveAuthDialog.js +++ b/src/components/views/dialogs/InteractiveAuthDialog.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. diff --git a/src/components/views/login/InteractiveAuthEntryComponents.js b/src/components/views/login/InteractiveAuthEntryComponents.js index a8c58ea76f..e4c48b1b1b 100644 --- a/src/components/views/login/InteractiveAuthEntryComponents.js +++ b/src/components/views/login/InteractiveAuthEntryComponents.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 8fc31045079c3a5c5df130500c508e2404cffc68 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 13 Feb 2017 18:52:33 +0000 Subject: [PATCH 10/30] Replace submit button with a spinner when busy and update test accordingly --- src/components/structures/InteractiveAuth.js | 1 + .../login/InteractiveAuthEntryComponents.js | 30 ++++++++++++++----- .../dialogs/InteractiveAuthDialog-test.js | 30 ++++++++++++------- 3 files changed, 43 insertions(+), 18 deletions(-) diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js index 205d2ca1b8..c29b0fe16a 100644 --- a/src/components/structures/InteractiveAuth.js +++ b/src/components/structures/InteractiveAuth.js @@ -125,6 +125,7 @@ export default React.createClass({ stageParams={this._authLogic.getStageParams(stage)} submitAuthDict={this._submitAuthDict} errorText={this.state.stageErrorText} + busy={this.state.busy} /> ); }, diff --git a/src/components/views/login/InteractiveAuthEntryComponents.js b/src/components/views/login/InteractiveAuthEntryComponents.js index e4c48b1b1b..4759a30c88 100644 --- a/src/components/views/login/InteractiveAuthEntryComponents.js +++ b/src/components/views/login/InteractiveAuthEntryComponents.js @@ -49,11 +49,14 @@ export const PasswordAuthEntry = React.createClass({ propTypes: { submitAuthDict: React.PropTypes.func.isRequired, errorText: React.PropTypes.string, + // is the auth logic currently waiting for something to + // happen? + busy: React.PropTypes.bool, }, getInitialState: function() { return { - enableSubmit: false, + passwordValid: false, }; }, @@ -63,7 +66,10 @@ export const PasswordAuthEntry = React.createClass({ } }, - _onSubmit: function() { + _onSubmit: function(e) { + e.preventDefault(); + if (this.props.busy) return; + this.props.submitAuthDict({ type: PasswordAuthEntry.LOGIN_TYPE, user: MatrixClientPeg.get().credentials.userId, @@ -74,7 +80,7 @@ export const PasswordAuthEntry = React.createClass({ _onPasswordFieldChange: function(ev) { // enable the submit button iff the password is non-empty this.setState({ - enableSubmit: Boolean(this.refs.passwordField.value), + passwordValid: Boolean(this.refs.passwordField.value), }); }, @@ -85,6 +91,19 @@ export const PasswordAuthEntry = React.createClass({ passwordBoxClass = 'error'; } + let submitButtonOrSpinner; + if (this.props.busy) { + const Loader = sdk.getComponent("elements.Spinner"); + submitButtonOrSpinner = ; + } else { + submitButtonOrSpinner = ( + + ); + } + return (

To continue, please enter your password.

@@ -97,10 +116,7 @@ export const PasswordAuthEntry = React.createClass({ type="password" />
- + {submitButtonOrSpinner}
diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.js b/test/components/views/dialogs/InteractiveAuthDialog-test.js index 35daace0f8..80f027ab44 100644 --- a/test/components/views/dialogs/InteractiveAuthDialog-test.js +++ b/test/components/views/dialogs/InteractiveAuthDialog-test.js @@ -67,16 +67,24 @@ describe('InteractiveAuthDialog', function () { onFinished={onFinished} />, parentDiv); - // at this point there should be a password box - const passwordNode = ReactTestUtils.findRenderedDOMComponentWithTag( + // at this point there should be a password box and a submit button + const formNode = ReactTestUtils.findRenderedDOMComponentWithTag(dlg, "form"); + const inputNodes = ReactTestUtils.scryRenderedDOMComponentsWithTag( dlg, "input" ); - expect(passwordNode.type).toEqual("password"); + let passwordNode; + let submitNode; + for (const node of inputNodes) { + if (node.type == 'password') { + passwordNode = node; + } else if (node.type == 'submit') { + submitNode = node; + } + } + expect(passwordNode).toExist(); + expect(submitNode).toExist(); // submit should be disabled - const submitNode = ReactTestUtils.findRenderedDOMComponentWithClass( - dlg, "mx_Dialog_primary" - ); expect(submitNode.disabled).toBe(true); // put something in the password box, and hit enter; that should @@ -84,9 +92,7 @@ describe('InteractiveAuthDialog', function () { passwordNode.value = "s3kr3t"; ReactTestUtils.Simulate.change(passwordNode); expect(submitNode.disabled).toBe(false); - ReactTestUtils.Simulate.keyDown(passwordNode, { - key: "Enter", keyCode: 13, which: 13, - }); + ReactTestUtils.Simulate.submit(formNode, {}); expect(doRequest.callCount).toEqual(1); expect(doRequest.calledWithExactly({ @@ -96,8 +102,10 @@ describe('InteractiveAuthDialog', function () { user: "@user:id", })).toBe(true); - // the submit button should now be disabled (and be a spinner) - expect(submitNode.disabled).toBe(true); + // there should now be a spinner + ReactTestUtils.findRenderedComponentWithType( + dlg, sdk.getComponent('elements.Spinner'), + ); // let the request complete q.delay(1).then(() => { From 36d126f3a9f9aa07be401e0265f0c1d3cf0d7b54 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 13 Feb 2017 19:09:43 +0000 Subject: [PATCH 11/30] PR feedback --- src/components/structures/InteractiveAuth.js | 10 ++++------ src/components/views/dialogs/BaseDialog.js | 2 -- src/components/views/dialogs/InteractiveAuthDialog.js | 4 ---- .../views/login/InteractiveAuthEntryComponents.js | 6 +++--- 4 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js index c29b0fe16a..70b3c2e306 100644 --- a/src/components/structures/InteractiveAuth.js +++ b/src/components/structures/InteractiveAuth.js @@ -38,6 +38,10 @@ export default React.createClass({ // callback makeRequest: React.PropTypes.func.isRequired, + // callback called when the auth process has finished + // @param {bool} status True if the operation requiring + // auth was completed sucessfully, false if canceled. + // @param result The result of the authenticated call onFinished: React.PropTypes.func.isRequired, }, @@ -107,10 +111,6 @@ export default React.createClass({ } }, - _onCancel: function() { - this.props.onFinished(false); - }, - _submitAuthDict: function(authData) { this._authLogic.submitAuthDict(authData); }, @@ -131,8 +131,6 @@ export default React.createClass({ }, render: function() { - const Loader = sdk.getComponent("elements.Spinner"); - let error = null; if (this.state.errorText) { error = ( diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index 01a16e86ac..e83403ef7c 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -61,8 +61,6 @@ export default React.createClass({ }, _onCancelClick: function(e) { - e.stopPropagation(); - e.preventDefault(); this.props.onFinished(); }, diff --git a/src/components/views/dialogs/InteractiveAuthDialog.js b/src/components/views/dialogs/InteractiveAuthDialog.js index ecca00358f..66b662b23d 100644 --- a/src/components/views/dialogs/InteractiveAuthDialog.js +++ b/src/components/views/dialogs/InteractiveAuthDialog.js @@ -49,10 +49,6 @@ export default React.createClass({ }; }, - _onCancelClick: function() { - this.props.onFinished(false); - }, - render: function() { const InteractiveAuth = sdk.getComponent("structures.InteractiveAuth"); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); diff --git a/src/components/views/login/InteractiveAuthEntryComponents.js b/src/components/views/login/InteractiveAuthEntryComponents.js index 4759a30c88..62f59472ec 100644 --- a/src/components/views/login/InteractiveAuthEntryComponents.js +++ b/src/components/views/login/InteractiveAuthEntryComponents.js @@ -21,7 +21,7 @@ import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; /* This file contains a collection of components which are used by the - * InteractiveAuthDialog to prompt the user to enter the information needed + * InteractiveAuth to prompt the user to enter the information needed * for an auth stage. (The intention is that they could also be used for other * components, such as the registration flow). * @@ -187,7 +187,7 @@ export const FallbackAuthEntry = React.createClass({ } }, - onSubmitClick: function() { + _onShowFallbackClick: function() { var url = MatrixClientPeg.get().getFallbackAuthUrl( this.props.loginType, this.props.authSessionId @@ -207,7 +207,7 @@ export const FallbackAuthEntry = React.createClass({ render: function() { return (
- Click "Submit" to authenticate + Start authentication
{this.props.errorText}
From ba3e62e3950c8b9929f33d26b0166a8c0afdf488 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 14 Feb 2017 10:31:56 +0000 Subject: [PATCH 12/30] Remove old docs --- src/components/views/login/InteractiveAuthEntryComponents.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/login/InteractiveAuthEntryComponents.js b/src/components/views/login/InteractiveAuthEntryComponents.js index 62f59472ec..bde0da3020 100644 --- a/src/components/views/login/InteractiveAuthEntryComponents.js +++ b/src/components/views/login/InteractiveAuthEntryComponents.js @@ -35,7 +35,6 @@ import MatrixClientPeg from '../../../MatrixClientPeg'; * submitAuthDict: a function which will be called with the new auth dict * * Each component may also provide the following functions (beyond the standard React ones): - * onSubmitClick: handle a 'submit' button click * focus: set the input focus appropriately in the form. */ From 43a740df150ce70478e05c001e6f077bef575ec9 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 14 Feb 2017 10:34:43 +0000 Subject: [PATCH 13/30] Add busy param to docs --- src/components/views/login/InteractiveAuthEntryComponents.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/views/login/InteractiveAuthEntryComponents.js b/src/components/views/login/InteractiveAuthEntryComponents.js index bde0da3020..e18e60d7bc 100644 --- a/src/components/views/login/InteractiveAuthEntryComponents.js +++ b/src/components/views/login/InteractiveAuthEntryComponents.js @@ -33,6 +33,8 @@ import MatrixClientPeg from '../../../MatrixClientPeg'; * stageParams: params from the server for the stage being attempted * errorText: error message from a previous attempt to authenticate * submitAuthDict: a function which will be called with the new auth dict + * busy: a boolean indicating whether the auth logic is doing something + * the user needs to wait for. * * Each component may also provide the following functions (beyond the standard React ones): * focus: set the input focus appropriately in the form. From 6996291f0c210691853c24f57f4007167658c746 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 14 Feb 2017 11:00:40 +0000 Subject: [PATCH 14/30] Store retrieved sid in the signupInstance of EmailIdentityStage When registeration is complete, the RTS needs the sid, which was previously only sent to the HS. This update will also store it in the signupInstance so that it can be sent to the RTS. --- src/SignupStages.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/SignupStages.js b/src/SignupStages.js index cdb9d5989b..1441682c85 100644 --- a/src/SignupStages.js +++ b/src/SignupStages.js @@ -149,6 +149,7 @@ class EmailIdentityStage extends Stage { nextLink ).then(function(response) { self.sid = response.sid; + self.signupInstance.setIdSid(self.sid); return self._completeVerify(); }).then(function(request) { request.poll_for_success = true; From 1b8e93d4f2daf217762f9c476a0dc553fdd49760 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 14 Feb 2017 12:56:29 +0000 Subject: [PATCH 15/30] Treat the literal team token string "undefined" as undefined Some users appear to have gotten team tokens into their local storage. This fix will treat the literal string "undefined" as undefined. --- src/components/structures/MatrixChat.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 87cd2645db..a467771a58 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -217,6 +217,12 @@ module.exports = React.createClass({ window.localStorage.getItem('mx_team_token') || window.sessionStorage.getItem('mx_team_token'); + // Some users have ended up with "undefined" as their local storage team token, + // treat that as undefined. + if (this._teamToken === "undefined") { + this._teamToken = undefined; + } + if (this._teamToken) { console.info(`Team token set to ${this._teamToken}`); } From 8001c0b16b7e4b2fb1dcd9677b2c1f2caab65ea5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 14 Feb 2017 13:40:19 +0000 Subject: [PATCH 16/30] Add confirmation dialog to kick/ban buttons Add a specific dialog used for confirming member actions. Also remove onFinished from MemberInfo which did absolutely nothing. --- src/component-index.js | 2 + .../views/dialogs/ConfirmUserActionDialog.js | 83 ++++++++++++ src/components/views/rooms/MemberInfo.js | 124 +++++++++--------- 3 files changed, 145 insertions(+), 64 deletions(-) create mode 100644 src/components/views/dialogs/ConfirmUserActionDialog.js diff --git a/src/component-index.js b/src/component-index.js index b357472ad7..62d26d4ce7 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -77,6 +77,8 @@ import views$dialogs$BaseDialog from './components/views/dialogs/BaseDialog'; views$dialogs$BaseDialog && (module.exports.components['views.dialogs.BaseDialog'] = views$dialogs$BaseDialog); import views$dialogs$ChatInviteDialog from './components/views/dialogs/ChatInviteDialog'; views$dialogs$ChatInviteDialog && (module.exports.components['views.dialogs.ChatInviteDialog'] = views$dialogs$ChatInviteDialog); +import views$dialogs$ConfirmUserActionDialog from './components/views/dialogs/ConfirmUserActionDialog'; +views$dialogs$ConfirmUserActionDialog && (module.exports.components['views.dialogs.ConfirmUserActionDialog'] = views$dialogs$ConfirmUserActionDialog); import views$dialogs$DeactivateAccountDialog from './components/views/dialogs/DeactivateAccountDialog'; views$dialogs$DeactivateAccountDialog && (module.exports.components['views.dialogs.DeactivateAccountDialog'] = views$dialogs$DeactivateAccountDialog); import views$dialogs$ErrorDialog from './components/views/dialogs/ErrorDialog'; diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.js b/src/components/views/dialogs/ConfirmUserActionDialog.js new file mode 100644 index 0000000000..8dc96bb5f6 --- /dev/null +++ b/src/components/views/dialogs/ConfirmUserActionDialog.js @@ -0,0 +1,83 @@ +/* +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. +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 classnames from 'classnames'; + +/* + * A dialog for confirming an operation on another user. + * Takes a user ID and a verb, displays the target user prominently + * such that it should be easy to confirm that tne operation is being + * performed on the right person, and displays the operation prominently + * to make it obvious what is going to happen. + * Also tweaks the style for 'dangerous' actions (albeit only with colour) + */ +export default React.createClass({ + displayName: 'ConfirmUserActionDialog', + propTypes: { + member: React.PropTypes.object.isRequired, // member object + action: React.PropTypes.string.isRequired, // eg. 'Ban' + danger: React.PropTypes.bool, + onFinished: React.PropTypes.func.isRequired, + }, + + defaultProps: { + danger: false, + }, + + onOk: function() { + this.props.onFinished(true); + }, + + onCancel: function() { + this.props.onFinished(false); + }, + + render: function() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar"); + + const title = this.props.action + " this person?"; + const confirmButtonClass = classnames({ + 'mx_Dialog_primary': true, + 'danger': this.props.danger, + }); + return ( + +
+
+ +
+
{this.props.member.name}
+
{this.props.member.userId}
+
+
+ + + +
+
+ ); + }, +}); diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index d33b8f3524..5f0578df60 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -25,16 +25,16 @@ limitations under the License. * 'muted': boolean, * 'isTargetMod': boolean */ -var React = require('react'); -var classNames = require('classnames'); -var dis = require("../../../dispatcher"); -var Modal = require("../../../Modal"); -var sdk = require('../../../index'); -var createRoom = require('../../../createRoom'); -var DMRoomMap = require('../../../utils/DMRoomMap'); -var Unread = require('../../../Unread'); -var Receipt = require('../../../utils/Receipt'); -var WithMatrixClient = require('../../../wrappers/WithMatrixClient'); +import React from 'react'; +import classNames from 'classnames'; +import dis from '../../../dispatcher'; +import Modal from '../../../Modal'; +import sdk from '../../../index'; +import createRoom from '../../../createRoom'; +import DMRoomMap from '../../../utils/DMRoomMap'; +import Unread from '../../../Unread'; +import Receipt from '../../../utils/Receipt'; +import WithMatrixClient from '../../../wrappers/WithMatrixClient'; import AccessibleButton from '../elements/AccessibleButton'; module.exports = WithMatrixClient(React.createClass({ @@ -43,13 +43,6 @@ module.exports = WithMatrixClient(React.createClass({ propTypes: { matrixClient: React.PropTypes.object.isRequired, member: React.PropTypes.object.isRequired, - onFinished: React.PropTypes.func, - }, - - getDefaultProps: function() { - return { - onFinished: function() {} - }; }, getInitialState: function() { @@ -224,46 +217,64 @@ module.exports = WithMatrixClient(React.createClass({ }, onKick: function() { - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - var roomId = this.props.member.roomId; - var target = this.props.member.userId; - this.setState({ updating: this.state.updating + 1 }); - this.props.matrixClient.kick(roomId, target).then(function() { - // NO-OP; rely on the m.room.member event coming down else we could - // get out of sync if we force setState here! - console.log("Kick success"); - }, function(err) { - Modal.createDialog(ErrorDialog, { - title: "Kick error", - description: err.message + const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); + Modal.createDialog(ConfirmUserActionDialog, { + member: this.props.member, + action: 'Kick', + danger: true, + onFinished: (proceed) => { + if (!proceed) return; + + this.setState({ updating: this.state.updating + 1 }); + this.props.matrixClient.kick( + this.props.member.roomId, this.props.member.userId, + ).then(function() { + // NO-OP; rely on the m.room.member event coming down else we could + // get out of sync if we force setState here! + console.log("Kick success"); + }, function(err) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Kick error", + description: err.message + }); + } + ).finally(()=>{ + this.setState({ updating: this.state.updating - 1 }); }); } - ).finally(()=>{ - this.setState({ updating: this.state.updating - 1 }); }); - this.props.onFinished(); }, onBan: function() { - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - var roomId = this.props.member.roomId; - var target = this.props.member.userId; - this.setState({ updating: this.state.updating + 1 }); - this.props.matrixClient.ban(roomId, target).then( - function() { - // NO-OP; rely on the m.room.member event coming down else we could - // get out of sync if we force setState here! - console.log("Ban success"); - }, function(err) { - Modal.createDialog(ErrorDialog, { - title: "Ban error", - description: err.message + const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); + Modal.createDialog(ConfirmUserActionDialog, { + member: this.props.member, + action: 'Ban', + danger: true, + onFinished: (proceed) => { + if (!proceed) return; + + this.setState({ updating: this.state.updating + 1 }); + this.props.matrixClient.ban( + this.props.member.roomId, this.props.member.userId, + ).then( + function() { + // NO-OP; rely on the m.room.member event coming down else we could + // get out of sync if we force setState here! + console.log("Ban success"); + }, function(err) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Ban error", + description: err.message, + }); + } + ).finally(()=>{ + this.setState({ updating: this.state.updating - 1 }); }); - } - ).finally(()=>{ - this.setState({ updating: this.state.updating - 1 }); + }, }); - this.props.onFinished(); }, onMuteToggle: function() { @@ -272,14 +283,12 @@ module.exports = WithMatrixClient(React.createClass({ var target = this.props.member.userId; var room = this.props.matrixClient.getRoom(roomId); if (!room) { - this.props.onFinished(); return; } var powerLevelEvent = room.currentState.getStateEvents( "m.room.power_levels", "" ); if (!powerLevelEvent) { - this.props.onFinished(); return; } var isMuted = this.state.muted; @@ -314,7 +323,6 @@ module.exports = WithMatrixClient(React.createClass({ this.setState({ updating: this.state.updating - 1 }); }); } - this.props.onFinished(); }, onModToggle: function() { @@ -323,19 +331,16 @@ module.exports = WithMatrixClient(React.createClass({ var target = this.props.member.userId; var room = this.props.matrixClient.getRoom(roomId); if (!room) { - this.props.onFinished(); return; } var powerLevelEvent = room.currentState.getStateEvents( "m.room.power_levels", "" ); if (!powerLevelEvent) { - this.props.onFinished(); return; } var me = room.getMember(this.props.matrixClient.credentials.userId); if (!me) { - this.props.onFinished(); return; } var defaultLevel = powerLevelEvent.getContent().users_default; @@ -366,7 +371,6 @@ module.exports = WithMatrixClient(React.createClass({ ).finally(()=>{ this.setState({ updating: this.state.updating - 1 }); }); - this.props.onFinished(); }, _applyPowerChange: function(roomId, target, powerLevel, powerLevelEvent) { @@ -386,7 +390,6 @@ module.exports = WithMatrixClient(React.createClass({ ).finally(()=>{ this.setState({ updating: this.state.updating - 1 }); }).done(); - this.props.onFinished(); }, onPowerChange: function(powerLevel) { @@ -396,14 +399,12 @@ module.exports = WithMatrixClient(React.createClass({ var room = this.props.matrixClient.getRoom(roomId); var self = this; if (!room) { - this.props.onFinished(); return; } var powerLevelEvent = room.currentState.getStateEvents( "m.room.power_levels", "" ); if (!powerLevelEvent) { - this.props.onFinished(); return; } if (powerLevelEvent.getContent().users) { @@ -422,9 +423,6 @@ module.exports = WithMatrixClient(React.createClass({ if (confirmed) { self._applyPowerChange(roomId, target, powerLevel, powerLevelEvent); } - else { - self.props.onFinished(); - } }, }); } @@ -440,7 +438,6 @@ module.exports = WithMatrixClient(React.createClass({ onNewDMClick: function() { this.setState({ updating: this.state.updating + 1 }); createRoom({dmUserId: this.props.member.userId}).finally(() => { - this.props.onFinished(); this.setState({ updating: this.state.updating - 1 }); }).done(); }, @@ -450,7 +447,6 @@ module.exports = WithMatrixClient(React.createClass({ action: 'leave_room', room_id: this.props.member.roomId, }); - this.props.onFinished(); }, _calculateOpsPermissions: function(member) { From 689972f0235c7df163cddcb2d15062964b99ba75 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 14 Feb 2017 13:57:22 +0000 Subject: [PATCH 17/30] Copyright --- src/components/views/rooms/MemberInfo.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 5f0578df60..de1218444c 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.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. From ff61b76bf7b1788a1b54962c9d5fdac87ba3333e Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 14 Feb 2017 13:58:29 +0000 Subject: [PATCH 18/30] Fix imports --- src/components/views/rooms/MemberInfo.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index de1218444c..7ee5af7af5 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -34,7 +34,7 @@ import sdk from '../../../index'; import createRoom from '../../../createRoom'; import DMRoomMap from '../../../utils/DMRoomMap'; import Unread from '../../../Unread'; -import Receipt from '../../../utils/Receipt'; +import findReadReceiptFromUserId from '../../../utils/Receipt'; import WithMatrixClient from '../../../wrappers/WithMatrixClient'; import AccessibleButton from '../elements/AccessibleButton'; @@ -158,7 +158,7 @@ module.exports = WithMatrixClient(React.createClass({ onRoomReceipt: function(receiptEvent, room) { // because if we read a notification, it will affect notification count // only bother updating if there's a receipt from us - if (Receipt.findReadReceiptFromUserId(receiptEvent, this.props.matrixClient.credentials.userId)) { + if (findReadReceiptFromUserId(receiptEvent, this.props.matrixClient.credentials.userId)) { this.forceUpdate(); } }, From 5e232d8500e4b842adc3c54766c7abd09fd7dc2a Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 14 Feb 2017 14:33:21 +0000 Subject: [PATCH 19/30] Argh, ES6 import syntax --- src/components/views/rooms/MemberInfo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 7ee5af7af5..fc2df0a5cc 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -34,7 +34,7 @@ import sdk from '../../../index'; import createRoom from '../../../createRoom'; import DMRoomMap from '../../../utils/DMRoomMap'; import Unread from '../../../Unread'; -import findReadReceiptFromUserId from '../../../utils/Receipt'; +import { findReadReceiptFromUserId } from '../../../utils/Receipt'; import WithMatrixClient from '../../../wrappers/WithMatrixClient'; import AccessibleButton from '../elements/AccessibleButton'; From 0303a42fc7b9cb0982a077ab091252c6988305a1 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 14 Feb 2017 15:35:14 +0000 Subject: [PATCH 20/30] Fix typo with Scalar popup --- src/components/views/messages/TextualBody.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index fd26ae58da..a625e63062 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -222,7 +222,8 @@ module.exports = React.createClass({ title: "Add an Integration", description:
- You are about to taken to a third-party site so you can authenticate your account for use with {integrationsUrl}.
+ You are about to be taken to a third-party site so you can + authenticate your account for use with {integrationsUrl}.
Do you wish to continue?
, button: "Continue", From a1c990a2ea0644c11dc0e49547d9759408d92c5e Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 14 Feb 2017 16:03:30 +0000 Subject: [PATCH 21/30] Make ban either ban or unban depending on whether the user is banned already Mostly gives some feedback that the ban has actually taken effect. --- src/components/views/rooms/MemberInfo.js | 42 +++++++++++++++--------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index fc2df0a5cc..babffb9a02 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -251,15 +251,23 @@ module.exports = WithMatrixClient(React.createClass({ const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); Modal.createDialog(ConfirmUserActionDialog, { member: this.props.member, - action: 'Ban', - danger: true, + action: this.props.member.membership == 'ban' ? 'Unban' : 'Ban', + danger: this.props.member.membership != 'ban', onFinished: (proceed) => { if (!proceed) return; this.setState({ updating: this.state.updating + 1 }); - this.props.matrixClient.ban( - this.props.member.roomId, this.props.member.userId, - ).then( + let promise; + if (this.props.member.membership == 'ban') { + promise = this.props.matrixClient.unban( + this.props.member.roomId, this.props.member.userId, + ); + } else { + promise = this.props.matrixClient.ban( + this.props.member.roomId, this.props.member.userId, + ); + } + promise.then( function() { // NO-OP; rely on the m.room.member event coming down else we could // get out of sync if we force setState here! @@ -451,26 +459,26 @@ module.exports = WithMatrixClient(React.createClass({ }, _calculateOpsPermissions: function(member) { - var defaultPerms = { + const defaultPerms = { can: {}, muted: false, modifyLevel: false }; - var room = this.props.matrixClient.getRoom(member.roomId); + const room = this.props.matrixClient.getRoom(member.roomId); if (!room) { return defaultPerms; } - var powerLevels = room.currentState.getStateEvents( + const powerLevels = room.currentState.getStateEvents( "m.room.power_levels", "" ); if (!powerLevels) { return defaultPerms; } - var me = room.getMember(this.props.matrixClient.credentials.userId); + const me = room.getMember(this.props.matrixClient.credentials.userId); if (!me) { return defaultPerms; } - var them = member; + const them = member; return { can: this._calculateCanPermissions( me, them, powerLevels.getContent() @@ -481,22 +489,22 @@ module.exports = WithMatrixClient(React.createClass({ }, _calculateCanPermissions: function(me, them, powerLevels) { - var can = { + const can = { kick: false, ban: false, mute: false, modifyLevel: false }; - var canAffectUser = them.powerLevel < me.powerLevel; + const canAffectUser = them.powerLevel < me.powerLevel; if (!canAffectUser) { //console.log("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel); return can; } - var editPowerLevel = ( + const editPowerLevel = ( (powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) || powerLevels.state_default ); - var levelToSend = ( + const levelToSend = ( (powerLevels.events ? powerLevels.events["m.room.message"] : null) || powerLevels.events_default ); @@ -643,10 +651,14 @@ module.exports = WithMatrixClient(React.createClass({ ); } if (this.state.can.ban) { + let label = 'Ban'; + if (this.props.member.membership == 'ban') { + label = 'Unban'; + } banButton = ( - Ban + {label} ); } From ec0ce76d87e019fa31f49c97ab2f9fe3e9ad53fd Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 14 Feb 2017 16:09:02 +0000 Subject: [PATCH 22/30] Clarify docs --- src/components/views/dialogs/ConfirmUserActionDialog.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.js b/src/components/views/dialogs/ConfirmUserActionDialog.js index 8dc96bb5f6..fbe719710b 100644 --- a/src/components/views/dialogs/ConfirmUserActionDialog.js +++ b/src/components/views/dialogs/ConfirmUserActionDialog.js @@ -21,7 +21,7 @@ import classnames from 'classnames'; /* * A dialog for confirming an operation on another user. * Takes a user ID and a verb, displays the target user prominently - * such that it should be easy to confirm that tne operation is being + * such that it should be easy to confirm that the operation is being * performed on the right person, and displays the operation prominently * to make it obvious what is going to happen. * Also tweaks the style for 'dangerous' actions (albeit only with colour) @@ -29,7 +29,7 @@ import classnames from 'classnames'; export default React.createClass({ displayName: 'ConfirmUserActionDialog', propTypes: { - member: React.PropTypes.object.isRequired, // member object + member: React.PropTypes.object.isRequired, // matrix-js-sdk member object action: React.PropTypes.string.isRequired, // eg. 'Ban' danger: React.PropTypes.bool, onFinished: React.PropTypes.func.isRequired, From 6663f5bff00b6f8a66a0430f0dca296c49098889 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 14 Feb 2017 16:12:04 +0000 Subject: [PATCH 23/30] Remove commented stuff That I've now broken such that it wouldnt work if it were uncommented --- src/components/structures/MatrixChat.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 87cd2645db..e5d0b560fa 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -904,14 +904,6 @@ module.exports = React.createClass({ onUserClick: function(event, userId) { event.preventDefault(); - // var MemberInfo = sdk.getComponent('rooms.MemberInfo'); - // var member = new Matrix.RoomMember(null, userId); - // ContextualMenu.createMenu(MemberInfo, { - // member: member, - // right: window.innerWidth - event.pageX, - // top: event.pageY - // }); - var member = new Matrix.RoomMember(null, userId); if (!member) { return; } dis.dispatch({ From f38b2dee78b03a7bb93993adfb3681df99d79ca8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 14 Feb 2017 17:06:16 +0000 Subject: [PATCH 24/30] Convert some missed buttons to AccessibleButton In RoomSettings --- src/components/views/rooms/RoomSettings.js | 31 +++++++++++----------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index a23368f5e8..4d1285678b 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -14,17 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -var q = require("q"); -var React = require('react'); -var MatrixClientPeg = require('../../../MatrixClientPeg'); -var SdkConfig = require('../../../SdkConfig'); -var sdk = require('../../../index'); -var Modal = require('../../../Modal'); -var ObjectUtils = require("../../../ObjectUtils"); -var dis = require("../../../dispatcher"); -var ScalarAuthClient = require("../../../ScalarAuthClient"); -var ScalarMessaging = require('../../../ScalarMessaging'); -var UserSettingsStore = require('../../../UserSettingsStore'); +import q from 'q'; +import React from 'react'; +import MatrixClientPeg from '../../../MatrixClientPeg'; +import SdkConfig from '../../../SdkConfig'; +import sdk from '../../../index'; +import Modal from '../../../Modal'; +import ObjectUtils from '../../../ObjectUtils'; +import dis from '../../../dispatcher'; +import ScalarAuthClient from '../../../ScalarAuthClient'; +import ScalarMessaging from '../../../ScalarMessaging'; +import UserSettingsStore from '../../../UserSettingsStore'; +import AccessibleButton from '../elements/AccessibleButton'; // parse a string as an integer; if the input is undefined, or cannot be parsed @@ -635,16 +636,16 @@ module.exports = React.createClass({ if (myMember) { if (myMember.membership === "join") { leaveButton = ( -
+ Leave room -
+ ); } else if (myMember.membership === "leave") { leaveButton = ( -
+ Forget room -
+ ); } } From 6fc70415cb4defa3c9cd5090d0993c470694fa84 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 14 Feb 2017 17:29:40 +0000 Subject: [PATCH 25/30] s/onBan/onBanOrUnban/ --- src/components/views/rooms/MemberInfo.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index babffb9a02..699ee8a3a2 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -247,7 +247,7 @@ module.exports = WithMatrixClient(React.createClass({ }); }, - onBan: function() { + onBanOrUnban: function() { const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); Modal.createDialog(ConfirmUserActionDialog, { member: this.props.member, @@ -657,7 +657,7 @@ module.exports = WithMatrixClient(React.createClass({ } banButton = ( + onClick={this.onBanOrUnban}> {label} ); From 8067bb627f9d5e3f84dbef2637206dc8052bf3a4 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 15 Feb 2017 16:29:08 +0000 Subject: [PATCH 26/30] Display avatar initials in typing notifications It seems they don't overlap hawkwawdly anymore, so this displays them always. Fixes https://github.com/vector-im/riot-web/issues/3084 --- src/components/structures/RoomStatusBar.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index 288ca0b974..ca50e1071a 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -223,8 +223,7 @@ module.exports = React.createClass({ users = users.slice(0, limit - 1); } - let avatars = users.map((u, index) => { - let showInitial = othersCount === 0 && index === users.length - 1; + const avatars = users.map((u) => { return ( ); }); From bdb8f9d05292300501ae5516bde780be17ed00b5 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 15 Feb 2017 18:44:15 +0000 Subject: [PATCH 27/30] Don't force-logout the user if reading localstorage fails Give them a modal dialog to give them a chance to abort. --- src/Lifecycle.js | 82 +++++++++++++------ src/component-index.js | 2 + .../dialogs/SessionRestoreErrorDialog.js | 74 +++++++++++++++++ 3 files changed, 133 insertions(+), 25 deletions(-) create mode 100644 src/components/views/dialogs/SessionRestoreErrorDialog.js diff --git a/src/Lifecycle.js b/src/Lifecycle.js index e899ec6ad8..e891ab5984 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.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. @@ -24,6 +25,8 @@ import Presence from './Presence'; import dis from './dispatcher'; import DMRoomMap from './utils/DMRoomMap'; import RtsClient from './RtsClient'; +import Modal from './Modal'; +import sdk from './index'; /** * Called at startup, to attempt to build a logged-in Matrix session. It tries @@ -109,16 +112,17 @@ export function loadSession(opts) { return q(); } - if (_restoreFromLocalStorage()) { - return q(); - } + return _restoreFromLocalStorage().then((success) => { + if (success) { + return; + } - if (enableGuest) { - return _registerAsGuest(guestHsUrl, guestIsUrl, defaultDeviceDisplayName); - } + if (enableGuest) { + return _registerAsGuest(guestHsUrl, guestIsUrl, defaultDeviceDisplayName); + } - // fall back to login screen - return q(); + // fall back to login screen + }); } function _loginWithToken(queryParams, defaultDeviceDisplayName) { @@ -178,10 +182,11 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) { }); } -// returns true if a session is found in localstorage +// returns a promise which resolves to true if a session is found in +// localstorage function _restoreFromLocalStorage() { if (!localStorage) { - return false; + return q(false); } const hs_url = localStorage.getItem("mx_hs_url"); const is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org'; @@ -208,28 +213,55 @@ function _restoreFromLocalStorage() { identityServerUrl: is_url, guest: is_guest, }); - return true; + return q(true); } catch (e) { - console.log("Unable to restore session", e); - - var msg = e.message; - if (msg == "OLM.BAD_LEGACY_ACCOUNT_PICKLE") { - msg = "You need to log back in to generate end-to-end encryption keys " - + "for this device and submit the public key to your homeserver. " - + "This is a once off; sorry for the inconvenience."; - } - - // don't leak things into the new session - _clearLocalStorage(); - - throw new Error("Unable to restore previous session: " + msg); + return _handleRestoreFailure(e); } } else { console.log("No previous session found."); - return false; + return q(false); } } +function _handleRestoreFailure(e) { + console.log("Unable to restore session", e); + + let msg = e.message; + if (msg == "OLM.BAD_LEGACY_ACCOUNT_PICKLE") { + msg = "You need to log back in to generate end-to-end encryption keys " + + "for this device and submit the public key to your homeserver. " + + "This is a once off; sorry for the inconvenience."; + + _clearLocalStorage(); + + return q.reject(new Error( + "Unable to restore previous session: " + msg, + )); + } + + const def = q.defer(); + const SessionRestoreErrorDialog = + sdk.getComponent('views.dialogs.SessionRestoreErrorDialog'); + + Modal.createDialog(SessionRestoreErrorDialog, { + error: msg, + onFinished: (success) => { + def.resolve(success); + }, + }); + + return def.promise.then((success) => { + if (success) { + // user clicked continue. + _clearLocalStorage(); + return false; + } + + // try, try again + return _restoreFromLocalStorage(); + }); +} + let rtsClient = null; export function initRtsClient(url) { rtsClient = new RtsClient(url); diff --git a/src/component-index.js b/src/component-index.js index 62d26d4ce7..c705150e12 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -89,6 +89,8 @@ import views$dialogs$NeedToRegisterDialog from './components/views/dialogs/NeedT views$dialogs$NeedToRegisterDialog && (module.exports.components['views.dialogs.NeedToRegisterDialog'] = views$dialogs$NeedToRegisterDialog); import views$dialogs$QuestionDialog from './components/views/dialogs/QuestionDialog'; 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$TextInputDialog from './components/views/dialogs/TextInputDialog'; diff --git a/src/components/views/dialogs/SessionRestoreErrorDialog.js b/src/components/views/dialogs/SessionRestoreErrorDialog.js new file mode 100644 index 0000000000..358bbf1fec --- /dev/null +++ b/src/components/views/dialogs/SessionRestoreErrorDialog.js @@ -0,0 +1,74 @@ +/* +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. +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 SdkConfig from '../../../SdkConfig'; +import Modal from '../../../Modal'; + + +export default React.createClass({ + displayName: 'SessionRestoreErrorDialog', + + propTypes: { + error: React.PropTypes.string.isRequired, + onFinished: React.PropTypes.func.isRequired, + }, + + _sendBugReport: function() { + const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog"); + Modal.createDialog(BugReportDialog, {}); + }, + + _continueClicked: function() { + this.props.onFinished(true); + }, + + render: function() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + let bugreport; + + if (SdkConfig.get().bug_report_endpoint_url) { + bugreport = ( +

Otherwise, + click here to send a bug report. +

+ ); + } + + return ( + +
+

We encountered an error trying to restore your previous session. If + you continue, you will need to log in again, and encrypted chat + history will be unreadable.

+ +

If you have previously used a more recent version of Riot, your session + may be incompatible with this version. Close this window and return + to the more recent version.

+ + {bugreport} +
+
+ +
+
+ ); + }, +}); From ebe7ec4000eca85b74ebd5d30dc5692dbd95c7ac Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Wed, 15 Feb 2017 09:16:17 +0530 Subject: [PATCH 28/30] Rename RTE labs option to "New Composer & Autocomplete" As per confusion around https://riot.im/develop/#/room/!DdJkzRliezrwpNebLk:matrix.org/$1487091505948TmtGn:t2l.io --- src/UserSettingsStore.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index d7d3e7bc7a..66a872958c 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -26,7 +26,7 @@ var Notifier = require("./Notifier"); module.exports = { LABS_FEATURES: [ { - name: 'Rich Text Editor', + name: 'New Composer & Autocomplete', id: 'rich_text_editor', default: false, }, From 74487c655d2d7b37d578b6e02c40d92916bc03e2 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 16 Feb 2017 09:22:44 +0000 Subject: [PATCH 29/30] If a referrer hasn't been specified, use empty string This is interpretted by the RTS as a non-referred team member who still needs the team token to access their welcome page etc. --- src/components/structures/login/Registration.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index fe9fd30062..db1147a5d2 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -218,13 +218,11 @@ module.exports = React.createClass({ // will just nop. The point of this being we might not have the email address // that the user registered with at this stage (depending on whether this // is the client they initiated registration). - if ( - self._rtsClient && - self.props.referrer - ) { - // Track referral, get team_token in order to retrieve team config + if (self._rtsClient) { + // Track referral if self.props.referrer set, get team_token in order to + // retrieve team config and see welcome page etc. self._rtsClient.trackReferral( - self.props.referrer, + self.props.referrer || '', // Default to empty string = not referred self.registerLogic.params.idSid, self.registerLogic.params.clientSecret ).then((data) => { From 0e66b370d46e3194f86d844c360fac2203adf31e Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Thu, 16 Feb 2017 16:49:00 +0530 Subject: [PATCH 30/30] fix eslint's no-invalid-this rule for class properties --- .eslintrc.js | 6 ++++++ package.json | 1 + 2 files changed, 7 insertions(+) diff --git a/.eslintrc.js b/.eslintrc.js index 34d3af270c..6cd0e1015e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -13,6 +13,7 @@ module.exports = { plugins: [ "react", "flowtype", + "babel" ], env: { es6: true, @@ -23,6 +24,11 @@ module.exports = { } }, rules: { + // eslint's built in no-invalid-this rule breaks with class properties + "no-invalid-this": "off", + // so we replace it with a version that is class property aware + "babel/no-invalid-this": "error", + /** react **/ // This just uses the react plugin to help eslint known when // variables have been used in JSX diff --git a/package.json b/package.json index 6e7013fb93..a07e2236aa 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "babel-preset-react": "^6.11.1", "eslint": "^3.13.1", "eslint-config-google": "^0.7.1", + "eslint-plugin-babel": "^4.0.1", "eslint-plugin-flowtype": "^2.30.0", "eslint-plugin-react": "^6.9.0", "expect": "^1.16.0",