summary refs log tree commit diff
path: root/webclient
diff options
context:
space:
mode:
authorErik Johnston <erik@matrix.org>2014-09-03 19:47:32 +0100
committerErik Johnston <erik@matrix.org>2014-09-03 19:47:32 +0100
commit0538a4098d797eaa901c168e3a76df2ac94da2ed (patch)
treebc1ef822fdb3184daf6d39a2ab4da1afb6370dd9 /webclient
parentAdd database upgrade script (diff)
parentBump versions. Update change logs. (diff)
downloadsynapse-0538a4098d797eaa901c168e3a76df2ac94da2ed.tar.xz
Merge branch 'release-v0.2.1' v0.2.1
Diffstat (limited to 'webclient')
-rw-r--r--webclient/app-controller.js4
-rw-r--r--webclient/app-directive.js2
-rw-r--r--webclient/app-filter.js50
-rwxr-xr-xwebclient/app.css16
-rw-r--r--webclient/app.js2
-rw-r--r--webclient/components/fileInput/file-input-directive.js2
-rw-r--r--webclient/components/fileUpload/file-upload-service.js2
-rw-r--r--webclient/components/matrix/event-handler-service.js28
-rw-r--r--webclient/components/matrix/event-stream-service.js2
-rw-r--r--webclient/components/matrix/matrix-call.js2
-rw-r--r--webclient/components/matrix/matrix-phone-service.js2
-rw-r--r--webclient/components/matrix/matrix-service.js75
-rw-r--r--webclient/components/matrix/presence-service.js2
-rw-r--r--webclient/components/utilities/utilities-service.js2
-rw-r--r--webclient/home/home-controller.js8
-rw-r--r--webclient/login/login-controller.js35
-rw-r--r--webclient/login/register-controller.js57
-rw-r--r--webclient/login/register.html36
-rw-r--r--webclient/recents/recents-controller.js2
-rw-r--r--webclient/recents/recents-filter.js2
-rw-r--r--webclient/recents/recents.html4
-rw-r--r--webclient/room/room-controller.js141
-rw-r--r--webclient/room/room-directive.js2
-rw-r--r--webclient/room/room.html21
-rw-r--r--webclient/settings/settings-controller.js2
-rw-r--r--webclient/settings/settings.html28
-rw-r--r--webclient/user/user-controller.js2
27 files changed, 434 insertions, 97 deletions
diff --git a/webclient/app-controller.js b/webclient/app-controller.js
index 42c45f7c31..ea48cbb011 100644
--- a/webclient/app-controller.js
+++ b/webclient/app-controller.js
@@ -1,5 +1,5 @@
 /*
-Copyright 2014 matrix.org
+Copyright 2014 OpenMarket Ltd
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -85,7 +85,7 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
         $scope.logout();
     });
     
-    $scope.updateHeader = function() {
+    $rootScope.updateHeader = function() {
         $scope.user_id = matrixService.config().user_id;
     };
 
diff --git a/webclient/app-directive.js b/webclient/app-directive.js
index eee0d3842f..75283598ab 100644
--- a/webclient/app-directive.js
+++ b/webclient/app-directive.js
@@ -1,5 +1,5 @@
 /*
- Copyright 2014 matrix.org
+ Copyright 2014 OpenMarket Ltd
  
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
diff --git a/webclient/app-filter.js b/webclient/app-filter.js
index b8d3d2a0d8..27f435674f 100644
--- a/webclient/app-filter.js
+++ b/webclient/app-filter.js
@@ -1,5 +1,5 @@
 /*
- Copyright 2014 matrix.org
+ Copyright 2014 OpenMarket Ltd
  
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
@@ -97,13 +97,55 @@ angular.module('matrixWebClient')
             // Else, build the name from its users
             var room = $rootScope.events.rooms[room_id];
             if (room) {
-                if (room.members) {
+                var room_name_event = room["m.room.name"];
+
+                if (room_name_event) {
+                    roomName = room_name_event.content.name;
+                }
+                else if (room.members) {
                     // Limit the room renaming to 1:1 room
                     if (2 === Object.keys(room.members).length) {
                         for (var i in room.members) {
                             var member = room.members[i];
-                            if (member.user_id !== matrixService.config().user_id) {
-                                roomName = member.content.displayname ?  member.content.displayname : member.user_id;
+                            if (member.state_key !== matrixService.config().user_id) {
+
+                                if (member.state_key in $rootScope.presence) {
+                                    // If the user is available in presence, use the displayname there
+                                    // as it is the most uptodate
+                                    roomName = $rootScope.presence[member.state_key].content.displayname;
+                                }
+                                else if (member.content.displayname) {
+                                    roomName = member.content.displayname;
+                                }
+                                else {
+                                    roomName = member.state_key;
+                                }
+                            }
+                        }
+                    }
+                    else if (1 === Object.keys(room.members).length) {
+                        // The other member may be in the invite list, get all invited users
+                        var invitedUserIDs = [];
+                        for (var i in room.messages) {
+                            var message = room.messages[i];
+                            if ("m.room.member" === message.type && "invite" === message.membership) {
+                                // Make sure there is no duplicate user
+                                if (-1 === invitedUserIDs.indexOf(message.state_key)) {
+                                    invitedUserIDs.push(message.state_key);
+                                }
+                            } 
+                        }
+                        
+                        // For now, only 1:1 room needs to be renamed. It means only 1 invited user
+                        if (1 === invitedUserIDs.length) {
+                            var userID = invitedUserIDs[0];
+
+                            // Try to resolve his displayname in presence global data
+                            if (userID in $rootScope.presence) {
+                                roomName = $rootScope.presence[userID].content.displayname;
+                            }
+                            else {
+                                roomName = userID;
                             }
                         }
                     }
diff --git a/webclient/app.css b/webclient/app.css
index c27ec797a4..425d5bb11a 100755
--- a/webclient/app.css
+++ b/webclient/app.css
@@ -270,9 +270,9 @@ a:active  { color: #000; }
 
 .userAvatar .userPowerLevel {
     position: absolute;
-    bottom: 20px;
-    height: 1px;
-    background-color: red;
+    bottom: 0px;
+    height: 2px;
+    background-color: #f00;
 }
 
 .userPresence {
@@ -525,3 +525,13 @@ a:active  { color: #000; }
     font-size: 24px;
 }
 
+#user-displayname-input {
+    width: 160px;
+    max-width: 155px;
+}
+
+#user-save-button {
+    width: 160px;
+    font-size: 14px;
+}
+
diff --git a/webclient/app.js b/webclient/app.js
index dac4f048cd..d25e2a6234 100644
--- a/webclient/app.js
+++ b/webclient/app.js
@@ -1,5 +1,5 @@
 /*
-Copyright 2014 matrix.org
+Copyright 2014 OpenMarket Ltd
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
diff --git a/webclient/components/fileInput/file-input-directive.js b/webclient/components/fileInput/file-input-directive.js
index c5e4ae07a8..14e2f772f7 100644
--- a/webclient/components/fileInput/file-input-directive.js
+++ b/webclient/components/fileInput/file-input-directive.js
@@ -1,5 +1,5 @@
 /*
- Copyright 2014 matrix.org
+ Copyright 2014 OpenMarket Ltd
  
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
diff --git a/webclient/components/fileUpload/file-upload-service.js b/webclient/components/fileUpload/file-upload-service.js
index 699a3cbffc..e0f67b2c6c 100644
--- a/webclient/components/fileUpload/file-upload-service.js
+++ b/webclient/components/fileUpload/file-upload-service.js
@@ -1,5 +1,5 @@
 /*
- Copyright 2014 matrix.org
+ Copyright 2014 OpenMarket Ltd
  
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
diff --git a/webclient/components/matrix/event-handler-service.js b/webclient/components/matrix/event-handler-service.js
index d6a0600132..ee478d2eb0 100644
--- a/webclient/components/matrix/event-handler-service.js
+++ b/webclient/components/matrix/event-handler-service.js
@@ -1,5 +1,5 @@
 /*
-Copyright 2014 matrix.org
+Copyright 2014 OpenMarket Ltd
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -32,7 +32,9 @@ angular.module('eventHandlerService', [])
     var MSG_EVENT = "MSG_EVENT";
     var MEMBER_EVENT = "MEMBER_EVENT";
     var PRESENCE_EVENT = "PRESENCE_EVENT";
+    var POWERLEVEL_EVENT = "POWERLEVEL_EVENT";
     var CALL_EVENT = "CALL_EVENT";
+    var NAME_EVENT = "NAME_EVENT";
 
     var InitialSyncDeferred = $q.defer();
     
@@ -95,7 +97,7 @@ angular.module('eventHandlerService', [])
             }
         }
         
-        $rootScope.events.rooms[event.room_id].members[event.user_id] = event;
+        $rootScope.events.rooms[event.room_id].members[event.state_key] = event;
         $rootScope.$broadcast(MEMBER_EVENT, event, isLiveEvent);
     };
     
@@ -107,10 +109,20 @@ angular.module('eventHandlerService', [])
     var handlePowerLevels = function(event, isLiveEvent) {
         initRoom(event.room_id);
 
-       $rootScope.events.rooms[event.room_id][event.type] = event;
+        // Keep the latest data. Do not care of events that come when paginating back
+        if (!$rootScope.events.rooms[event.room_id][event.type] || isLiveEvent) {
+            $rootScope.events.rooms[event.room_id][event.type] = event;
+            $rootScope.$broadcast(POWERLEVEL_EVENT, event, isLiveEvent);   
+        }
+    };
 
-        //TODO
-        //$rootScope.$broadcast(PRESENCE_EVENT, event, isLiveEvent);
+    var handleRoomName = function(event, isLiveEvent) {
+        console.log("handleRoomName " + isLiveEvent);
+
+        initRoom(event.room_id);
+
+        $rootScope.events.rooms[event.room_id][event.type] = event;
+        $rootScope.$broadcast(NAME_EVENT, event, isLiveEvent);
     };
 
     var handleCallEvent = function(event, isLiveEvent) {
@@ -122,7 +134,9 @@ angular.module('eventHandlerService', [])
         MSG_EVENT: MSG_EVENT,
         MEMBER_EVENT: MEMBER_EVENT,
         PRESENCE_EVENT: PRESENCE_EVENT,
+        POWERLEVEL_EVENT: POWERLEVEL_EVENT,
         CALL_EVENT: CALL_EVENT,
+        NAME_EVENT: NAME_EVENT,
         
     
         handleEvent: function(event, isLiveEvent) {
@@ -146,7 +160,9 @@ angular.module('eventHandlerService', [])
                 case 'm.room.power_levels':
                     handlePowerLevels(event, isLiveEvent);
                     break;
-
+                case 'm.room.name':
+                    handleRoomName(event, isLiveEvent);
+                    break;
                 default:
                     console.log("Unable to handle event type " + event.type);
                     console.log(JSON.stringify(event, undefined, 4));
diff --git a/webclient/components/matrix/event-stream-service.js b/webclient/components/matrix/event-stream-service.js
index 441148670e..1c0f7712b4 100644
--- a/webclient/components/matrix/event-stream-service.js
+++ b/webclient/components/matrix/event-stream-service.js
@@ -1,5 +1,5 @@
 /*
-Copyright 2014 matrix.org
+Copyright 2014 OpenMarket Ltd
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
diff --git a/webclient/components/matrix/matrix-call.js b/webclient/components/matrix/matrix-call.js
index 47b63d7f2f..3e13e4e81f 100644
--- a/webclient/components/matrix/matrix-call.js
+++ b/webclient/components/matrix/matrix-call.js
@@ -1,5 +1,5 @@
 /*
-Copyright 2014 matrix.org
+Copyright 2014 OpenMarket Ltd
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
diff --git a/webclient/components/matrix/matrix-phone-service.js b/webclient/components/matrix/matrix-phone-service.js
index d9e2e8baa3..ca86b473e7 100644
--- a/webclient/components/matrix/matrix-phone-service.js
+++ b/webclient/components/matrix/matrix-phone-service.js
@@ -1,5 +1,5 @@
 /*
-Copyright 2014 matrix.org
+Copyright 2014 OpenMarket Ltd
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
diff --git a/webclient/components/matrix/matrix-service.js b/webclient/components/matrix/matrix-service.js
index 2ae55bea9f..7c6d4ae50f 100644
--- a/webclient/components/matrix/matrix-service.js
+++ b/webclient/components/matrix/matrix-service.js
@@ -1,5 +1,5 @@
 /*
-Copyright 2014 matrix.org
+Copyright 2014 OpenMarket Ltd
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -84,13 +84,14 @@ angular.module('matrixService', [])
         prefix: prefixPath,
 
         // Register an user
-        register: function(user_name, password) {
+        register: function(user_name, password, threepidCreds) {
             // The REST path spec
             var path = "/register";
 
             return doRequest("POST", path, undefined, {
                  user_id: user_name,
-                 password: password
+                 password: password,
+                 threepidCreds: threepidCreds
             });
         },
 
@@ -166,6 +167,29 @@ angular.module('matrixService', [])
             return doRequest("POST", path, undefined, data);
         },
 
+        // Change the membership of an another user
+        setMembership: function(room_id, user_id, membershipValue) {
+            // The REST path spec
+            var path = "/rooms/$room_id/state/m.room.member/$user_id";
+            path = path.replace("$room_id", encodeURIComponent(room_id));
+            path = path.replace("$user_id", user_id);
+
+            return doRequest("PUT", path, undefined, {
+                membership: membershipValue
+            });
+        },
+           
+        // Bans a user from from a room
+        ban: function(room_id, user_id, reason) {
+            var path = "/rooms/$room_id/ban";
+            path = path.replace("$room_id", encodeURIComponent(room_id));
+            
+            return doRequest("POST", path, undefined, {
+                user_id: user_id,
+                reason: reason
+            });
+        },
+
         // Retrieves the room ID corresponding to a room alias
         resolveRoomAlias:function(room_alias) {
             var path = "/_matrix/client/api/v1/directory/room/$room_alias";
@@ -252,7 +276,7 @@ angular.module('matrixService', [])
 
         // get a list of public rooms on your home server
         publicRooms: function() {
-            var path = "/publicRooms"
+            var path = "/publicRooms";
             return doRequest("GET", path);
         },
         
@@ -308,16 +332,16 @@ angular.module('matrixService', [])
 
         // hit the Identity Server for a 3PID request.
         linkEmail: function(email, clientSecret, sendAttempt) {
-            var path = "/_matrix/identity/api/v1/validate/email/requestToken"
+            var path = "/_matrix/identity/api/v1/validate/email/requestToken";
             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(clientSecret, tokenId, code) {
+        authEmail: function(clientSecret, sid, code) {
             var path = "/_matrix/identity/api/v1/validate/email/submitToken";
-            var data = "token="+code+"&sid="+tokenId+"&clientSecret="+clientSecret;
+            var data = "token="+code+"&sid="+sid+"&clientSecret="+clientSecret;
             var headers = {};
             headers["Content-Type"] = "application/x-www-form-urlencoded";
             return doBaseRequest(config.identityServer, "POST", path, {}, data, headers);
@@ -330,6 +354,11 @@ angular.module('matrixService', [])
             headers["Content-Type"] = "application/x-www-form-urlencoded";
             return doBaseRequest(config.identityServer, "POST", path, {}, data, headers); 
         },
+
+        lookup3pid: function(medium, address) {
+            var path = "/_matrix/identity/api/v1/lookup?medium="+encodeURIComponent(medium)+"&address="+encodeURIComponent(address);
+            return doBaseRequest(config.identityServer, "GET", path, {}, undefined, {}); 
+        },
         
         uploadContent: function(file) {
             var path = "/_matrix/content";
@@ -408,7 +437,8 @@ angular.module('matrixService', [])
                 state: presence
             });
         },
-
+        
+        
         /****** Permanent storage of user information ******/
         
         // Returns the current config
