UNPKG

29.1 kBJavaScriptView Raw
1"use strict";
2
3var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard");
4
5Object.defineProperty(exports, "__esModule", {
6 value: true
7});
8exports.EventTimelineSet = EventTimelineSet;
9
10var _events = require("events");
11
12var _eventTimeline = require("./event-timeline");
13
14var _event = require("./event");
15
16var utils = _interopRequireWildcard(require("../utils"));
17
18var _logger = require("../logger");
19
20var _relations = require("./relations");
21
22/*
23Copyright 2016 OpenMarket Ltd
24Copyright 2019 The Matrix.org Foundation C.I.C.
25
26Licensed under the Apache License, Version 2.0 (the "License");
27you may not use this file except in compliance with the License.
28You may obtain a copy of the License at
29
30 http://www.apache.org/licenses/LICENSE-2.0
31
32Unless required by applicable law or agreed to in writing, software
33distributed under the License is distributed on an "AS IS" BASIS,
34WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
35See the License for the specific language governing permissions and
36limitations under the License.
37*/
38
39/**
40 * @module models/event-timeline-set
41 */
42// var DEBUG = false;
43const DEBUG = true;
44let debuglog;
45
46if (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
90function 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
107utils.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
113EventTimelineSet.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
122EventTimelineSet.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
132EventTimelineSet.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
146EventTimelineSet.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
164EventTimelineSet.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
174EventTimelineSet.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
185EventTimelineSet.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
206EventTimelineSet.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
249EventTimelineSet.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
261EventTimelineSet.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
279EventTimelineSet.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
311EventTimelineSet.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
499EventTimelineSet.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
551EventTimelineSet.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
575EventTimelineSet.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
602EventTimelineSet.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
635EventTimelineSet.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
721EventTimelineSet.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
743EventTimelineSet.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
776EventTimelineSet.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