Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move updated threads to the end of the thread list #2923

Merged
merged 3 commits into from
Dec 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 => {
richvdh marked this conversation as resolved.
Show resolved Hide resolved
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);
}
justjanne marked this conversation as resolved.
Show resolved Hide resolved
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