From ef88e02931f8de61960e362a636e47bc7fb102fc Mon Sep 17 00:00:00 2001 From: Sijmen Schoon Date: Sun, 8 Jan 2017 02:20:59 +0100 Subject: [PATCH 01/24] Add support for pasting into the text box Only supports the new rich-text-supporting text editor --- src/ContentMessages.js | 4 ++-- src/components/views/rooms/MessageComposer.js | 10 ++++++---- src/components/views/rooms/MessageComposerInput.js | 8 ++++++++ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/ContentMessages.js b/src/ContentMessages.js index c169ce64b5..765c7ed976 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -276,7 +276,7 @@ class ContentMessages { sendContentToRoom(file, roomId, matrixClient) { const content = { - body: file.name, + body: file.name || 'Attachment', info: { size: file.size, } @@ -316,7 +316,7 @@ class ContentMessages { } const upload = { - fileName: file.name, + fileName: file.name || 'Attachment', roomId: roomId, total: 0, loaded: 0, diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index ee9c49d52a..6810e75f53 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -91,8 +91,9 @@ export default class MessageComposer extends React.Component { this.refs.uploadInput.click(); } - onUploadFileSelected(ev) { - let files = ev.target.files; + onUploadFileSelected(files, isPasted) { + if (!isPasted) + files = files.target.files; let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); let TintableSvg = sdk.getComponent("elements.TintableSvg"); @@ -100,7 +101,7 @@ export default class MessageComposer extends React.Component { let fileList = []; for (let i=0; i - {files[i].name} + {files[i].name || 'Attachment'} ); } @@ -171,7 +172,7 @@ export default class MessageComposer extends React.Component { } onUpArrow() { - return this.refs.autocomplete.onUpArrow(); + return this.refs.autocomplete.onUpArrow(); } onDownArrow() { @@ -293,6 +294,7 @@ export default class MessageComposer extends React.Component { tryComplete={this._tryComplete} onUpArrow={this.onUpArrow} onDownArrow={this.onDownArrow} + onUploadFileSelected={this.onUploadFileSelected} tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete onContentChanged={this.onInputContentChanged} onInputStateChanged={this.onInputStateChanged} />, diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 37d937d6f5..f0658ab543 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -83,6 +83,7 @@ export default class MessageComposerInput extends React.Component { this.onAction = this.onAction.bind(this); this.handleReturn = this.handleReturn.bind(this); this.handleKeyCommand = this.handleKeyCommand.bind(this); + this.handlePastedFiles = this.handlePastedFiles.bind(this); this.onEditorContentChanged = this.onEditorContentChanged.bind(this); this.setEditorState = this.setEditorState.bind(this); this.onUpArrow = this.onUpArrow.bind(this); @@ -473,6 +474,10 @@ export default class MessageComposerInput extends React.Component { return false; } + handlePastedFiles(files) { + this.props.onUploadFileSelected(files, true); + } + handleReturn(ev) { if (ev.shiftKey) { this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState)); @@ -728,6 +733,7 @@ export default class MessageComposerInput extends React.Component { keyBindingFn={MessageComposerInput.getKeyBinding} handleKeyCommand={this.handleKeyCommand} handleReturn={this.handleReturn} + handlePastedFiles={this.handlePastedFiles} stripPastedStyles={!this.state.isRichtextEnabled} onTab={this.onTab} onUpArrow={this.onUpArrow} @@ -757,6 +763,8 @@ MessageComposerInput.propTypes = { onDownArrow: React.PropTypes.func, + onUploadFileSelected: React.PropTypes.func, + // attempts to confirm currently selected completion, returns whether actually confirmed tryComplete: React.PropTypes.func, From 2bd9885288c09e4fe0d56c5154ca1d816f5f9efc Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 3 Mar 2017 15:42:24 +0000 Subject: [PATCH 02/24] Start to show redacted events --- src/TextForEvent.js | 1 - src/components/structures/MessagePanel.js | 11 +++++++++-- src/components/views/messages/TextualBody.js | 4 ++++ src/components/views/rooms/EventTile.js | 11 +++++++---- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/TextForEvent.js b/src/TextForEvent.js index 3f772e9cfb..3e1659f392 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -116,7 +116,6 @@ function textForRoomNameEvent(ev) { function textForMessageEvent(ev) { var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); - var message = senderDisplayName + ': ' + ev.getContent().body; if (ev.getContent().msgtype === "m.emote") { message = "* " + senderDisplayName + " " + message; diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 0981b7b706..21665bb421 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -295,7 +295,10 @@ module.exports = React.createClass({ var last = (i == lastShownEventIndex); // Wrap consecutive member events in a ListSummary, ignore if redacted - if (isMembershipChange(mxEv) && EventTile.haveTileForEvent(mxEv)) { + if (isMembershipChange(mxEv) && + EventTile.haveTileForEvent(mxEv) && + !mxEv.isRedacted() + ) { let ts1 = mxEv.getTs(); // Ensure that the key of the MemberEventListSummary does not change with new // member events. This will prevent it from being re-created unnecessarily, and @@ -481,13 +484,17 @@ module.exports = React.createClass({ // here. return !this.props.suppressFirstDateSeparator; } + const prevEventDate = prevEvent.getDate(); + if (!nextEventDate || !prevEventDate) { + return false; + } // Return early for events that are > 24h apart if (Math.abs(prevEvent.getTs() - nextEventDate.getTime()) > MILLIS_IN_DAY) { return true; } // Compare weekdays - return prevEvent.getDate().getDay() !== nextEventDate.getDay(); + return prevEventDate.getDay() !== nextEventDate.getDay(); }, // get a list of read receipts that should be shown next to this event diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index a625e63062..0030fe6575 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -246,6 +246,10 @@ module.exports = React.createClass({ var mxEvent = this.props.mxEvent; var content = mxEvent.getContent(); + if (mxEvent.isRedacted()) { + content = {body: "Message redacted by " + mxEvent.event.redacted_because.sender}; + } + var body = HtmlUtils.bodyToHtml(content, this.props.highlights, {}); if (this.props.highlightLink) { diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index c9508428ba..f011b5517a 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -396,6 +396,7 @@ module.exports = WithMatrixClient(React.createClass({ var e2eEnabled = this.props.matrixClient.isRoomEncrypted(this.props.mxEvent.getRoomId()); var isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1); + const isRedacted = this.props.mxEvent.isRedacted(); var classes = classNames({ mx_EventTile: true, @@ -412,6 +413,7 @@ module.exports = WithMatrixClient(React.createClass({ mx_EventTile_verified: this.state.verified == true, mx_EventTile_unverified: this.state.verified == false, mx_EventTile_bad: this.props.mxEvent.getContent().msgtype === 'm.bad.encrypted', + mx_EventTile_redacted: isRedacted, }); var permalink = "#/room/" + this.props.mxEvent.getRoomId() +"/"+ this.props.mxEvent.getId(); @@ -486,6 +488,8 @@ module.exports = WithMatrixClient(React.createClass({ else if (e2eEnabled) { e2e = ; } + const timestamp = this.props.mxEvent.isRedacted() ? + null : ; if (this.props.tileShape === "notif") { var room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId()); @@ -501,7 +505,7 @@ module.exports = WithMatrixClient(React.createClass({ { avatar } { sender } - + { timestamp }
@@ -530,7 +534,7 @@ module.exports = WithMatrixClient(React.createClass({
{ sender } - + { timestamp }
@@ -546,7 +550,7 @@ module.exports = WithMatrixClient(React.createClass({ { sender }
- + { timestamp } { e2e } Date: Fri, 3 Mar 2017 15:51:14 +0000 Subject: [PATCH 03/24] Remove seemingly unused "bounce" --- src/components/views/rooms/EventTile.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index f011b5517a..c262fea15f 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -29,14 +29,6 @@ var dispatcher = require("../../../dispatcher"); var ObjectUtils = require('../../../ObjectUtils'); -var bounce = false; -try { - if (global.localStorage) { - bounce = global.localStorage.getItem('avatar_bounce') == 'true'; - } -} catch (e) { -} - var eventTileTypes = { 'm.room.message': 'messages.MessageEvent', 'm.room.member' : 'messages.TextualEvent', From 5ef61b7c35f0ca55695b09e5a5f1892bbcd22af8 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 3 Mar 2017 16:45:29 +0000 Subject: [PATCH 04/24] Only show a redaction tile for messages --- src/components/views/messages/TextualBody.js | 4 ---- src/components/views/messages/UnknownBody.js | 7 +++++-- src/components/views/rooms/EventTile.js | 9 +++++++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 0030fe6575..a625e63062 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -246,10 +246,6 @@ module.exports = React.createClass({ var mxEvent = this.props.mxEvent; var content = mxEvent.getContent(); - if (mxEvent.isRedacted()) { - content = {body: "Message redacted by " + mxEvent.event.redacted_because.sender}; - } - var body = HtmlUtils.bodyToHtml(content, this.props.highlights, {}); if (this.props.highlightLink) { diff --git a/src/components/views/messages/UnknownBody.js b/src/components/views/messages/UnknownBody.js index 00784b18b0..5504c0b1fe 100644 --- a/src/components/views/messages/UnknownBody.js +++ b/src/components/views/messages/UnknownBody.js @@ -22,10 +22,13 @@ module.exports = React.createClass({ displayName: 'UnknownBody', render: function() { - var content = this.props.mxEvent.getContent(); + var text = this.props.mxEvent.getContent().body; + if (this.props.mxEvent.isRedacted()) { + text = "This event was redacted"; + } return ( - {content.body} + {text} ); }, diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index c262fea15f..087cef7689 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -388,7 +388,7 @@ module.exports = WithMatrixClient(React.createClass({ var e2eEnabled = this.props.matrixClient.isRoomEncrypted(this.props.mxEvent.getRoomId()); var isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1); - const isRedacted = this.props.mxEvent.isRedacted(); + const isRedacted = (eventType === 'm.room.message') && this.props.mxEvent.isRedacted(); var classes = classNames({ mx_EventTile: true, @@ -415,7 +415,10 @@ module.exports = WithMatrixClient(React.createClass({ let avatarSize; let needsSenderProfile; - if (this.props.tileShape === "notif") { + if (isRedacted) { + avatarSize = 0; + needsSenderProfile = false; + } else if (this.props.tileShape === "notif") { avatarSize = 24; needsSenderProfile = true; } else if (isInfoMessage) { @@ -560,6 +563,8 @@ module.exports = WithMatrixClient(React.createClass({ })); module.exports.haveTileForEvent = function(e) { + // Only messages have a tile (black-rectangle) if redacted + if (e.isRedacted() && e.getType() !== 'm.room.message') return false; if (eventTileTypes[e.getType()] == undefined) return false; if (eventTileTypes[e.getType()] == 'messages.TextualEvent') { return TextForEvent.textForEvent(e) !== ''; From 9bae9368165e76b8622df6cb574b4c866ba9cbf5 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 3 Mar 2017 17:35:42 +0000 Subject: [PATCH 05/24] Add the redacter display name to the redaction text --- src/components/views/messages/UnknownBody.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/views/messages/UnknownBody.js b/src/components/views/messages/UnknownBody.js index 5504c0b1fe..95b3a1b54a 100644 --- a/src/components/views/messages/UnknownBody.js +++ b/src/components/views/messages/UnknownBody.js @@ -17,14 +17,19 @@ limitations under the License. 'use strict'; var React = require('react'); +var MatrixClientPeg = require('../../../MatrixClientPeg'); module.exports = React.createClass({ displayName: 'UnknownBody', render: function() { - var text = this.props.mxEvent.getContent().body; - if (this.props.mxEvent.isRedacted()) { - text = "This event was redacted"; + const ev = this.props.mxEvent; + var text = ev.getContent().body; + if (ev.isRedacted()) { + const room = MatrixClientPeg.get().getRoom(ev.getRoomId()); + const because = ev.getUnsigned().redacted_because; + const name = room.getMember(because.sender).name || because.sender; + text = "This event was redacted by " + name; } return ( From abd71cd2ac19bd7ba12a4c683cff05908daee1d7 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 3 Mar 2017 17:57:13 +0000 Subject: [PATCH 06/24] No need for "redactor" as we dont currently show it --- src/components/views/messages/UnknownBody.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/views/messages/UnknownBody.js b/src/components/views/messages/UnknownBody.js index 95b3a1b54a..374a4b9396 100644 --- a/src/components/views/messages/UnknownBody.js +++ b/src/components/views/messages/UnknownBody.js @@ -17,7 +17,6 @@ limitations under the License. 'use strict'; var React = require('react'); -var MatrixClientPeg = require('../../../MatrixClientPeg'); module.exports = React.createClass({ displayName: 'UnknownBody', @@ -26,10 +25,7 @@ module.exports = React.createClass({ const ev = this.props.mxEvent; var text = ev.getContent().body; if (ev.isRedacted()) { - const room = MatrixClientPeg.get().getRoom(ev.getRoomId()); - const because = ev.getUnsigned().redacted_because; - const name = room.getMember(because.sender).name || because.sender; - text = "This event was redacted by " + name; + text = "This event was redacted"; } return ( From edccfeb20b28e0306e1fca1bffbf1b36d99bc821 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 6 Mar 2017 10:26:26 +0000 Subject: [PATCH 07/24] No text required, do not continuate after redacted even It's curious, however, that a continuation occured after a redacted event, given that the event shouldn't have a sender --- src/components/structures/MessagePanel.js | 4 +++- src/components/views/messages/UnknownBody.js | 6 +----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 21665bb421..0b16a41590 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -411,7 +411,9 @@ module.exports = React.createClass({ // is this a continuation of the previous message? var continuation = false; - if (prevEvent !== null && prevEvent.sender && mxEv.sender + + if (prevEvent !== null + && !prevEvent.isRedacted() && prevEvent.sender && mxEv.sender && mxEv.sender.userId === prevEvent.sender.userId && mxEv.getType() == prevEvent.getType()) { continuation = true; diff --git a/src/components/views/messages/UnknownBody.js b/src/components/views/messages/UnknownBody.js index 374a4b9396..a0fe8fdf74 100644 --- a/src/components/views/messages/UnknownBody.js +++ b/src/components/views/messages/UnknownBody.js @@ -22,11 +22,7 @@ module.exports = React.createClass({ displayName: 'UnknownBody', render: function() { - const ev = this.props.mxEvent; - var text = ev.getContent().body; - if (ev.isRedacted()) { - text = "This event was redacted"; - } + const text = this.props.mxEvent.getContent().body; return ( {text} From c0fc3ba3fe6418aeae9df721a0ddbe9f5a916565 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 6 Mar 2017 14:20:24 +0000 Subject: [PATCH 08/24] Make redactions appear when the event has been redacted (on Room.redaction) --- src/components/structures/MessagePanel.js | 1 + src/components/views/rooms/EventTile.js | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 0b16a41590..ff507b6f90 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -466,6 +466,7 @@ module.exports = React.createClass({ ref={this._collectEventNode.bind(this, eventId)} data-scroll-token={scrollToken}> Date: Wed, 8 Mar 2017 10:25:54 +0000 Subject: [PATCH 09/24] Decide on which screen to show after login in one place This follows from a small amount of refactoring done when RTS was introduced. Instead of setting the screen after sync, do it only after login. This requires as-yet-to-be-PRd riot-web changes. This includes: - initialScreenAfterLogin, which can be used to set the screen after login, and represents the screen that would be viewed if the window.location at the time of initialising Riot were routed. - guestCreds are now part of state, because otherwise they don't cause the login/registration views to update when set. - instead of worrying about races and using this._setPage, use a dispatch. --- src/components/structures/MatrixChat.js | 79 +++++++++++-------- .../structures/login/Registration.js | 1 - 2 files changed, 44 insertions(+), 36 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 44fdfcf23e..7c398b39f9 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -63,6 +63,13 @@ module.exports = React.createClass({ // called when the session load completes onLoadCompleted: React.PropTypes.func, + // Represents the screen to display as a result of parsing the initial + // window.location + initialScreenAfterLogin: React.PropTypes.shape({ + screen: React.PropTypes.string.isRequired, + params: React.PropTypes.object, + }), + // displayname, if any, to set on the device when logging // in/registering. defaultDeviceDisplayName: React.PropTypes.string, @@ -89,6 +96,12 @@ module.exports = React.createClass({ var s = { loading: true, screen: undefined, + screenAfterLogin: this.props.initialScreenAfterLogin, + + // Stashed guest credentials if the user logs out + // whilst logged in as a guest user (so they can change + // their mind & log back in) + guestCreds: null, // What the LoggedInView would be showing if visible page_type: null, @@ -184,11 +197,6 @@ module.exports = React.createClass({ componentWillMount: function() { SdkConfig.put(this.props.config); - // Stashed guest credentials if the user logs out - // whilst logged in as a guest user (so they can change - // their mind & log back in) - this.guestCreds = null; - // if the automatic session load failed, the error this.sessionLoadError = null; @@ -322,9 +330,6 @@ module.exports = React.createClass({ var self = this; switch (payload.action) { case 'logout': - if (MatrixClientPeg.get().isGuest()) { - this.guestCreds = MatrixClientPeg.getCredentials(); - } Lifecycle.logout(); break; case 'start_registration': @@ -344,7 +349,11 @@ module.exports = React.createClass({ this.notifyNewScreen('register'); break; case 'start_login': - if (this.state.logged_in) return; + if (MatrixClientPeg.get().isGuest()) { + this.setState({ + guestCreds: MatrixClientPeg.getCredentials(), + }); + } this.setStateForNewScreen({ screen: 'login', }); @@ -359,8 +368,8 @@ module.exports = React.createClass({ // also stash our credentials, then if we restore the session, // we can just do it the same way whether we started upgrade // registration or explicitly logged out - this.guestCreds = MatrixClientPeg.getCredentials(); this.setStateForNewScreen({ + guestCreds: MatrixClientPeg.getCredentials(), screen: "register", upgradeUsername: MatrixClientPeg.get().getUserIdLocalpart(), guestAccessToken: MatrixClientPeg.get().getAccessToken(), @@ -708,19 +717,34 @@ module.exports = React.createClass({ * Called when a new logged in session has started */ _onLoggedIn: function(teamToken) { - this.guestCreds = null; - this.notifyNewScreen(''); this.setState({ - screen: undefined, + guestCreds: null, logged_in: true, }); + // If screenAfterLogin is set, use that, then null it so that a second login will + // result in view_home_page, _user_settings or _room_directory + if (this.state.screenAfterLogin) { + this.showScreen( + this.state.screenAfterLogin.screen, + this.state.screenAfterLogin.params + ); + this.setState({screenAfterLogin: null}); + return; + } else { + this.setState({screen: undefined}); + } + if (teamToken) { this._teamToken = teamToken; - this._setPage(PageTypes.HomePage); + dis.dispatch({action: 'view_home_page'}); + return; } else if (this._is_registered) { - this._setPage(PageTypes.UserSettings); + dis.dispatch({action: 'view_user_settings'}); + return; } + + dis.dispatch({action: 'view_room_directory'}); }, /** @@ -768,12 +792,6 @@ module.exports = React.createClass({ cli.getRooms() )[0].roomId; self.setState({ready: true, currentRoomId: firstRoom, page_type: PageTypes.RoomView}); - } else { - if (self._teamToken) { - self.setState({ready: true, page_type: PageTypes.HomePage}); - } else { - self.setState({ready: true, page_type: PageTypes.RoomDirectory}); - } } } else { self.setState({ready: true, page_type: PageTypes.RoomView}); @@ -790,16 +808,7 @@ module.exports = React.createClass({ if (presentedId != undefined) { self.notifyNewScreen('room/'+presentedId); - } else { - // There is no information on presentedId - // so point user to fallback like /directory - if (self._teamToken) { - self.notifyNewScreen('home'); - } else { - self.notifyNewScreen('directory'); - } } - dis.dispatch({action: 'focus_composer'}); } else { self.setState({ready: true}); @@ -1002,9 +1011,9 @@ module.exports = React.createClass({ onReturnToGuestClick: function() { // reanimate our guest login - if (this.guestCreds) { - Lifecycle.setLoggedIn(this.guestCreds); - this.guestCreds = null; + if (this.state.guestCreds) { + Lifecycle.setLoggedIn(this.state.guestCreds); + this.setState({guestCreds: null}); } }, @@ -1153,7 +1162,7 @@ module.exports = React.createClass({ onLoggedIn={this.onRegistered} onLoginClick={this.onLoginClick} onRegisterClick={this.onRegisterClick} - onCancelClick={this.guestCreds ? this.onReturnToGuestClick : null} + onCancelClick={this.state.guestCreds ? this.onReturnToGuestClick : null} /> ); } else if (this.state.screen == 'forgot_password') { @@ -1180,7 +1189,7 @@ module.exports = React.createClass({ defaultDeviceDisplayName={this.props.defaultDeviceDisplayName} onForgotPasswordClick={this.onForgotPasswordClick} enableGuest={this.props.enableGuest} - onCancelClick={this.guestCreds ? this.onReturnToGuestClick : null} + onCancelClick={this.state.guestCreds ? this.onReturnToGuestClick : null} initialErrorText={this.sessionLoadError} /> ); diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 92f64eb6ab..57a7d6e19d 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -192,7 +192,6 @@ module.exports = React.createClass({ const teamToken = data.team_token; // Store for use /w welcome pages window.localStorage.setItem('mx_team_token', teamToken); - this.props.onTeamMemberRegistered(teamToken); this._rtsClient.getTeam(teamToken).then((team) => { console.log( From eca82bdb42fd03e889d4618bedf2695212dc8e51 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 8 Mar 2017 10:45:07 +0000 Subject: [PATCH 10/24] Make sure the screen is set, otherwise ignore screenAfterLogin --- 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 7c398b39f9..bae1f0849a 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -724,7 +724,7 @@ module.exports = React.createClass({ // If screenAfterLogin is set, use that, then null it so that a second login will // result in view_home_page, _user_settings or _room_directory - if (this.state.screenAfterLogin) { + if (this.state.screenAfterLogin && this.state.screenAfterLogin.screen) { this.showScreen( this.state.screenAfterLogin.screen, this.state.screenAfterLogin.params From c4001b5c5d43c78101ca51b48f4cc63a2c5667f2 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 8 Mar 2017 15:11:38 +0000 Subject: [PATCH 11/24] Use else instead of two returns --- src/components/structures/MatrixChat.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index bae1f0849a..fbb585924e 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -738,13 +738,11 @@ module.exports = React.createClass({ if (teamToken) { this._teamToken = teamToken; dis.dispatch({action: 'view_home_page'}); - return; } else if (this._is_registered) { dis.dispatch({action: 'view_user_settings'}); - return; + } else { + dis.dispatch({action: 'view_room_directory'}); } - - dis.dispatch({action: 'view_room_directory'}); }, /** From 2513bfa612a79d61b342f43a61975543deca1975 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 8 Mar 2017 16:55:44 +0000 Subject: [PATCH 12/24] Add onClick to permalinks to route within Riot --- src/components/views/rooms/EventTile.js | 32 +++++++++++++++++++------ 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 5fb65096a5..52bc856c31 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -25,7 +25,7 @@ var TextForEvent = require('../../../TextForEvent'); import WithMatrixClient from '../../../wrappers/WithMatrixClient'; var ContextualMenu = require('../../structures/ContextualMenu'); -var dispatcher = require("../../../dispatcher"); +import dis from '../../../dispatcher'; var ObjectUtils = require('../../../ObjectUtils'); @@ -356,7 +356,7 @@ module.exports = WithMatrixClient(React.createClass({ onSenderProfileClick: function(event) { var mxEvent = this.props.mxEvent; - dispatcher.dispatch({ + dis.dispatch({ action: 'insert_displayname', displayname: (mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()).replace(' (IRC)', ''), }); @@ -372,6 +372,17 @@ module.exports = WithMatrixClient(React.createClass({ }); }, + onPermalinkClicked: function(e) { + // This allows the permalink to be open in a new tab/window or copied as + // matrix.to, but also for it to enable routing within Riot when clicked. + e.preventDefault(); + dis.dispatch({ + action: 'view_room', + event_id: this.props.mxEvent.getId(), + room_id: this.props.mxEvent.getRoomId(), + }); + }, + render: function() { var MessageTimestamp = sdk.getComponent('messages.MessageTimestamp'); var SenderProfile = sdk.getComponent('messages.SenderProfile'); @@ -413,7 +424,10 @@ module.exports = WithMatrixClient(React.createClass({ mx_EventTile_unverified: this.state.verified == false, mx_EventTile_bad: this.props.mxEvent.getContent().msgtype === 'm.bad.encrypted', }); - var permalink = "https://matrix.to/#/" + this.props.mxEvent.getRoomId() +"/"+ this.props.mxEvent.getId(); + + const permalink = "https://matrix.to/#/" + + this.props.mxEvent.getRoomId() + "/" + + this.props.mxEvent.getId(); var readAvatars = this.getReadAvatars(); @@ -493,13 +507,13 @@ module.exports = WithMatrixClient(React.createClass({ return (
{ avatar } - + { sender } @@ -527,7 +541,11 @@ module.exports = WithMatrixClient(React.createClass({ tileShape={this.props.tileShape} onWidgetLoad={this.props.onWidgetLoad} />
- +
{ sender } @@ -545,7 +563,7 @@ module.exports = WithMatrixClient(React.createClass({ { avatar } { sender }
- + { e2e } From 173daddb04f7be1466d9ce81a772dd44f7b9b1b6 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 9 Mar 2017 09:56:52 +0000 Subject: [PATCH 13/24] Comment typo --- src/components/views/rooms/EventTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 52bc856c31..74fc4af400 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -373,7 +373,7 @@ module.exports = WithMatrixClient(React.createClass({ }, onPermalinkClicked: function(e) { - // This allows the permalink to be open in a new tab/window or copied as + // This allows the permalink to be opened in a new tab/window or copied as // matrix.to, but also for it to enable routing within Riot when clicked. e.preventDefault(); dis.dispatch({ From 02695623834215634244ce733e079149e98673bb Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 9 Mar 2017 10:59:22 +0000 Subject: [PATCH 14/24] Support registration & login with phone number (#742) * WIP msisdn sign in * A mostly working country picker * Fix bug where you'dbe logged out after registering Stop the guest sync, otherwise it gets 401ed for using a guest access token for a non-guest, causing us to beliebe we've been logged out. * Use InteractiveAuth component for registration * Fix tests * Remove old signup code * Signup -> Login Now that Signup contains no code whatsoever related to signing up, rename it to Login. Get rid of the Signup class. * Stray newline * Fix more merge failing * Get phone country & number to the right place * More-or-less working msisdn auth component * Send the bind_msisdn param on registration * Refinements to country dropdown Rendering the whole lot when the component was rendered just makes the page load really slow, so just show 2 at a time and rely on type-to-search. Make type-to-search always display an exact iso2 match first * Propagate initial inputs to the phone input * Support msisdn login * semicolon * Fix PropTypes * Oops, use the 1qst element of the array Not the array of object keys which has no particular order * Make dropdown/countrydropdown controlled * Unused line * Add note on DOM layout * onOptionChange is required * More docs * Add missing propTypes * Don't resume promise on error * Use React.Children to manipulate children * Make catch less weird * Fix null dereference Assuming [0] of an empty list == undefined doesn't work if you're then taking a property of it. --- src/HtmlUtils.js | 16 + src/Login.js | 41 +- src/component-index.js | 4 + src/components/structures/login/Login.js | 36 +- .../structures/login/Registration.js | 18 +- .../views/elements/AccessibleButton.js | 4 +- src/components/views/elements/Dropdown.js | 324 +++++ src/components/views/login/CountryDropdown.js | 123 ++ .../login/InteractiveAuthEntryComponents.js | 134 ++ src/components/views/login/PasswordLogin.js | 46 +- .../views/login/RegistrationForm.js | 43 + src/phonenumber.js | 1273 +++++++++++++++++ 12 files changed, 2032 insertions(+), 30 deletions(-) create mode 100644 src/components/views/elements/Dropdown.js create mode 100644 src/components/views/login/CountryDropdown.js create mode 100644 src/phonenumber.js diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index c500076783..f1420d0a22 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -58,6 +58,22 @@ export function unicodeToImage(str) { return str; } +/** + * Given one or more unicode characters (represented by unicode + * character number), return an image node with the corresponding + * emoji. + * + * @param alt {string} String to use for the image alt text + * @param unicode {integer} One or more integers representing unicode characters + * @returns A img node with the corresponding emoji + */ +export function charactersToImageNode(alt, ...unicode) { + const fileName = unicode.map((u) => { + return u.toString(16); + }).join('-'); + return {alt}; +} + export function stripParagraphs(html: string): string { const contentDiv = document.createElement('div'); contentDiv.innerHTML = html; diff --git a/src/Login.js b/src/Login.js index 96f953c130..053f88ce93 100644 --- a/src/Login.js +++ b/src/Login.js @@ -105,21 +105,38 @@ export default class Login { }); } - loginViaPassword(username, pass) { - var self = this; - var isEmail = username.indexOf("@") > 0; - var loginParams = { - password: pass, - initial_device_display_name: this._defaultDeviceDisplayName, - }; - if (isEmail) { - loginParams.medium = 'email'; - loginParams.address = username; + loginViaPassword(username, phoneCountry, phoneNumber, pass) { + const self = this; + + const isEmail = username.indexOf("@") > 0; + + let identifier; + if (phoneCountry && phoneNumber) { + identifier = { + type: 'm.id.phone', + country: phoneCountry, + number: phoneNumber, + }; + } else if (isEmail) { + identifier = { + type: 'm.id.thirdparty', + medium: 'email', + address: username, + }; } else { - loginParams.user = username; + identifier = { + type: 'm.id.user', + user: username, + }; } - var client = this._createTemporaryClient(); + const loginParams = { + password: pass, + identifier: identifier, + initial_device_display_name: this._defaultDeviceDisplayName, + }; + + const client = this._createTemporaryClient(); return client.login('m.login.password', loginParams).then(function(data) { return q({ homeserverUrl: self._hsUrl, diff --git a/src/component-index.js b/src/component-index.js index 2644f1a379..59d3ad53e4 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -109,6 +109,8 @@ import views$elements$DeviceVerifyButtons from './components/views/elements/Devi views$elements$DeviceVerifyButtons && (module.exports.components['views.elements.DeviceVerifyButtons'] = views$elements$DeviceVerifyButtons); import views$elements$DirectorySearchBox from './components/views/elements/DirectorySearchBox'; views$elements$DirectorySearchBox && (module.exports.components['views.elements.DirectorySearchBox'] = views$elements$DirectorySearchBox); +import views$elements$Dropdown from './components/views/elements/Dropdown'; +views$elements$Dropdown && (module.exports.components['views.elements.Dropdown'] = views$elements$Dropdown); import views$elements$EditableText from './components/views/elements/EditableText'; views$elements$EditableText && (module.exports.components['views.elements.EditableText'] = views$elements$EditableText); import views$elements$EditableTextContainer from './components/views/elements/EditableTextContainer'; @@ -131,6 +133,8 @@ import views$login$CaptchaForm from './components/views/login/CaptchaForm'; views$login$CaptchaForm && (module.exports.components['views.login.CaptchaForm'] = views$login$CaptchaForm); import views$login$CasLogin from './components/views/login/CasLogin'; views$login$CasLogin && (module.exports.components['views.login.CasLogin'] = views$login$CasLogin); +import views$login$CountryDropdown from './components/views/login/CountryDropdown'; +views$login$CountryDropdown && (module.exports.components['views.login.CountryDropdown'] = views$login$CountryDropdown); import views$login$CustomServerDialog from './components/views/login/CustomServerDialog'; views$login$CustomServerDialog && (module.exports.components['views.login.CustomServerDialog'] = views$login$CustomServerDialog); import views$login$InteractiveAuthEntryComponents from './components/views/login/InteractiveAuthEntryComponents'; diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index 69195fc715..0a1549f75b 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.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. @@ -64,8 +65,10 @@ module.exports = React.createClass({ enteredHomeserverUrl: this.props.customHsUrl || this.props.defaultHsUrl, enteredIdentityServerUrl: this.props.customIsUrl || this.props.defaultIsUrl, - // used for preserving username when changing homeserver + // used for preserving form values when changing homeserver username: "", + phoneCountry: null, + phoneNumber: "", }; }, @@ -73,20 +76,21 @@ module.exports = React.createClass({ this._initLoginLogic(); }, - onPasswordLogin: function(username, password) { - var self = this; - self.setState({ + onPasswordLogin: function(username, phoneCountry, phoneNumber, password) { + this.setState({ busy: true, errorText: null, loginIncorrect: false, }); - this._loginLogic.loginViaPassword(username, password).then(function(data) { - self.props.onLoggedIn(data); - }, function(error) { - self._setStateFromError(error, true); - }).finally(function() { - self.setState({ + this._loginLogic.loginViaPassword( + username, phoneCountry, phoneNumber, password, + ).then((data) => { + this.props.onLoggedIn(data); + }, (error) => { + this._setStateFromError(error, true); + }).finally(() => { + this.setState({ busy: false }); }).done(); @@ -119,6 +123,14 @@ module.exports = React.createClass({ this.setState({ username: username }); }, + onPhoneCountryChanged: function(phoneCountry) { + this.setState({ phoneCountry: phoneCountry }); + }, + + onPhoneNumberChanged: function(phoneNumber) { + this.setState({ phoneNumber: phoneNumber }); + }, + onHsUrlChanged: function(newHsUrl) { var self = this; this.setState({ @@ -225,7 +237,11 @@ module.exports = React.createClass({ diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index cbc8929158..f4805ef044 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -262,6 +262,9 @@ module.exports = React.createClass({ case "RegistrationForm.ERR_EMAIL_INVALID": errMsg = "This doesn't look like a valid email address"; break; + case "RegistrationForm.ERR_PHONE_NUMBER_INVALID": + errMsg = "This doesn't look like a valid phone number"; + break; case "RegistrationForm.ERR_USERNAME_INVALID": errMsg = "User names may only contain letters, numbers, dots, hyphens and underscores."; break; @@ -296,15 +299,20 @@ module.exports = React.createClass({ guestAccessToken = null; } + // Only send the bind params if we're sending username / pw params + // (Since we need to send no params at all to use the ones saved in the + // session). + const bindThreepids = this.state.formVals.password ? { + email: true, + msisdn: true, + } : {}; + return this._matrixClient.register( this.state.formVals.username, this.state.formVals.password, undefined, // session id: included in the auth dict already auth, - // Only send the bind_email param if we're sending username / pw params - // (Since we need to send no params at all to use the ones saved in the - // session). - Boolean(this.state.formVals.username) || undefined, + bindThreepids, guestAccessToken, ); }, @@ -355,6 +363,8 @@ module.exports = React.createClass({ + {this.props.children} +
+ } +}; + +MenuOption.propTypes = { + children: React.PropTypes.oneOfType([ + React.PropTypes.arrayOf(React.PropTypes.node), + React.PropTypes.node + ]), + highlighted: React.PropTypes.bool, + dropdownKey: React.PropTypes.string, + onClick: React.PropTypes.func.isRequired, + onMouseEnter: React.PropTypes.func.isRequired, +}; + +/* + * Reusable dropdown select control, akin to react-select, + * but somewhat simpler as react-select is 79KB of minified + * javascript. + * + * TODO: Port NetworkDropdown to use this. + */ +export default class Dropdown extends React.Component { + constructor(props) { + super(props); + + this.dropdownRootElement = null; + this.ignoreEvent = null; + + this._onInputClick = this._onInputClick.bind(this); + this._onRootClick = this._onRootClick.bind(this); + this._onDocumentClick = this._onDocumentClick.bind(this); + this._onMenuOptionClick = this._onMenuOptionClick.bind(this); + this._onInputKeyPress = this._onInputKeyPress.bind(this); + this._onInputKeyUp = this._onInputKeyUp.bind(this); + this._onInputChange = this._onInputChange.bind(this); + this._collectRoot = this._collectRoot.bind(this); + this._collectInputTextBox = this._collectInputTextBox.bind(this); + this._setHighlightedOption = this._setHighlightedOption.bind(this); + + this.inputTextBox = null; + + this._reindexChildren(this.props.children); + + const firstChild = React.Children.toArray(props.children)[0]; + + this.state = { + // True if the menu is dropped-down + expanded: false, + // The key of the highlighted option + // (the option that would become selected if you pressed enter) + highlightedOption: firstChild ? firstChild.key : null, + // the current search query + searchQuery: '', + }; + } + + componentWillMount() { + // Listen for all clicks on the document so we can close the + // menu when the user clicks somewhere else + document.addEventListener('click', this._onDocumentClick, false); + } + + componentWillUnmount() { + document.removeEventListener('click', this._onDocumentClick, false); + } + + componentWillReceiveProps(nextProps) { + this._reindexChildren(nextProps.children); + const firstChild = React.Children.toArray(nextProps.children)[0]; + this.setState({ + highlightedOption: firstChild ? firstChild.key : null, + }); + } + + _reindexChildren(children) { + this.childrenByKey = {}; + React.Children.forEach(children, (child) => { + this.childrenByKey[child.key] = child; + }); + } + + _onDocumentClick(ev) { + // Close the dropdown if the user clicks anywhere that isn't + // within our root element + if (ev !== this.ignoreEvent) { + this.setState({ + expanded: false, + }); + } + } + + _onRootClick(ev) { + // This captures any clicks that happen within our elements, + // such that we can then ignore them when they're seen by the + // click listener on the document handler, ie. not close the + // dropdown immediately after opening it. + // NB. We can't just stopPropagation() because then the event + // doesn't reach the React onClick(). + this.ignoreEvent = ev; + } + + _onInputClick(ev) { + this.setState({ + expanded: !this.state.expanded, + }); + ev.preventDefault(); + } + + _onMenuOptionClick(dropdownKey) { + this.setState({ + expanded: false, + }); + this.props.onOptionChange(dropdownKey); + } + + _onInputKeyPress(e) { + // This needs to be on the keypress event because otherwise + // it can't cancel the form submission + if (e.key == 'Enter') { + this.setState({ + expanded: false, + }); + this.props.onOptionChange(this.state.highlightedOption); + e.preventDefault(); + } + } + + _onInputKeyUp(e) { + // These keys don't generate keypress events and so needs to + // be on keyup + if (e.key == 'Escape') { + this.setState({ + expanded: false, + }); + } else if (e.key == 'ArrowDown') { + this.setState({ + highlightedOption: this._nextOption(this.state.highlightedOption), + }); + } else if (e.key == 'ArrowUp') { + this.setState({ + highlightedOption: this._prevOption(this.state.highlightedOption), + }); + } + } + + _onInputChange(e) { + this.setState({ + searchQuery: e.target.value, + }); + if (this.props.onSearchChange) { + this.props.onSearchChange(e.target.value); + } + } + + _collectRoot(e) { + if (this.dropdownRootElement) { + this.dropdownRootElement.removeEventListener( + 'click', this._onRootClick, false, + ); + } + if (e) { + e.addEventListener('click', this._onRootClick, false); + } + this.dropdownRootElement = e; + } + + _collectInputTextBox(e) { + this.inputTextBox = e; + if (e) e.focus(); + } + + _setHighlightedOption(optionKey) { + this.setState({ + highlightedOption: optionKey, + }); + } + + _nextOption(optionKey) { + const keys = Object.keys(this.childrenByKey); + const index = keys.indexOf(optionKey); + return keys[(index + 1) % keys.length]; + } + + _prevOption(optionKey) { + const keys = Object.keys(this.childrenByKey); + const index = keys.indexOf(optionKey); + return keys[(index - 1) % keys.length]; + } + + _getMenuOptions() { + const options = React.Children.map(this.props.children, (child) => { + return ( + + {child} + + ); + }); + + if (!this.state.searchQuery) { + options.push( +
+ Type to search... +
+ ); + } + return options; + } + + render() { + let currentValue; + + const menuStyle = {}; + if (this.props.menuWidth) menuStyle.width = this.props.menuWidth; + + let menu; + if (this.state.expanded) { + currentValue = ; + menu =
+ {this._getMenuOptions()} +
; + } else { + const selectedChild = this.props.getShortOption ? + this.props.getShortOption(this.props.value) : + this.childrenByKey[this.props.value]; + currentValue =
+ {selectedChild} +
+ } + + const dropdownClasses = { + mx_Dropdown: true, + }; + if (this.props.className) { + dropdownClasses[this.props.className] = true; + } + + // Note the menu sits inside the AccessibleButton div so it's anchored + // to the input, but overflows below it. The root contains both. + return
+ + {currentValue} + + {menu} + +
; + } +} + +Dropdown.propTypes = { + // The width that the dropdown should be. If specified, + // the dropped-down part of the menu will be set to this + // width. + menuWidth: React.PropTypes.number, + // Called when the selected option changes + onOptionChange: React.PropTypes.func.isRequired, + // Called when the value of the search field changes + onSearchChange: React.PropTypes.func, + // Function that, given the key of an option, returns + // a node representing that option to be displayed in the + // box itself as the currently-selected option (ie. as + // opposed to in the actual dropped-down part). If + // unspecified, the appropriate child element is used as + // in the dropped-down menu. + getShortOption: React.PropTypes.func, + value: React.PropTypes.string, +} diff --git a/src/components/views/login/CountryDropdown.js b/src/components/views/login/CountryDropdown.js new file mode 100644 index 0000000000..fc1e89661b --- /dev/null +++ b/src/components/views/login/CountryDropdown.js @@ -0,0 +1,123 @@ +/* +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 { COUNTRIES } from '../../../phonenumber'; +import { charactersToImageNode } from '../../../HtmlUtils'; + +const COUNTRIES_BY_ISO2 = new Object(null); +for (const c of COUNTRIES) { + COUNTRIES_BY_ISO2[c.iso2] = c; +} + +function countryMatchesSearchQuery(query, country) { + if (country.name.toUpperCase().indexOf(query.toUpperCase()) == 0) return true; + if (country.iso2 == query.toUpperCase()) return true; + if (country.prefix == query) return true; + return false; +} + +const MAX_DISPLAYED_ROWS = 2; + +export default class CountryDropdown extends React.Component { + constructor(props) { + super(props); + this._onSearchChange = this._onSearchChange.bind(this); + + this.state = { + searchQuery: '', + } + + if (!props.value) { + // If no value is given, we start with the first + // country selected, but our parent component + // doesn't know this, therefore we do this. + this.props.onOptionChange(COUNTRIES[0].iso2); + } + } + + _onSearchChange(search) { + this.setState({ + searchQuery: search, + }); + } + + _flagImgForIso2(iso2) { + // Unicode Regional Indicator Symbol letter 'A' + const RIS_A = 0x1F1E6; + const ASCII_A = 65; + return charactersToImageNode(iso2, + RIS_A + (iso2.charCodeAt(0) - ASCII_A), + RIS_A + (iso2.charCodeAt(1) - ASCII_A), + ); + } + + render() { + const Dropdown = sdk.getComponent('elements.Dropdown'); + + let displayedCountries; + if (this.state.searchQuery) { + displayedCountries = COUNTRIES.filter( + countryMatchesSearchQuery.bind(this, this.state.searchQuery), + ); + if ( + this.state.searchQuery.length == 2 && + COUNTRIES_BY_ISO2[this.state.searchQuery.toUpperCase()] + ) { + // exact ISO2 country name match: make the first result the matches ISO2 + const matched = COUNTRIES_BY_ISO2[this.state.searchQuery.toUpperCase()]; + displayedCountries = displayedCountries.filter((c) => { + return c.iso2 != matched.iso2; + }); + displayedCountries.unshift(matched); + } + } else { + displayedCountries = COUNTRIES; + } + + if (displayedCountries.length > MAX_DISPLAYED_ROWS) { + displayedCountries = displayedCountries.slice(0, MAX_DISPLAYED_ROWS); + } + + const options = displayedCountries.map((country) => { + return
+ {this._flagImgForIso2(country.iso2)} + {country.name} +
; + }); + + // default value here too, otherwise we need to handle null / undefined + // values between mounting and the initial value propgating + const value = this.props.value || COUNTRIES[0].iso2; + + return + {options} + + } +} + +CountryDropdown.propTypes = { + className: React.PropTypes.string, + onOptionChange: React.PropTypes.func.isRequired, + value: React.PropTypes.string, +}; diff --git a/src/components/views/login/InteractiveAuthEntryComponents.js b/src/components/views/login/InteractiveAuthEntryComponents.js index e75cb082d4..2d8abf9216 100644 --- a/src/components/views/login/InteractiveAuthEntryComponents.js +++ b/src/components/views/login/InteractiveAuthEntryComponents.js @@ -16,6 +16,8 @@ limitations under the License. */ import React from 'react'; +import url from 'url'; +import classnames from 'classnames'; import sdk from '../../../index'; @@ -255,6 +257,137 @@ export const EmailIdentityAuthEntry = React.createClass({ }, }); +export const MsisdnAuthEntry = React.createClass({ + displayName: 'MsisdnAuthEntry', + + statics: { + LOGIN_TYPE: "m.login.msisdn", + }, + + propTypes: { + inputs: React.PropTypes.shape({ + phoneCountry: React.PropTypes.string, + phoneNumber: React.PropTypes.string, + }), + fail: React.PropTypes.func, + clientSecret: React.PropTypes.func, + submitAuthDict: React.PropTypes.func.isRequired, + matrixClient: React.PropTypes.object, + submitAuthDict: React.PropTypes.func, + }, + + getInitialState: function() { + return { + token: '', + requestingToken: false, + }; + }, + + componentWillMount: function() { + this._sid = null; + this._msisdn = null; + this._tokenBox = null; + + this.setState({requestingToken: true}); + this._requestMsisdnToken().catch((e) => { + this.props.fail(e); + }).finally(() => { + this.setState({requestingToken: false}); + }).done(); + }, + + /* + * Requests a verification token by SMS. + */ + _requestMsisdnToken: function() { + return this.props.matrixClient.requestRegisterMsisdnToken( + this.props.inputs.phoneCountry, + this.props.inputs.phoneNumber, + this.props.clientSecret, + 1, // TODO: Multiple send attempts? + ).then((result) => { + this._sid = result.sid; + this._msisdn = result.msisdn; + }); + }, + + _onTokenChange: function(e) { + this.setState({ + token: e.target.value, + }); + }, + + _onFormSubmit: function(e) { + e.preventDefault(); + if (this.state.token == '') return; + + this.setState({ + errorText: null, + }); + + this.props.matrixClient.submitMsisdnToken( + this._sid, this.props.clientSecret, this.state.token + ).then((result) => { + if (result.success) { + const idServerParsedUrl = url.parse( + this.props.matrixClient.getIdentityServerUrl(), + ) + this.props.submitAuthDict({ + type: MsisdnAuthEntry.LOGIN_TYPE, + threepid_creds: { + sid: this._sid, + client_secret: this.props.clientSecret, + id_server: idServerParsedUrl.host, + }, + }); + } else { + this.setState({ + errorText: "Token incorrect", + }); + } + }).catch((e) => { + this.props.fail(e); + console.log("Failed to submit msisdn token"); + }).done(); + }, + + render: function() { + if (this.state.requestingToken) { + const Loader = sdk.getComponent("elements.Spinner"); + return ; + } else { + const enableSubmit = Boolean(this.state.token); + const submitClasses = classnames({ + mx_InteractiveAuthEntryComponents_msisdnSubmit: true, + mx_UserSettings_button: true, // XXX button classes + }); + return ( +
+

A text message has been sent to +{this._msisdn}

+

Please enter the code it contains:

+
+
+ +
+ +
+
+ {this.state.errorText} +
+
+
+ ); + } + }, +}); + export const FallbackAuthEntry = React.createClass({ displayName: 'FallbackAuthEntry', @@ -313,6 +446,7 @@ const AuthEntryComponents = [ PasswordAuthEntry, RecaptchaAuthEntry, EmailIdentityAuthEntry, + MsisdnAuthEntry, ]; export function getEntryComponentForLoginType(loginType) { diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index 6f6081858b..61cb3da652 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.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. @@ -17,6 +18,7 @@ limitations under the License. import React from 'react'; import ReactDOM from 'react-dom'; import classNames from 'classnames'; +import sdk from '../../../index'; import {field_input_incorrect} from '../../../UiEffects'; @@ -28,8 +30,12 @@ module.exports = React.createClass({displayName: 'PasswordLogin', onSubmit: React.PropTypes.func.isRequired, // fn(username, password) onForgotPasswordClick: React.PropTypes.func, // fn() initialUsername: React.PropTypes.string, + initialPhoneCountry: React.PropTypes.string, + initialPhoneNumber: React.PropTypes.string, initialPassword: React.PropTypes.string, onUsernameChanged: React.PropTypes.func, + onPhoneCountryChanged: React.PropTypes.func, + onPhoneNumberChanged: React.PropTypes.func, onPasswordChanged: React.PropTypes.func, loginIncorrect: React.PropTypes.bool, }, @@ -38,7 +44,11 @@ module.exports = React.createClass({displayName: 'PasswordLogin', return { onUsernameChanged: function() {}, onPasswordChanged: function() {}, + onPhoneCountryChanged: function() {}, + onPhoneNumberChanged: function() {}, initialUsername: "", + initialPhoneCountry: "", + initialPhoneNumber: "", initialPassword: "", loginIncorrect: false, }; @@ -48,6 +58,8 @@ module.exports = React.createClass({displayName: 'PasswordLogin', return { username: this.props.initialUsername, password: this.props.initialPassword, + phoneCountry: this.props.initialPhoneCountry, + phoneNumber: this.props.initialPhoneNumber, }; }, @@ -63,7 +75,12 @@ module.exports = React.createClass({displayName: 'PasswordLogin', onSubmitForm: function(ev) { ev.preventDefault(); - this.props.onSubmit(this.state.username, this.state.password); + this.props.onSubmit( + this.state.username, + this.state.phoneCountry, + this.state.phoneNumber, + this.state.password, + ); }, onUsernameChanged: function(ev) { @@ -71,6 +88,16 @@ module.exports = React.createClass({displayName: 'PasswordLogin', this.props.onUsernameChanged(ev.target.value); }, + onPhoneCountryChanged: function(country) { + this.setState({phoneCountry: country}); + this.props.onPhoneCountryChanged(country); + }, + + onPhoneNumberChanged: function(ev) { + this.setState({phoneNumber: ev.target.value}); + this.props.onPhoneNumberChanged(ev.target.value); + }, + onPasswordChanged: function(ev) { this.setState({password: ev.target.value}); this.props.onPasswordChanged(ev.target.value); @@ -92,13 +119,28 @@ module.exports = React.createClass({displayName: 'PasswordLogin', error: this.props.loginIncorrect, }); + const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); return (
- + or +
+ + +

{this._passwordField = e;}} type="password" name="password" diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index 93e3976834..4868c9de63 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -19,9 +19,12 @@ import React from 'react'; import { field_input_incorrect } from '../../../UiEffects'; import sdk from '../../../index'; import Email from '../../../email'; +import { looksValid as phoneNumberLooksValid } from '../../../phonenumber'; import Modal from '../../../Modal'; const FIELD_EMAIL = 'field_email'; +const FIELD_PHONE_COUNTRY = 'field_phone_country'; +const FIELD_PHONE_NUMBER = 'field_phone_number'; const FIELD_USERNAME = 'field_username'; const FIELD_PASSWORD = 'field_password'; const FIELD_PASSWORD_CONFIRM = 'field_password_confirm'; @@ -35,6 +38,8 @@ module.exports = React.createClass({ propTypes: { // Values pre-filled in the input boxes when the component loads defaultEmail: React.PropTypes.string, + defaultPhoneCountry: React.PropTypes.string, + defaultPhoneNumber: React.PropTypes.string, defaultUsername: React.PropTypes.string, defaultPassword: React.PropTypes.string, teamsConfig: React.PropTypes.shape({ @@ -71,6 +76,8 @@ module.exports = React.createClass({ return { fieldValid: {}, selectedTeam: null, + // The ISO2 country code selected in the phone number entry + phoneCountry: this.props.defaultPhoneCountry, }; }, @@ -85,6 +92,7 @@ module.exports = React.createClass({ this.validateField(FIELD_PASSWORD_CONFIRM); this.validateField(FIELD_PASSWORD); this.validateField(FIELD_USERNAME); + this.validateField(FIELD_PHONE_NUMBER); this.validateField(FIELD_EMAIL); var self = this; @@ -118,6 +126,8 @@ module.exports = React.createClass({ username: this.refs.username.value.trim() || this.props.guestUsername, password: this.refs.password.value.trim(), email: email, + phoneCountry: this.state.phoneCountry, + phoneNumber: this.refs.phoneNumber.value.trim(), }); if (promise) { @@ -174,6 +184,11 @@ module.exports = React.createClass({ const emailValid = email === '' || Email.looksValid(email); this.markFieldValid(field_id, emailValid, "RegistrationForm.ERR_EMAIL_INVALID"); break; + case FIELD_PHONE_NUMBER: + const phoneNumber = this.refs.phoneNumber.value; + const phoneNumberValid = phoneNumber === '' || phoneNumberLooksValid(phoneNumber); + this.markFieldValid(field_id, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID"); + break; case FIELD_USERNAME: // XXX: SPEC-1 var username = this.refs.username.value.trim() || this.props.guestUsername; @@ -233,6 +248,8 @@ module.exports = React.createClass({ switch (field_id) { case FIELD_EMAIL: return this.refs.email; + case FIELD_PHONE_NUMBER: + return this.refs.phoneNumber; case FIELD_USERNAME: return this.refs.username; case FIELD_PASSWORD: @@ -251,6 +268,12 @@ module.exports = React.createClass({ return cls; }, + _onPhoneCountryChange(newVal) { + this.setState({ + phoneCountry: newVal, + }); + }, + render: function() { var self = this; @@ -286,6 +309,25 @@ module.exports = React.createClass({ } } + const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); + const phoneSection = ( +
+ + +
+ ); + const registerButton = ( ); @@ -300,6 +342,7 @@ module.exports = React.createClass({ {emailSection} {belowEmailSection} + {phoneSection} Date: Thu, 9 Mar 2017 18:32:44 +0000 Subject: [PATCH 15/24] Revert "Support registration & login with phone number (#742)" This reverts commit 02695623834215634244ce733e079149e98673bb. This breaks against the current synapse release. We need to think more carefully about backwards compatibility. --- src/HtmlUtils.js | 16 - src/Login.js | 39 +- src/component-index.js | 4 - src/components/structures/login/Login.js | 36 +- .../structures/login/Registration.js | 18 +- .../views/elements/AccessibleButton.js | 4 +- src/components/views/elements/Dropdown.js | 324 ----- src/components/views/login/CountryDropdown.js | 123 -- .../login/InteractiveAuthEntryComponents.js | 134 -- src/components/views/login/PasswordLogin.js | 46 +- .../views/login/RegistrationForm.js | 43 - src/phonenumber.js | 1273 ----------------- 12 files changed, 29 insertions(+), 2031 deletions(-) delete mode 100644 src/components/views/elements/Dropdown.js delete mode 100644 src/components/views/login/CountryDropdown.js delete mode 100644 src/phonenumber.js diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index f1420d0a22..c500076783 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -58,22 +58,6 @@ export function unicodeToImage(str) { return str; } -/** - * Given one or more unicode characters (represented by unicode - * character number), return an image node with the corresponding - * emoji. - * - * @param alt {string} String to use for the image alt text - * @param unicode {integer} One or more integers representing unicode characters - * @returns A img node with the corresponding emoji - */ -export function charactersToImageNode(alt, ...unicode) { - const fileName = unicode.map((u) => { - return u.toString(16); - }).join('-'); - return {alt}; -} - export function stripParagraphs(html: string): string { const contentDiv = document.createElement('div'); contentDiv.innerHTML = html; diff --git a/src/Login.js b/src/Login.js index 053f88ce93..96f953c130 100644 --- a/src/Login.js +++ b/src/Login.js @@ -105,38 +105,21 @@ export default class Login { }); } - loginViaPassword(username, phoneCountry, phoneNumber, pass) { - const self = this; - - const isEmail = username.indexOf("@") > 0; - - let identifier; - if (phoneCountry && phoneNumber) { - identifier = { - type: 'm.id.phone', - country: phoneCountry, - number: phoneNumber, - }; - } else if (isEmail) { - identifier = { - type: 'm.id.thirdparty', - medium: 'email', - address: username, - }; - } else { - identifier = { - type: 'm.id.user', - user: username, - }; - } - - const loginParams = { + loginViaPassword(username, pass) { + var self = this; + var isEmail = username.indexOf("@") > 0; + var loginParams = { password: pass, - identifier: identifier, initial_device_display_name: this._defaultDeviceDisplayName, }; + if (isEmail) { + loginParams.medium = 'email'; + loginParams.address = username; + } else { + loginParams.user = username; + } - const client = this._createTemporaryClient(); + var client = this._createTemporaryClient(); return client.login('m.login.password', loginParams).then(function(data) { return q({ homeserverUrl: self._hsUrl, diff --git a/src/component-index.js b/src/component-index.js index 59d3ad53e4..2644f1a379 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -109,8 +109,6 @@ import views$elements$DeviceVerifyButtons from './components/views/elements/Devi views$elements$DeviceVerifyButtons && (module.exports.components['views.elements.DeviceVerifyButtons'] = views$elements$DeviceVerifyButtons); import views$elements$DirectorySearchBox from './components/views/elements/DirectorySearchBox'; views$elements$DirectorySearchBox && (module.exports.components['views.elements.DirectorySearchBox'] = views$elements$DirectorySearchBox); -import views$elements$Dropdown from './components/views/elements/Dropdown'; -views$elements$Dropdown && (module.exports.components['views.elements.Dropdown'] = views$elements$Dropdown); import views$elements$EditableText from './components/views/elements/EditableText'; views$elements$EditableText && (module.exports.components['views.elements.EditableText'] = views$elements$EditableText); import views$elements$EditableTextContainer from './components/views/elements/EditableTextContainer'; @@ -133,8 +131,6 @@ import views$login$CaptchaForm from './components/views/login/CaptchaForm'; views$login$CaptchaForm && (module.exports.components['views.login.CaptchaForm'] = views$login$CaptchaForm); import views$login$CasLogin from './components/views/login/CasLogin'; views$login$CasLogin && (module.exports.components['views.login.CasLogin'] = views$login$CasLogin); -import views$login$CountryDropdown from './components/views/login/CountryDropdown'; -views$login$CountryDropdown && (module.exports.components['views.login.CountryDropdown'] = views$login$CountryDropdown); import views$login$CustomServerDialog from './components/views/login/CustomServerDialog'; views$login$CustomServerDialog && (module.exports.components['views.login.CustomServerDialog'] = views$login$CustomServerDialog); import views$login$InteractiveAuthEntryComponents from './components/views/login/InteractiveAuthEntryComponents'; diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index 0a1549f75b..69195fc715 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -1,6 +1,5 @@ /* 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. @@ -65,10 +64,8 @@ module.exports = React.createClass({ enteredHomeserverUrl: this.props.customHsUrl || this.props.defaultHsUrl, enteredIdentityServerUrl: this.props.customIsUrl || this.props.defaultIsUrl, - // used for preserving form values when changing homeserver + // used for preserving username when changing homeserver username: "", - phoneCountry: null, - phoneNumber: "", }; }, @@ -76,21 +73,20 @@ module.exports = React.createClass({ this._initLoginLogic(); }, - onPasswordLogin: function(username, phoneCountry, phoneNumber, password) { - this.setState({ + onPasswordLogin: function(username, password) { + var self = this; + self.setState({ busy: true, errorText: null, loginIncorrect: false, }); - this._loginLogic.loginViaPassword( - username, phoneCountry, phoneNumber, password, - ).then((data) => { - this.props.onLoggedIn(data); - }, (error) => { - this._setStateFromError(error, true); - }).finally(() => { - this.setState({ + this._loginLogic.loginViaPassword(username, password).then(function(data) { + self.props.onLoggedIn(data); + }, function(error) { + self._setStateFromError(error, true); + }).finally(function() { + self.setState({ busy: false }); }).done(); @@ -123,14 +119,6 @@ module.exports = React.createClass({ this.setState({ username: username }); }, - onPhoneCountryChanged: function(phoneCountry) { - this.setState({ phoneCountry: phoneCountry }); - }, - - onPhoneNumberChanged: function(phoneNumber) { - this.setState({ phoneNumber: phoneNumber }); - }, - onHsUrlChanged: function(newHsUrl) { var self = this; this.setState({ @@ -237,11 +225,7 @@ module.exports = React.createClass({ diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index f4805ef044..cbc8929158 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -262,9 +262,6 @@ module.exports = React.createClass({ case "RegistrationForm.ERR_EMAIL_INVALID": errMsg = "This doesn't look like a valid email address"; break; - case "RegistrationForm.ERR_PHONE_NUMBER_INVALID": - errMsg = "This doesn't look like a valid phone number"; - break; case "RegistrationForm.ERR_USERNAME_INVALID": errMsg = "User names may only contain letters, numbers, dots, hyphens and underscores."; break; @@ -299,20 +296,15 @@ module.exports = React.createClass({ guestAccessToken = null; } - // Only send the bind params if we're sending username / pw params - // (Since we need to send no params at all to use the ones saved in the - // session). - const bindThreepids = this.state.formVals.password ? { - email: true, - msisdn: true, - } : {}; - return this._matrixClient.register( this.state.formVals.username, this.state.formVals.password, undefined, // session id: included in the auth dict already auth, - bindThreepids, + // Only send the bind_email param if we're sending username / pw params + // (Since we need to send no params at all to use the ones saved in the + // session). + Boolean(this.state.formVals.username) || undefined, guestAccessToken, ); }, @@ -363,8 +355,6 @@ module.exports = React.createClass({ - {this.props.children} -
- } -}; - -MenuOption.propTypes = { - children: React.PropTypes.oneOfType([ - React.PropTypes.arrayOf(React.PropTypes.node), - React.PropTypes.node - ]), - highlighted: React.PropTypes.bool, - dropdownKey: React.PropTypes.string, - onClick: React.PropTypes.func.isRequired, - onMouseEnter: React.PropTypes.func.isRequired, -}; - -/* - * Reusable dropdown select control, akin to react-select, - * but somewhat simpler as react-select is 79KB of minified - * javascript. - * - * TODO: Port NetworkDropdown to use this. - */ -export default class Dropdown extends React.Component { - constructor(props) { - super(props); - - this.dropdownRootElement = null; - this.ignoreEvent = null; - - this._onInputClick = this._onInputClick.bind(this); - this._onRootClick = this._onRootClick.bind(this); - this._onDocumentClick = this._onDocumentClick.bind(this); - this._onMenuOptionClick = this._onMenuOptionClick.bind(this); - this._onInputKeyPress = this._onInputKeyPress.bind(this); - this._onInputKeyUp = this._onInputKeyUp.bind(this); - this._onInputChange = this._onInputChange.bind(this); - this._collectRoot = this._collectRoot.bind(this); - this._collectInputTextBox = this._collectInputTextBox.bind(this); - this._setHighlightedOption = this._setHighlightedOption.bind(this); - - this.inputTextBox = null; - - this._reindexChildren(this.props.children); - - const firstChild = React.Children.toArray(props.children)[0]; - - this.state = { - // True if the menu is dropped-down - expanded: false, - // The key of the highlighted option - // (the option that would become selected if you pressed enter) - highlightedOption: firstChild ? firstChild.key : null, - // the current search query - searchQuery: '', - }; - } - - componentWillMount() { - // Listen for all clicks on the document so we can close the - // menu when the user clicks somewhere else - document.addEventListener('click', this._onDocumentClick, false); - } - - componentWillUnmount() { - document.removeEventListener('click', this._onDocumentClick, false); - } - - componentWillReceiveProps(nextProps) { - this._reindexChildren(nextProps.children); - const firstChild = React.Children.toArray(nextProps.children)[0]; - this.setState({ - highlightedOption: firstChild ? firstChild.key : null, - }); - } - - _reindexChildren(children) { - this.childrenByKey = {}; - React.Children.forEach(children, (child) => { - this.childrenByKey[child.key] = child; - }); - } - - _onDocumentClick(ev) { - // Close the dropdown if the user clicks anywhere that isn't - // within our root element - if (ev !== this.ignoreEvent) { - this.setState({ - expanded: false, - }); - } - } - - _onRootClick(ev) { - // This captures any clicks that happen within our elements, - // such that we can then ignore them when they're seen by the - // click listener on the document handler, ie. not close the - // dropdown immediately after opening it. - // NB. We can't just stopPropagation() because then the event - // doesn't reach the React onClick(). - this.ignoreEvent = ev; - } - - _onInputClick(ev) { - this.setState({ - expanded: !this.state.expanded, - }); - ev.preventDefault(); - } - - _onMenuOptionClick(dropdownKey) { - this.setState({ - expanded: false, - }); - this.props.onOptionChange(dropdownKey); - } - - _onInputKeyPress(e) { - // This needs to be on the keypress event because otherwise - // it can't cancel the form submission - if (e.key == 'Enter') { - this.setState({ - expanded: false, - }); - this.props.onOptionChange(this.state.highlightedOption); - e.preventDefault(); - } - } - - _onInputKeyUp(e) { - // These keys don't generate keypress events and so needs to - // be on keyup - if (e.key == 'Escape') { - this.setState({ - expanded: false, - }); - } else if (e.key == 'ArrowDown') { - this.setState({ - highlightedOption: this._nextOption(this.state.highlightedOption), - }); - } else if (e.key == 'ArrowUp') { - this.setState({ - highlightedOption: this._prevOption(this.state.highlightedOption), - }); - } - } - - _onInputChange(e) { - this.setState({ - searchQuery: e.target.value, - }); - if (this.props.onSearchChange) { - this.props.onSearchChange(e.target.value); - } - } - - _collectRoot(e) { - if (this.dropdownRootElement) { - this.dropdownRootElement.removeEventListener( - 'click', this._onRootClick, false, - ); - } - if (e) { - e.addEventListener('click', this._onRootClick, false); - } - this.dropdownRootElement = e; - } - - _collectInputTextBox(e) { - this.inputTextBox = e; - if (e) e.focus(); - } - - _setHighlightedOption(optionKey) { - this.setState({ - highlightedOption: optionKey, - }); - } - - _nextOption(optionKey) { - const keys = Object.keys(this.childrenByKey); - const index = keys.indexOf(optionKey); - return keys[(index + 1) % keys.length]; - } - - _prevOption(optionKey) { - const keys = Object.keys(this.childrenByKey); - const index = keys.indexOf(optionKey); - return keys[(index - 1) % keys.length]; - } - - _getMenuOptions() { - const options = React.Children.map(this.props.children, (child) => { - return ( - - {child} - - ); - }); - - if (!this.state.searchQuery) { - options.push( -
- Type to search... -
- ); - } - return options; - } - - render() { - let currentValue; - - const menuStyle = {}; - if (this.props.menuWidth) menuStyle.width = this.props.menuWidth; - - let menu; - if (this.state.expanded) { - currentValue = ; - menu =
- {this._getMenuOptions()} -
; - } else { - const selectedChild = this.props.getShortOption ? - this.props.getShortOption(this.props.value) : - this.childrenByKey[this.props.value]; - currentValue =
- {selectedChild} -
- } - - const dropdownClasses = { - mx_Dropdown: true, - }; - if (this.props.className) { - dropdownClasses[this.props.className] = true; - } - - // Note the menu sits inside the AccessibleButton div so it's anchored - // to the input, but overflows below it. The root contains both. - return
- - {currentValue} - - {menu} - -
; - } -} - -Dropdown.propTypes = { - // The width that the dropdown should be. If specified, - // the dropped-down part of the menu will be set to this - // width. - menuWidth: React.PropTypes.number, - // Called when the selected option changes - onOptionChange: React.PropTypes.func.isRequired, - // Called when the value of the search field changes - onSearchChange: React.PropTypes.func, - // Function that, given the key of an option, returns - // a node representing that option to be displayed in the - // box itself as the currently-selected option (ie. as - // opposed to in the actual dropped-down part). If - // unspecified, the appropriate child element is used as - // in the dropped-down menu. - getShortOption: React.PropTypes.func, - value: React.PropTypes.string, -} diff --git a/src/components/views/login/CountryDropdown.js b/src/components/views/login/CountryDropdown.js deleted file mode 100644 index fc1e89661b..0000000000 --- a/src/components/views/login/CountryDropdown.js +++ /dev/null @@ -1,123 +0,0 @@ -/* -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 { COUNTRIES } from '../../../phonenumber'; -import { charactersToImageNode } from '../../../HtmlUtils'; - -const COUNTRIES_BY_ISO2 = new Object(null); -for (const c of COUNTRIES) { - COUNTRIES_BY_ISO2[c.iso2] = c; -} - -function countryMatchesSearchQuery(query, country) { - if (country.name.toUpperCase().indexOf(query.toUpperCase()) == 0) return true; - if (country.iso2 == query.toUpperCase()) return true; - if (country.prefix == query) return true; - return false; -} - -const MAX_DISPLAYED_ROWS = 2; - -export default class CountryDropdown extends React.Component { - constructor(props) { - super(props); - this._onSearchChange = this._onSearchChange.bind(this); - - this.state = { - searchQuery: '', - } - - if (!props.value) { - // If no value is given, we start with the first - // country selected, but our parent component - // doesn't know this, therefore we do this. - this.props.onOptionChange(COUNTRIES[0].iso2); - } - } - - _onSearchChange(search) { - this.setState({ - searchQuery: search, - }); - } - - _flagImgForIso2(iso2) { - // Unicode Regional Indicator Symbol letter 'A' - const RIS_A = 0x1F1E6; - const ASCII_A = 65; - return charactersToImageNode(iso2, - RIS_A + (iso2.charCodeAt(0) - ASCII_A), - RIS_A + (iso2.charCodeAt(1) - ASCII_A), - ); - } - - render() { - const Dropdown = sdk.getComponent('elements.Dropdown'); - - let displayedCountries; - if (this.state.searchQuery) { - displayedCountries = COUNTRIES.filter( - countryMatchesSearchQuery.bind(this, this.state.searchQuery), - ); - if ( - this.state.searchQuery.length == 2 && - COUNTRIES_BY_ISO2[this.state.searchQuery.toUpperCase()] - ) { - // exact ISO2 country name match: make the first result the matches ISO2 - const matched = COUNTRIES_BY_ISO2[this.state.searchQuery.toUpperCase()]; - displayedCountries = displayedCountries.filter((c) => { - return c.iso2 != matched.iso2; - }); - displayedCountries.unshift(matched); - } - } else { - displayedCountries = COUNTRIES; - } - - if (displayedCountries.length > MAX_DISPLAYED_ROWS) { - displayedCountries = displayedCountries.slice(0, MAX_DISPLAYED_ROWS); - } - - const options = displayedCountries.map((country) => { - return
- {this._flagImgForIso2(country.iso2)} - {country.name} -
; - }); - - // default value here too, otherwise we need to handle null / undefined - // values between mounting and the initial value propgating - const value = this.props.value || COUNTRIES[0].iso2; - - return - {options} - - } -} - -CountryDropdown.propTypes = { - className: React.PropTypes.string, - onOptionChange: React.PropTypes.func.isRequired, - value: React.PropTypes.string, -}; diff --git a/src/components/views/login/InteractiveAuthEntryComponents.js b/src/components/views/login/InteractiveAuthEntryComponents.js index 2d8abf9216..e75cb082d4 100644 --- a/src/components/views/login/InteractiveAuthEntryComponents.js +++ b/src/components/views/login/InteractiveAuthEntryComponents.js @@ -16,8 +16,6 @@ limitations under the License. */ import React from 'react'; -import url from 'url'; -import classnames from 'classnames'; import sdk from '../../../index'; @@ -257,137 +255,6 @@ export const EmailIdentityAuthEntry = React.createClass({ }, }); -export const MsisdnAuthEntry = React.createClass({ - displayName: 'MsisdnAuthEntry', - - statics: { - LOGIN_TYPE: "m.login.msisdn", - }, - - propTypes: { - inputs: React.PropTypes.shape({ - phoneCountry: React.PropTypes.string, - phoneNumber: React.PropTypes.string, - }), - fail: React.PropTypes.func, - clientSecret: React.PropTypes.func, - submitAuthDict: React.PropTypes.func.isRequired, - matrixClient: React.PropTypes.object, - submitAuthDict: React.PropTypes.func, - }, - - getInitialState: function() { - return { - token: '', - requestingToken: false, - }; - }, - - componentWillMount: function() { - this._sid = null; - this._msisdn = null; - this._tokenBox = null; - - this.setState({requestingToken: true}); - this._requestMsisdnToken().catch((e) => { - this.props.fail(e); - }).finally(() => { - this.setState({requestingToken: false}); - }).done(); - }, - - /* - * Requests a verification token by SMS. - */ - _requestMsisdnToken: function() { - return this.props.matrixClient.requestRegisterMsisdnToken( - this.props.inputs.phoneCountry, - this.props.inputs.phoneNumber, - this.props.clientSecret, - 1, // TODO: Multiple send attempts? - ).then((result) => { - this._sid = result.sid; - this._msisdn = result.msisdn; - }); - }, - - _onTokenChange: function(e) { - this.setState({ - token: e.target.value, - }); - }, - - _onFormSubmit: function(e) { - e.preventDefault(); - if (this.state.token == '') return; - - this.setState({ - errorText: null, - }); - - this.props.matrixClient.submitMsisdnToken( - this._sid, this.props.clientSecret, this.state.token - ).then((result) => { - if (result.success) { - const idServerParsedUrl = url.parse( - this.props.matrixClient.getIdentityServerUrl(), - ) - this.props.submitAuthDict({ - type: MsisdnAuthEntry.LOGIN_TYPE, - threepid_creds: { - sid: this._sid, - client_secret: this.props.clientSecret, - id_server: idServerParsedUrl.host, - }, - }); - } else { - this.setState({ - errorText: "Token incorrect", - }); - } - }).catch((e) => { - this.props.fail(e); - console.log("Failed to submit msisdn token"); - }).done(); - }, - - render: function() { - if (this.state.requestingToken) { - const Loader = sdk.getComponent("elements.Spinner"); - return ; - } else { - const enableSubmit = Boolean(this.state.token); - const submitClasses = classnames({ - mx_InteractiveAuthEntryComponents_msisdnSubmit: true, - mx_UserSettings_button: true, // XXX button classes - }); - return ( -
-

A text message has been sent to +{this._msisdn}

-

Please enter the code it contains:

-
- - -
- - -
- {this.state.errorText} -
-
-
- ); - } - }, -}); - export const FallbackAuthEntry = React.createClass({ displayName: 'FallbackAuthEntry', @@ -446,7 +313,6 @@ const AuthEntryComponents = [ PasswordAuthEntry, RecaptchaAuthEntry, EmailIdentityAuthEntry, - MsisdnAuthEntry, ]; export function getEntryComponentForLoginType(loginType) { diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index 61cb3da652..6f6081858b 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -1,6 +1,5 @@ /* 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. @@ -18,7 +17,6 @@ limitations under the License. import React from 'react'; import ReactDOM from 'react-dom'; import classNames from 'classnames'; -import sdk from '../../../index'; import {field_input_incorrect} from '../../../UiEffects'; @@ -30,12 +28,8 @@ module.exports = React.createClass({displayName: 'PasswordLogin', onSubmit: React.PropTypes.func.isRequired, // fn(username, password) onForgotPasswordClick: React.PropTypes.func, // fn() initialUsername: React.PropTypes.string, - initialPhoneCountry: React.PropTypes.string, - initialPhoneNumber: React.PropTypes.string, initialPassword: React.PropTypes.string, onUsernameChanged: React.PropTypes.func, - onPhoneCountryChanged: React.PropTypes.func, - onPhoneNumberChanged: React.PropTypes.func, onPasswordChanged: React.PropTypes.func, loginIncorrect: React.PropTypes.bool, }, @@ -44,11 +38,7 @@ module.exports = React.createClass({displayName: 'PasswordLogin', return { onUsernameChanged: function() {}, onPasswordChanged: function() {}, - onPhoneCountryChanged: function() {}, - onPhoneNumberChanged: function() {}, initialUsername: "", - initialPhoneCountry: "", - initialPhoneNumber: "", initialPassword: "", loginIncorrect: false, }; @@ -58,8 +48,6 @@ module.exports = React.createClass({displayName: 'PasswordLogin', return { username: this.props.initialUsername, password: this.props.initialPassword, - phoneCountry: this.props.initialPhoneCountry, - phoneNumber: this.props.initialPhoneNumber, }; }, @@ -75,12 +63,7 @@ module.exports = React.createClass({displayName: 'PasswordLogin', onSubmitForm: function(ev) { ev.preventDefault(); - this.props.onSubmit( - this.state.username, - this.state.phoneCountry, - this.state.phoneNumber, - this.state.password, - ); + this.props.onSubmit(this.state.username, this.state.password); }, onUsernameChanged: function(ev) { @@ -88,16 +71,6 @@ module.exports = React.createClass({displayName: 'PasswordLogin', this.props.onUsernameChanged(ev.target.value); }, - onPhoneCountryChanged: function(country) { - this.setState({phoneCountry: country}); - this.props.onPhoneCountryChanged(country); - }, - - onPhoneNumberChanged: function(ev) { - this.setState({phoneNumber: ev.target.value}); - this.props.onPhoneNumberChanged(ev.target.value); - }, - onPasswordChanged: function(ev) { this.setState({password: ev.target.value}); this.props.onPasswordChanged(ev.target.value); @@ -119,28 +92,13 @@ module.exports = React.createClass({displayName: 'PasswordLogin', error: this.props.loginIncorrect, }); - const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); return (
- - or -
- - -

{this._passwordField = e;}} type="password" name="password" diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index 4868c9de63..93e3976834 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -19,12 +19,9 @@ import React from 'react'; import { field_input_incorrect } from '../../../UiEffects'; import sdk from '../../../index'; import Email from '../../../email'; -import { looksValid as phoneNumberLooksValid } from '../../../phonenumber'; import Modal from '../../../Modal'; const FIELD_EMAIL = 'field_email'; -const FIELD_PHONE_COUNTRY = 'field_phone_country'; -const FIELD_PHONE_NUMBER = 'field_phone_number'; const FIELD_USERNAME = 'field_username'; const FIELD_PASSWORD = 'field_password'; const FIELD_PASSWORD_CONFIRM = 'field_password_confirm'; @@ -38,8 +35,6 @@ module.exports = React.createClass({ propTypes: { // Values pre-filled in the input boxes when the component loads defaultEmail: React.PropTypes.string, - defaultPhoneCountry: React.PropTypes.string, - defaultPhoneNumber: React.PropTypes.string, defaultUsername: React.PropTypes.string, defaultPassword: React.PropTypes.string, teamsConfig: React.PropTypes.shape({ @@ -76,8 +71,6 @@ module.exports = React.createClass({ return { fieldValid: {}, selectedTeam: null, - // The ISO2 country code selected in the phone number entry - phoneCountry: this.props.defaultPhoneCountry, }; }, @@ -92,7 +85,6 @@ module.exports = React.createClass({ this.validateField(FIELD_PASSWORD_CONFIRM); this.validateField(FIELD_PASSWORD); this.validateField(FIELD_USERNAME); - this.validateField(FIELD_PHONE_NUMBER); this.validateField(FIELD_EMAIL); var self = this; @@ -126,8 +118,6 @@ module.exports = React.createClass({ username: this.refs.username.value.trim() || this.props.guestUsername, password: this.refs.password.value.trim(), email: email, - phoneCountry: this.state.phoneCountry, - phoneNumber: this.refs.phoneNumber.value.trim(), }); if (promise) { @@ -184,11 +174,6 @@ module.exports = React.createClass({ const emailValid = email === '' || Email.looksValid(email); this.markFieldValid(field_id, emailValid, "RegistrationForm.ERR_EMAIL_INVALID"); break; - case FIELD_PHONE_NUMBER: - const phoneNumber = this.refs.phoneNumber.value; - const phoneNumberValid = phoneNumber === '' || phoneNumberLooksValid(phoneNumber); - this.markFieldValid(field_id, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID"); - break; case FIELD_USERNAME: // XXX: SPEC-1 var username = this.refs.username.value.trim() || this.props.guestUsername; @@ -248,8 +233,6 @@ module.exports = React.createClass({ switch (field_id) { case FIELD_EMAIL: return this.refs.email; - case FIELD_PHONE_NUMBER: - return this.refs.phoneNumber; case FIELD_USERNAME: return this.refs.username; case FIELD_PASSWORD: @@ -268,12 +251,6 @@ module.exports = React.createClass({ return cls; }, - _onPhoneCountryChange(newVal) { - this.setState({ - phoneCountry: newVal, - }); - }, - render: function() { var self = this; @@ -309,25 +286,6 @@ module.exports = React.createClass({ } } - const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); - const phoneSection = ( -
- - -
- ); - const registerButton = ( ); @@ -342,7 +300,6 @@ module.exports = React.createClass({ {emailSection} {belowEmailSection} - {phoneSection} Date: Sun, 12 Mar 2017 20:03:05 +0000 Subject: [PATCH 16/24] beautify UserSettings error msg fix up default dialog cancel button --- src/components/structures/UserSettings.js | 2 +- src/components/views/dialogs/BaseDialog.js | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 10ffbca0d3..1e99a12e4d 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -208,7 +208,7 @@ module.exports = React.createClass({ var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Can't load user settings", - description: error.toString() + description: "Server may be unavailable or overloaded", }); }); }, diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index e83403ef7c..f404bdd33d 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -65,15 +65,14 @@ export default React.createClass({ }, render: function() { + const TintableSvg = sdk.getComponent("elements.TintableSvg"); + return (
- Cancel +
{ this.props.title } From e5a5b5cd08e6acb7449ac44cca1472752f6aa5ea Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 12 Mar 2017 20:13:39 +0000 Subject: [PATCH 17/24] oops --- src/components/views/dialogs/BaseDialog.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index f404bdd33d..0b2ca5225d 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -18,6 +18,7 @@ import React from 'react'; import * as KeyCode from '../../../KeyCode'; import AccessibleButton from '../elements/AccessibleButton'; +import sdk from '../../../index'; /** * Basic container for modal dialogs. From 71e0780eeecbe3db8b00722f9128ee717eaee31c Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 12 Mar 2017 22:24:16 +0000 Subject: [PATCH 18/24] beautify search fail error --- src/components/structures/RoomView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 696d15f84a..fe7dad3a69 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1018,7 +1018,7 @@ module.exports = React.createClass({ var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Search failed", - description: error.toString() + description: "Server may be unavailable, overloaded, or search timed out :(" }); }).finally(function() { self.setState({ From 3aaf37df1a02846f7122f964a6090ba581979b5c Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 12 Mar 2017 22:59:41 +0000 Subject: [PATCH 19/24] beautify a tonne more errors --- src/CallHandler.js | 3 ++- src/components/structures/MatrixChat.js | 3 ++- src/components/structures/RoomView.js | 4 ++- src/components/structures/UserSettings.js | 20 +++++++++------ .../views/dialogs/ChatInviteDialog.js | 12 ++++----- src/components/views/rooms/MemberInfo.js | 25 +++++++++++-------- .../views/rooms/MessageComposerInput.js | 2 +- .../views/rooms/MessageComposerInputOld.js | 2 +- src/components/views/rooms/RoomHeader.js | 3 ++- src/components/views/rooms/RoomSettings.js | 5 ++-- src/createRoom.js | 3 ++- 11 files changed, 50 insertions(+), 32 deletions(-) diff --git a/src/CallHandler.js b/src/CallHandler.js index bb46056d19..42cc681d08 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -310,9 +310,10 @@ function _onAction(payload) { placeCall(call); }, function(err) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Conference call failed: " + err); Modal.createDialog(ErrorDialog, { title: "Failed to set up conference call", - description: "Conference call failed: " + err, + description: "Conference call failed.", }); }); } diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 44fdfcf23e..2fa5e92608 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -402,9 +402,10 @@ module.exports = React.createClass({ dis.dispatch({action: 'view_next_room'}); }, function(err) { modal.close(); + console.error("Failed to leave room " + payload.room_id + " " + err); Modal.createDialog(ErrorDialog, { title: "Failed to leave room", - description: err.toString() + description: "Server may be unavailable, overloaded, or you hit a bug." }); }); } diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index fe7dad3a69..52161012aa 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -930,9 +930,10 @@ module.exports = React.createClass({ file, this.state.room.roomId, MatrixClientPeg.get() ).done(undefined, function(error) { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to upload file " + file + " " + error); Modal.createDialog(ErrorDialog, { title: "Failed to upload file", - description: error.toString() + description: "Server may be unavailable, overloaded, or the file too big", }); }); }, @@ -1016,6 +1017,7 @@ module.exports = React.createClass({ }); }, function(error) { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Search failed: " + error); Modal.createDialog(ErrorDialog, { title: "Search failed", description: "Server may be unavailable, overloaded, or search timed out :(" diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 1e99a12e4d..febdccd9c3 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -206,6 +206,7 @@ module.exports = React.createClass({ }); }, function(error) { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to load user settings: " + error); Modal.createDialog(ErrorDialog, { title: "Can't load user settings", description: "Server may be unavailable or overloaded", @@ -246,10 +247,11 @@ module.exports = React.createClass({ self._refreshFromServer(); }, function(err) { var errMsg = (typeof err === "string") ? err : (err.error || ""); + console.error("Failed to set avatar: " + err); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Error", - description: "Failed to set avatar. " + errMsg + description: "Failed to set avatar." }); }); }, @@ -286,6 +288,7 @@ module.exports = React.createClass({ errMsg += ` (HTTP status ${err.httpStatus})`; } var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to change password: " + errMsg); Modal.createDialog(ErrorDialog, { title: "Error", description: errMsg @@ -337,9 +340,10 @@ module.exports = React.createClass({ }); }, (err) => { this.setState({email_add_pending: false}); + console.error("Unable to add email address " + email_address + " " + err); Modal.createDialog(ErrorDialog, { - title: "Unable to add email address", - description: err.message + title: "Error", + description: "Unable to add email address" }); }); ReactDOM.findDOMNode(this.refs.add_threepid_input).blur(); @@ -361,9 +365,10 @@ module.exports = React.createClass({ return this._refreshFromServer(); }).catch((err) => { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Unable to remove contact information: " + err); Modal.createDialog(ErrorDialog, { - title: "Unable to remove contact information", - description: err.toString(), + title: "Error", + description: "Unable to remove contact information", }); }).done(); } @@ -401,9 +406,10 @@ module.exports = React.createClass({ }); } else { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Unable to verify email address: " + err); Modal.createDialog(ErrorDialog, { - title: "Unable to verify email address", - description: err.toString(), + title: "Error", + description: "Unable to verify email address", }); } }); diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index 0e6a2b62e6..f958b8887c 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -318,8 +318,8 @@ module.exports = React.createClass({ console.error(err.stack); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Failure to invite", - description: err.toString() + title: "Error", + description: "Failed to invite", }); return null; }) @@ -331,8 +331,8 @@ module.exports = React.createClass({ console.error(err.stack); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Failure to invite user", - description: err.toString() + title: "Error", + description: "Failed to invite user", }); return null; }) @@ -352,8 +352,8 @@ module.exports = React.createClass({ console.error(err.stack); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Failure to invite", - description: err.toString() + title: "Error", + description: "Failed to invite", }); return null; }) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 467c31eb2a..39a6c052f8 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -237,9 +237,10 @@ module.exports = WithMatrixClient(React.createClass({ console.log("Kick success"); }, function(err) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Kick error: " + err); Modal.createDialog(ErrorDialog, { - title: "Kick error", - description: err.message + title: "Error", + description: "Failed to kick user", }); } ).finally(()=>{ @@ -278,9 +279,10 @@ module.exports = WithMatrixClient(React.createClass({ console.log("Ban success"); }, function(err) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Ban error: " + err); Modal.createDialog(ErrorDialog, { - title: "Ban error", - description: err.message, + title: "Error", + description: "Failed to ban user", }); } ).finally(()=>{ @@ -327,9 +329,10 @@ module.exports = WithMatrixClient(React.createClass({ // get out of sync if we force setState here! console.log("Mute toggle success"); }, function(err) { + console.error("Mute error: " + err); Modal.createDialog(ErrorDialog, { - title: "Mute error", - description: err.message + title: "Error", + description: "Failed to mute user", }); } ).finally(()=>{ @@ -375,9 +378,10 @@ module.exports = WithMatrixClient(React.createClass({ description: "This action cannot be performed by a guest user. Please register to be able to do this." }); } else { + console.error("Toggle moderator error:" + err); Modal.createDialog(ErrorDialog, { - title: "Moderator toggle error", - description: err.message + title: "Error", + description: "Failed to toggle moderator status", }); } } @@ -395,9 +399,10 @@ module.exports = WithMatrixClient(React.createClass({ console.log("Power change success"); }, function(err) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to change power level " + err); Modal.createDialog(ErrorDialog, { - title: "Failure to change power level", - description: err.message + title: "Error", + description: "Failed to change power level", }); } ).finally(()=>{ diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index ef66942637..d702b7558d 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -509,7 +509,7 @@ export default class MessageComposerInput extends React.Component { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Server error", - description: err.message + description: "Server unavailable, overloaded, or something else went wrong.", }); }); } diff --git a/src/components/views/rooms/MessageComposerInputOld.js b/src/components/views/rooms/MessageComposerInputOld.js index 9f6464b69b..f0b650eb04 100644 --- a/src/components/views/rooms/MessageComposerInputOld.js +++ b/src/components/views/rooms/MessageComposerInputOld.js @@ -311,7 +311,7 @@ export default React.createClass({ var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Server error", - description: err.message + description: "Server unavailable, overloaded, or something else went wrong.", }); }); } diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 1a8776cd96..94f2691f2c 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -115,9 +115,10 @@ module.exports = React.createClass({ changeAvatar.onFileSelected(ev).catch(function(err) { var errMsg = (typeof err === "string") ? err : (err.error || ""); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to set avatar: " + errMsg); Modal.createDialog(ErrorDialog, { title: "Error", - description: "Failed to set avatar. " + errMsg + description: "Failed to set avatar.", }); }).done(); }, diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 3247f5a90b..2c7e1d7140 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -54,9 +54,10 @@ const BannedUser = React.createClass({ this.props.member.roomId, this.props.member.userId, ).catch((err) => { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to unban: " + err); Modal.createDialog(ErrorDialog, { - title: "Failed to unban", - description: err.message, + title: "Error", + description: "Failed to unban", }); }).done(); }, diff --git a/src/createRoom.js b/src/createRoom.js index 2a23fb0787..674fe23d28 100644 --- a/src/createRoom.js +++ b/src/createRoom.js @@ -102,9 +102,10 @@ function createRoom(opts) { }); return roomId; }, function(err) { + console.error("Failed to create room " + roomId + " " + err); Modal.createDialog(ErrorDialog, { title: "Failure to create room", - description: err.toString() + description: "Server may be unavailable, overloaded, or you hit a bug.", }); return null; }); From 185473b8982d0ea5575241f21f74cd5fc513cd1b Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 12 Mar 2017 23:48:49 +0000 Subject: [PATCH 20/24] copyright... --- src/UnknownDeviceErrorHandler.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/UnknownDeviceErrorHandler.js b/src/UnknownDeviceErrorHandler.js index 88f4f57fe4..d842cc3a6e 100644 --- a/src/UnknownDeviceErrorHandler.js +++ b/src/UnknownDeviceErrorHandler.js @@ -1,3 +1,19 @@ +/* +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 dis from './dispatcher'; import sdk from './index'; import Modal from './Modal'; From 3a849bce603542bcc5e1c9e2607ba164c10f6fa9 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 12 Mar 2017 23:48:57 +0000 Subject: [PATCH 21/24] name class to match file --- src/components/views/dialogs/ChatCreateOrReuseDialog.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/dialogs/ChatCreateOrReuseDialog.js b/src/components/views/dialogs/ChatCreateOrReuseDialog.js index 7761e25010..8f57bf9ae3 100644 --- a/src/components/views/dialogs/ChatCreateOrReuseDialog.js +++ b/src/components/views/dialogs/ChatCreateOrReuseDialog.js @@ -24,7 +24,7 @@ import Unread from '../../../Unread'; import classNames from 'classnames'; import createRoom from '../../../createRoom'; -export default class CreateOrReuseChatDialog extends React.Component { +export default class ChatCreateOrReuseChatDialog extends React.Component { constructor(props) { super(props); @@ -91,7 +91,7 @@ export default class CreateOrReuseChatDialog extends React.Component { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); return ( - { this.props.onFinished(false) }} @@ -105,7 +105,7 @@ export default class CreateOrReuseChatDialog extends React.Component { } } -CreateOrReuseChatDialog.propTyps = { +ChatCreateOrReuseChatDialog.propTyps = { userId: React.PropTypes.string.isRequired, onFinished: React.PropTypes.func.isRequired, }; From bf64f387ced295501a8e05d6e5f8dc04be73f1db Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 12 Mar 2017 23:50:12 +0000 Subject: [PATCH 22/24] name class to match file --- src/components/views/dialogs/ChatCreateOrReuseDialog.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/dialogs/ChatCreateOrReuseDialog.js b/src/components/views/dialogs/ChatCreateOrReuseDialog.js index 8f57bf9ae3..559a6f39a9 100644 --- a/src/components/views/dialogs/ChatCreateOrReuseDialog.js +++ b/src/components/views/dialogs/ChatCreateOrReuseDialog.js @@ -24,7 +24,7 @@ import Unread from '../../../Unread'; import classNames from 'classnames'; import createRoom from '../../../createRoom'; -export default class ChatCreateOrReuseChatDialog extends React.Component { +export default class ChatCreateOrReuseDialog extends React.Component { constructor(props) { super(props); @@ -91,7 +91,7 @@ export default class ChatCreateOrReuseChatDialog extends React.Component { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); return ( - { this.props.onFinished(false) }} @@ -105,7 +105,7 @@ export default class ChatCreateOrReuseChatDialog extends React.Component { } } -ChatCreateOrReuseChatDialog.propTyps = { +ChatCreateOrReuseDialog.propTyps = { userId: React.PropTypes.string.isRequired, onFinished: React.PropTypes.func.isRequired, }; From 8a0b08e7f620f0ea9b7cc3dcf68f4d5f50c980b5 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 13 Mar 2017 00:03:33 +0000 Subject: [PATCH 23/24] fix CSS for ChatCreateOrReuseDialog.js --- .../views/dialogs/ChatCreateOrReuseDialog.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/views/dialogs/ChatCreateOrReuseDialog.js b/src/components/views/dialogs/ChatCreateOrReuseDialog.js index 559a6f39a9..1a6ddf0456 100644 --- a/src/components/views/dialogs/ChatCreateOrReuseDialog.js +++ b/src/components/views/dialogs/ChatCreateOrReuseDialog.js @@ -97,9 +97,13 @@ export default class ChatCreateOrReuseDialog extends React.Component { }} title='Create a new chat or reuse an existing one' > - You already have existing direct chats with this user: - {tiles} - {startNewChat} +
+ You already have existing direct chats with this user: +
+ {tiles} + {startNewChat} +
+
); } From 925bbb79ad370d7ccf9c18852a85fc4080779706 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 13 Mar 2017 00:47:33 +0000 Subject: [PATCH 24/24] fix kick dialog CSS --- src/components/views/dialogs/ConfirmUserActionDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.js b/src/components/views/dialogs/ConfirmUserActionDialog.js index 4bd9cb669c..6cfaac65d4 100644 --- a/src/components/views/dialogs/ConfirmUserActionDialog.js +++ b/src/components/views/dialogs/ConfirmUserActionDialog.js @@ -97,7 +97,7 @@ export default React.createClass({ >
- +
{this.props.member.name}
{this.props.member.userId}