@@ -508,6 +538,35 @@ angular.module('matrixService', [])
                 }
             }
             return powerLevel;
+        },
+            
+        /**
+         * Change or reset the power level of a user
+         * @param {String} room_id the room id
+         * @param {String} user_id the user id
+         * @param {Number} powerLevel a value between 0 and 10
+         *    If undefined, the user power level will be reset, ie he will use the default room user power level
+         * @returns {promise} an $http promise
+         */
+        setUserPowerLevel: function(room_id, user_id, powerLevel) {
+            
+            // Hack: currently, there is no home server API so do it by hand by updating
+            // the current m.room.power_levels of the room and send it to the server
+            var room = $rootScope.events.rooms[room_id];
+            if (room && room["m.room.power_levels"]) {
+                var content = angular.copy(room["m.room.power_levels"].content);
+                content[user_id] = powerLevel;
+                
+                var path = "/rooms/$room_id/state/m.room.power_levels";
+                path = path.replace("$room_id", encodeURIComponent(room_id));
+                
+                return doRequest("PUT", path, undefined, content);
+            }
+            
+            // The room does not exist or does not contain power_levels data
+            var deferred = $q.defer();
+            deferred.reject({data:{error: "Invalid room: " + room_id}});
+            return deferred.promise;
         }
 
     };
