summary refs log tree commit diff
path: root/webclient
diff options
context:
space:
mode:
authorKegan Dougal <kegan@matrix.org>2014-08-26 09:26:33 +0100
committerKegan Dougal <kegan@matrix.org>2014-08-26 09:26:33 +0100
commit47c3a089c5c3016197964f16f611f72bc4aadde6 (patch)
treef0a4d1a660e4ae2a58077ad1c250353781445cff /webclient
parentRemoved member list servlet: now using generic state paths. (diff)
parentOrder 'get_recent_events_for_room' correctly. (diff)
downloadsynapse-47c3a089c5c3016197964f16f611f72bc4aadde6.tar.xz
Merge branch 'develop' of github.com:matrix-org/synapse into client_server_url_rename
Diffstat (limited to 'webclient')
-rw-r--r--webclient/app-controller.js28
-rw-r--r--webclient/app-filter.js7
-rw-r--r--webclient/app.css158
-rw-r--r--webclient/app.js17
-rw-r--r--webclient/components/fileUpload/file-upload-service.js3
-rw-r--r--webclient/components/matrix/event-handler-service.js26
-rw-r--r--webclient/components/matrix/event-stream-service.js51
-rw-r--r--webclient/components/utilities/utilities-service.js67
-rw-r--r--webclient/home/home-controller.js162
-rw-r--r--webclient/home/home.html63
-rw-r--r--webclient/index.html20
-rw-r--r--webclient/login/login-controller.js4
-rw-r--r--webclient/login/login.html4
-rw-r--r--webclient/room/room-controller.js38
-rw-r--r--webclient/room/room-directive.js30
-rw-r--r--webclient/room/room.html44
-rw-r--r--webclient/rooms/rooms-controller.js288
-rw-r--r--webclient/rooms/rooms.html101
-rw-r--r--webclient/settings/settings-controller.js146
-rw-r--r--webclient/settings/settings.html73
-rw-r--r--webclient/user/user.html1
21 files changed, 768 insertions, 563 deletions
diff --git a/webclient/app-controller.js b/webclient/app-controller.js
index 96656e12c3..84cb94dc74 100644
--- a/webclient/app-controller.js
+++ b/webclient/app-controller.js
@@ -31,31 +31,15 @@ angular.module('MatrixWebClientController', ['matrixService'])
     $rootScope.$on('$routeChangeSuccess', function (event, current, previous) {
         $scope.location = $location.path();
     });
-    
-    
-    // Manage the display of the current config
-    $scope.config;
-    
-    // Toggles the config display
-    $scope.showConfig = function() {
-        if ($scope.config) {
-            $scope.config = undefined;
-        }
-        else {
-            $scope.config = matrixService.config();        
-        }
-    };
-    
-    $scope.closeConfig = function() {
-        if ($scope.config) {
-            $scope.config = undefined;
-        }
-    };
 
     if (matrixService.isUserLoggedIn()) {
-        eventStreamService.resume();
+        // eventStreamService.resume();
     }
     
+    $scope.go = function(url) {
+        $location.url(url);
+    };
+    
     // Logs the user out 
     $scope.logout = function() {
         // kill the event stream
@@ -66,7 +50,7 @@ angular.module('MatrixWebClientController', ['matrixService'])
         matrixService.saveConfig();
         
         // And go to the login page
-        $location.path("login");
+        $location.url("login");
     };
 
     // Listen to the event indicating that the access token is no longer valid.
diff --git a/webclient/app-filter.js b/webclient/app-filter.js
index 64c3bb04de..b8f4ed25bc 100644
--- a/webclient/app-filter.js
+++ b/webclient/app-filter.js
@@ -54,12 +54,15 @@ angular.module('matrixWebClient')
         });
 
         // FIXME: we shouldn't disambiguate displayNames on every orderMembersList
-        // invocation but keep track of duplicates incrementally somewhere            
+        // invocation but keep track of duplicates incrementally somewhere
         angular.forEach(displayNames, function(value, key) {
             if (value.length > 1) {
                 // console.log(key + ": " + value);
-                for (i=0; i < value.length; i++) {
+                for (var i=0; i < value.length; i++) {
                     var v = value[i];
+                    // FIXME: this permenantly rewrites the displayname for a given
+                    // room member. which means we can't reset their name if it is
+                    // no longer ambiguous!
                     members[v].displayname += " (" + v + ")";
                     // console.log(v + " " + members[v]);
                 };
diff --git a/webclient/app.css b/webclient/app.css
index d2b951d3b6..dfa17fae62 100644
--- a/webclient/app.css
+++ b/webclient/app.css
@@ -1,3 +1,71 @@
+/*** Mobile voodoo ***/
+@media all and (max-device-width: 640px) {
+            
+    #messageTableWrapper {
+        margin-right: 0px ! important;
+    }
+    
+    .leftBlock {
+        width: 8em ! important;
+    }
+    
+    #header,
+    #messageTable,
+    #wrapper,
+    #roomName,
+    #controls {
+        max-width: 640px ! important;
+    }    
+    
+    #userIdCell,
+    #usersTableWrapper,
+    #extraControls {
+        display: none;
+    }
+    
+    #buttonsCell {
+        width: 60px ! important;
+        padding-left: 20px ! important;
+    }
+    
+    #roomLogo {
+        display: none;
+    }
+    
+    #roomName {
+        text-align: left ! important;
+        top: -35px ! important;
+    }
+    
+    .bubble {
+        font-size: 12px ! important;
+        min-height: 20px ! important;
+    }
+    
+    #page {
+        top: 35px ! important;
+        bottom: 70px ! important;
+    }
+    
+    #header,
+    #page {
+        margin: 5px ! important;
+    }
+    
+    #header {
+        padding: 5px ! important;
+    }
+        
+    /* stop zoom on select */
+    select:focus,
+    textarea,
+    input
+    {
+        font-size: 16px ! important;
+    }
+    
+}
+
 body {
     font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif;
     font-size: 12pt;
@@ -17,7 +85,6 @@ h1 {
     left: 0px;
     right: 0px;
     margin: 20px;
-    margin: 20px;
 }
 
 #wrapper {
@@ -32,8 +99,7 @@ h1 {
     text-align: right;
     top: -40px;
     position: absolute;
-    font-size: 16pt;
-    margin-bottom: 10px;
+    font-size: 16px;
 }
 
 #controlPanel {
@@ -50,6 +116,10 @@ h1 {
     margin: auto;
 }
 
+#buttonsCell {
+    width: 150px;
+}
+
 #inputBarTable {
     width: 100%;
 }
