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", diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 87a2878e37..d5683b35a0 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/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/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; 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, }, diff --git a/src/component-index.js b/src/component-index.js index 5b28be0627..c705150e12 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'; @@ -75,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'; @@ -85,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/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js new file mode 100644 index 0000000000..70b3c2e306 --- /dev/null +++ b/src/components/structures/InteractiveAuth.js @@ -0,0 +1,152 @@ +/* +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, + + // 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, + }, + + 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(); + } + }, + + _submitAuthDict: function(authData) { + this._authLogic.submitAuthDict(authData); + }, + + _renderCurrentStage: function() { + const stage = this.state.authStage; + var StageComponent = getEntryComponentForLoginType(stage); + return ( + + ); + }, + + render: function() { + let error = null; + if (this.state.errorText) { + error = ( +
+ {this.state.errorText} +
+ ); + } + + return ( +
+
+ {this._renderCurrentStage()} + {error} +
+
+ ); + }, +}); diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 8fdcf15e1b..72680a3eac 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 (teamName && 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,19 @@ 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'); + + // 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}`); + } }, componentDidMount: function() { @@ -888,14 +910,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({ @@ -975,6 +989,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 +1122,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/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 ( ); }); diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index efe7dae723..db1147a5d2 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, @@ -213,20 +214,22 @@ module.exports = React.createClass({ accessToken: response.access_token }); - if ( - self._rtsClient && - self.props.referrer && - self.state.teamSelected - ) { - // Track referral, get team_token in order to retrieve team config + // 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) { + // 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, - response.user_id, - self.state.formVals.email + self.props.referrer || '', // Default to empty string = not referred + self.registerLogic.params.idSid, + self.registerLogic.params.clientSecret ).then((data) => { const teamToken = data.team_token; // Store for use /w welcome pages window.localStorage.setItem('mx_team_token', teamToken); + self.props.onTeamMemberRegistered(teamToken); self._rtsClient.getTeam(teamToken).then((team) => { console.log( @@ -426,7 +429,12 @@ module.exports = React.createClass({ return (
- + {this._getRegisterContentJsx()}
diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index 2b3980c536..e83403ef7c 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,21 @@ export default React.createClass({ } }, + _onCancelClick: function(e) { + this.props.onFinished(); + }, + render: function() { return (
+ + Cancel +
{ this.props.title }
diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.js b/src/components/views/dialogs/ConfirmUserActionDialog.js new file mode 100644 index 0000000000..fbe719710b --- /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 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) + */ +export default React.createClass({ + displayName: 'ConfirmUserActionDialog', + propTypes: { + 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, + }, + + 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/dialogs/InteractiveAuthDialog.js b/src/components/views/dialogs/InteractiveAuthDialog.js index a4abbb17d9..66b662b23d 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. @@ -15,13 +16,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 +41,29 @@ 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() { - 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/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} +
+
+ +
+
+ ); + }, +}); diff --git a/src/components/views/login/InteractiveAuthEntryComponents.js b/src/components/views/login/InteractiveAuthEntryComponents.js index ec184ca09f..e18e60d7bc 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. @@ -20,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). * @@ -32,10 +33,10 @@ 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 + * 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): - * onSubmitClick: handle a 'submit' button click * focus: set the input focus appropriately in the form. */ @@ -48,12 +49,16 @@ export const PasswordAuthEntry = React.createClass({ propTypes: { submitAuthDict: React.PropTypes.func.isRequired, - setSubmitButtonEnabled: React.PropTypes.func.isRequired, errorText: React.PropTypes.string, + // is the auth logic currently waiting for something to + // happen? + busy: React.PropTypes.bool, }, - componentWillMount: function() { - this.props.setSubmitButtonEnabled(false); + getInitialState: function() { + return { + passwordValid: false, + }; }, focus: function() { @@ -62,7 +67,10 @@ export const PasswordAuthEntry = React.createClass({ } }, - onSubmitClick: function() { + _onSubmit: function(e) { + e.preventDefault(); + if (this.props.busy) return; + this.props.submitAuthDict({ type: PasswordAuthEntry.LOGIN_TYPE, user: MatrixClientPeg.get().credentials.userId, @@ -72,7 +80,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({ + passwordValid: Boolean(this.refs.passwordField.value), + }); }, render: function() { @@ -82,16 +92,34 @@ 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.

Password:

- +
+ +
+ {submitButtonOrSpinner} +
+
{this.props.errorText}
@@ -110,14 +138,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 +171,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 +178,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); }, @@ -167,13 +188,12 @@ export const FallbackAuthEntry = React.createClass({ } }, - onSubmitClick: function() { + _onShowFallbackClick: function() { var url = MatrixClientPeg.get().getFallbackAuthUrl( this.props.loginType, this.props.authSessionId ); this._popupWindow = window.open(url); - this.props.setSubmitButtonEnabled(false); }, _onReceiveMessage: function(event) { @@ -188,7 +208,7 @@ export const FallbackAuthEntry = React.createClass({ render: function() { return (
- Click "Submit" to authenticate + Start authentication
{this.props.errorText}
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", diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index d33b8f3524..699ee8a3a2 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. @@ -25,16 +26,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 { findReadReceiptFromUserId } from '../../../utils/Receipt'; +import WithMatrixClient from '../../../wrappers/WithMatrixClient'; import AccessibleButton from '../elements/AccessibleButton'; module.exports = WithMatrixClient(React.createClass({ @@ -43,13 +44,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() { @@ -164,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(); } }, @@ -224,46 +218,72 @@ 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 + onBanOrUnban: function() { + const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); + Modal.createDialog(ConfirmUserActionDialog, { + member: this.props.member, + 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 }); + 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! + 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 +292,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 +332,6 @@ module.exports = WithMatrixClient(React.createClass({ this.setState({ updating: this.state.updating - 1 }); }); } - this.props.onFinished(); }, onModToggle: function() { @@ -323,19 +340,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 +380,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 +399,6 @@ module.exports = WithMatrixClient(React.createClass({ ).finally(()=>{ this.setState({ updating: this.state.updating - 1 }); }).done(); - this.props.onFinished(); }, onPowerChange: function(powerLevel) { @@ -396,14 +408,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 +432,6 @@ module.exports = WithMatrixClient(React.createClass({ if (confirmed) { self._applyPowerChange(roomId, target, powerLevel, powerLevelEvent); } - else { - self.props.onFinished(); - } }, }); } @@ -440,7 +447,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,30 +456,29 @@ module.exports = WithMatrixClient(React.createClass({ action: 'leave_room', room_id: this.props.member.roomId, }); - this.props.onFinished(); }, _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() @@ -484,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 ); @@ -646,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 + onClick={this.onBanOrUnban}> + {label} ); } 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 -
+ ); } } 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(() => {