summaryrefslogtreecommitdiffstats
path: root/comm/chat/protocols/matrix/lib/matrix-sdk/models/event-timeline.js
blob: d5e9b4ac40938fe5cfcb920c6465e9169822dd20 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.EventTimeline = exports.Direction = void 0;
var _logger = require("../logger");
var _roomState = require("./room-state");
var _event = require("../@types/event");
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
                                                                                                                                                                                                                                                                                                                                                                                          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.
                                                                                                                                                                                                                                                                                                                                                                                          */
let Direction = /*#__PURE__*/function (Direction) {
  Direction["Backward"] = "b";
  Direction["Forward"] = "f";
  return Direction;
}({});
exports.Direction = Direction;
class EventTimeline {
  /**
   * Static helper method to set sender and target properties
   *
   * @param event -   the event whose metadata is to be set
   * @param stateContext -  the room state to be queried
   * @param toStartOfTimeline -  if true the event's forwardLooking flag is set false
   */
  static setEventMetadata(event, stateContext, toStartOfTimeline) {
    // When we try to generate a sentinel member before we have that member
    // in the members object, we still generate a sentinel but it doesn't
    // have a membership event, so test to see if events.member is set. We
    // check this to avoid overriding non-sentinel members by sentinel ones
    // when adding the event to a filtered timeline
    if (!event.sender?.events?.member) {
      event.sender = stateContext.getSentinelMember(event.getSender());
    }
    if (!event.target?.events?.member && event.getType() === _event.EventType.RoomMember) {
      event.target = stateContext.getSentinelMember(event.getStateKey());
    }
    if (event.isState()) {
      // room state has no concept of 'old' or 'current', but we want the
      // room state to regress back to previous values if toStartOfTimeline
      // is set, which means inspecting prev_content if it exists. This
      // is done by toggling the forwardLooking flag.
      if (toStartOfTimeline) {
        event.forwardLooking = false;
      }
    }
  }
  /**
   * Construct a new EventTimeline
   *
   * <p>An EventTimeline represents a contiguous sequence of events in a room.
   *
   * <p>As well as keeping track of the events themselves, it stores the state of
   * the room at the beginning and end of the timeline, and pagination tokens for
   * going backwards and forwards in the timeline.
   *
   * <p>In order that clients can meaningfully maintain an index into a timeline,
   * the EventTimeline object tracks a 'baseIndex'. This starts at zero, but is
   * incremented when events are prepended to the timeline. The index of an event
   * relative to baseIndex therefore remains constant.
   *
   * <p>Once a timeline joins up with its neighbour, they are linked together into a
   * doubly-linked list.
   *
   * @param eventTimelineSet - the set of timelines this is part of
   */
  constructor(eventTimelineSet) {
    this.eventTimelineSet = eventTimelineSet;
    _defineProperty(this, "roomId", void 0);
    _defineProperty(this, "name", void 0);
    _defineProperty(this, "events", []);
    _defineProperty(this, "baseIndex", 0);
    _defineProperty(this, "startState", void 0);
    _defineProperty(this, "endState", void 0);
    // If we have a roomId then we delegate pagination token storage to the room state objects `startState` and
    // `endState`, but for things like the notification timeline which mix multiple rooms we store the tokens ourselves.
    _defineProperty(this, "startToken", null);
    _defineProperty(this, "endToken", null);
    _defineProperty(this, "prevTimeline", null);
    _defineProperty(this, "nextTimeline", null);
    _defineProperty(this, "paginationRequests", {
      [Direction.Backward]: null,
      [Direction.Forward]: null
    });
    this.roomId = eventTimelineSet.room?.roomId ?? null;
    if (this.roomId) {
      this.startState = new _roomState.RoomState(this.roomId);
      this.endState = new _roomState.RoomState(this.roomId);
    }

    // this is used by client.js
    this.paginationRequests = {
      b: null,
      f: null
    };
    this.name = this.roomId + ":" + new Date().toISOString();
  }

  /**
   * Initialise the start and end state with the given events
   *
   * <p>This can only be called before any events are added.
   *
   * @param stateEvents - list of state events to initialise the
   * state with.
   * @throws Error if an attempt is made to call this after addEvent is called.
   */
  initialiseState(stateEvents, {
    timelineWasEmpty
  } = {}) {
    if (this.events.length > 0) {
      throw new Error("Cannot initialise state after events are added");
    }
    this.startState?.setStateEvents(stateEvents, {
      timelineWasEmpty
    });
    this.endState?.setStateEvents(stateEvents, {
      timelineWasEmpty
    });
  }

