Skip to content

Commit

Permalink
Move updated threads to the end of the thread list (#2923)
Browse files Browse the repository at this point in the history
* Move updated threads to the end of the thread list
* Write new tests
  • Loading branch information
justjanne authored Dec 2, 2022
1 parent 53a45a3 commit 8a7fd27
Show file tree
Hide file tree
Showing 2 changed files with 160 additions and 8 deletions.
154 changes: 151 additions & 3 deletions spec/integ/matrix-client-event-timeline.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@ import {
MatrixEvent,
PendingEventOrdering,
Room,
RoomEvent,
} from "../../src/matrix";
import { logger } from "../../src/logger";
import { encodeUri } from "../../src/utils";
import { TestClient } from "../TestClient";
import { FeatureSupport, Thread, THREAD_RELATION_TYPE } from "../../src/models/thread";
import { emitPromise } from "../test-utils/test-utils";

const userId = "@alice:localhost";
const userName = "Alice";
Expand Down Expand Up @@ -1093,21 +1095,46 @@ describe("MatrixClient event timelines", function() {
return request;
}

function respondToContext(): ExpectedHttpRequest {
function respondToThread(
root: Partial<IEvent>,
replies: Partial<IEvent>[],
): ExpectedHttpRequest {
const request = httpBackend.when("GET", "/_matrix/client/v1/rooms/!foo%3Abar/relations/" +
encodeURIComponent(root.event_id!) + "/" +
encodeURIComponent(THREAD_RELATION_TYPE.name) + "?dir=b&limit=1");
request.respond(200, function() {
return {
original_event: root,
chunk: [replies],
// no next batch as this is the oldest end of the timeline
};
});
return request;
}

function respondToContext(event: Partial<IEvent> = THREAD_ROOT): ExpectedHttpRequest {
const request = httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/context/$eventId", {
$roomId: roomId,
$eventId: THREAD_ROOT.event_id!,
$eventId: event.event_id!,
}));
request.respond(200, {
end: `${Direction.Forward}${RANDOM_TOKEN}1`,
start: `${Direction.Backward}${RANDOM_TOKEN}1`,
state: [],
events_before: [],
events_after: [],
event: THREAD_ROOT,
event: event,
});
return request;
}
function respondToEvent(event: Partial<IEvent> = THREAD_ROOT): ExpectedHttpRequest {
const request = httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/event/$eventId", {
$roomId: roomId,
$eventId: event.event_id!,
}));
request.respond(200, event);
return request;
}
function respondToMessagesRequest(): ExpectedHttpRequest {
const request = httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/messages", {
$roomId: roomId,
Expand Down Expand Up @@ -1193,6 +1220,127 @@ describe("MatrixClient event timelines", function() {
expect(myThreads.getPendingEvents()).toHaveLength(0);
expect(room.getPendingEvents()).toHaveLength(1);
});

it("should handle thread updates by reordering the thread list", async () => {
// Test data for a second thread
const THREAD2_ROOT = utils.mkEvent({
room: roomId,
user: userId,
type: "m.room.message",
content: {
"body": "thread root",
"msgtype": "m.text",
},
unsigned: {
"m.relations": {
"io.element.thread": {
//"latest_event": undefined,
"count": 1,
"current_user_participated": true,
},
},
},
event: false,
});

const THREAD2_REPLY = utils.mkEvent({
room: roomId,
user: userId,
type: "m.room.message",
content: {
"body": "thread reply",
"msgtype": "m.text",
"m.relates_to": {
// We can't use the const here because we change server support mode for test
rel_type: "io.element.thread",
event_id: THREAD_ROOT.event_id,
},
},
event: false,
});

// @ts-ignore we know this is a defined path for THREAD ROOT
THREAD2_ROOT.unsigned["m.relations"]["io.element.thread"].latest_event = THREAD2_REPLY;

// Test data for a second reply to the first thread
const THREAD_REPLY2 = utils.mkEvent({
room: roomId,
user: userId,
type: "m.room.message",
content: {
"body": "thread reply",
"msgtype": "m.text",
"m.relates_to": {
// We can't use the const here because we change server support mode for test
rel_type: "io.element.thread",
event_id: THREAD_ROOT.event_id,
},
},
event: false,
});

// Test data for the first thread, with the second reply
const THREAD_ROOT_UPDATED = {
...THREAD_ROOT,
unsigned: {
...THREAD_ROOT.unsigned,
"m.relations": {
...THREAD_ROOT.unsigned!["m.relations"],
"io.element.thread": {
...THREAD_ROOT.unsigned!["m.relations"]!["io.element.thread"],
count: 2,
latest_event: THREAD_REPLY2,
},
},
},
};

// Response with test data for the thread list request
const threadsResponse = {
chunk: [THREAD2_ROOT, THREAD_ROOT],
state: [],
next_batch: RANDOM_TOKEN as string | null,
};

// @ts-ignore
client.clientOpts.experimentalThreadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Stable);
Thread.setServerSideListSupport(FeatureSupport.Stable);
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);

await client.stopClient(); // we don't need the client to be syncing at this time
const room = client.getRoom(roomId)!;

// Setup room threads
const timelineSets = await room!.createThreadsTimelineSets();
expect(timelineSets).not.toBeNull();
respondToThreads(threadsResponse);
respondToThreads(threadsResponse);
respondToEvent(THREAD_ROOT);
respondToEvent(THREAD_ROOT);
respondToEvent(THREAD2_ROOT);
respondToEvent(THREAD2_ROOT);
respondToThread(THREAD_ROOT, [THREAD_REPLY]);
respondToThread(THREAD2_ROOT, [THREAD2_REPLY]);
await flushHttp(room.fetchRoomThreads());
const [allThreads] = timelineSets!;
const timeline = allThreads.getLiveTimeline()!;
// Test threads are in chronological order
expect(timeline.getEvents().map(it => it.event.event_id))
.toEqual([THREAD_ROOT.event_id, THREAD2_ROOT.event_id]);

// Test adding a second event to the first thread
const thread = room.getThread(THREAD_ROOT.event_id!)!;
const prom = emitPromise(allThreads!, RoomEvent.Timeline);
await thread.addEvent(client.getEventMapper()(THREAD_REPLY2), false);
respondToEvent(THREAD_ROOT_UPDATED);
respondToEvent(THREAD_ROOT_UPDATED);
await httpBackend.flushAllExpected();
await prom;
// Test threads are in chronological order
expect(timeline!.getEvents().map(it => it.event.event_id))
.toEqual([THREAD2_ROOT.event_id, THREAD_ROOT.event_id]);
});
});

