summary refs log tree commit diff
path: root/webclient
diff options
context:
space:
mode:
authorErik Johnston <erik@matrix.org>2014-08-22 15:50:23 +0100
committerErik Johnston <erik@matrix.org>2014-08-22 15:50:23 +0100
commite1297c922d7f88eed088b365bd6cf15951443506 (patch)
tree57fb71ff724a4046d3a8e3ea4ce3de4735b9dd69 /webclient
parentAdded initial swagger REST API spec. (diff)
parentMerge branch 'develop' of github.com:matrix-org/synapse into release-v0.0.1 (diff)
downloadsynapse-e1297c922d7f88eed088b365bd6cf15951443506.tar.xz
Merge branch 'release-v0.0.1' of github.com:matrix-org/synapse
Diffstat (limited to 'webclient')
-rw-r--r--webclient/app-controller.js4
-rw-r--r--webclient/app-filter.js7
-rw-r--r--webclient/app.css71
-rw-r--r--webclient/app.js2
-rw-r--r--webclient/components/fileUpload/file-upload-service.js140
-rw-r--r--webclient/components/matrix/event-handler-service.js26
-rw-r--r--webclient/components/matrix/event-stream-service.js52
-rw-r--r--webclient/components/matrix/matrix-service.js52
-rw-r--r--webclient/components/utilities/utilities-service.js151
-rw-r--r--webclient/index.html1
-rw-r--r--webclient/login/login-controller.js6
-rw-r--r--webclient/room/room-controller.js87
-rw-r--r--webclient/room/room-directive.js30
-rw-r--r--webclient/room/room.html50
-rw-r--r--webclient/rooms/rooms-controller.js97
-rw-r--r--webclient/rooms/rooms.html2
16 files changed, 628 insertions, 150 deletions
diff --git a/webclient/app-controller.js b/webclient/app-controller.js
index 96656e12c3..92ad01e4f9 100644
--- a/webclient/app-controller.js
+++ b/webclient/app-controller.js
@@ -53,7 +53,7 @@ angular.module('MatrixWebClientController', ['matrixService'])
     };
 
     if (matrixService.isUserLoggedIn()) {
-        eventStreamService.resume();
+        // eventStreamService.resume();
     }
     
     // Logs the user out 
@@ -66,7 +66,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 869db69cd6..207f35f5f3 100644
--- a/webclient/app.css
+++ b/webclient/app.css
@@ -66,6 +66,10 @@ h1 {
     background-color: #faa;
 }
 
+.mouse-pointer {
+    cursor: pointer;
+}
+
 /*** Participant list ***/
 
 #usersTableWrapper {
