init
This commit is contained in:
795
node_modules/matrix-js-sdk/lib/models/event-timeline-set.js
generated
vendored
Normal file
795
node_modules/matrix-js-sdk/lib/models/event-timeline-set.js
generated
vendored
Normal file
@@ -0,0 +1,795 @@
|
||||
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import { EventTimeline } from "./event-timeline.js";
|
||||
import { logger } from "../logger.js";
|
||||
import { RoomEvent } from "./room.js";
|
||||
import { TypedEventEmitter } from "./typed-event-emitter.js";
|
||||
import { RelationsContainer } from "./relations-container.js";
|
||||
const DEBUG = true;
|
||||
|
||||
/* istanbul ignore next */
|
||||
let debuglog;
|
||||
if (DEBUG) {
|
||||
// using bind means that we get to keep useful line numbers in the console
|
||||
debuglog = logger.log.bind(logger);
|
||||
} else {
|
||||
/* istanbul ignore next */
|
||||
debuglog = function () {};
|
||||
}
|
||||
export let DuplicateStrategy = /*#__PURE__*/function (DuplicateStrategy) {
|
||||
DuplicateStrategy["Ignore"] = "ignore";
|
||||
DuplicateStrategy["Replace"] = "replace";
|
||||
return DuplicateStrategy;
|
||||
}({});
|
||||
export class EventTimelineSet extends TypedEventEmitter {
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param room - Room for this timelineSet. May be null for non-room cases, such as the
|
||||
* notification timeline.
|
||||
* @param opts - Options inherited from Room.
|
||||
* @param client - the Matrix client which owns this EventTimelineSet,
|
||||
* can be omitted if room is specified.
|
||||
* @param thread - the thread to which this timeline set relates.
|
||||
* @param threadListType - the type of thread list represented, if any
|
||||
* (e.g., All threads or My threads)
|
||||
*/
|
||||
constructor(room, opts = {}, client, thread, threadListType = null) {
|
||||
super();
|
||||
_defineProperty(this, "relations", void 0);
|
||||
_defineProperty(this, "timelineSupport", void 0);
|
||||
_defineProperty(this, "displayPendingEvents", void 0);
|
||||
_defineProperty(this, "liveTimeline", void 0);
|
||||
_defineProperty(this, "timelines", void 0);
|
||||
_defineProperty(this, "_eventIdToTimeline", new Map());
|
||||
_defineProperty(this, "filter", void 0);
|
||||
this.room = room;
|
||||
this.thread = thread;
|
||||
this.threadListType = threadListType;
|
||||
this.timelineSupport = Boolean(opts.timelineSupport);
|
||||
this.liveTimeline = new EventTimeline(this);
|
||||
this.displayPendingEvents = opts.pendingEvents !== false;
|
||||
|
||||
// just a list - *not* ordered.
|
||||
this.timelines = [this.liveTimeline];
|
||||
this._eventIdToTimeline = new Map();
|
||||
this.filter = opts.filter;
|
||||
this.relations = this.room?.relations ?? new RelationsContainer(room?.client ?? client);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the timelines in this set
|
||||
* @returns the timelines in this set
|
||||
*/
|
||||
getTimelines() {
|
||||
return this.timelines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the filter object this timeline set is filtered on, if any
|
||||
* @returns the optional filter for this timelineSet
|
||||
*/
|
||||
getFilter() {
|
||||
return this.filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the filter object this timeline set is filtered on
|
||||
* (passed to the server when paginating via /messages).
|
||||
* @param filter - the filter for this timelineSet
|
||||
*/
|
||||
setFilter(filter) {
|
||||
this.filter = filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of pending sent events for this timelineSet's room, filtered
|
||||
* by the timelineSet's filter if appropriate.
|
||||
*
|
||||
* @returns A list of the sent events
|
||||
* waiting for remote echo.
|
||||
*
|
||||
* @throws If `opts.pendingEventOrdering` was not 'detached'
|
||||
*/
|
||||
getPendingEvents() {
|
||||
if (!this.room || !this.displayPendingEvents) {
|
||||
return [];
|
||||
}
|
||||
return this.room.getPendingEvents();
|
||||
}
|
||||
/**
|
||||
* Get the live timeline for this room.
|
||||
*
|
||||
* @returns live timeline
|
||||
*/
|
||||
getLiveTimeline() {
|
||||
return this.liveTimeline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the live timeline for this room.
|
||||
*
|
||||
* @returns live timeline
|
||||
*/
|
||||
setLiveTimeline(timeline) {
|
||||
this.liveTimeline = timeline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the timeline (if any) this event is in.
|
||||
* @param eventId - the eventId being sought
|
||||
* @returns timeline
|
||||
*/
|
||||
eventIdToTimeline(eventId) {
|
||||
return this._eventIdToTimeline.get(eventId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Track a new event as if it were in the same timeline as an old event,
|
||||
* replacing it.
|
||||
* @param oldEventId - event ID of the original event
|
||||
* @param newEventId - event ID of the replacement event
|
||||
*/
|
||||
replaceEventId(oldEventId, newEventId) {
|
||||
const existingTimeline = this._eventIdToTimeline.get(oldEventId);
|
||||
if (existingTimeline) {
|
||||
this._eventIdToTimeline.delete(oldEventId);
|
||||
this._eventIdToTimeline.set(newEventId, existingTimeline);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the live timeline, and start a new one.
|
||||
*
|
||||
* <p>This is used when /sync returns a 'limited' timeline.
|
||||
*
|
||||
* @param backPaginationToken - token for back-paginating the new timeline
|
||||
* @param forwardPaginationToken - token for forward-paginating the old live timeline,
|
||||
* if absent or null, all timelines are reset.
|
||||
*
|
||||
* @remarks
|
||||
* Fires {@link RoomEvent.TimelineReset}
|
||||
*/
|
||||
resetLiveTimeline(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 = new Map();
|
||||
} 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 ?? null, EventTimeline.BACKWARDS);
|
||||
|
||||
// Now we can swap the live timeline to the new one.
|
||||
this.liveTimeline = newTimeline;
|
||||
this.emit(RoomEvent.TimelineReset, this.room, this, resetAllTimelines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the timeline which contains the given event, if any
|
||||
*
|
||||
* @param eventId - event ID to look for
|
||||
* @returns timeline containing
|
||||
* the given event, or null if unknown
|
||||
*/
|
||||
getTimelineForEvent(eventId) {
|
||||
if (eventId === null || eventId === undefined) {
|
||||
return null;
|
||||
}
|
||||
const res = this._eventIdToTimeline.get(eventId);
|
||||
return res === undefined ? null : res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an event which is stored in our timelines
|
||||
*
|
||||
* @param eventId - event ID to look for
|
||||
* @returns the given event, or undefined if unknown
|
||||
*/
|
||||
findEventById(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
|
||||
*
|
||||
* @returns newly-created timeline
|
||||
*/
|
||||
addTimeline() {
|
||||
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 events - A list of events to add.
|
||||
*
|
||||
* @param 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 timeline - timeline to
|
||||
* add events to.
|
||||
*
|
||||
* @param paginationToken - token for the next batch of events
|
||||
*
|
||||
* @remarks
|
||||
* Fires {@link RoomEvent.Timeline}
|
||||
*
|
||||
*/
|
||||
addEventsToTimeline(events, toStartOfTimeline, addToState, 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 (const event of events) {
|
||||
const eventId = event.getId();
|
||||
const existingTimeline = this._eventIdToTimeline.get(eventId);
|
||||
if (!existingTimeline) {
|
||||
// we don't know about this event yet. Just add it to the timeline.
|
||||
this.addEventToTimeline(event, timeline, {
|
||||
toStartOfTimeline,
|
||||
addToState
|
||||
});
|
||||
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 ?? null, direction);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an event to the end of this live timeline.
|
||||
*
|
||||
* @param event - Event to be added
|
||||
* @param options - addLiveEvent options
|
||||
*/
|
||||
addLiveEvent(event, {
|
||||
duplicateStrategy,
|
||||
fromCache,
|
||||
roomState,
|
||||
timelineWasEmpty,
|
||||
addToState
|
||||
}) {
|
||||
if (this.filter) {
|
||||
const events = this.filter.filterRoomTimeline([event]);
|
||||
if (!events.length) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const timeline = this._eventIdToTimeline.get(event.getId());
|
||||
if (timeline) {
|
||||
if (duplicateStrategy === 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
|
||||
if (!roomState) {
|
||||
roomState = timeline.getState(EventTimeline.FORWARDS);
|
||||
}
|
||||
EventTimeline.setEventMetadata(event, roomState, 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, {
|
||||
toStartOfTimeline: false,
|
||||
fromCache,
|
||||
roomState,
|
||||
timelineWasEmpty,
|
||||
addToState
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 event - the event to add
|
||||
* @param timeline - the timeline onto which to add it
|
||||
* @param options - addEventToTimeline options
|
||||
*
|
||||
* @remarks
|
||||
* Fires {@link RoomEvent.Timeline}
|
||||
*/
|
||||
addEventToTimeline(event, timeline, {
|
||||
toStartOfTimeline,
|
||||
fromCache = false,
|
||||
roomState,
|
||||
timelineWasEmpty,
|
||||
addToState
|
||||
}) {
|
||||
if (timeline.getTimelineSet() !== this) {
|
||||
throw new Error(`EventTimelineSet.addEventToTimeline: Timeline=${timeline.toString()} does not belong " +
|
||||
"in timelineSet(threadId=${this.thread?.id})`);
|
||||
}
|
||||
const eventId = event.getId();
|
||||
this.relations.aggregateParentEvent(event);
|
||||
this.relations.aggregateChildEvent(event, this);
|
||||
|
||||
// Make sure events don't get mixed in timelines they shouldn't be in (e.g. a
|
||||
// threaded message should not be in the main timeline).
|
||||
//
|
||||
// We can only run this check for timelines with a `room` because `canContain`
|
||||
// requires it
|
||||
if (this.room && !this.canContain(event)) {
|
||||
let eventDebugString = `event=${eventId}`;
|
||||
if (event.threadRootId) {
|
||||
eventDebugString += `(belongs to thread=${event.threadRootId})`;
|
||||
}
|
||||
logger.warn(`EventTimelineSet.addEventToTimeline: Ignoring ${eventDebugString} that does not belong ` + `in timeline=${timeline.toString()} timelineSet(threadId=${this.thread?.id})`);
|
||||
return;
|
||||
}
|
||||
timeline.addEvent(event, {
|
||||
toStartOfTimeline,
|
||||
roomState,
|
||||
timelineWasEmpty,
|
||||
addToState
|
||||
});
|
||||
this._eventIdToTimeline.set(eventId, timeline);
|
||||
const data = {
|
||||
timeline: timeline,
|
||||
liveEvent: !toStartOfTimeline && timeline == this.liveTimeline && !fromCache
|
||||
};
|
||||
this.emit(RoomEvent.Timeline, event, this.room, Boolean(toStartOfTimeline), false, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert event to the given timeline, and emit Room.timeline. Assumes
|
||||
* we have already checked we don't know about this event.
|
||||
*
|
||||
* TEMPORARY: until we have recursive relations, we need this function
|
||||
* to exist to allow us to insert events in timeline order, which is our
|
||||
* best guess for Sync Order.
|
||||
* This is a copy of addEventToTimeline above, modified to insert the event
|
||||
* after the event it relates to, and before any event with a later
|
||||
* timestamp. This is our best guess at Sync Order.
|
||||
*
|
||||
* Will fire "Room.timeline" for each event added.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @remarks
|
||||
* Fires {@link RoomEvent.Timeline}
|
||||
*/
|
||||
insertEventIntoTimeline(event, timeline, roomState, addToState) {
|
||||
if (timeline.getTimelineSet() !== this) {
|
||||
throw new Error(`EventTimelineSet.insertEventIntoTimeline: Timeline=${timeline.toString()} does not belong " +
|
||||
"in timelineSet(threadId=${this.thread?.id})`);
|
||||
}
|
||||
const eventId = event.getId();
|
||||
this.relations.aggregateParentEvent(event);
|
||||
this.relations.aggregateChildEvent(event, this);
|
||||
|
||||
// Make sure events don't get mixed in timelines they shouldn't be in (e.g. a
|
||||
// threaded message should not be in the main timeline).
|
||||
//
|
||||
// We can only run this check for timelines with a `room` because `canContain`
|
||||
// requires it
|
||||
if (this.room && !this.canContain(event)) {
|
||||
let eventDebugString = `event=${eventId}`;
|
||||
if (event.threadRootId) {
|
||||
eventDebugString += `(belongs to thread=${event.threadRootId})`;
|
||||
}
|
||||
logger.warn(`EventTimelineSet.insertEventIntoTimeline: Ignoring ${eventDebugString} that does not belong ` + `in timeline=${timeline.toString()} timelineSet(threadId=${this.thread?.id})`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the event that this event is related to - the "parent"
|
||||
const parentEventId = event.relationEventId;
|
||||
if (!parentEventId) {
|
||||
// Not related to anything - we just append
|
||||
this.addEventToTimeline(event, timeline, {
|
||||
toStartOfTimeline: false,
|
||||
fromCache: false,
|
||||
timelineWasEmpty: false,
|
||||
roomState,
|
||||
addToState
|
||||
});
|
||||
return;
|
||||
}
|
||||
const parentEvent = this.findEventById(parentEventId);
|
||||
const timelineEvents = timeline.getEvents();
|
||||
|
||||
// Start searching from the parent event, or if it's not loaded, start
|
||||
// at the beginning and insert purely using timestamp order.
|
||||
const parentIndex = parentEvent !== undefined ? timelineEvents.indexOf(parentEvent) : 0;
|
||||
let insertIndex = parentIndex;
|
||||
for (; insertIndex < timelineEvents.length; insertIndex++) {
|
||||
const nextEvent = timelineEvents[insertIndex];
|
||||
if (nextEvent.getTs() > event.getTs()) {
|
||||
// We found an event later than ours, so insert before that.
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If we got to the end of the loop, insertIndex points at the end of
|
||||
// the list.
|
||||
|
||||
timeline.insertEvent(event, insertIndex, roomState, addToState);
|
||||
this._eventIdToTimeline.set(eventId, timeline);
|
||||
const data = {
|
||||
timeline: timeline,
|
||||
// The purpose of this method is inserting events in the middle of the
|
||||
// timeline, so the events are, by definition, not live (whether or not
|
||||
// we're adding them to the live timeline).
|
||||
liveEvent: false
|
||||
};
|
||||
this.emit(RoomEvent.Timeline, event, this.room, false, 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 localEvent - the new event to be added to the timeline
|
||||
* @param oldEventId - the ID of the original event
|
||||
* @param newEventId - the ID of the replacement event
|
||||
*
|
||||
* @remarks
|
||||
* Fires {@link RoomEvent.Timeline}
|
||||
*/
|
||||
handleRemoteEcho(localEvent, oldEventId, newEventId) {
|
||||
// XXX: why don't we infer newEventId from localEvent?
|
||||
const existingTimeline = this._eventIdToTimeline.get(oldEventId);
|
||||
if (existingTimeline) {
|
||||
this._eventIdToTimeline.delete(oldEventId);
|
||||
this._eventIdToTimeline.set(newEventId, existingTimeline);
|
||||
} else if (!this.filter || this.filter.filterRoomTimeline([localEvent]).length) {
|
||||
this.addEventToTimeline(localEvent, this.liveTimeline, {
|
||||
toStartOfTimeline: false,
|
||||
addToState: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a single event from this room.
|
||||
*
|
||||
* @param eventId - The id of the event to remove
|
||||
*
|
||||
* @returns the removed event, or null if the event was not found
|
||||
* in this room.
|
||||
*/
|
||||
removeEvent(eventId) {
|
||||
const timeline = this._eventIdToTimeline.get(eventId);
|
||||
if (!timeline) {
|
||||
return null;
|
||||
}
|
||||
const removed = timeline.removeEvent(eventId);
|
||||
if (removed) {
|
||||
this._eventIdToTimeline.delete(eventId);
|
||||
const data = {
|
||||
timeline: timeline
|
||||
};
|
||||
this.emit(RoomEvent.Timeline, removed, this.room, undefined, true, data);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine where two events appear in the timeline relative to one another
|
||||
*
|
||||
* @param eventId1 - The id of the first event
|
||||
* @param eventId2 - The id of the second event
|
||||
* @returns -1 if eventId1 precedes eventId2, and +1 eventId1 succeeds
|
||||
* eventId2. 0 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).
|
||||
*/
|
||||
compareEventOrdering(eventId1, eventId2) {
|
||||
if (eventId1 == eventId2) {
|
||||
// optimise this case
|
||||
return 0;
|
||||
}
|
||||
const timeline1 = this._eventIdToTimeline.get(eventId1);
|
||||
const timeline2 = this._eventIdToTimeline.get(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 = undefined;
|
||||
let idx2 = undefined;
|
||||
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;
|
||||
}
|
||||
}
|
||||
const difference = idx1 - idx2;
|
||||
|
||||
// Return the sign of difference.
|
||||
if (difference < 0) {
|
||||
return -1;
|
||||
} else if (difference > 0) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether a given event can sanely be added to this event timeline set,
|
||||
* for timeline sets relating to a thread, only return true for events in the same
|
||||
* thread timeline, for timeline sets not relating to a thread only return true
|
||||
* for events which should be shown in the main room timeline.
|
||||
* Requires the `room` property to have been set at EventTimelineSet construction time.
|
||||
*
|
||||
* @param event - the event to check whether it belongs to this timeline set.
|
||||
* @throws Error if `room` was not set when constructing this timeline set.
|
||||
* @returns whether the event belongs to this timeline set.
|
||||
*/
|
||||
canContain(event) {
|
||||
if (!this.room) {
|
||||
throw new Error("Cannot call `EventTimelineSet::canContain without a `room` set. " + "Set the room when creating the EventTimelineSet to call this method.");
|
||||
}
|
||||
const {
|
||||
threadId,
|
||||
shouldLiveInRoom,
|
||||
shouldLiveInThread
|
||||
} = this.room.eventShouldLiveIn(event);
|
||||
if (this.thread) {
|
||||
return this.thread.id === threadId;
|
||||
}
|
||||
if (!shouldLiveInRoom && !shouldLiveInThread) {
|
||||
logger.warn(`EventTimelineSet:canContain event encountered which cannot be added to any timeline roomId=${this.room?.roomId} eventId=${event.getId()} threadId=${event.threadRootId}`);
|
||||
}
|
||||
return shouldLiveInRoom;
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=event-timeline-set.js.map
|
||||
Reference in New Issue
Block a user