describe("without server compatibility", function() {
Expand Down
14 changes: 9 additions & 5 deletions src/models/room.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1843,7 +1843,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
}

private onThreadNewReply(thread: Thread): void {
this.updateThreadRootEvents(thread, false);
this.updateThreadRootEvents(thread, false, true);
}

private onThreadDelete(thread: Thread): void {
Expand Down Expand Up @@ -1968,11 +1968,11 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
));
}

private updateThreadRootEvents = (thread: Thread, toStartOfTimeline: boolean): void => {
private updateThreadRootEvents = (thread: Thread, toStartOfTimeline: boolean, recreateEvent: boolean): void => {
if (thread.length) {
this.updateThreadRootEvent(this.threadsTimelineSets?.[0], thread, toStartOfTimeline);
this.updateThreadRootEvent(this.threadsTimelineSets?.[0], thread, toStartOfTimeline, recreateEvent);
if (thread.hasCurrentUserParticipated) {
this.updateThreadRootEvent(this.threadsTimelineSets?.[1], thread, toStartOfTimeline);
this.updateThreadRootEvent(this.threadsTimelineSets?.[1], thread, toStartOfTimeline, recreateEvent);
}
}
};
Expand All @@ -1981,8 +1981,12 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
timelineSet: Optional<EventTimelineSet>,
thread: Thread,
toStartOfTimeline: boolean,
recreateEvent: boolean,
): void => {
if (timelineSet && thread.rootEvent) {
if (recreateEvent) {
timelineSet.removeEvent(thread.id);
}
if (Thread.hasServerSideSupport) {
timelineSet.addLiveEvent(thread.rootEvent, {
duplicateStrategy: DuplicateStrategy.Replace,
Expand Down Expand Up @@ -2046,7 +2050,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
}

if (this.threadsReady) {
this.updateThreadRootEvents(thread, toStartOfTimeline);
this.updateThreadRootEvents(thread, toStartOfTimeline, false);
}

this.emit(ThreadEvent.New, thread, toStartOfTimeline);
Expand Down

0 comments on commit 8a7fd27

Please sign in to comment.