@@ -89,7 +93,6 @@ h1 {
     height: 100px;
     position: relative;
     background-color: #000;
-    cursor: pointer;
 }
 
 .userAvatar .userAvatarImage {
@@ -142,6 +145,7 @@ h1 {
     max-width: 1280px;
     width: 100%;
     border-collapse: collapse;
+    table-layout: fixed;
 }
         
 #messageTable td {
@@ -149,7 +153,8 @@ h1 {
 }
 
 .leftBlock {
-    width: 10em;
+    width: 14em;
+    word-wrap: break-word;
     vertical-align: top;
     background-color: #fff;
     color: #888;
@@ -187,24 +192,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;
 }
 
@@ -216,33 +210,66 @@ 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 {
+.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;
 }
 
-.mine .emote {
-    background-color: #fff ! important;    
+#room-fullscreen-image {
+    position: absolute;
+    top: 0px;
+    height: 0px;
+    width: 100%;
+    height: 100%;
 }
 
-.mine .text .bubble {
-    text-align: left ! important;
+#room-fullscreen-image img {
+    max-width: 100%;
+    max-height: 100%;
+    bottom: 0;
+    left: 0;
+    margin: auto;
+    overflow: auto;
+    position: fixed;
+    right: 0;
+    top: 0;
 }
 
 /*** Profile ***/
diff --git a/webclient/app.js b/webclient/app.js
index f27ebedc6f..944b8ec270 100644
--- a/webclient/app.js
+++ b/webclient/app.js
@@ -80,6 +80,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 d620e6a4d0..5f01478fd1 100644
--- a/webclient/components/fileUpload/file-upload-service.js
+++ b/webclient/components/fileUpload/file-upload-service.js
@@ -20,19 +20,20 @@
 /*
  * Upload an HTML5 file to a server
  */
-angular.module('mFileUpload', [])
-.service('mFileUpload', ['matrixService', '$q', function (matrixService, $q) {
+angular.module('mFileUpload', ['matrixService', 'mUtilities'])
+.service('mFileUpload', ['$q', 'matrixService', 'mUtilities', function ($q, matrixService, mUtilities) {
         
     /*
-     * Upload an HTML5 file to a server and returned a promise
+     * Upload an HTML5 file or blob to a server and returned a promise
      * that will provide the URL of the uploaded file.
+     * @param {File|Blob} file the file data to send
      */
     this.uploadFile = function(file) {
         var deferred = $q.defer();
         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);
             },
@@ -44,4 +45,135 @@ angular.module('mFileUpload', [])
         
         return deferred.promise;
     };
+    
+    /*
+     * Upload an image file plus generate a thumbnail of it and upload it so that
+     * we will have all information to fulfill an image message request data.
+     * @param {File} imageFile the imageFile to send
+     * @param {Integer} thumbnailSize the max side size of the thumbnail to create
+     * @returns {promise} A promise that will be resolved by a image message object
+     *   ready to be send with the Matrix API
+     */
+    this.uploadImageAndThumbnail = function(imageFile, thumbnailSize) {
+        var self = this;
+        var deferred = $q.defer();
+
+        console.log("uploadImageAndThumbnail " + imageFile.name + " - thumbnailSize: " + thumbnailSize);
+
+        // The message structure that will be returned in the promise
+        var imageMessage = {
+            msgtype: "m.image",
+            url: undefined,
+            body: {
+                size: undefined,
+                w: undefined,
+                h: undefined,
+                mimetype: undefined
+            },
+            thumbnail_url: undefined,
+            thumbnail_info: {
+                size: undefined,
+                w: undefined,
+                h: undefined,
+                mimetype: undefined
+            }
+        };
+
+        // 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() {
+                    self.uploadFile(imageFile).then(
+                        function(url) {
+                            // Update message metadata
+                            imageMessage.url = url;
+                            imageMessage.body = {
+                                size: imageFile.size,
+                                w: size.width,
+                                h: size.height,
+                                mimetype: imageFile.type
+                            };
+
+                            // If there is no thumbnail (because the original image is smaller than thumbnailSize),
+                            // reuse the original image info for thumbnail data
+                            if (!imageMessage.thumbnail_url) {
+                                imageMessage.thumbnail_url = imageMessage.url;
+                                imageMessage.thumbnail_info = imageMessage.body;
+                            }
+
+                            // We are done
+                            deferred.resolve(imageMessage);
+                        },
+                        function(error) {
+                            console.log("      -> Can't upload image");
+                            deferred.reject(error); 
+                        }
+                    );
+                };
+
+                // Create a thumbnail if the image size exceeds thumbnailSize
+                if (Math.max(size.width, size.height) > thumbnailSize) {
+                    console.log("    Creating thumbnail...");
+                    mUtilities.resizeImage(imageFile, thumbnailSize).then(
+                        function(thumbnailBlob) {
+
+                            // Get its size
+                            mUtilities.getImageSize(thumbnailBlob).then(
+                                function(thumbnailSize) {
+                                    console.log("      -> Thumbnail size: " + JSON.stringify(thumbnailSize));
+
+                                    // Upload it to the server
+                                    self.uploadFile(thumbnailBlob).then(
+                                        function(thumbnailUrl) {
+
+                                            // Update image message data
+                                            imageMessage.thumbnail_url = thumbnailUrl;
+                                            imageMessage.thumbnail_info = {
+                                                size: thumbnailBlob.size,
+                                                w: thumbnailSize.width,
+                                                h: thumbnailSize.height,
+                                                mimetype: thumbnailBlob.type
+                                            };
+
+                                            // Then, upload the original image
+                                            uploadImage();
+                                        },
+                                        function(error) {
+                                            console.log("      -> Can't upload thumbnail");
+                                            deferred.reject(error); 
+                                        }
+                                    );
+                                },
+                                function(error) {
+                                    console.log("      -> Failed to get thumbnail size");
+                                    deferred.reject(error); 
+                                }
+                            );
+
+                        },
+                        function(error) {
+                            console.log("      -> Failed to create thumbnail: " + error);
+                            deferred.reject(error); 
+                        }
+                    );
+                }
+                else {
+                    // No need of thumbnail
+                    console.log("   Thumbnail is not required");
+                    uploadImage();
+                }
+
+            },
+            function(error) {
+                console.log("   -> Failed to get image size");
+                deferred.reject(error); 
+            }
+        );
+
+        return deferred.promise;
+    };
+
 }]);
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 c94cf0fe72..a1a98b2a36 100644
--- a/webclient/components/matrix/event-stream-service.js
+++ b/webclient/components/matrix/event-stream-service.js
@@ -25,7 +25,6 @@ the eventHandlerService.
 angular.module('eventStreamService', [])
 .factory('eventStreamService', ['$q', '$timeout', 'matrixService', 'eventHandlerService', function($q, $timeout, matrixService, eventHandlerService) {
     var END = "END";
-    var START = "START";
     var TIMEOUT_MS = 30000;
     var ERR_TIMEOUT_MS = 5000;
     
@@ -49,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) {
@@ -64,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.");
@@ -84,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/matrix/matrix-service.js b/webclient/components/matrix/matrix-service.js
index 2463b51203..d5738e01c8 100644
--- a/webclient/components/matrix/matrix-service.js
+++ b/webclient/components/matrix/matrix-service.js
@@ -61,16 +61,23 @@ angular.module('matrixService', [])
         return doBaseRequest(config.homeserver, method, path, params, data, undefined);
     };
 
-    var doBaseRequest = function(baseUrl, method, path, params, data, headers) {
-        return $http({
+    var doBaseRequest = function(baseUrl, method, path, params, data, headers, $httpParams) {
+
+        var request = {
             method: method,
             url: baseUrl + path,
             params: params,
             data: data,
             headers: headers
-        });
-    };
+        };
+
+        // Add additional $http parameters
+        if ($httpParams) {
+            angular.extend(request, $httpParams);
+        }
 
+        return $http(request);
+    };
 
     return {
         /****** Home server API ******/
@@ -204,11 +211,11 @@ angular.module('matrixService', [])
         },
 
         // Send an image message
-        sendImageMessage: function(room_id, image_url, image_alt, msg_id) {
+        sendImageMessage: function(room_id, image_url, image_body, msg_id) {
             var content = {
                  msgtype: "m.image",
                  url: image_url,
-                 body: image_alt
+                 body: image_body
             };
 
             return this.sendMessage(room_id, msg_id, content);
@@ -239,8 +246,8 @@ angular.module('matrixService', [])
             path = path.replace("$room_id", room_id);
             var params = {
                 from: from_token,
-                to: "START",
-                limit: limit
+                limit: limit,
+                dir: 'b'
             };
             return doRequest("GET", path, params);
         },
@@ -302,17 +309,25 @@ angular.module('matrixService', [])
         },
 
         // hit the Identity Server for a 3PID request.
-        linkEmail: function(email) {
+        linkEmail: function(email, clientSecret, sendAttempt) {
             var path = "/matrix/identity/api/v1/validate/email/requestToken"
-            var data = "clientSecret=abc123&email=" + encodeURIComponent(email);
+            var data = "clientSecret="+clientSecret+"&email=" + encodeURIComponent(email)+"&sendAttempt="+sendAttempt;
             var headers = {};
             headers["Content-Type"] = "application/x-www-form-urlencoded";
             return doBaseRequest(config.identityServer, "POST", path, {}, data, headers); 
         },
 
-        authEmail: function(userId, tokenId, code) {
+        authEmail: function(clientSecret, tokenId, code) {
             var path = "/matrix/identity/api/v1/validate/email/submitToken";
-            var data = "token="+code+"&mxId="+encodeURIComponent(userId)+"&tokenId="+tokenId;
+            var data = "token="+code+"&sid="+tokenId+"&clientSecret="+clientSecret;
+            var headers = {};
+            headers["Content-Type"] = "application/x-www-form-urlencoded";
+            return doBaseRequest(config.identityServer, "POST", path, {}, data, headers);
+        },
+
+        bindEmail: function(userId, tokenId, clientSecret) {
+            var path = "/matrix/identity/api/v1/3pid/bind";
+            var data = "mxid="+encodeURIComponent(userId)+"&sid="+tokenId+"&clientSecret="+clientSecret;
             var headers = {};
             headers["Content-Type"] = "application/x-www-form-urlencoded";
             return doBaseRequest(config.identityServer, "POST", path, {}, data, headers); 
@@ -326,7 +341,17 @@ angular.module('matrixService', [])
             var params = {
                 access_token: config.access_token
             };
-            return doBaseRequest(config.homeserver, "POST", path, params, file, headers);
+
+            // If the file is actually a Blob object, prevent $http from JSON-stringified it before sending
+            // (Equivalent to jQuery ajax processData = false)
+            var $httpParams;
+            if (file instanceof Blob) {
+                $httpParams = {
+                    transformRequest: angular.identity
+                };
+            }
+
+            return doBaseRequest(config.homeserver, "POST", path, params, file, headers, $httpParams);
         },
         
         // start listening on /events
@@ -375,6 +400,7 @@ angular.module('matrixService', [])
         // Set a new config (Use saveConfig to actually store it permanently)
         setConfig: function(newConfig) {
             config = newConfig;
+            console.log("new IS: "+config.identityServer);
         },
         
         // Commits config into permanent storage
diff --git a/webclient/components/utilities/utilities-service.js b/webclient/components/utilities/utilities-service.js
new file mode 100644
index 0000000000..3df2f04458
--- /dev/null
+++ b/webclient/components/utilities/utilities-service.js
@@ -0,0 +1,151 @@
+/*
+ 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';
+
+/*
+ * This service contains multipurpose helper functions.
+ */
+angular.module('mUtilities', [])
+.service('mUtilities', ['$q', function ($q) {
+    /*
+     * Get the size of an image
+     * @param {File|Blob} imageFile the file containing the image
+     * @returns {promise} A promise that will be resolved by an object with 2 members:
+     *   width & height
+     */
+    this.getImageSize = function(imageFile) {
+        var deferred = $q.defer();
+        
+        // Load the file into an html element
+        var img = document.createElement("img");
+        
+        var reader = new FileReader();  
+        reader.onload = function(e) {   
+            img.src = e.target.result;
+            
+            // Once ready, returns its size
+            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);
+        };
+        reader.readAsDataURL(imageFile);
+        
+        return deferred.promise;
+    };
+
+    /*
+     * Resize the image to fit in a square of the side maxSize. 
+     * The aspect ratio is kept. The returned image data uses JPEG compression.
+     * Source: http://hacks.mozilla.org/2011/01/how-to-develop-a-html5-image-uploader/
+     * @param {File} imageFile the file containing the image 
+     * @param {Integer} maxSize the max side size 
+     * @returns {promise} A promise that will be resolved by a Blob object containing
+     *   the resized image data
+     */
+    this.resizeImage = function(imageFile, maxSize) {
+        var self = this;
+        var deferred = $q.defer();
+
+        var canvas = document.createElement("canvas");
+
+        var img = document.createElement("img");
+        var reader = new FileReader();  
+        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 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;
+                    }
+                }
+                canvas.width = width;
+                canvas.height = height;
+                var ctx = canvas.getContext("2d");
+                ctx.drawImage(img, 0, 0, width, height);
+
+                // 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);
+        };
+        reader.readAsDataURL(imageFile);
+
+        return deferred.promise;
+    };
+
+    /*
+     * Convert a dataURI string to a blob 
+     * Source: http://stackoverflow.com/a/17682951
+     * @param {String} dataURI the dataURI can be a base64 encoded string or an URL encoded string.
+     * @returns {Blob} the blob
+     */
+    this.dataURItoBlob = function(dataURI) {
+        // convert base64 to raw binary data held in a string
+        // doesn't handle URLEncoded DataURIs
+        var byteString;
+        if (dataURI.split(',')[0].indexOf('base64') >= 0)
+            byteString = atob(dataURI.split(',')[1]);
+        else
+            byteString = unescape(dataURI.split(',')[1]);
+        // separate out the mime component
+        var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
+
+        // write the bytes of the string to an ArrayBuffer
+        var ab = new ArrayBuffer(byteString.length);
+        var ia = new Uint8Array(ab);
+        for (var i = 0; i < byteString.length; i++) {
+            ia[i] = byteString.charCodeAt(i);
+        }
+
+        // write the ArrayBuffer to a blob, and you're done
+        return new Blob([ab],{type: mimeString});
+    };
+
+}]);
\ No newline at end of file
diff --git a/webclient/index.html b/webclient/index.html
index a7e9cd9341..27d9208193 100644
--- a/webclient/index.html
+++ b/webclient/index.html
@@ -25,6 +25,7 @@
     <script src="components/matrix/event-handler-service.js"></script>
     <script src="components/fileInput/file-input-directive.js"></script>
     <script src="components/fileUpload/file-upload-service.js"></script>
