diff --git a/lib/client.js b/lib/client.js index d4a51938d..b7028a0e0 100644 --- a/lib/client.js +++ b/lib/client.js @@ -146,6 +146,7 @@ function MatrixClient(opts) { this._ongoingScrollbacks = {}; this.timelineSupport = Boolean(opts.timelineSupport); this.urlPreviewCache = {}; + this._notifTimelineSet = null; this._crypto = null; if (CRYPTO_ENABLED && opts.sessionStore !== null && @@ -249,6 +250,24 @@ MatrixClient.prototype.retryImmediately = function() { return this._syncApi.retryImmediately(); }; +/** + * Return the global notification EventTimelineSet, if any + * + * @return {EventTimelineSet} the globl notification EventTimelineSet + */ +MatrixClient.prototype.getNotifTimelineSet = function() { + return this._notifTimelineSet; +}; + +/** + * Set the global notification EventTimelineSet + * + * @param {EventTimelineSet} notifTimelineSet + */ +MatrixClient.prototype.setNotifTimelineSet = function(notifTimelineSet) { + this._notifTimelineSet = notifTimelineSet; +}; + // Crypto bits // =========== @@ -1365,16 +1384,16 @@ function _membershipChange(client, roomId, userId, membership, reason, callback) /** * Obtain a dict of actions which should be performed for this event according - * to the push rules for this user. + * to the push rules for this user. Caches the dict on the event. * @param {MatrixEvent} event The event to get push actions for. * @return {module:pushprocessor~PushAction} A dict of actions to perform. */ MatrixClient.prototype.getPushActionsForEvent = function(event) { - if (event._pushActions === undefined) { + if (!event.getPushActions()) { var pushProcessor = new PushProcessor(this); - event._pushActions = pushProcessor.actionsForEvent(event); + event.setPushActions(pushProcessor.actionsForEvent(event)); } - return event._pushActions; + return event.getPushActions(); }; // Profile operations @@ -1774,6 +1793,88 @@ MatrixClient.prototype.paginateEventTimeline = function(eventTimeline, opts) { return promise; }; + +/** + * Take an EventTimeline, and backfill results from the notifications API. + * In future, the notifications API should probably be replaced by /messages + * with a custom filter or something - so we don't feel too bad about this being + * cargoculted from paginateEventTimeLine. + * + * @param {module:models/event-timeline~EventTimeline} eventTimeline timeline + * object to be updated + * @param {Object} [opts] + * @param {boolean} [opts.backwards = false] true to fill backwards, + * false to go forwards. Forwards is not implemented yet! + * @param {number} [opts.limit = 30] number of events to request + * + * @return {module:client.Promise} Resolves to a boolean: false if there are no + * events and we reached either end of the timeline; else true. + */ +MatrixClient.prototype.paginateNotifTimeline = function(eventTimeline, opts) { + // TODO: we should implement a backoff (as per scrollback()) to deal more + // nicely with HTTP errors. + opts = opts || {}; + var backwards = opts.backwards || false; + + if (!backwards) { + throw new Error("paginateNotifTimeline can only paginate backwards"); + } + + if (eventTimeline.getRoomId()) { + throw new Error("paginateNotifTimeline should never be called on a " + + "timeline associated with a room"); + } + + var dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS; + + var token = eventTimeline.getPaginationToken(dir); + + var pendingRequest = eventTimeline._paginationRequests[dir]; + if (pendingRequest) { + // already a request in progress - return the existing promise + return pendingRequest; + } + + var path = "/notifications"; + var params = { + from: token, + limit: ('limit' in opts) ? opts.limit : 30, + only: 'highlight', + }; + + var self = this; + var promise = + this._http.authedRequestWithPrefix(undefined, "GET", path, params, + undefined, httpApi.PREFIX_UNSTABLE + ).then(function(res) { + var token = res.next_token; + var matrixEvents = []; + + for (var i = 0; i < res.notifications.length; i++) { + var notification = res.notifications[i]; + var event = self.getEventMapper()(notification.event); + event.setPushActions(notification.actions); + matrixEvents[i] = event; + } + + eventTimeline.getEventTimelineSet() + .addEventsToTimeline(matrixEvents, backwards, eventTimeline, token); + + // if we've hit the end of the timeline, we need to stop trying to + // paginate. We need to keep the 'forwards' token though, to make sure + // we can recover from gappy syncs. + if (backwards && !res.next_token) { + eventTimeline.setPaginationToken(null, dir); + } + return res.next_token ? true : false; + }).finally(function() { + eventTimeline._paginationRequests[dir] = null; + }); + eventTimeline._paginationRequests[dir] = promise; + + return promise; +}; + /** * Peek into a room and receive updates about the room. This only works if the * history visibility for the room is world_readable. diff --git a/lib/models/event-timeline.js b/lib/models/event-timeline.js index b885e0469..40d52580d 100644 --- a/lib/models/event-timeline.js +++ b/lib/models/event-timeline.js @@ -100,6 +100,14 @@ EventTimeline.prototype.getFilter = function() { return this._eventTimelineSet.getFilter(); }; +/** + * Get the timelineSet for this timeline + * @return {EventTimelineSet} timelineSet + */ +EventTimeline.prototype.getTimelineSet = function() { + return this._eventTimelineSet; +}; + /** * Get the base index. * diff --git a/lib/models/event.js b/lib/models/event.js index 92a9a5398..90e8a39b4 100644 --- a/lib/models/event.js +++ b/lib/models/event.js @@ -76,6 +76,7 @@ module.exports.MatrixEvent = function MatrixEvent(event, clearEvent) { this.forwardLooking = true; this._clearEvent = clearEvent || {}; + this._pushActions = null; }; module.exports.MatrixEvent.prototype = { @@ -294,6 +295,24 @@ module.exports.MatrixEvent.prototype = { isRedacted: function() { return Boolean(this.getUnsigned().redacted_because); }, + + /** + * Get the push actions, if known, for this event + * + * @return {?Object} push actions + */ + getPushActions: function() { + return this._pushActions; + }, + + /** + * Set the push actions for this event. + * + * @param {Object} pushActions push actions + */ + setPushActions: function(pushActions) { + this._pushActions = pushActions; + }, }; diff --git a/lib/models/room.js b/lib/models/room.js index 467e26d83..35ed3f1cd 100644 --- a/lib/models/room.js +++ b/lib/models/room.js @@ -149,21 +149,18 @@ function Room(roomId, opts) { this._notificationCounts = {}; - // all our per-room timeline lists. the first one is the unfiltered ones; + // all our per-room timeline sets. the first one is the unfiltered ones; // the subsequent ones are the filtered ones in no particular order. this._timelineSets = [new EventTimelineSet(roomId, this, opts)]; reEmit(this, this._timelineSets[0], ["Room.timeline"]); this._fixUpLegacyTimelineFields(); - // any filtered timeline lists we're maintaining for this room + // any filtered timeline sets we're maintaining for this room this._filteredTimelineSets = { // filter_id: timelineSet }; - // a reference to our shared notification timeline list - this._notifTimelineSet = opts.notifTimelineSet; - if (this._opts.pendingEventOrdering == "detached") { this._pendingEventList = []; } @@ -241,14 +238,6 @@ Room.prototype.getTimelineSets = function() { return this._timelineSets; }; -/** - * Return the shared notification timeline set - * @return {EventTimelineSet} notification timeline set - */ -Room.prototype.getNotifTimelineSet = function() { - return this._notifTimelineSet; -}; - /** * Get the timeline which contains the given event from the unfiltered set, if any * @@ -261,7 +250,7 @@ Room.prototype.getTimelineForEvent = function(eventId) { }; /** - * Add a new timeline to this room's unfiltered timeline list + * Add a new timeline to this room's unfiltered timeline set * * @return {module:models/event-timeline~EventTimeline} newly-created timeline */ @@ -563,18 +552,11 @@ Room.prototype._addLiveEvent = function(event, duplicateStrategy) { } } - // add to our timeline lists + // add to our timeline sets for (i = 0; i < this._timelineSets.length; i++) { this._timelineSets[i].addLiveEvent(event, duplicateStrategy); } - // add to notification timeline list, if any - if (this._notifTimelineSet) { - if (event.isNotification()) { - this._notifTimelineSet.addLiveEvent(event, duplicateStrategy); - } - } - // synthesize and inject implicit read receipts // Done after adding the event because otherwise the app would get a read receipt // pointing to an event that wasn't yet in the timeline @@ -648,8 +630,6 @@ Room.prototype.addPendingEvent = function(event, txnId) { timelineSet.getLiveTimeline(), false); } } - // notifications are receive-only, so we don't need to worry - // about this._notifTimelineSet. } this.emit("Room.localEchoUpdated", event, this, null, null); diff --git a/lib/sync.js b/lib/sync.js index 2b6385608..623680b53 100644 --- a/lib/sync.js +++ b/lib/sync.js @@ -655,6 +655,8 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) { } } + this.notifEvents = []; + // Handle invites inviteRooms.forEach(function(inviteObj) { var room = inviteObj.room; @@ -788,6 +790,16 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) { timelineEvents.forEach(function(e) { client.emit("event", e); }); accountDataEvents.forEach(function(e) { client.emit("event", e); }); }); + + // update the notification timeline, if appropriate + if (this.notifEvents.length) { + this.notifEvents.sort(function(a, b) { + return a.getTs() - b.getTs(); + }); + this.notifEvents.forEach(function(event) { + client.getNotifTimelineSet().addLiveEvent(event); + }); + } }; /** @@ -976,6 +988,16 @@ SyncApi.prototype._processRoomEvents = function(room, stateEventList, // may make notifications appear which should have the right name. room.recalculate(this.client.credentials.userId); + // gather our notifications into this.notifEvents + if (client.getNotifTimelineSet()) { + for (var i = 0; i < timelineEventList.length; i++) { + var pushActions = client.getPushActionsForEvent(timelineEventList[i]); + if (pushActions && pushActions.notify) { + this.notifEvents.push(timelineEventList[i]); + } + } + } + // execute the timeline events, this will begin to diverge the current state // if the timeline has any state events in it. room.addLiveEvents(timelineEventList); diff --git a/lib/timeline-window.js b/lib/timeline-window.js index b022419aa..9c6b28345 100644 --- a/lib/timeline-window.js +++ b/lib/timeline-window.js @@ -254,7 +254,12 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest, debuglog("TimelineWindow: starting request"); var self = this; - var prom = this._client.paginateEventTimeline(tl.timeline, { + + var paginateTimeline = tl.timeline.getRoomId() ? + this._client.paginateEventTimeline : + this._client.paginateNotifTimeline; + + var prom = paginateTimeline.call(this._client, tl.timeline, { backwards: direction == EventTimeline.BACKWARDS, limit: size }).finally(function() {