@@ -111,13 +181,13 @@ h1 {
     color: #fff;
     margin: 2px;
     bottom: 0px;
-    font-size: 8pt;
+    font-size: 12px;
     word-break: break-all;
 }
 
 .userPresence {
     text-align: center;
-    font-size: 8pt;
+    font-size: 12px;
     color: #fff;
     background-color: #aaa;
     border-bottom: 1px #ddd solid;
@@ -145,6 +215,7 @@ h1 {
     max-width: 1280px;
     width: 100%;
     border-collapse: collapse;
+    table-layout: fixed;
 }
         
 #messageTable td {
@@ -152,12 +223,13 @@ h1 {
 }
 
 .leftBlock {
-    width: 10em;
+    width: 14em;
+    word-wrap: break-word;
     vertical-align: top;
     background-color: #fff;
     color: #888;
     font-weight: medium;
-    font-size: 8pt;
+    font-size: 12px;
     text-align: right;
     border-top: 1px #ddd solid;
 }
@@ -190,24 +262,13 @@ h1 {
     object-fit: cover;
 }
         
-.text {
-    background-color: #eee;
-    border: 1px solid #d8d8d8;
-    height: 31px;
-    display: inline-table;
-    max-width: 90%;
-    font-size: 16px;
-    /* word-wrap: break-word; */
-    word-break: break-all;
-}
-
 .emote {
-    background-color: #fff ! important;
+    background-color: transparent ! important;
     border: 0px ! important;
 }
 
 .membership {
-    background-color: #fff ! important;
+    background-color: transparent ! important;
     border: 0px ! important;
 }
 
@@ -219,32 +280,45 @@ h1 {
     height: auto;
 }
 
+.text {
+    vertical-align: top;
+}
+
 .bubble {
+    background-color: #eee;
+    border: 1px solid #d8d8d8;
+    display: inline-block;
+    margin-bottom: -1px;
+    max-width: 90%;
+    font-size: 16px;
+    word-wrap: break-word;
     padding-top: 7px;
     padding-bottom: 5px;
     padding-left: 1em;
     padding-right: 1em;
     vertical-align: middle;
+    -webkit-text-size-adjust:100%
 }
 
 .differentUser td {
-    padding-top: 5px ! important;
-    margin-top: 5px ! important;
+    padding-bottom: 5px ! important;
 }
 
 .mine {
     text-align: right;
 }
 
-.mine .text {
-    background-color: #f8f8ff ! important;    
-}
-
-.mine .emote {
-    background-color: #fff ! important;    
+.text.emote .bubble,
+.text.membership .bubble,
+.mine .text.emote .bubble,
+.mine .text.membership .bubble
+  {
+    background-color: transparent ! important;    
+    border: 0px ! important;
 }
 
 .mine .text .bubble {
+    background-color: #f8f8ff ! important;    
     text-align: left ! important;
 }
 
@@ -273,7 +347,7 @@ h1 {
 .profile-avatar {
     width: 160px;
     height: 160px;
-    display:table-cell;
+    display: table-cell;
     vertical-align: middle;
     text-align: center;
 }
@@ -289,31 +363,25 @@ h1 {
 }
 
 #user-displayname {
-    font-size: 16pt;
+    font-size: 24px;
 }
 /******************************/
 
-#header {
-    padding-left: 20px;
-    padding-right: 20px;
+#header
+{
+    padding: 20px;
     max-width: 1280px;
     margin: auto;
 }
 
-#header-buttons {
-    float: right;
+#logo,
+#roomLogo {
+    max-width: 1280px;
+    margin: auto;
 }
 
-#config {
-    position: absolute;
-    z-index: 100;
-    top: 100px;
-    left: 50%;
-    width: 500px;
-    margin-left: -250px;
-    text-align: center;
-    padding: 20px;
-    background-color: #aaa;
+#header-buttons {
+    float: right;
 }
 
 .text_entry_section {
diff --git a/webclient/app.js b/webclient/app.js
index f27ebedc6f..6cd50c5e54 100644
--- a/webclient/app.js
+++ b/webclient/app.js
@@ -19,7 +19,8 @@ var matrixWebClient = angular.module('matrixWebClient', [
     'MatrixWebClientController',
     'LoginController',
     'RoomController',
-    'RoomsController',
+    'HomeController',
+    'SettingsController',
     'UserController',
     'matrixService',
     'eventStreamService',
@@ -44,16 +45,20 @@ matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider',
                 templateUrl: 'room/room.html',
                 controller: 'RoomController'
             }).
-            when('/rooms', {
-                templateUrl: 'rooms/rooms.html',
-                controller: 'RoomsController'
+            when('/', {
+                templateUrl: 'home/home.html',
+                controller: 'HomeController'
+            }).
+            when('/settings', {
+                templateUrl: 'settings/settings.html',
+                controller: 'SettingsController'
             }).
             when('/user/:user_matrix_id', {
                 templateUrl: 'user/user.html',
                 controller: 'UserController'
             }).
             otherwise({
-                redirectTo: '/rooms'
+                redirectTo: '/'
             });
             
         $provide.factory('AccessTokenInterceptor', ['$q', '$rootScope', 
@@ -80,6 +85,6 @@ matrixWebClient.run(['$location', 'matrixService', 'eventStreamService', functio
         $location.path("login");
     }
     else {
-        eventStreamService.resume();
+        // eventStreamService.resume();
     }
 }]);
diff --git a/webclient/components/fileUpload/file-upload-service.js b/webclient/components/fileUpload/file-upload-service.js
index 6606f31e22..5f01478fd1 100644
--- a/webclient/components/fileUpload/file-upload-service.js
+++ b/webclient/components/fileUpload/file-upload-service.js
@@ -33,7 +33,7 @@ angular.module('mFileUpload', ['matrixService', 'mUtilities'])
         console.log("Uploading " + file.name + "... to /matrix/content");
         matrixService.uploadContent(file).then(
             function(response) {
-                var content_url = location.origin + "/matrix/content/" + response.data.content_token;
+                var content_url = response.data.content_token;
                 console.log("   -> Successfully uploaded! Available at " + content_url);
                 deferred.resolve(content_url);
             },
@@ -82,6 +82,7 @@ angular.module('mFileUpload', ['matrixService', 'mUtilities'])
         // First, get the image size
         mUtilities.getImageSize(imageFile).then(
             function(size) {
+                console.log("image size: " + JSON.stringify(size));
 
                 // The final operation: send imageFile
                 var uploadImage = function() {
diff --git a/webclient/components/matrix/event-handler-service.js b/webclient/components/matrix/event-handler-service.js
index b8529895fe..b5eb73d92b 100644
--- a/webclient/components/matrix/event-handler-service.js
+++ b/webclient/components/matrix/event-handler-service.js
@@ -35,6 +35,8 @@ angular.module('eventHandlerService', [])
     $rootScope.events = {
         rooms: {}, // will contain roomId: { messages:[], members:{userid1: event} }
     };
+
+    $rootScope.presence = {};
     
     var initRoom = function(room_id) {
         if (!(room_id in $rootScope.events.rooms)) {
@@ -44,6 +46,12 @@ angular.module('eventHandlerService', [])
             $rootScope.events.rooms[room_id].members = {};
         }
     }
+
+    var reInitRoom = function(room_id) {
+        $rootScope.events.rooms[room_id] = {};
+        $rootScope.events.rooms[room_id].messages = [];
+        $rootScope.events.rooms[room_id].members = {};
+    }
     
     var handleMessage = function(event, isLiveEvent) {
         if ("membership_target" in event.content) {
@@ -69,11 +77,23 @@ angular.module('eventHandlerService', [])
     
     var handleRoomMember = function(event, isLiveEvent) {
         initRoom(event.room_id);
+        
+        // add membership changes as if they were a room message if something interesting changed
+        if (event.content.prev !== event.content.membership) {
+            if (isLiveEvent) {
+                $rootScope.events.rooms[event.room_id].messages.push(event);
+            }
+            else {
+                $rootScope.events.rooms[event.room_id].messages.unshift(event);
+            }
+        }
+        
         $rootScope.events.rooms[event.room_id].members[event.user_id] = event;
         $rootScope.$broadcast(MEMBER_EVENT, event, isLiveEvent);
     };
     
     var handlePresence = function(event, isLiveEvent) {
+        $rootScope.presence[event.content.user_id] = event;
         $rootScope.$broadcast(PRESENCE_EVENT, event, isLiveEvent);
     };
     
@@ -107,6 +127,10 @@ angular.module('eventHandlerService', [])
             for (var i=0; i<events.length; i++) {
                 this.handleEvent(events[i], isLiveEvents);
             }
-        }
+        },
+
+        reInitRoom: function(room_id) {
+            reInitRoom(room_id);
+        },
     };
 }]);
diff --git a/webclient/components/matrix/event-stream-service.js b/webclient/components/matrix/event-stream-service.js
index a446fad5d4..a1a98b2a36 100644
--- a/webclient/components/matrix/event-stream-service.js
+++ b/webclient/components/matrix/event-stream-service.js
@@ -48,11 +48,12 @@ angular.module('eventStreamService', [])
     var saveStreamSettings = function() {
         localStorage.setItem("streamSettings", JSON.stringify(settings));
     };
-    
-    var startEventStream = function() {
+
+    var doEventStream = function(deferred) {
         settings.shouldPoll = true;
         settings.isActive = true;
-        var deferred = $q.defer();
+        deferred = deferred || $q.defer();
+
         // run the stream from the latest token
         matrixService.getEventStream(settings.from, TIMEOUT_MS).then(
             function(response) {
@@ -63,13 +64,16 @@ angular.module('eventStreamService', [])
                 
                 settings.from = response.data.end;
                 
-                console.log("[EventStream] Got response from "+settings.from+" to "+response.data.end);
+                console.log(
+                    "[EventStream] Got response from "+settings.from+
+                    " to "+response.data.end
+                );
                 eventHandlerService.handleEvents(response.data.chunk, true);
                 
                 deferred.resolve(response);
                 
                 if (settings.shouldPoll) {
-                    $timeout(startEventStream, 0);
+                    $timeout(doEventStream, 0);
                 }
                 else {
                     console.log("[EventStream] Stopping poll.");
@@ -83,13 +87,48 @@ angular.module('eventStreamService', [])
                 deferred.reject(error);
                 
                 if (settings.shouldPoll) {
-                    $timeout(startEventStream, ERR_TIMEOUT_MS);
+                    $timeout(doEventStream, ERR_TIMEOUT_MS);
                 }
                 else {
                     console.log("[EventStream] Stopping polling.");
                 }
             }
         );
+
+        return deferred.promise;
+    }    
+
+    var startEventStream = function() {
+        settings.shouldPoll = true;
+        settings.isActive = true;
+        var deferred = $q.defer();
+
+        // FIXME: We are discarding all the messages.
+        matrixService.rooms().then(
+            function(response) {
+                var rooms = response.data.rooms;
+                for (var i = 0; i < rooms.length; ++i) {
+                    var room = rooms[i];
+                    if ("state" in room) {
+                        for (var j = 0; j < room.state.length; ++j) {
+                            eventHandlerService.handleEvents(room.state[j], false);
+                        }
+                    }
+                }
+
+                var presence = response.data.presence;
+                for (var i = 0; i < presence.length; ++i) {
+                    eventHandlerService.handleEvent(presence[i], false);
+                }
+
+                settings.from = response.data.end
+                doEventStream(deferred);        
+            },
+            function(error) {
+                $scope.feedback = "Failure: " + error.data;
+            }
+        );
+
         return deferred.promise;
     };
     
diff --git a/webclient/components/utilities/utilities-service.js b/webclient/components/utilities/utilities-service.js
index 9cf858ef39..3df2f04458 100644
--- a/webclient/components/utilities/utilities-service.js
+++ b/webclient/components/utilities/utilities-service.js
@@ -38,10 +38,15 @@ angular.module('mUtilities', [])
             img.src = e.target.result;
             
             // Once ready, returns its size
-            deferred.resolve({
-                width: img.width,
-                height: img.height
-            });
+            img.onload = function() {
+                deferred.resolve({
+                    width: img.width,
+                    height: img.height
+                });
+            };
+            img.onerror = function(e) {
+                deferred.reject(e);
+            };
         };
         reader.onerror = function(e) {
             deferred.reject(e);
@@ -71,33 +76,41 @@ angular.module('mUtilities', [])
         reader.onload = function(e) {
 
             img.src = e.target.result;
+            
+            // Once ready, returns its size
+            img.onload = function() {
+                var ctx = canvas.getContext("2d");
+                ctx.drawImage(img, 0, 0);
 
-            var ctx = canvas.getContext("2d");
-            ctx.drawImage(img, 0, 0);
-
-            var MAX_WIDTH = maxSize;
-            var MAX_HEIGHT = maxSize;
-            var width = img.width;
-            var height = img.height;
+                var MAX_WIDTH = maxSize;
+                var MAX_HEIGHT = maxSize;
+                var width = img.width;
+                var height = img.height;
 
-            if (width > height) {
-                if (width > MAX_WIDTH) {
-                    height *= MAX_WIDTH / width;
-                    width = MAX_WIDTH;
-                }
-            } else {
-                if (height > MAX_HEIGHT) {
-                    width *= MAX_HEIGHT / height;
-                    height = MAX_HEIGHT;
+                if (width > height) {
+                    if (width > MAX_WIDTH) {
+                        height *= MAX_WIDTH / width;
+                        width = MAX_WIDTH;
+                    }
+                } else {
+                    if (height > MAX_HEIGHT) {
+                        width *= MAX_HEIGHT / height;
+                        height = MAX_HEIGHT;
+                    }
                 }
-            }
-            canvas.width = width;
-            canvas.height = height;
-            var ctx = canvas.getContext("2d");
-            ctx.drawImage(img, 0, 0, width, height);
+                canvas.width = width;
+                canvas.height = height;
+                var ctx = canvas.getContext("2d");
+                ctx.drawImage(img, 0, 0, width, height);
 
-            var dataUrl = canvas.toDataURL("image/jpeg", 0.7); 
-            deferred.resolve(self.dataURItoBlob(dataUrl));
+                // Extract image data in the same format as the original one.
+                // The 0.7 compression value will work with formats that supports it like JPEG.
+                var dataUrl = canvas.toDataURL(imageFile.type, 0.7); 
+                deferred.resolve(self.dataURItoBlob(dataUrl));
+            };
+            img.onerror = function(e) {
+                deferred.reject(e);
+            };
         };
         reader.onerror = function(e) {
             deferred.reject(e);
diff --git a/webclient/home/home-controller.js b/webclient/home/home-controller.js
new file mode 100644
index 0000000000..35d0ef1654
--- /dev/null
+++ b/webclient/home/home-controller.js
@@ -0,0 +1,162 @@
+/*
+Copyright 2014 matrix.org
+
+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';
+
+angular.module('HomeController', ['matrixService', 'mFileInput', 'mFileUpload', 'eventHandlerService'])
+.controller('HomeController', ['$scope', '$location', 'matrixService', 'mFileUpload', 'eventHandlerService', 'eventStreamService', 
+                               function($scope, $location, matrixService, mFileUpload, eventHandlerService, eventStreamService) {
+
+    $scope.config = matrixService.config();
+    $scope.rooms = {};
+    $scope.public_rooms = [];
+    $scope.newRoomId = "";
+    $scope.feedback = "";
+    
+    $scope.newRoom = {
+        room_id: "",
+        private: false
+    };
+    
+    $scope.goToRoom = {
+        room_id: "",
+    };
+
+    $scope.joinAlias = {
+        room_alias: "",
+    };
+    
+    $scope.$on(eventHandlerService.MEMBER_EVENT, function(ngEvent, event, isLive) {
+        var config = matrixService.config();
+        if (event.target_user_id === config.user_id && event.content.membership === "invite") {
+            console.log("Invited to room " + event.room_id);
+            // FIXME push membership to top level key to match /im/sync
+            event.membership = event.content.membership;
+            // FIXME bodge a nicer name than the room ID for this invite.
+            event.room_display_name = event.user_id + "'s room";
+            $scope.rooms[event.room_id] = event;
+        }
+    });
+    
+    var assignRoomAliases = function(data) {
+        for (var i=0; i<data.length; i++) {
+            var alias = matrixService.getRoomIdToAliasMapping(data[i].room_id);
+            if (alias) {
+                // use the existing alias from storage
+                data[i].room_alias = alias;
+                data[i].room_display_name = alias;
+            }
+            else if (data[i].aliases && data[i].aliases[0]) {
+                // save the mapping
+                // TODO: select the smarter alias from the array
+                matrixService.createRoomIdToAliasMapping(data[i].room_id, data[i].aliases[0]);
+                data[i].room_display_name = data[i].aliases[0];
+            }
+            else if (data[i].membership == "invite" && "inviter" in data[i]) {
+                data[i].room_display_name = data[i].inviter + "'s room"
+            }
+            else {
+                // last resort use the room id
+                data[i].room_display_name = data[i].room_id;
+            }
+        }
+        return data;
+    };
+
+    $scope.refresh = function() {
+        // List all rooms joined or been invited to
+        matrixService.rooms().then(
+            function(response) {
+                var data = assignRoomAliases(response.data.rooms);
+                $scope.feedback = "Success";
+                for (var i=0; i<data.length; i++) {
+                    $scope.rooms[data[i].room_id] = data[i];
+                }
+
+                var presence = response.data.presence;
+                for (var i = 0; i < presence.length; ++i) {
+                    eventHandlerService.handleEvent(presence[i], false);
+                }
+            },
+            function(error) {
+                $scope.feedback = "Failure: " + error.data;
+            });
+        
+        matrixService.publicRooms().then(
+            function(response) {
+                $scope.public_rooms = assignRoomAliases(response.data.chunk);
+            }
+        );
+
+        eventStreamService.resume();
+    };
+    
+    $scope.createNewRoom = function(room_id, isPrivate) {
+        
+        var visibility = "public";
+        if (isPrivate) {
+            visibility = "private";
+        }
+        
+        matrixService.create(room_id, visibility).then(
+            function(response) { 
+                // This room has been created. Refresh the rooms list
+                console.log("Created room " + response.data.room_alias + " with id: "+
+                response.data.room_id);
+                matrixService.createRoomIdToAliasMapping(
+                    response.data.room_id, response.data.room_alias);
+                $scope.refresh();
+            },
+            function(error) {
+                $scope.feedback = "Failure: " + error.data;
+            });
+    };
+    
+    // Go to a room
+    $scope.goToRoom = function(room_id) {
+        // Simply open the room page on this room id
+        //$location.url("room/" + room_id);
+        matrixService.join(room_id).then(
+            function(response) {
+                if (response.data.hasOwnProperty("room_id")) {
+                    if (response.data.room_id != room_id) {
+                        $location.url("room/" + response.data.room_id);
+                        return;
+                     }
+                }
+
+                $location.url("room/" + room_id);
+            },
+            function(error) {
+                $scope.feedback = "Can't join room: " + error.data;
+            }
+        );
+    };
+
+    $scope.joinAlias = function(room_alias) {
+        matrixService.joinAlias(room_alias).then(
+            function(response) {
+                // Go to this room
+                $location.url("room/" + room_alias);
+            },
+            function(error) {
+                $scope.feedback = "Can't join room: " + error.data;
+            }
+        );
+    };
+ 
+    $scope.refresh();
+}]);
diff --git a/webclient/home/home.html b/webclient/home/home.html
new file mode 100644
index 0000000000..4818d414b6
--- /dev/null
+++ b/webclient/home/home.html
@@ -0,0 +1,63 @@
+<div ng-controller="HomeController">
+
+    <div id="page">
+    <div id="wrapper">
+        
+    <div>
+        <form>
+            <table>
+                <tr>
+                    <td>
+                        <div class="profile-avatar">
+                            <img ng-src="{{ config.avatarUrl || 'img/default-profile.jpg' }}"/>
+                        </div>
+                    </td>
+                    <td>
+                        <div id="user-ids">
+                            <div id="user-displayname">{{ config.displayName }}</div>
+                            <div>{{ config.user_id }}</div>                        
+                        </div>
+                    </td>
+                </tr>
+            </table>
+        </form>
+    </div>
+    
+    <h3>My rooms</h3>
+    
+    <div class="rooms" ng-repeat="(rm_id, room) in rooms">
+        <div>
+            <a href="#/room/{{ room.room_alias ? room.room_alias : rm_id }}" >{{ room.room_display_name }}</a> {{room.membership === 'invite' ? ' (invited)' : ''}}
+        </div>
+    </div>
+    <br/>
+
+    <h3>Public rooms</h3>
+    
+    <div class="public_rooms" ng-repeat="room in public_rooms">
+        <div>
+            <a href="#/room/{{ room.room_alias ? room.room_alias : room.room_id }}" >{{ room.room_alias }}</a>
+        </div>
+    </div>
+    <br/>
+    
+    <div>
+        <form>
+            <input size="40" ng-model="newRoom.room_id" ng-enter="createNewRoom(newRoom.room_id, newRoom.private)" placeholder="(e.g. foo_channel)"/>
+            <input type="checkbox" ng-model="newRoom.private">private
+            <button ng-disabled="!newRoom.room_id" ng-click="createNewRoom(newRoom.room_id, newRoom.private)">Create room</button>    
+        </form>
+    </div>
+    <div>
+        <form>
+            <input size="40" ng-model="joinAlias.room_alias" ng-enter="joinAlias(joinAlias.room_alias)" placeholder="(e.g. #foo_channel:example.org)"/>
+            <button ng-disabled="!joinAlias.room_alias" ng-click="joinAlias(joinAlias.room_alias)">Join room</button>    
+        </form>
+    </div>
+    <br/>
+    
+    {{ feedback }}
+
+    </div>    
+    </div>
+</div>
diff --git a/webclient/index.html b/webclient/index.html
index 27d9208193..938d70c86d 100644
--- a/webclient/index.html
+++ b/webclient/index.html
@@ -2,10 +2,12 @@
 <html xmlns:ng="http://angularjs.org" ng-app="matrixWebClient" ng-controller="MatrixWebClientController">
 <head>
     <title>[matrix]</title>
-    
+        
     <link rel="stylesheet" href="app.css">
     <link rel="icon" href="favicon.ico">
    
+    <meta name="viewport" content="width=device-width">
+   
     <script type='text/javascript' src='js/jquery-1.8.3.min.js'></script> 
     <script src="js/angular.min.js"></script>
     <script src="js/angular-route.min.js"></script>
@@ -15,10 +17,11 @@
     <script src="app-controller.js"></script>
     <script src="app-directive.js"></script>
     <script src="app-filter.js"></script>
+    <script src="home/home-controller.js"></script>
     <script src="login/login-controller.js"></script>
     <script src="room/room-controller.js"></script>
     <script src="room/room-directive.js"></script>
-    <script src="rooms/rooms-controller.js"></script>
+    <script src="settings/settings-controller.js"></script>
     <script src="user/user-controller.js"></script>
     <script src="components/matrix/matrix-service.js"></script>
     <script src="components/matrix/event-stream-service.js"></script>
@@ -33,22 +36,11 @@
     <header id="header">
         <!-- Do not show buttons on the login page -->
         <div id="header-buttons" ng-hide="'/login' == location ">
-            <button ng-click="showConfig()">Config</button>
+            <button ng-click='go("settings")'>Settings</button>
             <button ng-click="logout()">Log out</button>
         </div>
-
-        <h1>[matrix]</h1>
     </header>
 
-    <div id="config" ng-hide="!config">
-        <div>Home server: {{ config.homeserver }} </div>
-        <div>User ID: {{ config.user_id }} </div>
-        <div>Access token: {{ config.access_token }} </div>
-        <div><button ng-click="requestNotifications()">Request notifications</button></div>
-        <div><button ng-click="closeConfig()">Close</button></div>
-    </div>
-
-
     <div ng-view></div>
 
 </body>
diff --git a/webclient/login/login-controller.js b/webclient/login/login-controller.js
index 35886c5583..51f9a3bdf4 100644
--- a/webclient/login/login-controller.js
+++ b/webclient/login/login-controller.js
@@ -53,7 +53,7 @@ angular.module('LoginController', ['matrixService'])
                 matrixService.saveConfig();
                 eventStreamService.resume();
                  // Go to the user's rooms list page
-                $location.path("rooms");
+                $location.url("home");
             },
             function(error) {
                 if (error.data) {
@@ -86,7 +86,7 @@ angular.module('LoginController', ['matrixService'])
                     });
                     matrixService.saveConfig();
                     eventStreamService.resume();
-                    $location.path("rooms");
+                    $location.url("home");
                 }
                 else {
                     $scope.feedback = "Failed to login: " + JSON.stringify(response.data);
diff --git a/webclient/login/login.html b/webclient/login/login.html
index b1488b37f0..4b2ea60928 100644
--- a/webclient/login/login.html
+++ b/webclient/login/login.html
@@ -1,4 +1,6 @@
-<div ng-controller="LoginController" class="login">
+<div ng-controller="LoginController" class="login">    
+    <h1 id="logo">[matrix]</h1>
+
     <div id="page">
     <div id="wrapper">
 
diff --git a/webclient/room/room-controller.js b/webclient/room/room-controller.js
index 7de50dd960..3311618825 100644
--- a/webclient/room/room-controller.js
+++ b/webclient/room/room-controller.js
@@ -15,8 +15,8 @@ limitations under the License.
 */
 
 angular.module('RoomController', ['ngSanitize', 'mUtilities'])
-.controller('RoomController', ['$scope', '$http', '$timeout', '$routeParams', '$location', 'matrixService', 'eventStreamService', 'eventHandlerService', 'mFileUpload', 'mUtilities',
-                               function($scope, $http, $timeout, $routeParams, $location, matrixService, eventStreamService, eventHandlerService, mFileUpload, mUtilities) {
+.controller('RoomController', ['$scope', '$http', '$timeout', '$routeParams', '$location', 'matrixService', 'eventStreamService', 'eventHandlerService', 'mFileUpload', 'mUtilities', '$rootScope',
+                               function($scope, $http, $timeout, $routeParams, $location, matrixService, eventStreamService, eventHandlerService, mFileUpload, mUtilities, $rootScope) {
    'use strict';
     var MESSAGES_PER_PAGINATION = 30;
     var THUMBNAIL_SIZE = 320;
@@ -29,9 +29,11 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
         user_id: matrixService.config().user_id,
         events_from: "END", // when to start the event stream from.
         earliest_token: "END", // stores how far back we've paginated.
+        first_pagination: true, // this is toggled off when the first pagination is done
         can_paginate: true, // this is toggled off when we run out of items
         paginating: false, // used to avoid concurrent pagination requests pulling in dup contents
         stream_failure: undefined, // the response when the stream fails
+        // FIXME: sending has been disabled, as surely messages should be sent in the background rather than locking the UI synchronously --Matthew
         sending: false // true when a message is being sent. It helps to disable the UI when a process is running
     };
     $scope.members = {};
@@ -100,7 +102,6 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
         var originalTopRow = $("#messageTable>tbody>tr:first")[0];
         matrixService.paginateBackMessages($scope.room_id, $scope.state.earliest_token, numItems).then(
             function(response) {
-                var firstPagination = !$scope.events.rooms[$scope.room_id];
                 eventHandlerService.handleEvents(response.data.chunk, false);
                 $scope.state.earliest_token = response.data.end;
                 if (response.data.chunk.length < MESSAGES_PER_PAGINATION) {
@@ -126,8 +127,9 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
                     }, 0);
                 }
                 
-                if (firstPagination) {
+                if ($scope.state.first_pagination) {
                     scrollToBottom();
+                    $scope.state.first_pagination = false;
                 }
                 else {
                     // lock the scroll position
@@ -150,6 +152,8 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
     };
 
     var updateMemberList = function(chunk) {
+        if (chunk.room_id != $scope.room_id) return;
+
         var isNewMember = !(chunk.target_user_id in $scope.members);
         if (isNewMember) {
             // FIXME: why are we copying these fields around inside chunk?
@@ -159,8 +163,7 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
             if ("mtime_age" in chunk.content) {
                 chunk.mtime_age = chunk.content.mtime_age;
             }
-/*            
-            // FIXME: once the HS reliably returns the displaynames & avatar_urls for both
+            // Once the HS reliably returns the displaynames & avatar_urls for both
             // local and remote users, we should use this rather than the evalAsync block
             // below
             if ("displayname" in chunk.content) {
@@ -169,9 +172,11 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
             if ("avatar_url" in chunk.content) {
                 chunk.avatar_url = chunk.content.avatar_url;
             }
- */      
             $scope.members[chunk.target_user_id] = chunk;
 
+/*
+            // Stale code for explicitly hammering the homeserver for every displayname & avatar_url
+            
             // get their display name and profile picture and set it to their
             // member entry in $scope.members. We HAVE to use $timeout with 0 delay 
             // to make this function run AFTER the current digest cycle, else the 
@@ -195,6 +200,11 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
                     }
                 );
             });
+*/            
+
+            if (chunk.target_user_id in $rootScope.presence) {
+                updatePresence($rootScope.presence[chunk.target_user_id]);
+            }
         }
         else {
             // selectively update membership else it will nuke the picture and displayname too :/
@@ -236,7 +246,7 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
         }
 
         $scope.state.sending = true;
-
+        
         // Send the text message
         var promise;
         // FIXME: handle other commands too
@@ -260,9 +270,8 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
     };
 
     $scope.onInit = function() {
-        // $timeout(function() { document.getElementById('textInput').focus() }, 0);
         console.log("onInit");
-        
+
         // Does the room ID provided in the URL?
         var room_id_or_alias;
         if ($routeParams.room_id_or_alias) {
@@ -290,7 +299,7 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
                 else {
                     // In case of issue, go to the default page
                     console.log("Error: cannot extract room alias");
-                    $location.path("/");
+                    $location.url("/");
                     return;
                 }
             }
@@ -307,12 +316,14 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
             function () {
                 // In case of issue, go to the default page
                 console.log("Error: cannot resolve room alias");
-                $location.path("/");
+                $location.url("/");
             });
         }
     };
 
     var onInit2 = function() {
+        eventHandlerService.reInitRoom($scope.room_id); 
+
         // Join the room
         matrixService.join($scope.room_id).then(
             function() {
@@ -325,6 +336,7 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
                             var chunk = response.data.chunk[i];
                             updateMemberList(chunk);
                         }
+                        eventStreamService.resume();
                     },
                     function(error) {
                         $scope.feedback = "Failed get member list: " + error.data.error;
@@ -360,7 +372,7 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
         matrixService.leave($scope.room_id).then(
             function(response) {
                 console.log("Left room ");
-                $location.path("rooms");
+                $location.url("home");
             },
             function(error) {
                 $scope.feedback = "Failed to leave room: " + error.data.error;
diff --git a/webclient/room/room-directive.js b/webclient/room/room-directive.js
index 94655336df..1a99a37abb 100644
--- a/webclient/room/room-directive.js
+++ b/webclient/room/room-directive.js
@@ -17,30 +17,30 @@
 'use strict';
 
 angular.module('RoomController')
-.directive('autoComplete', ['$timeout', function ($timeout) {
+.directive('tabComplete', ['$timeout', function ($timeout) {
     return function (scope, element, attrs) {
         element.bind("keydown keypress", function (event) {
             // console.log("event: " + event.which);
             if (event.which === 9) {
-                if (!scope.autoCompleting) { // cache our starting text
+                if (!scope.tabCompleting) { // cache our starting text
                     // console.log("caching " + element[0].value);
-                    scope.autoCompleteOriginal = element[0].value;
-                    scope.autoCompleting = true;
+                    scope.tabCompleteOriginal = element[0].value;
+                    scope.tabCompleting = true;
                 }
                 
                 if (event.shiftKey) {
-                    scope.autoCompleteIndex--;
-                    if (scope.autoCompleteIndex < 0) {
-                        scope.autoCompleteIndex = 0;
+                    scope.tabCompleteIndex--;
+                    if (scope.tabCompleteIndex < 0) {
+                        scope.tabCompleteIndex = 0;
                     }
                 }
                 else {
-                    scope.autoCompleteIndex++;
+                    scope.tabCompleteIndex++;
                 }
                 
                 var searchIndex = 0;
-                var targetIndex = scope.autoCompleteIndex;
-                var text = scope.autoCompleteOriginal;
+                var targetIndex = scope.tabCompleteIndex;
+                var text = scope.tabCompleteOriginal;
                 
                 // console.log("targetIndex: " + targetIndex + ", text=" + text);
                                     
@@ -90,17 +90,17 @@ angular.module('RoomController')
                              element[0].className = "";
                         }, 150);
                         element[0].value = text;
-                        scope.autoCompleteIndex = 0;
+                        scope.tabCompleteIndex = 0;
                     }
                 }
                 else {
-                    scope.autoCompleteIndex = 0;
+                    scope.tabCompleteIndex = 0;
                 }
                 event.preventDefault();
             }
-            else if (event.which !== 16 && scope.autoCompleting) {
-                scope.autoCompleting = false;
-                scope.autoCompleteIndex = 0;
+            else if (event.which !== 16 && scope.tabCompleting) {
+                scope.tabCompleting = false;
+                scope.tabCompleteIndex = 0;
             }
         });
     };
diff --git a/webclient/room/room.html b/webclient/room/room.html
index cb9cf1d1f3..06ca63d2ea 100644
--- a/webclient/room/room.html
+++ b/webclient/room/room.html
@@ -1,4 +1,5 @@
 <div ng-controller="RoomController" data-ng-init="onInit()" class="room">
+    <h1 id="roomLogo">[matrix]</h1>
 
     <div id="page">
     <div id="wrapper">
@@ -26,19 +27,25 @@
     </div>
     
     <div id="messageTableWrapper" keep-scroll>
+        <!-- FIXME: need to have better timestamp semantics than the (msg.content.hsob_ts || msg.ts) hack below -->
         <table id="messageTable" infinite-scroll="paginateMore()">
             <tr ng-repeat="msg in events.rooms[room_id].messages"
-                ng-class="(events.rooms[room_id].messages[$index - 1].user_id !== msg.user_id ? 'differentUser' : '') + (msg.user_id === state.user_id ? ' mine' : '')" scroll-item>
+                ng-class="(events.rooms[room_id].messages[$index + 1].user_id !== msg.user_id ? 'differentUser' : '') + (msg.user_id === state.user_id ? ' mine' : '')" scroll-item>
                 <td class="leftBlock">
                     <div class="sender" ng-hide="events.rooms[room_id].messages[$index - 1].user_id === msg.user_id">{{ members[msg.user_id].displayname || msg.user_id }}</div>
-                    <div class="timestamp">{{ msg.content.hsob_ts | date:'MMM d HH:mm:ss' }}</div>
+                    <div class="timestamp">{{ (msg.content.hsob_ts || msg.ts) | date:'MMM d HH:mm' }}</div>
                 </td>
                 <td class="avatar">
                     <img class="avatarImage" ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.jpg' }}" width="32" height="32"
                          ng-hide="events.rooms[room_id].messages[$index - 1].user_id === msg.user_id || msg.user_id === state.user_id"/>
                 </td>
-                <td ng-class="!msg.content.membership_target ? (msg.content.msgtype === 'm.emote' ? 'emote text' : 'text') : 'membership text'">
+                <td ng-class="!msg.content.membership ? (msg.content.msgtype === 'm.emote' ? 'emote text' : 'text') : 'membership text'">
                     <div class="bubble">
+                        <span ng-hide='msg.type !== "m.room.member"'>
+                            {{ members[msg.user_id].displayname || msg.user_id }}
+                            {{ {"join": "joined", "leave": "left", "invite": "invited"}[msg.content.membership] }}
+                            {{ msg.content.target_id || '' }}
+                        </span>
                         <span ng-hide='msg.content.msgtype !== "m.emote"' ng-bind-html="'* ' + (members[msg.user_id].displayname || msg.user_id) + ' ' + msg.content.body | linky:'_blank'"/>
                         <span ng-hide='msg.content.msgtype !== "m.text"' ng-bind-html="((msg.content.msgtype === 'm.text') ? msg.content.body : '') | linky:'_blank'"/>
                         <div ng-show='msg.content.msgtype === "m.image"'>
@@ -67,29 +74,28 @@
         <div id="controls">
             <table id="inputBarTable">
                 <tr>
-                    <td width="1">
+                    <td id="userIdCell" width="1px">
                         {{ state.user_id }} 
                     </td>
-                    <td width="*" style="min-width: 100px">
-                        <input id="mainInput" ng-model="textInput" ng-enter="send()" ng-disabled="state.sending" ng-focus="true" auto-complete/>
+                    <td width="*">
+                        <input id="mainInput" ng-model="textInput" ng-enter="send()" ng-focus="true" autocomplete="off" tab-complete/>
                     </td>
-                    <td width="150px">
-                        <button ng-click="send()" ng-disabled="state.sending">Send</button>
-                        <button m-file-input="imageFileToSend">Send Image</button>
-                    </td>
-                    <td width="1">
-                        
+                    <td id="buttonsCell">
+                        <button ng-click="send()">Send</button>
+                        <button m-file-input="imageFileToSend">Image</button>
                     </td>
                 </tr>
             </table>
 
-            <span>
-               Invite a user: 
-                    <input ng-model="userIDToInvite" size="32" type="text" placeholder="User ID (ex:@user:homeserver)"/>     
-                    <button ng-click="inviteUser(userIDToInvite)">Invite</button>
-            </span>
-            <button ng-click="leaveRoom()">Leave</button>
-            <button ng-click="loadMoreHistory()" ng-disabled="!state.can_paginate">Load more history</button>
+            <div id="extraControls">
+                <span>
+                   Invite a user: 
+                        <input ng-model="userIDToInvite" size="32" type="text" placeholder="User ID (ex:@user:homeserver)"/>     
+                        <button ng-click="inviteUser(userIDToInvite)">Invite</button>
+                </span>
+                <button ng-click="leaveRoom()">Leave</button>
+            </div>
+        
             {{ feedback }}
             <div ng-hide="!state.stream_failure">
                 {{ state.stream_failure.data.error || "Connection failure" }}
diff --git a/webclient/rooms/rooms-controller.js b/webclient/rooms/rooms-controller.js
deleted file mode 100644
index 65d345d7a6..0000000000
--- a/webclient/rooms/rooms-controller.js
+++ /dev/null
@@ -1,288 +0,0 @@
-/*
-Copyright 2014 matrix.org
-
-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';
-
-angular.module('RoomsController', ['matrixService', 'mFileInput', 'mFileUpload', 'eventHandlerService'])
-.controller('RoomsController', ['$scope', '$location', 'matrixService', 'mFileUpload', 'eventHandlerService',
-                               function($scope, $location, matrixService, mFileUpload, eventHandlerService) {
-                                   
-    $scope.rooms = {};
-    $scope.public_rooms = [];
-    $scope.newRoomId = "";
-    $scope.feedback = "";
-    
-    $scope.newRoom = {
-        room_id: "",
-        private: false
-    };
-    
-    $scope.goToRoom = {
-        room_id: "",
-    };
-
-    $scope.joinAlias = {
-        room_alias: "",
-    };
-
-    $scope.newProfileInfo = {
-        name: matrixService.config().displayName,
-        avatar: matrixService.config().avatarUrl,
-        avatarFile: undefined
-    };
-
-    $scope.linkedEmails = {
-        linkNewEmail: "", // the email entry box
-        emailBeingAuthed: undefined, // to populate verification text
-        authTokenId: undefined, // the token id from the IS
-        clientSecret: undefined, // our client secret
-        sendAttempt: 1,
-        emailCode: "", // the code entry box
-        linkedEmailList: matrixService.config().emailList // linked email list
-    };
-    
-    $scope.$on(eventHandlerService.MEMBER_EVENT, function(ngEvent, event, isLive) {
-        var config = matrixService.config();
-        if (event.target_user_id === config.user_id && event.content.membership === "invite") {
-            console.log("Invited to room " + event.room_id);
-            // FIXME push membership to top level key to match /im/sync
-            event.membership = event.content.membership;
-            // FIXME bodge a nicer name than the room ID for this invite.
-            event.room_alias = event.user_id + "'s room";
-            $scope.rooms[event.room_id] = event;
-        }
-    });
-    
-    var assignRoomAliases = function(data) {
-        for (var i=0; i<data.length; i++) {
-            var alias = matrixService.getRoomIdToAliasMapping(data[i].room_id);
-            if (alias) {
-                // use the existing alias from storage
-                data[i].room_alias = alias;
-            }
-            else if (data[i].aliases && data[i].aliases[0]) {
-                // save the mapping
-                // TODO: select the smarter alias from the array
-                matrixService.createRoomIdToAliasMapping(data[i].room_id, data[i].aliases[0]);
-            }
-            else {
-                // last resort use the room id
-                data[i].room_alias = data[i].room_id;
-            }
-        }
-        return data;
-    };
-
-    $scope.refresh = function() {
-        // List all rooms joined or been invited to
-        matrixService.rooms().then(
-            function(response) {
-                var data = assignRoomAliases(response.data);
-                $scope.feedback = "Success";
-                for (var i=0; i<data.length; i++) {
-                    $scope.rooms[data[i].room_id] = data[i];
-                }
-            },
-            function(error) {
-                $scope.feedback = "Failure: " + error.data;
-            });
-        
-        matrixService.publicRooms().then(
-            function(response) {
-                $scope.public_rooms = assignRoomAliases(response.data.chunk);
-            }
-        );
-    };
-    
-    $scope.createNewRoom = function(room_id, isPrivate) {
-        
-        var visibility = "public";
-        if (isPrivate) {
-            visibility = "private";
-        }
-        
-        matrixService.create(room_id, visibility).then(
-            function(response) { 
-                // This room has been created. Refresh the rooms list
-                console.log("Created room " + response.data.room_alias + " with id: "+
-                response.data.room_id);
-                matrixService.createRoomIdToAliasMapping(
-                    response.data.room_id, response.data.room_alias);
-                $scope.refresh();
-            },
-            function(error) {
-                $scope.feedback = "Failure: " + error.data;
-            });
-    };
-    
-    // Go to a room
-    $scope.goToRoom = function(room_id) {
-        // Simply open the room page on this room id
-        //$location.path("room/" + room_id);
-        matrixService.join(room_id).then(
-            function(response) {
-                if (response.data.hasOwnProperty("room_id")) {
-                    if (response.data.room_id != room_id) {
-                        $location.path("room/" + response.data.room_id);
-                        return;
-                     }
-                }
-
-                $location.path("room/" + room_id);
-            },
-            function(error) {
-                $scope.feedback = "Can't join room: " + error.data;
-            }
-        );
-    };
-
-    $scope.joinAlias = function(room_alias) {
-        matrixService.joinAlias(room_alias).then(
-            function(response) {
-                // Go to this room
-                $location.path("room/" + room_alias);
-            },
-            function(error) {
-                $scope.feedback = "Can't join room: " + error.data;
-            }
-        );
-    };
-
-    $scope.setDisplayName = function(newName) {
-        matrixService.setDisplayName(newName).then(
-            function(response) {
-                $scope.feedback = "Updated display name.";
-                var config = matrixService.config();
-                config.displayName = newName;
-                matrixService.setConfig(config);
-                matrixService.saveConfig();
-            },
-            function(error) {
-                $scope.feedback = "Can't update display name: " + error.data;
-            }
-        );
-    };
-
-
-    $scope.$watch("newProfileInfo.avatarFile", function(newValue, oldValue) {
-        if ($scope.newProfileInfo.avatarFile) {
-            console.log("Uploading new avatar file...");
-            mFileUpload.uploadFile($scope.newProfileInfo.avatarFile).then(
-                function(url) {
-                    $scope.newProfileInfo.avatar = url;
-                    $scope.setAvatar($scope.newProfileInfo.avatar);
-                },
-                function(error) {
-                    $scope.feedback = "Can't upload image";
-                } 
-            );
-        }
-    });
-
-    $scope.setAvatar = function(newUrl) {
-        console.log("Updating avatar to "+newUrl);
-        matrixService.setProfilePictureUrl(newUrl).then(
-            function(response) {
-                console.log("Updated avatar");
-                $scope.feedback = "Updated avatar.";
-                var config = matrixService.config();
-                config.avatarUrl = newUrl;
-                matrixService.setConfig(config);
-                matrixService.saveConfig();
-            },
-            function(error) {
-                $scope.feedback = "Can't update avatar: " + error.data;
-            }
-        );
-    };
-
-    var generateClientSecret = function() {
-        var ret = "";
-        var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
-
-        for (var i = 0; i < 32; i++) {
-            ret += chars.charAt(Math.floor(Math.random() * chars.length));
-        }
-
-        return ret;
-    };
-
-
-    $scope.linkEmail = function(email) {
-        if (email != $scope.linkedEmails.emailBeingAuthed) {
-            $scope.linkedEmails.clientSecret = generateClientSecret();
-            $scope.linkedEmails.sendAttempt = 1;
-        }
-        matrixService.linkEmail(email, $scope.linkedEmails.clientSecret, $scope.linkedEmails.sendAttempt).then(
-            function(response) {
-                if (response.data.success === true) {
-                    $scope.linkedEmails.authTokenId = response.data.sid;
-                    $scope.emailFeedback = "You have been sent an email.";
-                    $scope.linkedEmails.emailBeingAuthed = email;
-                }
-                else {
-                    $scope.emailFeedback = "Failed to send email.";
-                }
-            },
-            function(error) {
-                $scope.emailFeedback = "Can't send email: " + error.data;
-            }
-        );
-    };
-
-    $scope.submitEmailCode = function(code) {
-        var tokenId = $scope.linkedEmails.authTokenId;
-        if (tokenId === undefined) {
-            $scope.emailFeedback = "You have not requested a code with this email.";
-            return;
-        }
-        matrixService.authEmail(matrixService.config().user_id, tokenId, code, $scope.linkedEmails.clientSecret).then(
-            function(response) {
-                if ("success" in response.data && response.data.success === false) {
-                    $scope.emailFeedback = "Failed to authenticate email.";
-                    return;
-                }
-                matrixService.bindEmail(matrixService.config().user_id, tokenId, $scope.linkedEmails.clientSecret).then(
-                    function(response) {
-                         var config = matrixService.config();
-                         var emailList = {};
-                         if ("emailList" in config) {
-                             emailList = config.emailList;
-                         }
-                         emailList[$scope.linkedEmails.emailBeingAuthed] = response;
-                         // save the new email list
-                         config.emailList = emailList;
-                         matrixService.setConfig(config);
-                         matrixService.saveConfig();
-                         // invalidate the email being authed and update UI.
-                         $scope.linkedEmails.emailBeingAuthed = undefined;
-                         $scope.emailFeedback = "";
-                         $scope.linkedEmails.linkedEmailList = emailList;
-                         $scope.linkedEmails.linkNewEmail = "";
-                         $scope.linkedEmails.emailCode = "";
-                    }, function(reason) {
-                        $scope.emailFeedback = "Failed to link email: " + reason;
-                    }
-                );
-            },
-            function(reason) {
-                $scope.emailFeedback = "Failed to auth email: " + reason;
-            }
-        );
-    };
-    
-    $scope.refresh();
-}]);
diff --git a/webclient/rooms/rooms.html b/webclient/rooms/rooms.html
deleted file mode 100644
index 2602209bd3..0000000000
--- a/webclient/rooms/rooms.html
+++ /dev/null
@@ -1,101 +0,0 @@
-<div ng-controller="RoomsController" class="rooms">
-
-    <div id="page">
-    <div id="wrapper">
-            
-    <div>
-        <form>
-            <table>
-                <tr>
-                    <td>
-                        <div class="profile-avatar">
-                            <img  ng-src="{{ newProfileInfo.avatar || 'img/default-profile.jpg' }}" m-file-input="newProfileInfo.avatarFile"/>
-                        </div>
-                    </td>
-                    <td>
-                         <!-- TODO: To enable once we have an upload server
-                        <button  m-file-input="newProfileInfo.avatarFile">Upload new Avatar</button> 
-                        or use an existing image URL:
-                         -->
-                        <div>
-                            <input size="40" ng-model="newProfileInfo.avatar" ng-enter="setAvatar(newProfileInfo.avatar)" placeholder="Image URL"/>
-                            <button ng-disabled="!newProfileInfo.avatar" ng-click="setAvatar(newProfileInfo.avatar)">Update Avatar</button>   
-                        </div>
-                    </td>
-                </tr>
-            </table>
-        </form>
-    </div>
-
-    <div>
-        <form>
-            <input size="40" ng-model="newProfileInfo.name" ng-enter="setDisplayName(newProfileInfo.name)" />
-            <button ng-disabled="!newProfileInfo.name" ng-click="setDisplayName(newProfileInfo.name)">Update Name</button>    
-        </form>
-    </div>
-
-    <br/>
-
-    <div>
-        <form>
-            <input size="40" ng-model="linkedEmails.linkNewEmail" ng-enter="linkEmail(linkedEmails.linkNewEmail)" />
-            <button ng-disabled="!linkedEmails.linkNewEmail" ng-click="linkEmail(linkedEmails.linkNewEmail)">
-                Link Email
-            </button>
-            {{ emailFeedback }}    
-        </form>
-        <form ng-hide="!linkedEmails.emailBeingAuthed">
-            Enter validation token for {{ linkedEmails.emailBeingAuthed }}:
-            <br />
-            <input size="20" ng-model="linkedEmails.emailCode" ng-enter="submitEmailCode(linkedEmails.emailCode)" />
-            <button ng-disabled="!linkedEmails.emailCode || !linkedEmails.linkNewEmail" ng-click="submitEmailCode(linkedEmails.emailCode)">
-                Submit Code
-            </button>   
-        </form>
-        Linked emails:
-        <table>
-            <tr ng-repeat="(address, info) in linkedEmails.linkedEmailList">
-                <td>{{address}}</td>
-            </tr>
-        </table>
-    </div>
-    <br/>
-    
-    <h3>My rooms</h3>
-    
-    <div class="rooms" ng-repeat="(rm_id, room) in rooms">
-        <div>
-            <a href="#/room/{{ room.room_alias ? room.room_alias : rm_id }}" >{{ room.room_alias }}</a> {{room.membership === 'invite' ? ' (invited)' : ''}}
-        </div>
-    </div>
-    <br/>
-
-    <h3>Public rooms</h3>
-    
-    <div class="public_rooms" ng-repeat="room in public_rooms">
-        <div>
-            <a href="#/room/{{ room.room_alias ? room.room_alias : room.room_id }}" >{{ room.room_alias }}</a>
-        </div>
-    </div>
-    <br/>
-    
-    <div>
-        <form>
-            <input size="40" ng-model="newRoom.room_id" ng-enter="createNewRoom(newRoom.room_id, newRoom.private)" placeholder="(e.g. foo_channel)"/>
-            <input type="checkbox" ng-model="newRoom.private">private
-            <button ng-disabled="!newRoom.room_id" ng-click="createNewRoom(newRoom.room_id, newRoom.private)">Create room</button>    
-        </form>
-    </div>
-    <div>
-        <form>
-            <input size="40" ng-model="joinAlias.room_alias" ng-enter="joinAlias(joinAlias.room_alias)" placeholder="(e.g. #foo_channel:example.org)"/>
-            <button ng-disabled="!joinAlias.room_alias" ng-click="joinAlias(joinAlias.room_alias)">Join room</button>    
-        </form>
-    </div>
-    <br/>
-    
-    {{ feedback }}
-
-    </div>    
-    </div>
-</div>
diff --git a/webclient/settings/settings-controller.js b/webclient/settings/settings-controller.js
new file mode 100644
index 0000000000..5d3f7cb2b8
--- /dev/null
+++ b/webclient/settings/settings-controller.js
@@ -0,0 +1,146 @@
+/*
+Copyright 2014 matrix.org
+
+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';
+
+angular.module('SettingsController', ['matrixService', 'mFileUpload'])
+.controller('SettingsController', ['$scope', 'matrixService', 'mFileUpload',
+                              function($scope, matrixService, mFileUpload) {                 
+    $scope.config = matrixService.config();
+
+    $scope.profile = {
+        displayName: $scope.config.displayName,
+        avatarUrl: $scope.config.avatarUrl
+    };
+
+    $scope.$watch("profile.avatarFile", function(newValue, oldValue) {
+        if ($scope.profile.avatarFile) {
+            console.log("Uploading new avatar file...");
+            mFileUpload.uploadFile($scope.profile.avatarFile).then(
+                function(url) {
+                    $scope.profile.avatarUrl = url;
+                },
+                function(error) {
+                    $scope.feedback = "Can't upload image";
+                } 
+            );
+        }
+    });
+    
+    $scope.saveProfile = function() {
+        if ($scope.profile.displayName !== $scope.config.displayName) {
+            setDisplayName($scope.profile.displayName);
+        }
+        if ($scope.profile.avatarUrl !== $scope.config.avatarUrl) {
+            setAvatar($scope.profile.avatarUrl);
+        }
+    };
+    
+    var setDisplayName = function(displayName) {
+        matrixService.setDisplayName(displayName).then(
+            function(response) {
+                $scope.feedback = "Updated display name.";
+                
+                var config = matrixService.config();
+                config.displayName = displayName;
+                matrixService.setConfig(config);
+                matrixService.saveConfig();
+            },
+            function(error) {
+                $scope.feedback = "Can't update display name: " + error.data;
+            }
+        );
+    };
+
+    var setAvatar = function(avatarURL) {
+        console.log("Updating avatar to " + avatarURL);
+        matrixService.setProfilePictureUrl(avatarURL).then(
+            function(response) {
+                console.log("Updated avatar");
+                $scope.feedback = "Updated avatar.";
+                
+                var config = matrixService.config();
+                config.avatarUrl = avatarURL;
+                matrixService.setConfig(config);
+                matrixService.saveConfig();
+            },
+            function(error) {
+                $scope.feedback = "Can't update avatar: " + error.data;
+            }
+        );
+    };
+
+    $scope.linkedEmails = {
+        linkNewEmail: "", // the email entry box
+        emailBeingAuthed: undefined, // to populate verification text
+        authTokenId: undefined, // the token id from the IS
+        emailCode: "", // the code entry box
+        linkedEmailList: matrixService.config().emailList // linked email list
+    };
+    
+    $scope.linkEmail = function(email) {
+        matrixService.linkEmail(email).then(
+            function(response) {
+                if (response.data.success === true) {
+                    $scope.linkedEmails.authTokenId = response.data.tokenId;
+                    $scope.emailFeedback = "You have been sent an email.";
+                    $scope.linkedEmails.emailBeingAuthed = email;
+                }
+                else {
+                    $scope.emailFeedback = "Failed to send email.";
+                }
+            },
+            function(error) {
+                $scope.emailFeedback = "Can't send email: " + error.data;
+            }
+        );
+    };
+
+    $scope.submitEmailCode = function(code) {
+        var tokenId = $scope.linkedEmails.authTokenId;
+        if (tokenId === undefined) {
+            $scope.emailFeedback = "You have not requested a code with this email.";
+            return;
+        }
+        matrixService.authEmail(matrixService.config().user_id, tokenId, code).then(
+            function(response) {
+                if ("success" in response.data && response.data.success === false) {
+                    $scope.emailFeedback = "Failed to authenticate email.";
+                    return;
+                }
+                var config = matrixService.config();
+                var emailList = {};
+                if ("emailList" in config) {
+                    emailList = config.emailList;
+                }
+                emailList[response.address] = response;
+                // save the new email list
+                config.emailList = emailList;
+                matrixService.setConfig(config);
+                matrixService.saveConfig();
+                // invalidate the email being authed and update UI.
+                $scope.linkedEmails.emailBeingAuthed = undefined;
+                $scope.emailFeedback = "";
+                $scope.linkedEmails.linkedEmailList = emailList;
+                $scope.linkedEmails.linkNewEmail = "";
+                $scope.linkedEmails.emailCode = "";
+            },
+            function(reason) {
+                $scope.emailFeedback = "Failed to auth email: " + reason;
+            }
+        );
+    };
+}]);
\ No newline at end of file
diff --git a/webclient/settings/settings.html b/webclient/settings/settings.html
new file mode 100644
index 0000000000..453a4fc35f
--- /dev/null
+++ b/webclient/settings/settings.html
@@ -0,0 +1,73 @@
+<div ng-controller="SettingsController" class="user">
+
+    <div id="page">
+    <div id="wrapper">
+        
+        <h3>Me</h3>
+        <div>
+            <form>
+                <table>
+                    <tr>
+                        <td>
+                            <div class="profile-avatar">
+                                <img ng-src="{{ profile.avatarUrl || 'img/default-profile.jpg' }}" m-file-input="profile.avatarFile"/>
+                            </div>
+                        </td>
+                        <td>
+                            <div id="user-ids">
+                                <input size="40" ng-model="profile.displayName" placeholder="Your name"/>            
+                            </div>
+                        </td>
+                        <td>
+                            <button ng-disabled="(profile.displayName == config.displayName) && (profile.avatarUrl == config.avatarUrl)"
+                                    ng-click="saveProfile()">Save</button>    
+                        </td>
+                    </tr>
+                </table>
+            </form>
+        </div>
+        <br/>
+
+        <h3>Linked emails</h3>
+        <div>
+            <form>
+                <input size="40" ng-model="linkedEmails.linkNewEmail" ng-enter="linkEmail(linkedEmails.linkNewEmail)" />
+                <button ng-disabled="!linkedEmails.linkNewEmail" ng-click="linkEmail(linkedEmails.linkNewEmail)">
+                    Link Email
+                </button>
+                {{ emailFeedback }}    
+            </form>
+            <form ng-hide="!linkedEmails.emailBeingAuthed">
+                Enter validation token for {{ linkedEmails.emailBeingAuthed }}:
+                <br />
+                <input size="20" ng-model="linkedEmails.emailCode" ng-enter="submitEmailCode(linkedEmails.emailCode)" />
+                <button ng-disabled="!linkedEmails.emailCode || !linkedEmails.linkNewEmail" ng-click="submitEmailCode(linkedEmails.emailCode)">
+                    Submit Code
+                </button>   
+            </form>
+            <table>
+                <tr ng-repeat="(address, info) in linkedEmails.linkedEmailList">
+                    <td>{{address}}</td>
+                </tr>
+            </table>
+        </div>
+        <br/>
+
+        <h3>Configuration</h3>
+        <div>
+            <div>Home server: {{ config.homeserver }} </div>
+            <div>User ID: {{ config.user_id }} </div>
+            <div>Access token: {{ config.access_token }} </div>
+        </div>
+        <br/>
+        
+        <div>
+            <div><button ng-click="requestNotifications()">Request notifications</button></div>
+        </div>
+        <br/>
+
+        {{ feedback }}
+
+    </div>    
+    </div>
+</div>
diff --git a/webclient/user/user.html b/webclient/user/user.html
index 47db09d1ee..4c91c8a48a 100644
--- a/webclient/user/user.html
+++ b/webclient/user/user.html
@@ -1,4 +1,5 @@
 <div ng-controller="UserController" class="user">
+    <h1 id="logo">[matrix]</h1>
 
     <div id="page">
     <div id="wrapper">