Improve threads and timelines
This commit is contained in:
parent
b9f6b82611
commit
ac59e44ac5
25 changed files with 356 additions and 63 deletions
7
dist/src/api.d.ts
vendored
7
dist/src/api.d.ts
vendored
|
@ -2,11 +2,12 @@ export type RoomId = string;
|
|||
export type UserId = string;
|
||||
export type EventId = string;
|
||||
export interface ApiEvent {
|
||||
event_id: EventId;
|
||||
sender: UserId;
|
||||
type: string;
|
||||
content: any;
|
||||
event_id: EventId;
|
||||
origin_server_ts: number;
|
||||
sender: UserId;
|
||||
state_key?: string;
|
||||
type: string;
|
||||
unsigned?: any;
|
||||
}
|
||||
export type ApiStateEvent = ApiEvent & {
|
||||
|
|
1
dist/src/event.d.ts
vendored
1
dist/src/event.d.ts
vendored
|
@ -6,6 +6,7 @@ export declare class Event {
|
|||
client: Client;
|
||||
content: any;
|
||||
id: EventId;
|
||||
originTs: number;
|
||||
sender: UserId;
|
||||
stateKey?: string;
|
||||
type: string;
|
||||
|
|
11
dist/src/event.js
vendored
11
dist/src/event.js
vendored
|
@ -1,4 +1,8 @@
|
|||
export class Event {
|
||||
// How do I do the relations api? Event or EventId?
|
||||
// Event is more ergonomic, but EventId means I don't have to wait
|
||||
// public relationsFrom: Array<any>;
|
||||
// public relationsTo: Array<any>;
|
||||
constructor(room, json) {
|
||||
Object.defineProperty(this, "room", {
|
||||
enumerable: true,
|
||||
|
@ -24,6 +28,12 @@ export class Event {
|
|||
writable: true,
|
||||
value: void 0
|
||||
});
|
||||
Object.defineProperty(this, "originTs", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: void 0
|
||||
});
|
||||
Object.defineProperty(this, "sender", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
|
@ -50,6 +60,7 @@ export class Event {
|
|||
});
|
||||
this.content = json.content;
|
||||
this.id = json.event_id;
|
||||
this.originTs = json.origin_server_ts;
|
||||
this.sender = json.sender;
|
||||
this.stateKey = json.state_key;
|
||||
this.type = json.type;
|
||||
|
|
2
dist/src/event.js.map
vendored
2
dist/src/event.js.map
vendored
|
@ -1 +1 @@
|
|||
{"version":3,"file":"event.js","sourceRoot":"","sources":["../../src/event.ts"],"names":[],"mappings":"AAIA,MAAM,OAAO,KAAK;IAShB,YACS,IAAU,EACjB,IAAc;QADd;;;;mBAAO,IAAI;WAAM;QATZ;;;;mBAAiB,IAAI,CAAC,IAAI,CAAC,MAAM;WAAC;QAClC;;;;;WAAa;QACb;;;;;WAAY;QACZ;;;;;WAAe;QACf;;;;;WAAkB;QAClB;;;;;WAAa;QACb;;;;;WAAc;QAMnB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;QAC5B,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAC;QACxB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC1B,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC;QAC/B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC;IAChC,CAAC;IAED,WAAW;QACT,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,CAAA;IACjC,CAAC;IAED,aAAa;IAEb,CAAC;CACF;AAED,MAAM,OAAO,UAAW,SAAQ,KAAK;IAGnC,YAAY,IAAU,EAAE,IAAmB;QACzC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAHb;;;;;WAAiB;QAItB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC;IACjC,CAAC;CACF"}
|
||||
{"version":3,"file":"event.js","sourceRoot":"","sources":["../../src/event.ts"],"names":[],"mappings":"AAIA,MAAM,OAAO,KAAK;IAUhB,mDAAmD;IACnD,kEAAkE;IAClE,oCAAoC;IACpC,kCAAkC;IAElC,YACS,IAAU,EACjB,IAAc;QADd;;;;mBAAO,IAAI;WAAM;QAfZ;;;;mBAAiB,IAAI,CAAC,IAAI,CAAC,MAAM;WAAC;QAClC;;;;;WAAa;QACb;;;;;WAAY;QACZ;;;;;WAAiB;QACjB;;;;;WAAe;QACf;;;;;WAAkB;QAClB;;;;;WAAa;QACb;;;;;WAAc;QAWnB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;QAC5B,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAC;QACxB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,gBAAgB,CAAC;QACtC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC1B,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC;QAC/B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC;IAChC,CAAC;IAED,WAAW;QACT,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,CAAA;IACjC,CAAC;IAED,aAAa;IAEb,CAAC;CACF;AAED,MAAM,OAAO,UAAW,SAAQ,KAAK;IAGnC,YAAY,IAAU,EAAE,IAAmB;QACzC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAHb;;;;;WAAiB;QAItB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC;IACjC,CAAC;CACF"}
|
1
dist/src/index.d.ts
vendored
1
dist/src/index.d.ts
vendored
|
@ -1,4 +1,5 @@
|
|||
export { Client } from "./client.js";
|
||||
export { ThreadPaginator } from "./room.js";
|
||||
export type { Event, StateEvent } from "./event.js";
|
||||
export type { Room } from "./room.js";
|
||||
export type { Thread } from "./thread.js";
|
||||
|
|
1
dist/src/index.js
vendored
1
dist/src/index.js
vendored
|
@ -1,2 +1,3 @@
|
|||
export { Client } from "./client.js";
|
||||
export { ThreadPaginator } from "./room.js";
|
||||
//# sourceMappingURL=index.js.map
|
2
dist/src/index.js.map
vendored
2
dist/src/index.js.map
vendored
|
@ -1 +1 @@
|
|||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC"}
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC"}
|
2
dist/src/net.js
vendored
2
dist/src/net.js
vendored
|
@ -134,7 +134,7 @@ export class Network {
|
|||
fetchRelations(roomId, eventId, opts) {
|
||||
if (opts.eventType && !opts.relType)
|
||||
throw new Error("you can't set eventType without relType");
|
||||
let path = `/_matrix/client/v3/rooms/${e(roomId)}/relations/${e(eventId)}`;
|
||||
let path = `/_matrix/client/v1/rooms/${e(roomId)}/relations/${e(eventId)}`;
|
||||
if (opts.relType)
|
||||
path += `/${e(opts.relType)}`;
|
||||
if (opts.eventType)
|
||||
|
|
13
dist/src/room.d.ts
vendored
13
dist/src/room.d.ts
vendored
|
@ -1,5 +1,5 @@
|
|||
import TypedEmitter from "typed-emitter";
|
||||
import { ApiEphemeralEvent, EventId, SyncResponseRoom, Unreads } from "./api.js";
|
||||
import { ApiEphemeralEvent, EventId, IncludeThreads, SyncResponseRoom, Unreads } from "./api.js";
|
||||
import { Client } from "./client.js";
|
||||
import { Event, StateEvent } from "./event.js";
|
||||
import { TimelineSet } from "./timeline.js";
|
||||
|
@ -32,4 +32,15 @@ export declare class Room extends Room_base {
|
|||
leave(reason?: string): Promise<void>;
|
||||
ack(eventId?: EventId): Promise<void>;
|
||||
}
|
||||
export declare class ThreadPaginator extends Map<EventId, Thread> {
|
||||
client: Client;
|
||||
rooms: Array<Room>;
|
||||
watching: boolean;
|
||||
include: Array<IncludeThreads>;
|
||||
list: Array<Thread>;
|
||||
private prevBatch;
|
||||
private isAtEnd;
|
||||
constructor(client: Client, rooms: Array<Room>, watching: boolean, include: Array<IncludeThreads>);
|
||||
paginate(limit?: number): Promise<boolean>;
|
||||
}
|
||||
export {};
|
||||
|
|
97
dist/src/room.js
vendored
97
dist/src/room.js
vendored
|
@ -58,8 +58,8 @@ export class Room extends EventEmitter {
|
|||
value: new Map()
|
||||
});
|
||||
this.timelines = new TimelineSet(this);
|
||||
this.timelines.live.prevBatch = data.prev_batch || null;
|
||||
this.threads = new Threads(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
|
||||
|
@ -75,8 +75,12 @@ export class Room extends EventEmitter {
|
|||
}
|
||||
}
|
||||
if (data.timeline?.length) {
|
||||
const events = data.timeline.map(raw => new Event(this, raw));
|
||||
const events = data.timeline
|
||||
.filter(raw => !this.events.has(raw.event_id))
|
||||
.map(raw => new Event(this, raw));
|
||||
this.timelines._appendEvents(events);
|
||||
for (const ev of events)
|
||||
this.emit("timeline", ev);
|
||||
}
|
||||
}
|
||||
getState(type, stateKey = "") {
|
||||
|
@ -113,7 +117,8 @@ export class Room extends EventEmitter {
|
|||
});
|
||||
}
|
||||
}
|
||||
class Threads extends Map {
|
||||
// TODO: threads across multiple rooms
|
||||
class RoomThreads extends Map {
|
||||
constructor(room) {
|
||||
super();
|
||||
Object.defineProperty(this, "room", {
|
||||
|
@ -133,10 +138,86 @@ class Threads extends Map {
|
|||
}
|
||||
async paginate(opts) {
|
||||
const data = await this.room.client.net.fetchThreads({ ...opts, roomIds: [this.room.id] });
|
||||
return {
|
||||
threads: data.chunk?.map(raw => new Thread(new Event(this.room, raw))),
|
||||
next: data.next_batch
|
||||
};
|
||||
const threads = data.chunk?.map(raw => new Thread(new Event(this.room, raw))) || [];
|
||||
for (const th of threads)
|
||||
this.set(th.id, th);
|
||||
return { threads, nextBatch: data.next_batch };
|
||||
}
|
||||
}
|
||||
export class ThreadPaginator extends Map {
|
||||
constructor(client,
|
||||
// i need the user to pass a room, not id, so that its guaranteed to exist on the client
|
||||
rooms, watching, include) {
|
||||
super();
|
||||
Object.defineProperty(this, "client", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: client
|
||||
});
|
||||
Object.defineProperty(this, "rooms", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: rooms
|
||||
});
|
||||
Object.defineProperty(this, "watching", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: watching
|
||||
});
|
||||
Object.defineProperty(this, "include", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: include
|
||||
});
|
||||
Object.defineProperty(this, "list", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: []
|
||||
});
|
||||
Object.defineProperty(this, "prevBatch", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: void 0
|
||||
});
|
||||
Object.defineProperty(this, "isAtEnd", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: false
|
||||
});
|
||||
}
|
||||
async paginate(limit = 10) {
|
||||
if (this.isAtEnd)
|
||||
return false;
|
||||
const data = await this.client.net.fetchThreads({
|
||||
roomIds: this.rooms.map(i => i.id),
|
||||
watching: this.watching,
|
||||
include: this.include,
|
||||
from: this.prevBatch,
|
||||
limit,
|
||||
});
|
||||
for (const raw of data.chunk ?? []) {
|
||||
const room = this.client.rooms.get(raw.room_id);
|
||||
const roomId = raw.room_id;
|
||||
const existing = room.threads.get(roomId);
|
||||
if (existing) {
|
||||
this.set(existing.id, existing);
|
||||
this.list.unshift(existing);
|
||||
}
|
||||
else {
|
||||
const thread = new Thread(new Event(room, raw));
|
||||
this.set(thread.id, thread);
|
||||
room.threads.set(thread.id, thread);
|
||||
this.list.unshift(thread);
|
||||
}
|
||||
}
|
||||
return !!data.next_batch;
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=room.js.map
|
2
dist/src/room.js.map
vendored
2
dist/src/room.js.map
vendored
File diff suppressed because one or more lines are too long
1
dist/src/thread.d.ts
vendored
1
dist/src/thread.d.ts
vendored
|
@ -12,6 +12,7 @@ export declare class Thread extends Thread_base {
|
|||
baseEvent: Event;
|
||||
room: Room;
|
||||
timeline: ThreadTimeline;
|
||||
id: string;
|
||||
constructor(baseEvent: Event);
|
||||
ack(eventId?: EventId): Promise<void>;
|
||||
}
|
||||
|
|
6
dist/src/thread.js
vendored
6
dist/src/thread.js
vendored
|
@ -21,6 +21,12 @@ export class Thread extends EventEmitter {
|
|||
writable: true,
|
||||
value: new ThreadTimeline(this)
|
||||
});
|
||||
Object.defineProperty(this, "id", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: this.baseEvent.id
|
||||
});
|
||||
}
|
||||
// // TODO: local echo(?), return event
|
||||
// async sendEvent(type: string, content: any) {
|
||||
|
|
2
dist/src/thread.js.map
vendored
2
dist/src/thread.js.map
vendored
|
@ -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;IAI3F,YAAmB,SAAgB;QACjC,KAAK,EAAE,CAAC;QADE;;;;mBAAO,SAAS;WAAO;QAH5B;;;;mBAAa,IAAI,CAAC,SAAS,CAAC,IAAI;WAAC;QACjC;;;;mBAA2B,IAAI,cAAc,CAAC,IAAI,CAAC;WAAC;IAI3D,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;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"}
|
14
dist/src/timeline.d.ts
vendored
14
dist/src/timeline.d.ts
vendored
|
@ -4,6 +4,8 @@ import { Event } from "./event.js";
|
|||
import { Thread } from "./thread.js";
|
||||
export interface Timeline {
|
||||
isLive: boolean;
|
||||
isAtBeginning: boolean;
|
||||
isAtEnd: boolean;
|
||||
events: Array<Event>;
|
||||
paginate(dir: "f" | "b", limit: number): Promise<boolean>;
|
||||
}
|
||||
|
@ -11,8 +13,10 @@ export declare class RoomTimeline implements Timeline {
|
|||
room: Room;
|
||||
events: Array<Event>;
|
||||
isLive: boolean;
|
||||
prevBatch: string | null | undefined;
|
||||
nextBatch: string | null | undefined;
|
||||
isAtBeginning: boolean;
|
||||
isAtEnd: boolean;
|
||||
prevBatch: string | undefined;
|
||||
nextBatch: string | undefined;
|
||||
constructor(room: Room);
|
||||
paginate(dir: "f" | "b", limit?: number): Promise<boolean>;
|
||||
}
|
||||
|
@ -20,8 +24,10 @@ export declare class ThreadTimeline implements Timeline {
|
|||
thread: Thread;
|
||||
events: Array<Event>;
|
||||
isLive: boolean;
|
||||
prevBatch: string | null | undefined;
|
||||
nextBatch: string | null | undefined;
|
||||
isAtBeginning: boolean;
|
||||
isAtEnd: boolean;
|
||||
prevBatch: string | undefined;
|
||||
nextBatch: string | undefined;
|
||||
constructor(thread: Thread);
|
||||
paginate(dir: "f" | "b", limit?: number): Promise<boolean>;
|
||||
}
|
||||
|
|
95
dist/src/timeline.js
vendored
95
dist/src/timeline.js
vendored
|
@ -20,21 +20,37 @@ export class RoomTimeline {
|
|||
writable: true,
|
||||
value: false
|
||||
});
|
||||
Object.defineProperty(this, "isAtBeginning", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: false
|
||||
});
|
||||
Object.defineProperty(this, "isAtEnd", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: false
|
||||
});
|
||||
Object.defineProperty(this, "prevBatch", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: null
|
||||
value: void 0
|
||||
});
|
||||
Object.defineProperty(this, "nextBatch", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: null
|
||||
value: void 0
|
||||
});
|
||||
}
|
||||
async paginate(dir, limit = 50) {
|
||||
const from = (dir === "f" ? this.nextBatch : this.prevBatch) || undefined; // FIXME: don't paginate at ends
|
||||
if (dir === "b" && this.isAtBeginning)
|
||||
return false;
|
||||
if (dir === "f" && this.isAtEnd)
|
||||
return false;
|
||||
const from = dir === "f" ? this.nextBatch : this.prevBatch;
|
||||
const data = await this.room.client.net.fetchMessages({
|
||||
roomId: this.room.id,
|
||||
dir,
|
||||
|
@ -43,14 +59,24 @@ export class RoomTimeline {
|
|||
});
|
||||
if (dir === "f") {
|
||||
const events = data.chunk.map(raw => new Event(this.room, raw));
|
||||
this.nextBatch = data.end || null;
|
||||
if (data.start) {
|
||||
this.nextBatch = data.start;
|
||||
}
|
||||
else {
|
||||
this.isAtEnd = true;
|
||||
}
|
||||
this.events.push(...events);
|
||||
for (const event of events)
|
||||
this.room.events.set(event.id, event);
|
||||
}
|
||||
else {
|
||||
const events = data.chunk.reverse().map(raw => new Event(this.room, raw));
|
||||
this.prevBatch = data.end || null;
|
||||
if (data.end) {
|
||||
this.prevBatch = data.end;
|
||||
}
|
||||
else {
|
||||
this.isAtBeginning = true;
|
||||
}
|
||||
this.events.push(...events);
|
||||
for (const event of events)
|
||||
this.room.events.set(event.id, event);
|
||||
|
@ -73,6 +99,18 @@ export class ThreadTimeline {
|
|||
value: []
|
||||
});
|
||||
Object.defineProperty(this, "isLive", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: true
|
||||
}); // FIXME: live threads
|
||||
Object.defineProperty(this, "isAtBeginning", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: false
|
||||
});
|
||||
Object.defineProperty(this, "isAtEnd", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
|
@ -82,18 +120,30 @@ export class ThreadTimeline {
|
|||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: undefined
|
||||
value: void 0
|
||||
});
|
||||
Object.defineProperty(this, "nextBatch", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: undefined
|
||||
value: void 0
|
||||
});
|
||||
}
|
||||
async paginate(dir, limit = 50) {
|
||||
if (dir === "b" && this.isAtBeginning)
|
||||
return false;
|
||||
if (dir === "f" && this.isAtEnd)
|
||||
return false;
|
||||
// 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!
|
||||
// FIXME: implement /context in threads
|
||||
if (dir === "f")
|
||||
this.isAtBeginning = true;
|
||||
if (dir === "b")
|
||||
this.isAtEnd = true;
|
||||
const { room } = this.thread;
|
||||
const from = (dir === "f" ? this.nextBatch : this.prevBatch) || undefined; // FIXME: don't paginate at ends
|
||||
const from = dir === "f" ? this.nextBatch : this.prevBatch;
|
||||
const data = await room.client.net.fetchRelations(room.id, this.thread.baseEvent.id, {
|
||||
dir,
|
||||
from,
|
||||
|
@ -101,19 +151,31 @@ export class ThreadTimeline {
|
|||
});
|
||||
if (dir === "f") {
|
||||
const events = data.chunk.map(raw => new Event(room, raw));
|
||||
this.nextBatch = data.next_batch || null;
|
||||
if (data.next_batch) {
|
||||
this.nextBatch = data.next_batch;
|
||||
}
|
||||
else {
|
||||
this.isAtEnd = true;
|
||||
}
|
||||
this.events.push(...events);
|
||||
for (const event of events)
|
||||
room.events.set(event.id, event);
|
||||
return !!data.next_batch;
|
||||
}
|
||||
else {
|
||||
// FIXME: conduit doesn't implement ?dir=b
|
||||
const events = data.chunk.map(raw => new Event(room, raw));
|
||||
this.prevBatch = data.prev_batch || null;
|
||||
if (data.prev_batch) {
|
||||
this.prevBatch = data.prev_batch;
|
||||
}
|
||||
else {
|
||||
this.isAtBeginning = true;
|
||||
}
|
||||
this.events.push(...events);
|
||||
for (const event of events)
|
||||
room.events.set(event.id, event);
|
||||
return !!data.prev_batch;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
export class TimelineSet {
|
||||
|
@ -166,13 +228,20 @@ export class TimelineSet {
|
|||
_appendEvents(events) {
|
||||
// FIXME: there should only be one live timeline for each room and
|
||||
// thread, they need to be merged together
|
||||
for (const event of events)
|
||||
for (const event of events) {
|
||||
this.room.events.set(event.id, 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);
|
||||
}
|
||||
}
|
||||
for (const timeline of this.timelines) {
|
||||
// if (timeline.isLive) timeline.events.push(...events);
|
||||
if (timeline.isLive && timeline instanceof RoomTimeline)
|
||||
timeline.events.push(...events);
|
||||
// events[0].content["m.relations"].find(rel => rel.rel_type === "m.thread")?.event_id
|
||||
// this.room.threads.get()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
2
dist/src/timeline.js.map
vendored
2
dist/src/timeline.js.map
vendored
File diff suppressed because one or more lines are too long
2
dist/tsconfig.tsbuildinfo
vendored
2
dist/tsconfig.tsbuildinfo
vendored
File diff suppressed because one or more lines are too long
|
@ -5,11 +5,12 @@ export type UserId = string;
|
|||
export type EventId = string;
|
||||
|
||||
export interface ApiEvent {
|
||||
event_id: EventId,
|
||||
sender: UserId,
|
||||
type: string,
|
||||
content: any,
|
||||
event_id: EventId,
|
||||
origin_server_ts: number,
|
||||
sender: UserId,
|
||||
state_key?: string,
|
||||
type: string,
|
||||
unsigned?: any,
|
||||
}
|
||||
|
||||
|
|
|
@ -6,17 +6,24 @@ export class Event {
|
|||
public client: Client = this.room.client;
|
||||
public content: any;
|
||||
public id: EventId;
|
||||
public originTs: number;
|
||||
public sender: UserId;
|
||||
public stateKey?: string;
|
||||
public type: string;
|
||||
public unsigned: any;
|
||||
|
||||
// How do I do the relations api? Event or EventId?
|
||||
// Event is more ergonomic, but EventId means I don't have to wait
|
||||
// public relationsFrom: Array<any>;
|
||||
// public relationsTo: Array<any>;
|
||||
|
||||
constructor(
|
||||
public room: Room,
|
||||
json: ApiEvent
|
||||
) {
|
||||
this.content = json.content;
|
||||
this.id = json.event_id;
|
||||
this.originTs = json.origin_server_ts;
|
||||
this.sender = json.sender;
|
||||
this.stateKey = json.state_key;
|
||||
this.type = json.type;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
export { Client } from "./client.js";
|
||||
export { ThreadPaginator } from "./room.js";
|
||||
export type { Event, StateEvent } from "./event.js";
|
||||
export type { Room } from "./room.js";
|
||||
export type { Thread } from "./thread.js";
|
||||
|
|
|
@ -162,7 +162,7 @@ export class Network {
|
|||
to?: string,
|
||||
}): Promise<t.RelationsResponse> {
|
||||
if (opts.eventType && !opts.relType) throw new Error("you can't set eventType without relType");
|
||||
let path = `/_matrix/client/v3/rooms/${e(roomId)}/relations/${e(eventId)}`;
|
||||
let path = `/_matrix/client/v1/rooms/${e(roomId)}/relations/${e(eventId)}`;
|
||||
if (opts.relType) path += `/${e(opts.relType)}`;
|
||||
if (opts.eventType) path += `/${e(opts.eventType)}`;
|
||||
path += `?limit=${opts.limit || 50}&dir=${opts.dir}`;
|
||||
|
|
63
src/room.ts
63
src/room.ts
|
@ -1,6 +1,6 @@
|
|||
import EventEmitter from "events";
|
||||
import TypedEmitter from "typed-emitter";
|
||||
import { ApiEphemeralEvent, EventId, IncludeThreads, SyncResponseRoom, Unreads } from "./api.js";
|
||||
import { ApiEphemeralEvent, EventId, IncludeThreads, RoomId, SyncResponseRoom, Unreads } from "./api.js";
|
||||
import { Client } from "./client.js";
|
||||
import { Event, StateEvent } from "./event.js";
|
||||
import { TimelineSet } from "./timeline.js";
|
||||
|
@ -57,8 +57,8 @@ room.unban(userid)
|
|||
) {
|
||||
super();
|
||||
this.timelines = new TimelineSet(this);
|
||||
this.timelines.live.prevBatch = data.prev_batch || null;
|
||||
this.threads = new Threads(this);
|
||||
this.timelines.live.prevBatch = data.prev_batch;
|
||||
this.threads = new RoomThreads(this);
|
||||
this._merge(data);
|
||||
}
|
||||
|
||||
|
@ -74,8 +74,11 @@ room.unban(userid)
|
|||
}
|
||||
}
|
||||
if (data.timeline?.length) {
|
||||
const events = data.timeline.map(raw => new Event(this, raw));
|
||||
const events = data.timeline
|
||||
.filter(raw => !this.events.has(raw.event_id))
|
||||
.map(raw => new Event(this, raw));
|
||||
this.timelines._appendEvents(events);
|
||||
for (const ev of events) this.emit("timeline", ev);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -120,7 +123,8 @@ room.unban(userid)
|
|||
}
|
||||
}
|
||||
|
||||
class Threads extends Map<EventId, Thread> {
|
||||
// TODO: threads across multiple rooms
|
||||
class RoomThreads extends Map<EventId, Thread> {
|
||||
constructor(public room: Room) {
|
||||
super();
|
||||
}
|
||||
|
@ -135,9 +139,50 @@ class Threads extends Map<EventId, Thread> {
|
|||
|
||||
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] });
|
||||
return {
|
||||
threads: data.chunk?.map(raw => new Thread(new Event(this.room, raw))),
|
||||
next: data.next_batch
|
||||
}
|
||||
const threads = data.chunk?.map(raw => new Thread(new Event(this.room, raw))) || [];
|
||||
for (const th of threads) this.set(th.id, th);
|
||||
return { threads, nextBatch: data.next_batch };
|
||||
}
|
||||
}
|
||||
|
||||
export class ThreadPaginator extends Map<EventId, Thread> {
|
||||
public list: Array<Thread> = [];
|
||||
private prevBatch: string | undefined;
|
||||
private isAtEnd = false;
|
||||
|
||||
constructor(
|
||||
public client: Client,
|
||||
// i need the user to pass a room, not id, so that its guaranteed to exist on the client
|
||||
public rooms: Array<Room>,
|
||||
public watching: boolean,
|
||||
public include: Array<IncludeThreads>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async paginate(limit = 10): Promise<boolean> {
|
||||
if (this.isAtEnd) return false;
|
||||
const data = await this.client.net.fetchThreads({
|
||||
roomIds: this.rooms.map(i => i.id),
|
||||
watching: this.watching,
|
||||
include: this.include,
|
||||
from: this.prevBatch,
|
||||
limit,
|
||||
});
|
||||
for (const raw of data.chunk ?? []) {
|
||||
const room = this.client.rooms.get((raw as any).room_id)!;
|
||||
const roomId: RoomId = (raw as any).room_id;
|
||||
const existing = room.threads.get(roomId);
|
||||
if (existing) {
|
||||
this.set(existing.id, existing);
|
||||
this.list.unshift(existing);
|
||||
} else {
|
||||
const thread = new Thread(new Event(room, raw));
|
||||
this.set(thread.id, thread);
|
||||
room.threads.set(thread.id, thread);
|
||||
this.list.unshift(thread);
|
||||
}
|
||||
}
|
||||
return !!data.next_batch;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ 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;
|
||||
|
||||
constructor(public baseEvent: Event) {
|
||||
super();
|
||||
|
|
|
@ -10,6 +10,12 @@ export interface Timeline {
|
|||
// if this timeline is actively receiving new events
|
||||
isLive: boolean,
|
||||
|
||||
// if this timeline has reached the beginning (earliest event)
|
||||
isAtBeginning: boolean,
|
||||
|
||||
// if this timeline has reached the end (latest event)
|
||||
isAtEnd: boolean,
|
||||
|
||||
// isPaginating: boolean,
|
||||
|
||||
// the events in this timeline
|
||||
|
@ -24,14 +30,19 @@ export interface Timeline {
|
|||
export class RoomTimeline implements Timeline {
|
||||
public events: Array<Event> = [];
|
||||
public isLive: boolean = false;
|
||||
public isAtBeginning: boolean = false;
|
||||
public isAtEnd: boolean = false;
|
||||
|
||||
prevBatch: string | null | undefined = null;
|
||||
nextBatch: string | null | undefined = null;
|
||||
prevBatch: string | undefined;
|
||||
nextBatch: string | undefined;
|
||||
|
||||
constructor(public room: Room) {}
|
||||
|
||||
public async paginate(dir: "f" | "b", limit: number = 50): Promise<boolean> {
|
||||
const from = (dir === "f" ? this.nextBatch : this.prevBatch) || undefined; // FIXME: don't paginate at ends
|
||||
if (dir === "b" && this.isAtBeginning) return false;
|
||||
if (dir === "f" && this.isAtEnd) return false;
|
||||
|
||||
const from = dir === "f" ? this.nextBatch : this.prevBatch;
|
||||
const data = await this.room.client.net.fetchMessages({
|
||||
roomId: this.room.id,
|
||||
dir,
|
||||
|
@ -40,12 +51,20 @@ export class RoomTimeline implements Timeline {
|
|||
});
|
||||
if (dir === "f") {
|
||||
const events = data.chunk.map(raw => new Event(this.room, raw));
|
||||
this.nextBatch = data.end || null;
|
||||
if (data.start) {
|
||||
this.nextBatch = data.start;
|
||||
} else {
|
||||
this.isAtEnd = true;
|
||||
}
|
||||
this.events.push(...events);
|
||||
for (const event of events) this.room.events.set(event.id, event);
|
||||
} else {
|
||||
const events = data.chunk.reverse().map(raw => new Event(this.room, raw));
|
||||
this.prevBatch = data.end || null;
|
||||
if (data.end) {
|
||||
this.prevBatch = data.end;
|
||||
} else {
|
||||
this.isAtBeginning = true;
|
||||
}
|
||||
this.events.push(...events);
|
||||
for (const event of events) this.room.events.set(event.id, event);
|
||||
}
|
||||
|
@ -62,16 +81,28 @@ export class RoomTimeline implements Timeline {
|
|||
|
||||
export class ThreadTimeline implements Timeline {
|
||||
public events: Array<Event> = [];
|
||||
public isLive: boolean = false;
|
||||
public isLive: boolean = true; // FIXME: live threads
|
||||
public isAtBeginning: boolean = false;
|
||||
public isAtEnd: boolean = false;
|
||||
|
||||
prevBatch: string | null | undefined = undefined;
|
||||
nextBatch: string | null | undefined = undefined;
|
||||
prevBatch: string | undefined;
|
||||
nextBatch: string | undefined;
|
||||
|
||||
constructor(public thread: Thread) {}
|
||||
|
||||
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;
|
||||
|
||||
// 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!
|
||||
// FIXME: implement /context in threads
|
||||
if (dir === "f") this.isAtBeginning = true;
|
||||
if (dir === "b") this.isAtEnd = true;
|
||||
|
||||
const { room } = this.thread;
|
||||
const from = (dir === "f" ? this.nextBatch : this.prevBatch) || undefined; // FIXME: don't paginate at ends
|
||||
const from = dir === "f" ? this.nextBatch : this.prevBatch;
|
||||
const data = await room.client.net.fetchRelations(room.id, this.thread.baseEvent.id, {
|
||||
dir,
|
||||
from,
|
||||
|
@ -79,16 +110,26 @@ export class ThreadTimeline implements Timeline {
|
|||
});
|
||||
if (dir === "f") {
|
||||
const events = data.chunk.map(raw => new Event(room, raw));
|
||||
this.nextBatch = data.next_batch || null;
|
||||
if (data.next_batch) {
|
||||
this.nextBatch = data.next_batch;
|
||||
} else {
|
||||
this.isAtEnd = true;
|
||||
}
|
||||
this.events.push(...events);
|
||||
for (const event of events) room.events.set(event.id, event);
|
||||
return !!data.next_batch;
|
||||
} else {
|
||||
// FIXME: conduit doesn't implement ?dir=b
|
||||
const events = data.chunk.map(raw => new Event(room, raw));
|
||||
this.prevBatch = data.prev_batch || null;
|
||||
if (data.prev_batch) {
|
||||
this.prevBatch = data.prev_batch;
|
||||
} else {
|
||||
this.isAtBeginning = true;
|
||||
}
|
||||
this.events.push(...events);
|
||||
for (const event of events) room.events.set(event.id, event);
|
||||
return !!data.prev_batch;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -130,11 +171,19 @@ export class TimelineSet {
|
|||
_appendEvents(events: Array<Event>) {
|
||||
// FIXME: there should only be one live timeline for each room and
|
||||
// thread, they need to be merged together
|
||||
for (const event of events) this.room.events.set(event.id, event);
|
||||
for (const event of events) {
|
||||
this.room.events.set(event.id, 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);
|
||||
}
|
||||
}
|
||||
|
||||
for (const timeline of this.timelines) {
|
||||
// if (timeline.isLive) timeline.events.push(...events);
|
||||
if (timeline.isLive && timeline instanceof RoomTimeline) timeline.events.push(...events);
|
||||
// events[0].content["m.relations"].find(rel => rel.rel_type === "m.thread")?.event_id
|
||||
// this.room.threads.get()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue