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-filter.js35
-rw-r--r--webclient/components/matrix/event-handler-service.js96
-rw-r--r--webclient/components/matrix/matrix-filter.js90
-rw-r--r--webclient/components/matrix/matrix-service.js11
-rw-r--r--webclient/home/home-controller.js29
-rw-r--r--webclient/img/close.pngbin0 -> 397 bytes
-rw-r--r--webclient/recents/recents.html53
-rw-r--r--webclient/room/room-controller.js28
-rw-r--r--webclient/room/room-directive.js26
-rw-r--r--webclient/room/room.html15
11 files changed, 257 insertions, 136 deletions
diff --git a/webclient/app-controller.js b/webclient/app-controller.js
index 0e823b43e7..7d61207554 100644
--- a/webclient/app-controller.js
+++ b/webclient/app-controller.js
@@ -67,6 +67,16 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
         }
     };
     
+    $scope.leave = function(room_id) {
+        matrixService.leave(room_id).then(
+            function(response) {
+                console.log("Left room " + room_id);
+            },
+            function(error) {
+                console.log("Failed to leave room " + room_id + ": " + error.data.error);
+            });
+    };
+    
     // Logs the user out 
     $scope.logout = function() {
         
diff --git a/webclient/app-filter.js b/webclient/app-filter.js
index 0d059e1621..fc16492ef3 100644
--- a/webclient/app-filter.js
+++ b/webclient/app-filter.js
@@ -45,34 +45,19 @@ angular.module('matrixWebClient')
         angular.forEach(members, function(value, key) {
             value["id"] = key;
             filtered.push( value );
-            if (value["displayname"]) {
-                if (!displayNames[value["displayname"]]) {
-                    displayNames[value["displayname"]] = [];
-                }
-                displayNames[value["displayname"]].push(key);
-            }
-        });
-
-        // FIXME: we shouldn't disambiguate displayNames on every orderMembersList
-        // invocation but keep track of duplicates incrementally somewhere
-        angular.forEach(displayNames, function(value, key) {
-            if (value.length > 1) {
-                // console.log(key + ": " + value);
-                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]);
-                };
-            }
         });
 
         filtered.sort(function (a, b) {
-            // Sort members on their last_active_ago value
-            if (undefined !== a.last_active_ago || undefined !== b.last_active_ago) {
-                return ((a.last_active_ago || 10e10) > (b.last_active_ago || 10e10) ? 1 : -1);
+            // Sort members on their last_active absolute time
+            var aLastActiveTS = 0, bLastActiveTS = 0;
+            if (undefined !== a.last_active_ago) {
+                aLastActiveTS = a.last_updated - a.last_active_ago;
+            }
+            if (undefined !== b.last_active_ago) {
+                bLastActiveTS = b.last_updated - b.last_active_ago;
+            }
+            if (aLastActiveTS || bLastActiveTS) {
+                return bLastActiveTS - aLastActiveTS;
             }
             else {
                 // If they do not have last_active_ago, sort them according to their presence state
diff --git a/webclient/components/matrix/event-handler-service.js b/webclient/components/matrix/event-handler-service.js
index 9c19f306c0..e990d42d05 100644
--- a/webclient/components/matrix/event-handler-service.js
+++ b/webclient/components/matrix/event-handler-service.js
@@ -101,7 +101,7 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
 
     var initRoom = function(room_id, room) {
         if (!(room_id in $rootScope.events.rooms)) {
-            console.log("Creating new handler entry for " + room_id);
+            console.log("Creating new rooms entry for " + room_id);
             $rootScope.events.rooms[room_id] = {
                 room_id: room_id,
                 messages: [],
@@ -113,10 +113,12 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
             };
         }
 
-        if (room) {
+        if (room) { // we got an existing room object from initialsync, seemingly.
             // Report all other metadata of the room object (membership, inviter, visibility, ...)
             for (var field in room) {
-                if (-1 === ["room_id", "messages", "state"].indexOf(field)) {
+                if (!room.hasOwnProperty(field)) continue;
+
+                if (-1 === ["room_id", "messages", "state"].indexOf(field)) { // why indexOf - why not ===? --Matthew
                     $rootScope.events.rooms[room_id][field] = room[field];
                 }
             }
@@ -211,11 +213,8 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
                 
                 if (shouldBing && isIdle) {
                     console.log("Displaying notification for "+JSON.stringify(event));
-                    var member = $rootScope.events.rooms[event.room_id].members[event.user_id];
-                    var displayname = undefined;
-                    if (member) {
-                        displayname = member.displayname;
-                    }
+                    var member = getMember(event.room_id, event.user_id);
+                    var displayname = getUserDisplayName(event.room_id, event.user_id);
 
                     var message = event.content.body;
                     if (event.content.msgtype === "m.emote") {
@@ -223,7 +222,7 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
                     }
 
                     var notification = new window.Notification(
-                        (displayname || event.user_id) +
+                        displayname +
                         " (" + (matrixService.getRoomIdToAliasMapping(event.room_id) || event.room_id) + ")", // FIXME: don't leak room_ids here
                     {
                         "body": message,
@@ -345,6 +344,65 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
         return index;
     };
     
+    /**
+     * Get the member object of a room member
+     * @param {String} room_id the room id
+     * @param {String} user_id the id of the user
+     * @returns {undefined | Object} the member object of this user in this room if he is part of the room
+     */
+    var getMember = function(room_id, user_id) {
+        var member;
+
+        var room = $rootScope.events.rooms[room_id];
+        if (room) {
+            member = room.members[user_id];
+        }
+        return member;
+    };
+
+    /**
+     * Return the display name of an user acccording to data already downloaded
+     * @param {String} room_id the room id
+     * @param {String} user_id the id of the user
+     * @returns {String} the user displayname or user_id if not available
+     */
+    var getUserDisplayName = function(room_id, user_id) {
+        var displayName;
+
+        // Get the user display name from the member list of the room
+        var member = getMember(room_id, user_id);
+        if (member && member.content.displayname) { // Do not consider null displayname
+            displayName = member.content.displayname;
+
+            // Disambiguate users who have the same displayname in the room
+            if (user_id !== matrixService.config().user_id) {
+                var room = $rootScope.events.rooms[room_id];
+
+                for (var member_id in room.members) {
+                    if (room.members.hasOwnProperty(member_id) && member_id !== user_id) {
+                        var member2 = room.members[member_id];
+                        if (member2.content.displayname && member2.content.displayname === displayName) {
+                            displayName = displayName + " (" + user_id + ")";
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+
+        // The user may not have joined the room yet. So try to resolve display name from presence data
+        // Note: This data may not be available
+        if (undefined === displayName && user_id in $rootScope.presence) {
+            displayName = $rootScope.presence[user_id].content.displayname;
+        }
+
+        if (undefined === displayName) {
+            // By default, use the user ID
+            displayName = user_id;
+        }
+        return displayName;
+    };
+
     return {
         ROOM_CREATE_EVENT: ROOM_CREATE_EVENT,
         MSG_EVENT: MSG_EVENT,
@@ -516,6 +574,8 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
                 memberCount = 0;
 
                 for (var i in room.members) {
+                    if (!room.members.hasOwnProperty(i)) continue;
+
                     var member = room.members[i];
 
                     if ("join" === member.membership) {
@@ -534,15 +594,19 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
          * @returns {undefined | Object} the member object of this user in this room if he is part of the room
          */
         getMember: function(room_id, user_id) {
-            var member;
-            
-            var room = $rootScope.events.rooms[room_id];
-            if (room) {
-                member = room.members[user_id];
-            }
-            return member;
+            return getMember(room_id, user_id);
         },
         
+        /**
+         * Return the display name of an user acccording to data already downloaded
+         * @param {String} room_id the room id
+         * @param {String} user_id the id of the user
+         * @returns {String} the user displayname or user_id if not available
+         */
+        getUserDisplayName: function(room_id, user_id) {
+            return getUserDisplayName(room_id, user_id);
+        },
+
         setRoomVisibility: function(room_id, visible) {
             if (!visible) {
                 return;
diff --git a/webclient/components/matrix/matrix-filter.js b/webclient/components/matrix/matrix-filter.js
index 328e3a7086..e6f2acc5fd 100644
--- a/webclient/components/matrix/matrix-filter.js
+++ b/webclient/components/matrix/matrix-filter.js
@@ -19,7 +19,7 @@
 angular.module('matrixFilter', [])
 
 // Compute the room name according to information we have
-.filter('mRoomName', ['$rootScope', 'matrixService', function($rootScope, matrixService) {
+.filter('mRoomName', ['$rootScope', 'matrixService', 'eventHandlerService', function($rootScope, matrixService, eventHandlerService) {
     return function(room_id) {
         var roomName;
 
@@ -31,49 +31,57 @@ angular.module('matrixFilter', [])
         if (room) {
             // Get name from room state date
             var room_name_event = room["m.room.name"];
+
+            // Determine if it is a public room
+            var isPublicRoom = false;
+            if (room["m.room.join_rules"] && room["m.room.join_rules"].content) {
+                isPublicRoom = ("public" === room["m.room.join_rules"].content.join_rule);
+            }
+
             if (room_name_event) {
                 roomName = room_name_event.content.name;
             }
             else if (alias) {
                 roomName = alias;
             }
-            else if (room.members) {
-
+            else if (room.members && !isPublicRoom) {    // Do not rename public room
+            
                 var user_id = matrixService.config().user_id;
 
                 // Else, build the name from its users
                 // Limit the room renaming to 1:1 room
                 if (2 === Object.keys(room.members).length) {
                     for (var i in room.members) {
+                        if (!room.members.hasOwnProperty(i)) continue;
+
                         var member = room.members[i];
                         if (member.state_key !== 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;
-                            }
+                            roomName = eventHandlerService.getUserDisplayName(room_id, member.state_key);
+                            break;
                         }
                     }
                 }
-                else if (1 === Object.keys(room.members).length) {
+                else if (Object.keys(room.members).length <= 1) {
+                    
                     var otherUserId;
 
-                    if (Object.keys(room.members)[0] !== user_id) {
+                    if (Object.keys(room.members)[0] && Object.keys(room.members)[0] !== user_id) {
                         otherUserId = Object.keys(room.members)[0];
                     }
                     else {
+                        // it's got to be an invite, or failing that a self-chat;
+                        otherUserId = room.inviter || user_id;
+/*                        
+                        // XXX: This should all be unnecessary now thanks to using the /rooms/<room>/roomid API
+
                         // The other member may be in the invite list, get all invited users
                         var invitedUserIDs = [];
+                        
+                        // XXX: *SURELY* we shouldn't have to trawl through the whole messages list to
+                        // find invite - surely the other user should be in room.members with state invited? :/ --Matthew
                         for (var i in room.messages) {
                             var message = room.messages[i];
-                            if ("m.room.member" === message.type && "invite" === message.membership) {
+                            if ("m.room.member" === message.type && "invite" === message.content.membership) {
                                 // Filter out the current user
                                 var member_id = message.state_key;
                                 if (member_id === user_id) {
@@ -92,15 +100,11 @@ angular.module('matrixFilter', [])
                         if (1 === invitedUserIDs.length) {
                             otherUserId = invitedUserIDs[0];
                         }
+*/                        
                     }
-
-                    // Try to resolve his displayname in presence global data
-                    if (otherUserId in $rootScope.presence) {
-                        roomName = $rootScope.presence[otherUserId].content.displayname;
-                    }
-                    else {
-                        roomName = otherUserId;
-                    }
+                    
+                    // Get the user display name
+                    roomName = eventHandlerService.getUserDisplayName(room_id, otherUserId);
                 }
             }
         }
@@ -127,37 +131,9 @@ angular.module('matrixFilter', [])
     };
 }])
 
-// Compute the user display name in a room according to the data already downloaded
-.filter('mUserDisplayName', ['$rootScope', function($rootScope) {
+// Return the user display name
+.filter('mUserDisplayName', ['eventHandlerService', function(eventHandlerService) {
     return function(user_id, room_id) {
-        var displayName;
-    
-        // Try to find the user name among presence data
-        // Warning: that means we have received before a presence event for this
-        // user which cannot be guaranted.
-        // However, if we get the info by this way, we are sure this is the latest user display name
-        // See FIXME comment below
-        if (user_id in $rootScope.presence) {
-            displayName = $rootScope.presence[user_id].content.displayname;
-        }
-            
-        // FIXME: Would like to use the display name as defined in room members of the room.
-        // But this information is the display name of the user when he has joined the room.
-        // It does not take into account user display name update
-        if (room_id) {
-            var room = $rootScope.events.rooms[room_id];
-            if (room && (user_id in room.members)) {
-                var member = room.members[user_id];
-                if (member.content.displayname) {
-                    displayName = member.content.displayname;
-                }
-            }
-        }
-        
-        if (undefined === displayName) {
-            // By default, use the user ID
-            displayName = user_id;
-        }
-        return displayName;
+        return eventHandlerService.getUserDisplayName(room_id, user_id);
     };
 }]);
diff --git a/webclient/components/matrix/matrix-service.js b/webclient/components/matrix/matrix-service.js
index cb827a0b4d..a4f0568bce 100644
--- a/webclient/components/matrix/matrix-service.js
+++ b/webclient/components/matrix/matrix-service.js
@@ -264,7 +264,13 @@ angular.module('matrixService', [])
 
             return doRequest("GET", path, params);
         },
-
+        
+        // get room state for a specific room
+        roomState: function(room_id) {
+            var path = "/rooms/" + room_id + "/state";
+            return doRequest("GET", path);
+        },
+        
         // Joins a room
         join: function(room_id) {
             return this.membershipChange(room_id, undefined, "join");
@@ -697,11 +703,10 @@ angular.module('matrixService', [])
         createRoomIdToAliasMapping: function(roomId, alias) {
             roomIdToAlias[roomId] = alias;
             aliasToRoomId[alias] = roomId;
-            // localStorage.setItem(MAPPING_PREFIX+roomId, alias);
         },
         
         getRoomIdToAliasMapping: function(roomId) {
-            var alias = roomIdToAlias[roomId]; // was localStorage.getItem(MAPPING_PREFIX+roomId)
+            var alias = roomIdToAlias[roomId];
             //console.log("looking for alias for " + roomId + "; found: " + alias);
             return alias;
         },
diff --git a/webclient/home/home-controller.js b/webclient/home/home-controller.js
index a535d439d3..f1295560ef 100644
--- a/webclient/home/home-controller.js
+++ b/webclient/home/home-controller.js
@@ -86,18 +86,24 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen
     
     // Go to a room
     $scope.goToRoom = function(room_id) {
-        // Simply open the room page on this room id
-        //$location.url("room/" + room_id);
         matrixService.join(room_id).then(
             function(response) {
+                var final_room_id = room_id;
                 if (response.data.hasOwnProperty("room_id")) {
-                    if (response.data.room_id != room_id) {
-                        $location.url("room/" + response.data.room_id);
-                        return;
-                     }
+                    final_room_id = response.data.room_id;
                 }
 
-                $location.url("room/" + room_id);
+                // TODO: factor out the common housekeeping whenever we try to join a room or alias
+                matrixService.roomState(final_room_id).then(
+                    function(response) {
+                        eventHandlerService.handleEvents(response.data, false, true);
+                    },
+                    function(error) {
+                        $scope.feedback = "Failed to get room state for: " + final_room_id;
+                    }
+                );                                        
+
+                $location.url("room/" + final_room_id);
             },
             function(error) {
                 $scope.feedback = "Can't join room: " + JSON.stringify(error.data);
@@ -108,6 +114,15 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen
     $scope.joinAlias = function(room_alias) {
         matrixService.joinAlias(room_alias).then(
             function(response) {
+                // TODO: factor out the common housekeeping whenever we try to join a room or alias
+                matrixService.roomState(response.room_id).then(
+                    function(response) {
+                        eventHandlerService.handleEvents(response.data, false, true);
+                    },
+                    function(error) {
+                        $scope.feedback = "Failed to get room state for: " + response.room_id;
+                    }
+                );                                        
                 // Go to this room
                 $location.url("room/" + room_alias);
             },
diff --git a/webclient/img/close.png b/webclient/img/close.png
new file mode 100644
index 0000000000..fbcdb51e6b
--- /dev/null
+++ b/webclient/img/close.png
Binary files differdiff --git a/webclient/recents/recents.html b/webclient/recents/recents.html
index edfc1677eb..9cbdcd357a 100644
--- a/webclient/recents/recents.html
+++ b/webclient/recents/recents.html
@@ -2,7 +2,7 @@
     <table class="recentsTable">
         <tbody ng-repeat="(index, room) in events.rooms | orderRecents" 
                ng-click="goToPage('room/' + (room.room_alias ? room.room_alias : room.room_id) )" 
-               class ="recentsRoom" 
+               class="recentsRoom" 
                ng-class="{'recentsRoomSelected': (room.room_id === recentsSelectedRoomID)}">                                           
             <tr>
                 <td ng-class="room['m.room.join_rules'].content.join_rule == 'public' ? 'recentsRoomName recentsPublicRoom' : 'recentsRoomName'">
@@ -19,6 +19,8 @@
                     {{ lastMsg = eventHandlerService.getLastMessage(room.room_id, true);"" }}
 
                     {{ (lastMsg.ts) | date:'MMM d HH:mm' }}
+                    
+                    <img ng-click="leave(room.room_id); $event.stopPropagation();" src="img/close.png" width="10" height="10" style="margin-bottom: -1px; margin-left: 2px;" alt="close"/>
                 </td>
             </tr>
 
@@ -31,28 +33,35 @@
                     
                     <div ng-hide="room.membership === 'invite'" ng-switch="lastMsg.type">
                         <div ng-switch-when="m.room.member">
-                            <span ng-if="'join' === lastMsg.content.membership">
-                                {{ lastMsg.state_key | mUserDisplayName: room.room_id}} joined
-                            </span>
-                            <span ng-if="'leave' === lastMsg.content.membership">
-                                <span ng-if="lastMsg.user_id === lastMsg.state_key">
-                                    {{lastMsg.state_key | mUserDisplayName: room.room_id }} left
-                                </span>
-                                <span ng-if="lastMsg.user_id !== lastMsg.state_key">
-                                    {{ lastMsg.user_id | mUserDisplayName: room.room_id }}
-                                    {{ {"join": "kicked", "ban": "unbanned"}[lastMsg.content.prev] }}
-                                    {{ lastMsg.state_key | mUserDisplayName: room.room_id }}
-                                </span>
-                                <span ng-if="'join' === lastMsg.content.prev && lastMsg.content.reason">
-                                    : {{ lastMsg.content.reason }}
+                            <span ng-switch="lastMsg.changedKey">
+                                <span ng-switch-when="membership">
+                                    <span ng-if="'join' === lastMsg.content.membership">
+                                        {{ lastMsg.state_key | mUserDisplayName: room.room_id }} joined
+                                    </span>
+                                    <span ng-if="'leave' === lastMsg.content.membership">
+                                        <span ng-if="lastMsg.user_id === lastMsg.state_key">
+                                            {{lastMsg.state_key | mUserDisplayName: room.room_id }} left
+                                        </span>
+                                        <span ng-if="lastMsg.user_id !== lastMsg.state_key">
+                                            {{ lastMsg.user_id | mUserDisplayName: room.room_id }}
+                                            {{ {"join": "kicked", "ban": "unbanned"}[lastMsg.content.prev] }}
+                                            {{ lastMsg.state_key | mUserDisplayName: room.room_id }}
+                                        </span>
+                                        <span ng-if="'join' === lastMsg.content.prev && lastMsg.content.reason">
+                                            : {{ lastMsg.content.reason }}
+                                        </span>
+                                    </span>
+                                    <span ng-if="'invite' === lastMsg.content.membership || 'ban' === lastMsg.content.membership">
+                                        {{ lastMsg.user_id | mUserDisplayName: room.room_id }}
+                                        {{ {"invite": "invited", "ban": "banned"}[lastMsg.content.membership] }}
+                                        {{ lastMsg.state_key | mUserDisplayName: room.room_id }}
+                                        <span ng-if="'ban' === lastMsg.content.prev && lastMsg.content.reason">
+                                            : {{ lastMsg.content.reason }}
+                                        </span>
+                                    </span>         
                                 </span>
-                            </span>
-                            <span ng-if="'invite' === lastMsg.content.membership || 'ban' === lastMsg.content.membership">
-                                {{ lastMsg.user_id | mUserDisplayName: room.room_id }}
-                                {{ {"invite": "invited", "ban": "banned"}[lastMsg.content.membership] }}
-                                {{ lastMsg.state_key | mUserDisplayName: room.room_id }}
-                                <span ng-if="'ban' === lastMsg.content.prev && lastMsg.content.reason">
-                                    : {{ lastMsg.content.reason }}
+                                <span ng-switch-when="displayname">
+                                    {{ lastMsg.user_id }} changed their display name from {{ lastMsg.prev_content.displayname }} to {{ lastMsg.content.displayname }}
                                 </span>
                             </span>
                         </div>
diff --git a/webclient/room/room-controller.js b/webclient/room/room-controller.js
index c8104e39e6..f188d92cea 100644
--- a/webclient/room/room-controller.js
+++ b/webclient/room/room-controller.js
@@ -400,6 +400,8 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
         // Find the max power level
         var maxPowerLevel = 0;
         for (var i in $scope.members) {
+            if (!$scope.members.hasOwnProperty(i)) continue;
+
             var member = $scope.members[i];
             if (member.powerLevel) {
                 maxPowerLevel = Math.max(maxPowerLevel, member.powerLevel);
@@ -409,6 +411,8 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
         // Normalized them on a 0..100% scale to be use in css width
         if (maxPowerLevel) {
             for (var i in $scope.members) {
+                if (!$scope.members.hasOwnProperty(i)) continue;
+
                 var member = $scope.members[i];
                 member.powerLevelNorm = (member.powerLevel * 100) / maxPowerLevel;
             }
@@ -479,6 +483,15 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
                             else {
                                 promise = matrixService.joinAlias(room_alias).then(
                                     function(response) {
+                                        // TODO: factor out the common housekeeping whenever we try to join a room or alias
+                                        matrixService.roomState(response.room_id).then(
+                                            function(response) {
+                                                eventHandlerService.handleEvents(response.data, false, true);
+                                            },
+                                            function(error) {
+                                                $scope.feedback = "Failed to get room state for: " + response.room_id;
+                                            }
+                                        );                                        
                                         $location.url("room/" + room_alias);
                                     },
                                     function(error) {
@@ -715,6 +728,8 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
 
                     // Update the member list
                     for (var i in members) {
+                        if (!members.hasOwnProperty(i)) continue;
+
                         var member = members[i];
                         updateMemberList(member);
                     }
@@ -732,6 +747,16 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
                     $scope.state.waiting_for_joined_event = true;
                     matrixService.join($scope.room_id).then(
                         function() {
+                            // TODO: factor out the common housekeeping whenever we try to join a room or alias
+                            matrixService.roomState($scope.room_id).then(
+                                function(response) {
+                                    eventHandlerService.handleEvents(response.data, false, true);
+                                },
+                                function(error) {
+                                    console.error("Failed to get room state for: " + $scope.room_id);
+                                }
+                            );                                        
+                            
                             // onInit3 will be called once the joined m.room.member event is received from the events stream
                             // This avoids to get the joined information twice in parallel:
                             //    - one from the events stream
@@ -740,6 +765,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
                         },
                         function(reason) {
                             console.log("Can't join room: " + JSON.stringify(reason));
+                            // FIXME: what if it wasn't a perms problem?
                             $scope.state.permission_denied = "You do not have permission to join this room";
                         });
                 }
@@ -809,7 +835,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
         
         matrixService.leave($scope.room_id).then(
             function(response) {
-                console.log("Left room ");
+                console.log("Left room " + $scope.room_id);
                 $location.url("home");
             },
             function(error) {
diff --git a/webclient/room/room-directive.js b/webclient/room/room-directive.js
index 8db4cb5d9a..05382cfcd3 100644
--- a/webclient/room/room-directive.js
+++ b/webclient/room/room-directive.js
@@ -52,12 +52,21 @@ angular.module('RoomController')
                 // console.log("targetIndex: " + targetIndex + ", 
                 // text=" + text);
                                     
-                // FIXME: use the correct regexp to recognise userIDs
+                // FIXME: use the correct regexp to recognise userIDs --M
+                //
                 // XXX: I don't really know what the point of this is. You
                 // WANT to match freeform text given you want to match display
                 // names AND user IDs. Surely you just want to get the last
                 // word out of the input text and that's that?
                 // Am I missing something here? -- Kegan
+                //
+                // You're not missing anything - my point was that we should
+                // explicitly define the syntax for user IDs /somewhere/.
+                // Meanwhile as long as the delimeters are well defined, we
+                // could just pick "the last word".  But to know what the
+                // correct delimeters are, we probably do need a formal
+                // syntax for user IDs to refer to... --Matthew
+                
                 var search = /@?([a-zA-Z0-9_\-:\.]+)$/.exec(text);
 
                 if (targetIndex === 0) { // 0 is always the original text
@@ -135,6 +144,21 @@ angular.module('RoomController')
         });
     };
 }])
+.directive('commandHistory', [ function() {
+    return function (scope, element, attrs) {
+        element.bind("keydown", function (event) {
+            var keycodePressed = event.which;
+            var UP_ARROW = 38;
+            var DOWN_ARROW = 40;
+            if (keycodePressed === UP_ARROW) {
+                scope.history.goUp(event);
+            }
+            else if (keycodePressed === DOWN_ARROW) {
+                scope.history.goDown(event);
+            } 
+        });
+    }
+}])
 
 // A directive to anchor the scroller position at the bottom when the browser is resizing.
 // When the screen resizes, the bottom of the element remains the same, not the top.
diff --git a/webclient/room/room.html b/webclient/room/room.html
index c807e2afe1..b99413cbbf 100644
--- a/webclient/room/room.html
+++ b/webclient/room/room.html
@@ -48,7 +48,15 @@
                          width="80" height="80"/>
                     <img class="userAvatarGradient" src="img/gradient.png" title="{{ member.id }}" width="80" height="24"/>
                     <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>
+                    <div class="userName">
+                        <div ng-show="member.displayname">
+                            {{ member.id | mUserDisplayName: room_id }}
+                        </div>
+                        <div ng-hide="member.displayname">
+                            {{ member.id.substr(0, member.id.indexOf(':')) }}<br/>
+                            {{ member.id.substr(member.id.indexOf(':')) }}
+                        </div>
+                    </div>
                 </td>
                 <td class="userPresence" ng-class="(member.presence === 'online' ? 'online' : (member.presence === 'unavailable' ? 'unavailable' : '')) + ' ' + (member.membership == 'invite' ? 'invited' : '')">
                     <span ng-show="member.last_active_ago">{{ member.last_active_ago + (now - member.last_updated) | duration }}<br/>ago</span>
@@ -65,7 +73,7 @@
             <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>
                 <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="sender" ng-hide="events.rooms[room_id].messages[$index - 1].user_id === msg.user_id"> {{ msg.user_id | mUserDisplayName: room_id }}</div>
                     <div class="timestamp"
                          ng-class="msg.echo_msg_state">
                         {{ (msg.content.hsob_ts || msg.ts) | date:'MMM d HH:mm' }}
@@ -163,8 +171,7 @@
                     <td width="*">
                         <textarea id="mainInput" rows="1" ng-enter="send()"
                                   ng-disabled="state.permission_denied"
-                                  ng-keydown="(38 === $event.which) ? history.goUp($event) : ((40 === $event.which) ? history.goDown($event) : 0)"
-                                  ng-focus="true" autocomplete="off" tab-complete/>
+                                  ng-focus="true" autocomplete="off" tab-complete command-history/>
                     </td>
                     <td id="buttonsCell">
                         <button ng-click="send()" ng-disabled="state.permission_denied">Send</button>