summary refs log tree commit diff
path: root/webclient
diff options
context:
space:
mode:
Diffstat (limited to 'webclient')
-rw-r--r--webclient/app-controller.js10
-rw-r--r--webclient/app.css14
-rw-r--r--webclient/app.js22
-rw-r--r--webclient/components/fileInput/file-input-directive.js43
-rw-r--r--webclient/components/fileUpload/file-upload-service.js47
-rw-r--r--webclient/components/matrix/matrix-service.js36
-rw-r--r--webclient/index.html2
-rw-r--r--webclient/login/login-controller.js46
-rw-r--r--webclient/login/login.html5
-rw-r--r--webclient/room/room-controller.js114
-rw-r--r--webclient/room/room.html6
-rw-r--r--webclient/rooms/rooms-controller.js83
-rw-r--r--webclient/rooms/rooms.html25
13 files changed, 343 insertions, 110 deletions
diff --git a/webclient/app-controller.js b/webclient/app-controller.js
index 41055bdcd2..086fa3d946 100644
--- a/webclient/app-controller.js
+++ b/webclient/app-controller.js
@@ -55,8 +55,14 @@ angular.module('MatrixWebClientController', ['matrixService'])
         
         // And go to the login page
         $location.path("login");
-    };    
-                          
+    };
+
+    // Listen to the event indicating that the access token is no more valid.
+    // In this case, the user needs to log in again.
+    $scope.$on("M_UNKNOWN_TOKEN", function() {
+        console.log("Invalid access token -> log user out");
+        $scope.logout();
+    });
 }]);
 
    
\ No newline at end of file
diff --git a/webclient/app.css b/webclient/app.css
index 65049c95c9..122f25c9ff 100644
--- a/webclient/app.css
+++ b/webclient/app.css
@@ -219,6 +219,20 @@ h1 {
     background-color: #fff ! important;
 }
 