  /**
   * Forks the (live) timeline, taking ownership of the existing directional state of this timeline.
   * All attached listeners will keep receiving state updates from the new live timeline state.
   * The end state of this timeline gets replaced with an independent copy of the current RoomState,
   * and will need a new pagination token if it ever needs to paginate forwards.
    * @param direction -   EventTimeline.BACKWARDS to get the state at the
   *   start of the timeline; EventTimeline.FORWARDS to get the state at the end
   *   of the timeline.
   *
   * @returns the new timeline
   */
  forkLive(direction) {
    const forkState = this.getState(direction);
    const timeline = new EventTimeline(this.eventTimelineSet);
    timeline.startState = forkState?.clone();
    // Now clobber the end state of the new live timeline with that from the
    // previous live timeline. It will be identical except that we'll keep
    // using the same RoomMember objects for the 'live' set of members with any
    // listeners still attached
    timeline.endState = forkState;
    // Firstly, we just stole the current timeline's end state, so it needs a new one.
    // Make an immutable copy of the state so back pagination will get the correct sentinels.
    this.endState = forkState?.clone();
    return timeline;
  }

  /**
   * Creates an independent timeline, inheriting the directional state from this timeline.
   *
   * @param direction -   EventTimeline.BACKWARDS to get the state at the
   *   start of the timeline; EventTimeline.FORWARDS to get the state at the end
   *   of the timeline.
   *
   * @returns the new timeline
   */
  fork(direction) {
    const forkState = this.getState(direction);
    const timeline = new EventTimeline(this.eventTimelineSet);
    timeline.startState = forkState?.clone();
    timeline.endState = forkState?.clone();
    return timeline;
  }

  /**
   * Get the ID of the room for this timeline
   * @returns room ID
   */
  getRoomId() {
    return this.roomId;
  }

  /**
   * Get the filter for this timeline's timelineSet (if any)
   * @returns filter
   */
  getFilter() {
    return this.eventTimelineSet.getFilter();
  }

  /**
   * Get the timelineSet for this timeline
   * @returns timelineSet
   */
  getTimelineSet() {
    return this.eventTimelineSet;
  }

  /**
   * Get the base index.
   *
   * <p>This is an index which is incremented when events are prepended to the
   * timeline. An individual event therefore stays at the same index in the array
   * relative to the base index (although note that a given event's index may
   * well be less than the base index, thus giving that event a negative relative
   * index).
   */
  getBaseIndex() {
    return this.baseIndex;
  }

  /**
   * Get the list of events in this context
   *
   * @returns An array of MatrixEvents
   */
  getEvents() {
    return this.events;
  }

  /**
   * Get the room state at the start/end of the timeline
   *
   * @param direction -   EventTimeline.BACKWARDS to get the state at the
   *   start of the timeline; EventTimeline.FORWARDS to get the state at the end
   *   of the timeline.
   *
   * @returns state at the start/end of the timeline
   */
  getState(direction) {
    if (direction == EventTimeline.BACKWARDS) {
      return this.startState;
    } else if (direction == EventTimeline.FORWARDS) {
      return this.endState;
    } else {
      throw new Error("Invalid direction '" + direction + "'");
    }
  }

  /**
   * Get a pagination token
   *
   * @param direction -   EventTimeline.BACKWARDS to get the pagination
   *   token for going backwards in time; EventTimeline.FORWARDS to get the
   *   pagination token for going forwards in time.
   *
   * @returns pagination token
   */
  getPaginationToken(direction) {
    if (this.roomId) {
      return this.getState(direction).paginationToken;
    } else if (direction === Direction.Backward) {
      return this.startToken;
    } else {
      return this.endToken;
    }
  }

  /**
   * Set a pagination token
   *
   * @param token -       pagination token
   *
   * @param direction -    EventTimeline.BACKWARDS to set the pagination
   *   token for going backwards in time; EventTimeline.FORWARDS to set the
   *   pagination token for going forwards in time.
   */
  setPaginationToken(token, direction) {
    if (this.roomId) {
      this.getState(direction).paginationToken = token;
    } else if (direction === Direction.Backward) {
      this.startToken = token;
    } else {
      this.endToken = token;
    }
  }

  /**
   * Get the next timeline in the series
   *
   * @param direction - EventTimeline.BACKWARDS to get the previous
   *   timeline; EventTimeline.FORWARDS to get the next timeline.
   *
   * @returns previous or following timeline, if they have been
   * joined up.
   */
  getNeighbouringTimeline(direction) {
    if (direction == EventTimeline.BACKWARDS) {
      return this.prevTimeline;
    } else if (direction == EventTimeline.FORWARDS) {
      return this.nextTimeline;
    } else {
      throw new Error("Invalid direction '" + direction + "'");
    }
  }

  /**
   * Set the next timeline in the series
   *
   * @param neighbour - previous/following timeline
   *
   * @param direction - EventTimeline.BACKWARDS to set the previous
   *   timeline; EventTimeline.FORWARDS to set the next timeline.
   *
   * @throws Error if an attempt is made to set the neighbouring timeline when
   * it is already set.
   */
  setNeighbouringTimeline(neighbour, direction) {
    if (this.getNeighbouringTimeline(direction)) {
      throw new Error("timeline already has a neighbouring timeline - " + "cannot reset neighbour (direction: " + direction + ")");
    }
    if (direction == EventTimeline.BACKWARDS) {
      this.prevTimeline = neighbour;
    } else if (direction == EventTimeline.FORWARDS) {
      this.nextTimeline = neighbour;
    } else {
      throw new Error("Invalid direction '" + direction + "'");
    }

    // make sure we don't try to paginate this timeline
    this.setPaginationToken(null, direction);
  }

