summary refs log tree commit diff
diff options
context:
space:
mode:
authorErik Johnston <erik@matrix.org>2014-11-07 10:55:28 +0000
committerErik Johnston <erik@matrix.org>2014-11-07 10:55:28 +0000
commit3cb678f84cb3252f08788a31bef1a205edffde9c (patch)
tree01ad785dc284526ecd4aa1a04ce132bcf639f5ac
parentFix joining over federation (diff)
parentDon't cache isWebRTCSupported because whether webRTC is supported might chang... (diff)
downloadsynapse-3cb678f84cb3252f08788a31bef1a205edffde9c.tar.xz
Merge branch 'develop' of github.com:matrix-org/synapse into federation_authorization
-rw-r--r--.gitignore2
-rw-r--r--MANIFEST.in3
-rw-r--r--README.rst53
-rwxr-xr-xdemo/start.sh2
-rwxr-xr-xsetup.py3
-rwxr-xr-xsynapse/app/homeserver.py5
-rw-r--r--synapse/rest/room.py6
-rw-r--r--syweb/__init__.py0
-rw-r--r--syweb/webclient/CAPTCHA_SETUP (renamed from webclient/CAPTCHA_SETUP)0
-rw-r--r--syweb/webclient/README (renamed from webclient/README)0
-rw-r--r--syweb/webclient/app-controller.js (renamed from webclient/app-controller.js)6
-rw-r--r--syweb/webclient/app-directive.js (renamed from webclient/app-directive.js)0
-rw-r--r--syweb/webclient/app-filter.js (renamed from webclient/app-filter.js)15
-rwxr-xr-xsyweb/webclient/app.css (renamed from webclient/app.css)0
-rw-r--r--syweb/webclient/app.js (renamed from webclient/app.js)1
-rw-r--r--syweb/webclient/bootstrap.css (renamed from webclient/bootstrap.css)0
-rw-r--r--syweb/webclient/components/fileInput/file-input-directive.js (renamed from webclient/components/fileInput/file-input-directive.js)0
-rw-r--r--syweb/webclient/components/fileUpload/file-upload-service.js (renamed from webclient/components/fileUpload/file-upload-service.js)7
-rw-r--r--syweb/webclient/components/matrix/event-handler-service.js (renamed from webclient/components/matrix/event-handler-service.js)497
-rw-r--r--syweb/webclient/components/matrix/event-stream-service.js (renamed from webclient/components/matrix/event-stream-service.js)19
-rw-r--r--syweb/webclient/components/matrix/matrix-call.js (renamed from webclient/components/matrix/matrix-call.js)15
-rw-r--r--syweb/webclient/components/matrix/matrix-filter.js120
-rw-r--r--syweb/webclient/components/matrix/matrix-phone-service.js (renamed from webclient/components/matrix/matrix-phone-service.js)2
-rw-r--r--syweb/webclient/components/matrix/matrix-service.js (renamed from webclient/components/matrix/matrix-service.js)77
-rw-r--r--syweb/webclient/components/matrix/model-service.js172
-rw-r--r--syweb/webclient/components/matrix/notification-service.js (renamed from webclient/components/matrix/notification-service.js)0
-rw-r--r--syweb/webclient/components/matrix/presence-service.js (renamed from webclient/components/matrix/presence-service.js)0
-rw-r--r--syweb/webclient/components/utilities/utilities-service.js (renamed from webclient/components/utilities/utilities-service.js)0
-rw-r--r--syweb/webclient/favicon.ico (renamed from webclient/favicon.ico)bin198 -> 198 bytes
-rw-r--r--syweb/webclient/home/home-controller.js (renamed from webclient/home/home-controller.js)1
-rw-r--r--syweb/webclient/home/home.html (renamed from webclient/home/home.html)0
-rw-r--r--syweb/webclient/img/close.png (renamed from webclient/img/close.png)bin397 -> 397 bytes
-rw-r--r--syweb/webclient/img/default-profile.png (renamed from webclient/img/default-profile.png)bin1722 -> 1722 bytes
-rw-r--r--syweb/webclient/img/gradient.png (renamed from webclient/img/gradient.png)bin194 -> 194 bytes
-rw-r--r--syweb/webclient/img/green_phone.png (renamed from webclient/img/green_phone.png)bin434 -> 434 bytes
-rw-r--r--syweb/webclient/img/logo-small.png (renamed from webclient/img/logo-small.png)bin910 -> 910 bytes
-rw-r--r--syweb/webclient/img/logo.png (renamed from webclient/img/logo.png)bin4060 -> 4060 bytes
-rw-r--r--syweb/webclient/img/red_phone.png (renamed from webclient/img/red_phone.png)bin378 -> 378 bytes
-rw-r--r--syweb/webclient/index.html (renamed from webclient/index.html)5
-rw-r--r--syweb/webclient/js/angular-animate.js (renamed from webclient/js/angular-animate.js)0
-rw-r--r--syweb/webclient/js/angular-animate.min.js (renamed from webclient/js/angular-animate.min.js)0
-rwxr-xr-xsyweb/webclient/js/angular-mocks.js (renamed from webclient/js/angular-mocks.js)308
-rw-r--r--syweb/webclient/js/angular-route.js (renamed from webclient/js/angular-route.js)0
-rw-r--r--syweb/webclient/js/angular-route.min.js (renamed from webclient/js/angular-route.min.js)0
-rw-r--r--syweb/webclient/js/angular-sanitize.js (renamed from webclient/js/angular-sanitize.js)0
-rw-r--r--syweb/webclient/js/angular-sanitize.min.js (renamed from webclient/js/angular-sanitize.min.js)0
-rw-r--r--syweb/webclient/js/angular.js (renamed from webclient/js/angular.js)0
-rw-r--r--syweb/webclient/js/angular.min.js (renamed from webclient/js/angular.min.js)0
-rwxr-xr-xsyweb/webclient/js/autofill-event.js (renamed from webclient/js/autofill-event.js)0
-rw-r--r--syweb/webclient/js/elastic.js (renamed from webclient/js/elastic.js)0
-rw-r--r--syweb/webclient/js/jquery-1.8.3.min.js (renamed from webclient/js/jquery-1.8.3.min.js)0
-rw-r--r--syweb/webclient/js/ng-infinite-scroll-matrix.js (renamed from webclient/js/ng-infinite-scroll-matrix.js)0
-rw-r--r--syweb/webclient/js/ui-bootstrap-tpls-0.11.2.js (renamed from webclient/js/ui-bootstrap-tpls-0.11.2.js)0
-rw-r--r--syweb/webclient/login/login-controller.js (renamed from webclient/login/login-controller.js)0
-rw-r--r--syweb/webclient/login/login.html (renamed from webclient/login/login.html)0
-rw-r--r--syweb/webclient/login/register-controller.js (renamed from webclient/login/register-controller.js)2
-rw-r--r--syweb/webclient/login/register.html (renamed from webclient/login/register.html)0
-rw-r--r--syweb/webclient/media/busy.mp3 (renamed from webclient/media/busy.mp3)bin24834 -> 24834 bytes
-rw-r--r--syweb/webclient/media/busy.ogg (renamed from webclient/media/busy.ogg)bin13960 -> 13960 bytes
-rw-r--r--syweb/webclient/media/callend.mp3 (renamed from webclient/media/callend.mp3)bin12971 -> 12971 bytes
-rw-r--r--syweb/webclient/media/callend.ogg (renamed from webclient/media/callend.ogg)bin13932 -> 13932 bytes
-rw-r--r--syweb/webclient/media/ring.mp3 (renamed from webclient/media/ring.mp3)bin19662 -> 19662 bytes
-rw-r--r--syweb/webclient/media/ring.ogg (renamed from webclient/media/ring.ogg)bin20636 -> 20636 bytes
-rw-r--r--syweb/webclient/media/ringback.mp3 (renamed from webclient/media/ringback.mp3)bin18398 -> 18398 bytes
-rw-r--r--syweb/webclient/media/ringback.ogg (renamed from webclient/media/ringback.ogg)bin8352 -> 8352 bytes
-rw-r--r--syweb/webclient/mobile.css (renamed from webclient/mobile.css)0
-rw-r--r--syweb/webclient/recents/recents-controller.js (renamed from webclient/recents/recents-controller.js)7
-rw-r--r--syweb/webclient/recents/recents-filter.js (renamed from webclient/recents/recents-filter.js)23
-rw-r--r--syweb/webclient/recents/recents.html (renamed from webclient/recents/recents.html)14
-rw-r--r--syweb/webclient/room/room-controller.js (renamed from webclient/room/room-controller.js)53
-rw-r--r--syweb/webclient/room/room-directive.js (renamed from webclient/room/room-directive.js)0
-rw-r--r--syweb/webclient/room/room.html (renamed from webclient/room/room.html)51
-rw-r--r--syweb/webclient/settings/settings-controller.js (renamed from webclient/settings/settings-controller.js)0
-rw-r--r--syweb/webclient/settings/settings.html (renamed from webclient/settings/settings.html)0
-rw-r--r--syweb/webclient/test/README (renamed from webclient/test/README)30
-rw-r--r--syweb/webclient/test/e2e/home.spec.js (renamed from webclient/test/e2e/home.spec.js)0
-rw-r--r--syweb/webclient/test/karma.conf.js (renamed from webclient/test/karma.conf.js)31
-rw-r--r--syweb/webclient/test/protractor.conf.js (renamed from webclient/test/protractor.conf.js)0
-rw-r--r--syweb/webclient/test/unit/event-handler-service.spec.js117
-rw-r--r--syweb/webclient/test/unit/filters.spec.js488
-rw-r--r--syweb/webclient/test/unit/matrix-service.spec.js504
-rw-r--r--syweb/webclient/test/unit/model-service.spec.js30
-rw-r--r--syweb/webclient/test/unit/register-controller.spec.js84
-rw-r--r--syweb/webclient/test/unit/user-controller.spec.js (renamed from webclient/test/unit/user-controller.spec.js)0
-rw-r--r--syweb/webclient/user/user-controller.js (renamed from webclient/user/user-controller.js)0
-rw-r--r--syweb/webclient/user/user.html (renamed from webclient/user/user.html)0
-rw-r--r--webclient/components/matrix/matrix-filter.js146
87 files changed, 2155 insertions, 744 deletions
diff --git a/.gitignore b/.gitignore
index b91b52b615..bd37f92dc4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,7 +24,7 @@ graph/*.svg
 graph/*.png
 graph/*.dot
 
-webclient/config.js
+**/webclient/config.js
 webclient/test/environment-protractor.js
 
 uploads
diff --git a/MANIFEST.in b/MANIFEST.in
index 73e0eff6e4..a1a77ff540 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,3 +1,4 @@
 recursive-include docs *
 recursive-include tests *.py
-recursive-include synapse/persistence/schema *.sql
+recursive-include synapse/storage/schema *.sql
+recursive-include syweb/webclient *
diff --git a/README.rst b/README.rst
index f40492b8a0..b3b2a94dbf 100644
--- a/README.rst
+++ b/README.rst
@@ -122,12 +122,12 @@ Thanks for trying Matrix!
 
 [2] End-to-end encryption is currently in development
 
-
 Homeserver Installation
 =======================
 
-First, the dependencies need to be installed.  Start by installing
-'python2.7-dev' and the various tools of the compiler toolchain.
+Synapse is written in python but some of the libraries is uses are written in
+C. So before we can install synapse itself we need a working C compiler and the
+header files for python C extensions.
 
 Installing prerequisites on Ubuntu::
 
@@ -137,29 +137,34 @@ Installing prerequisites on Mac OS X::
 
     $ xcode-select --install
 
