You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-25 05:23:13 +03:00
Make threads use 'm.thread' relation
This commit is contained in:
@@ -92,6 +92,12 @@ export enum EventType {
|
|||||||
export enum RelationType {
|
export enum RelationType {
|
||||||
Annotation = "m.annotation",
|
Annotation = "m.annotation",
|
||||||
Replace = "m.replace",
|
Replace = "m.replace",
|
||||||
|
/**
|
||||||
|
* Note, "io.element.thread" is hardcoded
|
||||||
|
* TypeScript does not allow computed values in enums
|
||||||
|
* https://github.com/microsoft/TypeScript/issues/27976
|
||||||
|
*/
|
||||||
|
Thread = "io.element.thread",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum MsgType {
|
export enum MsgType {
|
||||||
@@ -168,9 +174,14 @@ export const UNSTABLE_ELEMENT_FUNCTIONAL_USERS = new UnstableValue(
|
|||||||
"io.element.functional_members",
|
"io.element.functional_members",
|
||||||
"io.element.functional_members");
|
"io.element.functional_members");
|
||||||
|
|
||||||
export const UNSTABLE_ELEMENT_REPLY_IN_THREAD = new UnstableValue(
|
/**
|
||||||
"m.in_thread",
|
* Note, "io.element.thread" is hardcoded in the RelationType enum
|
||||||
"io.element.in_thread",
|
* TypeScript does not allow computed values in enums
|
||||||
|
* https://github.com/microsoft/TypeScript/issues/27976
|
||||||
|
*/
|
||||||
|
export const UNSTABLE_ELEMENT_THREAD_RELATION = new UnstableValue(
|
||||||
|
"m.thread",
|
||||||
|
"io.element.thread",
|
||||||
);
|
);
|
||||||
|
|
||||||
export interface IEncryptedFile {
|
export interface IEncryptedFile {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import {
|
|||||||
EventType,
|
EventType,
|
||||||
MsgType,
|
MsgType,
|
||||||
RelationType,
|
RelationType,
|
||||||
UNSTABLE_ELEMENT_REPLY_IN_THREAD,
|
UNSTABLE_ELEMENT_THREAD_RELATION,
|
||||||
} from "../@types/event";
|
} from "../@types/event";
|
||||||
import { Crypto } from "../crypto";
|
import { Crypto } from "../crypto";
|
||||||
import { deepSortedObjectEntries } from "../utils";
|
import { deepSortedObjectEntries } from "../utils";
|
||||||
@@ -119,7 +119,7 @@ interface IAggregatedRelation {
|
|||||||
key?: string;
|
key?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IEventRelation {
|
export interface IEventRelation {
|
||||||
rel_type: RelationType | string;
|
rel_type: RelationType | string;
|
||||||
event_id: string;
|
event_id: string;
|
||||||
key?: string;
|
key?: string;
|
||||||
@@ -419,38 +419,39 @@ export class MatrixEvent extends EventEmitter {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @experimental
|
* @experimental
|
||||||
* Get the event ID of the replied event
|
* Get the event ID of the thread head
|
||||||
*/
|
*/
|
||||||
public get replyEventId(): string {
|
public get threadRootId(): string {
|
||||||
const relations = this.getWireContent()["m.relates_to"];
|
const relatesTo = this.getWireContent()?.["m.relates_to"];
|
||||||
return relations?.["m.in_reply_to"]?.["event_id"];
|
if (relatesTo?.rel_type === UNSTABLE_ELEMENT_THREAD_RELATION.name) {
|
||||||
|
return relatesTo.event_id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @experimental
|
* @experimental
|
||||||
* Determines whether a reply should be rendered in a thread
|
|
||||||
* or in the main room timeline
|
|
||||||
*/
|
*/
|
||||||
public get replyInThread(): boolean {
|
public get isThreadRelation(): boolean {
|
||||||
/**
|
return !!this.threadRootId;
|
||||||
* UNSTABLE_ELEMENT_REPLY_IN_THREAD can live either
|
}
|
||||||
* at the m.relates_to and m.in_reply_to level
|
|
||||||
* This will likely change once we settle on a
|
|
||||||
* way to achieve threads
|
|
||||||
* TODO: Clean this up once we have a clear way forward
|
|
||||||
*/
|
|
||||||
|
|
||||||
const relatesTo = this.getWireContent()?.["m.relates_to"];
|
/**
|
||||||
const replyTo = relatesTo?.["m.in_reply_to"];
|
* @experimental
|
||||||
|
*/
|
||||||
return relatesTo?.[UNSTABLE_ELEMENT_REPLY_IN_THREAD.name]
|
public get isThreadRoot(): boolean {
|
||||||
|| (this.replyEventId && replyTo[UNSTABLE_ELEMENT_REPLY_IN_THREAD.name])
|
const thread = this.getThread();
|
||||||
|| this.thread instanceof Thread;
|
return thread.id === this.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
public get parentEventId(): string {
|
public get parentEventId(): string {
|
||||||
return this.replyEventId
|
const relations = this.getWireContent()["m.relates_to"];
|
||||||
|| this.getWireContent()["m.relates_to"]?.event_id;
|
return relations?.["m.in_reply_to"]?.["event_id"]
|
||||||
|
|| relations?.event_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get replyEventId(): string {
|
||||||
|
const relations = this.getWireContent()["m.relates_to"];
|
||||||
|
return relations?.["m.in_reply_to"]?.["event_id"];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ export class Room extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* @experimental
|
* @experimental
|
||||||
*/
|
*/
|
||||||
public threads = new Set<Thread>();
|
public threads = new Map<string, Thread>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a new Room.
|
* Construct a new Room.
|
||||||
@@ -1068,19 +1068,6 @@ export class Room extends EventEmitter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @experimental
|
|
||||||
*/
|
|
||||||
public addThread(thread: Thread): Set<Thread> {
|
|
||||||
this.threads.add(thread);
|
|
||||||
if (!thread.ready) {
|
|
||||||
thread.once(ThreadEvent.Ready, this.dedupeThreads);
|
|
||||||
this.emit(ThreadEvent.Update, thread);
|
|
||||||
this.reEmitter.reEmit(thread, [ThreadEvent.Update, ThreadEvent.Ready]);
|
|
||||||
}
|
|
||||||
return this.threads;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @experimental
|
* @experimental
|
||||||
*/
|
*/
|
||||||
@@ -1097,26 +1084,6 @@ export class Room extends EventEmitter {
|
|||||||
return Array.from(this.threads.values());
|
return Array.from(this.threads.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Two threads starting from a different child event can end up
|
|
||||||
* with the same event root. This method ensures that the duplicates
|
|
||||||
* are removed
|
|
||||||
* @experimental
|
|
||||||
*/
|
|
||||||
private dedupeThreads = (readyThread): void => {
|
|
||||||
const deduped = Array.from(this.threads).reduce((dedupedThreads, thread) => {
|
|
||||||
if (dedupedThreads.has(thread.id)) {
|
|
||||||
dedupedThreads.get(thread.id).merge(thread);
|
|
||||||
} else {
|
|
||||||
dedupedThreads.set(thread.id, thread);
|
|
||||||
}
|
|
||||||
|
|
||||||
return dedupedThreads;
|
|
||||||
}, new Map<string, Thread>());
|
|
||||||
|
|
||||||
this.threads = new Set<Thread>(deduped.values());
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a member from the current room state.
|
* Get a member from the current room state.
|
||||||
* @param {string} userId The user ID of the member.
|
* @param {string} userId The user ID of the member.
|
||||||
@@ -1293,21 +1260,33 @@ export class Room extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public findThreadForEvent(event: MatrixEvent): Thread {
|
||||||
|
if (!event) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (event.isThreadRelation) {
|
||||||
|
return this.threads.get(event.threadRootId);
|
||||||
|
} else {
|
||||||
|
const parentEvent = this.findEventById(event.parentEventId);
|
||||||
|
return this.findThreadForEvent(parentEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add an event to a thread's timeline. Will fire "Thread.update"
|
* Add an event to a thread's timeline. Will fire "Thread.update"
|
||||||
* @experimental
|
* @experimental
|
||||||
*/
|
*/
|
||||||
public addThreadedEvent(event: MatrixEvent): void {
|
public addThreadedEvent(event: MatrixEvent): void {
|
||||||
let thread = this.findEventById(event.parentEventId)?.getThread();
|
let thread = this.findThreadForEvent(event);
|
||||||
if (thread) {
|
if (thread) {
|
||||||
thread.addEvent(event);
|
thread.addEvent(event);
|
||||||
} else {
|
} else {
|
||||||
thread = new Thread([event], this, this.client);
|
const rootEvent = this.findEventById(event.threadRootId);
|
||||||
}
|
thread = new Thread([rootEvent, event], this, this.client);
|
||||||
|
this.reEmitter.reEmit(thread, [ThreadEvent.Update, ThreadEvent.Ready]);
|
||||||
if (!this.threads.has(thread)) {
|
this.threads.set(thread.id, thread);
|
||||||
this.addThread(thread);
|
|
||||||
}
|
}
|
||||||
|
this.emit(ThreadEvent.Update, thread);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1409,7 +1388,7 @@ export class Room extends EventEmitter {
|
|||||||
// TODO: Enable "pending events" for threads
|
// TODO: Enable "pending events" for threads
|
||||||
// There's a fair few things to update to make them work with Threads
|
// There's a fair few things to update to make them work with Threads
|
||||||
// Will get back to it when the plan is to build a more polished UI ready for production
|
// Will get back to it when the plan is to build a more polished UI ready for production
|
||||||
if (this.client?.supportsExperimentalThreads() && event.replyInThread) {
|
if (this.client?.supportsExperimentalThreads() && event.threadRootId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1585,14 +1564,6 @@ export class Room extends EventEmitter {
|
|||||||
oldEventId, oldStatus);
|
oldEventId, oldStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
public findThreadByEventId(eventId: string): Thread {
|
|
||||||
for (const thread of this.threads) {
|
|
||||||
if (thread.has(eventId)) {
|
|
||||||
return thread;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the status / event id on a pending event, to reflect its transmission
|
* Update the status / event id on a pending event, to reflect its transmission
|
||||||
* progress.
|
* progress.
|
||||||
|
|||||||
@@ -26,11 +26,6 @@ export enum ThreadEvent {
|
|||||||
Update = "Thread.update"
|
Update = "Thread.update"
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ISerialisedThread {
|
|
||||||
id: string;
|
|
||||||
tails: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @experimental
|
* @experimental
|
||||||
*/
|
*/
|
||||||
@@ -42,7 +37,6 @@ export class Thread extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* A reference to all the events ID at the bottom of the threads
|
* A reference to all the events ID at the bottom of the threads
|
||||||
*/
|
*/
|
||||||
public readonly tail = new Set<string>();
|
|
||||||
public readonly timelineSet: EventTimelineSet;
|
public readonly timelineSet: EventTimelineSet;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -69,13 +63,12 @@ export class Thread extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.tail.has(event.replyEventId)) {
|
if (!this.root) {
|
||||||
this.tail.delete(event.replyEventId);
|
if (event.isThreadRelation) {
|
||||||
}
|
this.root = event.threadRootId;
|
||||||
this.tail.add(event.getId());
|
} else {
|
||||||
|
this.root = event.getId();
|
||||||
if (!event.replyEventId || !this.timelineSet.findEventById(event.replyEventId)) {
|
}
|
||||||
this.root = event.getId();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// all the relevant membership info to hydrate events with a sender
|
// all the relevant membership info to hydrate events with a sender
|
||||||
@@ -99,31 +92,6 @@ export class Thread extends EventEmitter {
|
|||||||
this.emit(ThreadEvent.Update, this);
|
this.emit(ThreadEvent.Update, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Completes the reply chain with all events
|
|
||||||
* missing from the current sync data
|
|
||||||
* Will fire "Thread.ready"
|
|
||||||
*/
|
|
||||||
public async fetchReplyChain(): Promise<void> {
|
|
||||||
if (!this.ready) {
|
|
||||||
let mxEvent = this.room.findEventById(this.rootEvent.replyEventId);
|
|
||||||
if (!mxEvent) {
|
|
||||||
mxEvent = await this.fetchEventById(
|
|
||||||
this.rootEvent.getRoomId(),
|
|
||||||
this.rootEvent.replyEventId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.addEvent(mxEvent, true);
|
|
||||||
if (mxEvent.replyEventId) {
|
|
||||||
await this.fetchReplyChain();
|
|
||||||
} else {
|
|
||||||
await this.decryptEvents();
|
|
||||||
this.emit(ThreadEvent.Ready, this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async decryptEvents(): Promise<void> {
|
private async decryptEvents(): Promise<void> {
|
||||||
await Promise.allSettled(
|
await Promise.allSettled(
|
||||||
Array.from(this.timelineSet.getLiveTimeline().getEvents()).map(event => {
|
Array.from(this.timelineSet.getLiveTimeline().getEvents()).map(event => {
|
||||||
@@ -132,18 +100,6 @@ export class Thread extends EventEmitter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches an event over the network
|
|
||||||
*/
|
|
||||||
private async fetchEventById(roomId: string, eventId: string): Promise<MatrixEvent> {
|
|
||||||
const response = await this.client.http.authedRequest(
|
|
||||||
undefined,
|
|
||||||
"GET",
|
|
||||||
`/rooms/${roomId}/event/${eventId}`,
|
|
||||||
);
|
|
||||||
return new MatrixEvent(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds an event by ID in the current thread
|
* Finds an event by ID in the current thread
|
||||||
*/
|
*/
|
||||||
@@ -155,7 +111,7 @@ export class Thread extends EventEmitter {
|
|||||||
* Determines thread's ready status
|
* Determines thread's ready status
|
||||||
*/
|
*/
|
||||||
public get ready(): boolean {
|
public get ready(): boolean {
|
||||||
return this.rootEvent.replyEventId === undefined;
|
return this.rootEvent !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -217,29 +173,26 @@ export class Thread extends EventEmitter {
|
|||||||
return this.timelineSet.findEventById(eventId) instanceof MatrixEvent;
|
return this.timelineSet.findEventById(eventId) instanceof MatrixEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
public toJson(): ISerialisedThread {
|
|
||||||
return {
|
|
||||||
id: this.id,
|
|
||||||
tails: Array.from(this.tail),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public on(event: ThreadEvent, listener: (...args: any[]) => void): this {
|
public on(event: ThreadEvent, listener: (...args: any[]) => void): this {
|
||||||
super.on(event, listener);
|
super.on(event, listener);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public once(event: ThreadEvent, listener: (...args: any[]) => void): this {
|
public once(event: ThreadEvent, listener: (...args: any[]) => void): this {
|
||||||
super.once(event, listener);
|
super.once(event, listener);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public off(event: ThreadEvent, listener: (...args: any[]) => void): this {
|
public off(event: ThreadEvent, listener: (...args: any[]) => void): this {
|
||||||
super.off(event, listener);
|
super.off(event, listener);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public addListener(event: ThreadEvent, listener: (...args: any[]) => void): this {
|
public addListener(event: ThreadEvent, listener: (...args: any[]) => void): this {
|
||||||
super.addListener(event, listener);
|
super.addListener(event, listener);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeListener(event: ThreadEvent, listener: (...args: any[]) => void): this {
|
public removeListener(event: ThreadEvent, listener: (...args: any[]) => void): this {
|
||||||
super.removeListener(event, listener);
|
super.removeListener(event, listener);
|
||||||
return this;
|
return this;
|
||||||
|
|||||||
@@ -319,13 +319,13 @@ export class SyncApi {
|
|||||||
// An event should live in the thread timeline if
|
// An event should live in the thread timeline if
|
||||||
// - It's a reply in thread event
|
// - It's a reply in thread event
|
||||||
// - It's related to a reply in thread event
|
// - It's related to a reply in thread event
|
||||||
let shouldLiveInThreadTimeline = event.replyInThread;
|
let shouldLiveInThreadTimeline = event.isThreadRelation;
|
||||||
if (!shouldLiveInThreadTimeline) {
|
if (!shouldLiveInThreadTimeline) {
|
||||||
const parentEventId = event.parentEventId;
|
const parentEventId = event.parentEventId;
|
||||||
const parentEvent = room?.findEventById(parentEventId) || events.find((mxEv: MatrixEvent) => {
|
const parentEvent = room?.findEventById(parentEventId) || events.find((mxEv: MatrixEvent) => {
|
||||||
return mxEv.getId() === parentEventId;
|
return mxEv.getId() === parentEventId;
|
||||||
});
|
});
|
||||||
shouldLiveInThreadTimeline = parentEvent?.replyInThread;
|
shouldLiveInThreadTimeline = parentEvent?.isThreadRelation;
|
||||||
}
|
}
|
||||||
memo[shouldLiveInThreadTimeline ? 1 : 0].push(event);
|
memo[shouldLiveInThreadTimeline ? 1 : 0].push(event);
|
||||||
return memo;
|
return memo;
|
||||||
|
|||||||
Reference in New Issue
Block a user