1 | ;
|
2 |
|
3 | Object.defineProperty(exports, "__esModule", {
|
4 | value: true
|
5 | });
|
6 | exports.EventTimeline = EventTimeline;
|
7 |
|
8 | var _roomState = require("./room-state");
|
9 |
|
10 | /*
|
11 | Copyright 2016, 2017 OpenMarket Ltd
|
12 | Copyright 2019 The Matrix.org Foundation C.I.C.
|
13 |
|
14 | Licensed under the Apache License, Version 2.0 (the "License");
|
15 | you may not use this file except in compliance with the License.
|
16 | You may obtain a copy of the License at
|
17 |
|
18 | http://www.apache.org/licenses/LICENSE-2.0
|
19 |
|
20 | Unless required by applicable law or agreed to in writing, software
|
21 | distributed under the License is distributed on an "AS IS" BASIS,
|
22 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
23 | See the License for the specific language governing permissions and
|
24 | limitations under the License.
|
25 | */
|
26 |
|
27 | /**
|
28 | * @module models/event-timeline
|
29 | */
|
30 |
|
31 | /**
|
32 | * Construct a new EventTimeline
|
33 | *
|
34 | * <p>An EventTimeline represents a contiguous sequence of events in a room.
|
35 | *
|
36 | * <p>As well as keeping track of the events themselves, it stores the state of
|
37 | * the room at the beginning and end of the timeline, and pagination tokens for
|
38 | * going backwards and forwards in the timeline.
|
39 | *
|
40 | * <p>In order that clients can meaningfully maintain an index into a timeline,
|
41 | * the EventTimeline object tracks a 'baseIndex'. This starts at zero, but is
|
42 | * incremented when events are prepended to the timeline. The index of an event
|
43 | * relative to baseIndex therefore remains constant.
|
44 | *
|
45 | * <p>Once a timeline joins up with its neighbour, they are linked together into a
|
46 | * doubly-linked list.
|
47 | *
|
48 | * @param {EventTimelineSet} eventTimelineSet the set of timelines this is part of
|
49 | * @constructor
|
50 | */
|
51 | function EventTimeline(eventTimelineSet) {
|
52 | this._eventTimelineSet = eventTimelineSet;
|
53 | this._roomId = eventTimelineSet.room ? eventTimelineSet.room.roomId : null;
|
54 | this._events = [];
|
55 | this._baseIndex = 0;
|
56 | this._startState = new _roomState.RoomState(this._roomId);
|
57 | this._startState.paginationToken = null;
|
58 | this._endState = new _roomState.RoomState(this._roomId);
|
59 | this._endState.paginationToken = null;
|
60 | this._prevTimeline = null;
|
61 | this._nextTimeline = null; // this is used by client.js
|
62 |
|
63 | this._paginationRequests = {
|
64 | 'b': null,
|
65 | 'f': null
|
66 | };
|
67 | this._name = this._roomId + ":" + new Date().toISOString();
|
68 | }
|
69 | /**
|
70 | * Symbolic constant for methods which take a 'direction' argument:
|
71 | * refers to the start of the timeline, or backwards in time.
|
72 | */
|
73 |
|
74 |
|
75 | EventTimeline.BACKWARDS = "b";
|
76 | /**
|
77 | * Symbolic constant for methods which take a 'direction' argument:
|
78 | * refers to the end of the timeline, or forwards in time.
|
79 | */
|
80 |
|
81 | EventTimeline.FORWARDS = "f";
|
82 | /**
|
83 | * Initialise the start and end state with the given events
|
84 | *
|
85 | * <p>This can only be called before any events are added.
|
86 | *
|
87 | * @param {MatrixEvent[]} stateEvents list of state events to initialise the
|
88 | * state with.
|
89 | * @throws {Error} if an attempt is made to call this after addEvent is called.
|
90 | */
|
91 |
|
92 | EventTimeline.prototype.initialiseState = function (stateEvents) {
|
93 | if (this._events.length > 0) {
|
94 | throw new Error("Cannot initialise state after events are added");
|
95 | } // We previously deep copied events here and used different copies in
|
96 | // the oldState and state events: this decision seems to date back
|
97 | // quite a way and was apparently made to fix a bug where modifications
|
98 | // made to the start state leaked through to the end state.
|
99 | // This really shouldn't be possible though: the events themselves should
|
100 | // not change. Duplicating the events uses a lot of extra memory,
|
101 | // so we now no longer do it. To assert that they really do never change,
|
102 | // freeze them! Note that we can't do this for events in general:
|
103 | // although it looks like the only things preventing us are the
|
104 | // 'status' flag, forwardLooking (which is only set once when adding to the
|
105 | // timeline) and possibly the sender (which seems like it should never be
|
106 | // reset but in practice causes a lot of the tests to break).
|
107 |
|
108 |
|
109 | for (const e of stateEvents) {
|
110 | Object.freeze(e);
|
111 | }
|
112 |
|
113 | this._startState.setStateEvents(stateEvents);
|
114 |
|
115 | this._endState.setStateEvents(stateEvents);
|
116 | };
|
117 | /**
|
118 | * Forks the (live) timeline, taking ownership of the existing directional state of this timeline.
|
119 | * All attached listeners will keep receiving state updates from the new live timeline state.
|
120 | * The end state of this timeline gets replaced with an independent copy of the current RoomState,
|
121 | * and will need a new pagination token if it ever needs to paginate forwards.
|
122 |
|
123 | * @param {string} direction EventTimeline.BACKWARDS to get the state at the
|
124 | * start of the timeline; EventTimeline.FORWARDS to get the state at the end
|
125 | * of the timeline.
|
126 | *
|
127 | * @return {EventTimeline} the new timeline
|
128 | */
|
129 |
|
130 |
|
131 | EventTimeline.prototype.forkLive = function (direction) {
|
132 | const forkState = this.getState(direction);
|
133 | const timeline = new EventTimeline(this._eventTimelineSet);
|
134 | timeline._startState = forkState.clone(); // Now clobber the end state of the new live timeline with that from the
|
135 | // previous live timeline. It will be identical except that we'll keep
|
136 | // using the same RoomMember objects for the 'live' set of members with any
|
137 | // listeners still attached
|
138 |
|
139 | timeline._endState = forkState; // Firstly, we just stole the current timeline's end state, so it needs a new one.
|
140 | // Make an immutable copy of the state so back pagination will get the correct sentinels.
|
141 |
|
142 | this._endState = forkState.clone();
|
143 | return timeline;
|
144 | };
|
145 | /**
|
146 | * Creates an independent timeline, inheriting the directional state from this timeline.
|
147 | *
|
148 | * @param {string} direction EventTimeline.BACKWARDS to get the state at the
|
149 | * start of the timeline; EventTimeline.FORWARDS to get the state at the end
|
150 | * of the timeline.
|
151 | *
|
152 | * @return {EventTimeline} the new timeline
|
153 | */
|
154 |
|
155 |
|
156 | EventTimeline.prototype.fork = function (direction) {
|
157 | const forkState = this.getState(direction);
|
158 | const timeline = new EventTimeline(this._eventTimelineSet);
|
159 | timeline._startState = forkState.clone();
|
160 | timeline._endState = forkState.clone();
|
161 | return timeline;
|
162 | };
|
163 | /**
|
164 | * Get the ID of the room for this timeline
|
165 | * @return {string} room ID
|
166 | */
|
167 |
|
168 |
|
169 | EventTimeline.prototype.getRoomId = function () {
|
170 | return this._roomId;
|
171 | };
|
172 | /**
|
173 | * Get the filter for this timeline's timelineSet (if any)
|
174 | * @return {Filter} filter
|
175 | */
|
176 |
|
177 |
|
178 | EventTimeline.prototype.getFilter = function () {
|
179 | return this._eventTimelineSet.getFilter();
|
180 | };
|
181 | /**
|
182 | * Get the timelineSet for this timeline
|
183 | * @return {EventTimelineSet} timelineSet
|
184 | */
|
185 |
|
186 |
|
187 | EventTimeline.prototype.getTimelineSet = function () {
|
188 | return this._eventTimelineSet;
|
189 | };
|
190 | /**
|
191 | * Get the base index.
|
192 | *
|
193 | * <p>This is an index which is incremented when events are prepended to the
|
194 | * timeline. An individual event therefore stays at the same index in the array
|
195 | * relative to the base index (although note that a given event's index may
|
196 | * well be less than the base index, thus giving that event a negative relative
|
197 | * index).
|
198 | *
|
199 | * @return {number}
|
200 | */
|
201 |
|
202 |
|
203 | EventTimeline.prototype.getBaseIndex = function () {
|
204 | return this._baseIndex;
|
205 | };
|
206 | /**
|
207 | * Get the list of events in this context
|
208 | *
|
209 | * @return {MatrixEvent[]} An array of MatrixEvents
|
210 | */
|
211 |
|
212 |
|
213 | EventTimeline.prototype.getEvents = function () {
|
214 | return this._events;
|
215 | };
|
216 | /**
|
217 | * Get the room state at the start/end of the timeline
|
218 | *
|
219 | * @param {string} direction EventTimeline.BACKWARDS to get the state at the
|
220 | * start of the timeline; EventTimeline.FORWARDS to get the state at the end
|
221 | * of the timeline.
|
222 | *
|
223 | * @return {RoomState} state at the start/end of the timeline
|
224 | */
|
225 |
|
226 |
|
227 | EventTimeline.prototype.getState = function (direction) {
|
228 | if (direction == EventTimeline.BACKWARDS) {
|
229 | return this._startState;
|
230 | } else if (direction == EventTimeline.FORWARDS) {
|
231 | return this._endState;
|
232 | } else {
|
233 | throw new Error("Invalid direction '" + direction + "'");
|
234 | }
|
235 | };
|
236 | /**
|
237 | * Get a pagination token
|
238 | *
|
239 | * @param {string} direction EventTimeline.BACKWARDS to get the pagination
|
240 | * token for going backwards in time; EventTimeline.FORWARDS to get the
|
241 | * pagination token for going forwards in time.
|
242 | *
|
243 | * @return {?string} pagination token
|
244 | */
|
245 |
|
246 |
|
247 | EventTimeline.prototype.getPaginationToken = function (direction) {
|
248 | return this.getState(direction).paginationToken;
|
249 | };
|
250 | /**
|
251 | * Set a pagination token
|
252 | *
|
253 | * @param {?string} token pagination token
|
254 | *
|
255 | * @param {string} direction EventTimeline.BACKWARDS to set the pagination
|
256 | * token for going backwards in time; EventTimeline.FORWARDS to set the
|
257 | * pagination token for going forwards in time.
|
258 | */
|
259 |
|
260 |
|
261 | EventTimeline.prototype.setPaginationToken = function (token, direction) {
|
262 | this.getState(direction).paginationToken = token;
|
263 | };
|
264 | /**
|
265 | * Get the next timeline in the series
|
266 | *
|
267 | * @param {string} direction EventTimeline.BACKWARDS to get the previous
|
268 | * timeline; EventTimeline.FORWARDS to get the next timeline.
|
269 | *
|
270 | * @return {?EventTimeline} previous or following timeline, if they have been
|
271 | * joined up.
|
272 | */
|
273 |
|
274 |
|
275 | EventTimeline.prototype.getNeighbouringTimeline = function (direction) {
|
276 | if (direction == EventTimeline.BACKWARDS) {
|
277 | return this._prevTimeline;
|
278 | } else if (direction == EventTimeline.FORWARDS) {
|
279 | return this._nextTimeline;
|
280 | } else {
|
281 | throw new Error("Invalid direction '" + direction + "'");
|
282 | }
|
283 | };
|
284 | /**
|
285 | * Set the next timeline in the series
|
286 | *
|
287 | * @param {EventTimeline} neighbour previous/following timeline
|
288 | *
|
289 | * @param {string} direction EventTimeline.BACKWARDS to set the previous
|
290 | * timeline; EventTimeline.FORWARDS to set the next timeline.
|
291 | *
|
292 | * @throws {Error} if an attempt is made to set the neighbouring timeline when
|
293 | * it is already set.
|
294 | */
|
295 |
|
296 |
|
297 | EventTimeline.prototype.setNeighbouringTimeline = function (neighbour, direction) {
|
298 | if (this.getNeighbouringTimeline(direction)) {
|
299 | throw new Error("timeline already has a neighbouring timeline - " + "cannot reset neighbour (direction: " + direction + ")");
|
300 | }
|
301 |
|
302 | if (direction == EventTimeline.BACKWARDS) {
|
303 | this._prevTimeline = neighbour;
|
304 | } else if (direction == EventTimeline.FORWARDS) {
|
305 | this._nextTimeline = neighbour;
|
306 | } else {
|
307 | throw new Error("Invalid direction '" + direction + "'");
|
308 | } // make sure we don't try to paginate this timeline
|
309 |
|
310 |
|
311 | this.setPaginationToken(null, direction);
|
312 | };
|
313 | /**
|
314 | * Add a new event to the timeline, and update the state
|
315 | *
|
316 | * @param {MatrixEvent} event new event
|
317 | * @param {boolean} atStart true to insert new event at the start
|
318 | */
|
319 |
|
320 |
|
321 | EventTimeline.prototype.addEvent = function (event, atStart) {
|
322 | const stateContext = atStart ? this._startState : this._endState; // only call setEventMetadata on the unfiltered timelineSets
|
323 |
|
324 | const timelineSet = this.getTimelineSet();
|
325 |
|
326 | if (timelineSet.room && timelineSet.room.getUnfilteredTimelineSet() === timelineSet) {
|
327 | EventTimeline.setEventMetadata(event, stateContext, atStart); // modify state
|
328 |
|
329 | if (event.isState()) {
|
330 | stateContext.setStateEvents([event]); // it is possible that the act of setting the state event means we
|
331 | // can set more metadata (specifically sender/target props), so try
|
332 | // it again if the prop wasn't previously set. It may also mean that
|
333 | // the sender/target is updated (if the event set was a room member event)
|
334 | // so we want to use the *updated* member (new avatar/name) instead.
|
335 | //
|
336 | // However, we do NOT want to do this on member events if we're going
|
337 | // back in time, else we'll set the .sender value for BEFORE the given
|
338 | // member event, whereas we want to set the .sender value for the ACTUAL
|
339 | // member event itself.
|
340 |
|
341 | if (!event.sender || event.getType() === "m.room.member" && !atStart) {
|
342 | EventTimeline.setEventMetadata(event, stateContext, atStart);
|
343 | }
|
344 | }
|
345 | }
|
346 |
|
347 | let insertIndex;
|
348 |
|
349 | if (atStart) {
|
350 | insertIndex = 0;
|
351 | } else {
|
352 | insertIndex = this._events.length;
|
353 | }
|
354 |
|
355 | this._events.splice(insertIndex, 0, event); // insert element
|
356 |
|
357 |
|
358 | if (atStart) {
|
359 | this._baseIndex++;
|
360 | }
|
361 | };
|
362 | /**
|
363 | * Static helper method to set sender and target properties
|
364 | *
|
365 | * @param {MatrixEvent} event the event whose metadata is to be set
|
366 | * @param {RoomState} stateContext the room state to be queried
|
367 | * @param {bool} toStartOfTimeline if true the event's forwardLooking flag is set false
|
368 | */
|
369 |
|
370 |
|
371 | EventTimeline.setEventMetadata = function (event, stateContext, toStartOfTimeline) {
|
372 | // set sender and target properties
|
373 | event.sender = stateContext.getSentinelMember(event.getSender());
|
374 |
|
375 | if (event.getType() === "m.room.member") {
|
376 | event.target = stateContext.getSentinelMember(event.getStateKey());
|
377 | }
|
378 |
|
379 | if (event.isState()) {
|
380 | // room state has no concept of 'old' or 'current', but we want the
|
381 | // room state to regress back to previous values if toStartOfTimeline
|
382 | // is set, which means inspecting prev_content if it exists. This
|
383 | // is done by toggling the forwardLooking flag.
|
384 | if (toStartOfTimeline) {
|
385 | event.forwardLooking = false;
|
386 | }
|
387 | }
|
388 | };
|
389 | /**
|
390 | * Remove an event from the timeline
|
391 | *
|
392 | * @param {string} eventId ID of event to be removed
|
393 | * @return {?MatrixEvent} removed event, or null if not found
|
394 | */
|
395 |
|
396 |
|
397 | EventTimeline.prototype.removeEvent = function (eventId) {
|
398 | for (let i = this._events.length - 1; i >= 0; i--) {
|
399 | const ev = this._events[i];
|
400 |
|
401 | if (ev.getId() == eventId) {
|
402 | this._events.splice(i, 1);
|
403 |
|
404 | if (i < this._baseIndex) {
|
405 | this._baseIndex--;
|
406 | }
|
407 |
|
408 | return ev;
|
409 | }
|
410 | }
|
411 |
|
412 | return null;
|
413 | };
|
414 | /**
|
415 | * Return a string to identify this timeline, for debugging
|
416 | *
|
417 | * @return {string} name for this timeline
|
418 | */
|
419 |
|
420 |
|
421 | EventTimeline.prototype.toString = function () {
|
422 | return this._name;
|
423 | }; |
\ | No newline at end of file |