+    <script src="components/utilities/utilities-service.js"></script>
 </head>
 
 <body>
diff --git a/webclient/login/login-controller.js b/webclient/login/login-controller.js
index 67d0b7b90c..e3d0eca946 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("rooms");
             },
             function(error) {
                 if (error.data) {
@@ -70,6 +70,7 @@ angular.module('LoginController', ['matrixService'])
     $scope.login = function() {
         matrixService.setConfig({
             homeserver: $scope.account.homeserver,
+            identityServer: $scope.account.identityServer,
             user_id: $scope.account.user_id
         });
         // try to login
@@ -79,12 +80,13 @@ angular.module('LoginController', ['matrixService'])
                     $scope.feedback = "Login successful.";
                     matrixService.setConfig({
                         homeserver: $scope.account.homeserver,
+                        identityServer: $scope.account.identityServer,
                         user_id: response.data.user_id,
                         access_token: response.data.access_token
                     });
                     matrixService.saveConfig();
                     eventStreamService.resume();
-                    $location.path("rooms");
+                    $location.url("rooms");
                 }
                 else {
                     $scope.feedback = "Failed to login: " + JSON.stringify(response.data);
diff --git a/webclient/room/room-controller.js b/webclient/room/room-controller.js
index 364ca41510..58ba432ce5 100644
--- a/webclient/room/room-controller.js
+++ b/webclient/room/room-controller.js
@@ -14,11 +14,12 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-angular.module('RoomController', ['ngSanitize'])
-.controller('RoomController', ['$scope', '$http', '$timeout', '$routeParams', '$location', 'matrixService', 'eventStreamService', 'eventHandlerService',
-                               function($scope, $http, $timeout, $routeParams, $location, matrixService, eventStreamService, eventHandlerService) {
+angular.module('RoomController', ['ngSanitize', '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;
 
     // Room ids. Computed and resolved in onInit
     $scope.room_id = undefined;
@@ -28,9 +29,12 @@ angular.module('RoomController', ['ngSanitize'])
         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
+        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 = {};
     $scope.autoCompleting = false;
@@ -88,7 +92,7 @@ angular.module('RoomController', ['ngSanitize'])
         
     var paginate = function(numItems) {
         // console.log("paginate " + numItems);
-        if ($scope.state.paginating) {
+        if ($scope.state.paginating || !$scope.room_id) {
             return;
         }
         else {
@@ -98,7 +102,6 @@ angular.module('RoomController', ['ngSanitize'])
         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) {
@@ -124,8 +127,9 @@ angular.module('RoomController', ['ngSanitize'])
                     }, 0);
                 }
                 
-                if (firstPagination) {
+                if ($scope.state.first_pagination) {
                     scrollToBottom();
+                    $scope.state.first_pagination = false;
                 }
                 else {
                     // lock the scroll position
@@ -144,10 +148,12 @@ angular.module('RoomController', ['ngSanitize'])
                 console.log("Failed to paginateBackMessages: " + JSON.stringify(error));
                 $scope.state.paginating = false;
             }
-        )
+        );
     };
 
     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?
@@ -157,8 +163,7 @@ angular.module('RoomController', ['ngSanitize'])
             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) {
@@ -167,9 +172,11 @@ angular.module('RoomController', ['ngSanitize'])
             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 
@@ -193,6 +200,11 @@ angular.module('RoomController', ['ngSanitize'])
                     }
                 );
             });
+*/            
+
+            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 :/
@@ -232,7 +244,9 @@ angular.module('RoomController', ['ngSanitize'])
         if ($scope.textInput == "") {
             return;
         }
-                    
+
+        $scope.state.sending = true;
+        
         // Send the text message
         var promise;
         // FIXME: handle other commands too
@@ -247,16 +261,17 @@ angular.module('RoomController', ['ngSanitize'])
             function() {
                 console.log("Sent message");
                 $scope.textInput = "";
+                $scope.state.sending = false;
             },
             function(error) {
                 $scope.feedback = "Failed to send: " + error.data.error;
-            });               
+                $scope.state.sending = false;
+            });
     };
 
     $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) {
@@ -284,7 +299,7 @@ angular.module('RoomController', ['ngSanitize'])
                 else {
                     // In case of issue, go to the default page
                     console.log("Error: cannot extract room alias");
-                    $location.path("/");
+                    $location.url("/");
                     return;
                 }
             }
