From b42db46abd501f34fec4f5341aade60e435e8711 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 29 Aug 2016 21:06:53 +0100 Subject: [PATCH] WIP refactor --- lib/models/event-timeline.js | 24 -- lib/models/room.js | 524 +++++------------------------------ 2 files changed, 66 insertions(+), 482 deletions(-) diff --git a/lib/models/event-timeline.js b/lib/models/event-timeline.js index f68bbe4d3..4123849ac 100644 --- a/lib/models/event-timeline.js +++ b/lib/models/event-timeline.js @@ -36,7 +36,6 @@ function EventTimeline(roomId) { this._startState.paginationToken = null; this._endState = new RoomState(roomId); this._endState.paginationToken = null; - this._filter = null; this._prevTimeline = null; this._nextTimeline = null; @@ -59,21 +58,6 @@ EventTimeline.BACKWARDS = "b"; */ EventTimeline.FORWARDS = "f"; -/** - * Get the filter object this timeline is filtered on - */ -EventTimeline.prototype.getFilter = function() { - return this._filter; -} - -/** - * Set the filter object this timeline is filtered on - * (passed to the server when paginating via /messages). - */ -EventTimeline.prototype.setFilter = function(filter) { - this._filter = filter; -} - /** * Initialise the start and end state with the given events * @@ -233,14 +217,6 @@ EventTimeline.prototype.setNeighbouringTimeline = function(neighbour, direction) EventTimeline.prototype.addEvent = function(event, atStart) { var stateContext = atStart ? this._startState : this._endState; - // manually filter the event if we have a filter, as currently we insert - // events incrementally only from the main /sync rather than a filtered - // /sync to avoid running multiple redundant /syncs. - if (this._filter) { - var events = this._filter.filterRoomTimeline([event]); - if (!events) return; - } - setEventMetadata(event, stateContext, atStart); // modify state diff --git a/lib/models/room.js b/lib/models/room.js index 166a5762e..27e3aa5d3 100644 --- a/lib/models/room.js +++ b/lib/models/room.js @@ -25,6 +25,7 @@ var MatrixEvent = require("./event").MatrixEvent; var utils = require("../utils"); var ContentRepo = require("../content-repo"); var EventTimeline = require("./event-timeline"); +var EventTimelineList = require("./event-timeline-list"); // var DEBUG = false; @@ -159,13 +160,17 @@ function Room(roomId, opts) { this._notificationCounts = {}; - this._liveTimeline = new EventTimeline(this.roomId); - this._fixUpLegacyTimelineFields(); + // all our per-room timeline lists. the first one is the unfiltered ones; + // the subsequent ones are the filtered ones in no particular order. + this._timelineLists = [ new EventTimelineList(roomId, opts) ]; - // just a list - *not* ordered. - this._timelines = [this._liveTimeline]; - this._eventIdToTimeline = {}; - this._timelineSupport = Boolean(opts.timelineSupport); + // any filtered timeline lists we're maintaining for this room + this._filteredTimelineLists = { + // filter_id: timelineList + }; + + // a reference to our shared notification timeline list + this._notifTimelineList = opts.notifTimelineList; if (this._opts.pendingEventOrdering == "detached") { this._pendingEventList = []; @@ -192,101 +197,6 @@ Room.prototype.getPendingEvents = function() { }; -/** - * Get the live timeline for this room. - * - * @return {module:models/event-timeline~EventTimeline} live timeline - */ -Room.prototype.getLiveTimeline = function() { - return this._liveTimeline; -}; - -/** - * Reset the live timeline, and start a new one. - * - *

This is used when /sync returns a 'limited' timeline. - * - * @param {string=} backPaginationToken token for back-paginating the new timeline - * - * @fires module:client~MatrixClient#event:"Room.timelineReset" - */ -Room.prototype.resetLiveTimeline = function(backPaginationToken) { - var newTimeline; - - if (!this._timelineSupport) { - // if timeline support is disabled, forget about the old timelines - newTimeline = new EventTimeline(this.roomId); - this._timelines = [newTimeline]; - this._eventIdToTimeline = {}; - } else { - newTimeline = this.addTimeline(); - } - - // initialise the state in the new timeline from our last known state - var evMap = this._liveTimeline.getState(EventTimeline.FORWARDS).events; - var events = []; - for (var evtype in evMap) { - if (!evMap.hasOwnProperty(evtype)) { continue; } - for (var stateKey in evMap[evtype]) { - if (!evMap[evtype].hasOwnProperty(stateKey)) { continue; } - events.push(evMap[evtype][stateKey]); - } - } - newTimeline.initialiseState(events); - - // make sure we set the pagination token before firing timelineReset, - // otherwise clients which start back-paginating will fail, and then get - // stuck without realising that they *can* back-paginate. - newTimeline.setPaginationToken(backPaginationToken, EventTimeline.BACKWARDS); - - this._liveTimeline = newTimeline; - this._fixUpLegacyTimelineFields(); - this.emit("Room.timelineReset", this); -}; - -/** - * Fix up this.timeline, this.oldState and this.currentState - * - * @private - */ -Room.prototype._fixUpLegacyTimelineFields = function() { - // maintain this.timeline as a reference to the live timeline, - // and this.oldState and this.currentState as references to the - // state at the start and end of that timeline. These are more - // for backwards-compatibility than anything else. - this.timeline = this._liveTimeline.getEvents(); - this.oldState = this._liveTimeline.getState(EventTimeline.BACKWARDS); - this.currentState = this._liveTimeline.getState(EventTimeline.FORWARDS); -}; - -/** - * Get the timeline which contains the given event, if any - * - * @param {string} eventId event ID to look for - * @return {?module:models/event-timeline~EventTimeline} timeline containing - * the given event, or null if unknown - */ -Room.prototype.getTimelineForEvent = function(eventId) { - var res = this._eventIdToTimeline[eventId]; - return (res === undefined) ? null : res; -}; - -/** - * Get an event which is stored in our timelines - * - * @param {string} eventId event ID to look for - * @return {?module:models/event~MatrixEvent} the given event, or undefined if unknown - */ -Room.prototype.findEventById = function(eventId) { - var tl = this.getTimelineForEvent(eventId); - if (!tl) { - return undefined; - } - return utils.findElement(tl.getEvents(), - function(ev) { return ev.getId() == eventId; }); -}; - - /** * Get one of the notification counts for this room * @param {String} type The type of notification count to get. default: 'total' @@ -438,224 +348,33 @@ Room.prototype.getCanonicalAlias = function() { }; /** - * Add a new timeline to this room - * - * @return {module:models/event-timeline~EventTimeline} newly-created timeline + * Add a timelineList for this room with the given filter */ -Room.prototype.addTimeline = function() { - if (!this._timelineSupport) { - throw new Error("timeline support is disabled. Set the 'timelineSupport'" + - " parameter to true when creating MatrixClient to enable" + - " it."); - } - - var timeline = new EventTimeline(this.roomId); - this._timelines.push(timeline); - return timeline; +Room.prototype.addFilteredTimelineList(filter) { + var timelineList = new EventTimelineList( + this.roomId, { + filter: filter, + } + ); + this._filteredTimelineLists[filter.filterId] = timelineList; + this._timelineLists.push(timelineList); }; - /** - * Add events to a timeline - * - *

Will fire "Room.timeline" for each event added. - * - * @param {MatrixEvent[]} events A list of events to add. - * - * @param {boolean} toStartOfTimeline True to add these events to the start - * (oldest) instead of the end (newest) of the timeline. If true, the oldest - * event will be the last element of 'events'. - * - * @param {module:models/event-timeline~EventTimeline} timeline timeline to - * add events to. - * - * @param {string=} paginationToken token for the next batch of events - * - * @fires module:client~MatrixClient#event:"Room.timeline" - * + * Forget the timelineList for this room with the given filter */ -Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline, - timeline, paginationToken) { - if (!timeline) { - throw new Error( - "'timeline' not specified for Room.addEventsToTimeline" - ); - } - - if (!toStartOfTimeline && timeline == this._liveTimeline) { - throw new Error( - "Room.addEventsToTimeline cannot be used for adding events to " + - "the live timeline - use Room.addLiveEvents instead" - ); - } - - var direction = toStartOfTimeline ? EventTimeline.BACKWARDS : - EventTimeline.FORWARDS; - var inverseDirection = toStartOfTimeline ? EventTimeline.FORWARDS : - EventTimeline.BACKWARDS; - - // Adding events to timelines can be quite complicated. The following - // illustrates some of the corner-cases. - // - // Let's say we start by knowing about four timelines. timeline3 and - // timeline4 are neighbours: - // - // timeline1 timeline2 timeline3 timeline4 - // [M] [P] [S] <------> [T] - // - // Now we paginate timeline1, and get the following events from the server: - // [M, N, P, R, S, T, U]. - // - // 1. First, we ignore event M, since we already know about it. - // - // 2. Next, we append N to timeline 1. - // - // 3. Next, we don't add event P, since we already know about it, - // but we do link together the timelines. We now have: - // - // timeline1 timeline2 timeline3 timeline4 - // [M, N] <---> [P] [S] <------> [T] - // - // 4. Now we add event R to timeline2: - // - // timeline1 timeline2 timeline3 timeline4 - // [M, N] <---> [P, R] [S] <------> [T] - // - // Note that we have switched the timeline we are working on from - // timeline1 to timeline2. - // - // 5. We ignore event S, but again join the timelines: - // - // timeline1 timeline2 timeline3 timeline4 - // [M, N] <---> [P, R] <---> [S] <------> [T] - // - // 6. We ignore event T, and the timelines are already joined, so there - // is nothing to do. - // - // 7. Finally, we add event U to timeline4: - // - // timeline1 timeline2 timeline3 timeline4 - // [M, N] <---> [P, R] <---> [S] <------> [T, U] - // - // The important thing to note in the above is what happened when we - // already knew about a given event: - // - // - if it was appropriate, we joined up the timelines (steps 3, 5). - // - in any case, we started adding further events to the timeline which - // contained the event we knew about (steps 3, 5, 6). - // - // - // So much for adding events to the timeline. But what do we want to do - // with the pagination token? - // - // In the case above, we will be given a pagination token which tells us how to - // get events beyond 'U' - in this case, it makes sense to store this - // against timeline4. But what if timeline4 already had 'U' and beyond? in - // that case, our best bet is to throw away the pagination token we were - // given and stick with whatever token timeline4 had previously. In short, - // we want to only store the pagination token if the last event we receive - // is one we didn't previously know about. - // - // We make an exception for this if it turns out that we already knew about - // *all* of the events, and we weren't able to join up any timelines. When - // that happens, it means our existing pagination token is faulty, since it - // is only telling us what we already know. Rather than repeatedly - // paginating with the same token, we might as well use the new pagination - // token in the hope that we eventually work our way out of the mess. - - var didUpdate = false; - var lastEventWasNew = false; - for (var i = 0; i < events.length; i++) { - var event = events[i]; - var eventId = event.getId(); - - var existingTimeline = this._eventIdToTimeline[eventId]; - - if (!existingTimeline) { - // we don't know about this event yet. Just add it to the timeline. - this._addEventToTimeline(event, timeline, toStartOfTimeline); - lastEventWasNew = true; - didUpdate = true; - continue; - } - - lastEventWasNew = false; - - if (existingTimeline == timeline) { - debuglog("Event " + eventId + " already in timeline " + timeline); - continue; - } - - var neighbour = timeline.getNeighbouringTimeline(direction); - if (neighbour) { - // this timeline already has a neighbour in the relevant direction; - // let's assume the timelines are already correctly linked up, and - // skip over to it. - // - // there's probably some edge-case here where we end up with an - // event which is in a timeline a way down the chain, and there is - // a break in the chain somewhere. But I can't really imagine how - // that would happen, so I'm going to ignore it for now. - // - if (existingTimeline == neighbour) { - debuglog("Event " + eventId + " in neighbouring timeline - " + - "switching to " + existingTimeline); - } else { - debuglog("Event " + eventId + " already in a different " + - "timeline " + existingTimeline); - } - timeline = existingTimeline; - continue; - } - - // time to join the timelines. - console.info("Already have timeline for " + eventId + - " - joining timeline " + timeline + " to " + - existingTimeline); - timeline.setNeighbouringTimeline(existingTimeline, direction); - existingTimeline.setNeighbouringTimeline(timeline, inverseDirection); - timeline = existingTimeline; - didUpdate = true; - } - - // see above - if the last event was new to us, or if we didn't find any - // new information, we update the pagination token for whatever - // timeline we ended up on. - if (lastEventWasNew || !didUpdate) { - timeline.setPaginationToken(paginationToken, direction); +Room.prototype.removeFilteredTimelineList(filter) { + var timelineList = this._filteredTimelineLists[filter.filterId]; + delete this._filteredTimelineLists[filter.filterId]; + var i = this._timelineLists.indexOf(timelineList); + if (i > -1) { + this._timelineList.splice(i, 1); } }; /** - * Add event to the given timeline, and emit Room.timeline. Assumes - * we have already checked we don't know about this event. - * - * Will fire "Room.timeline" for each event added. - * - * @param {MatrixEvent} event - * @param {EventTimeline} timeline - * @param {boolean} toStartOfTimeline - * - * @fires module:client~MatrixClient#event:"Room.timeline" - * - * @private - */ -Room.prototype._addEventToTimeline = function(event, timeline, toStartOfTimeline) { - var eventId = event.getId(); - timeline.addEvent(event, toStartOfTimeline); - this._eventIdToTimeline[eventId] = timeline; - - var data = { - timeline: timeline, - liveEvent: !toStartOfTimeline && timeline == this._liveTimeline, - }; - this.emit("Room.timeline", event, this, Boolean(toStartOfTimeline), false, data); -}; - - -/** - * Add an event to the end of this room's live timeline. Will fire - * "Room.timeline".. + * Add an event to the end of this room's live timelines. Will fire + * "Room.timeline". * * @param {MatrixEvent} event Event to be added * @param {string?} duplicateStrategy 'ignore' or 'replace' @@ -693,38 +412,17 @@ Room.prototype._addLiveEvent = function(event, duplicateStrategy) { } } - var timeline = this._eventIdToTimeline[event.getId()]; - if (timeline) { - if (duplicateStrategy === "replace") { - debuglog("Room._addLiveEvent: replacing duplicate event " + - event.getId()); - var tlEvents = timeline.getEvents(); - for (var j = 0; j < tlEvents.length; j++) { - if (tlEvents[j].getId() === event.getId()) { - // still need to set the right metadata on this event - setEventMetadata( - event, - timeline.getState(EventTimeline.FORWARDS), - false - ); - - if (!tlEvents[j].encryptedType) { - tlEvents[j] = event; - } - - // XXX: we need to fire an event when this happens. - break; - } - } - } else { - debuglog("Room._addLiveEvent: ignoring duplicate event " + - event.getId()); - } - return; + // add to our timeline lists + for (var i = 0; i < this._timelineLists.length; i++) { + this._timelineLists[i].addLiveEvent(event, duplicateStrategy); } - // TODO: pass through filter to see if this should be added to the timeline. - this._addEventToTimeline(event, this._liveTimeline, false); + // add to notification timeline list, if any + if (this._notifTimelineList) { + if (event.isNotification()) { + this._notifTimelineList.addLiveEvent(event, duplicateStrategy); + } + } // synthesize and inject implicit read receipts // Done after adding the event because otherwise the app would get a read receipt @@ -773,9 +471,11 @@ Room.prototype.addPendingEvent = function(event, txnId) { } // call setEventMetadata to set up event.sender etc - setEventMetadata( + // as event is shared over all timelinelists, we set up its metadata based + // on the unfiltered timelineList. + this._timelineLists[0].setEventMetadata( event, - this._liveTimeline.getState(EventTimeline.FORWARDS), + this._timelineLists[0].getLiveTimeline().getState(EventTimeline.FORWARDS), false ); @@ -784,7 +484,10 @@ Room.prototype.addPendingEvent = function(event, txnId) { if (this._opts.pendingEventOrdering == "detached") { this._pendingEventList.push(event); } else { - this._addEventToTimeline(event, this._liveTimeline, false); + for (var i = 0; i < this._timelineLists.length; i++) { + this._timelineLists[i].addEventToTimeline(event, this._timelineLists[i].getLiveTimeline(), false); + } + // notifications are receive-only, so we don't need to worry about this._notifTimelineList. } this.emit("Room.localEchoUpdated", event, this, null, null); @@ -828,13 +531,17 @@ Room.prototype._handleRemoteEcho = function(remoteEvent, localEvent) { // successfully sent. localEvent.status = null; - // if it's already in the timeline, update the timeline map. If it's not, add it. - var existingTimeline = this._eventIdToTimeline[oldEventId]; - if (existingTimeline) { - delete this._eventIdToTimeline[oldEventId]; - this._eventIdToTimeline[newEventId] = existingTimeline; - } else { - this._addEventToTimeline(localEvent, this._liveTimeline, false); + for (var i = 0; i < this._timelineLists.length; i++) { + var timelineList = this._timelineLists[i]; + + // if it's already in the timeline, update the timeline map. If it's not, add it. + var existingTimeline = timelineList._eventIdToTimeline[oldEventId]; + if (existingTimeline) { + delete timelineList._eventIdToTimeline[oldEventId]; + timelineList._eventIdToTimeline[newEventId] = existingTimeline; + } else { + timelineList._addEventToTimeline(localEvent, timelineList._liveTimeline, false); + } } this.emit("Room.localEchoUpdated", localEvent, this, @@ -1013,94 +720,16 @@ Room.prototype.removeEvents = function(event_ids) { * in this room. */ Room.prototype.removeEvent = function(eventId) { - var timeline = this._eventIdToTimeline[eventId]; - if (!timeline) { - return null; + var removedAny; + for (var i = 0; i < this._timelineLists.length; i++) { + var removed = this._timelineLists[i].removeEvent(eventId); + if (removed) { + removedAny = true; + } } - - var removed = timeline.removeEvent(eventId); - if (removed) { - delete this._eventIdToTimeline[eventId]; - var data = { - timeline: timeline, - }; - this.emit("Room.timeline", removed, this, undefined, true, data); - } - return removed; + return removedAny; }; -/** - * Determine where two events appear in the timeline relative to one another - * - * @param {string} eventId1 The id of the first event - * @param {string} eventId2 The id of the second event - - * @return {?number} a number less than zero if eventId1 precedes eventId2, and - * greater than zero if eventId1 succeeds eventId2. zero if they are the - * same event; null if we can't tell (either because we don't know about one - * of the events, or because they are in separate timelines which don't join - * up). - */ -Room.prototype.compareEventOrdering = function(eventId1, eventId2) { - if (eventId1 == eventId2) { - // optimise this case - return 0; - } - - var timeline1 = this._eventIdToTimeline[eventId1]; - var timeline2 = this._eventIdToTimeline[eventId2]; - - if (timeline1 === undefined) { - return null; - } - if (timeline2 === undefined) { - return null; - } - - if (timeline1 === timeline2) { - // both events are in the same timeline - figure out their - // relative indices - var idx1, idx2; - var events = timeline1.getEvents(); - for (var idx = 0; idx < events.length && - (idx1 === undefined || idx2 === undefined); idx++) { - var evId = events[idx].getId(); - if (evId == eventId1) { - idx1 = idx; - } - if (evId == eventId2) { - idx2 = idx; - } - } - return idx1 - idx2; - } - - // the events are in different timelines. Iterate through the - // linkedlist to see which comes first. - - // first work forwards from timeline1 - var tl = timeline1; - while (tl) { - if (tl === timeline2) { - // timeline1 is before timeline2 - return -1; - } - tl = tl.getNeighbouringTimeline(EventTimeline.FORWARDS); - } - - // now try backwards from timeline1 - tl = timeline1; - while (tl) { - if (tl === timeline2) { - // timeline2 is before timeline1 - return 1; - } - tl = tl.getNeighbouringTimeline(EventTimeline.BACKWARDS); - } - - // the timelines are not contiguous. - return null; -}; /** * Recalculate various aspects of the room, including the room name and @@ -1221,7 +850,7 @@ Room.prototype.addReceipt = function(event, fake) { // as there's nothing that would read it. } this._addReceiptsToStructure(event, this._receipts); - this._receiptCacheByEventId = this._buildReciptCache(this._receipts); + this._receiptCacheByEventId = this._buildReceiptCache(this._receipts); // send events after we've regenerated the cache, otherwise things that // listened for the event would read from a stale cache @@ -1275,7 +904,7 @@ Room.prototype._addReceiptsToStructure = function(event, receipts) { * @param {Object} receipts A map of receipts * @return {Object} Map of receipts by event ID */ -Room.prototype._buildReciptCache = function(receipts) { +Room.prototype._buildReceiptCache = function(receipts) { var receiptCacheByEventId = {}; utils.keys(receipts).forEach(function(receiptType) { utils.keys(receipts[receiptType]).forEach(function(userId) { @@ -1350,27 +979,6 @@ Room.prototype.getAccountData = function(type) { return this.accountData[type]; }; -function setEventMetadata(event, stateContext, toStartOfTimeline) { - // set sender and target properties - event.sender = stateContext.getSentinelMember( - event.getSender() - ); - if (event.getType() === "m.room.member") { - event.target = stateContext.getSentinelMember( - event.getStateKey() - ); - } - if (event.isState()) { - // room state has no concept of 'old' or 'current', but we want the - // room state to regress back to previous values if toStartOfTimeline - // is set, which means inspecting prev_content if it exists. This - // is done by toggling the forwardLooking flag. - if (toStartOfTimeline) { - event.forwardLooking = false; - } - } -} - /** * This is an internal method. Calculates the name of the room from the current * room state.