You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-26 17:03:12 +03:00
Convert EventTimeline, EventTimelineSet and TimelineWindow to TS
This commit is contained in:
@@ -4502,7 +4502,7 @@ export class MatrixClient extends EventEmitter {
|
|||||||
return Promise.resolve(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pendingRequest = eventTimeline._paginationRequests[dir];
|
const pendingRequest = eventTimeline.paginationRequests[dir];
|
||||||
|
|
||||||
if (pendingRequest) {
|
if (pendingRequest) {
|
||||||
// already a request in progress - return the existing promise
|
// already a request in progress - return the existing promise
|
||||||
@@ -4551,9 +4551,9 @@ export class MatrixClient extends EventEmitter {
|
|||||||
}
|
}
|
||||||
return res.next_token ? true : false;
|
return res.next_token ? true : false;
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
eventTimeline._paginationRequests[dir] = null;
|
eventTimeline.paginationRequests[dir] = null;
|
||||||
});
|
});
|
||||||
eventTimeline._paginationRequests[dir] = promise;
|
eventTimeline.paginationRequests[dir] = promise;
|
||||||
} else {
|
} else {
|
||||||
const room = this.getRoom(eventTimeline.getRoomId());
|
const room = this.getRoom(eventTimeline.getRoomId());
|
||||||
if (!room) {
|
if (!room) {
|
||||||
@@ -4585,9 +4585,9 @@ export class MatrixClient extends EventEmitter {
|
|||||||
}
|
}
|
||||||
return res.end != res.start;
|
return res.end != res.start;
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
eventTimeline._paginationRequests[dir] = null;
|
eventTimeline.paginationRequests[dir] = null;
|
||||||
});
|
});
|
||||||
eventTimeline._paginationRequests[dir] = promise;
|
eventTimeline.paginationRequests[dir] = promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
return promise;
|
return promise;
|
||||||
|
|||||||
@@ -292,7 +292,7 @@ export class CrossSigningInfo extends EventEmitter {
|
|||||||
CrossSigningLevel.USER_SIGNING |
|
CrossSigningLevel.USER_SIGNING |
|
||||||
CrossSigningLevel.SELF_SIGNING
|
CrossSigningLevel.SELF_SIGNING
|
||||||
);
|
);
|
||||||
} else if (level === 0) {
|
} else if (level === 0 as CrossSigningLevel) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,11 +15,7 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { MatrixEvent } from "./event";
|
import { MatrixEvent } from "./event";
|
||||||
|
import { Direction } from "./event-timeline";
|
||||||
enum Direction {
|
|
||||||
Backward = "b",
|
|
||||||
Forward = "f",
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @module models/event-context
|
* @module models/event-context
|
||||||
|
|||||||
@@ -1,848 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2016 OpenMarket Ltd
|
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @module models/event-timeline-set
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { EventEmitter } from "events";
|
|
||||||
import { EventTimeline } from "./event-timeline";
|
|
||||||
import { EventStatus } from "./event";
|
|
||||||
import * as utils from "../utils";
|
|
||||||
import { logger } from '../logger';
|
|
||||||
import { Relations } from './relations';
|
|
||||||
|
|
||||||
// var DEBUG = false;
|
|
||||||
const DEBUG = true;
|
|
||||||
|
|
||||||
let debuglog;
|
|
||||||
if (DEBUG) {
|
|
||||||
// using bind means that we get to keep useful line numbers in the console
|
|
||||||
debuglog = logger.log.bind(logger);
|
|
||||||
} else {
|
|
||||||
debuglog = function() {};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Construct a set of EventTimeline objects, typically on behalf of a given
|
|
||||||
* room. A room may have multiple EventTimelineSets for different levels
|
|
||||||
* of filtering. The global notification list is also an EventTimelineSet, but
|
|
||||||
* lacks a room.
|
|
||||||
*
|
|
||||||
* <p>This is an ordered sequence of timelines, which may or may not
|
|
||||||
* be continuous. Each timeline lists a series of events, as well as tracking
|
|
||||||
* the room state at the start and the end of the timeline (if appropriate).
|
|
||||||
* It also tracks forward and backward pagination tokens, as well as containing
|
|
||||||
* links to the next timeline in the sequence.
|
|
||||||
*
|
|
||||||
* <p>There is one special timeline - the 'live' timeline, which represents the
|
|
||||||
* timeline to which events are being added in real-time as they are received
|
|
||||||
* from the /sync API. Note that you should not retain references to this
|
|
||||||
* timeline - even if it is the current timeline right now, it may not remain
|
|
||||||
* so if the server gives us a timeline gap in /sync.
|
|
||||||
*
|
|
||||||
* <p>In order that we can find events from their ids later, we also maintain a
|
|
||||||
* map from event_id to timeline and index.
|
|
||||||
*
|
|
||||||
* @constructor
|
|
||||||
* @param {?Room} room
|
|
||||||
* Room for this timelineSet. May be null for non-room cases, such as the
|
|
||||||
* notification timeline.
|
|
||||||
* @param {Object} opts Options inherited from Room.
|
|
||||||
*
|
|
||||||
* @param {boolean} [opts.timelineSupport = false]
|
|
||||||
* Set to true to enable improved timeline support.
|
|
||||||
* @param {Object} [opts.filter = null]
|
|
||||||
* The filter object, if any, for this timelineSet.
|
|
||||||
* @param {boolean} [opts.unstableClientRelationAggregation = false]
|
|
||||||
* Optional. Set to true to enable client-side aggregation of event relations
|
|
||||||
* via `getRelationsForEvent`.
|
|
||||||
* This feature is currently unstable and the API may change without notice.
|
|
||||||
*/
|
|
||||||
export function EventTimelineSet(room, opts) {
|
|
||||||
this.room = room;
|
|
||||||
|
|
||||||
this._timelineSupport = Boolean(opts.timelineSupport);
|
|
||||||
this._liveTimeline = new EventTimeline(this);
|
|
||||||
this._unstableClientRelationAggregation = !!opts.unstableClientRelationAggregation;
|
|
||||||
|
|
||||||
// just a list - *not* ordered.
|
|
||||||
this._timelines = [this._liveTimeline];
|
|
||||||
this._eventIdToTimeline = {};
|
|
||||||
|
|
||||||
this._filter = opts.filter || null;
|
|
||||||
|
|
||||||
if (this._unstableClientRelationAggregation) {
|
|
||||||
// A tree of objects to access a set of relations for an event, as in:
|
|
||||||
// this._relations[relatesToEventId][relationType][relationEventType]
|
|
||||||
this._relations = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
utils.inherits(EventTimelineSet, EventEmitter);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all the timelines in this set
|
|
||||||
* @return {module:models/event-timeline~EventTimeline[]} the timelines in this set
|
|
||||||
*/
|
|
||||||
EventTimelineSet.prototype.getTimelines = function() {
|
|
||||||
return this._timelines;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Get the filter object this timeline set is filtered on, if any
|
|
||||||
* @return {?Filter} the optional filter for this timelineSet
|
|
||||||
*/
|
|
||||||
EventTimelineSet.prototype.getFilter = function() {
|
|
||||||
return this._filter;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the filter object this timeline set is filtered on
|
|
||||||
* (passed to the server when paginating via /messages).
|
|
||||||
* @param {Filter} filter the filter for this timelineSet
|
|
||||||
*/
|
|
||||||
EventTimelineSet.prototype.setFilter = function(filter) {
|
|
||||||
this._filter = filter;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the list of pending sent events for this timelineSet's room, filtered
|
|
||||||
* by the timelineSet's filter if appropriate.
|
|
||||||
*
|
|
||||||
* @return {module:models/event.MatrixEvent[]} A list of the sent events
|
|
||||||
* waiting for remote echo.
|
|
||||||
*
|
|
||||||
* @throws If <code>opts.pendingEventOrdering</code> was not 'detached'
|
|
||||||
*/
|
|
||||||
EventTimelineSet.prototype.getPendingEvents = function() {
|
|
||||||
if (!this.room) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._filter) {
|
|
||||||
return this._filter.filterRoomTimeline(this.room.getPendingEvents());
|
|
||||||
} else {
|
|
||||||
return this.room.getPendingEvents();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the live timeline for this room.
|
|
||||||
*
|
|
||||||
* @return {module:models/event-timeline~EventTimeline} live timeline
|
|
||||||
*/
|
|
||||||
EventTimelineSet.prototype.getLiveTimeline = function() {
|
|
||||||
return this._liveTimeline;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the timeline (if any) this event is in.
|
|
||||||
* @param {String} eventId the eventId being sought
|
|
||||||
* @return {module:models/event-timeline~EventTimeline} timeline
|
|
||||||
*/
|
|
||||||
EventTimelineSet.prototype.eventIdToTimeline = function(eventId) {
|
|
||||||
return this._eventIdToTimeline[eventId];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Track a new event as if it were in the same timeline as an old event,
|
|
||||||
* replacing it.
|
|
||||||
* @param {String} oldEventId event ID of the original event
|
|
||||||
* @param {String} newEventId event ID of the replacement event
|
|
||||||
*/
|
|
||||||
EventTimelineSet.prototype.replaceEventId = function(oldEventId, newEventId) {
|
|
||||||
const existingTimeline = this._eventIdToTimeline[oldEventId];
|
|
||||||
if (existingTimeline) {
|
|
||||||
delete this._eventIdToTimeline[oldEventId];
|
|
||||||
this._eventIdToTimeline[newEventId] = existingTimeline;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset the live timeline, and start a new one.
|
|
||||||
*
|
|
||||||
* <p>This is used when /sync returns a 'limited' timeline.
|
|
||||||
*
|
|
||||||
* @param {string=} backPaginationToken token for back-paginating the new timeline
|
|
||||||
* @param {string=} forwardPaginationToken token for forward-paginating the old live timeline,
|
|
||||||
* if absent or null, all timelines are reset.
|
|
||||||
*
|
|
||||||
* @fires module:client~MatrixClient#event:"Room.timelineReset"
|
|
||||||
*/
|
|
||||||
EventTimelineSet.prototype.resetLiveTimeline = function(
|
|
||||||
backPaginationToken, forwardPaginationToken,
|
|
||||||
) {
|
|
||||||
// Each EventTimeline has RoomState objects tracking the state at the start
|
|
||||||
// and end of that timeline. The copies at the end of the live timeline are
|
|
||||||
// special because they will have listeners attached to monitor changes to
|
|
||||||
// the current room state, so we move this RoomState from the end of the
|
|
||||||
// current live timeline to the end of the new one and, if necessary,
|
|
||||||
// replace it with a newly created one. We also make a copy for the start
|
|
||||||
// of the new timeline.
|
|
||||||
|
|
||||||
// if timeline support is disabled, forget about the old timelines
|
|
||||||
const resetAllTimelines = !this._timelineSupport || !forwardPaginationToken;
|
|
||||||
|
|
||||||
const oldTimeline = this._liveTimeline;
|
|
||||||
const newTimeline = resetAllTimelines ?
|
|
||||||
oldTimeline.forkLive(EventTimeline.FORWARDS) :
|
|
||||||
oldTimeline.fork(EventTimeline.FORWARDS);
|
|
||||||
|
|
||||||
if (resetAllTimelines) {
|
|
||||||
this._timelines = [newTimeline];
|
|
||||||
this._eventIdToTimeline = {};
|
|
||||||
} else {
|
|
||||||
this._timelines.push(newTimeline);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (forwardPaginationToken) {
|
|
||||||
// Now set the forward pagination token on the old live timeline
|
|
||||||
// so it can be forward-paginated.
|
|
||||||
oldTimeline.setPaginationToken(
|
|
||||||
forwardPaginationToken, EventTimeline.FORWARDS,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
// Now we can swap the live timeline to the new one.
|
|
||||||
this._liveTimeline = newTimeline;
|
|
||||||
this.emit("Room.timelineReset", this.room, this, resetAllTimelines);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
*/
|
|
||||||
EventTimelineSet.prototype.getTimelineForEvent = function(eventId) {
|
|
||||||
const 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
|
|
||||||
*/
|
|
||||||
EventTimelineSet.prototype.findEventById = function(eventId) {
|
|
||||||
const tl = this.getTimelineForEvent(eventId);
|
|
||||||
if (!tl) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return tl.getEvents().find(function(ev) {
|
|
||||||
return ev.getId() == eventId;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a new timeline to this timeline list
|
|
||||||
*
|
|
||||||
* @return {module:models/event-timeline~EventTimeline} newly-created timeline
|
|
||||||
*/
|
|
||||||
EventTimelineSet.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.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeline = new EventTimeline(this);
|
|
||||||
this._timelines.push(timeline);
|
|
||||||
return timeline;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add events to a timeline
|
|
||||||
*
|
|
||||||
* <p>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 <b>last</b> 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"
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
EventTimelineSet.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
|
|
||||||
timeline, paginationToken) {
|
|
||||||
if (!timeline) {
|
|
||||||
throw new Error(
|
|
||||||
"'timeline' not specified for EventTimelineSet.addEventsToTimeline",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!toStartOfTimeline && timeline == this._liveTimeline) {
|
|
||||||
throw new Error(
|
|
||||||
"EventTimelineSet.addEventsToTimeline cannot be used for adding events to " +
|
|
||||||
"the live timeline - use Room.addLiveEvents instead",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._filter) {
|
|
||||||
events = this._filter.filterRoomTimeline(events);
|
|
||||||
if (!events.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const direction = toStartOfTimeline ? EventTimeline.BACKWARDS :
|
|
||||||
EventTimeline.FORWARDS;
|
|
||||||
const 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.
|
|
||||||
|
|
||||||
let didUpdate = false;
|
|
||||||
let lastEventWasNew = false;
|
|
||||||
for (let i = 0; i < events.length; i++) {
|
|
||||||
const event = events[i];
|
|
||||||
const eventId = event.getId();
|
|
||||||
|
|
||||||
const 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const 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.
|
|
||||||
logger.info("Already have timeline for " + eventId +
|
|
||||||
" - joining timeline " + timeline + " to " +
|
|
||||||
existingTimeline);
|
|
||||||
|
|
||||||
// Variables to keep the line length limited below.
|
|
||||||
const existingIsLive = existingTimeline === this._liveTimeline;
|
|
||||||
const timelineIsLive = timeline === this._liveTimeline;
|
|
||||||
|
|
||||||
const backwardsIsLive = direction === EventTimeline.BACKWARDS && existingIsLive;
|
|
||||||
const forwardsIsLive = direction === EventTimeline.FORWARDS && timelineIsLive;
|
|
||||||
|
|
||||||
if (backwardsIsLive || forwardsIsLive) {
|
|
||||||
// The live timeline should never be spliced into a non-live position.
|
|
||||||
// We use independent logging to better discover the problem at a glance.
|
|
||||||
if (backwardsIsLive) {
|
|
||||||
logger.warn(
|
|
||||||
"Refusing to set a preceding existingTimeLine on our " +
|
|
||||||
"timeline as the existingTimeLine is live (" + existingTimeline + ")",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (forwardsIsLive) {
|
|
||||||
logger.warn(
|
|
||||||
"Refusing to set our preceding timeline on a existingTimeLine " +
|
|
||||||
"as our timeline is live (" + timeline + ")",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
continue; // abort splicing - try next event
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
if (direction === EventTimeline.FORWARDS && timeline === this._liveTimeline) {
|
|
||||||
logger.warn({ lastEventWasNew, didUpdate }); // for debugging
|
|
||||||
logger.warn(
|
|
||||||
`Refusing to set forwards pagination token of live timeline ` +
|
|
||||||
`${timeline} to ${paginationToken}`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
timeline.setPaginationToken(paginationToken, direction);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add an event to the end of this live timeline.
|
|
||||||
*
|
|
||||||
* @param {MatrixEvent} event Event to be added
|
|
||||||
* @param {string?} duplicateStrategy 'ignore' or 'replace'
|
|
||||||
* @param {boolean} fromCache whether the sync response came from cache
|
|
||||||
*/
|
|
||||||
EventTimelineSet.prototype.addLiveEvent = function(event, duplicateStrategy, fromCache) {
|
|
||||||
if (this._filter) {
|
|
||||||
const events = this._filter.filterRoomTimeline([event]);
|
|
||||||
if (!events.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeline = this._eventIdToTimeline[event.getId()];
|
|
||||||
if (timeline) {
|
|
||||||
if (duplicateStrategy === "replace") {
|
|
||||||
debuglog("EventTimelineSet.addLiveEvent: replacing duplicate event " +
|
|
||||||
event.getId());
|
|
||||||
const tlEvents = timeline.getEvents();
|
|
||||||
for (let j = 0; j < tlEvents.length; j++) {
|
|
||||||
if (tlEvents[j].getId() === event.getId()) {
|
|
||||||
// still need to set the right metadata on this event
|
|
||||||
EventTimeline.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("EventTimelineSet.addLiveEvent: ignoring duplicate event " +
|
|
||||||
event.getId());
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.addEventToTimeline(event, this._liveTimeline, false, fromCache);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
* @param {boolean} fromCache whether the sync response came from cache
|
|
||||||
*
|
|
||||||
* @fires module:client~MatrixClient#event:"Room.timeline"
|
|
||||||
*/
|
|
||||||
EventTimelineSet.prototype.addEventToTimeline = function(event, timeline,
|
|
||||||
toStartOfTimeline, fromCache) {
|
|
||||||
const eventId = event.getId();
|
|
||||||
timeline.addEvent(event, toStartOfTimeline);
|
|
||||||
this._eventIdToTimeline[eventId] = timeline;
|
|
||||||
|
|
||||||
this.setRelationsTarget(event);
|
|
||||||
this.aggregateRelations(event);
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
timeline: timeline,
|
|
||||||
liveEvent: !toStartOfTimeline && timeline == this._liveTimeline && !fromCache,
|
|
||||||
};
|
|
||||||
this.emit("Room.timeline", event, this.room,
|
|
||||||
Boolean(toStartOfTimeline), false, data);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replaces event with ID oldEventId with one with newEventId, if oldEventId is
|
|
||||||
* recognised. Otherwise, add to the live timeline. Used to handle remote echos.
|
|
||||||
*
|
|
||||||
* @param {MatrixEvent} localEvent the new event to be added to the timeline
|
|
||||||
* @param {String} oldEventId the ID of the original event
|
|
||||||
* @param {boolean} newEventId the ID of the replacement event
|
|
||||||
*
|
|
||||||
* @fires module:client~MatrixClient#event:"Room.timeline"
|
|
||||||
*/
|
|
||||||
EventTimelineSet.prototype.handleRemoteEcho = function(localEvent, oldEventId,
|
|
||||||
newEventId) {
|
|
||||||
// XXX: why don't we infer newEventId from localEvent?
|
|
||||||
const existingTimeline = this._eventIdToTimeline[oldEventId];
|
|
||||||
if (existingTimeline) {
|
|
||||||
delete this._eventIdToTimeline[oldEventId];
|
|
||||||
this._eventIdToTimeline[newEventId] = existingTimeline;
|
|
||||||
} else {
|
|
||||||
if (this._filter) {
|
|
||||||
if (this._filter.filterRoomTimeline([localEvent]).length) {
|
|
||||||
this.addEventToTimeline(localEvent, this._liveTimeline, false);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.addEventToTimeline(localEvent, this._liveTimeline, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a single event from this room.
|
|
||||||
*
|
|
||||||
* @param {String} eventId The id of the event to remove
|
|
||||||
*
|
|
||||||
* @return {?MatrixEvent} the removed event, or null if the event was not found
|
|
||||||
* in this room.
|
|
||||||
*/
|
|
||||||
EventTimelineSet.prototype.removeEvent = function(eventId) {
|
|
||||||
const timeline = this._eventIdToTimeline[eventId];
|
|
||||||
if (!timeline) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const removed = timeline.removeEvent(eventId);
|
|
||||||
if (removed) {
|
|
||||||
delete this._eventIdToTimeline[eventId];
|
|
||||||
const data = {
|
|
||||||
timeline: timeline,
|
|
||||||
};
|
|
||||||
this.emit("Room.timeline", removed, this.room, undefined, true, data);
|
|
||||||
}
|
|
||||||
return removed;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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).
|
|
||||||
*/
|
|
||||||
EventTimelineSet.prototype.compareEventOrdering = function(eventId1, eventId2) {
|
|
||||||
if (eventId1 == eventId2) {
|
|
||||||
// optimise this case
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeline1 = this._eventIdToTimeline[eventId1];
|
|
||||||
const 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
|
|
||||||
let idx1;
|
|
||||||
let idx2;
|
|
||||||
const events = timeline1.getEvents();
|
|
||||||
for (let idx = 0; idx < events.length &&
|
|
||||||
(idx1 === undefined || idx2 === undefined); idx++) {
|
|
||||||
const 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
|
|
||||||
let 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;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a collection of relations to a given event in this timeline set.
|
|
||||||
*
|
|
||||||
* @param {String} eventId
|
|
||||||
* The ID of the event that you'd like to access relation events for.
|
|
||||||
* For example, with annotations, this would be the ID of the event being annotated.
|
|
||||||
* @param {String} relationType
|
|
||||||
* The type of relation involved, such as "m.annotation", "m.reference", "m.replace", etc.
|
|
||||||
* @param {String} eventType
|
|
||||||
* The relation event's type, such as "m.reaction", etc.
|
|
||||||
* @throws If <code>eventId</code>, <code>relationType</code> or <code>eventType</code>
|
|
||||||
* are not valid.
|
|
||||||
*
|
|
||||||
* @returns {?Relations}
|
|
||||||
* A container for relation events or undefined if there are no relation events for
|
|
||||||
* the relationType.
|
|
||||||
*/
|
|
||||||
EventTimelineSet.prototype.getRelationsForEvent = function(
|
|
||||||
eventId, relationType, eventType,
|
|
||||||
) {
|
|
||||||
if (!this._unstableClientRelationAggregation) {
|
|
||||||
throw new Error("Client-side relation aggregation is disabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!eventId || !relationType || !eventType) {
|
|
||||||
throw new Error("Invalid arguments for `getRelationsForEvent`");
|
|
||||||
}
|
|
||||||
|
|
||||||
// debuglog("Getting relations for: ", eventId, relationType, eventType);
|
|
||||||
|
|
||||||
const relationsForEvent = this._relations[eventId] || {};
|
|
||||||
const relationsWithRelType = relationsForEvent[relationType] || {};
|
|
||||||
return relationsWithRelType[eventType];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set an event as the target event if any Relations exist for it already
|
|
||||||
*
|
|
||||||
* @param {MatrixEvent} event
|
|
||||||
* The event to check as relation target.
|
|
||||||
*/
|
|
||||||
EventTimelineSet.prototype.setRelationsTarget = function(event) {
|
|
||||||
if (!this._unstableClientRelationAggregation) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const relationsForEvent = this._relations[event.getId()];
|
|
||||||
if (!relationsForEvent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const relationsWithRelType of Object.values(relationsForEvent)) {
|
|
||||||
for (const relationsWithEventType of Object.values(relationsWithRelType)) {
|
|
||||||
relationsWithEventType.setTargetEvent(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add relation events to the relevant relation collection.
|
|
||||||
*
|
|
||||||
* @param {MatrixEvent} event
|
|
||||||
* The new relation event to be aggregated.
|
|
||||||
*/
|
|
||||||
EventTimelineSet.prototype.aggregateRelations = function(event) {
|
|
||||||
if (!this._unstableClientRelationAggregation) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.isRedacted() || event.status === EventStatus.CANCELLED) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the event is currently encrypted, wait until it has been decrypted.
|
|
||||||
if (event.isBeingDecrypted() || event.shouldAttemptDecryption()) {
|
|
||||||
event.once("Event.decrypted", () => {
|
|
||||||
this.aggregateRelations(event);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const relation = event.getRelation();
|
|
||||||
if (!relation) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const relatesToEventId = relation.event_id;
|
|
||||||
const relationType = relation.rel_type;
|
|
||||||
const eventType = event.getType();
|
|
||||||
|
|
||||||
// debuglog("Aggregating relation: ", event.getId(), eventType, relation);
|
|
||||||
|
|
||||||
let relationsForEvent = this._relations[relatesToEventId];
|
|
||||||
if (!relationsForEvent) {
|
|
||||||
relationsForEvent = this._relations[relatesToEventId] = {};
|
|
||||||
}
|
|
||||||
let relationsWithRelType = relationsForEvent[relationType];
|
|
||||||
if (!relationsWithRelType) {
|
|
||||||
relationsWithRelType = relationsForEvent[relationType] = {};
|
|
||||||
}
|
|
||||||
let relationsWithEventType = relationsWithRelType[eventType];
|
|
||||||
|
|
||||||
let relatesToEvent;
|
|
||||||
if (!relationsWithEventType) {
|
|
||||||
relationsWithEventType = relationsWithRelType[eventType] = new Relations(
|
|
||||||
relationType,
|
|
||||||
eventType,
|
|
||||||
this.room,
|
|
||||||
);
|
|
||||||
relatesToEvent = this.findEventById(relatesToEventId) || this.room.getPendingEvent(relatesToEventId);
|
|
||||||
if (relatesToEvent) {
|
|
||||||
relationsWithEventType.setTargetEvent(relatesToEvent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
relationsWithEventType.addEvent(event);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires whenever the timeline in a room is updated.
|
|
||||||
* @event module:client~MatrixClient#"Room.timeline"
|
|
||||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
|
||||||
* @param {?Room} room The room, if any, whose timeline was updated.
|
|
||||||
* @param {boolean} toStartOfTimeline True if this event was added to the start
|
|
||||||
* @param {boolean} removed True if this event has just been removed from the timeline
|
|
||||||
* (beginning; oldest) of the timeline e.g. due to pagination.
|
|
||||||
*
|
|
||||||
* @param {object} data more data about the event
|
|
||||||
*
|
|
||||||
* @param {module:models/event-timeline.EventTimeline} data.timeline the timeline the
|
|
||||||
* event was added to/removed from
|
|
||||||
*
|
|
||||||
* @param {boolean} data.liveEvent true if the event was a real-time event
|
|
||||||
* added to the end of the live timeline
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* matrixClient.on("Room.timeline",
|
|
||||||
* function(event, room, toStartOfTimeline, removed, data) {
|
|
||||||
* if (!toStartOfTimeline && data.liveEvent) {
|
|
||||||
* var messageToAppend = room.timeline.[room.timeline.length - 1];
|
|
||||||
* }
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires whenever the live timeline in a room is reset.
|
|
||||||
*
|
|
||||||
* When we get a 'limited' sync (for example, after a network outage), we reset
|
|
||||||
* the live timeline to be empty before adding the recent events to the new
|
|
||||||
* timeline. This event is fired after the timeline is reset, and before the
|
|
||||||
* new events are added.
|
|
||||||
*
|
|
||||||
* @event module:client~MatrixClient#"Room.timelineReset"
|
|
||||||
* @param {Room} room The room whose live timeline was reset, if any
|
|
||||||
* @param {EventTimelineSet} timelineSet timelineSet room whose live timeline was reset
|
|
||||||
* @param {boolean} resetAllTimelines True if all timelines were reset.
|
|
||||||
*/
|
|
||||||
874
src/models/event-timeline-set.ts
Normal file
874
src/models/event-timeline-set.ts
Normal file
@@ -0,0 +1,874 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @module models/event-timeline-set
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EventEmitter } from "events";
|
||||||
|
|
||||||
|
import { EventTimeline } from "./event-timeline";
|
||||||
|
import { EventStatus, MatrixEvent } from "./event";
|
||||||
|
import { logger } from '../logger';
|
||||||
|
import { Relations } from './relations';
|
||||||
|
import { Room } from "./room";
|
||||||
|
import { Filter } from "../filter";
|
||||||
|
import { EventType, RelationType } from "../@types/event";
|
||||||
|
|
||||||
|
// var DEBUG = false;
|
||||||
|
const DEBUG = true;
|
||||||
|
|
||||||
|
let debuglog;
|
||||||
|
if (DEBUG) {
|
||||||
|
// using bind means that we get to keep useful line numbers in the console
|
||||||
|
debuglog = logger.log.bind(logger);
|
||||||
|
} else {
|
||||||
|
debuglog = function() {};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IOpts {
|
||||||
|
timelineSupport?: boolean;
|
||||||
|
filter?: Filter;
|
||||||
|
unstableClientRelationAggregation?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EventTimelineSet extends EventEmitter {
|
||||||
|
private readonly timelineSupport: boolean;
|
||||||
|
private unstableClientRelationAggregation: boolean;
|
||||||
|
private liveTimeline: EventTimeline;
|
||||||
|
private timelines: EventTimeline[];
|
||||||
|
private _eventIdToTimeline: Record<string, EventTimeline>;
|
||||||
|
private filter?: Filter;
|
||||||
|
private relations: Record<string, Record<string, Record<RelationType, Relations>>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a set of EventTimeline objects, typically on behalf of a given
|
||||||
|
* room. A room may have multiple EventTimelineSets for different levels
|
||||||
|
* of filtering. The global notification list is also an EventTimelineSet, but
|
||||||
|
* lacks a room.
|
||||||
|
*
|
||||||
|
* <p>This is an ordered sequence of timelines, which may or may not
|
||||||
|
* be continuous. Each timeline lists a series of events, as well as tracking
|
||||||
|
* the room state at the start and the end of the timeline (if appropriate).
|
||||||
|
* It also tracks forward and backward pagination tokens, as well as containing
|
||||||
|
* links to the next timeline in the sequence.
|
||||||
|
*
|
||||||
|
* <p>There is one special timeline - the 'live' timeline, which represents the
|
||||||
|
* timeline to which events are being added in real-time as they are received
|
||||||
|
* from the /sync API. Note that you should not retain references to this
|
||||||
|
* timeline - even if it is the current timeline right now, it may not remain
|
||||||
|
* so if the server gives us a timeline gap in /sync.
|
||||||
|
*
|
||||||
|
* <p>In order that we can find events from their ids later, we also maintain a
|
||||||
|
* map from event_id to timeline and index.
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
* @param {?Room} room
|
||||||
|
* Room for this timelineSet. May be null for non-room cases, such as the
|
||||||
|
* notification timeline.
|
||||||
|
* @param {Object} opts Options inherited from Room.
|
||||||
|
*
|
||||||
|
* @param {boolean} [opts.timelineSupport = false]
|
||||||
|
* Set to true to enable improved timeline support.
|
||||||
|
* @param {Object} [opts.filter = null]
|
||||||
|
* The filter object, if any, for this timelineSet.
|
||||||
|
* @param {boolean} [opts.unstableClientRelationAggregation = false]
|
||||||
|
* Optional. Set to true to enable client-side aggregation of event relations
|
||||||
|
* via `getRelationsForEvent`.
|
||||||
|
* This feature is currently unstable and the API may change without notice.
|
||||||
|
*/
|
||||||
|
constructor(public readonly room: Room, opts: IOpts) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.timelineSupport = Boolean(opts.timelineSupport);
|
||||||
|
this.liveTimeline = new EventTimeline(this);
|
||||||
|
this.unstableClientRelationAggregation = !!opts.unstableClientRelationAggregation;
|
||||||
|
|
||||||
|
// just a list - *not* ordered.
|
||||||
|
this.timelines = [this.liveTimeline];
|
||||||
|
this._eventIdToTimeline = {};
|
||||||
|
|
||||||
|
this.filter = opts.filter;
|
||||||
|
|
||||||
|
if (this.unstableClientRelationAggregation) {
|
||||||
|
// A tree of objects to access a set of relations for an event, as in:
|
||||||
|
// this.relations[relatesToEventId][relationType][relationEventType]
|
||||||
|
this.relations = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all the timelines in this set
|
||||||
|
* @return {module:models/event-timeline~EventTimeline[]} the timelines in this set
|
||||||
|
*/
|
||||||
|
public getTimelines(): EventTimeline[] {
|
||||||
|
return this.timelines;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the filter object this timeline set is filtered on, if any
|
||||||
|
* @return {?Filter} the optional filter for this timelineSet
|
||||||
|
*/
|
||||||
|
public getFilter(): Filter | undefined {
|
||||||
|
return this.filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the filter object this timeline set is filtered on
|
||||||
|
* (passed to the server when paginating via /messages).
|
||||||
|
* @param {Filter} filter the filter for this timelineSet
|
||||||
|
*/
|
||||||
|
public setFilter(filter?: Filter): void {
|
||||||
|
this.filter = filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of pending sent events for this timelineSet's room, filtered
|
||||||
|
* by the timelineSet's filter if appropriate.
|
||||||
|
*
|
||||||
|
* @return {module:models/event.MatrixEvent[]} A list of the sent events
|
||||||
|
* waiting for remote echo.
|
||||||
|
*
|
||||||
|
* @throws If <code>opts.pendingEventOrdering</code> was not 'detached'
|
||||||
|
*/
|
||||||
|
public getPendingEvents(): MatrixEvent[] {
|
||||||
|
if (!this.room) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.filter) {
|
||||||
|
return this.filter.filterRoomTimeline(this.room.getPendingEvents());
|
||||||
|
} else {
|
||||||
|
return this.room.getPendingEvents();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the live timeline for this room.
|
||||||
|
*
|
||||||
|
* @return {module:models/event-timeline~EventTimeline} live timeline
|
||||||
|
*/
|
||||||
|
public getLiveTimeline(): EventTimeline {
|
||||||
|
return this.liveTimeline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the timeline (if any) this event is in.
|
||||||
|
* @param {String} eventId the eventId being sought
|
||||||
|
* @return {module:models/event-timeline~EventTimeline} timeline
|
||||||
|
*/
|
||||||
|
public eventIdToTimeline(eventId: string): EventTimeline {
|
||||||
|
return this._eventIdToTimeline[eventId];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track a new event as if it were in the same timeline as an old event,
|
||||||
|
* replacing it.
|
||||||
|
* @param {String} oldEventId event ID of the original event
|
||||||
|
* @param {String} newEventId event ID of the replacement event
|
||||||
|
*/
|
||||||
|
public replaceEventId(oldEventId: string, newEventId: string): void {
|
||||||
|
const existingTimeline = this._eventIdToTimeline[oldEventId];
|
||||||
|
if (existingTimeline) {
|
||||||
|
delete this._eventIdToTimeline[oldEventId];
|
||||||
|
this._eventIdToTimeline[newEventId] = existingTimeline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the live timeline, and start a new one.
|
||||||
|
*
|
||||||
|
* <p>This is used when /sync returns a 'limited' timeline.
|
||||||
|
*
|
||||||
|
* @param {string=} backPaginationToken token for back-paginating the new timeline
|
||||||
|
* @param {string=} forwardPaginationToken token for forward-paginating the old live timeline,
|
||||||
|
* if absent or null, all timelines are reset.
|
||||||
|
*
|
||||||
|
* @fires module:client~MatrixClient#event:"Room.timelineReset"
|
||||||
|
*/
|
||||||
|
public resetLiveTimeline(backPaginationToken: string, forwardPaginationToken?: string): void {
|
||||||
|
// Each EventTimeline has RoomState objects tracking the state at the start
|
||||||
|
// and end of that timeline. The copies at the end of the live timeline are
|
||||||
|
// special because they will have listeners attached to monitor changes to
|
||||||
|
// the current room state, so we move this RoomState from the end of the
|
||||||
|
// current live timeline to the end of the new one and, if necessary,
|
||||||
|
// replace it with a newly created one. We also make a copy for the start
|
||||||
|
// of the new timeline.
|
||||||
|
|
||||||
|
// if timeline support is disabled, forget about the old timelines
|
||||||
|
const resetAllTimelines = !this.timelineSupport || !forwardPaginationToken;
|
||||||
|
|
||||||
|
const oldTimeline = this.liveTimeline;
|
||||||
|
const newTimeline = resetAllTimelines ?
|
||||||
|
oldTimeline.forkLive(EventTimeline.FORWARDS) :
|
||||||
|
oldTimeline.fork(EventTimeline.FORWARDS);
|
||||||
|
|
||||||
|
if (resetAllTimelines) {
|
||||||
|
this.timelines = [newTimeline];
|
||||||
|
this._eventIdToTimeline = {};
|
||||||
|
} else {
|
||||||
|
this.timelines.push(newTimeline);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forwardPaginationToken) {
|
||||||
|
// Now set the forward pagination token on the old live timeline
|
||||||
|
// so it can be forward-paginated.
|
||||||
|
oldTimeline.setPaginationToken(
|
||||||
|
forwardPaginationToken, EventTimeline.FORWARDS,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// Now we can swap the live timeline to the new one.
|
||||||
|
this.liveTimeline = newTimeline;
|
||||||
|
this.emit("Room.timelineReset", this.room, this, resetAllTimelines);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
public getTimelineForEvent(eventId: string): EventTimeline | null {
|
||||||
|
const 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
|
||||||
|
*/
|
||||||
|
public findEventById(eventId: string): MatrixEvent | undefined {
|
||||||
|
const tl = this.getTimelineForEvent(eventId);
|
||||||
|
if (!tl) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return tl.getEvents().find(function(ev) {
|
||||||
|
return ev.getId() == eventId;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new timeline to this timeline list
|
||||||
|
*
|
||||||
|
* @return {module:models/event-timeline~EventTimeline} newly-created timeline
|
||||||
|
*/
|
||||||
|
public addTimeline(): EventTimeline {
|
||||||
|
if (!this.timelineSupport) {
|
||||||
|
throw new Error("timeline support is disabled. Set the 'timelineSupport'" +
|
||||||
|
" parameter to true when creating MatrixClient to enable" +
|
||||||
|
" it.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeline = new EventTimeline(this);
|
||||||
|
this.timelines.push(timeline);
|
||||||
|
return timeline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add events to a timeline
|
||||||
|
*
|
||||||
|
* <p>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 <b>last</b> 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"
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public addEventsToTimeline(
|
||||||
|
events: MatrixEvent[],
|
||||||
|
toStartOfTimeline: boolean,
|
||||||
|
timeline: EventTimeline,
|
||||||
|
paginationToken: string,
|
||||||
|
): void {
|
||||||
|
if (!timeline) {
|
||||||
|
throw new Error(
|
||||||
|
"'timeline' not specified for EventTimelineSet.addEventsToTimeline",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!toStartOfTimeline && timeline == this.liveTimeline) {
|
||||||
|
throw new Error(
|
||||||
|
"EventTimelineSet.addEventsToTimeline cannot be used for adding events to " +
|
||||||
|
"the live timeline - use Room.addLiveEvents instead",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.filter) {
|
||||||
|
events = this.filter.filterRoomTimeline(events);
|
||||||
|
if (!events.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const direction = toStartOfTimeline ? EventTimeline.BACKWARDS :
|
||||||
|
EventTimeline.FORWARDS;
|
||||||
|
const 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.
|
||||||
|
|
||||||
|
let didUpdate = false;
|
||||||
|
let lastEventWasNew = false;
|
||||||
|
for (let i = 0; i < events.length; i++) {
|
||||||
|
const event = events[i];
|
||||||
|
const eventId = event.getId();
|
||||||
|
|
||||||
|
const 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const 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.
|
||||||
|
logger.info("Already have timeline for " + eventId +
|
||||||
|
" - joining timeline " + timeline + " to " +
|
||||||
|
existingTimeline);
|
||||||
|
|
||||||
|
// Variables to keep the line length limited below.
|
||||||
|
const existingIsLive = existingTimeline === this.liveTimeline;
|
||||||
|
const timelineIsLive = timeline === this.liveTimeline;
|
||||||
|
|
||||||
|
const backwardsIsLive = direction === EventTimeline.BACKWARDS && existingIsLive;
|
||||||
|
const forwardsIsLive = direction === EventTimeline.FORWARDS && timelineIsLive;
|
||||||
|
|
||||||
|
if (backwardsIsLive || forwardsIsLive) {
|
||||||
|
// The live timeline should never be spliced into a non-live position.
|
||||||
|
// We use independent logging to better discover the problem at a glance.
|
||||||
|
if (backwardsIsLive) {
|
||||||
|
logger.warn(
|
||||||
|
"Refusing to set a preceding existingTimeLine on our " +
|
||||||
|
"timeline as the existingTimeLine is live (" + existingTimeline + ")",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (forwardsIsLive) {
|
||||||
|
logger.warn(
|
||||||
|
"Refusing to set our preceding timeline on a existingTimeLine " +
|
||||||
|
"as our timeline is live (" + timeline + ")",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
continue; // abort splicing - try next event
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
if (direction === EventTimeline.FORWARDS && timeline === this.liveTimeline) {
|
||||||
|
logger.warn({ lastEventWasNew, didUpdate }); // for debugging
|
||||||
|
logger.warn(
|
||||||
|
`Refusing to set forwards pagination token of live timeline ` +
|
||||||
|
`${timeline} to ${paginationToken}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
timeline.setPaginationToken(paginationToken, direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an event to the end of this live timeline.
|
||||||
|
*
|
||||||
|
* @param {MatrixEvent} event Event to be added
|
||||||
|
* @param {string?} duplicateStrategy 'ignore' or 'replace'
|
||||||
|
* @param {boolean} fromCache whether the sync response came from cache
|
||||||
|
*/
|
||||||
|
public addLiveEvent(event: MatrixEvent, duplicateStrategy?: "ignore" | "replace", fromCache = false): void {
|
||||||
|
if (this.filter) {
|
||||||
|
const events = this.filter.filterRoomTimeline([event]);
|
||||||
|
if (!events.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeline = this._eventIdToTimeline[event.getId()];
|
||||||
|
if (timeline) {
|
||||||
|
if (duplicateStrategy === "replace") {
|
||||||
|
debuglog("EventTimelineSet.addLiveEvent: replacing duplicate event " +
|
||||||
|
event.getId());
|
||||||
|
const tlEvents = timeline.getEvents();
|
||||||
|
for (let j = 0; j < tlEvents.length; j++) {
|
||||||
|
if (tlEvents[j].getId() === event.getId()) {
|
||||||
|
// still need to set the right metadata on this event
|
||||||
|
EventTimeline.setEventMetadata(
|
||||||
|
event,
|
||||||
|
timeline.getState(EventTimeline.FORWARDS),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
tlEvents[j] = event;
|
||||||
|
|
||||||
|
// XXX: we need to fire an event when this happens.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debuglog("EventTimelineSet.addLiveEvent: ignoring duplicate event " +
|
||||||
|
event.getId());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addEventToTimeline(event, this.liveTimeline, false, fromCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* @param {boolean} fromCache whether the sync response came from cache
|
||||||
|
*
|
||||||
|
* @fires module:client~MatrixClient#event:"Room.timeline"
|
||||||
|
*/
|
||||||
|
public addEventToTimeline(
|
||||||
|
event: MatrixEvent,
|
||||||
|
timeline: EventTimeline,
|
||||||
|
toStartOfTimeline: boolean,
|
||||||
|
fromCache = false,
|
||||||
|
) {
|
||||||
|
const eventId = event.getId();
|
||||||
|
timeline.addEvent(event, toStartOfTimeline);
|
||||||
|
this._eventIdToTimeline[eventId] = timeline;
|
||||||
|
|
||||||
|
this.setRelationsTarget(event);
|
||||||
|
this.aggregateRelations(event);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
timeline: timeline,
|
||||||
|
liveEvent: !toStartOfTimeline && timeline == this.liveTimeline && !fromCache,
|
||||||
|
};
|
||||||
|
this.emit("Room.timeline", event, this.room,
|
||||||
|
Boolean(toStartOfTimeline), false, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces event with ID oldEventId with one with newEventId, if oldEventId is
|
||||||
|
* recognised. Otherwise, add to the live timeline. Used to handle remote echos.
|
||||||
|
*
|
||||||
|
* @param {MatrixEvent} localEvent the new event to be added to the timeline
|
||||||
|
* @param {String} oldEventId the ID of the original event
|
||||||
|
* @param {boolean} newEventId the ID of the replacement event
|
||||||
|
*
|
||||||
|
* @fires module:client~MatrixClient#event:"Room.timeline"
|
||||||
|
*/
|
||||||
|
public handleRemoteEcho(
|
||||||
|
localEvent: MatrixEvent,
|
||||||
|
oldEventId: string,
|
||||||
|
newEventId: string,
|
||||||
|
): void {
|
||||||
|
// XXX: why don't we infer newEventId from localEvent?
|
||||||
|
const existingTimeline = this._eventIdToTimeline[oldEventId];
|
||||||
|
if (existingTimeline) {
|
||||||
|
delete this._eventIdToTimeline[oldEventId];
|
||||||
|
this._eventIdToTimeline[newEventId] = existingTimeline;
|
||||||
|
} else {
|
||||||
|
if (this.filter) {
|
||||||
|
if (this.filter.filterRoomTimeline([localEvent]).length) {
|
||||||
|
this.addEventToTimeline(localEvent, this.liveTimeline, false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.addEventToTimeline(localEvent, this.liveTimeline, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a single event from this room.
|
||||||
|
*
|
||||||
|
* @param {String} eventId The id of the event to remove
|
||||||
|
*
|
||||||
|
* @return {?MatrixEvent} the removed event, or null if the event was not found
|
||||||
|
* in this room.
|
||||||
|
*/
|
||||||
|
public removeEvent(eventId: string): MatrixEvent | null {
|
||||||
|
const timeline = this._eventIdToTimeline[eventId];
|
||||||
|
if (!timeline) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const removed = timeline.removeEvent(eventId);
|
||||||
|
if (removed) {
|
||||||
|
delete this._eventIdToTimeline[eventId];
|
||||||
|
const data = {
|
||||||
|
timeline: timeline,
|
||||||
|
};
|
||||||
|
this.emit("Room.timeline", removed, this.room, undefined, true, data);
|
||||||
|
}
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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).
|
||||||
|
*/
|
||||||
|
public compareEventOrdering(eventId1: string, eventId2: string): number | null {
|
||||||
|
if (eventId1 == eventId2) {
|
||||||
|
// optimise this case
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeline1 = this._eventIdToTimeline[eventId1];
|
||||||
|
const 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
|
||||||
|
let idx1;
|
||||||
|
let idx2;
|
||||||
|
const events = timeline1.getEvents();
|
||||||
|
for (let idx = 0; idx < events.length &&
|
||||||
|
(idx1 === undefined || idx2 === undefined); idx++) {
|
||||||
|
const 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
|
||||||
|
let 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a collection of relations to a given event in this timeline set.
|
||||||
|
*
|
||||||
|
* @param {String} eventId
|
||||||
|
* The ID of the event that you'd like to access relation events for.
|
||||||
|
* For example, with annotations, this would be the ID of the event being annotated.
|
||||||
|
* @param {String} relationType
|
||||||
|
* The type of relation involved, such as "m.annotation", "m.reference", "m.replace", etc.
|
||||||
|
* @param {String} eventType
|
||||||
|
* The relation event's type, such as "m.reaction", etc.
|
||||||
|
* @throws If <code>eventId</code>, <code>relationType</code> or <code>eventType</code>
|
||||||
|
* are not valid.
|
||||||
|
*
|
||||||
|
* @returns {?Relations}
|
||||||
|
* A container for relation events or undefined if there are no relation events for
|
||||||
|
* the relationType.
|
||||||
|
*/
|
||||||
|
public getRelationsForEvent(
|
||||||
|
eventId: string,
|
||||||
|
relationType: RelationType,
|
||||||
|
eventType: EventType | string,
|
||||||
|
): Relations | undefined {
|
||||||
|
if (!this.unstableClientRelationAggregation) {
|
||||||
|
throw new Error("Client-side relation aggregation is disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!eventId || !relationType || !eventType) {
|
||||||
|
throw new Error("Invalid arguments for `getRelationsForEvent`");
|
||||||
|
}
|
||||||
|
|
||||||
|
// debuglog("Getting relations for: ", eventId, relationType, eventType);
|
||||||
|
|
||||||
|
const relationsForEvent = this.relations[eventId] || {};
|
||||||
|
const relationsWithRelType = relationsForEvent[relationType] || {};
|
||||||
|
return relationsWithRelType[eventType];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set an event as the target event if any Relations exist for it already
|
||||||
|
*
|
||||||
|
* @param {MatrixEvent} event
|
||||||
|
* The event to check as relation target.
|
||||||
|
*/
|
||||||
|
public setRelationsTarget(event: MatrixEvent): void {
|
||||||
|
if (!this.unstableClientRelationAggregation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const relationsForEvent = this.relations[event.getId()];
|
||||||
|
if (!relationsForEvent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const relationsWithRelType of Object.values(relationsForEvent)) {
|
||||||
|
for (const relationsWithEventType of Object.values(relationsWithRelType)) {
|
||||||
|
relationsWithEventType.setTargetEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add relation events to the relevant relation collection.
|
||||||
|
*
|
||||||
|
* @param {MatrixEvent} event
|
||||||
|
* The new relation event to be aggregated.
|
||||||
|
*/
|
||||||
|
public aggregateRelations(event: MatrixEvent): void {
|
||||||
|
if (!this.unstableClientRelationAggregation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.isRedacted() || event.status === EventStatus.CANCELLED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the event is currently encrypted, wait until it has been decrypted.
|
||||||
|
if (event.isBeingDecrypted() || event.shouldAttemptDecryption()) {
|
||||||
|
event.once("Event.decrypted", () => {
|
||||||
|
this.aggregateRelations(event);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const relation = event.getRelation();
|
||||||
|
if (!relation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const relatesToEventId = relation.event_id;
|
||||||
|
const relationType = relation.rel_type;
|
||||||
|
const eventType = event.getType();
|
||||||
|
|
||||||
|
// debuglog("Aggregating relation: ", event.getId(), eventType, relation);
|
||||||
|
|
||||||
|
let relationsForEvent: Record<string, Partial<Record<string, Relations>>> = this.relations[relatesToEventId];
|
||||||
|
if (!relationsForEvent) {
|
||||||
|
relationsForEvent = this.relations[relatesToEventId] = {};
|
||||||
|
}
|
||||||
|
let relationsWithRelType = relationsForEvent[relationType];
|
||||||
|
if (!relationsWithRelType) {
|
||||||
|
relationsWithRelType = relationsForEvent[relationType] = {};
|
||||||
|
}
|
||||||
|
let relationsWithEventType = relationsWithRelType[eventType];
|
||||||
|
|
||||||
|
let relatesToEvent;
|
||||||
|
if (!relationsWithEventType) {
|
||||||
|
relationsWithEventType = relationsWithRelType[eventType] = new Relations(
|
||||||
|
relationType,
|
||||||
|
eventType,
|
||||||
|
this.room,
|
||||||
|
);
|
||||||
|
relatesToEvent = this.findEventById(relatesToEventId) || this.room.getPendingEvent(relatesToEventId);
|
||||||
|
if (relatesToEvent) {
|
||||||
|
relationsWithEventType.setTargetEvent(relatesToEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
relationsWithEventType.addEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires whenever the timeline in a room is updated.
|
||||||
|
* @event module:client~MatrixClient#"Room.timeline"
|
||||||
|
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||||
|
* @param {?Room} room The room, if any, whose timeline was updated.
|
||||||
|
* @param {boolean} toStartOfTimeline True if this event was added to the start
|
||||||
|
* @param {boolean} removed True if this event has just been removed from the timeline
|
||||||
|
* (beginning; oldest) of the timeline e.g. due to pagination.
|
||||||
|
*
|
||||||
|
* @param {object} data more data about the event
|
||||||
|
*
|
||||||
|
* @param {module:models/event-timeline.EventTimeline} data.timeline the timeline the
|
||||||
|
* event was added to/removed from
|
||||||
|
*
|
||||||
|
* @param {boolean} data.liveEvent true if the event was a real-time event
|
||||||
|
* added to the end of the live timeline
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* matrixClient.on("Room.timeline",
|
||||||
|
* function(event, room, toStartOfTimeline, removed, data) {
|
||||||
|
* if (!toStartOfTimeline && data.liveEvent) {
|
||||||
|
* var messageToAppend = room.timeline.[room.timeline.length - 1];
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires whenever the live timeline in a room is reset.
|
||||||
|
*
|
||||||
|
* When we get a 'limited' sync (for example, after a network outage), we reset
|
||||||
|
* the live timeline to be empty before adding the recent events to the new
|
||||||
|
* timeline. This event is fired after the timeline is reset, and before the
|
||||||
|
* new events are added.
|
||||||
|
*
|
||||||
|
* @event module:client~MatrixClient#"Room.timelineReset"
|
||||||
|
* @param {Room} room The room whose live timeline was reset, if any
|
||||||
|
* @param {EventTimelineSet} timelineSet timelineSet room whose live timeline was reset
|
||||||
|
* @param {boolean} resetAllTimelines True if all timelines were reset.
|
||||||
|
*/
|
||||||
@@ -1,398 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2016, 2017 OpenMarket Ltd
|
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @module models/event-timeline
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { RoomState } from "./room-state";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Construct a new EventTimeline
|
|
||||||
*
|
|
||||||
* <p>An EventTimeline represents a contiguous sequence of events in a room.
|
|
||||||
*
|
|
||||||
* <p>As well as keeping track of the events themselves, it stores the state of
|
|
||||||
* the room at the beginning and end of the timeline, and pagination tokens for
|
|
||||||
* going backwards and forwards in the timeline.
|
|
||||||
*
|
|
||||||
* <p>In order that clients can meaningfully maintain an index into a timeline,
|
|
||||||
* the EventTimeline object tracks a 'baseIndex'. This starts at zero, but is
|
|
||||||
* incremented when events are prepended to the timeline. The index of an event
|
|
||||||
* relative to baseIndex therefore remains constant.
|
|
||||||
*
|
|
||||||
* <p>Once a timeline joins up with its neighbour, they are linked together into a
|
|
||||||
* doubly-linked list.
|
|
||||||
*
|
|
||||||
* @param {EventTimelineSet} eventTimelineSet the set of timelines this is part of
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
export function EventTimeline(eventTimelineSet) {
|
|
||||||
this._eventTimelineSet = eventTimelineSet;
|
|
||||||
this._roomId = eventTimelineSet.room ? eventTimelineSet.room.roomId : null;
|
|
||||||
this._events = [];
|
|
||||||
this._baseIndex = 0;
|
|
||||||
this._startState = new RoomState(this._roomId);
|
|
||||||
this._startState.paginationToken = null;
|
|
||||||
this._endState = new RoomState(this._roomId);
|
|
||||||
this._endState.paginationToken = null;
|
|
||||||
|
|
||||||
this._prevTimeline = null;
|
|
||||||
this._nextTimeline = null;
|
|
||||||
|
|
||||||
// this is used by client.js
|
|
||||||
this._paginationRequests = { 'b': null, 'f': null };
|
|
||||||
|
|
||||||
this._name = this._roomId + ":" + new Date().toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Symbolic constant for methods which take a 'direction' argument:
|
|
||||||
* refers to the start of the timeline, or backwards in time.
|
|
||||||
*/
|
|
||||||
EventTimeline.BACKWARDS = "b";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Symbolic constant for methods which take a 'direction' argument:
|
|
||||||
* refers to the end of the timeline, or forwards in time.
|
|
||||||
*/
|
|
||||||
EventTimeline.FORWARDS = "f";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialise the start and end state with the given events
|
|
||||||
*
|
|
||||||
* <p>This can only be called before any events are added.
|
|
||||||
*
|
|
||||||
* @param {MatrixEvent[]} stateEvents list of state events to initialise the
|
|
||||||
* state with.
|
|
||||||
* @throws {Error} if an attempt is made to call this after addEvent is called.
|
|
||||||
*/
|
|
||||||
EventTimeline.prototype.initialiseState = function(stateEvents) {
|
|
||||||
if (this._events.length > 0) {
|
|
||||||
throw new Error("Cannot initialise state after events are added");
|
|
||||||
}
|
|
||||||
|
|
||||||
// We previously deep copied events here and used different copies in
|
|
||||||
// the oldState and state events: this decision seems to date back
|
|
||||||
// quite a way and was apparently made to fix a bug where modifications
|
|
||||||
// made to the start state leaked through to the end state.
|
|
||||||
// This really shouldn't be possible though: the events themselves should
|
|
||||||
// not change. Duplicating the events uses a lot of extra memory,
|
|
||||||
// so we now no longer do it. To assert that they really do never change,
|
|
||||||
// freeze them! Note that we can't do this for events in general:
|
|
||||||
// although it looks like the only things preventing us are the
|
|
||||||
// 'status' flag, forwardLooking (which is only set once when adding to the
|
|
||||||
// timeline) and possibly the sender (which seems like it should never be
|
|
||||||
// reset but in practice causes a lot of the tests to break).
|
|
||||||
for (const e of stateEvents) {
|
|
||||||
Object.freeze(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._startState.setStateEvents(stateEvents);
|
|
||||||
this._endState.setStateEvents(stateEvents);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Forks the (live) timeline, taking ownership of the existing directional state of this timeline.
|
|
||||||
* All attached listeners will keep receiving state updates from the new live timeline state.
|
|
||||||
* The end state of this timeline gets replaced with an independent copy of the current RoomState,
|
|
||||||
* and will need a new pagination token if it ever needs to paginate forwards.
|
|
||||||
|
|
||||||
* @param {string} direction EventTimeline.BACKWARDS to get the state at the
|
|
||||||
* start of the timeline; EventTimeline.FORWARDS to get the state at the end
|
|
||||||
* of the timeline.
|
|
||||||
*
|
|
||||||
* @return {EventTimeline} the new timeline
|
|
||||||
*/
|
|
||||||
EventTimeline.prototype.forkLive = function(direction) {
|
|
||||||
const forkState = this.getState(direction);
|
|
||||||
const timeline = new EventTimeline(this._eventTimelineSet);
|
|
||||||
timeline._startState = forkState.clone();
|
|
||||||
// Now clobber the end state of the new live timeline with that from the
|
|
||||||
// previous live timeline. It will be identical except that we'll keep
|
|
||||||
// using the same RoomMember objects for the 'live' set of members with any
|
|
||||||
// listeners still attached
|
|
||||||
timeline._endState = forkState;
|
|
||||||
// Firstly, we just stole the current timeline's end state, so it needs a new one.
|
|
||||||
// Make an immutable copy of the state so back pagination will get the correct sentinels.
|
|
||||||
this._endState = forkState.clone();
|
|
||||||
return timeline;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an independent timeline, inheriting the directional state from this timeline.
|
|
||||||
*
|
|
||||||
* @param {string} direction EventTimeline.BACKWARDS to get the state at the
|
|
||||||
* start of the timeline; EventTimeline.FORWARDS to get the state at the end
|
|
||||||
* of the timeline.
|
|
||||||
*
|
|
||||||
* @return {EventTimeline} the new timeline
|
|
||||||
*/
|
|
||||||
EventTimeline.prototype.fork = function(direction) {
|
|
||||||
const forkState = this.getState(direction);
|
|
||||||
const timeline = new EventTimeline(this._eventTimelineSet);
|
|
||||||
timeline._startState = forkState.clone();
|
|
||||||
timeline._endState = forkState.clone();
|
|
||||||
return timeline;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the ID of the room for this timeline
|
|
||||||
* @return {string} room ID
|
|
||||||
*/
|
|
||||||
EventTimeline.prototype.getRoomId = function() {
|
|
||||||
return this._roomId;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the filter for this timeline's timelineSet (if any)
|
|
||||||
* @return {Filter} filter
|
|
||||||
*/
|
|
||||||
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.
|
|
||||||
*
|
|
||||||
* <p>This is an index which is incremented when events are prepended to the
|
|
||||||
* timeline. An individual event therefore stays at the same index in the array
|
|
||||||
* relative to the base index (although note that a given event's index may
|
|
||||||
* well be less than the base index, thus giving that event a negative relative
|
|
||||||
* index).
|
|
||||||
*
|
|
||||||
* @return {number}
|
|
||||||
*/
|
|
||||||
EventTimeline.prototype.getBaseIndex = function() {
|
|
||||||
return this._baseIndex;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the list of events in this context
|
|
||||||
*
|
|
||||||
* @return {MatrixEvent[]} An array of MatrixEvents
|
|
||||||
*/
|
|
||||||
EventTimeline.prototype.getEvents = function() {
|
|
||||||
return this._events;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the room state at the start/end of the timeline
|
|
||||||
*
|
|
||||||
* @param {string} direction EventTimeline.BACKWARDS to get the state at the
|
|
||||||
* start of the timeline; EventTimeline.FORWARDS to get the state at the end
|
|
||||||
* of the timeline.
|
|
||||||
*
|
|
||||||
* @return {RoomState} state at the start/end of the timeline
|
|
||||||
*/
|
|
||||||
EventTimeline.prototype.getState = function(direction) {
|
|
||||||
if (direction == EventTimeline.BACKWARDS) {
|
|
||||||
return this._startState;
|
|
||||||
} else if (direction == EventTimeline.FORWARDS) {
|
|
||||||
return this._endState;
|
|
||||||
} else {
|
|
||||||
throw new Error("Invalid direction '" + direction + "'");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a pagination token
|
|
||||||
*
|
|
||||||
* @param {string} direction EventTimeline.BACKWARDS to get the pagination
|
|
||||||
* token for going backwards in time; EventTimeline.FORWARDS to get the
|
|
||||||
* pagination token for going forwards in time.
|
|
||||||
*
|
|
||||||
* @return {?string} pagination token
|
|
||||||
*/
|
|
||||||
EventTimeline.prototype.getPaginationToken = function(direction) {
|
|
||||||
return this.getState(direction).paginationToken;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set a pagination token
|
|
||||||
*
|
|
||||||
* @param {?string} token pagination token
|
|
||||||
*
|
|
||||||
* @param {string} direction EventTimeline.BACKWARDS to set the pagination
|
|
||||||
* token for going backwards in time; EventTimeline.FORWARDS to set the
|
|
||||||
* pagination token for going forwards in time.
|
|
||||||
*/
|
|
||||||
EventTimeline.prototype.setPaginationToken = function(token, direction) {
|
|
||||||
this.getState(direction).paginationToken = token;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the next timeline in the series
|
|
||||||
*
|
|
||||||
* @param {string} direction EventTimeline.BACKWARDS to get the previous
|
|
||||||
* timeline; EventTimeline.FORWARDS to get the next timeline.
|
|
||||||
*
|
|
||||||
* @return {?EventTimeline} previous or following timeline, if they have been
|
|
||||||
* joined up.
|
|
||||||
*/
|
|
||||||
EventTimeline.prototype.getNeighbouringTimeline = function(direction) {
|
|
||||||
if (direction == EventTimeline.BACKWARDS) {
|
|
||||||
return this._prevTimeline;
|
|
||||||
} else if (direction == EventTimeline.FORWARDS) {
|
|
||||||
return this._nextTimeline;
|
|
||||||
} else {
|
|
||||||
throw new Error("Invalid direction '" + direction + "'");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the next timeline in the series
|
|
||||||
*
|
|
||||||
* @param {EventTimeline} neighbour previous/following timeline
|
|
||||||
*
|
|
||||||
* @param {string} direction EventTimeline.BACKWARDS to set the previous
|
|
||||||
* timeline; EventTimeline.FORWARDS to set the next timeline.
|
|
||||||
*
|
|
||||||
* @throws {Error} if an attempt is made to set the neighbouring timeline when
|
|
||||||
* it is already set.
|
|
||||||
*/
|
|
||||||
EventTimeline.prototype.setNeighbouringTimeline = function(neighbour, direction) {
|
|
||||||
if (this.getNeighbouringTimeline(direction)) {
|
|
||||||
throw new Error("timeline already has a neighbouring timeline - " +
|
|
||||||
"cannot reset neighbour (direction: " + direction + ")");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (direction == EventTimeline.BACKWARDS) {
|
|
||||||
this._prevTimeline = neighbour;
|
|
||||||
} else if (direction == EventTimeline.FORWARDS) {
|
|
||||||
this._nextTimeline = neighbour;
|
|
||||||
} else {
|
|
||||||
throw new Error("Invalid direction '" + direction + "'");
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure we don't try to paginate this timeline
|
|
||||||
this.setPaginationToken(null, direction);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a new event to the timeline, and update the state
|
|
||||||
*
|
|
||||||
* @param {MatrixEvent} event new event
|
|
||||||
* @param {boolean} atStart true to insert new event at the start
|
|
||||||
*/
|
|
||||||
EventTimeline.prototype.addEvent = function(event, atStart) {
|
|
||||||
const stateContext = atStart ? this._startState : this._endState;
|
|
||||||
|
|
||||||
// only call setEventMetadata on the unfiltered timelineSets
|
|
||||||
const timelineSet = this.getTimelineSet();
|
|
||||||
if (timelineSet.room &&
|
|
||||||
timelineSet.room.getUnfilteredTimelineSet() === timelineSet) {
|
|
||||||
EventTimeline.setEventMetadata(event, stateContext, atStart);
|
|
||||||
|
|
||||||
// modify state
|
|
||||||
if (event.isState()) {
|
|
||||||
stateContext.setStateEvents([event]);
|
|
||||||
// it is possible that the act of setting the state event means we
|
|
||||||
// can set more metadata (specifically sender/target props), so try
|
|
||||||
// it again if the prop wasn't previously set. It may also mean that
|
|
||||||
// the sender/target is updated (if the event set was a room member event)
|
|
||||||
// so we want to use the *updated* member (new avatar/name) instead.
|
|
||||||
//
|
|
||||||
// However, we do NOT want to do this on member events if we're going
|
|
||||||
// back in time, else we'll set the .sender value for BEFORE the given
|
|
||||||
// member event, whereas we want to set the .sender value for the ACTUAL
|
|
||||||
// member event itself.
|
|
||||||
if (!event.sender || (event.getType() === "m.room.member" && !atStart)) {
|
|
||||||
EventTimeline.setEventMetadata(event, stateContext, atStart);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let insertIndex;
|
|
||||||
|
|
||||||
if (atStart) {
|
|
||||||
insertIndex = 0;
|
|
||||||
} else {
|
|
||||||
insertIndex = this._events.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._events.splice(insertIndex, 0, event); // insert element
|
|
||||||
if (atStart) {
|
|
||||||
this._baseIndex++;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Static helper method to set sender and target properties
|
|
||||||
*
|
|
||||||
* @param {MatrixEvent} event the event whose metadata is to be set
|
|
||||||
* @param {RoomState} stateContext the room state to be queried
|
|
||||||
* @param {bool} toStartOfTimeline if true the event's forwardLooking flag is set false
|
|
||||||
*/
|
|
||||||
EventTimeline.setEventMetadata = function(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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove an event from the timeline
|
|
||||||
*
|
|
||||||
* @param {string} eventId ID of event to be removed
|
|
||||||
* @return {?MatrixEvent} removed event, or null if not found
|
|
||||||
*/
|
|
||||||
EventTimeline.prototype.removeEvent = function(eventId) {
|
|
||||||
for (let i = this._events.length - 1; i >= 0; i--) {
|
|
||||||
const ev = this._events[i];
|
|
||||||
if (ev.getId() == eventId) {
|
|
||||||
this._events.splice(i, 1);
|
|
||||||
if (i < this._baseIndex) {
|
|
||||||
this._baseIndex--;
|
|
||||||
}
|
|
||||||
return ev;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a string to identify this timeline, for debugging
|
|
||||||
*
|
|
||||||
* @return {string} name for this timeline
|
|
||||||
*/
|
|
||||||
EventTimeline.prototype.toString = function() {
|
|
||||||
return this._name;
|
|
||||||
};
|
|
||||||
|
|
||||||
416
src/models/event-timeline.ts
Normal file
416
src/models/event-timeline.ts
Normal file
@@ -0,0 +1,416 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @module models/event-timeline
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { RoomState } from "./room-state";
|
||||||
|
import { EventTimelineSet } from "./event-timeline-set";
|
||||||
|
import { MatrixEvent } from "./event";
|
||||||
|
import { Filter } from "../filter";
|
||||||
|
|
||||||
|
export enum Direction {
|
||||||
|
Backward = "b",
|
||||||
|
Forward = "f",
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EventTimeline {
|
||||||
|
/**
|
||||||
|
* Symbolic constant for methods which take a 'direction' argument:
|
||||||
|
* refers to the start of the timeline, or backwards in time.
|
||||||
|
*/
|
||||||
|
static BACKWARDS = Direction.Backward;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Symbolic constant for methods which take a 'direction' argument:
|
||||||
|
* refers to the end of the timeline, or forwards in time.
|
||||||
|
*/
|
||||||
|
static FORWARDS = Direction.Forward;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static helper method to set sender and target properties
|
||||||
|
*
|
||||||
|
* @param {MatrixEvent} event the event whose metadata is to be set
|
||||||
|
* @param {RoomState} stateContext the room state to be queried
|
||||||
|
* @param {boolean} toStartOfTimeline if true the event's forwardLooking flag is set false
|
||||||
|
*/
|
||||||
|
static setEventMetadata(event: MatrixEvent, stateContext: RoomState, toStartOfTimeline: boolean): void {
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly roomId: string | null;
|
||||||
|
private readonly name: string;
|
||||||
|
private events: MatrixEvent[] = [];
|
||||||
|
private baseIndex = 0;
|
||||||
|
private startState: RoomState;
|
||||||
|
private endState: RoomState;
|
||||||
|
private prevTimeline?: EventTimeline;
|
||||||
|
private nextTimeline?: EventTimeline;
|
||||||
|
public paginationRequests: Record<Direction, Promise<boolean>> = {
|
||||||
|
[Direction.Backward]: null,
|
||||||
|
[Direction.Forward]: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new EventTimeline
|
||||||
|
*
|
||||||
|
* <p>An EventTimeline represents a contiguous sequence of events in a room.
|
||||||
|
*
|
||||||
|
* <p>As well as keeping track of the events themselves, it stores the state of
|
||||||
|
* the room at the beginning and end of the timeline, and pagination tokens for
|
||||||
|
* going backwards and forwards in the timeline.
|
||||||
|
*
|
||||||
|
* <p>In order that clients can meaningfully maintain an index into a timeline,
|
||||||
|
* the EventTimeline object tracks a 'baseIndex'. This starts at zero, but is
|
||||||
|
* incremented when events are prepended to the timeline. The index of an event
|
||||||
|
* relative to baseIndex therefore remains constant.
|
||||||
|
*
|
||||||
|
* <p>Once a timeline joins up with its neighbour, they are linked together into a
|
||||||
|
* doubly-linked list.
|
||||||
|
*
|
||||||
|
* @param {EventTimelineSet} eventTimelineSet the set of timelines this is part of
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
constructor(private readonly eventTimelineSet: EventTimelineSet) {
|
||||||
|
this.roomId = eventTimelineSet.room?.roomId ?? null;
|
||||||
|
this.startState = new RoomState(this.roomId);
|
||||||
|
this.startState.paginationToken = null;
|
||||||
|
this.endState = new RoomState(this.roomId);
|
||||||
|
this.endState.paginationToken = null;
|
||||||
|
|
||||||
|
this.prevTimeline = null;
|
||||||
|
this.nextTimeline = null;
|
||||||
|
|
||||||
|
// this is used by client.js
|
||||||
|
this.paginationRequests = { 'b': null, 'f': null };
|
||||||
|
|
||||||
|
this.name = this.roomId + ":" + new Date().toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise the start and end state with the given events
|
||||||
|
*
|
||||||
|
* <p>This can only be called before any events are added.
|
||||||
|
*
|
||||||
|
* @param {MatrixEvent[]} stateEvents list of state events to initialise the
|
||||||
|
* state with.
|
||||||
|
* @throws {Error} if an attempt is made to call this after addEvent is called.
|
||||||
|
*/
|
||||||
|
public initialiseState(stateEvents: MatrixEvent[]): void {
|
||||||
|
if (this.events.length > 0) {
|
||||||
|
throw new Error("Cannot initialise state after events are added");
|
||||||
|
}
|
||||||
|
|
||||||
|
// We previously deep copied events here and used different copies in
|
||||||
|
// the oldState and state events: this decision seems to date back
|
||||||
|
// quite a way and was apparently made to fix a bug where modifications
|
||||||
|
// made to the start state leaked through to the end state.
|
||||||
|
// This really shouldn't be possible though: the events themselves should
|
||||||
|
// not change. Duplicating the events uses a lot of extra memory,
|
||||||
|
// so we now no longer do it. To assert that they really do never change,
|
||||||
|
// freeze them! Note that we can't do this for events in general:
|
||||||
|
// although it looks like the only things preventing us are the
|
||||||
|
// 'status' flag, forwardLooking (which is only set once when adding to the
|
||||||
|
// timeline) and possibly the sender (which seems like it should never be
|
||||||
|
// reset but in practice causes a lot of the tests to break).
|
||||||
|
for (const e of stateEvents) {
|
||||||
|
Object.freeze(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.startState.setStateEvents(stateEvents);
|
||||||
|
this.endState.setStateEvents(stateEvents);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forks the (live) timeline, taking ownership of the existing directional state of this timeline.
|
||||||
|
* All attached listeners will keep receiving state updates from the new live timeline state.
|
||||||
|
* The end state of this timeline gets replaced with an independent copy of the current RoomState,
|
||||||
|
* and will need a new pagination token if it ever needs to paginate forwards.
|
||||||
|
|
||||||
|
* @param {string} direction EventTimeline.BACKWARDS to get the state at the
|
||||||
|
* start of the timeline; EventTimeline.FORWARDS to get the state at the end
|
||||||
|
* of the timeline.
|
||||||
|
*
|
||||||
|
* @return {EventTimeline} the new timeline
|
||||||
|
*/
|
||||||
|
public forkLive(direction: Direction): EventTimeline {
|
||||||
|
const forkState = this.getState(direction);
|
||||||
|
const timeline = new EventTimeline(this.eventTimelineSet);
|
||||||
|
timeline.startState = forkState.clone();
|
||||||
|
// Now clobber the end state of the new live timeline with that from the
|
||||||
|
// previous live timeline. It will be identical except that we'll keep
|
||||||
|
// using the same RoomMember objects for the 'live' set of members with any
|
||||||
|
// listeners still attached
|
||||||
|
timeline.endState = forkState;
|
||||||
|
// Firstly, we just stole the current timeline's end state, so it needs a new one.
|
||||||
|
// Make an immutable copy of the state so back pagination will get the correct sentinels.
|
||||||
|
this.endState = forkState.clone();
|
||||||
|
return timeline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an independent timeline, inheriting the directional state from this timeline.
|
||||||
|
*
|
||||||
|
* @param {string} direction EventTimeline.BACKWARDS to get the state at the
|
||||||
|
* start of the timeline; EventTimeline.FORWARDS to get the state at the end
|
||||||
|
* of the timeline.
|
||||||
|
*
|
||||||
|
* @return {EventTimeline} the new timeline
|
||||||
|
*/
|
||||||
|
public fork(direction: Direction): EventTimeline {
|
||||||
|
const forkState = this.getState(direction);
|
||||||
|
const timeline = new EventTimeline(this.eventTimelineSet);
|
||||||
|
timeline.startState = forkState.clone();
|
||||||
|
timeline.endState = forkState.clone();
|
||||||
|
return timeline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the ID of the room for this timeline
|
||||||
|
* @return {string} room ID
|
||||||
|
*/
|
||||||
|
public getRoomId(): string {
|
||||||
|
return this.roomId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the filter for this timeline's timelineSet (if any)
|
||||||
|
* @return {Filter} filter
|
||||||
|
*/
|
||||||
|
public getFilter(): Filter {
|
||||||
|
return this.eventTimelineSet.getFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the timelineSet for this timeline
|
||||||
|
* @return {EventTimelineSet} timelineSet
|
||||||
|
*/
|
||||||
|
public getTimelineSet(): EventTimelineSet {
|
||||||
|
return this.eventTimelineSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the base index.
|
||||||
|
*
|
||||||
|
* <p>This is an index which is incremented when events are prepended to the
|
||||||
|
* timeline. An individual event therefore stays at the same index in the array
|
||||||
|
* relative to the base index (although note that a given event's index may
|
||||||
|
* well be less than the base index, thus giving that event a negative relative
|
||||||
|
* index).
|
||||||
|
*
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
public getBaseIndex(): number {
|
||||||
|
return this.baseIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of events in this context
|
||||||
|
*
|
||||||
|
* @return {MatrixEvent[]} An array of MatrixEvents
|
||||||
|
*/
|
||||||
|
public getEvents(): MatrixEvent[] {
|
||||||
|
return this.events;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the room state at the start/end of the timeline
|
||||||
|
*
|
||||||
|
* @param {string} direction EventTimeline.BACKWARDS to get the state at the
|
||||||
|
* start of the timeline; EventTimeline.FORWARDS to get the state at the end
|
||||||
|
* of the timeline.
|
||||||
|
*
|
||||||
|
* @return {RoomState} state at the start/end of the timeline
|
||||||
|
*/
|
||||||
|
public getState(direction: Direction): RoomState {
|
||||||
|
if (direction == EventTimeline.BACKWARDS) {
|
||||||
|
return this.startState;
|
||||||
|
} else if (direction == EventTimeline.FORWARDS) {
|
||||||
|
return this.endState;
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid direction '" + direction + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a pagination token
|
||||||
|
*
|
||||||
|
* @param {string} direction EventTimeline.BACKWARDS to get the pagination
|
||||||
|
* token for going backwards in time; EventTimeline.FORWARDS to get the
|
||||||
|
* pagination token for going forwards in time.
|
||||||
|
*
|
||||||
|
* @return {?string} pagination token
|
||||||
|
*/
|
||||||
|
public getPaginationToken(direction: Direction): string | null {
|
||||||
|
return this.getState(direction).paginationToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a pagination token
|
||||||
|
*
|
||||||
|
* @param {?string} token pagination token
|
||||||
|
*
|
||||||
|
* @param {string} direction EventTimeline.BACKWARDS to set the pagination
|
||||||
|
* token for going backwards in time; EventTimeline.FORWARDS to set the
|
||||||
|
* pagination token for going forwards in time.
|
||||||
|
*/
|
||||||
|
public setPaginationToken(token: string, direction: Direction): void {
|
||||||
|
this.getState(direction).paginationToken = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the next timeline in the series
|
||||||
|
*
|
||||||
|
* @param {string} direction EventTimeline.BACKWARDS to get the previous
|
||||||
|
* timeline; EventTimeline.FORWARDS to get the next timeline.
|
||||||
|
*
|
||||||
|
* @return {?EventTimeline} previous or following timeline, if they have been
|
||||||
|
* joined up.
|
||||||
|
*/
|
||||||
|
public getNeighbouringTimeline(direction: Direction): EventTimeline {
|
||||||
|
if (direction == EventTimeline.BACKWARDS) {
|
||||||
|
return this.prevTimeline;
|
||||||
|
} else if (direction == EventTimeline.FORWARDS) {
|
||||||
|
return this.nextTimeline;
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid direction '" + direction + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the next timeline in the series
|
||||||
|
*
|
||||||
|
* @param {EventTimeline} neighbour previous/following timeline
|
||||||
|
*
|
||||||
|
* @param {string} direction EventTimeline.BACKWARDS to set the previous
|
||||||
|
* timeline; EventTimeline.FORWARDS to set the next timeline.
|
||||||
|
*
|
||||||
|
* @throws {Error} if an attempt is made to set the neighbouring timeline when
|
||||||
|
* it is already set.
|
||||||
|
*/
|
||||||
|
public setNeighbouringTimeline(neighbour: EventTimeline, direction: Direction): void {
|
||||||
|
if (this.getNeighbouringTimeline(direction)) {
|
||||||
|
throw new Error("timeline already has a neighbouring timeline - " +
|
||||||
|
"cannot reset neighbour (direction: " + direction + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (direction == EventTimeline.BACKWARDS) {
|
||||||
|
this.prevTimeline = neighbour;
|
||||||
|
} else if (direction == EventTimeline.FORWARDS) {
|
||||||
|
this.nextTimeline = neighbour;
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid direction '" + direction + "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure we don't try to paginate this timeline
|
||||||
|
this.setPaginationToken(null, direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new event to the timeline, and update the state
|
||||||
|
*
|
||||||
|
* @param {MatrixEvent} event new event
|
||||||
|
* @param {boolean} atStart true to insert new event at the start
|
||||||
|
*/
|
||||||
|
public addEvent(event: MatrixEvent, atStart: boolean): void {
|
||||||
|
const stateContext = atStart ? this.startState : this.endState;
|
||||||
|
|
||||||
|
// only call setEventMetadata on the unfiltered timelineSets
|
||||||
|
const timelineSet = this.getTimelineSet();
|
||||||
|
if (timelineSet.room &&
|
||||||
|
timelineSet.room.getUnfilteredTimelineSet() === timelineSet) {
|
||||||
|
EventTimeline.setEventMetadata(event, stateContext, atStart);
|
||||||
|
|
||||||
|
// modify state
|
||||||
|
if (event.isState()) {
|
||||||
|
stateContext.setStateEvents([event]);
|
||||||
|
// it is possible that the act of setting the state event means we
|
||||||
|
// can set more metadata (specifically sender/target props), so try
|
||||||
|
// it again if the prop wasn't previously set. It may also mean that
|
||||||
|
// the sender/target is updated (if the event set was a room member event)
|
||||||
|
// so we want to use the *updated* member (new avatar/name) instead.
|
||||||
|
//
|
||||||
|
// However, we do NOT want to do this on member events if we're going
|
||||||
|
// back in time, else we'll set the .sender value for BEFORE the given
|
||||||
|
// member event, whereas we want to set the .sender value for the ACTUAL
|
||||||
|
// member event itself.
|
||||||
|
if (!event.sender || (event.getType() === "m.room.member" && !atStart)) {
|
||||||
|
EventTimeline.setEventMetadata(event, stateContext, atStart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let insertIndex;
|
||||||
|
|
||||||
|
if (atStart) {
|
||||||
|
insertIndex = 0;
|
||||||
|
} else {
|
||||||
|
insertIndex = this.events.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.events.splice(insertIndex, 0, event); // insert element
|
||||||
|
if (atStart) {
|
||||||
|
this.baseIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an event from the timeline
|
||||||
|
*
|
||||||
|
* @param {string} eventId ID of event to be removed
|
||||||
|
* @return {?MatrixEvent} removed event, or null if not found
|
||||||
|
*/
|
||||||
|
public removeEvent(eventId: string): MatrixEvent | null {
|
||||||
|
for (let i = this.events.length - 1; i >= 0; i--) {
|
||||||
|
const ev = this.events[i];
|
||||||
|
if (ev.getId() == eventId) {
|
||||||
|
this.events.splice(i, 1);
|
||||||
|
if (i < this.baseIndex) {
|
||||||
|
this.baseIndex--;
|
||||||
|
}
|
||||||
|
return ev;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a string to identify this timeline, for debugging
|
||||||
|
*
|
||||||
|
* @return {string} name for this timeline
|
||||||
|
*/
|
||||||
|
public toString(): string {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -112,7 +112,7 @@ interface IAggregatedRelation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface IEventRelation {
|
interface IEventRelation {
|
||||||
rel_type: string;
|
rel_type: RelationType | string;
|
||||||
event_id: string;
|
event_id: string;
|
||||||
key?: string;
|
key?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export class Relations extends EventEmitter {
|
|||||||
* notification timeline.
|
* notification timeline.
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
public readonly relationType: RelationType,
|
public readonly relationType: RelationType | string,
|
||||||
public readonly eventType: string,
|
public readonly eventType: string,
|
||||||
private readonly room: Room,
|
private readonly room: Room,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const SERVICE_TYPES = Object.freeze({
|
export enum SERVICE_TYPES {
|
||||||
IS: 'SERVICE_TYPE_IS', // An Identity Service
|
IS = 'SERVICE_TYPE_IS', // An Identity Service
|
||||||
IM: 'SERVICE_TYPE_IM', // An Integration Manager
|
IM = 'SERVICE_TYPE_IM', // An Integration Manager
|
||||||
});
|
}
|
||||||
@@ -1,521 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2016 OpenMarket Ltd
|
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** @module timeline-window */
|
|
||||||
|
|
||||||
import { EventTimeline } from './models/event-timeline';
|
|
||||||
import { logger } from './logger';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
const DEBUG = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
const debuglog = DEBUG ? logger.log.bind(logger) : function() {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the number of times we ask the server for more events before giving up
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
const DEFAULT_PAGINATE_LOOP_LIMIT = 5;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Construct a TimelineWindow.
|
|
||||||
*
|
|
||||||
* <p>This abstracts the separate timelines in a Matrix {@link
|
|
||||||
* module:models/room|Room} into a single iterable thing. It keeps track of
|
|
||||||
* the start and endpoints of the window, which can be advanced with the help
|
|
||||||
* of pagination requests.
|
|
||||||
*
|
|
||||||
* <p>Before the window is useful, it must be initialised by calling {@link
|
|
||||||
* module:timeline-window~TimelineWindow#load|load}.
|
|
||||||
*
|
|
||||||
* <p>Note that the window will not automatically extend itself when new events
|
|
||||||
* are received from /sync; you should arrange to call {@link
|
|
||||||
* module:timeline-window~TimelineWindow#paginate|paginate} on {@link
|
|
||||||
* module:client~MatrixClient.event:"Room.timeline"|Room.timeline} events.
|
|
||||||
*
|
|
||||||
* @param {MatrixClient} client MatrixClient to be used for context/pagination
|
|
||||||
* requests.
|
|
||||||
*
|
|
||||||
* @param {EventTimelineSet} timelineSet The timelineSet to track
|
|
||||||
*
|
|
||||||
* @param {Object} [opts] Configuration options for this window
|
|
||||||
*
|
|
||||||
* @param {number} [opts.windowLimit = 1000] maximum number of events to keep
|
|
||||||
* in the window. If more events are retrieved via pagination requests,
|
|
||||||
* excess events will be dropped from the other end of the window.
|
|
||||||
*
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
export function TimelineWindow(client, timelineSet, opts) {
|
|
||||||
opts = opts || {};
|
|
||||||
this._client = client;
|
|
||||||
this._timelineSet = timelineSet;
|
|
||||||
|
|
||||||
// these will be TimelineIndex objects; they delineate the 'start' and
|
|
||||||
// 'end' of the window.
|
|
||||||
//
|
|
||||||
// _start.index is inclusive; _end.index is exclusive.
|
|
||||||
this._start = null;
|
|
||||||
this._end = null;
|
|
||||||
|
|
||||||
this._eventCount = 0;
|
|
||||||
this._windowLimit = opts.windowLimit || 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialise the window to point at a given event, or the live timeline
|
|
||||||
*
|
|
||||||
* @param {string} [initialEventId] If given, the window will contain the
|
|
||||||
* given event
|
|
||||||
* @param {number} [initialWindowSize = 20] Size of the initial window
|
|
||||||
*
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
|
|
||||||
const self = this;
|
|
||||||
initialWindowSize = initialWindowSize || 20;
|
|
||||||
|
|
||||||
// given an EventTimeline, find the event we were looking for, and initialise our
|
|
||||||
// fields so that the event in question is in the middle of the window.
|
|
||||||
const initFields = function(timeline) {
|
|
||||||
let eventIndex;
|
|
||||||
|
|
||||||
const events = timeline.getEvents();
|
|
||||||
|
|
||||||
if (!initialEventId) {
|
|
||||||
// we were looking for the live timeline: initialise to the end
|
|
||||||
eventIndex = events.length;
|
|
||||||
} else {
|
|
||||||
for (let i = 0; i < events.length; i++) {
|
|
||||||
if (events[i].getId() == initialEventId) {
|
|
||||||
eventIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventIndex === undefined) {
|
|
||||||
throw new Error("getEventTimeline result didn't include requested event");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const endIndex = Math.min(events.length,
|
|
||||||
eventIndex + Math.ceil(initialWindowSize / 2));
|
|
||||||
const startIndex = Math.max(0, endIndex - initialWindowSize);
|
|
||||||
self._start = new TimelineIndex(timeline, startIndex - timeline.getBaseIndex());
|
|
||||||
self._end = new TimelineIndex(timeline, endIndex - timeline.getBaseIndex());
|
|
||||||
self._eventCount = endIndex - startIndex;
|
|
||||||
};
|
|
||||||
|
|
||||||
// We avoid delaying the resolution of the promise by a reactor tick if
|
|
||||||
// we already have the data we need, which is important to keep room-switching
|
|
||||||
// feeling snappy.
|
|
||||||
//
|
|
||||||
if (initialEventId) {
|
|
||||||
const timeline = this._timelineSet.getTimelineForEvent(initialEventId);
|
|
||||||
if (timeline) {
|
|
||||||
// hot-path optimization to save a reactor tick by replicating the sync check getTimelineForEvent does.
|
|
||||||
initFields(timeline);
|
|
||||||
return Promise.resolve(timeline);
|
|
||||||
}
|
|
||||||
|
|
||||||
const prom = this._client.getEventTimeline(this._timelineSet, initialEventId);
|
|
||||||
return prom.then(initFields);
|
|
||||||
} else {
|
|
||||||
const tl = this._timelineSet.getLiveTimeline();
|
|
||||||
initFields(tl);
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the TimelineIndex of the window in the given direction.
|
|
||||||
*
|
|
||||||
* @param {string} direction EventTimeline.BACKWARDS to get the TimelineIndex
|
|
||||||
* at the start of the window; EventTimeline.FORWARDS to get the TimelineIndex at
|
|
||||||
* the end.
|
|
||||||
*
|
|
||||||
* @return {TimelineIndex} The requested timeline index if one exists, null
|
|
||||||
* otherwise.
|
|
||||||
*/
|
|
||||||
TimelineWindow.prototype.getTimelineIndex = function(direction) {
|
|
||||||
if (direction == EventTimeline.BACKWARDS) {
|
|
||||||
return this._start;
|
|
||||||
} else if (direction == EventTimeline.FORWARDS) {
|
|
||||||
return this._end;
|
|
||||||
} else {
|
|
||||||
throw new Error("Invalid direction '" + direction + "'");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Try to extend the window using events that are already in the underlying
|
|
||||||
* TimelineIndex.
|
|
||||||
*
|
|
||||||
* @param {string} direction EventTimeline.BACKWARDS to try extending it
|
|
||||||
* backwards; EventTimeline.FORWARDS to try extending it forwards.
|
|
||||||
* @param {number} size number of events to try to extend by.
|
|
||||||
*
|
|
||||||
* @return {boolean} true if the window was extended, false otherwise.
|
|
||||||
*/
|
|
||||||
TimelineWindow.prototype.extend = function(direction, size) {
|
|
||||||
const tl = this.getTimelineIndex(direction);
|
|
||||||
|
|
||||||
if (!tl) {
|
|
||||||
debuglog("TimelineWindow: no timeline yet");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const count = (direction == EventTimeline.BACKWARDS) ?
|
|
||||||
tl.retreat(size) : tl.advance(size);
|
|
||||||
|
|
||||||
if (count) {
|
|
||||||
this._eventCount += count;
|
|
||||||
debuglog("TimelineWindow: increased cap by " + count +
|
|
||||||
" (now " + this._eventCount + ")");
|
|
||||||
// remove some events from the other end, if necessary
|
|
||||||
const excess = this._eventCount - this._windowLimit;
|
|
||||||
if (excess > 0) {
|
|
||||||
this.unpaginate(excess, direction != EventTimeline.BACKWARDS);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this window can be extended
|
|
||||||
*
|
|
||||||
* <p>This returns true if we either have more events, or if we have a
|
|
||||||
* pagination token which means we can paginate in that direction. It does not
|
|
||||||
* necessarily mean that there are more events available in that direction at
|
|
||||||
* this time.
|
|
||||||
*
|
|
||||||
* @param {string} direction EventTimeline.BACKWARDS to check if we can
|
|
||||||
* paginate backwards; EventTimeline.FORWARDS to check if we can go forwards
|
|
||||||
*
|
|
||||||
* @return {boolean} true if we can paginate in the given direction
|
|
||||||
*/
|
|
||||||
TimelineWindow.prototype.canPaginate = function(direction) {
|
|
||||||
const tl = this.getTimelineIndex(direction);
|
|
||||||
|
|
||||||
if (!tl) {
|
|
||||||
debuglog("TimelineWindow: no timeline yet");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (direction == EventTimeline.BACKWARDS) {
|
|
||||||
if (tl.index > tl.minIndex()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (tl.index < tl.maxIndex()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Boolean(tl.timeline.getNeighbouringTimeline(direction) ||
|
|
||||||
tl.timeline.getPaginationToken(direction));
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempt to extend the window
|
|
||||||
*
|
|
||||||
* @param {string} direction EventTimeline.BACKWARDS to extend the window
|
|
||||||
* backwards (towards older events); EventTimeline.FORWARDS to go forwards.
|
|
||||||
*
|
|
||||||
* @param {number} size number of events to try to extend by. If fewer than this
|
|
||||||
* number are immediately available, then we return immediately rather than
|
|
||||||
* making an API call.
|
|
||||||
*
|
|
||||||
* @param {boolean} [makeRequest = true] whether we should make API calls to
|
|
||||||
* fetch further events if we don't have any at all. (This has no effect if
|
|
||||||
* the room already knows about additional events in the relevant direction,
|
|
||||||
* even if there are fewer than 'size' of them, as we will just return those
|
|
||||||
* we already know about.)
|
|
||||||
*
|
|
||||||
* @param {number} [requestLimit = 5] limit for the number of API requests we
|
|
||||||
* should make.
|
|
||||||
*
|
|
||||||
* @return {Promise} Resolves to a boolean which is true if more events
|
|
||||||
* were successfully retrieved.
|
|
||||||
*/
|
|
||||||
TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
|
|
||||||
requestLimit) {
|
|
||||||
// Either wind back the message cap (if there are enough events in the
|
|
||||||
// timeline to do so), or fire off a pagination request.
|
|
||||||
|
|
||||||
if (makeRequest === undefined) {
|
|
||||||
makeRequest = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requestLimit === undefined) {
|
|
||||||
requestLimit = DEFAULT_PAGINATE_LOOP_LIMIT;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tl = this.getTimelineIndex(direction);
|
|
||||||
|
|
||||||
if (!tl) {
|
|
||||||
debuglog("TimelineWindow: no timeline yet");
|
|
||||||
return Promise.resolve(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tl.pendingPaginate) {
|
|
||||||
return tl.pendingPaginate;
|
|
||||||
}
|
|
||||||
|
|
||||||
// try moving the cap
|
|
||||||
if (this.extend(direction, size)) {
|
|
||||||
return Promise.resolve(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!makeRequest || requestLimit === 0) {
|
|
||||||
// todo: should we return something different to indicate that there
|
|
||||||
// might be more events out there, but we haven't found them yet?
|
|
||||||
return Promise.resolve(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// try making a pagination request
|
|
||||||
const token = tl.timeline.getPaginationToken(direction);
|
|
||||||
if (!token) {
|
|
||||||
debuglog("TimelineWindow: no token");
|
|
||||||
return Promise.resolve(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
debuglog("TimelineWindow: starting request");
|
|
||||||
const self = this;
|
|
||||||
|
|
||||||
const prom = this._client.paginateEventTimeline(tl.timeline, {
|
|
||||||
backwards: direction == EventTimeline.BACKWARDS,
|
|
||||||
limit: size,
|
|
||||||
}).finally(function() {
|
|
||||||
tl.pendingPaginate = null;
|
|
||||||
}).then(function(r) {
|
|
||||||
debuglog("TimelineWindow: request completed with result " + r);
|
|
||||||
if (!r) {
|
|
||||||
// end of timeline
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// recurse to advance the index into the results.
|
|
||||||
//
|
|
||||||
// If we don't get any new events, we want to make sure we keep asking
|
|
||||||
// the server for events for as long as we have a valid pagination
|
|
||||||
// token. In particular, we want to know if we've actually hit the
|
|
||||||
// start of the timeline, or if we just happened to know about all of
|
|
||||||
// the events thanks to https://matrix.org/jira/browse/SYN-645.
|
|
||||||
//
|
|
||||||
// On the other hand, we necessarily want to wait forever for the
|
|
||||||
// server to make its mind up about whether there are other events,
|
|
||||||
// because it gives a bad user experience
|
|
||||||
// (https://github.com/vector-im/vector-web/issues/1204).
|
|
||||||
return self.paginate(direction, size, true, requestLimit - 1);
|
|
||||||
});
|
|
||||||
tl.pendingPaginate = prom;
|
|
||||||
return prom;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove `delta` events from the start or end of the timeline.
|
|
||||||
*
|
|
||||||
* @param {number} delta number of events to remove from the timeline
|
|
||||||
* @param {boolean} startOfTimeline if events should be removed from the start
|
|
||||||
* of the timeline.
|
|
||||||
*/
|
|
||||||
TimelineWindow.prototype.unpaginate = function(delta, startOfTimeline) {
|
|
||||||
const tl = startOfTimeline ? this._start : this._end;
|
|
||||||
|
|
||||||
// sanity-check the delta
|
|
||||||
if (delta > this._eventCount || delta < 0) {
|
|
||||||
throw new Error("Attemting to unpaginate " + delta + " events, but " +
|
|
||||||
"only have " + this._eventCount + " in the timeline");
|
|
||||||
}
|
|
||||||
|
|
||||||
while (delta > 0) {
|
|
||||||
const count = startOfTimeline ? tl.advance(delta) : tl.retreat(delta);
|
|
||||||
if (count <= 0) {
|
|
||||||
// sadness. This shouldn't be possible.
|
|
||||||
throw new Error(
|
|
||||||
"Unable to unpaginate any further, but still have " +
|
|
||||||
this._eventCount + " events");
|
|
||||||
}
|
|
||||||
|
|
||||||
delta -= count;
|
|
||||||
this._eventCount -= count;
|
|
||||||
debuglog("TimelineWindow.unpaginate: dropped " + count +
|
|
||||||
" (now " + this._eventCount + ")");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a list of the events currently in the window
|
|
||||||
*
|
|
||||||
* @return {MatrixEvent[]} the events in the window
|
|
||||||
*/
|
|
||||||
TimelineWindow.prototype.getEvents = function() {
|
|
||||||
if (!this._start) {
|
|
||||||
// not yet loaded
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = [];
|
|
||||||
|
|
||||||
// iterate through each timeline between this._start and this._end
|
|
||||||
// (inclusive).
|
|
||||||
let timeline = this._start.timeline;
|
|
||||||
while (true) {
|
|
||||||
const events = timeline.getEvents();
|
|
||||||
|
|
||||||
// For the first timeline in the chain, we want to start at
|
|
||||||
// this._start.index. For the last timeline in the chain, we want to
|
|
||||||
// stop before this._end.index. Otherwise, we want to copy all of the
|
|
||||||
// events in the timeline.
|
|
||||||
//
|
|
||||||
// (Note that both this._start.index and this._end.index are relative
|
|
||||||
// to their respective timelines' BaseIndex).
|
|
||||||
//
|
|
||||||
let startIndex = 0;
|
|
||||||
let endIndex = events.length;
|
|
||||||
if (timeline === this._start.timeline) {
|
|
||||||
startIndex = this._start.index + timeline.getBaseIndex();
|
|
||||||
}
|
|
||||||
if (timeline === this._end.timeline) {
|
|
||||||
endIndex = this._end.index + timeline.getBaseIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = startIndex; i < endIndex; i++) {
|
|
||||||
result.push(events[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we're not done, iterate to the next timeline.
|
|
||||||
if (timeline === this._end.timeline) {
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
timeline = timeline.getNeighbouringTimeline(EventTimeline.FORWARDS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* a thing which contains a timeline reference, and an index into it.
|
|
||||||
*
|
|
||||||
* @constructor
|
|
||||||
* @param {EventTimeline} timeline
|
|
||||||
* @param {number} index
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
export function TimelineIndex(timeline, index) {
|
|
||||||
this.timeline = timeline;
|
|
||||||
|
|
||||||
// the indexes are relative to BaseIndex, so could well be negative.
|
|
||||||
this.index = index;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return {number} the minimum possible value for the index in the current
|
|
||||||
* timeline
|
|
||||||
*/
|
|
||||||
TimelineIndex.prototype.minIndex = function() {
|
|
||||||
return this.timeline.getBaseIndex() * -1;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return {number} the maximum possible value for the index in the current
|
|
||||||
* timeline (exclusive - ie, it actually returns one more than the index
|
|
||||||
* of the last element).
|
|
||||||
*/
|
|
||||||
TimelineIndex.prototype.maxIndex = function() {
|
|
||||||
return this.timeline.getEvents().length - this.timeline.getBaseIndex();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Try move the index forward, or into the neighbouring timeline
|
|
||||||
*
|
|
||||||
* @param {number} delta number of events to advance by
|
|
||||||
* @return {number} number of events successfully advanced by
|
|
||||||
*/
|
|
||||||
TimelineIndex.prototype.advance = function(delta) {
|
|
||||||
if (!delta) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// first try moving the index in the current timeline. See if there is room
|
|
||||||
// to do so.
|
|
||||||
let cappedDelta;
|
|
||||||
if (delta < 0) {
|
|
||||||
// we want to wind the index backwards.
|
|
||||||
//
|
|
||||||
// (this.minIndex() - this.index) is a negative number whose magnitude
|
|
||||||
// is the amount of room we have to wind back the index in the current
|
|
||||||
// timeline. We cap delta to this quantity.
|
|
||||||
cappedDelta = Math.max(delta, this.minIndex() - this.index);
|
|
||||||
if (cappedDelta < 0) {
|
|
||||||
this.index += cappedDelta;
|
|
||||||
return cappedDelta;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// we want to wind the index forwards.
|
|
||||||
//
|
|
||||||
// (this.maxIndex() - this.index) is a (positive) number whose magnitude
|
|
||||||
// is the amount of room we have to wind forward the index in the current
|
|
||||||
// timeline. We cap delta to this quantity.
|
|
||||||
cappedDelta = Math.min(delta, this.maxIndex() - this.index);
|
|
||||||
if (cappedDelta > 0) {
|
|
||||||
this.index += cappedDelta;
|
|
||||||
return cappedDelta;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// the index is already at the start/end of the current timeline.
|
|
||||||
//
|
|
||||||
// next see if there is a neighbouring timeline to switch to.
|
|
||||||
const neighbour = this.timeline.getNeighbouringTimeline(
|
|
||||||
delta < 0 ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS);
|
|
||||||
if (neighbour) {
|
|
||||||
this.timeline = neighbour;
|
|
||||||
if (delta < 0) {
|
|
||||||
this.index = this.maxIndex();
|
|
||||||
} else {
|
|
||||||
this.index = this.minIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
debuglog("paginate: switched to new neighbour");
|
|
||||||
|
|
||||||
// recurse, using the next timeline
|
|
||||||
return this.advance(delta);
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Try move the index backwards, or into the neighbouring timeline
|
|
||||||
*
|
|
||||||
* @param {number} delta number of events to retreat by
|
|
||||||
* @return {number} number of events successfully retreated by
|
|
||||||
*/
|
|
||||||
TimelineIndex.prototype.retreat = function(delta) {
|
|
||||||
return this.advance(delta * -1) * -1;
|
|
||||||
};
|
|
||||||
526
src/timeline-window.ts
Normal file
526
src/timeline-window.ts
Normal file
@@ -0,0 +1,526 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @module timeline-window */
|
||||||
|
|
||||||
|
import { Direction, EventTimeline } from './models/event-timeline';
|
||||||
|
import { logger } from './logger';
|
||||||
|
import { MatrixClient } from "./client";
|
||||||
|
import { EventTimelineSet } from "./models/event-timeline-set";
|
||||||
|
import { MatrixEvent } from "./models/event";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
const DEBUG = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
const debuglog = DEBUG ? logger.log.bind(logger) : function() {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the number of times we ask the server for more events before giving up
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
const DEFAULT_PAGINATE_LOOP_LIMIT = 5;
|
||||||
|
|
||||||
|
interface IOpts {
|
||||||
|
windowLimit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TimelineWindow {
|
||||||
|
private readonly windowLimit: number;
|
||||||
|
// these will be TimelineIndex objects; they delineate the 'start' and
|
||||||
|
// 'end' of the window.
|
||||||
|
//
|
||||||
|
// start.index is inclusive; end.index is exclusive.
|
||||||
|
private start?: TimelineIndex = null;
|
||||||
|
private end?: TimelineIndex = null;
|
||||||
|
private eventCount = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a TimelineWindow.
|
||||||
|
*
|
||||||
|
* <p>This abstracts the separate timelines in a Matrix {@link
|
||||||
|
* module:models/room|Room} into a single iterable thing. It keeps track of
|
||||||
|
* the start and endpoints of the window, which can be advanced with the help
|
||||||
|
* of pagination requests.
|
||||||
|
*
|
||||||
|
* <p>Before the window is useful, it must be initialised by calling {@link
|
||||||
|
* module:timeline-window~TimelineWindow#load|load}.
|
||||||
|
*
|
||||||
|
* <p>Note that the window will not automatically extend itself when new events
|
||||||
|
* are received from /sync; you should arrange to call {@link
|
||||||
|
* module:timeline-window~TimelineWindow#paginate|paginate} on {@link
|
||||||
|
* module:client~MatrixClient.event:"Room.timeline"|Room.timeline} events.
|
||||||
|
*
|
||||||
|
* @param {MatrixClient} client MatrixClient to be used for context/pagination
|
||||||
|
* requests.
|
||||||
|
*
|
||||||
|
* @param {EventTimelineSet} timelineSet The timelineSet to track
|
||||||
|
*
|
||||||
|
* @param {Object} [opts] Configuration options for this window
|
||||||
|
*
|
||||||
|
* @param {number} [opts.windowLimit = 1000] maximum number of events to keep
|
||||||
|
* in the window. If more events are retrieved via pagination requests,
|
||||||
|
* excess events will be dropped from the other end of the window.
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
private readonly client: MatrixClient,
|
||||||
|
private readonly timelineSet: EventTimelineSet,
|
||||||
|
opts: IOpts = {},
|
||||||
|
) {
|
||||||
|
this.windowLimit = opts.windowLimit || 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise the window to point at a given event, or the live timeline
|
||||||
|
*
|
||||||
|
* @param {string} [initialEventId] If given, the window will contain the
|
||||||
|
* given event
|
||||||
|
* @param {number} [initialWindowSize = 20] Size of the initial window
|
||||||
|
*
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
public load(initialEventId: string, initialWindowSize = 20): Promise<any> {
|
||||||
|
// given an EventTimeline, find the event we were looking for, and initialise our
|
||||||
|
// fields so that the event in question is in the middle of the window.
|
||||||
|
const initFields = (timeline: EventTimeline) => {
|
||||||
|
let eventIndex;
|
||||||
|
|
||||||
|
const events = timeline.getEvents();
|
||||||
|
|
||||||
|
if (!initialEventId) {
|
||||||
|
// we were looking for the live timeline: initialise to the end
|
||||||
|
eventIndex = events.length;
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < events.length; i++) {
|
||||||
|
if (events[i].getId() == initialEventId) {
|
||||||
|
eventIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventIndex === undefined) {
|
||||||
|
throw new Error("getEventTimeline result didn't include requested event");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const endIndex = Math.min(events.length,
|
||||||
|
eventIndex + Math.ceil(initialWindowSize / 2));
|
||||||
|
const startIndex = Math.max(0, endIndex - initialWindowSize);
|
||||||
|
this.start = new TimelineIndex(timeline, startIndex - timeline.getBaseIndex());
|
||||||
|
this.end = new TimelineIndex(timeline, endIndex - timeline.getBaseIndex());
|
||||||
|
this.eventCount = endIndex - startIndex;
|
||||||
|
};
|
||||||
|
|
||||||
|
// We avoid delaying the resolution of the promise by a reactor tick if
|
||||||
|
// we already have the data we need, which is important to keep room-switching
|
||||||
|
// feeling snappy.
|
||||||
|
//
|
||||||
|
if (initialEventId) {
|
||||||
|
const timeline = this.timelineSet.getTimelineForEvent(initialEventId);
|
||||||
|
if (timeline) {
|
||||||
|
// hot-path optimization to save a reactor tick by replicating the sync check getTimelineForEvent does.
|
||||||
|
initFields(timeline);
|
||||||
|
return Promise.resolve(timeline);
|
||||||
|
}
|
||||||
|
|
||||||
|
const prom = this.client.getEventTimeline(this.timelineSet, initialEventId);
|
||||||
|
return prom.then(initFields);
|
||||||
|
} else {
|
||||||
|
const tl = this.timelineSet.getLiveTimeline();
|
||||||
|
initFields(tl);
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the TimelineIndex of the window in the given direction.
|
||||||
|
*
|
||||||
|
* @param {string} direction EventTimeline.BACKWARDS to get the TimelineIndex
|
||||||
|
* at the start of the window; EventTimeline.FORWARDS to get the TimelineIndex at
|
||||||
|
* the end.
|
||||||
|
*
|
||||||
|
* @return {TimelineIndex} The requested timeline index if one exists, null
|
||||||
|
* otherwise.
|
||||||
|
*/
|
||||||
|
public getTimelineIndex(direction: Direction): TimelineIndex {
|
||||||
|
if (direction == EventTimeline.BACKWARDS) {
|
||||||
|
return this.start;
|
||||||
|
} else if (direction == EventTimeline.FORWARDS) {
|
||||||
|
return this.end;
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid direction '" + direction + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to extend the window using events that are already in the underlying
|
||||||
|
* TimelineIndex.
|
||||||
|
*
|
||||||
|
* @param {string} direction EventTimeline.BACKWARDS to try extending it
|
||||||
|
* backwards; EventTimeline.FORWARDS to try extending it forwards.
|
||||||
|
* @param {number} size number of events to try to extend by.
|
||||||
|
*
|
||||||
|
* @return {boolean} true if the window was extended, false otherwise.
|
||||||
|
*/
|
||||||
|
public extend(direction: Direction, size: number): boolean {
|
||||||
|
const tl = this.getTimelineIndex(direction);
|
||||||
|
|
||||||
|
if (!tl) {
|
||||||
|
debuglog("TimelineWindow: no timeline yet");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const count = (direction == EventTimeline.BACKWARDS) ?
|
||||||
|
tl.retreat(size) : tl.advance(size);
|
||||||
|
|
||||||
|
if (count) {
|
||||||
|
this.eventCount += count;
|
||||||
|
debuglog("TimelineWindow: increased cap by " + count +
|
||||||
|
" (now " + this.eventCount + ")");
|
||||||
|
// remove some events from the other end, if necessary
|
||||||
|
const excess = this.eventCount - this.windowLimit;
|
||||||
|
if (excess > 0) {
|
||||||
|
this.unpaginate(excess, direction != EventTimeline.BACKWARDS);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this window can be extended
|
||||||
|
*
|
||||||
|
* <p>This returns true if we either have more events, or if we have a
|
||||||
|
* pagination token which means we can paginate in that direction. It does not
|
||||||
|
* necessarily mean that there are more events available in that direction at
|
||||||
|
* this time.
|
||||||
|
*
|
||||||
|
* @param {string} direction EventTimeline.BACKWARDS to check if we can
|
||||||
|
* paginate backwards; EventTimeline.FORWARDS to check if we can go forwards
|
||||||
|
*
|
||||||
|
* @return {boolean} true if we can paginate in the given direction
|
||||||
|
*/
|
||||||
|
public canPaginate(direction: Direction): boolean {
|
||||||
|
const tl = this.getTimelineIndex(direction);
|
||||||
|
|
||||||
|
if (!tl) {
|
||||||
|
debuglog("TimelineWindow: no timeline yet");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (direction == EventTimeline.BACKWARDS) {
|
||||||
|
if (tl.index > tl.minIndex()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (tl.index < tl.maxIndex()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Boolean(tl.timeline.getNeighbouringTimeline(direction) ||
|
||||||
|
tl.timeline.getPaginationToken(direction));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to extend the window
|
||||||
|
*
|
||||||
|
* @param {string} direction EventTimeline.BACKWARDS to extend the window
|
||||||
|
* backwards (towards older events); EventTimeline.FORWARDS to go forwards.
|
||||||
|
*
|
||||||
|
* @param {number} size number of events to try to extend by. If fewer than this
|
||||||
|
* number are immediately available, then we return immediately rather than
|
||||||
|
* making an API call.
|
||||||
|
*
|
||||||
|
* @param {boolean} [makeRequest = true] whether we should make API calls to
|
||||||
|
* fetch further events if we don't have any at all. (This has no effect if
|
||||||
|
* the room already knows about additional events in the relevant direction,
|
||||||
|
* even if there are fewer than 'size' of them, as we will just return those
|
||||||
|
* we already know about.)
|
||||||
|
*
|
||||||
|
* @param {number} [requestLimit = 5] limit for the number of API requests we
|
||||||
|
* should make.
|
||||||
|
*
|
||||||
|
* @return {Promise} Resolves to a boolean which is true if more events
|
||||||
|
* were successfully retrieved.
|
||||||
|
*/
|
||||||
|
public paginate(direction: Direction, size: number, makeRequest: boolean, requestLimit: number): Promise<boolean> {
|
||||||
|
// Either wind back the message cap (if there are enough events in the
|
||||||
|
// timeline to do so), or fire off a pagination request.
|
||||||
|
|
||||||
|
if (makeRequest === undefined) {
|
||||||
|
makeRequest = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestLimit === undefined) {
|
||||||
|
requestLimit = DEFAULT_PAGINATE_LOOP_LIMIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tl = this.getTimelineIndex(direction);
|
||||||
|
|
||||||
|
if (!tl) {
|
||||||
|
debuglog("TimelineWindow: no timeline yet");
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tl.pendingPaginate) {
|
||||||
|
return tl.pendingPaginate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// try moving the cap
|
||||||
|
if (this.extend(direction, size)) {
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!makeRequest || requestLimit === 0) {
|
||||||
|
// todo: should we return something different to indicate that there
|
||||||
|
// might be more events out there, but we haven't found them yet?
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// try making a pagination request
|
||||||
|
const token = tl.timeline.getPaginationToken(direction);
|
||||||
|
if (!token) {
|
||||||
|
debuglog("TimelineWindow: no token");
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
debuglog("TimelineWindow: starting request");
|
||||||
|
|
||||||
|
const prom = this.client.paginateEventTimeline(tl.timeline, {
|
||||||
|
backwards: direction == EventTimeline.BACKWARDS,
|
||||||
|
limit: size,
|
||||||
|
}).finally(function() {
|
||||||
|
tl.pendingPaginate = null;
|
||||||
|
}).then((r) => {
|
||||||
|
debuglog("TimelineWindow: request completed with result " + r);
|
||||||
|
if (!r) {
|
||||||
|
// end of timeline
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// recurse to advance the index into the results.
|
||||||
|
//
|
||||||
|
// If we don't get any new events, we want to make sure we keep asking
|
||||||
|
// the server for events for as long as we have a valid pagination
|
||||||
|
// token. In particular, we want to know if we've actually hit the
|
||||||
|
// start of the timeline, or if we just happened to know about all of
|
||||||
|
// the events thanks to https://matrix.org/jira/browse/SYN-645.
|
||||||
|
//
|
||||||
|
// On the other hand, we necessarily want to wait forever for the
|
||||||
|
// server to make its mind up about whether there are other events,
|
||||||
|
// because it gives a bad user experience
|
||||||
|
// (https://github.com/vector-im/vector-web/issues/1204).
|
||||||
|
return this.paginate(direction, size, true, requestLimit - 1);
|
||||||
|
});
|
||||||
|
tl.pendingPaginate = prom;
|
||||||
|
return prom;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove `delta` events from the start or end of the timeline.
|
||||||
|
*
|
||||||
|
* @param {number} delta number of events to remove from the timeline
|
||||||
|
* @param {boolean} startOfTimeline if events should be removed from the start
|
||||||
|
* of the timeline.
|
||||||
|
*/
|
||||||
|
public unpaginate(delta: number, startOfTimeline: boolean): void {
|
||||||
|
const tl = startOfTimeline ? this.start : this.end;
|
||||||
|
|
||||||
|
// sanity-check the delta
|
||||||
|
if (delta > this.eventCount || delta < 0) {
|
||||||
|
throw new Error("Attemting to unpaginate " + delta + " events, but " +
|
||||||
|
"only have " + this.eventCount + " in the timeline");
|
||||||
|
}
|
||||||
|
|
||||||
|
while (delta > 0) {
|
||||||
|
const count = startOfTimeline ? tl.advance(delta) : tl.retreat(delta);
|
||||||
|
if (count <= 0) {
|
||||||
|
// sadness. This shouldn't be possible.
|
||||||
|
throw new Error(
|
||||||
|
"Unable to unpaginate any further, but still have " +
|
||||||
|
this.eventCount + " events");
|
||||||
|
}
|
||||||
|
|
||||||
|
delta -= count;
|
||||||
|
this.eventCount -= count;
|
||||||
|
debuglog("TimelineWindow.unpaginate: dropped " + count +
|
||||||
|
" (now " + this.eventCount + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of the events currently in the window
|
||||||
|
*
|
||||||
|
* @return {MatrixEvent[]} the events in the window
|
||||||
|
*/
|
||||||
|
public getEvents(): MatrixEvent[] {
|
||||||
|
if (!this.start) {
|
||||||
|
// not yet loaded
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
// iterate through each timeline between this.start and this.end
|
||||||
|
// (inclusive).
|
||||||
|
let timeline = this.start.timeline;
|
||||||
|
// eslint-disable-next-line no-constant-condition
|
||||||
|
while (true) {
|
||||||
|
const events = timeline.getEvents();
|
||||||
|
|
||||||
|
// For the first timeline in the chain, we want to start at
|
||||||
|
// this.start.index. For the last timeline in the chain, we want to
|
||||||
|
// stop before this.end.index. Otherwise, we want to copy all of the
|
||||||
|
// events in the timeline.
|
||||||
|
//
|
||||||
|
// (Note that both this.start.index and this.end.index are relative
|
||||||
|
// to their respective timelines' BaseIndex).
|
||||||
|
//
|
||||||
|
let startIndex = 0;
|
||||||
|
let endIndex = events.length;
|
||||||
|
if (timeline === this.start.timeline) {
|
||||||
|
startIndex = this.start.index + timeline.getBaseIndex();
|
||||||
|
}
|
||||||
|
if (timeline === this.end.timeline) {
|
||||||
|
endIndex = this.end.index + timeline.getBaseIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = startIndex; i < endIndex; i++) {
|
||||||
|
result.push(events[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we're not done, iterate to the next timeline.
|
||||||
|
if (timeline === this.end.timeline) {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
timeline = timeline.getNeighbouringTimeline(EventTimeline.FORWARDS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* a thing which contains a timeline reference, and an index into it.
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
* @param {EventTimeline} timeline
|
||||||
|
* @param {number} index
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
export class TimelineIndex {
|
||||||
|
public pendingPaginate?: Promise<boolean>;
|
||||||
|
|
||||||
|
// index: the indexes are relative to BaseIndex, so could well be negative.
|
||||||
|
constructor(public timeline: EventTimeline, public index: number) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {number} the minimum possible value for the index in the current
|
||||||
|
* timeline
|
||||||
|
*/
|
||||||
|
public minIndex(): number {
|
||||||
|
return this.timeline.getBaseIndex() * -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {number} the maximum possible value for the index in the current
|
||||||
|
* timeline (exclusive - ie, it actually returns one more than the index
|
||||||
|
* of the last element).
|
||||||
|
*/
|
||||||
|
public maxIndex(): number {
|
||||||
|
return this.timeline.getEvents().length - this.timeline.getBaseIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try move the index forward, or into the neighbouring timeline
|
||||||
|
*
|
||||||
|
* @param {number} delta number of events to advance by
|
||||||
|
* @return {number} number of events successfully advanced by
|
||||||
|
*/
|
||||||
|
public advance(delta: number): number {
|
||||||
|
if (!delta) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// first try moving the index in the current timeline. See if there is room
|
||||||
|
// to do so.
|
||||||
|
let cappedDelta;
|
||||||
|
if (delta < 0) {
|
||||||
|
// we want to wind the index backwards.
|
||||||
|
//
|
||||||
|
// (this.minIndex() - this.index) is a negative number whose magnitude
|
||||||
|
// is the amount of room we have to wind back the index in the current
|
||||||
|
// timeline. We cap delta to this quantity.
|
||||||
|
cappedDelta = Math.max(delta, this.minIndex() - this.index);
|
||||||
|
if (cappedDelta < 0) {
|
||||||
|
this.index += cappedDelta;
|
||||||
|
return cappedDelta;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// we want to wind the index forwards.
|
||||||
|
//
|
||||||
|
// (this.maxIndex() - this.index) is a (positive) number whose magnitude
|
||||||
|
// is the amount of room we have to wind forward the index in the current
|
||||||
|
// timeline. We cap delta to this quantity.
|
||||||
|
cappedDelta = Math.min(delta, this.maxIndex() - this.index);
|
||||||
|
if (cappedDelta > 0) {
|
||||||
|
this.index += cappedDelta;
|
||||||
|
return cappedDelta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the index is already at the start/end of the current timeline.
|
||||||
|
//
|
||||||
|
// next see if there is a neighbouring timeline to switch to.
|
||||||
|
const neighbour = this.timeline.getNeighbouringTimeline(
|
||||||
|
delta < 0 ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS);
|
||||||
|
if (neighbour) {
|
||||||
|
this.timeline = neighbour;
|
||||||
|
if (delta < 0) {
|
||||||
|
this.index = this.maxIndex();
|
||||||
|
} else {
|
||||||
|
this.index = this.minIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
debuglog("paginate: switched to new neighbour");
|
||||||
|
|
||||||
|
// recurse, using the next timeline
|
||||||
|
return this.advance(delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try move the index backwards, or into the neighbouring timeline
|
||||||
|
*
|
||||||
|
* @param {number} delta number of events to retreat by
|
||||||
|
* @return {number} number of events successfully retreated by
|
||||||
|
*/
|
||||||
|
public retreat(delta: number): number {
|
||||||
|
return this.advance(delta * -1) * -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user