diff options
-rw-r--r-- | docs/client-server/specification.rst | 17 | ||||
-rw-r--r-- | docs/specification.rst | 168 | ||||
-rw-r--r-- | webclient/app-controller.js | 36 | ||||
-rw-r--r-- | webclient/app-filter.js | 41 | ||||
-rwxr-xr-x | webclient/app.css | 4 | ||||
-rw-r--r-- | webclient/index.html | 13 | ||||
-rw-r--r-- | webclient/recents/recents-controller.js | 7 | ||||
-rw-r--r-- | webclient/recents/recents.html | 2 | ||||
-rw-r--r-- | webclient/room/room-controller.js | 53 | ||||
-rw-r--r-- | webclient/room/room.html | 19 |
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> <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 }} |