From 8bf3994c2e4726278355bc1398c4b9c94d242ad0 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 15 Aug 2014 10:20:14 +0100 Subject: Added event stream service which neatly blobs together requests / state for the event stream. This depends on matrix service to do the actual hit. Currently this has exactly the same behaviour as before. --- webclient/app.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'webclient/app.js') diff --git a/webclient/app.js b/webclient/app.js index 0b613fa206..547431d9b2 100644 --- a/webclient/app.js +++ b/webclient/app.js @@ -20,7 +20,8 @@ var matrixWebClient = angular.module('matrixWebClient', [ 'LoginController', 'RoomController', 'RoomsController', - 'matrixService' + 'matrixService', + 'eventStreamService' ]); matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider', -- cgit 1.5.1 From 5dbceaf5a40a7e90a4aca1a1612fa9ea13290a02 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 15 Aug 2014 11:31:13 +0100 Subject: Added event handler service which.. handles events. More specifically, it $broadcasts events depending on their type, and does processing on events (shuffling keys, adding events to $rootScope so displays will automatically update, sending delivery receipts, and so on). Some of this logic was previously contained in the RoomController, which fails the moment you add >1 room into the mix, hence requiring a Service to handle events, rather than having each individual controller maintain their part of the world. --- webclient/app.js | 3 +- .../components/matrix/event-handler-service.js | 87 ++++++++++++++++++++++ .../components/matrix/event-stream-service.js | 17 ++++- webclient/index.html | 1 + webclient/room/room-controller.js | 48 ++++++------ 5 files changed, 124 insertions(+), 32 deletions(-) create mode 100644 webclient/components/matrix/event-handler-service.js (limited to 'webclient/app.js') diff --git a/webclient/app.js b/webclient/app.js index 547431d9b2..bc78eb9d17 100644 --- a/webclient/app.js +++ b/webclient/app.js @@ -21,7 +21,8 @@ var matrixWebClient = angular.module('matrixWebClient', [ 'RoomController', 'RoomsController', 'matrixService', - 'eventStreamService' + 'eventStreamService', + 'eventHandlerService' ]); matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider', diff --git a/webclient/components/matrix/event-handler-service.js b/webclient/components/matrix/event-handler-service.js new file mode 100644 index 0000000000..30d7ab35ca --- /dev/null +++ b/webclient/components/matrix/event-handler-service.js @@ -0,0 +1,87 @@ +/* +Copyright 2014 matrix.org + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +'use strict'; + +/* +This service handles what should happen when you get an event. This service does +not care where the event came from, it only needs enough context to be able to +process them. Events may be coming from the event stream, the REST API (via +direct GETs or via a pagination stream API), etc. + +Typically, this service will store events or broadcast them to any listeners +(e.g. controllers) via $broadcast. Alternatively, it may update the $rootScope +if typically all the $on method would do is update its own $scope. +*/ +angular.module('eventHandlerService', []) +.factory('eventHandlerService', ['matrixService', '$rootScope', function(matrixService, $rootScope) { + var MSG_EVENT = "MSG_EVENT"; + var MEMBER_EVENT = "MEMBER_EVENT"; + var PRESENCE_EVENT = "PRESENCE_EVENT"; + + var handleMessage = function(event, isLiveEvent) { + if ("membership_target" in event.content) { + // event.user_id = event.content.membership_target; + } + + // $broadcast this, as controllers may want to do funky things such as + // scroll to the bottom, etc which cannot be expressed via simple $scope + // updates. + console.log("Bcast " + JSON.stringify(event)); + $rootScope.$broadcast(MSG_EVENT, event, isLiveEvent); + }; + + var handleRoomMember = function(event, isLiveEvent) { + $rootScope.$broadcast(MEMBER_EVENT, event, isLiveEvent); + }; + + var handlePresence = function(event, isLiveEvent) { + $rootScope.$broadcast(PRESENCE_EVENT, event, isLiveEvent); + }; + + + return { + MSG_EVENT: MSG_EVENT, + MEMBER_EVENT: MEMBER_EVENT, + PRESENCE_EVENT: PRESENCE_EVENT, + + + handleEvent: function(event, isLiveEvent) { + switch(event.type) { + case "m.room.message": + handleMessage(event, isLiveEvent); + break; + case "m.room.member": + handleRoomMember(event, isLiveEvent); + break; + case "m.presence": + handlePresence(event, isLiveEvent); + break; + default: + console.log("Unable to handle event type " + event.type); + break; + } + }, + + // isLiveEvents determines whether notifications should be shown, whether + // messages get appended to the start/end of lists, etc. + handleEvents: function(events, isLiveEvents) { + for (var i=0; i + diff --git a/webclient/room/room-controller.js b/webclient/room/room-controller.js index 3f69a12c23..301d3d6447 100644 --- a/webclient/room/room-controller.js +++ b/webclient/room/room-controller.js @@ -15,8 +15,8 @@ limitations under the License. */ angular.module('RoomController', []) -.controller('RoomController', ['$scope', '$http', '$timeout', '$routeParams', '$location', 'matrixService', 'eventStreamService', - function($scope, $http, $timeout, $routeParams, $location, matrixService, eventStreamService) { +.controller('RoomController', ['$scope', '$http', '$timeout', '$routeParams', '$location', 'matrixService', 'eventStreamService', 'eventHandlerService', + function($scope, $http, $timeout, $routeParams, $location, matrixService, eventStreamService, eventHandlerService) { 'use strict'; var MESSAGES_PER_PAGINATION = 10; $scope.room_id = $routeParams.room_id; @@ -42,34 +42,28 @@ angular.module('RoomController', []) },0); }; - var parseChunk = function(chunks, appendToStart) { - for (var i = 0; i < chunks.length; i++) { - var chunk = chunks[i]; - if (chunk.room_id == $scope.room_id && chunk.type == "m.room.message") { - if ("membership_target" in chunk.content) { - chunk.user_id = chunk.content.membership_target; - } - if (appendToStart) { - $scope.messages.unshift(chunk); - } - else { - $scope.messages.push(chunk); - scrollToBottom(); - } - } - else if (chunk.room_id == $scope.room_id && chunk.type == "m.room.member") { - updateMemberList(chunk); - } - else if (chunk.type === "m.presence") { - updatePresence(chunk); - } + $scope.$on(eventHandlerService.MSG_EVENT, function(ngEvent, event, isLive) { + if (isLive) { + $scope.messages.push(event); + scrollToBottom(); } - }; + else { + $scope.messages.unshift(event); + } + }); + + $scope.$on(eventHandlerService.MEMBER_EVENT, function(ngEvent, event, isLive) { + updateMemberList(event); + }); + + $scope.$on(eventHandlerService.PRESENCE_EVENT, function(ngEvent, event, isLive) { + updatePresence(event); + }); var paginate = function(numItems) { matrixService.paginateBackMessages($scope.room_id, $scope.state.earliest_token, numItems).then( function(response) { - parseChunk(response.data.chunk, true); + eventHandlerService.handleEvents(response.data.chunk, false); $scope.state.earliest_token = response.data.end; if (response.data.chunk.length < MESSAGES_PER_PAGINATION) { // no more messages to paginate :( @@ -89,8 +83,8 @@ angular.module('RoomController', []) console.log("Got response from "+$scope.state.events_from+" to "+response.data.end); $scope.state.events_from = response.data.end; $scope.feedback = ""; - - parseChunk(response.data.chunk, false); + + eventHandlerService.handleEvents(response.data.chunk, true); if ($scope.stopPoll) { console.log("Stopping polling."); -- cgit 1.5.1 From 7ddb7a5cbbe9e4576742dc060ba35ca863b8d8b0 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 15 Aug 2014 13:43:07 +0100 Subject: Event streaming now happens on an app level, rather than a per-room level. Make eventStreamService manage it's own repolling provided no one calls stop() on it. Couple the stream with eventHandlerService so any controller can just blithely call eventStreamService.resume() and expect to 'get stuff' without having to handle promises (though resume() still returns a promise for that request and proxies it through $q). Kill and reset the stream if you logout. --- webclient/app-controller.js | 16 +++--- webclient/app.js | 6 ++- .../components/matrix/event-stream-service.js | 62 ++++++++++++++++++++-- webclient/login/login-controller.js | 7 +-- webclient/room/room-controller.js | 4 -- 5 files changed, 76 insertions(+), 19 deletions(-) (limited to 'webclient/app.js') diff --git a/webclient/app-controller.js b/webclient/app-controller.js index 086fa3d946..7f5f93ef72 100644 --- a/webclient/app-controller.js +++ b/webclient/app-controller.js @@ -21,8 +21,8 @@ limitations under the License. 'use strict'; angular.module('MatrixWebClientController', ['matrixService']) -.controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', 'matrixService', - function($scope, $location, $rootScope, matrixService) { +.controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', 'matrixService', 'eventStreamService', + function($scope, $location, $rootScope, matrixService, eventStreamService) { // Check current URL to avoid to display the logout button on the login page $scope.location = $location.path(); @@ -44,11 +44,15 @@ angular.module('MatrixWebClientController', ['matrixService']) else { $scope.config = matrixService.config(); } - }; - + }; + + eventStreamService.resume(); // Logs the user out $scope.logout = function() { + // kill the event stream + eventStreamService.stop(); + // Clean permanent data matrixService.setConfig({}); matrixService.saveConfig(); @@ -57,7 +61,7 @@ angular.module('MatrixWebClientController', ['matrixService']) $location.path("login"); }; - // Listen to the event indicating that the access token is no more valid. + // Listen to the event indicating that the access token is no longer valid. // In this case, the user needs to log in again. $scope.$on("M_UNKNOWN_TOKEN", function() { console.log("Invalid access token -> log user out"); @@ -65,4 +69,4 @@ angular.module('MatrixWebClientController', ['matrixService']) }); }]); - \ No newline at end of file + diff --git a/webclient/app.js b/webclient/app.js index bc78eb9d17..6e0351067f 100644 --- a/webclient/app.js +++ b/webclient/app.js @@ -61,12 +61,16 @@ matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider', $httpProvider.interceptors.push('AccessTokenInterceptor'); }]); -matrixWebClient.run(['$location', 'matrixService' , function($location, matrixService) { +matrixWebClient.run(['$location', 'matrixService', 'eventStreamService', function($location, matrixService, eventStreamService) { // If we have no persistent login information, go to the login page var config = matrixService.config(); if (!config || !config.access_token) { + eventStreamService.stop(); $location.path("login"); } + else { + eventStreamService.resume(); + } }]); matrixWebClient diff --git a/webclient/components/matrix/event-stream-service.js b/webclient/components/matrix/event-stream-service.js index 1cb9960b9a..97018df881 100644 --- a/webclient/components/matrix/event-stream-service.js +++ b/webclient/components/matrix/event-stream-service.js @@ -19,19 +19,21 @@ limitations under the License. /* This service manages where in the event stream the web client currently is and provides methods to resume/pause/stop the event stream. This service is not -responsible for parsing event data. For that, see the eventDataHandler. +responsible for parsing event data. For that, see the eventHandlerService. */ angular.module('eventStreamService', []) -.factory('eventStreamService', ['matrixService', function(matrixService) { +.factory('eventStreamService', ['$q', '$timeout', 'matrixService', 'eventHandlerService', function($q, $timeout, matrixService, eventHandlerService) { var END = "END"; var START = "START"; var TIMEOUT_MS = 5000; + var ERR_TIMEOUT_MS = 5000; var settings = { from: "END", to: undefined, limit: undefined, - shouldPoll: true + shouldPoll: true, + isActive: false }; // interrupts the stream. Only valid if there is a stream conneciton @@ -39,19 +41,69 @@ angular.module('eventStreamService', []) var interrupt = function(shouldPoll) { console.log("p[EventStream] interrupt("+shouldPoll+") "+ JSON.stringify(settings)); + settings.shouldPoll = shouldPoll; + settings.isActive = false; }; var saveStreamSettings = function() { localStorage.setItem("streamSettings", JSON.stringify(settings)); }; + var startEventStream = function() { + settings.shouldPoll = true; + settings.isActive = true; + var deferred = $q.defer(); + // run the stream from the latest token + matrixService.getEventStream(settings.from, TIMEOUT_MS).then( + function(response) { + if (!settings.isActive) { + console.log("[EventStream] Got response but now inactive. Dropping data."); + return; + } + + settings.from = response.data.end; + + console.log("[EventStream] Got response from "+settings.from+" to "+response.data.end); + eventHandlerService.handleEvents(response.data.chunk, true); + + deferred.resolve(response); + + if (settings.shouldPoll) { + $timeout(startEventStream, 0); + } + else { + console.log("[EventStream] Stopping poll."); + } + }, + function(error) { + if (error.status == 403) { + settings.shouldPoll = false; + } + + deferred.reject(error); + + if (settings.shouldPoll) { + $timeout(startEventStream, ERR_TIMEOUT_MS); + } + else { + console.log("[EventStream] Stopping polling."); + } + } + ); + return deferred.promise; + }; + return { // resume the stream from whereever it last got up to. Typically used // when the page is opened. resume: function() { + if (settings.isActive) { + console.log("[EventStream] Already active, ignoring resume()"); + return; + } + console.log("[EventStream] resume "+JSON.stringify(settings)); - // run the stream from the latest token - return matrixService.getEventStream(settings.from, TIMEOUT_MS); + return startEventStream(); }, // pause the stream. Resuming it will continue from the current position diff --git a/webclient/login/login-controller.js b/webclient/login/login-controller.js index 8bd6a4e84f..aa928ef48d 100644 --- a/webclient/login/login-controller.js +++ b/webclient/login/login-controller.js @@ -1,6 +1,6 @@ angular.module('LoginController', ['matrixService']) -.controller('LoginController', ['$scope', '$location', 'matrixService', - function($scope, $location, matrixService) { +.controller('LoginController', ['$scope', '$location', 'matrixService', 'eventStreamService', + function($scope, $location, matrixService, eventStreamService) { 'use strict'; @@ -51,7 +51,7 @@ angular.module('LoginController', ['matrixService']) // And permanently save it matrixService.saveConfig(); - + eventStreamService.resume(); // Go to the user's rooms list page $location.path("rooms"); }, @@ -83,6 +83,7 @@ angular.module('LoginController', ['matrixService']) access_token: response.data.access_token }); matrixService.saveConfig(); + eventStreamService.resume(); $location.path("rooms"); } else { diff --git a/webclient/room/room-controller.js b/webclient/room/room-controller.js index 4c5415eace..49c73ff4bb 100644 --- a/webclient/room/room-controller.js +++ b/webclient/room/room-controller.js @@ -119,7 +119,6 @@ angular.module('RoomController', []) function(response) { var member = $scope.members[chunk.target_user_id]; if (member !== undefined) { - console.log("Updated displayname "+chunk.target_user_id+" to " + response.data.displayname); member.displayname = response.data.displayname; } } @@ -128,7 +127,6 @@ angular.module('RoomController', []) function(response) { var member = $scope.members[chunk.target_user_id]; if (member !== undefined) { - console.log("Updated image for "+chunk.target_user_id+" to " + response.data.avatar_url); member.avatar_url = response.data.avatar_url; } } @@ -204,8 +202,6 @@ angular.module('RoomController', []) matrixService.join($scope.room_id).then( function() { console.log("Joined room "+$scope.room_id); - // Now start reading from the stream - $timeout(shortPoll, 0); // Get the current member list matrixService.getMemberList($scope.room_id).then( -- cgit 1.5.1