Skip to content

Commit

Permalink
Merge pull request #1980 from matrix-org/gsouquet/threads-relations
Browse files Browse the repository at this point in the history
  • Loading branch information
germain-gg authored Oct 15, 2021
2 parents 6804e42 + efbc469 commit fc80082
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 139 deletions.
13 changes: 8 additions & 5 deletions src/@types/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ export enum EventType {
export enum RelationType {
Annotation = "m.annotation",
Replace = "m.replace",
/**
* Note, "io.element.thread" is hardcoded
* Should be replaced with "m.thread" once MSC3440 lands
* Can not use `UnstableValue` as TypeScript does not
* allow computed values in enums
* https://github.com/microsoft/TypeScript/issues/27976
*/
Thread = "io.element.thread",
}

export enum MsgType {
Expand Down Expand Up @@ -168,11 +176,6 @@ export const UNSTABLE_ELEMENT_FUNCTIONAL_USERS = new UnstableValue(
"io.element.functional_members",
"io.element.functional_members");

export const UNSTABLE_ELEMENT_REPLY_IN_THREAD = new UnstableValue(
"m.in_thread",
"io.element.in_thread",
);

export interface IEncryptedFile {
url: string;
mimetype?: string;
Expand Down
50 changes: 25 additions & 25 deletions src/models/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import {
EventType,
MsgType,
RelationType,
UNSTABLE_ELEMENT_REPLY_IN_THREAD,
} from "../@types/event";
import { Crypto } from "../crypto";
import { deepSortedObjectEntries } from "../utils";
Expand Down Expand Up @@ -119,7 +118,7 @@ interface IAggregatedRelation {
key?: string;
}

interface IEventRelation {
export interface IEventRelation {
rel_type: RelationType | string;
event_id: string;
key?: string;
Expand Down Expand Up @@ -419,38 +418,39 @@ export class MatrixEvent extends EventEmitter {

/**
* @experimental
* Get the event ID of the replied event
* Get the event ID of the thread head
*/
public get replyEventId(): string {
const relations = this.getWireContent()["m.relates_to"];
return relations?.["m.in_reply_to"]?.["event_id"];
public get threadRootId(): string {
const relatesTo = this.getWireContent()?.["m.relates_to"];
if (relatesTo?.rel_type === RelationType.Thread) {
return relatesTo.event_id;
}
}

/**
* @experimental
* Determines whether a reply should be rendered in a thread
* or in the main room timeline
*/
public get replyInThread(): boolean {
/**
* 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"];
*/
public get isThreadRelation(): boolean {
return !!this.threadRootId;
}

return relatesTo?.[UNSTABLE_ELEMENT_REPLY_IN_THREAD.name]
|| (this.replyEventId && replyTo[UNSTABLE_ELEMENT_REPLY_IN_THREAD.name])
|| this.thread instanceof Thread;
/**
* @experimental
*/
public get isThreadRoot(): boolean {
const thread = this.getThread();
return thread?.id === this.getId();
}

public get parentEventId(): string {
return this.replyEventId
|| this.getWireContent()["m.relates_to"]?.event_id;
const relations = this.getWireContent()["m.relates_to"];
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"];
}

/**
Expand Down
69 changes: 20 additions & 49 deletions src/models/room.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ export class Room extends EventEmitter {
/**
* @experimental
*/
public threads = new Set<Thread>();
public threads = new Map<string, Thread>();

/**
* Construct a new Room.
Expand Down Expand Up @@ -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
*/
Expand All @@ -1097,26 +1084,6 @@ export class Room extends EventEmitter {
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.
* @param {string} userId The user ID of the member.
Expand Down Expand Up @@ -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"
* @experimental
*/
public addThreadedEvent(event: MatrixEvent): void {
let thread = this.findEventById(event.parentEventId)?.getThread();
let thread = this.findThreadForEvent(event);
if (thread) {
thread.addEvent(event);
} else {
thread = new Thread([event], this, this.client);
}

if (!this.threads.has(thread)) {
this.addThread(thread);
const rootEvent = this.findEventById(event.threadRootId);
thread = new Thread([rootEvent, event], this, this.client);
this.reEmitter.reEmit(thread, [ThreadEvent.Update, ThreadEvent.Ready]);
this.threads.set(thread.id, thread);
}
this.emit(ThreadEvent.Update, thread);
}

/**
Expand Down Expand Up @@ -1409,7 +1388,7 @@ export class Room extends EventEmitter {
// TODO: Enable "pending events" for 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
if (this.client?.supportsExperimentalThreads() && event.replyInThread) {
if (this.client?.supportsExperimentalThreads() && event.threadRootId) {
return;
}

Expand Down Expand Up @@ -1585,14 +1564,6 @@ export class Room extends EventEmitter {
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
* progress.
Expand Down
69 changes: 11 additions & 58 deletions src/models/thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,6 @@ export enum ThreadEvent {
Update = "Thread.update"
}

interface ISerialisedThread {
id: string;
tails: string[];
}

/**
* @experimental
*/
Expand All @@ -42,7 +37,6 @@ export class Thread extends EventEmitter {
/**
* A reference to all the events ID at the bottom of the threads
*/
public readonly tail = new Set<string>();
public readonly timelineSet: EventTimelineSet;

constructor(
Expand All @@ -69,13 +63,12 @@ export class Thread extends EventEmitter {
return;
}

if (this.tail.has(event.replyEventId)) {
this.tail.delete(event.replyEventId);
}
this.tail.add(event.getId());

if (!event.replyEventId || !this.timelineSet.findEventById(event.replyEventId)) {
this.root = event.getId();
if (!this.root) {
if (event.isThreadRelation) {
this.root = event.threadRootId;
} else {
this.root = event.getId();
}
}

// all the relevant membership info to hydrate events with a sender
Expand All @@ -99,31 +92,6 @@ export class Thread extends EventEmitter {
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> {
await Promise.allSettled(
Array.from(this.timelineSet.getLiveTimeline().getEvents()).map(event => {
Expand All @@ -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
*/
Expand All @@ -155,7 +111,7 @@ export class Thread extends EventEmitter {
* Determines thread's ready status
*/
public get ready(): boolean {
return this.rootEvent.replyEventId === undefined;
return this.rootEvent !== undefined;
}

/**
Expand Down Expand Up @@ -217,29 +173,26 @@ export class Thread extends EventEmitter {
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 {
super.on(event, listener);
return this;
}

public once(event: ThreadEvent, listener: (...args: any[]) => void): this {
super.once(event, listener);
return this;
}

public off(event: ThreadEvent, listener: (...args: any[]) => void): this {
super.off(event, listener);
return this;
}

public addListener(event: ThreadEvent, listener: (...args: any[]) => void): this {
super.addListener(event, listener);
return this;
}

public removeListener(event: ThreadEvent, listener: (...args: any[]) => void): this {
super.removeListener(event, listener);
return this;
Expand Down
4 changes: 2 additions & 2 deletions src/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,13 +319,13 @@ export class SyncApi {
// An event should live in the thread timeline if
// - It's a reply in thread event
// - It's related to a reply in thread event
let shouldLiveInThreadTimeline = event.replyInThread;
let shouldLiveInThreadTimeline = event.isThreadRelation;
if (!shouldLiveInThreadTimeline) {
const parentEventId = event.parentEventId;
const parentEvent = room?.findEventById(parentEventId) || events.find((mxEv: MatrixEvent) => {
return mxEv.getId() === parentEventId;
});
shouldLiveInThreadTimeline = parentEvent?.replyInThread;
shouldLiveInThreadTimeline = parentEvent?.isThreadRelation;
}
memo[shouldLiveInThreadTimeline ? 1 : 0].push(event);
return memo;
Expand Down

0 comments on commit fc80082

Please sign in to comment.