UNPKG

13.6 kBJavaScriptView Raw
1"use strict";
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6exports.EventTimeline = EventTimeline;
7
8var _roomState = require("./room-state");
9
10/*
11Copyright 2016, 2017 OpenMarket Ltd
12Copyright 2019 The Matrix.org Foundation C.I.C.
13
14Licensed under the Apache License, Version 2.0 (the "License");
15you may not use this file except in compliance with the License.
16You may obtain a copy of the License at
17
18 http://www.apache.org/licenses/LICENSE-2.0
19
20Unless required by applicable law or agreed to in writing, software
21distributed under the License is distributed on an "AS IS" BASIS,
22WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
23See the License for the specific language governing permissions and
24limitations 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 */
51function 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
75EventTimeline.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
81EventTimeline.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
92EventTimeline.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
131EventTimeline.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
156EventTimeline.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
169EventTimeline.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
178EventTimeline.prototype.getFilter = function () {
179 return this._eventTimelineSet.getFilter();
180};
181/**
182 * Get the timelineSet for this timeline
183 * @return {EventTimelineSet} timelineSet
184 */
185
186
187EventTimeline.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
203EventTimeline.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
213EventTimeline.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
227EventTimeline.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
247EventTimeline.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
261EventTimeline.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
275EventTimeline.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
297EventTimeline.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
321EventTimeline.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
371EventTimeline.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
397EventTimeline.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
421EventTimeline.prototype.toString = function () {
422 return this._name;
423};
\No newline at end of file