diff --git a/webclient/components/matrix/presence-service.js b/webclient/components/matrix/presence-service.js
index 555118133b..952c8ec8a9 100644
--- a/webclient/components/matrix/presence-service.js
+++ b/webclient/components/matrix/presence-service.js
@@ -1,5 +1,5 @@
 /*
- Copyright 2014 matrix.org
+ Copyright 2014 OpenMarket Ltd
  
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
diff --git a/webclient/components/utilities/utilities-service.js b/webclient/components/utilities/utilities-service.js
index 3df2f04458..b417cc5b39 100644
--- a/webclient/components/utilities/utilities-service.js
+++ b/webclient/components/utilities/utilities-service.js
@@ -1,5 +1,5 @@
 /*
- Copyright 2014 matrix.org
+ Copyright 2014 OpenMarket Ltd
  
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
diff --git a/webclient/home/home-controller.js b/webclient/home/home-controller.js
index f4ce3053ea..85e8990c29 100644
--- a/webclient/home/home-controller.js
+++ b/webclient/home/home-controller.js
@@ -1,5 +1,5 @@
 /*
-Copyright 2014 matrix.org
+Copyright 2014 OpenMarket Ltd
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -74,7 +74,7 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen
                     response.data.room_id, response.data.room_alias);
             },
             function(error) {
-                $scope.feedback = "Failure: " + error.data;
+                $scope.feedback = "Failure: " + JSON.stringify(error.data);
             });
     };
     
@@ -94,7 +94,7 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen
                 $location.url("room/" + room_id);
             },
             function(error) {
-                $scope.feedback = "Can't join room: " + error.data;
+                $scope.feedback = "Can't join room: " + JSON.stringify(error.data);
             }
         );
     };
@@ -106,7 +106,7 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen
                 $location.url("room/" + room_alias);
             },
             function(error) {
-                $scope.feedback = "Can't join room: " + error.data;
+                $scope.feedback = "Can't join room: " + JSON.stringify(error.data);
             }
         );
     };
diff --git a/webclient/login/login-controller.js b/webclient/login/login-controller.js
index 7369a28ef0..5ef39a7122 100644
--- a/webclient/login/login-controller.js
+++ b/webclient/login/login-controller.js
@@ -1,5 +1,5 @@
 /*
- Copyright 2014 matrix.org
+ Copyright 2014 OpenMarket Ltd
  
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
@@ -15,8 +15,8 @@
  */
  
 angular.module('LoginController', ['matrixService'])
