summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--docs/client-server/specification.rst17
-rw-r--r--docs/specification.rst168
-rw-r--r--webclient/app-controller.js36
-rw-r--r--webclient/app-filter.js41
-rwxr-xr-xwebclient/app.css4
-rw-r--r--webclient/index.html13
-rw-r--r--webclient/recents/recents-controller.js7
-rw-r--r--webclient/recents/recents.html2
-rw-r--r--webclient/room/room-controller.js53
-rw-r--r--webclient/room/room.html19
10 files changed, 260 insertions, 100 deletions
diff --git a/docs/client-server/specification.rst b/docs/client-server/specification.rst
index 4c9e313a6a..2f6645ceb9 100644
--- a/docs/client-server/specification.rst
+++ b/docs/client-server/specification.rst
@@ -1007,26 +1007,15 @@ for users from other servers entirely.
 Presence
 ========
 
-In the following messages, the presence state is an integer enumeration of the
-following states:
-  0 : OFFLINE
-  1 : BUSY
-  2 : ONLINE
-  3 : FREE_TO_CHAT
-
-Aside from OFFLINE, the protocol doesn't assign any special meaning to these
-states; they are provided as an approximate signal for users to give to other
-users and for clients to present them in some way that may be useful. Clients
-could have different behaviours for different states of the user's presence, for
-example to decide how much prominence or sound to use for incoming event
-notifications.
+In the following messages, the presence state is a presence string as described in
+the main specification document.
 
 Getting/Setting your own presence state
 ---------------------------------------
   REST Path: /presence/$user_id/status
   Valid methods: GET/PUT
   Required keys:
-    presence : [0|1|2|3] - The user's new presence state
+    presence : <string> - The user's new presence state
   Optional keys:
     status_msg : text string provided by the user to explain their status
 
diff --git a/docs/specification.rst b/docs/specification.rst
index 9a494a4c0f..23c6b12091 100644
--- a/docs/specification.rst
+++ b/docs/specification.rst
@@ -417,7 +417,7 @@ State events can be sent by ``PUT`` ing to ``/rooms/<room id>/state/<event type>
 These events will be overwritten if ``<room id>``, ``<event type>`` and ``<state key>`` all match.
 If the state event has no ``state_key``, it can be omitted from the path. These requests 
 **cannot use transaction IDs** like other ``PUT`` paths because they cannot be differentiated 
-from the ``state key``. Furthermore, ``POST`` is unsupported on state paths. Valid requests
+from the ``state_key``. Furthermore, ``POST`` is unsupported on state paths. Valid requests
 look like::
 
   PUT /rooms/!roomid:domain/state/m.example.event
@@ -440,7 +440,7 @@ Care should be taken to avoid setting the wrong ``state key``::
   { "key" : "with '11' as the state key, but was probably intended to be a txnId" }
 
 The ``state_key`` is often used to store state about individual users, by using the user ID as the
-value. For example::
+``state_key`` value. For example::
 
   PUT /rooms/!roomid:domain/state/m.favorite.animal.event/%40my_user%3Adomain.com
   { "animal" : "cat", "reason": "fluffy" }
@@ -471,7 +471,8 @@ Syncing rooms
 -------------
 When a client logs in, they may have a list of rooms which they have already joined. These rooms
 may also have a list of events associated with them. The purpose of 'syncing' is to present the
-current room and event information in a convenient, compact manner. There are two APIs provided:
+current room and event information in a convenient, compact manner. The events returned are not
+limited to room events; presence events will also be returned. There are two APIs provided:
 
  - ``/initialSync`` : A global sync which will present room and event information for all rooms
    the user has joined.
@@ -482,10 +483,40 @@ current room and event information in a convenient, compact manner. There are tw
 - TODO: JSON response format for both types
 - TODO: when would you use global? when would you use scoped?
 
