You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-07-30 04:23:07 +03:00
Loading threads with server-side assistance (#2735)
* Fix bug where undefined vs null in pagination tokens wasn't correctly handled * Fix bug where thread list results were sorted incorrectly * Allow removing the relationship of an event to a thread * Implement feature detection for new threads MSCs and specs * Prefix dir parameter for threads pagination if necessary * Make threads conform to the same timeline APIs as any other timeline * Extract thread timeline loading out of thread class * fix thread roots not being updated correctly * fix jumping to events by link * implement new thread timeline loading * Fix fetchRoomEvent incorrect return type Co-authored-by: Germain <germains@element.io> Co-authored-by: Germain <germain@souquet.com>
This commit is contained in:
committed by
GitHub
parent
b44787192d
commit
068fbb7660
@ -342,8 +342,14 @@ describe("MatrixClient event timelines", function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
client.stopClient();
|
||||
Thread.setServerSideSupport(FeatureSupport.None);
|
||||
Thread.setServerSideListSupport(FeatureSupport.None);
|
||||
Thread.setServerSideFwdPaginationSupport(FeatureSupport.None);
|
||||
});
|
||||
|
||||
async function flushHttp<T>(promise: Promise<T>): Promise<T> {
|
||||
return Promise.all([promise, httpBackend.flushAllExpected()]).then(([result]) => result);
|
||||
}
|
||||
|
||||
describe("getEventTimeline", function() {
|
||||
it("should create a new timeline for new events", function() {
|
||||
const room = client.getRoom(roomId)!;
|
||||
@ -595,22 +601,8 @@ describe("MatrixClient event timelines", function() {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
client.stopClient(); // we don't need the client to be syncing at this time
|
||||
await client.stopClient(); // we don't need the client to be syncing at this time
|
||||
const room = client.getRoom(roomId)!;
|
||||
const thread = room.createThread(THREAD_ROOT.event_id!, undefined, [], false);
|
||||
const timelineSet = thread.timelineSet;
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_REPLY.event_id!))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token0",
|
||||
events_before: [],
|
||||
event: THREAD_REPLY,
|
||||
events_after: [],
|
||||
end: "end_token0",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function() {
|
||||
@ -619,7 +611,7 @@ describe("MatrixClient event timelines", function() {
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
|
||||
encodeURIComponent(THREAD_ROOT.event_id!) + "/" +
|
||||
encodeURIComponent(THREAD_RELATION_TYPE.name) + "?limit=20")
|
||||
encodeURIComponent(THREAD_RELATION_TYPE.name) + "?dir=b&limit=1")
|
||||
.respond(200, function() {
|
||||
return {
|
||||
original_event: THREAD_ROOT,
|
||||
@ -628,9 +620,45 @@ describe("MatrixClient event timelines", function() {
|
||||
};
|
||||
});
|
||||
|
||||
const timelinePromise = client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!);
|
||||
const thread = room.createThread(THREAD_ROOT.event_id!, undefined, [], false);
|
||||
await httpBackend.flushAllExpected();
|
||||
const timelineSet = thread.timelineSet;
|
||||
|
||||
const timelinePromise = client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!);
|
||||
const timeline = await timelinePromise;
|
||||
|
||||
expect(timeline!.getEvents().find(e => e.getId() === THREAD_ROOT.event_id!)).toBeTruthy();
|
||||
expect(timeline!.getEvents().find(e => e.getId() === THREAD_REPLY.event_id!)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should handle thread replies with server support by fetching a contiguous thread timeline", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
await client.stopClient(); // we don't need the client to be syncing at this time
|
||||
const room = client.getRoom(roomId)!;
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, function() {
|
||||
return THREAD_ROOT;
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
|
||||
encodeURIComponent(THREAD_ROOT.event_id!) + "/" +
|
||||
encodeURIComponent(THREAD_RELATION_TYPE.name) + "?dir=b&limit=1")
|
||||
.respond(200, function() {
|
||||
return {
|
||||
original_event: THREAD_ROOT,
|
||||
chunk: [THREAD_REPLY],
|
||||
// no next batch as this is the oldest end of the timeline
|
||||
};
|
||||
});
|
||||
|
||||
const thread = room.createThread(THREAD_ROOT.event_id!, undefined, [], false);
|
||||
await httpBackend.flushAllExpected();
|
||||
const timelineSet = thread.timelineSet;
|
||||
|
||||
const timelinePromise = client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!);
|
||||
const timeline = await timelinePromise;
|
||||
|
||||
expect(timeline!.getEvents().find(e => e.getId() === THREAD_ROOT.event_id!)).toBeTruthy();
|
||||
@ -1025,10 +1053,6 @@ describe("MatrixClient event timelines", function() {
|
||||
});
|
||||
|
||||
describe("paginateEventTimeline for thread list timeline", function() {
|
||||
async function flushHttp<T>(promise: Promise<T>): Promise<T> {
|
||||
return Promise.all([promise, httpBackend.flushAllExpected()]).then(([result]) => result);
|
||||
}
|
||||
|
||||
const RANDOM_TOKEN = "7280349c7bee430f91defe2a38a0a08c";
|
||||
|
||||
function respondToFilter(): ExpectedHttpRequest {
|
||||
@ -1050,7 +1074,7 @@ describe("MatrixClient event timelines", function() {
|
||||
next_batch: RANDOM_TOKEN as string | null,
|
||||
},
|
||||
): ExpectedHttpRequest {
|
||||
const request = httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/threads", {
|
||||
const request = httpBackend.when("GET", encodeUri("/_matrix/client/v1/rooms/$roomId/threads", {
|
||||
$roomId: roomId,
|
||||
}));
|
||||
request.respond(200, response);
|
||||
@ -1089,8 +1113,9 @@ describe("MatrixClient event timelines", function() {
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
|
||||
});
|
||||
|
||||
async function testPagination(timelineSet: EventTimelineSet, direction: Direction) {
|
||||
@ -1111,7 +1136,7 @@ describe("MatrixClient event timelines", function() {
|
||||
|
||||
it("should allow you to paginate all threads backwards", async function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSets = await (room?.createThreadsTimelineSets());
|
||||
const timelineSets = await room!.createThreadsTimelineSets();
|
||||
expect(timelineSets).not.toBeNull();
|
||||
const [allThreads, myThreads] = timelineSets!;
|
||||
await testPagination(allThreads, Direction.Backward);
|
||||
@ -1120,7 +1145,7 @@ describe("MatrixClient event timelines", function() {
|
||||
|
||||
it("should allow you to paginate all threads forwards", async function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSets = await (room?.createThreadsTimelineSets());
|
||||
const timelineSets = await room!.createThreadsTimelineSets();
|
||||
expect(timelineSets).not.toBeNull();
|
||||
const [allThreads, myThreads] = timelineSets!;
|
||||
|
||||
@ -1130,7 +1155,7 @@ describe("MatrixClient event timelines", function() {
|
||||
|
||||
it("should allow fetching all threads", async function() {
|
||||
const room = client.getRoom(roomId)!;
|
||||
const timelineSets = await room.createThreadsTimelineSets();
|
||||
const timelineSets = await room!.createThreadsTimelineSets();
|
||||
expect(timelineSets).not.toBeNull();
|
||||
respondToThreads();
|
||||
respondToThreads();
|
||||
@ -1418,74 +1443,115 @@ describe("MatrixClient event timelines", function() {
|
||||
});
|
||||
});
|
||||
|
||||
it("should re-insert room IDs for bundled thread relation events", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_4",
|
||||
rooms: {
|
||||
join: {
|
||||
[roomId]: {
|
||||
timeline: {
|
||||
events: [
|
||||
SYNC_THREAD_ROOT,
|
||||
],
|
||||
prev_batch: "f_1_1",
|
||||
describe("should re-insert room IDs for bundled thread relation events", () => {
|
||||
async function doTest() {
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_4",
|
||||
rooms: {
|
||||
join: {
|
||||
[roomId]: {
|
||||
timeline: {
|
||||
events: [
|
||||
SYNC_THREAD_ROOT,
|
||||
],
|
||||
prev_batch: "f_1_1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await Promise.all([httpBackend.flushAllExpected(), utils.syncPromise(client)]);
|
||||
|
||||
const room = client.getRoom(roomId)!;
|
||||
const thread = room.getThread(THREAD_ROOT.event_id!)!;
|
||||
const timelineSet = thread.timelineSet;
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, {
|
||||
start: "start_token",
|
||||
events_before: [],
|
||||
event: THREAD_ROOT,
|
||||
events_after: [],
|
||||
state: [],
|
||||
end: "end_token",
|
||||
});
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
|
||||
encodeURIComponent(THREAD_ROOT.event_id!) + "/" +
|
||||
encodeURIComponent(THREAD_RELATION_TYPE.name) + "?limit=20")
|
||||
.respond(200, function() {
|
||||
return {
|
||||
original_event: THREAD_ROOT,
|
||||
chunk: [THREAD_REPLY],
|
||||
// no next batch as this is the oldest end of the timeline
|
||||
};
|
||||
});
|
||||
await Promise.all([
|
||||
client.getEventTimeline(timelineSet, THREAD_ROOT.event_id!),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
await Promise.all([httpBackend.flushAllExpected(), utils.syncPromise(client)]);
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_5",
|
||||
rooms: {
|
||||
join: {
|
||||
[roomId]: {
|
||||
timeline: {
|
||||
events: [
|
||||
SYNC_THREAD_REPLY,
|
||||
],
|
||||
prev_batch: "f_1_2",
|
||||
const room = client.getRoom(roomId)!;
|
||||
const thread = room.getThread(THREAD_ROOT.event_id!)!;
|
||||
const timelineSet = thread.timelineSet;
|
||||
|
||||
const buildParams = (direction: Direction, token: string): string => {
|
||||
if (Thread.hasServerSideFwdPaginationSupport === FeatureSupport.Experimental) {
|
||||
return `?from=${token}&org.matrix.msc3715.dir=${direction}`;
|
||||
} else {
|
||||
return `?dir=${direction}&from=${token}`;
|
||||
}
|
||||
};
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_ROOT.event_id!))
|
||||
.respond(200, {
|
||||
start: "start_token",
|
||||
events_before: [],
|
||||
event: THREAD_ROOT,
|
||||
events_after: [],
|
||||
state: [],
|
||||
end: "end_token",
|
||||
});
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
|
||||
encodeURIComponent(THREAD_ROOT.event_id!) + "/" +
|
||||
encodeURIComponent(THREAD_RELATION_TYPE.name) + buildParams(Direction.Backward, "start_token"))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
original_event: THREAD_ROOT,
|
||||
chunk: [],
|
||||
};
|
||||
});
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
|
||||
encodeURIComponent(THREAD_ROOT.event_id!) + "/" +
|
||||
encodeURIComponent(THREAD_RELATION_TYPE.name) + buildParams(Direction.Forward, "end_token"))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
original_event: THREAD_ROOT,
|
||||
chunk: [THREAD_REPLY],
|
||||
};
|
||||
});
|
||||
const timeline = await flushHttp(client.getEventTimeline(timelineSet, THREAD_ROOT.event_id!));
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_5",
|
||||
rooms: {
|
||||
join: {
|
||||
[roomId]: {
|
||||
timeline: {
|
||||
events: [
|
||||
SYNC_THREAD_REPLY,
|
||||
],
|
||||
prev_batch: "f_1_2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all([httpBackend.flushAllExpected(), utils.syncPromise(client)]);
|
||||
|
||||
expect(timeline!.getEvents()[1]!.event).toEqual(THREAD_REPLY);
|
||||
}
|
||||
|
||||
it("in stable mode", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
|
||||
|
||||
return doTest();
|
||||
});
|
||||
|
||||
await Promise.all([httpBackend.flushAllExpected(), utils.syncPromise(client)]);
|
||||
it("in backwards compatible unstable mode", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Experimental);
|
||||
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Experimental);
|
||||
|
||||
expect(thread.liveTimeline.getEvents()[1].event).toEqual(THREAD_REPLY);
|
||||
return doTest();
|
||||
});
|
||||
|
||||
it("in backwards compatible mode", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
Thread.setServerSideListSupport(FeatureSupport.None);
|
||||
Thread.setServerSideFwdPaginationSupport(FeatureSupport.None);
|
||||
|
||||
return doTest();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -60,7 +60,7 @@ describe("MatrixClient relations", () => {
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" });
|
||||
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
|
||||
});
|
||||
|
||||
it("should read related events with relation type", async () => {
|
||||
@ -72,7 +72,7 @@ describe("MatrixClient relations", () => {
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" });
|
||||
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
|
||||
});
|
||||
|
||||
it("should read related events with relation type and event type", async () => {
|
||||
@ -87,7 +87,7 @@ describe("MatrixClient relations", () => {
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" });
|
||||
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
|
||||
});
|
||||
|
||||
it("should read related events with custom options", async () => {
|
||||
@ -107,7 +107,7 @@ describe("MatrixClient relations", () => {
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" });
|
||||
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
|
||||
});
|
||||
|
||||
it('should use default direction in the fetchRelations endpoint', async () => {
|
||||
|
@ -231,6 +231,130 @@ describe("MatrixClient", function() {
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
describe("sendEvent", () => {
|
||||
const roomId = "!room:example.org";
|
||||
const body = "This is the body";
|
||||
const content = { body };
|
||||
|
||||
it("overload without threadId works", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const txnId = client.makeTxnId();
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: content,
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, EventType.RoomMessage, { ...content }, txnId);
|
||||
});
|
||||
|
||||
it("overload with null threadId works", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const txnId = client.makeTxnId();
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: content,
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, null, EventType.RoomMessage, { ...content }, txnId);
|
||||
});
|
||||
|
||||
it("overload with threadId works", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const txnId = client.makeTxnId();
|
||||
const threadId = "$threadId:server";
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: {
|
||||
...content,
|
||||
"m.relates_to": {
|
||||
"event_id": threadId,
|
||||
"is_falling_back": true,
|
||||
"rel_type": "m.thread",
|
||||
},
|
||||
},
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, threadId, EventType.RoomMessage, { ...content }, txnId);
|
||||
});
|
||||
|
||||
it("should add thread relation if threadId is passed and the relation is missing", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const threadId = "$threadId:server";
|
||||
const txnId = client.makeTxnId();
|
||||
|
||||
const room = new Room(roomId, client, userId);
|
||||
store.getRoom.mockReturnValue(room);
|
||||
|
||||
const rootEvent = new MatrixEvent({ event_id: threadId });
|
||||
room.createThread(threadId, rootEvent, [rootEvent], false);
|
||||
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: {
|
||||
...content,
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
event_id: threadId,
|
||||
},
|
||||
"event_id": threadId,
|
||||
"is_falling_back": true,
|
||||
"rel_type": "m.thread",
|
||||
},
|
||||
},
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, threadId, EventType.RoomMessage, { ...content }, txnId);
|
||||
});
|
||||
|
||||
it("should add thread relation if threadId is passed and the relation is missing with reply", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const threadId = "$threadId:server";
|
||||
const txnId = client.makeTxnId();
|
||||
|
||||
const content = {
|
||||
body,
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
event_id: "$other:event",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const room = new Room(roomId, client, userId);
|
||||
store.getRoom.mockReturnValue(room);
|
||||
|
||||
const rootEvent = new MatrixEvent({ event_id: threadId });
|
||||
room.createThread(threadId, rootEvent, [rootEvent], false);
|
||||
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: {
|
||||
...content,
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
event_id: "$other:event",
|
||||
},
|
||||
"event_id": threadId,
|
||||
"is_falling_back": false,
|
||||
"rel_type": "m.thread",
|
||||
},
|
||||
},
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, threadId, EventType.RoomMessage, { ...content }, txnId);
|
||||
});
|
||||
});
|
||||
|
||||
it("should create (unstable) file trees", async () => {
|
||||
const userId = "@test:example.org";
|
||||
const roomId = "!room:example.org";
|
||||
@ -777,130 +901,6 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendEvent", () => {
|
||||
const roomId = "!room:example.org";
|
||||
const body = "This is the body";
|
||||
const content = { body };
|
||||
|
||||
it("overload without threadId works", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const txnId = client.makeTxnId();
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: content,
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, EventType.RoomMessage, { ...content }, txnId);
|
||||
});
|
||||
|
||||
it("overload with null threadId works", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const txnId = client.makeTxnId();
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: content,
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, null, EventType.RoomMessage, { ...content }, txnId);
|
||||
});
|
||||
|
||||
it("overload with threadId works", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const txnId = client.makeTxnId();
|
||||
const threadId = "$threadId:server";
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: {
|
||||
...content,
|
||||
"m.relates_to": {
|
||||
"event_id": threadId,
|
||||
"is_falling_back": true,
|
||||
"rel_type": "m.thread",
|
||||
},
|
||||
},
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, threadId, EventType.RoomMessage, { ...content }, txnId);
|
||||
});
|
||||
|
||||
it("should add thread relation if threadId is passed and the relation is missing", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const threadId = "$threadId:server";
|
||||
const txnId = client.makeTxnId();
|
||||
|
||||
const room = new Room(roomId, client, userId);
|
||||
store.getRoom.mockReturnValue(room);
|
||||
|
||||
const rootEvent = new MatrixEvent({ event_id: threadId });
|
||||
room.createThread(threadId, rootEvent, [rootEvent], false);
|
||||
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: {
|
||||
...content,
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
event_id: threadId,
|
||||
},
|
||||
"event_id": threadId,
|
||||
"is_falling_back": true,
|
||||
"rel_type": "m.thread",
|
||||
},
|
||||
},
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, threadId, EventType.RoomMessage, { ...content }, txnId);
|
||||
});
|
||||
|
||||
it("should add thread relation if threadId is passed and the relation is missing with reply", async () => {
|
||||
const eventId = "$eventId:example.org";
|
||||
const threadId = "$threadId:server";
|
||||
const txnId = client.makeTxnId();
|
||||
|
||||
const content = {
|
||||
body,
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
event_id: "$other:event",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const room = new Room(roomId, client, userId);
|
||||
store.getRoom.mockReturnValue(room);
|
||||
|
||||
const rootEvent = new MatrixEvent({ event_id: threadId });
|
||||
room.createThread(threadId, rootEvent, [rootEvent], false);
|
||||
|
||||
httpLookups = [{
|
||||
method: "PUT",
|
||||
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
data: { event_id: eventId },
|
||||
expectBody: {
|
||||
...content,
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
event_id: "$other:event",
|
||||
},
|
||||
"event_id": threadId,
|
||||
"is_falling_back": false,
|
||||
"rel_type": "m.thread",
|
||||
},
|
||||
},
|
||||
}];
|
||||
|
||||
await client.sendEvent(roomId, threadId, EventType.RoomMessage, { ...content }, txnId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("redactEvent", () => {
|
||||
const roomId = "!room:example.org";
|
||||
const mockRoom = {
|
||||
|
@ -39,7 +39,7 @@ import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { emitPromise } from "../test-utils/test-utils";
|
||||
import { ReceiptType } from "../../src/@types/read_receipts";
|
||||
import { FeatureSupport, Thread, ThreadEvent } from "../../src/models/thread";
|
||||
import { FeatureSupport, Thread, ThreadEvent, THREAD_RELATION_TYPE } from "../../src/models/thread";
|
||||
import { WrappedReceipt } from "../../src/models/read-receipt";
|
||||
import { Crypto } from "../../src/crypto";
|
||||
|
||||
@ -2203,6 +2203,7 @@ describe("Room", function() {
|
||||
|
||||
it("Edits update the lastReply event", async () => {
|
||||
room.client.supportsExperimentalThreads = () => true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
|
||||
const randomMessage = mkMessage();
|
||||
const threadRoot = mkMessage();
|
||||
@ -2216,7 +2217,7 @@ describe("Room", function() {
|
||||
unsigned: {
|
||||
"age": 123,
|
||||
"m.relations": {
|
||||
"m.thread": {
|
||||
[THREAD_RELATION_TYPE.name]: {
|
||||
latest_event: threadResponse.event,
|
||||
count: 2,
|
||||
current_user_participated: true,
|
||||
@ -2228,11 +2229,29 @@ describe("Room", function() {
|
||||
let prom = emitPromise(room, ThreadEvent.New);
|
||||
room.addLiveEvents([randomMessage, threadRoot, threadResponse]);
|
||||
const thread = await prom;
|
||||
await emitPromise(room, ThreadEvent.Update);
|
||||
|
||||
expect(thread.replyToEvent).toBe(threadResponse);
|
||||
expect(thread.replyToEvent.event).toEqual(threadResponse.event);
|
||||
expect(thread.replyToEvent.getContent().body).toBe(threadResponse.getContent().body);
|
||||
|
||||
prom = emitPromise(thread, ThreadEvent.Update);
|
||||
room.client.fetchRoomEvent = (eventId: string) => Promise.resolve({
|
||||
...threadRoot.event,
|
||||
unsigned: {
|
||||
"age": 123,
|
||||
"m.relations": {
|
||||
[THREAD_RELATION_TYPE.name]: {
|
||||
latest_event: {
|
||||
...threadResponse.event,
|
||||
content: threadResponseEdit.event.content,
|
||||
},
|
||||
count: 2,
|
||||
current_user_participated: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
prom = emitPromise(room, ThreadEvent.Update);
|
||||
room.addLiveEvents([threadResponseEdit]);
|
||||
await prom;
|
||||
expect(thread.replyToEvent.getContent().body).toBe(threadResponseEdit.getContent()["m.new_content"].body);
|
||||
@ -2240,6 +2259,7 @@ describe("Room", function() {
|
||||
|
||||
it("Redactions to thread responses decrement the length", async () => {
|
||||
room.client.supportsExperimentalThreads = () => true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
|
||||
const threadRoot = mkMessage();
|
||||
const threadResponse1 = mkThreadResponse(threadRoot);
|
||||
@ -2252,7 +2272,7 @@ describe("Room", function() {
|
||||
unsigned: {
|
||||
"age": 123,
|
||||
"m.relations": {
|
||||
"m.thread": {
|
||||
[THREAD_RELATION_TYPE.name]: {
|
||||
latest_event: threadResponse2.event,
|
||||
count: 2,
|
||||
current_user_participated: true,
|
||||
@ -2264,10 +2284,36 @@ describe("Room", function() {
|
||||
let prom = emitPromise(room, ThreadEvent.New);
|
||||
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2]);
|
||||
const thread = await prom;
|
||||
await emitPromise(room, ThreadEvent.Update);
|
||||
|
||||
expect(thread).toHaveLength(2);
|
||||
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
|
||||
|
||||
thread.timelineSet.addEventToTimeline(
|
||||
threadResponse1,
|
||||
thread.liveTimeline,
|
||||
{ toStartOfTimeline: true, fromCache: false, roomState: thread.roomState },
|
||||
);
|
||||
thread.timelineSet.addEventToTimeline(
|
||||
threadResponse2,
|
||||
thread.liveTimeline,
|
||||
{ toStartOfTimeline: true, fromCache: false, roomState: thread.roomState },
|
||||
);
|
||||
|
||||
room.client.fetchRoomEvent = (eventId: string) => Promise.resolve({
|
||||
...threadRoot.event,
|
||||
unsigned: {
|
||||
"age": 123,
|
||||
"m.relations": {
|
||||
[THREAD_RELATION_TYPE.name]: {
|
||||
latest_event: threadResponse2.event,
|
||||
count: 1,
|
||||
current_user_participated: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
prom = emitPromise(thread, ThreadEvent.Update);
|
||||
const threadResponse1Redaction = mkRedaction(threadResponse1);
|
||||
room.addLiveEvents([threadResponse1Redaction]);
|
||||
@ -2278,6 +2324,7 @@ describe("Room", function() {
|
||||
|
||||
it("Redactions to reactions in threads do not decrement the length", async () => {
|
||||
room.client.supportsExperimentalThreads = () => true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
|
||||
const threadRoot = mkMessage();
|
||||
const threadResponse1 = mkThreadResponse(threadRoot);
|
||||
@ -2291,7 +2338,7 @@ describe("Room", function() {
|
||||
unsigned: {
|
||||
"age": 123,
|
||||
"m.relations": {
|
||||
"m.thread": {
|
||||
[THREAD_RELATION_TYPE.name]: {
|
||||
latest_event: threadResponse2.event,
|
||||
count: 2,
|
||||
current_user_participated: true,
|
||||
@ -2303,6 +2350,7 @@ describe("Room", function() {
|
||||
const prom = emitPromise(room, ThreadEvent.New);
|
||||
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2, threadResponse2Reaction]);
|
||||
const thread = await prom;
|
||||
await emitPromise(room, ThreadEvent.Update);
|
||||
|
||||
expect(thread).toHaveLength(2);
|
||||
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
|
||||
@ -2315,6 +2363,7 @@ describe("Room", function() {
|
||||
|
||||
it("should not decrement the length when the thread root is redacted", async () => {
|
||||
room.client.supportsExperimentalThreads = () => true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
|
||||
const threadRoot = mkMessage();
|
||||
const threadResponse1 = mkThreadResponse(threadRoot);
|
||||
@ -2328,7 +2377,7 @@ describe("Room", function() {
|
||||
unsigned: {
|
||||
"age": 123,
|
||||
"m.relations": {
|
||||
"m.thread": {
|
||||
[THREAD_RELATION_TYPE.name]: {
|
||||
latest_event: threadResponse2.event,
|
||||
count: 2,
|
||||
current_user_participated: true,
|
||||
@ -2340,6 +2389,7 @@ describe("Room", function() {
|
||||
let prom = emitPromise(room, ThreadEvent.New);
|
||||
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2, threadResponse2Reaction]);
|
||||
const thread = await prom;
|
||||
await emitPromise(room, ThreadEvent.Update);
|
||||
|
||||
expect(thread).toHaveLength(2);
|
||||
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
|
||||
@ -2353,6 +2403,18 @@ describe("Room", function() {
|
||||
|
||||
it("Redacting the lastEvent finds a new lastEvent", async () => {
|
||||
room.client.supportsExperimentalThreads = () => true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
||||
|
||||
room.client.createThreadListMessagesRequest = () => Promise.resolve({
|
||||
start: null,
|
||||
end: null,
|
||||
chunk: [],
|
||||
state: [],
|
||||
});
|
||||
|
||||
await room.createThreadsTimelineSets();
|
||||
await room.fetchRoomThreads();
|
||||
|
||||
const threadRoot = mkMessage();
|
||||
const threadResponse1 = mkThreadResponse(threadRoot);
|
||||
@ -2377,21 +2439,53 @@ describe("Room", function() {
|
||||
let prom = emitPromise(room, ThreadEvent.New);
|
||||
room.addLiveEvents([threadRoot, threadResponse1, threadResponse2]);
|
||||
const thread = await prom;
|
||||
await emitPromise(room, ThreadEvent.Update);
|
||||
|
||||
expect(thread).toHaveLength(2);
|
||||
expect(thread.replyToEvent.getId()).toBe(threadResponse2.getId());
|
||||
|
||||
room.client.fetchRoomEvent = (eventId: string) => Promise.resolve({
|
||||
...threadRoot.event,
|
||||
unsigned: {
|
||||
"age": 123,
|
||||
"m.relations": {
|
||||
"m.thread": {
|
||||
latest_event: threadResponse1.event,
|
||||
count: 1,
|
||||
current_user_participated: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
prom = emitPromise(room, ThreadEvent.Update);
|
||||
const threadResponse2Redaction = mkRedaction(threadResponse2);
|
||||
room.addLiveEvents([threadResponse2Redaction]);
|
||||
await prom;
|
||||
await emitPromise(room, ThreadEvent.Update);
|
||||
expect(thread).toHaveLength(1);
|
||||
expect(thread.replyToEvent.getId()).toBe(threadResponse1.getId());
|
||||
|
||||
prom = emitPromise(room, ThreadEvent.Update);
|
||||
room.client.fetchRoomEvent = (eventId: string) => Promise.resolve({
|
||||
...threadRoot.event,
|
||||
unsigned: {
|
||||
"age": 123,
|
||||
"m.relations": {
|
||||
"m.thread": {
|
||||
latest_event: threadRoot.event,
|
||||
count: 0,
|
||||
current_user_participated: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
prom = emitPromise(room, ThreadEvent.Delete);
|
||||
const prom2 = emitPromise(room, RoomEvent.Timeline);
|
||||
const threadResponse1Redaction = mkRedaction(threadResponse1);
|
||||
room.addLiveEvents([threadResponse1Redaction]);
|
||||
await prom;
|
||||
await prom2;
|
||||
expect(thread).toHaveLength(0);
|
||||
expect(thread.replyToEvent.getId()).toBe(threadRoot.getId());
|
||||
});
|
||||
@ -2400,6 +2494,7 @@ describe("Room", function() {
|
||||
describe("eventShouldLiveIn", () => {
|
||||
const client = new TestClient(userA).client;
|
||||
client.supportsExperimentalThreads = () => true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
const room = new Room(roomId, client, userA);
|
||||
|
||||
it("thread root and its relations&redactions should be in both", () => {
|
||||
|
357
src/client.ts
357
src/client.ts
@ -36,7 +36,7 @@ import { CallEvent, CallEventHandlerMap, createNewMatrixCall, MatrixCall, suppor
|
||||
import { Filter, IFilterDefinition, IRoomEventFilter } from "./filter";
|
||||
import { CallEventHandlerEvent, CallEventHandler, CallEventHandlerEventHandlerMap } from './webrtc/callEventHandler';
|
||||
import * as utils from './utils';
|
||||
import { QueryDict, sleep } from './utils';
|
||||
import { replaceParam, QueryDict, sleep } from './utils';
|
||||
import { Direction, EventTimeline } from "./models/event-timeline";
|
||||
import { IActionsObject, PushProcessor } from "./pushprocessor";
|
||||
import { AutoDiscovery, AutoDiscoveryAction } from "./autodiscovery";
|
||||
@ -193,7 +193,14 @@ import { TypedEventEmitter } from "./models/typed-event-emitter";
|
||||
import { ReceiptType } from "./@types/read_receipts";
|
||||
import { MSC3575SlidingSyncRequest, MSC3575SlidingSyncResponse, SlidingSync } from "./sliding-sync";
|
||||
import { SlidingSyncSdk } from "./sliding-sync-sdk";
|
||||
import { FeatureSupport, Thread, THREAD_RELATION_TYPE, determineFeatureSupport } from "./models/thread";
|
||||
import {
|
||||
FeatureSupport,
|
||||
Thread,
|
||||
THREAD_RELATION_TYPE,
|
||||
determineFeatureSupport,
|
||||
ThreadFilterType,
|
||||
threadFilterTypeToFilter,
|
||||
} from "./models/thread";
|
||||
import { MBeaconInfoEventContent, M_BEACON_INFO } from "./@types/beacon";
|
||||
import { UnstableValue } from "./NamespacedValue";
|
||||
import { ToDeviceMessageQueue } from "./ToDeviceMessageQueue";
|
||||
@ -1192,9 +1199,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
const support = this.canSupport.get(Feature.ThreadUnreadNotifications);
|
||||
UNREAD_THREAD_NOTIFICATIONS.setPreferUnstable(support === ServerSupport.Unstable);
|
||||
|
||||
const { threads, list } = await this.doesServerSupportThread();
|
||||
const { threads, list, fwdPagination } = await this.doesServerSupportThread();
|
||||
Thread.setServerSideSupport(threads);
|
||||
Thread.setServerSideListSupport(list);
|
||||
Thread.setServerSideFwdPaginationSupport(fwdPagination);
|
||||
|
||||
// shallow-copy the opts dict before modifying and storing it
|
||||
this.clientOpts = Object.assign({}, opts) as IStoredClientOpts;
|
||||
@ -5171,6 +5179,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
return timelineSet.getTimelineForEvent(eventId);
|
||||
}
|
||||
|
||||
if (timelineSet.thread && this.supportsExperimentalThreads()) {
|
||||
return this.getThreadTimeline(timelineSet, eventId);
|
||||
}
|
||||
|
||||
const path = utils.encodeUri(
|
||||
"/rooms/$roomId/context/$eventId", {
|
||||
$roomId: timelineSet.room.roomId,
|
||||
@ -5196,6 +5208,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
const mapper = this.getEventMapper();
|
||||
const event = mapper(res.event);
|
||||
if (event.isRelation(THREAD_RELATION_TYPE.name)) {
|
||||
logger.warn("Tried loading a regular timeline at the position of a thread event");
|
||||
return undefined;
|
||||
}
|
||||
const events = [
|
||||
// Order events from most recent to oldest (reverse-chronological).
|
||||
// We start with the last event, since that's the point at which we have known state.
|
||||
@ -5205,38 +5221,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
...res.events_before.map(mapper),
|
||||
];
|
||||
|
||||
if (this.supportsExperimentalThreads()) {
|
||||
if (!timelineSet.canContain(event)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Where the event is a thread reply (not a root) and running in MSC-enabled mode the Thread timeline only
|
||||
// functions contiguously, so we have to jump through some hoops to get our target event in it.
|
||||
// XXX: workaround for https://github.com/vector-im/element-meta/issues/150
|
||||
if (Thread.hasServerSideSupport && timelineSet.thread) {
|
||||
const thread = timelineSet.thread;
|
||||
const opts: IRelationsRequestOpts = {
|
||||
dir: Direction.Backward,
|
||||
limit: 50,
|
||||
};
|
||||
|
||||
await thread.fetchInitialEvents();
|
||||
let nextBatch: string | null | undefined = thread.liveTimeline.getPaginationToken(Direction.Backward);
|
||||
|
||||
// Fetch events until we find the one we were asked for, or we run out of pages
|
||||
while (!thread.findEventById(eventId)) {
|
||||
if (nextBatch) {
|
||||
opts.from = nextBatch;
|
||||
}
|
||||
|
||||
({ nextBatch } = await thread.fetchEvents(opts));
|
||||
if (!nextBatch) break;
|
||||
}
|
||||
|
||||
return thread.liveTimeline;
|
||||
}
|
||||
}
|
||||
|
||||
// Here we handle non-thread timelines only, but still process any thread events to populate thread summaries.
|
||||
let timeline = timelineSet.getTimelineForEvent(events[0].getId()!);
|
||||
if (timeline) {
|
||||
@ -5261,6 +5245,154 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
?? timeline;
|
||||
}
|
||||
|
||||
public async getThreadTimeline(timelineSet: EventTimelineSet, eventId: string): Promise<EventTimeline | undefined> {
|
||||
if (!this.supportsExperimentalThreads()) {
|
||||
throw new Error("could not get thread timeline: no client support");
|
||||
}
|
||||
|
||||
if (!timelineSet.room) {
|
||||
throw new Error("could not get thread timeline: not a room timeline");
|
||||
}
|
||||
|
||||
if (!timelineSet.thread) {
|
||||
throw new Error("could not get thread timeline: not a thread timeline");
|
||||
}
|
||||
|
||||
const path = utils.encodeUri(
|
||||
"/rooms/$roomId/context/$eventId", {
|
||||
$roomId: timelineSet.room.roomId,
|
||||
$eventId: eventId,
|
||||
},
|
||||
);
|
||||
|
||||
const params: Record<string, string | string[]> = {
|
||||
limit: "0",
|
||||
};
|
||||
if (this.clientOpts?.lazyLoadMembers) {
|
||||
params.filter = JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER);
|
||||
}
|
||||
|
||||
// TODO: we should implement a backoff (as per scrollback()) to deal more nicely with HTTP errors.
|
||||
const res = await this.http.authedRequest<IContextResponse>(Method.Get, path, params);
|
||||
const mapper = this.getEventMapper();
|
||||
const event = mapper(res.event);
|
||||
|
||||
if (!timelineSet.canContain(event)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (Thread.hasServerSideSupport) {
|
||||
if (Thread.hasServerSideFwdPaginationSupport) {
|
||||
if (!timelineSet.thread) {
|
||||
throw new Error("could not get thread timeline: not a thread timeline");
|
||||
}
|
||||
|
||||
const thread = timelineSet.thread;
|
||||
const resOlder: IRelationsResponse = await this.fetchRelations(
|
||||
timelineSet.room.roomId,
|
||||
thread.id,
|
||||
THREAD_RELATION_TYPE.name,
|
||||
null,
|
||||
{ dir: Direction.Backward, from: res.start },
|
||||
);
|
||||
const resNewer: IRelationsResponse = await this.fetchRelations(
|
||||
timelineSet.room.roomId,
|
||||
thread.id,
|
||||
THREAD_RELATION_TYPE.name,
|
||||
null,
|
||||
{ dir: Direction.Forward, from: res.end },
|
||||
);
|
||||
const events = [
|
||||
// Order events from most recent to oldest (reverse-chronological).
|
||||
// We start with the last event, since that's the point at which we have known state.
|
||||
// events_after is already backwards; events_before is forwards.
|
||||
...resNewer.chunk.reverse().map(mapper),
|
||||
event,
|
||||
...resOlder.chunk.map(mapper),
|
||||
];
|
||||
for (const event of events) {
|
||||
await timelineSet.thread?.processEvent(event);
|
||||
}
|
||||
|
||||
// Here we handle non-thread timelines only, but still process any thread events to populate thread summaries.
|
||||
let timeline = timelineSet.getTimelineForEvent(event.getId());
|
||||
if (timeline) {
|
||||
timeline.getState(EventTimeline.BACKWARDS).setUnknownStateEvents(res.state.map(mapper));
|
||||
} else {
|
||||
timeline = timelineSet.addTimeline();
|
||||
timeline.initialiseState(res.state.map(mapper));
|
||||
}
|
||||
|
||||
timelineSet.addEventsToTimeline(events, true, timeline, resNewer.next_batch);
|
||||
if (!resOlder.next_batch) {
|
||||
timelineSet.addEventsToTimeline([mapper(resOlder.original_event)], true, timeline, null);
|
||||
}
|
||||
timeline.setPaginationToken(resOlder.next_batch ?? null, Direction.Backward);
|
||||
timeline.setPaginationToken(resNewer.next_batch ?? null, Direction.Forward);
|
||||
this.processBeaconEvents(timelineSet.room, events);
|
||||
|
||||
// There is no guarantee that the event ended up in "timeline" (we might have switched to a neighbouring
|
||||
// timeline) - so check the room's index again. On the other hand, there's no guarantee the event ended up
|
||||
// anywhere, if it was later redacted, so we just return the timeline we first thought of.
|
||||
return timelineSet.getTimelineForEvent(eventId)
|
||||
?? timeline;
|
||||
} else {
|
||||
// Where the event is a thread reply (not a root) and running in MSC-enabled mode the Thread timeline only
|
||||
// functions contiguously, so we have to jump through some hoops to get our target event in it.
|
||||
// XXX: workaround for https://github.com/vector-im/element-meta/issues/150
|
||||
|
||||
const thread = timelineSet.thread;
|
||||
|
||||
const resOlder = await this.fetchRelations(
|
||||
timelineSet.room.roomId,
|
||||
thread.id,
|
||||
THREAD_RELATION_TYPE.name,
|
||||
null,
|
||||
{ dir: Direction.Backward, from: res.start },
|
||||
);
|
||||
const eventsNewer: IEvent[] = [];
|
||||
let nextBatch: Optional<string> = res.end;
|
||||
while (nextBatch) {
|
||||
const resNewer: IRelationsResponse = await this.fetchRelations(
|
||||
timelineSet.room.roomId,
|
||||
thread.id,
|
||||
THREAD_RELATION_TYPE.name,
|
||||
null,
|
||||
{ dir: Direction.Forward, from: nextBatch },
|
||||
);
|
||||
nextBatch = resNewer.next_batch ?? null;
|
||||
eventsNewer.push(...resNewer.chunk);
|
||||
}
|
||||
const events = [
|
||||
// Order events from most recent to oldest (reverse-chronological).
|
||||
// We start with the last event, since that's the point at which we have known state.
|
||||
// events_after is already backwards; events_before is forwards.
|
||||
...eventsNewer.reverse().map(mapper),
|
||||
event,
|
||||
...resOlder.chunk.map(mapper),
|
||||
];
|
||||
for (const event of events) {
|
||||
await timelineSet.thread?.processEvent(event);
|
||||
}
|
||||
|
||||
// Here we handle non-thread timelines only, but still process any thread events to populate thread
|
||||
// summaries.
|
||||
const timeline = timelineSet.getLiveTimeline();
|
||||
timeline.getState(EventTimeline.BACKWARDS).setUnknownStateEvents(res.state.map(mapper));
|
||||
|
||||
timelineSet.addEventsToTimeline(events, true, timeline, null);
|
||||
if (!resOlder.next_batch) {
|
||||
timelineSet.addEventsToTimeline([mapper(resOlder.original_event)], true, timeline, null);
|
||||
}
|
||||
timeline.setPaginationToken(resOlder.next_batch ?? null, Direction.Backward);
|
||||
timeline.setPaginationToken(null, Direction.Forward);
|
||||
this.processBeaconEvents(timelineSet.room, events);
|
||||
|
||||
return timeline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an EventTimeline for the latest events in the room. This will just
|
||||
* call `/messages` to get the latest message in the room, then use
|
||||
@ -5282,28 +5414,45 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
throw new Error("getLatestTimeline only supports room timelines");
|
||||
}
|
||||
|
||||
let res: IMessagesResponse;
|
||||
const roomId = timelineSet.room.roomId;
|
||||
if (timelineSet.isThreadTimeline) {
|
||||
res = await this.createThreadListMessagesRequest(
|
||||
roomId,
|
||||
let event;
|
||||
if (timelineSet.threadListType !== null) {
|
||||
const res = await this.createThreadListMessagesRequest(
|
||||
timelineSet.room.roomId,
|
||||
null,
|
||||
1,
|
||||
Direction.Backward,
|
||||
timelineSet.threadListType,
|
||||
timelineSet.getFilter(),
|
||||
);
|
||||
event = res.chunk?.[0];
|
||||
} else if (timelineSet.thread && Thread.hasServerSideSupport) {
|
||||
const res = await this.fetchRelations(
|
||||
timelineSet.room.roomId,
|
||||
timelineSet.thread.id,
|
||||
THREAD_RELATION_TYPE.name,
|
||||
null,
|
||||
{ dir: Direction.Backward, limit: 1 },
|
||||
);
|
||||
event = res.chunk?.[0];
|
||||
} else {
|
||||
res = await this.createMessagesRequest(
|
||||
roomId,
|
||||
null,
|
||||
1,
|
||||
Direction.Backward,
|
||||
timelineSet.getFilter(),
|
||||
const messagesPath = utils.encodeUri(
|
||||
"/rooms/$roomId/messages", {
|
||||
$roomId: timelineSet.room.roomId,
|
||||
},
|
||||
);
|
||||
|
||||
const params: Record<string, string | string[]> = {
|
||||
dir: 'b',
|
||||
};
|
||||
if (this.clientOpts?.lazyLoadMembers) {
|
||||
params.filter = JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER);
|
||||
}
|
||||
|
||||
const res = await this.http.authedRequest<IMessagesResponse>(Method.Get, messagesPath, params);
|
||||
event = res.chunk?.[0];
|
||||
}
|
||||
const event = res.chunk?.[0];
|
||||
if (!event) {
|
||||
throw new Error("No message returned from /messages when trying to construct getLatestTimeline");
|
||||
throw new Error("No message returned when trying to construct getLatestTimeline");
|
||||
}
|
||||
|
||||
return this.getEventTimeline(timelineSet, event.event_id);
|
||||
@ -5376,6 +5525,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
fromToken: string | null,
|
||||
limit = 30,
|
||||
dir = Direction.Backward,
|
||||
threadListType: ThreadFilterType | null = ThreadFilterType.All,
|
||||
timelineFilter?: Filter,
|
||||
): Promise<IMessagesResponse> {
|
||||
const path = utils.encodeUri("/rooms/$roomId/threads", { $roomId: roomId });
|
||||
@ -5383,7 +5533,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
const params: Record<string, string> = {
|
||||
limit: limit.toString(),
|
||||
dir: dir,
|
||||
include: 'all',
|
||||
include: threadFilterTypeToFilter(threadListType),
|
||||
};
|
||||
|
||||
if (fromToken) {
|
||||
@ -5395,7 +5545,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
// create a shallow copy of LAZY_LOADING_MESSAGES_FILTER,
|
||||
// so the timelineFilter doesn't get written into it below
|
||||
filter = {
|
||||
...filter,
|
||||
...Filter.LAZY_LOADING_MESSAGES_FILTER,
|
||||
};
|
||||
}
|
||||
@ -5411,14 +5560,16 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
params.filter = JSON.stringify(filter);
|
||||
}
|
||||
|
||||
const opts: { prefix?: string } = {};
|
||||
if (Thread.hasServerSideListSupport === FeatureSupport.Experimental) {
|
||||
opts.prefix = "/_matrix/client/unstable/org.matrix.msc3856";
|
||||
}
|
||||
const opts = {
|
||||
prefix: Thread.hasServerSideListSupport === FeatureSupport.Stable
|
||||
? "/_matrix/client/v1"
|
||||
: "/_matrix/client/unstable/org.matrix.msc3856",
|
||||
};
|
||||
|
||||
return this.http.authedRequest<IThreadedMessagesResponse>(Method.Get, path, params, undefined, opts)
|
||||
.then(res => ({
|
||||
...res,
|
||||
chunk: res.chunk?.reverse(),
|
||||
start: res.prev_batch,
|
||||
end: res.next_batch,
|
||||
}));
|
||||
@ -5440,7 +5591,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
public paginateEventTimeline(eventTimeline: EventTimeline, opts: IPaginateOpts): Promise<boolean> {
|
||||
const isNotifTimeline = (eventTimeline.getTimelineSet() === this.notifTimelineSet);
|
||||
const room = this.getRoom(eventTimeline.getRoomId()!);
|
||||
const isThreadTimeline = eventTimeline.getTimelineSet().isThreadTimeline;
|
||||
const threadListType = eventTimeline.getTimelineSet().threadListType;
|
||||
const thread = eventTimeline.getTimelineSet().thread;
|
||||
|
||||
// TODO: we should implement a backoff (as per scrollback()) to deal more
|
||||
// nicely with HTTP errors.
|
||||
@ -5511,16 +5663,21 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
eventTimeline.paginationRequests[dir] = null;
|
||||
});
|
||||
eventTimeline.paginationRequests[dir] = promise;
|
||||
} else if (isThreadTimeline) {
|
||||
} else if (threadListType !== null) {
|
||||
if (!room) {
|
||||
throw new Error("Unknown room " + eventTimeline.getRoomId());
|
||||
}
|
||||
|
||||
if (!Thread.hasServerSideFwdPaginationSupport && dir === Direction.Forward) {
|
||||
throw new Error("Cannot paginate threads forwards without server-side support for MSC 3715");
|
||||
}
|
||||
|
||||
promise = this.createThreadListMessagesRequest(
|
||||
eventTimeline.getRoomId()!,
|
||||
token,
|
||||
opts.limit,
|
||||
dir,
|
||||
threadListType,
|
||||
eventTimeline.getFilter(),
|
||||
).then((res) => {
|
||||
if (res.state) {
|
||||
@ -5547,6 +5704,45 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
eventTimeline.paginationRequests[dir] = null;
|
||||
});
|
||||
eventTimeline.paginationRequests[dir] = promise;
|
||||
} else if (thread) {
|
||||
const room = this.getRoom(eventTimeline.getRoomId() ?? undefined);
|
||||
if (!room) {
|
||||
throw new Error("Unknown room " + eventTimeline.getRoomId());
|
||||
}
|
||||
|
||||
promise = this.fetchRelations(
|
||||
eventTimeline.getRoomId() ?? "",
|
||||
thread.id,
|
||||
THREAD_RELATION_TYPE.name,
|
||||
null,
|
||||
{ dir, limit: opts.limit, from: token ?? undefined },
|
||||
).then(async (res) => {
|
||||
const mapper = this.getEventMapper();
|
||||
const matrixEvents = res.chunk.map(mapper);
|
||||
for (const event of matrixEvents) {
|
||||
await eventTimeline.getTimelineSet()?.thread?.processEvent(event);
|
||||
}
|
||||
|
||||
const newToken = res.next_batch;
|
||||
|
||||
const timelineSet = eventTimeline.getTimelineSet();
|
||||
timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, newToken ?? null);
|
||||
if (!newToken && backwards) {
|
||||
timelineSet.addEventsToTimeline([mapper(res.original_event)], true, eventTimeline, null);
|
||||
}
|
||||
this.processBeaconEvents(timelineSet.room, matrixEvents);
|
||||
|
||||
// if we've hit the end of the timeline, we need to stop trying to
|
||||
// paginate. We need to keep the 'forwards' token though, to make sure
|
||||
// we can recover from gappy syncs.
|
||||
if (backwards && !newToken) {
|
||||
eventTimeline.setPaginationToken(null, dir);
|
||||
}
|
||||
return Boolean(newToken);
|
||||
}).finally(() => {
|
||||
eventTimeline.paginationRequests[dir] = null;
|
||||
});
|
||||
eventTimeline.paginationRequests[dir] = promise;
|
||||
} else {
|
||||
if (!room) {
|
||||
throw new Error("Unknown room " + eventTimeline.getRoomId());
|
||||
@ -5568,10 +5764,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
const matrixEvents = res.chunk.map(this.getEventMapper());
|
||||
|
||||
const timelineSet = eventTimeline.getTimelineSet();
|
||||
const [timelineEvents, threadedEvents] = room.partitionThreadedEvents(matrixEvents);
|
||||
const [timelineEvents] = room.partitionThreadedEvents(matrixEvents);
|
||||
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
|
||||
this.processBeaconEvents(room, timelineEvents);
|
||||
this.processThreadEvents(room, threadedEvents, backwards);
|
||||
this.processThreadRoots(room,
|
||||
timelineEvents.filter(it => it.isRelation(THREAD_RELATION_TYPE.name)),
|
||||
false);
|
||||
|
||||
const atEnd = res.end === undefined || res.end === res.start;
|
||||
|
||||
@ -6654,25 +6852,40 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
public async doesServerSupportThread(): Promise<{
|
||||
threads: FeatureSupport;
|
||||
list: FeatureSupport;
|
||||
fwdPagination: FeatureSupport;
|
||||
}> {
|
||||
if (await this.isVersionSupported("v1.4")) {
|
||||
return {
|
||||
threads: FeatureSupport.Stable,
|
||||
list: FeatureSupport.Stable,
|
||||
fwdPagination: FeatureSupport.Stable,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const [threadUnstable, threadStable, listUnstable, listStable] = await Promise.all([
|
||||
const [
|
||||
threadUnstable, threadStable,
|
||||
listUnstable, listStable,
|
||||
fwdPaginationUnstable, fwdPaginationStable,
|
||||
] = await Promise.all([
|
||||
this.doesServerSupportUnstableFeature("org.matrix.msc3440"),
|
||||
this.doesServerSupportUnstableFeature("org.matrix.msc3440.stable"),
|
||||
this.doesServerSupportUnstableFeature("org.matrix.msc3856"),
|
||||
this.doesServerSupportUnstableFeature("org.matrix.msc3856.stable"),
|
||||
this.doesServerSupportUnstableFeature("org.matrix.msc3715"),
|
||||
this.doesServerSupportUnstableFeature("org.matrix.msc3715.stable"),
|
||||
]);
|
||||
|
||||
// TODO: Use `this.isVersionSupported("v1.3")` for whatever spec version includes MSC3440 formally.
|
||||
|
||||
return {
|
||||
threads: determineFeatureSupport(threadStable, threadUnstable),
|
||||
list: determineFeatureSupport(listStable, listUnstable),
|
||||
fwdPagination: determineFeatureSupport(fwdPaginationStable, fwdPaginationUnstable),
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
threads: FeatureSupport.None,
|
||||
list: FeatureSupport.None,
|
||||
fwdPagination: FeatureSupport.None,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -6732,12 +6945,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
eventType?: EventType | string | null,
|
||||
opts: IRelationsRequestOpts = { dir: Direction.Backward },
|
||||
): Promise<{
|
||||
originalEvent?: MatrixEvent;
|
||||
originalEvent?: MatrixEvent | null;
|
||||
events: MatrixEvent[];
|
||||
nextBatch?: string;
|
||||
prevBatch?: string;
|
||||
nextBatch?: string | null;
|
||||
prevBatch?: string | null;
|
||||
}> {
|
||||
const fetchedEventType = this.getEncryptedIfNeededEventType(roomId, eventType);
|
||||
const fetchedEventType = eventType ? this.getEncryptedIfNeededEventType(roomId, eventType) : null;
|
||||
const result = await this.fetchRelations(
|
||||
roomId,
|
||||
eventId,
|
||||
@ -6761,10 +6974,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
events = events.filter(e => e.getSender() === originalEvent.getSender());
|
||||
}
|
||||
return {
|
||||
originalEvent,
|
||||
originalEvent: originalEvent ?? null,
|
||||
events,
|
||||
nextBatch: result.next_batch,
|
||||
prevBatch: result.prev_batch,
|
||||
nextBatch: result.next_batch ?? null,
|
||||
prevBatch: result.prev_batch ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
@ -7281,7 +7494,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
eventType?: EventType | string | null,
|
||||
opts: IRelationsRequestOpts = { dir: Direction.Backward },
|
||||
): Promise<IRelationsResponse> {
|
||||
const queryString = utils.encodeParams(opts as Record<string, string | number>);
|
||||
let params = opts as QueryDict;
|
||||
if (Thread.hasServerSideFwdPaginationSupport === FeatureSupport.Experimental) {
|
||||
params = replaceParam("dir", "org.matrix.msc3715.dir", params);
|
||||
}
|
||||
const queryString = utils.encodeParams(params);
|
||||
|
||||
let templatedUrl = "/rooms/$roomId/relations/$eventId";
|
||||
if (relationType !== null) {
|
||||
@ -7327,7 +7544,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @return {Promise} Resolves to an object containing the event.
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
public fetchRoomEvent(roomId: string, eventId: string): Promise<IMinimalEvent> {
|
||||
public fetchRoomEvent(roomId: string, eventId: string): Promise<Partial<IEvent>> {
|
||||
const path = utils.encodeUri(
|
||||
"/rooms/$roomId/event/$eventId", {
|
||||
$roomId: roomId,
|
||||
|
@ -27,7 +27,7 @@ import { RoomState } from "./room-state";
|
||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||
import { RelationsContainer } from "./relations-container";
|
||||
import { MatrixClient } from "../client";
|
||||
import { Thread } from "./thread";
|
||||
import { Thread, ThreadFilterType } from "./thread";
|
||||
|
||||
const DEBUG = true;
|
||||
|
||||
@ -140,7 +140,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
opts: IOpts = {},
|
||||
client?: MatrixClient,
|
||||
public readonly thread?: Thread,
|
||||
public readonly isThreadTimeline: boolean = false,
|
||||
public readonly threadListType: ThreadFilterType | null = null,
|
||||
) {
|
||||
super();
|
||||
|
||||
@ -297,8 +297,8 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
* @return {?module:models/event-timeline~EventTimeline} timeline containing
|
||||
* the given event, or null if unknown
|
||||
*/
|
||||
public getTimelineForEvent(eventId: string | null): EventTimeline | null {
|
||||
if (eventId === null) { return null; }
|
||||
public getTimelineForEvent(eventId?: string): EventTimeline | null {
|
||||
if (eventId === null || eventId === undefined) { return null; }
|
||||
const res = this._eventIdToTimeline.get(eventId);
|
||||
return (res === undefined) ? null : res;
|
||||
}
|
||||
@ -359,7 +359,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
events: MatrixEvent[],
|
||||
toStartOfTimeline: boolean,
|
||||
timeline: EventTimeline,
|
||||
paginationToken?: string,
|
||||
paginationToken?: string | null,
|
||||
): void {
|
||||
if (!timeline) {
|
||||
throw new Error(
|
||||
|
@ -1542,10 +1542,15 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
public setThread(thread: Thread): void {
|
||||
public setThread(thread?: Thread): void {
|
||||
if (this.thread) {
|
||||
this.reEmitter.stopReEmitting(this.thread, [ThreadEvent.Update]);
|
||||
}
|
||||
this.thread = thread;
|
||||
this.setThreadId(thread.id);
|
||||
this.reEmitter.reEmit(thread, [ThreadEvent.Update]);
|
||||
this.setThreadId(thread?.id);
|
||||
if (thread) {
|
||||
this.reEmitter.reEmit(thread, [ThreadEvent.Update]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1555,7 +1560,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
return this.thread;
|
||||
}
|
||||
|
||||
public setThreadId(threadId: string): void {
|
||||
public setThreadId(threadId?: string): void {
|
||||
this.threadId = threadId;
|
||||
}
|
||||
}
|
||||
|
@ -146,6 +146,7 @@ export type RoomEmittedEvents = RoomEvent
|
||||
| ThreadEvent.New
|
||||
| ThreadEvent.Update
|
||||
| ThreadEvent.NewReply
|
||||
| ThreadEvent.Delete
|
||||
| MatrixEventEvent.BeforeRedaction
|
||||
| BeaconEvent.New
|
||||
| BeaconEvent.Update
|
||||
@ -180,7 +181,7 @@ export type RoomEventHandlerMap = {
|
||||
[ThreadEvent.New]: (thread: Thread, toStartOfTimeline: boolean) => void;
|
||||
} & Pick<
|
||||
ThreadHandlerMap,
|
||||
ThreadEvent.Update | ThreadEvent.NewReply
|
||||
ThreadEvent.Update | ThreadEvent.NewReply | ThreadEvent.Delete
|
||||
>
|
||||
& EventTimelineSetHandlerMap
|
||||
& Pick<MatrixEventHandlerMap, MatrixEventEvent.BeforeRedaction>
|
||||
@ -1006,7 +1007,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
* timeline which would otherwise be unable to paginate forwards without this token).
|
||||
* Removing just the old live timeline whilst preserving previous ones is not supported.
|
||||
*/
|
||||
public resetLiveTimeline(backPaginationToken: string | null, forwardPaginationToken: string | null): void {
|
||||
public resetLiveTimeline(backPaginationToken?: string | null, forwardPaginationToken?: string | null): void {
|
||||
for (let i = 0; i < this.timelineSets.length; i++) {
|
||||
this.timelineSets[i].resetLiveTimeline(
|
||||
backPaginationToken ?? undefined,
|
||||
@ -1651,7 +1652,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
let timelineSet: EventTimelineSet;
|
||||
if (Thread.hasServerSideListSupport) {
|
||||
timelineSet =
|
||||
new EventTimelineSet(this, this.opts, undefined, undefined, Boolean(Thread.hasServerSideListSupport));
|
||||
new EventTimelineSet(this, this.opts, undefined, undefined, filterType ?? ThreadFilterType.All);
|
||||
this.reEmitter.reEmit(timelineSet, [
|
||||
RoomEvent.Timeline,
|
||||
RoomEvent.TimelineReset,
|
||||
@ -1758,7 +1759,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
let latestMyThreadsRootEvent: MatrixEvent | undefined;
|
||||
const roomState = this.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
||||
for (const rootEvent of threadRoots) {
|
||||
this.threadsTimelineSets[0].addLiveEvent(rootEvent, {
|
||||
this.threadsTimelineSets[0]?.addLiveEvent(rootEvent, {
|
||||
duplicateStrategy: DuplicateStrategy.Ignore,
|
||||
fromCache: false,
|
||||
roomState,
|
||||
@ -1767,7 +1768,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
const threadRelationship = rootEvent
|
||||
.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name);
|
||||
if (threadRelationship?.current_user_participated) {
|
||||
this.threadsTimelineSets[1].addLiveEvent(rootEvent, {
|
||||
this.threadsTimelineSets[1]?.addLiveEvent(rootEvent, {
|
||||
duplicateStrategy: DuplicateStrategy.Ignore,
|
||||
fromCache: false,
|
||||
roomState,
|
||||
@ -1785,6 +1786,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
}
|
||||
|
||||
this.on(ThreadEvent.NewReply, this.onThreadNewReply);
|
||||
this.on(ThreadEvent.Delete, this.onThreadDelete);
|
||||
this.threadsReady = true;
|
||||
}
|
||||
|
||||
@ -1803,6 +1805,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
null,
|
||||
undefined,
|
||||
Direction.Backward,
|
||||
timelineSet.threadListType,
|
||||
timelineSet.getFilter(),
|
||||
);
|
||||
|
||||
@ -1823,14 +1826,21 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
}
|
||||
|
||||
private onThreadNewReply(thread: Thread): void {
|
||||
const roomState = this.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
||||
this.updateThreadRootEvents(thread, false);
|
||||
}
|
||||
|
||||
private onThreadDelete(thread: Thread): void {
|
||||
this.threads.delete(thread.id);
|
||||
|
||||
const timeline = this.getTimelineForEvent(thread.id);
|
||||
const roomEvent = timeline?.getEvents()?.find(it => it.getId() === thread.id);
|
||||
if (roomEvent) {
|
||||
thread.clearEventMetadata(roomEvent);
|
||||
} else {
|
||||
logger.debug("onThreadDelete: Could not find root event in room timeline");
|
||||
}
|
||||
for (const timelineSet of this.threadsTimelineSets) {
|
||||
timelineSet.removeEvent(thread.id);
|
||||
timelineSet.addLiveEvent(thread.rootEvent, {
|
||||
duplicateStrategy: DuplicateStrategy.Replace,
|
||||
fromCache: false,
|
||||
roomState,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1912,13 +1922,12 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
private addThreadedEvents(threadId: string, events: MatrixEvent[], toStartOfTimeline = false): void {
|
||||
let thread = this.getThread(threadId);
|
||||
|
||||
if (thread) {
|
||||
thread.addEvents(events, toStartOfTimeline);
|
||||
} else {
|
||||
if (!thread) {
|
||||
const rootEvent = this.findEventById(threadId) ?? events.find(e => e.getId() === threadId);
|
||||
thread = this.createThread(threadId, rootEvent, events, toStartOfTimeline);
|
||||
this.emit(ThreadEvent.Update, thread);
|
||||
}
|
||||
|
||||
thread.addEvents(events, toStartOfTimeline);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1942,6 +1951,37 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
));
|
||||
}
|
||||
|
||||
private updateThreadRootEvents = (thread: Thread, toStartOfTimeline: boolean) => {
|
||||
if (thread.length) {
|
||||
this.updateThreadRootEvent(this.threadsTimelineSets?.[0], thread, toStartOfTimeline);
|
||||
if (thread.hasCurrentUserParticipated) {
|
||||
this.updateThreadRootEvent(this.threadsTimelineSets?.[1], thread, toStartOfTimeline);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private updateThreadRootEvent = (
|
||||
timelineSet: Optional<EventTimelineSet>,
|
||||
thread: Thread,
|
||||
toStartOfTimeline: boolean,
|
||||
) => {
|
||||
if (timelineSet && thread.rootEvent) {
|
||||
if (Thread.hasServerSideSupport) {
|
||||
timelineSet.addLiveEvent(thread.rootEvent, {
|
||||
duplicateStrategy: DuplicateStrategy.Replace,
|
||||
fromCache: false,
|
||||
roomState: this.currentState,
|
||||
});
|
||||
} else {
|
||||
timelineSet.addEventToTimeline(
|
||||
thread.rootEvent,
|
||||
timelineSet.getLiveTimeline(),
|
||||
{ toStartOfTimeline },
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public createThread(
|
||||
threadId: string,
|
||||
rootEvent: MatrixEvent | undefined,
|
||||
@ -1958,38 +1998,37 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
}
|
||||
|
||||
const thread = new Thread(threadId, rootEvent, {
|
||||
initialEvents: events,
|
||||
room: this,
|
||||
client: this.client,
|
||||
});
|
||||
|
||||
// This is necessary to be able to jump to events in threads:
|
||||
// If we jump to an event in a thread where neither the event, nor the root,
|
||||
// nor any thread event are loaded yet, we'll load the event as well as the thread root, create the thread,
|
||||
// and pass the event through this.
|
||||
for (const event of events) {
|
||||
thread.setEventMetadata(event);
|
||||
}
|
||||
|
||||
// If we managed to create a thread and figure out its `id` then we can use it
|
||||
this.threads.set(thread.id, thread);
|
||||
this.reEmitter.reEmit(thread, [
|
||||
ThreadEvent.Delete,
|
||||
ThreadEvent.Update,
|
||||
ThreadEvent.NewReply,
|
||||
RoomEvent.Timeline,
|
||||
RoomEvent.TimelineReset,
|
||||
]);
|
||||
const isNewer = this.lastThread?.rootEvent
|
||||
&& rootEvent?.localTimestamp
|
||||
&& this.lastThread.rootEvent?.localTimestamp < rootEvent?.localTimestamp;
|
||||
|
||||
if (!this.lastThread || this.lastThread.rootEvent?.localTimestamp < rootEvent?.localTimestamp) {
|
||||
if (!this.lastThread || isNewer) {
|
||||
this.lastThread = thread;
|
||||
}
|
||||
|
||||
if (this.threadsReady) {
|
||||
this.threadsTimelineSets.forEach(timelineSet => {
|
||||
if (thread.rootEvent) {
|
||||
if (Thread.hasServerSideSupport) {
|
||||
timelineSet.addLiveEvent(thread.rootEvent);
|
||||
} else {
|
||||
timelineSet.addEventToTimeline(
|
||||
thread.rootEvent,
|
||||
timelineSet.getLiveTimeline(),
|
||||
toStartOfTimeline,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.updateThreadRootEvents(thread, toStartOfTimeline);
|
||||
}
|
||||
|
||||
this.emit(ThreadEvent.New, thread, toStartOfTimeline);
|
||||
|
@ -18,9 +18,8 @@ import { Optional } from "matrix-events-sdk";
|
||||
|
||||
import { MatrixClient, MatrixEventEvent, RelationType, RoomEvent } from "../matrix";
|
||||
import { TypedReEmitter } from "../ReEmitter";
|
||||
import { IRelationsRequestOpts } from "../@types/requests";
|
||||
import { IThreadBundledRelationship, MatrixEvent } from "./event";
|
||||
import { Direction, EventTimeline } from "./event-timeline";
|
||||
import { EventTimeline } from "./event-timeline";
|
||||
import { EventTimelineSet, EventTimelineSetHandlerMap } from './event-timeline-set';
|
||||
import { Room } from './room';
|
||||
import { RoomState } from "./room-state";
|
||||
@ -33,6 +32,7 @@ export enum ThreadEvent {
|
||||
Update = "Thread.update",
|
||||
NewReply = "Thread.newReply",
|
||||
ViewThread = "Thread.viewThread",
|
||||
Delete = "Thread.delete"
|
||||
}
|
||||
|
||||
type EmittedEvents = Exclude<ThreadEvent, ThreadEvent.New>
|
||||
@ -43,10 +43,10 @@ export type EventHandlerMap = {
|
||||
[ThreadEvent.Update]: (thread: Thread) => void;
|
||||
[ThreadEvent.NewReply]: (thread: Thread, event: MatrixEvent) => void;
|
||||
[ThreadEvent.ViewThread]: () => void;
|
||||
[ThreadEvent.Delete]: (thread: Thread) => void;
|
||||
} & EventTimelineSetHandlerMap;
|
||||
|
||||
interface IThreadOpts {
|
||||
initialEvents?: MatrixEvent[];
|
||||
room: Room;
|
||||
client: MatrixClient;
|
||||
}
|
||||
@ -73,6 +73,7 @@ export function determineFeatureSupport(stable: boolean, unstable: boolean): Fea
|
||||
export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
||||
public static hasServerSideSupport = FeatureSupport.None;
|
||||
public static hasServerSideListSupport = FeatureSupport.None;
|
||||
public static hasServerSideFwdPaginationSupport = FeatureSupport.None;
|
||||
|
||||
/**
|
||||
* A reference to all the events ID at the bottom of the threads
|
||||
@ -83,7 +84,7 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
||||
|
||||
private reEmitter: TypedReEmitter<EmittedEvents, EventHandlerMap>;
|
||||
|
||||
private lastEvent!: MatrixEvent;
|
||||
private lastEvent: MatrixEvent | undefined;
|
||||
private replyCount = 0;
|
||||
|
||||
public readonly room: Room;
|
||||
@ -122,14 +123,10 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
||||
this.room.on(RoomEvent.LocalEchoUpdated, this.onEcho);
|
||||
this.timelineSet.on(RoomEvent.Timeline, this.onEcho);
|
||||
|
||||
if (opts.initialEvents) {
|
||||
this.addEvents(opts.initialEvents, false);
|
||||
}
|
||||
// even if this thread is thought to be originating from this client, we initialise it as we may be in a
|
||||
// gappy sync and a thread around this event may already exist.
|
||||
this.initialiseThread();
|
||||
|
||||
this.rootEvent?.setThread(this);
|
||||
this.setEventMetadata(this.rootEvent);
|
||||
}
|
||||
|
||||
private async fetchRootEvent(): Promise<void> {
|
||||
@ -142,13 +139,7 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
||||
} catch (e) {
|
||||
logger.error("Failed to fetch thread root to construct thread with", e);
|
||||
}
|
||||
|
||||
// The root event might be not be visible to the person requesting it.
|
||||
// If it wasn't fetched successfully the thread will work in "limited" mode and won't
|
||||
// benefit from all the APIs a homeserver can provide to enhance the thread experience
|
||||
this.rootEvent?.setThread(this);
|
||||
|
||||
this.emit(ThreadEvent.Update, this);
|
||||
await this.processEvent(this.rootEvent);
|
||||
}
|
||||
|
||||
public static setServerSideSupport(
|
||||
@ -168,6 +159,12 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
||||
Thread.hasServerSideListSupport = status;
|
||||
}
|
||||
|
||||
public static setServerSideFwdPaginationSupport(
|
||||
status: FeatureSupport,
|
||||
): void {
|
||||
Thread.hasServerSideFwdPaginationSupport = status;
|
||||
}
|
||||
|
||||
private onBeforeRedaction = (event: MatrixEvent, redaction: MatrixEvent) => {
|
||||
if (event?.isRelation(THREAD_RELATION_TYPE.name) &&
|
||||
this.room.eventShouldLiveIn(event).threadId === this.id &&
|
||||
@ -179,42 +176,27 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
||||
}
|
||||
};
|
||||
|
||||
private onRedaction = (event: MatrixEvent) => {
|
||||
private onRedaction = async (event: MatrixEvent) => {
|
||||
if (event.threadRootId !== this.id) return; // ignore redactions for other timelines
|
||||
const events = [...this.timelineSet.getLiveTimeline().getEvents()].reverse();
|
||||
this.lastEvent = events.find(e => (
|
||||
!e.isRedacted() &&
|
||||
e.isRelation(THREAD_RELATION_TYPE.name)
|
||||
)) ?? this.rootEvent!;
|
||||
this.emit(ThreadEvent.Update, this);
|
||||
if (this.replyCount <= 0) {
|
||||
for (const threadEvent of this.events) {
|
||||
this.clearEventMetadata(threadEvent);
|
||||
}
|
||||
this.lastEvent = this.rootEvent;
|
||||
this._currentUserParticipated = false;
|
||||
this.emit(ThreadEvent.Delete, this);
|
||||
} else {
|
||||
await this.initialiseThread();
|
||||
}
|
||||
};
|
||||
|
||||
private onEcho = (event: MatrixEvent) => {
|
||||
private onEcho = async (event: MatrixEvent) => {
|
||||
if (event.threadRootId !== this.id) return; // ignore echoes for other timelines
|
||||
if (this.lastEvent === event) return;
|
||||
if (!event.isRelation(THREAD_RELATION_TYPE.name)) return;
|
||||
|
||||
// There is a risk that the `localTimestamp` approximation will not be accurate
|
||||
// when threads are used over federation. That could result in the reply
|
||||
// count value drifting away from the value returned by the server
|
||||
const isThreadReply = event.isRelation(THREAD_RELATION_TYPE.name);
|
||||
if (!this.lastEvent || this.lastEvent.isRedacted() || (isThreadReply
|
||||
&& (event.getId() !== this.lastEvent.getId())
|
||||
&& (event.localTimestamp > this.lastEvent.localTimestamp))
|
||||
) {
|
||||
this.lastEvent = event;
|
||||
if (this.lastEvent.getId() !== this.id) {
|
||||
// This counting only works when server side support is enabled as we started the counting
|
||||
// from the value returned within the bundled relationship
|
||||
if (Thread.hasServerSideSupport) {
|
||||
this.replyCount++;
|
||||
}
|
||||
|
||||
this.emit(ThreadEvent.NewReply, this, event);
|
||||
}
|
||||
}
|
||||
|
||||
this.emit(ThreadEvent.Update, this);
|
||||
await this.initialiseThread();
|
||||
this.emit(ThreadEvent.NewReply, this, event);
|
||||
};
|
||||
|
||||
public get roomState(): RoomState {
|
||||
@ -237,7 +219,7 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
||||
|
||||
public addEvents(events: MatrixEvent[], toStartOfTimeline: boolean): void {
|
||||
events.forEach(ev => this.addEvent(ev, toStartOfTimeline, false));
|
||||
this.emit(ThreadEvent.Update, this);
|
||||
this.initialiseThread();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -249,12 +231,11 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
||||
* to the start (and not the end) of the timeline.
|
||||
* @param {boolean} emit whether to emit the Update event if the thread was updated or not.
|
||||
*/
|
||||
public addEvent(event: MatrixEvent, toStartOfTimeline: boolean, emit = true): void {
|
||||
event.setThread(this);
|
||||
public async addEvent(event: MatrixEvent, toStartOfTimeline: boolean, emit = true): Promise<void> {
|
||||
this.setEventMetadata(event);
|
||||
|
||||
if (!this._currentUserParticipated && event.getSender() === this.client.getUserId()) {
|
||||
this._currentUserParticipated = true;
|
||||
}
|
||||
const lastReply = this.lastReply();
|
||||
const isNewestReply = !lastReply || event.localTimestamp > lastReply!.localTimestamp;
|
||||
|
||||
// Add all incoming events to the thread's timeline set when there's no server support
|
||||
if (!Thread.hasServerSideSupport) {
|
||||
@ -265,16 +246,13 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
||||
this.addEventToTimeline(event, toStartOfTimeline);
|
||||
|
||||
this.client.decryptEventIfNeeded(event, {});
|
||||
} else if (!toStartOfTimeline &&
|
||||
this.initialEventsFetched &&
|
||||
event.localTimestamp > this.lastReply()!.localTimestamp
|
||||
) {
|
||||
this.fetchEditsWhereNeeded(event);
|
||||
} else if (!toStartOfTimeline && this.initialEventsFetched && isNewestReply) {
|
||||
await this.fetchEditsWhereNeeded(event);
|
||||
this.addEventToTimeline(event, false);
|
||||
} else if (event.isRelation(RelationType.Annotation) || event.isRelation(RelationType.Replace)) {
|
||||
// Apply annotations and replace relations to the relations of the timeline only
|
||||
this.timelineSet.relations.aggregateParentEvent(event);
|
||||
this.timelineSet.relations.aggregateChildEvent(event, this.timelineSet);
|
||||
this.timelineSet.relations?.aggregateParentEvent(event);
|
||||
this.timelineSet.relations?.aggregateChildEvent(event, this.timelineSet);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -285,7 +263,15 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
||||
}
|
||||
|
||||
if (emit) {
|
||||
this.emit(ThreadEvent.Update, this);
|
||||
this.emit(ThreadEvent.NewReply, this, event);
|
||||
this.initialiseThread();
|
||||
}
|
||||
}
|
||||
|
||||
public async processEvent(event: Optional<MatrixEvent>): Promise<void> {
|
||||
if (event) {
|
||||
this.setEventMetadata(event);
|
||||
await this.fetchEditsWhereNeeded(event);
|
||||
}
|
||||
}
|
||||
|
||||
@ -293,9 +279,9 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
||||
return rootEvent?.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name);
|
||||
}
|
||||
|
||||
private async initialiseThread(): Promise<void> {
|
||||
public async initialiseThread(): Promise<void> {
|
||||
let bundledRelationship = this.getRootEventBundledRelationship();
|
||||
if (Thread.hasServerSideSupport && !bundledRelationship) {
|
||||
if (Thread.hasServerSideSupport) {
|
||||
await this.fetchRootEvent();
|
||||
bundledRelationship = this.getRootEventBundledRelationship();
|
||||
}
|
||||
@ -304,15 +290,25 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
||||
this.replyCount = bundledRelationship.count;
|
||||
this._currentUserParticipated = !!bundledRelationship.current_user_participated;
|
||||
|
||||
const event = new MatrixEvent({
|
||||
room_id: this.room.roomId,
|
||||
...bundledRelationship.latest_event,
|
||||
});
|
||||
this.setEventMetadata(event);
|
||||
event.setThread(this);
|
||||
this.lastEvent = event;
|
||||
const mapper = this.client.getEventMapper();
|
||||
this.lastEvent = mapper(bundledRelationship.latest_event);
|
||||
await this.processEvent(this.lastEvent);
|
||||
}
|
||||
|
||||
this.fetchEditsWhereNeeded(event);
|
||||
if (!this.initialEventsFetched) {
|
||||
this.initialEventsFetched = true;
|
||||
// fetch initial event to allow proper pagination
|
||||
try {
|
||||
// if the thread has regular events, this will just load the last reply.
|
||||
// if the thread is newly created, this will load the root event.
|
||||
await this.client.paginateEventTimeline(this.liveTimeline, { backwards: true, limit: 1 });
|
||||
// just to make sure that, if we've created a timeline window for this thread before the thread itself
|
||||
// existed (e.g. when creating a new thread), we'll make sure the panel is force refreshed correctly.
|
||||
this.emit(RoomEvent.TimelineReset, this.room, this.timelineSet, true);
|
||||
} catch (e) {
|
||||
logger.error("Failed to load start of newly created thread: ", e);
|
||||
this.initialEventsFetched = false;
|
||||
}
|
||||
}
|
||||
|
||||
this.emit(ThreadEvent.Update, this);
|
||||
@ -334,15 +330,18 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
||||
}));
|
||||
}
|
||||
|
||||
public async fetchInitialEvents(): Promise<void> {
|
||||
if (this.initialEventsFetched) return;
|
||||
await this.fetchEvents();
|
||||
this.initialEventsFetched = true;
|
||||
public setEventMetadata(event: Optional<MatrixEvent>): void {
|
||||
if (event) {
|
||||
EventTimeline.setEventMetadata(event, this.roomState, false);
|
||||
event.setThread(this);
|
||||
}
|
||||
}
|
||||
|
||||
private setEventMetadata(event: MatrixEvent): void {
|
||||
EventTimeline.setEventMetadata(event, this.roomState, false);
|
||||
event.setThread(this);
|
||||
public clearEventMetadata(event: Optional<MatrixEvent>): void {
|
||||
if (event) {
|
||||
event.setThread(undefined);
|
||||
delete event.event?.unsigned?.["m.relations"]?.[THREAD_RELATION_TYPE.name];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -406,55 +405,6 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
||||
return this.timelineSet.getLiveTimeline();
|
||||
}
|
||||
|
||||
public async fetchEvents(opts: IRelationsRequestOpts = { limit: 20, dir: Direction.Backward }): Promise<{
|
||||
originalEvent?: MatrixEvent;
|
||||
events: MatrixEvent[];
|
||||
nextBatch?: string | null;
|
||||
prevBatch?: string;
|
||||
}> {
|
||||
let {
|
||||
originalEvent,
|
||||
events,
|
||||
prevBatch,
|
||||
nextBatch,
|
||||
} = await this.client.relations(
|
||||
this.room.roomId,
|
||||
this.id,
|
||||
THREAD_RELATION_TYPE.name,
|
||||
null,
|
||||
opts,
|
||||
);
|
||||
|
||||
// When there's no nextBatch returned with a `from` request we have reached
|
||||
// the end of the thread, and therefore want to return an empty one
|
||||
if (!opts.to && !nextBatch && originalEvent) {
|
||||
events = [...events, originalEvent];
|
||||
}
|
||||
|
||||
await this.fetchEditsWhereNeeded(...events);
|
||||
|
||||
await Promise.all(events.map(event => {
|
||||
this.setEventMetadata(event);
|
||||
return this.client.decryptEventIfNeeded(event);
|
||||
}));
|
||||
|
||||
const prependEvents = (opts.dir ?? Direction.Backward) === Direction.Backward;
|
||||
|
||||
this.timelineSet.addEventsToTimeline(
|
||||
events,
|
||||
prependEvents,
|
||||
this.liveTimeline,
|
||||
prependEvents ? nextBatch : prevBatch,
|
||||
);
|
||||
|
||||
return {
|
||||
originalEvent,
|
||||
events,
|
||||
prevBatch,
|
||||
nextBatch,
|
||||
};
|
||||
}
|
||||
|
||||
public getUnfilteredTimelineSet(): EventTimelineSet {
|
||||
return this.timelineSet;
|
||||
}
|
||||
@ -485,3 +435,12 @@ export enum ThreadFilterType {
|
||||
"My",
|
||||
"All"
|
||||
}
|
||||
|
||||
export function threadFilterTypeToFilter(type: ThreadFilterType | null): 'all' | 'participated' {
|
||||
switch (type) {
|
||||
case ThreadFilterType.My:
|
||||
return 'participated';
|
||||
default:
|
||||
return 'all';
|
||||
}
|
||||
}
|
||||
|
@ -133,18 +133,14 @@ export class TimelineWindow {
|
||||
|
||||
// We avoid delaying the resolution of the promise by a reactor tick if we already have the data we need,
|
||||
// which is important to keep room-switching feeling snappy.
|
||||
if (initialEventId) {
|
||||
const timeline = this.timelineSet.getTimelineForEvent(initialEventId);
|
||||
if (timeline) {
|
||||
// hot-path optimization to save a reactor tick by replicating the sync check getTimelineForEvent does.
|
||||
initFields(timeline);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return this.client.getEventTimeline(this.timelineSet, initialEventId).then(initFields);
|
||||
if (this.timelineSet.getTimelineForEvent(initialEventId)) {
|
||||
initFields(this.timelineSet.getTimelineForEvent(initialEventId));
|
||||
return Promise.resolve();
|
||||
} else if (initialEventId) {
|
||||
return this.client.getEventTimeline(this.timelineSet, initialEventId)
|
||||
.then(initFields);
|
||||
} else {
|
||||
const tl = this.timelineSet.getLiveTimeline();
|
||||
initFields(tl);
|
||||
initFields(this.timelineSet.getLiveTimeline());
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
@ -236,8 +232,9 @@ export class TimelineWindow {
|
||||
}
|
||||
}
|
||||
|
||||
return Boolean(tl.timeline.getNeighbouringTimeline(direction) ||
|
||||
tl.timeline.getPaginationToken(direction) !== null);
|
||||
const hasNeighbouringTimeline = tl.timeline.getNeighbouringTimeline(direction);
|
||||
const paginationToken = tl.timeline.getPaginationToken(direction);
|
||||
return Boolean(hasNeighbouringTimeline) || Boolean(paginationToken);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -262,7 +259,7 @@ export class TimelineWindow {
|
||||
* @return {Promise} Resolves to a boolean which is true if more events
|
||||
* were successfully retrieved.
|
||||
*/
|
||||
public paginate(
|
||||
public async paginate(
|
||||
direction: Direction,
|
||||
size: number,
|
||||
makeRequest = true,
|
||||
@ -274,7 +271,7 @@ export class TimelineWindow {
|
||||
|
||||
if (!tl) {
|
||||
debuglog("TimelineWindow: no timeline yet");
|
||||
return Promise.resolve(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tl.pendingPaginate) {
|
||||
@ -283,20 +280,20 @@ export class TimelineWindow {
|
||||
|
||||
// try moving the cap
|
||||
if (this.extend(direction, size)) {
|
||||
return Promise.resolve(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!makeRequest || requestLimit === 0) {
|
||||
// todo: should we return something different to indicate that there
|
||||
// might be more events out there, but we haven't found them yet?
|
||||
return Promise.resolve(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
// try making a pagination request
|
||||
const token = tl.timeline.getPaginationToken(direction);
|
||||
if (token === null) {
|
||||
if (!token) {
|
||||
debuglog("TimelineWindow: no token");
|
||||
return Promise.resolve(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
debuglog("TimelineWindow: starting request");
|
||||
@ -309,8 +306,7 @@ export class TimelineWindow {
|
||||
}).then((r) => {
|
||||
debuglog("TimelineWindow: request completed with result " + r);
|
||||
if (!r) {
|
||||
// end of timeline
|
||||
return false;
|
||||
return this.paginate(direction, size, false, 0);
|
||||
}
|
||||
|
||||
// recurse to advance the index into the results.
|
||||
|
28
src/utils.ts
28
src/utils.ts
@ -22,6 +22,7 @@ limitations under the License.
|
||||
|
||||
import unhomoglyph from "unhomoglyph";
|
||||
import promiseRetry from "p-retry";
|
||||
import { Optional } from "matrix-events-sdk";
|
||||
|
||||
import { MatrixEvent } from "./models/event";
|
||||
import { M_TIMESTAMP } from "./@types/location";
|
||||
@ -76,6 +77,25 @@ export function encodeParams(params: QueryDict, urlSearchParams?: URLSearchParam
|
||||
|
||||
export type QueryDict = Record<string, string[] | string | number | boolean | undefined>;
|
||||
|
||||
/**
|
||||
* Replace a stable parameter with the unstable naming for params
|
||||
* @param stable
|
||||
* @param unstable
|
||||
* @param dict
|
||||
*/
|
||||
export function replaceParam(
|
||||
stable: string,
|
||||
unstable: string,
|
||||
dict: QueryDict,
|
||||
): QueryDict {
|
||||
const result = {
|
||||
...dict,
|
||||
[unstable]: dict[stable],
|
||||
};
|
||||
delete result[stable];
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a query string in `application/x-www-form-urlencoded` format.
|
||||
* @param {string} query A query string to decode e.g.
|
||||
@ -103,13 +123,17 @@ export function decodeParams(query: string): Record<string, string | string[]> {
|
||||
* variables with. E.g. { "$bar": "baz" }.
|
||||
* @return {string} The result of replacing all template variables e.g. '/foo/baz'.
|
||||
*/
|
||||
export function encodeUri(pathTemplate: string, variables: Record<string, string>): string {
|
||||
export function encodeUri(pathTemplate: string, variables: Record<string, Optional<string>>): string {
|
||||
for (const key in variables) {
|
||||
if (!variables.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
const value = variables[key];
|
||||
if (value === undefined || value === null) {
|
||||
continue;
|
||||
}
|
||||
pathTemplate = pathTemplate.replace(
|
||||
key, encodeURIComponent(variables[key]),
|
||||
key, encodeURIComponent(value),
|
||||
);
|
||||
}
|
||||
return pathTemplate;
|
||||
|
Reference in New Issue
Block a user