diff --git a/src/GuestAccess.js b/src/GuestAccess.js
new file mode 100644
index 0000000000000000000000000000000000000000..ef48d23deddee4e1311e513005d7d3217289413f
--- /dev/null
+++ b/src/GuestAccess.js
@@ -0,0 +1,51 @@
+/*
+Copyright 2015 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.
+*/
+const IS_GUEST_KEY = "matrix-is-guest";
+
+class GuestAccess {
+
+    constructor(localStorage) {
+        this.localStorage = localStorage;
+        try {
+            this._isGuest = localStorage.getItem(IS_GUEST_KEY) === "true";
+        }
+        catch (e) {} // don't care
+    }
+
+    setPeekedRoom(roomId) {
+        // we purposefully do not persist this to local storage as peeking is
+        // entirely transient.
+        this._peekedRoomId = roomId;
+    }
+
+    getPeekedRoom() {
+        return this._peekedRoomId;
+    }
+
+    isGuest() {
+        return this._isGuest;
+    }
+
+    markAsGuest(isGuest) {
+        try {
+            this.localStorage.setItem(IS_GUEST_KEY, JSON.stringify(isGuest));
+        } catch (e) {} // ignore. If they don't do LS, they'll just get a new account.
+        this._isGuest = isGuest;
+        this._peekedRoomId = null;
+    }
+}
+
+module.exports = GuestAccess;
diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js
index 4a83ed09d9b4e13420a7ab240128f017dd87986e..dbb3dbf83e75fa0d14be7a5df02563fc51fbfbe1 100644
--- a/src/MatrixClientPeg.js
+++ b/src/MatrixClientPeg.js
@@ -18,6 +18,7 @@ limitations under the License.
 
 // A thing that holds your Matrix Client
 var Matrix = require("matrix-js-sdk");
+var GuestAccess = require("./GuestAccess");
 
 var matrixClient = null;
 
@@ -33,7 +34,7 @@ function deviceId() {
     return id;
 }
 
