Merge remote-tracking branch 'origin/develop' into server2server_tls

paul/schema_breaking_changes
Mark Haines 2014-09-01 18:30:07 +01:00
commit 00b042a3eb
10 changed files with 261 additions and 101 deletions

View File

@ -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

View File

@ -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,25 +524,110 @@ 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.
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.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.
``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
``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.
Non-state messages
------------------
- m.room.message
- m.room.message.feedback (and compressed format)
- voip?
What are they, when are they used, what do they contain, how should they be used
m.room.message msgtypes
-----------------------
Each ``m.room.message`` MUST have a ``msgtype`` key which identifies the type of
@ -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

View File

@ -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();
@ -37,7 +37,11 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
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.
@ -85,6 +89,26 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
$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() {
}
}]);

View File

@ -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;
};
}]);

View File

@ -43,6 +43,10 @@ a:active { color: #000; }
height: 32px;
}
#callBar {
float: left;
}
#headerContent {
color: #ccc;
max-width: 1280px;

View File

@ -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>

View File

@ -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();
});
};
}]);

View File

@ -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' }}

View File

@ -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;
$rootScope.currentCall = call;
}
$scope.onCallError = function(errStr) {
$scope.feedback = errStr;
}
$scope.onCallHangup = function() {
}
}]);

View File

@ -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 }}