-.controller('LoginController', ['$scope', '$location', 'matrixService', 'eventStreamService',
-                                    function($scope, $location, matrixService, eventStreamService) {
+.controller('LoginController', ['$scope', '$rootScope', '$location', 'matrixService', 'eventStreamService',
+                                    function($scope, $rootScope, $location, matrixService, eventStreamService) {
     'use strict';
     
     
@@ -51,10 +51,36 @@ angular.module('LoginController', ['matrixService'])
         matrixService.setConfig({
             homeserver: $scope.account.homeserver,
             identityServer: $scope.account.identityServer,
+        });
+        switch ($scope.login_type) {
+            case 'mxid':
+                $scope.login_with_mxid($scope.account.user_id, $scope.account.password);
+                break;
+            case 'email':
+                matrixService.lookup3pid('email', $scope.account.user_id).then(
+                    function(response) {
+                        if (response.data['address'] == undefined) {
+                            $scope.login_error_msg = "Invalid email address / password";
+                        } else {
+                            console.log("Got address "+response.data['mxid']+" for email "+$scope.account.user_id);
+                            $scope.login_with_mxid(response.data['mxid'], $scope.account.password);
+                        }
+                    },
+                    function() {
+                        $scope.login_error_msg = "Couldn't look up email address. Is your identity server set correctly?";
+                    }
+                );
+        }
+    };
+
+    $scope.login_with_mxid = function(mxid, password) {
+        matrixService.setConfig({
+            homeserver: $scope.account.homeserver,
+            identityServer: $scope.account.identityServer,
             user_id: $scope.account.user_id
         });
         // try to login
-        matrixService.login($scope.account.user_id, $scope.account.password).then(
+        matrixService.login(mxid, password).then(
             function(response) {
                 if ("access_token" in response.data) {
                     $scope.feedback = "Login successful.";
@@ -65,6 +91,7 @@ angular.module('LoginController', ['matrixService'])
                         access_token: response.data.access_token
                     });
                     matrixService.saveConfig();
+                    $rootScope.updateHeader();
                     eventStreamService.resume();
                     $location.url("home");
                 }
diff --git a/webclient/login/register-controller.js b/webclient/login/register-controller.js
index 0ece57502b..b7584a7d33 100644
--- a/webclient/login/register-controller.js
+++ b/webclient/login/register-controller.js
@@ -1,5 +1,5 @@
 /*
- Copyright 2014 matrix.org
+ Copyright 2014 OpenMarket Ltd
  
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
@@ -15,8 +15,8 @@
  */
  
 angular.module('RegisterController', ['matrixService'])
-.controller('RegisterController', ['$scope', '$location', 'matrixService', 'eventStreamService',
-                                    function($scope, $location, matrixService, eventStreamService) {
+.controller('RegisterController', ['$scope', '$rootScope', '$location', 'matrixService', 'eventStreamService',
+                                    function($scope, $rootScope, $location, matrixService, eventStreamService) {
     'use strict';
     
     // FIXME: factor out duplication with login-controller.js
@@ -30,6 +30,17 @@ angular.module('RegisterController', ['matrixService'])
     {
         hs_url += ":" + $location.port();
     }
+
+    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.account = {
         homeserver: hs_url,
@@ -43,7 +54,6 @@ angular.module('RegisterController', ['matrixService'])
     };
     
     $scope.register = function() {
-
         // Set the urls
         matrixService.setConfig({
             homeserver: $scope.account.homeserver,
@@ -59,7 +69,25 @@ angular.module('RegisterController', ['matrixService'])
             return;
         }
 
-        matrixService.register($scope.account.desired_user_id, $scope.account.pwd1).then(
+        if ($scope.account.email) {
+            $scope.clientSecret = generateClientSecret();
+            matrixService.linkEmail($scope.account.email, $scope.clientSecret, 1).then(
+                function(response) {
+                    $scope.wait_3pid_code = true;
+                    $scope.sid = response.data.sid;
+                    $scope.feedback = "";
+                },
+                function(response) {
+                    $scope.feedback = "Couldn't request verification email!";
+                }
+            );
+        } else {
+            registerWithMxidAndPassword($scope.account.desired_user_id, $scope.account.pwd1);
+        }
+    };
+
+    $scope.registerWithMxidAndPassword = function(mxid, password, threepidCreds) {
+        matrixService.register(mxid, password, threepidCreds).then(
             function(response) {
                 $scope.feedback = "Success";
                 // Update the current config 
@@ -74,7 +102,7 @@ angular.module('RegisterController', ['matrixService'])
                 matrixService.saveConfig();
                 
                 // Update the global scoped used_id var (used in the app header)
-                $scope.updateHeader();
+                $rootScope.updateHeader();
                 
                 eventStreamService.resume();
                 
@@ -87,15 +115,32 @@ angular.module('RegisterController', ['matrixService'])
                 $location.url("home");
             },
             function(error) {
+                console.trace("Registration error: "+error);
                 if (error.data) {
                     if (error.data.errcode === "M_USER_IN_USE") {
                         $scope.feedback = "Username already taken.";
+                        $scope.reenter_username = true;
                     }
                 }
                 else if (error.status === 0) {
                     $scope.feedback = "Unable to talk to the server.";
                 }
             });
+    }
+
+    $scope.verifyToken = function() {
+        matrixService.authEmail($scope.clientSecret, $scope.sid, $scope.account.threepidtoken).then(
+            function(response) {
+                if (!response.data.success) {
+                    $scope.feedback = "Unable to verify code.";
+                } else {
+                    $scope.registerWithMxidAndPassword($scope.account.desired_user_id, $scope.account.pwd1, [{'sid':$scope.sid, 'clientSecret':$scope.clientSecret, 'idServer': $scope.account.identityServer.split('//')[1]}]);
+                }
+            },
+            function(error) {
+                $scope.feedback = "Unable to verify code.";
+            }
+        );
     };
 
 }]);
diff --git a/webclient/login/register.html b/webclient/login/register.html
index 81995f1ae0..06a6526b70 100644
--- a/webclient/login/register.html
+++ b/webclient/login/register.html
@@ -12,26 +12,34 @@
                 
                 <div style="text-align: center">
                     <br/>
-                    <input id="email" size="32" type="text" ng-focus="true" ng-model="account.email" placeholder="Email address (optional)" style="display: none"/>
-                    <div class="smallPrint" style="display: none;">Specifying an email address lets other users find you on Matrix more easily,<br/>
-                        and gives you a way to reset your password</div>
-                    <input id="desired_user_id" size="32" type="text" ng-model="account.desired_user_id" placeholder="Matrix ID (e.g. bob)"/>
-                    <br/>
-                    <input id="pwd1" size="32" type="password" ng-model="account.pwd1" placeholder="Type a password"/>
-                    <br/>
-                    <input id="pwd2" size="32" type="password" ng-model="account.pwd2" placeholder="Confirm your password"/>
-                    <br/>
-                    <input id="displayName" size="32" type="text" ng-model="account.displayName" placeholder="Display name (e.g. Bob Obson)"/>
-                    <br/>
-                    <br/>
+
+                    <input ng-show="!wait_3pid_code" id="email" size="32" type="text" ng-focus="true" ng-model="account.email" placeholder="Email address (optional)"/>
+                    <div ng-show="!wait_3pid_code" class="smallPrint">Specifying an email address lets other users find you on Matrix more easily,<br/>
+                        and will give you a way to reset your password in the future</div>
+                    <span ng-show="reenter_username">Choose another username:</span>
+                    <input ng-show="!wait_3pid_code || reenter_username" id="desired_user_id" size="32" type="text" ng-model="account.desired_user_id" placeholder="Matrix ID (e.g. bob)"/>
+                    <br ng-show="!wait_3pid_code" />
+                    <input ng-show="!wait_3pid_code" id="pwd1" size="32" type="password" ng-model="account.pwd1" placeholder="Type a password"/>
+                    <br ng-show="!wait_3pid_code" />
+                    <input ng-show="!wait_3pid_code" id="pwd2" size="32" type="password" ng-model="account.pwd2" placeholder="Confirm your password"/>
+                    <br ng-show="!wait_3pid_code" />
+                    <input ng-show="!wait_3pid_code" id="displayName" size="32" type="text" ng-model="account.displayName" placeholder="Display name (e.g. Bob Obson)"/>
+                    <br ng-show="!wait_3pid_code" />
+                    <br ng-show="!wait_3pid_code" />
                     
-                    <button ng-click="register()" ng-disabled="!account.desired_user_id || !account.homeserver || !account.pwd1 || !account.pwd2 || account.pwd1 !== account.pwd2">Sign up</button>
+                    <button ng-show="!wait_3pid_code" ng-click="register()" ng-disabled="!account.desired_user_id || !account.homeserver || !account.pwd1 || !account.pwd2 || account.pwd1 !== account.pwd2">Sign up</button>
+
+                    <div ng-show="wait_3pid_code">
+                    <span>Please enter the verification code sent to {{ account.email }}</span><br />
+                    <input id="threepidtoken" size="32" type="text" ng-focus="true" ng-model="account.threepidtoken" placeholder="Verification Code"/><br />
+                    <button ng-click="verifyToken()" ng-disabled="!account.threepidtoken">Validate</button>
+                    </div>
                     <br/><br/>
                 </div>
 
                 <div class="feedback">{{ feedback }} {{ login_error_msg }}</div>
                 
-                <div id="serverConfig">
+                <div id="serverConfig" ng-show="!wait_3pid_code">
                     <label for="homeserver">Home Server:</label> 
                     <input id="homeserver" size="32" type="text" ng-model="account.homeserver" placeholder="URL (e.g. http://matrix.org:8080)"/>
                     <div class="smallPrint">Your home server stores all your conversation and account data.</div>
diff --git a/webclient/recents/recents-controller.js b/webclient/recents/recents-controller.js
index d7d3bf4053..3209f2cbdf 100644
--- a/webclient/recents/recents-controller.js
+++ b/webclient/recents/recents-controller.js
@@ -1,5 +1,5 @@
 /*
- Copyright 2014 matrix.org
+ Copyright 2014 OpenMarket Ltd
  
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
diff --git a/webclient/recents/recents-filter.js b/webclient/recents/recents-filter.js
index 45653fca96..d80de6fbeb 100644
--- a/webclient/recents/recents-filter.js
+++ b/webclient/recents/recents-filter.js
@@ -1,5 +1,5 @@
 /*
- Copyright 2014 matrix.org
+ Copyright 2014 OpenMarket Ltd
  
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
diff --git a/webclient/recents/recents.html b/webclient/recents/recents.html
index db3b0fb32f..9978e08b13 100644
--- a/webclient/recents/recents.html
+++ b/webclient/recents/recents.html
@@ -23,8 +23,8 @@
                     <div ng-hide="room.membership === 'invite'" ng-switch="room.lastMsg.type" >
                          <div ng-switch-when="m.room.member">
                             {{ room.lastMsg.user_id }}
-                            {{ {"join": "joined", "leave": "left", "invite": "invited"}[room.lastMsg.content.membership] }}
-                            {{ room.lastMsg.content.membership === "invite" ? (room.lastMsg.state_key || '') : '' }}
+                            {{ {"join": "joined", "leave": "left", "invite": "invited", "ban": "banned"}[msg.content.membership] }}
+                            {{ (msg.content.membership === "invite" || msg.content.membership === "ban") ? (msg.state_key || '') : '' }}
                         </div>
 
                         <div ng-switch-when="m.room.message">
diff --git a/webclient/room/room-controller.js b/webclient/room/room-controller.js
index 1f90472c67..c3f72c9d25 100644
--- a/webclient/room/room-controller.js
+++ b/webclient/room/room-controller.js
@@ -1,5 +1,5 @@
 /*
-Copyright 2014 matrix.org
+Copyright 2014 OpenMarket Ltd
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -85,6 +85,14 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
             updatePresence(event);
         }
     });
+    
+    $scope.$on(eventHandlerService.POWERLEVEL_EVENT, function(ngEvent, event, isLive) {
+        if (isLive && event.room_id === $scope.room_id) {
+            for (var user_id in event.content) {
+                updateUserPowerLevel(user_id);
+            }
+        }
+    });
 
     $scope.memberCount = function() {
         return Object.keys($scope.members).length;
@@ -161,10 +169,13 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
     var updateMemberList = function(chunk) {
         if (chunk.room_id != $scope.room_id) return;
 
+        // Ignore banned and kicked (leave) people
+        if ("ban" === chunk.membership || "leave" === chunk.membership) {
+            return;
+        }
+
         // set target_user_id to keep things clear
         var target_user_id = chunk.state_key;
-        
-        var now = new Date().getTime();
 
         var isNewMember = !(target_user_id in $scope.members);
         if (isNewMember) {
@@ -174,6 +185,8 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
             }
             if ("last_active_ago" in chunk.content) {
                 chunk.last_active_ago = chunk.content.last_active_ago;
+                $scope.now = new Date().getTime();
+                chunk.last_updated = $scope.now;
             }
             if ("displayname" in chunk.content) {
                 chunk.displayname = chunk.content.displayname;
@@ -181,7 +194,6 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
             if ("avatar_url" in chunk.content) {
                 chunk.avatar_url = chunk.content.avatar_url;
             }
-            chunk.last_updated = now;
             $scope.members[target_user_id] = chunk;   
 
             if (target_user_id in $rootScope.presence) {
@@ -197,6 +209,8 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
             }
             if ("last_active_ago" in chunk.content) {
                 member.last_active_ago = chunk.content.last_active_ago;
+                $scope.now = new Date().getTime();
+                member.last_updated = $scope.now;
             }
         }
     };
@@ -221,6 +235,8 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
 
         if ("last_active_ago" in chunk.content) {
             member.last_active_ago = chunk.content.last_active_ago;
+            $scope.now = new Date().getTime();
+            member.last_updated = $scope.now;
         }
 
         // this may also contain a new display name or avatar url, so check.
@@ -237,6 +253,29 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
         var member = $scope.members[user_id];
         if (member) {
             member.powerLevel = matrixService.getUserPowerLevel($scope.room_id, user_id);
+            
+            normaliseMembersPowerLevels();
+        }
+    }
+
+    // Normalise users power levels so that the user with the higher power level
+    // will have a bar covering 100% of the width of his avatar
+    var normaliseMembersPowerLevels = function() {
+        // Find the max power level
+        var maxPowerLevel = 0;
+        for (var i in $scope.members) {
+            var member = $scope.members[i];
+            if (member.powerLevel) {
+                maxPowerLevel = Math.max(maxPowerLevel, member.powerLevel);
+            }
+        }
+
+        // Normalized them on a 0..100% scale to be use in css width
+        if (maxPowerLevel) {
+            for (var i in $scope.members) {
+                var member = $scope.members[i];
+                member.powerLevelNorm = (member.powerLevel * 100) / maxPowerLevel;
+            }
         }
     }
 
@@ -247,28 +286,93 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
 
         $scope.state.sending = true;
         
-        // Send the text message
         var promise;
-        // FIXME: handle other commands too
-        if ($scope.textInput.indexOf("/me") === 0) {
-            promise = matrixService.sendEmoteMessage($scope.room_id, $scope.textInput.substr(4));
-        }
-        else if ($scope.textInput.indexOf("/nick ") === 0) {
-            // Change user display name
-            promise = matrixService.setDisplayName($scope.textInput.substr(6));
+        
+        // Check for IRC style commands first
+        if ($scope.textInput.indexOf("/") === 0) {
+            var args = $scope.textInput.split(' ');
+            var cmd = args[0];
+            
+            switch (cmd) {
+                case "/me":
+                    var emoteMsg = args.slice(1).join(' ');
+                    promise = matrixService.sendEmoteMessage($scope.room_id, emoteMsg);
+                    break;
+                    
+                case "/nick":
+                    // Change user display name
+                    if (2 === args.length) {
+                        promise = matrixService.setDisplayName(args[1]);
+                    }
+                    break;
+                    
+                case "/kick":
+                    // Kick a user from the room
+                    if (2 === args.length) {
+                        var user_id = args[1];
+
+                        // Set his state in the room as leave
+                        promise = matrixService.setMembership($scope.room_id, user_id, "leave");
+                    }
+                    break;
+                    
+                case "/ban":
+                    // Ban a user from the room
+                    if (2 <= args.length) {
+                        // TODO: The user may have entered the display name
+                        // Need display name -> user_id resolution. Pb: how to manage user with same display names?
+                        var user_id = args[1];
+
+                        // Does the user provide a reason?
+                        if (3 <= args.length) {
+                            var reason = args.slice(2).join(' ');
+                        }
+                        promise = matrixService.ban($scope.room_id, user_id, reason);
+                    }
+                    break;
+                    
+                case "/unban":
+                    // Unban a user from the room
+                    if (2 === args.length) {
+                        var user_id = args[1];
+
+                        // Reset the user membership to leave to unban him
+                        promise = matrixService.setMembership($scope.room_id, user_id, "leave");
+                    }
+                    break;
+                    
+                case "/op":
+                    // Define the power level of a user
+                    if (3 === args.length) {
+                        var user_id = args[1];
+                        var powerLevel = parseInt(args[2]);
+                        promise = matrixService.setUserPowerLevel($scope.room_id, user_id, powerLevel);
+                    }
+                    break;
+                    
+                case "/deop":
+                    // Reset the power level of a user
+                    if (2 === args.length) {
+                        var user_id = args[1];
+                        promise = matrixService.setUserPowerLevel($scope.room_id, user_id, undefined);
+                    }
+                    break;
+            }
         }
-        else {
+        
+        if (!promise) {
+            // Send the text message
             promise = matrixService.sendTextMessage($scope.room_id, $scope.textInput);
         }
         
         promise.then(
             function() {
-                console.log("Sent message");
+                console.log("Request successfully sent");
                 $scope.textInput = "";
                 $scope.state.sending = false;
             },
             function(error) {
-                $scope.feedback = "Failed to send: " + error.data.error;
+                $scope.feedback = "Request failed: " + error.data.error;
                 $scope.state.sending = false;
             });
     };
@@ -332,10 +436,6 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
         eventHandlerService.waitForInitialSyncCompletion().then(
             function() {
                 
-                // Some data has been retrieved from the iniialSync request
-                // So, the relative time starts here
-                $scope.now = new Date().getTime();
-                
                 var needsToJoin = true;
                 
                 // The room members is available in the data fetched by initialSync
@@ -364,7 +464,8 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
                             onInit3();
                         },
                         function(reason) {
-                            $scope.feedback = "Can't join room: " + reason;
+                            console.log("Can't join room: " + JSON.stringify(reason));
+                            $scope.feedback = "You do not have permission to join this room";
                         });
                 }
                 else {
diff --git a/webclient/room/room-directive.js b/webclient/room/room-directive.js
index 1a99a37abb..659bcbc60f 100644
--- a/webclient/room/room-directive.js
+++ b/webclient/room/room-directive.js
@@ -1,5 +1,5 @@
 /*
- Copyright 2014 matrix.org
+ Copyright 2014 OpenMarket Ltd
  
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
diff --git a/webclient/room/room.html b/webclient/room/room.html
index e672b1d7e2..6732a7b3ae 100644
--- a/webclient/room/room.html
+++ b/webclient/room/room.html
@@ -24,7 +24,7 @@
                          title="{{ member.id }}"
                          width="80" height="80"/>
                     <img class="userAvatarGradient" src="img/gradient.png" title="{{ member.id }}" width="80" height="24"/>
-                    <div class="userPowerLevel" ng-style="{'width': (10 * member.powerLevel) +'%'}"></div>
+                    <div class="userPowerLevel" ng-style="{'width': member.powerLevelNorm +'%'}"></div>
                     <div class="userName">{{ member.displayname || member.id.substr(0, member.id.indexOf(':')) }}<br/>{{ member.displayname ? "" : member.id.substr(member.id.indexOf(':')) }}</div>
                 </td>
                 <td class="userPresence" ng-class="(member.presence === 'online' ? 'online' : (member.presence === 'unavailable' ? 'unavailable' : '')) + ' ' + (member.membership == 'invite' ? 'invited' : '')">
@@ -48,10 +48,23 @@
                 </td>
                 <td ng-class="!msg.content.membership ? (msg.content.msgtype === 'm.emote' ? 'emote text' : 'text') : 'membership text'">
                     <div class="bubble">
-                        <span ng-show='msg.type === "m.room.member"'>
+                        <span ng-if="'join' === msg.content.membership">
+                            {{ members[msg.state_key].displayname || msg.state_key }} joined
+                        </span>
+                        <span ng-if="'leave' === msg.content.membership">
+                            <span ng-if="msg.user_id === msg.state_key">
+                                {{ members[msg.state_key].displayname || msg.state_key }} left
+                            </span>
+                            <span ng-if="msg.user_id !== msg.state_key">
+                                {{ members[msg.user_id].displayname || msg.user_id }}
+                                {{ {"join": "kicked", "ban": "unbanned"}[msg.content.prev] }}
+                                {{ members[msg.state_key].displayname || msg.state_key }}
+                            </span>
+                        </span>
+                        <span ng-if="'invite' === msg.content.membership || 'ban' === msg.content.membership">
                             {{ members[msg.user_id].displayname || msg.user_id }}
-                            {{ {"join": "joined", "leave": "left", "invite": "invited"}[msg.content.membership] }}
-                            {{ msg.content.membership === "invite" ? (msg.state_key || '') : '' }}
+                            {{ {"invite": "invited", "ban": "banned"}[msg.content.membership] }}
+                            {{ members[msg.state_key].displayname || msg.state_key }}
                         </span>
                         <span ng-show='msg.content.msgtype === "m.emote"' ng-bind-html="'* ' + (members[msg.user_id].displayname || msg.user_id) + ' ' + msg.content.body | linky:'_blank'"/>
                         <span ng-show='msg.content.msgtype === "m.text"' ng-bind-html="((msg.content.msgtype === 'm.text') ? msg.content.body : '') | linky:'_blank'"/>
diff --git a/webclient/settings/settings-controller.js b/webclient/settings/settings-controller.js
index dc680ef075..7a26367a1b 100644
--- a/webclient/settings/settings-controller.js
+++ b/webclient/settings/settings-controller.js
@@ -1,5 +1,5 @@
 /*
-Copyright 2014 matrix.org
+Copyright 2014 OpenMarket Ltd
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
diff --git a/webclient/settings/settings.html b/webclient/settings/settings.html
index a69a8de300..b7fd5dfb50 100644
--- a/webclient/settings/settings.html
+++ b/webclient/settings/settings.html
@@ -12,18 +12,19 @@
                 <div class="profile-avatar">
                     <img ng-src="{{ (null !== profile.avatarUrl) ? profile.avatarUrl : 'img/default-profile.png' }}" m-file-input="profile.avatarFile"/>
                 </div>
-                <div id="user-ids">
-                    <input size="40" ng-model="profile.displayName" placeholder="Your display name"/>
+                <div>
+                    <input id="user-displayname-input" size="40" ng-model="profile.displayName" placeholder="Your display name"/>
                     <br/>
-                    <button ng-disabled="(profile.displayName == profileOnServer.displayName) && (profile.avatarUrl == profileOnServer.avatarUrl)"
-                            ng-click="saveProfile()">Save</button>    
+                    <button id="user-save-button"
+                            ng-disabled="(profile.displayName === profileOnServer.displayName) && (profile.avatarUrl === profileOnServer.avatarUrl)"
+                            ng-click="saveProfile()">Save changes</button>
                 </div>
             </form>
         </div>
         <br/>
 
-        <h3>Linked emails</h3>
-        <div class="section">
+        <h3 style="display: none; ">Linked emails</h3>
+        <div class="section" style="display: none; ">
             <form>
                 <input size="40" ng-model="linkedEmails.linkNewEmail" ng-enter="linkEmail(linkedEmails.linkNewEmail)" />
                 <button ng-disabled="!linkedEmails.linkNewEmail" ng-click="linkEmail(linkedEmails.linkNewEmail)">
@@ -73,6 +74,21 @@
             <div>Access token: {{ config.access_token }} </div>
         </div>
         <br/>
+        
+        <h3>Commands</h3>
+        <div class="section">
+            The following commands are available in the room chat:
+            <ul>
+                <li>/nick &lt;display_name&gt;: change your display name</li>
+                <li>/me &lt;action&gt;: send the action you are doing. /me will be replaced by your display name</li>
+                <li>/kick &lt;user_id&gt;: kick the user</li>
+                <li>/ban &lt;user_id&gt; [&lt;reason&gt;]: ban the user</li>
+                <li>/unban &lt;user_id&gt;: unban the user</li>
+                <li>/op &lt;user_id&gt; &lt;power_level&gt;: set user power level</li>
+                <li>/deop &lt;user_id&gt;: reset user power level to the room default value</li>
+            </ul>
+        </div>
+        <br/>
 
         {{ feedback }}
 
diff --git a/webclient/user/user-controller.js b/webclient/user/user-controller.js
index b5b2d439a2..3940db6683 100644
--- a/webclient/user/user-controller.js
+++ b/webclient/user/user-controller.js
@@ -1,5 +1,5 @@
 /*
-Copyright 2014 matrix.org
+Copyright 2014 OpenMarket Ltd
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.