diff --git a/examples/trivial/fonts b/examples/trivial/fonts
new file mode 120000
index 00000000..27f04cad
--- /dev/null
+++ b/examples/trivial/fonts
@@ -0,0 +1 @@
+../../skins/base/fonts/
\ No newline at end of file
diff --git a/examples/trivial/index.js b/examples/trivial/index.js
index 2be90549..b601cfce 100644
--- a/examples/trivial/index.js
+++ b/examples/trivial/index.js
@@ -40,9 +40,17 @@ function routeUrl(location) {
             }
         }
         window.matrixChat.showScreen('register', params);
+    } else {
+        window.matrixChat.showScreen(location.hash.substring(2));
     }
 }
 
+function onHashChange(ev) {
+    routeUrl(window.location);
+}
+
+window.addEventListener('hashchange', onHashChange);
+
 var loaded = false;
 
 window.onload = function() {
diff --git a/skins/base/css/molecules/MemberInfo.css b/skins/base/css/molecules/MemberInfo.css
index db41c51e..144212d7 100644
--- a/skins/base/css/molecules/MemberInfo.css
+++ b/skins/base/css/molecules/MemberInfo.css
@@ -55,6 +55,8 @@ limitations under the License.
 
 .mx_MemberInfo_field {
     padding: 6px;
+    overflow: hidden;
+    text-overflow: ellipsis;
 }
 
 .mx_MemberInfo_button {
diff --git a/skins/base/css/molecules/MemberTile.css b/skins/base/css/molecules/MemberTile.css
index 2f735f62..d47768f3 100644
--- a/skins/base/css/molecules/MemberTile.css
+++ b/skins/base/css/molecules/MemberTile.css
@@ -17,6 +17,7 @@ limitations under the License.
 .mx_MemberTile {
     cursor: pointer;
     display: table-row;
+    height: 49px;
 }
 
 .mx_MemberTile_avatar {
@@ -36,6 +37,24 @@ limitations under the License.
     background-color: #dbdbdb;
 }
 
+.mx_MemberTile_inviteEditing .mx_MemberTile_avatar {
+    display: none;
+}
+
+.mx_MemberTile_inviteEditing .mx_MemberTile_name {
+    position: absolute;
+    width: 200px;
+}
+
+.mx_MemberTile_inviteEditing .mx_MemberTile_name input {
+    border-radius: 3px;
+    border: 1px solid #c7c7c7;
+    font-weight: 300;
+    font-size: 14px;
+    padding: 9px;
+    margin-top: 6px;
+}
+
 .mx_MemberTile_power {
     z-index: 10;
     position: absolute;
@@ -52,10 +71,14 @@ limitations under the License.
     text-overflow: ellipsis;
 }
 
-.mx_MemberTile_unavailable {
+.mx_MemberTile_unavailable .mx_MemberTile_avatar,
+.mx_MemberTile_unavailable .mx_MemberTile_name
+{
     opacity: 0.75;
 }
 
-.mx_MemberTile_offline {
+.mx_MemberTile_offline .mx_MemberTile_avatar,
+.mx_MemberTile_offline .mx_MemberTile_name
+{
     opacity: 0.5;
 }
\ No newline at end of file
diff --git a/skins/base/css/molecules/RoomHeader.css b/skins/base/css/molecules/RoomHeader.css
index c107e181..67743877 100644
--- a/skins/base/css/molecules/RoomHeader.css
+++ b/skins/base/css/molecules/RoomHeader.css
@@ -43,6 +43,26 @@ limitations under the License.
     width: 100%;
 }
 
+.mx_RoomHeader_hangupButton {
+    height: 48px;
+    margin-top: 18px;
+    background-color: #80cef4;
+    border-radius: 48px;
+    margin-right: 8px;
+    color: #fff;
+    line-height: 48px;
+    text-align: center;
+
+    -webkit-box-ordinal-group: 2;
+    -moz-box-ordinal-group: 2;
+    -ms-flex-order: 2;
+    -webkit-order: 2;
+    order: 2;
+
+    -webkit-flex: 0 0 90px;
+    flex: 0 0 90px;
+}
+
 .mx_RoomHeader_rightRow {
     height: 48px;
     margin-top: 18px;
@@ -50,11 +70,11 @@ limitations under the License.
     border-radius: 48px;
     border: 1px solid #a9dbf4;
 
-    -webkit-box-ordinal-group: 2;
-    -moz-box-ordinal-group: 2;
-    -ms-flex-order: 2;
-    -webkit-order: 2;
-    order: 2;
+    -webkit-box-ordinal-group: 3;
+    -moz-box-ordinal-group: 3;
+    -ms-flex-order: 3;
+    -webkit-order: 3;
+    order: 3;
 
     -webkit-flex: 0 0 200px;
     flex: 0 0 200px;    
@@ -66,6 +86,15 @@ limitations under the License.
     vertical-align: middle;
 }
 
+.mx_RoomHeader_simpleHeader {
+    line-height: 88px;
+    color: #80cef4;
+    font-weight: 400;
+    font-size: 20px;
+    overflow: scroll;
+    text-overflow: ellipsis;
+}
+
 .mx_RoomHeader_name {
     vertical-align: middle;
     height: 28px;
diff --git a/skins/base/css/organisms/LeftPanel.css b/skins/base/css/organisms/LeftPanel.css
index ac10942d..bfc43026 100644
--- a/skins/base/css/organisms/LeftPanel.css
+++ b/skins/base/css/organisms/LeftPanel.css
@@ -43,7 +43,7 @@ limitations under the License.
     overflow-y: scroll;
 }
 
-.mx_LeftPanel .mx_DirectoryMenu {
+.mx_LeftPanel .mx_BottomLeftMenu {
     -webkit-box-ordinal-group: 3;
     -moz-box-ordinal-group: 3;
     -ms-flex-order: 3;
@@ -56,15 +56,15 @@ limitations under the License.
     border-top: 1px solid #f3f8fa;
 }
 
-.mx_LeftPanel .mx_DirectoryMenu .mx_RoomTile {
+.mx_LeftPanel .mx_BottomLeftMenu .mx_RoomTile {
     color: #378bb4;
 }
 
-.mx_LeftPanel .mx_DirectoryMenu .mx_RoomTile_avatar {
+.mx_LeftPanel .mx_BottomLeftMenu .mx_RoomTile_avatar {
     padding-left: 14px;
 }
 
-.mx_LeftPanel .mx_DirectoryMenu .mx_DirectoryMenu_options {
+.mx_LeftPanel .mx_BottomLeftMenu .mx_BottomLeftMenu_options {
     margin-top: 12px;
     width: 100%;
 }
\ No newline at end of file
diff --git a/skins/base/css/organisms/RoomDirectory.css b/skins/base/css/organisms/RoomDirectory.css
new file mode 100644
index 00000000..9ca7811f
--- /dev/null
+++ b/skins/base/css/organisms/RoomDirectory.css
@@ -0,0 +1,40 @@
+/*
+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.
+*/
+
+.mx_RoomDirectory {
+    max-width: 720px;
+    margin: auto;
+}
+
+.mx_RoomDirectory_input {
+    margin: auto;
+    border-radius: 3px;
+    border: 1px solid #c7c7c7;
+    font-weight: 300;
+    font-size: 14px;
+    padding: 9px;
+    margin-top: 12px;
+    margin-bottom: 12px;
+}
+
+.mx_RoomDirectory_table {
+    width: 100%;
+}
+
+.mx_RoomDirectory_table td,
+.mx_RoomDirectory_table th, {
+    padding: 6px;
+}
\ No newline at end of file
diff --git a/skins/base/css/organisms/RoomView.css b/skins/base/css/organisms/RoomView.css
index 88f52aa6..77893741 100644
--- a/skins/base/css/organisms/RoomView.css
+++ b/skins/base/css/organisms/RoomView.css
@@ -105,6 +105,20 @@ limitations under the License.
     border-top: 1px solid #a8dbf3;
 }
 
+.mx_RoomView_typingBar {
+    margin-top: 17px;
+    margin-left: 56px;
+    color: #818794;
+}
+
+.mx_RoomView_typingBar img {
+    padding-left: 12px;
+    padding-right: 12px;
+    margin-left: -64px;
+    margin-top: -7px;
+    float: left;    
+}
+
 .mx_RoomView .mx_MessageComposer {
     -webkit-box-ordinal-group: 5;
     -moz-box-ordinal-group: 5;
diff --git a/skins/base/img/typing.png b/skins/base/img/typing.png
new file mode 100644
index 00000000..066a0ce8
Binary files /dev/null and b/skins/base/img/typing.png differ
diff --git a/skins/base/views/atoms/EditableText.js b/skins/base/views/atoms/EditableText.js
index a4508744..38aa5c8d 100644
--- a/skins/base/views/atoms/EditableText.js
+++ b/skins/base/views/atoms/EditableText.js
@@ -33,6 +33,7 @@ module.exports = React.createClass({
     },
 
     onClickDiv: function() {
+        console.log("onClickDiv triggered");
         this.setState({
             phase: this.Phases.Edit,
         })
@@ -57,12 +58,12 @@ module.exports = React.createClass({
             if (this.state.value) {
                 editable_el = <div ref="display_div" onClick={this.onClickDiv}>{this.state.value}</div>;
             } else {
-                editable_el = <div ref="display_div" onClick={this.onClickDiv}><i>{this.props.placeHolder}</i></div>;
+                editable_el = <div ref="display_div" onClick={this.onClickDiv}>{this.props.label}</div>;
             }
         } else if (this.state.phase == this.Phases.Edit) {
             editable_el = (
                 <div>
-                    <input type="text" defaultValue={this.state.value} onKeyUp={this.onKeyUp} onFocus={this.onFocus} onBlur={this.onFinish} autoFocus/>
+                    <input type="text" defaultValue={this.state.value} onKeyUp={this.onKeyUp} onFocus={this.onFocus} onBlur={this.onFinish} placeholder={this.props.placeHolder} autoFocus/>
                 </div>
             );
         }
diff --git a/skins/base/views/molecules/DirectoryMenu.js b/skins/base/views/molecules/BottomLeftMenu.js
similarity index 83%
rename from skins/base/views/molecules/DirectoryMenu.js
rename to skins/base/views/molecules/BottomLeftMenu.js
index 8ffb4180..d2340d7b 100644
--- a/skins/base/views/molecules/DirectoryMenu.js
+++ b/skins/base/views/molecules/BottomLeftMenu.js
@@ -21,34 +21,34 @@ var classNames = require('classnames');
 
 var dis = require("../../../../src/dispatcher");
 
-//var DirectoryMenuController = require("../../../../src/controllers/molecules/DirectoryMenuController");
-
 var MatrixClientPeg = require("../../../../src/MatrixClientPeg");
 
 module.exports = React.createClass({
-    displayName: 'DirectoryMenu',
-    // mixins: [DirectoryMenuController],
+    displayName: 'BottomLeftMenu',
 
-    // FIXME: should these onClicks be in the controller instead?
     onSettingsClick: function() {
         dis.dispatch({action: 'view_user_settings'});
     },
 
+    onRoomDirectoryClick: function() {
+        dis.dispatch({action: 'view_room_directory'});
+    },
+
     onCreateRoomClick: function() {
         dis.dispatch({action: 'view_create_room'});
     },
 
     render: function() {
         return (
-            <div className="mx_DirectoryMenu">
-                <div className="mx_DirectoryMenu_options">
+            <div className="mx_BottomLeftMenu">
+                <div className="mx_BottomLeftMenu_options">
                     <div className="mx_RoomTile" onClick={this.onCreateRoomClick}>
                         <div className="mx_RoomTile_avatar">
                             <img src="img/create-big.png" width="42" height="42"/>
                         </div>
                         <div className="mx_RoomTile_name">Create new room</div>
                     </div>
-                    <div className="mx_RoomTile">
+                    <div className="mx_RoomTile" onClick={this.onRoomDirectoryClick}>
                         <div className="mx_RoomTile_avatar">
                             <img src="img/directory-big.png" width="42" height="42"/>
                         </div>
diff --git a/skins/base/views/molecules/MemberTile.js b/skins/base/views/molecules/MemberTile.js
index 1286173d..8dd3f00a 100644
--- a/skins/base/views/molecules/MemberTile.js
+++ b/skins/base/views/molecules/MemberTile.js
@@ -59,8 +59,7 @@ module.exports = React.createClass({
         mainClassName += presenceClass;
 
         return (
-            <div className={mainClassName} onMouseEnter={ this.mouseEnter } onMouseLeave={ this.mouseLeave }
-            >
+            <div className={mainClassName} onMouseEnter={ this.mouseEnter } onMouseLeave={ this.mouseLeave }>
                 <div className="mx_MemberTile_avatar">
                     <img className="mx_MemberTile_avatarImg"
                          src={ this.props.member ? MatrixClientPeg.get().getAvatarUrlForMember(this.props.member, 40, 40, "crop") : null }
diff --git a/skins/base/views/molecules/RoomHeader.js b/skins/base/views/molecules/RoomHeader.js
index 6d01c6a4..fdf4d7e5 100644
--- a/skins/base/views/molecules/RoomHeader.js
+++ b/skins/base/views/molecules/RoomHeader.js
@@ -39,46 +39,55 @@ module.exports = React.createClass({
 
     render: function() {
 
-        var topic = this.props.room.currentState.getStateEvents('m.room.topic', '');
+        var header;
+        if (this.props.simpleHeader) {
+            header =
+                <div className="mx_RoomHeader_wrapper">
+                    <div className="mx_RoomHeader_simpleHeader">
+                        { this.props.simpleHeader }
+                    </div>
+                </div>
+        }
+        else {
+            var topic = this.props.room.currentState.getStateEvents('m.room.topic', '');
 
-        var callButtons;
-        if (this.state) {
-            switch (this.state.call_state) {
-                case "ringback":
-                case "connected":
-                    callButtons = (
-                        <div className="mx_RoomHeader_button" onClick={this.onHangupClick}>
-                        BYEBYE
-                        </div>
-                    );
-                    break;
+            var callButtons;
+            if (this.state) {
+                switch (this.state.call_state) {
+                    case "ringback":
+                    case "connected":
+                        callButtons = (
+                            <div className="mx_RoomHeader_hangupButton" onClick={this.onHangupClick}>
+                                End call
+                            </div>
+                        );
+                        break;
+                }
             }
-        }
 
-        var name = null;
-        var topic_el = null;
-        var save_button = null;
-        var settings_button = null;
-        if (this.props.editing) {
-            name = <input type="text" defaultValue={this.props.room.name} ref="name_edit"/>;
-            // if (topic) topic_el = <div className="mx_RoomHeader_topic"><textarea>{ topic.getContent().topic }</textarea></div>
-            save_button = (
-                <div className="mx_RoomHeader_button"onClick={this.props.onSaveClick}>
-                    Save
-                </div>
-            );
-        } else {
-            name = <EditableText initialValue={this.props.room.name} onValueChanged={this.onNameChange} />;
-            if (topic) topic_el = <div className="mx_RoomHeader_topic">{ topic.getContent().topic }</div>;
-            settings_button = (
-                <div className="mx_RoomHeader_button" onClick={this.props.onSettingsClick}>
-                    <img src="img/settings.png" width="32" height="32"/>
-                </div>
-            );
-        }
+            var name = null;
+            var topic_el = null;
+            var save_button = null;
+            var settings_button = null;
+            if (this.props.editing) {
+                name = <input type="text" defaultValue={this.props.room.name} ref="name_edit"/>;
+                // if (topic) topic_el = <div className="mx_RoomHeader_topic"><textarea>{ topic.getContent().topic }</textarea></div>
+                save_button = (
+                    <div className="mx_RoomHeader_button"onClick={this.props.onSaveClick}>
+                        Save
+                    </div>
+                );
+            } else {
+                name = <EditableText initialValue={this.props.room.name} onValueChanged={this.onNameChange} />;
+                if (topic) topic_el = <div className="mx_RoomHeader_topic">{ topic.getContent().topic }</div>;
+                settings_button = (
+                    <div className="mx_RoomHeader_button" onClick={this.props.onSettingsClick}>
+                        <img src="img/settings.png" width="32" height="32"/>
+                    </div>
+                );
+            }
 
-        return (
-            <div className="mx_RoomHeader">
+            header =
                 <div className="mx_RoomHeader_wrapper">
                     <div className="mx_RoomHeader_leftRow">
                         <div className="mx_RoomHeader_avatar">
@@ -91,13 +100,13 @@ module.exports = React.createClass({
                             { topic_el }
                         </div>
                     </div>
+                    {callButtons}
                     <div className="mx_RoomHeader_rightRow">
                         { save_button }
                         { settings_button }
                         <div className="mx_RoomHeader_button">
                             <img src="img/search.png" width="32" height="32"/>
                         </div>
-                        {callButtons}
                         <div className="mx_RoomHeader_button" onClick={this.onVideoClick}>
                             <img src="img/video.png" width="32" height="32"/>
                         </div>
@@ -106,6 +115,11 @@ module.exports = React.createClass({
                         </div>
                     </div>
                 </div>
+        }
+
+        return (
+            <div className="mx_RoomHeader">
+                { header }
             </div>
         );
     },
diff --git a/skins/base/views/organisms/LeftPanel.js b/skins/base/views/organisms/LeftPanel.js
index 252a5d06..16575910 100644
--- a/skins/base/views/organisms/LeftPanel.js
+++ b/skins/base/views/organisms/LeftPanel.js
@@ -20,7 +20,7 @@ var React = require('react');
 var ComponentBroker = require('../../../../src/ComponentBroker');
 
 var RoomList = ComponentBroker.get('organisms/RoomList');
-var DirectoryMenu = ComponentBroker.get('molecules/DirectoryMenu');
+var BottomLeftMenu = ComponentBroker.get('molecules/BottomLeftMenu');
 var IncomingCallBox = ComponentBroker.get('molecules/voip/IncomingCallBox');
 var RoomCreate = ComponentBroker.get('molecules/RoomCreate');
 
@@ -33,7 +33,7 @@ module.exports = React.createClass({
                 <img className="mx_LeftPanel_hideButton" src="img/hide.png" width="32" height="32" alt="<"/>
                 <IncomingCallBox />
                 <RoomList selectedRoom={this.props.selectedRoom} />
-                <DirectoryMenu />
+                <BottomLeftMenu />
             </div>
         );
     }
diff --git a/skins/base/views/organisms/MemberList.js b/skins/base/views/organisms/MemberList.js
index 3e586e56..dfeecb03 100644
--- a/skins/base/views/organisms/MemberList.js
+++ b/skins/base/views/organisms/MemberList.js
@@ -17,6 +17,7 @@ limitations under the License.
 'use strict';
 
 var React = require('react');
+var classNames = require('classnames');
 
 var MemberListController = require("../../../../src/controllers/organisms/MemberList");
 
@@ -30,6 +31,10 @@ module.exports = React.createClass({
     displayName: 'MemberList',
     mixins: [MemberListController],
 
+    getInitialState: function() {
+        return { editing: false };
+    },
+
     // FIXME: combine this more nicely with the MemberInfo positioning stuff...
     onMemberListScroll: function(ev) {
         if (this.refs.memberListScroll) {
@@ -55,23 +60,40 @@ module.exports = React.createClass({
     onPopulateInvite: function(inputText, shouldSubmit) {
         // reset back to placeholder
         this.refs.invite.setValue("Invite", false, true);
+        this.setState({ editing: false });
         if (!shouldSubmit) {
             return; // enter key wasn't pressed
         }
         this.onInvite(inputText);
     },
 
+    onClickInvite: function(ev) {
+        this.setState({ editing: true });
+        this.refs.invite.onClickDiv();
+        console.log("forcing update on memberlist after having clicked invite");
+        ev.stopPropagation();
+        ev.preventDefault();
+    },
+
     inviteTile: function() {
-        if (this.state.inviting) {
-            return (
-                <div></div>
-            );
-        }
+        // if (this.state.inviting) {
+        //     return (
+        //         <div></div>
+        //     );
+        // }
+
+        var classes = classNames({
+            mx_MemberTile: true,
+            mx_MemberTile_inviteEditing: this.state.editing,
+        });
+
+        console.log("rendering inviteTile, with phase as " + (this.refs.invite ? this.refs.invite.state.phase : "unknown"));
+
         return (
-            <div className="mx_MemberTile">
+            <div className={ classes } onClick={ this.onClickInvite } >
                 <div className="mx_MemberTile_avatar"><img src="img/create-big.png" width="40" height="40" alt=""/></div>            
                 <div className="mx_MemberTile_name">
-                    <EditableText ref="invite" placeHolder="Invite" onValueChanged={this.onPopulateInvite}/>
+                    <EditableText ref="invite" label="Invite" placeHolder="@user:domain.com" initialValue="" onValueChanged={this.onPopulateInvite}/>
                 </div>
             </div>
         );
diff --git a/skins/base/views/organisms/RightPanel.js b/skins/base/views/organisms/RightPanel.js
index e1634adc..5b6477a0 100644
--- a/skins/base/views/organisms/RightPanel.js
+++ b/skins/base/views/organisms/RightPanel.js
@@ -24,20 +24,53 @@ var MemberList = ComponentBroker.get('organisms/MemberList');
 module.exports = React.createClass({
     displayName: 'RightPanel',
 
+    Phase : {
+        Blank: 'Blank',
+        None: 'None',
+        MemberList: 'MemberList',
+        FileList: 'FileList',
+    },
+
+    getInitialState: function() {
+        return {
+            phase : this.Phase.None
+        }
+    },
+
+    onMemberListButtonClick: function() {
+        if (this.state.phase == this.Phase.None) {
+            this.setState({ phase: this.Phase.MemberList });            
+        }
+        else {
+            this.setState({ phase: this.Phase.None });
+        }
+    },
+
     render: function() {
-        return (
-            <div className="mx_RightPanel">
-                <div className="mx_RightPanel_header">
+        var buttonGroup;
+        var panel;
+        if (this.props.roomId) {
+            buttonGroup =
                     <div className="mx_RightPanel_headerButtonGroup">
                         <div className="mx_RightPanel_headerButton">
                             <img src="img/file.png" width="32" height="32" alt="Files"/>
                         </div>
-                        <div className="mx_RightPanel_headerButton">
+                        <div className="mx_RightPanel_headerButton" onClick={ this.onMemberListButtonClick }>
                             <img src="img/members.png" width="32" height="32" alt="Members"/>
                         </div>
-                    </div>
+                    </div>;
+
+            if (this.state.phase == this.Phase.MemberList) {
+                panel = <MemberList roomId={this.props.roomId} key={this.props.roomId} />
+            }
+        }
+
+        return (
+            <div className="mx_RightPanel">
+                <div className="mx_RightPanel_header">
+                    { buttonGroup }
                 </div>
-                <MemberList roomId={this.props.roomId} key={this.props.roomId} />
+                { panel }
             </div>
         );
     }
diff --git a/skins/base/views/organisms/RoomDirectory.js b/skins/base/views/organisms/RoomDirectory.js
new file mode 100644
index 00000000..7ffb4977
--- /dev/null
+++ b/skins/base/views/organisms/RoomDirectory.js
@@ -0,0 +1,130 @@
+/*
+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.
+*/
+
+'use strict';
+
+var React = require('react');
+
+var MatrixClientPeg = require("../../../../src/MatrixClientPeg");
+var Modal = require("../../../../src/Modal");
+var ComponentBroker = require('../../../../src/ComponentBroker');
+var ErrorDialog = ComponentBroker.get("organisms/ErrorDialog");
+var RoomHeader = ComponentBroker.get('molecules/RoomHeader');
+var dis = require("../../../../src/dispatcher");
+
+
+module.exports = React.createClass({
+    displayName: 'RoomDirectory',
+
+    getInitialState: function() {
+        return {
+            publicRooms: [],
+            roomAlias: '',
+        }
+    },
+
+    componentDidMount: function() {
+        var self = this;
+        MatrixClientPeg.get().publicRooms(function (err, data) {
+            if (err) {
+                console.error("Failed to get publicRooms: %s", JSON.stringify(err));
+                Modal.createDialog(ErrorDialog, {
+                    title: "Failed to get public room list",
+                    description: err.message
+                });
+            }
+            else {
+                self.setState({
+                    publicRooms: data.chunk
+                });
+                self.forceUpdate();
+            }
+        });
+    },
+
+    joinRoom: function(roomId) {        
+        // XXX: check that JS SDK suppresses duplicate attempts to join the same room
+        MatrixClientPeg.get().joinRoom(roomId).done(function() {
+            dis.dispatch({
+                action: 'view_room',
+                room_id: roomId
+            });
+        }, function(err) {
+            console.error("Failed to join room: %s", JSON.stringify(err));
+            Modal.createDialog(ErrorDialog, {
+                title: "Failed to join room",
+                description: err.message
+            });
+        });
+    },
+
+    getRows: function(filter) {
+        if (!this.state.publicRooms) return [];
+
+        var rooms = this.state.publicRooms.filter(function(a) {
+            // FIXME: if incrementally typing, keep narrowing down the search set
+            return (a.aliases[0].search(filter) >= 0);
+        }).sort(function(a,b) {
+            return a.num_joined_members > b.num_joined_members;
+        });
+        var rows = [];
+        var self = this;
+        for (var i = 0; i < rooms.length; i++) {
+            var name = rooms[i].name;
+            if (!name) {
+                if (rooms[i].aliases[0]) name = rooms[i].aliases[0] 
+            }
+            else {
+                if (rooms[i].aliases[0]) name += " (" + rooms[i].aliases[0] + ")";
+            }
+            rows.unshift(
+                <tr key={ rooms[i].room_id } onClick={ function() { self.joinRoom(rooms[i].room_id); } }>
+                    <td><img src={ MatrixClientPeg.get().getAvatarUrlForRoom(rooms[i].room_id, 40, 40, "crop") } width="40" height="40" alt=""/> { name }</td>
+                    <td>{ rooms[i].topic }</td>
+                    <td style={ {'text-align' : 'center'} }>{ rooms[i].num_joined_members }</td>
+                </tr>
+            );
+        }
+        return rows;
+    },
+
+    onKeyUp: function(ev) {
+        this.forceUpdate();
+        this.setState({ roomAlias : this.refs.roomAlias.getDOMNode().value })
+        if (ev.key == "Enter") {
+            this.joinRoom(this.refs.roomAlias.getDOMNode().value);
+        }
+        if (ev.key == "Down") {
+
+        }
+    },
+
+    render: function() {
+        return (
+            <div className="mx_RoomDirectory">
+                <RoomHeader simpleHeader="Public Rooms" />
+                <div className="mx_RoomDirectory_list">
+                    <input ref="roomAlias" placeholder="Join a room (e.g. #foo:domain.com)" className="mx_RoomDirectory_input" size="64" onKeyUp={ this.onKeyUp }/>
+                    <table className="mx_RoomDirectory_table">
+                        <tr><th>Room</th><th>Topic</th><th>Users</th></tr>
+                        { this.getRows(this.state.roomAlias) }
+                    </table>
+                </div>
+            </div>
+        );
+    }
+});
+
diff --git a/skins/base/views/organisms/RoomView.js b/skins/base/views/organisms/RoomView.js
index 56699165..4f19b9b3 100644
--- a/skins/base/views/organisms/RoomView.js
+++ b/skins/base/views/organisms/RoomView.js
@@ -166,6 +166,7 @@ module.exports = React.createClass({
                 if (typingString) {
                     statusBar = (
                         <div className="mx_RoomView_typingBar">
+                            <img src="img/typing.png" width="40" height="40" alt=""/>
                             {typingString}
                         </div>
                     );
diff --git a/skins/base/views/organisms/UserSettings.js b/skins/base/views/organisms/UserSettings.js
index 9afa3c03..7a57ec76 100644
--- a/skins/base/views/organisms/UserSettings.js
+++ b/skins/base/views/organisms/UserSettings.js
@@ -73,7 +73,7 @@ module.exports = React.createClass({
                                 </div>
 
                                 <div className="mx_UserSettings_DisplayName">
-                                    <EditableText ref="displayname" initialValue={this.state.displayName} placeHolder="Click to set display name." onValueChanged={this.changeDisplayname}/>
+                                    <EditableText ref="displayname" initialValue={this.state.displayName} label="Click to set display name." onValueChanged={this.changeDisplayname}/>
                                     <div className="mx_UserSettings_DisplayName_Edit" onClick={this.editDisplayName}>Edit</div>
                                 </div>
 
diff --git a/skins/base/views/pages/MatrixChat.js b/skins/base/views/pages/MatrixChat.js
index 85a29264..73af5082 100644
--- a/skins/base/views/pages/MatrixChat.js
+++ b/skins/base/views/pages/MatrixChat.js
@@ -26,6 +26,7 @@ var Login = ComponentBroker.get('templates/Login');
 var UserSettings = ComponentBroker.get('organisms/UserSettings');
 var Register = ComponentBroker.get('templates/Register');
 var CreateRoom = ComponentBroker.get('organisms/CreateRoom');
+var RoomDirectory = ComponentBroker.get('organisms/RoomDirectory');
 
 var MatrixChatController = require("../../../../src/controllers/pages/MatrixChat");
 
@@ -59,9 +60,15 @@ module.exports = React.createClass({
                     break;
                 case this.PageTypes.UserSettings:
                     page_element = <UserSettings />
+                    right_panel = <RightPanel/>
                     break;
                 case this.PageTypes.CreateRoom:
                     page_element = <CreateRoom onRoomCreated={this.onRoomCreated}/>
+                    right_panel = <RightPanel/>
+                    break;
+                case this.PageTypes.RoomDirectory:
+                    page_element = <RoomDirectory />
+                    right_panel = <RightPanel/>
                     break;
             }
 
diff --git a/src/ComponentBroker.js b/src/ComponentBroker.js
index a9ccebd1..6e5d1e11 100644
--- a/src/ComponentBroker.js
+++ b/src/ComponentBroker.js
@@ -97,9 +97,10 @@ require('../skins/base/views/molecules/RoomSettings');
 require('../skins/base/views/organisms/LeftPanel');
 require('../skins/base/views/organisms/RightPanel');
 require('../skins/base/views/organisms/LogoutPrompt');
+require('../skins/base/views/organisms/RoomDirectory');
 require('../skins/base/views/molecules/RoomCreate');
 require('../skins/base/views/molecules/RoomDropTarget');
-require('../skins/base/views/molecules/DirectoryMenu');
+require('../skins/base/views/molecules/BottomLeftMenu');
 require('../skins/base/views/molecules/DateSeparator');
 require('../skins/base/views/atoms/voip/VideoFeed');
 require('../skins/base/views/molecules/voip/VideoView');
diff --git a/src/controllers/atoms/EditableText.js b/src/controllers/atoms/EditableText.js
index 15ee58a7..5ea4ce8c 100644
--- a/src/controllers/atoms/EditableText.js
+++ b/src/controllers/atoms/EditableText.js
@@ -22,6 +22,7 @@ module.exports = {
     propTypes: {
         onValueChanged: React.PropTypes.func,
         initialValue: React.PropTypes.string,
+        label: React.PropTypes.string,
         placeHolder: React.PropTypes.string,
     },
 
@@ -34,7 +35,8 @@ module.exports = {
         return {
             onValueChanged: function() {},
             initialValue: '',
-            placeHolder: 'Click to set',
+            label: 'Click to set',
+            placeholder: '',
         };
     },
 
@@ -77,6 +79,7 @@ module.exports = {
         this.setState({
             phase: this.Phases.Display,
         });
+        this.onValueChanged(false);
     },
 
     onValueChanged: function(shouldSubmit) {
diff --git a/src/controllers/molecules/MessageComposer.js b/src/controllers/molecules/MessageComposer.js
index a9de008d..2ece5636 100644
--- a/src/controllers/molecules/MessageComposer.js
+++ b/src/controllers/molecules/MessageComposer.js
@@ -200,7 +200,7 @@ module.exports = {
                 }, function(err) {
                     console.error("Command failure: %s", err);
                     Modal.createDialog(ErrorDialog, {
-                        title: "Server Error",
+                        title: "Server error",
                         description: err.message
                     });
                 });
@@ -208,7 +208,7 @@ module.exports = {
             else if (cmd.error) {
                 console.error(cmd.error);
                 Modal.createDialog(ErrorDialog, {
-                    title: "Command Error",
+                    title: "Command error",
                     description: cmd.error
                 });
             }
diff --git a/src/controllers/organisms/MemberList.js b/src/controllers/organisms/MemberList.js
index 06952700..29d4f20a 100644
--- a/src/controllers/organisms/MemberList.js
+++ b/src/controllers/organisms/MemberList.js
@@ -101,7 +101,7 @@ module.exports = {
         }, function(err) {
             console.error("Failed to invite: %s", JSON.stringify(err));
             Modal.createDialog(ErrorDialog, {
-                title: "Invite Server Error",
+                title: "Server error whilst inviting",
                 description: err.message
             });
             self.setState({
diff --git a/src/controllers/pages/MatrixChat.js b/src/controllers/pages/MatrixChat.js
index 1e38a215..30135a2b 100644
--- a/src/controllers/pages/MatrixChat.js
+++ b/src/controllers/pages/MatrixChat.js
@@ -33,6 +33,7 @@ module.exports = {
         RoomView: "room_view",
         UserSettings: "user_settings",
         CreateRoom: "create_room",
+        RoomDirectory: "room_directory",
     },
 
     AuxPanel: {
@@ -43,7 +44,7 @@ module.exports = {
         return {
             logged_in: !!(MatrixClientPeg.get() && MatrixClientPeg.get().credentials),
             ready: false,
-            page_type: this.PageTypes.RoomView,
+            page_type: MatrixClientPeg.get().getRooms().length ? this.PageTypes.RoomView : this.PageTypes.RoomDirectory,
             aux_panel: null,
         };
     },
@@ -127,6 +128,7 @@ module.exports = {
                     currentRoom: payload.room_id,
                     page_type: this.PageTypes.RoomView,
                 });
+                this.notifyNewScreen('room/'+payload.room_id);
                 break;
             case 'view_prev_room':
                 roomIndexDelta = -1;
@@ -156,6 +158,11 @@ module.exports = {
                     page_type: this.PageTypes.CreateRoom,
                 });
                 break;
+            case 'view_room_directory':
+                this.setState({
+                    page_type: this.PageTypes.RoomDirectory,
+                });
+                break;
         }
     },
 
@@ -172,13 +179,18 @@ module.exports = {
         var cli = MatrixClientPeg.get();
         var self = this;
         cli.on('syncComplete', function() {
-            var firstRoom = null;
-            if (cli.getRooms() && cli.getRooms().length) {
-                firstRoom = RoomListSorter.mostRecentActivityFirst(
-                    cli.getRooms()
-                )[0].roomId;
+            if (!self.state.currentRoom) {
+                var firstRoom = null;
+                if (cli.getRooms() && cli.getRooms().length) {
+                    firstRoom = RoomListSorter.mostRecentActivityFirst(
+                        cli.getRooms()
+                    )[0].roomId;
+                }
+                self.setState({ready: true, currentRoom: firstRoom});
+                self.notifyNewScreen('room/'+firstRoom);
+            } else {
+                self.setState({ready: true});
             }
-            self.setState({ready: true, currentRoom: firstRoom});
             dis.dispatch({action: 'focus_composer'});
         });
         cli.on('Call.incoming', function(call) {
@@ -222,6 +234,12 @@ module.exports = {
                 action: 'start_login',
                 params: params
             });
+        } else if (screen.indexOf('room/') == 0) {
+            var roomId = screen.split('/')[1];
+            dis.dispatch({
+                action: 'view_room',
+                room_id: roomId
+            });
         }
     },