From bf6466f02a6ffd7d73f6e0506115e611540d0cb7 Mon Sep 17 00:00:00 2001 From: Emmanuel ROHEE Date: Fri, 29 Aug 2014 15:29:26 +0200 Subject: [PATCH 01/45] The away state is unavailable not offline --- webclient/components/matrix/presence-service.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/webclient/components/matrix/presence-service.js b/webclient/components/matrix/presence-service.js index 6a1edcaf4..555118133 100644 --- a/webclient/components/matrix/presence-service.js +++ b/webclient/components/matrix/presence-service.js @@ -23,9 +23,9 @@ angular.module('mPresence', []) .service('mPresence', ['$timeout', 'matrixService', function ($timeout, matrixService) { - // Time in ms after that a user is considered as offline/away - var OFFLINE_TIME = 5 * 60000; // 5 mins - + // Time in ms after that a user is considered as unavailable/away + var UNAVAILABLE_TIME = 5 * 60000; // 5 mins + // The current presence state var state = undefined; @@ -88,11 +88,11 @@ angular.module('mPresence', []) }; /** - * Callback called when the user made no action on the page for OFFLINE_TIME ms. + * Callback called when the user made no action on the page for UNAVAILABLE_TIME ms. * @private */ - function onOfflineTimerFire() { - self.setState(matrixService.presence.offline); + function onUnvailableTimerFire() { + self.setState(matrixService.presence.unavailable); } /** @@ -105,7 +105,7 @@ angular.module('mPresence', []) // Re-arm the timer $timeout.cancel(timer); - timer = $timeout(onOfflineTimerFire, OFFLINE_TIME); + timer = $timeout(onUnvailableTimerFire, UNAVAILABLE_TIME); } }]); From 7c4b47652e0acafecfbee60b033bc34d11d8b2b3 Mon Sep 17 00:00:00 2001 From: Emmanuel ROHEE Date: Fri, 29 Aug 2014 15:29:26 +0200 Subject: [PATCH 02/45] The away state is unavailable not offline --- webclient/components/matrix/presence-service.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/webclient/components/matrix/presence-service.js b/webclient/components/matrix/presence-service.js index 6a1edcaf4..555118133 100644 --- a/webclient/components/matrix/presence-service.js +++ b/webclient/components/matrix/presence-service.js @@ -23,9 +23,9 @@ angular.module('mPresence', []) .service('mPresence', ['$timeout', 'matrixService', function ($timeout, matrixService) { - // Time in ms after that a user is considered as offline/away - var OFFLINE_TIME = 5 * 60000; // 5 mins - + // Time in ms after that a user is considered as unavailable/away + var UNAVAILABLE_TIME = 5 * 60000; // 5 mins + // The current presence state var state = undefined; @@ -88,11 +88,11 @@ angular.module('mPresence', []) }; /** - * Callback called when the user made no action on the page for OFFLINE_TIME ms. + * Callback called when the user made no action on the page for UNAVAILABLE_TIME ms. * @private */ - function onOfflineTimerFire() { - self.setState(matrixService.presence.offline); + function onUnvailableTimerFire() { + self.setState(matrixService.presence.unavailable); } /** @@ -105,7 +105,7 @@ angular.module('mPresence', []) // Re-arm the timer $timeout.cancel(timer); - timer = $timeout(onOfflineTimerFire, OFFLINE_TIME); + timer = $timeout(onUnvailableTimerFire, UNAVAILABLE_TIME); } }]); From 339dd3dc6c44d84e869e435ca6e1520ccc587680 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 29 Aug 2014 14:12:12 +0100 Subject: [PATCH 03/45] Update API swagger JSON to default to POSTs not PUTs when describing the API. --- docs/client-server/swagger_matrix/rooms | 80 ++++++------------------- 1 file changed, 19 insertions(+), 61 deletions(-) diff --git a/docs/client-server/swagger_matrix/rooms b/docs/client-server/swagger_matrix/rooms index bb49ec5a6..1c32d135a 100644 --- a/docs/client-server/swagger_matrix/rooms +++ b/docs/client-server/swagger_matrix/rooms @@ -14,12 +14,12 @@ }, "apis": [ { - "path": "/rooms/{roomId}/send/{eventType}/{txnId}", + "path": "/rooms/{roomId}/send/{eventType}", "operations": [ { - "method": "PUT", + "method": "POST", "summary": "Send a generic non-state event to this room.", - "notes": "This operation can also be done as a POST to /rooms/{roomId}/send/{eventType}", + "notes": "This operation can also be done as a PUT by suffixing /{txnId}.", "type": "EventId", "nickname": "send_non_state_event", "consumes": [ @@ -46,13 +46,6 @@ "required": true, "type": "string", "paramType": "path" - }, - { - "name": "txnId", - "description": "A client transaction ID to ensure idempotency. This can only be omitted if the HTTP method becomes a POST.", - "required": true, - "type": "string", - "paramType": "path" } ] } @@ -104,12 +97,12 @@ ] }, { - "path": "/rooms/{roomId}/send/m.room.message/{txnId}", + "path": "/rooms/{roomId}/send/m.room.message", "operations": [ { - "method": "PUT", + "method": "POST", "summary": "Send a message in this room.", - "notes": "This operation can also be done as a POST to /rooms/{roomId}/send/m.room.message", + "notes": "This operation can also be done as a PUT by suffixing /{txnId}.", "type": "EventId", "nickname": "send_message", "consumes": [ @@ -129,13 +122,6 @@ "required": true, "type": "string", "paramType": "path" - }, - { - "name": "txnId", - "description": "A client transaction ID to ensure idempotency. This can only be omitted if the HTTP method becomes a POST.", - "required": true, - "type": "string", - "paramType": "path" } ] } @@ -195,12 +181,12 @@ ] }, { - "path": "/rooms/{roomId}/send/m.room.message.feedback/{txnId}", + "path": "/rooms/{roomId}/send/m.room.message.feedback", "operations": [ { - "method": "PUT", + "method": "POST", "summary": "Send feedback to a message.", - "notes": "This operation can also be done as a POST to /rooms/{roomId}/send/m.room.message.feedback", + "notes": "This operation can also be done as a PUT by suffixing /{txnId}.", "type": "EventId", "nickname": "send_feedback", "consumes": [ @@ -220,13 +206,6 @@ "required": true, "type": "string", "paramType": "path" - }, - { - "name": "txnId", - "description": "A client transaction ID to ensure idempotency. This can only be omitted if the HTTP method becomes a POST.", - "required": true, - "type": "string", - "paramType": "path" } ], "responseMessages": [ @@ -239,12 +218,12 @@ ] }, { - "path": "/rooms/{roomId}/invite/{txnId}", + "path": "/rooms/{roomId}/invite", "operations": [ { - "method": "PUT", + "method": "POST", "summary": "Invite a user to this room.", - "notes": "This operation can also be done as a POST to /rooms/{roomId}/invite", + "notes": "This operation can also be done as a PUT by suffixing /{txnId}.", "type": "void", "nickname": "invite", "consumes": [ @@ -258,13 +237,6 @@ "type": "string", "paramType": "path" }, - { - "name": "txnId", - "description": "A client transaction ID for this PUT to ensure idempotency. This can only be omitted if the HTTP method becomes a POST. ", - "required": false, - "type": "string", - "paramType": "path" - }, { "name": "body", "description": "The user to invite.", @@ -277,12 +249,12 @@ ] }, { - "path": "/rooms/{roomId}/join/{txnId}", + "path": "/rooms/{roomId}/join", "operations": [ { - "method": "PUT", + "method": "POST", "summary": "Join this room.", - "notes": "This operation can also be done as a POST to /rooms/{roomId}/join", + "notes": "This operation can also be done as a PUT by suffixing /{txnId}.", "type": "void", "nickname": "join_room", "consumes": [ @@ -295,25 +267,18 @@ "required": true, "type": "string", "paramType": "path" - }, - { - "name": "txnId", - "description": "A client transaction ID for this PUT to ensure idempotency. This can only be omitted if the HTTP method becomes a POST. ", - "required": false, - "type": "string", - "paramType": "path" } ] } ] }, { - "path": "/rooms/{roomId}/leave/{txnId}", + "path": "/rooms/{roomId}/leave", "operations": [ { - "method": "PUT", + "method": "POST", "summary": "Leave this room.", - "notes": "This operation can also be done as a POST to /rooms/{roomId}/leave", + "notes": "This operation can also be done as a PUT by suffixing /{txnId}.", "type": "void", "nickname": "leave", "consumes": [ @@ -326,13 +291,6 @@ "required": true, "type": "string", "paramType": "path" - }, - { - "name": "txnId", - "description": "A client transaction ID for this PUT to ensure idempotency. This can only be omitted if the HTTP method becomes a POST. ", - "required": false, - "type": "string", - "paramType": "path" } ] } @@ -476,7 +434,7 @@ "parameters": [ { "name": "body", - "description": "The desired configuration for the room.", + "description": "The desired configuration for the room. This operation can also be done as a PUT by suffixing /{txnId}.", "required": true, "type": "RoomConfig", "paramType": "body" From 1cdc29e2602a17e156babb9c8a35f96a1f53a174 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 29 Aug 2014 15:01:46 +0100 Subject: [PATCH 04/45] Fix JSFiddles to work with the new C-S API. --- jsfiddles/create_room_send_msg/demo.js | 17 +++--- jsfiddles/event_stream/demo.js | 18 +++---- jsfiddles/example_app/demo.js | 56 +++++++++++++------- jsfiddles/register_login/demo.js | 4 +- jsfiddles/room_memberships/demo.html | 6 +-- jsfiddles/room_memberships/demo.js | 73 ++++++++++++-------------- 6 files changed, 93 insertions(+), 81 deletions(-) diff --git a/jsfiddles/create_room_send_msg/demo.js b/jsfiddles/create_room_send_msg/demo.js index c17eb26b9..db2ae2d60 100644 --- a/jsfiddles/create_room_send_msg/demo.js +++ b/jsfiddles/create_room_send_msg/demo.js @@ -25,11 +25,12 @@ $('.login').live('click', function() { }); var getCurrentRoomList = function() { - var url = "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=" + accountInfo.access_token + "&from=END&to=START&limit=1"; + var url = "http://localhost:8080/matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1"; $.getJSON(url, function(data) { - for (var i=0; i=0; --i) { addMessage(data.chunk[i]); @@ -190,7 +193,8 @@ var getMessages = function(roomId) { var getMemberList = function(roomId) { $("#members").empty(); memberInfo = []; - var url = "http://localhost:8080/matrix/client/api/v1/rooms/" + roomId + "/members/list?access_token=" + accountInfo.access_token; + var url = "http://localhost:8080/matrix/client/api/v1/rooms/" + + encodeURIComponent(roomId) + "/members?access_token=" + accountInfo.access_token; $.getJSON(url, function(data) { for (var i=0; i"; + } + else if (data.content.membership === "join") { + msg = "joined the room"; + } + else if (data.content.membership === "leave") { + msg = "left the room"; + } + else { + msg = "" + data.content.membership + ""; + } + } + var row = "" + ""+data.user_id+"" + - ""+data.content.body+"" + + ""+msg+"" + ""; $("#messages").append(row); }; var addMember = function(data) { var row = "" + - ""+data.target_user_id+"" + + ""+data.state_key+"" + ""+data.content.membership+"" + ""; $("#members").append(row); diff --git a/jsfiddles/register_login/demo.js b/jsfiddles/register_login/demo.js index 1644f76ac..270a96786 100644 --- a/jsfiddles/register_login/demo.js +++ b/jsfiddles/register_login/demo.js @@ -45,7 +45,7 @@ $('.login').live('click', function() { var user = $("#userLogin").val(); var password = $("#passwordLogin").val(); $.getJSON("http://localhost:8080/matrix/client/api/v1/login", function(data) { - if (data.type !== "m.login.password") { + if (data.flows[0].type !== "m.login.password") { alert("I don't know how to login with this type: " + data.type); return; } @@ -60,7 +60,7 @@ $('.logout').live('click', function() { }); $('.testToken').live('click', function() { - var url = "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=" + accountInfo.access_token + "&from=END&to=START&limit=1"; + var url = "http://localhost:8080/matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1"; $.getJSON(url, function(data) { $("#imSyncText").text(JSON.stringify(data, undefined, 2)); }).fail(function(err) { diff --git a/jsfiddles/room_memberships/demo.html b/jsfiddles/room_memberships/demo.html index 96232e827..4c1bf6b4b 100644 --- a/jsfiddles/room_memberships/demo.html +++ b/jsfiddles/room_memberships/demo.html @@ -14,9 +14,9 @@ diff --git a/jsfiddles/room_memberships/demo.js b/jsfiddles/room_memberships/demo.js index b3ce0f0e3..91cc96ab6 100644 --- a/jsfiddles/room_memberships/demo.js +++ b/jsfiddles/room_memberships/demo.js @@ -4,6 +4,14 @@ var showLoggedIn = function(data) { accountInfo = data; getCurrentRoomList(); $(".loggedin").css({visibility: "visible"}); + $("#membership").change(function() { + if ($("#membership").val() === "invite") { + $("#targetUser").css({visibility: "visible"}); + } + else { + $("#targetUser").css({visibility: "hidden"}); + } +}); }; $('.login').live('click', function() { @@ -31,10 +39,11 @@ var getCurrentRoomList = function() { // solution but that is out of scope of this fiddle. $("#rooms").find("tr:gt(0)").remove(); - var url = "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=" + accountInfo.access_token + "&from=END&to=START&limit=1"; + var url = "http://localhost:8080/matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1"; $.getJSON(url, function(data) { - for (var i=0; i Date: Fri, 29 Aug 2014 15:18:37 +0100 Subject: [PATCH 05/45] better support for call hangups --- webclient/components/matrix/matrix-call.js | 32 +++++++++++----------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/webclient/components/matrix/matrix-call.js b/webclient/components/matrix/matrix-call.js index 45d00ee79..c0a7735a7 100644 --- a/webclient/components/matrix/matrix-call.js +++ b/webclient/components/matrix/matrix-call.js @@ -73,9 +73,7 @@ angular.module('MatrixCall', []) this.state = 'wait_local_media'; }; - MatrixCall.prototype.hangup = function() { - console.trace("Ending call "+this.call_id); - + MatrixCall.prototype.stopAllMedia = function() { if (this.localAVStream) { forAllTracksOnStream(this.localAVStream, function(t) { t.stop(); @@ -86,6 +84,12 @@ angular.module('MatrixCall', []) t.stop(); }); } + }; + + MatrixCall.prototype.hangup = function() { + console.trace("Ending call "+this.call_id); + + this.stopAllMedia(); var content = { version: 0, @@ -232,8 +236,9 @@ angular.module('MatrixCall', []) t.onstarted = self.onRemoteStreamTrackStarted; }); + event.stream.onended = function(e) { self.onRemoteStreamEnded(e); }; // not currently implemented in chrome - event.stream.onstarted = this.onRemoteStreamStarted; + event.stream.onstarted = function(e) { self.onRemoteStreamStarted(e); }; var player = new Audio(); player.src = URL.createObjectURL(s); player.play(); @@ -243,24 +248,19 @@ angular.module('MatrixCall', []) this.state = 'connected'; }; + MatrixCall.prototype.onRemoteStreamEnded = function(event) { + this.state = 'ended'; + this.stopAllMedia(); + this.onHangup(); + }; + MatrixCall.prototype.onRemoteStreamTrackStarted = function(event) { this.state = 'connected'; }; MatrixCall.prototype.onHangupReceived = function() { this.state = 'ended'; - - if (this.localAVStream) { - forAllTracksOnStream(this.localAVStream, function(t) { - t.stop(); - }); - } - if (this.remoteAVStream) { - forAllTracksOnStream(this.remoteAVStream, function(t) { - t.stop(); - }); - } - + this.stopAllMedia(); this.onHangup(); }; From c3a774e414324746c484ad21d19c4be4e2167cd0 Mon Sep 17 00:00:00 2001 From: Emmanuel ROHEE Date: Fri, 29 Aug 2014 17:11:03 +0200 Subject: [PATCH 06/45] Show desktop notification state. Provide help if the user has previously denied permission to display them. --- webclient/app-controller.js | 7 ------- webclient/settings/settings-controller.js | 21 ++++++++++++++++++- webclient/settings/settings.html | 25 +++++++++++++++++------ 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/webclient/app-controller.js b/webclient/app-controller.js index 80474bb8d..775113bc8 100644 --- a/webclient/app-controller.js +++ b/webclient/app-controller.js @@ -69,13 +69,6 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even $scope.logout(); }); - $scope.requestNotifications = function() { - if (window.Notification) { - console.log("Notification.permission: " + window.Notification.permission); - window.Notification.requestPermission(function(){}); - } - }; - }]); diff --git a/webclient/settings/settings-controller.js b/webclient/settings/settings-controller.js index f7d5e8eb7..3a9060eba 100644 --- a/webclient/settings/settings-controller.js +++ b/webclient/settings/settings-controller.js @@ -25,7 +25,7 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu displayName: $scope.config.displayName, avatarUrl: $scope.config.avatarUrl }; - + $scope.$watch("profile.avatarFile", function(newValue, oldValue) { if ($scope.profile.avatarFile) { console.log("Uploading new avatar file..."); @@ -143,4 +143,23 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu } ); }; + + + /*** Desktop notifications section ***/ + $scope.settings = { + notifications: undefined + }; + + // If the browser supports it, check the desktop notification state + if ("Notification" in window) { + $scope.settings.notifications = window.Notification.permission; + } + + $scope.requestNotifications = function() { + console.log("requestNotifications"); + window.Notification.requestPermission(function (permission) { + console.log(" -> User decision: " + permission); + $scope.settings.notifications = permission; + }); + }; }]); \ No newline at end of file diff --git a/webclient/settings/settings.html b/webclient/settings/settings.html index 453a4fc35..d06a0083f 100644 --- a/webclient/settings/settings.html +++ b/webclient/settings/settings.html @@ -52,7 +52,25 @@
- + +

Desktop notifications

+
+
+ Notifications are enabled. +
+
+ You have denied permission for notifications.
+ To enable it, reset the notification setting for this web site into your browser settings. +
+
+ +
+
+ Sorry, your browser does not support notifications. +
+
+
+

Configuration

Home server: {{ config.homeserver }}
@@ -60,11 +78,6 @@
Access token: {{ config.access_token }}

- -
-
-
-
{{ feedback }} From ca025c2b1d6477508e4d9af47ae772ee2a5f8db8 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 29 Aug 2014 16:50:26 +0100 Subject: [PATCH 07/45] Rooms: More subsections, more bullet points, more detail. --- docs/specification.rst | 97 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 88 insertions(+), 9 deletions(-) diff --git a/docs/specification.rst b/docs/specification.rst index d4a01a3fc..e571db7c3 100644 --- a/docs/specification.rst +++ b/docs/specification.rst @@ -216,22 +216,101 @@ In contrast, these are invalid requests:: Receiving live updates on a client ---------------------------------- -- C-S longpoll event stream -- Concept of start/end tokens. -- Mention /initialSync to get token. +Clients can receive new events by long-polling the home server. This will hold open the +HTTP connection for a short period of time waiting for new events, returning early if an +event occurs. This is called the "Event Stream". All events which the client is authorised +to view will appear in the event stream. When the stream is closed, an ``end`` token is +returned. This token can be used in the next request to continue where the client left off. +When the client first logs in, they will need to initially synchronise with their home +server. This is achieved via the ``/initialSync`` API. This API also returns an ``end`` +token which can be used with the event stream. Rooms ===== -- How are they created? PDU anchor point: "root of the tree". + +Creation +-------- +To create a room, a client has to use the ``/createRoom`` API. There are various options +which can be set when creating a room: + +``visibility`` + Type: + String + Optional: + Yes + Value: + Either ``public`` or ``private``. + Description: + A ``public`` visibility indicates that the room will be shown in the public room list. A + ``private`` visibility will hide the room from the public room list. Rooms default to + ``public`` visibility if this key is not included. + +``room_alias_name`` + Type: + String + Optional: + Yes + Value: + The room alias localpart. + Description: + If this is included, a room alias will be created and mapped to the newly created room. + The alias will belong on the same home server which created the room, e.g. + ``!qadnasoi:domain.com >>> #room_alias_name:domain.com`` + +Example:: + + { + "visibility": "public", + "room_alias_name": "the pub" + } + +- TODO: This creates a room creation event which serves as the root of the PDU graph for this room. + +Modifying aliases +----------------- - Adding / removing aliases. -- Invite/join dance -- State and non-state data (+extensibility) -TODO : Room permissions / config / power levels. +Permissions +----------- +- TODO : Room permissions / config / power levels. What they are. How do they work. Examples. -Messages -======== +Inviting users +-------------- +- API to hit (``$roomid/invite``) with ``user_id`` key. Needs FQ user ID, explain why. +- Outline invite join dance + +Joining rooms +------------- +- API to hit (``/join/$alias or id``). Explain how alias joining works (auto-resolving). +- Outline invite join dance + +Leaving rooms +------------- +- API to hit (``$roomid/leave``). +- Is there a dance? + +Room events +----------- +- Split into state and non-state data +- Explain what they are, semantics, give examples of clobbering / not, use cases (msgs vs room names). + Not too much detail on the actual event contents. +- API to hit. +- Extensibility provided by the API for custom events. Examples. +- How this hooks into ``initialSync``. +- See the "Room Events" section for actual spec on each type. + +Syncing a room +-------------- +- Single room initial sync. API to hit. Why it might be used (lazy loading) + +Getting grouped state events +---------------------------- +- ``/members`` and ``/messages`` and the events they return. +- ``/state`` and it returns ALL THE THINGS. + +Room Events +=========== This specification outlines several standard event types, all of which are prefixed with ``m.`` From 20d0db6cfb8efce079376eb6bd2c8cca4f4cab16 Mon Sep 17 00:00:00 2001 From: "Paul \"LeoNerd\" Evans" Date: Fri, 29 Aug 2014 17:09:15 +0100 Subject: [PATCH 08/45] Move the *EventSource classes into the handlers they relate to, so it's easier to find the code --- synapse/handlers/presence.py | 78 +++++++++++++++++++++ synapse/handlers/room.py | 48 +++++++++++++ synapse/streams/events.py | 131 ++--------------------------------- tests/rest/test_presence.py | 4 +- 4 files changed, 132 insertions(+), 129 deletions(-) diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index 7731de85c..1d3b02a9d 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -722,6 +722,84 @@ class PresenceHandler(BaseHandler): ) +class PresenceEventSource(object): + def __init__(self, hs): + self.hs = hs + self.clock = hs.get_clock() + + def get_new_events_for_user(self, user, from_token, limit): + from_key = int(from_token.presence_key) + + presence = self.hs.get_handlers().presence_handler + cachemap = presence._user_cachemap + + # TODO(paul): limit, and filter by visibility + updates = [(k, cachemap[k]) for k in cachemap + if from_key < cachemap[k].serial] + + if updates: + clock = self.clock + + latest_serial = max([x[1].serial for x in updates]) + data = [x[1].make_event(user=x[0], clock=clock) for x in updates] + + end_token = from_token.copy_and_replace( + "presence_key", latest_serial + ) + return ((data, end_token)) + else: + end_token = from_token.copy_and_replace( + "presence_key", presence._user_cachemap_latest_serial + ) + return (([], end_token)) + + def get_current_token_part(self): + presence = self.hs.get_handlers().presence_handler + return presence._user_cachemap_latest_serial + + def get_pagination_rows(self, user, pagination_config, key): + # TODO (erikj): Does this make sense? Ordering? + + from_token = pagination_config.from_token + to_token = pagination_config.to_token + + from_key = int(from_token.presence_key) + + if to_token: + to_key = int(to_token.presence_key) + else: + to_key = -1 + + presence = self.hs.get_handlers().presence_handler + cachemap = presence._user_cachemap + + # TODO(paul): limit, and filter by visibility + updates = [(k, cachemap[k]) for k in cachemap + if to_key < cachemap[k].serial < from_key] + + if updates: + clock = self.clock + + earliest_serial = max([x[1].serial for x in updates]) + data = [x[1].make_event(user=x[0], clock=clock) for x in updates] + + if to_token: + next_token = to_token + else: + next_token = from_token + + next_token = next_token.copy_and_replace( + "presence_key", earliest_serial + ) + return ((data, next_token)) + else: + if not to_token: + to_token = from_token.copy_and_replace( + "presence_key", 0 + ) + return (([], to_token)) + + class UserPresenceCache(object): """Store an observed user's state and status message. diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 3e41d7a46..6fbb4bc18 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -462,3 +462,51 @@ class RoomListHandler(BaseRoomHandler): chunk = yield self.store.get_rooms(is_public=True) # FIXME (erikj): START is no longer a valid value defer.returnValue({"start": "START", "end": "END", "chunk": chunk}) + + +class RoomEventSource(object): + def __init__(self, hs): + self.store = hs.get_datastore() + + @defer.inlineCallbacks + def get_new_events_for_user(self, user, from_token, limit): + # We just ignore the key for now. + + to_key = yield self.get_current_token_part() + + events, end_key = yield self.store.get_room_events_stream( + user_id=user.to_string(), + from_key=from_token.events_key, + to_key=to_key, + room_id=None, + limit=limit, + ) + + end_token = from_token.copy_and_replace("events_key", end_key) + + defer.returnValue((events, end_token)) + + def get_current_token_part(self): + return self.store.get_room_events_max_id() + + @defer.inlineCallbacks + def get_pagination_rows(self, user, pagination_config, key): + from_token = pagination_config.from_token + to_token = pagination_config.to_token + limit = pagination_config.limit + direction = pagination_config.direction + + to_key = to_token.events_key if to_token else None + + events, next_key = yield self.store.paginate_room_events( + room_id=key, + from_key=from_token.events_key, + to_key=to_key, + direction=direction, + limit=limit, + with_feedback=True + ) + + next_token = from_token.copy_and_replace("events_key", next_key) + + defer.returnValue((events, next_token)) diff --git a/synapse/streams/events.py b/synapse/streams/events.py index c68cf1a59..321faf4b0 100644 --- a/synapse/streams/events.py +++ b/synapse/streams/events.py @@ -17,6 +17,9 @@ from twisted.internet import defer from synapse.types import StreamToken +from synapse.handlers.presence import PresenceEventSource +from synapse.handlers.room import RoomEventSource + class NullSource(object): """This event source never yields any events and its token remains at @@ -34,136 +37,10 @@ class NullSource(object): return defer.succeed(([], pagination_config.from_token)) -class RoomEventSource(object): - def __init__(self, hs): - self.store = hs.get_datastore() - - @defer.inlineCallbacks - def get_new_events_for_user(self, user, from_token, limit): - # We just ignore the key for now. - - to_key = yield self.get_current_token_part() - - events, end_key = yield self.store.get_room_events_stream( - user_id=user.to_string(), - from_key=from_token.events_key, - to_key=to_key, - room_id=None, - limit=limit, - ) - - end_token = from_token.copy_and_replace("events_key", end_key) - - defer.returnValue((events, end_token)) - - def get_current_token_part(self): - return self.store.get_room_events_max_id() - - @defer.inlineCallbacks - def get_pagination_rows(self, user, pagination_config, key): - from_token = pagination_config.from_token - to_token = pagination_config.to_token - limit = pagination_config.limit - direction = pagination_config.direction - - to_key = to_token.events_key if to_token else None - - events, next_key = yield self.store.paginate_room_events( - room_id=key, - from_key=from_token.events_key, - to_key=to_key, - direction=direction, - limit=limit, - with_feedback=True - ) - - next_token = from_token.copy_and_replace("events_key", next_key) - - defer.returnValue((events, next_token)) - - -class PresenceSource(object): - def __init__(self, hs): - self.hs = hs - self.clock = hs.get_clock() - - def get_new_events_for_user(self, user, from_token, limit): - from_key = int(from_token.presence_key) - - presence = self.hs.get_handlers().presence_handler - cachemap = presence._user_cachemap - - # TODO(paul): limit, and filter by visibility - updates = [(k, cachemap[k]) for k in cachemap - if from_key < cachemap[k].serial] - - if updates: - clock = self.clock - - latest_serial = max([x[1].serial for x in updates]) - data = [x[1].make_event(user=x[0], clock=clock) for x in updates] - - end_token = from_token.copy_and_replace( - "presence_key", latest_serial - ) - return ((data, end_token)) - else: - end_token = from_token.copy_and_replace( - "presence_key", presence._user_cachemap_latest_serial - ) - return (([], end_token)) - - def get_current_token_part(self): - presence = self.hs.get_handlers().presence_handler - return presence._user_cachemap_latest_serial - - def get_pagination_rows(self, user, pagination_config, key): - # TODO (erikj): Does this make sense? Ordering? - - from_token = pagination_config.from_token - to_token = pagination_config.to_token - - from_key = int(from_token.presence_key) - - if to_token: - to_key = int(to_token.presence_key) - else: - to_key = -1 - - presence = self.hs.get_handlers().presence_handler - cachemap = presence._user_cachemap - - # TODO(paul): limit, and filter by visibility - updates = [(k, cachemap[k]) for k in cachemap - if to_key < cachemap[k].serial < from_key] - - if updates: - clock = self.clock - - earliest_serial = max([x[1].serial for x in updates]) - data = [x[1].make_event(user=x[0], clock=clock) for x in updates] - - if to_token: - next_token = to_token - else: - next_token = from_token - - next_token = next_token.copy_and_replace( - "presence_key", earliest_serial - ) - return ((data, next_token)) - else: - if not to_token: - to_token = from_token.copy_and_replace( - "presence_key", 0 - ) - return (([], to_token)) - - class EventSources(object): SOURCE_TYPES = { "room": RoomEventSource, - "presence": PresenceSource, + "presence": PresenceEventSource, } def __init__(self, hs): diff --git a/tests/rest/test_presence.py b/tests/rest/test_presence.py index 7f7347dcf..0f5fc2143 100644 --- a/tests/rest/test_presence.py +++ b/tests/rest/test_presence.py @@ -229,7 +229,7 @@ class PresenceEventStreamTestCase(unittest.TestCase): # HIDEOUS HACKERY # TODO(paul): This should be injected in via the HomeServer DI system from synapse.streams.events import ( - PresenceSource, NullSource, EventSources + PresenceEventSource, NullSource, EventSources ) old_SOURCE_TYPES = EventSources.SOURCE_TYPES @@ -240,7 +240,7 @@ class PresenceEventStreamTestCase(unittest.TestCase): EventSources.SOURCE_TYPES = { k: NullSource for k in old_SOURCE_TYPES.keys() } - EventSources.SOURCE_TYPES["presence"] = PresenceSource + EventSources.SOURCE_TYPES["presence"] = PresenceEventSource hs = HomeServer("test", db_pool=None, From d4145abd335135407128684022f4561840234b81 Mon Sep 17 00:00:00 2001 From: "Paul \"LeoNerd\" Evans" Date: Fri, 29 Aug 2014 17:17:11 +0100 Subject: [PATCH 09/45] Use str.join() properly --- synapse/types.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/synapse/types.py b/synapse/types.py index 63154855d..abc3031ea 100644 --- a/synapse/types.py +++ b/synapse/types.py @@ -115,9 +115,8 @@ class StreamToken( raise SynapseError(400, "Invalid Token") def to_string(self): - return "".join([ + return self._SEPARATOR.join([ str(self.events_key), - self._SEPARATOR, str(self.presence_key), ]) From b86d2a2d4fa6e9093bf08df88dce68e5f274c697 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 29 Aug 2014 17:21:57 +0100 Subject: [PATCH 10/45] update presence times in realtime through the magic of two-way binding --- webclient/room/room-controller.js | 48 +++++++++---------------------- webclient/room/room.html | 2 +- 2 files changed, 15 insertions(+), 35 deletions(-) diff --git a/webclient/room/room-controller.js b/webclient/room/room-controller.js index 09dac85d2..ed7de62c0 100644 --- a/webclient/room/room-controller.js +++ b/webclient/room/room-controller.js @@ -88,7 +88,7 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) call.onHangup = $scope.onCallHangup; $scope.currentCall = call; }); - + $scope.memberCount = function() { return Object.keys($scope.members).length; }; @@ -175,6 +175,8 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) // set target_user_id to keep things clear var target_user_id = chunk.state_key; + + var now = new Date().getTime(); var isNewMember = !(target_user_id in $scope.members); if (isNewMember) { @@ -185,44 +187,14 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) if ("mtime_age" in chunk.content) { chunk.mtime_age = chunk.content.mtime_age; } - // 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) { chunk.displayname = chunk.content.displayname; } if ("avatar_url" in chunk.content) { chunk.avatar_url = chunk.content.avatar_url; } - $scope.members[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 - // response may update a STALE VERSION of the member list (manifesting - // as no member names appearing, or appearing sporadically). - $scope.$evalAsync(function() { - matrixService.getDisplayName(chunk.target_user_id).then( - function(response) { - var member = $scope.members[chunk.target_user_id]; - if (member !== undefined) { - member.displayname = response.data.displayname; - } - } - ); - matrixService.getProfilePictureUrl(chunk.target_user_id).then( - function(response) { - var member = $scope.members[chunk.target_user_id]; - if (member !== undefined) { - member.avatar_url = response.data.avatar_url; - } - } - ); - }); -*/ + chunk.last_updated = now; + $scope.members[target_user_id] = chunk; if (target_user_id in $rootScope.presence) { updatePresence($rootScope.presence[target_user_id]); @@ -234,6 +206,12 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) member.content.membership = chunk.content.membership; } }; + + var updateMemberListPresenceAge = function() { + $scope.now = new Date().getTime(); + //console.log("updateMemberListPresenceAge() - now = " + $scope.now); + $timeout(updateMemberListPresenceAge, 5 * 1000); + }; var updatePresence = function(chunk) { if (!(chunk.content.user_id in $scope.members)) { @@ -395,8 +373,10 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) // Make recents highlight the current room $scope.recentsSelectedRoomID = $scope.room_id; - + paginate(MESSAGES_PER_PAGINATION); + + updateMemberListPresenceAge(); }; $scope.inviteUser = function(user_id) { diff --git a/webclient/room/room.html b/webclient/room/room.html index a3514c3a9..cb9239ed5 100644 --- a/webclient/room/room.html +++ b/webclient/room/room.html @@ -25,7 +25,7 @@
{{ member.displayname || member.id.substr(0, member.id.indexOf(':')) }}
{{ member.displayname ? "" : member.id.substr(member.id.indexOf(':')) }}
- {{ member.mtime_age | duration }}
{{ member.mtime_age ? "ago" : "" }} + {{ member.mtime_age + (now - member.last_updated) | duration }}
{{ member.mtime_age ? "ago" : "" }} From 67f42b2f26a6ea76ec480167c58e1fa115809e23 Mon Sep 17 00:00:00 2001 From: Emmanuel ROHEE Date: Fri, 29 Aug 2014 18:22:05 +0200 Subject: [PATCH 11/45] Get user display name and avatar from the server rather than storing them in the local storage --- webclient/home/home-controller.js | 27 ++++++++++++ webclient/home/home.html | 6 +-- webclient/settings/settings-controller.js | 50 ++++++++++++++++------- webclient/settings/settings.html | 6 +-- 4 files changed, 68 insertions(+), 21 deletions(-) diff --git a/webclient/home/home-controller.js b/webclient/home/home-controller.js index 547a5c560..7ae13f12e 100644 --- a/webclient/home/home-controller.js +++ b/webclient/home/home-controller.js @@ -37,6 +37,11 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen $scope.joinAlias = { room_alias: "" }; + + $scope.profile = { + displayName: "", + avatarUrl: "" + }; var refresh = function() { @@ -108,6 +113,28 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen }; $scope.onInit = function() { + // Load profile data + // Display name + matrixService.getDisplayName($scope.config.user_id).then( + function(response) { + $scope.profile.displayName = response.data.displayname; + $scope.profileOnServer.displayName = response.data.displayname; + }, + function(error) { + $scope.feedback = "Can't load display name"; + } + ); + // Avatar + matrixService.getProfilePictureUrl($scope.config.user_id).then( + function(response) { + $scope.profile.avatarUrl = response.data.avatar_url; + $scope.profileOnServer.avatarUrl = response.data.avatar_url; + }, + function(error) { + $scope.feedback = "Can't load avatar URL"; + } + ); + refresh(); }; }]); diff --git a/webclient/home/home.html b/webclient/home/home.html index d38b843d8..1b1c21d9d 100644 --- a/webclient/home/home.html +++ b/webclient/home/home.html @@ -9,13 +9,13 @@
- +
-
{{ config.displayName }}
-
{{ config.user_id }}
+
{{ profile.displayName }}
+
{{ config.user_id }}
diff --git a/webclient/settings/settings-controller.js b/webclient/settings/settings-controller.js index 3a9060eba..dc680ef07 100644 --- a/webclient/settings/settings-controller.js +++ b/webclient/settings/settings-controller.js @@ -22,10 +22,40 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu $scope.config = matrixService.config(); $scope.profile = { - displayName: $scope.config.displayName, - avatarUrl: $scope.config.avatarUrl + displayName: "", + avatarUrl: "" }; - + + // The profile as stored on the server + $scope.profileOnServer = { + displayName: "", + avatarUrl: "" + }; + + $scope.onInit = function() { + // Load profile data + // Display name + matrixService.getDisplayName($scope.config.user_id).then( + function(response) { + $scope.profile.displayName = response.data.displayname; + $scope.profileOnServer.displayName = response.data.displayname; + }, + function(error) { + $scope.feedback = "Can't load display name"; + } + ); + // Avatar + matrixService.getProfilePictureUrl($scope.config.user_id).then( + function(response) { + $scope.profile.avatarUrl = response.data.avatar_url; + $scope.profileOnServer.avatarUrl = response.data.avatar_url; + }, + function(error) { + $scope.feedback = "Can't load avatar URL"; + } + ); + }; + $scope.$watch("profile.avatarFile", function(newValue, oldValue) { if ($scope.profile.avatarFile) { console.log("Uploading new avatar file..."); @@ -41,10 +71,10 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu }); $scope.saveProfile = function() { - if ($scope.profile.displayName !== $scope.config.displayName) { + if ($scope.profile.displayName !== $scope.profileOnServer.displayName) { setDisplayName($scope.profile.displayName); } - if ($scope.profile.avatarUrl !== $scope.config.avatarUrl) { + if ($scope.profile.avatarUrl !== $scope.profileOnServer.avatarUrl) { setAvatar($scope.profile.avatarUrl); } }; @@ -53,11 +83,6 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu matrixService.setDisplayName(displayName).then( function(response) { $scope.feedback = "Updated display name."; - - var config = matrixService.config(); - config.displayName = displayName; - matrixService.setConfig(config); - matrixService.saveConfig(); }, function(error) { $scope.feedback = "Can't update display name: " + error.data; @@ -71,11 +96,6 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu function(response) { console.log("Updated avatar"); $scope.feedback = "Updated avatar."; - - var config = matrixService.config(); - config.avatarUrl = avatarURL; - matrixService.setConfig(config); - matrixService.saveConfig(); }, function(error) { $scope.feedback = "Can't update avatar: " + error.data; diff --git a/webclient/settings/settings.html b/webclient/settings/settings.html index d06a0083f..51884e720 100644 --- a/webclient/settings/settings.html +++ b/webclient/settings/settings.html @@ -1,4 +1,4 @@ -
+
@@ -10,7 +10,7 @@
- +
@@ -19,7 +19,7 @@
- From e1f249ce20caebf3882278f657102efa05941373 Mon Sep 17 00:00:00 2001 From: Emmanuel ROHEE Date: Fri, 29 Aug 2014 18:24:13 +0200 Subject: [PATCH 12/45] Implemented /nick --- webclient/room/room-controller.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/webclient/room/room-controller.js b/webclient/room/room-controller.js index ed7de62c0..23764fb72 100644 --- a/webclient/room/room-controller.js +++ b/webclient/room/room-controller.js @@ -253,6 +253,10 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) if ($scope.textInput.indexOf("/me") === 0) { promise = matrixService.sendEmoteMessage($scope.room_id, $scope.textInput.substr(4)); } + else if ($scope.textInput.indexOf("/nick ") === 0) { + // Change user display name + promise = matrixService.setDisplayName($scope.textInput.substr(6)); + } else { promise = matrixService.sendTextMessage($scope.room_id, $scope.textInput); } From 95cbd026ccb7c15daa17bd8eeb1a4551ea62d3fb Mon Sep 17 00:00:00 2001 From: Emmanuel ROHEE Date: Fri, 29 Aug 2014 18:29:04 +0200 Subject: [PATCH 13/45] oops. Should not have c+p all lines --- webclient/home/home-controller.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/webclient/home/home-controller.js b/webclient/home/home-controller.js index 7ae13f12e..438ffe1b0 100644 --- a/webclient/home/home-controller.js +++ b/webclient/home/home-controller.js @@ -118,7 +118,6 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen matrixService.getDisplayName($scope.config.user_id).then( function(response) { $scope.profile.displayName = response.data.displayname; - $scope.profileOnServer.displayName = response.data.displayname; }, function(error) { $scope.feedback = "Can't load display name"; @@ -128,7 +127,6 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen matrixService.getProfilePictureUrl($scope.config.user_id).then( function(response) { $scope.profile.avatarUrl = response.data.avatar_url; - $scope.profileOnServer.avatarUrl = response.data.avatar_url; }, function(error) { $scope.feedback = "Can't load avatar URL"; From f85a3757cf5d0aafc575423f2147e5821d8d5a67 Mon Sep 17 00:00:00 2001 From: "Paul \"LeoNerd\" Evans" Date: Fri, 29 Aug 2014 17:31:33 +0100 Subject: [PATCH 14/45] Avoid hardcoding names of individual stream token keys in its own implementation; this at least reduces the number of places in source code the individual parts are stored --- synapse/types.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/synapse/types.py b/synapse/types.py index abc3031ea..aa6f589a2 100644 --- a/synapse/types.py +++ b/synapse/types.py @@ -105,20 +105,14 @@ class StreamToken( @classmethod def from_string(cls, string): try: - events_key, presence_key = string.split(cls._SEPARATOR) + keys = string.split(cls._SEPARATOR) - return cls( - events_key=events_key, - presence_key=presence_key, - ) + return cls(*keys) except: raise SynapseError(400, "Invalid Token") def to_string(self): - return self._SEPARATOR.join([ - str(self.events_key), - str(self.presence_key), - ]) + return self._SEPARATOR.join([str(k) for k in self]) def copy_and_replace(self, key, new_value): d = self._asdict() From 6dd50da54ea944610eb3836621c45a2d6b2a532b Mon Sep 17 00:00:00 2001 From: "Paul \"LeoNerd\" Evans" Date: Fri, 29 Aug 2014 17:39:33 +0100 Subject: [PATCH 15/45] Define a new event stream data source for typing notifications (currently null) --- synapse/handlers/typing.py | 14 ++++++++++++++ synapse/streams/events.py | 20 +++++++++++++------- synapse/types.py | 2 +- tests/rest/test_presence.py | 8 +++++--- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/synapse/handlers/typing.py b/synapse/handlers/typing.py index 9fab0ff37..8a1e3dc5e 100644 --- a/synapse/handlers/typing.py +++ b/synapse/handlers/typing.py @@ -145,3 +145,17 @@ class TypingNotificationHandler(BaseHandler): typing): # TODO(paul) steal this from presence.py pass + + +class TypingNotificationEventSource(object): + def __init__(self, hs): + self.hs = hs + + def get_new_events_for_user(self, user, from_token, limit): + return ([], 0) + + def get_current_token_part(self): + return 0 + + def get_pagination_rows(self, user, pagination_config, key): + return ([], 0) diff --git a/synapse/streams/events.py b/synapse/streams/events.py index 321faf4b0..4bec6605b 100644 --- a/synapse/streams/events.py +++ b/synapse/streams/events.py @@ -19,6 +19,7 @@ from synapse.types import StreamToken from synapse.handlers.presence import PresenceEventSource from synapse.handlers.room import RoomEventSource +from synapse.handlers.typing import TypingNotificationEventSource class NullSource(object): @@ -41,6 +42,7 @@ class EventSources(object): SOURCE_TYPES = { "room": RoomEventSource, "presence": PresenceEventSource, + "typing": TypingNotificationEventSource, } def __init__(self, hs): @@ -49,15 +51,19 @@ class EventSources(object): for name, cls in EventSources.SOURCE_TYPES.items() } - @staticmethod - def create_token(events_key, presence_key): - return StreamToken(events_key=events_key, presence_key=presence_key) - @defer.inlineCallbacks def get_current_token(self): - events_key = yield self.sources["room"].get_current_token_part() - presence_key = yield self.sources["presence"].get_current_token_part() - token = EventSources.create_token(events_key, presence_key) + token = StreamToken( + events_key=( + yield self.sources["room"].get_current_token_part() + ), + presence_key=( + yield self.sources["presence"].get_current_token_part() + ), + typing_key=( + yield self.sources["typing"].get_current_token_part() + ) + ) defer.returnValue(token) diff --git a/synapse/types.py b/synapse/types.py index aa6f589a2..d93b02a56 100644 --- a/synapse/types.py +++ b/synapse/types.py @@ -97,7 +97,7 @@ class RoomID(DomainSpecificString): class StreamToken( namedtuple( "Token", - ("events_key", "presence_key") + ("events_key", "presence_key", "typing_key") ) ): _SEPARATOR = "_" diff --git a/tests/rest/test_presence.py b/tests/rest/test_presence.py index 0f5fc2143..ab0d580c7 100644 --- a/tests/rest/test_presence.py +++ b/tests/rest/test_presence.py @@ -295,7 +295,9 @@ class PresenceEventStreamTestCase(unittest.TestCase): # all be ours # I'll already get my own presence state change - self.assertEquals({"start": "0_1", "end": "0_1", "chunk": []}, response) + self.assertEquals({"start": "0_1_0", "end": "0_1_0", "chunk": []}, + response + ) self.mock_datastore.set_presence_state.return_value = defer.succeed( {"state": ONLINE}) @@ -306,10 +308,10 @@ class PresenceEventStreamTestCase(unittest.TestCase): state={"state": ONLINE}) (code, response) = yield self.mock_resource.trigger("GET", - "/events?from=0_1&timeout=0", None) + "/events?from=0_1_0&timeout=0", None) self.assertEquals(200, code) - self.assertEquals({"start": "0_1", "end": "0_2", "chunk": [ + self.assertEquals({"start": "0_1_0", "end": "0_2_0", "chunk": [ {"type": "m.presence", "content": { "user_id": "@banana:test", From 74cc722b9641e0263da22a12a3c5f4b2b0cd51e8 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 29 Aug 2014 17:45:01 +0100 Subject: [PATCH 16/45] Added case-sensitivity notes on IDs, added TODO on UTF-8. --- docs/specification.rst | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/docs/specification.rst b/docs/specification.rst index e571db7c3..4f6c00050 100644 --- a/docs/specification.rst +++ b/docs/specification.rst @@ -35,8 +35,8 @@ namespaced to the home server which allocated the account and looks like:: @localpart:domain -The ``localpart`` of a user ID may be a user name, or an opaque ID identifying this user. - +The ``localpart`` of a user ID may be a user name, or an opaque ID identifying this user. They are +case-insensitive. A "Home Server" is a server which provides C-S APIs and has the ability to federate with other HSes. It is typically responsible for multiple clients. "Federation" is the term used to describe the @@ -60,7 +60,8 @@ identified via a "Room ID", which look like:: There is exactly one room ID for each room. Whilst the room ID does contain a domain, it is simply for namespacing room IDs. The room does NOT reside on the -domain specified. Room IDs are not meant to be human readable. +domain specified. Room IDs are not meant to be human readable. They ARE +case-sensitive. The following diagram shows an ``m.room.message`` event being sent in the room ``!qporfwt:matrix.org``:: @@ -102,10 +103,10 @@ Each room can also have multiple "Room Aliases", which looks like:: A room alias "points" to a room ID. The room ID the alias is pointing to can be obtained by visiting the domain specified. Room aliases are designed to be human readable strings -which can be used to publicise rooms. Note that the mapping from a room alias to a -room ID is not fixed, and may change over time to point to a different room ID. For this -reason, Clients SHOULD resolve the room alias to a room ID once and then use that ID on -subsequent requests. +which can be used to publicise rooms. They are case-insensitive. Note that the mapping +from a room alias to a room ID is not fixed, and may change over time to point to a +different room ID. For this reason, Clients SHOULD resolve the room alias to a room ID +once and then use that ID on subsequent requests. :: @@ -214,6 +215,12 @@ In contrast, these are invalid requests:: "key": "This is a put but it is missing a txnId." } + + +- TODO: All strings everywhere are UTF-8 + + + Receiving live updates on a client ---------------------------------- Clients can receive new events by long-polling the home server. This will hold open the @@ -288,6 +295,7 @@ Joining rooms Leaving rooms ------------- - API to hit (``$roomid/leave``). +- If no more HSes in room, can delete room? - Is there a dance? Room events From e006f101c30136f4079f95c0e5c63469fe8887ff Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 29 Aug 2014 17:53:48 +0100 Subject: [PATCH 17/45] fix mobile skin by hiding recents LHS in /rooms --- webclient/app.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/webclient/app.css b/webclient/app.css index 16f9dd72b..4e93bc993 100644 --- a/webclient/app.css +++ b/webclient/app.css @@ -28,6 +28,7 @@ } #userIdCell, + #roomRecentsTableWrapper, #usersTableWrapper, #extraControls { display: none; @@ -353,6 +354,7 @@ h1 { } /*** Recents ***/ + .recentsTable { max-width: 480px; width: 100%; From 26766c22ebbd3fd267fef4f8b13e6e5c31d51d91 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 29 Aug 2014 17:54:11 +0100 Subject: [PATCH 18/45] todo --- webclient/room/room-controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webclient/room/room-controller.js b/webclient/room/room-controller.js index 23764fb72..c745ce945 100644 --- a/webclient/room/room-controller.js +++ b/webclient/room/room-controller.js @@ -209,7 +209,7 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) var updateMemberListPresenceAge = function() { $scope.now = new Date().getTime(); - //console.log("updateMemberListPresenceAge() - now = " + $scope.now); + // TODO: don't bother polling every 5s if we know none of our counters are younger than 1 minute $timeout(updateMemberListPresenceAge, 5 * 1000); }; From 490f142d739c7dd30c4ca8e262af5f84c577ebbe Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 29 Aug 2014 18:01:01 +0100 Subject: [PATCH 19/45] Give basic feedback on the state of VoIP calls in the UI. --- webclient/components/matrix/matrix-call.js | 3 ++- webclient/room/room-controller.js | 2 -- webclient/room/room.html | 4 ++++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/webclient/components/matrix/matrix-call.js b/webclient/components/matrix/matrix-call.js index c0a7735a7..47b63d7f2 100644 --- a/webclient/components/matrix/matrix-call.js +++ b/webclient/components/matrix/matrix-call.js @@ -36,7 +36,7 @@ var forAllTracksOnStream = function(s, f) { } angular.module('MatrixCall', []) -.factory('MatrixCall', ['matrixService', 'matrixPhoneService', function MatrixCallFactory(matrixService, matrixPhoneService) { +.factory('MatrixCall', ['matrixService', 'matrixPhoneService', '$rootScope', function MatrixCallFactory(matrixService, matrixPhoneService, $rootScope) { var MatrixCall = function(room_id) { this.room_id = room_id; this.call_id = "c" + new Date().getTime(); @@ -208,6 +208,7 @@ angular.module('MatrixCall', []) // ideally we'd consider the call to be connected when we get media but chrome doesn't implement nay of the 'onstarted' events yet if (this.peerConn.iceConnectionState == 'completed' || this.peerConn.iceConnectionState == 'connected') { this.state = 'connected'; + $rootScope.$apply(); } }; diff --git a/webclient/room/room-controller.js b/webclient/room/room-controller.js index c745ce945..75adf259c 100644 --- a/webclient/room/room-controller.js +++ b/webclient/room/room-controller.js @@ -471,7 +471,5 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput']) } $scope.onCallHangup = function() { - $scope.feedback = "Call ended"; - $scope.currentCall = undefined; } }]); diff --git a/webclient/room/room.html b/webclient/room/room.html index cb9239ed5..aa7879ea8 100644 --- a/webclient/room/room.html +++ b/webclient/room/room.html @@ -105,6 +105,10 @@
+ Calling... + Call Connecting... + Call Connected + Call Ended {{ currentCall.state }}
From 4bfdec1eb28aa272391607b2cf1f24c781d9ba74 Mon Sep 17 00:00:00 2001 From: "Paul \"LeoNerd\" Evans" Date: Fri, 29 Aug 2014 18:39:09 +0100 Subject: [PATCH 20/45] Rename 'events_key' to 'room_key' so it matches the name of the event source --- synapse/handlers/message.py | 6 +++--- synapse/handlers/room.py | 10 +++++----- synapse/streams/events.py | 2 +- synapse/types.py | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 3d7f97bcf..c8ff34e5f 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -274,11 +274,11 @@ class MessageHandler(BaseRoomHandler): messages, token = yield self.store.get_recent_events_for_room( event.room_id, limit=limit, - end_token=now_token.events_key, + end_token=now_token.room_key, ) - start_token = now_token.copy_and_replace("events_key", token[0]) - end_token = now_token.copy_and_replace("events_key", token[1]) + start_token = now_token.copy_and_replace("room_key", token[0]) + end_token = now_token.copy_and_replace("room_key", token[1]) d["messages"] = { "chunk": [m.get_dict() for m in messages], diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 6fbb4bc18..b27bdecd4 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -476,13 +476,13 @@ class RoomEventSource(object): events, end_key = yield self.store.get_room_events_stream( user_id=user.to_string(), - from_key=from_token.events_key, + from_key=from_token.room_key, to_key=to_key, room_id=None, limit=limit, ) - end_token = from_token.copy_and_replace("events_key", end_key) + end_token = from_token.copy_and_replace("room_key", end_key) defer.returnValue((events, end_token)) @@ -496,17 +496,17 @@ class RoomEventSource(object): limit = pagination_config.limit direction = pagination_config.direction - to_key = to_token.events_key if to_token else None + to_key = to_token.room_key if to_token else None events, next_key = yield self.store.paginate_room_events( room_id=key, - from_key=from_token.events_key, + from_key=from_token.room_key, to_key=to_key, direction=direction, limit=limit, with_feedback=True ) - next_token = from_token.copy_and_replace("events_key", next_key) + next_token = from_token.copy_and_replace("room_key", next_key) defer.returnValue((events, next_token)) diff --git a/synapse/streams/events.py b/synapse/streams/events.py index 4bec6605b..848036867 100644 --- a/synapse/streams/events.py +++ b/synapse/streams/events.py @@ -54,7 +54,7 @@ class EventSources(object): @defer.inlineCallbacks def get_current_token(self): token = StreamToken( - events_key=( + room_key=( yield self.sources["room"].get_current_token_part() ), presence_key=( diff --git a/synapse/types.py b/synapse/types.py index d93b02a56..1a9dceabf 100644 --- a/synapse/types.py +++ b/synapse/types.py @@ -97,7 +97,7 @@ class RoomID(DomainSpecificString): class StreamToken( namedtuple( "Token", - ("events_key", "presence_key", "typing_key") + ("room_key", "presence_key", "typing_key") ) ): _SEPARATOR = "_" From 6797c7f1b138c7db59d8a7134091e0806cc44f74 Mon Sep 17 00:00:00 2001 From: "Paul \"LeoNerd\" Evans" Date: Fri, 29 Aug 2014 19:00:45 +0100 Subject: [PATCH 21/45] TypingNotificationEventSource has to return proper tokens, not int 0 --- synapse/handlers/typing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/synapse/handlers/typing.py b/synapse/handlers/typing.py index 8a1e3dc5e..ecb9318d1 100644 --- a/synapse/handlers/typing.py +++ b/synapse/handlers/typing.py @@ -152,10 +152,10 @@ class TypingNotificationEventSource(object): self.hs = hs def get_new_events_for_user(self, user, from_token, limit): - return ([], 0) + return ([], from_token) def get_current_token_part(self): return 0 def get_pagination_rows(self, user, pagination_config, key): - return ([], 0) + return ([], pagination_config.from_token) From 56424eca5c2dd6b4a1f71b676bb702ab50e9b893 Mon Sep 17 00:00:00 2001 From: "Paul \"LeoNerd\" Evans" Date: Fri, 29 Aug 2014 19:12:22 +0100 Subject: [PATCH 22/45] Bugfix for rest presence test - datastore needs to implement profile methods --- tests/rest/test_presence.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/rest/test_presence.py b/tests/rest/test_presence.py index ab0d580c7..6631b7797 100644 --- a/tests/rest/test_presence.py +++ b/tests/rest/test_presence.py @@ -274,6 +274,15 @@ class PresenceEventStreamTestCase(unittest.TestCase): lambda u: defer.succeed([])) self.mock_datastore = hs.get_datastore() + + def get_profile_displayname(user_id): + return defer.succeed("Frank") + self.mock_datastore.get_profile_displayname = get_profile_displayname + + def get_profile_avatar_url(user_id): + return defer.succeed(None) + self.mock_datastore.get_profile_avatar_url = get_profile_avatar_url + self.presence = hs.get_handlers().presence_handler self.u_apple = hs.parse_userid("@apple:test") @@ -316,6 +325,7 @@ class PresenceEventStreamTestCase(unittest.TestCase): "content": { "user_id": "@banana:test", "state": ONLINE, + "displayname": "Frank", "mtime_age": 0, }}, ]}, response) From eec67a675f7ea3545bfba79c6b753f63f7fd9b3b Mon Sep 17 00:00:00 2001 From: "Paul \"LeoNerd\" Evans" Date: Fri, 29 Aug 2014 19:13:55 +0100 Subject: [PATCH 23/45] Have EventSource's get_new_events_for_user() API work only on keys within that source, not overall eventstream tokens --- synapse/handlers/presence.py | 14 ++++---------- synapse/handlers/room.py | 8 +++----- synapse/handlers/typing.py | 4 ++-- synapse/notifier.py | 30 ++++++++++++++++++++---------- synapse/streams/events.py | 7 ++++--- 5 files changed, 33 insertions(+), 30 deletions(-) diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index 1d3b02a9d..05bf14524 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -727,8 +727,8 @@ class PresenceEventSource(object): self.hs = hs self.clock = hs.get_clock() - def get_new_events_for_user(self, user, from_token, limit): - from_key = int(from_token.presence_key) + def get_new_events_for_user(self, user, from_key, limit): + from_key = int(from_key) presence = self.hs.get_handlers().presence_handler cachemap = presence._user_cachemap @@ -743,15 +743,9 @@ class PresenceEventSource(object): latest_serial = max([x[1].serial for x in updates]) data = [x[1].make_event(user=x[0], clock=clock) for x in updates] - end_token = from_token.copy_and_replace( - "presence_key", latest_serial - ) - return ((data, end_token)) + return ((data, latest_serial)) else: - end_token = from_token.copy_and_replace( - "presence_key", presence._user_cachemap_latest_serial - ) - return (([], end_token)) + return (([], presence._user_cachemap_latest_serial)) def get_current_token_part(self): presence = self.hs.get_handlers().presence_handler diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index b27bdecd4..ce15420bf 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -469,22 +469,20 @@ class RoomEventSource(object): self.store = hs.get_datastore() @defer.inlineCallbacks - def get_new_events_for_user(self, user, from_token, limit): + def get_new_events_for_user(self, user, from_key, limit): # We just ignore the key for now. to_key = yield self.get_current_token_part() events, end_key = yield self.store.get_room_events_stream( user_id=user.to_string(), - from_key=from_token.room_key, + from_key=from_key, to_key=to_key, room_id=None, limit=limit, ) - end_token = from_token.copy_and_replace("room_key", end_key) - - defer.returnValue((events, end_token)) + defer.returnValue((events, end_key)) def get_current_token_part(self): return self.store.get_room_events_max_id() diff --git a/synapse/handlers/typing.py b/synapse/handlers/typing.py index ecb9318d1..238b06348 100644 --- a/synapse/handlers/typing.py +++ b/synapse/handlers/typing.py @@ -151,8 +151,8 @@ class TypingNotificationEventSource(object): def __init__(self, hs): self.hs = hs - def get_new_events_for_user(self, user, from_token, limit): - return ([], from_token) + def get_new_events_for_user(self, user, from_key, limit): + return ([], from_key) def get_current_token_part(self): return 0 diff --git a/synapse/notifier.py b/synapse/notifier.py index b6d5ec482..cb544e988 100644 --- a/synapse/notifier.py +++ b/synapse/notifier.py @@ -95,7 +95,7 @@ class Notifier(object): """ room_id = event.room_id - source = self.event_sources.sources["room"] + room_source = self.event_sources.sources["room"] listeners = self.rooms_to_listeners.get(room_id, set()).copy() @@ -107,13 +107,17 @@ class Notifier(object): # TODO (erikj): Can we make this more efficient by hitting the # db once? for listener in listeners: - events, end_token = yield source.get_new_events_for_user( + events, end_key = yield room_source.get_new_events_for_user( listener.user, - listener.from_token, + listener.from_token.room_key, listener.limit, ) if events: + end_token = listener.from_token.copy_and_replace( + "room_key", end_key + ) + listener.notify( self, events, listener.from_token, end_token ) @@ -126,7 +130,7 @@ class Notifier(object): Will wake up all listeners for the given users and rooms. """ - source = self.event_sources.sources["presence"] + presence_source = self.event_sources.sources["presence"] listeners = set() @@ -137,13 +141,17 @@ class Notifier(object): listeners |= self.rooms_to_listeners.get(room, set()).copy() for listener in listeners: - events, end_token = yield source.get_new_events_for_user( + events, end_key = yield presence_source.get_new_events_for_user( listener.user, - listener.from_token, + listener.from_token.presence_key, listener.limit, ) if events: + end_token = listener.from_token.copy_and_replace( + "presence_key", end_key + ) + listener.notify( self, events, listener.from_token, end_token ) @@ -216,16 +224,18 @@ class Notifier(object): limit = listener.limit # TODO (erikj): DeferredList? - for source in self.event_sources.sources.values(): - stuff, new_token = yield source.get_new_events_for_user( + for name, source in self.event_sources.sources.items(): + keyname = "%s_key" % name + + stuff, new_key = yield source.get_new_events_for_user( listener.user, - from_token, + getattr(from_token, keyname), limit, ) events.extend(stuff) - from_token = new_token + from_token = from_token.copy_and_replace(keyname, new_key) end_token = from_token diff --git a/synapse/streams/events.py b/synapse/streams/events.py index 848036867..43b6b1eba 100644 --- a/synapse/streams/events.py +++ b/synapse/streams/events.py @@ -28,8 +28,8 @@ class NullSource(object): def __init__(self, hs): pass - def get_new_events_for_user(self, user, from_token, limit): - return defer.succeed(([], from_token)) + def get_new_events_for_user(self, user, from_key, limit): + return defer.succeed(([], from_key)) def get_current_token_part(self): return defer.succeed(0) @@ -68,7 +68,8 @@ class EventSources(object): class StreamSource(object): - def get_new_events_for_user(self, user, from_token, limit): + def get_new_events_for_user(self, user, from_key, limit): + """from_key is the key within this event source.""" raise NotImplementedError("get_new_events_for_user") def get_current_token_part(self): From a8e8d1d06c078de49711768357267cf4168999ea Mon Sep 17 00:00:00 2001 From: "Paul \"LeoNerd\" Evans" Date: Fri, 29 Aug 2014 19:15:23 +0100 Subject: [PATCH 24/45] Renamed get_current_token_part to get_current_key --- synapse/handlers/presence.py | 2 +- synapse/handlers/room.py | 4 ++-- synapse/handlers/typing.py | 2 +- synapse/streams/events.py | 12 ++++++------ 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index 05bf14524..cc28151e3 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -747,7 +747,7 @@ class PresenceEventSource(object): else: return (([], presence._user_cachemap_latest_serial)) - def get_current_token_part(self): + def get_current_key(self): presence = self.hs.get_handlers().presence_handler return presence._user_cachemap_latest_serial diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index ce15420bf..c54e0f963 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -472,7 +472,7 @@ class RoomEventSource(object): def get_new_events_for_user(self, user, from_key, limit): # We just ignore the key for now. - to_key = yield self.get_current_token_part() + to_key = yield self.get_current_key() events, end_key = yield self.store.get_room_events_stream( user_id=user.to_string(), @@ -484,7 +484,7 @@ class RoomEventSource(object): defer.returnValue((events, end_key)) - def get_current_token_part(self): + def get_current_key(self): return self.store.get_room_events_max_id() @defer.inlineCallbacks diff --git a/synapse/handlers/typing.py b/synapse/handlers/typing.py index 238b06348..3268427ec 100644 --- a/synapse/handlers/typing.py +++ b/synapse/handlers/typing.py @@ -154,7 +154,7 @@ class TypingNotificationEventSource(object): def get_new_events_for_user(self, user, from_key, limit): return ([], from_key) - def get_current_token_part(self): + def get_current_key(self): return 0 def get_pagination_rows(self, user, pagination_config, key): diff --git a/synapse/streams/events.py b/synapse/streams/events.py index 43b6b1eba..08d6e6f73 100644 --- a/synapse/streams/events.py +++ b/synapse/streams/events.py @@ -31,7 +31,7 @@ class NullSource(object): def get_new_events_for_user(self, user, from_key, limit): return defer.succeed(([], from_key)) - def get_current_token_part(self): + def get_current_key(self): return defer.succeed(0) def get_pagination_rows(self, user, pagination_config, key): @@ -55,13 +55,13 @@ class EventSources(object): def get_current_token(self): token = StreamToken( room_key=( - yield self.sources["room"].get_current_token_part() + yield self.sources["room"].get_current_key() ), presence_key=( - yield self.sources["presence"].get_current_token_part() + yield self.sources["presence"].get_current_key() ), typing_key=( - yield self.sources["typing"].get_current_token_part() + yield self.sources["typing"].get_current_key() ) ) defer.returnValue(token) @@ -72,8 +72,8 @@ class StreamSource(object): """from_key is the key within this event source.""" raise NotImplementedError("get_new_events_for_user") - def get_current_token_part(self): - raise NotImplementedError("get_current_token_part") + def get_current_key(self): + raise NotImplementedError("get_current_key") def get_pagination_rows(self, user, pagination_config, key): raise NotImplementedError("get_rows") From 93407cf7cff05fa99709ba5055bce329524cef32 Mon Sep 17 00:00:00 2001 From: "Paul \"LeoNerd\" Evans" Date: Fri, 29 Aug 2014 19:53:33 +0100 Subject: [PATCH 25/45] Bugfixes on presence pushes on user joining: * No need to inform clients of status of remote users; as that will arrive in due course anyway. We don't -have- the state currently, so we'd only send an unknown message * Remember to bump the presence serial for the event source, so the notifiers will wake up and report it --- synapse/handlers/presence.py | 17 ++--- tests/handlers/test_presence.py | 128 ++++++++++++++++++-------------- 2 files changed, 79 insertions(+), 66 deletions(-) diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index cc28151e3..93bd07b19 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -260,19 +260,18 @@ class PresenceHandler(BaseHandler): @defer.inlineCallbacks def user_joined_room(self, user, room_id): - if user.is_mine: + statuscache = self._get_or_make_usercache(user) + + # No actual update but we need to bump the serial anyway for the + # event source + self._user_cachemap_latest_serial += 1 + statuscache.update({}, serial=self._user_cachemap_latest_serial) + self.push_update_to_local_and_remote( observed_user=user, room_ids=[room_id], - statuscache=self._get_or_offline_usercache(user), - ) - - else: - self.push_update_to_clients( - observed_user=user, - room_ids=[room_id], - statuscache=self._get_or_offline_usercache(user), + statuscache=statuscache, ) # We also want to tell them about current presence of people. diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py index fcd7a784c..0a176bdd4 100644 --- a/tests/handlers/test_presence.py +++ b/tests/handlers/test_presence.py @@ -514,13 +514,6 @@ class PresencePushTestCase(unittest.TestCase): ) hs.handlers = JustPresenceHandlers(hs) - def update(*args,**kwargs): - # print "mock_update_client: Args=%s, kwargs=%s" %(args, kwargs,) - return defer.succeed(None) - - self.mock_update_client = Mock() - self.mock_update_client.side_effect = update - self.datastore = hs.get_datastore() def get_received_txn_response(*args): @@ -528,7 +521,7 @@ class PresencePushTestCase(unittest.TestCase): self.datastore.get_received_txn_response = get_received_txn_response self.handler = hs.get_handlers().presence_handler - self.handler.push_update_to_clients = self.mock_update_client + self.event_source = hs.get_event_sources().sources["presence"] # Mock the RoomMemberHandler hs.handlers.room_member_handler = Mock(spec=[ @@ -622,16 +615,23 @@ class PresencePushTestCase(unittest.TestCase): apple_set.add(self.u_banana) apple_set.add(self.u_clementine) + self.assertEquals(self.event_source.get_current_key(), 0) + yield self.handler.set_state(self.u_apple, self.u_apple, {"state": ONLINE}) - self.mock_update_client.assert_has_calls([ - call(users_to_push=set([self.u_apple, self.u_banana, self.u_clementine]), - room_ids=["a-room"], - observed_user=self.u_apple, - statuscache=ANY), # self-reflection - ], any_order=True) - self.mock_update_client.reset_mock() + self.assertEquals(self.event_source.get_current_key(), 1) + self.assertEquals( + self.event_source.get_new_events_for_user(self.u_apple, 0, None)[0], + [ + {"type": "m.presence", + "content": { + "user_id": "@apple:test", + "state": ONLINE, + "mtime_age": 0, + }}, + ], + ) presence = yield self.handler.get_presence_list( observer_user=self.u_apple, accepted=True) @@ -657,31 +657,24 @@ class PresencePushTestCase(unittest.TestCase): "state": OFFLINE}, ], presence) - self.mock_update_client.assert_has_calls([ - call(users_to_push=set([self.u_banana]), - room_ids=[], - observed_user=self.u_banana, - statuscache=ANY), # self-reflection - ]) # and no others... + self.assertEquals(self.event_source.get_current_key(), 2) + self.assertEquals( + self.event_source.get_new_events_for_user( + self.u_banana, 1, None + )[0], + [ + {"type": "m.presence", + "content": { + "user_id": "@banana:test", + "state": ONLINE, + "mtime_age": 2000 + }}, + ] + ) @defer.inlineCallbacks def test_push_remote(self): put_json = self.mock_http_client.put_json -# put_json.expect_call_and_return( -# call("remote", -# path=ANY, # Can't guarantee which txn ID will be which -# data=_expect_edu("remote", "m.presence", -# content={ -# "push": [ -# {"user_id": "@apple:test", -# "state": "online", -# "mtime_age": 0}, -# ], -# } -# ) -# ), -# defer.succeed((200, "OK")) -# ) put_json.expect_call_and_return( call("farm", path=ANY, # Can't guarantee which txn ID will be which @@ -724,6 +717,8 @@ class PresencePushTestCase(unittest.TestCase): self.room_members = [self.u_banana, self.u_potato] + self.assertEquals(self.event_source.get_current_key(), 0) + yield self.mock_federation_resource.trigger("PUT", "/matrix/federation/v1/send/1000000/", _make_edu_json("elsewhere", "m.presence", @@ -737,12 +732,20 @@ class PresencePushTestCase(unittest.TestCase): ) ) - self.mock_update_client.assert_has_calls([ - call(users_to_push=set([self.u_apple]), - room_ids=["a-room"], - observed_user=self.u_potato, - statuscache=ANY), - ], any_order=True) + self.assertEquals(self.event_source.get_current_key(), 1) + self.assertEquals( + self.event_source.get_new_events_for_user( + self.u_apple, 0, None + )[0], + [ + {"type": "m.presence", + "content": { + "user_id": "@potato:remote", + "state": ONLINE, + "mtime_age": 1000, + }} + ] + ) self.clock.advance_time(2) @@ -754,24 +757,35 @@ class PresencePushTestCase(unittest.TestCase): def test_join_room_local(self): self.room_members = [self.u_apple, self.u_banana] - yield self.distributor.fire("user_joined_room", self.u_elderberry, + self.assertEquals(self.event_source.get_current_key(), 0) + + # TODO(paul): Gut-wrenching + self.handler._user_cachemap[self.u_clementine] = UserPresenceCache() + self.handler._user_cachemap[self.u_clementine].update( + { + "state": PresenceState.ONLINE, + "mtime": self.clock.time_msec(), + }, self.u_clementine + ) + + yield self.distributor.fire("user_joined_room", self.u_clementine, "a-room" ) - self.mock_update_client.assert_has_calls([ - call(room_ids=["a-room"], - observed_user=self.u_elderberry, - users_to_push=set(), - statuscache=ANY), - call(users_to_push=set([self.u_elderberry]), - observed_user=self.u_apple, - room_ids=[], - statuscache=ANY), - call(users_to_push=set([self.u_elderberry]), - observed_user=self.u_banana, - room_ids=[], - statuscache=ANY), - ], any_order=True) + self.assertEquals(self.event_source.get_current_key(), 1) + self.assertEquals( + self.event_source.get_new_events_for_user( + self.u_apple, 0, None + )[0], + [ + {"type": "m.presence", + "content": { + "user_id": "@clementine:test", + "state": ONLINE, + "mtime_age": 0, + }} + ] + ) @defer.inlineCallbacks def test_join_room_remote(self): From c715660cb8acd5333ee3202e2d06bebe6867e47f Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 30 Aug 2014 00:41:36 +0100 Subject: [PATCH 26/45] shrink text bubble font a bit; make image thumbnails always fit in their bubbles --- webclient/app.css | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/webclient/app.css b/webclient/app.css index 4e93bc993..9ba0aa997 100644 --- a/webclient/app.css +++ b/webclient/app.css @@ -301,7 +301,7 @@ h1 { display: inline-block; margin-bottom: -1px; max-width: 90%; - font-size: 16px; + font-size: 14px; word-wrap: break-word; padding-top: 7px; padding-bottom: 5px; @@ -311,6 +311,11 @@ h1 { -webkit-text-size-adjust:100% } +.bubble img { + max-width: 100%; + max-height: auto; +} + .differentUser td { padding-bottom: 5px ! important; } From f64ce52305d1486dfcb1ce9f880be97578bbeeec Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 30 Aug 2014 00:44:56 +0100 Subject: [PATCH 27/45] actually add a 'home' button --- webclient/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/webclient/index.html b/webclient/index.html index 5faf16562..fdc50a521 100644 --- a/webclient/index.html +++ b/webclient/index.html @@ -41,6 +41,7 @@
From b040bd61572704df800440f42bcf28c48ba939cc Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 31 Aug 2014 00:38:45 +0100 Subject: [PATCH 33/45] factor out mobile css into its own file --- webclient/mobile.css | 92 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 webclient/mobile.css diff --git a/webclient/mobile.css b/webclient/mobile.css new file mode 100644 index 000000000..7c62a072d --- /dev/null +++ b/webclient/mobile.css @@ -0,0 +1,92 @@ +/*** Mobile voodoo ***/ +@media all and (max-device-width: 640px) { + + #messageTableWrapper { + margin-right: 0px ! important; + } + + .leftBlock { + width: 8em ! important; + font-size: 8px ! important; + } + + .rightBlock { + width: 0px ! important; + display: none ! important; + } + + .avatar { + width: 36px ! important; + } + + #header { + background-color: transparent; + } + + #headerContent { + padding-right: 5px; + } + + #headerContent button { + font-size: 8px; + } + + #messageTable, + #wrapper, + #controls { + max-width: 640px ! important; + } + + #headerUserId, + #roomHeader img, + #userIdCell, + #roomRecentsTableWrapper, + #usersTableWrapper, + .extraControls { + display: none; + } + + #buttonsCell { + width: 60px ! important; + padding-left: 20px ! important; + } + + #roomLogo { + display: none; + } + + .bubble { + font-size: 12px ! important; + min-height: 20px ! important; + } + + #roomHeader { + padding-top: 10px; + } + + #roomName { + float: left; + font-size: 14px ! important; + margin-top: 0px ! important; + } + + #roomPage { + top: 35px ! important; + left: 5px ! important; + right: 5px ! important; + bottom: 70px ! important; + } + + #controlPanel { + height: 70px; + } + + /* stop zoom on select */ + select:focus, + textarea, + input + { + font-size: 16px ! important; + } + +} From 1bc036a12d6877f78f1f5033603d803ac01a13d2 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 31 Aug 2014 00:40:42 +0100 Subject: [PATCH 34/45] nasty big monolithic commit of a whole bunch of UI/UX improvements: - add a simple CSS template across the app for navigation & cosmetics - split login into login & register, and totally reskin it - restructure room CSS to play nicely with it - implement basis 1:1 chat from user pages - disable autofocus on iOS to improve UX --- webclient/app-controller.js | 12 + webclient/app-directive.js | 7 +- webclient/app.css | 302 ++++++++++-------- webclient/app.js | 10 +- webclient/components/matrix/matrix-service.js | 14 +- webclient/home/home-controller.js | 4 +- webclient/home/home.html | 42 ++- webclient/index.html | 18 +- webclient/login/login-controller.js | 59 +--- webclient/login/login.html | 92 +++--- webclient/room/room-controller.js | 7 +- webclient/room/room.html | 30 +- webclient/settings/settings.html | 17 +- webclient/user/user-controller.js | 28 ++ webclient/user/user.html | 38 +-- 15 files changed, 363 insertions(+), 317 deletions(-) diff --git a/webclient/app-controller.js b/webclient/app-controller.js index 775113bc8..77a7ad03a 100644 --- a/webclient/app-controller.js +++ b/webclient/app-controller.js @@ -37,6 +37,8 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even mPresence.start(); } + $scope.user_id = matrixService.config().user_id; + /** * Open a given page. * @param {String} url url of the page @@ -45,6 +47,16 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even $location.url(url); }; + // Open the given user profile page + $scope.goToUserPage = function(user_id) { + if (user_id === $scope.user_id) { + $location.url("/settings"); + } + else { + $location.url("/user/" + user_id); + } + }; + // Logs the user out $scope.logout = function() { diff --git a/webclient/app-directive.js b/webclient/app-directive.js index 01f60fdad..eee0d3842 100644 --- a/webclient/app-directive.js +++ b/webclient/app-directive.js @@ -32,7 +32,12 @@ angular.module('matrixWebClient') .directive('ngFocus', ['$timeout', function($timeout) { return { link: function(scope, element, attr) { - $timeout(function() { element[0].focus(); }, 0); + // XXX: slightly evil hack to disable autofocus on iOS, as in general + // it causes more problems than it fixes, by bouncing the page + // around + if (!/(iPad|iPhone|iPod)/g.test(navigator.userAgent)) { + $timeout(function() { element[0].focus(); }, 0); + } } }; }]); \ No newline at end of file diff --git a/webclient/app.css b/webclient/app.css index a5d0199ba..142e764ef 100644 --- a/webclient/app.css +++ b/webclient/app.css @@ -1,122 +1,195 @@ -/*** Mobile voodoo ***/ -@media all and (max-device-width: 640px) { - - #messageTableWrapper { - margin-right: 0px ! important; - } - - .leftBlock { - width: 8em ! important; - font-size: 8px ! important; - } - - .rightBlock { - width: 0px ! important; - display: none ! important; - } - - .avatar { - width: 36px ! important; - } - - #header, - #messageTable, - #wrapper, - #roomName, - #controls { - max-width: 640px ! important; - } - - #userIdCell, - #roomRecentsTableWrapper, - #usersTableWrapper, - #extraControls { - display: none; - } - - #buttonsCell { - width: 60px ! important; - padding-left: 20px ! important; - } - - #roomLogo { - display: none; - } - - #roomName { - text-align: left ! important; - top: -35px ! important; - } - - .bubble { - font-size: 12px ! important; - min-height: 20px ! important; - } - - #page { - top: 35px ! important; - bottom: 70px ! important; - } - - #header, - #page { - margin: 5px ! important; - } - - #header { - padding: 5px ! important; - } - - /* stop zoom on select */ - select:focus, - textarea, - input - { - font-size: 16px ! important; - } - +/** Common layout **/ + +html { + height: 100%; } body { + height: 100%; font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif; font-size: 12pt; margin: 0px; } h1 { - font-family: Helvetica, Arial, sans-serif; + font-size: 20pt; } -/*** Overall page layout ***/ +a:link { color: #666; } +a:visited { color: #666; } +a:hover { color: #000; } +a:active { color: #000; } #page { - position: absolute; - top: 80px; - bottom: 100px; - left: 0px; - right: 0px; - margin: 20px; + min-height: 100%; + margin-bottom: -32px; /* to make room for the footer */ } #wrapper { margin: auto; max-width: 1280px; height: 100%; + padding-top: 40px; + padding-bottom: 40px; + padding-left: 20px; + padding-right: 20px; +} + +#header +{ + position: absolute; + top: 0px; + width: 100%; + background-color: #333; + height: 32px; +} + +#headerContent { + color: #ccc; + max-width: 1280px; + margin: auto; + text-align: right; + height: 32px; + line-height: 32px; +} + +#headerContent a:link, +#headerContent a:visited, +#headerContent a:hover, +#headerContent a:active { + color: #fff; +} + +#footer +{ + width: 100%; + border-top: #666 1px solid; + background-color: #aaa; + height: 32px; +} + +#footerContent +{ + font-size: 8pt; + color: #fff; + max-width: 1280px; + margin: auto; + text-align: center; + height: 32px; + line-height: 32px; +} + +#genericHeading +{ + margin-top: 13px; +} + +#feedback { + color: #800; +} + +.mouse-pointer { + cursor: pointer; +} + +.invited { + opacity: 0.2; +} + +/*** Login Pages ***/ + +.loginWrapper { + text-align: center; +} + +#loginForm { + text-align: left; + padding: 1em; + margin-bottom: 40px; + display: inline-block; + + -webkit-border-radius: 10px; + -moz-border-radius: 10px; + border-radius: 10px; + + -webkit-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15); + -moz-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15); + box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15); + + background-color: #f8f8f8; + border: 1px #ccc solid; +} + +#loginForm input[type='radio'] { + margin-right: 1em; +} + +#serverConfig { + text-align: center; +} + +#serverConfig, +#serverConfig input, +#serverConfig button +{ + font-size: 10pt ! important; +} + +.smallPrint { + color: #888; + font-size: 9pt ! important; + font-style: italic ! important; +} + +#serverConfig label { + display: inline-block; + text-align: right; + margin-right: 0.5em; + width: 7em; +} + +#loginForm, +#loginForm input, +#loginForm button, +#loginForm select { + font-size: 18px; +} + +/*** Room page ***/ + +#roomPage { + position: absolute; + top: 120px; + bottom: 120px; + left: 20px; + right: 20px; +} + +#roomWrapper { + margin: auto; + max-width: 1280px; + height: 100%; } #roomName { - max-width: 1280px; - width: 100%; - text-align: right; - top: -40px; - position: absolute; + float: right; font-size: 16px; + margin-top: 15px; +} + +#roomHeader { + margin: auto; + padding-left: 20px; + padding-right: 20px; + padding-top: 53px; + max-width: 1280px; } #controlPanel { position: absolute; bottom: 0px; width: 100%; + height: 100px; background-color: #f8f8f8; border-top: #aaa 1px solid; } @@ -147,10 +220,6 @@ h1 { background-color: #faa; } -.mouse-pointer { - cursor: pointer; -} - /*** Participant list ***/ #usersTableWrapper { @@ -409,11 +478,14 @@ h1 { } /*** Recents in the room page ***/ + #roomRecentsTableWrapper { float: left; max-width: 320px; - margin-right: 20px; + padding-right: 10px; + margin-right: 10px; height: 100%; + border-right: 1px solid #ddd; overflow-y: auto; } @@ -434,46 +506,8 @@ h1 { } /*** User profile page ***/ + #user-displayname { font-size: 24px; } -/******************************/ -#header -{ - padding: 20px; - max-width: 1280px; - margin: auto; -} - -#logo, -#roomLogo { - max-width: 1280px; - margin: auto; -} - -#header-buttons { - float: right; -} - -.text_entry_section { - position: fixed; - bottom: 0; - z-index: 100; - left: 0; - right: 10em; - width: 100%; - background: #e0e0e0; -} - -.member_invited { - color: blue; -} - -.member_joined { - -} - -.member_left { - color: gray; -} diff --git a/webclient/app.js b/webclient/app.js index 02695c3ae..9663ddf96 100644 --- a/webclient/app.js +++ b/webclient/app.js @@ -18,6 +18,7 @@ var matrixWebClient = angular.module('matrixWebClient', [ 'ngRoute', 'MatrixWebClientController', 'LoginController', + 'RegisterController', 'RoomController', 'HomeController', 'RecentsController', @@ -38,6 +39,10 @@ matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider', templateUrl: 'login/login.html', controller: 'LoginController' }). + when('/register', { + templateUrl: 'login/register.html', + controller: 'RegisterController' + }). when('/room/:room_id_or_alias', { templateUrl: 'room/room.html', controller: 'RoomController' @@ -84,7 +89,10 @@ matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider', matrixWebClient.run(['$location', 'matrixService', function($location, matrixService) { // If user auth details are not in cache, go to the login page - if (!matrixService.isUserLoggedIn()) { + if (!matrixService.isUserLoggedIn() && + $location.path() !== "/login" && + $location.path() !== "/register") + { $location.path("login"); } diff --git a/webclient/components/matrix/matrix-service.js b/webclient/components/matrix/matrix-service.js index 8543491dc..746442c28 100644 --- a/webclient/components/matrix/matrix-service.js +++ b/webclient/components/matrix/matrix-service.js @@ -95,14 +95,18 @@ angular.module('matrixService', []) }, // Create a room - create: function(room_id, visibility) { + create: function(room_alias, visibility) { // The REST path spec var path = "/createRoom"; - return doRequest("POST", path, undefined, { - visibility: visibility, - room_alias_name: room_id - }); + var req = { + "visibility": visibility + }; + if (room_alias) { + req.room_alias_name = room_alias; + } + + return doRequest("POST", path, undefined, req); }, // List all rooms joined or been invited to diff --git a/webclient/home/home-controller.js b/webclient/home/home-controller.js index 438ffe1b0..847918d5d 100644 --- a/webclient/home/home-controller.js +++ b/webclient/home/home-controller.js @@ -58,14 +58,14 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen ); }; - $scope.createNewRoom = function(room_id, isPrivate) { + $scope.createNewRoom = function(room_alias, isPrivate) { var visibility = "public"; if (isPrivate) { visibility = "private"; } - matrixService.create(room_id, visibility).then( + matrixService.create(room_alias, visibility).then( function(response) { // This room has been created. Refresh the rooms list console.log("Created room " + response.data.room_alias + " with id: "+ diff --git a/webclient/home/home.html b/webclient/home/home.html index 1b1c21d9d..8d35eb515 100644 --- a/webclient/home/home.html +++ b/webclient/home/home.html @@ -1,29 +1,24 @@
-
- -
-
- - - - - -
-
- -
-
-
-
{{ profile.displayName }}
-
{{ config.user_id }}
-
-
-
+ +
+ [matrix]
-

Recents

+

Welcome to homeserver {{ config.homeserver }}

+ +
+
+ +
+
+
{{ profile.displayName }}
+
{{ config.user_id }}
+
+
+ +

Recent conversations


@@ -38,9 +33,9 @@
- + private - +
@@ -54,5 +49,4 @@ {{ feedback }}
-
diff --git a/webclient/index.html b/webclient/index.html index fdc50a521..3c31a8a05 100644 --- a/webclient/index.html +++ b/webclient/index.html @@ -4,6 +4,8 @@ [matrix] + + @@ -19,6 +21,7 @@ + @@ -38,16 +41,23 @@ - +
-
+
+ diff --git a/webclient/login/login-controller.js b/webclient/login/login-controller.js index 51f9a3bdf..2b9192695 100644 --- a/webclient/login/login-controller.js +++ b/webclient/login/login-controller.js @@ -10,63 +10,26 @@ angular.module('LoginController', ['matrixService']) if ($location.port()) { hs_url += ":" + $location.port(); } + var example_domain = $location.host(); $scope.account = { homeserver: hs_url, + example_domain: example_domain, desired_user_name: "", user_id: "", password: "", - identityServer: "", + identityServer: "http://matrix.org:8090", pwd1: "", - pwd2: "" + pwd2: "", }; - - $scope.register = function() { - - // Set the urls - matrixService.setConfig({ - homeserver: $scope.account.homeserver, - identityServer: $scope.account.identityServer - }); - - if ($scope.account.pwd1 !== $scope.account.pwd2) { - $scope.feedback = "Passwords don't match."; - return; - } - else if ($scope.account.pwd1.length < 6) { - $scope.feedback = "Password must be at least 6 characters."; - return; - } - - matrixService.register($scope.account.desired_user_name, $scope.account.pwd1).then( - function(response) { - $scope.feedback = "Success"; - // Update the current config - var config = matrixService.config(); - angular.extend(config, { - access_token: response.data.access_token, - user_id: response.data.user_id - }); - matrixService.setConfig(config); - - // And permanently save it - matrixService.saveConfig(); - eventStreamService.resume(); - // Go to the user's rooms list page - $location.url("home"); - }, - function(error) { - if (error.data) { - if (error.data.errcode === "M_USER_IN_USE") { - $scope.feedback = "Username already taken."; - } - } - else if (error.status === 0) { - $scope.feedback = "Unable to talk to the server."; - } - }); + + $scope.login_types = [ "email", "mxid" ]; + $scope.login_type_label = { + "email": "Email address", + "mxid": "Matrix ID (e.g. @bob:matrix.org or bob)", }; - + $scope.login_type = 'mxid'; // TODO: remember the user's preferred login_type + $scope.login = function() { matrixService.setConfig({ homeserver: $scope.account.homeserver, diff --git a/webclient/login/login.html b/webclient/login/login.html index 4b2ea6092..8d5a53ebb 100644 --- a/webclient/login/login.html +++ b/webclient/login/login.html @@ -1,55 +1,49 @@