feat: complete Ancestor quote bot with production-ready Docker support

This commit is contained in:
unfunny
2026-09-13 14:14:38 -04:00
parent f2016da05a
commit fe7351a7dc
1145 changed files with 30890 additions and 97684 deletions

View File

@@ -1,34 +1,42 @@
import _objectSpread from "@babel/runtime/helpers/objectSpread2";
import _defineProperty from "@babel/runtime/helpers/defineProperty";
/*
Copyright 2021 - 2023 The Matrix.org Foundation C.I.C.
"use strict";
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 { PendingEventOrdering } from "../client.js";
import { TypedReEmitter } from "../ReEmitter.js";
import { RelationType } from "../@types/event.js";
import { MatrixEvent, MatrixEventEvent } from "./event.js";
import { Direction, EventTimeline } from "./event-timeline.js";
import { EventTimelineSet } from "./event-timeline-set.js";
import { RoomEvent } from "./room.js";
import { ServerControlledNamespacedValue } from "../NamespacedValue.js";
import { logger } from "../logger.js";
import { ReadReceipt } from "./read-receipt.js";
import { ReceiptType } from "../@types/read_receipts.js";
import { Feature, ServerSupport } from "../feature.js";
export let ThreadEvent = /*#__PURE__*/function (ThreadEvent) {
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.ThreadFilterType = exports.ThreadEvent = exports.Thread = exports.THREAD_RELATION_TYPE = exports.FeatureSupport = exports.FILTER_RELATED_BY_SENDERS = exports.FILTER_RELATED_BY_REL_TYPES = void 0;
exports.determineFeatureSupport = determineFeatureSupport;
exports.threadFilterTypeToFilter = threadFilterTypeToFilter;
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _client = require("../client");
var _ReEmitter = require("../ReEmitter");
var _event = require("../@types/event");
var _event2 = require("./event");
var _eventTimeline = require("./event-timeline");
var _eventTimelineSet = require("./event-timeline-set");
var _room = require("./room");
var _NamespacedValue = require("../NamespacedValue");
var _logger = require("../logger");
var _readReceipt = require("./read-receipt");
var _read_receipts = require("../@types/read_receipts");
var _feature = require("../feature");
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } /*
Copyright 2021 - 2023 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 ThreadEvent = /*#__PURE__*/function (ThreadEvent) {
ThreadEvent["New"] = "Thread.new";
ThreadEvent["Update"] = "Thread.update";
ThreadEvent["NewReply"] = "Thread.newReply";
@@ -36,13 +44,18 @@ export let ThreadEvent = /*#__PURE__*/function (ThreadEvent) {
ThreadEvent["Delete"] = "Thread.delete";
return ThreadEvent;
}({});
export let FeatureSupport = /*#__PURE__*/function (FeatureSupport) {
/**
* @deprecated please use ThreadEventHandlerMap instead
*/
exports.ThreadEvent = ThreadEvent;
let FeatureSupport = /*#__PURE__*/function (FeatureSupport) {
FeatureSupport[FeatureSupport["None"] = 0] = "None";
FeatureSupport[FeatureSupport["Experimental"] = 1] = "Experimental";
FeatureSupport[FeatureSupport["Stable"] = 2] = "Stable";
return FeatureSupport;
}({});
export function determineFeatureSupport(stable, unstable) {
exports.FeatureSupport = FeatureSupport;
function determineFeatureSupport(stable, unstable) {
if (stable) {
return FeatureSupport.Stable;
} else if (unstable) {
@@ -51,80 +64,40 @@ export function determineFeatureSupport(stable, unstable) {
return FeatureSupport.None;
}
}
export class Thread extends ReadReceipt {
class Thread extends _readReceipt.ReadReceipt {
constructor(id, rootEvent, opts) {
var _opts$pendingEventOrd;
super();
// each Event in the thread adds a reemitter, so we could hit the listener limit.
this.id = id;
this.rootEvent = rootEvent;
/**
* A reference to all the events ID at the bottom of the threads
*/
_defineProperty(this, "timelineSet", void 0);
_defineProperty(this, "_currentUserParticipated", false);
_defineProperty(this, "reEmitter", void 0);
/**
* The last event in this thread, if we don't yet have this in the timeline.
*
* When we run {@link processRootEvent} (which I think happens during the
* setting-up of the thread), we set this to the event pointed to by the
* server in `latest_event` [1] that came through with the thread root.
*
* [1]: https://spec.matrix.org/v1.8/client-server-api/#server-side-aggregation-of-mthread-relationships
*
* Later, when we have populated the timeline, this is set to undefined, so
* that methods like {@link replyToEvent} fall through to use lastReply,
* which looks in the timeline for the latest event that is a "thread reply"
* i.e. directly refers to the thread root with an m.thread relation.
*
* So it looks like this is only really relevant when initialEventsFetched
* is false, because as soon as the initial events have been fetched, we
* should have a proper chunk of timeline from the pagination fetch.
*
* If all replies in this thread are redacted, this is set to the root
* event. I'm not clear what the meaning of this is, since usually after the
* initial events have been fetched, lastEvent should be undefined.
* In fact, the whole usage inside onRedaction looks suspect - it may be
* that we were thinking lastEvent always refers to the actual last event,
* but it only does so before initialEventsFetched becomes true.
*
* The usage of lastEvent inside {@link onEcho} looks suspicious, since I'd
* think we probably mean {@link replyToEvent} there - we are trying not to
* echo a duplicate event, and we probably want that behaviour even after
* initialEventsFetched has become true.
*
* -- andyb
*/
_defineProperty(this, "lastEvent", void 0);
_defineProperty(this, "replyCount", 0);
_defineProperty(this, "lastPendingEvent", void 0);
_defineProperty(this, "pendingReplyCount", 0);
_defineProperty(this, "room", void 0);
_defineProperty(this, "client", void 0);
_defineProperty(this, "pendingEventOrdering", void 0);
_defineProperty(this, "processRootEventPromise", void 0);
/**
* Whether or not we need to fetch the initial set of events for the thread. We can
* only do this if the server has support for it, so if it doesn't we just pretend
* that we've already fetched them.
*/
_defineProperty(this, "initialEventsFetched", !Thread.hasServerSideSupport);
_defineProperty(this, "initalEventFetchProm", void 0);
_defineProperty(this, "preInitEventTargets", new Map());
_defineProperty(this, "preInitObserverState", {
active: true
});
(0, _defineProperty2.default)(this, "timelineSet", void 0);
(0, _defineProperty2.default)(this, "timeline", []);
(0, _defineProperty2.default)(this, "_currentUserParticipated", false);
(0, _defineProperty2.default)(this, "reEmitter", void 0);
(0, _defineProperty2.default)(this, "lastEvent", void 0);
(0, _defineProperty2.default)(this, "replyCount", 0);
(0, _defineProperty2.default)(this, "lastPendingEvent", void 0);
(0, _defineProperty2.default)(this, "pendingReplyCount", 0);
(0, _defineProperty2.default)(this, "room", void 0);
(0, _defineProperty2.default)(this, "client", void 0);
(0, _defineProperty2.default)(this, "pendingEventOrdering", void 0);
(0, _defineProperty2.default)(this, "processRootEventPromise", void 0);
(0, _defineProperty2.default)(this, "initialEventsFetched", !Thread.hasServerSideSupport);
/**
* An array of events to add to the timeline once the thread has been initialised
* with server suppport.
*/
_defineProperty(this, "replayEvents", []);
_defineProperty(this, "onTimelineReset", async () => {
(0, _defineProperty2.default)(this, "replayEvents", []);
(0, _defineProperty2.default)(this, "onTimelineReset", async () => {
// We hit a gappy sync, ask the server for an update
await this.processRootEventPromise;
this.processRootEventPromise = undefined;
});
_defineProperty(this, "onBeforeRedaction", (event, redaction) => {
if (event?.isRelation(THREAD_RELATION_TYPE.name) && this.room.eventShouldLiveIn(event).threadId === this.id && event.getId() !== this.id &&
(0, _defineProperty2.default)(this, "onBeforeRedaction", (event, redaction) => {
if (event !== null && event !== void 0 && event.isRelation(THREAD_RELATION_TYPE.name) && this.room.eventShouldLiveIn(event).threadId === this.id && event.getId() !== this.id &&
// the root event isn't counted in the length so ignore this redaction
!redaction.status // only respect it when it succeeds
) {
@@ -133,8 +106,8 @@ export class Thread extends ReadReceipt {
this.emit(ThreadEvent.Update, this);
}
});
_defineProperty(this, "onRedaction", async (event, room, threadRootId) => {
if (threadRootId !== this.id) return; // ignore redactions for other timelines
(0, _defineProperty2.default)(this, "onRedaction", async event => {
if (event.threadRootId !== this.id) return; // ignore redactions for other timelines
if (this.replyCount <= 0) {
for (const threadEvent of this.timeline) {
this.clearEventMetadata(threadEvent);
@@ -143,7 +116,8 @@ export class Thread extends ReadReceipt {
this._currentUserParticipated = false;
this.emit(ThreadEvent.Delete, this);
} else {
if (this.lastEvent?.getId() === event.getAssociatedId()) {
var _this$lastEvent;
if (((_this$lastEvent = this.lastEvent) === null || _this$lastEvent === void 0 ? void 0 : _this$lastEvent.getId()) === event.getAssociatedId()) {
// XXX: If our last event got redacted we query the server for the last event once again
await this.processRootEventPromise;
this.processRootEventPromise = undefined;
@@ -151,23 +125,23 @@ export class Thread extends ReadReceipt {
await this.updateThreadMetadata();
}
});
_defineProperty(this, "onTimelineEvent", (event, room, toStartOfTimeline) => {
(0, _defineProperty2.default)(this, "onTimelineEvent", (event, room, toStartOfTimeline) => {
// Add a synthesized receipt when paginating forward in the timeline
if (!toStartOfTimeline) {
const sender = event.getSender();
if (sender && room && this.shouldSendLocalEchoReceipt(sender, event)) {
room.addLocalEchoReceipt(sender, event, ReceiptType.Read);
room.addLocalEchoReceipt(sender, event, _read_receipts.ReceiptType.Read);
}
if (event.getId() !== this.id && event.isRelation(THREAD_RELATION_TYPE.name)) {
this.replyCount++;
}
}
this.onEcho(event, toStartOfTimeline ?? false);
this.onEcho(event, toStartOfTimeline !== null && toStartOfTimeline !== void 0 ? toStartOfTimeline : false);
});
_defineProperty(this, "onLocalEcho", event => {
(0, _defineProperty2.default)(this, "onLocalEcho", event => {
this.onEcho(event, false);
});
_defineProperty(this, "onEcho", async (event, toStartOfTimeline) => {
(0, _defineProperty2.default)(this, "onEcho", async (event, toStartOfTimeline) => {
if (event.threadRootId !== this.id) return; // ignore echoes for other timelines
if (this.lastEvent === event) return; // ignore duplicate events
await this.updateThreadMetadata();
@@ -177,31 +151,25 @@ export class Thread extends ReadReceipt {
this.lastEvent = undefined;
this.emit(ThreadEvent.NewReply, this, event);
});
this.id = id;
this.rootEvent = rootEvent;
this.setMaxListeners(1000);
if (!opts?.room) {
if (!(opts !== null && opts !== void 0 && opts.room)) {
// Logging/debugging for https://github.com/vector-im/element-web/issues/22141
// Hope is that we end up with a more obvious stack trace.
throw new Error("element-web#22141: A thread requires a room in order to function");
}
this.room = opts.room;
this.client = opts.client;
this.pendingEventOrdering = opts.pendingEventOrdering ?? PendingEventOrdering.Chronological;
this.timelineSet = new EventTimelineSet(this.room, {
this.pendingEventOrdering = (_opts$pendingEventOrd = opts.pendingEventOrdering) !== null && _opts$pendingEventOrd !== void 0 ? _opts$pendingEventOrd : _client.PendingEventOrdering.Chronological;
this.timelineSet = new _eventTimelineSet.EventTimelineSet(this.room, {
timelineSupport: true,
pendingEvents: true
}, this.client, this);
this.reEmitter = new TypedReEmitter(this);
this.reEmitter.reEmit(this.timelineSet, [RoomEvent.Timeline, RoomEvent.TimelineReset]);
this.once(ThreadEvent.Delete, () => {
this.preInitObserverState.active = false;
});
this.room.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
this.room.on(RoomEvent.Redaction, this.onRedaction);
this.room.on(RoomEvent.LocalEchoUpdated, this.onLocalEcho);
this.room.on(RoomEvent.TimelineReset, this.onTimelineReset);
this.timelineSet.on(RoomEvent.Timeline, this.onTimelineEvent);
this.reEmitter = new _ReEmitter.TypedReEmitter(this);
this.reEmitter.reEmit(this.timelineSet, [_room.RoomEvent.Timeline, _room.RoomEvent.TimelineReset]);
this.room.on(_event2.MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
this.room.on(_room.RoomEvent.Redaction, this.onRedaction);
this.room.on(_room.RoomEvent.LocalEchoUpdated, this.onLocalEcho);
this.room.on(_room.RoomEvent.TimelineReset, this.onTimelineReset);
this.timelineSet.on(_room.RoomEvent.Timeline, this.onTimelineEvent);
this.processReceipts(opts.receipts);
// even if this thread is thought to be originating from this client, we initialise it as we may be in a
@@ -210,20 +178,19 @@ export class Thread extends ReadReceipt {
this.setEventMetadata(this.rootEvent);
}
async fetchRootEvent() {
// Always fetch the root event, even if we already have it, so we can get the latest
// state (via unsigned).
this.rootEvent = this.room.findEventById(this.id);
// If the rootEvent does not exist in the local stores, then fetch it from the server.
try {
const eventData = await this.client.fetchRoomEvent(this.roomId, this.id);
const mapper = this.client.getEventMapper();
this.rootEvent = mapper(eventData); // will merge with existing event object if such is known
} catch (e) {
logger.error("Failed to fetch thread root to construct thread with", e);
_logger.logger.error("Failed to fetch thread root to construct thread with", e);
}
await this.processEvent(this.rootEvent);
}
static setServerSideSupport(status) {
Thread.hasServerSideSupport = status;
// XXX: This global latching behaviour is really unexpected and means that you can't undo when moving to a server without support
if (status !== FeatureSupport.Stable) {
FILTER_RELATED_BY_SENDERS.setPreferUnstable(true);
FILTER_RELATED_BY_REL_TYPES.setPreferUnstable(true);
@@ -237,12 +204,14 @@ export class Thread extends ReadReceipt {
Thread.hasServerSideFwdPaginationSupport = status;
}
shouldSendLocalEchoReceipt(sender, event) {
const recursionSupport = this.client.canSupport.get(Feature.RelationsRecursion) ?? ServerSupport.Unsupported;
if (recursionSupport === ServerSupport.Unsupported) {
var _this$client$canSuppo;
const recursionSupport = (_this$client$canSuppo = this.client.canSupport.get(_feature.Feature.RelationsRecursion)) !== null && _this$client$canSuppo !== void 0 ? _this$client$canSuppo : _feature.ServerSupport.Unsupported;
if (recursionSupport === _feature.ServerSupport.Unsupported) {
var _this$getReadReceiptF;
// Normally we add a local receipt, but if we don't have
// recursion support, then events may arrive out of order, so we
// only create a receipt if it's after our existing receipt.
const oldReceiptEventId = this.getReadReceiptForUserId(sender)?.eventId;
const oldReceiptEventId = (_this$getReadReceiptF = this.getReadReceiptForUserId(sender)) === null || _this$getReadReceiptF === void 0 ? void 0 : _this$getReadReceiptF.eventId;
if (oldReceiptEventId) {
const receiptEvent = this.findEventById(oldReceiptEventId);
if (receiptEvent && receiptEvent.getTs() > event.getTs()) {
@@ -253,16 +222,16 @@ export class Thread extends ReadReceipt {
return true;
}
get roomState() {
return this.room.getLiveTimeline().getState(EventTimeline.FORWARDS);
return this.room.getLiveTimeline().getState(_eventTimeline.EventTimeline.FORWARDS);
}
addEventToTimeline(event, toStartOfTimeline) {
if (!this.findEventById(event.getId())) {
this.timelineSet.addEventToTimeline(event, this.liveTimeline, {
toStartOfTimeline,
fromCache: false,
roomState: this.roomState,
addToState: false
roomState: this.roomState
});
this.timeline = this.events;
}
}
@@ -288,7 +257,10 @@ export class Thread extends ReadReceipt {
if (this.findEventById(eventId)) {
return;
}
this.timelineSet.insertEventIntoTimeline(event, this.liveTimeline, this.roomState, false);
this.timelineSet.insertEventIntoTimeline(event, this.liveTimeline, this.roomState);
// As far as we know, timeline should always be the same as events
this.timeline = this.events;
}
addEvents(events, toStartOfTimeline) {
events.forEach(ev => this.addEvent(ev, toStartOfTimeline, false));
@@ -304,47 +276,49 @@ export class Thread extends ReadReceipt {
* to the start (and not the end) of the timeline.
* @param emit - whether to emit the Update event if the thread was updated or not.
*/
addEvent(event, toStartOfTimeline, emit = true) {
// Modify this event to point at our room's state, and mark its thread
// as this.
async addEvent(event, toStartOfTimeline, emit = true) {
this.setEventMetadata(event);
const eventId = event.getId();
if (!this.initialEventsFetched && eventId && !event.isRelation(RelationType.Annotation) && !event.isRelation(RelationType.Replace)) {
this.preInitEventTargets.set(eventId, event);
}
// Decide whether this event is going to be added at the end of the timeline.
const lastReply = this.lastReply();
const isNewestReply = !lastReply || event.localTimestamp >= lastReply.localTimestamp;
// Add all incoming events to the thread's timeline set when there's no server support
if (!Thread.hasServerSideSupport) {
// When there's no server-side support, just add it to the end of the timeline.
// all the relevant membership info to hydrate events with a sender
// is held in the main room timeline
// We want to fetch the room state from there and pass it down to this thread
// timeline set to let it reconcile an event with its relevant RoomMember
this.addEventToTimeline(event, toStartOfTimeline);
this.client.decryptEventIfNeeded(event);
} else if (event.isRelation(RelationType.Annotation) || event.isRelation(RelationType.Replace)) {
this.addRelatedThreadEvent(event, toStartOfTimeline);
return;
} else if (!toStartOfTimeline && isNewestReply) {
// When we've asked for the event to be added to the end,
// and this event belongs at the end, add it.
this.client.decryptEventIfNeeded(event, {});
} else if (!toStartOfTimeline && this.initialEventsFetched && isNewestReply) {
this.addEventToTimeline(event, false);
this.fetchEditsWhereNeeded(event);
} else {
// Otherwise, we should add it, but we suspect it is out of order.
// This may be because we've just created the thread object and are
// still fetching events, in which case add it where we think is sensible
// and it will be removed and replaced with the events from the pagination
// request once that completes.
if (toStartOfTimeline) {
// If we're adding at the start of the timeline, it doesn't
// matter that it's out of order.
this.addEventToTimeline(event, toStartOfTimeline);
} else if (event.isRelation(_event.RelationType.Annotation) || event.isRelation(_event.RelationType.Replace)) {
var _this$timelineSet$rel, _this$timelineSet$rel2;
if (!this.initialEventsFetched) {
var _this$replayEvents;
/**
* A thread can be fully discovered via a single sync response
* And when that's the case we still ask the server to do an initialisation
* as it's the safest to ensure we have everything.
* However when we are in that scenario we might loose annotation or edits
*
* This fix keeps a reference to those events and replay them once the thread
* has been initialised properly.
*/
(_this$replayEvents = this.replayEvents) === null || _this$replayEvents === void 0 ? void 0 : _this$replayEvents.push(event);
} else {
// We think this event might be out of order, because isNewestReply
// is false (otherwise we would have gone into the earlier if
// clause), so try to insert it in the right place based on
// timestamp.
this.insertEventIntoTimeline(event);
var _this$client$canSuppo2;
const recursionSupport = (_this$client$canSuppo2 = this.client.canSupport.get(_feature.Feature.RelationsRecursion)) !== null && _this$client$canSuppo2 !== void 0 ? _this$client$canSuppo2 : _feature.ServerSupport.Unsupported;
if (recursionSupport === _feature.ServerSupport.Unsupported) {
this.insertEventIntoTimeline(event);
} else {
this.addEventToTimeline(event, toStartOfTimeline);
}
}
// Apply annotations and replace relations to the relations of the timeline only
(_this$timelineSet$rel = this.timelineSet.relations) === null || _this$timelineSet$rel === void 0 ? void 0 : _this$timelineSet$rel.aggregateParentEvent(event);
(_this$timelineSet$rel2 = this.timelineSet.relations) === null || _this$timelineSet$rel2 === void 0 ? void 0 : _this$timelineSet$rel2.aggregateChildEvent(event, this.timelineSet);
return;
}
if (event.getId() !== this.id && event.isRelation(THREAD_RELATION_TYPE.name) && !toStartOfTimeline && isNewestReply) {
// Clear the last event as we have the latest end of the timeline
@@ -355,87 +329,12 @@ export class Thread extends ReadReceipt {
this.updateThreadMetadata();
}
}
addRelatedThreadEvent(event, toStartOfTimeline) {
// If this event is not a direct member of the thread, but is a
// reference to something that is, then we have two cases:
if (!this.initialEventsFetched) {
// Case 1: we haven't yet fetched events from the server. In
// this case, when we do, the events we get back might only be
// the first-order ones, so this event (which is second-order -
// a reference to something directly in the thread) needs to be
// kept so we can replay it when the first-order ones turn up.
/**
* A thread can be fully discovered via a single sync response
* And when that's the case we still ask the server to do an initialisation
* as it's the safest to ensure we have everything.
* However when we are in that scenario we might loose annotation or edits
*
* This fix keeps a reference to those events and replay them once the thread
* has been initialised properly.
*/
this.replayEvents?.push(event);
// Aggregate annotations immediately to keep reaction counts visible.
if (event.isRelation(RelationType.Annotation)) {
this.timelineSet.relations?.aggregateChildEvent(event, this.timelineSet);
}
// Edits can also be aggregated immediately when their target is already
// known. If it is not known yet, replay still handles them after pagination.
if (event.isRelation(RelationType.Replace)) {
const targetEventId = event.getRelation()?.event_id;
const targetEvent = targetEventId ? this.findPreInitTargetEvent(targetEventId) : undefined;
if (targetEvent && targetEventId) {
const weakThread = new WeakRef(this);
const observerState = this.preInitObserverState;
const eventId = event.getId();
void this.timelineSet.relations.aggregateChildEvent(event, this.timelineSet, targetEvent).then(() => {
const thread = weakThread.deref();
if (!thread || !observerState.active) return;
const effectiveTarget = thread.findPreInitTargetEvent(targetEventId);
if (effectiveTarget?.replacingEvent()?.getId() === eventId) {
thread.emit(ThreadEvent.Update, thread);
}
}).catch(error => logger.error("Failed to aggregate pre-initialization thread edit: ", error));
}
}
} else {
// Case 2: this is happening later, and we have a timeline. In
// this case, these events might be out-of order.
//
// Specifically, if the server doesn't support recursion, so we
// only get these events through sync, they might be coming
// later than the first-order ones, so we insert them based on
// timestamp (despite the problems with this documented in
// #3325).
//
// If the server does support recursion, we should have got all
// the interspersed events from the server when we fetched the
// initial events, so if they are coming via sync they should be
// the latest ones, so we can add them as normal.
//
// (Note that both insertEventIntoTimeline and addEventToTimeline
// do nothing if we have seen this event before.)
const recursionSupport = this.client.canSupport.get(Feature.RelationsRecursion) ?? ServerSupport.Unsupported;
if (recursionSupport === ServerSupport.Unsupported) {
this.insertEventIntoTimeline(event);
} else {
this.addEventToTimeline(event, toStartOfTimeline);
}
// Aggregation is handled by EventTimelineSet when inserting/adding.
}
}
findPreInitTargetEvent(eventId) {
return this.timelineSet.findEventById(eventId) ?? this.room.findEventById(eventId) ?? this.preInitEventTargets.get(eventId) ?? (this.lastEvent?.getId() === eventId ? this.lastEvent : undefined);
}
async processEvent(event) {
if (event) {
this.setEventMetadata(event);
await this.fetchEditsWhereNeeded(event);
}
this.timeline = this.events;
}
/**
@@ -456,7 +355,7 @@ export class Thread extends ReadReceipt {
}
}
getRootEventBundledRelationship(rootEvent = this.rootEvent) {
return rootEvent?.getServerAggregatedRelation(THREAD_RELATION_TYPE.name);
return rootEvent === null || rootEvent === void 0 ? void 0 : rootEvent.getServerAggregatedRelation(THREAD_RELATION_TYPE.name);
}
async processRootEvent() {
const bundledRelationship = this.getRootEventBundledRelationship();
@@ -473,8 +372,11 @@ export class Thread extends ReadReceipt {
}
}
updatePendingReplyCount() {
const unfilteredPendingEvents = this.pendingEventOrdering === PendingEventOrdering.Detached ? this.room.getPendingEvents() : this.events;
const pendingEvents = unfilteredPendingEvents.filter(ev => ev.threadRootId === this.id && ev.isRelation(THREAD_RELATION_TYPE.name) && ev.status !== null && ev.getId() !== this.lastEvent?.getId());
const unfilteredPendingEvents = this.pendingEventOrdering === _client.PendingEventOrdering.Detached ? this.room.getPendingEvents() : this.events;
const pendingEvents = unfilteredPendingEvents.filter(ev => {
var _this$lastEvent2;
return ev.threadRootId === this.id && ev.isRelation(THREAD_RELATION_TYPE.name) && ev.status !== null && ev.getId() !== ((_this$lastEvent2 = this.lastEvent) === null || _this$lastEvent2 === void 0 ? void 0 : _this$lastEvent2.getId());
});
this.lastPendingEvent = pendingEvents.length ? pendingEvents[pendingEvents.length - 1] : undefined;
this.pendingReplyCount = pendingEvents.length;
}
@@ -495,7 +397,7 @@ export class Thread extends ReadReceipt {
*/
async resetLiveTimeline(backPaginationToken, forwardPaginationToken) {
const oldLive = this.liveTimeline;
this.timelineSet.resetLiveTimeline(backPaginationToken ?? undefined, forwardPaginationToken ?? undefined);
this.timelineSet.resetLiveTimeline(backPaginationToken !== null && backPaginationToken !== void 0 ? backPaginationToken : undefined, forwardPaginationToken !== null && forwardPaginationToken !== void 0 ? forwardPaginationToken : undefined);
const newLive = this.liveTimeline;
// FIXME: Remove the following as soon as https://github.com/matrix-org/synapse/issues/14830 is resolved.
@@ -511,20 +413,22 @@ export class Thread extends ReadReceipt {
let newBackward;
let oldForward;
if (backPaginationToken) {
const res = await this.client.createMessagesRequest(this.roomId, backPaginationToken, 1, Direction.Forward);
const res = await this.client.createMessagesRequest(this.roomId, backPaginationToken, 1, _eventTimeline.Direction.Forward);
newBackward = res.end;
}
if (forwardPaginationToken) {
const res = await this.client.createMessagesRequest(this.roomId, forwardPaginationToken, 1, Direction.Backward);
const res = await this.client.createMessagesRequest(this.roomId, forwardPaginationToken, 1, _eventTimeline.Direction.Backward);
oldForward = res.start;
}
// Only replace the token if we don't have paginated away from this position already. This situation doesn't
// occur today, but if the above issue is resolved, we'd have to go down this path.
if (forwardPaginationToken && oldLive.getPaginationToken(Direction.Forward) === forwardPaginationToken) {
oldLive.setPaginationToken(oldForward ?? null, Direction.Forward);
if (forwardPaginationToken && oldLive.getPaginationToken(_eventTimeline.Direction.Forward) === forwardPaginationToken) {
var _oldForward;
oldLive.setPaginationToken((_oldForward = oldForward) !== null && _oldForward !== void 0 ? _oldForward : null, _eventTimeline.Direction.Forward);
}
if (backPaginationToken && newLive.getPaginationToken(Direction.Backward) === backPaginationToken) {
newLive.setPaginationToken(newBackward ?? null, Direction.Backward);
if (backPaginationToken && newLive.getPaginationToken(_eventTimeline.Direction.Backward) === backPaginationToken) {
var _newBackward;
newLive.setPaginationToken((_newBackward = newBackward) !== null && _newBackward !== void 0 ? _newBackward : null, _eventTimeline.Direction.Backward);
}
}
async updateThreadFromRootEvent() {
@@ -546,49 +450,30 @@ export class Thread extends ReadReceipt {
}
await this.processRootEventPromise;
if (!this.initialEventsFetched) {
if (this.initalEventFetchProm) {
await this.initalEventFetchProm;
} else {
// fetch initial events to allow proper pagination
try {
// clear out any events that were added before the pagination request
// completed (eg. from sync). They'll be replaced by those from the pagination.
// Really, we should do this after the pagination request completes, but
// paginateEventTimeline does the request and adds the events in one go, so
// this would need a refactor in order to do. It's therefore possible there's
// a remaining race where an event comes in while the pagination request is
// happening.
this.timelineSet.resetLiveTimeline();
// if the thread has regular events, this will just load the last reply.
// if the thread is newly created, this will load the root event.
if (this.replyCount === 0 && this.rootEvent) {
this.timelineSet.addEventsToTimeline([this.rootEvent], true, false, this.liveTimeline, null);
this.liveTimeline.setPaginationToken(null, Direction.Backward);
} else {
this.initalEventFetchProm = this.client.paginateEventTimeline(this.liveTimeline, {
backwards: true
});
await this.initalEventFetchProm;
}
// We have now fetched the initial events, so set the flag. We need to do this before
// we actually add the events, so `this.addEvents` knows that it can now safely add
// them rather than buffer them in the pending event list. The main thing is that this
// must remain false while the async fetch happens, so we don't try to add events before
// the pagination has finished. The important thing is that we're not await-ing anything
// else between setting this and adding events, so no races.
this.initialEventsFetched = true;
for (const event of this.replayEvents) {
this.addEvent(event, false);
}
this.replayEvents = null;
this.preInitEventTargets.clear();
// just to make sure that, if we've created a timeline window for this thread before the thread itself
// existed (e.g. when creating a new thread), we'll make sure the panel is force refreshed correctly.
this.emit(RoomEvent.TimelineReset, this.room, this.timelineSet, true);
} catch (e) {
logger.error("Failed to load start of newly created thread: ", e);
this.initialEventsFetched = false;
this.initialEventsFetched = true;
// fetch initial event to allow proper pagination
try {
// if the thread has regular events, this will just load the last reply.
// if the thread is newly created, this will load the root event.
if (this.replyCount === 0 && this.rootEvent) {
this.timelineSet.addEventsToTimeline([this.rootEvent], true, this.liveTimeline, null);
this.liveTimeline.setPaginationToken(null, _eventTimeline.Direction.Backward);
} else {
await this.client.paginateEventTimeline(this.liveTimeline, {
backwards: true,
limit: Math.max(1, this.length)
});
}
for (const event of this.replayEvents) {
this.addEvent(event, false);
}
this.replayEvents = null;
// just to make sure that, if we've created a timeline window for this thread before the thread itself
// existed (e.g. when creating a new thread), we'll make sure the panel is force refreshed correctly.
this.emit(_room.RoomEvent.TimelineReset, this.room, this.timelineSet, true);
} catch (e) {
_logger.logger.error("Failed to load start of newly created thread: ", e);
this.initialEventsFetched = false;
}
}
this.emit(ThreadEvent.Update, this);
@@ -596,11 +481,12 @@ export class Thread extends ReadReceipt {
// XXX: Workaround for https://github.com/matrix-org/matrix-spec-proposals/pull/2676/files#r827240084
async fetchEditsWhereNeeded(...events) {
const recursionSupport = this.client.canSupport.get(Feature.RelationsRecursion) ?? ServerSupport.Unsupported;
if (recursionSupport === ServerSupport.Unsupported) {
var _this$client$canSuppo3;
const recursionSupport = (_this$client$canSuppo3 = this.client.canSupport.get(_feature.Feature.RelationsRecursion)) !== null && _this$client$canSuppo3 !== void 0 ? _this$client$canSuppo3 : _feature.ServerSupport.Unsupported;
if (recursionSupport === _feature.ServerSupport.Unsupported) {
return Promise.all(events.filter(isAnEncryptedThreadMessage).map(async event => {
try {
const relations = await this.client.relations(this.roomId, event.getId(), RelationType.Replace, event.getType(), {
const relations = await this.client.relations(this.roomId, event.getId(), _event.RelationType.Replace, event.getType(), {
limit: 1
});
if (relations.events.length) {
@@ -609,21 +495,22 @@ export class Thread extends ReadReceipt {
this.insertEventIntoTimeline(editEvent);
}
} catch (e) {
logger.error("Failed to load edits for encrypted thread event", e);
_logger.logger.error("Failed to load edits for encrypted thread event", e);
}
}));
}
}
setEventMetadata(event) {
if (event) {
EventTimeline.setEventMetadata(event, this.roomState, false);
_eventTimeline.EventTimeline.setEventMetadata(event, this.roomState, false);
event.setThread(this);
}
}
clearEventMetadata(event) {
if (event) {
var _event$event;
event.setThread(undefined);
delete event.event?.unsigned?.["m.relations"]?.[THREAD_RELATION_TYPE.name];
(_event$event = event.event) === null || _event$event === void 0 || (_event$event = _event$event.unsigned) === null || _event$event === void 0 || (_event$event = _event$event["m.relations"]) === null || _event$event === void 0 || delete _event$event[THREAD_RELATION_TYPE.name];
}
}
@@ -637,7 +524,7 @@ export class Thread extends ReadReceipt {
/**
* Return last reply to the thread, if known.
*/
lastReply(matches = ev => ev.isRelation(THREAD_RELATION_TYPE.name)) {
lastReply(matches = ev => ev.isRelation(_event.RelationType.Thread)) {
for (let i = this.timeline.length - 1; i >= 0; i--) {
const event = this.timeline[i];
if (matches(event)) {
@@ -664,23 +551,14 @@ export class Thread extends ReadReceipt {
* This might be a synthesized event, if so, it will not emit any events to listeners.
*/
get replyToEvent() {
return this.lastPendingEvent ?? this.lastEvent ?? this.lastReply();
}
/**
* The live event timeline for this thread.
* @deprecated Present for backwards compatibility.
* Use this.events instead
* @returns The live event timeline for this thread.
*/
get timeline() {
return this.events;
var _ref, _this$lastPendingEven;
return (_ref = (_this$lastPendingEven = this.lastPendingEvent) !== null && _this$lastPendingEven !== void 0 ? _this$lastPendingEven : this.lastEvent) !== null && _ref !== void 0 ? _ref : this.lastReply();
}
get events() {
return this.liveTimeline.getEvents();
}
has(eventId) {
return this.timelineSet.findEventById(eventId) instanceof MatrixEvent;
return this.timelineSet.findEventById(eventId) instanceof _event2.MatrixEvent;
}
get hasCurrentUserParticipated() {
return this._currentUserParticipated;
@@ -705,27 +583,6 @@ export class Thread extends ReadReceipt {
* @returns ID of the latest event that the given user has read, or null.
*/
getEventReadUpTo(userId, ignoreSynthesized) {
// TODO: we think the implementation here is not right. Here is a sketch
// of the right answer:
//
// for event in timeline.events.reversed():
// if room.hasUserReadEvent(event):
// return event
// return null
//
// If this is too slow, we might be able to improve it by trying walking
// forward from the threaded receipt in this thread. We could alternate
// between backwards-from-front and forwards-from-threaded-receipt to
// improve our chances of hitting the right answer sooner.
//
// Either way, it's still fundamentally slow because we have to walk
// events.
//
// We also might just want to limit the time we spend on this by giving
// up after, say, 100 events.
//
// --- andyb
const isCurrentUser = userId === this.client.getUserId();
const lastReply = this.timeline[this.timeline.length - 1];
if (isCurrentUser && lastReply) {
@@ -749,7 +606,8 @@ export class Thread extends ReadReceipt {
if (!unthreadedReceipt) {
return readUpToId;
}
for (let i = this.timeline?.length - 1; i >= 0; --i) {
for (let i = ((_this$timeline = this.timeline) === null || _this$timeline === void 0 ? void 0 : _this$timeline.length) - 1; i >= 0; --i) {
var _this$timeline, _ev$getId;
const ev = this.timeline[i];
// If we encounter the `readUpToId` we do not need to look further
// there is no "more recent" unthreaded read receipt
@@ -759,7 +617,7 @@ export class Thread extends ReadReceipt {
// whether an unthreaded read receipt is more recent that the current event.
// We usually prefer relying on the order of the DAG but in this scenario
// it is not possible and we have to rely on timestamp
if (ev.getTs() < unthreadedReceipt.ts) return ev.getId() ?? readUpToId;
if (ev.getTs() < unthreadedReceipt.ts) return (_ev$getId = ev.getId()) !== null && _ev$getId !== void 0 ? _ev$getId : readUpToId;
}
}
return readUpToId;
@@ -778,18 +636,19 @@ export class Thread extends ReadReceipt {
*/
hasUserReadEvent(userId, eventId) {
if (userId === this.client.getUserId()) {
var _this$lastReply$getTs, _this$lastReply, _this$room$getLastUnt, _this$room$getLastUnt2, _this$lastReply$getTs2, _this$lastReply2;
// Consider an event read if it's part of a thread that is before the
// first threaded receipt sent in that room. It is likely that it is
// part of a thread that was created before MSC3771 was implemented.
// Or before the last unthreaded receipt for the logged in user
const beforeFirstThreadedReceipt = (this.lastReply()?.getTs() ?? 0) < this.room.getOldestThreadedReceiptTs();
const unthreadedReceiptTs = this.room.getLastUnthreadedReceiptFor(userId)?.ts ?? 0;
const beforeLastUnthreadedReceipt = (this?.lastReply()?.getTs() ?? 0) < unthreadedReceiptTs;
const beforeFirstThreadedReceipt = ((_this$lastReply$getTs = (_this$lastReply = this.lastReply()) === null || _this$lastReply === void 0 ? void 0 : _this$lastReply.getTs()) !== null && _this$lastReply$getTs !== void 0 ? _this$lastReply$getTs : 0) < this.room.getOldestThreadedReceiptTs();
const unthreadedReceiptTs = (_this$room$getLastUnt = (_this$room$getLastUnt2 = this.room.getLastUnthreadedReceiptFor(userId)) === null || _this$room$getLastUnt2 === void 0 ? void 0 : _this$room$getLastUnt2.ts) !== null && _this$room$getLastUnt !== void 0 ? _this$room$getLastUnt : 0;
const beforeLastUnthreadedReceipt = ((_this$lastReply$getTs2 = this === null || this === void 0 || (_this$lastReply2 = this.lastReply()) === null || _this$lastReply2 === void 0 ? void 0 : _this$lastReply2.getTs()) !== null && _this$lastReply$getTs2 !== void 0 ? _this$lastReply$getTs2 : 0) < unthreadedReceiptTs;
if (beforeFirstThreadedReceipt || beforeLastUnthreadedReceipt) {
return true;
}
}
return this.room.hasUserReadEvent(userId, eventId);
return super.hasUserReadEvent(userId, eventId);
}
setUnread(type, count) {
return this.room.setThreadUnreadNotificationCount(this.id, type, count);
@@ -813,21 +672,26 @@ export class Thread extends ReadReceipt {
* @returns true if this event is encrypted and is a message that is part of a
* thread - either inside it, or a root.
*/
_defineProperty(Thread, "hasServerSideSupport", FeatureSupport.None);
_defineProperty(Thread, "hasServerSideListSupport", FeatureSupport.None);
_defineProperty(Thread, "hasServerSideFwdPaginationSupport", FeatureSupport.None);
exports.Thread = Thread;
(0, _defineProperty2.default)(Thread, "hasServerSideSupport", FeatureSupport.None);
(0, _defineProperty2.default)(Thread, "hasServerSideListSupport", FeatureSupport.None);
(0, _defineProperty2.default)(Thread, "hasServerSideFwdPaginationSupport", FeatureSupport.None);
function isAnEncryptedThreadMessage(event) {
return event.isEncrypted() && (event.isRelation(THREAD_RELATION_TYPE.name) || event.isThreadRoot);
}
export const FILTER_RELATED_BY_SENDERS = new ServerControlledNamespacedValue("related_by_senders", "io.element.relation_senders");
export const FILTER_RELATED_BY_REL_TYPES = new ServerControlledNamespacedValue("related_by_rel_types", "io.element.relation_types");
export const THREAD_RELATION_TYPE = new ServerControlledNamespacedValue("m.thread", "io.element.thread");
export let ThreadFilterType = /*#__PURE__*/function (ThreadFilterType) {
const FILTER_RELATED_BY_SENDERS = new _NamespacedValue.ServerControlledNamespacedValue("related_by_senders", "io.element.relation_senders");
exports.FILTER_RELATED_BY_SENDERS = FILTER_RELATED_BY_SENDERS;
const FILTER_RELATED_BY_REL_TYPES = new _NamespacedValue.ServerControlledNamespacedValue("related_by_rel_types", "io.element.relation_types");
exports.FILTER_RELATED_BY_REL_TYPES = FILTER_RELATED_BY_REL_TYPES;
const THREAD_RELATION_TYPE = new _NamespacedValue.ServerControlledNamespacedValue("m.thread", "io.element.thread");
exports.THREAD_RELATION_TYPE = THREAD_RELATION_TYPE;
let ThreadFilterType = /*#__PURE__*/function (ThreadFilterType) {
ThreadFilterType[ThreadFilterType["My"] = 0] = "My";
ThreadFilterType[ThreadFilterType["All"] = 1] = "All";
return ThreadFilterType;
}({});
export function threadFilterTypeToFilter(type) {
exports.ThreadFilterType = ThreadFilterType;
function threadFilterTypeToFilter(type) {
switch (type) {
case ThreadFilterType.My:
return "participated";