-Getting grouped state events for a room
----------------------------------------
-- ``/members`` and ``/messages`` and the event types they return. Spec JSON response format.
-- ``/state`` and it returns ALL THE THINGS. 
+Getting events for a room
+-------------------------
+There are several APIs provided to ``GET`` events for a room:
+
+``/rooms/<room id>/state/<event type>/<state key>``
+  Description:
+    Get the state event identified.
+  Response format:
+    A JSON object representing the state event **content**.
+  Example:
+    ``/rooms/!room:domain.com/state/m.room.name`` returns ``{ "name": "Room name" }``
+
+``/rooms/<room id>/state``
+  Description:
+    Get all state events for a room.
+  Response format:
+    ``[ { state event }, { state event }, ... ]``
+  Example:
+    TODO
+
+
+``/rooms/<room id>/members``
+  Description:
+    Get all ``m.room.member`` state events.
+  Response format:
+    ``{ "start": "token", "end": "token", "chunk": [ { m.room.member event }, ... ] }``
+  Example:
+    TODO
+
+
+
+- ``/rooms/<room id>/messages`` : Get all ``m.room.message`` events.
+- ``/rooms/<room id>/initialSync`` : Get all relevant events for a room.
+
 
 Room Events
 ===========
@@ -493,24 +524,109 @@ Room Events
 This specification outlines several standard event types, all of which are
 prefixed with ``m.``
 
-State messages
---------------
-- m.room.name
-- m.room.topic
-- m.room.member
-- m.room.config
-- m.room.invite_join
+``m.room.name``
+  Summary:
+    Set the human-readable name for the room.
+  Type: 
+    State event
+  JSON format:
+    ``{ "name" : "string" }``
+  Example:
+    ``{ "name" : "My Room" }``
+  Description:
+    A room has an opaque room ID which is not human-friendly to read. A room alias is
+    human-friendly, but not all rooms have room aliases. The room name is a human-friendly
+    string designed to be displayed to the end-user. The room name is not *unique*, as
+    multiple rooms can have the same room name set. The room name can also be set when 
+    creating a room using ``/createRoom`` with the ``name`` key.
+
+``m.room.topic``
+  Summary:
+    Set a topic for the room.
+  Type: 
+    State event
+  JSON format:
+    ``{ "topic" : "string" }``
+  Example:
+    ``{ "topic" : "Welcome to the real world." }``
+  Description:
+    A topic is a short message detailing what is currently being discussed in the room. 
+    It can also be used as a way to display extra information about the room, which may
+    not be suitable for the room name.
 
-What are they, when are they used, what do they contain, how should they be used.
-Link back to explanatory sections (e.g. invite/join/leave sections for m.room.member)
+``m.room.member``
+  Summary:
+    The current membership state of a user in the room.
+  Type: 
+    State event
+  JSON format:
+    ``{ "membership" : "enum[ invite|join|leave|ban ]" }``
+  Example:
+    ``{ "membership" : "join" }``
+  Description:
+    Adjusts the membership state for a user in a room. It is preferable to use the
+    membership APIs (``/rooms/<room id>/invite`` etc) when performing membership actions
+    rather than adjusting the state directly as there are a restricted set of valid
+    transformations. For example, user A cannot force user B to join a room, and trying
+    to force this state change directly will fail. See the "Rooms" section for how to 
+    use the membership APIs.
+
+``m.room.config``
+  Summary:
+    The room config.
+  Type: 
+    State event
+  JSON format:
+    TODO
+  Example:
+    TODO
+  Description:
+    TODO
 
