summaryrefslogtreecommitdiffstats
path: root/comm/chat/protocols/matrix/lib/matrix-sdk/timeline-window.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /comm/chat/protocols/matrix/lib/matrix-sdk/timeline-window.js
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/chat/protocols/matrix/lib/matrix-sdk/timeline-window.js')
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/timeline-window.js462
1 files changed, 462 insertions, 0 deletions
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/timeline-window.js b/comm/chat/protocols/matrix/lib/matrix-sdk/timeline-window.js
new file mode 100644
index 0000000000..2c7365bdff
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/timeline-window.js
@@ -0,0 +1,462 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.TimelineWindow = exports.TimelineIndex = void 0;
+var _eventTimeline = require("./models/event-timeline");
+var _logger = require("./logger");
+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.
+ */
+/**
+ * @internal
+ */
+const DEBUG = false;
+
+/**
+ * @internal
+ */
+/* istanbul ignore next */
+const debuglog = DEBUG ? _logger.logger.log.bind(_logger.logger) : function () {};
+
+/**
+ * the number of times we ask the server for more events before giving up
+ *
+ * @internal
+ */
+const DEFAULT_PAGINATE_LOOP_LIMIT = 5;
+class TimelineWindow {
+ /**
+ * Construct a TimelineWindow.
+ *
+ * <p>This abstracts the separate timelines in a Matrix {@link Room} into a single iterable thing.
+ * It keeps track of the start and endpoints of the window, which can be advanced with the help
+ * of pagination requests.
+ *
+ * <p>Before the window is useful, it must be initialised by calling {@link TimelineWindow#load}.
+ *
+ * <p>Note that the window will not automatically extend itself when new events
+ * are received from /sync; you should arrange to call {@link TimelineWindow#paginate}
+ * on {@link RoomEvent.Timeline} events.
+ *
+ * @param client - MatrixClient to be used for context/pagination
+ * requests.
+ *
+ * @param timelineSet - The timelineSet to track
+ *
+ * @param opts - Configuration options for this window
+ */
+ constructor(client, timelineSet, opts = {}) {
+ this.client = client;
+ this.timelineSet = timelineSet;
+ _defineProperty(this, "windowLimit", void 0);
+ // these will be TimelineIndex objects; they delineate the 'start' and
+ // 'end' of the window.
+ //
+ // start.index is inclusive; end.index is exclusive.
+ _defineProperty(this, "start", void 0);
+ _defineProperty(this, "end", void 0);
+ _defineProperty(this, "eventCount", 0);
+ this.windowLimit = opts.windowLimit || 1000;
+ }
+
+ /**
+ * Initialise the window to point at a given event, or the live timeline
+ *
+ * @param initialEventId - If given, the window will contain the
+ * given event
+ * @param initialWindowSize - Size of the initial window
+ */
+ load(initialEventId, initialWindowSize = 20) {
+ // given an EventTimeline, find the event we were looking for, and initialise our
+ // fields so that the event in question is in the middle of the window.
+ const initFields = timeline => {
+ if (!timeline) {
+ throw new Error("No timeline given to initFields");
+ }
+ let eventIndex;
+ const events = timeline.getEvents();
+ if (!initialEventId) {
+ // we were looking for the live timeline: initialise to the end
+ eventIndex = events.length;
+ } else {
+ eventIndex = events.findIndex(e => e.getId() === initialEventId);
+ if (eventIndex < 0) {
+ throw new Error("getEventTimeline result didn't include requested event");
+ }
+ }
+ const endIndex = Math.min(events.length, eventIndex + Math.ceil(initialWindowSize / 2));
+ const startIndex = Math.max(0, endIndex - initialWindowSize);
+ this.start = new TimelineIndex(timeline, startIndex - timeline.getBaseIndex());
+ this.end = new TimelineIndex(timeline, endIndex - timeline.getBaseIndex());
+ this.eventCount = endIndex - startIndex;
+ };
+
+ // We avoid delaying the resolution of the promise by a reactor tick if we already have the data we need,
+ // which is important to keep room-switching feeling snappy.
+ if (this.timelineSet.getTimelineForEvent(initialEventId)) {
+ initFields(this.timelineSet.getTimelineForEvent(initialEventId));
+ return Promise.resolve();
+ } else if (initialEventId) {
+ return this.client.getEventTimeline(this.timelineSet, initialEventId).then(initFields);
+ } else {
+ initFields(this.timelineSet.getLiveTimeline());
+ return Promise.resolve();
+ }
+ }
+
+ /**
+ * Get the TimelineIndex of the window in the given direction.
+ *
+ * @param direction - EventTimeline.BACKWARDS to get the TimelineIndex
+ * at the start of the window; EventTimeline.FORWARDS to get the TimelineIndex at
+ * the end.
+ *
+ * @returns The requested timeline index if one exists, null
+ * otherwise.
+ */
+ getTimelineIndex(direction) {
+ if (direction == _eventTimeline.EventTimeline.BACKWARDS) {
+ return this.start ?? null;
+ } else if (direction == _eventTimeline.EventTimeline.FORWARDS) {
+ return this.end ?? null;
+ } else {
+ throw new Error("Invalid direction '" + direction + "'");
+ }
+ }
+
+ /**
+ * Try to extend the window using events that are already in the underlying
+ * TimelineIndex.
+ *
+ * @param direction - EventTimeline.BACKWARDS to try extending it
+ * backwards; EventTimeline.FORWARDS to try extending it forwards.
+ * @param size - number of events to try to extend by.
+ *
+ * @returns true if the window was extended, false otherwise.
+ */
+ extend(direction, size) {
+ const tl = this.getTimelineIndex(direction);
+ if (!tl) {
+ debuglog("TimelineWindow: no timeline yet");
+ return false;
+ }
+ const count = direction == _eventTimeline.EventTimeline.BACKWARDS ? tl.retreat(size) : tl.advance(size);
+ if (count) {
+ this.eventCount += count;
+ debuglog("TimelineWindow: increased cap by " + count + " (now " + this.eventCount + ")");
+ // remove some events from the other end, if necessary
+ const excess = this.eventCount - this.windowLimit;
+ if (excess > 0) {
+ this.unpaginate(excess, direction != _eventTimeline.EventTimeline.BACKWARDS);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Check if this window can be extended
+ *
+ * <p>This returns true if we either have more events, or if we have a
+ * pagination token which means we can paginate in that direction. It does not
+ * necessarily mean that there are more events available in that direction at
+ * this time.
+ *
+ * @param direction - EventTimeline.BACKWARDS to check if we can
+ * paginate backwards; EventTimeline.FORWARDS to check if we can go forwards
+ *
+ * @returns true if we can paginate in the given direction
+ */
+ canPaginate(direction) {
+ const tl = this.getTimelineIndex(direction);
+ if (!tl) {
+ debuglog("TimelineWindow: no timeline yet");
+ return false;
+ }
+ if (direction == _eventTimeline.EventTimeline.BACKWARDS) {
+ if (tl.index > tl.minIndex()) {
+ return true;
+ }
+ } else {
+ if (tl.index < tl.maxIndex()) {
+ return true;
+ }
+ }
+ const hasNeighbouringTimeline = tl.timeline.getNeighbouringTimeline(direction);
+ const paginationToken = tl.timeline.getPaginationToken(direction);
+ return Boolean(hasNeighbouringTimeline) || Boolean(paginationToken);
+ }
+
+ /**
+ * Attempt to extend the window
+ *
+ * @param direction - EventTimeline.BACKWARDS to extend the window
+ * backwards (towards older events); EventTimeline.FORWARDS to go forwards.
+ *
+ * @param size - number of events to try to extend by. If fewer than this
+ * number are immediately available, then we return immediately rather than
+ * making an API call.
+ *
+ * @param makeRequest - whether we should make API calls to
+ * fetch further events if we don't have any at all. (This has no effect if
+ * the room already knows about additional events in the relevant direction,
+ * even if there are fewer than 'size' of them, as we will just return those
+ * we already know about.)
+ *
+ * @param requestLimit - limit for the number of API requests we
+ * should make.
+ *
+ * @returns Promise which resolves to a boolean which is true if more events
+ * were successfully retrieved.
+ */
+ async paginate(direction, size, makeRequest = true, requestLimit = DEFAULT_PAGINATE_LOOP_LIMIT) {
+ // Either wind back the message cap (if there are enough events in the
+ // timeline to do so), or fire off a pagination request.
+ const tl = this.getTimelineIndex(direction);
+ if (!tl) {
+ debuglog("TimelineWindow: no timeline yet");
+ return false;
+ }
+ if (tl.pendingPaginate) {
+ return tl.pendingPaginate;
+ }
+
+ // try moving the cap
+ if (this.extend(direction, size)) {
+ return true;
+ }
+ if (!makeRequest || requestLimit === 0) {
+ // todo: should we return something different to indicate that there
+ // might be more events out there, but we haven't found them yet?
+ return false;
+ }
+
+ // try making a pagination request
+ const token = tl.timeline.getPaginationToken(direction);
+ if (!token) {
+ debuglog("TimelineWindow: no token");
+ return false;
+ }
+ debuglog("TimelineWindow: starting request");
+ const prom = this.client.paginateEventTimeline(tl.timeline, {
+ backwards: direction == _eventTimeline.EventTimeline.BACKWARDS,
+ limit: size
+ }).finally(function () {
+ tl.pendingPaginate = undefined;
+ }).then(r => {
+ debuglog("TimelineWindow: request completed with result " + r);
+ if (!r) {
+ return this.paginate(direction, size, false, 0);
+ }
+
+ // recurse to advance the index into the results.
+ //
+ // If we don't get any new events, we want to make sure we keep asking
+ // the server for events for as long as we have a valid pagination
+ // token. In particular, we want to know if we've actually hit the
+ // start of the timeline, or if we just happened to know about all of
+ // the events thanks to https://matrix.org/jira/browse/SYN-645.
+ //
+ // On the other hand, we necessarily want to wait forever for the
+ // server to make its mind up about whether there are other events,
+ // because it gives a bad user experience
+ // (https://github.com/vector-im/vector-web/issues/1204).
+ return this.paginate(direction, size, true, requestLimit - 1);
+ });
+ tl.pendingPaginate = prom;
+ return prom;
+ }
+
+ /**
+ * Remove `delta` events from the start or end of the timeline.
+ *
+ * @param delta - number of events to remove from the timeline
+ * @param startOfTimeline - if events should be removed from the start
+ * of the timeline.
+ */
+ unpaginate(delta, startOfTimeline) {
+ const tl = startOfTimeline ? this.start : this.end;
+ if (!tl) {
+ throw new Error(`Attempting to unpaginate startOfTimeline=${startOfTimeline} but don't have this direction`);
+ }
+
+ // sanity-check the delta
+ if (delta > this.eventCount || delta < 0) {
+ throw new Error(`Attemting to unpaginate ${delta} events, but only have ${this.eventCount} in the timeline`);
+ }
+ while (delta > 0) {
+ const count = startOfTimeline ? tl.advance(delta) : tl.retreat(delta);
+ if (count <= 0) {
+ // sadness. This shouldn't be possible.
+ throw new Error("Unable to unpaginate any further, but still have " + this.eventCount + " events");
+ }
+ delta -= count;
+ this.eventCount -= count;
+ debuglog("TimelineWindow.unpaginate: dropped " + count + " (now " + this.eventCount + ")");
+ }
+ }
+
+ /**
+ * Get a list of the events currently in the window
+ *
+ * @returns the events in the window
+ */
+ getEvents() {
+ if (!this.start) {
+ // not yet loaded
+ return [];
+ }
+ const result = [];
+
+ // iterate through each timeline between this.start and this.end
+ // (inclusive).
+ let timeline = this.start.timeline;
+ // eslint-disable-next-line no-constant-condition
+ while (timeline) {
+ const events = timeline.getEvents();
+
+ // For the first timeline in the chain, we want to start at
+ // this.start.index. For the last timeline in the chain, we want to
+ // stop before this.end.index. Otherwise, we want to copy all of the
+ // events in the timeline.
+ //
+ // (Note that both this.start.index and this.end.index are relative
+ // to their respective timelines' BaseIndex).
+ //
+ let startIndex = 0;
+ let endIndex = events.length;
+ if (timeline === this.start.timeline) {
+ startIndex = this.start.index + timeline.getBaseIndex();
+ }
+ if (timeline === this.end?.timeline) {
+ endIndex = this.end.index + timeline.getBaseIndex();
+ }
+ for (let i = startIndex; i < endIndex; i++) {
+ result.push(events[i]);
+ }
+
+ // if we're not done, iterate to the next timeline.
+ if (timeline === this.end?.timeline) {
+ break;
+ } else {
+ timeline = timeline.getNeighbouringTimeline(_eventTimeline.EventTimeline.FORWARDS);
+ }
+ }
+ return result;
+ }
+}
+
+/**
+ * A thing which contains a timeline reference, and an index into it.
+ * @internal
+ */
+exports.TimelineWindow = TimelineWindow;
+class TimelineIndex {
+ // index: the indexes are relative to BaseIndex, so could well be negative.
+ constructor(timeline, index) {
+ this.timeline = timeline;
+ this.index = index;
+ _defineProperty(this, "pendingPaginate", void 0);
+ }
+
+ /**
+ * @returns the minimum possible value for the index in the current
+ * timeline
+ */
+ minIndex() {
+ return this.timeline.getBaseIndex() * -1;
+ }
+
+ /**
+ * @returns the maximum possible value for the index in the current
+ * timeline (exclusive - ie, it actually returns one more than the index
+ * of the last element).
+ */
+ maxIndex() {
+ return this.timeline.getEvents().length - this.timeline.getBaseIndex();
+ }
+
+ /**
+ * Try move the index forward, or into the neighbouring timeline
+ *
+ * @param delta - number of events to advance by
+ * @returns number of events successfully advanced by
+ */
+ advance(delta) {
+ if (!delta) {
+ return 0;
+ }
+
+ // first try moving the index in the current timeline. See if there is room
+ // to do so.
+ let cappedDelta;
+ if (delta < 0) {
+ // we want to wind the index backwards.
+ //
+ // (this.minIndex() - this.index) is a negative number whose magnitude
+ // is the amount of room we have to wind back the index in the current
+ // timeline. We cap delta to this quantity.
+ cappedDelta = Math.max(delta, this.minIndex() - this.index);
+ if (cappedDelta < 0) {
+ this.index += cappedDelta;
+ return cappedDelta;
+ }
+ } else {
+ // we want to wind the index forwards.
+ //
+ // (this.maxIndex() - this.index) is a (positive) number whose magnitude
+ // is the amount of room we have to wind forward the index in the current
+ // timeline. We cap delta to this quantity.
+ cappedDelta = Math.min(delta, this.maxIndex() - this.index);
+ if (cappedDelta > 0) {
+ this.index += cappedDelta;
+ return cappedDelta;
+ }
+ }
+
+ // the index is already at the start/end of the current timeline.
+ //
+ // next see if there is a neighbouring timeline to switch to.
+ const neighbour = this.timeline.getNeighbouringTimeline(delta < 0 ? _eventTimeline.EventTimeline.BACKWARDS : _eventTimeline.EventTimeline.FORWARDS);
+ if (neighbour) {
+ this.timeline = neighbour;
+ if (delta < 0) {
+ this.index = this.maxIndex();
+ } else {
+ this.index = this.minIndex();
+ }
+ debuglog("paginate: switched to new neighbour");
+
+ // recurse, using the next timeline
+ return this.advance(delta);
+ }
+ return 0;
+ }
+
+ /**
+ * Try move the index backwards, or into the neighbouring timeline
+ *
+ * @param delta - number of events to retreat by
+ * @returns number of events successfully retreated by
+ */
+ retreat(delta) {
+ return this.advance(delta * -1) * -1;
+ }
+}
+exports.TimelineIndex = TimelineIndex; \ No newline at end of file