1 | ;
|
2 |
|
3 | var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard");
|
4 |
|
5 | Object.defineProperty(exports, "__esModule", {
|
6 | value: true
|
7 | });
|
8 | exports.EventTimelineSet = EventTimelineSet;
|
9 |
|
10 | var _events = require("events");
|
11 |
|
12 | var _eventTimeline = require("./event-timeline");
|
13 |
|
14 | var _event = require("./event");
|
15 |
|
16 | var utils = _interopRequireWildcard(require("../utils"));
|
17 |
|
18 | var _logger = require("../logger");
|
19 |
|
20 | var _relations = require("./relations");
|
21 |
|
22 | /*
|
23 | Copyright 2016 OpenMarket Ltd
|
24 | Copyright 2019 The Matrix.org Foundation C.I.C.
|
25 |
|
26 | Licensed under the Apache License, Version 2.0 (the "License");
|
27 | you may not use this file except in compliance with the License.
|
28 | You may obtain a copy of the License at
|
29 |
|
30 | http://www.apache.org/licenses/LICENSE-2.0
|
31 |
|
32 | Unless required by applicable law or agreed to in writing, software
|
33 | distributed under the License is distributed on an "AS IS" BASIS,
|
34 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
35 | See the License for the specific language governing permissions and
|
36 | limitations under the License.
|
37 | */
|
38 |
|
39 | /**
|
40 | * @module models/event-timeline-set
|
41 | */
|
42 | // var DEBUG = false;
|
43 | const DEBUG = true;
|
44 | let debuglog;
|
45 |
|
46 | if (DEBUG) {
|
47 | // using bind means that we get to keep useful line numbers in the console
|
48 | debuglog = _logger.logger.log.bind(_logger.logger);
|
49 | } else {
|
50 | debuglog = function () {};
|
51 | }
|
52 | /**
|
53 | * Construct a set of EventTimeline objects, typically on behalf of a given
|
54 | * room. A room may have multiple EventTimelineSets for different levels
|
55 | * of filtering. The global notification list is also an EventTimelineSet, but
|
56 | * lacks a room.
|
57 | *
|
58 | * <p>This is an ordered sequence of timelines, which may or may not
|
59 | * be continuous. Each timeline lists a series of events, as well as tracking
|
60 | * the room state at the start and the end of the timeline (if appropriate).
|
61 | * It also tracks forward and backward pagination tokens, as well as containing
|
62 | * links to the next timeline in the sequence.
|
63 | *
|
64 | * <p>There is one special timeline - the 'live' timeline, which represents the
|
65 | * timeline to which events are being added in real-time as they are received
|
66 | * from the /sync API. Note that you should not retain references to this
|
67 | * timeline - even if it is the current timeline right now, it may not remain
|
68 | * so if the server gives us a timeline gap in /sync.
|
69 | *
|
70 | * <p>In order that we can find events from their ids later, we also maintain a
|
71 | * map from event_id to timeline and index.
|
72 | *
|
73 | * @constructor
|
74 | * @param {?Room} room
|
75 | * Room for this timelineSet. May be null for non-room cases, such as the
|
76 | * notification timeline.
|
77 | * @param {Object} opts Options inherited from Room.
|
78 | *
|
79 | * @param {boolean} [opts.timelineSupport = false]
|
80 | * Set to true to enable improved timeline support.
|
81 | * @param {Object} [opts.filter = null]
|
82 | * The filter object, if any, for this timelineSet.
|
83 | * @param {boolean} [opts.unstableClientRelationAggregation = false]
|
84 | * Optional. Set to true to enable client-side aggregation of event relations
|
85 | * via `getRelationsForEvent`.
|
86 | * This feature is currently unstable and the API may change without notice.
|
87 | */
|
88 |
|
89 |
|
90 | function EventTimelineSet(room, opts) {
|
91 | this.room = room;
|
92 | this._timelineSupport = Boolean(opts.timelineSupport);
|
93 | this._liveTimeline = new _eventTimeline.EventTimeline(this);
|
94 | this._unstableClientRelationAggregation = !!opts.unstableClientRelationAggregation; // just a list - *not* ordered.
|
95 |
|
96 | this._timelines = [this._liveTimeline];
|
97 | this._eventIdToTimeline = {};
|
98 | this._filter = opts.filter || null;
|
99 |
|
100 | if (this._unstableClientRelationAggregation) {
|
101 | // A tree of objects to access a set of relations for an event, as in:
|
102 | // this._relations[relatesToEventId][relationType][relationEventType]
|
103 | this._relations = {};
|
104 | }
|
105 | }
|
106 |
|
107 | utils.inherits(EventTimelineSet, _events.EventEmitter);
|
108 | /**
|
109 | * Get all the timelines in this set
|
110 | * @return {module:models/event-timeline~EventTimeline[]} the timelines in this set
|
111 | */
|
112 |
|
113 | EventTimelineSet.prototype.getTimelines = function () {
|
114 | return this._timelines;
|
115 | };
|
116 | /**
|
117 | * Get the filter object this timeline set is filtered on, if any
|
118 | * @return {?Filter} the optional filter for this timelineSet
|
119 | */
|
120 |
|
121 |
|
122 | EventTimelineSet.prototype.getFilter = function () {
|
123 | return this._filter;
|
124 | };
|
125 | /**
|
126 | * Set the filter object this timeline set is filtered on
|
127 | * (passed to the server when paginating via /messages).
|
128 | * @param {Filter} filter the filter for this timelineSet
|
129 | */
|
130 |
|
131 |
|
132 | EventTimelineSet.prototype.setFilter = function (filter) {
|
133 | this._filter = filter;
|
134 | };
|
135 | /**
|
136 | * Get the list of pending sent events for this timelineSet's room, filtered
|
137 | * by the timelineSet's filter if appropriate.
|
138 | *
|
139 | * @return {module:models/event.MatrixEvent[]} A list of the sent events
|
140 | * waiting for remote echo.
|
141 | *
|
142 | * @throws If <code>opts.pendingEventOrdering</code> was not 'detached'
|
143 | */
|
144 |
|
145 |
|
146 | EventTimelineSet.prototype.getPendingEvents = function () {
|
147 | if (!this.room) {
|
148 | return [];
|
149 | }
|
150 |
|
151 | if (this._filter) {
|
152 | return this._filter.filterRoomTimeline(this.room.getPendingEvents());
|
153 | } else {
|
154 | return this.room.getPendingEvents();
|
155 | }
|
156 | };
|
157 | /**
|
158 | * Get the live timeline for this room.
|
159 | *
|
160 | * @return {module:models/event-timeline~EventTimeline} live timeline
|
161 | */
|
162 |
|
163 |
|
164 | EventTimelineSet.prototype.getLiveTimeline = function () {
|
165 | return this._liveTimeline;
|
166 | };
|
167 | /**
|
168 | * Return the timeline (if any) this event is in.
|
169 | * @param {String} eventId the eventId being sought
|
170 | * @return {module:models/event-timeline~EventTimeline} timeline
|
171 | */
|
172 |
|
173 |
|
174 | EventTimelineSet.prototype.eventIdToTimeline = function (eventId) {
|
175 | return this._eventIdToTimeline[eventId];
|
176 | };
|
177 | /**
|
178 | * Track a new event as if it were in the same timeline as an old event,
|
179 | * replacing it.
|
180 | * @param {String} oldEventId event ID of the original event
|
181 | * @param {String} newEventId event ID of the replacement event
|
182 | */
|
183 |
|
184 |
|
185 | EventTimelineSet.prototype.replaceEventId = function (oldEventId, newEventId) {
|
186 | const existingTimeline = this._eventIdToTimeline[oldEventId];
|
187 |
|
188 | if (existingTimeline) {
|
189 | delete this._eventIdToTimeline[oldEventId];
|
190 | this._eventIdToTimeline[newEventId] = existingTimeline;
|
191 | }
|
192 | };
|
193 | /**
|
194 | * Reset the live timeline, and start a new one.
|
195 | *
|
196 | * <p>This is used when /sync returns a 'limited' timeline.
|
197 | *
|
198 | * @param {string=} backPaginationToken token for back-paginating the new timeline
|
199 | * @param {string=} forwardPaginationToken token for forward-paginating the old live timeline,
|
200 | * if absent or null, all timelines are reset.
|
201 | *
|
202 | * @fires module:client~MatrixClient#event:"Room.timelineReset"
|
203 | */
|
204 |
|
205 |
|
206 | EventTimelineSet.prototype.resetLiveTimeline = function (backPaginationToken, forwardPaginationToken) {
|
207 | // Each EventTimeline has RoomState objects tracking the state at the start
|
208 | // and end of that timeline. The copies at the end of the live timeline are
|
209 | // special because they will have listeners attached to monitor changes to
|
210 | // the current room state, so we move this RoomState from the end of the
|
211 | // current live timeline to the end of the new one and, if necessary,
|
212 | // replace it with a newly created one. We also make a copy for the start
|
213 | // of the new timeline.
|
214 | // if timeline support is disabled, forget about the old timelines
|
215 | const resetAllTimelines = !this._timelineSupport || !forwardPaginationToken;
|
216 | const oldTimeline = this._liveTimeline;
|
217 | const newTimeline = resetAllTimelines ? oldTimeline.forkLive(_eventTimeline.EventTimeline.FORWARDS) : oldTimeline.fork(_eventTimeline.EventTimeline.FORWARDS);
|
218 |
|
219 | if (resetAllTimelines) {
|
220 | this._timelines = [newTimeline];
|
221 | this._eventIdToTimeline = {};
|
222 | } else {
|
223 | this._timelines.push(newTimeline);
|
224 | }
|
225 |
|
226 | if (forwardPaginationToken) {
|
227 | // Now set the forward pagination token on the old live timeline
|
228 | // so it can be forward-paginated.
|
229 | oldTimeline.setPaginationToken(forwardPaginationToken, _eventTimeline.EventTimeline.FORWARDS);
|
230 | } // make sure we set the pagination token before firing timelineReset,
|
231 | // otherwise clients which start back-paginating will fail, and then get
|
232 | // stuck without realising that they *can* back-paginate.
|
233 |
|
234 |
|
235 | newTimeline.setPaginationToken(backPaginationToken, _eventTimeline.EventTimeline.BACKWARDS); // Now we can swap the live timeline to the new one.
|
236 |
|
237 | this._liveTimeline = newTimeline;
|
238 | this.emit("Room.timelineReset", this.room, this, resetAllTimelines);
|
239 | };
|
240 | /**
|
241 | * Get the timeline which contains the given event, if any
|
242 | *
|
243 | * @param {string} eventId event ID to look for
|
244 | * @return {?module:models/event-timeline~EventTimeline} timeline containing
|
245 | * the given event, or null if unknown
|
246 | */
|
247 |
|
248 |
|
249 | EventTimelineSet.prototype.getTimelineForEvent = function (eventId) {
|
250 | const res = this._eventIdToTimeline[eventId];
|
251 | return res === undefined ? null : res;
|
252 | };
|
253 | /**
|
254 | * Get an event which is stored in our timelines
|
255 | *
|
256 | * @param {string} eventId event ID to look for
|
257 | * @return {?module:models/event~MatrixEvent} the given event, or undefined if unknown
|
258 | */
|
259 |
|
260 |
|
261 | EventTimelineSet.prototype.findEventById = function (eventId) {
|
262 | const tl = this.getTimelineForEvent(eventId);
|
263 |
|
264 | if (!tl) {
|
265 | return undefined;
|
266 | }
|
267 |
|
268 | return utils.findElement(tl.getEvents(), function (ev) {
|
269 | return ev.getId() == eventId;
|
270 | });
|
271 | };
|
272 | /**
|
273 | * Add a new timeline to this timeline list
|
274 | *
|
275 | * @return {module:models/event-timeline~EventTimeline} newly-created timeline
|
276 | */
|
277 |
|
278 |
|
279 | EventTimelineSet.prototype.addTimeline = function () {
|
280 | if (!this._timelineSupport) {
|
281 | throw new Error("timeline support is disabled. Set the 'timelineSupport'" + " parameter to true when creating MatrixClient to enable" + " it.");
|
282 | }
|
283 |
|
284 | const timeline = new _eventTimeline.EventTimeline(this);
|
285 |
|
286 | this._timelines.push(timeline);
|
287 |
|
288 | return timeline;
|
289 | };
|
290 | /**
|
291 | * Add events to a timeline
|
292 | *
|
293 | * <p>Will fire "Room.timeline" for each event added.
|
294 | *
|
295 | * @param {MatrixEvent[]} events A list of events to add.
|
296 | *
|
297 | * @param {boolean} toStartOfTimeline True to add these events to the start
|
298 | * (oldest) instead of the end (newest) of the timeline. If true, the oldest
|
299 | * event will be the <b>last</b> element of 'events'.
|
300 | *
|
301 | * @param {module:models/event-timeline~EventTimeline} timeline timeline to
|
302 | * add events to.
|
303 | *
|
304 | * @param {string=} paginationToken token for the next batch of events
|
305 | *
|
306 | * @fires module:client~MatrixClient#event:"Room.timeline"
|
307 | *
|
308 | */
|
309 |
|
310 |
|
311 | EventTimelineSet.prototype.addEventsToTimeline = function (events, toStartOfTimeline, timeline, paginationToken) {
|
312 | if (!timeline) {
|
313 | throw new Error("'timeline' not specified for EventTimelineSet.addEventsToTimeline");
|
314 | }
|
315 |
|
316 | if (!toStartOfTimeline && timeline == this._liveTimeline) {
|
317 | throw new Error("EventTimelineSet.addEventsToTimeline cannot be used for adding events to " + "the live timeline - use Room.addLiveEvents instead");
|
318 | }
|
319 |
|
320 | if (this._filter) {
|
321 | events = this._filter.filterRoomTimeline(events);
|
322 |
|
323 | if (!events.length) {
|
324 | return;
|
325 | }
|
326 | }
|
327 |
|
328 | const direction = toStartOfTimeline ? _eventTimeline.EventTimeline.BACKWARDS : _eventTimeline.EventTimeline.FORWARDS;
|
329 | const inverseDirection = toStartOfTimeline ? _eventTimeline.EventTimeline.FORWARDS : _eventTimeline.EventTimeline.BACKWARDS; // Adding events to timelines can be quite complicated. The following
|
330 | // illustrates some of the corner-cases.
|
331 | //
|
332 | // Let's say we start by knowing about four timelines. timeline3 and
|
333 | // timeline4 are neighbours:
|
334 | //
|
335 | // timeline1 timeline2 timeline3 timeline4
|
336 | // [M] [P] [S] <------> [T]
|
337 | //
|
338 | // Now we paginate timeline1, and get the following events from the server:
|
339 | // [M, N, P, R, S, T, U].
|
340 | //
|
341 | // 1. First, we ignore event M, since we already know about it.
|
342 | //
|
343 | // 2. Next, we append N to timeline 1.
|
344 | //
|
345 | // 3. Next, we don't add event P, since we already know about it,
|
346 | // but we do link together the timelines. We now have:
|
347 | //
|
348 | // timeline1 timeline2 timeline3 timeline4
|
349 | // [M, N] <---> [P] [S] <------> [T]
|
350 | //
|
351 | // 4. Now we add event R to timeline2:
|
352 | //
|
353 | // timeline1 timeline2 timeline3 timeline4
|
354 | // [M, N] <---> [P, R] [S] <------> [T]
|
355 | //
|
356 | // Note that we have switched the timeline we are working on from
|
357 | // timeline1 to timeline2.
|
358 | //
|
359 | // 5. We ignore event S, but again join the timelines:
|
360 | //
|
361 | // timeline1 timeline2 timeline3 timeline4
|
362 | // [M, N] <---> [P, R] <---> [S] <------> [T]
|
363 | //
|
364 | // 6. We ignore event T, and the timelines are already joined, so there
|
365 | // is nothing to do.
|
366 | //
|
367 | // 7. Finally, we add event U to timeline4:
|
368 | //
|
369 | // timeline1 timeline2 timeline3 timeline4
|
370 | // [M, N] <---> [P, R] <---> [S] <------> [T, U]
|
371 | //
|
372 | // The important thing to note in the above is what happened when we
|
373 | // already knew about a given event:
|
374 | //
|
375 | // - if it was appropriate, we joined up the timelines (steps 3, 5).
|
376 | // - in any case, we started adding further events to the timeline which
|
377 | // contained the event we knew about (steps 3, 5, 6).
|
378 | //
|
379 | //
|
380 | // So much for adding events to the timeline. But what do we want to do
|
381 | // with the pagination token?
|
382 | //
|
383 | // In the case above, we will be given a pagination token which tells us how to
|
384 | // get events beyond 'U' - in this case, it makes sense to store this
|
385 | // against timeline4. But what if timeline4 already had 'U' and beyond? in
|
386 | // that case, our best bet is to throw away the pagination token we were
|
387 | // given and stick with whatever token timeline4 had previously. In short,
|
388 | // we want to only store the pagination token if the last event we receive
|
389 | // is one we didn't previously know about.
|
390 | //
|
391 | // We make an exception for this if it turns out that we already knew about
|
392 | // *all* of the events, and we weren't able to join up any timelines. When
|
393 | // that happens, it means our existing pagination token is faulty, since it
|
394 | // is only telling us what we already know. Rather than repeatedly
|
395 | // paginating with the same token, we might as well use the new pagination
|
396 | // token in the hope that we eventually work our way out of the mess.
|
397 |
|
398 | let didUpdate = false;
|
399 | let lastEventWasNew = false;
|
400 |
|
401 | for (let i = 0; i < events.length; i++) {
|
402 | const event = events[i];
|
403 | const eventId = event.getId();
|
404 | const existingTimeline = this._eventIdToTimeline[eventId];
|
405 |
|
406 | if (!existingTimeline) {
|
407 | // we don't know about this event yet. Just add it to the timeline.
|
408 | this.addEventToTimeline(event, timeline, toStartOfTimeline);
|
409 | lastEventWasNew = true;
|
410 | didUpdate = true;
|
411 | continue;
|
412 | }
|
413 |
|
414 | lastEventWasNew = false;
|
415 |
|
416 | if (existingTimeline == timeline) {
|
417 | debuglog("Event " + eventId + " already in timeline " + timeline);
|
418 | continue;
|
419 | }
|
420 |
|
421 | const neighbour = timeline.getNeighbouringTimeline(direction);
|
422 |
|
423 | if (neighbour) {
|
424 | // this timeline already has a neighbour in the relevant direction;
|
425 | // let's assume the timelines are already correctly linked up, and
|
426 | // skip over to it.
|
427 | //
|
428 | // there's probably some edge-case here where we end up with an
|
429 | // event which is in a timeline a way down the chain, and there is
|
430 | // a break in the chain somewhere. But I can't really imagine how
|
431 | // that would happen, so I'm going to ignore it for now.
|
432 | //
|
433 | if (existingTimeline == neighbour) {
|
434 | debuglog("Event " + eventId + " in neighbouring timeline - " + "switching to " + existingTimeline);
|
435 | } else {
|
436 | debuglog("Event " + eventId + " already in a different " + "timeline " + existingTimeline);
|
437 | }
|
438 |
|
439 | timeline = existingTimeline;
|
440 | continue;
|
441 | } // time to join the timelines.
|
442 |
|
443 |
|
444 | _logger.logger.info("Already have timeline for " + eventId + " - joining timeline " + timeline + " to " + existingTimeline); // Variables to keep the line length limited below.
|
445 |
|
446 |
|
447 | const existingIsLive = existingTimeline === this._liveTimeline;
|
448 | const timelineIsLive = timeline === this._liveTimeline;
|
449 | const backwardsIsLive = direction === _eventTimeline.EventTimeline.BACKWARDS && existingIsLive;
|
450 | const forwardsIsLive = direction === _eventTimeline.EventTimeline.FORWARDS && timelineIsLive;
|
451 |
|
452 | if (backwardsIsLive || forwardsIsLive) {
|
453 | // The live timeline should never be spliced into a non-live position.
|
454 | // We use independent logging to better discover the problem at a glance.
|
455 | if (backwardsIsLive) {
|
456 | _logger.logger.warn("Refusing to set a preceding existingTimeLine on our " + "timeline as the existingTimeLine is live (" + existingTimeline + ")");
|
457 | }
|
458 |
|
459 | if (forwardsIsLive) {
|
460 | _logger.logger.warn("Refusing to set our preceding timeline on a existingTimeLine " + "as our timeline is live (" + timeline + ")");
|
461 | }
|
462 |
|
463 | continue; // abort splicing - try next event
|
464 | }
|
465 |
|
466 | timeline.setNeighbouringTimeline(existingTimeline, direction);
|
467 | existingTimeline.setNeighbouringTimeline(timeline, inverseDirection);
|
468 | timeline = existingTimeline;
|
469 | didUpdate = true;
|
470 | } // see above - if the last event was new to us, or if we didn't find any
|
471 | // new information, we update the pagination token for whatever
|
472 | // timeline we ended up on.
|
473 |
|
474 |
|
475 | if (lastEventWasNew || !didUpdate) {
|
476 | if (direction === _eventTimeline.EventTimeline.FORWARDS && timeline === this._liveTimeline) {
|
477 | _logger.logger.warn({
|
478 | lastEventWasNew,
|
479 | didUpdate
|
480 | }); // for debugging
|
481 |
|
482 |
|
483 | _logger.logger.warn(`Refusing to set forwards pagination token of live timeline ` + `${timeline} to ${paginationToken}`);
|
484 |
|
485 | return;
|
486 | }
|
487 |
|
488 | timeline.setPaginationToken(paginationToken, direction);
|
489 | }
|
490 | };
|
491 | /**
|
492 | * Add an event to the end of this live timeline.
|
493 | *
|
494 | * @param {MatrixEvent} event Event to be added
|
495 | * @param {string?} duplicateStrategy 'ignore' or 'replace'
|
496 | */
|
497 |
|
498 |
|
499 | EventTimelineSet.prototype.addLiveEvent = function (event, duplicateStrategy) {
|
500 | if (this._filter) {
|
501 | const events = this._filter.filterRoomTimeline([event]);
|
502 |
|
503 | if (!events.length) {
|
504 | return;
|
505 | }
|
506 | }
|
507 |
|
508 | const timeline = this._eventIdToTimeline[event.getId()];
|
509 |
|
510 | if (timeline) {
|
511 | if (duplicateStrategy === "replace") {
|
512 | debuglog("EventTimelineSet.addLiveEvent: replacing duplicate event " + event.getId());
|
513 | const tlEvents = timeline.getEvents();
|
514 |
|
515 | for (let j = 0; j < tlEvents.length; j++) {
|
516 | if (tlEvents[j].getId() === event.getId()) {
|
517 | // still need to set the right metadata on this event
|
518 | _eventTimeline.EventTimeline.setEventMetadata(event, timeline.getState(_eventTimeline.EventTimeline.FORWARDS), false);
|
519 |
|
520 | if (!tlEvents[j].encryptedType) {
|
521 | tlEvents[j] = event;
|
522 | } // XXX: we need to fire an event when this happens.
|
523 |
|
524 |
|
525 | break;
|
526 | }
|
527 | }
|
528 | } else {
|
529 | debuglog("EventTimelineSet.addLiveEvent: ignoring duplicate event " + event.getId());
|
530 | }
|
531 |
|
532 | return;
|
533 | }
|
534 |
|
535 | this.addEventToTimeline(event, this._liveTimeline, false);
|
536 | };
|
537 | /**
|
538 | * Add event to the given timeline, and emit Room.timeline. Assumes
|
539 | * we have already checked we don't know about this event.
|
540 | *
|
541 | * Will fire "Room.timeline" for each event added.
|
542 | *
|
543 | * @param {MatrixEvent} event
|
544 | * @param {EventTimeline} timeline
|
545 | * @param {boolean} toStartOfTimeline
|
546 | *
|
547 | * @fires module:client~MatrixClient#event:"Room.timeline"
|
548 | */
|
549 |
|
550 |
|
551 | EventTimelineSet.prototype.addEventToTimeline = function (event, timeline, toStartOfTimeline) {
|
552 | const eventId = event.getId();
|
553 | timeline.addEvent(event, toStartOfTimeline);
|
554 | this._eventIdToTimeline[eventId] = timeline;
|
555 | this.setRelationsTarget(event);
|
556 | this.aggregateRelations(event);
|
557 | const data = {
|
558 | timeline: timeline,
|
559 | liveEvent: !toStartOfTimeline && timeline == this._liveTimeline
|
560 | };
|
561 | this.emit("Room.timeline", event, this.room, Boolean(toStartOfTimeline), false, data);
|
562 | };
|
563 | /**
|
564 | * Replaces event with ID oldEventId with one with newEventId, if oldEventId is
|
565 | * recognised. Otherwise, add to the live timeline. Used to handle remote echos.
|
566 | *
|
567 | * @param {MatrixEvent} localEvent the new event to be added to the timeline
|
568 | * @param {String} oldEventId the ID of the original event
|
569 | * @param {boolean} newEventId the ID of the replacement event
|
570 | *
|
571 | * @fires module:client~MatrixClient#event:"Room.timeline"
|
572 | */
|
573 |
|
574 |
|
575 | EventTimelineSet.prototype.handleRemoteEcho = function (localEvent, oldEventId, newEventId) {
|
576 | // XXX: why don't we infer newEventId from localEvent?
|
577 | const existingTimeline = this._eventIdToTimeline[oldEventId];
|
578 |
|
579 | if (existingTimeline) {
|
580 | delete this._eventIdToTimeline[oldEventId];
|
581 | this._eventIdToTimeline[newEventId] = existingTimeline;
|
582 | } else {
|
583 | if (this._filter) {
|
584 | if (this._filter.filterRoomTimeline([localEvent]).length) {
|
585 | this.addEventToTimeline(localEvent, this._liveTimeline, false);
|
586 | }
|
587 | } else {
|
588 | this.addEventToTimeline(localEvent, this._liveTimeline, false);
|
589 | }
|
590 | }
|
591 | };
|
592 | /**
|
593 | * Removes a single event from this room.
|
594 | *
|
595 | * @param {String} eventId The id of the event to remove
|
596 | *
|
597 | * @return {?MatrixEvent} the removed event, or null if the event was not found
|
598 | * in this room.
|
599 | */
|
600 |
|
601 |
|
602 | EventTimelineSet.prototype.removeEvent = function (eventId) {
|
603 | const timeline = this._eventIdToTimeline[eventId];
|
604 |
|
605 | if (!timeline) {
|
606 | return null;
|
607 | }
|
608 |
|
609 | const removed = timeline.removeEvent(eventId);
|
610 |
|
611 | if (removed) {
|
612 | delete this._eventIdToTimeline[eventId];
|
613 | const data = {
|
614 | timeline: timeline
|
615 | };
|
616 | this.emit("Room.timeline", removed, this.room, undefined, true, data);
|
617 | }
|
618 |
|
619 | return removed;
|
620 | };
|
621 | /**
|
622 | * Determine where two events appear in the timeline relative to one another
|
623 | *
|
624 | * @param {string} eventId1 The id of the first event
|
625 | * @param {string} eventId2 The id of the second event
|
626 |
|
627 | * @return {?number} a number less than zero if eventId1 precedes eventId2, and
|
628 | * greater than zero if eventId1 succeeds eventId2. zero if they are the
|
629 | * same event; null if we can't tell (either because we don't know about one
|
630 | * of the events, or because they are in separate timelines which don't join
|
631 | * up).
|
632 | */
|
633 |
|
634 |
|
635 | EventTimelineSet.prototype.compareEventOrdering = function (eventId1, eventId2) {
|
636 | if (eventId1 == eventId2) {
|
637 | // optimise this case
|
638 | return 0;
|
639 | }
|
640 |
|
641 | const timeline1 = this._eventIdToTimeline[eventId1];
|
642 | const timeline2 = this._eventIdToTimeline[eventId2];
|
643 |
|
644 | if (timeline1 === undefined) {
|
645 | return null;
|
646 | }
|
647 |
|
648 | if (timeline2 === undefined) {
|
649 | return null;
|
650 | }
|
651 |
|
652 | if (timeline1 === timeline2) {
|
653 | // both events are in the same timeline - figure out their
|
654 | // relative indices
|
655 | let idx1, idx2;
|
656 | const events = timeline1.getEvents();
|
657 |
|
658 | for (let idx = 0; idx < events.length && (idx1 === undefined || idx2 === undefined); idx++) {
|
659 | const evId = events[idx].getId();
|
660 |
|
661 | if (evId == eventId1) {
|
662 | idx1 = idx;
|
663 | }
|
664 |
|
665 | if (evId == eventId2) {
|
666 | idx2 = idx;
|
667 | }
|
668 | }
|
669 |
|
670 | return idx1 - idx2;
|
671 | } // the events are in different timelines. Iterate through the
|
672 | // linkedlist to see which comes first.
|
673 | // first work forwards from timeline1
|
674 |
|
675 |
|
676 | let tl = timeline1;
|
677 |
|
678 | while (tl) {
|
679 | if (tl === timeline2) {
|
680 | // timeline1 is before timeline2
|
681 | return -1;
|
682 | }
|
683 |
|
684 | tl = tl.getNeighbouringTimeline(_eventTimeline.EventTimeline.FORWARDS);
|
685 | } // now try backwards from timeline1
|
686 |
|
687 |
|
688 | tl = timeline1;
|
689 |
|
690 | while (tl) {
|
691 | if (tl === timeline2) {
|
692 | // timeline2 is before timeline1
|
693 | return 1;
|
694 | }
|
695 |
|
696 | tl = tl.getNeighbouringTimeline(_eventTimeline.EventTimeline.BACKWARDS);
|
697 | } // the timelines are not contiguous.
|
698 |
|
699 |
|
700 | return null;
|
701 | };
|
702 | /**
|
703 | * Get a collection of relations to a given event in this timeline set.
|
704 | *
|
705 | * @param {String} eventId
|
706 | * The ID of the event that you'd like to access relation events for.
|
707 | * For example, with annotations, this would be the ID of the event being annotated.
|
708 | * @param {String} relationType
|
709 | * The type of relation involved, such as "m.annotation", "m.reference", "m.replace", etc.
|
710 | * @param {String} eventType
|
711 | * The relation event's type, such as "m.reaction", etc.
|
712 | * @throws If <code>eventId</code>, <code>relationType</code> or <code>eventType</code>
|
713 | * are not valid.
|
714 | *
|
715 | * @returns {?Relations}
|
716 | * A container for relation events or undefined if there are no relation events for
|
717 | * the relationType.
|
718 | */
|
719 |
|
720 |
|
721 | EventTimelineSet.prototype.getRelationsForEvent = function (eventId, relationType, eventType) {
|
722 | if (!this._unstableClientRelationAggregation) {
|
723 | throw new Error("Client-side relation aggregation is disabled");
|
724 | }
|
725 |
|
726 | if (!eventId || !relationType || !eventType) {
|
727 | throw new Error("Invalid arguments for `getRelationsForEvent`");
|
728 | } // debuglog("Getting relations for: ", eventId, relationType, eventType);
|
729 |
|
730 |
|
731 | const relationsForEvent = this._relations[eventId] || {};
|
732 | const relationsWithRelType = relationsForEvent[relationType] || {};
|
733 | return relationsWithRelType[eventType];
|
734 | };
|
735 | /**
|
736 | * Set an event as the target event if any Relations exist for it already
|
737 | *
|
738 | * @param {MatrixEvent} event
|
739 | * The event to check as relation target.
|
740 | */
|
741 |
|
742 |
|
743 | EventTimelineSet.prototype.setRelationsTarget = function (event) {
|
744 | if (!this._unstableClientRelationAggregation) {
|
745 | return;
|
746 | }
|
747 |
|
748 | const relationsForEvent = this._relations[event.getId()];
|
749 |
|
750 | if (!relationsForEvent) {
|
751 | return;
|
752 | } // don't need it for non m.replace relations for now
|
753 |
|
754 |
|
755 | const relationsWithRelType = relationsForEvent["m.replace"];
|
756 |
|
757 | if (!relationsWithRelType) {
|
758 | return;
|
759 | } // only doing replacements for messages for now (e.g. edits)
|
760 |
|
761 |
|
762 | const relationsWithEventType = relationsWithRelType["m.room.message"];
|
763 |
|
764 | if (relationsWithEventType) {
|
765 | relationsWithEventType.setTargetEvent(event);
|
766 | }
|
767 | };
|
768 | /**
|
769 | * Add relation events to the relevant relation collection.
|
770 | *
|
771 | * @param {MatrixEvent} event
|
772 | * The new relation event to be aggregated.
|
773 | */
|
774 |
|
775 |
|
776 | EventTimelineSet.prototype.aggregateRelations = function (event) {
|
777 | if (!this._unstableClientRelationAggregation) {
|
778 | return;
|
779 | }
|
780 |
|
781 | if (event.isRedacted() || event.status === _event.EventStatus.CANCELLED) {
|
782 | return;
|
783 | } // If the event is currently encrypted, wait until it has been decrypted.
|
784 |
|
785 |
|
786 | if (event.isBeingDecrypted()) {
|
787 | event.once("Event.decrypted", () => {
|
788 | this.aggregateRelations(event);
|
789 | });
|
790 | return;
|
791 | }
|
792 |
|
793 | const relation = event.getRelation();
|
794 |
|
795 | if (!relation) {
|
796 | return;
|
797 | }
|
798 |
|
799 | const relatesToEventId = relation.event_id;
|
800 | const relationType = relation.rel_type;
|
801 | const eventType = event.getType(); // debuglog("Aggregating relation: ", event.getId(), eventType, relation);
|
802 |
|
803 | let relationsForEvent = this._relations[relatesToEventId];
|
804 |
|
805 | if (!relationsForEvent) {
|
806 | relationsForEvent = this._relations[relatesToEventId] = {};
|
807 | }
|
808 |
|
809 | let relationsWithRelType = relationsForEvent[relationType];
|
810 |
|
811 | if (!relationsWithRelType) {
|
812 | relationsWithRelType = relationsForEvent[relationType] = {};
|
813 | }
|
814 |
|
815 | let relationsWithEventType = relationsWithRelType[eventType];
|
816 | let isNewRelations = false;
|
817 | let relatesToEvent;
|
818 |
|
819 | if (!relationsWithEventType) {
|
820 | relationsWithEventType = relationsWithRelType[eventType] = new _relations.Relations(relationType, eventType, this.room);
|
821 | isNewRelations = true;
|
822 | relatesToEvent = this.findEventById(relatesToEventId);
|
823 |
|
824 | if (relatesToEvent) {
|
825 | relationsWithEventType.setTargetEvent(relatesToEvent);
|
826 | }
|
827 | }
|
828 |
|
829 | relationsWithEventType.addEvent(event); // only emit once event has been added to relations
|
830 |
|
831 | if (isNewRelations && relatesToEvent) {
|
832 | relatesToEvent.emit("Event.relationsCreated", relationType, eventType);
|
833 | }
|
834 | };
|
835 | /**
|
836 | * Fires whenever the timeline in a room is updated.
|
837 | * @event module:client~MatrixClient#"Room.timeline"
|
838 | * @param {MatrixEvent} event The matrix event which caused this event to fire.
|
839 | * @param {?Room} room The room, if any, whose timeline was updated.
|
840 | * @param {boolean} toStartOfTimeline True if this event was added to the start
|
841 | * @param {boolean} removed True if this event has just been removed from the timeline
|
842 | * (beginning; oldest) of the timeline e.g. due to pagination.
|
843 | *
|
844 | * @param {object} data more data about the event
|
845 | *
|
846 | * @param {module:event-timeline.EventTimeline} data.timeline the timeline the
|
847 | * event was added to/removed from
|
848 | *
|
849 | * @param {boolean} data.liveEvent true if the event was a real-time event
|
850 | * added to the end of the live timeline
|
851 | *
|
852 | * @example
|
853 | * matrixClient.on("Room.timeline",
|
854 | * function(event, room, toStartOfTimeline, removed, data) {
|
855 | * if (!toStartOfTimeline && data.liveEvent) {
|
856 | * var messageToAppend = room.timeline.[room.timeline.length - 1];
|
857 | * }
|
858 | * });
|
859 | */
|
860 |
|
861 | /**
|
862 | * Fires whenever the live timeline in a room is reset.
|
863 | *
|
864 | * When we get a 'limited' sync (for example, after a network outage), we reset
|
865 | * the live timeline to be empty before adding the recent events to the new
|
866 | * timeline. This event is fired after the timeline is reset, and before the
|
867 | * new events are added.
|
868 | *
|
869 | * @event module:client~MatrixClient#"Room.timelineReset"
|
870 | * @param {Room} room The room whose live timeline was reset, if any
|
871 | * @param {EventTimelineSet} timelineSet timelineSet room whose live timeline was reset
|
872 | * @param {boolean} resetAllTimelines True if all timelines were reset.
|
873 | */ |
\ | No newline at end of file |