Skip to content

Commit 8a7fd27

Browse files
authored
Move updated threads to the end of the thread list (#2923)
* Move updated threads to the end of the thread list * Write new tests
1 parent 53a45a3 commit 8a7fd27

File tree

2 files changed

+160
-8
lines changed

2 files changed

+160
-8
lines changed

spec/integ/matrix-client-event-timeline.spec.ts

Lines changed: 151 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@ import {
2727
MatrixEvent,
2828
PendingEventOrdering,
2929
Room,
30+
RoomEvent,
3031
} from "../../src/matrix";
3132
import { logger } from "../../src/logger";
3233
import { encodeUri } from "../../src/utils";
3334
import { TestClient } from "../TestClient";
3435
import { FeatureSupport, Thread, THREAD_RELATION_TYPE } from "../../src/models/thread";
36+
import { emitPromise } from "../test-utils/test-utils";
3537

3638
const userId = "@alice:localhost";
3739
const userName = "Alice";
@@ -1093,21 +1095,46 @@ describe("MatrixClient event timelines", function() {
10931095
return request;
10941096
}
10951097

1096-
function respondToContext(): ExpectedHttpRequest {
1098+
function respondToThread(
1099+
root: Partial<IEvent>,
1100+
replies: Partial<IEvent>[],
1101+
): ExpectedHttpRequest {
1102+
const request = httpBackend.when("GET", "/_matrix/client/v1/rooms/!foo%3Abar/relations/" +
1103+
encodeURIComponent(root.event_id!) + "/" +
1104+
encodeURIComponent(THREAD_RELATION_TYPE.name) + "?dir=b&limit=1");
1105+
request.respond(200, function() {
1106+
return {
1107+
original_event: root,
1108+
chunk: [replies],
1109+
// no next batch as this is the oldest end of the timeline
1110+
};
1111+
});
1112+
return request;
1113+
}
1114+
1115+
function respondToContext(event: Partial<IEvent> = THREAD_ROOT): ExpectedHttpRequest {
10971116
const request = httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/context/$eventId", {
10981117
$roomId: roomId,
1099-
$eventId: THREAD_ROOT.event_id!,
1118+
$eventId: event.event_id!,
11001119
}));
11011120
request.respond(200, {
11021121
end: `${Direction.Forward}${RANDOM_TOKEN}1`,
11031122
start: `${Direction.Backward}${RANDOM_TOKEN}1`,
11041123
state: [],
11051124
events_before: [],
11061125
events_after: [],
1107-
event: THREAD_ROOT,
1126+
event: event,
11081127
});
11091128
return request;
11101129
}
1130+
function respondToEvent(event: Partial<IEvent> = THREAD_ROOT): ExpectedHttpRequest {
1131+
const request = httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/event/$eventId", {
1132+
$roomId: roomId,
1133+
$eventId: event.event_id!,
1134+
}));
1135+
request.respond(200, event);
1136+
return request;
1137+
}
11111138
function respondToMessagesRequest(): ExpectedHttpRequest {
11121139
const request = httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/messages", {
11131140
$roomId: roomId,
@@ -1193,6 +1220,127 @@ describe("MatrixClient event timelines", function() {
11931220
expect(myThreads.getPendingEvents()).toHaveLength(0);
11941221
expect(room.getPendingEvents()).toHaveLength(1);
11951222
});
1223+
1224+
it("should handle thread updates by reordering the thread list", async () => {
1225+
// Test data for a second thread
1226+
const THREAD2_ROOT = utils.mkEvent({
1227+
room: roomId,
1228+
user: userId,
1229+
type: "m.room.message",
1230+
content: {
1231+
"body": "thread root",
1232+
"msgtype": "m.text",
1233+
},
1234+
unsigned: {
1235+
"m.relations": {
1236+
"io.element.thread": {
1237+
//"latest_event": undefined,
1238+
"count": 1,
1239+
"current_user_participated": true,
1240+
},
1241+
},
1242+
},
1243+
event: false,
1244+
});
1245+
1246+
const THREAD2_REPLY = utils.mkEvent({
1247+
room: roomId,
1248+
user: userId,
1249+
type: "m.room.message",
1250+
content: {
1251+
"body": "thread reply",
1252+
"msgtype": "m.text",
1253+
"m.relates_to": {
1254+
// We can't use the const here because we change server support mode for test
1255+
rel_type: "io.element.thread",
1256+
event_id: THREAD_ROOT.event_id,
1257+
},
1258+
},
1259+
event: false,
1260+
});
1261+
1262+
// @ts-ignore we know this is a defined path for THREAD ROOT
1263+
THREAD2_ROOT.unsigned["m.relations"]["io.element.thread"].latest_event = THREAD2_REPLY;
1264+
1265+
// Test data for a second reply to the first thread
1266+
const THREAD_REPLY2 = utils.mkEvent({
1267+
room: roomId,
1268+
user: userId,
1269+
type: "m.room.message",
1270+
content: {
1271+
"body": "thread reply",
1272+
"msgtype": "m.text",
1273+
"m.relates_to": {
1274+
// We can't use the const here because we change server support mode for test
1275+
rel_type: "io.element.thread",
1276+
event_id: THREAD_ROOT.event_id,
1277+
},
1278+
},
1279+
event: false,
1280+
});
1281+
1282+
// Test data for the first thread, with the second reply
1283+
const THREAD_ROOT_UPDATED = {
1284+
...THREAD_ROOT,
1285+
unsigned: {
1286+
...THREAD_ROOT.unsigned,
1287+
"m.relations": {
1288+
...THREAD_ROOT.unsigned!["m.relations"],
1289+
"io.element.thread": {
1290+
...THREAD_ROOT.unsigned!["m.relations"]!["io.element.thread"],
1291+
count: 2,
1292+
latest_event: THREAD_REPLY2,
1293+
},
1294+
},
1295+
},
1296+
};
1297+
1298+
// Response with test data for the thread list request
1299+
const threadsResponse = {
1300+
chunk: [THREAD2_ROOT, THREAD_ROOT],
1301+
state: [],
1302+
next_batch: RANDOM_TOKEN as string | null,
1303+
};
1304+
1305+
// @ts-ignore
1306+
client.clientOpts.experimentalThreadSupport = true;
1307+
Thread.setServerSideSupport(FeatureSupport.Stable);
1308+
Thread.setServerSideListSupport(FeatureSupport.Stable);
1309+
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
1310+
1311+
await client.stopClient(); // we don't need the client to be syncing at this time
1312+
const room = client.getRoom(roomId)!;
1313+
1314+
// Setup room threads
1315+
const timelineSets = await room!.createThreadsTimelineSets();
1316+
expect(timelineSets).not.toBeNull();
1317+
respondToThreads(threadsResponse);
1318+
respondToThreads(threadsResponse);
1319+
respondToEvent(THREAD_ROOT);
1320+
respondToEvent(THREAD_ROOT);
1321+
respondToEvent(THREAD2_ROOT);
1322+
respondToEvent(THREAD2_ROOT);
1323+
respondToThread(THREAD_ROOT, [THREAD_REPLY]);
1324+
respondToThread(THREAD2_ROOT, [THREAD2_REPLY]);
1325+
await flushHttp(room.fetchRoomThreads());
1326+
const [allThreads] = timelineSets!;
1327+
const timeline = allThreads.getLiveTimeline()!;
1328+
// Test threads are in chronological order
1329+
expect(timeline.getEvents().map(it => it.event.event_id))
1330+
.toEqual([THREAD_ROOT.event_id, THREAD2_ROOT.event_id]);
1331+
1332+
// Test adding a second event to the first thread
1333+
const thread = room.getThread(THREAD_ROOT.event_id!)!;
1334+
const prom = emitPromise(allThreads!, RoomEvent.Timeline);
1335+
await thread.addEvent(client.getEventMapper()(THREAD_REPLY2), false);
1336+
respondToEvent(THREAD_ROOT_UPDATED);
1337+
respondToEvent(THREAD_ROOT_UPDATED);
1338+
await httpBackend.flushAllExpected();
1339+
await prom;
1340+
// Test threads are in chronological order
1341+
expect(timeline!.getEvents().map(it => it.event.event_id))
1342+
.toEqual([THREAD2_ROOT.event_id, THREAD_ROOT.event_id]);
1343+
});
11961344
});
11971345

