diff options
author | RaindropsSys <contact@minteck.org> | 2023-04-24 14:03:36 +0200 |
---|---|---|
committer | RaindropsSys <contact@minteck.org> | 2023-04-24 14:03:36 +0200 |
commit | 633c92eae865e957121e08de634aeee11a8b3992 (patch) | |
tree | 09d881bee1dae0b6eee49db1dfaf0f500240606c /includes/external/matrix/node_modules/matrix-js-sdk/src/models/thread.ts | |
parent | c4657e4509733699c0f26a3c900bab47e915d5a0 (diff) | |
download | pluralconnect-633c92eae865e957121e08de634aeee11a8b3992.tar.gz pluralconnect-633c92eae865e957121e08de634aeee11a8b3992.tar.bz2 pluralconnect-633c92eae865e957121e08de634aeee11a8b3992.zip |
Updated 18 files, added 1692 files and deleted includes/system/compare.inc (automated)
Diffstat (limited to 'includes/external/matrix/node_modules/matrix-js-sdk/src/models/thread.ts')
-rw-r--r-- | includes/external/matrix/node_modules/matrix-js-sdk/src/models/thread.ts | 669 |
1 files changed, 669 insertions, 0 deletions
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/thread.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/thread.ts new file mode 100644 index 0000000..9a4ead3 --- /dev/null +++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/thread.ts @@ -0,0 +1,669 @@ +/* +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. +*/ + +import { Optional } from "matrix-events-sdk"; + +import { MatrixClient, PendingEventOrdering } from "../client"; +import { TypedReEmitter } from "../ReEmitter"; +import { RelationType } from "../@types/event"; +import { IThreadBundledRelationship, MatrixEvent, MatrixEventEvent } from "./event"; +import { Direction, EventTimeline } from "./event-timeline"; +import { EventTimelineSet, EventTimelineSetHandlerMap } from "./event-timeline-set"; +import { NotificationCountType, Room, RoomEvent } from "./room"; +import { RoomState } from "./room-state"; +import { ServerControlledNamespacedValue } from "../NamespacedValue"; +import { logger } from "../logger"; +import { ReadReceipt } from "./read-receipt"; +import { CachedReceiptStructure, ReceiptType } from "../@types/read_receipts"; + +export enum ThreadEvent { + New = "Thread.new", + Update = "Thread.update", + NewReply = "Thread.newReply", + ViewThread = "Thread.viewThread", + Delete = "Thread.delete", +} + +type EmittedEvents = Exclude<ThreadEvent, ThreadEvent.New> | RoomEvent.Timeline | RoomEvent.TimelineReset; + +export type EventHandlerMap = { + [ThreadEvent.Update]: (thread: Thread) => void; + [ThreadEvent.NewReply]: (thread: Thread, event: MatrixEvent) => void; + [ThreadEvent.ViewThread]: () => void; + [ThreadEvent.Delete]: (thread: Thread) => void; +} & EventTimelineSetHandlerMap; + +interface IThreadOpts { + room: Room; + client: MatrixClient; + pendingEventOrdering?: PendingEventOrdering; + receipts?: CachedReceiptStructure[]; +} + +export enum FeatureSupport { + None = 0, + Experimental = 1, + Stable = 2, +} + +export function determineFeatureSupport(stable: boolean, unstable: boolean): FeatureSupport { + if (stable) { + return FeatureSupport.Stable; + } else if (unstable) { + return FeatureSupport.Experimental; + } else { + return FeatureSupport.None; + } +} + +export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> { + public static hasServerSideSupport = FeatureSupport.None; + public static hasServerSideListSupport = FeatureSupport.None; + public static hasServerSideFwdPaginationSupport = FeatureSupport.None; + + /** + * A reference to all the events ID at the bottom of the threads + */ + public readonly timelineSet: EventTimelineSet; + public timeline: MatrixEvent[] = []; + + private _currentUserParticipated = false; + + private reEmitter: TypedReEmitter<EmittedEvents, EventHandlerMap>; + + private lastEvent: MatrixEvent | undefined; + private replyCount = 0; + private lastPendingEvent: MatrixEvent | undefined; + private pendingReplyCount = 0; + + public readonly room: Room; + public readonly client: MatrixClient; + private readonly pendingEventOrdering: PendingEventOrdering; + + public initialEventsFetched = !Thread.hasServerSideSupport; + /** + * An array of events to add to the timeline once the thread has been initialised + * with server suppport. + */ + public replayEvents: MatrixEvent[] | null = []; + + public constructor(public readonly id: string, public rootEvent: MatrixEvent | undefined, opts: IThreadOpts) { + super(); + + if (!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, + { + timelineSupport: true, + pendingEvents: true, + }, + this.client, + this, + ); + this.reEmitter = new TypedReEmitter(this); + + this.reEmitter.reEmit(this.timelineSet, [RoomEvent.Timeline, RoomEvent.TimelineReset]); + + this.room.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction); + this.room.on(RoomEvent.Redaction, this.onRedaction); + this.room.on(RoomEvent.LocalEchoUpdated, this.onLocalEcho); + this.timelineSet.on(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 + // gappy sync and a thread around this event may already exist. + this.updateThreadMetadata(); + this.setEventMetadata(this.rootEvent); + } + + private async fetchRootEvent(): Promise<void> { + 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); + } + await this.processEvent(this.rootEvent); + } + + public static setServerSideSupport(status: FeatureSupport): void { + Thread.hasServerSideSupport = status; + if (status !== FeatureSupport.Stable) { + FILTER_RELATED_BY_SENDERS.setPreferUnstable(true); + FILTER_RELATED_BY_REL_TYPES.setPreferUnstable(true); + THREAD_RELATION_TYPE.setPreferUnstable(true); + } + } + + public static setServerSideListSupport(status: FeatureSupport): void { + Thread.hasServerSideListSupport = status; + } + + public static setServerSideFwdPaginationSupport(status: FeatureSupport): void { + Thread.hasServerSideFwdPaginationSupport = status; + } + + private onBeforeRedaction = (event: MatrixEvent, redaction: MatrixEvent): void => { + if ( + 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.updatePendingReplyCount(); + this.emit(ThreadEvent.Update, this); + } + }; + + private onRedaction = async (event: MatrixEvent): Promise<void> => { + if (event.threadRootId !== this.id) return; // ignore redactions for other timelines + if (this.replyCount <= 0) { + for (const threadEvent of this.timeline) { + this.clearEventMetadata(threadEvent); + } + this.lastEvent = this.rootEvent; + this._currentUserParticipated = false; + this.emit(ThreadEvent.Delete, this); + } else { + await this.updateThreadMetadata(); + } + }; + + private onTimelineEvent = ( + event: MatrixEvent, + room: Room | undefined, + toStartOfTimeline: boolean | undefined, + ): void => { + // Add a synthesized receipt when paginating forward in the timeline + if (!toStartOfTimeline) { + room!.addLocalEchoReceipt(event.getSender()!, event, ReceiptType.Read); + } + this.onEcho(event, toStartOfTimeline ?? false); + }; + + private onLocalEcho = (event: MatrixEvent): void => { + this.onEcho(event, false); + }; + + private onEcho = async (event: MatrixEvent, toStartOfTimeline: boolean): Promise<void> => { + if (event.threadRootId !== this.id) return; // ignore echoes for other timelines + if (this.lastEvent === event) return; // ignore duplicate events + await this.updateThreadMetadata(); + if (!event.isRelation(THREAD_RELATION_TYPE.name)) return; // don't send a new reply event for reactions or edits + if (toStartOfTimeline) return; // ignore messages added to the start of the timeline + this.emit(ThreadEvent.NewReply, this, event); + }; + + public get roomState(): RoomState { + return this.room.getLiveTimeline().getState(EventTimeline.FORWARDS)!; + } + + private addEventToTimeline(event: MatrixEvent, toStartOfTimeline: boolean): void { + if (!this.findEventById(event.getId()!)) { + this.timelineSet.addEventToTimeline(event, this.liveTimeline, { + toStartOfTimeline, + fromCache: false, + roomState: this.roomState, + }); + this.timeline = this.events; + } + } + + public addEvents(events: MatrixEvent[], toStartOfTimeline: boolean): void { + events.forEach((ev) => this.addEvent(ev, toStartOfTimeline, false)); + this.updateThreadMetadata(); + } + + /** + * 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 toStartOfTimeline - whether the event is being added + * 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. + */ + public async addEvent(event: MatrixEvent, toStartOfTimeline: boolean, emit = true): Promise<void> { + this.setEventMetadata(event); + + 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) { + // 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 && isNewestReply) { + this.addEventToTimeline(event, false); + this.fetchEditsWhereNeeded(event); + } else if (event.isRelation(RelationType.Annotation) || event.isRelation(RelationType.Replace)) { + if (!this.initialEventsFetched) { + /** + * 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); + } else { + this.addEventToTimeline(event, toStartOfTimeline); + } + // 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.NewReply, this, event); + this.updateThreadMetadata(); + } + } + + public async processEvent(event: Optional<MatrixEvent>): Promise<void> { + if (event) { + this.setEventMetadata(event); + await this.fetchEditsWhereNeeded(event); + } + this.timeline = this.events; + } + + /** + * Processes the receipts that were caught during initial sync + * When clients become aware of a thread, they try to retrieve those read receipts + * and apply them to the current thread + * @param receipts - A collection of the receipts cached from initial sync + */ + private processReceipts(receipts: CachedReceiptStructure[] = []): void { + for (const { eventId, receiptType, userId, receipt, synthetic } of receipts) { + this.addReceiptToStructure(eventId, receiptType as ReceiptType, userId, receipt, synthetic); + } + } + + private getRootEventBundledRelationship(rootEvent = this.rootEvent): IThreadBundledRelationship | undefined { + return rootEvent?.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name); + } + + private async processRootEvent(): Promise<void> { + const bundledRelationship = this.getRootEventBundledRelationship(); + if (Thread.hasServerSideSupport && bundledRelationship) { + this.replyCount = bundledRelationship.count; + this._currentUserParticipated = !!bundledRelationship.current_user_participated; + + const mapper = this.client.getEventMapper(); + // re-insert roomId + this.lastEvent = mapper({ + ...bundledRelationship.latest_event, + room_id: this.roomId, + }); + this.updatePendingReplyCount(); + await this.processEvent(this.lastEvent); + } + } + + private updatePendingReplyCount(): void { + 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(), + ); + this.lastPendingEvent = pendingEvents.length ? pendingEvents[pendingEvents.length - 1] : undefined; + this.pendingReplyCount = pendingEvents.length; + } + + /** + * Reset the live timeline of all timelineSets, and start new ones. + * + * <p>This is used when /sync returns a 'limited' timeline. 'Limited' means that there's a gap between the messages + * /sync returned, and the last known message in our timeline. In such a case, our live timeline isn't live anymore + * and has to be replaced by a new one. To make sure we can continue paginating our timelines correctly, we have to + * set new pagination tokens on the old and the new timeline. + * + * @param backPaginationToken - token for back-paginating the new timeline + * @param forwardPaginationToken - token for forward-paginating the old live timeline, + * if absent or null, all timelines are reset, removing old ones (including the previous live + * timeline which would otherwise be unable to paginate forwards without this token). + * Removing just the old live timeline whilst preserving previous ones is not supported. + */ + public async resetLiveTimeline( + backPaginationToken?: string | null, + forwardPaginationToken?: string | null, + ): Promise<void> { + const oldLive = this.liveTimeline; + this.timelineSet.resetLiveTimeline(backPaginationToken ?? undefined, forwardPaginationToken ?? undefined); + const newLive = this.liveTimeline; + + // FIXME: Remove the following as soon as https://github.com/matrix-org/synapse/issues/14830 is resolved. + // + // The pagination API for thread timelines currently can't handle the type of pagination tokens returned by sync + // + // To make this work anyway, we'll have to transform them into one of the types that the API can handle. + // One option is passing the tokens to /messages, which can handle sync tokens, and returns the right format. + // /messages does not return new tokens on requests with a limit of 0. + // This means our timelines might overlap a slight bit, but that's not an issue, as we deduplicate messages + // anyway. + + let newBackward: string | undefined; + let oldForward: string | undefined; + if (backPaginationToken) { + const res = await this.client.createMessagesRequest(this.roomId, backPaginationToken, 1, Direction.Forward); + newBackward = res.end; + } + if (forwardPaginationToken) { + const res = await this.client.createMessagesRequest( + this.roomId, + forwardPaginationToken, + 1, + 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 (backPaginationToken && newLive.getPaginationToken(Direction.Backward) === backPaginationToken) { + newLive.setPaginationToken(newBackward ?? null, Direction.Backward); + } + } + + private async updateThreadMetadata(): Promise<void> { + this.updatePendingReplyCount(); + + if (Thread.hasServerSideSupport) { + // Ensure we show *something* as soon as possible, we'll update it as soon as we get better data, but we + // don't want the thread preview to be empty if we can avoid it + if (!this.initialEventsFetched) { + await this.processRootEvent(); + } + await this.fetchRootEvent(); + } + await this.processRootEvent(); + + if (!this.initialEventsFetched) { + 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, 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(RoomEvent.TimelineReset, this.room, this.timelineSet, true); + } catch (e) { + logger.error("Failed to load start of newly created thread: ", e); + this.initialEventsFetched = false; + } + } + + this.emit(ThreadEvent.Update, this); + } + + // XXX: Workaround for https://github.com/matrix-org/matrix-spec-proposals/pull/2676/files#r827240084 + private async fetchEditsWhereNeeded(...events: MatrixEvent[]): Promise<unknown> { + return Promise.all( + events + .filter((e) => e.isEncrypted()) + .map((event: MatrixEvent) => { + if (event.isRelation()) return; // skip - relations don't get edits + return this.client + .relations(this.roomId, event.getId()!, RelationType.Replace, event.getType(), { + limit: 1, + }) + .then((relations) => { + if (relations.events.length) { + event.makeReplaced(relations.events[0]); + } + }) + .catch((e) => { + logger.error("Failed to load edits for encrypted thread event", e); + }); + }), + ); + } + + public setEventMetadata(event: Optional<MatrixEvent>): void { + if (event) { + EventTimeline.setEventMetadata(event, this.roomState, false); + event.setThread(this); + } + } + + public clearEventMetadata(event: Optional<MatrixEvent>): void { + if (event) { + event.setThread(undefined); + delete event.event?.unsigned?.["m.relations"]?.[THREAD_RELATION_TYPE.name]; + } + } + + /** + * Finds an event by ID in the current thread + */ + public findEventById(eventId: string): MatrixEvent | undefined { + return this.timelineSet.findEventById(eventId); + } + + /** + * Return last reply to the thread, if known. + */ + public lastReply(matches: (ev: MatrixEvent) => boolean = (): boolean => true): MatrixEvent | null { + for (let i = this.timeline.length - 1; i >= 0; i--) { + const event = this.timeline[i]; + if (matches(event)) { + return event; + } + } + return null; + } + + public get roomId(): string { + 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 + */ + public get length(): number { + return this.replyCount + this.pendingReplyCount; + } + + /** + * A getter for the last event of the thread. + * This might be a synthesized event, if so, it will not emit any events to listeners. + */ + public get replyToEvent(): Optional<MatrixEvent> { + return this.lastPendingEvent ?? this.lastEvent ?? this.lastReply(); + } + + public get events(): MatrixEvent[] { + return this.liveTimeline.getEvents(); + } + + public has(eventId: string): boolean { + return this.timelineSet.findEventById(eventId) instanceof MatrixEvent; + } + + public get hasCurrentUserParticipated(): boolean { + return this._currentUserParticipated; + } + + public get liveTimeline(): EventTimeline { + return this.timelineSet.getLiveTimeline(); + } + + public getUnfilteredTimelineSet(): EventTimelineSet { + return this.timelineSet; + } + + public addReceipt(event: MatrixEvent, synthetic: boolean): void { + throw new Error("Unsupported function on the thread model"); + } + + /** + * Get the ID of the event that a given user has read up to within this thread, + * or null if we have received no read receipt (at all) from them. + * @param userId - The user ID to get read receipt event ID for + * @param ignoreSynthesized - If true, return only receipts that have been + * sent by the server, not implicit ones generated + * by the JS SDK. + * @returns ID of the latest event that the given user has read, or null. + */ + public getEventReadUpTo(userId: string, ignoreSynthesized?: boolean): string | null { + const isCurrentUser = userId === this.client.getUserId(); + const lastReply = this.timeline[this.timeline.length - 1]; + if (isCurrentUser && lastReply) { + // If the last activity in a thread is prior to the first threaded read receipt + // sent in the room (suggesting that it was sent before the user started + // using a client that supported threaded read receipts), we want to + // consider this thread as read. + const beforeFirstThreadedReceipt = lastReply.getTs() < this.room.getOldestThreadedReceiptTs(); + const lastReplyId = lastReply.getId(); + // Some unsent events do not have an ID, we do not want to consider them read + if (beforeFirstThreadedReceipt && lastReplyId) { + return lastReplyId; + } + } + + const readUpToId = super.getEventReadUpTo(userId, ignoreSynthesized); + + // Check whether the unthreaded read receipt for that user is more recent + // than the read receipt inside that thread. + if (lastReply) { + const unthreadedReceipt = this.room.getLastUnthreadedReceiptFor(userId); + if (!unthreadedReceipt) { + return readUpToId; + } + + for (let i = this.timeline?.length - 1; i >= 0; --i) { + 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 + if (ev.getId() === readUpToId) return readUpToId; + + // Inspecting events from most recent to oldest, we're checking + // 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; + } + } + + return readUpToId; + } + + /** + * Determine if the given user has read a particular event. + * + * It is invalid to call this method with an event that is not part of this thread. + * + * This is not a definitive check as it only checks the events that have been + * loaded client-side at the time of execution. + * @param userId - The user ID to check the read state of. + * @param eventId - The event ID to check if the user read. + * @returns True if the user has read the event, false otherwise. + */ + public hasUserReadEvent(userId: string, eventId: string): boolean { + if (userId === this.client.getUserId()) { + // 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; + if (beforeFirstThreadedReceipt || beforeLastUnthreadedReceipt) { + return true; + } + } + + return super.hasUserReadEvent(userId, eventId); + } + + public setUnread(type: NotificationCountType, count: number): void { + return this.room.setThreadUnreadNotificationCount(this.id, type, count); + } +} + +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 enum ThreadFilterType { + "My", + "All", +} + +export function threadFilterTypeToFilter(type: ThreadFilterType | null): "all" | "participated" { + switch (type) { + case ThreadFilterType.My: + return "participated"; + default: + return "all"; + } +} |