summary refs log tree commit diff
path: root/syweb/webclient
diff options
context:
space:
mode:
Diffstat (limited to 'syweb/webclient')
-rwxr-xr-xsyweb/webclient/app.css8
-rw-r--r--syweb/webclient/app.js1
-rw-r--r--syweb/webclient/components/matrix/event-handler-service.js24
-rw-r--r--syweb/webclient/components/matrix/matrix-call.js20
-rw-r--r--syweb/webclient/components/matrix/matrix-service.js2
-rw-r--r--syweb/webclient/components/matrix/recents-service.js99
-rw-r--r--syweb/webclient/index.html1
-rw-r--r--syweb/webclient/recents/recents-controller.js31
-rw-r--r--syweb/webclient/recents/recents.html6
-rw-r--r--syweb/webclient/room/room-controller.js50
-rw-r--r--syweb/webclient/room/room.html2
-rw-r--r--syweb/webclient/test/karma.conf.js16
-rw-r--r--syweb/webclient/test/unit/recents-service.spec.js153
13 files changed, 362 insertions, 51 deletions
diff --git a/syweb/webclient/app.css b/syweb/webclient/app.css
index 403d615bf1..648388cdb9 100755
--- a/syweb/webclient/app.css
+++ b/syweb/webclient/app.css
@@ -812,6 +812,14 @@ textarea, input {
     background-color: #eee;
 }
 
