summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.rst20
-rw-r--r--README.rst21
-rw-r--r--UPGRADE.rst24
-rw-r--r--VERSION1
-rwxr-xr-xdatabase-prepare-for-0.0.1.sh21
-rwxr-xr-xdatabase-save.sh2
-rw-r--r--setup.py2
-rw-r--r--synapse/__init__.py2
-rw-r--r--synapse/api/events/__init__.py1
-rw-r--r--synapse/api/events/factory.py7
-rwxr-xr-xsynapse/app/homeserver.py9
-rw-r--r--synapse/handlers/_base.py1
-rw-r--r--synapse/handlers/federation.py29
-rw-r--r--synapse/handlers/room.py31
-rw-r--r--synapse/http/server.py26
-rw-r--r--synapse/server.py2
-rw-r--r--synapse/storage/__init__.py7
-rw-r--r--synapse/storage/_base.py5
-rw-r--r--synapse/storage/schema/im.sql1
-rw-r--r--synapse/storage/stream.py8
-rw-r--r--webclient/app-controller.js4
-rw-r--r--webclient/app-filter.js7
-rw-r--r--webclient/app.css48
-rw-r--r--webclient/app.js2
-rw-r--r--webclient/components/fileUpload/file-upload-service.js3
-rw-r--r--webclient/components/matrix/event-handler-service.js26
-rw-r--r--webclient/components/matrix/event-stream-service.js51
-rw-r--r--webclient/components/utilities/utilities-service.js67
-rw-r--r--webclient/login/login-controller.js4
-rw-r--r--webclient/room/room-controller.js38
-rw-r--r--webclient/room/room-directive.js30
-rw-r--r--webclient/room/room.html16
-rw-r--r--webclient/rooms/rooms-controller.js30
-rw-r--r--webclient/rooms/rooms.html2
34 files changed, 408 insertions, 140 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
new file mode 100644
index 0000000000..055f8bc01b
--- /dev/null
+++ b/CHANGES.rst
@@ -0,0 +1,20 @@
+Changes in synapse 0.0.1
+=======================
+Homeserver:
+ * Completely change the database schema to support generic event types.
+ * Improve presence reliability.
+ * Improve reliability of joining remote rooms.
+ * Fix bug where room join events were duplicated.
+ * Improve initial sync API to return more information to the client.
+ * Stop generating fake messages for room membership events.
+
+Webclient:
+ * Add tab completion of names.
+ * Add ability to upload and send images.
+ * Add profile pages.
+ * Improve CSS layout of room.
+ * Disambiguate identical display names.
+ * Don't get remote users display names and avatars individually.
+ * Use the new initial sync API to reduce number of round trips to the homeserver.
+ * Change url scheme to use room aliases instead of room ids where known.
+ * Increase longpoll timeout.
diff --git a/README.rst b/README.rst
index 378b460d0b..069f37ec06 100644
--- a/README.rst
+++ b/README.rst
@@ -24,11 +24,8 @@ To get up and running:
     
     - To run your own **private** homeserver on localhost:8080, install synapse 
       with ``python setup.py develop --user`` and then run one with
-      ``python synapse/app/homeserver.py``
-      
-    - To run your own webclient, add ``-w``:
-      ``python synapse/app/homeserver.py -w`` and hit http://localhost:8080/matrix/client
-      in your web browser (a recent Chrome, Safari or Firefox for now,
+      ``python synapse/app/homeserver.py`` - you will find a webclient running
+      at http://localhost:8080 (use a recent Chrome, Safari or Firefox for now,
       please...)
              
     - To make the homeserver **public** and let it exchange messages with 
@@ -36,7 +33,8 @@ To get up and running:
       up port 8080 and run ``python synapse/app/homeserver.py --host 
       machine.my.domain.name``.  Then come join ``#matrix:matrix.org`` and
       say hi! :)
-    
+
+   
 About Matrix
 ============
 
@@ -146,6 +144,13 @@ This should end with a 'PASSED' result::
     PASSED (successes=143)
 
 
+Upgrading an existing homeserver
+================================
+
+Before upgrading an existing homeserver to a new version, please refer to
+UPGRADE.rst for any additional instructions.
+ 
+
 Setting up Federation
 =====================
 
@@ -201,9 +206,7 @@ http://localhost:8080. Simply run::
 Running The Demo Web Client
 ===========================
 
-You can run the web client when you run the homeserver by adding ``-w`` to the
-command to run ``homeserver.py``. The web client can be accessed via 
-http://localhost:8080/matrix/client
+The homeserver runs a web client by default at http://localhost:8080.
 
 If this is the first time you have used the client from that browser (it uses
 HTML5 local storage to remember its config), you will need to log in to your
diff --git a/UPGRADE.rst b/UPGRADE.rst
new file mode 100644
index 0000000000..2e75d77bca
--- /dev/null
+++ b/UPGRADE.rst
@@ -0,0 +1,24 @@
+Upgrading to v0.0.1
+===================
+
+This release completely changes the database schema and so requires upgrading
+it before starting the new version of the homeserver.
+
+The script "database-prepare-for-0.0.1.sh" should be used to upgrade the
+database. This will save all user information, such as logins and profiles, 
+but will otherwise purge the database. This includes messages, which
+rooms the home server was a member of and room alias mappings.
+
+Before running the command the homeserver should be first completely 
+shutdown. To run it, simply specify the location of the database, e.g.:
+
+  ./database-prepare-for-0.0.1.sh "homeserver.db"
+
+Once this has successfully completed it will be safe to restart the 
+homeserver. You may notice that the homeserver takes a few seconds longer to 
+restart than usual as it reinitializes the database.
+
+On startup of the new version, users can either rejoin remote rooms using room
+aliases or by being reinvited. Alternatively, if any other homeserver sends a
+message to a room that the homeserver was previously in the local HS will 
+automatically rejoin the room.
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000000..8acdd82b76
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+0.0.1
diff --git a/database-prepare-for-0.0.1.sh b/database-prepare-for-0.0.1.sh
new file mode 100755
index 0000000000..43d759a5cd
--- /dev/null
+++ b/database-prepare-for-0.0.1.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+
+# This is will prepare a synapse database for running with v0.0.1 of synapse. 
+# It will store all the user information, but will *delete* all messages and
+# room data.
+
+set -e
+
+cp "$1" "$1.bak"
+
+DUMP=$(sqlite3 "$1" << 'EOF'
+.dump users
+.dump access_tokens
+.dump presence
+.dump profiles
+EOF
+)
+
+rm "$1"
+
+sqlite3 "$1" <<< "$DUMP" 
diff --git a/database-save.sh b/database-save.sh
index c80f676f76..040c8a4943 100755
--- a/database-save.sh
+++ b/database-save.sh
@@ -8,7 +8,7 @@
 #
 #   $ sqlite3 homeserver.db < table-save.sql
 
