1
0
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:
Germain Souquet
2021-10-14 16:56:48 +01:00
parent b288c97372
commit 7b89862ed0
5 changed files with 72 additions and 136 deletions

View File

@@ -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 {

View File

@@ -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"];
} }
/** /**

View File

@@ -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.

View File

@@ -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;

View File

@@ -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;