UNPKG

matrix-js-sdk

Version:
409 lines (324 loc) 16.1 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.ThreadFilterType = exports.ThreadEvent = exports.Thread = exports.THREAD_RELATION_TYPE = exports.FILTER_RELATED_BY_SENDERS = exports.FILTER_RELATED_BY_REL_TYPES = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _matrix = require("../matrix"); var _ReEmitter = require("../ReEmitter"); var _event = require("./event"); var _eventTimeline = require("./event-timeline"); var _eventTimelineSet = require("./event-timeline-set"); var _typedEventEmitter = require("./typed-event-emitter"); var _NamespacedValue = require("../NamespacedValue"); var _logger = require("../logger"); 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; } let ThreadEvent; exports.ThreadEvent = ThreadEvent; (function (ThreadEvent) { ThreadEvent["New"] = "Thread.new"; ThreadEvent["Update"] = "Thread.update"; ThreadEvent["NewReply"] = "Thread.newReply"; ThreadEvent["ViewThread"] = "Thread.viewThread"; })(ThreadEvent || (exports.ThreadEvent = ThreadEvent = {})); /** * @experimental */ class Thread extends _typedEventEmitter.TypedEventEmitter { /** * A reference to all the events ID at the bottom of the threads */ constructor(id, rootEvent, opts) { var _this$rootEvent; super(); this.id = id; this.rootEvent = rootEvent; (0, _defineProperty2.default)(this, "timelineSet", void 0); (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, "room", void 0); (0, _defineProperty2.default)(this, "client", void 0); (0, _defineProperty2.default)(this, "initialEventsFetched", !Thread.hasServerSideSupport); (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 ) { this.replyCount--; this.emit(ThreadEvent.Update, this); } }); (0, _defineProperty2.default)(this, "onRedaction", event => { var _events$find; if (event.threadRootId !== this.id) return; // ignore redactions for other timelines const events = [...this.timelineSet.getLiveTimeline().getEvents()].reverse(); this.lastEvent = (_events$find = events.find(e => !e.isRedacted() && e.isRelation(THREAD_RELATION_TYPE.name))) !== null && _events$find !== void 0 ? _events$find : this.rootEvent; this.emit(ThreadEvent.Update, this); }); (0, _defineProperty2.default)(this, "onEcho", event => { if (event.threadRootId !== this.id) return; // ignore echoes for other timelines if (this.lastEvent === event) return; if (!event.isRelation(THREAD_RELATION_TYPE.name)) return; // There is a risk that the `localTimestamp` approximation will not be accurate // when threads are used over federation. That could result in the reply // count value drifting away from the value returned by the server const isThreadReply = event.isRelation(THREAD_RELATION_TYPE.name); if (!this.lastEvent || this.lastEvent.isRedacted() || isThreadReply && event.getId() !== this.lastEvent.getId() && event.localTimestamp > this.lastEvent.localTimestamp) { this.lastEvent = event; if (this.lastEvent.getId() !== this.id) { // This counting only works when server side support is enabled as we started the counting // from the value returned within the bundled relationship if (Thread.hasServerSideSupport) { this.replyCount++; } this.emit(ThreadEvent.NewReply, this, event); } } this.emit(ThreadEvent.Update, this); }); 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.timelineSet = new _eventTimelineSet.EventTimelineSet(this.room, { timelineSupport: true, pendingEvents: true }, this.client, this); this.reEmitter = new _ReEmitter.TypedReEmitter(this); this.reEmitter.reEmit(this.timelineSet, [_matrix.RoomEvent.Timeline, _matrix.RoomEvent.TimelineReset]); this.room.on(_matrix.MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction); this.room.on(_matrix.RoomEvent.Redaction, this.onRedaction); this.room.on(_matrix.RoomEvent.LocalEchoUpdated, this.onEcho); this.timelineSet.on(_matrix.RoomEvent.Timeline, this.onEcho); if (opts.initialEvents) { this.addEvents(opts.initialEvents, false); } // even if this thread is thought to be originating from this client, we initialise it as we may be in a // gappy sync and a thread around this event may already exist. this.initialiseThread(); (_this$rootEvent = this.rootEvent) === null || _this$rootEvent === void 0 ? void 0 : _this$rootEvent.setThread(this); } async fetchRootEvent() { var _this$rootEvent2; 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.logger.error("Failed to fetch thread root to construct thread with", e); } // The root event might be not be visible to the person requesting it. // If it wasn't fetched successfully the thread will work in "limited" mode and won't // benefit from all the APIs a homeserver can provide to enhance the thread experience (_this$rootEvent2 = this.rootEvent) === null || _this$rootEvent2 === void 0 ? void 0 : _this$rootEvent2.setThread(this); this.emit(ThreadEvent.Update, this); } static setServerSideSupport(hasServerSideSupport, useStable) { Thread.hasServerSideSupport = hasServerSideSupport; if (!useStable) { FILTER_RELATED_BY_SENDERS.setPreferUnstable(true); FILTER_RELATED_BY_REL_TYPES.setPreferUnstable(true); THREAD_RELATION_TYPE.setPreferUnstable(true); } } get roomState() { 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 }); } } addEvents(events, toStartOfTimeline) { events.forEach(ev => this.addEvent(ev, toStartOfTimeline, false)); this.emit(ThreadEvent.Update, this); } /** * Add an event to the thread and updates * the tail/root references if needed * Will fire "Thread.update" * @param event The event to add * @param {boolean} toStartOfTimeline whether the event is being added * to the start (and not the end) of the timeline. * @param {boolean} emit whether to emit the Update event if the thread was updated or not. */ addEvent(event, toStartOfTimeline, emit = true) { var _this$lastReply; event.setThread(this); if (!this._currentUserParticipated && event.getSender() === this.client.getUserId()) { this._currentUserParticipated = true; } // Add all incoming events to the thread's timeline set when there's no server support if (!Thread.hasServerSideSupport) { // 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 (!toStartOfTimeline && this.initialEventsFetched && event.localTimestamp > ((_this$lastReply = this.lastReply()) === null || _this$lastReply === void 0 ? void 0 : _this$lastReply.localTimestamp)) { this.fetchEditsWhereNeeded(event); this.addEventToTimeline(event, false); } else if (event.isRelation(_matrix.RelationType.Annotation) || event.isRelation(_matrix.RelationType.Replace)) { // Apply annotations and replace relations to the relations of the timeline only this.timelineSet.relations.aggregateParentEvent(event); this.timelineSet.relations.aggregateChildEvent(event, this.timelineSet); return; } // If no thread support exists we want to count all thread relation // added as a reply. We can't rely on the bundled relationships count if ((!Thread.hasServerSideSupport || !this.rootEvent) && event.isRelation(THREAD_RELATION_TYPE.name)) { this.replyCount++; } if (emit) { this.emit(ThreadEvent.Update, this); } } getRootEventBundledRelationship(rootEvent = this.rootEvent) { return rootEvent === null || rootEvent === void 0 ? void 0 : rootEvent.getServerAggregatedRelation(THREAD_RELATION_TYPE.name); } async initialiseThread() { let bundledRelationship = this.getRootEventBundledRelationship(); if (Thread.hasServerSideSupport && !bundledRelationship) { await this.fetchRootEvent(); bundledRelationship = this.getRootEventBundledRelationship(); } if (Thread.hasServerSideSupport && bundledRelationship) { this.replyCount = bundledRelationship.count; this._currentUserParticipated = bundledRelationship.current_user_participated; const event = new _event.MatrixEvent(_objectSpread({ room_id: this.rootEvent.getRoomId() }, bundledRelationship.latest_event)); this.setEventMetadata(event); event.setThread(this); this.lastEvent = event; this.fetchEditsWhereNeeded(event); } this.emit(ThreadEvent.Update, this); } // XXX: Workaround for https://github.com/matrix-org/matrix-spec-proposals/pull/2676/files#r827240084 async fetchEditsWhereNeeded(...events) { return Promise.all(events.filter(e => e.isEncrypted()).map(event => { if (event.isRelation()) return; // skip - relations don't get edits return this.client.relations(this.roomId, event.getId(), _matrix.RelationType.Replace, event.getType(), { limit: 1 }).then(relations => { if (relations.events.length) { event.makeReplaced(relations.events[0]); } }).catch(e => { _logger.logger.error("Failed to load edits for encrypted thread event", e); }); })); } async fetchInitialEvents() { if (this.initialEventsFetched) return; await this.fetchEvents(); this.initialEventsFetched = true; } setEventMetadata(event) { _eventTimeline.EventTimeline.setEventMetadata(event, this.roomState, false); event.setThread(this); } /** * Finds an event by ID in the current thread */ findEventById(eventId) { var _this$lastEvent; // Check the lastEvent as it may have been created based on a bundled relationship and not in a timeline if (((_this$lastEvent = this.lastEvent) === null || _this$lastEvent === void 0 ? void 0 : _this$lastEvent.getId()) === eventId) { return this.lastEvent; } return this.timelineSet.findEventById(eventId); } /** * Return last reply to the thread, if known. */ lastReply(matches = () => true) { for (let i = this.events.length - 1; i >= 0; i--) { const event = this.events[i]; if (matches(event)) { return event; } } return null; } get roomId() { return this.room.roomId; } /** * The number of messages in the thread * Only count rel_type=m.thread as we want to * exclude annotations from that number */ get length() { return this.replyCount; } /** * A getter for the last event added to the thread, if known. */ get replyToEvent() { var _this$lastEvent2; return (_this$lastEvent2 = this.lastEvent) !== null && _this$lastEvent2 !== void 0 ? _this$lastEvent2 : this.lastReply(); } get events() { return this.liveTimeline.getEvents(); } has(eventId) { return this.timelineSet.findEventById(eventId) instanceof _event.MatrixEvent; } get hasCurrentUserParticipated() { return this._currentUserParticipated; } get liveTimeline() { return this.timelineSet.getLiveTimeline(); } async fetchEvents(opts = { limit: 20, direction: _eventTimeline.Direction.Backward }) { var _opts$direction; let { originalEvent, events, prevBatch, nextBatch } = await this.client.relations(this.room.roomId, this.id, THREAD_RELATION_TYPE.name, null, opts); // When there's no nextBatch returned with a `from` request we have reached // the end of the thread, and therefore want to return an empty one if (!opts.to && !nextBatch) { events = [...events, originalEvent]; } await this.fetchEditsWhereNeeded(...events); await Promise.all(events.map(event => { this.setEventMetadata(event); return this.client.decryptEventIfNeeded(event); })); const prependEvents = ((_opts$direction = opts.direction) !== null && _opts$direction !== void 0 ? _opts$direction : _eventTimeline.Direction.Backward) === _eventTimeline.Direction.Backward; this.timelineSet.addEventsToTimeline(events, prependEvents, this.liveTimeline, prependEvents ? nextBatch : prevBatch); return { originalEvent, events, prevBatch, nextBatch }; } } exports.Thread = Thread; (0, _defineProperty2.default)(Thread, "hasServerSideSupport", void 0); 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; exports.ThreadFilterType = ThreadFilterType; (function (ThreadFilterType) { ThreadFilterType[ThreadFilterType["My"] = 0] = "My"; ThreadFilterType[ThreadFilterType["All"] = 1] = "All"; })(ThreadFilterType || (exports.ThreadFilterType = ThreadFilterType = {}));