-sqlite3 homeserver.db <<'EOF' >table-save.sql
+sqlite3 "$1" <<'EOF' >table-save.sql
 .dump users
 .dump access_tokens
 .dump presence
diff --git a/setup.py b/setup.py
index fca3c77700..f01eec436f 100644
--- a/setup.py
+++ b/setup.py
@@ -25,7 +25,7 @@ def read(fname):
 
 setup(
     name="SynapseHomeServer",
-    version="0.1",
+    version="0.0.1",
     packages=find_packages(exclude=["tests"]),
     description="Reference Synapse Home Server",
     install_requires=[
diff --git a/synapse/__init__.py b/synapse/__init__.py
index 1e7b2ab272..47fc1b2ea4 100644
--- a/synapse/__init__.py
+++ b/synapse/__init__.py
@@ -15,3 +15,5 @@
 
 """ This is a reference implementation of a synapse home server.
 """
+
+__version__ = "0.0.1"
diff --git a/synapse/api/events/__init__.py b/synapse/api/events/__init__.py
index 921fd08832..aa04dbece7 100644
--- a/synapse/api/events/__init__.py
+++ b/synapse/api/events/__init__.py
@@ -51,6 +51,7 @@ class SynapseEvent(JsonEncodedObject):
         "depth",
         "destinations",
         "origin",
+        "outlier",
     ]
 
     required_keys = [
diff --git a/synapse/api/events/factory.py b/synapse/api/events/factory.py
index b61dac7acd..c2cdcddf41 100644
--- a/synapse/api/events/factory.py
+++ b/synapse/api/events/factory.py
@@ -33,16 +33,21 @@ class EventFactory(object):
         RoomConfigEvent
     ]
 
-    def __init__(self):
+    def __init__(self, hs):
         self._event_list = {}  # dict of TYPE to event class
         for event_class in EventFactory._event_classes:
             self._event_list[event_class.TYPE] = event_class
 
+        self.clock = hs.get_clock()
+
     def create_event(self, etype=None, **kwargs):
         kwargs["type"] = etype
         if "event_id" not in kwargs:
             kwargs["event_id"] = random_string(10)
 
+        if "ts" not in kwargs:
+            kwargs["ts"] = int(self.clock.time_msec())
+
         if etype in self._event_list:
             handler = self._event_list[etype]
         else:
diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py
index ca102236cf..495149466c 100755
--- a/synapse/app/homeserver.py
+++ b/synapse/app/homeserver.py
@@ -56,7 +56,7 @@ class SynapseHomeServer(HomeServer):
         return File("webclient")  # TODO configurable?
 
     def build_resource_for_content_repo(self):
-        return ContentRepoResource("uploads", self.auth)
+        return ContentRepoResource(self, self.upload_dir, self.auth)
 
     def build_db_pool(self):
         """ Set up all the dbs. Since all the *.sql have IF NOT EXISTS, so we
@@ -235,8 +235,8 @@ def setup():
     parser.add_argument('--pid-file', dest="pid", help="When running as a "
                         "daemon, the file to store the pid in",
                         default="hs.pid")
-    parser.add_argument("-w", "--webclient", dest="webclient",
-                        action="store_true", help="Host the web client.")
+    parser.add_argument("-W", "--webclient", dest="webclient", default=True,
+                        action="store_false", help="Don't host a web client.")
     args = parser.parse_args()
 
     verbosity = int(args.verbose) if args.verbose else None
@@ -257,7 +257,8 @@ def setup():
 
     hs = SynapseHomeServer(
         args.host,
-        db_name=db_name
+        upload_dir=os.path.abspath("uploads"),
+        db_name=db_name,
     )
 
     # This object doesn't need to be saved because it's set as the handler for
diff --git a/synapse/handlers/_base.py b/synapse/handlers/_base.py
index c2f4685c92..3f07b5aa4a 100644
--- a/synapse/handlers/_base.py
+++ b/synapse/handlers/_base.py
@@ -24,4 +24,5 @@ class BaseHandler(object):
         self.notifier = hs.get_notifier()
         self.room_lock = hs.get_room_lock_manager()
         self.state_handler = hs.get_state_handler()
+        self.distributor = hs.get_distributor()
         self.hs = hs
diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py
index aa3bf273f7..9cff444779 100644
--- a/synapse/handlers/federation.py
+++ b/synapse/handlers/federation.py
@@ -32,6 +32,15 @@ logger = logging.getLogger(__name__)
 class FederationHandler(BaseHandler):
 
     """Handles events that originated from federation."""
+    def __init__(self, hs):
+        super(FederationHandler, self).__init__(hs)
+
+        self.distributor.observe(
+            "user_joined_room",
+            self._on_user_joined
+        )
+
+        self.waiting_for_join_list = {}
 
     @log_function
     @defer.inlineCallbacks
@@ -103,6 +112,13 @@ class FederationHandler(BaseHandler):
             if not backfilled:
                 yield self.notifier.on_new_room_event(event, store_id)
 
+        if event.type == RoomMemberEvent.TYPE:
+            if event.membership == Membership.JOIN:
+                user = self.hs.parse_userid(event.target_user_id)
+                self.distributor.fire(
+                    "user_joined_room", user=user, room_id=event.room_id
+                )
+
 
     @log_function
     @defer.inlineCallbacks
@@ -152,8 +168,10 @@ class FederationHandler(BaseHandler):
 
         yield federation.handle_new_event(new_event)
 
-        store_id = yield self.store.persist_event(new_event)
-        self.notifier.on_new_room_event(new_event, store_id)
+        # TODO (erikj): Time out here.
+        d = defer.Deferred()
+        self.waiting_for_join_list.setdefault((joinee, room_id), []).append(d)
+        yield d
 
         try:
             yield self.store.store_room(
@@ -166,3 +184,10 @@ class FederationHandler(BaseHandler):
 
 
         defer.returnValue(True)
+
+
+    @log_function
+    def _on_user_joined(self, user, room_id):
+        waiters = self.waiting_for_join_list.get((user.to_string(), room_id), [])
+        while waiters:
+            waiters.pop().callback(None)
diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py
index 6229ee9bfa..899b653fb7 100644
--- a/synapse/handlers/room.py
+++ b/synapse/handlers/room.py
@@ -24,6 +24,7 @@ from synapse.api.events.room import (
     RoomConfigEvent
 )
 from synapse.api.streams.event import EventStream, EventsStreamData
+from synapse.handlers.presence import PresenceStreamData
 from synapse.util import stringutils
 from ._base import BaseHandler
 
@@ -257,21 +258,38 @@ class MessageHandler(BaseHandler):
             membership_list=[Membership.INVITE, Membership.JOIN]
         )
 
-        ret = []
+        rooms_ret = []
+
+        now_rooms_token = yield self.store.get_room_events_max_id()
+
+        # FIXME (erikj): Fix this.
+        presence_stream = PresenceStreamData(self.hs)
+        now_presence_token = yield presence_stream.max_token()
+        presence = yield presence_stream.get_rows(
+            user_id, 0, now_presence_token, None, None
+        )
+
+        # FIXME (erikj): We need to not generate this token,
+        now_token = "%s_%s" % (now_rooms_token, now_presence_token)
 
         for event in room_list:
             d = {
                 "room_id": event.room_id,
                 "membership": event.membership,
             }
-            ret.append(d)
+
+            if event.membership == Membership.INVITE:
+                d["inviter"] = event.user_id
+
+            rooms_ret.append(d)
 
             if event.membership != Membership.JOIN:
                 continue
             try:
                 messages, token = yield self.store.get_recent_events_for_room(
                     event.room_id,
-                    limit=50,
+                    limit=10,
+                    end_token=now_rooms_token,
                 )
 
                 d["messages"] = {
@@ -279,9 +297,16 @@ class MessageHandler(BaseHandler):
                     "start": token[0],
                     "end": token[1],
                 }
+
+                current_state = yield self.store.get_current_state(event.room_id)
+                d["state"] = [c.get_dict() for c in current_state]
             except:
                 logger.exception("Failed to get snapshot")
 
+        user = self.hs.parse_userid(user_id)
+
+        ret = {"rooms": rooms_ret, "presence": presence[0], "end": now_token}
+
         logger.debug("snapshot_all_rooms returning: %s", ret)
 
         defer.returnValue(ret)
diff --git a/synapse/http/server.py b/synapse/http/server.py
index c28d9a33f9..d1f99460c1 100644
--- a/synapse/http/server.py
+++ b/synapse/http/server.py
@@ -212,8 +212,9 @@ class ContentRepoResource(resource.Resource):
     """
     isLeaf = True
 
-    def __init__(self, directory, auth):
+    def __init__(self, hs, directory, auth):
         resource.Resource.__init__(self)
+        self.hs = hs
         self.directory = directory
         self.auth = auth
 
@@ -250,7 +251,8 @@ class ContentRepoResource(resource.Resource):
                 file_ext = re.sub("[^a-z]", "", file_ext)
                 suffix += "." + file_ext
 
-        file_path = os.path.join(self.directory, prefix + main_part + suffix)
+        file_name = prefix + main_part + suffix
+        file_path = os.path.join(self.directory, file_name)
         logger.info("User %s is uploading a file to path %s",
                     auth_user.to_string(),
                     file_path)
@@ -259,8 +261,8 @@ class ContentRepoResource(resource.Resource):
         attempts = 0
         while os.path.exists(file_path):
             main_part = random_string(24)
-            file_path = os.path.join(self.directory,
-                                     prefix + main_part + suffix)
+            file_name = prefix + main_part + suffix
+            file_path = os.path.join(self.directory, file_name)
             attempts += 1
             if attempts > 25:  # really? Really?
                 raise SynapseError(500, "Unable to create file.")
@@ -272,11 +274,14 @@ class ContentRepoResource(resource.Resource):
         # servers.
 
         # TODO: A little crude here, we could do this better.
-        filename = request.path.split(self.directory + "/")[1]
+        filename = request.path.split('/')[-1]
         # be paranoid
         filename = re.sub("[^0-9A-z.-_]", "", filename)
 
         file_path = self.directory + "/" + filename
+
+        logger.debug("Searching for %s", file_path)
+
         if os.path.isfile(file_path):
             # filename has the content type
             base64_contentype = filename.split(".")[1]
@@ -304,6 +309,10 @@ class ContentRepoResource(resource.Resource):
         self._async_render(request)
         return server.NOT_DONE_YET
 
+    def render_OPTIONS(self, request):
+        respond_with_json_bytes(request, 200, {}, send_cors=True)
+        return server.NOT_DONE_YET
+
     @defer.inlineCallbacks
     def _async_render(self, request):
         try:
@@ -313,8 +322,13 @@ class ContentRepoResource(resource.Resource):
             with open(fname, "wb") as f:
                 f.write(request.content.read())
 
+
+            # FIXME (erikj): These should use constants.
+            file_name = os.path.basename(fname)
+            url = "http://%s/matrix/content/%s" % (self.hs.hostname, file_name)
+
             respond_with_json_bytes(request, 200,
-                                    json.dumps({"content_token": fname}),
+                                    json.dumps({"content_token": url}),
                                     send_cors=True)
 
         except CodeMessageException as e:
diff --git a/synapse/server.py b/synapse/server.py
index d4c2481483..c5b0a32757 100644
--- a/synapse/server.py
+++ b/synapse/server.py
@@ -159,7 +159,7 @@ class HomeServer(BaseHomeServer):
         return DataStore(self)
 
     def build_event_factory(self):
-        return EventFactory()
+        return EventFactory(self)
 
     def build_handlers(self):
         return Handlers(self)
diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py
index 7732906927..d06033b980 100644
--- a/synapse/storage/__init__.py
+++ b/synapse/storage/__init__.py
@@ -105,6 +105,11 @@ class DataStore(RoomMemberStore, RoomStore,
             "processed": True,
         }
 
+        if hasattr(event, "outlier"):
+            vals["outlier"] = event.outlier
+        else:
+            vals["outlier"] = False
+
         if backfilled:
             if not self.min_token_deferred.called:
                 yield self.min_token_deferred
@@ -123,7 +128,7 @@ class DataStore(RoomMemberStore, RoomStore,
         except:
             logger.exception(
                 "Failed to persist, probably duplicate: %s",
-                event_id
+                event.event_id
             )
             return
 
diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py
index 36cc57c1b8..75aab2d3b9 100644
--- a/synapse/storage/_base.py
+++ b/synapse/storage/_base.py
@@ -294,6 +294,11 @@ class SQLBaseStore(object):
 
     def _parse_event_from_row(self, row_dict):
         d = copy.deepcopy({k: v for k, v in row_dict.items() if v})
+
+        d.pop("stream_ordering", None)
+        d.pop("topological_ordering", None)
+        d.pop("processed", None)
+
         d.update(json.loads(row_dict["unrecognized_keys"]))
         d["content"] = json.loads(d["content"])
         del d["unrecognized_keys"]
diff --git a/synapse/storage/schema/im.sql b/synapse/storage/schema/im.sql
index ea04261ff0..39a1ed703e 100644
--- a/synapse/storage/schema/im.sql
+++ b/synapse/storage/schema/im.sql
@@ -22,6 +22,7 @@ CREATE TABLE IF NOT EXISTS events(
     content TEXT NOT NULL,
     unrecognized_keys TEXT,
     processed BOOL NOT NULL,
+    outlier BOOL NOT NULL,
     CONSTRAINT ev_uniq UNIQUE (event_id)
 );
 
diff --git a/synapse/storage/stream.py b/synapse/storage/stream.py
index e994017bf2..87ae961ccd 100644
--- a/synapse/storage/stream.py
+++ b/synapse/storage/stream.py
@@ -177,6 +177,7 @@ class StreamStore(SQLBaseStore):
             "((room_id IN (%(current)s)) OR "
             "(event_id IN (%(invites)s))) "
             "AND e.stream_ordering > ? AND e.stream_ordering < ? "
+            "AND e.outlier = 0 "
             "ORDER BY stream_ordering ASC LIMIT %(limit)d "
         ) % {
             "current": current_room_membership_sql,
@@ -224,7 +225,7 @@ class StreamStore(SQLBaseStore):
 
         sql = (
             "SELECT * FROM events "
-            "WHERE room_id = ? AND %(bounds)s "
+            "WHERE outlier = 0 AND room_id = ? AND %(bounds)s "
             "ORDER BY topological_ordering %(order)s, stream_ordering %(order)s %(limit)s "
         ) % {"bounds": bounds, "order": order, "limit": limit_str}
 
@@ -249,11 +250,10 @@ class StreamStore(SQLBaseStore):
         )
 
     @defer.inlineCallbacks
-    def get_recent_events_for_room(self, room_id, limit, with_feedback=False):
+    def get_recent_events_for_room(self, room_id, limit, end_token,
+                                   with_feedback=False):
         # TODO (erikj): Handle compressed feedback
 
-        end_token = yield self.get_room_events_max_id()
-
         sql = (
             "SELECT * FROM events "
             "WHERE room_id = ? AND stream_ordering <= ? "
diff --git a/webclient/app-controller.js b/webclient/app-controller.js
index 96656e12c3..92ad01e4f9 100644
--- a/webclient/app-controller.js
+++ b/webclient/app-controller.js
@@ -53,7 +53,7 @@ angular.module('MatrixWebClientController', ['matrixService'])
     };
 
     if (matrixService.isUserLoggedIn()) {
-        eventStreamService.resume();
+        // eventStreamService.resume();
     }
     
     // Logs the user out 
@@ -66,7 +66,7 @@ angular.module('MatrixWebClientController', ['matrixService'])
         matrixService.saveConfig();
         
         // And go to the login page
-        $location.path("login");
+        $location.url("login");
     };
 
     // Listen to the event indicating that the access token is no longer valid.