  /**
   * Add a new event to the timeline, and update the state
   *
   * @param event - new event
   * @param options - addEvent options
   */

  /**
   * @deprecated In favor of the overload with `IAddEventOptions`
   */

  addEvent(event, toStartOfTimelineOrOpts, roomState) {
    let toStartOfTimeline = !!toStartOfTimelineOrOpts;
    let timelineWasEmpty;
    if (typeof toStartOfTimelineOrOpts === "object") {
      ({
        toStartOfTimeline,
        roomState,
        timelineWasEmpty
      } = toStartOfTimelineOrOpts);
    } else if (toStartOfTimelineOrOpts !== undefined) {
      // Deprecation warning
      // FIXME: Remove after 2023-06-01 (technical debt)
      _logger.logger.warn("Overload deprecated: " + "`EventTimeline.addEvent(event, toStartOfTimeline, roomState?)` " + "is deprecated in favor of the overload with `EventTimeline.addEvent(event, IAddEventOptions)`");
    }
    if (!roomState) {
      roomState = toStartOfTimeline ? this.startState : this.endState;
    }
    const timelineSet = this.getTimelineSet();
    if (timelineSet.room) {
      EventTimeline.setEventMetadata(event, roomState, toStartOfTimeline);

      // modify state but only on unfiltered timelineSets
      if (event.isState() && timelineSet.room.getUnfilteredTimelineSet() === timelineSet) {
        roomState?.setStateEvents([event], {
          timelineWasEmpty
        });
        // it is possible that the act of setting the state event means we
        // can set more metadata (specifically sender/target props), so try
        // it again if the prop wasn't previously set. It may also mean that
        // the sender/target is updated (if the event set was a room member event)
        // so we want to use the *updated* member (new avatar/name) instead.
        //
        // However, we do NOT want to do this on member events if we're going
        // back in time, else we'll set the .sender value for BEFORE the given
        // member event, whereas we want to set the .sender value for the ACTUAL
        // member event itself.
        if (!event.sender || event.getType() === _event.EventType.RoomMember && !toStartOfTimeline) {
          EventTimeline.setEventMetadata(event, roomState, toStartOfTimeline);
        }
      }
    }
    let insertIndex;
    if (toStartOfTimeline) {
      insertIndex = 0;
    } else {
      insertIndex = this.events.length;
    }
    this.events.splice(insertIndex, 0, event); // insert element
    if (toStartOfTimeline) {
      this.baseIndex++;
    }
  }

  /**
   * Insert a new event into the timeline, and update the state.
   *
   * 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 addEvent above, modified to allow inserting an event at
   * a specific index.
   *
   * @internal
   */
  insertEvent(event, insertIndex, roomState) {
    const timelineSet = this.getTimelineSet();
    if (timelineSet.room) {
      EventTimeline.setEventMetadata(event, roomState, false);

      // modify state but only on unfiltered timelineSets
      if (event.isState() && timelineSet.room.getUnfilteredTimelineSet() === timelineSet) {
        roomState.setStateEvents([event], {});
        // it is possible that the act of setting the state event means we
        // can set more metadata (specifically sender/target props), so try
        // it again if the prop wasn't previously set. It may also mean that
        // the sender/target is updated (if the event set was a room member event)
        // so we want to use the *updated* member (new avatar/name) instead.
        //
        // However, we do NOT want to do this on member events if we're going
        // back in time, else we'll set the .sender value for BEFORE the given
        // member event, whereas we want to set the .sender value for the ACTUAL
        // member event itself.
        if (!event.sender || event.getType() === _event.EventType.RoomMember) {
          EventTimeline.setEventMetadata(event, roomState, false);
        }
      }
    }
    this.events.splice(insertIndex, 0, event); // insert element
  }

  /**
   * Remove an event from the timeline
   *
   * @param eventId -  ID of event to be removed
   * @returns removed event, or null if not found
   */
  removeEvent(eventId) {
    for (let i = this.events.length - 1; i >= 0; i--) {
      const ev = this.events[i];
      if (ev.getId() == eventId) {
        this.events.splice(i, 1);
        if (i < this.baseIndex) {
          this.baseIndex--;
        }
        return ev;
      }
    }
    return null;
  }

  /**
   * Return a string to identify this timeline, for debugging
   *
   * @returns name for this timeline
   */
  toString() {
    return this.name;
  }
}
exports.EventTimeline = EventTimeline;
/**
 * Symbolic constant for methods which take a 'direction' argument:
 * refers to the start of the timeline, or backwards in time.
 */
_defineProperty(EventTimeline, "BACKWARDS", Direction.Backward);
/**
 * Symbolic constant for methods which take a 'direction' argument:
 * refers to the end of the timeline, or forwards in time.
 */
_defineProperty(EventTimeline, "FORWARDS", Direction.Forward);