+.recentsRoomUnread {
+    background-color: #fee;
+}
+
+.recentsRoomBing {
+    background-color: #eef;
+}
+
 .recentsRoomName {
     font-size: 16px;
     padding-top: 7px;
diff --git a/syweb/webclient/app.js b/syweb/webclient/app.js
index 17b2bb6e8f..35190a71f4 100644
--- a/syweb/webclient/app.js
+++ b/syweb/webclient/app.js
@@ -31,6 +31,7 @@ var matrixWebClient = angular.module('matrixWebClient', [
     'eventStreamService',
     'eventHandlerService',
     'notificationService',
+    'recentsService',
     'modelService',
     'infinite-scroll',
     'ui.bootstrap',
diff --git a/syweb/webclient/components/matrix/event-handler-service.js b/syweb/webclient/components/matrix/event-handler-service.js
index 7b2a75507d..6645d20374 100644
--- a/syweb/webclient/components/matrix/event-handler-service.js
+++ b/syweb/webclient/components/matrix/event-handler-service.js
@@ -95,14 +95,22 @@ function(matrixService, $rootScope, $q, $timeout, $filter, mPresence, notificati
         modelService.createRoomIdToAliasMapping(event.room_id, event.content.aliases[0]);
     };
     
+    var containsBingWord = function(event) {
+        if (!event.content || !event.content.body) {
+            return false;
+        }
+    
+        return notificationService.containsBingWord(
+            matrixService.config().user_id,
+            matrixService.config().display_name,
+            matrixService.config().bingWords,
+            event.content.body
+        );
+    };
+    
     var displayNotification = function(event) {
         if (window.Notification && event.user_id != matrixService.config().user_id) {
-            var shouldBing = notificationService.containsBingWord(
-                matrixService.config().user_id,
-                matrixService.config().display_name,
-                matrixService.config().bingWords,
-                event.content.body
-            );
+            var shouldBing = containsBingWord(event);
 
             // Ideally we would notify only when the window is hidden (i.e. document.hidden = true).
             //
@@ -529,6 +537,10 @@ function(matrixService, $rootScope, $q, $timeout, $filter, mPresence, notificati
             resetRoomMessages(room_id);
         },
         
+        eventContainsBingWord: function(event) {
+            return containsBingWord(event);
+        },
+        
         /**
          * Return the last message event of a room
          * @param {String} room_id the room id
diff --git a/syweb/webclient/components/matrix/matrix-call.js b/syweb/webclient/components/matrix/matrix-call.js
index b560cf7daa..a1c3aaa103 100644
--- a/syweb/webclient/components/matrix/matrix-call.js
+++ b/syweb/webclient/components/matrix/matrix-call.js
@@ -82,7 +82,7 @@ angular.module('MatrixCall', [])
         });
     }
 
-    // FIXME: we should prevent any class from being placed or accepted before this has finished
+    // FIXME: we should prevent any calls from being placed or accepted before this has finished
     MatrixCall.getTurnServer();
 
     MatrixCall.CALL_TIMEOUT = 60000;
@@ -92,7 +92,8 @@ angular.module('MatrixCall', [])
         var pc;
         if (window.mozRTCPeerConnection) {
             var iceServers = [];
-            if (MatrixCall.turnServer) {
+            // https://github.com/EricssonResearch/openwebrtc/issues/85
+            if (MatrixCall.turnServer /*&& !this.isOpenWebRTC()*/) {
                 if (MatrixCall.turnServer.uris) {
                     for (var i = 0; i < MatrixCall.turnServer.uris.length; i++) {
                         iceServers.push({
@@ -110,7 +111,8 @@ angular.module('MatrixCall', [])
             pc = new window.mozRTCPeerConnection({"iceServers":iceServers});
         } else {
             var iceServers = [];
-            if (MatrixCall.turnServer) {
+            // https://github.com/EricssonResearch/openwebrtc/issues/85
+            if (MatrixCall.turnServer /*&& !this.isOpenWebRTC()*/) {
                 if (MatrixCall.turnServer.uris) {
                     iceServers.push({
                         'urls': MatrixCall.turnServer.uris,
@@ -492,6 +494,8 @@ angular.module('MatrixCall', [])
             $timeout(function() {
                 var vel = self.getRemoteVideoElement();
                 if (vel.play) vel.play();
+                // OpenWebRTC does not support oniceconnectionstatechange yet
+                if (self.isOpenWebRTC()) self.state = 'connected';
             });
         }
     };
@@ -641,5 +645,15 @@ angular.module('MatrixCall', [])
         return null;
     };
 
+    MatrixCall.prototype.isOpenWebRTC = function() {
+        var scripts = angular.element('script');
+        for (var i = 0; i < scripts.length; i++) {
+            if (scripts[i].src.indexOf("owr.js") > -1) {
+                return true;
+            }
+        }
+        return false;
+    };
+
     return MatrixCall;
 }]);
diff --git a/syweb/webclient/components/matrix/matrix-service.js b/syweb/webclient/components/matrix/matrix-service.js
index c1264887c8..cfe8691f85 100644
--- a/syweb/webclient/components/matrix/matrix-service.js
+++ b/syweb/webclient/components/matrix/matrix-service.js
@@ -23,7 +23,7 @@ This serves to isolate the caller from changes to the underlying url paths, as
 well as attach common params (e.g. access_token) to requests.
 */
 angular.module('matrixService', [])
-.factory('matrixService', ['$http', '$q', '$rootScope', function($http, $q, $rootScope) {
+.factory('matrixService', ['$http', '$q', function($http, $q) {
         
    /* 
     * Permanent storage of user information
diff --git a/syweb/webclient/components/matrix/recents-service.js b/syweb/webclient/components/matrix/recents-service.js
new file mode 100644
index 0000000000..3d82b8218b
--- /dev/null
+++ b/syweb/webclient/components/matrix/recents-service.js
@@ -0,0 +1,99 @@
+/*
+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.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+'use strict';
+
+/*
+This service manages shared state between *instances* of recent lists. The
+recents controller will hook into this central service to get things like:
+- which rooms should be highlighted
+- which rooms have been binged
+- which room is currently selected
+- etc.
+This is preferable to polluting the $rootScope with recents specific info, and
+makes the dependency on this shared state *explicit*.
+*/
+angular.module('recentsService', [])
+.factory('recentsService', ['$rootScope', 'eventHandlerService', function($rootScope, eventHandlerService) {
+    // notify listeners when variables in the service are updated. We need to do
+    // this since we do not tie them to any scope.
+    var BROADCAST_SELECTED_ROOM_ID = "recentsService:BROADCAST_SELECTED_ROOM_ID(room_id)";
+    var selectedRoomId = undefined;
+    
+    var BROADCAST_UNREAD_MESSAGES = "recentsService:BROADCAST_UNREAD_MESSAGES(room_id, unreadCount)";
+    var unreadMessages = {
+        // room_id: <number>
+    };
+    
+    var BROADCAST_UNREAD_BING_MESSAGES = "recentsService:BROADCAST_UNREAD_BING_MESSAGES(room_id, event)";
+    var unreadBingMessages = {
+        // room_id: bingEvent
+    };
+    
+    // listen for new unread messages
+    $rootScope.$on(eventHandlerService.MSG_EVENT, function(ngEvent, event, isLive) {
+        if (isLive && event.room_id !== selectedRoomId) {
+            if (eventHandlerService.eventContainsBingWord(event)) {
+                if (!unreadBingMessages[event.room_id]) {
+                    unreadBingMessages[event.room_id] = {};
+                }
+                unreadBingMessages[event.room_id] = event;
+                $rootScope.$broadcast(BROADCAST_UNREAD_BING_MESSAGES, event.room_id, event);
+            }
+        
+            if (!unreadMessages[event.room_id]) {
+                unreadMessages[event.room_id] = 0;
+            }
+            unreadMessages[event.room_id] += 1;
+            $rootScope.$broadcast(BROADCAST_UNREAD_MESSAGES, event.room_id, unreadMessages[event.room_id]);
+        }
+    });
+    
+    return {
+        BROADCAST_SELECTED_ROOM_ID: BROADCAST_SELECTED_ROOM_ID,
+        BROADCAST_UNREAD_MESSAGES: BROADCAST_UNREAD_MESSAGES,
+    
+        getSelectedRoomId: function() {
+            return selectedRoomId;
+        },
+        
+        setSelectedRoomId: function(room_id) {
+            selectedRoomId = room_id;
+            $rootScope.$broadcast(BROADCAST_SELECTED_ROOM_ID, room_id);
+        },
+        
+        getUnreadMessages: function() {
+            return unreadMessages;
+        },
+        
+        getUnreadBingMessages: function() {
+            return unreadBingMessages;
+        },
+        
+        markAsRead: function(room_id) {
+            if (unreadMessages[room_id]) {
+                unreadMessages[room_id] = 0;
+            }
+            if (unreadBingMessages[room_id]) {
+                unreadBingMessages[room_id] = undefined;
+            }
+            $rootScope.$broadcast(BROADCAST_UNREAD_MESSAGES, room_id, 0);
+            $rootScope.$broadcast(BROADCAST_UNREAD_BING_MESSAGES, room_id, undefined);
+        }
+    
+    };
+
+}]);
diff --git a/syweb/webclient/index.html b/syweb/webclient/index.html
index f6487f381d..4bca320e77 100644
--- a/syweb/webclient/index.html
+++ b/syweb/webclient/index.html
@@ -44,6 +44,7 @@
     <script src="components/matrix/event-stream-service.js"></script>
     <script src="components/matrix/event-handler-service.js"></script>
     <script src="components/matrix/notification-service.js"></script>
+    <script src="components/matrix/recents-service.js"></script>
     <script src="components/matrix/model-service.js"></script>
     <script src="components/matrix/presence-service.js"></script>
     <script src="components/fileInput/file-input-directive.js"></script>
diff --git a/syweb/webclient/recents/recents-controller.js b/syweb/webclient/recents/recents-controller.js
index 6f0be18f1a..41720d4cb0 100644
--- a/syweb/webclient/recents/recents-controller.js
+++ b/syweb/webclient/recents/recents-controller.js
@@ -17,18 +17,37 @@
 'use strict';
 
 angular.module('RecentsController', ['matrixService', 'matrixFilter'])
-.controller('RecentsController', ['$rootScope', '$scope', 'eventHandlerService', 'modelService', 
-                               function($rootScope, $scope, eventHandlerService, modelService) {
+.controller('RecentsController', ['$rootScope', '$scope', 'eventHandlerService', 'modelService', 'recentsService',
+                               function($rootScope, $scope, eventHandlerService, modelService, recentsService) {
 
     // Expose the service to the view
     $scope.eventHandlerService = eventHandlerService;
     
     // retrieve all rooms and expose them
     $scope.rooms = modelService.getRooms();
-
-    // $rootScope of the parent where the recents component is included can override this value
-    // in order to highlight a specific room in the list
-    $rootScope.recentsSelectedRoomID;
+    
+    // track the selected room ID: the html will use this
+    $scope.recentsSelectedRoomID = recentsService.getSelectedRoomId();
+    $scope.$on(recentsService.BROADCAST_SELECTED_ROOM_ID, function(ngEvent, room_id) {
+        $scope.recentsSelectedRoomID = room_id;
+    });
+    
+    // track the list of unread messages: the html will use this
+    $scope.unreadMessages = recentsService.getUnreadMessages();
+    $scope.$on(recentsService.BROADCAST_UNREAD_MESSAGES, function(ngEvent, room_id, unreadCount) {
+        $scope.unreadMessages = recentsService.getUnreadMessages();
+    });
+    
+    // track the list of unread BING messages: the html will use this
+    $scope.unreadBings = recentsService.getUnreadBingMessages();
+    $scope.$on(recentsService.BROADCAST_UNREAD_BING_MESSAGES, function(ngEvent, room_id, event) {
+        $scope.unreadBings = recentsService.getUnreadBingMessages();
+    });
+    
+    $scope.selectRoom = function(room) {
+        recentsService.markAsRead(room.room_id);
+        $rootScope.goToPage('room/' + (room.room_alias ? room.room_alias : room.room_id) );
+    };
 
 }]);
 
diff --git a/syweb/webclient/recents/recents.html b/syweb/webclient/recents/recents.html
index 7297e23703..0b3a77ca11 100644
--- a/syweb/webclient/recents/recents.html
+++ b/syweb/webclient/recents/recents.html
@@ -1,9 +1,9 @@
 <div ng-controller="RecentsController">
     <table class="recentsTable">
         <tbody ng-repeat="(index, room) in rooms | orderRecents" 
-               ng-click="goToPage('room/' + (room.room_alias ? room.room_alias : room.room_id) )" 
-               class="recentsRoom" 
-               ng-class="{'recentsRoomSelected': (room.room_id === recentsSelectedRoomID)}">                                           
+               ng-click="selectRoom(room)" 
+               class="recentsRoom"
+               ng-class="{'recentsRoomSelected': (room.room_id === recentsSelectedRoomID), 'recentsRoomBing': (unreadBings[room.room_id]), 'recentsRoomUnread': (unreadMessages[room.room_id])}">
             <tr>
                 <td ng-class="room.current_room_state.state('m.room.join_rules').content.join_rule == 'public' ? 'recentsRoomName recentsPublicRoom' : 'recentsRoomName'">
                     {{ room.room_id | mRoomName }}
diff --git a/syweb/webclient/room/room-controller.js b/syweb/webclient/room/room-controller.js
index 6928754c5d..6670201707 100644
--- a/syweb/webclient/room/room-controller.js
+++ b/syweb/webclient/room/room-controller.js
@@ -15,21 +15,14 @@ limitations under the License.
 */
 
 angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput', 'angular-peity'])
-.controller('RoomController', ['$modal', '$filter', '$scope', '$timeout', '$routeParams', '$location', '$rootScope', 'matrixService', 'mPresence', 'eventHandlerService', 'mFileUpload', 'matrixPhoneService', 'MatrixCall', 'notificationService', 'modelService',
-                               function($modal, $filter, $scope, $timeout, $routeParams, $location, $rootScope, matrixService, mPresence, eventHandlerService, mFileUpload, matrixPhoneService, MatrixCall, notificationService, modelService) {
+.controller('RoomController', ['$modal', '$filter', '$scope', '$timeout', '$routeParams', '$location', '$rootScope', 'matrixService', 'mPresence', 'eventHandlerService', 'mFileUpload', 'matrixPhoneService', 'MatrixCall', 'notificationService', 'modelService', 'recentsService',
+                               function($modal, $filter, $scope, $timeout, $routeParams, $location, $rootScope, matrixService, mPresence, eventHandlerService, mFileUpload, matrixPhoneService, MatrixCall, notificationService, modelService, recentsService) {
    'use strict';
     var MESSAGES_PER_PAGINATION = 30;
     var THUMBNAIL_SIZE = 320;
     
     // .html needs this
-    $scope.containsBingWord = function(content) {
-        return notificationService.containsBingWord(
-            matrixService.config().user_id,
-            matrixService.config().display_name,
-            matrixService.config().bingWords,
-            content
-        );
-    };
+    $scope.containsBingWord = eventHandlerService.eventContainsBingWord;
 
     // Room ids. Computed and resolved in onInit
     $scope.room_id = undefined;
@@ -46,12 +39,8 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput', 'a
         messages_visibility: "hidden", // In order to avoid flickering when scrolling down the message table at the page opening, delay the message table display
     };
     $scope.members = {};
-    $scope.autoCompleting = false;
-    $scope.autoCompleteIndex = 0;    
-    $scope.autoCompleteOriginal = "";
 
     $scope.imageURLToSend = "";
-    $scope.userIDToInvite = "";
     
 
     // vars and functions for updating the name
@@ -162,7 +151,6 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput', 'a
 
     $scope.$on(eventHandlerService.MSG_EVENT, function(ngEvent, event, isLive) {
         if (isLive && event.room_id === $scope.room_id) {
-            
             scrollToBottom();
         }
     });
@@ -804,7 +792,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput', 'a
         console.log("onInit3");
 
         // Make recents highlight the current room
-        $scope.recentsSelectedRoomID = $scope.room_id;
+        recentsService.setSelectedRoomId($scope.room_id);
 
         // Init the history for this room
         history.init();
@@ -841,19 +829,6 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput', 'a
             }
         );
     }; 
-    
-    $scope.inviteUser = function() {
-        
-        matrixService.invite($scope.room_id, $scope.userIDToInvite).then(
-            function() {
-                console.log("Invited.");
-                $scope.feedback = "Invite successfully sent to " + $scope.userIDToInvite;
-                $scope.userIDToInvite = "";
-            },
-            function(reason) {
-                $scope.feedback = "Failure: " + reason.data.error;
-            });
-    };
 
     $scope.leaveRoom = function() {
         
@@ -923,7 +898,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput', 'a
         call.onError = $rootScope.onCallError;
         call.onHangup = $rootScope.onCallHangup;
         // remote video element is used for playing audio in voice calls
-        call.remoteVideoElement = angular.element('#remoteVideo')[0];
+        call.remoteVideoSelector = angular.element('#remoteVideo')[0];
         call.placeVoiceCall();
         $rootScope.currentCall = call;
     };
@@ -1091,6 +1066,21 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput', 'a
 })
 .controller('RoomInfoController', function($scope, $modalInstance, $filter, matrixService) {
     console.log("Displaying room info.");
+    
+    $scope.userIDToInvite = "";
+    
+    $scope.inviteUser = function() {
+        
+        matrixService.invite($scope.room_id, $scope.userIDToInvite).then(
+            function() {
+                console.log("Invited.");
+                $scope.feedback = "Invite successfully sent to " + $scope.userIDToInvite;
+                $scope.userIDToInvite = "";
+            },
+            function(reason) {
+                $scope.feedback = "Failure: " + reason.data.error;
+            });
+    };
 
     $scope.submit = function(event) {
         if (event.content) {
diff --git a/syweb/webclient/room/room.html b/syweb/webclient/room/room.html
index 1f1cd9baef..b97b839b41 100644
--- a/syweb/webclient/room/room.html
+++ b/syweb/webclient/room/room.html
@@ -203,7 +203,7 @@
                         
                         <span ng-show='msg.content.msgtype === "m.text"' 
                               class="message"
-                              ng-class="containsBingWord(msg.content.body) && msg.user_id != state.user_id ? msg.echo_msg_state + ' messageBing' : msg.echo_msg_state"
+                              ng-class="containsBingWord(msg) && msg.user_id != state.user_id ? msg.echo_msg_state + ' messageBing' : msg.echo_msg_state"
                               ng-bind-html="(msg.content.msgtype === 'm.text' && msg.type === 'm.room.message' && msg.content.format === 'org.matrix.custom.html') ? 
                                                                                         (msg.content.formatted_body | unsanitizedLinky) :
                                              (msg.content.msgtype === 'm.text' && msg.type === 'm.room.message') ? (msg.content.body | linky:'_blank') : '' "/>
diff --git a/syweb/webclient/test/karma.conf.js b/syweb/webclient/test/karma.conf.js
index 5f0642ca33..37a9eaf1c1 100644
--- a/syweb/webclient/test/karma.conf.js
+++ b/syweb/webclient/test/karma.conf.js
@@ -52,18 +52,32 @@ module.exports = function(config) {
     // preprocess matching files before serving them to the browser
     // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
     preprocessors: {
+      '../login/**/*.js': 'coverage', 
+      '../room/**/*.js': 'coverage',
+      '../components/**/*.js': 'coverage',
+      '../user/**/*.js': 'coverage',
+      '../home/**/*.js': 'coverage',
+      '../recents/**/*.js': 'coverage',
+      '../settings/**/*.js': 'coverage',
+      '../app.js': 'coverage'
     },
 
 
     // test results reporter to use
     // possible values: 'dots', 'progress'
     // available reporters: https://npmjs.org/browse/keyword/karma-reporter
-    reporters: ['progress', 'junit'],
+    reporters: ['progress', 'junit', 'coverage'],
     junitReporter: {
         outputFile: 'test-results.xml',
         suite: ''
     },
 
+    coverageReporter: {
+        type: 'cobertura',
+        dir: 'coverage/',
+        file: 'coverage.xml'
+    },
+
     // web server port
     port: 9876,
 
diff --git a/syweb/webclient/test/unit/recents-service.spec.js b/syweb/webclient/test/unit/recents-service.spec.js
new file mode 100644
index 0000000000..a2f9ecbaf8
--- /dev/null
+++ b/syweb/webclient/test/unit/recents-service.spec.js
@@ -0,0 +1,153 @@
+describe('RecentsService', function() {
+    var scope;
+    var MSG_EVENT = "__test__";
+    
+    var testEventContainsBingWord, testIsLive, testEvent;
+    
+    var eventHandlerService = {
+        MSG_EVENT: MSG_EVENT,
+        eventContainsBingWord: function(event) {
+            return testEventContainsBingWord;
+        }
+    };
+
+    // setup the service and mocked dependencies
+    beforeEach(function() {
+        
+        // set default mock values
+        testEventContainsBingWord = false;
+        testIsLive = true;
+        testEvent = {
+            content: {
+                body: "Hello world",
+                msgtype: "m.text"
+            },
+            user_id: "@alfred:localhost",
+            room_id: "!fl1bb13:localhost",
+            event_id: "fwuegfw@localhost"
+        }
+        
+        // mocked dependencies
+        module(function ($provide) {
+          $provide.value('eventHandlerService', eventHandlerService);
+        });
+        
+        // tested service
+        module('recentsService');
+    });
+    
+    beforeEach(inject(function($rootScope) {
+        scope = $rootScope;
+    }));
+
+    it('should start with no unread messages.', inject(
+    function(recentsService) {
+        expect(recentsService.getUnreadMessages()).toEqual({});
+        expect(recentsService.getUnreadBingMessages()).toEqual({});
+    }));
+    
+    it('should NOT add an unread message to the room currently selected.', inject(
+    function(recentsService) {
+        recentsService.setSelectedRoomId(testEvent.room_id);
+        scope.$broadcast(MSG_EVENT, testEvent, testIsLive);
+        expect(recentsService.getUnreadMessages()).toEqual({});
+        expect(recentsService.getUnreadBingMessages()).toEqual({});
+    }));
+    
+    it('should add an unread message to the room NOT currently selected.', inject(
+    function(recentsService) {
+        recentsService.setSelectedRoomId("!someotherroomid:localhost");
+        scope.$broadcast(MSG_EVENT, testEvent, testIsLive);
+        
+        var unread = {};
+        unread[testEvent.room_id] = 1;
+        expect(recentsService.getUnreadMessages()).toEqual(unread);
+    }));
+    
+    it('should add an unread message and an unread bing message if a message contains a bing word.', inject(
+    function(recentsService) {
+        recentsService.setSelectedRoomId("!someotherroomid:localhost");
+        testEventContainsBingWord = true;
+        scope.$broadcast(MSG_EVENT, testEvent, testIsLive);
+        
+        var unread = {};
+        unread[testEvent.room_id] = 1;
+        expect(recentsService.getUnreadMessages()).toEqual(unread);
+        
+        var bing = {};
+        bing[testEvent.room_id] = testEvent;
+        expect(recentsService.getUnreadBingMessages()).toEqual(bing);
+    }));
+    
+    it('should clear both unread and unread bing messages when markAsRead is called.', inject(
+    function(recentsService) {
+        recentsService.setSelectedRoomId("!someotherroomid:localhost");
+        testEventContainsBingWord = true;
+        scope.$broadcast(MSG_EVENT, testEvent, testIsLive);
+        
+        var unread = {};
+        unread[testEvent.room_id] = 1;
+        expect(recentsService.getUnreadMessages()).toEqual(unread);
+        
+        var bing = {};
+        bing[testEvent.room_id] = testEvent;
+        expect(recentsService.getUnreadBingMessages()).toEqual(bing);
+        
+        recentsService.markAsRead(testEvent.room_id);
+        
+        unread[testEvent.room_id] = 0;
+        bing[testEvent.room_id] = undefined;
+        expect(recentsService.getUnreadMessages()).toEqual(unread);
+        expect(recentsService.getUnreadBingMessages()).toEqual(bing);
+    }));
+    
+    it('should not add messages as unread if they are not live.', inject(
+    function(recentsService) {
+        testIsLive = false;
+        
+        recentsService.setSelectedRoomId("!someotherroomid:localhost");
+        testEventContainsBingWord = true;
+        scope.$broadcast(MSG_EVENT, testEvent, testIsLive);
+    
+        expect(recentsService.getUnreadMessages()).toEqual({});
+        expect(recentsService.getUnreadBingMessages()).toEqual({});
+    }));
+    
+    it('should increment the unread message count.', inject(
+    function(recentsService) {
+        recentsService.setSelectedRoomId("!someotherroomid:localhost");
+        scope.$broadcast(MSG_EVENT, testEvent, testIsLive);
+    
+        var unread = {};
+        unread[testEvent.room_id] = 1;
+        expect(recentsService.getUnreadMessages()).toEqual(unread);
+        
+        scope.$broadcast(MSG_EVENT, testEvent, testIsLive);
+        
+        unread[testEvent.room_id] = 2;
+        expect(recentsService.getUnreadMessages()).toEqual(unread);
+    }));
+    
+    it('should set the bing event to the latest message to contain a bing word.', inject(
+    function(recentsService) {
+        recentsService.setSelectedRoomId("!someotherroomid:localhost");
+        testEventContainsBingWord = true;
+        scope.$broadcast(MSG_EVENT, testEvent, testIsLive);
+    
+        var nextEvent = angular.copy(testEvent);
+        nextEvent.content.body = "Goodbye cruel world.";
+        nextEvent.event_id = "erfuerhfeaaaa@localhost";
+        scope.$broadcast(MSG_EVENT, nextEvent, testIsLive);
+        
+        var bing = {};
+        bing[testEvent.room_id] = nextEvent;
+        expect(recentsService.getUnreadBingMessages()).toEqual(bing);
+    }));
+    
+    it('should do nothing when marking an unknown room ID as read.', inject(
+    function(recentsService) {
+        recentsService.markAsRead("!someotherroomid:localhost");
+        expect(recentsService.getUnreadMessages()).toEqual({});
+        expect(recentsService.getUnreadBingMessages()).toEqual({});
+    }));
+});