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>