-Non-state messages
-------------------
-- m.room.message
-- m.room.message.feedback (and compressed format)
-- voip?
+``m.room.invite_join``
+  Summary:
+    TODO.
+  Type: 
+    State event
+  JSON format:
+    TODO
+  Example:
+    TODO
+  Description:
+    TODO
+
+``m.room.message``
+  Summary:
+    A message.
+  Type: 
+    Non-state event
+  JSON format:
+    ``{ "msgtype": "string" }``
+  Example:
+    ``{ "msgtype": "m.text", "body": "Testing" }``
+  Description:
+    This event is used when sending messages in a room. Messages are not limited to be text.
+    The ``msgtype`` key outlines the type of message, e.g. text, audio, image, video, etc.
+    Whilst not required, the ``body`` key SHOULD be used with every kind of ``msgtype`` as
+    a fallback mechanism when a client cannot render the message. For more information on 
+    the types of messages which can be sent, see "m.room.message msgtypes".
+
+``m.room.message.feedback``
+  Summary:
+    A receipt for a message.
+  Type: 
+    Non-state event
+  JSON format:
+    ``{ "type": "enum [ delivered|read ]", "target_event_id": "string" }``
+  Example:
+    ``{ "type": "delivered", "target_event_id": "e3b2icys" }``
+  Description:
+    Feedback events are events sent to acknowledge a message in some way. There are two
+    supported acknowledgements: ``delivered`` (sent when the event has been received) and 
+    ``read`` (sent when the event has been observed by the end-user). The ``target_event_id``
+    should reference the ``m.room.message`` event being acknowledged. 
 
-What are they, when are they used, what do they contain, how should they be used
+- voip?
 
 m.room.message msgtypes
 -----------------------
@@ -636,6 +752,14 @@ client devices they have connected. The home server should synchronise this
 status choice among multiple devices to ensure the user gets a consistent
 experience.
 
+In addition, the server maintains a timestamp of the last time it saw an active
+action from the user; either sending a message to a room, or changing presence
+state from a lower to a higher level of availability (thus: changing state from
+``unavailable`` to ``online`` will count as an action for being active, whereas
+in the other direction will not). This timestamp is presented via a key called
+``last_active_ago``, which gives the relative number of miliseconds since the
+message is generated/emitted, that the user was last seen active.
+
 Idle Time
 ---------
 As well as the basic ``presence`` field, the presence information can also show