-function createClient(hs_url, is_url, user_id, access_token) {
+function createClient(hs_url, is_url, user_id, access_token, guestAccess) {
     var opts = {
         baseUrl: hs_url,
         idBaseUrl: is_url,
@@ -47,6 +48,15 @@ function createClient(hs_url, is_url, user_id, access_token) {
     }
 
     matrixClient = Matrix.createClient(opts);
+    if (guestAccess) {
+        console.log("Guest: %s", guestAccess.isGuest());
+        matrixClient.setGuest(guestAccess.isGuest());
+        var peekedRoomId = guestAccess.getPeekedRoom();
+        if (peekedRoomId) {
+            console.log("Peeking in room %s", peekedRoomId);
+            matrixClient.peekInRoom(peekedRoomId);
+        }
+    }
 }
 
 if (localStorage) {
@@ -54,12 +64,18 @@ if (localStorage) {
     var is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org';
     var access_token = localStorage.getItem("mx_access_token");
     var user_id = localStorage.getItem("mx_user_id");
+    var guestAccess = new GuestAccess(localStorage);
     if (access_token && user_id && hs_url) {
-        createClient(hs_url, is_url, user_id, access_token);
+        createClient(hs_url, is_url, user_id, access_token, guestAccess);
     }
 }
 
 class MatrixClient {
+
+    constructor(guestAccess) {
+        this.guestAccess = guestAccess;
+    }
+
     get() {
         return matrixClient;
     }
@@ -97,7 +113,7 @@ class MatrixClient {
         }
     }
 
-    replaceUsingAccessToken(hs_url, is_url, user_id, access_token) {
+    replaceUsingAccessToken(hs_url, is_url, user_id, access_token, isGuest) {
         if (localStorage) {
             try {
                 localStorage.clear();
@@ -105,7 +121,8 @@ class MatrixClient {
                 console.warn("Error using local storage");
             }
         }
-        createClient(hs_url, is_url, user_id, access_token);
+        this.guestAccess.markAsGuest(Boolean(isGuest));
+        createClient(hs_url, is_url, user_id, access_token, this.guestAccess);
         if (localStorage) {
             try {
                 localStorage.setItem("mx_hs_url", hs_url);
@@ -122,6 +139,6 @@ class MatrixClient {
 }
 
 if (!global.mxMatrixClient) {
-    global.mxMatrixClient = new MatrixClient();
+    global.mxMatrixClient = new MatrixClient(new GuestAccess(localStorage));
 }
 module.exports = global.mxMatrixClient;
diff --git a/src/PasswordReset.js b/src/PasswordReset.js
new file mode 100644
index 0000000000000000000000000000000000000000..1029b07b709a0214788abb971be9b366e3822fce
--- /dev/null
+++ b/src/PasswordReset.js
@@ -0,0 +1,104 @@
+/*
+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 Matrix = require("matrix-js-sdk");
+
+/**
+ * Allows a user to reset their password on a homeserver.
+ *
+ * This involves getting an email token from the identity server to "prove" that
+ * the client owns the given email address, which is then passed to the password
+ * API on the homeserver in question with the new password.
+ */
+class PasswordReset {
+
+    /**
+     * Configure the endpoints for password resetting.
+     * @param {string} homeserverUrl The URL to the HS which has the account to reset.
+     * @param {string} identityUrl The URL to the IS which has linked the email -> mxid mapping.
+     */
+    constructor(homeserverUrl, identityUrl) {
+        this.client = Matrix.createClient({
+            baseUrl: homeserverUrl,
+            idBaseUrl: identityUrl
+        });
+        this.clientSecret = generateClientSecret();
+        this.identityServerDomain = identityUrl.split("://")[1];
+    }
+
+    /**
+     * Attempt to reset the user's password. This will trigger a side-effect of
+     * sending an email to the provided email address.
+     * @param {string} emailAddress The email address
+     * @param {string} newPassword The new password for the account.
+     * @return {Promise} Resolves when the email has been sent. Then call checkEmailLinkClicked().
+     */
+    resetPassword(emailAddress, newPassword) {
+        this.password = newPassword;
+        return this.client.requestEmailToken(emailAddress, this.clientSecret, 1).then((res) => {
+            this.sessionId = res.sid;
+            return res;
+        }, function(err) {
+            if (err.httpStatus) {
+                err.message = err.message + ` (Status ${err.httpStatus})`;
+            }
+            throw err;
+        });
+    }
+
+    /**
+     * Checks if the email link has been clicked by attempting to change the password
+     * for the mxid linked to the email.
+     * @return {Promise} Resolves if the password was reset. Rejects with an object
+     * with a "message" property which contains a human-readable message detailing why
+     * the reset failed, e.g. "There is no mapped matrix user ID for the given email address".
+     */
+    checkEmailLinkClicked() {
+        return this.client.setPassword({
+            type: "m.login.email.identity",
+            threepid_creds: {
+                sid: this.sessionId,
+                client_secret: this.clientSecret,
+                id_server: this.identityServerDomain
+            }
+        }, this.password).catch(function(err) {
+            if (err.httpStatus === 401) {
+                err.message = "Failed to verify email address: make sure you clicked the link in the email";
+            }
+            else if (err.httpStatus === 404) {
+                err.message = "Your email address does not appear to be associated with a Matrix ID on this Homeserver.";
+            }
+            else if (err.httpStatus) {
+                err.message += ` (Status ${err.httpStatus})`;
+            }
+            throw err;
+        });
+    }
+}
+
+// from Angular SDK
+function generateClientSecret() {
+    var ret = "";
+    var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+
+    for (var i = 0; i < 32; i++) {
+        ret += chars.charAt(Math.floor(Math.random() * chars.length));
+    }
+
+    return ret;
+}
+
+module.exports = PasswordReset;
diff --git a/src/Presence.js b/src/Presence.js
index 5c9d6945a3852e6dd96d87073a418c129235bb78..4152d7a48743fedd6d7135ab2a9ad5ee8c4564c7 100644
--- a/src/Presence.js
+++ b/src/Presence.js
@@ -73,6 +73,11 @@ class Presence {
         }
         var old_state = this.state;
         this.state = newState;
+
+        if (MatrixClientPeg.get().isGuest()) {
+            return; // don't try to set presence when a guest; it won't work.
+        }
+
         var self = this;
         MatrixClientPeg.get().setPresence(this.state).done(function() {
             console.log("Presence: %s", newState);
diff --git a/src/Signup.js b/src/Signup.js
index 74c4ad5f19851c1a0739ba8e69dbb311439d6e3d..42468959fe7838c9c2c8ecda0d132b7b2398af8a 100644
--- a/src/Signup.js
+++ b/src/Signup.js
@@ -69,6 +69,10 @@ class Register extends Signup {
         this.params.idSid = idSid;
     }
 
+    setGuestAccessToken(token) {
+        this.guestAccessToken = token;
+    }
+
     getStep() {
         return this._step;
     }
@@ -126,7 +130,8 @@ class Register extends Signup {
         }
 
         return MatrixClientPeg.get().register(
-            this.username, this.password, this.params.sessionId, authDict, bindEmail
+            this.username, this.password, this.params.sessionId, authDict, bindEmail,
+            this.guestAccessToken
         ).then(function(result) {
             self.credentials = result;
             self.setStep("COMPLETE");
diff --git a/src/UserActivity.js b/src/UserActivity.js
index 3048ad4454792f9bc3f4d0fc5438bd4dee867399..8b136c0bcc1d6796184d6607a992d6cebf5ac1da 100644
--- a/src/UserActivity.js
+++ b/src/UserActivity.js
@@ -31,6 +31,11 @@ class UserActivity {
     start() {
         document.onmousemove = this._onUserActivity.bind(this);
         document.onkeypress = this._onUserActivity.bind(this);
+        // can't use document.scroll here because that's only the document
+        // itself being scrolled. Need to use addEventListener's useCapture.
+        // also this needs to be the wheel event, not scroll, as scroll is
+        // fired when the view scrolls down for a new message.
+        window.addEventListener('wheel', this._onUserActivity.bind(this), true);
         this.lastActivityAtTs = new Date().getTime();
         this.lastDispatchAtTs = 0;
     }
@@ -41,10 +46,11 @@ class UserActivity {
     stop() {
         document.onmousemove = undefined;
         document.onkeypress = undefined;
+        window.removeEventListener('wheel', this._onUserActivity.bind(this), true);
     }
 
     _onUserActivity(event) {
-        if (event.screenX) {
+        if (event.screenX && event.type == "mousemove") {
             if (event.screenX === this.lastScreenX &&
                 event.screenY === this.lastScreenY)
             {
diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js
index 1b1e8810a9a4c7e9c93a7b8620a2df96b8c6bbf9..45aca1f0dc24386a4a39230fd33a167d24740881 100644
--- a/src/UserSettingsStore.js
+++ b/src/UserSettingsStore.js
@@ -15,7 +15,7 @@ limitations under the License.
 */
 
 'use strict';
-
+var q = require("q");
 var MatrixClientPeg = require("./MatrixClientPeg");
 var Notifier = require("./Notifier");
 
@@ -35,6 +35,11 @@ module.exports = {
     },
 
     loadThreePids: function() {
+        if (MatrixClientPeg.get().isGuest()) {
+            return q({
+                threepids: []
+            }); // guests can't poke 3pid endpoint
+        }
         return MatrixClientPeg.get().getThreePids();
     },
 
diff --git a/src/Velociraptor.js b/src/Velociraptor.js
index d973a17f7f0d011c197f1f7bd2a0eda07994c0cc..066b1e2d054817ecc71f680398163eb8e65628fb 100644
--- a/src/Velociraptor.js
+++ b/src/Velociraptor.js
@@ -36,7 +36,7 @@ module.exports = React.createClass({
                 var old = oldChildren[c.key];
                 var oldNode = ReactDom.findDOMNode(self.nodes[old.key]);
 
-                if (oldNode.style.left != c.props.style.left) {
+                if (oldNode && oldNode.style.left != c.props.style.left) {
                     Velocity(oldNode, { left: c.props.style.left }, self.props.transition).then(function() {
                         // special case visibility because it's nonsensical to animate an invisible element
                         // so we always hidden->visible pre-transition and visible->hidden after
@@ -73,6 +73,7 @@ module.exports = React.createClass({
 
     collectNode: function(k, node) {
         if (
+            node &&
             this.nodes[k] === undefined &&
             node.props.startStyle &&
             Object.keys(node.props.startStyle).length
diff --git a/src/component-index.js b/src/component-index.js
index 0c08d70b7383c36d83cc7fa1b9d147ed7875021a..ac9a83346ae5d4a9ce77b751051f393736994390 100644
--- a/src/component-index.js
+++ b/src/component-index.js
@@ -23,14 +23,15 @@ limitations under the License.
 
 module.exports.components = {};
 module.exports.components['structures.CreateRoom'] = require('./components/structures/CreateRoom');
+module.exports.components['structures.login.ForgotPassword'] = require('./components/structures/login/ForgotPassword');
+module.exports.components['structures.login.Login'] = require('./components/structures/login/Login');
+module.exports.components['structures.login.PostRegistration'] = require('./components/structures/login/PostRegistration');
+module.exports.components['structures.login.Registration'] = require('./components/structures/login/Registration');
 module.exports.components['structures.MatrixChat'] = require('./components/structures/MatrixChat');
 module.exports.components['structures.RoomView'] = require('./components/structures/RoomView');
 module.exports.components['structures.ScrollPanel'] = require('./components/structures/ScrollPanel');
 module.exports.components['structures.UploadBar'] = require('./components/structures/UploadBar');
 module.exports.components['structures.UserSettings'] = require('./components/structures/UserSettings');
-module.exports.components['structures.login.Login'] = require('./components/structures/login/Login');
-module.exports.components['structures.login.PostRegistration'] = require('./components/structures/login/PostRegistration');
-module.exports.components['structures.login.Registration'] = require('./components/structures/login/Registration');
 module.exports.components['views.avatars.MemberAvatar'] = require('./components/views/avatars/MemberAvatar');
 module.exports.components['views.avatars.RoomAvatar'] = require('./components/views/avatars/RoomAvatar');
 module.exports.components['views.create_room.CreateRoomButton'] = require('./components/views/create_room/CreateRoomButton');
@@ -51,10 +52,10 @@ module.exports.components['views.login.LoginHeader'] = require('./components/vie
 module.exports.components['views.login.PasswordLogin'] = require('./components/views/login/PasswordLogin');
 module.exports.components['views.login.RegistrationForm'] = require('./components/views/login/RegistrationForm');
 module.exports.components['views.login.ServerConfig'] = require('./components/views/login/ServerConfig');
+module.exports.components['views.messages.MessageEvent'] = require('./components/views/messages/MessageEvent');
 module.exports.components['views.messages.MFileBody'] = require('./components/views/messages/MFileBody');
 module.exports.components['views.messages.MImageBody'] = require('./components/views/messages/MImageBody');
 module.exports.components['views.messages.MVideoBody'] = require('./components/views/messages/MVideoBody');
-module.exports.components['views.messages.MessageEvent'] = require('./components/views/messages/MessageEvent');
 module.exports.components['views.messages.TextualBody'] = require('./components/views/messages/TextualBody');
 module.exports.components['views.messages.TextualEvent'] = require('./components/views/messages/TextualEvent');
 module.exports.components['views.messages.UnknownBody'] = require('./components/views/messages/UnknownBody');
diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js
index ef77787035cf0cb096effe1a1ee94fa9c05fd9bc..e5af2a86b550686c5d953e0fc6538210caf5bf78 100644
--- a/src/components/structures/MatrixChat.js
+++ b/src/components/structures/MatrixChat.js
@@ -43,6 +43,7 @@ module.exports = React.createClass({
         ConferenceHandler: React.PropTypes.any,
         onNewScreen: React.PropTypes.func,
         registrationUrl: React.PropTypes.string,
+        enableGuest: React.PropTypes.bool,
         startingQueryParams: React.PropTypes.object
     },
 
@@ -84,8 +85,21 @@ module.exports = React.createClass({
     },
 
     componentDidMount: function() {
+        this._autoRegisterAsGuest = false;
+        if (this.props.enableGuest) {
+            if (!this.props.config || !this.props.config.default_hs_url) {
+                console.error("Cannot enable guest access: No supplied config prop for HS/IS URLs");
+            }
+            else {
+                this._autoRegisterAsGuest = true;
+            }
+        }
+
         this.dispatcherRef = dis.register(this.onAction);
         if (this.state.logged_in) {
+            // Don't auto-register as a guest. This applies if you refresh the page on a
+            // logged in client THEN hit the Sign Out button.
+            this._autoRegisterAsGuest = false;
             this.startMatrixClient();
         }
         this.focusComposer = false;
@@ -94,8 +108,11 @@ module.exports = React.createClass({
         this.scrollStateMap = {};
         document.addEventListener("keydown", this.onKeyDown);
         window.addEventListener("focus", this.onFocus);
+
         if (this.state.logged_in) {
             this.notifyNewScreen('');
+        } else if (this._autoRegisterAsGuest) {
+            this._registerAsGuest();
         } else {
             this.notifyNewScreen('login');
         }
@@ -127,6 +144,34 @@ module.exports = React.createClass({
         }
     },
 
+    _registerAsGuest: function() {
+        var self = this;
+        var config = this.props.config;
+        console.log("Doing guest login on %s", config.default_hs_url);
+        MatrixClientPeg.replaceUsingUrls(
+            config.default_hs_url, config.default_is_url
+        );
+        MatrixClientPeg.get().registerGuest().done(function(creds) {
+            console.log("Registered as guest: %s", creds.user_id);
+            self._setAutoRegisterAsGuest(false);
+            self.onLoggedIn({
+                userId: creds.user_id,
+                accessToken: creds.access_token,
+                homeserverUrl: config.default_hs_url,
+                identityServerUrl: config.default_is_url,
+                guest: true
+            });
+        }, function(err) {
+            console.error(err.data);
+            self._setAutoRegisterAsGuest(false);
+        });
+    },
+
+    _setAutoRegisterAsGuest: function(shouldAutoRegister) {
+        this._autoRegisterAsGuest = shouldAutoRegister;
+        this.forceUpdate();
+    },
+
     onAction: function(payload) {
         var roomIndexDelta = 1;
 
@@ -181,6 +226,21 @@ module.exports = React.createClass({
                     screen: 'post_registration'
                 });
                 break;
+            case 'start_upgrade_registration':
+                this.replaceState({
+                    screen: "register",
+                    upgradeUsername: MatrixClientPeg.get().getUserIdLocalpart(),
+                    guestAccessToken: MatrixClientPeg.get().getAccessToken()
+                });
+                this.notifyNewScreen('register');
+                break;
+            case 'start_password_recovery':
+                if (this.state.logged_in) return;
+                this.replaceState({
+                    screen: 'forgot_password'
+                });
+                this.notifyNewScreen('forgot_password');
+                break;
             case 'token_login':
                 if (this.state.logged_in) return;
 
@@ -392,10 +452,11 @@ module.exports = React.createClass({
     },
 
     onLoggedIn: function(credentials) {
-        console.log("onLoggedIn => %s", credentials.userId);
+        credentials.guest = Boolean(credentials.guest);
+        console.log("onLoggedIn => %s (guest=%s)", credentials.userId, credentials.guest);
         MatrixClientPeg.replaceUsingAccessToken(
             credentials.homeserverUrl, credentials.identityServerUrl,
-            credentials.userId, credentials.accessToken
+            credentials.userId, credentials.accessToken, credentials.guest
         );
         this.setState({
             screen: undefined,
@@ -515,6 +576,11 @@ module.exports = React.createClass({
                 action: 'token_login',
                 params: params
             });
+        } else if (screen == 'forgot_password') {
+            dis.dispatch({
+                action: 'start_password_recovery',
+                params: params
+            });
         } else if (screen == 'new') {
             dis.dispatch({
                 action: 'view_create_room',
@@ -624,6 +690,10 @@ module.exports = React.createClass({
         this.showScreen("login");
     },
 
+    onForgotPasswordClick: function() {
+        this.showScreen("forgot_password");
+    },
+
     onRegistered: function(credentials) {
         this.onLoggedIn(credentials);
         // do post-registration stuff
@@ -662,6 +732,7 @@ module.exports = React.createClass({
         var CreateRoom = sdk.getComponent('structures.CreateRoom');
         var RoomDirectory = sdk.getComponent('structures.RoomDirectory');
         var MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
+        var ForgotPassword = sdk.getComponent('structures.login.ForgotPassword');
 
         // needs to be before normal PageTypes as you are logged in technically
         if (this.state.screen == 'post_registration') {
@@ -725,12 +796,20 @@ module.exports = React.createClass({
                         </div>
                 );
             }
-        } else if (this.state.logged_in) {
+        } else if (this.state.logged_in || (!this.state.logged_in && this._autoRegisterAsGuest)) {
             var Spinner = sdk.getComponent('elements.Spinner');
+            var logoutLink;
+            if (this.state.logged_in) {
+                logoutLink = (
+                    <a href="#" className="mx_MatrixChat_splashButtons" onClick={ this.onLogoutClick }>
+                    Logout
+                    </a>
+                );
+            }
             return (
                 <div className="mx_MatrixChat_splash">
                     <Spinner />
-                    <a href="#" className="mx_MatrixChat_splashButtons" onClick={ this.onLogoutClick }>Logout</a>
+                    {logoutLink}
                 </div>
             );
         } else if (this.state.screen == 'register') {
@@ -740,19 +819,30 @@ module.exports = React.createClass({
                     sessionId={this.state.register_session_id}
                     idSid={this.state.register_id_sid}
                     email={this.props.startingQueryParams.email}
+                    username={this.state.upgradeUsername}
+                    disableUsernameChanges={Boolean(this.state.upgradeUsername)}
+                    guestAccessToken={this.state.guestAccessToken}
                     hsUrl={this.props.config.default_hs_url}
                     isUrl={this.props.config.default_is_url}
                     registrationUrl={this.props.registrationUrl}
                     onLoggedIn={this.onRegistered}
                     onLoginClick={this.onLoginClick} />
             );
+        } else if (this.state.screen == 'forgot_password') {
+            return (
+                <ForgotPassword
+                    homeserverUrl={this.props.config.default_hs_url}
+                    identityServerUrl={this.props.config.default_is_url}
+                    onComplete={this.onLoginClick} />
+            );
         } else {
             return (
                 <Login
                     onLoggedIn={this.onLoggedIn}
                     onRegisterClick={this.onRegisterClick}
                     homeserverUrl={this.props.config.default_hs_url}
-                    identityServerUrl={this.props.config.default_is_url} />
+                    identityServerUrl={this.props.config.default_is_url}
+                    onForgotPasswordClick={this.onForgotPasswordClick} />
             );
         }
     }
diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js
index 625f4da98b2e77237063acc9a8fadaf19f9dd81a..4392975b9af0431a50241adccfbd9dc2255b7769 100644
--- a/src/components/structures/RoomView.js
+++ b/src/components/structures/RoomView.js
@@ -41,6 +41,7 @@ var Tinter = require("../../Tinter");
 
 var PAGINATE_SIZE = 20;
 var INITIAL_SIZE = 20;
+var SEND_READ_RECEIPT_DELAY = 2000;
 
 var DEBUG_SCROLL = false;
 
@@ -75,6 +76,8 @@ module.exports = React.createClass({
             syncState: MatrixClientPeg.get().getSyncState(),
             hasUnsentMessages: this._hasUnsentMessages(room),
             callState: null,
+            readMarkerEventId: room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId),
+            readMarkerGhostEventId: undefined,
         }
     },
 
@@ -99,9 +102,33 @@ module.exports = React.createClass({
                 this.forceUpdate();
             }
         });
+        // if this is an unknown room then we're in one of three states:
+        // - This is a room we can peek into (search engine) (we can /peek)
+        // - This is a room we can publicly join or were invited to. (we can /join)
+        // - This is a room we cannot join at all. (no action can help us)
+        // We can't try to /join because this may implicitly accept invites (!)
+        // We can /peek though. If it fails then we present the join UI. If it
+        // succeeds then great, show the preview (but we still may be able to /join!).
+        if (!this.state.room) {
+            console.log("Attempting to peek into room %s", this.props.roomId);
+            MatrixClientPeg.get().peekInRoom(this.props.roomId).done(function() {
+                // we don't need to do anything - JS SDK will emit Room events
+                // which will update the UI.
+            }, function(err) {
+                console.error("Failed to peek into room: %s", err);
+            });
+        }
+
+
     },
 
     componentWillUnmount: function() {
+        // set a boolean to say we've been unmounted, which any pending
+        // promises can use to throw away their results.
+        //
+        // (We could use isMounted, but facebook have deprecated that.)
+        this.unmounted = true;
+
         if (this.refs.messagePanel) {
             // disconnect the D&D event listeners from the message panel. This
             // is really just for hygiene - the messagePanel is going to be
@@ -201,7 +228,7 @@ module.exports = React.createClass({
     },*/
 
     onRoomTimeline: function(ev, room, toStartOfTimeline) {
-        if (!this.isMounted()) return;
+        if (this.unmounted) return;
 
         // ignore anything that comes in whilst paginating: we get one
         // event for each new matrix event so this would cause a huge
@@ -265,7 +292,33 @@ module.exports = React.createClass({
 
     onRoomReceipt: function(receiptEvent, room) {
         if (room.roomId == this.props.roomId) {
-            this.forceUpdate();
+            var readMarkerEventId = this.state.room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId);
+            var readMarkerGhostEventId = this.state.readMarkerGhostEventId;
+            if (this.state.readMarkerEventId !== undefined && this.state.readMarkerEventId != readMarkerEventId) {
+                readMarkerGhostEventId = this.state.readMarkerEventId;
+            }
+
+
+            // if the event after the one referenced in the read receipt if sent by us, do nothing since
+            // this is a temporary period before the synthesized receipt for our own message arrives
+            var readMarkerGhostEventIndex;
+            for (var i = 0; i < room.timeline.length; ++i) {
+                if (room.timeline[i].getId() == readMarkerGhostEventId) {
+                    readMarkerGhostEventIndex = i;
+                    break;
+                }
+            }
+            if (readMarkerGhostEventIndex + 1 < room.timeline.length) {
+                var nextEvent = room.timeline[readMarkerGhostEventIndex + 1];
+                if (nextEvent.sender && nextEvent.sender.userId == MatrixClientPeg.get().credentials.userId) {
+                    readMarkerGhostEventId = undefined;
+                }
+            }
+
+            this.setState({
+                readMarkerEventId: readMarkerEventId,
+                readMarkerGhostEventId: readMarkerGhostEventId,
+            });
         }
     },
 
@@ -383,11 +436,14 @@ module.exports = React.createClass({
     _paginateCompleted: function() {
         debuglog("paginate complete");
 
+        // we might have switched rooms since the paginate started - just bin
+        // the results if so.
+        if (this.unmounted) return;
+
         this.setState({
-            room: MatrixClientPeg.get().getRoom(this.props.roomId)
+            room: MatrixClientPeg.get().getRoom(this.props.roomId),
+            paginating: false,
         });
-
-        this.setState({paginating: false});
     },
 
     onSearchResultsFillRequest: function(backwards) {
@@ -452,6 +508,12 @@ module.exports = React.createClass({
                 joining: false,
                 joinError: error
             });
+            var msg = error.message ? error.message : JSON.stringify(error);
+            var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+            Modal.createDialog(ErrorDialog, {
+                title: "Failed to join room",
+                description: msg
+            });
         });
         this.setState({
             joining: true
@@ -565,7 +627,7 @@ module.exports = React.createClass({
 
         return searchPromise.then(function(results) {
             debuglog("search complete");
-            if (!self.state.searching || self.searchId != localSearchId) {
+            if (self.unmounted || !self.state.searching || self.searchId != localSearchId) {
                 console.error("Discarding stale search results");
                 return;
             }
@@ -583,7 +645,8 @@ module.exports = React.createClass({
 
             // For overlapping highlights,
             // favour longer (more specific) terms first
-            highlights = highlights.sort(function(a, b) { b.length - a.length });
+            highlights = highlights.sort(function(a, b) {
+                return b.length - a.length });
 
             self.setState({
                 searchHighlights: highlights,
@@ -678,10 +741,10 @@ module.exports = React.createClass({
 
         var EventTile = sdk.getComponent('rooms.EventTile');
 
-
         var prevEvent = null; // the last event we showed
-        var readReceiptEventId = this.state.room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId);
         var startIdx = Math.max(0, this.state.room.timeline.length - this.state.messageCap);
+        var readMarkerIndex;
+        var ghostIndex;
         for (var i = startIdx; i < this.state.room.timeline.length; i++) {
             var mxEv = this.state.room.timeline[i];
 
@@ -695,6 +758,25 @@ module.exports = React.createClass({
                 }
             }
 
+            // now we've decided whether or not to show this message,
+            // add the read up to marker if appropriate
+            // doing this here means we implicitly do not show the marker
+            // if it's at the bottom
+            // NB. it would be better to decide where the read marker was going
+            // when the state changed rather than here in the render method, but
+            // this is where we decide what messages we show so it's the only
+            // place we know whether we're at the bottom or not.
+            var self = this;
+            var mxEvSender = mxEv.sender ? mxEv.sender.userId : null;
+            if (prevEvent && prevEvent.getId() == this.state.readMarkerEventId && mxEvSender != MatrixClientPeg.get().credentials.userId) {
+                var hr;
+                hr = (<hr className="mx_RoomView_myReadMarker" style={{opacity: 1, width: '99%'}} ref={function(n) {
+                    self.readMarkerNode = n;
+                }} />);
+                readMarkerIndex = ret.length;
+                ret.push(<li key="_readupto" className="mx_RoomView_myReadMarker_container">{hr}</li>);
+            }
+
             // is this a continuation of the previous message?
             var continuation = false;
             if (prevEvent !== null) {
@@ -731,17 +813,33 @@ module.exports = React.createClass({
                 </li>
             );
 
-            if (eventId == readReceiptEventId) {
-                ret.push(<hr className="mx_RoomView_myReadMarker" />);
+            // A read up to marker has died and returned as a ghost!
+            // Lives in the dom as the ghost of the previous one while it fades away
+            if (eventId == this.state.readMarkerGhostEventId) {
+                ghostIndex = ret.length;
             }
 
             prevEvent = mxEv;
         }
 
+        // splice the read marker ghost in now that we know whether the read receipt
+        // is the last element or not, because we only decide as we're going along.
+        if (readMarkerIndex === undefined && ghostIndex && ghostIndex <= ret.length) {
+            var hr;
+            hr = (<hr className="mx_RoomView_myReadMarker" style={{opacity: 1, width: '85%'}} ref={function(n) {
+                Velocity(n, {opacity: '0', width: '10%'}, {duration: 400, easing: 'easeInSine', delay: 1000, complete: function() {
+                    self.setState({readMarkerGhostEventId: undefined});
+                }});
+            }} />);
+            ret.splice(ghostIndex, 0, (
+                <li key="_readuptoghost" className="mx_RoomView_myReadMarker_container">{hr}</li>
+            ));
+        }
+
         return ret;
     },
 
-    uploadNewState: function(new_name, new_topic, new_join_rule, new_history_visibility, new_power_levels, new_color_scheme) {
+    uploadNewState: function(newVals) {
         var old_name = this.state.room.name;
 
         var old_topic = this.state.room.currentState.getStateEvents('m.room.topic', '');
@@ -767,54 +865,62 @@ module.exports = React.createClass({
 
         var deferreds = [];
 
-        if (old_name != new_name && new_name != undefined && new_name) {
+        if (old_name != newVals.name && newVals.name != undefined && newVals.name) {
             deferreds.push(
-                MatrixClientPeg.get().setRoomName(this.state.room.roomId, new_name)
+                MatrixClientPeg.get().setRoomName(this.state.room.roomId, newVals.name)
             );
         }
 
-        if (old_topic != new_topic && new_topic != undefined) {
+        if (old_topic != newVals.topic && newVals.topic != undefined) {
             deferreds.push(
-                MatrixClientPeg.get().setRoomTopic(this.state.room.roomId, new_topic)
+                MatrixClientPeg.get().setRoomTopic(this.state.room.roomId, newVals.topic)
             );
         }
 
-        if (old_join_rule != new_join_rule && new_join_rule != undefined) {
+        if (old_join_rule != newVals.join_rule && newVals.join_rule != undefined) {
             deferreds.push(
                 MatrixClientPeg.get().sendStateEvent(
                     this.state.room.roomId, "m.room.join_rules", {
-                        join_rule: new_join_rule,
+                        join_rule: newVals.join_rule,
                     }, ""
                 )
             );
         }
 
-        if (old_history_visibility != new_history_visibility && new_history_visibility != undefined) {
+        if (old_history_visibility != newVals.history_visibility &&
+                newVals.history_visibility != undefined) {
             deferreds.push(
                 MatrixClientPeg.get().sendStateEvent(
                     this.state.room.roomId, "m.room.history_visibility", {
-                        history_visibility: new_history_visibility,
+                        history_visibility: newVals.history_visibility,
                     }, ""
                 )
             );
         }
 
-        if (new_power_levels) {
+        if (newVals.power_levels) {
             deferreds.push(
                 MatrixClientPeg.get().sendStateEvent(
-                    this.state.room.roomId, "m.room.power_levels", new_power_levels, ""
+                    this.state.room.roomId, "m.room.power_levels", newVals.power_levels, ""
                 )
             );
         }
 
-        if (new_color_scheme) {
+        if (newVals.color_scheme) {
             deferreds.push(
                 MatrixClientPeg.get().setRoomAccountData(
-                    this.state.room.roomId, "org.matrix.room.color_scheme", new_color_scheme
+                    this.state.room.roomId, "org.matrix.room.color_scheme", newVals.color_scheme
                 )
             );
         }
 
+        deferreds.push(
+            MatrixClientPeg.get().setGuestAccess(this.state.room.roomId, {
+                allowRead: newVals.guest_read,
+                allowJoin: newVals.guest_join
+            })
+        );
+
         if (deferreds.length) {
             var self = this;
             q.all(deferreds).fail(function(err) {
@@ -899,21 +1005,16 @@ module.exports = React.createClass({
             uploadingRoomSettings: true,
         });
 
-        var new_name = this.refs.header.getRoomName();
-        var new_topic = this.refs.room_settings.getTopic();
-        var new_join_rule = this.refs.room_settings.getJoinRules();
-        var new_history_visibility = this.refs.room_settings.getHistoryVisibility();
-        var new_power_levels = this.refs.room_settings.getPowerLevels();
-        var new_color_scheme = this.refs.room_settings.getColorScheme();
-
-        this.uploadNewState(
-            new_name,
-            new_topic,
-            new_join_rule,
-            new_history_visibility,
-            new_power_levels,
-            new_color_scheme
-        );
+        this.uploadNewState({
+            name: this.refs.header.getRoomName(),
+            topic: this.refs.room_settings.getTopic(),
+            join_rule: this.refs.room_settings.getJoinRules(),
+            history_visibility: this.refs.room_settings.getHistoryVisibility(),
+            power_levels: this.refs.room_settings.getPowerLevels(),
+            guest_join: this.refs.room_settings.canGuestsJoin(),
+            guest_read: this.refs.room_settings.canGuestsRead(),
+            color_scheme: this.refs.room_settings.getColorScheme(),
+        });
     },
 
     onCancelClick: function() {
@@ -1067,17 +1168,23 @@ module.exports = React.createClass({
         // a maxHeight on the underlying remote video tag.
         var auxPanelMaxHeight;
         if (this.refs.callView) {
-            // XXX: don't understand why we have to call findDOMNode here in react 0.14 - it should already be a DOM node.
-            var video = ReactDOM.findDOMNode(this.refs.callView.refs.video.refs.remote);
+            var video = this.refs.callView.getVideoView().getRemoteVideoElement();
 
             // header + footer + status + give us at least 100px of scrollback at all times.
-            auxPanelMaxHeight = window.innerHeight - (83 + 72 + 36 + 100);
+            auxPanelMaxHeight = window.innerHeight -
+                (83 + 72 +
+                 sdk.getComponent('rooms.MessageComposer').MAX_HEIGHT +
+                 100);
 
             // XXX: this is a bit of a hack and might possibly cause the video to push out the page anyway
             // but it's better than the video going missing entirely
             if (auxPanelMaxHeight < 50) auxPanelMaxHeight = 50;
 
             video.style.maxHeight = auxPanelMaxHeight + "px";
+
+            // the above might have made the video panel resize itself, so now
+            // we need to tell the gemini panel to adapt.
+            this.onChildResize();
         }
     },
 
@@ -1112,6 +1219,15 @@ module.exports = React.createClass({
         });
     },
 
+    onChildResize: function() {
+        // When the video or the message composer resizes, the scroll panel
+        // also changes size.  Work around GeminiScrollBar fail by telling it
+        // about it. This also ensures that the scroll offset is updated.
+        if (this.refs.messagePanel) {
+            this.refs.messagePanel.forceUpdate();
+        }
+    },
+
     render: function() {
         var RoomHeader = sdk.getComponent('rooms.RoomHeader');
         var MessageComposer = sdk.getComponent('rooms.MessageComposer');
@@ -1302,7 +1418,7 @@ module.exports = React.createClass({
             if (canSpeak) {
                 messageComposer =
                     <MessageComposer
-                        room={this.state.room} roomView={this} uploadFile={this.uploadFile}
+                        room={this.state.room} onResize={this.onChildResize} uploadFile={this.uploadFile}
                         callState={this.state.callState} tabComplete={this.tabComplete} />
             }
 
@@ -1405,7 +1521,8 @@ module.exports = React.createClass({
                         } />
                     { fileDropTarget }    
                     <div className="mx_RoomView_auxPanel">
-                        <CallView ref="callView" room={this.state.room} ConferenceHandler={this.props.ConferenceHandler}/>
+                        <CallView ref="callView" room={this.state.room} ConferenceHandler={this.props.ConferenceHandler}
+                            onResize={this.onChildResize} />
                         { conferenceCallNotification }
                         { aux }
                     </div>
diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js
index 042458717d3a1e51205d4da5ec4ef6ed01d348e2..8d26b2e365868b3be49696f1abb17884788da79c 100644
--- a/src/components/structures/ScrollPanel.js
+++ b/src/components/structures/ScrollPanel.js
@@ -112,6 +112,14 @@ module.exports = React.createClass({
         this.checkFillState();
     },
 
+    componentWillUnmount: function() {
+        // set a boolean to say we've been unmounted, which any pending
+        // promises can use to throw away their results.
+        //
+        // (We could use isMounted(), but facebook have deprecated that.)
+        this.unmounted = true;
+    },
+
     onScroll: function(ev) {
         var sn = this._getScrollNode();
         debuglog("Scroll event: offset now:", sn.scrollTop, "recentEventScroll:", this.recentEventScroll);
@@ -158,6 +166,10 @@ module.exports = React.createClass({
 
     // check the scroll state and send out backfill requests if necessary.
     checkFillState: function() {
+        if (this.unmounted) {
+            return;
+        }
+
         var sn = this._getScrollNode();
 
         // if there is less than a screenful of messages above or below the
@@ -346,6 +358,12 @@ module.exports = React.createClass({
      * message panel.
      */
     _getScrollNode: function() {
+        if (this.unmounted) {
+            // this shouldn't happen, but when it does, turn the NPE into
+            // something more meaningful.
+            throw new Error("ScrollPanel._getScrollNode called when unmounted");
+        }
+
         var panel = ReactDOM.findDOMNode(this.refs.geminiPanel);
 
         // If the gemini scrollbar is doing its thing, this will be a div within
diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js
index c1550f9b6b4855b34d856acf1ad311bcc1c7f418..ddf4229170ea48b2b9ea7947c0ff16abc5dda988 100644
--- a/src/components/structures/UserSettings.js
+++ b/src/components/structures/UserSettings.js
@@ -135,6 +135,12 @@ module.exports = React.createClass({
         });
     },
 
+    onUpgradeClicked: function() {
+        dis.dispatch({
+            action: "start_upgrade_registration"
+        });
+    },
+
     onLogoutPromptCancel: function() {
         this.logoutModal.closeDialog();
     },
@@ -164,6 +170,28 @@ module.exports = React.createClass({
             this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null
         );
 
+        var accountJsx;
+
+        if (MatrixClientPeg.get().isGuest()) {
+            accountJsx = (
+                <div className="mx_UserSettings_button" onClick={this.onUpgradeClicked}>
+                    Upgrade (It's free!)
+                </div>
+            );
+        }
+        else {
+            accountJsx = (
+                <ChangePassword
+                        className="mx_UserSettings_accountTable"
+                        rowClassName="mx_UserSettings_profileTableRow"
+                        rowLabelClassName="mx_UserSettings_profileLabelCell"
+                        rowInputClassName="mx_UserSettings_profileInputCell"
+                        buttonClassName="mx_UserSettings_button"
+                        onError={this.onPasswordChangeError}
+                        onFinished={this.onPasswordChanged} />
+            );
+        }
+
         return (
             <div className="mx_UserSettings">
                 <RoomHeader simpleHeader="Settings" />
@@ -213,14 +241,7 @@ module.exports = React.createClass({
                 <h2>Account</h2>
 
                 <div className="mx_UserSettings_section">
-                    <ChangePassword
-                        className="mx_UserSettings_accountTable"
-                        rowClassName="mx_UserSettings_profileTableRow"
-                        rowLabelClassName="mx_UserSettings_profileLabelCell"
-                        rowInputClassName="mx_UserSettings_profileInputCell"
-                        buttonClassName="mx_UserSettings_button"
-                        onError={this.onPasswordChangeError}
-                        onFinished={this.onPasswordChanged} />                   
+                    {accountJsx}
                 </div>
 
                 <div className="mx_UserSettings_logout">
diff --git a/src/components/structures/login/ForgotPassword.js b/src/components/structures/login/ForgotPassword.js
new file mode 100644
index 0000000000000000000000000000000000000000..dcf6a7c28e00ca747d59e8bfe1c73d402f2793d5
--- /dev/null
+++ b/src/components/structures/login/ForgotPassword.js
@@ -0,0 +1,199 @@
+/*
+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.
+*/
+
+'use strict';
+
+var React = require('react');
+var sdk = require('../../../index');
+var Modal = require("../../../Modal");
+var MatrixClientPeg = require('../../../MatrixClientPeg');
+
+var PasswordReset = require("../../../PasswordReset");
+
+module.exports = React.createClass({
+    displayName: 'ForgotPassword',
+
+    propTypes: {
+        homeserverUrl: React.PropTypes.string,
+        identityServerUrl: React.PropTypes.string,
+        onComplete: React.PropTypes.func.isRequired
+    },
+
+    getInitialState: function() {
+        return {
+            enteredHomeserverUrl: this.props.homeserverUrl,
+            enteredIdentityServerUrl: this.props.identityServerUrl,
+            progress: null
+        };
+    },
+
+    submitPasswordReset: function(hsUrl, identityUrl, email, password) {
+        this.setState({
+            progress: "sending_email"
+        });
+        this.reset = new PasswordReset(hsUrl, identityUrl);
+        this.reset.resetPassword(email, password).done(() => {
+            this.setState({
+                progress: "sent_email"
+            });
+        }, (err) => {
+            this.showErrorDialog("Failed to send email: " + err.message);
+            this.setState({
+                progress: null
+            });
+        })
+    },
+
+    onVerify: function(ev) {
+        ev.preventDefault();
+        if (!this.reset) {
+            console.error("onVerify called before submitPasswordReset!");
+            return;
+        }
+        this.reset.checkEmailLinkClicked().done((res) => {
+            this.setState({ progress: "complete" });
+        }, (err) => {
+            this.showErrorDialog(err.message);
+        })
+    },
+
+    onSubmitForm: function(ev) {
+        ev.preventDefault();
+
+        if (!this.state.email) {
+            this.showErrorDialog("The email address linked to your account must be entered.");
+        }
+        else if (!this.state.password || !this.state.password2) {
+            this.showErrorDialog("A new password must be entered.");
+        }
+        else if (this.state.password !== this.state.password2) {
+            this.showErrorDialog("New passwords must match each other.");
+        }
+        else {
+            this.submitPasswordReset(
+                this.state.enteredHomeserverUrl, this.state.enteredIdentityServerUrl,
+                this.state.email, this.state.password
+            );
+        }
+    },
+
+    onInputChanged: function(stateKey, ev) {
+        this.setState({
+            [stateKey]: ev.target.value
+        });
+    },
+
+    onHsUrlChanged: function(newHsUrl) {
+        this.setState({
+            enteredHomeserverUrl: newHsUrl
+        });
+    },
+
+    onIsUrlChanged: function(newIsUrl) {
+        this.setState({
+            enteredIdentityServerUrl: newIsUrl
+        });
+    },
+
+    showErrorDialog: function(body, title) {
+        var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+        Modal.createDialog(ErrorDialog, {
+            title: title,
+            description: body
+        });
+    },
+
+    render: function() {
+        var LoginHeader = sdk.getComponent("login.LoginHeader");
+        var LoginFooter = sdk.getComponent("login.LoginFooter");
+        var ServerConfig = sdk.getComponent("login.ServerConfig");
+        var Spinner = sdk.getComponent("elements.Spinner");
+
+        var resetPasswordJsx;
+
+        if (this.state.progress === "sending_email") {
+            resetPasswordJsx = <Spinner />
+        }
+        else if (this.state.progress === "sent_email") {
+            resetPasswordJsx = (
+                <div>
+                    An email has been sent to {this.state.email}. Once you&#39;ve followed
+                    the link it contains, click below.
+                    <br />
+                    <input className="mx_Login_submit" type="button" onClick={this.onVerify}
+                        value="I have verified my email address" />
+                </div>
+            );
+        }
+        else if (this.state.progress === "complete") {
+            resetPasswordJsx = (
+                <div>
+                    <p>Your password has been reset.</p>
+                    <p>You have been logged out of all devices and will no longer receive push notifications.
+                    To re-enable notifications, re-log in on each device.</p>
+                    <input className="mx_Login_submit" type="button" onClick={this.props.onComplete}
+                        value="Return to login screen" />
+                </div>
+            );
+        }
+        else {
+            resetPasswordJsx = (
+            <div>
+                To reset your password, enter the email address linked to your account:
+                <br />
+                <div>
+                    <form onSubmit={this.onSubmitForm}>
+                        <input className="mx_Login_field" ref="user" type="text"
+                            value={this.state.email}
+                            onChange={this.onInputChanged.bind(this, "email")}
+                            placeholder="Email address" autoFocus />
+                        <br />
+                        <input className="mx_Login_field" ref="pass" type="password"
+                            value={this.state.password}
+                            onChange={this.onInputChanged.bind(this, "password")}
+                            placeholder="New password" />
+                        <br />
+                        <input className="mx_Login_field" ref="pass" type="password"
+                            value={this.state.password2}
+                            onChange={this.onInputChanged.bind(this, "password2")}
+                            placeholder="Confirm your new password" />
+                        <br />
+                        <input className="mx_Login_submit" type="submit" value="Send Reset Email" />
+                    </form>
+                    <ServerConfig ref="serverConfig"
+                        withToggleButton={true}
+                        defaultHsUrl={this.props.homeserverUrl}
+                        defaultIsUrl={this.props.identityServerUrl}
+                        onHsUrlChanged={this.onHsUrlChanged}
+                        onIsUrlChanged={this.onIsUrlChanged}
+                        delayTimeMs={0}/>
+                    <LoginFooter />
+                </div>
+            </div>
+            );
+        }
+
+
+        return (
+            <div className="mx_Login">
+                <div className="mx_Login_box">
+                    <LoginHeader />
+                    {resetPasswordJsx}
+                </div>
+            </div>
+        );
+    }
+});
diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js
index b7d2d762a472913021681c6fe5fad52ca932f514..b853b8fd95cc8da920b07a18626e530814221e4b 100644
--- a/src/components/structures/login/Login.js
+++ b/src/components/structures/login/Login.js
@@ -33,7 +33,9 @@ module.exports = React.createClass({displayName: 'Login',
         homeserverUrl: React.PropTypes.string,
         identityServerUrl: React.PropTypes.string,
         // login shouldn't know or care how registration is done.
-        onRegisterClick: React.PropTypes.func.isRequired
+        onRegisterClick: React.PropTypes.func.isRequired,
+        // login shouldn't care how password recovery is done.
+        onForgotPasswordClick: React.PropTypes.func
     },
 
     getDefaultProps: function() {
@@ -138,7 +140,9 @@ module.exports = React.createClass({displayName: 'Login',
         switch (step) {
             case 'm.login.password':
                 return (
-                    <PasswordLogin onSubmit={this.onPasswordLogin} />
+                    <PasswordLogin
+                        onSubmit={this.onPasswordLogin}
+                        onForgotPasswordClick={this.props.onForgotPasswordClick} />
                 );
             case 'm.login.cas':
                 return (
diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js
index 7f6e408fef2e85b499ea1708e11981957cc5e4cb..f89d65d7404bce5a98006173f344b915a3a3d1ce 100644
--- a/src/components/structures/login/Registration.js
+++ b/src/components/structures/login/Registration.js
@@ -19,7 +19,6 @@ limitations under the License.
 var React = require('react');
 
 var sdk = require('../../../index');
-var MatrixClientPeg = require('../../../MatrixClientPeg');
 var dis = require('../../../dispatcher');
 var Signup = require("../../../Signup");
 var ServerConfig = require("../../views/login/ServerConfig");
@@ -40,6 +39,9 @@ module.exports = React.createClass({
         hsUrl: React.PropTypes.string,
         isUrl: React.PropTypes.string,
         email: React.PropTypes.string,
+        username: React.PropTypes.string,
+        guestAccessToken: React.PropTypes.string,
+        disableUsernameChanges: React.PropTypes.bool,
         // registration shouldn't know or care how login is done.
         onLoginClick: React.PropTypes.func.isRequired
     },
@@ -63,6 +65,7 @@ module.exports = React.createClass({
         this.registerLogic.setSessionId(this.props.sessionId);
         this.registerLogic.setRegistrationUrl(this.props.registrationUrl);
         this.registerLogic.setIdSid(this.props.idSid);
+        this.registerLogic.setGuestAccessToken(this.props.guestAccessToken);
         this.registerLogic.recheckState();
     },
 
@@ -186,7 +189,9 @@ module.exports = React.createClass({
                 registerStep = (
                     <RegistrationForm
                         showEmail={true}
+                        defaultUsername={this.props.username}
                         defaultEmail={this.props.email}
+                        disableUsernameChanges={this.props.disableUsernameChanges}
                         minPasswordLength={MIN_PASSWORD_LENGTH}
                         onError={this.onFormValidationFailed}
                         onRegisterClick={this.onFormSubmit} />
diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js
index 3367ac32573e6258a0689042159f8e6ae2c4c901..a8751da1a78b94330fd1006797dd919039bdc062 100644
--- a/src/components/views/login/PasswordLogin.js
+++ b/src/components/views/login/PasswordLogin.js
@@ -22,7 +22,8 @@ var ReactDOM = require('react-dom');
  */
 module.exports = React.createClass({displayName: 'PasswordLogin',
     propTypes: {
-        onSubmit: React.PropTypes.func.isRequired // fn(username, password)
+        onSubmit: React.PropTypes.func.isRequired, // fn(username, password)
+        onForgotPasswordClick: React.PropTypes.func // fn()
     },
 
     getInitialState: function() {
@@ -46,6 +47,16 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
     },
 
     render: function() {
+        var forgotPasswordJsx;
+
+        if (this.props.onForgotPasswordClick) {
+            forgotPasswordJsx = (
+                <a className="mx_Login_forgot" onClick={this.props.onForgotPasswordClick} href="#">
+                    Forgot your password?
+                </a>
+            );
+        }
+
         return (
             <div>
                 <form onSubmit={this.onSubmitForm}>
@@ -57,6 +68,7 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
                     value={this.state.password} onChange={this.onPasswordChanged}
                     placeholder="Password" />
                 <br />
+                {forgotPasswordJsx}
                 <input className="mx_Login_submit" type="submit" value="Log in" />
                 </form>
             </div>
diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js
index bc461dd1bb5f6baf44476f925b352f8fe48db71a..534464a4ae8df07307e6427d34353840cde95c1e 100644
--- a/src/components/views/login/RegistrationForm.js
+++ b/src/components/views/login/RegistrationForm.js
@@ -30,6 +30,7 @@ module.exports = React.createClass({
         defaultUsername: React.PropTypes.string,
         showEmail: React.PropTypes.bool,
         minPasswordLength: React.PropTypes.number,
+        disableUsernameChanges: React.PropTypes.bool,
         onError: React.PropTypes.func,
         onRegisterClick: React.PropTypes.func // onRegisterClick(Object) => ?Promise
     },
@@ -109,7 +110,8 @@ module.exports = React.createClass({
                     {emailSection}
                     <br />
                     <input className="mx_Login_field" type="text" ref="username"
-                        placeholder="User name" defaultValue={this.state.username} />
+                        placeholder="User name" defaultValue={this.state.username}
+                        disabled={this.props.disableUsernameChanges} />
                     <br />
                     <input className="mx_Login_field" type="password" ref="password"
                         placeholder="Password" defaultValue={this.state.password} />
diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index 083cee46a21c5c15524b1e9ffda41fbfabe173ab..a3ad033acc9ae36675ddaaeee1dad4e79fc3c5ca 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -65,8 +65,17 @@ function mdownToHtml(mdown) {
 module.exports = React.createClass({
     displayName: 'MessageComposer',
 
+    statics: {
+        // the height we limit the composer to
+        MAX_HEIGHT: 100,
+    },
+
     propTypes: {
-        tabComplete: React.PropTypes.any
+        tabComplete: React.PropTypes.any,
+
+        // a callback which is called when the height of the composer is
+        // changed due to a change in content.
+        onResize: React.PropTypes.func,
     },
 
     componentWillMount: function() {
@@ -237,13 +246,15 @@ module.exports = React.createClass({
         // scrollHeight is at least equal to clientHeight, so we have to
         // temporarily crimp clientHeight to 0 to get an accurate scrollHeight value
         this.refs.textarea.style.height = "0px";
-        var newHeight = this.refs.textarea.scrollHeight < 100 ? this.refs.textarea.scrollHeight : 100;
+        var newHeight = Math.min(this.refs.textarea.scrollHeight,
+                                 this.constructor.MAX_HEIGHT);
         this.refs.textarea.style.height = Math.ceil(newHeight) + "px";
-        if (this.props.roomView) {
+        this.oldScrollHeight = this.refs.textarea.scrollHeight;
+
+        if (this.props.onResize) {
             // kick gemini-scrollbar to re-layout
-            this.props.roomView.forceUpdate();
+            this.props.onResize();
         }
-        this.oldScrollHeight = this.refs.textarea.scrollHeight;
     },
 
     onKeyUp: function(ev) {
diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js
index 74ffa7f5a7943b94a67fd01231fec563a8d10d91..c5e37521c5b0c4cbe0be3a957679c5b9124393ed 100644
--- a/src/components/views/rooms/RoomSettings.js
+++ b/src/components/views/rooms/RoomSettings.js
@@ -75,6 +75,14 @@ module.exports = React.createClass({
         };
     },
 
+    canGuestsJoin: function() {
+        return this.refs.guests_join.checked;
+    },
+
+    canGuestsRead: function() {
+        return this.refs.guests_read.checked;
+    },
+
     getTopic: function() {
         return this.refs.topic.value;
     },
@@ -146,6 +154,10 @@ module.exports = React.createClass({
         if (history_visibility) history_visibility = history_visibility.getContent().history_visibility;
 
         var power_levels = this.props.room.currentState.getStateEvents('m.room.power_levels', '');
+        var guest_access = this.props.room.currentState.getStateEvents('m.room.guest_access', '');
+        if (guest_access) {
+            guest_access = guest_access.getContent().guest_access;
+        }
 
         var events_levels = power_levels.events || {};
 
@@ -281,10 +293,19 @@ module.exports = React.createClass({
                 <textarea className="mx_RoomSettings_description" placeholder="Topic" defaultValue={topic} ref="topic"/> <br/>
                 <label><input type="checkbox" ref="is_private" defaultChecked={join_rule != "public"}/> Make this room private</label> <br/>
                 <label><input type="checkbox" ref="share_history" defaultChecked={history_visibility == "shared"}/> Share message history with new users</label> <br/>
+                <label>
+                    <input type="checkbox" ref="guests_read" defaultChecked={history_visibility === "world_readable"}/>
+                    Allow guests to read messages in this room
+                </label> <br/>
+                <label>
+                    <input type="checkbox" ref="guests_join" defaultChecked={guest_access === "can_join"}/>
+                    Allow guests to join this room
+                </label> <br/>
                 <label className="mx_RoomSettings_encrypt"><input type="checkbox" /> Encrypt room</label>
 
                 { room_colors_section }
 
+
                 <h3>Power levels</h3>
                 <div className="mx_RoomSettings_powerLevels mx_RoomSettings_settings">
                     <div>
diff --git a/src/components/views/voip/CallView.js b/src/components/views/voip/CallView.js
index bc825561631c59d2a1ea41c3eb8ad876b5a7c6dc..ed44313b9e4fb42389a556e84c2dd1055f77efb4 100644
--- a/src/components/views/voip/CallView.js
+++ b/src/components/views/voip/CallView.js
@@ -33,6 +33,12 @@ var MatrixClientPeg = require("../../../MatrixClientPeg");
 module.exports = React.createClass({
     displayName: 'CallView',
 
+    propTypes: {
+        // a callback which is called when the video within the callview
+        // due to a change in video metadata
+        onResize: React.PropTypes.func,
+    },
+
     componentDidMount: function() {
         this.dispatcherRef = dis.register(this.onAction);
         if (this.props.room) {
@@ -97,7 +103,7 @@ module.exports = React.createClass({
     render: function(){
         var VideoView = sdk.getComponent('voip.VideoView');
         return (
-            <VideoView ref="video" onClick={ this.props.onClick }/>
+            <VideoView ref="video" onClick={ this.props.onClick } onResize={ this.props.onResize }/>
         );
     }
 });
diff --git a/src/components/views/voip/VideoFeed.js b/src/components/views/voip/VideoFeed.js
index e4833dba9f252217c06865b73cc979353b39abec..c4a65d11458e1fa4bf93b778667398abf8afcf0e 100644
--- a/src/components/views/voip/VideoFeed.js
+++ b/src/components/views/voip/VideoFeed.js
@@ -21,9 +21,29 @@ var React = require('react');
 module.exports = React.createClass({
     displayName: 'VideoFeed',
 
+    propTypes: {
+        // a callback which is called when the video element is resized
+        // due to a change in video metadata
+        onResize: React.PropTypes.func,
+    },
+
+    componentDidMount() {
+        this.refs.vid.addEventListener('resize', this.onResize);
+    },
+
+    componentWillUnmount() {
+        this.refs.vid.removeEventListener('resize', this.onResize);
+    },
+
+    onResize: function(e) {
+        if(this.props.onResize) {
+            this.props.onResize(e);
+        }
+    },
+
     render: function() {
         return (
-            <video>
+            <video ref="vid">
             </video>
         );
     },
diff --git a/src/components/views/voip/VideoView.js b/src/components/views/voip/VideoView.js
index 15ce14a29939a48dcb0737b0f62616368a46e57c..08e587f47f02d0e6d1e9da24a0d7d67803a439f1 100644
--- a/src/components/views/voip/VideoView.js
+++ b/src/components/views/voip/VideoView.js
@@ -85,7 +85,7 @@ module.exports = React.createClass({
         return (
             <div className="mx_VideoView" ref={this.setContainer} onClick={ this.props.onClick }>
                 <div className="mx_VideoView_remoteVideoFeed">
-                    <VideoFeed ref="remote"/>
+                    <VideoFeed ref="remote" onResize={this.props.onResize}/>
                     <audio ref="remoteAudio"/>
                 </div>
                 <div className="mx_VideoView_localVideoFeed">