Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Thread store and event parsing to create threads #1828

Merged
merged 12 commits into from
Aug 31, 2021
5 changes: 5 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,11 @@ export interface IStartClientOpts {
* This should be in the order of hours. Default: undefined.
*/
clientWellKnownPollPeriod?: number;

/**
* @experimental
*/
experimentalThreadSupport?: boolean;
}

export interface IStoredClientOpts extends IStartClientOpts {
Expand Down
35 changes: 35 additions & 0 deletions src/models/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ import { EventType, MsgType, RelationType } from "../@types/event";
import { Crypto } from "../crypto";
import { deepSortedObjectEntries } from "../utils";
import { RoomMember } from "./room-member";
import { Thread } from "./thread";
import { IActionsObject } from '../pushprocessor';
import { ReEmitter } from '../ReEmitter';

/**
* Enum for event statuses.
Expand Down Expand Up @@ -191,6 +193,12 @@ export class MatrixEvent extends EventEmitter {
*/
private txnId: string = null;

/**
* @experimental
* A reference to the thread this event belongs to
*/
private thread: Thread = null;

/* Set an approximate timestamp for the event relative the local clock.
* This will inherently be approximate because it doesn't take into account
* the time between the server putting the 'age' field on the event as it sent
Expand All @@ -212,6 +220,8 @@ export class MatrixEvent extends EventEmitter {
*/
public verificationRequest = null;

private readonly reEmitter: ReEmitter;

/**
* Construct a Matrix Event object
* @constructor
Expand Down Expand Up @@ -261,6 +271,7 @@ export class MatrixEvent extends EventEmitter {

this.txnId = event.txn_id || null;
this.localTimestamp = Date.now() - this.getAge();
this.reEmitter = new ReEmitter(this);
}

/**
Expand Down Expand Up @@ -381,6 +392,15 @@ export class MatrixEvent extends EventEmitter {
return this.event.content || {};
}

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

/**
* Get the previous event content JSON. This will only return something for
* state events which exist in the timeline.
Expand Down Expand Up @@ -1271,6 +1291,21 @@ export class MatrixEvent extends EventEmitter {
public getTxnId(): string | undefined {
return this.txnId;
}

/**
* @experimental
*/
public setThread(thread: Thread): void {
this.thread = thread;
this.reEmitter.reEmit(thread, ["Thread.ready", "Thread.update"]);
}

/**
* @experimental
*/
public getThread(): Thread {
return this.thread;
}
}

/* REDACT_KEEP_KEYS gives the keys we keep when an event is redacted
Expand Down
93 changes: 91 additions & 2 deletions src/models/room.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersio
import { ResizeMethod } from "../@types/partials";
import { Filter } from "../filter";
import { RoomState } from "./room-state";
import { Thread } from "./thread";

// These constants are used as sane defaults when the homeserver doesn't support
// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
Expand Down Expand Up @@ -145,6 +146,11 @@ export class Room extends EventEmitter {
public oldState: RoomState;
public currentState: RoomState;

/**
* @experimental
*/
public threads = new Set<Thread>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's a little dangerous exposing this publicly, but not the end of the world. Can it get some jsdoc to guide the developer on using it safely? (ie: is it safe to mutate, or should it be treated as readonly?)


/**
* Construct a new Room.
*
Expand Down Expand Up @@ -857,13 +863,26 @@ export class Room extends EventEmitter {
}

/**
* Get an event which is stored in our unfiltered timeline set
* Get an event which is stored in our unfiltered timeline set or in a thread
*
* @param {string} eventId event ID to look for
* @return {?module:models/event.MatrixEvent} the given event, or undefined if unknown
*/
public findEventById(eventId: string): MatrixEvent | undefined {
return this.getUnfilteredTimelineSet().findEventById(eventId);
let event = this.getUnfilteredTimelineSet().findEventById(eventId);

if (event) {
return event;
} else {
const threads = this.getThreads();
for (let i = 0; i < threads.length; i++) {
const thread = threads[i];
event = thread.findEventById(eventId);
if (event) {
return event;
}
}
}
}

/**
Expand Down Expand Up @@ -1049,6 +1068,54 @@ export class Room extends EventEmitter {
);
}

/**
* @experimental
*/
public addThread(thread: Thread): Set<Thread> {
this.threads.add(thread);
if (!thread.ready) {
thread.once("Thread.ready", this.dedupeThreads);
this.emit("Thread.update", thread);
this.reEmitter.reEmit(thread, ["Thread.update", "Thread.ready"]);
}
return this.threads;
}

/**
* @experimental
*/
public getThread(eventId: string): Thread {
return this.getThreads().find(thread => {
return thread.id === eventId;
});
}

/**
* @experimental
*/
public getThreads(): Thread[] {
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 threads = Array.from(this.threads);
if (threads.includes(readyThread)) {
this.threads = new Set(threads.filter(thread => {
if (readyThread.id === thread.id && readyThread !== thread) {
return false;
} else {
return true;
}
}));
}
};

/**
* Get a member from the current room state.
* @param {string} userId The user ID of the member.
Expand Down Expand Up @@ -1225,6 +1292,28 @@ export class Room extends EventEmitter {
}
}

/**
* Add an event to a thread's timeline. Will fire "Thread.update"
* @experimental
*/
public addThreadedEvent(event: MatrixEvent): void {
if (event.getUnsigned().transaction_id) {
const existingEvent = this.txnToEvent[event.getUnsigned().transaction_id];
if (existingEvent) {
// remote echo of an event we sent earlier
this.handleRemoteEcho(event, existingEvent);
}
}

let thread = this.findEventById(event.replyEventId)?.getThread();
if (thread) {
thread.addEvent(event);
} else {
thread = new Thread([event], this, this.client);
this.addThread(thread);
}
}

/**
* Add an event to the end of this room's live timelines. Will fire
* "Room.timeline".
Expand Down
Loading