@@ -301,12 +316,14 @@ angular.module('RoomController', ['ngSanitize'])
             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() {
@@ -319,6 +336,7 @@ angular.module('RoomController', ['ngSanitize'])
                             var chunk = response.data.chunk[i];
                             updateMemberList(chunk);
                         }
+                        eventStreamService.resume();
                     },
                     function(error) {
                         $scope.feedback = "Failed get member list: " + error.data.error;
@@ -354,36 +372,51 @@ angular.module('RoomController', ['ngSanitize'])
         matrixService.leave($scope.room_id).then(
             function(response) {
                 console.log("Left room ");
-                $location.path("rooms");
+                $location.url("rooms");
             },
             function(error) {
                 $scope.feedback = "Failed to leave room: " + error.data.error;
             });
     };
 
-    $scope.sendImage = function(url) {
-        matrixService.sendImageMessage($scope.room_id, url).then(
+    $scope.sendImage = function(url, body) {
+        $scope.state.sending = true;
+
+        matrixService.sendImageMessage($scope.room_id, url, body).then(
             function() {
                 console.log("Image sent");
+                $scope.state.sending = false;
             },
             function(error) {
                 $scope.feedback = "Failed to send image: " + error.data.error;
+                $scope.state.sending = false;
             });
     };
     
     $scope.imageFileToSend;
     $scope.$watch("imageFileToSend", function(newValue, oldValue) {
         if ($scope.imageFileToSend) {
-            // First download the image to the Internet
-            console.log("Uploading image...");
-            mFileUpload.uploadFile($scope.imageFileToSend).then(
-                function(url) {
-                    // Then share the URL
-                    $scope.sendImage(url);
+
+            $scope.state.sending = true;
+
+            // Upload this image with its thumbnail to Internet
+            mFileUpload.uploadImageAndThumbnail($scope.imageFileToSend, THUMBNAIL_SIZE).then(
+                function(imageMessage) {
+                    // imageMessage is complete message structure, send it as is
+                    matrixService.sendMessage($scope.room_id, undefined, imageMessage).then(
+                        function() {
+                            console.log("Image message sent");
+                            $scope.state.sending = false;
+                        },
+                        function(error) {
+                            $scope.feedback = "Failed to send image message: " + error.data.error;
+                            $scope.state.sending = false;
+                        });
                 },
                 function(error) {
                     $scope.feedback = "Can't upload image";
-                } 
+                    $scope.state.sending = false;
+                }
             );
         }
     });
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 8cae7ee515..95da067714 100644
--- a/webclient/room/room.html
+++ b/webclient/room/room.html
@@ -10,7 +10,7 @@
     <div id="usersTableWrapper">
         <table id="usersTable">
             <tr ng-repeat="member in members | orderMembersList">
-                <td class="userAvatar" ng-click="goToUserPage(member.id)">
+                <td class="userAvatar mouse-pointer" ng-click="goToUserPage(member.id)">
                     <img class="userAvatarImage" 
                          ng-src="{{member.avatar_url || 'img/default-profile.jpg'}}" 
                          alt="{{ member.displayname || member.id.substr(0, member.id.indexOf(':')) }}"
@@ -26,22 +26,36 @@
     </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:ss' }}</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.body | linky:'_blank'"/>
-                        <img class="image" ng-hide='msg.content.msgtype !== "m.image"' ng-src="{{ msg.content.url }}" alt="{{ msg.content.body }}"/>
+                        <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"'>
+                            <div ng-hide='msg.content.thumbnail_url' ng-style="msg.content.body.h && { 'height' : (msg.content.body.h < 320) ? msg.content.body.h : 320}">
+                                <img class="image" ng-src="{{ msg.content.url }}"/>
+                            </div>
+                            <div ng-show='msg.content.thumbnail_url' ng-style="{ 'height' : msg.content.thumbnail_info.h }">
+                                <img class="image mouse-pointer" ng-src="{{ msg.content.thumbnail_url }}"
+                                     ng-click="$parent.fullScreenImageURL = msg.content.url"/>
+                            </div>
+                        </div>
                     </div>
                 </td>
                 <td class="rightBlock">
@@ -63,30 +77,16 @@
                         {{ state.user_id }} 
                     </td>
                     <td width="*" style="min-width: 100px">
-                        <input id="mainInput" ng-model="textInput" ng-enter="send()" ng-focus="true" auto-complete/>
+                        <input id="mainInput" ng-model="textInput" ng-enter="send()" ng-focus="true" autocomplete="off" tab-complete/>
                     </td>
-                    <td width="1">
+                    <td width="150px">
                         <button ng-click="send()">Send</button>
+                        <button m-file-input="imageFileToSend">Send Image</button>
                     </td>
                     <td width="1">
                         
                     </td>
                 </tr>
-                <tr>
-                    <td>
-                    </td>
-                    <td>
-                        <input id="mainInput" ng-model="imageURLToSend" ng-enter="sendImage(imageURLToSend)" placeholder="Image URL"/>
-                    </td>
-                    <td width="100px">
-                        <button ng-click="sendImage(imageURLToSend)">Send URL</button>
-                    </td>
-                    <!-- TODO: To enable once we have an upload server
-                    <td width="100px">
-                        <button m-file-input="imageFileToSend">Send Image</button>
-                    </td>
-                    -->
-                </tr>
             </table>
 
             <span>
@@ -103,4 +103,8 @@
         </div>
     </div>
 
+    <div id="room-fullscreen-image" ng-show="fullScreenImageURL" ng-click="fullScreenImageURL = undefined;">
+        <img ng-src="{{ fullScreenImageURL }}"/>
+    </div>
+
  </div>
diff --git a/webclient/rooms/rooms-controller.js b/webclient/rooms/rooms-controller.js
index a237b59b4e..d9c8baff47 100644
--- a/webclient/rooms/rooms-controller.js
+++ b/webclient/rooms/rooms-controller.js
@@ -17,8 +17,8 @@ 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) {
+.controller('RoomsController', ['$scope', '$location', 'matrixService', 'mFileUpload', 'eventHandlerService', 'eventStreamService', 
+                               function($scope, $location, matrixService, mFileUpload, eventHandlerService, eventStreamService) {
                                    
     $scope.rooms = {};
     $scope.public_rooms = [];
@@ -48,6 +48,8 @@ angular.module('RoomsController', ['matrixService', 'mFileInput', 'mFileUpload',
         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
     };
@@ -59,7 +61,7 @@ angular.module('RoomsController', ['matrixService', 'mFileInput', 'mFileUpload',
             // 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";
+            event.room_display_name = event.user_id + "'s room";
             $scope.rooms[event.room_id] = event;
         }
     });
@@ -70,14 +72,20 @@ angular.module('RoomsController', ['matrixService', 'mFileInput', 'mFileUpload',
             if (alias) {
                 // use the existing alias from storage
                 data[i].room_alias = alias;
+                data[i].room_display_name = alias;
             }
-            else if (data[i].room_alias) {
+            else if (data[i].aliases && data[i].aliases[0]) {
                 // save the mapping
-                matrixService.createRoomIdToAliasMapping(data[i].room_id, data[i].room_alias);
+                // 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_alias = data[i].room_id;
+                data[i].room_display_name = data[i].room_id;
             }
         }
         return data;
@@ -87,11 +95,16 @@ angular.module('RoomsController', ['matrixService', 'mFileInput', 'mFileUpload',
         // List all rooms joined or been invited to
         matrixService.rooms().then(
             function(response) {
-                var data = assignRoomAliases(response.data);
+                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;
@@ -102,6 +115,8 @@ angular.module('RoomsController', ['matrixService', 'mFileInput', 'mFileUpload',
                 $scope.public_rooms = assignRoomAliases(response.data.chunk);
             }
         );
+
+        eventStreamService.resume();
     };
     
     $scope.createNewRoom = function(room_id, isPrivate) {
@@ -128,17 +143,17 @@ angular.module('RoomsController', ['matrixService', 'mFileInput', 'mFileUpload',
     // Go to a room
     $scope.goToRoom = function(room_id) {
         // Simply open the room page on this room id
-        //$location.path("room/" + 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.path("room/" + response.data.room_id);
+                        $location.url("room/" + response.data.room_id);
                         return;
                      }
                 }
 
-                $location.path("room/" + room_id);
+                $location.url("room/" + room_id);
             },
             function(error) {
                 $scope.feedback = "Can't join room: " + error.data;
@@ -150,7 +165,7 @@ angular.module('RoomsController', ['matrixService', 'mFileInput', 'mFileUpload',
         matrixService.joinAlias(room_alias).then(
             function(response) {
                 // Go to this room
-                $location.path("room/" + room_alias);
+                $location.url("room/" + room_alias);
             },
             function(error) {
                 $scope.feedback = "Can't join room: " + error.data;
@@ -206,11 +221,27 @@ angular.module('RoomsController', ['matrixService', 'mFileInput', 'mFileUpload',
         );
     };
 
+    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) {
-        matrixService.linkEmail(email).then(
+        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.tokenId;
+                    $scope.linkedEmails.authTokenId = response.data.sid;
                     $scope.emailFeedback = "You have been sent an email.";
                     $scope.linkedEmails.emailBeingAuthed = email;
                 }
@@ -230,28 +261,34 @@ angular.module('RoomsController', ['matrixService', 'mFileInput', 'mFileUpload',
             $scope.emailFeedback = "You have not requested a code with this email.";
             return;
         }
-        matrixService.authEmail(matrixService.config().user_id, tokenId, code).then(
+        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;
                 }
-                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 = "";
+                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;
diff --git a/webclient/rooms/rooms.html b/webclient/rooms/rooms.html
index 2602209bd3..ba3b7d8bad 100644
--- a/webclient/rooms/rooms.html
+++ b/webclient/rooms/rooms.html
@@ -65,7 +65,7 @@
     
     <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)' : ''}}
+            <a href="#/room/{{ room.room_alias ? room.room_alias : rm_id }}" >{{ room.room_display_name }}</a> {{room.membership === 'invite' ? ' (invited)' : ''}}
         </div>
     </div>
     <br/>