+/*** Profile ***/
+
+.profile-avatar {
+    width: 160px;
+    height: 160px;
+    display:table-cell;
+    vertical-align: middle;
+}
+
+.profile-avatar img {
+    max-width: 100%;
+    max-height: 100%;
+}
+
 /******************************/
 
 .header {
diff --git a/webclient/app.js b/webclient/app.js
index 651aeeaa77..0b613fa206 100644
--- a/webclient/app.js
+++ b/webclient/app.js
@@ -23,8 +23,8 @@ var matrixWebClient = angular.module('matrixWebClient', [
     'matrixService'
 ]);
 
-matrixWebClient.config(['$routeProvider',
-    function($routeProvider) {
+matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider',
+    function($routeProvider, $provide, $httpProvider) {
         $routeProvider.
             when('/login', {
                 templateUrl: 'login/login.html',
@@ -41,6 +41,22 @@ matrixWebClient.config(['$routeProvider',
             otherwise({
                 redirectTo: '/rooms'
             });
+            
+        $provide.factory('AccessTokenInterceptor', ['$q', '$rootScope', 
+            function ($q, $rootScope) {
+            return {
+                responseError: function(rejection) {
+                    if (rejection.status === 403 && "data" in rejection && 
+                            "errcode" in rejection.data && 
+                            rejection.data.errcode === "M_UNKNOWN_TOKEN") {
+                        console.log("Got a 403 with an unknown token. Logging out.")
+                        $rootScope.$broadcast("M_UNKNOWN_TOKEN");
+                    }
+                    return $q.reject(rejection);
+                }
+            };
+        }]);
+        $httpProvider.interceptors.push('AccessTokenInterceptor');
     }]);
 
 matrixWebClient.run(['$location', 'matrixService' , function($location, matrixService) {
@@ -75,4 +91,4 @@ matrixWebClient
         return function(text) {
             return $sce.trustAsHtml(text);
         };
-    }]);
\ No newline at end of file
+    }]);
diff --git a/webclient/components/fileInput/file-input-directive.js b/webclient/components/fileInput/file-input-directive.js
new file mode 100644
index 0000000000..9b73f877e9
--- /dev/null
+++ b/webclient/components/fileInput/file-input-directive.js
@@ -0,0 +1,43 @@
+/*
+ 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';
+
+/*
+ * Transform an element into an image file input button.
+ * Watch to the passed variable change. It will contain the selected HTML5 file object.
+ */
+angular.module('mFileInput', [])
+.directive('mFileInput', function() {
+    return {
+        restrict: 'A',
+        transclude: 'true',
+        template: '<div ng-transclude></div><input ng-hide="true" type="file" accept="image/*"/>',
+        scope: {
+            selectedFile: '=mFileInput'
+        },
+        
+        link: function(scope, element, attrs, ctrl) {
+            element.bind("click", function() {
+                element.find("input")[0].click();
+                element.find("input").bind("change", function(e) {
+                    scope.selectedFile = this.files[0];
+                    scope.$apply();
+                });
+            });
+      }
+    };
+});
\ No newline at end of file
diff --git a/webclient/components/fileUpload/file-upload-service.js b/webclient/components/fileUpload/file-upload-service.js
new file mode 100644
index 0000000000..5729d5da48
--- /dev/null
+++ b/webclient/components/fileUpload/file-upload-service.js
@@ -0,0 +1,47 @@
+/*
+ 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';
+
+/*
+ * Upload an HTML5 file to a server
+ */
+angular.module('mFileUpload', [])
+.service('mFileUpload', ['$http', '$q', function ($http, $q) {
+        
+    /*
+     * Upload an HTML5 file to a server and returned a promise
+     * that will provide the URL of the uploaded file.
+     */
+    this.uploadFile = function(file) {
+        var deferred = $q.defer();
+        
+        // @TODO: This service runs with the do_POST hacky implementation of /synapse/demos/webserver.py.
+        // This is temporary until we have a true file upload service
+        console.log("Uploading " + file.name + "...");
+        $http.post(file.name, file)
+        .success(function(data, status, headers, config) {
+            deferred.resolve(location.origin + data.url);
+            console.log("   -> Successfully uploaded! Available at " + location.origin + data.url);
+        }).
+        error(function(data, status, headers, config) {
+            console.log("   -> Failed to upload"  + file.name);
+            deferred.reject();
+        });
+        
+        return deferred.promise;
+    };
+}]);
\ No newline at end of file
diff --git a/webclient/components/matrix/matrix-service.js b/webclient/components/matrix/matrix-service.js
index f054bf301e..6d66111469 100644
--- a/webclient/components/matrix/matrix-service.js
+++ b/webclient/components/matrix/matrix-service.js
@@ -17,7 +17,7 @@ limitations under the License.
 'use strict';
 
 angular.module('matrixService', [])
-.factory('matrixService', ['$http', '$q', function($http, $q) {
+.factory('matrixService', ['$http', '$q', '$rootScope', function($http, $q, $rootScope) {
         
    /* 
     * Permanent storage of user information
@@ -49,28 +49,13 @@ angular.module('matrixService', [])
         if (path.indexOf(prefixPath) !== 0) {
             path = prefixPath + path;
         }
-        // Do not directly return the $http instance but return a promise
-        // with enriched or cleaned information
-        var deferred = $q.defer();
-        $http({
+        return $http({
             method: method,
             url: baseUrl + path,
             params: params,
             data: data,
             headers: headers
         })
-        .success(function(data, status, headers, config) {
-            // @TODO: We could detect a bad access token here and make an automatic logout
-            deferred.resolve(data, status, headers, config);
-        })
-        .error(function(data, status, headers, config) {
-            // Enrich the error callback with an human readable error reason
-            var reason = data.error;
-            if (!data.error) {
-                reason = JSON.stringify(data);
-            }
-            deferred.reject(reason, data, status, headers, config);
-        });
 
         return deferred.promise;
     };
@@ -227,6 +212,17 @@ angular.module('matrixService', [])
             path = path.replace("$room_id", room_id);
             return doRequest("GET", path);
         },
+        
+        paginateBackMessages: function(room_id, from_token, limit) {
+            var path = "/rooms/$room_id/messages/list";
+            path = path.replace("$room_id", room_id);
+            var params = {
+                from: from_token,
+                to: "START",
+                limit: limit
+            };
+            return doRequest("GET", path, params);
+        },
 
         // get a list of public rooms on your home server
         publicRooms: function() {
@@ -301,6 +297,12 @@ angular.module('matrixService', [])
             return doBaseRequest(config.identityServer, "POST", path, {}, data, headers); 
         },
         
+        
+        // 
+        testLogin: function() {
+            
+        },
+        
         /****** Permanent storage of user information ******/
         
         // Returns the current config
diff --git a/webclient/index.html b/webclient/index.html
index ddc9ab5e32..e62ec39669 100644
--- a/webclient/index.html
+++ b/webclient/index.html
@@ -14,6 +14,8 @@
     <script src="room/room-controller.js"></script>
     <script src="rooms/rooms-controller.js"></script>
     <script src="components/matrix/matrix-service.js"></script>
+    <script src="components/fileInput/file-input-directive.js"></script>
+    <script src="components/fileUpload/file-upload-service.js"></script>
 </head>
 
 <body>
diff --git a/webclient/login/login-controller.js b/webclient/login/login-controller.js
index 26590da686..8bd6a4e84f 100644
--- a/webclient/login/login-controller.js
+++ b/webclient/login/login-controller.js
@@ -3,8 +3,16 @@ angular.module('LoginController', ['matrixService'])
                                     function($scope, $location, matrixService) {
     'use strict';
     
+    
+    // Assume that this is hosted on the home server, in which case the URL
+    // contains the home server.
+    var hs_url = $location.protocol() + "://" + $location.host();
+    if ($location.port()) {
+        hs_url += ":" + $location.port();
+    }
+    
     $scope.account = {
-        homeserver: "http://localhost:8080",
+        homeserver: hs_url,
         desired_user_name: "",
         user_id: "",
         password: "",
@@ -31,14 +39,13 @@ angular.module('LoginController', ['matrixService'])
         }
 
         matrixService.register($scope.account.desired_user_name, $scope.account.pwd1).then(
-            function(data) {
+            function(response) {
                 $scope.feedback = "Success";
-
                 // Update the current config 
                 var config = matrixService.config();
                 angular.extend(config, {
-                    access_token: data.access_token,
-                    user_id: data.user_id
+                    access_token: response.data.access_token,
+                    user_id: response.data.user_id
                 });
                 matrixService.setConfig(config);
 
@@ -48,8 +55,15 @@ angular.module('LoginController', ['matrixService'])
                  // Go to the user's rooms list page
                 $location.path("rooms");
             },
-            function(reason) {
-                $scope.feedback = "Failure: " + reason;
+            function(error) {
+                if (error.data) {
+                    if (error.data.errcode === "M_USER_IN_USE") {
+                        $scope.feedback = "Username already taken.";
+                    }
+                }
+                else if (error.status === 0) {
+                    $scope.feedback = "Unable to talk to the server.";
+                }
             });
     };
 
@@ -61,18 +75,28 @@ angular.module('LoginController', ['matrixService'])
         // try to login
         matrixService.login($scope.account.user_id, $scope.account.password).then(
             function(response) {
-                if ("access_token" in response) {
+                if ("access_token" in response.data) {
                     $scope.feedback = "Login successful.";
                     matrixService.setConfig({
                         homeserver: $scope.account.homeserver,
-                        user_id: $scope.account.user_id,
-                        access_token: response.access_token
+                        user_id: response.data.user_id,
+                        access_token: response.data.access_token
                     });
                     matrixService.saveConfig();
                     $location.path("rooms");
                 }
                 else {
-                    $scope.feedback = "Failed to login: " + JSON.stringify(response);
+                    $scope.feedback = "Failed to login: " + JSON.stringify(response.data);
+                }
+            },
+            function(error) {
+                if (error.data) {
+                    if (error.data.errcode === "M_FORBIDDEN") {
+                        $scope.login_error_msg = "Incorrect username or password.";
+                    }
+                }
+                else if (error.status === 0) {
+                    $scope.login_error_msg = "Unable to talk to the server.";
                 }
             }
         );
diff --git a/webclient/login/login.html b/webclient/login/login.html
index 508ff5e4bf..a8b2b1f12d 100644
--- a/webclient/login/login.html
+++ b/webclient/login/login.html
@@ -15,15 +15,16 @@
         <!-- New user registration -->
         <div>
             <br/>
-            <button ng-click="register()" ng-disabled="!account.desired_user_name || !account.homeserver || !account.identityServer || !account.pwd1 || !account.pwd2">Register</button>
+            <button ng-click="register()" ng-disabled="!account.desired_user_name || !account.homeserver || !account.identityServer || !account.pwd1 || !account.pwd2 || account.pwd1 !== account.pwd2">Register</button>
         </div>
     </form>
 
     <h3>Got an account?</h3>
     <form novalidate>
         <!-- Login with an registered user -->
+        <div>{{ login_error_msg }} </div>
         <div>
-            <input id="user_id" size="70" type="text" auto-focus ng-model="account.user_id" placeholder="User ID (ex:@bob:localhost)"/>
+            <input id="user_id" size="70" type="text" auto-focus ng-model="account.user_id" placeholder="User ID (ex:@bob:localhost or bob)"/>
             <br />
             <input id="password" size="70" type="password" ng-model="account.password" placeholder="Password"/><br />
             <br/>
diff --git a/webclient/room/room-controller.js b/webclient/room/room-controller.js
index 470f41521a..fb6e2025fc 100644
--- a/webclient/room/room-controller.js
+++ b/webclient/room/room-controller.js
@@ -18,11 +18,15 @@ angular.module('RoomController', [])
 .controller('RoomController', ['$scope', '$http', '$timeout', '$routeParams', '$location', 'matrixService',
                                function($scope, $http, $timeout, $routeParams, $location, matrixService) {
    'use strict';
+    var MESSAGES_PER_PAGINATION = 10;
     $scope.room_id = $routeParams.room_id;
     $scope.room_alias = matrixService.getRoomIdToAliasMapping($scope.room_id);
     $scope.state = {
         user_id: matrixService.config().user_id,
-        events_from: "START"
+        events_from: "END", // when to start the event stream from.
+        earliest_token: "END", // stores how far back we've paginated.
+        can_paginate: true, // this is toggled off when we run out of items
+        stream_failure: undefined // the response when the stream fails
     };
     $scope.messages = [];
     $scope.members = {};
@@ -30,6 +34,53 @@ angular.module('RoomController', [])
 
     $scope.imageURLToSend = "";
     $scope.userIDToInvite = "";
+    
+    var scrollToBottom = function() {
+        $timeout(function() {
+            var objDiv = document.getElementsByClassName("messageTableWrapper")[0];
+            objDiv.scrollTop = objDiv.scrollHeight;
+        },0);
+    };
+    
+    var parseChunk = function(chunks, appendToStart) {
+        for (var i = 0; i < chunks.length; i++) {
+            var chunk = chunks[i];
+            if (chunk.room_id == $scope.room_id && chunk.type == "m.room.message") {
+                if ("membership_target" in chunk.content) {
+                    chunk.user_id = chunk.content.membership_target;
+                }
+                if (appendToStart) {
+                    $scope.messages.unshift(chunk);
+                }
+                else {
+                    $scope.messages.push(chunk);
+                    scrollToBottom();
+                }
+            }
+            else if (chunk.room_id == $scope.room_id && chunk.type == "m.room.member") {
+                updateMemberList(chunk);
+            }
+            else if (chunk.type === "m.presence") {
+                updatePresence(chunk);
+            }
+        }
+    };
+    
+    var paginate = function(numItems) {
+        matrixService.paginateBackMessages($scope.room_id, $scope.state.earliest_token, numItems).then(
+            function(response) {
+                parseChunk(response.data.chunk, true);
+                $scope.state.earliest_token = response.data.end;
+                if (response.data.chunk.length < MESSAGES_PER_PAGINATION) {
+                    // no more messages to paginate :(
+                    $scope.state.can_paginate = false;
+                }
+            },
+            function(error) {
+                console.log("Failed to paginateBackMessages: " + JSON.stringify(error));
+            }
+        )
+    };
 
     var shortPoll = function() {
         $http.get(matrixService.config().homeserver + matrixService.prefix + "/events", {
@@ -39,30 +90,13 @@ angular.module('RoomController', [])
                 "timeout": 5000
             }})
             .then(function(response) {
+                $scope.state.stream_failure = undefined;
                 console.log("Got response from "+$scope.state.events_from+" to "+response.data.end);
                 $scope.state.events_from = response.data.end;
-
                 $scope.feedback = "";
 
-                for (var i = 0; i < response.data.chunk.length; i++) {
-                    var chunk = response.data.chunk[i];
-                    if (chunk.room_id == $scope.room_id && chunk.type == "m.room.message") {
-                        if ("membership_target" in chunk.content) {
-                            chunk.user_id = chunk.content.membership_target;
-                        }
-                        $scope.messages.push(chunk);
-                        $timeout(function() {
-                            var objDiv = document.getElementsByClassName("messageTableWrapper")[0];
-                            objDiv.scrollTop = objDiv.scrollHeight;
-                        },0);
-                    }
-                    else if (chunk.room_id == $scope.room_id && chunk.type == "m.room.member") {
-                        updateMemberList(chunk);
-                    }
-                    else if (chunk.type === "m.presence") {
-                        updatePresence(chunk);
-                    }
-                }
+                parseChunk(response.data.chunk, false);
+                
                 if ($scope.stopPoll) {
                     console.log("Stopping polling.");
                 }
@@ -70,7 +104,7 @@ angular.module('RoomController', [])
                     $timeout(shortPoll, 0);
                 }
             }, function(response) {
-                $scope.feedback = "Can't stream: " + response.data;
+                $scope.state.stream_failure = response;
 
                 if (response.status == 403) {
                     $scope.stopPoll = true;
@@ -99,8 +133,8 @@ angular.module('RoomController', [])
                     function(response) {
                         var member = $scope.members[chunk.target_user_id];
                         if (member !== undefined) {
-                            console.log("Updated displayname "+chunk.target_user_id+" to " + response.displayname);
-                            member.displayname = response.displayname;
+                            console.log("Updated displayname "+chunk.target_user_id+" to " + response.data.displayname);
+                            member.displayname = response.data.displayname;
                         }
                     }
                 ); 
@@ -108,8 +142,8 @@ angular.module('RoomController', [])
                     function(response) {
                          var member = $scope.members[chunk.target_user_id];
                          if (member !== undefined) {
-                            console.log("Updated image for "+chunk.target_user_id+" to " + response.avatar_url);
-                            member.avatar_url = response.avatar_url;
+                            console.log("Updated image for "+chunk.target_user_id+" to " + response.data.avatar_url);
+                            member.avatar_url = response.data.avatar_url;
                          }
                     }
                 );
@@ -171,8 +205,8 @@ angular.module('RoomController', [])
                 console.log("Sent message");
                 $scope.textInput = "";
             },
-            function(reason) {
-                $scope.feedback = "Failed to send: " + reason;
+            function(error) {
+                $scope.feedback = "Failed to send: " + error.data.error;
             });               
     };
 
@@ -183,22 +217,24 @@ angular.module('RoomController', [])
         // Join the room
         matrixService.join($scope.room_id).then(
             function() {
-                console.log("Joined room");
+                console.log("Joined room "+$scope.room_id);
                 // Now start reading from the stream
                 $timeout(shortPoll, 0);
 
                 // Get the current member list
                 matrixService.getMemberList($scope.room_id).then(
                     function(response) {
-                        for (var i = 0; i < response.chunk.length; i++) {
-                            var chunk = response.chunk[i];
+                        for (var i = 0; i < response.data.chunk.length; i++) {
+                            var chunk = response.data.chunk[i];
                             updateMemberList(chunk);
                         }
                     },
-                    function(reason) {
-                        $scope.feedback = "Failed get member list: " + reason;
+                    function(error) {
+                        $scope.feedback = "Failed get member list: " + error.data.error;
                     }
                 );
+                
+                paginate(MESSAGES_PER_PAGINATION);
             },
             function(reason) {
                 $scope.feedback = "Can't join room: " + reason;
@@ -224,8 +260,8 @@ angular.module('RoomController', [])
                 console.log("Left room ");
                 $location.path("rooms");
             },
-            function(reason) {
-                $scope.feedback = "Failed to leave room: " + reason;
+            function(error) {
+                $scope.feedback = "Failed to leave room: " + error.data.error;
             });
     };
 
@@ -234,10 +270,14 @@ angular.module('RoomController', [])
             function() {
                 console.log("Image sent");
             },
-            function(reason) {
-                $scope.feedback = "Failed to send image: " + reason;
+            function(error) {
+                $scope.feedback = "Failed to send image: " + error.data.error;
             });
     };
+    
+    $scope.loadMoreHistory = function() {
+        paginate(MESSAGES_PER_PAGINATION);
+    };
 
     $scope.$on('$destroy', function(e) {
         console.log("onDestroyed: Stopping poll.");
diff --git a/webclient/room/room.html b/webclient/room/room.html
index 8fc7d5d360..3b9ba713de 100644
--- a/webclient/room/room.html
+++ b/webclient/room/room.html
@@ -35,7 +35,7 @@
                     <div class="bubble">
                         {{ msg.content.msgtype === "m.emote" ? ("* " + (members[msg.user_id].displayname || msg.user_id) + " " + msg.content.body) : "" }}
                         {{ msg.content.msgtype === "m.text" ? msg.content.body : "" }}
-                        <img class="image" ng-hide='msg.content.msgtype !== "m.image"' src="{{ msg.content.url }}" alt="{{ msg.content.body }}"/>
+                        <img class="image" ng-hide='msg.content.msgtype !== "m.image"' ng-src="{{ msg.content.url }}" alt="{{ msg.content.body }}"/>
                     </div>
                 </td>
                 <td class="rightBlock">
@@ -86,6 +86,10 @@
                     <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 ng-hide="!state.stream_failure">
+                {{ state.stream_failure.data.error || "Connection failure" }}
+            </div>
         </div>
     </div>
 
diff --git a/webclient/rooms/rooms-controller.js b/webclient/rooms/rooms-controller.js
index 293ea8bc8b..2ce14e1d49 100644
--- a/webclient/rooms/rooms-controller.js
+++ b/webclient/rooms/rooms-controller.js
@@ -16,9 +16,9 @@ limitations under the License.
 
 'use strict';
 
-angular.module('RoomsController', ['matrixService'])
-.controller('RoomsController', ['$scope', '$location', 'matrixService',
-                               function($scope, $location, matrixService) {
+angular.module('RoomsController', ['matrixService', 'mFileInput', 'mFileUpload'])
+.controller('RoomsController', ['$scope', '$location', 'matrixService', 'mFileUpload',
+                               function($scope, $location, matrixService, mFileUpload) {
                                    
     $scope.rooms = [];
     $scope.public_rooms = [];
@@ -40,7 +40,8 @@ angular.module('RoomsController', ['matrixService'])
 
     $scope.newProfileInfo = {
         name: matrixService.config().displayName,
-        avatar: matrixService.config().avatarUrl
+        avatar: matrixService.config().avatarUrl,
+        avatarFile: undefined
     };
 
     $scope.linkedEmails = {
@@ -74,18 +75,18 @@ angular.module('RoomsController', ['matrixService'])
         // List all rooms joined or been invited to
         $scope.rooms = matrixService.rooms();
         matrixService.rooms().then(
-            function(data) {
-                data = assignRoomAliases(data);
+            function(response) {
+                var data = assignRoomAliases(response.data);
                 $scope.feedback = "Success";
                 $scope.rooms = data;
             },
-            function(reason) {
-                $scope.feedback = "Failure: " + reason;
+            function(error) {
+                $scope.feedback = "Failure: " + error.data;
             });
         
         matrixService.publicRooms().then(
-            function(data) {
-                $scope.public_rooms = assignRoomAliases(data.chunk);
+            function(response) {
+                $scope.public_rooms = assignRoomAliases(response.data.chunk);
             }
         );
     };
@@ -100,14 +101,14 @@ angular.module('RoomsController', ['matrixService'])
         matrixService.create(room_id, visibility).then(
             function(response) { 
                 // This room has been created. Refresh the rooms list
-                console.log("Created room " + response.room_alias + " with id: "+
-                response.room_id);
+                console.log("Created room " + response.data.room_alias + " with id: "+
+                response.data.room_id);
                 matrixService.createRoomIdToAliasMapping(
-                    response.room_id, response.room_alias);
+                    response.data.room_id, response.data.room_alias);
                 $scope.refresh();
             },
-            function(reason) {
-                $scope.feedback = "Failure: " + reason;
+            function(error) {
+                $scope.feedback = "Failure: " + error.data;
             });
     };
     
@@ -117,17 +118,17 @@ angular.module('RoomsController', ['matrixService'])
         //$location.path("room/" + room_id);
         matrixService.join(room_id).then(
             function(response) {
-                if (response.hasOwnProperty("room_id")) {
-                    if (response.room_id != room_id) {
-                        $location.path("room/" + response.room_id);
+                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(reason) {
-                $scope.feedback = "Can't join room: " + reason;
+            function(error) {
+                $scope.feedback = "Can't join room: " + error.data;
             }
         );
     };
@@ -135,15 +136,15 @@ angular.module('RoomsController', ['matrixService'])
     $scope.joinAlias = function(room_alias) {
         matrixService.joinAlias(room_alias).then(
             function(response) {
-                if (response.hasOwnProperty("room_id")) {
-                    $location.path("room/" + response.room_id);
+                if (response.data.hasOwnProperty("room_id")) {
+                    $location.path("room/" + response.data.room_id);
                     return;
                 } else {
                     // TODO (erikj): Do something here?
                 }
             },
-            function(reason) {
-                $scope.feedback = "Can't join room: " + reason;
+            function(error) {
+                $scope.feedback = "Can't join room: " + error.data;
             }
         );
     };
@@ -157,12 +158,28 @@ angular.module('RoomsController', ['matrixService'])
                 matrixService.setConfig(config);
                 matrixService.saveConfig();
             },
-            function(reason) {
-                $scope.feedback = "Can't update display name: " + reason;
+            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(
@@ -174,8 +191,8 @@ angular.module('RoomsController', ['matrixService'])
                 matrixService.setConfig(config);
                 matrixService.saveConfig();
             },
-            function(reason) {
-                $scope.feedback = "Can't update avatar: " + reason;
+            function(error) {
+                $scope.feedback = "Can't update avatar: " + error.data;
             }
         );
     };
@@ -183,8 +200,8 @@ angular.module('RoomsController', ['matrixService'])
     $scope.linkEmail = function(email) {
         matrixService.linkEmail(email).then(
             function(response) {
-                if (response.success === true) {
-                    $scope.linkedEmails.authTokenId = response.tokenId;
+                if (response.data.success === true) {
+                    $scope.linkedEmails.authTokenId = response.data.tokenId;
                     $scope.emailFeedback = "You have been sent an email.";
                     $scope.linkedEmails.emailBeingAuthed = email;
                 }
@@ -192,8 +209,8 @@ angular.module('RoomsController', ['matrixService'])
                     $scope.emailFeedback = "Failed to send email.";
                 }
             },
-            function(reason) {
-                $scope.emailFeedback = "Can't send email: " + reason;
+            function(error) {
+                $scope.emailFeedback = "Can't send email: " + error.data;
             }
         );
     };
@@ -206,7 +223,7 @@ angular.module('RoomsController', ['matrixService'])
         }
         matrixService.authEmail(matrixService.config().user_id, tokenId, code).then(
             function(response) {
-                if ("success" in response && response.success === false) {
+                if ("success" in response.data && response.data.success === false) {
                     $scope.emailFeedback = "Failed to authenticate email.";
                     return;
                 }
diff --git a/webclient/rooms/rooms.html b/webclient/rooms/rooms.html
index d303e143b9..5974bd940c 100644
--- a/webclient/rooms/rooms.html
+++ b/webclient/rooms/rooms.html
@@ -5,16 +5,33 @@
             
     <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>    
+            <table>
+                <tr>
+                    <td>
+                        <div class="profile-avatar">
+                            <img  ng-src="{{ newProfileInfo.avatar || 'img/default-profile.jpg' }}" m-file-input="newProfileInfo.avatarFile"/>
+                        </div>
+                    </td>
+                    <td>
+                        <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)" />
+                            <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.avatar" ng-enter="setAvatar(newProfileInfo.avatar)" />
-            <button ng-disabled="!newProfileInfo.avatar" ng-click="setAvatar(newProfileInfo.avatar)">Update Avatar</button>    
+            <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>