diff --git a/CHANGELOG.md b/CHANGELOG.md
index 262d55c6da..70f946d7cc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,43 @@
+Changes in [0.6.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.6.3) (2016-06-03)
+===================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.6.2...v0.6.3)
+
+ * Change invite text field wording
+ * Fix bug with new email invite UX where the invite could get wedged
+ * Label app versions sensibly in UserSettings
+
+Changes in [0.6.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.6.2) (2016-06-02)
+===================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.6.1...v0.6.2)
+
+ * Correctly bump dep on matrix-js-sdk 0.5.4
+
+Changes in [0.6.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.6.1) (2016-06-02)
+===================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.6.0...v0.6.1)
+
+ * Fix focusing race in new UX for 3pid invites
+ * Fix jenkins.sh
+
+Changes in [0.6.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.6.0) (2016-06-02)
+===================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.5.2...v0.6.0)
+
+ * implement new UX for 3pid invites
+   [\#297](https://github.com/matrix-org/matrix-react-sdk/pull/297)
+ * multiple URL preview support
+   [\#290](https://github.com/matrix-org/matrix-react-sdk/pull/290)
+ * Add a fallback home server to log into
+   [\#293](https://github.com/matrix-org/matrix-react-sdk/pull/293)
+ * Hopefully fix memory leak with velocity
+   [\#291](https://github.com/matrix-org/matrix-react-sdk/pull/291)
+ * Support for enabling email notifications
+   [\#289](https://github.com/matrix-org/matrix-react-sdk/pull/289)
+ * Correct Readme instructions how to customize the UI
+   [\#286](https://github.com/matrix-org/matrix-react-sdk/pull/286)
+ * Avoid rerendering during Room unmount
+   [\#285](https://github.com/matrix-org/matrix-react-sdk/pull/285)
+
 Changes in [0.5.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.5.2) (2016-04-22)
 ===================================================================================================
 [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.5.1...v0.5.2)
diff --git a/jenkins.sh b/jenkins.sh
index 51fab5d020..eeb7d7d56e 100755
--- a/jenkins.sh
+++ b/jenkins.sh
@@ -8,9 +8,6 @@ nvm use 4
 
 set -x
 
-# install the version of js-sdk provided to us by jenkins
-npm install ./node_modules/matrix-js-sdk-*.tgz
-
 # install the other dependencies
 npm install
 
diff --git a/package.json b/package.json
index d46e9b2621..5c9a67c734 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "matrix-react-sdk",
-  "version": "0.5.2",
+  "version": "0.6.3",
   "description": "SDK for matrix.org using React",
   "author": "matrix.org",
   "repository": {
@@ -31,15 +31,14 @@
     "highlight.js": "^8.9.1",
     "linkifyjs": "^2.0.0-beta.4",
     "marked": "^0.3.5",
-    "matrix-js-sdk": "^0.5.2",
+    "matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
     "optimist": "^0.6.1",
     "q": "^1.4.1",
     "react": "^15.0.1",
     "react-dom": "^15.0.1",
     "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#c3d942e",
     "sanitize-html": "^1.11.1",
-    "velocity-animate": "^1.2.3",
-    "velocity-ui-pack": "^1.2.2"
+    "velocity-vector": "vector-im/velocity#059e3b2"
   },
   "//babelversion": [
     "brief experiments with babel6 seems to show that it generates source ",
diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js
index cf7131eb7b..9bb1388e76 100644
--- a/src/UserSettingsStore.js
+++ b/src/UserSettingsStore.js
@@ -100,7 +100,7 @@ module.exports = {
         return this.getEmailPusher(pushers, address) !== undefined;
     },
 
-    addEmailPusher: function(address) {
+    addEmailPusher: function(address, data) {
         return MatrixClientPeg.get().setPusher({
             kind: 'email',
             app_id: "m.email",
@@ -108,7 +108,7 @@ module.exports = {
             app_display_name: 'Email Notifications',
             device_display_name: address,
             lang: navigator.language,
-            data: {},
+            data: data,
             append: true,  // We always append for email pushers since we don't want to stop other accounts notifying to the same email address
         });
     },
diff --git a/src/Velociraptor.js b/src/Velociraptor.js
index 0abf34b230..f45925867f 100644
--- a/src/Velociraptor.js
+++ b/src/Velociraptor.js
@@ -1,6 +1,6 @@
 var React = require('react');
 var ReactDom = require('react-dom');
-var Velocity = require('velocity-animate');
+var Velocity = require('velocity-vector');
 
 /**
  * The Velociraptor contains components and animates transitions with velocity.
@@ -117,7 +117,8 @@ module.exports = React.createClass({
             // and the FAQ entry, "Preventing memory leaks when
             // creating/destroying large numbers of elements"
             // (https://github.com/julianshapiro/velocity/issues/47)
-            Velocity.Utilities.removeData(this.nodes[k]);
+            var domNode = ReactDom.findDOMNode(this.nodes[k]);
+            Velocity.Utilities.removeData(domNode);
         }
         this.nodes[k] = node;
     },
diff --git a/src/VelocityBounce.js b/src/VelocityBounce.js
index c85aa254fa..168b0b14af 100644
--- a/src/VelocityBounce.js
+++ b/src/VelocityBounce.js
@@ -1,4 +1,4 @@
-var Velocity = require('velocity-animate');
+var Velocity = require('velocity-vector');
 
 // courtesy of https://github.com/julianshapiro/velocity/issues/283
 // We only use easeOutBounce (easeInBounce is just sort of nonsensical)
diff --git a/src/component-index.js b/src/component-index.js
index 967cc5d685..3570523bde 100644
--- a/src/component-index.js
+++ b/src/component-index.js
@@ -79,6 +79,7 @@ module.exports.components['views.rooms.EntityTile'] = require('./components/view
 module.exports.components['views.rooms.EventTile'] = require('./components/views/rooms/EventTile');
 module.exports.components['views.rooms.InviteMemberList'] = require('./components/views/rooms/InviteMemberList');
 module.exports.components['views.rooms.LinkPreviewWidget'] = require('./components/views/rooms/LinkPreviewWidget');
+module.exports.components['views.rooms.MemberDeviceInfo'] = require('./components/views/rooms/MemberDeviceInfo');
 module.exports.components['views.rooms.MemberInfo'] = require('./components/views/rooms/MemberInfo');
 module.exports.components['views.rooms.MemberList'] = require('./components/views/rooms/MemberList');
 module.exports.components['views.rooms.MemberTile'] = require('./components/views/rooms/MemberTile');
diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js
index 2f7a6ed8ec..b70c89e2d8 100644
--- a/src/components/structures/MatrixChat.js
+++ b/src/components/structures/MatrixChat.js
@@ -37,11 +37,13 @@ var MatrixTools = require('../../MatrixTools');
 var linkifyMatrix = require("../../linkify-matrix");
 var KeyCode = require('../../KeyCode');
 
+var createRoom = require("../../createRoom");
+
 module.exports = React.createClass({
     displayName: 'MatrixChat',
 
     propTypes: {
-        config: React.PropTypes.object.isRequired,
+        config: React.PropTypes.object,
         ConferenceHandler: React.PropTypes.any,
         onNewScreen: React.PropTypes.func,
         registrationUrl: React.PropTypes.string,
@@ -84,7 +86,8 @@ module.exports = React.createClass({
 
     getDefaultProps: function() {
         return {
-            startingQueryParams: {}
+            startingQueryParams: {},
+            config: {},
         };
     },
 
@@ -97,10 +100,9 @@ module.exports = React.createClass({
         else if (window.localStorage && window.localStorage.getItem("mx_hs_url")) {
             return window.localStorage.getItem("mx_hs_url");
         }
-        else if (this.props.config) {
-            return this.props.config.default_hs_url
+        else {
+            return this.props.config.default_hs_url || "https://matrix.org";
         }
-        return "https://matrix.org";
     },
 
     getFallbackHsUrl: function() {
@@ -116,10 +118,9 @@ module.exports = React.createClass({
         else if (window.localStorage && window.localStorage.getItem("mx_is_url")) {
             return window.localStorage.getItem("mx_is_url");
         }
-        else if (this.props.config) {
-            return this.props.config.default_is_url
+        else {
+            return this.props.config.default_is_url || "https://vector.im"
         }
-        return "https://matrix.org";
     },
 
     componentWillMount: function() {
@@ -391,6 +392,10 @@ module.exports = React.createClass({
                 });
                 break;
             case 'view_room':
+                // Takes both room ID and room alias: if switching to a room the client is already
+                // know to be in (eg. user clicks on a room in the recents panel), supply only the
+                // ID. If the user is clicking on a room in the context of the alias being presented
+                // to them, supply the room alias and optionally the room ID.
                 this._viewRoom(
                     payload.room_id, payload.room_alias, payload.show_settings, payload.event_id,
                     payload.third_party_invite, payload.oob_data
@@ -422,42 +427,6 @@ module.exports = React.createClass({
                     this._viewRoom(allRooms[roomIndex].roomId);
                 }
                 break;
-            case 'view_room_alias':
-                if (!this.state.logged_in) {
-                    this.starting_room_alias_payload = payload;
-                    // Login is the default screen, so we'd do this anyway,
-                    // but this will set the URL bar appropriately.
-                    dis.dispatch({ action: 'start_login' });
-                    return;
-                }
-
-                var foundRoom = MatrixTools.getRoomForAlias(
-                    MatrixClientPeg.get().getRooms(), payload.room_alias
-                );
-                if (foundRoom) {
-                    dis.dispatch({
-                        action: 'view_room',
-                        room_id: foundRoom.roomId,
-                        room_alias: payload.room_alias,
-                        event_id: payload.event_id,
-                        third_party_invite: payload.third_party_invite,
-                        oob_data: payload.oob_data,
-                    });
-                    return;
-                }
-                // resolve the alias and *then* view it
-                MatrixClientPeg.get().getRoomIdForAlias(payload.room_alias).done(
-                function(result) {
-                    dis.dispatch({
-                        action: 'view_room',
-                        room_id: result.room_id,
-                        room_alias: payload.room_alias,
-                        event_id: payload.event_id,
-                        third_party_invite: payload.third_party_invite,
-                        oob_data: payload.oob_data,
-                    });
-                });
-                break;
             case 'view_user_settings':
                 this._setPage(this.PageTypes.UserSettings);
                 this.notifyNewScreen('settings');
@@ -466,48 +435,7 @@ module.exports = React.createClass({
                 //this._setPage(this.PageTypes.CreateRoom);
                 //this.notifyNewScreen('new');
 
-                var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
-                var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
-                var Loader = sdk.getComponent("elements.Spinner");
-                var modal = Modal.createDialog(Loader);
-
-                if (MatrixClientPeg.get().isGuest()) {
-                    Modal.createDialog(NeedToRegisterDialog, {
-                        title: "Please Register",
-                        description: "Guest users can't create new rooms. Please register to create room and start a chat."
-                    });
-                    return;
-                }
-
-                // XXX: FIXME: deduplicate this with MemberInfo's 'start chat' impl
-                MatrixClientPeg.get().createRoom({
-                    preset: "private_chat",
-                    // Allow guests by default since the room is private and they'd
-                    // need an invite. This means clicking on a 3pid invite email can
-                    // actually drop you right in to a chat.
-                    initial_state: [
-                        {
-                            content: {
-                                guest_access: 'can_join'
-                            },
-                            type: 'm.room.guest_access',
-                            state_key: '',
-                        }
-                    ],
-                }).done(function(res) {
-                    modal.close();
-                    dis.dispatch({
-                        action: 'view_room',
-                        room_id: res.room_id,
-                        // show_settings: true,
-                    });
-                }, function(err) {
-                    modal.close();
-                    Modal.createDialog(ErrorDialog, {
-                        title: "Failed to create room",
-                        description: err.toString()
-                    });
-                });
+                createRoom().done();
                 break;
             case 'view_room_directory':
                 this._setPage(this.PageTypes.RoomDirectory);
@@ -572,8 +500,6 @@ module.exports = React.createClass({
         this.focusComposer = true;
 
         var newState = {
-            currentRoom: roomId,
-            currentRoomAlias: roomAlias,
             initialEventId: eventId,
             highlightedEventId: eventId,
             initialEventPixelOffset: undefined,
@@ -582,6 +508,18 @@ module.exports = React.createClass({
             roomOobData: oob_data,
         };
 
+        // If an alias has been provided, we use that and only that,
+        // since otherwise we'll prefer to pass in an ID to RoomView
+        // but if we're not in the room, we should join by alias rather
+        // than ID.
+        if (roomAlias) {
+            newState.currentRoomAlias = roomAlias;
+            newState.currentRoom = null;
+        } else {
+            newState.currentRoomAlias = null;
+            newState.currentRoom = roomId;
+        }
+
         // if we aren't given an explicit event id, look for one in the
         // scrollStateMap.
         if (!eventId) {
@@ -858,22 +796,28 @@ module.exports = React.createClass({
                 inviterName: params.inviter_name,
             };
 
+            var payload = {
+                action: 'view_room',
+                event_id: eventId,
+                third_party_invite: third_party_invite,
+                oob_data: oob_data,
+            };
             if (roomString[0] == '#') {
-                dis.dispatch({
-                    action: 'view_room_alias',
-                    room_alias: roomString,
-                    event_id: eventId,
-                    third_party_invite: third_party_invite,
-                    oob_data: oob_data,
-                });
+                payload.room_alias = roomString;
             } else {
-                dis.dispatch({
-                    action: 'view_room',
-                    room_id: roomString,
-                    event_id: eventId,
-                    third_party_invite: third_party_invite,
-                    oob_data: oob_data,
-                });
+                payload.room_id = roomString;
+            }
+
+            // we can't view a room unless we're logged in
+            // (a guest account is fine)
+            if (!this.state.logged_in) {
+                this.starting_room_alias_payload = payload;
+                // Login is the default screen, so we'd do this anyway,
+                // but this will set the URL bar appropriately.
+                dis.dispatch({ action: 'start_login' });
+                return;
+            } else {
+                dis.dispatch(payload);
             }
         }
         else {
@@ -889,7 +833,7 @@ module.exports = React.createClass({
 
     onAliasClick: function(event, alias) {
         event.preventDefault();
-        dis.dispatch({action: 'view_room_alias', room_alias: alias});
+        dis.dispatch({action: 'view_room', room_alias: alias});
     },
 
     onUserClick: function(event, userId) {
@@ -1084,14 +1028,14 @@ module.exports = React.createClass({
                             oobData={this.state.roomOobData}
                             highlightedEventId={this.state.highlightedEventId}
                             eventPixelOffset={this.state.initialEventPixelOffset}
-                            key={this.state.currentRoom}
+                            key={this.state.currentRoom || this.state.currentRoomAlias}
                             opacity={this.state.middleOpacity}
                             ConferenceHandler={this.props.ConferenceHandler} />
                     );
                     right_panel = <RightPanel roomId={this.state.currentRoom} collapsed={this.state.collapse_rhs} opacity={this.state.sideOpacity} />
                     break;
                 case this.PageTypes.UserSettings:
-                    page_element = <UserSettings onClose={this.onUserSettingsClose} version={this.state.version} />
+                    page_element = <UserSettings onClose={this.onUserSettingsClose} version={this.state.version} brand={this.props.config.brand} />
                     right_panel = <RightPanel collapsed={this.state.collapse_rhs} opacity={this.state.sideOpacity}/>
                     break;
                 case this.PageTypes.CreateRoom:
@@ -1159,6 +1103,7 @@ module.exports = React.createClass({
                     guestAccessToken={this.state.guestAccessToken}
                     defaultHsUrl={this.props.config.default_hs_url}
                     defaultIsUrl={this.props.config.default_is_url}
+                    brand={this.props.config.brand}
                     customHsUrl={this.getCurrentHsUrl()}
                     customIsUrl={this.getCurrentIsUrl()}
                     registrationUrl={this.props.registrationUrl}
diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index 16b4892bc0..c8e878118b 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -86,6 +86,10 @@ module.exports = React.createClass({
         // to manage its animations
         this._readReceiptMap = {};
 
+        // Remember the read marker ghost node so we can do the cleanup that
+        // Velocity requires
+        this._readMarkerGhostNode = null;
+
         this._isMounted = true;
     },
 
@@ -422,9 +426,16 @@ module.exports = React.createClass({
     },
 
     _startAnimation: function(ghostNode) {
-        Velocity(ghostNode, {opacity: '0', width: '10%'},
-                 {duration: 400, easing: 'easeInSine',
-                  delay: 1000});
+        if (this._readMarkerGhostNode) {
+            Velocity.Utilities.removeData(this._readMarkerGhostNode);
+        }
+        this._readMarkerGhostNode = ghostNode;
+
+        if (ghostNode) {
+            Velocity(ghostNode, {opacity: '0', width: '10%'},
+                     {duration: 400, easing: 'easeInSine',
+                      delay: 1000});
+        }
     },
 
     _getReadMarkerGhostTile: function() {
diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js
index 33bbb510e3..9fc335236c 100644
--- a/src/components/structures/RoomView.js
+++ b/src/components/structures/RoomView.js
@@ -39,6 +39,7 @@ var dis = require("../../dispatcher");
 var Tinter = require("../../Tinter");
 var rate_limited_func = require('../../ratelimitedfunc');
 var ObjectUtils = require('../../ObjectUtils');
+var MatrixTools = require('../../MatrixTools');
 
 var DEBUG = false;
 
@@ -55,13 +56,6 @@ module.exports = React.createClass({
         ConferenceHandler: React.PropTypes.any,
 
         // the ID for this room (or, if we don't know it, an alias for it)
-        //
-        // XXX: if this is an alias, we will display a 'join' dialogue,
-        // regardless of whether we are already a member, or if the room is
-        // peekable. Currently there is a big mess, where at least four
-        // different components (RoomView, MatrixChat, RoomDirectory,
-        // SlashCommands) have logic for turning aliases into rooms, and each
-        // of them do it differently and have different edge cases.
         roomAddress: React.PropTypes.string.isRequired,
 
         // An object representing a third party invite to join this room
@@ -100,7 +94,14 @@ module.exports = React.createClass({
     },
 
     getInitialState: function() {
-        var room = MatrixClientPeg.get().getRoom(this.props.roomAddress);
+        var room;
+        if (this.props.roomAddress[0] == '!') {
+            room = MatrixClientPeg.get().getRoom(this.props.roomAddress);
+        } else {
+            room = MatrixTools.getRoomForAlias(
+                MatrixClientPeg.get().getRooms(), this.props.roomAddress
+            );
+        }
         return {
             room: room,
             roomLoading: !room,
@@ -677,6 +678,16 @@ module.exports = React.createClass({
 
     uploadFile: function(file) {
         var self = this;
+
+        if (MatrixClientPeg.get().isGuest()) {
+            var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
+            Modal.createDialog(NeedToRegisterDialog, {
+                title: "Please Register",
+                description: "Guest users can't upload files. Please register to upload."
+            });
+            return;
+        }
+
         ContentMessages.sendContentToRoom(
             file, this.state.room.roomId, MatrixClientPeg.get()
         ).done(undefined, function(error) {
diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js
index 0be6271ea4..635f9c5413 100644
--- a/src/components/structures/UserSettings.js
+++ b/src/components/structures/UserSettings.js
@@ -31,7 +31,9 @@ module.exports = React.createClass({
 
     propTypes: {
         version: React.PropTypes.string,
-        onClose: React.PropTypes.func
+        onClose: React.PropTypes.func,
+        // The brand string given when creating email pushers
+        brand: React.PropTypes.string,
     },
 
     getDefaultProps: function() {
@@ -244,6 +246,23 @@ module.exports = React.createClass({
         });
     },
 
+    _renderDeviceInfo: function() {
+        var client = MatrixClientPeg.get();
+        var deviceId = client.deviceId;
+        var olmKey = client.getDeviceEd25519Key() || "<not supported>";
+        return (
+            <div>
+                <h3>Cryptography</h3>
+                <div className="mx_UserSettings_section">
+                    <ul>
+                        <li>Device ID: {deviceId}</li>
+                        <li>Device key: {olmKey}</li>
+                    </ul>
+                </div>
+            </div>
+        );
+    },
+
     render: function() {
         var self = this;
         var Loader = sdk.getComponent("elements.Spinner");
@@ -299,7 +318,7 @@ module.exports = React.createClass({
                             onValueChanged={ this.onAddThreepidClicked } />
                     </div>
                     <div className="mx_UserSettings_addThreepid">
-                         <img src="img/plus.svg" width="14" height="14" alt="Add" onClick={ this.onAddThreepidClicked }/>
+                         <img src="img/plus.svg" width="14" height="14" alt="Add" onClick={ this.onAddThreepidClicked.bind(this, undefined, true) }/>
                     </div>
                 </div>
             );
@@ -333,7 +352,7 @@ module.exports = React.createClass({
                 <h3>Notifications</h3>
 
                 <div className="mx_UserSettings_section">
-                    <Notifications threepids={this.state.threepids} />
+                    <Notifications threepids={this.state.threepids} brand={this.props.brand} />
                 </div>
             </div>);
         }
@@ -390,6 +409,8 @@ module.exports = React.createClass({
 
                 {notification_area}
 
+                {this._renderDeviceInfo()}
+
                 <h3>Advanced</h3>
 
                 <div className="mx_UserSettings_section">
@@ -403,9 +424,8 @@ module.exports = React.createClass({
                         Identity Server is { MatrixClientPeg.get().getIdentityServerUrl() }
                     </div>
                     <div className="mx_UserSettings_advanced">
-                        Version {this.state.clientVersion}
-                        <br />
-                        {this.props.version}
+                        matrix-react-sdk version: {this.state.clientVersion}<br/>
+                        vector-web version: {this.props.version}<br/>
                     </div>
                 </div>
 
diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js
index d852991b9c..2f15a3b5df 100644
--- a/src/components/structures/login/Registration.js
+++ b/src/components/structures/login/Registration.js
@@ -22,6 +22,7 @@ var sdk = require('../../../index');
 var dis = require('../../../dispatcher');
 var Signup = require("../../../Signup");
 var ServerConfig = require("../../views/login/ServerConfig");
+var MatrixClientPeg = require("../../../MatrixClientPeg");
 var RegistrationForm = require("../../views/login/RegistrationForm");
 var CaptchaForm = require("../../views/login/CaptchaForm");
 
@@ -40,6 +41,7 @@ module.exports = React.createClass({
         customIsUrl: React.PropTypes.string,
         defaultHsUrl: React.PropTypes.string,
         defaultIsUrl: React.PropTypes.string,
+        brand: React.PropTypes.string,
         email: React.PropTypes.string,
         username: React.PropTypes.string,
         guestAccessToken: React.PropTypes.string,
@@ -145,6 +147,26 @@ module.exports = React.createClass({
                 identityServerUrl: self.registerLogic.getIdentityServerUrl(),
                 accessToken: response.access_token
             });
+
+            if (self.props.brand) {
+                MatrixClientPeg.get().getPushers().done((resp)=>{
+                    var pushers = resp.pushers;
+                    for (var i = 0; i < pushers.length; ++i) {
+                        if (pushers[i].kind == 'email') {
+                            var emailPusher = pushers[i];
+                            emailPusher.data = { brand: self.props.brand };
+                            MatrixClientPeg.get().setPusher(emailPusher).done(() => {
+                                console.log("Set email branding to " + self.props.brand);
+                            }, (error) => {
+                                console.error("Couldn't set email branding: " + error);
+                            });
+                        }
+                    }
+                }, (error) => {
+                    console.error("Couldn't get pushers: " + error);
+                });
+            }
+
         }, function(err) {
             if (err.message) {
                 self.setState({
diff --git a/src/components/views/dialogs/TextInputDialog.js b/src/components/views/dialogs/TextInputDialog.js
index d81ae98718..fed7ff079a 100644
--- a/src/components/views/dialogs/TextInputDialog.js
+++ b/src/components/views/dialogs/TextInputDialog.js
@@ -39,11 +39,11 @@ module.exports = React.createClass({
             focus: true
         };
     },
-    
+
     componentDidMount: function() {
         if (this.props.focus) {
-            // Set the cursor at the end of the text input 
-            this.refs.textinput.value = this.props.value;        
+            // Set the cursor at the end of the text input
+            this.refs.textinput.value = this.props.value;
         }
     },
 
@@ -83,13 +83,12 @@ module.exports = React.createClass({
                     </div>
                 </div>
                 <div className="mx_Dialog_buttons">
-                    <button onClick={this.onOk}>
-                        {this.props.button}
-                    </button>
-
                     <button onClick={this.onCancel}>
                         Cancel
                     </button>
+                    <button onClick={this.onOk}>
+                        {this.props.button}
+                    </button>
                 </div>
             </div>
         );
diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js
index 83bd1ab17c..a172d77bb4 100644
--- a/src/components/views/login/RegistrationForm.js
+++ b/src/components/views/login/RegistrationForm.js
@@ -17,8 +17,8 @@ limitations under the License.
 'use strict';
 
 var React = require('react');
-var Velocity = require('velocity-animate');
-require('velocity-ui-pack');
+var Velocity = require('velocity-vector');
+require('velocity-vector/velocity.ui');
 var sdk = require('../../../index');
 var Email = require('../../../email');
 var Modal = require("../../../Modal");
diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js
index a72608d329..310da598fa 100644
--- a/src/components/views/messages/TextualBody.js
+++ b/src/components/views/messages/TextualBody.js
@@ -45,9 +45,9 @@ module.exports = React.createClass({
 
     getInitialState: function() {
         return {
-            // the URL (if any) to be previewed with a LinkPreviewWidget
+            // the URLs (if any) to be previewed with a LinkPreviewWidget
             // inside this TextualBody.
-            link: null,
+            links: [],
 
             // track whether the preview widget is hidden
             widgetHidden: false,
@@ -57,9 +57,11 @@ module.exports = React.createClass({
     componentDidMount: function() {
         linkifyElement(this.refs.content, linkifyMatrix.options);
 
-        var link = this.findLink(this.refs.content.children);
-        if (link) {
-            this.setState({ link: link.getAttribute("href") });
+        var links = this.findLinks(this.refs.content.children);
+        if (links.length) {
+            this.setState({ links: links.map((link)=>{
+                return link.getAttribute("href");
+            })});
 
             // lazy-load the hidden state of the preview widget from localstorage
             if (global.localStorage) {
@@ -74,27 +76,32 @@ module.exports = React.createClass({
 
     shouldComponentUpdate: function(nextProps, nextState) {
         // exploit that events are immutable :)
+        // ...and that .links is only ever set in componentDidMount and never changes
         return (nextProps.mxEvent.getId() !== this.props.mxEvent.getId() ||
                 nextProps.highlights !== this.props.highlights ||
                 nextProps.highlightLink !== this.props.highlightLink ||
-                nextState.link !== this.state.link ||
+                nextState.links !== this.state.links ||
                 nextState.widgetHidden !== this.state.widgetHidden);
     },
 
-    findLink: function(nodes) {
+    findLinks: function(nodes) {
+        var links = [];
         for (var i = 0; i < nodes.length; i++) {
             var node = nodes[i];
             if (node.tagName === "A" && node.getAttribute("href"))
             {
-                return this.isLinkPreviewable(node) ? node : undefined;
+                if (this.isLinkPreviewable(node)) {
+                    links.push(node);
+                }
             }
             else if (node.tagName === "PRE" || node.tagName === "CODE") {
-                return;
+                continue;
             }
             else if (node.children && node.children.length) {
-                return this.findLink(node.children)
+                links = links.concat(this.findLinks(node.children));
             }
         }
+        return links;
     },
 
     isLinkPreviewable: function(node) {
@@ -160,14 +167,17 @@ module.exports = React.createClass({
                                        {highlightLink: this.props.highlightLink});
 
 
-        var widget;
-        if (this.state.link && !this.state.widgetHidden) {
+        var widgets;
+        if (this.state.links.length && !this.state.widgetHidden) {
             var LinkPreviewWidget = sdk.getComponent('rooms.LinkPreviewWidget');
-            widget = <LinkPreviewWidget
-                link={ this.state.link }
-                mxEvent={ this.props.mxEvent }
-                onCancelClick={ this.onCancelClick }
-                onWidgetLoad={ this.props.onWidgetLoad }/>;
+            widgets = this.state.links.map((link)=>{
+                return <LinkPreviewWidget
+                            key={ link }
+                            link={ link }
+                            mxEvent={ this.props.mxEvent }
+                            onCancelClick={ this.onCancelClick }
+                            onWidgetLoad={ this.props.onWidgetLoad }/>;
+            });
         }
 
         switch (content.msgtype) {
@@ -176,21 +186,21 @@ module.exports = React.createClass({
                 return (
                     <span ref="content" className="mx_MEmoteBody mx_EventTile_content">
                         * { name } { body }
-                        { widget }
+                        { widgets }
                     </span>
                 );
             case "m.notice":
                 return (
                     <span ref="content" className="mx_MNoticeBody mx_EventTile_content">
                         { body }
-                        { widget }
+                        { widgets }
                     </span>
                 );
             default: // including "m.text"
                 return (
                     <span ref="content" className="mx_MTextBody mx_EventTile_content">
                         { body }
-                        { widget }
+                        { widgets }
                     </span>
                 );
         }
diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js
index 7db8af9312..ff02139d87 100644
--- a/src/components/views/rooms/EventTile.js
+++ b/src/components/views/rooms/EventTile.js
@@ -128,16 +128,24 @@ module.exports = React.createClass({
     },
 
     getInitialState: function() {
-        return {menu: false, allReadAvatars: false};
+        return {menu: false, allReadAvatars: false, verified: null};
     },
 
     componentWillMount: function() {
         // don't do RR animations until we are mounted
         this._suppressReadReceiptAnimation = true;
+        this._verifyEvent(this.props.mxEvent);
     },
 
     componentDidMount: function() {
         this._suppressReadReceiptAnimation = false;
+        MatrixClientPeg.get().on("deviceVerified", this.onDeviceVerified);
+    },
+
+    componentWillReceiveProps: function (nextProps) {
+        if (nextProps.mxEvent !== this.props.mxEvent) {
+            this._verifyEvent(nextProps.mxEvent);
+        }
     },
 
     shouldComponentUpdate: function (nextProps, nextState) {
@@ -152,6 +160,31 @@ module.exports = React.createClass({
         return false;
     },
 
+    componentWillUnmount: function() {
+        var client = MatrixClientPeg.get();
+        if (client) {
+            client.removeListener("deviceVerified", this.onDeviceVerified);
+        }
+    },
+
+    onDeviceVerified: function(userId, device) {
+        if (userId == this.props.mxEvent.getSender()) {
+            this._verifyEvent(this.props.mxEvent);
+        }
+    },
+
+    _verifyEvent: function(mxEvent) {
+        var verified = null;
+
+        if (mxEvent.isEncrypted()) {
+            verified = MatrixClientPeg.get().isEventSenderVerified(mxEvent);
+        }
+
+        this.setState({
+            verified: verified
+        });
+    },
+
     _propsEqual: function(objA, objB) {
         var keysA = Object.keys(objA);
         var keysB = Object.keys(objB);
@@ -346,6 +379,8 @@ module.exports = React.createClass({
             mx_EventTile_last: this.props.last,
             mx_EventTile_contextual: this.props.contextual,
             menu: this.state.menu,
+            mx_EventTile_verified: this.state.verified == true,
+            mx_EventTile_unverified: this.state.verified == false,
         });
         var timestamp = <a href={ "#/room/" + this.props.mxEvent.getRoomId() +"/"+ this.props.mxEvent.getId() }>
                             <MessageTimestamp ts={this.props.mxEvent.getTs()} />
diff --git a/src/components/views/rooms/InviteMemberList.js b/src/components/views/rooms/InviteMemberList.js
index 480066771b..5246e2e54d 100644
--- a/src/components/views/rooms/InviteMemberList.js
+++ b/src/components/views/rooms/InviteMemberList.js
@@ -26,6 +26,7 @@ module.exports = React.createClass({
     propTypes: {
         roomId: React.PropTypes.string.isRequired,
         onInvite: React.PropTypes.func.isRequired, // fn(inputText)
+        onThirdPartyInvite: React.PropTypes.func.isRequired, // fn(inputText)
         onSearchQueryChanged: React.PropTypes.func // fn(inputText)
     },
 
@@ -49,10 +50,19 @@ module.exports = React.createClass({
         }
     },
 
+    componentDidMount: function() {
+        // initialise the email tile
+        this.onSearchQueryChanged('');
+    },
+
     onInvite: function(ev) {
         this.props.onInvite(this._input);
     },
 
+    onThirdPartyInvite: function(ev) {
+        this.props.onThirdPartyInvite(this._input);
+    },
+
     onSearchQueryChanged: function(input) {
         this._input = input;
         var EntityTile = sdk.getComponent("rooms.EntityTile");
@@ -68,9 +78,10 @@ module.exports = React.createClass({
 
         this._emailEntity = new Entities.newEntity(
             <EntityTile key="dynamic_invite_tile" suppressOnHover={true} showInviteButton={true}
-            avatarJsx={ <BaseAvatar name="@" width={36} height={36} /> }
-            className="mx_EntityTile_invitePlaceholder"
-            presenceState="online" onClick={this.onInvite} name={label} />,
+                avatarJsx={ <BaseAvatar name="@" width={36} height={36} /> }
+                className="mx_EntityTile_invitePlaceholder"
+                presenceState="online" onClick={this.onThirdPartyInvite} name={"Invite by email"} 
+            />,
             function(query) {
                 return true; // always show this
             }
@@ -89,7 +100,7 @@ module.exports = React.createClass({
         }
 
         return (
-            <SearchableEntityList searchPlaceholderText={"Invite/search by name, email, id"}
+            <SearchableEntityList searchPlaceholderText={"Search/invite by name, email, id"}
                 onSubmit={this.props.onInvite}
                 onQueryChanged={this.onSearchQueryChanged}
                 entities={entities}
diff --git a/src/components/views/rooms/MemberDeviceInfo.js b/src/components/views/rooms/MemberDeviceInfo.js
new file mode 100644
index 0000000000..da53c744b2
--- /dev/null
+++ b/src/components/views/rooms/MemberDeviceInfo.js
@@ -0,0 +1,55 @@
+/*
+Copyright 2016 OpenMarket 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.
+*/
+
+var React = require('react');
+var MatrixClientPeg = require("../../../MatrixClientPeg");
+
+module.exports = React.createClass({
+    displayName: 'MemberDeviceInfo',
+    propTypes: {
+        userId: React.PropTypes.string.isRequired,
+        device: React.PropTypes.object.isRequired,
+    },
+
+    onVerifyClick: function() {
+        MatrixClientPeg.get().setDeviceVerified(this.props.userId,
+                                                this.props.device.id);
+    },
+
+    render: function() {
+        var indicator = null, button = null;
+        if (this.props.device.verified) {
+            indicator = (
+                <div className="mx_MemberDeviceInfo_verified">&#x2714;</div>
+            );
+        } else {
+            button = (
+                <div className="mx_MemberDeviceInfo_textButton"
+                  onClick={this.onVerifyClick}>
+                    Verify
+                </div>
+            );
+        }
+        return (
+            <div className="mx_MemberDeviceInfo">
+                <div className="mx_MemberDeviceInfo_deviceId">{this.props.device.id}</div>
+                <div className="mx_MemberDeviceInfo_deviceKey">{this.props.device.key}</div>
+                {indicator}
+                {button}
+            </div>
+        );
+    },
+});
diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js
index 76e5af7612..1eee280eb5 100644
--- a/src/components/views/rooms/MemberInfo.js
+++ b/src/components/views/rooms/MemberInfo.js
@@ -30,27 +30,106 @@ var MatrixClientPeg = require("../../../MatrixClientPeg");
 var dis = require("../../../dispatcher");
 var Modal = require("../../../Modal");
 var sdk = require('../../../index');
+var createRoom = require('../../../createRoom');
 
 module.exports = React.createClass({
     displayName: 'MemberInfo',
 
+    propTypes: {
+        member: React.PropTypes.object.isRequired,
+        onFinished: React.PropTypes.func,
+    },
+
     getDefaultProps: function() {
         return {
             onFinished: function() {}
         };
     },
 
-    componentDidMount: function() {
-        // work out the current state
-        if (this.props.member) {
-            var memberState = this._calculateOpsPermissions(this.props.member);
-            this.setState(memberState);
+    getInitialState: function() {
+        return {
+            can: {
+                kick: false,
+                ban: false,
+                mute: false,
+                modifyLevel: false
+            },
+            muted: false,
+            isTargetMod: false,
+            updating: 0,
+            devicesLoading: true,
+            devices: null,
         }
     },
 
+
+    componentWillMount: function() {
+        this._cancelDeviceList = null;
+    },
+
+    componentDidMount: function() {
+        this._updateStateForNewMember(this.props.member);
+        MatrixClientPeg.get().on("deviceVerified", this.onDeviceVerified);
+    },
+
     componentWillReceiveProps: function(newProps) {
-        var memberState = this._calculateOpsPermissions(newProps.member);
-        this.setState(memberState);
+        if (this.props.member.userId != newProps.member.userId) {
+            this._updateStateForNewMember(newProps.member);
+        }
+    },
+
+    componentWillUnmount: function() {
+        var client = MatrixClientPeg.get();
+        if (client) {
+            client.removeListener("deviceVerified", this.onDeviceVerified);
+        }
+        if (this._cancelDeviceList) {
+            this._cancelDeviceList();
+        }
+    },
+
+    onDeviceVerified: function(userId, device) {
+        if (userId == this.props.member.userId) {
+            // no need to re-download the whole thing; just update our copy of
+            // the list.
+            var devices = MatrixClientPeg.get().listDeviceKeys(userId);
+            this.setState({devices: devices});
+        }
+    },
+
+    _updateStateForNewMember: function(member) {
+        var newState = this._calculateOpsPermissions(member);
+        newState.devicesLoading = true;
+        newState.devices = null;
+        this.setState(newState);
+
+        if (this._cancelDeviceList) {
+            this._cancelDeviceList();
+            this._cancelDeviceList = null;
+        }
+
+        this._downloadDeviceList(member);
+    },
+
+    _downloadDeviceList: function(member) {
+        var cancelled = false;
+        this._cancelDeviceList = function() { cancelled = true; }
+
+        var client = MatrixClientPeg.get();
+        var self = this;
+        client.downloadKeys([member.userId], true).finally(function() {
+            self._cancelDeviceList = null;
+        }).done(function() {
+            if (cancelled) {
+                // we got cancelled - presumably a different user now
+                return;
+            }
+            var devices = client.listDeviceKeys(member.userId);
+            self.setState({devicesLoading: false, devices: devices});
+        }, function(err) {
+            console.log("Error downloading devices", err);
+            self.setState({devicesLoading: false});
+        });
     },
 
     onKick: function() {
@@ -315,50 +394,15 @@ module.exports = React.createClass({
             this.props.onFinished();
         }
         else {
-            if (MatrixClientPeg.get().isGuest()) {
-                var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
-                Modal.createDialog(NeedToRegisterDialog, {
-                    title: "Please Register",
-                    description: "Guest users can't create new rooms. Please register to create room and start a chat."
-                });
-                self.props.onFinished();
-                return;
-            }
-
             self.setState({ updating: self.state.updating + 1 });
-            MatrixClientPeg.get().createRoom({
-                // XXX: FIXME: deduplicate this with "view_create_room" in MatrixChat
-                invite: [this.props.member.userId],
-                preset: "private_chat",
-                // Allow guests by default since the room is private and they'd
-                // need an invite. This means clicking on a 3pid invite email can
-                // actually drop you right in to a chat.
-                initial_state: [
-                    {
-                        content: {
-                            guest_access: 'can_join'
-                        },
-                        type: 'm.room.guest_access',
-                        state_key: '',
-                    }
-                ],
-            }).then(
-                function(res) {
-                    dis.dispatch({
-                        action: 'view_room',
-                        room_id: res.room_id
-                    });
-                    self.props.onFinished();
-                }, function(err) {
-                    Modal.createDialog(ErrorDialog, {
-                        title: "Failure to start chat",
-                        description: err.message
-                    });
-                    self.props.onFinished();
-                }
-            ).finally(()=>{
+            createRoom({
+                createOpts: {
+                    invite: [this.props.member.userId],
+                },
+            }).finally(function() {
+                self.props.onFinished();
                 self.setState({ updating: self.state.updating - 1 });
-            });
+            }).done();
         }
     },
 
@@ -367,21 +411,7 @@ module.exports = React.createClass({
             action: 'leave_room',
             room_id: this.props.member.roomId,
         });
-        this.props.onFinished();        
-    },
-
-    getInitialState: function() {
-        return {
-            can: {
-                kick: false,
-                ban: false,
-                mute: false,
-                modifyLevel: false
-            },
-            muted: false,
-            isTargetMod: false,
-            updating: 0,
-        }
+        this.props.onFinished();
     },
 
     _calculateOpsPermissions: function(member) {
@@ -475,6 +505,36 @@ module.exports = React.createClass({
         Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
     },
 
+    _renderDevices: function() {
+        var devices = this.state.devices;
+        var MemberDeviceInfo = sdk.getComponent('rooms.MemberDeviceInfo');
+        var Spinner = sdk.getComponent("elements.Spinner");
+
+        var devComponents;
+        if (this.state.devicesLoading) {
+            // still loading
+            devComponents = <Spinner />;
+        } else if (devices === null) {
+            devComponents = "Unable to load device list";
+        } else if (devices.length === 0) {
+            devComponents = "No registered devices";
+        } else {
+            devComponents = [];
+            for (var i = 0; i < devices.length; i++) {
+                devComponents.push(<MemberDeviceInfo key={i}
+                                       userId={this.props.member.userId}
+                                       device={devices[i]}/>);
+            }
+        }
+
+        return (
+            <div>
+                <h3>Devices</h3>
+                {devComponents}
+            </div>
+        );
+    },
+
     render: function() {
         var startChat, kickButton, banButton, muteButton, giveModButton, spinner;
         if (this.props.member.userId !== MatrixClientPeg.get().credentials.userId) {
@@ -551,6 +611,8 @@ module.exports = React.createClass({
 
                 { startChat }
 
+                { this._renderDevices() }
+
                 { adminTools }
 
                 { spinner }
@@ -558,4 +620,3 @@ module.exports = React.createClass({
         );
     }
 });
-
diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js
index f029c519bc..328f9774c7 100644
--- a/src/components/views/rooms/MemberList.js
+++ b/src/components/views/rooms/MemberList.js
@@ -166,6 +166,25 @@ module.exports = React.createClass({
         });
     }, 500),
 
+    onThirdPartyInvite: function(inputText) {
+        var TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
+        Modal.createDialog(TextInputDialog, {
+            title: "Invite members by email",
+            description: "Please enter one or more email addresses",
+            value: inputText,
+            button: "Invite",
+            onFinished: (should_invite, addresses)=>{
+                if (should_invite) {
+                    // defer the actual invite to the next event loop to give this
+                    // Modal a chance to unmount in case onInvite() triggers a new one
+                    setTimeout(()=>{
+                        this.onInvite(addresses);
+                    }, 0);
+                }
+            }
+        });
+    },
+
     onInvite: function(inputText) {
         var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
         var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
@@ -387,7 +406,9 @@ module.exports = React.createClass({
                     // console.log(memberA + " and " + memberB + " have same power level");
                     if (memberA.name && memberB.name) {
                         // console.log("comparing names: " + memberA.name + " and " + memberB.name);
-                        return memberA.name.localeCompare(memberB.name);
+                        var nameA = memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name;
+                        var nameB = memberB.name[0] === '@' ? memberB.name.substr(1) : memberB.name;
+                        return nameA.localeCompare(nameB);
                     }
                     else {
                         return 0;
@@ -512,6 +533,7 @@ module.exports = React.createClass({
             inviteMemberListSection = (
                 <InviteMemberList roomId={this.props.roomId}
                     onSearchQueryChanged={this.onSearchQueryChanged}
+                    onThirdPartyInvite={this.onThirdPartyInvite}
                     onInvite={this.onInvite} />
             );
         }
diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index 2d17accd45..15b13a6bcc 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -53,6 +53,15 @@ module.exports = React.createClass({
     },
 
     onUploadClick: function(ev) {
+        if (MatrixClientPeg.get().isGuest()) {
+            var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
+            Modal.createDialog(NeedToRegisterDialog, {
+                title: "Please Register",
+                description: "Guest users can't upload files. Please register to upload."
+            });
+            return;
+        }
+
         this.refs.uploadInput.click();
     },
 
diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js
index fd8bcbfe96..8764700c5a 100644
--- a/src/components/views/rooms/RoomSettings.js
+++ b/src/components/views/rooms/RoomSettings.js
@@ -34,7 +34,7 @@ module.exports = React.createClass({
     getInitialState: function() {
         var tags = {};
         Object.keys(this.props.room.tags).forEach(function(tagName) {
-            tags[tagName] = {};
+            tags[tagName] = ['yep'];
         });
 
         var areNotifsMuted = false;
@@ -180,7 +180,7 @@ module.exports = React.createClass({
         // tags
         if (this.state.tags_changed) {
             var tagDiffs = ObjectUtils.getKeyValueArrayDiffs(originalState.tags, this.state.tags);
-            // [ {place: add, key: "m.favourite", val: "yep"} ]
+            // [ {place: add, key: "m.favourite", val: ["yep"]} ]
             tagDiffs.forEach(function(diff) {
                 switch (diff.place) {
                     case "add":
diff --git a/src/components/views/rooms/SearchableEntityList.js b/src/components/views/rooms/SearchableEntityList.js
index c09fc2faee..a22126025c 100644
--- a/src/components/views/rooms/SearchableEntityList.js
+++ b/src/components/views/rooms/SearchableEntityList.js
@@ -48,6 +48,7 @@ var SearchableEntityList = React.createClass({
     getInitialState: function() {
         return {
             query: "",
+            focused: false,
             truncateAt: this.props.truncateAt,
             results: this.getSearchResults("", this.props.entities)
         };
@@ -101,7 +102,7 @@ var SearchableEntityList = React.createClass({
 
     getSearchResults: function(query, entities) {
         if (!query || query.length === 0) {
-            return this.props.emptyQueryShowsAll ? entities : []
+            return this.props.emptyQueryShowsAll ? entities : [ entities[0] ]
         }
         return entities.filter(function(e) {
             return e.matches(query);
@@ -134,13 +135,27 @@ var SearchableEntityList = React.createClass({
                 <form onSubmit={this.onQuerySubmit} autoComplete="off">
                     <input className="mx_SearchableEntityList_query" id="mx_SearchableEntityList_query" type="text"
                         onChange={this.onQueryChanged} value={this.state.query}
+                        onFocus={ ()=>{
+                            if (this._blurTimeout) {
+                                clearTimeout(this.blurTimeout);
+                            }
+                            this.setState({ focused: true });
+                        } }
+                        onBlur={ ()=>{
+                            // nasty setTimeout heuristic to avoid the 'invite by email' prompt disappearing
+                            // due to the onBlur before we can click on it
+                            this._blurTimeout = setTimeout(
+                                ()=>{ this.setState({ focused: false }) },
+                                300
+                            );
+                        } }
                         placeholder={this.props.searchPlaceholderText} />
                 </form>
             );
         }
 
         var list;
-        if (this.state.results.length) {
+        if (this.state.results.length > 1 || this.state.focused) {
             if (this.props.truncateAt) { // caller wants list truncated
                 var TruncatedList = sdk.getComponent("elements.TruncatedList");
                 list = (
@@ -172,10 +187,10 @@ var SearchableEntityList = React.createClass({
         }
 
         return (
-            <div className={ "mx_SearchableEntityList " + (this.state.query.length ? "mx_SearchableEntityList_expanded" : "") }>
+            <div className={ "mx_SearchableEntityList " + (list ? "mx_SearchableEntityList_expanded" : "") }>
                 { inputBox }
                 { list }
-                { this.state.query.length ? <div className="mx_SearchableEntityList_hrWrapper"><hr/></div> : '' }
+                { list ? <div className="mx_SearchableEntityList_hrWrapper"><hr/></div> : '' }
             </div>
         );
     }
diff --git a/src/createRoom.js b/src/createRoom.js
new file mode 100644
index 0000000000..658561e78a
--- /dev/null
+++ b/src/createRoom.js
@@ -0,0 +1,86 @@
+/*
+Copyright 2015, 2016 OpenMarket 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.
+*/
+
+var MatrixClientPeg = require('./MatrixClientPeg');
+var Modal = require('./Modal');
+var sdk = require('./index');
+var dis = require("./dispatcher");
+
+var q = require('q');
+
+/**
+ * Create a new room, and switch to it.
+ *
+ * Returns a promise which resolves to the room id, or null if the
+ * action was aborted or failed.
+ *
+ * @param {object=} opts parameters for creating the room
+ * @param {object=} opts.createOpts set of options to pass to createRoom call.
+ */
+function createRoom(opts) {
+    var opts = opts || {};
+
+    var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+    var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
+    var Loader = sdk.getComponent("elements.Spinner");
+
+    var client = MatrixClientPeg.get();
+    if (client.isGuest()) {
+        Modal.createDialog(NeedToRegisterDialog, {
+            title: "Please Register",
+            description: "Guest users can't create new rooms. Please register to create room and start a chat."
+        });
+        return q(null);
+    }
+
+    // set some defaults for the creation
+    var createOpts = opts.createOpts || {};
+    createOpts.preset = createOpts.preset || 'private_chat';
+    createOpts.visibility = createOpts.visibility || 'private';
+
+    // Allow guests by default since the room is private and they'd
+    // need an invite. This means clicking on a 3pid invite email can
+    // actually drop you right in to a chat.
+    createOpts.initial_state = createOpts.initial_state || [
+        {
+            content: {
+                guest_access: 'can_join'
+            },
+            type: 'm.room.guest_access',
+            state_key: '',
+        }
+    ];
+
+    var modal = Modal.createDialog(Loader);
+
+    return client.createRoom(createOpts).finally(function() {
+        modal.close();
+    }).then(function(res) {
+        dis.dispatch({
+            action: 'view_room',
+            room_id: res.room_id
+        });
+        return res.room_id;
+    }, function(err) {
+        Modal.createDialog(ErrorDialog, {
+            title: "Failure to create room",
+            description: err.toString()
+        });
+        return null;
+    });
+}
+
+module.exports = createRoom;