-The homeserver has a number of external dependencies, that are easiest
-to install by making setup.py do so, in --user mode::
+Synapse uses NaCl (http://nacl.cr.yp.to/) for encryption and digital
+signatures. Unfortunately PyNACL currently has a few issues
+(https://github.com/pyca/pynacl/issues/53) and
+(https://github.com/pyca/pynacl/issues/79) that mean it may not install
+correctly. To fix try re-installing from PyPI or directly from (https://github.com/pyca/pynacl)::
 
-    $ python setup.py develop --user
+    $ # Install from PyPI
+    $ pip install --user --upgrade --force pynacl
+    $ # Install from github
+    $ pip install --user https://github.com/pyca/pynacl/tarball/master
 
-You'll need a version of setuptools new enough to know about git, so you
-may need to also run::
+On OSX, if you encounter ``clang: error: unknown argument: '-mno-fused-madd'``
+you will need to ``export CFLAGS=-Qunused-arguments``.
 
-    $ sudo apt-get install python-pip
-    $ sudo pip install --upgrade setuptools
+To install the synapse homeserver run::
 
-If you don't have access to github, then you may need to install ``syutil``
-manually by checking it out and running ``python setup.py develop --user`` on
-it too.
+    $ pip install --user --process-dependency-links https://github.com/matrix-org/synapse/tarball/master
 
-If you get errors about ``sodium.h`` being missing, you may also need to
-manually install a newer PyNaCl via pip as setuptools installs an old one. Or
-you can check PyNaCl out of git directly (https://github.com/pyca/pynacl) and
-installing it. Installing PyNaCl using pip may also work (remember to remove
-any other versions installed by setuputils in, for example, ~/.local/lib).
+This installs synapse, along with the libraries it uses, into
+``$HOME/.local/lib/``.
 
-On OSX, if you encounter ``clang: error: unknown argument: '-mno-fused-madd'``
-you will need to ``export CFLAGS=-Qunused-arguments``.
+Homeserver Development
+======================
+
+The homeserver has a number of external dependencies, that are easiest
+to install by making setup.py do so, in --user mode::
+
+    $ python setup.py develop --user
 
 This will run a process of downloading and installing into your
 user's .local/lib directory all of the required dependencies that are
@@ -204,11 +209,11 @@ IDs:
 For the first form, simply pass the required hostname (of the machine) as the
 --host parameter::
 
-    $ python synapse/app/homeserver.py \
+    $ python -m synapse.app.homeserver \
         --server-name machine.my.domain.name \
         --config-path homeserver.config \
         --generate-config
-    $ python synapse/app/homeserver.py --config-path homeserver.config
+    $ python -m synapse.app.homeserver --config-path homeserver.config
 
 Alternatively, you can run synapse via synctl - running ``synctl start`` to
 generate a homeserver.yaml config file, where you can then edit server-name to
@@ -226,12 +231,12 @@ record would then look something like::
 At this point, you should then run the homeserver with the hostname of this
 SRV record, as that is the name other machines will expect it to have::
 
-    $ python synapse/app/homeserver.py \
+    $ python -m synapse.app.homeserver \
         --server-name YOURDOMAIN \
         --bind-port 8448 \
         --config-path homeserver.config \
         --generate-config
-    $ python synapse/app/homeserver.py --config-path homeserver.config
+    $ python -m synapse.app.homeserver --config-path homeserver.config
 
 
 You may additionally want to pass one or more "-v" options, in order to
diff --git a/demo/start.sh b/demo/start.sh
index 0530f0a26e..886d21cfa8 100755
--- a/demo/start.sh
+++ b/demo/start.sh
@@ -41,6 +41,6 @@ for port in 8080 8081 8082; do
 done
 
 echo "Starting webclient on port 8000..."
-python "demo/webserver.py" -p 8000 -P "$DIR/webserver.pid" "webclient"
+python "demo/webserver.py" -p 8000 -P "$DIR/webserver.pid" "syweb/webclient"
 
 cd "$CWD"
diff --git a/setup.py b/setup.py
index 660efd5b89..f5976cd762 100755
--- a/setup.py
+++ b/setup.py
@@ -28,7 +28,7 @@ def read(fname):
 setup(
     name="SynapseHomeServer",
     version="0.0.1",
-    packages=find_packages(exclude=["tests"]),
+    packages=find_packages(exclude=["tests", "tests.*"]),
     description="Reference Synapse Home Server",
     install_requires=[
         "syutil==0.0.2",
@@ -43,6 +43,7 @@ setup(
     ],
     dependency_links=[
         "https://github.com/matrix-org/syutil/tarball/v0.0.2#egg=syutil-0.0.2",
+        "https://github.com/pyca/pynacl/tarball/52dbe2dc33f1#egg=pynacl-0.3.0",
     ],
     setup_requires=[
         "setuptools_trial",
diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py
index a20376b9d6..43164c8d67 100755
--- a/synapse/app/homeserver.py
+++ b/synapse/app/homeserver.py
@@ -42,6 +42,7 @@ import os
 import re
 import sys
 import sqlite3
+import syweb
 
 logger = logging.getLogger(__name__)
 
@@ -58,7 +59,9 @@ class SynapseHomeServer(HomeServer):
         return JsonResource()
 
     def build_resource_for_web_client(self):
-        return File("webclient")  # TODO configurable?
+        syweb_path = os.path.dirname(syweb.__file__)
+        webclient_path = os.path.join(syweb_path, "webclient")
+        return File(webclient_path)  # TODO configurable?
 
     def build_resource_for_content_repo(self):
         return ContentRepoResource(
diff --git a/synapse/rest/room.py b/synapse/rest/room.py
index 997895dab0..5c9c9d3af4 100644
--- a/synapse/rest/room.py
+++ b/synapse/rest/room.py
@@ -148,7 +148,7 @@ class RoomStateEventRestServlet(RestServlet):
         content = _parse_json(request)
 
         event = self.event_factory.create_event(
-            etype=event_type,
+            etype=urllib.unquote(event_type),
             content=content,
             room_id=urllib.unquote(room_id),
             user_id=user.to_string(),
@@ -182,7 +182,7 @@ class RoomSendEventRestServlet(RestServlet):
         content = _parse_json(request)
 
         event = self.event_factory.create_event(
-            etype=event_type,
+            etype=urllib.unquote(event_type),
             room_id=urllib.unquote(room_id),
             user_id=user.to_string(),
             content=content
@@ -458,7 +458,7 @@ class RoomRedactEventRestServlet(RestServlet):
             room_id=urllib.unquote(room_id),
             user_id=user.to_string(),
             content=content,
-            redacts=event_id,
+            redacts=urllib.unquote(event_id),
         )
 
         msg_handler = self.handlers.message_handler
diff --git a/syweb/__init__.py b/syweb/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/syweb/__init__.py
diff --git a/webclient/CAPTCHA_SETUP b/syweb/webclient/CAPTCHA_SETUP
index ebc8a5f3b0..ebc8a5f3b0 100644
--- a/webclient/CAPTCHA_SETUP
+++ b/syweb/webclient/CAPTCHA_SETUP
diff --git a/webclient/README b/syweb/webclient/README
index ef79b25708..ef79b25708 100644
--- a/webclient/README
+++ b/syweb/webclient/README
diff --git a/webclient/app-controller.js b/syweb/webclient/app-controller.js
index e4b7cd286f..2d82a42cf8 100644
--- a/webclient/app-controller.js
+++ b/syweb/webclient/app-controller.js
@@ -21,8 +21,8 @@ limitations under the License.
 'use strict';
 
 angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'eventStreamService'])
-.controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', '$timeout', '$animate', 'matrixService', 'mPresence', 'eventStreamService', 'eventHandlerService', 'matrixPhoneService',
-                               function($scope, $location, $rootScope, $timeout, $animate, matrixService, mPresence, eventStreamService, eventHandlerService, matrixPhoneService) {
+.controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', '$timeout', '$animate', 'matrixService', 'mPresence', 'eventStreamService', 'eventHandlerService', 'matrixPhoneService', 'modelService',
+                               function($scope, $location, $rootScope, $timeout, $animate, matrixService, mPresence, eventStreamService, eventHandlerService, matrixPhoneService, modelService) {
          
     // Check current URL to avoid to display the logout button on the login page
     $scope.location = $location.path();
@@ -117,7 +117,7 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
             return;
         }
 
-        var roomMembers = angular.copy($rootScope.events.rooms[$rootScope.currentCall.room_id].members);
+        var roomMembers = angular.copy(modelService.getRoom($rootScope.currentCall.room_id).current_room_state.members);
         delete roomMembers[matrixService.config().user_id];
 
         $rootScope.currentCall.user_id = Object.keys(roomMembers)[0];
diff --git a/webclient/app-directive.js b/syweb/webclient/app-directive.js
index c1ba0af3a9..c1ba0af3a9 100644
--- a/webclient/app-directive.js
+++ b/syweb/webclient/app-directive.js
diff --git a/webclient/app-filter.js b/syweb/webclient/app-filter.js
index f19db4141d..65da0d312d 100644
--- a/webclient/app-filter.js
+++ b/syweb/webclient/app-filter.js
@@ -29,10 +29,10 @@ angular.module('matrixWebClient')
             return s + "s";
         }
         if (t < 60 * 60) {
-            return m + "m "; //  + s + "s";
+            return m + "m"; //  + s + "s";
         }
         if (t < 24 * 60 * 60) {
-            return h + "h "; // + m + "m";
+            return h + "h"; // + m + "m";
         }
         return d + "d "; // + h + "h";
     };
@@ -76,17 +76,6 @@ angular.module('matrixWebClient')
         return filtered;
     };
 })
-.filter('stateEventsFilter', function($sce) {
-    return function(events) {
-        var filtered = {};
-        angular.forEach(events, function(value, key) {
-            if (value && typeof(value.state_key) === "string") {
-                filtered[key] = value;
-            }
-        });
-        return filtered;
-    };
-})
 .filter('unsafe', ['$sce', function($sce) {
     return function(text) {
         return $sce.trustAsHtml(text);
diff --git a/webclient/app.css b/syweb/webclient/app.css
index 5ab8e2b8fd..5ab8e2b8fd 100755
--- a/webclient/app.css
+++ b/syweb/webclient/app.css
diff --git a/webclient/app.js b/syweb/webclient/app.js
index c091f8c6cf..17b2bb6e8f 100644
--- a/webclient/app.js
+++ b/syweb/webclient/app.js
@@ -31,6 +31,7 @@ var matrixWebClient = angular.module('matrixWebClient', [
     'eventStreamService',
     'eventHandlerService',
     'notificationService',
+    'modelService',
     'infinite-scroll',
     'ui.bootstrap',
     'monospaced.elastic'
diff --git a/webclient/bootstrap.css b/syweb/webclient/bootstrap.css
index 7ebcb2a007..7ebcb2a007 100644
--- a/webclient/bootstrap.css
+++ b/syweb/webclient/bootstrap.css
diff --git a/webclient/components/fileInput/file-input-directive.js b/syweb/webclient/components/fileInput/file-input-directive.js
index 9c849a140f..9c849a140f 100644
--- a/webclient/components/fileInput/file-input-directive.js
+++ b/syweb/webclient/components/fileInput/file-input-directive.js
diff --git a/webclient/components/fileUpload/file-upload-service.js b/syweb/webclient/components/fileUpload/file-upload-service.js
index e0f67b2c6c..b544e29509 100644
--- a/webclient/components/fileUpload/file-upload-service.js
+++ b/syweb/webclient/components/fileUpload/file-upload-service.js
@@ -64,7 +64,8 @@ angular.module('mFileUpload', ['matrixService', 'mUtilities'])
         var imageMessage = {
             msgtype: "m.image",
             url: undefined,
-            body: {
+            body: "Image",
+            info: {
                 size: undefined,
                 w: undefined,
                 h: undefined,
@@ -90,7 +91,7 @@ angular.module('mFileUpload', ['matrixService', 'mUtilities'])
                         function(url) {
                             // Update message metadata
                             imageMessage.url = url;
-                            imageMessage.body = {
+                            imageMessage.info = {
                                 size: imageFile.size,
                                 w: size.width,
                                 h: size.height,
@@ -101,7 +102,7 @@ angular.module('mFileUpload', ['matrixService', 'mUtilities'])
                             // reuse the original image info for thumbnail data
                             if (!imageMessage.thumbnail_url) {
                                 imageMessage.thumbnail_url = imageMessage.url;
-                                imageMessage.thumbnail_info = imageMessage.body;
+                                imageMessage.thumbnail_info = imageMessage.info;
                             }
 
                             // We are done
diff --git a/webclient/components/matrix/event-handler-service.js b/syweb/webclient/components/matrix/event-handler-service.js
index e63584510b..a9c6eb34c7 100644
--- a/webclient/components/matrix/event-handler-service.js
+++ b/syweb/webclient/components/matrix/event-handler-service.js
@@ -22,13 +22,12 @@ not care where the event came from, it only needs enough context to be able to
 process them. Events may be coming from the event stream, the REST API (via 
 direct GETs or via a pagination stream API), etc.
 
-Typically, this service will store events or broadcast them to any listeners
-(e.g. controllers) via $broadcast. Alternatively, it may update the $rootScope
-if typically all the $on method would do is update its own $scope.
+Typically, this service will store events and broadcast them to any listeners
+(e.g. controllers) via $broadcast. 
 */
 angular.module('eventHandlerService', [])
-.factory('eventHandlerService', ['matrixService', '$rootScope', '$q', '$timeout', 'mPresence', 'notificationService',
-function(matrixService, $rootScope, $q, $timeout, mPresence, notificationService) {
+.factory('eventHandlerService', ['matrixService', '$rootScope', '$q', '$timeout', '$filter', 'mPresence', 'notificationService', 'modelService',
+function(matrixService, $rootScope, $q, $timeout, $filter, mPresence, notificationService, modelService) {
     var ROOM_CREATE_EVENT = "ROOM_CREATE_EVENT";
     var MSG_EVENT = "MSG_EVENT";
     var MEMBER_EVENT = "MEMBER_EVENT";
@@ -44,94 +43,113 @@ function(matrixService, $rootScope, $q, $timeout, mPresence, notificationService
     // of the app, given we never try to reap memory yet)
     var eventMap = {};
 
+    // TODO: Remove this and replace with modelService.User objects.
     $rootScope.presence = {};
 
     var initialSyncDeferred;
 
     var reset = function() {
         initialSyncDeferred = $q.defer();
-
-        $rootScope.events = {
-            rooms: {} // will contain roomId: { messages:[], members:{userid1: event} }
-        };
-
+        
         $rootScope.presence = {};
 
         eventMap = {};
     };
     reset();
 
-    var initRoom = function(room_id, room) {
-        if (!(room_id in $rootScope.events.rooms)) {
-            console.log("Creating new rooms entry for " + room_id);
-            $rootScope.events.rooms[room_id] = {
-                room_id: room_id,
-                messages: [],
-                members: {},
-                // Pagination information
-                pagination: {
-                    earliest_token: "END"   // how far back we've paginated
-                }
-            };
-        }
-
-        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 (!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];
-                }
-            }
-            $rootScope.events.rooms[room_id].membership = room.membership;
-        }
-    };
-
     var resetRoomMessages = function(room_id) {
-        if ($rootScope.events.rooms[room_id]) {
-            $rootScope.events.rooms[room_id].messages = [];
-        }
+        var room = modelService.getRoom(room_id);
+        room.events = [];
     };
     
     // Generic method to handle events data
-    var handleRoomDateEvent = function(event, isLiveEvent, addToRoomMessages) {
-        // Add topic changes as if they were a room message
+    var handleRoomStateEvent = function(event, isLiveEvent, addToRoomMessages) {
+        var room = modelService.getRoom(event.room_id);
         if (addToRoomMessages) {
-            if (isLiveEvent) {
-                $rootScope.events.rooms[event.room_id].messages.push(event);
-            }
-            else {
-                $rootScope.events.rooms[event.room_id].messages.unshift(event);
-            }
+            // some state events are displayed as messages, so add them.
+            room.addMessageEvent(event, !isLiveEvent);
         }
-
-        // live events always update, but non-live events only update if the
-        // ts is later.
-        var latestData = true;
-        if (!isLiveEvent) {
+        
+        if (isLiveEvent) {
+            // update the current room state with the latest state
+            room.current_room_state.storeStateEvent(event);
+        }
+        else {
             var eventTs = event.origin_server_ts;
-            var storedEvent = $rootScope.events.rooms[event.room_id][event.type];
+            var storedEvent = room.current_room_state.getStateEvent(event.type, event.state_key);
             if (storedEvent) {
-                if (storedEvent.origin_server_ts > eventTs) {
-                    // ignore it, we have a newer one already.
-                    latestData = false;
+                if (storedEvent.origin_server_ts < eventTs) {
+                    // the incoming event is newer, use it.
+                    room.current_room_state.storeStateEvent(event);
                 }
             }
         }
-        if (latestData) {
-            $rootScope.events.rooms[event.room_id][event.type] = event;         
-        }
+        // TODO: handle old_room_state
     };
     
     var handleRoomCreate = function(event, isLiveEvent) {
-        // For now, we do not use the event data. Simply signal it to the app controllers
         $rootScope.$broadcast(ROOM_CREATE_EVENT, event, isLiveEvent);
     };
 
     var handleRoomAliases = function(event, isLiveEvent) {
         matrixService.createRoomIdToAliasMapping(event.room_id, event.content.aliases[0]);
     };
+    
+    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
+            );
+
+            // Ideally we would notify only when the window is hidden (i.e. document.hidden = true).
+            //
+            // However, Chrome on Linux and OSX currently returns document.hidden = false unless the window is
+            // explicitly showing a different tab.  So we need another metric to determine hiddenness - we
+            // simply use idle time.  If the user has been idle enough that their presence goes to idle, then
+            // we also display notifs when things happen.
+            //
+            // This is far far better than notifying whenever anything happens anyway, otherwise you get spammed
+            // to death with notifications when the window is in the foreground, which is horrible UX (especially
+            // if you have not defined any bingers and so get notified for everything).
+            var isIdle = (document.hidden || matrixService.presence.unavailable === mPresence.getState());
+            
+            // We need a way to let people get notifications for everything, if they so desire.  The way to do this
+            // is to specify zero bingwords.
+            var bingWords = matrixService.config().bingWords;
+            if (bingWords === undefined || bingWords.length === 0) {
+                shouldBing = true;
+            }
+            
+            if (shouldBing && isIdle) {
+                console.log("Displaying notification for "+JSON.stringify(event));
+                var member = modelService.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") {
+                    message = "* " + displayname + " " + message;
+                }
+                else if (event.content.msgtype === "m.image") {
+                    message = displayname + " sent an image.";
+                }
+                
+                var roomTitle = $filter("mRoomName")(event.room_id);
+                
+                notificationService.showNotification(
+                    displayname + " (" + roomTitle + ")",
+                    message,
+                    member ? member.event.content.avatar_url : undefined,
+                    function() {
+                        console.log("notification.onclick() room=" + event.room_id);
+                        $rootScope.goToPage('room/' + event.room_id); 
+                    }
+                );
+            }
+        }
+    };
 
     var handleMessage = function(event, isLiveEvent) {
         // Check for empty event content
@@ -144,136 +162,80 @@ function(matrixService, $rootScope, $q, $timeout, mPresence, notificationService
             // empty json object is a redacted event, so ignore.
             return;
         }
-
-        if (isLiveEvent) {
-            if (event.user_id === matrixService.config().user_id &&
-                (event.content.msgtype === "m.text" || event.content.msgtype === "m.emote") ) {
-                // Assume we've already echoed it. So, there is a fake event in the messages list of the room
-                // Replace this fake event by the true one
-                var index = getRoomEventIndex(event.room_id, event.event_id);
-                if (index) {
-                    $rootScope.events.rooms[event.room_id].messages[index] = event;
-                }
-                else {
-                    $rootScope.events.rooms[event.room_id].messages.push(event);
-                }
-            }
-            else {
-                $rootScope.events.rooms[event.room_id].messages.push(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
-                );
-
-                // Ideally we would notify only when the window is hidden (i.e. document.hidden = true).
-                //
-                // However, Chrome on Linux and OSX currently returns document.hidden = false unless the window is
-                // explicitly showing a different tab.  So we need another metric to determine hiddenness - we
-                // simply use idle time.  If the user has been idle enough that their presence goes to idle, then
-                // we also display notifs when things happen.
-                //
-                // This is far far better than notifying whenever anything happens anyway, otherwise you get spammed
-                // to death with notifications when the window is in the foreground, which is horrible UX (especially
-                // if you have not defined any bingers and so get notified for everything).
-                var isIdle = (document.hidden || matrixService.presence.unavailable === mPresence.getState());
-                
-                // We need a way to let people get notifications for everything, if they so desire.  The way to do this
-                // is to specify zero bingwords.
-                var bingWords = matrixService.config().bingWords;
-                if (bingWords === undefined || bingWords.length === 0) {
-                    shouldBing = true;
-                }
-                
-                if (shouldBing && isIdle) {
-                    console.log("Displaying notification for "+JSON.stringify(event));
-                    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") {
-                        message = "* " + displayname + " " + message;
-                    }
-                    else if (event.content.msgtype === "m.image") {
-                        message = displayname + " sent an image.";
-                    }
-
-                    var roomTitle = matrixService.getRoomIdToAliasMapping(event.room_id);
-                    var theRoom = $rootScope.events.rooms[event.room_id];
-                    if (!roomTitle && theRoom && theRoom["m.room.name"] && theRoom["m.room.name"].content) {
-                        roomTitle = theRoom["m.room.name"].content.name;
-                    }
-
-                    if (!roomTitle) {
-                        roomTitle = event.room_id;
-                    }
-                    
-                    notificationService.showNotification(
-                        displayname + " (" + roomTitle + ")",
-                        message,
-                        member ? member.avatar_url : undefined,
-                        function() {
-                            console.log("notification.onclick() room=" + event.room_id);
-                            $rootScope.goToPage('room/' + event.room_id); 
-                        }
-                    );
-                }
-            }
+        
+        // =======================
+        
+        var room = modelService.getRoom(event.room_id);
+        
+        if (event.user_id !== matrixService.config().user_id) {
+            room.addMessageEvent(event, !isLiveEvent);
+            displayNotification(event);
         }
         else {
-            $rootScope.events.rooms[event.room_id].messages.unshift(event);
+            // we may have locally echoed this, so we should replace the event
+            // instead of just adding.
+            room.addOrReplaceMessageEvent(event, !isLiveEvent);
         }
         
         // TODO send delivery receipt if isLiveEvent
         
-        // $broadcast this, as controllers may want to do funky things such as
-        // scroll to the bottom, etc which cannot be expressed via simple $scope
-        // updates.
         $rootScope.$broadcast(MSG_EVENT, event, isLiveEvent);
     };
     
     var handleRoomMember = function(event, isLiveEvent, isStateEvent) {
+        var room = modelService.getRoom(event.room_id);
         
-        // add membership changes as if they were a room message if something interesting changed
-        // Exception: Do not do this if the event is a room state event because such events already come
-        // as room messages events. Moreover, when they come as room messages events, they are relatively ordered
-        // with other other room messages
+        // did something change?
+        var memberChanges = undefined;
         if (!isStateEvent) {
             // could be a membership change, display name change, etc.
             // Find out which one.
-            var memberChanges = undefined;
             if ((event.prev_content === undefined && event.content.membership) || (event.prev_content && (event.prev_content.membership !== event.content.membership))) {
                 memberChanges = "membership";
             }
             else if (event.prev_content && (event.prev_content.displayname !== event.content.displayname)) {
                 memberChanges = "displayname";
             }
-
             // mark the key which changed
             event.changedKey = memberChanges;
-
-            // If there was a change we want to display, dump it in the message
-            // list.
-            if (memberChanges) {
-                if (isLiveEvent) {
-                    $rootScope.events.rooms[event.room_id].messages.push(event);
-                }
-                else {
-                    $rootScope.events.rooms[event.room_id].messages.unshift(event);
-                }
-            }
         }
         
-        // Use data from state event or the latest data from the stream.
-        // Do not care of events that come when paginating back
+        
+        // modify state before adding the message so it points to the right thing.
+        // The events are copied to avoid referencing the same event when adding
+        // the message (circular json structures)
         if (isStateEvent || isLiveEvent) {
-            $rootScope.events.rooms[event.room_id].members[event.state_key] = event;
+            var newEvent = angular.copy(event);
+            newEvent.cnt = event.content;
+            room.current_room_state.storeStateEvent(newEvent);
+        }
+        else if (!isLiveEvent) {
+            // mutate the old room state
+            var oldEvent = angular.copy(event);
+            oldEvent.cnt = event.content;
+            if (event.prev_content) {
+                // the m.room.member event we are handling is the NEW event. When
+                // we keep going back in time, we want the PREVIOUS value for displaying
+                // names/etc, hence the clobber here.
+                oldEvent.cnt = event.prev_content;
+            }
+            
+            if (event.changedKey === "membership" && event.content.membership === "join") {
+                // join has a prev_content but it doesn't contain all the info unlike the join, so use that.
+                oldEvent.cnt = event.content;
+            }
+            
+            room.old_room_state.storeStateEvent(oldEvent);
+        }
+        
+        // If there was a change we want to display, dump it in the message
+        // list. This has to be done after room state is updated.
+        if (memberChanges) {
+            room.addMessageEvent(event, !isLiveEvent);
         }
         
+        
+        
         $rootScope.$broadcast(MEMBER_EVENT, event, isLiveEvent, isStateEvent);
     };
     
@@ -283,30 +245,28 @@ function(matrixService, $rootScope, $q, $timeout, mPresence, notificationService
     };
     
     var handlePowerLevels = function(event, isLiveEvent) {
-        // 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);   
-        }
+        handleRoomStateEvent(event, isLiveEvent);
+        $rootScope.$broadcast(POWERLEVEL_EVENT, event, isLiveEvent);   
     };
 
     var handleRoomName = function(event, isLiveEvent, isStateEvent) {
         console.log("handleRoomName room_id: " + event.room_id + " - isLiveEvent: " + isLiveEvent + " - name: " + event.content.name);
-        handleRoomDateEvent(event, isLiveEvent, !isStateEvent);
+        handleRoomStateEvent(event, isLiveEvent, !isStateEvent);
         $rootScope.$broadcast(NAME_EVENT, event, isLiveEvent);
     };
     
 
     var handleRoomTopic = function(event, isLiveEvent, isStateEvent) {
         console.log("handleRoomTopic room_id: " + event.room_id + " - isLiveEvent: " + isLiveEvent + " - topic: " + event.content.topic);
-        handleRoomDateEvent(event, isLiveEvent, !isStateEvent);
+        handleRoomStateEvent(event, isLiveEvent, !isStateEvent);
         $rootScope.$broadcast(TOPIC_EVENT, event, isLiveEvent);
     };
 
     var handleCallEvent = function(event, isLiveEvent) {
         $rootScope.$broadcast(CALL_EVENT, event, isLiveEvent);
         if (event.type === 'm.call.invite') {
-            $rootScope.events.rooms[event.room_id].messages.push(event);
+            var room = modelService.getRoom(event.room_id);
+            room.addMessageEvent(event, !isLiveEvent);
         }
     };
 
@@ -320,8 +280,9 @@ function(matrixService, $rootScope, $q, $timeout, mPresence, notificationService
         // we need to remove something possibly: do we know the redacted
         // event ID?
         if (eventMap[event.redacts]) {
+            var room = modelService.getRoom(event.room_id);
             // remove event from list of messages in this room.
-            var eventList = $rootScope.events.rooms[event.room_id].messages;
+            var eventList = room.events;
             for (var i=0; i<eventList.length; i++) {
                 if (eventList[i].event_id === event.redacts) {
                     console.log("Removing event " + event.redacts);
@@ -330,50 +291,9 @@ function(matrixService, $rootScope, $q, $timeout, mPresence, notificationService
                 }
             }
 
-            // broadcast the redaction so controllers can nuke this
             console.log("Redacted an event.");
         }
     }
-    
-    /**
-     * Get the index of the event in $rootScope.events.rooms[room_id].messages
-     * @param {type} room_id the room id
-     * @param {type} event_id the event id to look for
-     * @returns {Number | undefined} the index. undefined if not found.
-     */
-    var getRoomEventIndex = function(room_id, event_id) {
-        var index;
-
-        var room = $rootScope.events.rooms[room_id];
-        if (room) {
-            // Start looking from the tail since the first goal of this function 
-            // is to find a messaged among the latest ones
-            for (var i = room.messages.length - 1; i > 0; i--) {
-                var message = room.messages[i];
-                if (event_id === message.event_id) {
-                    index = i;
-                    break;
-                }
-            }
-        }
-        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
@@ -385,17 +305,20 @@ function(matrixService, $rootScope, $q, $timeout, mPresence, notificationService
         var displayName;
 
         // Get the user display name from the member list of the room
-        var member = getMember(room_id, user_id);
+        var member = modelService.getMember(room_id, user_id);
+        if (member) {
+            member = member.event;
+        }
         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];
+                var room = modelService.getRoom(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];
+                for (var member_id in room.current_room_state.members) {
+                    if (room.current_room_state.members.hasOwnProperty(member_id) && member_id !== user_id) {
+                        var member2 = room.current_room_state.members[member_id].event;
                         if (member2.content.displayname && member2.content.displayname === displayName) {
                             displayName = displayName + " (" + user_id + ")";
                             break;
@@ -433,19 +356,9 @@ function(matrixService, $rootScope, $q, $timeout, mPresence, notificationService
             reset();
             $rootScope.$broadcast(RESET_EVENT);
         },
-        
-        initRoom: function(room) {
-            initRoom(room.room_id, room);
-        },
     
         handleEvent: function(event, isLiveEvent, isStateEvent) {
 
-            // FIXME: /initialSync on a particular room is not yet available
-            // So initRoom on a new room is not called. Make sure the room data is initialised here
-            if (event.room_id) {
-                initRoom(event.room_id);
-            }
-
             // Avoid duplicated events
             // Needed for rooms where initialSync has not been done. 
             // In this case, we do not know where to start pagination. So, it starts from the END
@@ -504,11 +417,11 @@ function(matrixService, $rootScope, $q, $timeout, mPresence, notificationService
                         // displays on the Room Info screen.
                         if (typeof(event.state_key) === "string") { // incls. 0-len strings
                             if (event.room_id) {
-                                handleRoomDateEvent(event, isLiveEvent, false);
+                                handleRoomStateEvent(event, isLiveEvent, false);
                             }
                         }
                         console.log("Unable to handle event type " + event.type);
-                        console.log(JSON.stringify(event, undefined, 4));
+                        // console.log(JSON.stringify(event, undefined, 4));
                         break;
                 }
             }
@@ -524,8 +437,6 @@ function(matrixService, $rootScope, $q, $timeout, mPresence, notificationService
 
         // Handle messages from /initialSync or /messages
         handleRoomMessages: function(room_id, messages, isLiveEvents, dir) {
-            initRoom(room_id);
-
             var events = messages.chunk;
 
             // Handles messages according to their time order
@@ -536,21 +447,67 @@ function(matrixService, $rootScope, $q, $timeout, mPresence, notificationService
                 }
                 
                 // Store how far back we've paginated
-                $rootScope.events.rooms[room_id].pagination.earliest_token = messages.end;
+                var room = modelService.getRoom(room_id);
+                room.old_room_state.pagination_token = messages.end;
+
             }
             else {
-                // InitialSync returns messages in chronological order
+                // InitialSync returns messages in chronological order, so invert
+                // it to get most recent > oldest
                 for (var i=events.length - 1; i>=0; i--) {
                     this.handleEvent(events[i], isLiveEvents, isLiveEvents);
                 }
                 // Store where to start pagination
-                $rootScope.events.rooms[room_id].pagination.earliest_token = messages.start;
+                var room = modelService.getRoom(room_id);
+                room.old_room_state.pagination_token = messages.start;
             }
         },
 
-        handleInitialSyncDone: function(initialSyncData) {
+        handleInitialSyncDone: function(response) {
             console.log("# handleInitialSyncDone");
-            initialSyncDeferred.resolve(initialSyncData);
+
+            var rooms = response.data.rooms;
+            for (var i = 0; i < rooms.length; ++i) {
+                var room = rooms[i];
+                
+                // FIXME: This is ming: the HS should be sending down the m.room.member
+                // event for the invite in .state but it isn't, so fudge it for now.
+                if (room.inviter && room.membership === "invite") {
+                    var me = matrixService.config().user_id;
+                    var fakeEvent = {
+                        event_id: "__FAKE__" + room.room_id,
+                        user_id: room.inviter,
+                        origin_server_ts: 0,
+                        room_id: room.room_id,
+                        state_key: me,
+                        type: "m.room.member",
+                        content: {
+                            membership: "invite"
+                        }
+                    };
+                    if (!room.state) {
+                        room.state = [];
+                    }
+                    room.state.push(fakeEvent);
+                    console.log("RECV /initialSync invite >> "+room.room_id);
+                }
+                
+                var newRoom = modelService.getRoom(room.room_id);
+                newRoom.current_room_state.storeStateEvents(room.state);
+                newRoom.old_room_state.storeStateEvents(room.state);
+
+                // this should be done AFTER storing state events since these
+                // messages may make the old_room_state diverge.
+                if ("messages" in room) {
+                    this.handleRoomMessages(room.room_id, room.messages, false);
+                    newRoom.current_room_state.pagination_token = room.messages.end;
+                    newRoom.old_room_state.pagination_token = room.messages.start;
+                }
+            }
+            var presence = response.data.presence;
+            this.handleEvents(presence, false);
+
+            initialSyncDeferred.resolve(response);
         },
 
         // Returns a promise that resolves when the initialSync request has been processed
@@ -571,15 +528,13 @@ function(matrixService, $rootScope, $q, $timeout, mPresence, notificationService
         getLastMessage: function(room_id, filterEcho) {
             var lastMessage;
 
-            var room = $rootScope.events.rooms[room_id];
-            if (room) {
-                for (var i = room.messages.length - 1; i >= 0; i--) {
-                    var message = room.messages[i];
+            var events = modelService.getRoom(room_id).events;
+            for (var i = events.length - 1; i >= 0; i--) {
+                var message = events[i];
 
-                    if (!filterEcho || undefined === message.echo_msg_state) {
-                        lastMessage = message;
-                        break;
-                    }
+                if (!filterEcho || undefined === message.echo_msg_state) {
+                    lastMessage = message;
+                    break;
                 }
             }
 
@@ -594,18 +549,15 @@ function(matrixService, $rootScope, $q, $timeout, mPresence, notificationService
         getUsersCountInRoom: function(room_id) {
             var memberCount;
 
-            var room = $rootScope.events.rooms[room_id];
-            if (room) {
-                memberCount = 0;
-
-                for (var i in room.members) {
-                    if (!room.members.hasOwnProperty(i)) continue;
+            var room = modelService.getRoom(room_id);
+            memberCount = 0;
+            for (var i in room.current_room_state.members) {
+                if (!room.current_room_state.members.hasOwnProperty(i)) continue;
 
-                    var member = room.members[i];
+                var member = room.current_room_state.members[i].event;
 
-                    if ("join" === member.membership) {
-                        memberCount = memberCount + 1;
-                    }
+                if ("join" === member.content.membership) {
+                    memberCount = memberCount + 1;
                 }
             }
 
@@ -613,13 +565,24 @@ function(matrixService, $rootScope, $q, $timeout, mPresence, notificationService
         },
         
         /**
-         * Get the member object of a room member
+         * Return the power level of an user in a particular room
          * @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
+         * @param {String} user_id the user id
+         * @returns {Number} a value between 0 and 10
          */
-        getMember: function(room_id, user_id) {
-            return getMember(room_id, user_id);
+        getUserPowerLevel: function(room_id, user_id) {
+            var powerLevel = 0;
+            var room = modelService.getRoom(room_id).current_room_state;
+            if (room.state("m.room.power_levels")) {
+                if (user_id in room.state("m.room.power_levels").content) {
+                    powerLevel = room.state("m.room.power_levels").content[user_id];
+                }
+                else {
+                    // Use the room default user power
+                    powerLevel = room.state("m.room.power_levels").content["default"];
+                }
+            }
+            return powerLevel;
         },
         
         /**
@@ -630,18 +593,6 @@ function(matrixService, $rootScope, $q, $timeout, mPresence, notificationService
          */
         getUserDisplayName: function(room_id, user_id) {
             return getUserDisplayName(room_id, user_id);
-        },
-
-        setRoomVisibility: function(room_id, visible) {
-            if (!visible) {
-                return;
-            }
-            initRoom(room_id);
-            
-            var room = $rootScope.events.rooms[room_id];
-            if (room) {
-                room.visibility = visible;
-            }
         }
     };
 }]);
diff --git a/webclient/components/matrix/event-stream-service.js b/syweb/webclient/components/matrix/event-stream-service.js
index 05469a3ded..c03f0b953b 100644
--- a/webclient/components/matrix/event-stream-service.js
+++ b/syweb/webclient/components/matrix/event-stream-service.js
@@ -109,25 +109,6 @@ angular.module('eventStreamService', [])
         // without requiring to make an additional request
         matrixService.initialSync(30, false).then(
             function(response) {
-                var rooms = response.data.rooms;
-                for (var i = 0; i < rooms.length; ++i) {
-                    var room = rooms[i];
-                    
-                    eventHandlerService.initRoom(room);
-
-                    if ("messages" in room) {
-                        eventHandlerService.handleRoomMessages(room.room_id, room.messages, false);
-                    }
-                    
-                    if ("state" in room) {
-                        eventHandlerService.handleEvents(room.state, false, true);
-                    }
-                }
-
-                var presence = response.data.presence;
-                eventHandlerService.handleEvents(presence, false);
-
-                // Initial sync is done
                 eventHandlerService.handleInitialSyncDone(response);
 
                 // Start event streaming from that point
diff --git a/webclient/components/matrix/matrix-call.js b/syweb/webclient/components/matrix/matrix-call.js
index 3e8811e5fc..c13083298e 100644
--- a/webclient/components/matrix/matrix-call.js
+++ b/syweb/webclient/components/matrix/matrix-call.js
@@ -40,14 +40,11 @@ window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConne
 window.RTCSessionDescription = window.RTCSessionDescription || window.webkitRTCSessionDescription || window.mozRTCSessionDescription;
 window.RTCIceCandidate = window.RTCIceCandidate || window.webkitRTCIceCandidate || window.mozRTCIceCandidate;
 
-// Returns true if the browser supports all required features to make WebRTC call
-var isWebRTCSupported = function () {
-    return !!(navigator.getUserMedia || window.RTCPeerConnection || window.RTCSessionDescription || window.RTCIceCandidate);
-};
-
 angular.module('MatrixCall', [])
-.factory('MatrixCall', ['matrixService', 'matrixPhoneService', '$rootScope', '$timeout', function MatrixCallFactory(matrixService, matrixPhoneService, $rootScope, $timeout) {
-    $rootScope.isWebRTCSupported = isWebRTCSupported();
+.factory('MatrixCall', ['matrixService', 'matrixPhoneService', 'modelService', '$rootScope', '$timeout', function MatrixCallFactory(matrixService, matrixPhoneService, modelService, $rootScope, $timeout) {
+    $rootScope.isWebRTCSupported = function () {
+        return !!(navigator.getUserMedia || window.RTCPeerConnection || window.RTCSessionDescription || window.RTCIceCandidate);
+    };
 
     var MatrixCall = function(room_id) {
         this.room_id = room_id;
@@ -213,8 +210,8 @@ angular.module('MatrixCall', [])
 
         var self = this;
 
-        var roomMembers = $rootScope.events.rooms[this.room_id].members;
-        if (roomMembers[matrixService.config().user_id].membership != 'join') {
+        var roomMembers = modelService.getRoom(this.room_id).current_room_state.members;
+        if (roomMembers[matrixService.config().user_id].event.content.membership != 'join') {
             console.log("We need to join the room before we can accept this call");
             matrixService.join(this.room_id).then(function() {
                 self.answer();
diff --git a/syweb/webclient/components/matrix/matrix-filter.js b/syweb/webclient/components/matrix/matrix-filter.js
new file mode 100644
index 0000000000..aeebedc784
--- /dev/null
+++ b/syweb/webclient/components/matrix/matrix-filter.js
@@ -0,0 +1,120 @@
+/*
+ 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';
+
+angular.module('matrixFilter', [])
+
+// Compute the room name according to information we have
+// TODO: It would be nice if this was stateless and had no dependencies. That would
+//       make the business logic here a lot easier to see.
+.filter('mRoomName', ['$rootScope', 'matrixService', 'eventHandlerService', 'modelService', 
+function($rootScope, matrixService, eventHandlerService, modelService) {
+    return function(room_id) {
+        var roomName;
+
+        // If there is an alias, use it
+        // TODO: only one alias is managed for now
+        var alias = matrixService.getRoomIdToAliasMapping(room_id);
+        var room = modelService.getRoom(room_id).current_room_state;
+        
+        var room_name_event = room.state("m.room.name");
+
+        // Determine if it is a public room
+        var isPublicRoom = false;
+        if (room.state("m.room.join_rules") && room.state("m.room.join_rules").content) {
+            isPublicRoom = ("public" === room.state("m.room.join_rules").content.join_rule);
+        }
+        
+        if (room_name_event) {
+            roomName = room_name_event.content.name;
+        }
+        else if (alias) {
+            roomName = alias;
+        }
+        else if (Object.keys(room.members).length > 0 && !isPublicRoom) { // Do not rename public room
+            var user_id = matrixService.config().user_id;
+            
+            // this is a "one to one" room and should have the name of the other user.
+            if (Object.keys(room.members).length === 2) {
+                for (var i in room.members) {
+                    if (!room.members.hasOwnProperty(i)) continue;
+
+                    var member = room.members[i].event;
+                    if (member.state_key !== user_id) {
+                        roomName = eventHandlerService.getUserDisplayName(room_id, member.state_key);
+                        if (!roomName) {
+                            roomName = member.state_key;
+                        }
+                        break;
+                    }
+                }
+            }
+            else if (Object.keys(room.members).length === 1) {
+                // this could be just us (self-chat) or could be the other person
+                // in a room if they have invited us to the room. Find out which.
+                var otherUserId = Object.keys(room.members)[0];
+                if (otherUserId === user_id) {
+                    // it's us, we may have been invited to this room or it could
+                    // be a self chat.
+                    if (room.members[otherUserId].event.content.membership === "invite") {
+                        // someone invited us, use the right ID.
+                        roomName = eventHandlerService.getUserDisplayName(room_id, room.members[otherUserId].event.user_id);
+                        if (!roomName) {
+                            roomName = room.members[otherUserId].event.user_id;
+                        }
+                    }
+                    else {
+                        roomName = eventHandlerService.getUserDisplayName(room_id, otherUserId);
+                        if (!roomName) {
+                            roomName = user_id;
+                        }
+                    }
+                }
+                else { // it isn't us, so use their name if we know it.
+                    roomName = eventHandlerService.getUserDisplayName(room_id, otherUserId);
+                    if (!roomName) {
+                        roomName = otherUserId;
+                    }
+                }
+            }
+            else if (Object.keys(room.members).length === 0) {
+                // this shouldn't be possible
+                console.error("0 members in room >> " + room_id);
+            }
+        }
+        
+
+        // Always show the alias in the room displayed name
+        if (roomName && alias && alias !== roomName) {
+            roomName += " (" + alias + ")";
+        }
+
+        if (undefined === roomName) {
+            // By default, use the room ID
+            roomName = room_id;
+        }
+
+        return roomName;
+    };
+}])
+
+// Return the user display name
+.filter('mUserDisplayName', ['eventHandlerService', function(eventHandlerService) {
+    return function(user_id, room_id) {
+        return eventHandlerService.getUserDisplayName(room_id, user_id);
+    };
+}]);
diff --git a/webclient/components/matrix/matrix-phone-service.js b/syweb/webclient/components/matrix/matrix-phone-service.js
index 06465ed821..55dbbf522e 100644
--- a/webclient/components/matrix/matrix-phone-service.js
+++ b/syweb/webclient/components/matrix/matrix-phone-service.js
@@ -60,7 +60,7 @@ angular.module('matrixPhoneService', [])
             var MatrixCall = $injector.get('MatrixCall');
             var call = new MatrixCall(event.room_id);
 
-            if (!isWebRTCSupported()) {
+            if (!$rootScope.isWebRTCSupported()) {
                 console.log("Incoming call ID "+msg.call_id+" but this browser doesn't support WebRTC");
                 // don't hang up the call: there could be other clients connected that do support WebRTC and declining the
                 // the call on their behalf would be really annoying.
diff --git a/webclient/components/matrix/matrix-service.js b/syweb/webclient/components/matrix/matrix-service.js
index 1840cf46c0..63051c4f47 100644
--- a/webclient/components/matrix/matrix-service.js
+++ b/syweb/webclient/components/matrix/matrix-service.js
@@ -267,7 +267,7 @@ angular.module('matrixService', [])
         
         // get room state for a specific room
         roomState: function(room_id) {
-            var path = "/rooms/" + room_id + "/state";
+            var path = "/rooms/" + encodeURIComponent(room_id) + "/state";
             return doRequest("GET", path);
         },
         
@@ -375,9 +375,11 @@ angular.module('matrixService', [])
         
         
         sendStateEvent: function(room_id, eventType, content, state_key) {
-            var path = "/rooms/$room_id/state/"+eventType;
+            var path = "/rooms/$room_id/state/"+ eventType;
+            // TODO: uncomment this when matrix.org is updated, else all state events 500.
+            // var path = "/rooms/$room_id/state/"+ encodeURIComponent(eventType);
             if (state_key !== undefined) {
-                path += "/" + state_key;
+                path += "/" + encodeURIComponent(state_key);
             }
             room_id = encodeURIComponent(room_id);
             path = path.replace("$room_id", room_id);
@@ -422,7 +424,8 @@ angular.module('matrixService', [])
             var content = {
                  msgtype: "m.image",
                  url: image_url,
-                 body: image_body
+                 info: image_body,
+                 body: "Image"
             };
 
             return this.sendMessage(room_id, msg_id, content);
@@ -440,7 +443,8 @@ angular.module('matrixService', [])
 
         redactEvent: function(room_id, event_id) {
             var path = "/rooms/$room_id/redact/$event_id";
-            path = path.replace("$room_id", room_id);
+            path = path.replace("$room_id", encodeURIComponent(room_id));
+            // TODO: encodeURIComponent when HS updated.
             path = path.replace("$event_id", event_id);
             var content = {};
             return doRequest("POST", path, undefined, content);
@@ -458,7 +462,7 @@ angular.module('matrixService', [])
         
         paginateBackMessages: function(room_id, from_token, limit) {
             var path = "/rooms/$room_id/messages";
-            path = path.replace("$room_id", room_id);
+            path = path.replace("$room_id", encodeURIComponent(room_id));
             var params = {
                 from: from_token,
                 limit: limit,
@@ -506,12 +510,12 @@ angular.module('matrixService', [])
 
         setProfileInfo: function(data, info_segment) {
             var path = "/profile/$user/" + info_segment;
-            path = path.replace("$user", config.user_id);
+            path = path.replace("$user", encodeURIComponent(config.user_id));
             return doRequest("PUT", path, undefined, data);
         },
 
         getProfileInfo: function(userId, info_segment) {
-            var path = "/profile/"+userId
+            var path = "/profile/"+encodeURIComponent(userId);
             if (info_segment) path += '/' + info_segment;
             return doRequest("GET", path);
         },
@@ -630,7 +634,7 @@ angular.module('matrixService', [])
         // Set the logged in user presence state
         setUserPresence: function(presence) {
             var path = "/presence/$user_id/status";
-            path = path.replace("$user_id", config.user_id);
+            path = path.replace("$user_id", encodeURIComponent(config.user_id));
             return doRequest("PUT", path, undefined, {
                 presence: presence
             });
@@ -724,57 +728,30 @@ angular.module('matrixService', [])
             //console.log("looking for roomId for " + alias + "; found: " + roomId);
             return roomId;
         },
-
-        /****** Power levels management ******/
-
-        /**
-         * Return the power level of an user in a particular room
-         * @param {String} room_id the room id
-         * @param {String} user_id the user id
-         * @returns {Number} a value between 0 and 10
-         */
-        getUserPowerLevel: function(room_id, user_id) {
-            var powerLevel = 0;
-            var room = $rootScope.events.rooms[room_id];
-            if (room && room["m.room.power_levels"]) {
-                if (user_id in room["m.room.power_levels"].content) {
-                    powerLevel = room["m.room.power_levels"].content[user_id];
-                }
-                else {
-                    // Use the room default user power
-                    powerLevel = room["m.room.power_levels"].content["default"];
-                }
-            }
-            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
+         * @param {Number} powerLevel The desired power level.
          *    If undefined, the user power level will be reset, ie he will use the default room user power level
+         * @param event The existing m.room.power_levels event if one exists.
          * @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;
+        setUserPowerLevel: function(room_id, user_id, powerLevel, event) {
+            var content = {};
+            if (event) {
+                // if there is an existing event, copy the content as it contains
+                // the power level values for other members which we do not want
+                // to modify.
+                content = angular.copy(event.content);
+            }
+            content[user_id] = powerLevel;
                 
-                var path = "/rooms/$room_id/state/m.room.power_levels";
-                path = path.replace("$room_id", encodeURIComponent(room_id));
+            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;
+            return doRequest("PUT", path, undefined, content);
         },
 
         getTurnServer: function() {
diff --git a/syweb/webclient/components/matrix/model-service.js b/syweb/webclient/components/matrix/model-service.js
new file mode 100644
index 0000000000..8e0ce8d1a9
--- /dev/null
+++ b/syweb/webclient/components/matrix/model-service.js
@@ -0,0 +1,172 @@
+/*
+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 serves as the entry point for all models in the app. If access to
+underlying data in a room is required, then this service should be used as the
+dependency.
+*/
+// NB: This is more explicit than linking top-level models to $rootScope
+//     in that by adding this service as a dep you are clearly saying "this X
+//     needs access to the underlying data store", rather than polluting the
+//     $rootScope.
+angular.module('modelService', [])
+.factory('modelService', ['matrixService', function(matrixService) {
+    
+    /***** Room Object *****/
+    var Room = function Room(room_id) {
+        this.room_id = room_id;
+        this.old_room_state = new RoomState();
+        this.current_room_state = new RoomState();
+        this.events = []; // events which can be displayed on the UI. TODO move?
+    };
+    Room.prototype = {
+        addMessageEvents: function addMessageEvents(events, toFront) {
+            for (var i=0; i<events.length; i++) {
+                this.addMessageEvent(events[i], toFront);
+            }
+        },
+        
+        addMessageEvent: function addMessageEvent(event, toFront) {
+            // every message must reference the RoomMember which made it *at
+            // that time* so things like display names display correctly.
+            var stateAtTheTime = toFront ? this.old_room_state : this.current_room_state;
+            event.__room_member = stateAtTheTime.getStateEvent("m.room.member", event.user_id);
+            if (event.type === "m.room.member" && event.content.membership === "invite") {
+                // give information on both the inviter and invitee
+                event.__target_room_member = stateAtTheTime.getStateEvent("m.room.member", event.state_key);
+            }
+            
+            if (toFront) {
+                this.events.unshift(event);
+            }
+            else {
+                this.events.push(event);
+            }
+        },
+        
+        addOrReplaceMessageEvent: function addOrReplaceMessageEvent(event, toFront) {
+            // Start looking from the tail since the first goal of this function 
+            // is to find a message among the latest ones
+            for (var i = this.events.length - 1; i >= 0; i--) {
+                var storedEvent = this.events[i];
+                if (storedEvent.event_id === event.event_id) {
+                    // It's clobbering time!
+                    this.events[i] = event;
+                    return;
+                }
+            }
+            this.addMessageEvent(event, toFront);
+        },
+        
+        leave: function leave() {
+            return matrixService.leave(this.room_id);
+        }
+    };
+    
+    /***** Room State Object *****/
+    var RoomState = function RoomState() {
+        // list of RoomMember
+        this.members = {}; 
+        // state events, the key is a compound of event type + state_key
+        this.state_events = {}; 
+        this.pagination_token = ""; 
+    };
+    RoomState.prototype = {
+        // get a state event for this room from this.state_events. State events
+        // are unique per type+state_key tuple, with a lot of events using 0-len
+        // state keys. To make it not Really Annoying to access, this method is
+        // provided which can just be given the type and it will return the 
+        // 0-len event by default.
+        state: function state(type, state_key) {
+            if (!type) {
+                return undefined; // event type MUST be specified
+            }
+            if (!state_key) {
+                return this.state_events[type]; // treat as 0-len state key
+            }
+            return this.state_events[type + state_key];
+        },
+        
+        storeStateEvent: function storeState(event) {
+            this.state_events[event.type + event.state_key] = event;
+            if (event.type === "m.room.member") {
+                var rm = new RoomMember();
+                rm.event = event;
+                this.members[event.state_key] = rm;
+            }
+        },
+        
+        storeStateEvents: function storeState(events) {
+            if (!events) {
+                return;
+            }
+            for (var i=0; i<events.length; i++) {
+                this.storeStateEvent(events[i]);
+            }
+        },
+        
+        getStateEvent: function getStateEvent(event_type, state_key) {
+            return this.state_events[event_type + state_key];
+        }
+    };
+    
+    /***** Room Member Object *****/
+    var RoomMember = function RoomMember() {
+        this.event = {}; // the m.room.member event representing the RoomMember.
+        this.user = undefined; // the User
+    };
+    
+    /***** User Object *****/
+    var User = function User() {
+        this.event = {}; // the m.presence event representing the User.
+    };
+    
+    // rooms are stored here when they come in.
+    var rooms = {
+        // roomid: <Room>
+    };
+    
+    console.log("Models inited.");
+    
+    return {
+    
+        getRoom: function(roomId) {
+            if(!rooms[roomId]) {
+                rooms[roomId] = new Room(roomId);
+            }
+            return rooms[roomId];
+        },
+        
+        getRooms: function() {
+            return rooms;
+        },
+        
+        /**
+         * 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
+         */
+        getMember: function(room_id, user_id) {
+            var room = this.getRoom(room_id);
+            return room.current_room_state.members[user_id];
+        }
+    
+    };
+}]);
diff --git a/webclient/components/matrix/notification-service.js b/syweb/webclient/components/matrix/notification-service.js
index 9a911413c3..9a911413c3 100644
--- a/webclient/components/matrix/notification-service.js
+++ b/syweb/webclient/components/matrix/notification-service.js
diff --git a/webclient/components/matrix/presence-service.js b/syweb/webclient/components/matrix/presence-service.js
index b487e3d3bd..b487e3d3bd 100644
--- a/webclient/components/matrix/presence-service.js
+++ b/syweb/webclient/components/matrix/presence-service.js
diff --git a/webclient/components/utilities/utilities-service.js b/syweb/webclient/components/utilities/utilities-service.js
index b417cc5b39..b417cc5b39 100644
--- a/webclient/components/utilities/utilities-service.js
+++ b/syweb/webclient/components/utilities/utilities-service.js
diff --git a/webclient/favicon.ico b/syweb/webclient/favicon.ico
index ba193fabc8..ba193fabc8 100644
--- a/webclient/favicon.ico
+++ b/syweb/webclient/favicon.ico
Binary files differdiff --git a/webclient/home/home-controller.js b/syweb/webclient/home/home-controller.js
index f1295560ef..3a48e64ab4 100644
--- a/webclient/home/home-controller.js
+++ b/syweb/webclient/home/home-controller.js
@@ -58,7 +58,6 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen
                     // Add room_alias & room_display_name members
                     angular.extend(room, matrixService.getRoomAliasAndDisplayName(room));
                     
-                    eventHandlerService.setRoomVisibility(room.room_id, "public");
                 }
             }
         );
diff --git a/webclient/home/home.html b/syweb/webclient/home/home.html
index 0af382916e..0af382916e 100644
--- a/webclient/home/home.html
+++ b/syweb/webclient/home/home.html
diff --git a/webclient/img/close.png b/syweb/webclient/img/close.png
index fbcdb51e6b..fbcdb51e6b 100644
--- a/webclient/img/close.png
+++ b/syweb/webclient/img/close.png
Binary files differdiff --git a/webclient/img/default-profile.png b/syweb/webclient/img/default-profile.png
index 6f81a3c417..6f81a3c417 100644
--- a/webclient/img/default-profile.png
+++ b/syweb/webclient/img/default-profile.png
Binary files differdiff --git a/webclient/img/gradient.png b/syweb/webclient/img/gradient.png
index 8ac9e2193f..8ac9e2193f 100644
--- a/webclient/img/gradient.png
+++ b/syweb/webclient/img/gradient.png
Binary files differdiff --git a/webclient/img/green_phone.png b/syweb/webclient/img/green_phone.png
index 28807c749b..28807c749b 100644
--- a/webclient/img/green_phone.png
+++ b/syweb/webclient/img/green_phone.png
Binary files differdiff --git a/webclient/img/logo-small.png b/syweb/webclient/img/logo-small.png
index 411206dcdc..411206dcdc 100644
--- a/webclient/img/logo-small.png
+++ b/syweb/webclient/img/logo-small.png
Binary files differdiff --git a/webclient/img/logo.png b/syweb/webclient/img/logo.png
index c4b53a8487..c4b53a8487 100644
--- a/webclient/img/logo.png
+++ b/syweb/webclient/img/logo.png
Binary files differdiff --git a/webclient/img/red_phone.png b/syweb/webclient/img/red_phone.png
index 11fc44940c..11fc44940c 100644
--- a/webclient/img/red_phone.png
+++ b/syweb/webclient/img/red_phone.png
Binary files differdiff --git a/webclient/index.html b/syweb/webclient/index.html
index bc011a6c72..992e8d3377 100644
--- a/webclient/index.html
+++ b/syweb/webclient/index.html
@@ -13,7 +13,7 @@
    
     <script type='text/javascript' src='js/jquery-1.8.3.min.js'></script>
     <script type="text/javascript" src="https://www.google.com/recaptcha/api/js/recaptcha_ajax.js"></script> 
-    <script src="js/angular.min.js"></script>
+    <script src="js/angular.js"></script>
     <script src="js/angular-route.min.js"></script>
     <script src="js/angular-sanitize.min.js"></script>
     <script src="js/angular-animate.min.js"></script>
@@ -42,6 +42,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/model-service.js"></script>
     <script src="components/matrix/presence-service.js"></script>
     <script src="components/fileInput/file-input-directive.js"></script>
     <script src="components/fileUpload/file-upload-service.js"></script>
@@ -84,7 +85,7 @@
                     </span>
                 </div>
                 <span ng-show="currentCall.state == 'ringing'">
-                    <button ng-click="answerCall()" ng-disabled="!isWebRTCSupported" title="{{isWebRTCSupported ? '' : 'Your browser does not support VoIP' }}">Answer {{ currentCall.type }} call</button>
+                    <button ng-click="answerCall()" ng-disabled="!isWebRTCSupported()" title="{{isWebRTCSupported() ? '' : 'Your browser does not support VoIP' }}">Answer {{ currentCall.type }} call</button>
                     <button ng-click="hangupCall()">Reject</button>
                 </span>
                 <button ng-click="hangupCall()" ng-show="currentCall && currentCall.state != 'ringing' && currentCall.state != 'ended' && currentCall.state != 'fledgling'">Hang up</button>
diff --git a/webclient/js/angular-animate.js b/syweb/webclient/js/angular-animate.js
index c15f793c1b..c15f793c1b 100644
--- a/webclient/js/angular-animate.js
+++ b/syweb/webclient/js/angular-animate.js
diff --git a/webclient/js/angular-animate.min.js b/syweb/webclient/js/angular-animate.min.js
index 1ce2a93ac7..1ce2a93ac7 100644
--- a/webclient/js/angular-animate.min.js
+++ b/syweb/webclient/js/angular-animate.min.js
diff --git a/webclient/js/angular-mocks.js b/syweb/webclient/js/angular-mocks.js
index 48c0b5decb..24bbcd4137 100755
--- a/webclient/js/angular-mocks.js
+++ b/syweb/webclient/js/angular-mocks.js
@@ -1,10 +1,3 @@
-/**
- * @license AngularJS v1.2.22
- * (c) 2010-2014 Google, Inc. http://angularjs.org
- * License: MIT
- */
-(function(window, angular, undefined) {
-
 'use strict';
 
 /**
@@ -63,6 +56,8 @@ angular.mock.$Browser = function() {
     return listener;
   };
 
+  self.$$checkUrlChange = angular.noop;
+
   self.cookieHash = {};
   self.lastCookieHash = {};
   self.deferredFns = [];
@@ -125,7 +120,7 @@ angular.mock.$Browser = function() {
     }
   };
 
-  self.$$baseHref = '';
+  self.$$baseHref = '/';
   self.baseHref = function() {
     return this.$$baseHref;
   };
@@ -774,13 +769,22 @@ angular.mock.animate = angular.module('ngAnimateMock', ['ng'])
       };
     });
 
-    $provide.decorator('$animate', function($delegate, $$asyncCallback) {
+    $provide.decorator('$animate', ['$delegate', '$$asyncCallback', '$timeout', '$browser',
+                            function($delegate,   $$asyncCallback,   $timeout,   $browser) {
       var animate = {
         queue : [],
+        cancel : $delegate.cancel,
         enabled : $delegate.enabled,
-        triggerCallbacks : function() {
+        triggerCallbackEvents : function() {
           $$asyncCallback.flush();
         },
+        triggerCallbackPromise : function() {
+          $timeout.flush(0);
+        },
+        triggerCallbacks : function() {
+          this.triggerCallbackEvents();
+          this.triggerCallbackPromise();
+        },
         triggerReflow : function() {
           angular.forEach(reflowQueue, function(fn) {
             fn();
@@ -797,12 +801,12 @@ angular.mock.animate = angular.module('ngAnimateMock', ['ng'])
             element : arguments[0],
             args : arguments
           });
-          $delegate[method].apply($delegate, arguments);
+          return $delegate[method].apply($delegate, arguments);
         };
       });
 
       return animate;
-    });
+    }]);
 
   }]);
 
@@ -888,7 +892,7 @@ angular.mock.dump = function(object) {
  * development please see {@link ngMockE2E.$httpBackend e2e $httpBackend mock}.
  *
  * During unit testing, we want our unit tests to run quickly and have no external dependencies so
- * we don’t want to send [XHR](https://developer.mozilla.org/en/xmlhttprequest) or
+ * we don’t want to send [XHR](https://developer.mozilla.org/en/xmlhttprequest) or
  * [JSONP](http://en.wikipedia.org/wiki/JSONP) requests to a real server. All we really need is
  * to verify whether a certain request has been sent or not, or alternatively just let the
  * application make requests, respond with pre-trained responses and assert that the end result is
@@ -1007,13 +1011,14 @@ angular.mock.dump = function(object) {
   ```js
     // testing controller
     describe('MyController', function() {
-       var $httpBackend, $rootScope, createController;
+       var $httpBackend, $rootScope, createController, authRequestHandler;
 
        beforeEach(inject(function($injector) {
          // Set up the mock http service responses
          $httpBackend = $injector.get('$httpBackend');
          // backend definition common for all tests
-         $httpBackend.when('GET', '/auth.py').respond({userId: 'userX'}, {'A-Token': 'xxx'});
+         authRequestHandler = $httpBackend.when('GET', '/auth.py')
+                                .respond({userId: 'userX'}, {'A-Token': 'xxx'});
 
          // Get hold of a scope (i.e. the root scope)
          $rootScope = $injector.get('$rootScope');
@@ -1039,11 +1044,23 @@ angular.mock.dump = function(object) {
        });
 
 
+       it('should fail authentication', function() {
+
+         // Notice how you can change the response even after it was set
+         authRequestHandler.respond(401, '');
+
+         $httpBackend.expectGET('/auth.py');
+         var controller = createController();
+         $httpBackend.flush();
+         expect($rootScope.status).toBe('Failed...');
+       });
+
+
        it('should send msg to server', function() {
          var controller = createController();
          $httpBackend.flush();
 
-         // now you don’t care about the authentication, but
+         // now you don’t care about the authentication, but
          // the controller will still send the request and
          // $httpBackend will respond without you having to
          // specify the expectation and response for this request
@@ -1186,32 +1203,39 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
    * Creates a new backend definition.
    *
    * @param {string} method HTTP method.
-   * @param {string|RegExp} url HTTP url.
+   * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+   *   and returns true if the url match the current definition.
    * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
    *   data string and returns true if the data is as expected.
    * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
    *   object and returns true if the headers match the current definition.
    * @returns {requestHandler} Returns an object with `respond` method that controls how a matched
-   *   request is handled.
+   *   request is handled. You can save this object for later use and invoke `respond` again in
+   *   order to change how a matched request is handled.
    *
-   *  - respond –
+   *  - respond –
    *      `{function([status,] data[, headers, statusText])
    *      | function(function(method, url, data, headers)}`
-   *    – The respond method takes a set of static data to be returned or a function that can
+   *    – The respond method takes a set of static data to be returned or a function that can
    *    return an array containing response status (number), response data (string), response
-   *    headers (Object), and the text for the status (string).
+   *    headers (Object), and the text for the status (string). The respond method returns the
+   *    `requestHandler` object for possible overrides.
    */
   $httpBackend.when = function(method, url, data, headers) {
     var definition = new MockHttpExpectation(method, url, data, headers),
         chain = {
           respond: function(status, data, headers, statusText) {
+            definition.passThrough = undefined;
             definition.response = createResponse(status, data, headers, statusText);
+            return chain;
           }
         };
 
     if ($browser) {
       chain.passThrough = function() {
+        definition.response = undefined;
         definition.passThrough = true;
+        return chain;
       };
     }
 
@@ -1225,10 +1249,12 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
    * @description
    * Creates a new backend definition for GET requests. For more info see `when()`.
    *
-   * @param {string|RegExp} url HTTP url.
+   * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+   *   and returns true if the url match the current definition.
    * @param {(Object|function(Object))=} headers HTTP headers.
    * @returns {requestHandler} Returns an object with `respond` method that control how a matched
-   * request is handled.
+   * request is handled. You can save this object for later use and invoke `respond` again in
+   * order to change how a matched request is handled.
    */
 
   /**
@@ -1237,10 +1263,12 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
    * @description
    * Creates a new backend definition for HEAD requests. For more info see `when()`.
    *
-   * @param {string|RegExp} url HTTP url.
+   * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+   *   and returns true if the url match the current definition.
    * @param {(Object|function(Object))=} headers HTTP headers.
    * @returns {requestHandler} Returns an object with `respond` method that control how a matched
-   * request is handled.
+   * request is handled. You can save this object for later use and invoke `respond` again in
+   * order to change how a matched request is handled.
    */
 
   /**
@@ -1249,10 +1277,12 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
    * @description
    * Creates a new backend definition for DELETE requests. For more info see `when()`.
    *
-   * @param {string|RegExp} url HTTP url.
+   * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+   *   and returns true if the url match the current definition.
    * @param {(Object|function(Object))=} headers HTTP headers.
    * @returns {requestHandler} Returns an object with `respond` method that control how a matched
-   * request is handled.
+   * request is handled. You can save this object for later use and invoke `respond` again in
+   * order to change how a matched request is handled.
    */
 
   /**
@@ -1261,12 +1291,14 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
    * @description
    * Creates a new backend definition for POST requests. For more info see `when()`.
    *
-   * @param {string|RegExp} url HTTP url.
+   * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+   *   and returns true if the url match the current definition.
    * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
    *   data string and returns true if the data is as expected.
    * @param {(Object|function(Object))=} headers HTTP headers.
    * @returns {requestHandler} Returns an object with `respond` method that control how a matched
-   * request is handled.
+   * request is handled. You can save this object for later use and invoke `respond` again in
+   * order to change how a matched request is handled.
    */
 
   /**
@@ -1275,12 +1307,14 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
    * @description
    * Creates a new backend definition for PUT requests.  For more info see `when()`.
    *
-   * @param {string|RegExp} url HTTP url.
+   * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+   *   and returns true if the url match the current definition.
    * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
    *   data string and returns true if the data is as expected.
    * @param {(Object|function(Object))=} headers HTTP headers.
    * @returns {requestHandler} Returns an object with `respond` method that control how a matched
-   * request is handled.
+   * request is handled. You can save this object for later use and invoke `respond` again in
+   * order to change how a matched request is handled.
    */
 
   /**
@@ -1289,9 +1323,11 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
    * @description
    * Creates a new backend definition for JSONP requests. For more info see `when()`.
    *
-   * @param {string|RegExp} url HTTP url.
+   * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+   *   and returns true if the url match the current definition.
    * @returns {requestHandler} Returns an object with `respond` method that control how a matched
-   * request is handled.
+   * request is handled. You can save this object for later use and invoke `respond` again in
+   * order to change how a matched request is handled.
    */
   createShortMethods('when');
 
@@ -1303,30 +1339,36 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
    * Creates a new request expectation.
    *
    * @param {string} method HTTP method.
-   * @param {string|RegExp} url HTTP url.
+   * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+   *   and returns true if the url match the current definition.
    * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
    *  receives data string and returns true if the data is as expected, or Object if request body
    *  is in JSON format.
    * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
    *   object and returns true if the headers match the current expectation.
    * @returns {requestHandler} Returns an object with `respond` method that control how a matched
-   *  request is handled.
+   *  request is handled. You can save this object for later use and invoke `respond` again in
+   *  order to change how a matched request is handled.
    *
-   *  - respond –
+   *  - respond –
    *    `{function([status,] data[, headers, statusText])
    *    | function(function(method, url, data, headers)}`
-   *    – The respond method takes a set of static data to be returned or a function that can
+   *    – The respond method takes a set of static data to be returned or a function that can
    *    return an array containing response status (number), response data (string), response
-   *    headers (Object), and the text for the status (string).
+   *    headers (Object), and the text for the status (string). The respond method returns the
+   *    `requestHandler` object for possible overrides.
    */
   $httpBackend.expect = function(method, url, data, headers) {
-    var expectation = new MockHttpExpectation(method, url, data, headers);
+    var expectation = new MockHttpExpectation(method, url, data, headers),
+        chain = {
+          respond: function (status, data, headers, statusText) {
+            expectation.response = createResponse(status, data, headers, statusText);
+            return chain;
+          }
+        };
+
     expectations.push(expectation);
-    return {
-      respond: function (status, data, headers, statusText) {
-        expectation.response = createResponse(status, data, headers, statusText);
-      }
-    };
+    return chain;
   };
 
 
@@ -1336,10 +1378,12 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
    * @description
    * Creates a new request expectation for GET requests. For more info see `expect()`.
    *
-   * @param {string|RegExp} url HTTP url.
+   * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+   *   and returns true if the url match the current definition.
    * @param {Object=} headers HTTP headers.
    * @returns {requestHandler} Returns an object with `respond` method that control how a matched
-   * request is handled. See #expect for more info.
+   * request is handled. You can save this object for later use and invoke `respond` again in
+   * order to change how a matched request is handled. See #expect for more info.
    */
 
   /**
@@ -1348,10 +1392,12 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
    * @description
    * Creates a new request expectation for HEAD requests. For more info see `expect()`.
    *
-   * @param {string|RegExp} url HTTP url.
+   * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+   *   and returns true if the url match the current definition.
    * @param {Object=} headers HTTP headers.
    * @returns {requestHandler} Returns an object with `respond` method that control how a matched
-   *   request is handled.
+   *   request is handled. You can save this object for later use and invoke `respond` again in
+   *   order to change how a matched request is handled.
    */
 
   /**
@@ -1360,10 +1406,12 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
    * @description
    * Creates a new request expectation for DELETE requests. For more info see `expect()`.
    *
-   * @param {string|RegExp} url HTTP url.
+   * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+   *   and returns true if the url match the current definition.
    * @param {Object=} headers HTTP headers.
    * @returns {requestHandler} Returns an object with `respond` method that control how a matched
-   *   request is handled.
+   *   request is handled. You can save this object for later use and invoke `respond` again in
+   *   order to change how a matched request is handled.
    */
 
   /**
@@ -1372,13 +1420,15 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
    * @description
    * Creates a new request expectation for POST requests. For more info see `expect()`.
    *
-   * @param {string|RegExp} url HTTP url.
+   * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+   *   and returns true if the url match the current definition.
    * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
    *  receives data string and returns true if the data is as expected, or Object if request body
    *  is in JSON format.
    * @param {Object=} headers HTTP headers.
    * @returns {requestHandler} Returns an object with `respond` method that control how a matched
-   *   request is handled.
+   *   request is handled. You can save this object for later use and invoke `respond` again in
+   *   order to change how a matched request is handled.
    */
 
   /**
@@ -1387,13 +1437,15 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
    * @description
    * Creates a new request expectation for PUT requests. For more info see `expect()`.
    *
-   * @param {string|RegExp} url HTTP url.
+   * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+   *   and returns true if the url match the current definition.
    * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
    *  receives data string and returns true if the data is as expected, or Object if request body
    *  is in JSON format.
    * @param {Object=} headers HTTP headers.
    * @returns {requestHandler} Returns an object with `respond` method that control how a matched
-   *   request is handled.
+   *   request is handled. You can save this object for later use and invoke `respond` again in
+   *   order to change how a matched request is handled.
    */
 
   /**
@@ -1402,13 +1454,15 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
    * @description
    * Creates a new request expectation for PATCH requests. For more info see `expect()`.
    *
-   * @param {string|RegExp} url HTTP url.
+   * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+   *   and returns true if the url match the current definition.
    * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
    *  receives data string and returns true if the data is as expected, or Object if request body
    *  is in JSON format.
    * @param {Object=} headers HTTP headers.
    * @returns {requestHandler} Returns an object with `respond` method that control how a matched
-   *   request is handled.
+   *   request is handled. You can save this object for later use and invoke `respond` again in
+   *   order to change how a matched request is handled.
    */
 
   /**
@@ -1417,9 +1471,11 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
    * @description
    * Creates a new request expectation for JSONP requests. For more info see `expect()`.
    *
-   * @param {string|RegExp} url HTTP url.
+   * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+   *   and returns true if the url match the current definition.
    * @returns {requestHandler} Returns an object with `respond` method that control how a matched
-   *   request is handled.
+   *   request is handled. You can save this object for later use and invoke `respond` again in
+   *   order to change how a matched request is handled.
    */
   createShortMethods('expect');
 
@@ -1434,11 +1490,11 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
    *   all pending requests will be flushed. If there are no pending requests when the flush method
    *   is called an exception is thrown (as this typically a sign of programming error).
    */
-  $httpBackend.flush = function(count) {
-    $rootScope.$digest();
+  $httpBackend.flush = function(count, digest) {
+    if (digest !== false) $rootScope.$digest();
     if (!responses.length) throw new Error('No pending request to flush !');
 
-    if (angular.isDefined(count)) {
+    if (angular.isDefined(count) && count !== null) {
       while (count--) {
         if (!responses.length) throw new Error('No more pending request to flush !');
         responses.shift()();
@@ -1448,7 +1504,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
         responses.shift()();
       }
     }
-    $httpBackend.verifyNoOutstandingExpectation();
+    $httpBackend.verifyNoOutstandingExpectation(digest);
   };
 
 
@@ -1466,8 +1522,8 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
    *   afterEach($httpBackend.verifyNoOutstandingExpectation);
    * ```
    */
-  $httpBackend.verifyNoOutstandingExpectation = function() {
-    $rootScope.$digest();
+  $httpBackend.verifyNoOutstandingExpectation = function(digest) {
+    if (digest !== false) $rootScope.$digest();
     if (expectations.length) {
       throw new Error('Unsatisfied requests: ' + expectations.join(', '));
     }
@@ -1511,7 +1567,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) {
 
 
   function createShortMethods(prefix) {
-    angular.forEach(['GET', 'DELETE', 'JSONP'], function(method) {
+    angular.forEach(['GET', 'DELETE', 'JSONP', 'HEAD'], function(method) {
      $httpBackend[prefix + method] = function(url, headers) {
        return $httpBackend[prefix](method, url, undefined, headers);
      };
@@ -1541,6 +1597,7 @@ function MockHttpExpectation(method, url, data, headers) {
   this.matchUrl = function(u) {
     if (!url) return true;
     if (angular.isFunction(url.test)) return url.test(u);
+    if (angular.isFunction(url)) return url(u);
     return url == u;
   };
 
@@ -1627,7 +1684,7 @@ function MockXhr() {
  * that adds a "flush" and "verifyNoPendingTasks" methods.
  */
 
-angular.mock.$TimeoutDecorator = function($delegate, $browser) {
+angular.mock.$TimeoutDecorator = ['$delegate', '$browser', function ($delegate, $browser) {
 
   /**
    * @ngdoc method
@@ -1666,9 +1723,9 @@ angular.mock.$TimeoutDecorator = function($delegate, $browser) {
   }
 
   return $delegate;
-};
+}];
 
-angular.mock.$RAFDecorator = function($delegate) {
+angular.mock.$RAFDecorator = ['$delegate', function($delegate) {
   var queue = [];
   var rafFn = function(fn) {
     var index = queue.length;
@@ -1694,9 +1751,9 @@ angular.mock.$RAFDecorator = function($delegate) {
   };
 
   return rafFn;
-};
+}];
 
-angular.mock.$AsyncCallbackDecorator = function($delegate) {
+angular.mock.$AsyncCallbackDecorator = ['$delegate', function($delegate) {
   var callbacks = [];
   var addFn = function(fn) {
     callbacks.push(fn);
@@ -1708,7 +1765,7 @@ angular.mock.$AsyncCallbackDecorator = function($delegate) {
     callbacks = [];
   };
   return addFn;
-};
+}];
 
 /**
  *
@@ -1822,22 +1879,25 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
  * Creates a new backend definition.
  *
  * @param {string} method HTTP method.
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ *   and returns true if the url match the current definition.
  * @param {(string|RegExp)=} data HTTP request body.
  * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
  *   object and returns true if the headers match the current definition.
  * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
- *   control how a matched request is handled.
+ *   control how a matched request is handled. You can save this object for later use and invoke
+ *   `respond` or `passThrough` again in order to change how a matched request is handled.
  *
- *  - respond –
+ *  - respond –
  *    `{function([status,] data[, headers, statusText])
  *    | function(function(method, url, data, headers)}`
- *    – The respond method takes a set of static data to be returned or a function that can return
+ *    – The respond method takes a set of static data to be returned or a function that can return
  *    an array containing response status (number), response data (string), response headers
  *    (Object), and the text for the status (string).
- *  - passThrough – `{function()}` – Any request matching a backend definition with
+ *  - passThrough – `{function()}` – Any request matching a backend definition with
  *    `passThrough` handler will be passed through to the real backend (an XHR request will be made
  *    to the server.)
+ *  - Both methods return the `requestHandler` object for possible overrides.
  */
 
 /**
@@ -1847,10 +1907,12 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
  * @description
  * Creates a new backend definition for GET requests. For more info see `when()`.
  *
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ *   and returns true if the url match the current definition.
  * @param {(Object|function(Object))=} headers HTTP headers.
  * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
- *   control how a matched request is handled.
+ *   control how a matched request is handled. You can save this object for later use and invoke
+ *   `respond` or `passThrough` again in order to change how a matched request is handled.
  */
 
 /**
@@ -1860,10 +1922,12 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
  * @description
  * Creates a new backend definition for HEAD requests. For more info see `when()`.
  *
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ *   and returns true if the url match the current definition.
  * @param {(Object|function(Object))=} headers HTTP headers.
  * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
- *   control how a matched request is handled.
+ *   control how a matched request is handled. You can save this object for later use and invoke
+ *   `respond` or `passThrough` again in order to change how a matched request is handled.
  */
 
 /**
@@ -1873,10 +1937,12 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
  * @description
  * Creates a new backend definition for DELETE requests. For more info see `when()`.
  *
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ *   and returns true if the url match the current definition.
  * @param {(Object|function(Object))=} headers HTTP headers.
  * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
- *   control how a matched request is handled.
+ *   control how a matched request is handled. You can save this object for later use and invoke
+ *   `respond` or `passThrough` again in order to change how a matched request is handled.
  */
 
 /**
@@ -1886,11 +1952,13 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
  * @description
  * Creates a new backend definition for POST requests. For more info see `when()`.
  *
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ *   and returns true if the url match the current definition.
  * @param {(string|RegExp)=} data HTTP request body.
  * @param {(Object|function(Object))=} headers HTTP headers.
  * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
- *   control how a matched request is handled.
+ *   control how a matched request is handled. You can save this object for later use and invoke
+ *   `respond` or `passThrough` again in order to change how a matched request is handled.
  */
 
 /**
@@ -1900,11 +1968,13 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
  * @description
  * Creates a new backend definition for PUT requests.  For more info see `when()`.
  *
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ *   and returns true if the url match the current definition.
  * @param {(string|RegExp)=} data HTTP request body.
  * @param {(Object|function(Object))=} headers HTTP headers.
  * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
- *   control how a matched request is handled.
+ *   control how a matched request is handled. You can save this object for later use and invoke
+ *   `respond` or `passThrough` again in order to change how a matched request is handled.
  */
 
 /**
@@ -1914,11 +1984,13 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
  * @description
  * Creates a new backend definition for PATCH requests.  For more info see `when()`.
  *
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ *   and returns true if the url match the current definition.
  * @param {(string|RegExp)=} data HTTP request body.
  * @param {(Object|function(Object))=} headers HTTP headers.
  * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
- *   control how a matched request is handled.
+ *   control how a matched request is handled. You can save this object for later use and invoke
+ *   `respond` or `passThrough` again in order to change how a matched request is handled.
  */
 
 /**
@@ -1928,30 +2000,17 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
  * @description
  * Creates a new backend definition for JSONP requests. For more info see `when()`.
  *
- * @param {string|RegExp} url HTTP url.
+ * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
+ *   and returns true if the url match the current definition.
  * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
- *   control how a matched request is handled.
+ *   control how a matched request is handled. You can save this object for later use and invoke
+ *   `respond` or `passThrough` again in order to change how a matched request is handled.
  */
 angular.mock.e2e = {};
 angular.mock.e2e.$httpBackendDecorator =
   ['$rootScope', '$delegate', '$browser', createHttpBackendMock];
 
 
-angular.mock.clearDataCache = function() {
-  var key,
-      cache = angular.element.cache;
-
-  for(key in cache) {
-    if (Object.prototype.hasOwnProperty.call(cache,key)) {
-      var handle = cache[key].handle;
-
-      handle && angular.element(handle.elem).off();
-      delete cache[key];
-    }
-  }
-};
-
-
 if(window.jasmine || window.mocha) {
 
   var currentSpec = null,
@@ -1982,8 +2041,6 @@ if(window.jasmine || window.mocha) {
       injector.get('$browser').pollFns.length = 0;
     }
 
-    angular.mock.clearDataCache();
-
     // clean up jquery's fragment cache
     angular.forEach(angular.element.fragments, function(val, key) {
       delete angular.element.fragments[key];
@@ -2003,6 +2060,7 @@ if(window.jasmine || window.mocha) {
    * @description
    *
    * *NOTE*: This function is also published on window for easy access.<br>
+   * *NOTE*: This function is declared ONLY WHEN running tests with jasmine or mocha
    *
    * This function registers a module configuration code. It collects the configuration information
    * which will be used when the injector is created by {@link angular.mock.inject inject}.
@@ -2045,6 +2103,7 @@ if(window.jasmine || window.mocha) {
    * @description
    *
    * *NOTE*: This function is also published on window for easy access.<br>
+   * *NOTE*: This function is declared ONLY WHEN running tests with jasmine or mocha
    *
    * The inject function wraps a function into an injectable function. The inject() creates new
    * instance of {@link auto.$injector $injector} per test, which is then used for
@@ -2144,14 +2203,28 @@ if(window.jasmine || window.mocha) {
     /////////////////////
     function workFn() {
       var modules = currentSpec.$modules || [];
-
+      var strictDi = !!currentSpec.$injectorStrict;
       modules.unshift('ngMock');
       modules.unshift('ng');
       var injector = currentSpec.$injector;
       if (!injector) {
-        injector = currentSpec.$injector = angular.injector(modules);
+        if (strictDi) {
+          // If strictDi is enabled, annotate the providerInjector blocks
+          angular.forEach(modules, function(moduleFn) {
+            if (typeof moduleFn === "function") {
+              angular.injector.$$annotate(moduleFn);
+            }
+          });
+        }
+        injector = currentSpec.$injector = angular.injector(modules, strictDi);
+        currentSpec.$injectorStrict = strictDi;
       }
       for(var i = 0, ii = blockFns.length; i < ii; i++) {
+        if (currentSpec.$injectorStrict) {
+          // If the injector is strict / strictDi, and the spec wants to inject using automatic
+          // annotation, then annotate the function here.
+          injector.annotate(blockFns[i]);
+        }
         try {
           /* jshint -W040 *//* Jasmine explicitly provides a `this` object when calling functions */
           injector.invoke(blockFns[i] || angular.noop, this);
@@ -2167,7 +2240,20 @@ if(window.jasmine || window.mocha) {
       }
     }
   };
-}
 
 
-})(window, window.angular);
\ No newline at end of file
+  angular.mock.inject.strictDi = function(value) {
+    value = arguments.length ? !!value : true;
+    return isSpecRunning() ? workFn() : workFn;
+
+    function workFn() {
+      if (value !== currentSpec.$injectorStrict) {
+        if (currentSpec.$injector) {
+          throw new Error('Injector already created, can not modify strict annotations');
+        } else {
+          currentSpec.$injectorStrict = value;
+        }
+      }
+    }
+  };
+}
diff --git a/webclient/js/angular-route.js b/syweb/webclient/js/angular-route.js
index 305d92e855..305d92e855 100644
--- a/webclient/js/angular-route.js
+++ b/syweb/webclient/js/angular-route.js
diff --git a/webclient/js/angular-route.min.js b/syweb/webclient/js/angular-route.min.js
index 03da279ec3..03da279ec3 100644
--- a/webclient/js/angular-route.min.js
+++ b/syweb/webclient/js/angular-route.min.js
diff --git a/webclient/js/angular-sanitize.js b/syweb/webclient/js/angular-sanitize.js
index ec46895f68..ec46895f68 100644
--- a/webclient/js/angular-sanitize.js
+++ b/syweb/webclient/js/angular-sanitize.js
diff --git a/webclient/js/angular-sanitize.min.js b/syweb/webclient/js/angular-sanitize.min.js
index ce99bba18e..ce99bba18e 100644
--- a/webclient/js/angular-sanitize.min.js
+++ b/syweb/webclient/js/angular-sanitize.min.js
diff --git a/webclient/js/angular.js b/syweb/webclient/js/angular.js
index bdc97abb02..bdc97abb02 100644
--- a/webclient/js/angular.js
+++ b/syweb/webclient/js/angular.js
diff --git a/webclient/js/angular.min.js b/syweb/webclient/js/angular.min.js
index 5475589e2f..5475589e2f 100644
--- a/webclient/js/angular.min.js
+++ b/syweb/webclient/js/angular.min.js
diff --git a/webclient/js/autofill-event.js b/syweb/webclient/js/autofill-event.js
index 006f83e1be..006f83e1be 100755
--- a/webclient/js/autofill-event.js
+++ b/syweb/webclient/js/autofill-event.js
diff --git a/webclient/js/elastic.js b/syweb/webclient/js/elastic.js
index d585d81109..d585d81109 100644
--- a/webclient/js/elastic.js
+++ b/syweb/webclient/js/elastic.js
diff --git a/webclient/js/jquery-1.8.3.min.js b/syweb/webclient/js/jquery-1.8.3.min.js
index 3883779527..3883779527 100644
--- a/webclient/js/jquery-1.8.3.min.js
+++ b/syweb/webclient/js/jquery-1.8.3.min.js
diff --git a/webclient/js/ng-infinite-scroll-matrix.js b/syweb/webclient/js/ng-infinite-scroll-matrix.js
index 045ec8d93e..045ec8d93e 100644
--- a/webclient/js/ng-infinite-scroll-matrix.js
+++ b/syweb/webclient/js/ng-infinite-scroll-matrix.js
diff --git a/webclient/js/ui-bootstrap-tpls-0.11.2.js b/syweb/webclient/js/ui-bootstrap-tpls-0.11.2.js
index 260c2769b8..260c2769b8 100644
--- a/webclient/js/ui-bootstrap-tpls-0.11.2.js
+++ b/syweb/webclient/js/ui-bootstrap-tpls-0.11.2.js
diff --git a/webclient/login/login-controller.js b/syweb/webclient/login/login-controller.js
index 5ef39a7122..5ef39a7122 100644
--- a/webclient/login/login-controller.js
+++ b/syweb/webclient/login/login-controller.js
diff --git a/webclient/login/login.html b/syweb/webclient/login/login.html
index 6b321f8fc5..6b321f8fc5 100644
--- a/webclient/login/login.html
+++ b/syweb/webclient/login/login.html
diff --git a/webclient/login/register-controller.js b/syweb/webclient/login/register-controller.js
index be970ce1c3..b23a72b185 100644
--- a/webclient/login/register-controller.js
+++ b/syweb/webclient/login/register-controller.js
@@ -124,7 +124,7 @@ angular.module('RegisterController', ['matrixService'])
                 $location.url("home");
             },
             function(error) {
-                console.trace("Registration error: "+error);
+                console.error("Registration error: "+JSON.stringify(error));
                 if (useCaptcha) {
                     Recaptcha.reload();
                 }
diff --git a/webclient/login/register.html b/syweb/webclient/login/register.html
index a27f9ad4e8..a27f9ad4e8 100644
--- a/webclient/login/register.html
+++ b/syweb/webclient/login/register.html
diff --git a/webclient/media/busy.mp3 b/syweb/webclient/media/busy.mp3
index fec27ba4c5..fec27ba4c5 100644
--- a/webclient/media/busy.mp3
+++ b/syweb/webclient/media/busy.mp3
Binary files differdiff --git a/webclient/media/busy.ogg b/syweb/webclient/media/busy.ogg
index 5d64a7d0d9..5d64a7d0d9 100644
--- a/webclient/media/busy.ogg
+++ b/syweb/webclient/media/busy.ogg
Binary files differdiff --git a/webclient/media/callend.mp3 b/syweb/webclient/media/callend.mp3
index 50c34e5640..50c34e5640 100644
--- a/webclient/media/callend.mp3
+++ b/syweb/webclient/media/callend.mp3
Binary files differdiff --git a/webclient/media/callend.ogg b/syweb/webclient/media/callend.ogg
index 927ce1f634..927ce1f634 100644
--- a/webclient/media/callend.ogg
+++ b/syweb/webclient/media/callend.ogg
Binary files differdiff --git a/webclient/media/ring.mp3 b/syweb/webclient/media/ring.mp3
index 3c3cdde3f9..3c3cdde3f9 100644
--- a/webclient/media/ring.mp3
+++ b/syweb/webclient/media/ring.mp3
Binary files differdiff --git a/webclient/media/ring.ogg b/syweb/webclient/media/ring.ogg
index de49b8ae6f..de49b8ae6f 100644
--- a/webclient/media/ring.ogg
+++ b/syweb/webclient/media/ring.ogg
Binary files differdiff --git a/webclient/media/ringback.mp3 b/syweb/webclient/media/ringback.mp3
index 6ee34bf395..6ee34bf395 100644
--- a/webclient/media/ringback.mp3
+++ b/syweb/webclient/media/ringback.mp3
Binary files differdiff --git a/webclient/media/ringback.ogg b/syweb/webclient/media/ringback.ogg
index 7dbfdcd017..7dbfdcd017 100644
--- a/webclient/media/ringback.ogg
+++ b/syweb/webclient/media/ringback.ogg
Binary files differdiff --git a/webclient/mobile.css b/syweb/webclient/mobile.css
index 6fa9221ccf..6fa9221ccf 100644
--- a/webclient/mobile.css
+++ b/syweb/webclient/mobile.css
diff --git a/webclient/recents/recents-controller.js b/syweb/webclient/recents/recents-controller.js
index ee8a41c366..6f0be18f1a 100644
--- a/webclient/recents/recents-controller.js
+++ b/syweb/webclient/recents/recents-controller.js
@@ -17,11 +17,14 @@
 'use strict';
 
 angular.module('RecentsController', ['matrixService', 'matrixFilter'])
-.controller('RecentsController', ['$rootScope', '$scope', 'eventHandlerService', 
-                               function($rootScope, $scope, eventHandlerService) {
+.controller('RecentsController', ['$rootScope', '$scope', 'eventHandlerService', 'modelService', 
+                               function($rootScope, $scope, eventHandlerService, modelService) {
 
     // 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
diff --git a/webclient/recents/recents-filter.js b/syweb/webclient/recents/recents-filter.js
index ef8d9897f7..cfbc6f4bd8 100644
--- a/webclient/recents/recents-filter.js
+++ b/syweb/webclient/recents/recents-filter.js
@@ -17,7 +17,7 @@
 'use strict';
 
 angular.module('RecentsController')
-.filter('orderRecents', ["matrixService", "eventHandlerService", function(matrixService, eventHandlerService) {
+.filter('orderRecents', ["matrixService", "eventHandlerService", "modelService", function(matrixService, eventHandlerService, modelService) {
     return function(rooms) {
         var user_id = matrixService.config().user_id;
 
@@ -25,26 +25,33 @@ angular.module('RecentsController')
         // The key, room_id, is already in value objects
         var filtered = [];
         angular.forEach(rooms, function(room, room_id) {
-            
+            room.recent = {};
+            var meEvent = room.current_room_state.state("m.room.member", user_id);
             // Show the room only if the user has joined it or has been invited
             // (ie, do not show it if he has been banned)
-            var member = eventHandlerService.getMember(room_id, user_id);
-            if (member && ("invite" === member.membership || "join" === member.membership)) {
-            
+            var member = modelService.getMember(room_id, user_id);
+            if (member) {
+                member = member.event;
+            }
+            room.recent.me = member;
+            if (member && ("invite" === member.content.membership || "join" === member.content.membership)) {
+                if ("invite" === member.content.membership) {
+                    room.recent.inviter = member.user_id;
+                }
                 // Count users here
                 // TODO: Compute it directly in eventHandlerService
-                room.numUsersInRoom = eventHandlerService.getUsersCountInRoom(room_id);
+                room.recent.numUsersInRoom = eventHandlerService.getUsersCountInRoom(room_id);
 
                 filtered.push(room);
             }
-            else if ("invite" === room.membership) {
+            else if (meEvent && "invite" === meEvent.content.membership) {
                 // The only information we have about the room is that the user has been invited
                 filtered.push(room);
             }
         });
 
         // And time sort them
-        // The room with the lastest message at first
+        // The room with the latest message at first
         filtered.sort(function (roomA, roomB) {
 
             var lastMsgRoomA = eventHandlerService.getLastMessage(roomA.room_id, true);
diff --git a/webclient/recents/recents.html b/syweb/webclient/recents/recents.html
index a52b215c7e..7297e23703 100644
--- a/webclient/recents/recents.html
+++ b/syweb/webclient/recents/recents.html
@@ -1,16 +1,16 @@
 <div ng-controller="RecentsController">
     <table class="recentsTable">
-        <tbody ng-repeat="(index, room) in events.rooms | orderRecents" 
+        <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)}">                                           
             <tr>
-                <td ng-class="room['m.room.join_rules'].content.join_rule == 'public' ? 'recentsRoomName recentsPublicRoom' : 'recentsRoomName'">
+                <td ng-class="room.current_room_state.state('m.room.join_rules').content.join_rule == 'public' ? 'recentsRoomName recentsPublicRoom' : 'recentsRoomName'">
                     {{ room.room_id | mRoomName }}
                 </td>
                 <td class="recentsRoomSummaryUsersCount">
-                    <span ng-show="undefined !== room.numUsersInRoom">
-                        {{ room.numUsersInRoom || '1' }} {{ room.numUsersInRoom == 1 ? 'user' : 'users' }}                     
+                    <span ng-show="undefined !== room.recent.numUsersInRoom">
+                        {{ room.recent.numUsersInRoom || '1' }} {{ room.recent.numUsersInRoom == 1 ? 'user' : 'users' }}                     
                     </span>
                 </td>
                 <td class="recentsRoomSummaryTS">
@@ -27,11 +27,11 @@
             <tr>
                 <td colspan="3" class="recentsRoomSummary">
 
-                    <div ng-show="room.membership === 'invite'">
-                        {{ room.inviter | mUserDisplayName: room.room_id }} invited you
+                    <div ng-show="room.recent.me.content.membership === 'invite'">
+                        {{ room.recent.inviter | mUserDisplayName: room.room_id }} invited you
                     </div>
                     
-                    <div ng-hide="room.membership === 'invite'" ng-switch="lastMsg.type">
+                    <div ng-hide="room.recent.me.membership === 'invite'" ng-switch="lastMsg.type">
                         <div ng-switch-when="m.room.member">
                             <span ng-switch="lastMsg.changedKey">
                                 <span ng-switch-when="membership">
diff --git a/webclient/room/room-controller.js b/syweb/webclient/room/room-controller.js
index 486ead0da9..d3fb85b9dc 100644
--- a/webclient/room/room-controller.js
+++ b/syweb/webclient/room/room-controller.js
@@ -15,8 +15,8 @@ limitations under the License.
 */
 
 angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
-.controller('RoomController', ['$modal', '$filter', '$scope', '$timeout', '$routeParams', '$location', '$rootScope', 'matrixService', 'mPresence', 'eventHandlerService', 'mFileUpload', 'matrixPhoneService', 'MatrixCall', 'notificationService',
-                               function($modal, $filter, $scope, $timeout, $routeParams, $location, $rootScope, matrixService, mPresence, eventHandlerService, mFileUpload, matrixPhoneService, MatrixCall, notificationService) {
+.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) {
    'use strict';
     var MESSAGES_PER_PAGINATION = 30;
     var THUMBNAIL_SIZE = 320;
@@ -64,7 +64,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
                 return;
             };
 
-            var nameEvent = $rootScope.events.rooms[$scope.room_id]['m.room.name'];
+            var nameEvent = $scope.room.current_room_state.state_events['m.room.name'];
             if (nameEvent) {
                 $scope.name.newNameText = nameEvent.content.name;
             }
@@ -105,7 +105,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
                 console.log("Warning: Already editing topic.");
                 return;
             }
-            var topicEvent = $rootScope.events.rooms[$scope.room_id]['m.room.topic'];
+            var topicEvent = $scope.room.current_room_state.state_events['m.room.topic'];
             if (topicEvent) {
                 $scope.topic.newTopicText = topicEvent.content.topic;
             }
@@ -207,7 +207,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
                     }
                     notificationService.showNotification(
                         userName +
-                        " (" + (matrixService.getRoomIdToAliasMapping(event.room_id) || event.room_id) + ")",
+                        " (" + $filter("mRoomName")(event.room_id) + ")",
                         userName + " joined",
                         event.content.avatar_url ? event.content.avatar_url : undefined,
                         function() {
@@ -254,11 +254,11 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
             $scope.state.paginating = true;
         }
         
-        console.log("paginateBackMessages from " + $rootScope.events.rooms[$scope.room_id].pagination.earliest_token + " for " + numItems);
+        console.log("paginateBackMessages from " + $scope.room.old_room_state.pagination_token + " for " + numItems);
         var originalTopRow = $("#messageTable>tbody>tr:first")[0];
         
         // Paginate events from the point in cache
-        matrixService.paginateBackMessages($scope.room_id, $rootScope.events.rooms[$scope.room_id].pagination.earliest_token, numItems).then(
+        matrixService.paginateBackMessages($scope.room_id, $scope.room.old_room_state.pagination_token, numItems).then(
             function(response) {
 
                 eventHandlerService.handleRoomMessages($scope.room_id, response.data, false, 'b');
@@ -404,7 +404,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
     var updateUserPowerLevel = function(user_id) {
         var member = $scope.members[user_id];
         if (member) {
-            member.powerLevel = matrixService.getUserPowerLevel($scope.room_id, user_id);
+            member.powerLevel = eventHandlerService.getUserPowerLevel($scope.room_id, user_id);
             
             normaliseMembersPowerLevels();
         }
@@ -492,7 +492,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
                             
                             var room_id = matrixService.getAliasToRoomIdMapping(room_alias);
                             console.log("joining " + room_alias + " id=" + room_id);
-                            if ($rootScope.events.rooms[room_id]) {
+                            if ($scope.room) { // TODO actually check that you = join
                                 // don't send a join event for a room you're already in.
                                 $location.url("room/" + room_alias);
                             }
@@ -576,7 +576,8 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
                                 powerLevel = parseInt(matches[3]);
                             }
                             if (powerLevel !== NaN) {
-                                promise = matrixService.setUserPowerLevel($scope.room_id, user_id, powerLevel);
+                                var powerLevelEvent = $scope.room.current_room_state.state("m.room.power_levels");
+                                promise = matrixService.setUserPowerLevel($scope.room_id, user_id, powerLevel, powerLevelEvent);
                             }
                         }
                     }
@@ -591,7 +592,8 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
                     if (args) {
                         var matches = args.match(/^(\S+)$/);
                         if (matches) {
-                            promise = matrixService.setUserPowerLevel($scope.room_id, args, undefined);
+                            var powerLevelEvent = $scope.room.current_room_state.state("m.room.power_levels");
+                            promise = matrixService.setUserPowerLevel($scope.room_id, args, undefined, powerLevelEvent);
                         }
                     }
                     
@@ -629,7 +631,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
             };
 
             $('#mainInput').val('');
-            $rootScope.events.rooms[$scope.room_id].messages.push(echoMessage);
+            $scope.room.addMessageEvent(echoMessage);
             scrollToBottom();
         }
 
@@ -717,6 +719,9 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
     
     var onInit2 = function() {
         console.log("onInit2");
+        // =============================
+        $scope.room = modelService.getRoom($scope.room_id);
+        // =============================
         
         // Scroll down as soon as possible so that we point to the last message
         // if it already exists in memory
@@ -729,9 +734,9 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
                 var needsToJoin = true;
                 
                 // The room members is available in the data fetched by initialSync
-                if ($rootScope.events.rooms[$scope.room_id]) {
+                if ($scope.room) {
 
-                    var messages = $rootScope.events.rooms[$scope.room_id].messages;
+                    var messages = $scope.room.events;
 
                     if (0 === messages.length
                     || (1 === messages.length && "m.room.member" === messages[0].type && "invite" === messages[0].content.membership && $scope.state.user_id === messages[0].state_key)) {
@@ -743,19 +748,19 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
                         $scope.state.first_pagination = false;
                     }
 
-                    var members = $rootScope.events.rooms[$scope.room_id].members;
+                    var members = $scope.room.current_room_state.members;
 
                     // Update the member list
                     for (var i in members) {
                         if (!members.hasOwnProperty(i)) continue;
 
-                        var member = members[i];
+                        var member = members[i].event;
                         updateMemberList(member);
                     }
 
                     // Check if the user has already join the room
                     if ($scope.state.user_id in members) {
-                        if ("join" === members[$scope.state.user_id].membership) {
+                        if ("join" === members[$scope.state.user_id].event.content.membership) {
                             needsToJoin = false;
                         }
                     }
@@ -999,10 +1004,15 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
     };
 
     $scope.openJson = function(content) {
-        $scope.event_selected = content;
+        $scope.event_selected = angular.copy(content);
+        
+        // FIXME: Pre-calculated event data should be stripped in a nicer way.
+        $scope.event_selected.__room_member = undefined;
+        $scope.event_selected.__target_room_member = undefined;
+        
         // scope this so the template can check power levels and enable/disable
         // buttons
-        $scope.pow = matrixService.getUserPowerLevel;
+        $scope.pow = eventHandlerService.getUserPowerLevel;
 
         var modalInstance = $modal.open({
             templateUrl: 'eventInfoTemplate.html',
@@ -1039,8 +1049,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
             state_key: ""
         };
 
-        var stateFilter = $filter("stateEventsFilter");
-        var stateEvents = stateFilter($scope.events.rooms[$scope.room_id]);
+        var stateEvents = $scope.room.current_room_state.state_events;
         // The modal dialog will 2-way bind this field, so we MUST make a deep
         // copy of the state events else we will be *actually adjusing our view
         // of the world* when fiddling with the JSON!! Apparently parse/stringify
@@ -1059,7 +1068,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
     console.log("Displaying modal dialog for >>>> " + JSON.stringify($scope.event_selected));
     $scope.redact = function() {
         console.log("User level = "+$scope.pow($scope.room_id, $scope.state.user_id)+
-                    " Redact level = "+$scope.events.rooms[$scope.room_id]["m.room.ops_levels"].content.redact_level);
+                    " Redact level = "+$scope.room.current_room_state.state_events["m.room.ops_levels"].content.redact_level);
         console.log("Redact event >> " + JSON.stringify($scope.event_selected));
         $modalInstance.close("redact");
     };
diff --git a/webclient/room/room-directive.js b/syweb/webclient/room/room-directive.js
index 05382cfcd3..05382cfcd3 100644
--- a/webclient/room/room-directive.js
+++ b/syweb/webclient/room/room-directive.js
diff --git a/webclient/room/room.html b/syweb/webclient/room/room.html
index 5265f42dd8..e59cc30edc 100644
--- a/webclient/room/room.html
+++ b/syweb/webclient/room/room.html
@@ -6,7 +6,7 @@
         </div>
         <div class="modal-footer">
             <button ng-click="redact()" type="button" class="btn btn-danger" 
-             ng-disabled="!events.rooms[room_id]['m.room.ops_levels'].content.redact_level || !pow(room_id, state.user_id) || pow(room_id, state.user_id) < events.rooms[room_id]['m.room.ops_levels'].content.redact_level"
+             ng-disabled="!room.current_room_state.state('m.room.ops_levels').content.redact_level || !pow(room_id, state.user_id) || pow(room_id, state.user_id) < room.current_room_state.state('m.room.ops_levels').content.redact_level"
              title="Delete this event on all home servers. This cannot be undone.">
                 Redact
             </button>        
@@ -18,7 +18,8 @@
             <table class="room-info">
             <tr ng-repeat="(key, event) in roomInfo.stateEvents" class="room-info-event">
                 <td class="room-info-event-meta" width="30%">
-                    <span class="monospace">{{ key }}</span>
+                    <span class="monospace">{{ event.type }}</span>
+                    <span ng-show="event.state_key" class="monospace"> ({{event.state_key}})</span>
                     <br/>
                     {{ (event.origin_server_ts) | date:'MMM d HH:mm' }}
                     <br/>
@@ -68,13 +69,13 @@
             </div>
 
             <div class="roomTopicSection">
-                <button ng-hide="events.rooms[room_id]['m.room.topic'].content.topic || topic.isEditing"
+                <button ng-hide="room.current_room_state.state_events['m.room.topic'].content.topic || topic.isEditing"
                     ng-click="topic.editTopic()" class="roomTopicSetNew">
                     Set Topic
                 </button>
-                <div ng-show="events.rooms[room_id]['m.room.topic'].content.topic || topic.isEditing">
+                <div ng-show="room.current_room_state.state_events['m.room.topic'].content.topic || topic.isEditing">
                     <div ng-hide="topic.isEditing" ng-dblclick="topic.editTopic()" id="roomTopic">
-                        {{ events.rooms[room_id]['m.room.topic'].content.topic | limitTo: 200}}
+                        {{ room.current_room_state.state_events['m.room.topic'].content.topic | limitTo: 200}}
                     </div>
                     <form ng-submit="topic.updateTopic()" ng-show="topic.isEditing" class="roomTopicForm">
                         <input ng-model="topic.newTopicText" ng-blur="topic.cancelEdit()" class="roomTopicInput"  placeholder="Topic"/>
@@ -123,32 +124,34 @@
          ng-style="{ 'visibility': state.messages_visibility }"
          keep-scroll>
         <table id="messageTable" infinite-scroll="paginateMore()">
-            <tr ng-repeat="msg in events.rooms[room_id].messages"
-                ng-class="(events.rooms[room_id].messages[$index + 1].user_id !== msg.user_id ? 'differentUser' : '') + (msg.user_id === state.user_id ? ' mine' : '')" scroll-item>
+            <tr ng-repeat="msg in room.events"
+                ng-class="(room.events[$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"> {{ msg.user_id | mUserDisplayName: room_id }}</div>
+                    <div class="sender" ng-hide="room.events[$index - 1].user_id === msg.user_id"> {{ msg.__room_member.cnt.displayname || msg.user_id | mUserDisplayName: room_id }}</div>
                     <div class="timestamp"
                          ng-class="msg.echo_msg_state">
                         {{ (msg.origin_server_ts) | date:'MMM d HH:mm' }}
                     </div>
                 </td>
                 <td class="avatar">
-                    <img class="avatarImage" ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.png' }}" width="32" height="32" title="{{msg.user_id}}"
-                         ng-hide="events.rooms[room_id].messages[$index - 1].user_id === msg.user_id || msg.user_id === state.user_id"/>
+                    <!-- msg.__room_member.avatar_url is just backwards compat, and can be removed in the future. -->
+                    <img class="avatarImage" ng-src="{{ msg.__room_member.cnt.avatar_url || msg.__room_member.avatar_url || 'img/default-profile.png' }}" width="32" height="32" title="{{msg.user_id}}"
+                         ng-hide="room.events[$index - 1].user_id === msg.user_id || msg.user_id === state.user_id"/>
                 </td>
                 <td ng-class="(!msg.content.membership && ('m.room.topic' !== msg.type && 'm.room.name' !== msg.type))? (msg.content.msgtype === 'm.emote' ? 'emote text' : 'text') : 'membership text'">
-                    <div class="bubble" ng-click="openJson(msg)">
+                    <div class="bubble" ng-dblclick="openJson(msg)">
                         <span ng-if="'join' === msg.content.membership && msg.changedKey === 'membership'">
-                            {{ members[msg.state_key].displayname || msg.state_key }} joined
+                            {{ msg.content.displayname || members[msg.state_key].displayname || msg.state_key }} joined
                         </span>
                         <span ng-if="'leave' === msg.content.membership && msg.changedKey === 'membership'">
                             <span ng-if="msg.user_id === msg.state_key">
-                                {{ members[msg.state_key].displayname || msg.state_key }} left
+                                <!-- FIXME: This seems like a synapse bug that the 'leave' content doesn't give the displayname... -->
+                                {{ msg.__room_member.cnt.displayname || members[msg.state_key].displayname || msg.state_key }} left
                             </span>
                             <span ng-if="msg.user_id !== msg.state_key && msg.prev_content">
-                                {{ members[msg.user_id].displayname || msg.user_id }}
+                                {{ msg.content.displayname || members[msg.user_id].displayname || msg.user_id }}
                                 {{ {"invite": "kicked", "join": "kicked", "ban": "unbanned"}[msg.prev_content.membership] }}
-                                {{ members[msg.state_key].displayname || msg.state_key }}
+                                {{ msg.__target_room_member.content.displayname || msg.state_key }}
                                 <span ng-if="'join' === msg.prev_content.membership && msg.content.reason">
                                     : {{ msg.content.reason }}
                                 </span>
@@ -156,9 +159,9 @@
                         </span>
                         <span ng-if="'invite' === msg.content.membership && msg.changedKey === 'membership' || 
                                      'ban' === msg.content.membership && msg.changedKey === 'membership'">
-                            {{ members[msg.user_id].displayname || msg.user_id }}
+                            {{ msg.__room_member.cnt.displayname || msg.user_id }}
                             {{ {"invite": "invited", "ban": "banned"}[msg.content.membership] }}
-                            {{ members[msg.state_key].displayname || msg.state_key }}
+                            {{ msg.__target_room_member.cnt.displayname || msg.state_key }}
                             <span ng-if="msg.prev_content && 'ban' === msg.prev_content.membership && msg.content.reason">
                                 : {{ msg.content.reason }}
                             </span>
@@ -179,8 +182,8 @@
                                                                                         (msg.content.formatted_body | unsanitizedLinky) :
                                              (msg.content.msgtype === 'm.text' && msg.type === 'm.room.message') ? (msg.content.body | linky:'_blank') : '' "/>
 
-                        <span ng-show='msg.type === "m.call.invite" && msg.user_id == state.user_id'>Outgoing Call{{ isWebRTCSupported ? '' : ' (But your browser does not support VoIP)' }}</span>
-                        <span ng-show='msg.type === "m.call.invite" && msg.user_id != state.user_id'>Incoming Call{{ isWebRTCSupported ? '' : ' (But your browser does not support VoIP)' }}</span>
+                        <span ng-show='msg.type === "m.call.invite" && msg.user_id == state.user_id'>Outgoing Call{{ isWebRTCSupported() ? '' : ' (But your browser does not support VoIP)' }}</span>
+                        <span ng-show='msg.type === "m.call.invite" && msg.user_id != state.user_id'>Incoming Call{{ isWebRTCSupported() ? '' : ' (But your browser does not support VoIP)' }}</span>
 
                         <div ng-show='msg.content.msgtype === "m.image"'>
                             <div ng-hide='msg.content.thumbnail_url' ng-style="msg.content.body.h && { 'height' : (msg.content.body.h < 320) ? msg.content.body.h : 320}">
@@ -204,7 +207,7 @@
                 </td>
                 <td class="rightBlock">
                     <img class="avatarImage" ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.png' }}" width="32" height="32"
-                         ng-hide="events.rooms[room_id].messages[$index - 1].user_id === msg.user_id || msg.user_id !== state.user_id"/>
+                         ng-hide="room.events[$index - 1].user_id === msg.user_id || msg.user_id !== state.user_id"/>
                 </td>
             </tr>
         </table>
@@ -245,15 +248,15 @@
                 <button ng-click="leaveRoom()" ng-disabled="state.permission_denied">Leave</button>
                 <button ng-click="startVoiceCall()"
                         ng-show="(currentCall == undefined || currentCall.state == 'ended')"
-                        ng-disabled="state.permission_denied || !isWebRTCSupported || memberCount() != 2"
-                        title ="{{ !isWebRTCSupported ? 'VoIP requires webRTC but your browser does not support it' : (memberCount() == 2 ? '' : 'VoIP calls can only be made in rooms with two participants') }}"
+                        ng-disabled="state.permission_denied || !isWebRTCSupported() || memberCount() != 2"
+                        title ="{{ !isWebRTCSupported() ? 'VoIP requires webRTC but your browser does not support it' : (memberCount() == 2 ? '' : 'VoIP calls can only be made in rooms with two participants') }}"
                         >
                     Voice Call
                 </button>
                 <button ng-click="startVideoCall()"
                         ng-show="(currentCall == undefined || currentCall.state == 'ended')"
-                        ng-disabled="state.permission_denied || !isWebRTCSupported || memberCount() != 2"
-                        title ="{{ !isWebRTCSupported ? 'VoIP requires webRTC but your browser does not support it' : (memberCount() == 2 ? '' : 'VoIP calls can only be made in rooms with two participants') }}"
+                        ng-disabled="state.permission_denied || !isWebRTCSupported() || memberCount() != 2"
+                        title ="{{ !isWebRTCSupported() ? 'VoIP requires webRTC but your browser does not support it' : (memberCount() == 2 ? '' : 'VoIP calls can only be made in rooms with two participants') }}"
                         >
                     Video Call
                 </button>
diff --git a/webclient/settings/settings-controller.js b/syweb/webclient/settings/settings-controller.js
index 9cdace704a..9cdace704a 100644
--- a/webclient/settings/settings-controller.js
+++ b/syweb/webclient/settings/settings-controller.js
diff --git a/webclient/settings/settings.html b/syweb/webclient/settings/settings.html
index 094c846f8b..094c846f8b 100644
--- a/webclient/settings/settings.html
+++ b/syweb/webclient/settings/settings.html
diff --git a/webclient/test/README b/syweb/webclient/test/README
index 1a7bc832c7..e7ed4eaa87 100644
--- a/webclient/test/README
+++ b/syweb/webclient/test/README
@@ -1,13 +1,31 @@
-Requires:
- - nodejs/npm
- - npm install karma
+Testing is done using Karma.
+
+
+UNIT TESTING
+============
+
+Requires the following:
+ - npm/nodejs
+ - phantomjs
+
+Requires the following node packages:
  - npm install jasmine
- - npm install protractor (e2e testing)
+ - npm install karma
+ - npm install karma-jasmine
+ - npm install karma-phantomjs-launcher
+ - npm install karma-junit-reporter
 
-Setting up continuous integration / run the unit tests (make sure you're in
-this directory so it can find the config file):
+Make sure you're in this directory so it can find the config file and run:
   karma start
 
+You should see all the tests pass.
+
+
+E2E TESTING
+===========
+
+npm install protractor
+
 
 Setting up e2e tests (only if you don't have a selenium server to run the tests
 on. If you do, edit the config to point to that url):
diff --git a/webclient/test/e2e/home.spec.js b/syweb/webclient/test/e2e/home.spec.js
index 470237d557..470237d557 100644
--- a/webclient/test/e2e/home.spec.js
+++ b/syweb/webclient/test/e2e/home.spec.js
diff --git a/webclient/test/karma.conf.js b/syweb/webclient/test/karma.conf.js
index 22c4eaaafa..7ce958adc9 100644
--- a/webclient/test/karma.conf.js
+++ b/syweb/webclient/test/karma.conf.js
@@ -23,18 +23,24 @@ module.exports = function(config) {
       '../js/angular-animate.js',
       '../js/angular-sanitize.js',
       '../js/ng-infinite-scroll-matrix.js',
-      '../login/**/*.*',
-      '../room/**/*.*',
-      '../components/**/*.*',
-      '../user/**/*.*',
-      '../home/**/*.*',
-      '../recents/**/*.*',
-      '../settings/**/*.*',
+      '../js/ui-bootstrap*',
+      '../js/elastic.js',  
+      '../login/**/*.js',
+      '../room/**/*.js',
+      '../components/**/*.js',
+      '../user/**/*.js',
+      '../home/**/*.js',
+      '../recents/**/*.js',
+      '../settings/**/*.js',
       '../app.js',
       '../app*',
       './unit/**/*.js'
     ],
 
+    plugins: [
+        'karma-*',
+    ],
+
 
     // list of files to exclude
     exclude: [
@@ -50,8 +56,11 @@ module.exports = function(config) {
     // test results reporter to use
     // possible values: 'dots', 'progress'
     // available reporters: https://npmjs.org/browse/keyword/karma-reporter
-    reporters: ['progress'],
-
+    reporters: ['progress', 'junit'],
+    junitReporter: {
+        outputFile: 'test-results.xml',
+        suite: ''
+    },
 
     // web server port
     port: 9876,
@@ -72,11 +81,11 @@ module.exports = function(config) {
 
     // start these browsers
     // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
-    browsers: ['Chrome'],
+    browsers: ['PhantomJS'],
 
 
     // Continuous Integration mode
     // if true, Karma captures browsers, runs the tests and exits
-    singleRun: false
+    singleRun: true
   });
 };
diff --git a/webclient/test/protractor.conf.js b/syweb/webclient/test/protractor.conf.js
index 76ae7b712b..76ae7b712b 100644
--- a/webclient/test/protractor.conf.js
+++ b/syweb/webclient/test/protractor.conf.js
diff --git a/syweb/webclient/test/unit/event-handler-service.spec.js b/syweb/webclient/test/unit/event-handler-service.spec.js
new file mode 100644
index 0000000000..2a4dc3b5a5
--- /dev/null
+++ b/syweb/webclient/test/unit/event-handler-service.spec.js
@@ -0,0 +1,117 @@
+describe('EventHandlerService', function() {
+    var scope;
+    
+    var modelService = {};
+
+    // setup the service and mocked dependencies
+    beforeEach(function() {
+        // dependencies
+        module('matrixService');
+        module('notificationService');
+        module('mPresence');
+        
+        // cleanup mocked methods
+        modelService = {};
+        
+        // mocked dependencies
+        module(function ($provide) {
+          $provide.value('modelService', modelService);
+        });
+        
+        // tested service
+        module('eventHandlerService');
+    });
+    
+    beforeEach(inject(function($rootScope) {
+        scope = $rootScope;
+    }));
+
+    it('should be able to get the number of joined users in a room', inject(
+    function(eventHandlerService) {
+        var roomId = "!foo:matrix.org";
+        // set mocked data
+        modelService.getRoom = function(roomId) {
+            return {
+                room_id: roomId,
+                current_room_state: {
+                    members: {
+                        "@adam:matrix.org": {
+                            event: {
+                                content: { membership: "join" },
+                                user_id: "@adam:matrix.org"
+                            }
+                        },
+                        "@beth:matrix.org": {
+                            event: {
+                                content: { membership: "invite" },
+                                user_id: "@beth:matrix.org"
+                            }
+                        },
+                        "@charlie:matrix.org": {
+                            event: {
+                                content: { membership: "join" },
+                                user_id: "@charlie:matrix.org"
+                            }
+                        },
+                        "@danice:matrix.org": {
+                            event: {
+                                content: { membership: "leave" },
+                                user_id: "@danice:matrix.org"
+                            }
+                        }
+                    }
+                }
+            };
+        }
+        
+        var num = eventHandlerService.getUsersCountInRoom(roomId);
+        expect(num).toEqual(2);
+    }));
+    
+    it('should be able to get a users power level', inject(
+    function(eventHandlerService) {
+        var roomId = "!foo:matrix.org";
+        // set mocked data
+        modelService.getRoom = function(roomId) {
+            return {
+                room_id: roomId,
+                current_room_state: {
+                    members: {
+                        "@adam:matrix.org": {
+                            event: {
+                                content: { membership: "join" },
+                                user_id: "@adam:matrix.org"
+                            }
+                        },
+                        "@beth:matrix.org": {
+                            event: {
+                                content: { membership: "join" },
+                                user_id: "@beth:matrix.org"
+                            }
+                        }
+                    },
+                    s: {
+                        "m.room.power_levels": {
+                            content: {
+                                "@adam:matrix.org": 90,
+                                "default": 50
+                            }
+                        }
+                    },
+                    state: function(type, key) { 
+                        return key ? this.s[type+key] : this.s[type]
+                    }
+                }
+            };
+        };
+        
+        var num = eventHandlerService.getUserPowerLevel(roomId, "@beth:matrix.org");
+        expect(num).toEqual(50);
+        
+        num = eventHandlerService.getUserPowerLevel(roomId, "@adam:matrix.org");
+        expect(num).toEqual(90);
+        
+        num = eventHandlerService.getUserPowerLevel(roomId, "@unknown:matrix.org");
+        expect(num).toEqual(50);
+    }));
+});
diff --git a/syweb/webclient/test/unit/filters.spec.js b/syweb/webclient/test/unit/filters.spec.js
new file mode 100644
index 0000000000..7324a8e028
--- /dev/null
+++ b/syweb/webclient/test/unit/filters.spec.js
@@ -0,0 +1,488 @@
+describe('mRoomName filter', function() {
+    var filter, mRoomName;
+    
+    var roomId = "!weufhewifu:matrix.org";
+    
+    // test state values (f.e. test)
+    var testUserId, testAlias, testDisplayName, testOtherDisplayName, testRoomState;
+    
+    // mocked services which return the test values above.
+    var matrixService = {
+        getRoomIdToAliasMapping: function(room_id) {
+            return testAlias;
+        },
+        
+        config: function() {
+            return {
+                user_id: testUserId
+            };
+        }
+    };
+    
+    var eventHandlerService = {
+        getUserDisplayName: function(room_id, user_id) {
+            if (user_id === testUserId) {
+                return testDisplayName;
+            }
+            return testOtherDisplayName;
+        }
+    };
+    
+    var modelService = {
+        getRoom: function(room_id) {
+            return {
+                current_room_state: testRoomState
+            };
+        }
+    };
+    
+    beforeEach(function() {
+        // inject mocked dependencies
+        module(function ($provide) {
+            $provide.value('matrixService', matrixService);
+            $provide.value('eventHandlerService', eventHandlerService);
+            $provide.value('modelService', modelService);
+        });
+        
+        module('matrixFilter');
+    });
+    
+    beforeEach(inject(function($filter) {
+        filter = $filter;
+        mRoomName = filter("mRoomName");
+        
+        // purge the previous test values
+        testUserId = undefined;
+        testAlias = undefined;
+        testDisplayName = undefined;
+        testOtherDisplayName = undefined;
+        
+        // mock up a stub room state
+        testRoomState = {
+            s:{}, // internal; stores the state events
+            state: function(type, key) {
+                // accessor used by filter
+                return key ? this.s[type+key] : this.s[type];
+            },
+            members: {}, // struct used by filter
+            
+            // test helper methods
+            setJoinRule: function(rule) {
+                this.s["m.room.join_rules"] = {
+                    content: {
+                        join_rule: rule
+                    }
+                };
+            },
+            setRoomName: function(name) {
+                this.s["m.room.name"] = {
+                    content: {
+                        name: name
+                    }
+                };
+            },
+            setMember: function(user_id, membership, inviter_user_id) {
+                if (!inviter_user_id) {
+                    inviter_user_id = user_id;
+                }
+                this.s["m.room.member" + user_id] = {
+                    event: {
+                        content: {
+                            membership: membership
+                        },
+                        state_key: user_id,
+                        user_id: inviter_user_id 
+                    }
+                };
+                this.members[user_id] = this.s["m.room.member" + user_id];
+            }
+        };
+    }));
+    
+    /**** ROOM NAME ****/
+    
+    it("should show the room name if one exists for private (invite join_rules) rooms.", function() {
+        var roomName = "The Room Name";
+        testUserId = "@me:matrix.org";
+        testRoomState.setJoinRule("invite");
+        testRoomState.setRoomName(roomName);
+        testRoomState.setMember(testUserId, "join");
+        var output = mRoomName(roomId);
+        expect(output).toEqual(roomName);
+    });
+    
+    it("should show the room name if one exists for public (public join_rules) rooms.", function() {
+        var roomName = "The Room Name";
+        testUserId = "@me:matrix.org";
+        testRoomState.setJoinRule("public");
+        testRoomState.setRoomName(roomName);
+        testRoomState.setMember(testUserId, "join");
+        var output = mRoomName(roomId);
+        expect(output).toEqual(roomName);
+    });
+    
+    /**** ROOM ALIAS ****/
+    
+    it("should show the room alias if one exists for private (invite join_rules) rooms if a room name doesn't exist.", function() {
+        testAlias = "#thealias:matrix.org";
+        testUserId = "@me:matrix.org";
+        testRoomState.setJoinRule("invite");
+        testRoomState.setMember(testUserId, "join");
+        var output = mRoomName(roomId);
+        expect(output).toEqual(testAlias);
+    });
+    
+    it("should show the room alias if one exists for public (public join_rules) rooms if a room name doesn't exist.", function() {
+        testAlias = "#thealias:matrix.org";
+        testUserId = "@me:matrix.org";
+        testRoomState.setJoinRule("public");
+        testRoomState.setMember(testUserId, "join");
+        var output = mRoomName(roomId);
+        expect(output).toEqual(testAlias);
+    });
+    
+    /**** ROOM ID ****/
+    
+    it("should show the room ID for public (public join_rules) rooms if a room name and alias don't exist.", function() {
+        testUserId = "@me:matrix.org";
+        testRoomState.setJoinRule("public");
+        testRoomState.setMember(testUserId, "join");
+        var output = mRoomName(roomId);
+        expect(output).toEqual(roomId);
+    });
+    
+    it("should show the room ID for private (invite join_rules) rooms if a room name and alias don't exist and there are >2 members.", function() {
+        testUserId = "@me:matrix.org";
+        testRoomState.setJoinRule("public");
+        testRoomState.setMember(testUserId, "join");
+        testRoomState.setMember("@alice:matrix.org", "join");
+        testRoomState.setMember("@bob:matrix.org", "join");
+        var output = mRoomName(roomId);
+        expect(output).toEqual(roomId);
+    });
+    
+    /**** SELF-CHAT ****/
+    
+    it("should show your display name for private (invite join_rules) rooms if a room name and alias don't exist and it is a self-chat.", function() {
+        testUserId = "@me:matrix.org";
+        testDisplayName = "Me";
+        testRoomState.setJoinRule("private");
+        testRoomState.setMember(testUserId, "join");
+        var output = mRoomName(roomId);
+        expect(output).toEqual(testDisplayName);
+    });
+    
+    it("should show your user ID for private (invite join_rules) rooms if a room name and alias don't exist and it is a self-chat and they don't have a display name set.", function() {
+        testUserId = "@me:matrix.org";
+        testRoomState.setJoinRule("private");
+        testRoomState.setMember(testUserId, "join");
+        var output = mRoomName(roomId);
+        expect(output).toEqual(testUserId);
+    });
+    
+    /**** ONE-TO-ONE CHAT ****/
+    
+    it("should show the other user's display name for private (invite join_rules) rooms if a room name and alias don't exist and it is a 1:1-chat.", function() {
+        testUserId = "@me:matrix.org";
+        otherUserId = "@alice:matrix.org";
+        testOtherDisplayName = "Alice";
+        testRoomState.setJoinRule("private");
+        testRoomState.setMember(testUserId, "join");
+        testRoomState.setMember("@alice:matrix.org", "join");
+        var output = mRoomName(roomId);
+        expect(output).toEqual(testOtherDisplayName);
+    });
+    
+    it("should show the other user's ID for private (invite join_rules) rooms if a room name and alias don't exist and it is a 1:1-chat and they don't have a display name set.", function() {
+        testUserId = "@me:matrix.org";
+        otherUserId = "@alice:matrix.org";
+        testRoomState.setJoinRule("private");
+        testRoomState.setMember(testUserId, "join");
+        testRoomState.setMember("@alice:matrix.org", "join");
+        var output = mRoomName(roomId);
+        expect(output).toEqual(otherUserId);
+    });
+    
+    /**** INVITED TO ROOM ****/
+    
+    it("should show the other user's display name for private (invite join_rules) rooms if you are invited to it.", function() {
+        testUserId = "@me:matrix.org";
+        testDisplayName = "Me";
+        otherUserId = "@alice:matrix.org";
+        testOtherDisplayName = "Alice";
+        testRoomState.setJoinRule("private");
+        testRoomState.setMember(testUserId, "join");
+        testRoomState.setMember(otherUserId, "join");
+        testRoomState.setMember(testUserId, "invite");
+        var output = mRoomName(roomId);
+        expect(output).toEqual(testOtherDisplayName);
+    });
+    
+    it("should show the other user's ID for private (invite join_rules) rooms if you are invited to it and the inviter doesn't have a display name.", function() {
+        testUserId = "@me:matrix.org";
+        testDisplayName = "Me";
+        otherUserId = "@alice:matrix.org";
+        testRoomState.setJoinRule("private");
+        testRoomState.setMember(testUserId, "join");
+        testRoomState.setMember(otherUserId, "join");
+        testRoomState.setMember(testUserId, "invite");
+        var output = mRoomName(roomId);
+        expect(output).toEqual(otherUserId);
+    });
+});
+
+describe('duration filter', function() {
+    var filter, durationFilter;
+    
+    beforeEach(module('matrixWebClient'));
+    beforeEach(inject(function($filter) {
+        filter = $filter;
+        durationFilter = filter("duration");
+    }));
+    
+    it("should represent 15000 ms as '15s'", function() {
+        var output = durationFilter(15000);
+        expect(output).toEqual("15s");
+    });
+    
+    it("should represent 60000 ms as '1m'", function() {
+        var output = durationFilter(60000);
+        expect(output).toEqual("1m");
+    });
+    
+    it("should represent 65000 ms as '1m'", function() {
+        var output = durationFilter(65000);
+        expect(output).toEqual("1m");
+    });
+    
+    it("should represent 10 ms as '0s'", function() {
+        var output = durationFilter(10);
+        expect(output).toEqual("0s");
+    });
+    
+    it("should represent 4m as '4m'", function() {
+        var output = durationFilter(1000*60*4);
+        expect(output).toEqual("4m");
+    });
+    
+    it("should represent 4m30s as '4m'", function() {
+        var output = durationFilter(1000*60*4 + 1000*30);
+        expect(output).toEqual("4m");
+    });
+    
+    it("should represent 2h as '2h'", function() {
+        var output = durationFilter(1000*60*60*2);
+        expect(output).toEqual("2h");
+    });
+    
+    it("should represent 2h35m as '2h'", function() {
+        var output = durationFilter(1000*60*60*2 + 1000*60*35);
+        expect(output).toEqual("2h");
+    });
+});
+
+describe('orderMembersList filter', function() {
+    var filter, orderMembersList;
+    
+    beforeEach(module('matrixWebClient'));
+    beforeEach(inject(function($filter) {
+        filter = $filter;
+        orderMembersList = filter("orderMembersList");
+    }));
+    
+    it("should sort a single entry", function() {
+        var output = orderMembersList({
+            "@a:example.com": {
+                last_active_ago: 50,
+                last_updated: 1415266943964
+            }
+        });
+        expect(output).toEqual([{
+                id: "@a:example.com",
+                last_active_ago: 50,
+                last_updated: 1415266943964
+        }]);
+    });
+    
+    it("should sort by taking last_active_ago into account", function() {
+        var output = orderMembersList({
+            "@a:example.com": {
+                last_active_ago: 1000,
+                last_updated: 1415266943964
+            },
+            "@b:example.com": {
+                last_active_ago: 50,
+                last_updated: 1415266943964
+            },
+            "@c:example.com": {
+                last_active_ago: 99999,
+                last_updated: 1415266943964
+            }
+        });
+        expect(output).toEqual([
+            {
+                id: "@b:example.com",
+                last_active_ago: 50,
+                last_updated: 1415266943964
+            },
+            {
+                id: "@a:example.com",
+                last_active_ago: 1000,
+                last_updated: 1415266943964
+            },
+            {
+                id: "@c:example.com",
+                last_active_ago: 99999,
+                last_updated: 1415266943964
+            },
+        ]);
+    });
+    
+    it("should sort by taking last_updated into account", function() {
+        var output = orderMembersList({
+            "@a:example.com": {
+                last_active_ago: 1000,
+                last_updated: 1415266943964
+            },
+            "@b:example.com": {
+                last_active_ago: 1000,
+                last_updated: 1415266900000
+            },
+            "@c:example.com": {
+                last_active_ago: 1000,
+                last_updated: 1415266943000
+            }
+        });
+        expect(output).toEqual([
+            {
+                id: "@a:example.com",
+                last_active_ago: 1000,
+                last_updated: 1415266943964
+            },
+            {
+                id: "@c:example.com",
+                last_active_ago: 1000,
+                last_updated: 1415266943000
+            },
+            {
+                id: "@b:example.com",
+                last_active_ago: 1000,
+                last_updated: 1415266900000
+            },
+        ]);
+    });
+    
+    it("should sort by taking last_updated and last_active_ago into account", 
+    function() {
+        var output = orderMembersList({
+            "@a:example.com": {
+                last_active_ago: 1000,
+                last_updated: 1415266943000
+            },
+            "@b:example.com": {
+                last_active_ago: 100000,
+                last_updated: 1415266943900
+            },
+            "@c:example.com": {
+                last_active_ago: 1000,
+                last_updated: 1415266943964
+            }
+        });
+        expect(output).toEqual([
+            {
+                id: "@c:example.com",
+                last_active_ago: 1000,
+                last_updated: 1415266943964
+            },
+            {
+                id: "@a:example.com",
+                last_active_ago: 1000,
+                last_updated: 1415266943000
+            },
+            {
+                id: "@b:example.com",
+                last_active_ago: 100000,
+                last_updated: 1415266943900
+            },
+        ]);
+    });
+    
+    // SYWEB-26 comment
+    it("should sort members who do not have last_active_ago value at the end of the list", 
+    function() {
+        // single undefined entry
+        var output = orderMembersList({
+            "@a:example.com": {
+                last_active_ago: 1000,
+                last_updated: 1415266943964
+            },
+            "@b:example.com": {
+                last_active_ago: 100000,
+                last_updated: 1415266943964
+            },
+            "@c:example.com": {
+                last_active_ago: undefined,
+                last_updated: 1415266943964
+            }
+        });
+        expect(output).toEqual([
+            {
+                id: "@a:example.com",
+                last_active_ago: 1000,
+                last_updated: 1415266943964
+            },
+            {
+                id: "@b:example.com",
+                last_active_ago: 100000,
+                last_updated: 1415266943964
+            },
+            {
+                id: "@c:example.com",
+                last_active_ago: undefined,
+                last_updated: 1415266943964
+            },
+        ]);
+    });
+    
+    it("should sort multiple members who do not have last_active_ago according to presence", 
+    function() {
+        // single undefined entry
+        var output = orderMembersList({
+            "@a:example.com": {
+                last_active_ago: undefined,
+                last_updated: 1415266943964,
+                presence: "unavailable"
+            },
+            "@b:example.com": {
+                last_active_ago: undefined,
+                last_updated: 1415266943964,
+                presence: "online"
+            },
+            "@c:example.com": {
+                last_active_ago: undefined,
+                last_updated: 1415266943964,
+                presence: "offline"
+            }
+        });
+        expect(output).toEqual([
+            {
+                id: "@b:example.com",
+                last_active_ago: undefined,
+                last_updated: 1415266943964,
+                presence: "online"
+            },
+            {
+                id: "@a:example.com",
+                last_active_ago: undefined,
+                last_updated: 1415266943964,
+                presence: "unavailable"
+            },
+            {
+                id: "@c:example.com",
+                last_active_ago: undefined,
+                last_updated: 1415266943964,
+                presence: "offline"
+            },
+        ]);
+    });
+});
diff --git a/syweb/webclient/test/unit/matrix-service.spec.js b/syweb/webclient/test/unit/matrix-service.spec.js
new file mode 100644
index 0000000000..4959f2395d
--- /dev/null
+++ b/syweb/webclient/test/unit/matrix-service.spec.js
@@ -0,0 +1,504 @@
+describe('MatrixService', function() {
+    var scope, httpBackend;
+    var BASE = "http://example.com";
+    var PREFIX = "/_matrix/client/api/v1";
+    var URL = BASE + PREFIX;
+    var roomId = "!wejigf387t34:matrix.org";
+    
+    var CONFIG = {
+        access_token: "foobar",
+        homeserver: BASE
+    };
+    
+    beforeEach(module('matrixService'));
+
+    beforeEach(inject(function($rootScope, $httpBackend) {
+        httpBackend = $httpBackend;
+        scope = $rootScope;
+    }));
+
+    afterEach(function() {
+        httpBackend.verifyNoOutstandingExpectation();
+        httpBackend.verifyNoOutstandingRequest();
+    });
+
+    it('should be able to POST /createRoom with an alias', inject(
+    function(matrixService) {
+        matrixService.setConfig(CONFIG);
+        var alias = "flibble";
+        matrixService.create(alias).then(function(response) {
+            expect(response.data).toEqual({});
+        });
+
+        httpBackend.expectPOST(URL + "/createRoom?access_token=foobar",
+            {
+                room_alias_name: alias
+            })
+            .respond({});
+        httpBackend.flush();
+    }));
+
+    it('should be able to GET /initialSync', inject(function(matrixService) {
+        matrixService.setConfig(CONFIG);
+        var limit = 15;
+        matrixService.initialSync(limit).then(function(response) {
+            expect(response.data).toEqual([]);
+        });
+
+        httpBackend.expectGET(
+            URL + "/initialSync?access_token=foobar&limit=15")
+            .respond([]);
+        httpBackend.flush();
+    }));
+    
+    it('should be able to GET /rooms/$roomid/state', inject(
+    function(matrixService) {
+        matrixService.setConfig(CONFIG);
+        matrixService.roomState(roomId).then(function(response) {
+            expect(response.data).toEqual([]);
+        });
+
+        httpBackend.expectGET(
+            URL + "/rooms/" + encodeURIComponent(roomId) + 
+            "/state?access_token=foobar")
+            .respond([]);
+        httpBackend.flush();
+    }));
+    
+    it('should be able to POST /join', inject(function(matrixService) {
+        matrixService.setConfig(CONFIG);
+        matrixService.joinAlias(roomId).then(function(response) {
+            expect(response.data).toEqual({});
+        });
+
+        httpBackend.expectPOST(
+            URL + "/join/" + encodeURIComponent(roomId) + 
+            "?access_token=foobar",
+            {})
+            .respond({});
+        httpBackend.flush();
+    }));
+    
+    it('should be able to POST /rooms/$roomid/join', inject(
+    function(matrixService) {
+        matrixService.setConfig(CONFIG);
+        matrixService.join(roomId).then(function(response) {
+            expect(response.data).toEqual({});
+        });
+
+        httpBackend.expectPOST(
+            URL + "/rooms/" + encodeURIComponent(roomId) + 
+            "/join?access_token=foobar",
+            {})
+            .respond({});
+        httpBackend.flush();
+    }));
+    
+    it('should be able to POST /rooms/$roomid/invite', inject(
+    function(matrixService) {
+        matrixService.setConfig(CONFIG);
+        var inviteUserId = "@user:example.com";
+        matrixService.invite(roomId, inviteUserId).then(function(response) {
+            expect(response.data).toEqual({});
+        });
+
+        httpBackend.expectPOST(
+            URL + "/rooms/" + encodeURIComponent(roomId) + 
+            "/invite?access_token=foobar",
+            {
+                user_id: inviteUserId
+            })
+            .respond({});
+        httpBackend.flush();
+    }));
+    
+    it('should be able to POST /rooms/$roomid/leave', inject(
+    function(matrixService) {
+        matrixService.setConfig(CONFIG);
+        matrixService.leave(roomId).then(function(response) {
+            expect(response.data).toEqual({});
+        });
+
+        httpBackend.expectPOST(
+            URL + "/rooms/" + encodeURIComponent(roomId) + 
+            "/leave?access_token=foobar",
+            {})
+            .respond({});
+        httpBackend.flush();
+    }));
+    
+    it('should be able to POST /rooms/$roomid/ban', inject(
+    function(matrixService) {
+        matrixService.setConfig(CONFIG);
+        var userId = "@example:example.com";
+        var reason = "Because.";
+        matrixService.ban(roomId, userId, reason).then(function(response) {
+            expect(response.data).toEqual({});
+        });
+
+        httpBackend.expectPOST(
+            URL + "/rooms/" + encodeURIComponent(roomId) + 
+            "/ban?access_token=foobar",
+            {
+                user_id: userId,
+                reason: reason
+            })
+            .respond({});
+        httpBackend.flush();
+    }));
+    
+    it('should be able to GET /directory/room/$alias', inject(
+    function(matrixService) {
+        matrixService.setConfig(CONFIG);
+        var alias = "#test:example.com";
+        var roomId = "!wefuhewfuiw:example.com";
+        matrixService.resolveRoomAlias(alias).then(function(response) {
+            expect(response.data).toEqual({
+                room_id: roomId
+            });
+        });
+
+        httpBackend.expectGET(
+            URL + "/directory/room/" + encodeURIComponent(alias) +
+                    "?access_token=foobar")
+            .respond({
+                room_id: roomId
+            });
+        httpBackend.flush();
+    }));
+    
+    it('should be able to send m.room.name', inject(function(matrixService) {
+        matrixService.setConfig(CONFIG);
+        var roomId = "!fh38hfwfwef:example.com";
+        var name = "Room Name";
+        matrixService.setName(roomId, name).then(function(response) {
+            expect(response.data).toEqual({});
+        });
+
+        httpBackend.expectPUT(
+            URL + "/rooms/" + encodeURIComponent(roomId) + 
+            "/state/m.room.name?access_token=foobar",
+            {
+                name: name
+            })
+            .respond({});
+        httpBackend.flush();
+    }));
+    
+    it('should be able to send m.room.topic', inject(function(matrixService) {
+        matrixService.setConfig(CONFIG);
+        var roomId = "!fh38hfwfwef:example.com";
+        var topic = "A room topic can go here.";
+        matrixService.setTopic(roomId, topic).then(function(response) {
+            expect(response.data).toEqual({});
+        });
+
+        httpBackend.expectPUT(
+            URL + "/rooms/" + encodeURIComponent(roomId) + 
+            "/state/m.room.topic?access_token=foobar",
+            {
+                topic: topic
+            })
+            .respond({});
+        httpBackend.flush();
+    }));
+    
+    it('should be able to send generic state events without a state key', inject(
+    function(matrixService) {
+        matrixService.setConfig(CONFIG);
+        var roomId = "!fh38hfwfwef:example.com";
+        var eventType = "com.example.events.test";
+        var content = {
+            testing: "1 2 3"
+        };
+        matrixService.sendStateEvent(roomId, eventType, content).then(
+        function(response) {
+            expect(response.data).toEqual({});
+        });
+
+        httpBackend.expectPUT(
+            URL + "/rooms/" + encodeURIComponent(roomId) + "/state/" + 
+            encodeURIComponent(eventType) + "?access_token=foobar",
+            content)
+            .respond({});
+        httpBackend.flush();
+    }));
+    
+    // TODO: Skipped since the webclient is purposefully broken so as not to
+    // 500 matrix.org
+    xit('should be able to send generic state events with a state key', inject(
+    function(matrixService) {
+        matrixService.setConfig(CONFIG);
+        var roomId = "!fh38hfwfwef:example.com";
+        var eventType = "com.example.events.test:special@characters";
+        var content = {
+            testing: "1 2 3"
+        };
+        var stateKey = "version:1";
+        matrixService.sendStateEvent(roomId, eventType, content, stateKey).then(
+        function(response) {
+            expect(response.data).toEqual({});
+        });
+
+        httpBackend.expectPUT(
+            URL + "/rooms/" + encodeURIComponent(roomId) + "/state/" + 
+            encodeURIComponent(eventType) + "/" + encodeURIComponent(stateKey)+
+            "?access_token=foobar",
+            content)
+            .respond({});
+        httpBackend.flush();
+    }));
+    
+    it('should be able to PUT generic events ', inject(
+    function(matrixService) {
+        matrixService.setConfig(CONFIG);
+        var roomId = "!fh38hfwfwef:example.com";
+        var eventType = "com.example.events.test";
+        var txnId = "42";
+        var content = {
+            testing: "1 2 3"
+        };
+        matrixService.sendEvent(roomId, eventType, txnId, content).then(
+        function(response) {
+            expect(response.data).toEqual({});
+        });
+
+        httpBackend.expectPUT(
+            URL + "/rooms/" + encodeURIComponent(roomId) + "/send/" + 
+            encodeURIComponent(eventType) + "/" + encodeURIComponent(txnId)+
+            "?access_token=foobar",
+            content)
+            .respond({});
+        httpBackend.flush();
+    }));
+    
+    it('should be able to PUT text messages ', inject(
+    function(matrixService) {
+        matrixService.setConfig(CONFIG);
+        var roomId = "!fh38hfwfwef:example.com";
+        var body = "ABC 123";
+        matrixService.sendTextMessage(roomId, body).then(
+        function(response) {
+            expect(response.data).toEqual({});
+        });
+
+        httpBackend.expectPUT(
+            new RegExp(URL + "/rooms/" + encodeURIComponent(roomId) + 
+            "/send/m.room.message/(.*)" +
+            "?access_token=foobar"),
+            {
+                body: body,
+                msgtype: "m.text"
+            })
+            .respond({});
+        httpBackend.flush();
+    }));
+    
+    it('should be able to PUT emote messages ', inject(
+    function(matrixService) {
+        matrixService.setConfig(CONFIG);
+        var roomId = "!fh38hfwfwef:example.com";
+        var body = "ABC 123";
+        matrixService.sendEmoteMessage(roomId, body).then(
+        function(response) {
+            expect(response.data).toEqual({});
+        });
+
+        httpBackend.expectPUT(
+            new RegExp(URL + "/rooms/" + encodeURIComponent(roomId) + 
+            "/send/m.room.message/(.*)" +
+            "?access_token=foobar"),
+            {
+                body: body,
+                msgtype: "m.emote"
+            })
+            .respond({});
+        httpBackend.flush();
+    }));
+    
+    it('should be able to POST redactions', inject(
+    function(matrixService) {
+        matrixService.setConfig(CONFIG);
+        var roomId = "!fh38hfwfwef:example.com";
+        var eventId = "fwefwexample.com";
+        matrixService.redactEvent(roomId, eventId).then(
+        function(response) {
+            expect(response.data).toEqual({});
+        });
+
+        httpBackend.expectPOST(URL + "/rooms/" + encodeURIComponent(roomId) + 
+            "/redact/" + encodeURIComponent(eventId) +
+            "?access_token=foobar")
+            .respond({});
+        httpBackend.flush();
+    }));
+    
+    it('should be able to GET /directory/room/$alias', inject(
+    function(matrixService) {
+        matrixService.setConfig(CONFIG);
+        var alias = "#test:example.com";
+        var roomId = "!wefuhewfuiw:example.com";
+        matrixService.resolveRoomAlias(alias).then(function(response) {
+            expect(response.data).toEqual({
+                room_id: roomId
+            });
+        });
+
+        httpBackend.expectGET(
+            URL + "/directory/room/" + encodeURIComponent(alias) +
+                    "?access_token=foobar")
+            .respond({
+                room_id: roomId
+            });
+        httpBackend.flush();
+    }));
+    
+    it('should be able to GET /rooms/$roomid/members', inject(
+    function(matrixService) {
+        matrixService.setConfig(CONFIG);
+        var roomId = "!wefuhewfuiw:example.com";
+        matrixService.getMemberList(roomId).then(function(response) {
+            expect(response.data).toEqual({});
+        });
+
+        httpBackend.expectGET(
+            URL + "/rooms/" + encodeURIComponent(roomId) +
+                    "/members?access_token=foobar")
+            .respond({});
+        httpBackend.flush();
+    }));
+    
+    it('should be able to paginate a room', inject(
+    function(matrixService) {
+        matrixService.setConfig(CONFIG);
+        var roomId = "!wefuhewfuiw:example.com";
+        var from = "3t_44e_54z";
+        var limit = 20;
+        matrixService.paginateBackMessages(roomId, from, limit).then(function(response) {
+            expect(response.data).toEqual({});
+        });
+
+        httpBackend.expectGET(
+            URL + "/rooms/" + encodeURIComponent(roomId) +
+                    "/messages?access_token=foobar&dir=b&from="+
+                    encodeURIComponent(from)+"&limit="+limit)
+            .respond({});
+        httpBackend.flush();
+    }));
+    
+    it('should be able to GET /publicRooms', inject(
+    function(matrixService) {
+        matrixService.setConfig(CONFIG);
+        matrixService.publicRooms().then(function(response) {
+            expect(response.data).toEqual({});
+        });
+
+        httpBackend.expectGET(
+            new RegExp(URL + "/publicRooms(.*)"))
+            .respond({});
+        httpBackend.flush();
+    }));
+    
+    it('should be able to GET /profile/$userid/displayname', inject(
+    function(matrixService) {
+        matrixService.setConfig(CONFIG);
+        var userId = "@foo:example.com";
+        matrixService.getDisplayName(userId).then(function(response) {
+            expect(response.data).toEqual({});
+        });
+
+        httpBackend.expectGET(URL + "/profile/" + encodeURIComponent(userId) +
+            "/displayname?access_token=foobar")
+            .respond({});
+        httpBackend.flush();
+    }));
+    
+    it('should be able to GET /profile/$userid/avatar_url', inject(
+    function(matrixService) {
+        matrixService.setConfig(CONFIG);
+        var userId = "@foo:example.com";
+        matrixService.getProfilePictureUrl(userId).then(function(response) {
+            expect(response.data).toEqual({});
+        });
+
+        httpBackend.expectGET(URL + "/profile/" + encodeURIComponent(userId) +
+            "/avatar_url?access_token=foobar")
+            .respond({});
+        httpBackend.flush();
+    }));
+    
+    it('should be able to PUT /profile/$me/avatar_url', inject(
+    function(matrixService) {
+        var testConfig = angular.copy(CONFIG);
+        testConfig.user_id = "@bob:example.com";
+        matrixService.setConfig(testConfig);
+        var url = "http://example.com/mypic.jpg";
+        matrixService.setProfilePictureUrl(url).then(function(response) {
+            expect(response.data).toEqual({});
+        });
+        httpBackend.expectPUT(URL + "/profile/" + 
+            encodeURIComponent(testConfig.user_id) +
+            "/avatar_url?access_token=foobar",
+            {
+                avatar_url: url
+            })
+            .respond({});
+        httpBackend.flush();
+    }));
+    
+    it('should be able to PUT /profile/$me/displayname', inject(
+    function(matrixService) {
+        var testConfig = angular.copy(CONFIG);
+        testConfig.user_id = "@bob:example.com";
+        matrixService.setConfig(testConfig);
+        var displayname = "Bob Smith";
+        matrixService.setDisplayName(displayname).then(function(response) {
+            expect(response.data).toEqual({});
+        });
+        httpBackend.expectPUT(URL + "/profile/" + 
+            encodeURIComponent(testConfig.user_id) +
+            "/displayname?access_token=foobar",
+            {
+                displayname: displayname
+            })
+            .respond({});
+        httpBackend.flush();
+    }));
+    
+    it('should be able to login with password', inject(
+    function(matrixService) {
+        matrixService.setConfig(CONFIG);
+        var userId = "@bob:example.com";
+        var password = "monkey";
+        matrixService.login(userId, password).then(function(response) {
+            expect(response.data).toEqual({});
+        });
+        httpBackend.expectPOST(new RegExp(URL+"/login(.*)"),
+            {
+                user: userId,
+                password: password,
+                type: "m.login.password"
+            })
+            .respond({});
+        httpBackend.flush();
+    }));
+    
+    it('should be able to PUT presence status', inject(
+    function(matrixService) {
+        var testConfig = angular.copy(CONFIG);
+        testConfig.user_id = "@bob:example.com";
+        matrixService.setConfig(testConfig);
+        var status = "unavailable";
+        matrixService.setUserPresence(status).then(function(response) {
+            expect(response.data).toEqual({});
+        });
+        httpBackend.expectPUT(URL+"/presence/"+
+            encodeURIComponent(testConfig.user_id)+
+            "/status?access_token=foobar",
+            {
+                presence: status
+            })
+            .respond({});
+        httpBackend.flush();
+    }));
+});
diff --git a/syweb/webclient/test/unit/model-service.spec.js b/syweb/webclient/test/unit/model-service.spec.js
new file mode 100644
index 0000000000..e2fa8ceba3
--- /dev/null
+++ b/syweb/webclient/test/unit/model-service.spec.js
@@ -0,0 +1,30 @@
+describe('ModelService', function() {
+
+    // setup the dependencies
+    beforeEach(function() {
+        // dependencies
+        module('matrixService');
+        
+        // tested service
+        module('modelService');
+    });
+
+    it('should be able to get a member in a room', inject(
+    function(modelService) {
+        var roomId = "!wefiohwefuiow:matrix.org";
+        var userId = "@bob:matrix.org";
+        
+        modelService.getRoom(roomId).current_room_state.storeStateEvent({
+            type: "m.room.member",
+            id: "fwefw:matrix.org",
+            user_id: userId,
+            state_key: userId,
+            content: {
+                membership: "join"
+            }
+        });
+        
+        var user = modelService.getMember(roomId, userId);
+        expect(user.event.state_key).toEqual(userId);
+    }));
+});
diff --git a/syweb/webclient/test/unit/register-controller.spec.js b/syweb/webclient/test/unit/register-controller.spec.js
new file mode 100644
index 0000000000..b5c7842358
--- /dev/null
+++ b/syweb/webclient/test/unit/register-controller.spec.js
@@ -0,0 +1,84 @@
+describe("RegisterController ", function() {
+    var rootScope, scope, ctrl, $q, $timeout;
+    var userId = "@foo:bar";
+    var displayName = "Foo";
+    var avatarUrl = "avatar.url";
+    
+    window.webClientConfig = {
+        useCapatcha: false
+    };
+    
+    // test vars
+    var testRegisterData, testFailRegisterData;
+    
+    
+    // mock services
+    var matrixService = {
+        config: function() {
+            return {
+                user_id: userId
+            }
+        },
+        setConfig: function(){},
+        register: function(mxid, password, threepidCreds, useCaptcha) {
+            var d = $q.defer();
+            if (testFailRegisterData) {
+                d.reject({
+                    data: testFailRegisterData
+                });
+            }
+            else {
+                d.resolve({
+                    data: testRegisterData
+                });
+            }
+            return d.promise;
+        }
+    };
+    
+    var eventStreamService = {};
+    
+    beforeEach(function() {
+        module('matrixWebClient');
+        
+        // reset test vars
+        testRegisterData = undefined;
+        testFailRegisterData = undefined;
+    });
+
+    beforeEach(inject(function($rootScope, $injector, $location, $controller, _$q_, _$timeout_) {
+            $q = _$q_;
+            $timeout = _$timeout_;
+            scope = $rootScope.$new();
+            rootScope = $rootScope;
+            routeParams = {
+                user_matrix_id: userId
+            };
+            ctrl = $controller('RegisterController', {
+                '$scope': scope,
+                '$rootScope': $rootScope, 
+                '$location': $location,
+                'matrixService': matrixService,
+                'eventStreamService': eventStreamService
+            });
+        })
+    );
+
+    // SYWEB-109
+    it('should display an error if the HS rejects the username on registration', function() {
+        var prevFeedback = angular.copy(scope.feedback);
+    
+        testFailRegisterData = {
+            errcode: "M_UNKNOWN",
+            error: "I am rejecting you."
+        };
+    
+        scope.account.pwd1 = "password";
+        scope.account.pwd2 = "password";
+        scope.account.desired_user_id = "bob";
+        scope.register(); // this depends on the result of a deferred
+        rootScope.$digest(); // which is delivered after the digest
+        
+        expect(scope.feedback).not.toEqual(prevFeedback);
+    });
+});
diff --git a/webclient/test/unit/user-controller.spec.js b/syweb/webclient/test/unit/user-controller.spec.js
index 798cc4de48..798cc4de48 100644
--- a/webclient/test/unit/user-controller.spec.js
+++ b/syweb/webclient/test/unit/user-controller.spec.js
diff --git a/webclient/user/user-controller.js b/syweb/webclient/user/user-controller.js
index 0dbfa325d0..0dbfa325d0 100644
--- a/webclient/user/user-controller.js
+++ b/syweb/webclient/user/user-controller.js
diff --git a/webclient/user/user.html b/syweb/webclient/user/user.html
index 2aa981437b..2aa981437b 100644
--- a/webclient/user/user.html
+++ b/syweb/webclient/user/user.html
diff --git a/webclient/components/matrix/matrix-filter.js b/webclient/components/matrix/matrix-filter.js
deleted file mode 100644
index 3d64a569a1..0000000000
--- a/webclient/components/matrix/matrix-filter.js
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
- 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';
-
-angular.module('matrixFilter', [])
-
-// Compute the room name according to information we have
-.filter('mRoomName', ['$rootScope', 'matrixService', 'eventHandlerService', function($rootScope, matrixService, eventHandlerService) {
-    return function(room_id) {
-        var roomName;
-
-        // If there is an alias, use it
-        // TODO: only one alias is managed for now
-        var alias = matrixService.getRoomIdToAliasMapping(room_id);
-
-        var room = $rootScope.events.rooms[room_id];
-        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 && !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) {
-                            roomName = eventHandlerService.getUserDisplayName(room_id, member.state_key);
-                            break;
-                        }
-                    }
-                }
-                else if (Object.keys(room.members).length <= 1) {
-                    
-                    var otherUserId;
-
-                    if (Object.keys(room.members)[0]) {
-                        otherUserId = Object.keys(room.members)[0];
-                        // this could be an invite event (from event stream)
-                        if (otherUserId === user_id && 
-                                room.members[user_id].content.membership === "invite") {
-                            // this is us being invited to this room, so the
-                            // *user_id* is the other user ID and not the state
-                            // key.
-                            otherUserId = room.members[user_id].user_id;
-                        }
-                    }
-                    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.content.membership) {
-                                // Filter out the current user
-                                var member_id = message.state_key;
-                                if (member_id === user_id) {
-                                    member_id = message.user_id;
-                                }
-                                if (member_id !== user_id) {
-                                    // Make sure there is no duplicate user
-                                    if (-1 === invitedUserIDs.indexOf(member_id)) {
-                                        invitedUserIDs.push(member_id);
-                                    }
-                                }
-                            } 
-                        }
-
-                        // For now, only 1:1 room needs to be renamed. It means only 1 invited user
-                        if (1 === invitedUserIDs.length) {
-                            otherUserId = invitedUserIDs[0];
-                        }
-*/                        
-                    }
-                    
-                    // Get the user display name
-                    roomName = eventHandlerService.getUserDisplayName(room_id, otherUserId);
-                }
-            }
-        }
-
-        // Always show the alias in the room displayed name
-        if (roomName && alias && alias !== roomName) {
-            roomName += " (" + alias + ")";
-        }
-
-        if (undefined === roomName) {
-            // By default, use the room ID
-            roomName = room_id;
-
-            // XXX: this is *INCREDIBLY* heavy logging for a function that calls every single
-            // time any kind of digest runs which refreshes a room name...
-            // commenting it out for now.
-
-            // Log some information that lead to this leak
-            // console.log("Room ID leak for " + room_id);
-            // console.log("room object: " + JSON.stringify(room, undefined, 4));   
-        }
-
-        return roomName;
-    };
-}])
-
-// Return the user display name
-.filter('mUserDisplayName', ['eventHandlerService', function(eventHandlerService) {
-    return function(user_id, room_id) {
-        return eventHandlerService.getUserDisplayName(room_id, user_id);
-    };
-}]);