Skip to content

Commit 02ba233

Browse files
author
Germain
authored
Improved threads reliability with/without server side support (#2132)
1 parent 50493a3 commit 02ba233

File tree

7 files changed

+116
-52
lines changed

7 files changed

+116
-52
lines changed

src/client.ts

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -811,7 +811,7 @@ export class MatrixClient extends EventEmitter {
811811
// TODO: This should expire: https://github.com/matrix-org/matrix-js-sdk/issues/1020
812812
protected serverVersionsPromise: Promise<IServerVersions>;
813813

814-
protected cachedCapabilities: {
814+
public cachedCapabilities: {
815815
capabilities: ICapabilities;
816816
expiration: number;
817817
};
@@ -5055,7 +5055,7 @@ export class MatrixClient extends EventEmitter {
50555055
limit,
50565056
Direction.Backward,
50575057
);
5058-
}).then((res: IMessagesResponse) => {
5058+
}).then(async (res: IMessagesResponse) => {
50595059
const matrixEvents = res.chunk.map(this.getEventMapper());
50605060
if (res.state) {
50615061
const stateEvents = res.state.map(this.getEventMapper());
@@ -5065,7 +5065,7 @@ export class MatrixClient extends EventEmitter {
50655065
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(matrixEvents);
50665066

50675067
room.addEventsToTimeline(timelineEvents, true, room.getLiveTimeline());
5068-
this.processThreadEvents(room, threadedEvents);
5068+
await this.processThreadEvents(room, threadedEvents);
50695069

50705070
room.oldState.paginationToken = res.end;
50715071
if (res.chunk.length === 0) {
@@ -5143,7 +5143,7 @@ export class MatrixClient extends EventEmitter {
51435143

51445144
// TODO: we should implement a backoff (as per scrollback()) to deal more
51455145
// nicely with HTTP errors.
5146-
const promise = this.http.authedRequest<any>(undefined, Method.Get, path, params).then((res) => { // TODO types
5146+
const promise = this.http.authedRequest<any>(undefined, Method.Get, path, params).then(async (res) => { // TODO types
51475147
if (!res.event) {
51485148
throw new Error("'event' not in '/context' result - homeserver too old?");
51495149
}
@@ -5176,7 +5176,7 @@ export class MatrixClient extends EventEmitter {
51765176
const [timelineEvents, threadedEvents] = this.partitionThreadedEvents(matrixEvents);
51775177

51785178
timelineSet.addEventsToTimeline(timelineEvents, true, timeline, res.start);
5179-
this.processThreadEvents(timelineSet.room, threadedEvents);
5179+
await this.processThreadEvents(timelineSet.room, threadedEvents);
51805180

51815181
// there is no guarantee that the event ended up in "timeline" (we
51825182
// might have switched to a neighbouring timeline) - so check the
@@ -5291,7 +5291,7 @@ export class MatrixClient extends EventEmitter {
52915291

52925292
promise = this.http.authedRequest<any>( // TODO types
52935293
undefined, Method.Get, path, params, undefined,
5294-
).then((res) => {
5294+
).then(async (res) => {
52955295
const token = res.next_token;
52965296
const matrixEvents = [];
52975297

@@ -5309,7 +5309,7 @@ export class MatrixClient extends EventEmitter {
53095309

53105310
const timelineSet = eventTimeline.getTimelineSet();
53115311
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
5312-
this.processThreadEvents(timelineSet.room, threadedEvents);
5312+
await this.processThreadEvents(timelineSet.room, threadedEvents);
53135313

53145314
// if we've hit the end of the timeline, we need to stop trying to
53155315
// paginate. We need to keep the 'forwards' token though, to make sure
@@ -5334,7 +5334,7 @@ export class MatrixClient extends EventEmitter {
53345334
opts.limit,
53355335
dir,
53365336
eventTimeline.getFilter(),
5337-
).then((res) => {
5337+
).then(async (res) => {
53385338
if (res.state) {
53395339
const roomState = eventTimeline.getState(dir);
53405340
const stateEvents = res.state.map(this.getEventMapper());
@@ -5347,7 +5347,7 @@ export class MatrixClient extends EventEmitter {
53475347

53485348
eventTimeline.getTimelineSet()
53495349
.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
5350-
this.processThreadEvents(room, threadedEvents);
5350+
await this.processThreadEvents(room, threadedEvents);
53515351

53525352
// if we've hit the end of the timeline, we need to stop trying to
53535353
// paginate. We need to keep the 'forwards' token though, to make sure
@@ -9067,7 +9067,10 @@ export class MatrixClient extends EventEmitter {
90679067
const parentEvent = room?.findEventById(parentEventId) || events.find((mxEv: MatrixEvent) => {
90689068
return mxEv.getId() === parentEventId;
90699069
});
9070-
shouldLiveInThreadTimeline = parentEvent?.isThreadRelation;
9070+
if (parentEvent?.isThreadRelation) {
9071+
shouldLiveInThreadTimeline = true;
9072+
event.setThreadId(parentEvent.threadRootId);
9073+
}
90719074

90729075
// Copy all the reactions and annotations to the root event
90739076
// to the thread timeline. They will end up living in both
@@ -9094,12 +9097,11 @@ export class MatrixClient extends EventEmitter {
90949097
/**
90959098
* @experimental
90969099
*/
9097-
public processThreadEvents(room: Room, threadedEvents: MatrixEvent[]): void {
9098-
threadedEvents
9099-
.sort((a, b) => a.getTs() - b.getTs())
9100-
.forEach(event => {
9101-
room.addThreadedEvent(event);
9102-
});
9100+
public async processThreadEvents(room: Room, threadedEvents: MatrixEvent[]): Promise<void> {
9101+
threadedEvents.sort((a, b) => a.getTs() - b.getTs());
9102+
for (const event of threadedEvents) {
9103+
await room.addThreadedEvent(event);
9104+
}
91039105
}
91049106

91059107
/**

src/models/event.ts

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,13 @@ export interface IUnsigned {
9191
redacted_because?: IEvent;
9292
transaction_id?: string;
9393
invite_room_state?: StrippedState[];
94+
"m.relations"?: Record<RelationType | string, any>; // No common pattern for aggregated relations
95+
}
96+
97+
export interface IThreadBundledRelationship {
98+
latest_event: IEvent;
99+
count: number;
100+
current_user_participated?: boolean;
94101
}
95102

96103
export interface IEvent {
@@ -112,7 +119,7 @@ export interface IEvent {
112119
age?: number;
113120
}
114121

115-
interface IAggregatedRelation {
122+
export interface IAggregatedRelation {
116123
origin_server_ts: number;
117124
event_id?: string;
118125
sender?: string;
@@ -262,6 +269,7 @@ export class MatrixEvent extends EventEmitter {
262269
* A reference to the thread this event belongs to
263270
*/
264271
private thread: Thread = null;
272+
private threadId: string;
265273

266274
/* Set an approximate timestamp for the event relative the local clock.
267275
* This will inherently be approximate because it doesn't take into account
@@ -499,28 +507,34 @@ export class MatrixEvent extends EventEmitter {
499507
* @experimental
500508
* Get the event ID of the thread head
501509
*/
502-
public get threadRootId(): string {
510+
public get threadRootId(): string | undefined {
503511
const relatesTo = this.getWireContent()?.["m.relates_to"];
504512
if (relatesTo?.rel_type === RelationType.Thread) {
505513
return relatesTo.event_id;
514+
} else {
515+
return this.threadId
516+
|| this.getThread()?.id;
506517
}
507518
}
508519

509520
/**
510521
* @experimental
511522
*/
512523
public get isThreadRelation(): boolean {
513-
return !!this.threadRootId;
524+
return !!this.threadRootId && this.threadId !== this.getId();
514525
}
515526

516527
/**
517528
* @experimental
518529
*/
519530
public get isThreadRoot(): boolean {
520-
// TODO, change the inner working of this getter for it to use the
521-
// bundled relationship return on the event, view MSC3440
522-
const thread = this.getThread();
523-
return thread?.id === this.getId();
531+
const threadDetails = this
532+
.getServerAggregatedRelation<IThreadBundledRelationship>(RelationType.Thread);
533+
534+
// Bundled relationships only returned when the sync response is limited
535+
// hence us having to check both bundled relation and inspect the thread
536+
// model
537+
return !!threadDetails || (this.getThread()?.id === this.getId());
524538
}
525539

526540
public get parentEventId(): string {
@@ -1000,6 +1014,10 @@ export class MatrixEvent extends EventEmitter {
10001014
return this.event.unsigned || {};
10011015
}
10021016

1017+
public setUnsigned(unsigned: IUnsigned): void {
1018+
this.event.unsigned = unsigned;
1019+
}
1020+
10031021
public unmarkLocallyRedacted(): boolean {
10041022
const value = this._localRedactionEvent;
10051023
this._localRedactionEvent = null;
@@ -1340,11 +1358,8 @@ export class MatrixEvent extends EventEmitter {
13401358
return this.status;
13411359
}
13421360

1343-
public getServerAggregatedRelation(relType: RelationType): IAggregatedRelation {
1344-
const relations = this.getUnsigned()["m.relations"];
1345-
if (relations) {
1346-
return relations[relType];
1347-
}
1361+
public getServerAggregatedRelation<T>(relType: RelationType): T | undefined {
1362+
return this.getUnsigned()["m.relations"]?.[relType];
13481363
}
13491364

13501365
/**
@@ -1353,7 +1368,7 @@ export class MatrixEvent extends EventEmitter {
13531368
* @return {string?}
13541369
*/
13551370
public replacingEventId(): string | undefined {
1356-
const replaceRelation = this.getServerAggregatedRelation(RelationType.Replace);
1371+
const replaceRelation = this.getServerAggregatedRelation<IAggregatedRelation>(RelationType.Replace);
13571372
if (replaceRelation) {
13581373
return replaceRelation.event_id;
13591374
} else if (this._replacingEvent) {
@@ -1378,7 +1393,7 @@ export class MatrixEvent extends EventEmitter {
13781393
* @return {Date?}
13791394
*/
13801395
public replacingEventDate(): Date | undefined {
1381-
const replaceRelation = this.getServerAggregatedRelation(RelationType.Replace);
1396+
const replaceRelation = this.getServerAggregatedRelation<IAggregatedRelation>(RelationType.Replace);
13821397
if (replaceRelation) {
13831398
const ts = replaceRelation.origin_server_ts;
13841399
if (Number.isFinite(ts)) {
@@ -1544,9 +1559,13 @@ export class MatrixEvent extends EventEmitter {
15441559
/**
15451560
* @experimental
15461561
*/
1547-
public getThread(): Thread {
1562+
public getThread(): Thread | undefined {
15481563
return this.thread;
15491564
}
1565+
1566+
public setThreadId(threadId: string): void {
1567+
this.threadId = threadId;
1568+
}
15501569
}
15511570

15521571
/* REDACT_KEEP_KEYS gives the keys we keep when an event is redacted

src/models/relations.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ limitations under the License.
1616

1717
import { EventEmitter } from 'events';
1818

19-
import { EventStatus, MatrixEvent } from './event';
19+
import { EventStatus, MatrixEvent, IAggregatedRelation } from './event';
2020
import { Room } from './room';
2121
import { logger } from '../logger';
2222
import { RelationType } from "../@types/event";
@@ -319,7 +319,7 @@ export class Relations extends EventEmitter {
319319

320320
// the all-knowning server tells us that the event at some point had
321321
// this timestamp for its replacement, so any following replacement should definitely not be less
322-
const replaceRelation = this.targetEvent.getServerAggregatedRelation(RelationType.Replace);
322+
const replaceRelation = this.targetEvent.getServerAggregatedRelation<IAggregatedRelation>(RelationType.Replace);
323323
const minTs = replaceRelation && replaceRelation.origin_server_ts;
324324

325325
const lastReplacement = this.getRelations().reduce((last, event) => {

src/models/room.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1371,9 +1371,11 @@ export class Room extends EventEmitter {
13711371
let rootEvent = this.findEventById(event.threadRootId);
13721372
// If the rootEvent does not exist in the current sync, then look for
13731373
// it over the network
1374+
const eventData = await this.client.fetchRoomEvent(this.roomId, event.threadRootId);
13741375
if (!rootEvent) {
1375-
const eventData = await this.client.fetchRoomEvent(this.roomId, event.threadRootId);
13761376
rootEvent = new MatrixEvent(eventData);
1377+
} else {
1378+
rootEvent.setUnsigned(eventData.unsigned);
13771379
}
13781380
events.unshift(rootEvent);
13791381
thread = this.createThread(events);
@@ -1563,8 +1565,7 @@ export class Room extends EventEmitter {
15631565
}
15641566
} else {
15651567
if (thread) {
1566-
thread.timelineSet.addEventToTimeline(event,
1567-
thread.timelineSet.getLiveTimeline(), false);
1568+
thread.addEvent(event, false);
15681569
} else {
15691570
for (let i = 0; i < this.timelineSets.length; i++) {
15701571
const timelineSet = this.timelineSets[i];

src/models/thread.ts

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ limitations under the License.
1616

1717
import { MatrixClient } from "../matrix";
1818
import { ReEmitter } from "../ReEmitter";
19-
import { MatrixEvent } from "./event";
19+
import { RelationType } from "../@types/event";
20+
import { MatrixEvent, IThreadBundledRelationship } from "./event";
2021
import { EventTimeline } from "./event-timeline";
2122
import { EventTimelineSet } from './event-timeline-set';
2223
import { Room } from './room';
@@ -47,6 +48,9 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
4748

4849
private reEmitter: ReEmitter;
4950

51+
private lastEvent: MatrixEvent;
52+
private replyCount = 0;
53+
5054
constructor(
5155
events: MatrixEvent[] = [],
5256
public readonly room: Room,
@@ -76,6 +80,11 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
7680
room.on("Room.timeline", this.onEcho);
7781
}
7882

83+
public get hasServerSideSupport(): boolean {
84+
return this.client.cachedCapabilities
85+
?.capabilities?.[RelationType.Thread]?.enabled;
86+
}
87+
7988
onEcho = (event: MatrixEvent) => {
8089
if (this.timelineSet.eventIdToTimeline(event.getId())) {
8190
this.emit(ThreadEvent.Update, this);
@@ -89,7 +98,7 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
8998
* @param event The event to add
9099
*/
91100
public async addEvent(event: MatrixEvent, toStartOfTimeline = false): Promise<void> {
92-
if (this.timelineSet.findEventById(event.getId()) || event.status !== null) {
101+
if (this.timelineSet.findEventById(event.getId())) {
93102
return;
94103
}
95104

@@ -121,11 +130,46 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
121130
}
122131

123132
await this.client.decryptEventIfNeeded(event, {});
124-
this.emit(ThreadEvent.Update, this);
125133

126-
if (event.isThreadRelation) {
127-
this.emit(ThreadEvent.NewReply, this, event);
134+
const isThreadReply = event.getRelation()?.rel_type === RelationType.Thread;
135+
// If no thread support exists we want to count all thread relation
136+
// added as a reply. We can't rely on the bundled relationships count
137+
if (!this.hasServerSideSupport && isThreadReply) {
138+
this.replyCount++;
139+
}
140+
141+
if (!this.lastEvent || (isThreadReply && event.getTs() > this.lastEvent.getTs())) {
142+
this.lastEvent = event;
143+
if (this.lastEvent.getId() !== this.root) {
144+
// This counting only works when server side support is enabled
145+
// as we started the counting from the value returned in the
146+
// bundled relationship
147+
if (this.hasServerSideSupport) {
148+
this.replyCount++;
149+
}
150+
this.emit(ThreadEvent.NewReply, this, event);
151+
}
152+
}
153+
154+
if (event.getId() === this.root) {
155+
const bundledRelationship = event
156+
.getServerAggregatedRelation<IThreadBundledRelationship>(RelationType.Thread);
157+
158+
if (this.hasServerSideSupport && bundledRelationship) {
159+
this.replyCount = bundledRelationship.count;
160+
this._currentUserParticipated = bundledRelationship.current_user_participated;
161+
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+
}
128170
}
171+
172+
this.emit(ThreadEvent.Update, this);
129173
}
130174

131175
/**
@@ -171,17 +215,14 @@ export class Thread extends TypedEventEmitter<ThreadEvent> {
171215
* exclude annotations from that number
172216
*/
173217
public get length(): number {
174-
return this.events
175-
.filter(event => event.isThreadRelation)
176-
.length;
218+
return this.replyCount;
177219
}
178220

179221
/**
180222
* A getter for the last event added to the thread
181223
*/
182224
public get replyToEvent(): MatrixEvent {
183-
const events = this.events;
184-
return events[events.length -1];
225+
return this.lastEvent;
185226
}
186227

187228
public get events(): MatrixEvent[] {

0 commit comments

Comments
 (0)