From ef88e02931f8de61960e362a636e47bc7fb102fc Mon Sep 17 00:00:00 2001 From: Sijmen Schoon <me@sijmenschoon.nl> 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.length; i++) { fileList.push(<li key={i}> - <TintableSvg key={i} src="img/files.svg" width="16" height="16" /> {files[i].name} + <TintableSvg key={i} src="img/files.svg" width="16" height="16" /> {files[i].name || 'Attachment'} </li>); } @@ -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 <lukeb@openmarket.com> 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 = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" src="img/e2e-unencrypted.svg" width="12" height="12"/>; } + const timestamp = this.props.mxEvent.isRedacted() ? + null : <MessageTimestamp ts={this.props.mxEvent.getTs()} />; 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 } <a href={ permalink }> { sender } - <MessageTimestamp ts={this.props.mxEvent.getTs()} /> + { timestamp } </a> </div> <div className="mx_EventTile_line" > @@ -530,7 +534,7 @@ module.exports = WithMatrixClient(React.createClass({ <a className="mx_EventTile_senderDetailsLink" href={ permalink }> <div className="mx_EventTile_senderDetails"> { sender } - <MessageTimestamp ts={this.props.mxEvent.getTs()} /> + { timestamp } </div> </a> </div> @@ -546,7 +550,7 @@ module.exports = WithMatrixClient(React.createClass({ { sender } <div className="mx_EventTile_line"> <a href={ permalink }> - <MessageTimestamp ts={this.props.mxEvent.getTs()} /> + { timestamp } </a> { e2e } <EventTileType ref="tile" @@ -564,7 +568,6 @@ module.exports = WithMatrixClient(React.createClass({ })); module.exports.haveTileForEvent = function(e) { - if (e.isRedacted()) return false; if (eventTileTypes[e.getType()] == undefined) return false; if (eventTileTypes[e.getType()] == 'messages.TextualEvent') { return TextForEvent.textForEvent(e) !== ''; From 6c32e3720b069efd109849fb7e2125cac717e6cb Mon Sep 17 00:00:00 2001 From: Luke Barnard <lukeb@openmarket.com> 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 <lukeb@openmarket.com> 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 ( <span className="mx_UnknownBody"> - {content.body} + {text} </span> ); }, 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 <lukeb@openmarket.com> 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 ( <span className="mx_UnknownBody"> From abd71cd2ac19bd7ba12a4c683cff05908daee1d7 Mon Sep 17 00:00:00 2001 From: Luke Barnard <lukeb@openmarket.com> 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 ( <span className="mx_UnknownBody"> From edccfeb20b28e0306e1fca1bffbf1b36d99bc821 Mon Sep 17 00:00:00 2001 From: Luke Barnard <lukeb@openmarket.com> 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 ( <span className="mx_UnknownBody"> {text} From c0fc3ba3fe6418aeae9df721a0ddbe9f5a916565 Mon Sep 17 00:00:00 2001 From: Luke Barnard <lukeb@openmarket.com> 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}> <EventTile mxEvent={mxEv} continuation={continuation} + isRedacted={mxEv.isRedacted()} onWidgetLoad={this._onWidgetLoad} readReceipts={readReceipts} readReceiptMap={this._readReceiptMap} diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 087cef7689..334c28912b 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -65,6 +65,12 @@ module.exports = WithMatrixClient(React.createClass({ /* the MatrixEvent to show */ mxEvent: React.PropTypes.object.isRequired, + /* true if mxEvent is redacted. This is a prop because using mxEvent.isRedacted() + * might not be enough when deciding shouldComponentUpdate - prevProps.mxEvent + * references the same this.props.mxEvent. + */ + isRedacted: React.PropTypes.bool, + /* true if this is a continuation of the previous event (which has the * effect of not showing another avatar/displayname */ @@ -388,7 +394,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 = (eventType === 'm.room.message') && this.props.mxEvent.isRedacted(); + const isRedacted = (eventType === 'm.room.message') && this.props.isRedacted; var classes = classNames({ mx_EventTile: true, From 06a05c351d95fbb90ee2db65cb04bb902eb23228 Mon Sep 17 00:00:00 2001 From: Luke Barnard <lukeb@openmarket.com> 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 <lukeb@openmarket.com> 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 <lukeb@openmarket.com> 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 <lukeb@openmarket.com> 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 ( <div className={classes}> <div className="mx_EventTile_roomName"> - <a href={ permalink }> + <a href={ permalink } onClick={this.onPermalinkClicked}> { room ? room.name : '' } </a> </div> <div className="mx_EventTile_senderDetails"> { avatar } - <a href={ permalink }> + <a href={ permalink } onClick={this.onPermalinkClicked}> { sender } <MessageTimestamp ts={this.props.mxEvent.getTs()} /> </a> @@ -527,7 +541,11 @@ module.exports = WithMatrixClient(React.createClass({ tileShape={this.props.tileShape} onWidgetLoad={this.props.onWidgetLoad} /> </div> - <a className="mx_EventTile_senderDetailsLink" href={ permalink }> + <a + className="mx_EventTile_senderDetailsLink" + href={ permalink } + onClick={this.onPermalinkClicked} + > <div className="mx_EventTile_senderDetails"> { sender } <MessageTimestamp ts={this.props.mxEvent.getTs()} /> @@ -545,7 +563,7 @@ module.exports = WithMatrixClient(React.createClass({ { avatar } { sender } <div className="mx_EventTile_line"> - <a href={ permalink }> + <a href={ permalink } onClick={this.onPermalinkClicked}> <MessageTimestamp ts={this.props.mxEvent.getTs()} /> </a> { e2e } From 173daddb04f7be1466d9ce81a772dd44f7b9b1b6 Mon Sep 17 00:00:00 2001 From: Luke Barnard <lukeb@openmarket.com> 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 <dbkr@users.noreply.github.com> 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 <img alt={alt} src={`${emojione.imagePathSVG}${fileName}.svg${emojione.cacheBustParam}`}/>; +} + 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({ <PasswordLogin onSubmit={this.onPasswordLogin} initialUsername={this.state.username} + initialPhoneCountry={this.state.phoneCountry} + initialPhoneNumber={this.state.phoneNumber} onUsernameChanged={this.onUsernameChanged} + onPhoneCountryChanged={this.onPhoneCountryChanged} + onPhoneNumberChanged={this.onPhoneNumberChanged} onForgotPasswordClick={this.props.onForgotPasswordClick} loginIncorrect={this.state.loginIncorrect} /> 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({ <RegistrationForm defaultUsername={this.state.formVals.username} defaultEmail={this.state.formVals.email} + defaultPhoneCountry={this.state.formVals.phoneCountry} + defaultPhoneNumber={this.state.formVals.phoneNumber} defaultPassword={this.state.formVals.password} teamsConfig={this.state.teamsConfig} guestUsername={guestUsername} diff --git a/src/components/views/elements/AccessibleButton.js b/src/components/views/elements/AccessibleButton.js index ffea8e1ba7..2c23c0d208 100644 --- a/src/components/views/elements/AccessibleButton.js +++ b/src/components/views/elements/AccessibleButton.js @@ -27,8 +27,8 @@ import React from 'react'; export default function AccessibleButton(props) { const {element, onClick, children, ...restProps} = props; restProps.onClick = onClick; - restProps.onKeyDown = function(e) { - if (e.keyCode == 13 || e.keyCode == 32) return onClick(); + restProps.onKeyUp = function(e) { + if (e.keyCode == 13 || e.keyCode == 32) return onClick(e); }; restProps.tabIndex = restProps.tabIndex || "0"; restProps.role = "button"; diff --git a/src/components/views/elements/Dropdown.js b/src/components/views/elements/Dropdown.js new file mode 100644 index 0000000000..3b34d3cac1 --- /dev/null +++ b/src/components/views/elements/Dropdown.js @@ -0,0 +1,324 @@ +/* +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 classnames from 'classnames'; +import AccessibleButton from './AccessibleButton'; + +class MenuOption extends React.Component { + constructor(props) { + super(props); + this._onMouseEnter = this._onMouseEnter.bind(this); + this._onClick = this._onClick.bind(this); + } + + _onMouseEnter() { + this.props.onMouseEnter(this.props.dropdownKey); + } + + _onClick(e) { + e.preventDefault(); + e.stopPropagation(); + this.props.onClick(this.props.dropdownKey); + } + + render() { + const optClasses = classnames({ + mx_Dropdown_option: true, + mx_Dropdown_option_highlight: this.props.highlighted, + }); + + return <div className={optClasses} + onClick={this._onClick} onKeyPress={this._onKeyPress} + onMouseEnter={this._onMouseEnter} + > + {this.props.children} + </div> + } +}; + +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 ( + <MenuOption key={child.key} dropdownKey={child.key} + highlighted={this.state.highlightedOption == child.key} + onMouseEnter={this._setHighlightedOption} + onClick={this._onMenuOptionClick} + > + {child} + </MenuOption> + ); + }); + + if (!this.state.searchQuery) { + options.push( + <div key="_searchprompt" className="mx_Dropdown_searchPrompt"> + Type to search... + </div> + ); + } + return options; + } + + render() { + let currentValue; + + const menuStyle = {}; + if (this.props.menuWidth) menuStyle.width = this.props.menuWidth; + + let menu; + if (this.state.expanded) { + currentValue = <input type="text" className="mx_Dropdown_option" + ref={this._collectInputTextBox} onKeyPress={this._onInputKeyPress} + onKeyUp={this._onInputKeyUp} + onChange={this._onInputChange} + value={this.state.searchQuery} + />; + menu = <div className="mx_Dropdown_menu" style={menuStyle}> + {this._getMenuOptions()} + </div>; + } else { + const selectedChild = this.props.getShortOption ? + this.props.getShortOption(this.props.value) : + this.childrenByKey[this.props.value]; + currentValue = <div className="mx_Dropdown_option"> + {selectedChild} + </div> + } + + 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 <div className={classnames(dropdownClasses)} ref={this._collectRoot}> + <AccessibleButton className="mx_Dropdown_input" onClick={this._onInputClick}> + {currentValue} + <span className="mx_Dropdown_arrow"></span> + {menu} + </AccessibleButton> + </div>; + } +} + +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 <div key={country.iso2}> + {this._flagImgForIso2(country.iso2)} + {country.name} + </div>; + }); + + // 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 <Dropdown className={this.props.className} + onOptionChange={this.props.onOptionChange} onSearchChange={this._onSearchChange} + menuWidth={298} getShortOption={this._flagImgForIso2} + value={value} + > + {options} + </Dropdown> + } +} + +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 <Loader />; + } else { + const enableSubmit = Boolean(this.state.token); + const submitClasses = classnames({ + mx_InteractiveAuthEntryComponents_msisdnSubmit: true, + mx_UserSettings_button: true, // XXX button classes + }); + return ( + <div> + <p>A text message has been sent to +<i>{this._msisdn}</i></p> + <p>Please enter the code it contains:</p> + <div className="mx_InteractiveAuthEntryComponents_msisdnWrapper"> + <form onSubmit={this._onFormSubmit}> + <input type="text" + className="mx_InteractiveAuthEntryComponents_msisdnEntry" + value={this.state.token} + onChange={this._onTokenChange} + /> + <br /> + <input type="submit" value="Submit" + className={submitClasses} + disabled={!enableSubmit} + /> + </form> + <div className="error"> + {this.state.errorText} + </div> + </div> + </div> + ); + } + }, +}); + 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 ( <div> <form onSubmit={this.onSubmitForm}> - <input className="mx_Login_field" type="text" + <input className="mx_Login_field mx_Login_username" type="text" name="username" // make it a little easier for browser's remember-password value={this.state.username} onChange={this.onUsernameChanged} placeholder="Email or user name" autoFocus /> + or + <div className="mx_Login_phoneSection"> + <CountryDropdown ref="phone_country" onOptionChange={this.onPhoneCountryChanged} + className="mx_Login_phoneCountry" + value={this.state.phoneCountry} + /> + <input type="text" ref="phoneNumber" + onChange={this.onPhoneNumberChanged} + placeholder="Mobile phone number" + className="mx_Login_phoneNumberField mx_Login_field" + value={this.state.phoneNumber} + name="phoneNumber" + /> + </div> <br /> <input className={pwFieldClass} ref={(e) => {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 = ( + <div className="mx_Login_phoneSection"> + <CountryDropdown ref="phone_country" onOptionChange={this._onPhoneCountryChange} + className="mx_Login_phoneCountry" + value={this.state.phoneCountry} + /> + <input type="text" ref="phoneNumber" + placeholder="Mobile phone number (optional)" + defaultValue={this.props.defaultPhoneNumber} + className={this._classForField( + FIELD_PHONE_NUMBER, 'mx_Login_phoneNumberField', 'mx_Login_field' + )} + onBlur={function() {self.validateField(FIELD_PHONE_NUMBER);}} + value={self.state.phoneNumber} + /> + </div> + ); + const registerButton = ( <input className="mx_Login_submit" type="submit" value="Register" /> ); @@ -300,6 +342,7 @@ module.exports = React.createClass({ <form onSubmit={this.onSubmit}> {emailSection} {belowEmailSection} + {phoneSection} <input type="text" ref="username" placeholder={ placeholderUserName } defaultValue={this.props.defaultUsername} className={this._classForField(FIELD_USERNAME, 'mx_Login_field')} diff --git a/src/phonenumber.js b/src/phonenumber.js new file mode 100644 index 0000000000..aaf018ba26 --- /dev/null +++ b/src/phonenumber.js @@ -0,0 +1,1273 @@ +/* +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. +*/ + +const PHONE_NUMBER_REGEXP = /^[0-9 -\.]+$/; + +/* + * Do basic validation to determine if the given input could be + * a valid phone number. + * + * @param {String} phoneNumber The string to validate. This could be + * either an international format number (MSISDN or e.164) or + * a national-format number. + * @return True if the number could be a valid phone number, otherwise false. + */ +export function looksValid(phoneNumber) { + return PHONE_NUMBER_REGEXP.test(phoneNumber); +} + +export const COUNTRIES = [ + { + "iso2": "GB", + "name": "United Kingdom", + "prefix": "44", + }, + { + "iso2": "US", + "name": "United States", + "prefix": "1", + }, + { + "iso2": "AF", + "name": "Afghanistan", + "prefix": "93", + }, + { + "iso2": "AX", + "name": "\u00c5land Islands", + "prefix": "358", + }, + { + "iso2": "AL", + "name": "Albania", + "prefix": "355", + }, + { + "iso2": "DZ", + "name": "Algeria", + "prefix": "213", + }, + { + "iso2": "AS", + "name": "American Samoa", + "prefix": "1", + }, + { + "iso2": "AD", + "name": "Andorra", + "prefix": "376", + }, + { + "iso2": "AO", + "name": "Angola", + "prefix": "244", + }, + { + "iso2": "AI", + "name": "Anguilla", + "prefix": "1", + }, + { + "iso2": "AQ", + "name": "Antarctica", + "prefix": "672", + }, + { + "iso2": "AG", + "name": "Antigua & Barbuda", + "prefix": "1", + }, + { + "iso2": "AR", + "name": "Argentina", + "prefix": "54", + }, + { + "iso2": "AM", + "name": "Armenia", + "prefix": "374", + }, + { + "iso2": "AW", + "name": "Aruba", + "prefix": "297", + }, + { + "iso2": "AU", + "name": "Australia", + "prefix": "61", + }, + { + "iso2": "AT", + "name": "Austria", + "prefix": "43", + }, + { + "iso2": "AZ", + "name": "Azerbaijan", + "prefix": "994", + }, + { + "iso2": "BS", + "name": "Bahamas", + "prefix": "1", + }, + { + "iso2": "BH", + "name": "Bahrain", + "prefix": "973", + }, + { + "iso2": "BD", + "name": "Bangladesh", + "prefix": "880", + }, + { + "iso2": "BB", + "name": "Barbados", + "prefix": "1", + }, + { + "iso2": "BY", + "name": "Belarus", + "prefix": "375", + }, + { + "iso2": "BE", + "name": "Belgium", + "prefix": "32", + }, + { + "iso2": "BZ", + "name": "Belize", + "prefix": "501", + }, + { + "iso2": "BJ", + "name": "Benin", + "prefix": "229", + }, + { + "iso2": "BM", + "name": "Bermuda", + "prefix": "1", + }, + { + "iso2": "BT", + "name": "Bhutan", + "prefix": "975", + }, + { + "iso2": "BO", + "name": "Bolivia", + "prefix": "591", + }, + { + "iso2": "BA", + "name": "Bosnia", + "prefix": "387", + }, + { + "iso2": "BW", + "name": "Botswana", + "prefix": "267", + }, + { + "iso2": "BV", + "name": "Bouvet Island", + "prefix": "47", + }, + { + "iso2": "BR", + "name": "Brazil", + "prefix": "55", + }, + { + "iso2": "IO", + "name": "British Indian Ocean Territory", + "prefix": "246", + }, + { + "iso2": "VG", + "name": "British Virgin Islands", + "prefix": "1", + }, + { + "iso2": "BN", + "name": "Brunei", + "prefix": "673", + }, + { + "iso2": "BG", + "name": "Bulgaria", + "prefix": "359", + }, + { + "iso2": "BF", + "name": "Burkina Faso", + "prefix": "226", + }, + { + "iso2": "BI", + "name": "Burundi", + "prefix": "257", + }, + { + "iso2": "KH", + "name": "Cambodia", + "prefix": "855", + }, + { + "iso2": "CM", + "name": "Cameroon", + "prefix": "237", + }, + { + "iso2": "CA", + "name": "Canada", + "prefix": "1", + }, + { + "iso2": "CV", + "name": "Cape Verde", + "prefix": "238", + }, + { + "iso2": "BQ", + "name": "Caribbean Netherlands", + "prefix": "599", + }, + { + "iso2": "KY", + "name": "Cayman Islands", + "prefix": "1", + }, + { + "iso2": "CF", + "name": "Central African Republic", + "prefix": "236", + }, + { + "iso2": "TD", + "name": "Chad", + "prefix": "235", + }, + { + "iso2": "CL", + "name": "Chile", + "prefix": "56", + }, + { + "iso2": "CN", + "name": "China", + "prefix": "86", + }, + { + "iso2": "CX", + "name": "Christmas Island", + "prefix": "61", + }, + { + "iso2": "CC", + "name": "Cocos (Keeling) Islands", + "prefix": "61", + }, + { + "iso2": "CO", + "name": "Colombia", + "prefix": "57", + }, + { + "iso2": "KM", + "name": "Comoros", + "prefix": "269", + }, + { + "iso2": "CG", + "name": "Congo - Brazzaville", + "prefix": "242", + }, + { + "iso2": "CD", + "name": "Congo - Kinshasa", + "prefix": "243", + }, + { + "iso2": "CK", + "name": "Cook Islands", + "prefix": "682", + }, + { + "iso2": "CR", + "name": "Costa Rica", + "prefix": "506", + }, + { + "iso2": "HR", + "name": "Croatia", + "prefix": "385", + }, + { + "iso2": "CU", + "name": "Cuba", + "prefix": "53", + }, + { + "iso2": "CW", + "name": "Cura\u00e7ao", + "prefix": "599", + }, + { + "iso2": "CY", + "name": "Cyprus", + "prefix": "357", + }, + { + "iso2": "CZ", + "name": "Czech Republic", + "prefix": "420", + }, + { + "iso2": "CI", + "name": "C\u00f4te d\u2019Ivoire", + "prefix": "225", + }, + { + "iso2": "DK", + "name": "Denmark", + "prefix": "45", + }, + { + "iso2": "DJ", + "name": "Djibouti", + "prefix": "253", + }, + { + "iso2": "DM", + "name": "Dominica", + "prefix": "1", + }, + { + "iso2": "DO", + "name": "Dominican Republic", + "prefix": "1", + }, + { + "iso2": "EC", + "name": "Ecuador", + "prefix": "593", + }, + { + "iso2": "EG", + "name": "Egypt", + "prefix": "20", + }, + { + "iso2": "SV", + "name": "El Salvador", + "prefix": "503", + }, + { + "iso2": "GQ", + "name": "Equatorial Guinea", + "prefix": "240", + }, + { + "iso2": "ER", + "name": "Eritrea", + "prefix": "291", + }, + { + "iso2": "EE", + "name": "Estonia", + "prefix": "372", + }, + { + "iso2": "ET", + "name": "Ethiopia", + "prefix": "251", + }, + { + "iso2": "FK", + "name": "Falkland Islands", + "prefix": "500", + }, + { + "iso2": "FO", + "name": "Faroe Islands", + "prefix": "298", + }, + { + "iso2": "FJ", + "name": "Fiji", + "prefix": "679", + }, + { + "iso2": "FI", + "name": "Finland", + "prefix": "358", + }, + { + "iso2": "FR", + "name": "France", + "prefix": "33", + }, + { + "iso2": "GF", + "name": "French Guiana", + "prefix": "594", + }, + { + "iso2": "PF", + "name": "French Polynesia", + "prefix": "689", + }, + { + "iso2": "TF", + "name": "French Southern Territories", + "prefix": "262", + }, + { + "iso2": "GA", + "name": "Gabon", + "prefix": "241", + }, + { + "iso2": "GM", + "name": "Gambia", + "prefix": "220", + }, + { + "iso2": "GE", + "name": "Georgia", + "prefix": "995", + }, + { + "iso2": "DE", + "name": "Germany", + "prefix": "49", + }, + { + "iso2": "GH", + "name": "Ghana", + "prefix": "233", + }, + { + "iso2": "GI", + "name": "Gibraltar", + "prefix": "350", + }, + { + "iso2": "GR", + "name": "Greece", + "prefix": "30", + }, + { + "iso2": "GL", + "name": "Greenland", + "prefix": "299", + }, + { + "iso2": "GD", + "name": "Grenada", + "prefix": "1", + }, + { + "iso2": "GP", + "name": "Guadeloupe", + "prefix": "590", + }, + { + "iso2": "GU", + "name": "Guam", + "prefix": "1", + }, + { + "iso2": "GT", + "name": "Guatemala", + "prefix": "502", + }, + { + "iso2": "GG", + "name": "Guernsey", + "prefix": "44", + }, + { + "iso2": "GN", + "name": "Guinea", + "prefix": "224", + }, + { + "iso2": "GW", + "name": "Guinea-Bissau", + "prefix": "245", + }, + { + "iso2": "GY", + "name": "Guyana", + "prefix": "592", + }, + { + "iso2": "HT", + "name": "Haiti", + "prefix": "509", + }, + { + "iso2": "HM", + "name": "Heard & McDonald Islands", + "prefix": "672", + }, + { + "iso2": "HN", + "name": "Honduras", + "prefix": "504", + }, + { + "iso2": "HK", + "name": "Hong Kong", + "prefix": "852", + }, + { + "iso2": "HU", + "name": "Hungary", + "prefix": "36", + }, + { + "iso2": "IS", + "name": "Iceland", + "prefix": "354", + }, + { + "iso2": "IN", + "name": "India", + "prefix": "91", + }, + { + "iso2": "ID", + "name": "Indonesia", + "prefix": "62", + }, + { + "iso2": "IR", + "name": "Iran", + "prefix": "98", + }, + { + "iso2": "IQ", + "name": "Iraq", + "prefix": "964", + }, + { + "iso2": "IE", + "name": "Ireland", + "prefix": "353", + }, + { + "iso2": "IM", + "name": "Isle of Man", + "prefix": "44", + }, + { + "iso2": "IL", + "name": "Israel", + "prefix": "972", + }, + { + "iso2": "IT", + "name": "Italy", + "prefix": "39", + }, + { + "iso2": "JM", + "name": "Jamaica", + "prefix": "1", + }, + { + "iso2": "JP", + "name": "Japan", + "prefix": "81", + }, + { + "iso2": "JE", + "name": "Jersey", + "prefix": "44", + }, + { + "iso2": "JO", + "name": "Jordan", + "prefix": "962", + }, + { + "iso2": "KZ", + "name": "Kazakhstan", + "prefix": "7", + }, + { + "iso2": "KE", + "name": "Kenya", + "prefix": "254", + }, + { + "iso2": "KI", + "name": "Kiribati", + "prefix": "686", + }, + { + "iso2": "KW", + "name": "Kuwait", + "prefix": "965", + }, + { + "iso2": "KG", + "name": "Kyrgyzstan", + "prefix": "996", + }, + { + "iso2": "LA", + "name": "Laos", + "prefix": "856", + }, + { + "iso2": "LV", + "name": "Latvia", + "prefix": "371", + }, + { + "iso2": "LB", + "name": "Lebanon", + "prefix": "961", + }, + { + "iso2": "LS", + "name": "Lesotho", + "prefix": "266", + }, + { + "iso2": "LR", + "name": "Liberia", + "prefix": "231", + }, + { + "iso2": "LY", + "name": "Libya", + "prefix": "218", + }, + { + "iso2": "LI", + "name": "Liechtenstein", + "prefix": "423", + }, + { + "iso2": "LT", + "name": "Lithuania", + "prefix": "370", + }, + { + "iso2": "LU", + "name": "Luxembourg", + "prefix": "352", + }, + { + "iso2": "MO", + "name": "Macau", + "prefix": "853", + }, + { + "iso2": "MK", + "name": "Macedonia", + "prefix": "389", + }, + { + "iso2": "MG", + "name": "Madagascar", + "prefix": "261", + }, + { + "iso2": "MW", + "name": "Malawi", + "prefix": "265", + }, + { + "iso2": "MY", + "name": "Malaysia", + "prefix": "60", + }, + { + "iso2": "MV", + "name": "Maldives", + "prefix": "960", + }, + { + "iso2": "ML", + "name": "Mali", + "prefix": "223", + }, + { + "iso2": "MT", + "name": "Malta", + "prefix": "356", + }, + { + "iso2": "MH", + "name": "Marshall Islands", + "prefix": "692", + }, + { + "iso2": "MQ", + "name": "Martinique", + "prefix": "596", + }, + { + "iso2": "MR", + "name": "Mauritania", + "prefix": "222", + }, + { + "iso2": "MU", + "name": "Mauritius", + "prefix": "230", + }, + { + "iso2": "YT", + "name": "Mayotte", + "prefix": "262", + }, + { + "iso2": "MX", + "name": "Mexico", + "prefix": "52", + }, + { + "iso2": "FM", + "name": "Micronesia", + "prefix": "691", + }, + { + "iso2": "MD", + "name": "Moldova", + "prefix": "373", + }, + { + "iso2": "MC", + "name": "Monaco", + "prefix": "377", + }, + { + "iso2": "MN", + "name": "Mongolia", + "prefix": "976", + }, + { + "iso2": "ME", + "name": "Montenegro", + "prefix": "382", + }, + { + "iso2": "MS", + "name": "Montserrat", + "prefix": "1", + }, + { + "iso2": "MA", + "name": "Morocco", + "prefix": "212", + }, + { + "iso2": "MZ", + "name": "Mozambique", + "prefix": "258", + }, + { + "iso2": "MM", + "name": "Myanmar", + "prefix": "95", + }, + { + "iso2": "NA", + "name": "Namibia", + "prefix": "264", + }, + { + "iso2": "NR", + "name": "Nauru", + "prefix": "674", + }, + { + "iso2": "NP", + "name": "Nepal", + "prefix": "977", + }, + { + "iso2": "NL", + "name": "Netherlands", + "prefix": "31", + }, + { + "iso2": "NC", + "name": "New Caledonia", + "prefix": "687", + }, + { + "iso2": "NZ", + "name": "New Zealand", + "prefix": "64", + }, + { + "iso2": "NI", + "name": "Nicaragua", + "prefix": "505", + }, + { + "iso2": "NE", + "name": "Niger", + "prefix": "227", + }, + { + "iso2": "NG", + "name": "Nigeria", + "prefix": "234", + }, + { + "iso2": "NU", + "name": "Niue", + "prefix": "683", + }, + { + "iso2": "NF", + "name": "Norfolk Island", + "prefix": "672", + }, + { + "iso2": "KP", + "name": "North Korea", + "prefix": "850", + }, + { + "iso2": "MP", + "name": "Northern Mariana Islands", + "prefix": "1", + }, + { + "iso2": "NO", + "name": "Norway", + "prefix": "47", + }, + { + "iso2": "OM", + "name": "Oman", + "prefix": "968", + }, + { + "iso2": "PK", + "name": "Pakistan", + "prefix": "92", + }, + { + "iso2": "PW", + "name": "Palau", + "prefix": "680", + }, + { + "iso2": "PS", + "name": "Palestine", + "prefix": "970", + }, + { + "iso2": "PA", + "name": "Panama", + "prefix": "507", + }, + { + "iso2": "PG", + "name": "Papua New Guinea", + "prefix": "675", + }, + { + "iso2": "PY", + "name": "Paraguay", + "prefix": "595", + }, + { + "iso2": "PE", + "name": "Peru", + "prefix": "51", + }, + { + "iso2": "PH", + "name": "Philippines", + "prefix": "63", + }, + { + "iso2": "PN", + "name": "Pitcairn Islands", + "prefix": "870", + }, + { + "iso2": "PL", + "name": "Poland", + "prefix": "48", + }, + { + "iso2": "PT", + "name": "Portugal", + "prefix": "351", + }, + { + "iso2": "PR", + "name": "Puerto Rico", + "prefix": "1", + }, + { + "iso2": "QA", + "name": "Qatar", + "prefix": "974", + }, + { + "iso2": "RO", + "name": "Romania", + "prefix": "40", + }, + { + "iso2": "RU", + "name": "Russia", + "prefix": "7", + }, + { + "iso2": "RW", + "name": "Rwanda", + "prefix": "250", + }, + { + "iso2": "RE", + "name": "R\u00e9union", + "prefix": "262", + }, + { + "iso2": "WS", + "name": "Samoa", + "prefix": "685", + }, + { + "iso2": "SM", + "name": "San Marino", + "prefix": "378", + }, + { + "iso2": "SA", + "name": "Saudi Arabia", + "prefix": "966", + }, + { + "iso2": "SN", + "name": "Senegal", + "prefix": "221", + }, + { + "iso2": "RS", + "name": "Serbia", + "prefix": "381 p", + }, + { + "iso2": "SC", + "name": "Seychelles", + "prefix": "248", + }, + { + "iso2": "SL", + "name": "Sierra Leone", + "prefix": "232", + }, + { + "iso2": "SG", + "name": "Singapore", + "prefix": "65", + }, + { + "iso2": "SX", + "name": "Sint Maarten", + "prefix": "1", + }, + { + "iso2": "SK", + "name": "Slovakia", + "prefix": "421", + }, + { + "iso2": "SI", + "name": "Slovenia", + "prefix": "386", + }, + { + "iso2": "SB", + "name": "Solomon Islands", + "prefix": "677", + }, + { + "iso2": "SO", + "name": "Somalia", + "prefix": "252", + }, + { + "iso2": "ZA", + "name": "South Africa", + "prefix": "27", + }, + { + "iso2": "GS", + "name": "South Georgia & South Sandwich Islands", + "prefix": "500", + }, + { + "iso2": "KR", + "name": "South Korea", + "prefix": "82", + }, + { + "iso2": "SS", + "name": "South Sudan", + "prefix": "211", + }, + { + "iso2": "ES", + "name": "Spain", + "prefix": "34", + }, + { + "iso2": "LK", + "name": "Sri Lanka", + "prefix": "94", + }, + { + "iso2": "BL", + "name": "St. Barth\u00e9lemy", + "prefix": "590", + }, + { + "iso2": "SH", + "name": "St. Helena", + "prefix": "290 n", + }, + { + "iso2": "KN", + "name": "St. Kitts & Nevis", + "prefix": "1", + }, + { + "iso2": "LC", + "name": "St. Lucia", + "prefix": "1", + }, + { + "iso2": "MF", + "name": "St. Martin", + "prefix": "590", + }, + { + "iso2": "PM", + "name": "St. Pierre & Miquelon", + "prefix": "508", + }, + { + "iso2": "VC", + "name": "St. Vincent & Grenadines", + "prefix": "1", + }, + { + "iso2": "SD", + "name": "Sudan", + "prefix": "249", + }, + { + "iso2": "SR", + "name": "Suriname", + "prefix": "597", + }, + { + "iso2": "SJ", + "name": "Svalbard & Jan Mayen", + "prefix": "47", + }, + { + "iso2": "SZ", + "name": "Swaziland", + "prefix": "268", + }, + { + "iso2": "SE", + "name": "Sweden", + "prefix": "46", + }, + { + "iso2": "CH", + "name": "Switzerland", + "prefix": "41", + }, + { + "iso2": "SY", + "name": "Syria", + "prefix": "963", + }, + { + "iso2": "ST", + "name": "S\u00e3o Tom\u00e9 & Pr\u00edncipe", + "prefix": "239", + }, + { + "iso2": "TW", + "name": "Taiwan", + "prefix": "886", + }, + { + "iso2": "TJ", + "name": "Tajikistan", + "prefix": "992", + }, + { + "iso2": "TZ", + "name": "Tanzania", + "prefix": "255", + }, + { + "iso2": "TH", + "name": "Thailand", + "prefix": "66", + }, + { + "iso2": "TL", + "name": "Timor-Leste", + "prefix": "670", + }, + { + "iso2": "TG", + "name": "Togo", + "prefix": "228", + }, + { + "iso2": "TK", + "name": "Tokelau", + "prefix": "690", + }, + { + "iso2": "TO", + "name": "Tonga", + "prefix": "676", + }, + { + "iso2": "TT", + "name": "Trinidad & Tobago", + "prefix": "1", + }, + { + "iso2": "TN", + "name": "Tunisia", + "prefix": "216", + }, + { + "iso2": "TR", + "name": "Turkey", + "prefix": "90", + }, + { + "iso2": "TM", + "name": "Turkmenistan", + "prefix": "993", + }, + { + "iso2": "TC", + "name": "Turks & Caicos Islands", + "prefix": "1", + }, + { + "iso2": "TV", + "name": "Tuvalu", + "prefix": "688", + }, + { + "iso2": "VI", + "name": "U.S. Virgin Islands", + "prefix": "1", + }, + { + "iso2": "UG", + "name": "Uganda", + "prefix": "256", + }, + { + "iso2": "UA", + "name": "Ukraine", + "prefix": "380", + }, + { + "iso2": "AE", + "name": "United Arab Emirates", + "prefix": "971", + }, + { + "iso2": "UY", + "name": "Uruguay", + "prefix": "598", + }, + { + "iso2": "UZ", + "name": "Uzbekistan", + "prefix": "998", + }, + { + "iso2": "VU", + "name": "Vanuatu", + "prefix": "678", + }, + { + "iso2": "VA", + "name": "Vatican City", + "prefix": "39", + }, + { + "iso2": "VE", + "name": "Venezuela", + "prefix": "58", + }, + { + "iso2": "VN", + "name": "Vietnam", + "prefix": "84", + }, + { + "iso2": "WF", + "name": "Wallis & Futuna", + "prefix": "681", + }, + { + "iso2": "EH", + "name": "Western Sahara", + "prefix": "212", + }, + { + "iso2": "YE", + "name": "Yemen", + "prefix": "967", + }, + { + "iso2": "ZM", + "name": "Zambia", + "prefix": "260", + }, + { + "iso2": "ZW", + "name": "Zimbabwe", + "prefix": "263", + }, +]; From 2786fb0f467153c62463b906cc500e5fb169b1d7 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <richard@matrix.org> 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 <img alt={alt} src={`${emojione.imagePathSVG}${fileName}.svg${emojione.cacheBustParam}`}/>; -} - 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({ <PasswordLogin onSubmit={this.onPasswordLogin} initialUsername={this.state.username} - initialPhoneCountry={this.state.phoneCountry} - initialPhoneNumber={this.state.phoneNumber} onUsernameChanged={this.onUsernameChanged} - onPhoneCountryChanged={this.onPhoneCountryChanged} - onPhoneNumberChanged={this.onPhoneNumberChanged} onForgotPasswordClick={this.props.onForgotPasswordClick} loginIncorrect={this.state.loginIncorrect} /> 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({ <RegistrationForm defaultUsername={this.state.formVals.username} defaultEmail={this.state.formVals.email} - defaultPhoneCountry={this.state.formVals.phoneCountry} - defaultPhoneNumber={this.state.formVals.phoneNumber} defaultPassword={this.state.formVals.password} teamsConfig={this.state.teamsConfig} guestUsername={guestUsername} diff --git a/src/components/views/elements/AccessibleButton.js b/src/components/views/elements/AccessibleButton.js index 2c23c0d208..ffea8e1ba7 100644 --- a/src/components/views/elements/AccessibleButton.js +++ b/src/components/views/elements/AccessibleButton.js @@ -27,8 +27,8 @@ import React from 'react'; export default function AccessibleButton(props) { const {element, onClick, children, ...restProps} = props; restProps.onClick = onClick; - restProps.onKeyUp = function(e) { - if (e.keyCode == 13 || e.keyCode == 32) return onClick(e); + restProps.onKeyDown = function(e) { + if (e.keyCode == 13 || e.keyCode == 32) return onClick(); }; restProps.tabIndex = restProps.tabIndex || "0"; restProps.role = "button"; diff --git a/src/components/views/elements/Dropdown.js b/src/components/views/elements/Dropdown.js deleted file mode 100644 index 3b34d3cac1..0000000000 --- a/src/components/views/elements/Dropdown.js +++ /dev/null @@ -1,324 +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 classnames from 'classnames'; -import AccessibleButton from './AccessibleButton'; - -class MenuOption extends React.Component { - constructor(props) { - super(props); - this._onMouseEnter = this._onMouseEnter.bind(this); - this._onClick = this._onClick.bind(this); - } - - _onMouseEnter() { - this.props.onMouseEnter(this.props.dropdownKey); - } - - _onClick(e) { - e.preventDefault(); - e.stopPropagation(); - this.props.onClick(this.props.dropdownKey); - } - - render() { - const optClasses = classnames({ - mx_Dropdown_option: true, - mx_Dropdown_option_highlight: this.props.highlighted, - }); - - return <div className={optClasses} - onClick={this._onClick} onKeyPress={this._onKeyPress} - onMouseEnter={this._onMouseEnter} - > - {this.props.children} - </div> - } -}; - -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 ( - <MenuOption key={child.key} dropdownKey={child.key} - highlighted={this.state.highlightedOption == child.key} - onMouseEnter={this._setHighlightedOption} - onClick={this._onMenuOptionClick} - > - {child} - </MenuOption> - ); - }); - - if (!this.state.searchQuery) { - options.push( - <div key="_searchprompt" className="mx_Dropdown_searchPrompt"> - Type to search... - </div> - ); - } - return options; - } - - render() { - let currentValue; - - const menuStyle = {}; - if (this.props.menuWidth) menuStyle.width = this.props.menuWidth; - - let menu; - if (this.state.expanded) { - currentValue = <input type="text" className="mx_Dropdown_option" - ref={this._collectInputTextBox} onKeyPress={this._onInputKeyPress} - onKeyUp={this._onInputKeyUp} - onChange={this._onInputChange} - value={this.state.searchQuery} - />; - menu = <div className="mx_Dropdown_menu" style={menuStyle}> - {this._getMenuOptions()} - </div>; - } else { - const selectedChild = this.props.getShortOption ? - this.props.getShortOption(this.props.value) : - this.childrenByKey[this.props.value]; - currentValue = <div className="mx_Dropdown_option"> - {selectedChild} - </div> - } - - 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 <div className={classnames(dropdownClasses)} ref={this._collectRoot}> - <AccessibleButton className="mx_Dropdown_input" onClick={this._onInputClick}> - {currentValue} - <span className="mx_Dropdown_arrow"></span> - {menu} - </AccessibleButton> - </div>; - } -} - -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 <div key={country.iso2}> - {this._flagImgForIso2(country.iso2)} - {country.name} - </div>; - }); - - // 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 <Dropdown className={this.props.className} - onOptionChange={this.props.onOptionChange} onSearchChange={this._onSearchChange} - menuWidth={298} getShortOption={this._flagImgForIso2} - value={value} - > - {options} - </Dropdown> - } -} - -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 <Loader />; - } else { - const enableSubmit = Boolean(this.state.token); - const submitClasses = classnames({ - mx_InteractiveAuthEntryComponents_msisdnSubmit: true, - mx_UserSettings_button: true, // XXX button classes - }); - return ( - <div> - <p>A text message has been sent to +<i>{this._msisdn}</i></p> - <p>Please enter the code it contains:</p> - <div className="mx_InteractiveAuthEntryComponents_msisdnWrapper"> - <form onSubmit={this._onFormSubmit}> - <input type="text" - className="mx_InteractiveAuthEntryComponents_msisdnEntry" - value={this.state.token} - onChange={this._onTokenChange} - /> - <br /> - <input type="submit" value="Submit" - className={submitClasses} - disabled={!enableSubmit} - /> - </form> - <div className="error"> - {this.state.errorText} - </div> - </div> - </div> - ); - } - }, -}); - 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 ( <div> <form onSubmit={this.onSubmitForm}> - <input className="mx_Login_field mx_Login_username" type="text" + <input className="mx_Login_field" type="text" name="username" // make it a little easier for browser's remember-password value={this.state.username} onChange={this.onUsernameChanged} placeholder="Email or user name" autoFocus /> - or - <div className="mx_Login_phoneSection"> - <CountryDropdown ref="phone_country" onOptionChange={this.onPhoneCountryChanged} - className="mx_Login_phoneCountry" - value={this.state.phoneCountry} - /> - <input type="text" ref="phoneNumber" - onChange={this.onPhoneNumberChanged} - placeholder="Mobile phone number" - className="mx_Login_phoneNumberField mx_Login_field" - value={this.state.phoneNumber} - name="phoneNumber" - /> - </div> <br /> <input className={pwFieldClass} ref={(e) => {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 = ( - <div className="mx_Login_phoneSection"> - <CountryDropdown ref="phone_country" onOptionChange={this._onPhoneCountryChange} - className="mx_Login_phoneCountry" - value={this.state.phoneCountry} - /> - <input type="text" ref="phoneNumber" - placeholder="Mobile phone number (optional)" - defaultValue={this.props.defaultPhoneNumber} - className={this._classForField( - FIELD_PHONE_NUMBER, 'mx_Login_phoneNumberField', 'mx_Login_field' - )} - onBlur={function() {self.validateField(FIELD_PHONE_NUMBER);}} - value={self.state.phoneNumber} - /> - </div> - ); - const registerButton = ( <input className="mx_Login_submit" type="submit" value="Register" /> ); @@ -342,7 +300,6 @@ module.exports = React.createClass({ <form onSubmit={this.onSubmit}> {emailSection} {belowEmailSection} - {phoneSection} <input type="text" ref="username" placeholder={ placeholderUserName } defaultValue={this.props.defaultUsername} className={this._classForField(FIELD_USERNAME, 'mx_Login_field')} diff --git a/src/phonenumber.js b/src/phonenumber.js deleted file mode 100644 index aaf018ba26..0000000000 --- a/src/phonenumber.js +++ /dev/null @@ -1,1273 +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. -*/ - -const PHONE_NUMBER_REGEXP = /^[0-9 -\.]+$/; - -/* - * Do basic validation to determine if the given input could be - * a valid phone number. - * - * @param {String} phoneNumber The string to validate. This could be - * either an international format number (MSISDN or e.164) or - * a national-format number. - * @return True if the number could be a valid phone number, otherwise false. - */ -export function looksValid(phoneNumber) { - return PHONE_NUMBER_REGEXP.test(phoneNumber); -} - -export const COUNTRIES = [ - { - "iso2": "GB", - "name": "United Kingdom", - "prefix": "44", - }, - { - "iso2": "US", - "name": "United States", - "prefix": "1", - }, - { - "iso2": "AF", - "name": "Afghanistan", - "prefix": "93", - }, - { - "iso2": "AX", - "name": "\u00c5land Islands", - "prefix": "358", - }, - { - "iso2": "AL", - "name": "Albania", - "prefix": "355", - }, - { - "iso2": "DZ", - "name": "Algeria", - "prefix": "213", - }, - { - "iso2": "AS", - "name": "American Samoa", - "prefix": "1", - }, - { - "iso2": "AD", - "name": "Andorra", - "prefix": "376", - }, - { - "iso2": "AO", - "name": "Angola", - "prefix": "244", - }, - { - "iso2": "AI", - "name": "Anguilla", - "prefix": "1", - }, - { - "iso2": "AQ", - "name": "Antarctica", - "prefix": "672", - }, - { - "iso2": "AG", - "name": "Antigua & Barbuda", - "prefix": "1", - }, - { - "iso2": "AR", - "name": "Argentina", - "prefix": "54", - }, - { - "iso2": "AM", - "name": "Armenia", - "prefix": "374", - }, - { - "iso2": "AW", - "name": "Aruba", - "prefix": "297", - }, - { - "iso2": "AU", - "name": "Australia", - "prefix": "61", - }, - { - "iso2": "AT", - "name": "Austria", - "prefix": "43", - }, - { - "iso2": "AZ", - "name": "Azerbaijan", - "prefix": "994", - }, - { - "iso2": "BS", - "name": "Bahamas", - "prefix": "1", - }, - { - "iso2": "BH", - "name": "Bahrain", - "prefix": "973", - }, - { - "iso2": "BD", - "name": "Bangladesh", - "prefix": "880", - }, - { - "iso2": "BB", - "name": "Barbados", - "prefix": "1", - }, - { - "iso2": "BY", - "name": "Belarus", - "prefix": "375", - }, - { - "iso2": "BE", - "name": "Belgium", - "prefix": "32", - }, - { - "iso2": "BZ", - "name": "Belize", - "prefix": "501", - }, - { - "iso2": "BJ", - "name": "Benin", - "prefix": "229", - }, - { - "iso2": "BM", - "name": "Bermuda", - "prefix": "1", - }, - { - "iso2": "BT", - "name": "Bhutan", - "prefix": "975", - }, - { - "iso2": "BO", - "name": "Bolivia", - "prefix": "591", - }, - { - "iso2": "BA", - "name": "Bosnia", - "prefix": "387", - }, - { - "iso2": "BW", - "name": "Botswana", - "prefix": "267", - }, - { - "iso2": "BV", - "name": "Bouvet Island", - "prefix": "47", - }, - { - "iso2": "BR", - "name": "Brazil", - "prefix": "55", - }, - { - "iso2": "IO", - "name": "British Indian Ocean Territory", - "prefix": "246", - }, - { - "iso2": "VG", - "name": "British Virgin Islands", - "prefix": "1", - }, - { - "iso2": "BN", - "name": "Brunei", - "prefix": "673", - }, - { - "iso2": "BG", - "name": "Bulgaria", - "prefix": "359", - }, - { - "iso2": "BF", - "name": "Burkina Faso", - "prefix": "226", - }, - { - "iso2": "BI", - "name": "Burundi", - "prefix": "257", - }, - { - "iso2": "KH", - "name": "Cambodia", - "prefix": "855", - }, - { - "iso2": "CM", - "name": "Cameroon", - "prefix": "237", - }, - { - "iso2": "CA", - "name": "Canada", - "prefix": "1", - }, - { - "iso2": "CV", - "name": "Cape Verde", - "prefix": "238", - }, - { - "iso2": "BQ", - "name": "Caribbean Netherlands", - "prefix": "599", - }, - { - "iso2": "KY", - "name": "Cayman Islands", - "prefix": "1", - }, - { - "iso2": "CF", - "name": "Central African Republic", - "prefix": "236", - }, - { - "iso2": "TD", - "name": "Chad", - "prefix": "235", - }, - { - "iso2": "CL", - "name": "Chile", - "prefix": "56", - }, - { - "iso2": "CN", - "name": "China", - "prefix": "86", - }, - { - "iso2": "CX", - "name": "Christmas Island", - "prefix": "61", - }, - { - "iso2": "CC", - "name": "Cocos (Keeling) Islands", - "prefix": "61", - }, - { - "iso2": "CO", - "name": "Colombia", - "prefix": "57", - }, - { - "iso2": "KM", - "name": "Comoros", - "prefix": "269", - }, - { - "iso2": "CG", - "name": "Congo - Brazzaville", - "prefix": "242", - }, - { - "iso2": "CD", - "name": "Congo - Kinshasa", - "prefix": "243", - }, - { - "iso2": "CK", - "name": "Cook Islands", - "prefix": "682", - }, - { - "iso2": "CR", - "name": "Costa Rica", - "prefix": "506", - }, - { - "iso2": "HR", - "name": "Croatia", - "prefix": "385", - }, - { - "iso2": "CU", - "name": "Cuba", - "prefix": "53", - }, - { - "iso2": "CW", - "name": "Cura\u00e7ao", - "prefix": "599", - }, - { - "iso2": "CY", - "name": "Cyprus", - "prefix": "357", - }, - { - "iso2": "CZ", - "name": "Czech Republic", - "prefix": "420", - }, - { - "iso2": "CI", - "name": "C\u00f4te d\u2019Ivoire", - "prefix": "225", - }, - { - "iso2": "DK", - "name": "Denmark", - "prefix": "45", - }, - { - "iso2": "DJ", - "name": "Djibouti", - "prefix": "253", - }, - { - "iso2": "DM", - "name": "Dominica", - "prefix": "1", - }, - { - "iso2": "DO", - "name": "Dominican Republic", - "prefix": "1", - }, - { - "iso2": "EC", - "name": "Ecuador", - "prefix": "593", - }, - { - "iso2": "EG", - "name": "Egypt", - "prefix": "20", - }, - { - "iso2": "SV", - "name": "El Salvador", - "prefix": "503", - }, - { - "iso2": "GQ", - "name": "Equatorial Guinea", - "prefix": "240", - }, - { - "iso2": "ER", - "name": "Eritrea", - "prefix": "291", - }, - { - "iso2": "EE", - "name": "Estonia", - "prefix": "372", - }, - { - "iso2": "ET", - "name": "Ethiopia", - "prefix": "251", - }, - { - "iso2": "FK", - "name": "Falkland Islands", - "prefix": "500", - }, - { - "iso2": "FO", - "name": "Faroe Islands", - "prefix": "298", - }, - { - "iso2": "FJ", - "name": "Fiji", - "prefix": "679", - }, - { - "iso2": "FI", - "name": "Finland", - "prefix": "358", - }, - { - "iso2": "FR", - "name": "France", - "prefix": "33", - }, - { - "iso2": "GF", - "name": "French Guiana", - "prefix": "594", - }, - { - "iso2": "PF", - "name": "French Polynesia", - "prefix": "689", - }, - { - "iso2": "TF", - "name": "French Southern Territories", - "prefix": "262", - }, - { - "iso2": "GA", - "name": "Gabon", - "prefix": "241", - }, - { - "iso2": "GM", - "name": "Gambia", - "prefix": "220", - }, - { - "iso2": "GE", - "name": "Georgia", - "prefix": "995", - }, - { - "iso2": "DE", - "name": "Germany", - "prefix": "49", - }, - { - "iso2": "GH", - "name": "Ghana", - "prefix": "233", - }, - { - "iso2": "GI", - "name": "Gibraltar", - "prefix": "350", - }, - { - "iso2": "GR", - "name": "Greece", - "prefix": "30", - }, - { - "iso2": "GL", - "name": "Greenland", - "prefix": "299", - }, - { - "iso2": "GD", - "name": "Grenada", - "prefix": "1", - }, - { - "iso2": "GP", - "name": "Guadeloupe", - "prefix": "590", - }, - { - "iso2": "GU", - "name": "Guam", - "prefix": "1", - }, - { - "iso2": "GT", - "name": "Guatemala", - "prefix": "502", - }, - { - "iso2": "GG", - "name": "Guernsey", - "prefix": "44", - }, - { - "iso2": "GN", - "name": "Guinea", - "prefix": "224", - }, - { - "iso2": "GW", - "name": "Guinea-Bissau", - "prefix": "245", - }, - { - "iso2": "GY", - "name": "Guyana", - "prefix": "592", - }, - { - "iso2": "HT", - "name": "Haiti", - "prefix": "509", - }, - { - "iso2": "HM", - "name": "Heard & McDonald Islands", - "prefix": "672", - }, - { - "iso2": "HN", - "name": "Honduras", - "prefix": "504", - }, - { - "iso2": "HK", - "name": "Hong Kong", - "prefix": "852", - }, - { - "iso2": "HU", - "name": "Hungary", - "prefix": "36", - }, - { - "iso2": "IS", - "name": "Iceland", - "prefix": "354", - }, - { - "iso2": "IN", - "name": "India", - "prefix": "91", - }, - { - "iso2": "ID", - "name": "Indonesia", - "prefix": "62", - }, - { - "iso2": "IR", - "name": "Iran", - "prefix": "98", - }, - { - "iso2": "IQ", - "name": "Iraq", - "prefix": "964", - }, - { - "iso2": "IE", - "name": "Ireland", - "prefix": "353", - }, - { - "iso2": "IM", - "name": "Isle of Man", - "prefix": "44", - }, - { - "iso2": "IL", - "name": "Israel", - "prefix": "972", - }, - { - "iso2": "IT", - "name": "Italy", - "prefix": "39", - }, - { - "iso2": "JM", - "name": "Jamaica", - "prefix": "1", - }, - { - "iso2": "JP", - "name": "Japan", - "prefix": "81", - }, - { - "iso2": "JE", - "name": "Jersey", - "prefix": "44", - }, - { - "iso2": "JO", - "name": "Jordan", - "prefix": "962", - }, - { - "iso2": "KZ", - "name": "Kazakhstan", - "prefix": "7", - }, - { - "iso2": "KE", - "name": "Kenya", - "prefix": "254", - }, - { - "iso2": "KI", - "name": "Kiribati", - "prefix": "686", - }, - { - "iso2": "KW", - "name": "Kuwait", - "prefix": "965", - }, - { - "iso2": "KG", - "name": "Kyrgyzstan", - "prefix": "996", - }, - { - "iso2": "LA", - "name": "Laos", - "prefix": "856", - }, - { - "iso2": "LV", - "name": "Latvia", - "prefix": "371", - }, - { - "iso2": "LB", - "name": "Lebanon", - "prefix": "961", - }, - { - "iso2": "LS", - "name": "Lesotho", - "prefix": "266", - }, - { - "iso2": "LR", - "name": "Liberia", - "prefix": "231", - }, - { - "iso2": "LY", - "name": "Libya", - "prefix": "218", - }, - { - "iso2": "LI", - "name": "Liechtenstein", - "prefix": "423", - }, - { - "iso2": "LT", - "name": "Lithuania", - "prefix": "370", - }, - { - "iso2": "LU", - "name": "Luxembourg", - "prefix": "352", - }, - { - "iso2": "MO", - "name": "Macau", - "prefix": "853", - }, - { - "iso2": "MK", - "name": "Macedonia", - "prefix": "389", - }, - { - "iso2": "MG", - "name": "Madagascar", - "prefix": "261", - }, - { - "iso2": "MW", - "name": "Malawi", - "prefix": "265", - }, - { - "iso2": "MY", - "name": "Malaysia", - "prefix": "60", - }, - { - "iso2": "MV", - "name": "Maldives", - "prefix": "960", - }, - { - "iso2": "ML", - "name": "Mali", - "prefix": "223", - }, - { - "iso2": "MT", - "name": "Malta", - "prefix": "356", - }, - { - "iso2": "MH", - "name": "Marshall Islands", - "prefix": "692", - }, - { - "iso2": "MQ", - "name": "Martinique", - "prefix": "596", - }, - { - "iso2": "MR", - "name": "Mauritania", - "prefix": "222", - }, - { - "iso2": "MU", - "name": "Mauritius", - "prefix": "230", - }, - { - "iso2": "YT", - "name": "Mayotte", - "prefix": "262", - }, - { - "iso2": "MX", - "name": "Mexico", - "prefix": "52", - }, - { - "iso2": "FM", - "name": "Micronesia", - "prefix": "691", - }, - { - "iso2": "MD", - "name": "Moldova", - "prefix": "373", - }, - { - "iso2": "MC", - "name": "Monaco", - "prefix": "377", - }, - { - "iso2": "MN", - "name": "Mongolia", - "prefix": "976", - }, - { - "iso2": "ME", - "name": "Montenegro", - "prefix": "382", - }, - { - "iso2": "MS", - "name": "Montserrat", - "prefix": "1", - }, - { - "iso2": "MA", - "name": "Morocco", - "prefix": "212", - }, - { - "iso2": "MZ", - "name": "Mozambique", - "prefix": "258", - }, - { - "iso2": "MM", - "name": "Myanmar", - "prefix": "95", - }, - { - "iso2": "NA", - "name": "Namibia", - "prefix": "264", - }, - { - "iso2": "NR", - "name": "Nauru", - "prefix": "674", - }, - { - "iso2": "NP", - "name": "Nepal", - "prefix": "977", - }, - { - "iso2": "NL", - "name": "Netherlands", - "prefix": "31", - }, - { - "iso2": "NC", - "name": "New Caledonia", - "prefix": "687", - }, - { - "iso2": "NZ", - "name": "New Zealand", - "prefix": "64", - }, - { - "iso2": "NI", - "name": "Nicaragua", - "prefix": "505", - }, - { - "iso2": "NE", - "name": "Niger", - "prefix": "227", - }, - { - "iso2": "NG", - "name": "Nigeria", - "prefix": "234", - }, - { - "iso2": "NU", - "name": "Niue", - "prefix": "683", - }, - { - "iso2": "NF", - "name": "Norfolk Island", - "prefix": "672", - }, - { - "iso2": "KP", - "name": "North Korea", - "prefix": "850", - }, - { - "iso2": "MP", - "name": "Northern Mariana Islands", - "prefix": "1", - }, - { - "iso2": "NO", - "name": "Norway", - "prefix": "47", - }, - { - "iso2": "OM", - "name": "Oman", - "prefix": "968", - }, - { - "iso2": "PK", - "name": "Pakistan", - "prefix": "92", - }, - { - "iso2": "PW", - "name": "Palau", - "prefix": "680", - }, - { - "iso2": "PS", - "name": "Palestine", - "prefix": "970", - }, - { - "iso2": "PA", - "name": "Panama", - "prefix": "507", - }, - { - "iso2": "PG", - "name": "Papua New Guinea", - "prefix": "675", - }, - { - "iso2": "PY", - "name": "Paraguay", - "prefix": "595", - }, - { - "iso2": "PE", - "name": "Peru", - "prefix": "51", - }, - { - "iso2": "PH", - "name": "Philippines", - "prefix": "63", - }, - { - "iso2": "PN", - "name": "Pitcairn Islands", - "prefix": "870", - }, - { - "iso2": "PL", - "name": "Poland", - "prefix": "48", - }, - { - "iso2": "PT", - "name": "Portugal", - "prefix": "351", - }, - { - "iso2": "PR", - "name": "Puerto Rico", - "prefix": "1", - }, - { - "iso2": "QA", - "name": "Qatar", - "prefix": "974", - }, - { - "iso2": "RO", - "name": "Romania", - "prefix": "40", - }, - { - "iso2": "RU", - "name": "Russia", - "prefix": "7", - }, - { - "iso2": "RW", - "name": "Rwanda", - "prefix": "250", - }, - { - "iso2": "RE", - "name": "R\u00e9union", - "prefix": "262", - }, - { - "iso2": "WS", - "name": "Samoa", - "prefix": "685", - }, - { - "iso2": "SM", - "name": "San Marino", - "prefix": "378", - }, - { - "iso2": "SA", - "name": "Saudi Arabia", - "prefix": "966", - }, - { - "iso2": "SN", - "name": "Senegal", - "prefix": "221", - }, - { - "iso2": "RS", - "name": "Serbia", - "prefix": "381 p", - }, - { - "iso2": "SC", - "name": "Seychelles", - "prefix": "248", - }, - { - "iso2": "SL", - "name": "Sierra Leone", - "prefix": "232", - }, - { - "iso2": "SG", - "name": "Singapore", - "prefix": "65", - }, - { - "iso2": "SX", - "name": "Sint Maarten", - "prefix": "1", - }, - { - "iso2": "SK", - "name": "Slovakia", - "prefix": "421", - }, - { - "iso2": "SI", - "name": "Slovenia", - "prefix": "386", - }, - { - "iso2": "SB", - "name": "Solomon Islands", - "prefix": "677", - }, - { - "iso2": "SO", - "name": "Somalia", - "prefix": "252", - }, - { - "iso2": "ZA", - "name": "South Africa", - "prefix": "27", - }, - { - "iso2": "GS", - "name": "South Georgia & South Sandwich Islands", - "prefix": "500", - }, - { - "iso2": "KR", - "name": "South Korea", - "prefix": "82", - }, - { - "iso2": "SS", - "name": "South Sudan", - "prefix": "211", - }, - { - "iso2": "ES", - "name": "Spain", - "prefix": "34", - }, - { - "iso2": "LK", - "name": "Sri Lanka", - "prefix": "94", - }, - { - "iso2": "BL", - "name": "St. Barth\u00e9lemy", - "prefix": "590", - }, - { - "iso2": "SH", - "name": "St. Helena", - "prefix": "290 n", - }, - { - "iso2": "KN", - "name": "St. Kitts & Nevis", - "prefix": "1", - }, - { - "iso2": "LC", - "name": "St. Lucia", - "prefix": "1", - }, - { - "iso2": "MF", - "name": "St. Martin", - "prefix": "590", - }, - { - "iso2": "PM", - "name": "St. Pierre & Miquelon", - "prefix": "508", - }, - { - "iso2": "VC", - "name": "St. Vincent & Grenadines", - "prefix": "1", - }, - { - "iso2": "SD", - "name": "Sudan", - "prefix": "249", - }, - { - "iso2": "SR", - "name": "Suriname", - "prefix": "597", - }, - { - "iso2": "SJ", - "name": "Svalbard & Jan Mayen", - "prefix": "47", - }, - { - "iso2": "SZ", - "name": "Swaziland", - "prefix": "268", - }, - { - "iso2": "SE", - "name": "Sweden", - "prefix": "46", - }, - { - "iso2": "CH", - "name": "Switzerland", - "prefix": "41", - }, - { - "iso2": "SY", - "name": "Syria", - "prefix": "963", - }, - { - "iso2": "ST", - "name": "S\u00e3o Tom\u00e9 & Pr\u00edncipe", - "prefix": "239", - }, - { - "iso2": "TW", - "name": "Taiwan", - "prefix": "886", - }, - { - "iso2": "TJ", - "name": "Tajikistan", - "prefix": "992", - }, - { - "iso2": "TZ", - "name": "Tanzania", - "prefix": "255", - }, - { - "iso2": "TH", - "name": "Thailand", - "prefix": "66", - }, - { - "iso2": "TL", - "name": "Timor-Leste", - "prefix": "670", - }, - { - "iso2": "TG", - "name": "Togo", - "prefix": "228", - }, - { - "iso2": "TK", - "name": "Tokelau", - "prefix": "690", - }, - { - "iso2": "TO", - "name": "Tonga", - "prefix": "676", - }, - { - "iso2": "TT", - "name": "Trinidad & Tobago", - "prefix": "1", - }, - { - "iso2": "TN", - "name": "Tunisia", - "prefix": "216", - }, - { - "iso2": "TR", - "name": "Turkey", - "prefix": "90", - }, - { - "iso2": "TM", - "name": "Turkmenistan", - "prefix": "993", - }, - { - "iso2": "TC", - "name": "Turks & Caicos Islands", - "prefix": "1", - }, - { - "iso2": "TV", - "name": "Tuvalu", - "prefix": "688", - }, - { - "iso2": "VI", - "name": "U.S. Virgin Islands", - "prefix": "1", - }, - { - "iso2": "UG", - "name": "Uganda", - "prefix": "256", - }, - { - "iso2": "UA", - "name": "Ukraine", - "prefix": "380", - }, - { - "iso2": "AE", - "name": "United Arab Emirates", - "prefix": "971", - }, - { - "iso2": "UY", - "name": "Uruguay", - "prefix": "598", - }, - { - "iso2": "UZ", - "name": "Uzbekistan", - "prefix": "998", - }, - { - "iso2": "VU", - "name": "Vanuatu", - "prefix": "678", - }, - { - "iso2": "VA", - "name": "Vatican City", - "prefix": "39", - }, - { - "iso2": "VE", - "name": "Venezuela", - "prefix": "58", - }, - { - "iso2": "VN", - "name": "Vietnam", - "prefix": "84", - }, - { - "iso2": "WF", - "name": "Wallis & Futuna", - "prefix": "681", - }, - { - "iso2": "EH", - "name": "Western Sahara", - "prefix": "212", - }, - { - "iso2": "YE", - "name": "Yemen", - "prefix": "967", - }, - { - "iso2": "ZM", - "name": "Zambia", - "prefix": "260", - }, - { - "iso2": "ZW", - "name": "Zimbabwe", - "prefix": "263", - }, -]; From b7d5d2fd56d67410b03ed371d157375c778a9387 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson <matthew@matrix.org> 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 ( <div onKeyDown={this._onKeyDown} className={this.props.className}> <AccessibleButton onClick={this._onCancelClick} className="mx_Dialog_cancelButton" > - <img - src="img/cancel.svg" width="18" height="18" - alt="Cancel" title="Cancel" - /> + <TintableSvg src="img/icons-close-button.svg" width="35" height="35" /> </AccessibleButton> <div className='mx_Dialog_title'> { this.props.title } From e5a5b5cd08e6acb7449ac44cca1472752f6aa5ea Mon Sep 17 00:00:00 2001 From: Matthew Hodgson <matthew@matrix.org> 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 <matthew@matrix.org> 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 <matthew@matrix.org> 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 <matthew@matrix.org> 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 <matthew@matrix.org> 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 ( - <BaseDialog className='mx_CreateOrReuseChatDialog' + <BaseDialog className='mx_ChatCreateOrReuseChatDialog' onFinished={() => { 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 <matthew@matrix.org> 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 ( - <BaseDialog className='mx_ChatCreateOrReuseChatDialog' + <BaseDialog className='mx_ChatCreateOrReuseDialog' onFinished={() => { 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 <matthew@matrix.org> 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} + <div className="mx_Dialog_content"> + You already have existing direct chats with this user: + <div className="mx_ChatCreateOrReuseDialog_tiles"> + {tiles} + {startNewChat} + </div> + </div> </BaseDialog> ); } From 925bbb79ad370d7ccf9c18852a85fc4080779706 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson <matthew@matrix.org> 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({ > <div className="mx_Dialog_content"> <div className="mx_ConfirmUserActionDialog_avatar"> - <MemberAvatar member={this.props.member} width={72} height={72} /> + <MemberAvatar member={this.props.member} width={48} height={48} /> </div> <div className="mx_ConfirmUserActionDialog_name">{this.props.member.name}</div> <div className="mx_ConfirmUserActionDialog_userId">{this.props.member.userId}</div>