diff --git a/webclient/app-filter.js b/webclient/app-filter.js
index 64c3bb04de..b8f4ed25bc 100644
--- a/webclient/app-filter.js
+++ b/webclient/app-filter.js
@@ -54,12 +54,15 @@ angular.module('matrixWebClient')
         });
 
         // FIXME: we shouldn't disambiguate displayNames on every orderMembersList
-        // invocation but keep track of duplicates incrementally somewhere            
+        // invocation but keep track of duplicates incrementally somewhere
         angular.forEach(displayNames, function(value, key) {
             if (value.length > 1) {
                 // console.log(key + ": " + value);
-                for (i=0; i < value.length; i++) {
+                for (var i=0; i < value.length; i++) {
                     var v = value[i];
+                    // FIXME: this permenantly rewrites the displayname for a given
+                    // room member. which means we can't reset their name if it is
+                    // no longer ambiguous!
                     members[v].displayname += " (" + v + ")";
                     // console.log(v + " " + members[v]);
                 };
diff --git a/webclient/app.css b/webclient/app.css
index d2b951d3b6..207f35f5f3 100644
--- a/webclient/app.css
+++ b/webclient/app.css
@@ -145,6 +145,7 @@ h1 {
     max-width: 1280px;
     width: 100%;
     border-collapse: collapse;
+    table-layout: fixed;
 }
         
 #messageTable td {
@@ -152,7 +153,8 @@ h1 {
 }
 
 .leftBlock {
-    width: 10em;
+    width: 14em;
+    word-wrap: break-word;
     vertical-align: top;
     background-color: #fff;
     color: #888;
@@ -190,24 +192,13 @@ h1 {
     object-fit: cover;
 }
         
-.text {
-    background-color: #eee;
-    border: 1px solid #d8d8d8;
-    height: 31px;
-    display: inline-table;
-    max-width: 90%;
-    font-size: 16px;
-    /* word-wrap: break-word; */
-    word-break: break-all;
-}
-
 .emote {
-    background-color: #fff ! important;
+    background-color: transparent ! important;
     border: 0px ! important;
 }
 
 .membership {
-    background-color: #fff ! important;
+    background-color: transparent ! important;
     border: 0px ! important;
 }
 
@@ -219,32 +210,45 @@ h1 {
     height: auto;
 }
 
+.text {
+    vertical-align: top;
+}
+
 .bubble {
+    background-color: #eee;
+    border: 1px solid #d8d8d8;
+    display: inline-block;
+    margin-bottom: -1px;
+    max-width: 90%;
+    font-size: 16px;
+    word-wrap: break-word;
     padding-top: 7px;
     padding-bottom: 5px;
     padding-left: 1em;
     padding-right: 1em;
     vertical-align: middle;
+    -webkit-text-size-adjust:100%
 }
 
 .differentUser td {
-    padding-top: 5px ! important;
-    margin-top: 5px ! important;
+    padding-bottom: 5px ! important;
 }
 
 .mine {
     text-align: right;
 }
 
-.mine .text {
-    background-color: #f8f8ff ! important;    
-}
-
-.mine .emote {
-    background-color: #fff ! important;    
+.text.emote .bubble,
+.text.membership .bubble,
+.mine .text.emote .bubble,
+.mine .text.membership .bubble
+  {
+    background-color: transparent ! important;    
+    border: 0px ! important;
 }
 
 .mine .text .bubble {
+    background-color: #f8f8ff ! important;    
     text-align: left ! important;
 }
 
diff --git a/webclient/app.js b/webclient/app.js
index f27ebedc6f..944b8ec270 100644
--- a/webclient/app.js
+++ b/webclient/app.js
@@ -80,6 +80,6 @@ matrixWebClient.run(['$location', 'matrixService', 'eventStreamService', functio
         $location.path("login");
     }
     else {
-        eventStreamService.resume();
+        // eventStreamService.resume();
     }
 }]);
diff --git a/webclient/components/fileUpload/file-upload-service.js b/webclient/components/fileUpload/file-upload-service.js
index 6606f31e22..5f01478fd1 100644
--- a/webclient/components/fileUpload/file-upload-service.js
+++ b/webclient/components/fileUpload/file-upload-service.js
@@ -33,7 +33,7 @@ angular.module('mFileUpload', ['matrixService', 'mUtilities'])
         console.log("Uploading " + file.name + "... to /matrix/content");
         matrixService.uploadContent(file).then(
             function(response) {
-                var content_url = location.origin + "/matrix/content/" + response.data.content_token;
+                var content_url = response.data.content_token;
                 console.log("   -> Successfully uploaded! Available at " + content_url);
                 deferred.resolve(content_url);
             },
@@ -82,6 +82,7 @@ angular.module('mFileUpload', ['matrixService', 'mUtilities'])
         // First, get the image size
         mUtilities.getImageSize(imageFile).then(
             function(size) {
+                console.log("image size: " + JSON.stringify(size));
 
                 // The final operation: send imageFile
                 var uploadImage = function() {
diff --git a/webclient/components/matrix/event-handler-service.js b/webclient/components/matrix/event-handler-service.js
index b8529895fe..b5eb73d92b 100644
--- a/webclient/components/matrix/event-handler-service.js
+++ b/webclient/components/matrix/event-handler-service.js
@@ -35,6 +35,8 @@ angular.module('eventHandlerService', [])
     $rootScope.events = {
         rooms: {}, // will contain roomId: { messages:[], members:{userid1: event} }
     };
+
+    $rootScope.presence = {};
     
     var initRoom = function(room_id) {
         if (!(room_id in $rootScope.events.rooms)) {
@@ -44,6 +46,12 @@ angular.module('eventHandlerService', [])
             $rootScope.events.rooms[room_id].members = {};
         }
     }
+
+    var reInitRoom = function(room_id) {
+        $rootScope.events.rooms[room_id] = {};
+        $rootScope.events.rooms[room_id].messages = [];
+        $rootScope.events.rooms[room_id].members = {};
+    }
     
     var handleMessage = function(event, isLiveEvent) {
         if ("membership_target" in event.content) {
@@ -69,11 +77,23 @@ angular.module('eventHandlerService', [])
     
     var handleRoomMember = function(event, isLiveEvent) {
         initRoom(event.room_id);
+        
+        // add membership changes as if they were a room message if something interesting changed
+        if (event.content.prev !== event.content.membership) {
+            if (isLiveEvent) {
+                $rootScope.events.rooms[event.room_id].messages.push(event);
+            }
+            else {
+                $rootScope.events.rooms[event.room_id].messages.unshift(event);
+            }
+        }
+        
         $rootScope.events.rooms[event.room_id].members[event.user_id] = event;
         $rootScope.$broadcast(MEMBER_EVENT, event, isLiveEvent);
     };
     
     var handlePresence = function(event, isLiveEvent) {
+        $rootScope.presence[event.content.user_id] = event;
         $rootScope.$broadcast(PRESENCE_EVENT, event, isLiveEvent);
     };
     
@@ -107,6 +127,10 @@ angular.module('eventHandlerService', [])
             for (var i=0; i<events.length; i++) {
                 this.handleEvent(events[i], isLiveEvents);
             }
-        }
+        },
+
+        reInitRoom: function(room_id) {
+            reInitRoom(room_id);
+        },
     };
 }]);
diff --git a/webclient/components/matrix/event-stream-service.js b/webclient/components/matrix/event-stream-service.js
index a446fad5d4..a1a98b2a36 100644
--- a/webclient/components/matrix/event-stream-service.js
+++ b/webclient/components/matrix/event-stream-service.js
@@ -48,11 +48,12 @@ angular.module('eventStreamService', [])
     var saveStreamSettings = function() {
         localStorage.setItem("streamSettings", JSON.stringify(settings));
     };
-    
-    var startEventStream = function() {
+
+    var doEventStream = function(deferred) {
         settings.shouldPoll = true;
         settings.isActive = true;
-        var deferred = $q.defer();
+        deferred = deferred || $q.defer();
+
         // run the stream from the latest token
         matrixService.getEventStream(settings.from, TIMEOUT_MS).then(
             function(response) {
@@ -63,13 +64,16 @@ angular.module('eventStreamService', [])
                 
                 settings.from = response.data.end;
                 
-                console.log("[EventStream] Got response from "+settings.from+" to "+response.data.end);
+                console.log(
+                    "[EventStream] Got response from "+settings.from+
+                    " to "+response.data.end
+                );
                 eventHandlerService.handleEvents(response.data.chunk, true);
                 
                 deferred.resolve(response);
                 
                 if (settings.shouldPoll) {
-                    $timeout(startEventStream, 0);
+                    $timeout(doEventStream, 0);
                 }
                 else {
                     console.log("[EventStream] Stopping poll.");
@@ -83,13 +87,48 @@ angular.module('eventStreamService', [])
                 deferred.reject(error);
                 
                 if (settings.shouldPoll) {
-                    $timeout(startEventStream, ERR_TIMEOUT_MS);
+                    $timeout(doEventStream, ERR_TIMEOUT_MS);
                 }
                 else {
                     console.log("[EventStream] Stopping polling.");
                 }
             }
         );
+
+        return deferred.promise;
+    }    
+
+    var startEventStream = function() {
+        settings.shouldPoll = true;
+        settings.isActive = true;
+        var deferred = $q.defer();
+
+        // FIXME: We are discarding all the messages.
+        matrixService.rooms().then(
+            function(response) {
+                var rooms = response.data.rooms;
+                for (var i = 0; i < rooms.length; ++i) {
+                    var room = rooms[i];
+                    if ("state" in room) {
+                        for (var j = 0; j < room.state.length; ++j) {
+                            eventHandlerService.handleEvents(room.state[j], false);
+                        }
+                    }
+                }
+
+                var presence = response.data.presence;
+                for (var i = 0; i < presence.length; ++i) {
+                    eventHandlerService.handleEvent(presence[i], false);
+                }
+
+                settings.from = response.data.end
+                doEventStream(deferred);        
+            },
+            function(error) {
+                $scope.feedback = "Failure: " + error.data;
+            }
+        );
+
         return deferred.promise;
     };
     
diff --git a/webclient/components/utilities/utilities-service.js b/webclient/components/utilities/utilities-service.js
index 9cf858ef39..3df2f04458 100644
--- a/webclient/components/utilities/utilities-service.js
+++ b/webclient/components/utilities/utilities-service.js
@@ -38,10 +38,15 @@ angular.module('mUtilities', [])
             img.src = e.target.result;
             
             // Once ready, returns its size
-            deferred.resolve({
-                width: img.width,
-                height: img.height
-            });
+            img.onload = function() {
+                deferred.resolve({
+                    width: img.width,
+                    height: img.height
+                });
+            };
+            img.onerror = function(e) {
+                deferred.reject(e);
+            };
         };
         reader.onerror = function(e) {
             deferred.reject(e);
@@ -71,33 +76,41 @@ angular.module('mUtilities', [])
         reader.onload = function(e) {
 
             img.src = e.target.result;
+            
+            // Once ready, returns its size
+            img.onload = function() {
+                var ctx = canvas.getContext("2d");
+                ctx.drawImage(img, 0, 0);
 
-            var ctx = canvas.getContext("2d");
-            ctx.drawImage(img, 0, 0);
-
-            var MAX_WIDTH = maxSize;
-            var MAX_HEIGHT = maxSize;
-            var width = img.width;
-            var height = img.height;
+                var MAX_WIDTH = maxSize;
+                var MAX_HEIGHT = maxSize;
+                var width = img.width;
+                var height = img.height;
 
-            if (width > height) {
-                if (width > MAX_WIDTH) {
-                    height *= MAX_WIDTH / width;
-                    width = MAX_WIDTH;
-                }
-            } else {
-                if (height > MAX_HEIGHT) {
-                    width *= MAX_HEIGHT / height;
-                    height = MAX_HEIGHT;
+                if (width > height) {
+                    if (width > MAX_WIDTH) {
+                        height *= MAX_WIDTH / width;
+                        width = MAX_WIDTH;
+                    }
+                } else {
+                    if (height > MAX_HEIGHT) {
+                        width *= MAX_HEIGHT / height;
+                        height = MAX_HEIGHT;
+                    }
                 }
-            }
-            canvas.width = width;
-            canvas.height = height;
-            var ctx = canvas.getContext("2d");
-            ctx.drawImage(img, 0, 0, width, height);
+                canvas.width = width;
+                canvas.height = height;
+                var ctx = canvas.getContext("2d");
+                ctx.drawImage(img, 0, 0, width, height);
 
-            var dataUrl = canvas.toDataURL("image/jpeg", 0.7); 
-            deferred.resolve(self.dataURItoBlob(dataUrl));
+                // Extract image data in the same format as the original one.
+                // The 0.7 compression value will work with formats that supports it like JPEG.
+                var dataUrl = canvas.toDataURL(imageFile.type, 0.7); 
+                deferred.resolve(self.dataURItoBlob(dataUrl));
+            };
+            img.onerror = function(e) {
+                deferred.reject(e);
+            };
         };
         reader.onerror = function(e) {
             deferred.reject(e);
diff --git a/webclient/login/login-controller.js b/webclient/login/login-controller.js
index 35886c5583..e3d0eca946 100644
--- a/webclient/login/login-controller.js
+++ b/webclient/login/login-controller.js
@@ -53,7 +53,7 @@ angular.module('LoginController', ['matrixService'])
                 matrixService.saveConfig();
                 eventStreamService.resume();
                  // Go to the user's rooms list page
-                $location.path("rooms");
+                $location.url("rooms");
             },
             function(error) {
                 if (error.data) {
@@ -86,7 +86,7 @@ angular.module('LoginController', ['matrixService'])
                     });
                     matrixService.saveConfig();
                     eventStreamService.resume();
-                    $location.path("rooms");
+                    $location.url("rooms");
                 }
                 else {
                     $scope.feedback = "Failed to login: " + JSON.stringify(response.data);
diff --git a/webclient/room/room-controller.js b/webclient/room/room-controller.js
index 7de50dd960..58ba432ce5 100644
--- a/webclient/room/room-controller.js
+++ b/webclient/room/room-controller.js
@@ -15,8 +15,8 @@ limitations under the License.
 */
 
 angular.module('RoomController', ['ngSanitize', 'mUtilities'])
-.controller('RoomController', ['$scope', '$http', '$timeout', '$routeParams', '$location', 'matrixService', 'eventStreamService', 'eventHandlerService', 'mFileUpload', 'mUtilities',
-                               function($scope, $http, $timeout, $routeParams, $location, matrixService, eventStreamService, eventHandlerService, mFileUpload, mUtilities) {
+.controller('RoomController', ['$scope', '$http', '$timeout', '$routeParams', '$location', 'matrixService', 'eventStreamService', 'eventHandlerService', 'mFileUpload', 'mUtilities', '$rootScope',
+                               function($scope, $http, $timeout, $routeParams, $location, matrixService, eventStreamService, eventHandlerService, mFileUpload, mUtilities, $rootScope) {
    'use strict';
     var MESSAGES_PER_PAGINATION = 30;
     var THUMBNAIL_SIZE = 320;
@@ -29,9 +29,11 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
         user_id: matrixService.config().user_id,
         events_from: "END", // when to start the event stream from.
         earliest_token: "END", // stores how far back we've paginated.
+        first_pagination: true, // this is toggled off when the first pagination is done
         can_paginate: true, // this is toggled off when we run out of items
         paginating: false, // used to avoid concurrent pagination requests pulling in dup contents
         stream_failure: undefined, // the response when the stream fails
+        // FIXME: sending has been disabled, as surely messages should be sent in the background rather than locking the UI synchronously --Matthew
         sending: false // true when a message is being sent. It helps to disable the UI when a process is running
     };
     $scope.members = {};
@@ -100,7 +102,6 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
         var originalTopRow = $("#messageTable>tbody>tr:first")[0];
         matrixService.paginateBackMessages($scope.room_id, $scope.state.earliest_token, numItems).then(
             function(response) {
-                var firstPagination = !$scope.events.rooms[$scope.room_id];
                 eventHandlerService.handleEvents(response.data.chunk, false);
                 $scope.state.earliest_token = response.data.end;
                 if (response.data.chunk.length < MESSAGES_PER_PAGINATION) {
@@ -126,8 +127,9 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
                     }, 0);
                 }
                 
-                if (firstPagination) {
+                if ($scope.state.first_pagination) {
                     scrollToBottom();
+                    $scope.state.first_pagination = false;
                 }
                 else {
                     // lock the scroll position
@@ -150,6 +152,8 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
     };
 
     var updateMemberList = function(chunk) {
+        if (chunk.room_id != $scope.room_id) return;
+
         var isNewMember = !(chunk.target_user_id in $scope.members);
         if (isNewMember) {
             // FIXME: why are we copying these fields around inside chunk?
@@ -159,8 +163,7 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
             if ("mtime_age" in chunk.content) {
                 chunk.mtime_age = chunk.content.mtime_age;
             }
-/*            
-            // FIXME: once the HS reliably returns the displaynames & avatar_urls for both
+            // Once the HS reliably returns the displaynames & avatar_urls for both
             // local and remote users, we should use this rather than the evalAsync block
             // below
             if ("displayname" in chunk.content) {
@@ -169,9 +172,11 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
             if ("avatar_url" in chunk.content) {
                 chunk.avatar_url = chunk.content.avatar_url;
             }
- */      
             $scope.members[chunk.target_user_id] = chunk;
 
+/*
+            // Stale code for explicitly hammering the homeserver for every displayname & avatar_url
+            
             // get their display name and profile picture and set it to their
             // member entry in $scope.members. We HAVE to use $timeout with 0 delay 
             // to make this function run AFTER the current digest cycle, else the 
@@ -195,6 +200,11 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
                     }
                 );
             });
+*/            
+
+            if (chunk.target_user_id in $rootScope.presence) {
+                updatePresence($rootScope.presence[chunk.target_user_id]);
+            }
         }
         else {
             // selectively update membership else it will nuke the picture and displayname too :/
@@ -236,7 +246,7 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
         }
 
         $scope.state.sending = true;
-
+        
         // Send the text message
         var promise;
         // FIXME: handle other commands too
@@ -260,9 +270,8 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
     };
 
     $scope.onInit = function() {
-        // $timeout(function() { document.getElementById('textInput').focus() }, 0);
         console.log("onInit");
-        
+
         // Does the room ID provided in the URL?
         var room_id_or_alias;
         if ($routeParams.room_id_or_alias) {
@@ -290,7 +299,7 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
                 else {
                     // In case of issue, go to the default page
                     console.log("Error: cannot extract room alias");
-                    $location.path("/");
+                    $location.url("/");
                     return;
                 }
             }
@@ -307,12 +316,14 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
             function () {
                 // In case of issue, go to the default page
                 console.log("Error: cannot resolve room alias");
-                $location.path("/");
+                $location.url("/");
             });
         }
     };
 
     var onInit2 = function() {
+        eventHandlerService.reInitRoom($scope.room_id); 
+
         // Join the room
         matrixService.join($scope.room_id).then(
             function() {
@@ -325,6 +336,7 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
                             var chunk = response.data.chunk[i];
                             updateMemberList(chunk);
                         }
+                        eventStreamService.resume();
                     },
                     function(error) {
                         $scope.feedback = "Failed get member list: " + error.data.error;
@@ -360,7 +372,7 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
         matrixService.leave($scope.room_id).then(
             function(response) {
                 console.log("Left room ");
-                $location.path("rooms");
+                $location.url("rooms");
             },
             function(error) {
                 $scope.feedback = "Failed to leave room: " + error.data.error;
diff --git a/webclient/room/room-directive.js b/webclient/room/room-directive.js
index 94655336df..1a99a37abb 100644
--- a/webclient/room/room-directive.js
+++ b/webclient/room/room-directive.js
@@ -17,30 +17,30 @@
 'use strict';
 
 angular.module('RoomController')
-.directive('autoComplete', ['$timeout', function ($timeout) {
+.directive('tabComplete', ['$timeout', function ($timeout) {
     return function (scope, element, attrs) {
         element.bind("keydown keypress", function (event) {
             // console.log("event: " + event.which);
             if (event.which === 9) {
-                if (!scope.autoCompleting) { // cache our starting text
+                if (!scope.tabCompleting) { // cache our starting text
                     // console.log("caching " + element[0].value);
-                    scope.autoCompleteOriginal = element[0].value;
-                    scope.autoCompleting = true;
+                    scope.tabCompleteOriginal = element[0].value;
+                    scope.tabCompleting = true;
                 }
                 
                 if (event.shiftKey) {
-                    scope.autoCompleteIndex--;
-                    if (scope.autoCompleteIndex < 0) {
-                        scope.autoCompleteIndex = 0;
+                    scope.tabCompleteIndex--;
+                    if (scope.tabCompleteIndex < 0) {
+                        scope.tabCompleteIndex = 0;
                     }
                 }
                 else {
-                    scope.autoCompleteIndex++;
+                    scope.tabCompleteIndex++;
                 }
                 
                 var searchIndex = 0;
-                var targetIndex = scope.autoCompleteIndex;
-                var text = scope.autoCompleteOriginal;
+                var targetIndex = scope.tabCompleteIndex;
+                var text = scope.tabCompleteOriginal;
                 
                 // console.log("targetIndex: " + targetIndex + ", text=" + text);
                                     
@@ -90,17 +90,17 @@ angular.module('RoomController')
                              element[0].className = "";
                         }, 150);
                         element[0].value = text;
-                        scope.autoCompleteIndex = 0;
+                        scope.tabCompleteIndex = 0;
                     }
                 }
                 else {
-                    scope.autoCompleteIndex = 0;
+                    scope.tabCompleteIndex = 0;
                 }
                 event.preventDefault();
             }
-            else if (event.which !== 16 && scope.autoCompleting) {
-                scope.autoCompleting = false;
-                scope.autoCompleteIndex = 0;
+            else if (event.which !== 16 && scope.tabCompleting) {
+                scope.tabCompleting = false;
+                scope.tabCompleteIndex = 0;
             }
         });
     };
diff --git a/webclient/room/room.html b/webclient/room/room.html
index cb9cf1d1f3..95da067714 100644
--- a/webclient/room/room.html
+++ b/webclient/room/room.html
@@ -26,19 +26,25 @@
     </div>
     
     <div id="messageTableWrapper" keep-scroll>
+        <!-- FIXME: need to have better timestamp semantics than the (msg.content.hsob_ts || msg.ts) hack below -->
         <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>
+                ng-class="(events.rooms[room_id].messages[$index + 1].user_id !== msg.user_id ? 'differentUser' : '') + (msg.user_id === state.user_id ? ' mine' : '')" scroll-item>
                 <td class="leftBlock">
                     <div class="sender" ng-hide="events.rooms[room_id].messages[$index - 1].user_id === msg.user_id">{{ members[msg.user_id].displayname || msg.user_id }}</div>
-                    <div class="timestamp">{{ msg.content.hsob_ts | date:'MMM d HH:mm:ss' }}</div>
+                    <div class="timestamp">{{ (msg.content.hsob_ts || msg.ts) | date:'MMM d HH:mm:ss' }}</div>
                 </td>
                 <td class="avatar">
                     <img class="avatarImage" ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.jpg' }}" width="32" height="32"
                          ng-hide="events.rooms[room_id].messages[$index - 1].user_id === msg.user_id || msg.user_id === state.user_id"/>
                 </td>
-                <td ng-class="!msg.content.membership_target ? (msg.content.msgtype === 'm.emote' ? 'emote text' : 'text') : 'membership text'">
+                <td ng-class="!msg.content.membership ? (msg.content.msgtype === 'm.emote' ? 'emote text' : 'text') : 'membership text'">
                     <div class="bubble">
+                        <span ng-hide='msg.type !== "m.room.member"'>
+                            {{ members[msg.user_id].displayname || msg.user_id }}
+                            {{ {"join": "joined", "leave": "left", "invite": "invited"}[msg.content.membership] }}
+                            {{ msg.content.target_id || '' }}
+                        </span>
                         <span ng-hide='msg.content.msgtype !== "m.emote"' ng-bind-html="'* ' + (members[msg.user_id].displayname || msg.user_id) + ' ' + msg.content.body | linky:'_blank'"/>
                         <span ng-hide='msg.content.msgtype !== "m.text"' ng-bind-html="((msg.content.msgtype === 'm.text') ? msg.content.body : '') | linky:'_blank'"/>
                         <div ng-show='msg.content.msgtype === "m.image"'>
@@ -71,10 +77,10 @@
                         {{ state.user_id }} 
                     </td>
                     <td width="*" style="min-width: 100px">
-                        <input id="mainInput" ng-model="textInput" ng-enter="send()" ng-disabled="state.sending" ng-focus="true" auto-complete/>
+                        <input id="mainInput" ng-model="textInput" ng-enter="send()" ng-focus="true" autocomplete="off" tab-complete/>
                     </td>
                     <td width="150px">
-                        <button ng-click="send()" ng-disabled="state.sending">Send</button>
+                        <button ng-click="send()">Send</button>
                         <button m-file-input="imageFileToSend">Send Image</button>
                     </td>
                     <td width="1">
diff --git a/webclient/rooms/rooms-controller.js b/webclient/rooms/rooms-controller.js
index 65d345d7a6..d9c8baff47 100644
--- a/webclient/rooms/rooms-controller.js
+++ b/webclient/rooms/rooms-controller.js
@@ -17,8 +17,8 @@ limitations under the License.
 'use strict';
 
 angular.module('RoomsController', ['matrixService', 'mFileInput', 'mFileUpload', 'eventHandlerService'])
-.controller('RoomsController', ['$scope', '$location', 'matrixService', 'mFileUpload', 'eventHandlerService',
-                               function($scope, $location, matrixService, mFileUpload, eventHandlerService) {
+.controller('RoomsController', ['$scope', '$location', 'matrixService', 'mFileUpload', 'eventHandlerService', 'eventStreamService', 
+                               function($scope, $location, matrixService, mFileUpload, eventHandlerService, eventStreamService) {
                                    
     $scope.rooms = {};
     $scope.public_rooms = [];
@@ -61,7 +61,7 @@ angular.module('RoomsController', ['matrixService', 'mFileInput', 'mFileUpload',
             // FIXME push membership to top level key to match /im/sync
             event.membership = event.content.membership;
             // FIXME bodge a nicer name than the room ID for this invite.
-            event.room_alias = event.user_id + "'s room";
+            event.room_display_name = event.user_id + "'s room";
             $scope.rooms[event.room_id] = event;
         }
     });
@@ -72,15 +72,20 @@ angular.module('RoomsController', ['matrixService', 'mFileInput', 'mFileUpload',
             if (alias) {
                 // use the existing alias from storage
                 data[i].room_alias = alias;
+                data[i].room_display_name = alias;
             }
             else if (data[i].aliases && data[i].aliases[0]) {
                 // save the mapping
                 // TODO: select the smarter alias from the array
                 matrixService.createRoomIdToAliasMapping(data[i].room_id, data[i].aliases[0]);
+                data[i].room_display_name = data[i].aliases[0];
+            }
+            else if (data[i].membership == "invite" && "inviter" in data[i]) {
+                data[i].room_display_name = data[i].inviter + "'s room"
             }
             else {
                 // last resort use the room id
-                data[i].room_alias = data[i].room_id;
+                data[i].room_display_name = data[i].room_id;
             }
         }
         return data;
@@ -90,11 +95,16 @@ angular.module('RoomsController', ['matrixService', 'mFileInput', 'mFileUpload',
         // List all rooms joined or been invited to
         matrixService.rooms().then(
             function(response) {
-                var data = assignRoomAliases(response.data);
+                var data = assignRoomAliases(response.data.rooms);
                 $scope.feedback = "Success";
                 for (var i=0; i<data.length; i++) {
                     $scope.rooms[data[i].room_id] = data[i];
                 }
+
+                var presence = response.data.presence;
+                for (var i = 0; i < presence.length; ++i) {
+                    eventHandlerService.handleEvent(presence[i], false);
+                }
             },
             function(error) {
                 $scope.feedback = "Failure: " + error.data;
@@ -105,6 +115,8 @@ angular.module('RoomsController', ['matrixService', 'mFileInput', 'mFileUpload',
                 $scope.public_rooms = assignRoomAliases(response.data.chunk);
             }
         );
+
+        eventStreamService.resume();
     };
     
     $scope.createNewRoom = function(room_id, isPrivate) {
@@ -131,17 +143,17 @@ angular.module('RoomsController', ['matrixService', 'mFileInput', 'mFileUpload',
     // Go to a room
     $scope.goToRoom = function(room_id) {
         // Simply open the room page on this room id
-        //$location.path("room/" + room_id);
+        //$location.url("room/" + room_id);
         matrixService.join(room_id).then(
             function(response) {
                 if (response.data.hasOwnProperty("room_id")) {
                     if (response.data.room_id != room_id) {
-                        $location.path("room/" + response.data.room_id);
+                        $location.url("room/" + response.data.room_id);
                         return;
                      }
                 }
 
-                $location.path("room/" + room_id);
+                $location.url("room/" + room_id);
             },
             function(error) {
                 $scope.feedback = "Can't join room: " + error.data;
@@ -153,7 +165,7 @@ angular.module('RoomsController', ['matrixService', 'mFileInput', 'mFileUpload',
         matrixService.joinAlias(room_alias).then(
             function(response) {
                 // Go to this room
-                $location.path("room/" + room_alias);
+                $location.url("room/" + room_alias);
             },
             function(error) {
                 $scope.feedback = "Can't join room: " + error.data;
diff --git a/webclient/rooms/rooms.html b/webclient/rooms/rooms.html
index 2602209bd3..ba3b7d8bad 100644
--- a/webclient/rooms/rooms.html
+++ b/webclient/rooms/rooms.html
@@ -65,7 +65,7 @@
     
     <div class="rooms" ng-repeat="(rm_id, room) in rooms">
         <div>
-            <a href="#/room/{{ room.room_alias ? room.room_alias : rm_id }}" >{{ room.room_alias }}</a> {{room.membership === 'invite' ? ' (invited)' : ''}}
+            <a href="#/room/{{ room.room_alias ? room.room_alias : rm_id }}" >{{ room.room_display_name }}</a> {{room.membership === 'invite' ? ' (invited)' : ''}}
         </div>
     </div>
     <br/>