summaryrefslogtreecommitdiff
path: root/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-timeline.ts
diff options
context:
space:
mode:
Diffstat (limited to 'includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-timeline.ts')
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-timeline.ts458
1 files changed, 458 insertions, 0 deletions
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-timeline.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-timeline.ts
new file mode 100644
index 0000000..d1ba321
--- /dev/null
+++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-timeline.ts
@@ -0,0 +1,458 @@
+/*
+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.
+*/
+
+import { logger } from "../logger";
+import { IMarkerFoundOptions, RoomState } from "./room-state";
+import { EventTimelineSet } from "./event-timeline-set";
+import { MatrixEvent } from "./event";
+import { Filter } from "../filter";
+import { EventType } from "../@types/event";
+
+export interface IInitialiseStateOptions extends Pick<IMarkerFoundOptions, "timelineWasEmpty"> {
+ // This is a separate interface without any extra stuff currently added on
+ // top of `IMarkerFoundOptions` just because it feels like they have
+ // different concerns. One shouldn't necessarily look to add to
+ // `IMarkerFoundOptions` just because they want to add an extra option to
+ // `initialiseState`.
+}
+
+export interface IAddEventOptions extends Pick<IMarkerFoundOptions, "timelineWasEmpty"> {
+ /** Whether to insert the new event at the start of the timeline where the
+ * oldest events are (timeline is in chronological order, oldest to most
+ * recent) */
+ toStartOfTimeline: boolean;
+ /** The state events to reconcile metadata from */
+ roomState?: RoomState;
+}
+
+export enum Direction {
+ Backward = "b",
+ Forward = "f",
+}
+
+export class EventTimeline {
+ /**
+ * Symbolic constant for methods which take a 'direction' argument:
+ * refers to the start of the timeline, or backwards in time.
+ */
+ public static readonly BACKWARDS = Direction.Backward;
+
+ /**
+ * Symbolic constant for methods which take a 'direction' argument:
+ * refers to the end of the timeline, or forwards in time.
+ */
+ public static readonly FORWARDS = Direction.Forward;
+
+ /**
+ * 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
+ */
+ public static setEventMetadata(event: MatrixEvent, stateContext: RoomState, toStartOfTimeline: boolean): void {
+ // 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() === 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;
+ }
+ }
+ }
+
+ private readonly roomId: string | null;
+ private readonly name: string;
+ private events: MatrixEvent[] = [];
+ private baseIndex = 0;
+
+ private startState?: RoomState;
+ private endState?: RoomState;
+ // 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.
+ private startToken: string | null = null;
+ private endToken: string | null = null;
+
+ private prevTimeline: EventTimeline | null = null;
+ private nextTimeline: EventTimeline | null = null;
+ public paginationRequests: Record<Direction, Promise<boolean> | null> = {
+ [Direction.Backward]: null,
+ [Direction.Forward]: null,
+ };
+
+ /**
+ * 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
+ */
+ public constructor(private readonly eventTimelineSet: EventTimelineSet) {
+ this.roomId = eventTimelineSet.room?.roomId ?? null;
+ if (this.roomId) {
+ this.startState = new RoomState(this.roomId);
+ this.endState = new 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.
+ */
+ public initialiseState(stateEvents: MatrixEvent[], { timelineWasEmpty }: IInitialiseStateOptions = {}): void {
+ 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
+ */
+ public forkLive(direction: Direction): EventTimeline {
+ 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
+ */
+ public fork(direction: Direction): EventTimeline {
+ 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
+ */
+ public getRoomId(): string | null {
+ return this.roomId;
+ }
+
+ /**
+ * Get the filter for this timeline's timelineSet (if any)
+ * @returns filter
+ */
+ public getFilter(): Filter | undefined {
+ return this.eventTimelineSet.getFilter();
+ }
+
+ /**
+ * Get the timelineSet for this timeline
+ * @returns timelineSet
+ */
+ public getTimelineSet(): EventTimelineSet {
+ 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).
+ */
+ public getBaseIndex(): number {
+ return this.baseIndex;
+ }
+
+ /**
+ * Get the list of events in this context
+ *
+ * @returns An array of MatrixEvents
+ */
+ public getEvents(): MatrixEvent[] {
+ 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
+ */
+ public getState(direction: Direction): RoomState | undefined {
+ 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
+ */
+ public getPaginationToken(direction: Direction): string | null {
+ 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.
+ */
+ public setPaginationToken(token: string | null, direction: Direction): void {
+ 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.
+ */
+ public getNeighbouringTimeline(direction: Direction): EventTimeline | null {
+ 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.
+ */
+ public setNeighbouringTimeline(neighbour: EventTimeline, direction: Direction): void {
+ 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
+ */
+ public addEvent(event: MatrixEvent, { toStartOfTimeline, roomState, timelineWasEmpty }: IAddEventOptions): void;
+ /**
+ * @deprecated In favor of the overload with `IAddEventOptions`
+ */
+ public addEvent(event: MatrixEvent, toStartOfTimeline: boolean, roomState?: RoomState): void;
+ public addEvent(
+ event: MatrixEvent,
+ toStartOfTimelineOrOpts: boolean | IAddEventOptions,
+ roomState?: RoomState,
+ ): void {
+ let toStartOfTimeline = !!toStartOfTimelineOrOpts;
+ let timelineWasEmpty: boolean | undefined;
+ if (typeof toStartOfTimelineOrOpts === "object") {
+ ({ toStartOfTimeline, roomState, timelineWasEmpty } = toStartOfTimelineOrOpts);
+ } else if (toStartOfTimelineOrOpts !== undefined) {
+ // Deprecation warning
+ // FIXME: Remove after 2023-06-01 (technical debt)
+ 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() === EventType.RoomMember && !toStartOfTimeline)) {
+ EventTimeline.setEventMetadata(event, roomState!, toStartOfTimeline);
+ }
+ }
+ }
+
+ let insertIndex: number;
+
+ if (toStartOfTimeline) {
+ insertIndex = 0;
+ } else {
+ insertIndex = this.events.length;
+ }
+
+ this.events.splice(insertIndex, 0, event); // insert element
+ if (toStartOfTimeline) {
+ this.baseIndex++;
+ }
+ }
+
+ /**
+ * Remove an event from the timeline
+ *
+ * @param eventId - ID of event to be removed
+ * @returns removed event, or null if not found
+ */
+ public removeEvent(eventId: string): MatrixEvent | null {
+ 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
+ */
+ public toString(): string {
+ return this.name;
+ }
+}