diff --git a/webclient/app-controller.js b/webclient/app-controller.js
index 172770f82f..42c45f7c31 100644
--- a/webclient/app-controller.js
+++ b/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', 'matrixService', 'mPresence', 'eventStreamService',
-                               function($scope, $location, $rootScope, matrixService, mPresence, eventStreamService) {
+.controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', 'matrixService', 'mPresence', 'eventStreamService', 'matrixPhoneService',
+                               function($scope, $location, $rootScope, matrixService, mPresence, eventStreamService, matrixPhoneService) {
          
     // Check current URL to avoid to display the logout button on the login page
     $scope.location = $location.path();
@@ -36,8 +36,12 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
         eventStreamService.resume();
         mPresence.start();
     }
-    
-    $scope.user_id = matrixService.config().user_id;
+
+    $scope.user_id;
+    var config = matrixService.config();
+    if (config) {
+        $scope.user_id = matrixService.config().user_id;
+    }
     
     /**
      * Open a given page.
@@ -84,7 +88,27 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
     $scope.updateHeader = function() {
         $scope.user_id = matrixService.config().user_id;
     };
+
+    $rootScope.$on(matrixPhoneService.INCOMING_CALL_EVENT, function(ngEvent, call) {
+        console.trace("incoming call");
+        call.onError = $scope.onCallError;
+        call.onHangup = $scope.onCallHangup;
+        $rootScope.currentCall = call;
+    });
+
+    $scope.answerCall = function() {
+        $scope.currentCall.answer();
+    };
+
+    $scope.hangupCall = function() {
+        $scope.currentCall.hangup();
+        $scope.currentCall = undefined;
+    };
     
-}]);
+    $rootScope.onCallError = function(errStr) {
+        $scope.feedback = errStr;
+    }
 
-   
+    $rootScope.onCallHangup = function() {
+    }
+}]);
diff --git a/webclient/app-filter.js b/webclient/app-filter.js
index b8f4ed25bc..b8d3d2a0d8 100644
--- a/webclient/app-filter.js
+++ b/webclient/app-filter.js
@@ -70,7 +70,7 @@ angular.module('matrixWebClient')
         });
 
         filtered.sort(function (a, b) {
-            return ((a["mtime_age"] || 10e10) > (b["mtime_age"] || 10e10) ? 1 : -1);
+            return ((a["last_active_ago"] || 10e10) > (b["last_active_ago"] || 10e10) ? 1 : -1);
         });
         return filtered;
     };
@@ -79,4 +79,43 @@ angular.module('matrixWebClient')
     return function(text) {
         return $sce.trustAsHtml(text);
     };
+}])
+
+// Compute the room name according to information we have
+.filter('roomName', ['$rootScope', 'matrixService', function($rootScope, matrixService) {
+    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);
+        if (alias) {
+            roomName = alias;
+        }
+
+        if (undefined === roomName) {
+            // Else, build the name from its users
+            var room = $rootScope.events.rooms[room_id];
+            if (room) {
+                if (room.members) {
+                    // Limit the room renaming to 1:1 room
+                    if (2 === Object.keys(room.members).length) {
+                        for (var i in room.members) {
+                            var member = room.members[i];
+                            if (member.user_id !== matrixService.config().user_id) {
+                                roomName = member.content.displayname ?  member.content.displayname : member.user_id;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        if (undefined === roomName) {
+            // By default, use the room ID
+            roomName = room_id;
+        }
+
+        return roomName;
+    };
 }]);
diff --git a/webclient/app.css b/webclient/app.css
index cd1820e155..8685032d72 100755
--- a/webclient/app.css
+++ b/webclient/app.css
@@ -43,6 +43,10 @@ a:active  { color: #000; }
     height: 32px;
 }
 
+#callBar {
+   float: left;
+}
+
 #headerContent {
     color: #ccc;
     max-width: 1280px;
diff --git a/webclient/index.html b/webclient/index.html
index bf24e392ac..f016dbb877 100644
--- a/webclient/index.html
+++ b/webclient/index.html
@@ -44,6 +44,19 @@
     <div id="header">
         <!-- Do not show buttons on the login page -->
         <div id="headerContent" ng-hide="'/login' == location || '/register' == location">
+            <div id="callBar">
+                <div ng-show="currentCall.state == 'ringing'">
+                Incoming call from {{ currentCall.user_id }}
+                <button ng-click="answerCall()">Answer</button>
+                <button ng-click="hangupCall()">Reject</button>
+                </div>
+                <button ng-click="hangupCall()" ng-show="currentCall && currentCall.state != 'ringing' && currentCall.state != 'ended' && currentCall.state != 'fledgling'">Hang up</button>
+                <span ng-show="currentCall.state == 'invite_sent'">Calling...</span>
+                <span ng-show="currentCall.state == 'connecting'">Call Connecting...</span>
+                <span ng-show="currentCall.state == 'connected'">Call Connected</span>
+                <span ng-show="currentCall.state == 'ended'">Call Ended</span>
+                <span style="display: none; ">{{ currentCall.state }}</span>
+            </div>
             <a href id="headerUserId" ng-click='goToUserPage(user_id)'>{{ user_id }}</a>
             &nbsp;
             <button ng-click='goToPage("/")'>Home</button>
diff --git a/webclient/recents/recents-controller.js b/webclient/recents/recents-controller.js
index c9fd022d7f..947bd29de3 100644
--- a/webclient/recents/recents-controller.js
+++ b/webclient/recents/recents-controller.js
@@ -33,8 +33,7 @@ angular.module('RecentsController', ['matrixService', 'eventHandlerService'])
                 console.log("Invited to room " + event.room_id);
                 // 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_display_name = event.user_id + "'s room";
+
                 $scope.rooms[event.room_id] = event;
             }
         });
@@ -88,7 +87,9 @@ angular.module('RecentsController', ['matrixService', 'eventHandlerService'])
     };
 
     $scope.onInit = function() {
-        refresh();
+        eventHandlerService.waitForInitialSyncCompletion().then(function() {
+            refresh();
+        });
     };
     
 }]);
diff --git a/webclient/recents/recents.html b/webclient/recents/recents.html
index 56fb38b02a..db3b0fb32f 100644
--- a/webclient/recents/recents.html
+++ b/webclient/recents/recents.html
@@ -6,7 +6,7 @@
                ng-class="{'recentsRoomSelected': (room.room_id === recentsSelectedRoomID)}">
             <tr>
                 <td class="recentsRoomName">
-                    {{ room.room_display_name }}
+                    {{ room.room_id | roomName }}
                 </td>
                 <td class="recentsRoomSummaryTS">
                     {{ (room.lastMsg.ts) | date:'MMM d HH:mm' }}
diff --git a/webclient/room/room-controller.js b/webclient/room/room-controller.js
index 046d1ca204..9861b25617 100644
--- a/webclient/room/room-controller.js
+++ b/webclient/room/room-controller.js
@@ -82,13 +82,6 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
         updatePresence(event);
     });
 
-    $rootScope.$on(matrixPhoneService.INCOMING_CALL_EVENT, function(ngEvent, call) {
-        console.trace("incoming call");
-        call.onError = $scope.onCallError;
-        call.onHangup = $scope.onCallHangup;
-        $scope.currentCall = call;
-    });
-    
     $scope.memberCount = function() {
         return Object.keys($scope.members).length;
     };
@@ -100,15 +93,6 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
         }
     };
 
-    $scope.answerCall = function() {
-        $scope.currentCall.answer();
-    };
-
-    $scope.hangupCall = function() {
-        $scope.currentCall.hangup();
-        $scope.currentCall = undefined;
-    };
-        
     var paginate = function(numItems) {
         // console.log("paginate " + numItems);
         if ($scope.state.paginating || !$scope.room_id) {
@@ -181,11 +165,11 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
         var isNewMember = !(target_user_id in $scope.members);
         if (isNewMember) {
             // FIXME: why are we copying these fields around inside chunk?
-            if ("state" in chunk.content) {
-                chunk.presenceState = chunk.content.state; // why is this renamed?
+            if ("presence" in chunk.content) {
+                chunk.presence = chunk.content.presence;
             }
-            if ("mtime_age" in chunk.content) {
-                chunk.mtime_age = chunk.content.mtime_age;
+            if ("last_active_ago" in chunk.content) {
+                chunk.last_active_ago = chunk.content.last_active_ago;
             }
             if ("displayname" in chunk.content) {
                 chunk.displayname = chunk.content.displayname;
@@ -204,11 +188,11 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
             // selectively update membership and presence else it will nuke the picture and displayname too :/
             var member = $scope.members[target_user_id];
             member.membership = chunk.content.membership;
-            if ("state" in chunk.content) {
-                member.presenceState = chunk.content.state;
+            if ("presence" in chunk.content) {
+                member.presence = chunk.content.presence;
             }
-            if ("mtime_age" in chunk.content) {
-                member.mtime_age = chunk.content.mtime_age;
+            if ("last_active_ago" in chunk.content) {
+                member.last_active_ago = chunk.content.last_active_ago;
             }
         }
     };
@@ -227,13 +211,12 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
         var member = $scope.members[chunk.content.user_id];
 
         // XXX: why not just pass the chunk straight through?
-        if ("state" in chunk.content) {
-            member.presenceState = chunk.content.state;
+        if ("presence" in chunk.content) {
+            member.presence = chunk.content.presence;
         }
 
-        if ("mtime_age" in chunk.content) {
-            // FIXME: should probably keep updating mtime_age in realtime like FB does
-            member.mtime_age = chunk.content.mtime_age;
+        if ("last_active_ago" in chunk.content) {
+            member.last_active_ago = chunk.content.last_active_ago;
         }
 
         // this may also contain a new display name or avatar url, so check.
@@ -478,16 +461,10 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
 
     $scope.startVoiceCall = function() {
         var call = new MatrixCall($scope.room_id);
-        call.onError = $scope.onCallError;
-        call.onHangup = $scope.onCallHangup;
+        call.onError = $rootScope.onCallError;
+        call.onHangup = $rootScope.onCallHangup;
         call.placeCall();
-        $scope.currentCall = call;
-    }
-
-    $scope.onCallError = function(errStr) {
-        $scope.feedback = errStr;
+        $rootScope.currentCall = call;
     }
 
-    $scope.onCallHangup = function() {
-    }
 }]);
diff --git a/webclient/room/room.html b/webclient/room/room.html
index d5b0f0ab96..e25c837aa0 100644
--- a/webclient/room/room.html
+++ b/webclient/room/room.html
@@ -3,7 +3,7 @@
     <div id="roomHeader">
         <a href ng-click="goToPage('/')"><img src="img/logo-small.png" width="100" height="43" alt="[matrix]"/></a>
         <div id="roomName">
-            {{ room_alias || room_id }}
+            {{ room_id  | roomName }}
         </div>
     </div>
 
@@ -26,8 +26,8 @@
                     <img class="userAvatarGradient" src="img/gradient.png" title="{{ member.id }}" width="80" height="24"/>
                     <div class="userName">{{ member.displayname || member.id.substr(0, member.id.indexOf(':')) }}<br/>{{ member.displayname ? "" : member.id.substr(member.id.indexOf(':')) }}</div>
                 </td>
-                <td class="userPresence" ng-class="(member.presenceState === 'online' ? 'online' : (member.presenceState === 'unavailable' ? 'unavailable' : '')) + ' ' + (member.membership == 'invite' ? 'invited' : '')">
-                    <span ng-show="member.mtime_age">{{ member.mtime_age + (now - member.last_updated) | duration }}<br/>ago</span>
+                <td class="userPresence" ng-class="(member.presence === 'online' ? 'online' : (member.presence === 'unavailable' ? 'unavailable' : '')) + ' ' + (member.membership == 'invite' ? 'invited' : '')">
+                    <span ng-show="member.last_active_ago">{{ member.last_active_ago + (now - member.last_updated) | duration }}<br/>ago</span>
                 </td>
         </table>
     </div>
@@ -100,18 +100,7 @@
                         <button ng-click="inviteUser(userIDToInvite)">Invite</button>
                 </span>
                 <button ng-click="leaveRoom()">Leave</button>
-                <button ng-click="startVoiceCall()" ng-show="currentCall == undefined && memberCount() == 2">Voice Call</button>
-                <div ng-show="currentCall.state == 'ringing'">
-                Incoming call from {{ currentCall.user_id }}
-                <button ng-click="answerCall()">Answer</button>
-                <button ng-click="hangupCall()">Reject</button>
-                </div>
-                <button ng-click="hangupCall()" ng-show="currentCall && currentCall.state != 'ringing' && currentCall.state != 'ended' && currentCall.state != 'fledgling'">Hang up</button>
-                <span ng-show="currentCall.state == 'invite_sent'">Calling...</span>
-                <span ng-show="currentCall.state == 'connecting'">Call Connecting...</span>
-                <span ng-show="currentCall.state == 'connected'">Call Connected</span>
-                <span ng-show="currentCall.state == 'ended'">Call Ended</span>
-                <span style="display: none; ">{{ currentCall.state }}</span>
+                <button ng-click="startVoiceCall()" ng-show="(currentCall == undefined || currentCall.state == 'ended') && memberCount() == 2">Voice Call</button>
             </div>
         
             {{ feedback }}