Skip to content

Commit 66b9884

Browse files
author
Germain
authored
Refactor thread model to be created from the root event (#2142)
1 parent d03db00 commit 66b9884

File tree

4 files changed

+159
-79
lines changed

4 files changed

+159
-79
lines changed

src/@types/requests.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { IContent, IEvent } from "../models/event";
1919
import { Preset, Visibility } from "./partials";
2020
import { SearchKey } from "./search";
2121
import { IRoomEventFilter } from "../filter";
22+
import { Direction } from "../models/event-timeline";
2223

2324
// allow camelcase as these are things that go onto the wire
2425
/* eslint-disable camelcase */
@@ -144,6 +145,7 @@ export interface IRelationsRequestOpts {
144145
from?: string;
145146
to?: string;
146147
limit?: number;
148+
direction?: Direction;
147149
}
148150

149151
export interface IRelationsResponse {

src/models/event.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ export class MatrixEvent extends EventEmitter {
277277
* it to us and the time we're now constructing this event, but that's better
278278
* than assuming the local clock is in sync with the origin HS's clock.
279279
*/
280-
private readonly localTimestamp: number;
280+
public readonly localTimestamp: number;
281281

282282
// XXX: these should be read-only
283283
public sender: RoomMember = null;

src/models/room.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1377,8 +1377,7 @@ export class Room extends EventEmitter {
13771377
} else {
13781378
rootEvent.setUnsigned(eventData.unsigned);
13791379
}
1380-
events.unshift(rootEvent);
1381-
thread = this.createThread(events);
1380+
thread = this.createThread(rootEvent, events);
13821381
}
13831382

13841383
if (event.getUnsigned().transaction_id) {
@@ -1393,8 +1392,12 @@ export class Room extends EventEmitter {
13931392
this.emit(ThreadEvent.Update, thread);
13941393
}
13951394

1396-
public createThread(events: MatrixEvent[]): Thread {
1397-
const thread = new Thread(events, this, this.client);
1395+
public createThread(rootEvent: MatrixEvent, events?: MatrixEvent[]): Thread {
1396+
const thread = new Thread(rootEvent, {
1397+
initialEvents: events,
1398+
room: this,
1399+
client: this.client,
1400+
});
13981401
this.threads.set(thread.id, thread);
13991402
this.reEmitter.reEmit(thread, [
14001403
ThreadEvent.Update,

src/models/thread.ts

Lines changed: 149 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@ limitations under the License.
1717
import { MatrixClient } from "../matrix";
1818
import { ReEmitter } from "../ReEmitter";
1919
import { RelationType } from "../@types/event";
20+
import { IRelationsRequestOpts } from "../@types/requests";
2021
import { MatrixEvent, IThreadBundledRelationship } from "./event";
21-
import { EventTimeline } from "./event-timeline";
22+
import { Direction, EventTimeline } from "./event-timeline";
2223
import { EventTimelineSet } from './event-timeline-set';
2324
import { Room } from './room';
2425
import { TypedEventEmitter } from "./typed-event-emitter";
26+
import { RoomState } from "./room-state";
2527

2628
export enum ThreadEvent {
2729
New = "Thread.new",
@@ -31,14 +33,16 @@ export enum ThreadEvent {
3133
ViewThread = "Thred.viewThread",
3234
}
3335

36+
interface IThreadOpts {
37+
initialEvents?: MatrixEvent[];
38+
room: Room;
39+
client: MatrixClient;
40+
}
41+
3442
/**
3543
* @experimental
3644
*/
3745
export class Thread extends TypedEventEmitter<ThreadEvent> {
38-
/**
39-
* A reference to the event ID at the top of the thread
40-
*/
41-
private root: string;
4246
/**
4347
* A reference to all the events ID at the bottom of the threads
4448
*/
@@ -51,33 +55,37 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
5155
private lastEvent: MatrixEvent;
5256
private replyCount = 0;
5357

58+
public readonly room: Room;
59+
public readonly client: MatrixClient;
60+
61+
public initialEventsFetched = false;
62+
5463
constructor(
55-
events: MatrixEvent[] = [],
56-
public readonly room: Room,
57-
public readonly client: MatrixClient,
64+
public readonly rootEvent: MatrixEvent,
65+
opts: IThreadOpts,
5866
) {
5967
super();
60-
if (events.length === 0) {
61-
throw new Error("Can't create an empty thread");
62-
}
63-
64-
this.reEmitter = new ReEmitter(this);
6568

69+
this.room = opts.room;
70+
this.client = opts.client;
6671
this.timelineSet = new EventTimelineSet(this.room, {
6772
unstableClientRelationAggregation: true,
6873
timelineSupport: true,
6974
pendingEvents: true,
7075
});
76+
this.reEmitter = new ReEmitter(this);
77+
78+
this.initialiseThread(this.rootEvent);
7179

7280
this.reEmitter.reEmit(this.timelineSet, [
7381
"Room.timeline",
7482
"Room.timelineReset",
7583
]);
7684

77-
events.forEach(event => this.addEvent(event));
85+
opts?.initialEvents.forEach(event => this.addEvent(event));
7886

79-
room.on("Room.localEchoUpdated", this.onEcho);
80-
room.on("Room.timeline", this.onEcho);
87+
this.room.on("Room.localEchoUpdated", this.onEcho);
88+
this.room.on("Room.timeline", this.onEcho);
8189
}
8290

8391
public get hasServerSideSupport(): boolean {
@@ -91,85 +99,115 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
9199
}
92100
};
93101

102+
private get roomState(): RoomState {
103+
return this.room.getLiveTimeline().getState(EventTimeline.FORWARDS);
104+
}
105+
94106
/**
95107
* Add an event to the thread and updates
96108
* the tail/root references if needed
97109
* Will fire "Thread.update"
98110
* @param event The event to add
99111
*/
100112
public async addEvent(event: MatrixEvent, toStartOfTimeline = false): Promise<void> {
101-
if (this.timelineSet.findEventById(event.getId())) {
102-
return;
113+
// Add all incoming events to the thread's timeline set when there's
114+
// no server support
115+
if (!this.hasServerSideSupport) {
116+
if (this.timelineSet.findEventById(event.getId())) {
117+
return;
118+
}
119+
120+
// all the relevant membership info to hydrate events with a sender
121+
// is held in the main room timeline
122+
// We want to fetch the room state from there and pass it down to this thread
123+
// timeline set to let it reconcile an event with its relevant RoomMember
124+
125+
event.setThread(this);
126+
this.timelineSet.addEventToTimeline(
127+
event,
128+
this.liveTimeline,
129+
toStartOfTimeline,
130+
false,
131+
this.roomState,
132+
);
133+
134+
await this.client.decryptEventIfNeeded(event, {});
103135
}
104136

105-
if (!this.root) {
106-
if (event.isThreadRelation) {
107-
this.root = event.threadRootId;
108-
} else {
109-
this.root = event.getId();
137+
if (this.hasServerSideSupport && this.initialEventsFetched) {
138+
if (event.localTimestamp > this.lastReply().localTimestamp && !this.findEventById(event.getId())) {
139+
this.timelineSet.addEventToTimeline(
140+
event,
141+
this.liveTimeline,
142+
false,
143+
false,
144+
this.roomState,
145+
);
110146
}
111147
}
112148

113-
// all the relevant membership info to hydrate events with a sender
114-
// is held in the main room timeline
115-
// We want to fetch the room state from there and pass it down to this thread
116-
// timeline set to let it reconcile an event with its relevant RoomMember
117-
const roomState = this.room.getLiveTimeline().getState(EventTimeline.FORWARDS);
118-
119-
event.setThread(this);
120-
this.timelineSet.addEventToTimeline(
121-
event,
122-
this.timelineSet.getLiveTimeline(),
123-
toStartOfTimeline,
124-
false,
125-
roomState,
126-
);
127-
128149
if (!this._currentUserParticipated && event.getSender() === this.client.getUserId()) {
129150
this._currentUserParticipated = true;
130151
}
131152

132-
await this.client.decryptEventIfNeeded(event, {});
133-
134153
const isThreadReply = event.getRelation()?.rel_type === RelationType.Thread;
135154
// If no thread support exists we want to count all thread relation
136155
// added as a reply. We can't rely on the bundled relationships count
137156
if (!this.hasServerSideSupport && isThreadReply) {
138157
this.replyCount++;
139158
}
140159

141-
if (!this.lastEvent || (isThreadReply && event.getTs() > this.lastEvent.getTs())) {
160+
// There is a risk that the `localTimestamp` approximation will not be accurate
161+
// when threads are used over federation. That could results in the reply
162+
// count value drifting away from the value returned by the server
163+
if (!this.lastEvent || (isThreadReply && event.localTimestamp > this.replyToEvent.localTimestamp)) {
142164
this.lastEvent = event;
143-
if (this.lastEvent.getId() !== this.root) {
165+
if (this.lastEvent.getId() !== this.id) {
144166
// This counting only works when server side support is enabled
145167
// as we started the counting from the value returned in the
146168
// bundled relationship
147169
if (this.hasServerSideSupport) {
148170
this.replyCount++;
149171
}
172+
150173
this.emit(ThreadEvent.NewReply, this, event);
151174
}
152175
}
153176

154-
if (event.getId() === this.root) {
155-
const bundledRelationship = event
156-
.getServerAggregatedRelation<IThreadBundledRelationship>(RelationType.Thread);
177+
this.emit(ThreadEvent.Update, this);
178+
}
157179

158-
if (this.hasServerSideSupport && bundledRelationship) {
159-
this.replyCount = bundledRelationship.count;
160-
this._currentUserParticipated = bundledRelationship.current_user_participated;
180+
private initialiseThread(rootEvent: MatrixEvent): void {
181+
const bundledRelationship = rootEvent
182+
.getServerAggregatedRelation<IThreadBundledRelationship>(RelationType.Thread);
161183

162-
const lastReply = this.findEventById(bundledRelationship.latest_event.event_id);
163-
if (lastReply) {
164-
this.lastEvent = lastReply;
165-
} else {
166-
const event = new MatrixEvent(bundledRelationship.latest_event);
167-
this.lastEvent = event;
168-
}
169-
}
184+
if (this.hasServerSideSupport && bundledRelationship) {
185+
this.replyCount = bundledRelationship.count;
186+
this._currentUserParticipated = bundledRelationship.current_user_participated;
187+
188+
const event = new MatrixEvent(bundledRelationship.latest_event);
189+
this.setEventMetadata(event);
190+
this.lastEvent = event;
170191
}
171192

172-
this.emit(ThreadEvent.Update, this);
193+
if (!bundledRelationship) {
194+
this.addEvent(rootEvent);
195+
}
196+
}
197+
198+
public async fetchInitialEvents(): Promise<boolean> {
199+
try {
200+
await this.fetchEvents();
201+
this.initialEventsFetched = true;
202+
return true;
203+
} catch (e) {
204+
return false;
205+
}
206+
}
207+
208+
private setEventMetadata(event: MatrixEvent): void {
209+
EventTimeline.setEventMetadata(event, this.roomState, false);
210+
event.setThread(this);
173211
}
174212

175213
/**
@@ -185,7 +223,7 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
185223
public lastReply(matches: (ev: MatrixEvent) => boolean = () => true): MatrixEvent {
186224
for (let i = this.events.length - 1; i >= 0; i--) {
187225
const event = this.events[i];
188-
if (event.isThreadRelation && matches(event)) {
226+
if (matches(event)) {
189227
return event;
190228
}
191229
}
@@ -195,14 +233,7 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
195233
* The thread ID, which is the same as the root event ID
196234
*/
197235
public get id(): string {
198-
return this.root;
199-
}
200-
201-
/**
202-
* The thread root event
203-
*/
204-
public get rootEvent(): MatrixEvent {
205-
return this.findEventById(this.root);
236+
return this.rootEvent.getId();
206237
}
207238

208239
public get roomId(): string {
@@ -226,14 +257,7 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
226257
}
227258

228259
public get events(): MatrixEvent[] {
229-
return this.timelineSet.getLiveTimeline().getEvents();
230-
}
231-
232-
public merge(thread: Thread): void {
233-
thread.events.forEach(event => {
234-
this.addEvent(event);
235-
});
236-
this.events.forEach(event => event.setThread(this));
260+
return this.liveTimeline.getEvents();
237261
}
238262

239263
public has(eventId: string): boolean {
@@ -243,4 +267,55 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
243267
public get hasCurrentUserParticipated(): boolean {
244268
return this._currentUserParticipated;
245269
}
270+
271+
public get liveTimeline(): EventTimeline {
272+
return this.timelineSet.getLiveTimeline();
273+
}
274+
275+
public async fetchEvents(opts: IRelationsRequestOpts = { limit: 20 }): Promise<{
276+
originalEvent: MatrixEvent;
277+
events: MatrixEvent[];
278+
nextBatch?: string;
279+
prevBatch?: string;
280+
}> {
281+
let {
282+
originalEvent,
283+
events,
284+
prevBatch,
285+
nextBatch,
286+
} = await this.client.relations(
287+
this.room.roomId,
288+
this.id,
289+
RelationType.Thread,
290+
null,
291+
opts,
292+
);
293+
294+
// When there's no nextBatch returned with a `from` request we have reached
295+
// the end of the thread, and therefore want to return an empty one
296+
if (!opts.to && !nextBatch) {
297+
events = [originalEvent, ...events];
298+
}
299+
300+
for (const event of events) {
301+
await this.client.decryptEventIfNeeded(event);
302+
this.setEventMetadata(event);
303+
}
304+
305+
const prependEvents = !opts.direction || opts.direction === Direction.Backward;
306+
307+
this.timelineSet.addEventsToTimeline(
308+
events,
309+
prependEvents,
310+
this.liveTimeline,
311+
prependEvents ? nextBatch : prevBatch,
312+
);
313+
314+
return {
315+
originalEvent,
316+
events,
317+
prevBatch,
318+
nextBatch,
319+
};
320+
}
246321
}

0 commit comments

Comments
 (0)