11981346
describe("without server compatibility", function() {

src/models/room.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1843,7 +1843,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
18431843
}
18441844

18451845
private onThreadNewReply(thread: Thread): void {
1846-
this.updateThreadRootEvents(thread, false);
1846+
this.updateThreadRootEvents(thread, false, true);
18471847
}
18481848

18491849
private onThreadDelete(thread: Thread): void {
@@ -1968,11 +1968,11 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
19681968
));
19691969
}
19701970

1971-
private updateThreadRootEvents = (thread: Thread, toStartOfTimeline: boolean): void => {
1971+
private updateThreadRootEvents = (thread: Thread, toStartOfTimeline: boolean, recreateEvent: boolean): void => {
19721972
if (thread.length) {
1973-
this.updateThreadRootEvent(this.threadsTimelineSets?.[0], thread, toStartOfTimeline);
1973+
this.updateThreadRootEvent(this.threadsTimelineSets?.[0], thread, toStartOfTimeline, recreateEvent);
19741974
if (thread.hasCurrentUserParticipated) {
1975-
this.updateThreadRootEvent(this.threadsTimelineSets?.[1], thread, toStartOfTimeline);
1975+
this.updateThreadRootEvent(this.threadsTimelineSets?.[1], thread, toStartOfTimeline, recreateEvent);
19761976
}
19771977
}
19781978
};
@@ -1981,8 +1981,12 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
19811981
timelineSet: Optional<EventTimelineSet>,
19821982
thread: Thread,
19831983
toStartOfTimeline: boolean,
1984+
recreateEvent: boolean,
19841985
): void => {
19851986
if (timelineSet && thread.rootEvent) {
1987+
if (recreateEvent) {
1988+
timelineSet.removeEvent(thread.id);
1989+
}
19861990
if (Thread.hasServerSideSupport) {
19871991
timelineSet.addLiveEvent(thread.rootEvent, {
19881992
duplicateStrategy: DuplicateStrategy.Replace,
@@ -2046,7 +2050,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
20462050
}
20472051

20482052
if (this.threadsReady) {
2049-
this.updateThreadRootEvents(thread, toStartOfTimeline);
2053+
this.updateThreadRootEvents(thread, toStartOfTimeline, false);
20502054
}
20512055

20522056
this.emit(ThreadEvent.New, thread, toStartOfTimeline);

0 commit comments

Comments
 (0)