Fix threads and make timelines emitters

This commit is contained in:
tezlm 2023-12-11 00:09:36 -08:00
parent ac59e44ac5
commit 1e7daac4d5
Signed by: tezlm
GPG key ID: 649733FCD94AFBBA
19 changed files with 255 additions and 95 deletions

View file

@ -39,7 +39,7 @@ interface CreateRoom {
initialState?: Array<{
type: string;
content: any;
stateKey: string;
stateKey?: string;
}>;
version?: string;
}

3
dist/src/client.js vendored
View file

@ -18,7 +18,7 @@ class Rooms extends Map {
initial_state: options.initialState?.map(ev => ({
type: ev.type,
content: ev.content,
state_key: ev.stateKey,
state_key: ev.stateKey ?? "",
})),
creation_content: options.creationContent,
});
@ -111,6 +111,7 @@ export class Client extends EventEmitter {
await this.conn.sync();
}
catch (err) {
console.error(err);
this.setState({ state: "error", reason: err });
}
}

View file

@ -1 +1 @@
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"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"}

16
dist/src/room.d.ts vendored
View file

@ -22,7 +22,7 @@ export declare class Room extends Room_base {
private state;
timelines: TimelineSet;
events: Map<EventId, Event>;
threads: Map<EventId, Thread>;
threads: RoomThreads;
constructor(client: Client, id: string, data: SyncResponseRoom);
_merge(data: SyncResponseRoom): void;
getState(type: string, stateKey?: string): StateEvent | null;
@ -32,6 +32,20 @@ export declare class Room extends Room_base {
leave(reason?: string): Promise<void>;
ack(eventId?: EventId): Promise<void>;
}
declare class RoomThreads extends Map<EventId, Thread> {
room: Room;
constructor(room: Room);
fetch(eventId: EventId): Promise<Thread>;
paginate(opts?: {
from?: string;
limit?: number;
watching?: boolean;
include?: Array<IncludeThreads>;
}): Promise<{
threads: Thread[];
nextBatch: string | undefined;
}>;
}
export declare class ThreadPaginator extends Map<EventId, Thread> {
client: Client;
rooms: Array<Room>;

6
dist/src/room.js vendored
View file

@ -43,7 +43,7 @@ export class Room extends EventEmitter {
enumerable: true,
configurable: true,
writable: true,
value: void 0
value: new TimelineSet(this)
});
Object.defineProperty(this, "events", {
enumerable: true,
@ -55,11 +55,9 @@ export class Room extends EventEmitter {
enumerable: true,
configurable: true,
writable: true,
value: new Map()
value: new RoomThreads(this)
});
this.timelines = new TimelineSet(this);
this.timelines.live.prevBatch = data.prev_batch;
this.threads = new RoomThreads(this);
this._merge(data);
}
// should be private to only the library like pub(crate), but there's no way currently

File diff suppressed because one or more lines are too long

View file

@ -11,8 +11,11 @@ declare const Thread_base: new () => TypedEmitter<ThreadEvents>;
export declare class Thread extends Thread_base {
baseEvent: Event;
room: Room;
timeline: ThreadTimeline;
id: string;
participation: string;
messageCount: number;
latestEvent: Event;
timeline: ThreadTimeline;
constructor(baseEvent: Event);
ack(eventId?: EventId): Promise<void>;
}

42
dist/src/thread.js vendored
View file

@ -1,7 +1,11 @@
import EventEmitter from "events";
import { Event } from "./event.js";
import { ThreadTimeline } from "./timeline.js";
export class Thread extends EventEmitter {
constructor(baseEvent) {
const threadRel = baseEvent.unsigned["m.relations"]?.["m.thread"];
if (!threadRel)
throw new Error("Tried to create a thread from a non-thread event!");
super();
Object.defineProperty(this, "baseEvent", {
enumerable: true,
@ -15,18 +19,44 @@ export class Thread extends EventEmitter {
writable: true,
value: this.baseEvent.room
});
Object.defineProperty(this, "timeline", {
enumerable: true,
configurable: true,
writable: true,
value: new ThreadTimeline(this)
});
Object.defineProperty(this, "id", {
enumerable: true,
configurable: true,
writable: true,
value: this.baseEvent.id
});
Object.defineProperty(this, "participation", {
enumerable: true,
configurable: true,
writable: true,
value: "participating"
});
Object.defineProperty(this, "messageCount", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "latestEvent", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
// @deprecated: Threads will have multiple timelines
Object.defineProperty(this, "timeline", {
enumerable: true,
configurable: true,
writable: true,
value: new ThreadTimeline(this)
});
const latestRaw = threadRel.latest_event;
const latestCached = this.room.events.get(latestRaw.event_id);
const latestEvent = latestCached ?? new Event(this.room, latestRaw);
if (!latestCached)
this.room.events.set(latestEvent.id, latestEvent);
this.latestEvent = latestEvent;
this.messageCount = threadRel.count;
}
// // TODO: local echo(?), return event
// async sendEvent(type: string, content: any) {

View file

@ -1 +1 @@
{"version":3,"file":"thread.js","sourceRoot":"","sources":["../../src/thread.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,QAAQ,CAAC;AAKlC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAU/C,MAAM,OAAO,MAAO,SAAS,YAAgE;IAK3F,YAAmB,SAAgB;QACjC,KAAK,EAAE,CAAC;QADE;;;;mBAAO,SAAS;WAAO;QAJ5B;;;;mBAAa,IAAI,CAAC,SAAS,CAAC,IAAI;WAAC;QACjC;;;;mBAA2B,IAAI,cAAc,CAAC,IAAI,CAAC;WAAC;QACpD;;;;mBAAK,IAAI,CAAC,SAAS,CAAC,EAAE;WAAC;IAI9B,CAAC;IAED,uCAAuC;IACvC,gDAAgD;IAChD,uEAAuE;IACvE,IAAI;IAEJ,sDAAsD;IACtD,sDAAsD;IACtD,IAAI;IAEJ,KAAK,CAAC,GAAG,CAAC,OAAiB;QACzB,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC;YAC7B,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;SACnF,CAAC,CAAC;IACL,CAAC;CACF"}
{"version":3,"file":"thread.js","sourceRoot":"","sources":["../../src/thread.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,QAAQ,CAAC;AAIlC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAU/C,MAAM,OAAO,MAAO,SAAS,YAAgE;IAW3F,YAAmB,SAAgB;QACjC,MAAM,SAAS,GAAG,SAAS,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC;QAClE,IAAI,CAAC,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAC;QAErF,KAAK,EAAE,CAAC;QAJE;;;;mBAAO,SAAS;WAAO;QAV5B;;;;mBAAa,IAAI,CAAC,SAAS,CAAC,IAAI;WAAC;QACjC;;;;mBAAK,IAAI,CAAC,SAAS,CAAC,EAAE;WAAC;QAEvB;;;;mBAAgB,eAAe;WAAC;QAChC;;;;;WAAqB;QACrB;;;;;WAAmB;QAE1B,oDAAoD;QAC7C;;;;mBAA2B,IAAI,cAAc,CAAC,IAAI,CAAC;WAAC;QAQzD,MAAM,SAAS,GAAG,SAAS,CAAC,YAAY,CAAC;QACzC,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QAC9D,MAAM,WAAW,GAAG,YAAY,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;QACpE,IAAI,CAAC,YAAY;YAAE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,EAAE,WAAW,CAAC,CAAC;QACrE,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,YAAY,GAAG,SAAS,CAAC,KAAK,CAAC;IACtC,CAAC;IAED,uCAAuC;IACvC,gDAAgD;IAChD,uEAAuE;IACvE,IAAI;IAEJ,sDAAsD;IACtD,sDAAsD;IACtD,IAAI;IAEJ,KAAK,CAAC,GAAG,CAAC,OAAiB;QACzB,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC;YAC7B,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;SACnF,CAAC,CAAC;IACL,CAAC;CACF"}

View file

@ -1,34 +1,44 @@
import { EventId } from "./api.js";
import { ApiEphemeralEvent, EventId } from "./api.js";
import { Room } from "./room.js";
import { Event } from "./event.js";
import { Thread } from "./thread.js";
export interface Timeline {
import TypedEmitter from "typed-emitter";
type TimelineEvents = {
timelineUpdate: (batch: Array<Event>, toBeginning: boolean) => void;
timelineAppend: (event: Event) => void;
ephemeral: (event: ApiEphemeralEvent) => void;
};
export interface Timeline extends TypedEmitter<TimelineEvents> {
isLive: boolean;
isAtBeginning: boolean;
isAtEnd: boolean;
events: Array<Event>;
getEvents(): Array<Event>;
paginate(dir: "f" | "b", limit: number): Promise<boolean>;
}
export declare class RoomTimeline implements Timeline {
declare const RoomTimeline_base: new () => TypedEmitter<TimelineEvents>;
export declare class RoomTimeline extends RoomTimeline_base implements Timeline {
room: Room;
events: Array<Event>;
isLive: boolean;
isAtBeginning: boolean;
isAtEnd: boolean;
_events: Array<Event>;
prevBatch: string | undefined;
nextBatch: string | undefined;
constructor(room: Room);
getEvents(): Array<Event>;
paginate(dir: "f" | "b", limit?: number): Promise<boolean>;
}
export declare class ThreadTimeline implements Timeline {
declare const ThreadTimeline_base: new () => TypedEmitter<TimelineEvents>;
export declare class ThreadTimeline extends ThreadTimeline_base implements Timeline {
thread: Thread;
events: Array<Event>;
isLive: boolean;
isAtBeginning: boolean;
isAtEnd: boolean;
_events: Array<Event>;
prevBatch: string | undefined;
nextBatch: string | undefined;
constructor(thread: Thread);
getEvents(): Array<Event>;
paginate(dir: "f" | "b", limit?: number): Promise<boolean>;
}
export declare class TimelineSet {
@ -39,3 +49,4 @@ export declare class TimelineSet {
forEvent(eventId: EventId): Promise<RoomTimeline>;
_appendEvents(events: Array<Event>): void;
}
export {};

81
dist/src/timeline.js vendored
View file

@ -1,19 +1,15 @@
// Timelines are ordered sequences of events.
import { Event } from "./event.js";
export class RoomTimeline {
import EventEmitter from "events";
export class RoomTimeline extends EventEmitter {
constructor(room) {
super();
Object.defineProperty(this, "room", {
enumerable: true,
configurable: true,
writable: true,
value: room
});
Object.defineProperty(this, "events", {
enumerable: true,
configurable: true,
writable: true,
value: []
});
Object.defineProperty(this, "isLive", {
enumerable: true,
configurable: true,
@ -32,6 +28,13 @@ export class RoomTimeline {
writable: true,
value: false
});
// These should be private, but typescript doesn't have "private to module"
Object.defineProperty(this, "_events", {
enumerable: true,
configurable: true,
writable: true,
value: []
});
Object.defineProperty(this, "prevBatch", {
enumerable: true,
configurable: true,
@ -45,6 +48,9 @@ export class RoomTimeline {
value: void 0
});
}
getEvents() {
return this._events;
}
async paginate(dir, limit = 50) {
if (dir === "b" && this.isAtBeginning)
return false;
@ -65,9 +71,10 @@ export class RoomTimeline {
else {
this.isAtEnd = true;
}
this.events.push(...events);
this._events.push(...events);
for (const event of events)
this.room.events.set(event.id, event);
this.emit("timelineUpdate", events, false);
}
else {
const events = data.chunk.reverse().map(raw => new Event(this.room, raw));
@ -77,27 +84,23 @@ export class RoomTimeline {
else {
this.isAtBeginning = true;
}
this.events.push(...events);
this._events.push(...events);
for (const event of events)
this.room.events.set(event.id, event);
this.emit("timelineUpdate", events, true);
}
return true;
}
}
export class ThreadTimeline {
export class ThreadTimeline extends EventEmitter {
constructor(thread) {
super();
Object.defineProperty(this, "thread", {
enumerable: true,
configurable: true,
writable: true,
value: thread
});
Object.defineProperty(this, "events", {
enumerable: true,
configurable: true,
writable: true,
value: []
});
Object.defineProperty(this, "isLive", {
enumerable: true,
configurable: true,
@ -116,6 +119,13 @@ export class ThreadTimeline {
writable: true,
value: false
});
// These should be private, but typescript doesn't have "private to module"
Object.defineProperty(this, "_events", {
enumerable: true,
configurable: true,
writable: true,
value: []
});
Object.defineProperty(this, "prevBatch", {
enumerable: true,
configurable: true,
@ -129,6 +139,9 @@ export class ThreadTimeline {
value: void 0
});
}
getEvents() {
return this._events;
}
async paginate(dir, limit = 50) {
if (dir === "b" && this.isAtBeginning)
return false;
@ -137,6 +150,7 @@ export class ThreadTimeline {
// This is to prevent someone from trying to paginate forwards,
// then paginate backwards. The timeline will end up with events from
// the beginning and end!
// NOTE: deprecated?
// FIXME: implement /context in threads
if (dir === "f")
this.isAtBeginning = true;
@ -150,30 +164,31 @@ export class ThreadTimeline {
limit,
});
if (dir === "f") {
const events = data.chunk.map(raw => new Event(room, raw));
const events = data.chunk.reverse().map(raw => new Event(room, raw));
if (data.next_batch) {
this.nextBatch = data.next_batch;
}
else {
this.isAtEnd = true;
}
this.events.push(...events);
this._events.push(...events);
for (const event of events)
room.events.set(event.id, event);
this.emit("timelineUpdate", events, false);
return !!data.next_batch;
}
else {
// FIXME: conduit doesn't implement ?dir=b
const events = data.chunk.map(raw => new Event(room, raw));
const events = data.chunk.reverse().map(raw => new Event(room, raw));
if (data.prev_batch) {
this.prevBatch = data.prev_batch;
}
else {
this.isAtBeginning = true;
}
this.events.push(...events);
this._events.push(...events);
for (const event of events)
room.events.set(event.id, event);
this.emit("timelineUpdate", events, true);
return !!data.prev_batch;
}
}
@ -215,7 +230,7 @@ export class TimelineSet {
async forEvent(eventId) {
const context = await this.room.client.net.fetchContext(this.room.id, eventId);
const tl = new RoomTimeline(this.room);
tl.events = context.events_before
tl._events = context.events_before
.reverse()
.concat([context.event])
.concat(context.events_after)
@ -230,19 +245,21 @@ export class TimelineSet {
// thread, they need to be merged together
for (const event of events) {
this.room.events.set(event.id, event);
this.live._events.push(...events);
for (const event of events)
this.live.emit("timelineAppend", event);
const threadId = event.content["m.relations"]?.find((rel) => rel.rel_type === "m.thread")?.event_id;
if (threadId) {
const tl = this.room.threads.get(threadId)?.timeline;
if (tl?.isLive)
tl.events.push(event);
const thread = this.room.threads.get(threadId);
if (thread) {
thread.messageCount++;
thread.latestEvent = event;
const tl = thread.timeline;
if (tl?.isLive) {
tl._events.push(event);
tl.emit("timelineAppend", event);
}
}
}
for (const timeline of this.timelines) {
// if (timeline.isLive) timeline.events.push(...events);
if (timeline.isLive && timeline instanceof RoomTimeline)
timeline.events.push(...events);
// this.room.threads.get()
}
}
}
//# sourceMappingURL=timeline.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

32
docs/bot.ts Normal file
View file

@ -0,0 +1,32 @@
// An example pong-pong bot
// I might add a higher level bot sdk at some point, since this is relatively low level
import { Client, Event } from "../dist/src/index.js";
// Create the client
const client = new Client({
// baseUrl: "https://homeserver.tld",
// deviceId: "something",
// token: "supersecrettoken",
// userId: "@userid:homeserver.tld",
baseUrl: "http://localhost:6167",
deviceId: "something",
token: "9SWeOqc5g42O0fS3JesE43s1JujCDD8S",
userId: "@asdf:localhost",
});
function handleEvent(event: Event) {
console.log("received event from " + event.sender);
}
client.on("roomInit", (room) => room.timelines.live.on("timelineAppend", handleEvent));
client.on("roomDeinit", (room) => room.timelines.live.on("timelineAppend", handleEvent));
client.lists.subscribe("allrooms", {
ranges: [[0, 9999999]], // fetch all rooms
required_state: [["m.room.name", ""]], // get their names
timeline_limit: 1, // only listen to the last event (this will not trigger timelineAppend)
});
// Start syncing and receiving events
client.start();

View file

@ -188,7 +188,7 @@ export interface MessagesResponse {
}
export interface RelationsResponse {
chunk: Array<ApiEvent>,
chunk: Array<ApiEvent>, // always in reverse chronological order! (later relations are always first)
next_batch: string,
prev_batch: string,
}

View file

@ -53,7 +53,7 @@ type RoomList = {
interface CreateRoom {
creationContent?: Record<string, any>,
initialState?: Array<{ type: string, content: any, stateKey: string }>,
initialState?: Array<{ type: string, content: any, stateKey?: string }>,
version?: string,
}
@ -68,7 +68,7 @@ class Rooms extends Map<string, Room> {
initial_state: options.initialState?.map(ev => ({
type: ev.type,
content: ev.content,
state_key: ev.stateKey,
state_key: ev.stateKey ?? "",
})),
creation_content: options.creationContent,
});
@ -135,6 +135,7 @@ export class Client extends (EventEmitter as unknown as new () => TypedEmitter<C
try {
await this.conn.sync();
} catch (err) {
console.error(err);
this.setState({ state: "error", reason: err });
}
}

View file

@ -9,9 +9,11 @@ import { Thread } from "./thread.js";
type RoomEvents = {
// an event is appended to this room's live timeline
// @deprecated: use timelines.live
timeline: (event: Event) => void,
// an ephemeral event was received
// @deprecated: use timelines.live
ephemeral: (event: ApiEphemeralEvent) => void,
// this room's state updated
@ -31,9 +33,9 @@ export class Room extends (EventEmitter as unknown as new () => TypedEmitter<Roo
// The (possibly incomplete) state of this room
private state: Map<string, Map<string, StateEvent>> = new Map();
public timelines: TimelineSet;
public timelines = new TimelineSet(this);
public events: Map<EventId, Event> = new Map();
public threads: Map<EventId, Thread> = new Map();
public threads = new RoomThreads(this);
// public members: Members;
/*
@ -56,9 +58,7 @@ room.unban(userid)
data: SyncResponseRoom,
) {
super();
this.timelines = new TimelineSet(this);
this.timelines.live.prevBatch = data.prev_batch;
this.threads = new RoomThreads(this);
this._merge(data);
}
@ -137,7 +137,7 @@ class RoomThreads extends Map<EventId, Thread> {
return thread;
}
async paginate(opts: { from?: string, limit?: number, watching?: boolean, include?: Array<IncludeThreads> }) {
async paginate(opts?: { from?: string, limit?: number, watching?: boolean, include?: Array<IncludeThreads> }) {
const data = await this.room.client.net.fetchThreads({ ...opts, roomIds: [this.room.id] });
const threads = data.chunk?.map(raw => new Thread(new Event(this.room, raw))) || [];
for (const th of threads) this.set(th.id, th);

View file

@ -15,11 +15,27 @@ type ThreadEvents = {
export class Thread extends (EventEmitter as unknown as new () => TypedEmitter<ThreadEvents>) {
public room: Room = this.baseEvent.room;
public timeline: ThreadTimeline = new ThreadTimeline(this);
public id = this.baseEvent.id;
public participation = "participating";
public messageCount: number;
public latestEvent: Event;
// @deprecated: Threads will have multiple timelines
public timeline: ThreadTimeline = new ThreadTimeline(this);
constructor(public baseEvent: Event) {
const threadRel = baseEvent.unsigned["m.relations"]?.["m.thread"];
if (!threadRel) throw new Error("Tried to create a thread from a non-thread event!");
super();
const latestRaw = threadRel.latest_event;
const latestCached = this.room.events.get(latestRaw.event_id);
const latestEvent = latestCached ?? new Event(this.room, latestRaw);
if (!latestCached) this.room.events.set(latestEvent.id, latestEvent);
this.latestEvent = latestEvent;
this.messageCount = threadRel.count;
}
// // TODO: local echo(?), return event

View file

@ -1,12 +1,26 @@
// Timelines are ordered sequences of events.
// import TypedEventEmitter from "typed-emitter";
import { EventId } from "./api.js";
import { ApiEphemeralEvent, EventId } from "./api.js";
import { Room } from "./room.js";
import { Event } from "./event.js";
import { Thread } from "./thread.js";
import TypedEmitter from "typed-emitter";
import EventEmitter from "events";
export interface Timeline {
type TimelineEvents = {
// This room's live timeline is updated, usually via pagination.
timelineUpdate: (batch: Array<Event>, toBeginning: boolean) => void,
// An event is appended to this timeline.
// Only fires if this timeline is live.
timelineAppend: (event: Event) => void,
// An ephemeral event was received
ephemeral: (event: ApiEphemeralEvent) => void,
}
export interface Timeline extends TypedEmitter<TimelineEvents> {
// if this timeline is actively receiving new events
isLive: boolean,
@ -18,8 +32,8 @@ export interface Timeline {
// isPaginating: boolean,
// the events in this timeline
events: Array<Event>,
// Get the events in this timeline
getEvents(): Array<Event>,
// Paginate a timeline for more events
// TODO: fuse two neighboring timelines together
@ -27,16 +41,23 @@ export interface Timeline {
paginate(dir: "f" | "b", limit: number): Promise<boolean>;
}
export class RoomTimeline implements Timeline {
public events: Array<Event> = [];
export class RoomTimeline extends (EventEmitter as unknown as new () => TypedEmitter<TimelineEvents>) implements Timeline {
public isLive: boolean = false;
public isAtBeginning: boolean = false;
public isAtEnd: boolean = false;
// These should be private, but typescript doesn't have "private to module"
_events: Array<Event> = [];
prevBatch: string | undefined;
nextBatch: string | undefined;
constructor(public room: Room) {}
constructor(public room: Room) {
super();
}
public getEvents(): Array<Event> {
return this._events;
}
public async paginate(dir: "f" | "b", limit: number = 50): Promise<boolean> {
if (dir === "b" && this.isAtBeginning) return false;
@ -56,8 +77,9 @@ export class RoomTimeline implements Timeline {
} else {
this.isAtEnd = true;
}
this.events.push(...events);
this._events.push(...events);
for (const event of events) this.room.events.set(event.id, event);
this.emit("timelineUpdate", events, false);
} else {
const events = data.chunk.reverse().map(raw => new Event(this.room, raw));
if (data.end) {
@ -65,8 +87,9 @@ export class RoomTimeline implements Timeline {
} else {
this.isAtBeginning = true;
}
this.events.push(...events);
this._events.push(...events);
for (const event of events) this.room.events.set(event.id, event);
this.emit("timelineUpdate", events, true);
}
return true;
}
@ -79,17 +102,24 @@ export class RoomTimeline implements Timeline {
// }
}
export class ThreadTimeline implements Timeline {
public events: Array<Event> = [];
export class ThreadTimeline extends (EventEmitter as unknown as new () => TypedEmitter<TimelineEvents>) implements Timeline {
public isLive: boolean = true; // FIXME: live threads
public isAtBeginning: boolean = false;
public isAtEnd: boolean = false;
// These should be private, but typescript doesn't have "private to module"
_events: Array<Event> = [];
prevBatch: string | undefined;
nextBatch: string | undefined;
constructor(public thread: Thread) {}
constructor(public thread: Thread) {
super();
}
public getEvents(): Array<Event> {
return this._events;
}
public async paginate(dir: "f" | "b", limit: number = 50): Promise<boolean> {
if (dir === "b" && this.isAtBeginning) return false;
if (dir === "f" && this.isAtEnd) return false;
@ -97,6 +127,7 @@ export class ThreadTimeline implements Timeline {
// This is to prevent someone from trying to paginate forwards,
// then paginate backwards. The timeline will end up with events from
// the beginning and end!
// NOTE: deprecated?
// FIXME: implement /context in threads
if (dir === "f") this.isAtBeginning = true;
if (dir === "b") this.isAtEnd = true;
@ -109,25 +140,26 @@ export class ThreadTimeline implements Timeline {
limit,
});
if (dir === "f") {
const events = data.chunk.map(raw => new Event(room, raw));
const events = data.chunk.reverse().map(raw => new Event(room, raw));
if (data.next_batch) {
this.nextBatch = data.next_batch;
} else {
this.isAtEnd = true;
}
this.events.push(...events);
this._events.push(...events);
for (const event of events) room.events.set(event.id, event);
this.emit("timelineUpdate", events, false);
return !!data.next_batch;
} else {
// FIXME: conduit doesn't implement ?dir=b
const events = data.chunk.map(raw => new Event(room, raw));
const events = data.chunk.reverse().map(raw => new Event(room, raw));
if (data.prev_batch) {
this.prevBatch = data.prev_batch;
} else {
this.isAtBeginning = true;
}
this.events.push(...events);
this._events.push(...events);
for (const event of events) room.events.set(event.id, event);
this.emit("timelineUpdate", events, true);
return !!data.prev_batch;
}
}
@ -157,7 +189,7 @@ export class TimelineSet {
public async forEvent(eventId: EventId): Promise<RoomTimeline> {
const context = await this.room.client.net.fetchContext(this.room.id, eventId);
const tl = new RoomTimeline(this.room);
tl.events = context.events_before
tl._events = context.events_before
.reverse()
.concat([context.event])
.concat(context.events_after)
@ -173,17 +205,22 @@ export class TimelineSet {
// thread, they need to be merged together
for (const event of events) {
this.room.events.set(event.id, event);
this.live._events.push(...events);
for (const event of events) this.live.emit("timelineAppend", event);
const threadId = event.content["m.relations"]?.find((rel: any) => rel.rel_type === "m.thread")?.event_id;
if (threadId) {
const tl = this.room.threads.get(threadId)?.timeline;
if (tl?.isLive) tl.events.push(event);
const thread = this.room.threads.get(threadId);
if (thread) {
thread.messageCount++;
thread.latestEvent = event;
const tl = thread.timeline;
if (tl?.isLive) {
tl._events.push(event);
tl.emit("timelineAppend", event);
}
}
}
for (const timeline of this.timelines) {
// if (timeline.isLive) timeline.events.push(...events);
if (timeline.isLive && timeline instanceof RoomTimeline) timeline.events.push(...events);
// this.room.threads.get()
}
}
}